Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ resolver = "2"
members = [
"admin",
"api-client",
"builder",
"cargo-shuttle",
"codegen",
"common",
Expand All @@ -21,13 +22,15 @@ repository = "https://github.com/shuttle-hq/shuttle"

[workspace.dependencies]
shuttle-api-client = { path = "api-client", version = "0.56.1", default-features = false }
shuttle-builder = { path = "builder", version = "0.56.0" }
shuttle-codegen = { path = "codegen", version = "0.56.0" }
shuttle-common = { path = "common", version = "0.56.0" }
shuttle-ifc = { path = "ifc", version = "0.56.0" }
shuttle-mcp = { path = "mcp", version = "0.56.0" }
shuttle-service = { path = "service", version = "0.56.0" }

anyhow = "1.0.66"
askama = "0.14.0"
assert_cmd = "2.0.6"
async-trait = "0.1.58"
axum = { version = "0.8.1", default-features = false }
Expand Down
16 changes: 16 additions & 0 deletions builder/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "shuttle-builder"
version = "0.56.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
description = "Docker build recipes for the Shuttle platform (shuttle.dev)"
homepage = "https://www.shuttle.dev"

[dependencies]
shuttle-common = { workspace = true, features = ["models"] }

askama = { workspace = true }

[dev-dependencies]
pretty_assertions = { workspace = true }
68 changes: 68 additions & 0 deletions builder/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use askama::Template;
use shuttle_common::models::deployment::BuildArgsRust;

#[derive(Template)]
#[template(path = "rust.Dockerfile.jinja2", escape = "none")]
pub struct RustDockerfile<'a> {
/// local or remote image name for the chef image
pub chef_image: &'a str,
/// content of inlined chef dockerfile
pub cargo_chef_dockerfile: Option<&'a str>,
/// local or remote image name for the runtime image
pub runtime_image: &'a str,
/// content of inlined runtime dockerfile
pub runtime_base_dockerfile: Option<&'a str>,
pub build_args: &'a BuildArgsRust,
}

pub fn render_rust_dockerfile(build_args: &BuildArgsRust) -> String {
RustDockerfile {
chef_image: "cargo-chef",
cargo_chef_dockerfile: Some(include_str!("../templates/cargo-chef.Dockerfile")),
runtime_image: "runtime-base",
runtime_base_dockerfile: Some(include_str!("../templates/runtime-base.Dockerfile")),
build_args,
}
.render()
.unwrap()
}

#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_str_eq;

#[test]
fn rust_basic() {
let t = RustDockerfile {
chef_image: "chef",
cargo_chef_dockerfile: Some("foo"),
runtime_image: "rt",
runtime_base_dockerfile: Some("bar"),
build_args: &BuildArgsRust {
package_name: Some("hello".into()),
features: Some("asdf".into()),
..Default::default()
},
};

let s = t.render().unwrap();

assert!(s.contains("foo\n\n"));
assert!(s.contains("bar\n\n"));
assert!(s.contains("FROM chef AS chef"));
assert!(s.contains("FROM rt AS runtime"));
assert!(s.contains("RUN cargo chef cook --release --package hello --features asdf\n"));
assert!(s.contains("mv /app/target/release/hello"));
}

#[test]
fn rust_full() {
let s = render_rust_dockerfile(&BuildArgsRust {
package_name: Some("hello".into()),
features: Some("asdf".into()),
..Default::default()
});
assert_str_eq!(s, include_str!("../tests/rust.Dockerfile"));
}
}
47 changes: 47 additions & 0 deletions builder/templates/cargo-chef.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#syntax=docker/dockerfile:1.4

FROM lukemathwalker/cargo-chef:latest AS cargo-chef

SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]

RUN <<EOT
# Files and directories used by the Shuttle build process:
mkdir /build_assets
mkdir /app
# Create empty files in place for optional user scripts, etc.
# Having them empty means we can skip checking for them with [ -f ... ] etc.
touch /app/Shuttle.toml
touch /app/shuttle_prebuild.sh
touch /app/shuttle_postbuild.sh
touch /app/shuttle_setup_container.sh
EOT

# Install common build tools for external crates
# The image should already have these: https://github.com/docker-library/buildpack-deps/blob/fdfe65ea0743aa735b4a5f27cac8e281e43508f5/debian/bookworm/Dockerfile
RUN <<EOT
apt-get update

DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
clang \
cmake \
jq \
llvm-dev \
libclang-dev \
mold \
protobuf-compiler

apt-get clean
rm -rf /var/lib/apt/lists/*
EOT

# Add the wasm32 target for building frontend frameworks
RUN rustup target add wasm32-unknown-unknown

# cargo binstall
RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash

# Utility tools for build process
RUN cargo binstall -y --locked convert2json@1.1.5

# Common cargo build tools (for the user to use)
RUN cargo binstall -y --locked trunk@0.21.7
17 changes: 17 additions & 0 deletions builder/templates/runtime-base.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#syntax=docker/dockerfile:1.4

FROM debian:bookworm-slim AS runtime-base

SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]

# ca-certificates for native-tls, curl for health check
RUN <<EOT
apt-get update

DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates \
curl

apt-get clean
rm -rf /var/lib/apt/lists/*
EOT
69 changes: 69 additions & 0 deletions builder/templates/rust.Dockerfile.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{% if let Some(s) = cargo_chef_dockerfile %}{{s}}{% endif %}

{% if let Some(s) = runtime_base_dockerfile %}{{s}}{% endif %}

FROM {{ chef_image }} AS chef
WORKDIR /app
ENV SHUTTLE=true


{% if build_args.cargo_chef %}
FROM chef AS planner
COPY . .
RUN cargo chef prepare
{% endif %}


FROM chef AS builder

COPY shuttle_prebuild.sh .
RUN bash shuttle_prebuild.sh

{% if build_args.mold %}
export RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=/usr/local/bin/mold"
{% endif %}

{% if build_args.cargo_chef %}
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release
{%- if let Some(s) = build_args.package_name %} --package {{s}}{% endif %}
{%- if let Some(s) = build_args.binary_name %} --bin {{s}}{% endif %}
{%- if let Some(s) = build_args.features %} --features {{s}}{% endif %}
{%- if build_args.no_default_features %} --no-default-features{% endif %}
{% endif %}

COPY . .

{% if build_args.cargo_build %}
RUN cargo build --release
{%- if let Some(s) = build_args.package_name %} --package {{s}}{% endif %}
{%- if let Some(s) = build_args.binary_name %} --bin {{s}}{% endif %}
{%- if let Some(s) = build_args.features %} --features {{s}}{% endif %}
{%- if build_args.no_default_features %} --no-default-features{% endif %}
{% endif %}

RUN bash shuttle_postbuild.sh

RUN mv /app/target/release/
{%- if let Some(s) = build_args.binary_name -%}
{{s}}
{%- else if let Some(s) = build_args.package_name -%}
{{s}}
{%- endif %} /executable

{# Create folders and copy paths of all specified build assets #}
{# For loop is used so that no find command is run when the array is empty #}
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


FROM {{ runtime_image }} AS runtime
WORKDIR /app

COPY --from=builder /app/shuttle_setup_container.sh /tmp
RUN bash /tmp/shuttle_setup_container.sh; rm /tmp/shuttle_setup_container.sh

COPY --from=builder /build_assets /app
COPY --from=builder /executable /usr/local/bin/runtime

ENTRYPOINT ["/usr/local/bin/runtime"]

Loading