Cargo能否在构建应用之前下载并构建依赖项?

57
有没有一种方法可以告诉Cargo安装并构建所有依赖项,但不尝试构建我的应用程序?
我以为cargo install会这样做,但它实际上也会构建我的应用程序。我想达到这样的状态:cargo build会发现所有的依赖项都已准备好使用,但不触及/src目录。
我真正想要实现的目标是:
我正在尝试为Rust应用程序构建Docker镜像,我希望执行以下步骤:
构建时间(docker build .):
1.导入已安装了Rust工具的docker镜像 2.添加我的Cargo.toml和Cargo.lock文件 3.下载并构建所有依赖项 4.将我的源目录添加到镜像中 5.构建我的源代码
运行时间(docker run ...):
1.运行应用程序
我尝试了以下Dockerfile,但指定的步骤也会构建我的应用程序(这当然会失败,因为源目录还不存在):
FROM jimmycuadra/rust

ADD Cargo.toml /source
ADD Cargo.lock /source

RUN cargo install # <-- failure here

ADD src /source/src
RUN cargo build

ENTRYPOINT cargo run

我希望将安装依赖项的步骤与实际构建应用程序的步骤分开,原因是如果我不更改依赖关系,我希望Docker能够使用已经安装和构建所有依赖项的缓存映像。因此,在安装依赖项之前,我不能添加ADD /src /source/src,因为当我更改自己的代码时,这会使缓存的映像无效。

这是一个有趣的需求!您尝试过通过提供一个“假”的lib.rs文件来绕过这个问题吗?它可能会创建一个target 子目录,但是在执行cargo build之后,您总可以将其删除。 - Matthieu M.
这是一个有趣的想法!;)回家后将尝试。我对Rust完全不了解,所以我不太清楚内部工作原理或者需要什么来确保正确构建 - 到目前为止我的方法是试错... - Tomas Aschan
我自己对Cargo没有太多经验,所以这只是一个试探性的解决方法。就我所知,可能已经有你想要的命令了。 - Matthieu M.
1
这里是否有用cargo vendor - Chris Emerson
@ChrisEmerson:在努力一段时间后,我终于成功让 cargo vendor 在我的镜像上正确地安装和运行,但是我注意到依赖项已经被下载但并未被构建。我正在研究是否可以预编译它们... - Tomas Aschan
6个回答

23
据我所知,Cargo没有原生支持仅构建依赖项的功能。目前有一个针对此问题的未解决的问题。但是如果您想实现它,可以提交一些内容到Cargo中,或者创建第三方Cargo插件也不会让人惊讶。当我的代码太破损无法编译时,我也希望cargo doc具备这种功能。
然而,我维护的Rust playground实现了您想要的结果。其中有一个基础 Docker 容器,安装了 Rustup 并复制了Cargo.toml,其中包含可用于播放区的所有箱。构建步骤创建了一个空项目(带有虚拟的src/lib.rs),然后调用cargo buildcargo build --release来编译箱
RUN cd / && \
    cargo new playground
WORKDIR /playground

ADD Cargo.toml /playground/Cargo.toml
RUN cargo build
RUN cargo build --release
RUN rm src/*.rs

所有已下载的包都存储在 Docker 镜像的 $HOME/.cargo 目录中,所有已构建的包都存储在应用程序的 target/{debug,release} 目录中。

之后,真实的源代码文件被复制到容器中,可以再次使用现在已编译的包来运行 cargo build / cargo run 命令。

如果你正在构建一个可执行项目,你也需要复制 Cargo.lock 文件。


1
一般而言,在库/二进制文件上,可以使用语法COPY Cargo.* ./(ADD也类似,但可能不被鼓励 https://nickjanetakis.com/blog/docker-tip-2-the-difference-between-copy-and-add-in-a-dockerile) - VasiliNovikov
1
你好,为什么要使用 cargo buildcargo build --release 而不是只使用带有 release 选项的 cargo build 呢? - UselesssCat
3
@UselesssCat,playground 允许您在调试和发布模式下编译代码,因此我们需要在两种模式下预编译所有依赖项。 - Shepmaster

7
如果您添加一个虚拟的主文件或库文件,您可以使用来仅拉取依赖项。我目前正在为我的基于Docker的项目使用此解决方案:
COPY Cargo.toml .
RUN mkdir src \
    && echo "// dummy file" > src/lib.rs \
    && cargo build

我正在使用--volumes,所以到此为止就完成了。主机卷进来后会清除虚拟文件,当我稍后构建源代码时,cargo会使用缓存的依赖项。如果您想稍后添加COPY(或ADD)并使用缓存的依赖项,则此解决方案同样有效。

我使用 --volumes 参数,所以现在已经完成了。主机卷会覆盖虚拟文件,并且当我稍后构建源码时,Cargo将使用缓存的依赖项。如果你想稍后添加 COPY 或者 ADD 命令,并且要使用缓存的依赖项,那么这个解决方案同样适用。


5
基于GitHub评论
FROM rust:1.37

WORKDIR /usr/src

# Create blank project
RUN USER=root cargo new PROJ

# We want dependencies cached, so copy those first.
COPY Cargo.toml /usr/src/PROJ/
COPY Cargo.lock /usr/src/PROJ/

WORKDIR /usr/src/PROJ

# This is a dummy build to get the dependencies cached.
RUN cargo build --release

# Now copy in the rest of the sources
COPY MyPROJECT/src /usr/src/PROJ/src/

# This is the actual build.
RUN cargo build --release \
    && mv target/release/appname /bin \
    && rm -rf /usr/src/PROJ

WORKDIR /

EXPOSE 8888

CMD ["/bin/appname"]

如果项目中有一个 test 部分,例如 [[test]] name = "it" harness = false 那么这个方法就不起作用了,因为它依赖于有效的测试... -.-' - Jorge Leitao

4

cargo-chef 工具旨在解决这个问题。下面是在 Dockerfile 中如何使用它的示例:

FROM lukemathwalker/cargo-chef as planner
WORKDIR app
COPY . .
RUN cargo chef prepare  --recipe-path recipe.json

FROM lukemathwalker/cargo-chef as cacher
WORKDIR app
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json

FROM rust as builder
WORKDIR app
COPY . .
# Copy over the cached dependencies
COPY --from=cacher /app/target target
COPY --from=cacher $CARGO_HOME $CARGO_HOME
RUN cargo build --release --bin app

FROM rust as runtime
WORKDIR app
COPY --from=builder /app/target/release/app /usr/local/bin
ENTRYPOINT ["/usr/local/bin/app"]

0

我只是想在这里发布一下,以便其他人能够看到。有一个实验性的 Docker 工具,我刚开始使用叫做 cargo-wharf (https://github.com/denzp/cargo-wharf/tree/master/cargo-wharf-frontend)。它是一个 Docker BuildKit 前端,为您缓存构建的 cargo 依赖项。如果您只更改了一个源文件,那么当您调用 docker build 时,只有该文件会被重新构建。您可以通过注释您的 Cargo.toml 文件来使用它,然后将 Docker 指向您的 Cargo.toml 文件而不是 Dockerfile。去看看吧,这正是我想要的。(我与该项目没有任何关联。)


0
可以通过、和来完成。例如,对于一个名为的项目,定义以下:
FROM rust:slim-bullseye

# Build dependencies only.
RUN cargo init foo
COPY Cargo.toml foo/
RUN cargo build --release; \
    rm -rf foo

# Install `foo`.
COPY . .
RUN echo "// force Cargo cache invalidation" >> foo/src/main.rs; \
    cargo install --path foo

CMD ["foo"]

在这里,cargo init 创建了 Cargo 期望的占位文件,cargo build 构建了在 Cargo.toml 中指定的依赖项,cargo install 创建了 foo 二进制文件。由于某种原因,Docker 一直在构建由 cargo init foo 创建的默认项目。通过追加 // force Cargo cache invalidation 解决了上述问题,强制更新了 main.rs
为避免由于大型构建环境和大型层而导致的慢速构建,请确保通过 .dockerignore 忽略不重要的文件夹,如 target。例如,定义以下 .dockerignore
**/*.lock
LICENSE
README.md
target

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接