dagger-rust 0.2.0

A common set of components for dagger-sdk, which enables patterns such as build, test and publish
Documentation
use std::{path::PathBuf, sync::Arc};

use crate::source::RustSource;

#[allow(dead_code)]
pub struct RustBuild {
    client: Arc<dagger_sdk::Query>,
    registry: Option<String>,
}

impl RustBuild {
    pub fn new(client: Arc<dagger_sdk::Query>) -> Self {
        Self {
            client,
            registry: None,
        }
    }

    pub async fn build(
        &self,
        source_path: Option<impl Into<PathBuf>>,
        rust_version: impl AsRef<RustVersion>,
        target: impl AsRef<BuildTarget>,
        profile: impl AsRef<BuildProfile>,
        crates: &[&str],
        extra_deps: &[&str],
    ) -> eyre::Result<dagger_sdk::Container> {
        let rust_version = rust_version.as_ref();
        let target = target.as_ref();
        let profile = profile.as_ref();
        let source_path = source_path.map(|s| s.into());
        let source = source_path.clone().unwrap_or(PathBuf::from("."));

        let rust_source = RustSource::new(self.client.clone());
        let (src, dep_src) = rust_source
            .get_rust_src(source_path, crates.to_vec())
            .await?;
        let mut deps = vec!["apt", "install", "-y"];
        deps.extend(extra_deps);

        let rust_build_image = self
            .client
            .container()
            .from(rust_version.to_string())
            .with_exec(vec!["rustup", "target", "add", &target.to_string()])
            .with_exec(vec!["apt", "update"])
            .with_exec(deps);

        let target_cache = self.client.cache_volume(format!(
            "rust_target_{}_{}",
            profile.to_string(),
            target.to_string()
        ));

        let target_str = target.to_string();
        let mut build_options = vec!["cargo", "build", "--target", &target_str, "--workspace"];

        if matches!(profile, BuildProfile::Release) {
            build_options.push("--release");
        }
        let rust_prebuild = rust_build_image
            .with_workdir("/mnt/src")
            .with_directory("/mnt/src", dep_src.id().await?)
            .with_exec(build_options)
            .with_mounted_cache("/mnt/src/target/", target_cache.id().await?);

        let incremental_dir = rust_source
            .get_rust_target_src(&source, rust_prebuild.clone(), crates.to_vec())
            .await?;

        let rust_with_src = rust_build_image
            .with_workdir("/mnt/src")
            .with_directory(
                "/usr/local/cargo",
                rust_prebuild.directory("/usr/local/cargo").id().await?,
            )
            .with_directory("/mnt/src/target", incremental_dir.id().await?)
            .with_directory("/mnt/src/", src.id().await?);

        Ok(rust_with_src)
    }

    pub async fn build_release(
        &self,
        source_path: Option<impl Into<PathBuf>>,
        rust_version: impl AsRef<RustVersion>,
        crates: &[&str],
        extra_deps: &[&str],
        images: impl IntoIterator<Item = SlimImage>,
        bin_name: &str,
    ) -> eyre::Result<Vec<dagger_sdk::Container>> {
        let images = images.into_iter().collect::<Vec<_>>();
        let source_path = source_path.map(|s| s.into());

        let mut containers = Vec::new();
        for container_image in images {
            let container = match &container_image {
                SlimImage::Debian { image, deps, .. } => {
                    let target = BuildTarget::from_target(&container_image);

                    let build_container = self
                        .build(
                            source_path.clone(),
                            &rust_version,
                            BuildTarget::from_target(&container_image),
                            BuildProfile::Release,
                            crates,
                            extra_deps,
                        )
                        .await?;

                    let bin = build_container
                        .with_exec(vec![
                            "cargo",
                            "build",
                            "--target",
                            &target.to_string(),
                            "--release",
                            "-p",
                            bin_name,
                        ])
                        .file(format!(
                            "target/{}/release/{}",
                            target.to_string(),
                            bin_name
                        ));

                    self.build_debian_image(
                        bin,
                        image,
                        BuildTarget::from_target(&container_image),
                        deps.iter()
                            .map(|d| d.as_str())
                            .collect::<Vec<&str>>()
                            .as_slice(),
                        bin_name,
                    )
                    .await?
                }
                SlimImage::Alpine { image, deps, .. } => {
                    let target = BuildTarget::from_target(&container_image);

                    let build_container = self
                        .build(
                            source_path.clone(),
                            &rust_version,
                            BuildTarget::from_target(&container_image),
                            BuildProfile::Release,
                            crates,
                            extra_deps,
                        )
                        .await?;

                    let bin = build_container
                        .with_exec(vec![
                            "cargo",
                            "build",
                            "--target",
                            &target.to_string(),
                            "--release",
                            "-p",
                            bin_name,
                        ])
                        .file(format!(
                            "target/{}/release/{}",
                            target.to_string(),
                            bin_name
                        ));

                    self.build_alpine_image(
                        bin,
                        image,
                        BuildTarget::from_target(&container_image),
                        deps.iter()
                            .map(|d| d.as_str())
                            .collect::<Vec<&str>>()
                            .as_slice(),
                        bin_name,
                    )
                    .await?
                }
            };

            containers.push(container);
        }

        Ok(containers)
    }

    async fn build_debian_image(
        &self,
        bin: dagger_sdk::File,
        image: &str,
        target: BuildTarget,
        production_deps: &[&str],
        bin_name: &str,
    ) -> eyre::Result<dagger_sdk::Container> {
        let base_debian = self
            .client
            .container_opts(dagger_sdk::QueryContainerOpts {
                id: None,
                platform: Some(target.into_platform()),
            })
            .from(image);

        let mut packages = vec!["apt", "install", "-y"];
        packages.extend_from_slice(production_deps);
        let base_debian = base_debian
            .with_exec(vec!["apt", "update"])
            .with_exec(packages);

        let final_image = base_debian
            .with_file(format!("/usr/local/bin/{}", bin_name), bin.id().await?)
            .with_exec(vec![bin_name, "--help"]);

        final_image.exit_code().await?;

        Ok(final_image)
    }

    async fn build_alpine_image(
        &self,
        bin: dagger_sdk::File,
        image: &str,
        target: BuildTarget,
        production_deps: &[&str],
        bin_name: &str,
    ) -> eyre::Result<dagger_sdk::Container> {
        let base_debian = self
            .client
            .container_opts(dagger_sdk::QueryContainerOpts {
                id: None,
                platform: Some(target.into_platform()),
            })
            .from(image);

        let mut packages = vec!["apk", "add"];
        packages.extend_from_slice(production_deps);
        let base_debian = base_debian.with_exec(packages);

        let final_image =
            base_debian.with_file(format!("/usr/local/bin/{}", bin_name), bin.id().await?);

        Ok(final_image)
    }
}

pub enum RustVersion {
    Nightly,
    Stable(String),
}

impl AsRef<RustVersion> for RustVersion {
    fn as_ref(&self) -> &RustVersion {
        &self
    }
}

impl ToString for RustVersion {
    fn to_string(&self) -> String {
        match self {
            RustVersion::Nightly => "rustlang/rust:nightly".to_string(),
            RustVersion::Stable(version) => format!("rust:{}", version),
        }
    }
}

pub enum BuildTarget {
    LinuxAmd64,
    LinuxArm64,
    LinuxAmd64Musl,
    LinuxArm64Musl,
    MacOSAmd64,
    MacOSArm64,
}

impl BuildTarget {
    pub fn from_target(image: &SlimImage) -> Self {
        match image {
            SlimImage::Debian { architecture, .. } => match architecture {
                BuildArchitecture::Amd64 => Self::LinuxAmd64,
                BuildArchitecture::Arm64 => Self::LinuxArm64,
            },
            SlimImage::Alpine { architecture, .. } => match architecture {
                BuildArchitecture::Amd64 => Self::LinuxAmd64Musl,
                BuildArchitecture::Arm64 => Self::LinuxArm64Musl,
            },
        }
    }

    fn into_platform(&self) -> dagger_sdk::Platform {
        let platform = match self {
            BuildTarget::LinuxAmd64 => "linux/amd64",
            BuildTarget::LinuxArm64 => "linux/arm64",
            BuildTarget::LinuxAmd64Musl => "linux/amd64",
            BuildTarget::LinuxArm64Musl => "linux/arm64",
            BuildTarget::MacOSAmd64 => "darwin/amd64",
            BuildTarget::MacOSArm64 => "darwin/arm64",
        };

        dagger_sdk::Platform(platform.into())
    }
}

impl AsRef<BuildTarget> for BuildTarget {
    fn as_ref(&self) -> &BuildTarget {
        &self
    }
}

impl ToString for BuildTarget {
    fn to_string(&self) -> String {
        let target = match self {
            BuildTarget::LinuxAmd64 => "x86_64-unknown-linux-gnu",
            BuildTarget::LinuxArm64 => "aarch64-unknown-linux-gnu",
            BuildTarget::LinuxAmd64Musl => "x86_64-unknown-linux-musl",
            BuildTarget::LinuxArm64Musl => "aarch64-unknown-linux-musl",
            BuildTarget::MacOSAmd64 => "x86_64-apple-darwin",
            BuildTarget::MacOSArm64 => "aarch64-apple-darwin",
        };

        target.into()
    }
}

pub enum BuildProfile {
    Debug,
    Release,
}

impl AsRef<BuildProfile> for BuildProfile {
    fn as_ref(&self) -> &BuildProfile {
        &self
    }
}

impl ToString for BuildProfile {
    fn to_string(&self) -> String {
        let profile = match self {
            BuildProfile::Debug => "debug",
            BuildProfile::Release => "release",
        };

        profile.into()
    }
}

pub enum SlimImage {
    Debian {
        image: String,
        deps: Vec<String>,
        architecture: BuildArchitecture,
    },
    Alpine {
        image: String,
        deps: Vec<String>,
        architecture: BuildArchitecture,
    },
}

pub enum BuildArchitecture {
    Amd64,
    Arm64,
}