Skip to content

Commit 764a3cd

Browse files
authored
feat(builder, cargo-shuttle): build and run with docker locally (#2044)
* feat: shuttle build, shuttle build --docker, shuttle run --docker * todo * nit * refa: gather build args, de-dupe metadata call * clippy * simplify local build call a lot * better quiet flag * build --output-archive * clippy * hide new commands & args * build command experimental * cleanup * less panic * ci: builder * fix: dockerfiles + test * nit * nit
1 parent c83c0d4 commit 764a3cd

File tree

17 files changed

+786
-268
lines changed

17 files changed

+786
-268
lines changed

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ workflows:
409409
matrix:
410410
parameters:
411411
crate:
412+
- shuttle-builder
412413
- shuttle-codegen
413414
- shuttle-common
414415
- shuttle-ifc

Cargo.lock

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ resolver = "2"
33
members = [
44
"admin",
55
"api-client",
6+
"builder",
67
"cargo-shuttle",
78
"codegen",
89
"common",
@@ -21,13 +22,15 @@ repository = "https://github.com/shuttle-hq/shuttle"
2122

2223
[workspace.dependencies]
2324
shuttle-api-client = { path = "api-client", version = "0.56.1", default-features = false }
25+
shuttle-builder = { path = "builder", version = "0.56.0" }
2426
shuttle-codegen = { path = "codegen", version = "0.56.0" }
2527
shuttle-common = { path = "common", version = "0.56.0" }
2628
shuttle-ifc = { path = "ifc", version = "0.56.0" }
2729
shuttle-mcp = { path = "mcp", version = "0.56.0" }
2830
shuttle-service = { path = "service", version = "0.56.0" }
2931

3032
anyhow = "1.0.66"
33+
askama = "0.14.0"
3134
assert_cmd = "2.0.6"
3235
async-trait = "0.1.58"
3336
axum = { version = "0.8.1", default-features = false }

builder/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "shuttle-builder"
3+
version = "0.56.0"
4+
edition.workspace = true
5+
license.workspace = true
6+
repository.workspace = true
7+
description = "Docker build recipes for the Shuttle platform (shuttle.dev)"
8+
homepage = "https://www.shuttle.dev"
9+
10+
[dependencies]
11+
shuttle-common = { workspace = true, features = ["models"] }
12+
13+
askama = { workspace = true }
14+
15+
[dev-dependencies]
16+
pretty_assertions = { workspace = true }

builder/src/lib.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use askama::Template;
2+
use shuttle_common::models::deployment::BuildArgsRust;
3+
4+
#[derive(Template)]
5+
#[template(path = "rust.Dockerfile.jinja2", escape = "none")]
6+
pub struct RustDockerfile<'a> {
7+
/// local or remote image name for the chef image
8+
pub chef_image: &'a str,
9+
/// content of inlined chef dockerfile
10+
pub cargo_chef_dockerfile: Option<&'a str>,
11+
/// local or remote image name for the runtime image
12+
pub runtime_image: &'a str,
13+
/// content of inlined runtime dockerfile
14+
pub runtime_base_dockerfile: Option<&'a str>,
15+
pub build_args: &'a BuildArgsRust,
16+
}
17+
18+
pub fn render_rust_dockerfile(build_args: &BuildArgsRust) -> String {
19+
RustDockerfile {
20+
chef_image: "cargo-chef",
21+
cargo_chef_dockerfile: Some(include_str!("../templates/cargo-chef.Dockerfile")),
22+
runtime_image: "runtime-base",
23+
runtime_base_dockerfile: Some(include_str!("../templates/runtime-base.Dockerfile")),
24+
build_args,
25+
}
26+
.render()
27+
.unwrap()
28+
}
29+
30+
#[cfg(test)]
31+
mod tests {
32+
use super::*;
33+
use pretty_assertions::assert_str_eq;
34+
35+
#[test]
36+
fn rust_basic() {
37+
let t = RustDockerfile {
38+
chef_image: "chef",
39+
cargo_chef_dockerfile: Some("foo"),
40+
runtime_image: "rt",
41+
runtime_base_dockerfile: Some("bar"),
42+
build_args: &BuildArgsRust {
43+
package_name: Some("hello".into()),
44+
features: Some("asdf".into()),
45+
..Default::default()
46+
},
47+
};
48+
49+
let s = t.render().unwrap();
50+
51+
assert!(s.contains("foo\n\n"));
52+
assert!(s.contains("bar\n\n"));
53+
assert!(s.contains("FROM chef AS chef"));
54+
assert!(s.contains("FROM rt AS runtime"));
55+
assert!(s.contains("RUN cargo chef cook --release --package hello --features asdf\n"));
56+
assert!(s.contains("mv /app/target/release/hello"));
57+
}
58+
59+
#[test]
60+
fn rust_full() {
61+
let s = render_rust_dockerfile(&BuildArgsRust {
62+
package_name: Some("hello".into()),
63+
features: Some("asdf".into()),
64+
..Default::default()
65+
});
66+
assert_str_eq!(s, include_str!("../tests/rust.Dockerfile"));
67+
}
68+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
FROM lukemathwalker/cargo-chef:latest AS cargo-chef
2+
3+
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
4+
5+
RUN <<EOT
6+
# Files and directories used by the Shuttle build process:
7+
mkdir /build_assets
8+
mkdir /app
9+
# Create empty files in place for optional user scripts, etc.
10+
# Having them empty means we can skip checking for them with [ -f ... ] etc.
11+
touch /app/Shuttle.toml
12+
touch /app/shuttle_prebuild.sh
13+
touch /app/shuttle_postbuild.sh
14+
touch /app/shuttle_setup_container.sh
15+
EOT
16+
17+
# Install common build tools for external crates
18+
# The image should already have these: https://github.com/docker-library/buildpack-deps/blob/fdfe65ea0743aa735b4a5f27cac8e281e43508f5/debian/bookworm/Dockerfile
19+
RUN <<EOT
20+
apt-get update
21+
22+
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
23+
clang \
24+
cmake \
25+
jq \
26+
llvm-dev \
27+
libclang-dev \
28+
mold \
29+
protobuf-compiler
30+
31+
apt-get clean
32+
rm -rf /var/lib/apt/lists/*
33+
EOT
34+
35+
# Add the wasm32 target for building frontend frameworks
36+
RUN rustup target add wasm32-unknown-unknown
37+
38+
# cargo binstall
39+
RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
40+
41+
# Utility tools for build process
42+
RUN cargo binstall -y --locked [email protected]
43+
44+
# Common cargo build tools (for the user to use)
45+
RUN cargo binstall -y --locked [email protected]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM debian:bookworm-slim AS runtime-base
2+
3+
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
4+
5+
# ca-certificates for native-tls, curl for health check
6+
RUN <<EOT
7+
apt-get update
8+
9+
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
10+
ca-certificates \
11+
curl
12+
13+
apt-get clean
14+
rm -rf /var/lib/apt/lists/*
15+
EOT
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#syntax=docker/dockerfile:1.4
2+
3+
{% if let Some(s) = cargo_chef_dockerfile %}{{s}}{% endif %}
4+
5+
{% if let Some(s) = runtime_base_dockerfile %}{{s}}{% endif %}
6+
7+
FROM {{ chef_image }} AS chef
8+
WORKDIR /app
9+
ENV SHUTTLE=true
10+
11+
12+
{% if build_args.cargo_chef %}
13+
FROM chef AS planner
14+
COPY . .
15+
RUN cargo chef prepare
16+
{% endif %}
17+
18+
19+
FROM chef AS builder
20+
21+
COPY shuttle_prebuild.sh .
22+
RUN bash shuttle_prebuild.sh
23+
24+
{% if build_args.mold %}
25+
{# TODO: fix #}
26+
ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold"
27+
{% endif %}
28+
29+
{% if build_args.cargo_chef %}
30+
COPY --from=planner /app/recipe.json recipe.json
31+
RUN cargo chef cook --release
32+
{%- if let Some(s) = build_args.package_name %} --package {{s}}{% endif %}
33+
{%- if let Some(s) = build_args.binary_name %} --bin {{s}}{% endif %}
34+
{%- if let Some(s) = build_args.features %} --features {{s}}{% endif %}
35+
{%- if build_args.no_default_features %} --no-default-features{% endif %}
36+
{% endif %}
37+
38+
COPY . .
39+
40+
{% if build_args.cargo_build %}
41+
RUN cargo build --release
42+
{%- if let Some(s) = build_args.package_name %} --package {{s}}{% endif %}
43+
{%- if let Some(s) = build_args.binary_name %} --bin {{s}}{% endif %}
44+
{%- if let Some(s) = build_args.features %} --features {{s}}{% endif %}
45+
{%- if build_args.no_default_features %} --no-default-features{% endif %}
46+
{% endif %}
47+
48+
RUN bash shuttle_postbuild.sh
49+
50+
RUN mv /app/target/release/
51+
{%- if let Some(s) = build_args.binary_name -%}
52+
{{s}}
53+
{%- else if let Some(s) = build_args.package_name -%}
54+
{{s}}
55+
{%- endif %} /executable
56+
57+
{# Create folders and copy paths of all specified build assets #}
58+
{# For loop is used so that no find command is run when the array is empty #}
59+
RUN for path in $(tq -r '.build.assets // .build_assets // [] | join(" ")' Shuttle.toml); do find "$path" -type f -exec echo Copying \{\} \; -exec install -D \{\} /build_assets/\{\} \; ; done
60+
61+
62+
FROM {{ runtime_image }} AS runtime
63+
WORKDIR /app
64+
65+
COPY --from=builder /app/shuttle_setup_container.sh /tmp
66+
RUN bash /tmp/shuttle_setup_container.sh; rm /tmp/shuttle_setup_container.sh
67+
68+
COPY --from=builder /build_assets /app
69+
COPY --from=builder /executable /usr/local/bin/runtime
70+
71+
ENTRYPOINT ["/usr/local/bin/runtime"]
72+

0 commit comments

Comments
 (0)