use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use clap::Args; use clap::Parser; use clap::Subcommand; use clap::ValueEnum; use dagger_sdk::ContainerId; use dagger_sdk::ContainerPublishOpts; use dagger_sdk::Platform; use dagger_sdk::QueryContainerOpts; use futures::StreamExt; #[derive(Parser)] #[command(author, version, about, long_about = None, subcommand_required = true)] pub struct Command { #[command(subcommand)] commands: Commands, #[command(flatten)] global: GlobalArgs, } #[derive(Subcommand)] pub enum Commands { #[command(subcommand_required = true)] Local { #[command(subcommand)] command: LocalCommands, }, PullRequest, Main, Release, } #[derive(Subcommand)] pub enum LocalCommands { Build { #[arg(long, default_value = "debug")] profile: BuildProfile, #[arg(long)] bin_name: String, }, Test, DockerImage { #[arg(long)] image: String, #[arg(long)] tag: String, #[arg(long)] bin_name: String, }, PleaseRelease, } #[derive(Debug, Clone, ValueEnum)] pub enum BuildProfile { Debug, Release, } #[derive(Debug, Clone, Args)] pub struct GlobalArgs { #[arg(long, global = true, help_heading = "Global")] dry_run: bool, #[arg(long, global = true, help_heading = "Global")] rust_builder_image: Option, #[arg(long, global = true, help_heading = "Global")] production_image: Option, #[arg(long, global = true, help_heading = "Global")] source: Option, } #[tokio::main] async fn main() -> eyre::Result<()> { let _ = color_eyre::install(); let client = dagger_sdk::connect().await?; let cli = Command::parse(); match cli.commands { Commands::Local { command } => match command { LocalCommands::Build { profile: _, bin_name, } => { let base_image = base_rust_image(client.clone(), &cli.global, None, bin_name.clone()).await?; let prod_image = get_base_debian_image(client.clone(), &cli.global, None).await?; build::execute(client, &cli.global, base_image, prod_image, bin_name, None).await?; } LocalCommands::Test => { let base_image = base_rust_image(client.clone(), &cli.global, None, "cuddle-please".into()) .await?; test::execute(client, &cli.global, base_image).await?; } LocalCommands::DockerImage { tag, image, bin_name, } => { // let containers = vec!["linux/amd64", "linux/arm64"]; let containers = vec!["linux/amd64"]; let stream = futures::stream::iter(containers.into_iter().map(|c| { let client = Arc::new( client .clone() .pipeline(format!("docker:build:platform:{c}")), ); let args = cli.global.clone(); let platform = c.to_string(); let bin_name = bin_name.clone(); tokio::spawn(async move { let base_image = match get_base_debian_image( client.clone(), &args.clone(), Some(platform.clone()), ) .await { Ok(image) => image, Err(e) => { eprintln!("failed to get base image: {e}"); return None; } }; match base_rust_image( client.clone(), &args, Some(platform.clone()), bin_name.clone(), ) .await { Ok(container) => { let build_image = match build::execute( client, &args, container, base_image, bin_name, Some(platform), ) .await { Ok(image) => image, Err(e) => { eprintln!("could not build image: {e}"); return None; } }; match build_image.id().await { Ok(id) => return Some(id), Err(e) => { eprintln!("could not get id: {e}"); } } } Err(e) => { eprintln!("could not build container: {e}"); } } None }) })) .buffer_unordered(16) .filter_map(|f| async move { f.ok() }) .collect::>() .await; let _container = client .container() .publish_opts( format!("{image}:{tag}"), ContainerPublishOpts { platform_variants: stream .into_iter() .collect::>>(), }, ) .await?; } LocalCommands::PleaseRelease => todo!(), }, Commands::PullRequest => todo!(), Commands::Main => todo!(), Commands::Release => todo!(), } Ok(()) } mod build { use std::sync::Arc; use dagger_sdk::Container; use crate::GlobalArgs; pub async fn execute( _client: Arc, _args: &GlobalArgs, container: dagger_sdk::Container, base_image: dagger_sdk::Container, bin_name: String, platform: Option, ) -> eyre::Result { let rust_target = match platform.unwrap_or("linux/amd64".to_string()).as_str() { "linux/amd64" => "x86_64-unknown-linux-gnu", "linux/arm64" => "aarch64-unknown-linux-gnu", _ => eyre::bail!("architecture not supported"), }; let build_image = container.with_exec(vec![ "cargo", "build", "--target", rust_target, "--release", "-p", &bin_name, ]); let final_image = base_image .with_file( format!("/usr/local/bin/{}", &bin_name), build_image .file(format!("target/{}/release/{}", rust_target, &bin_name)) .id() .await?, ) .with_exec(vec![&bin_name, "--help"]); let output = final_image.stdout().await?; println!("{output}"); //.with_entrypoint(vec![&bin_name, "--log-level=debug"]); Ok(final_image) } } mod test { use std::sync::Arc; use crate::GlobalArgs; pub async fn execute( _client: Arc, _args: &GlobalArgs, container: dagger_sdk::Container, ) -> eyre::Result<()> { let test_image = container .pipeline("rust:test") .with_exec(vec!["cargo", "test"]); test_image.exit_code().await?; Ok(()) } } pub async fn get_base_debian_image( client: Arc, args: &GlobalArgs, platform: Option, ) -> eyre::Result { let default_platform = client.default_platform().await?; let platform = platform.map(Platform).unwrap_or(default_platform); let image = client .container_opts(QueryContainerOpts { id: None, platform: Some(platform), }) .from( args.production_image .clone() .unwrap_or("debian:bullseye".to_string()), ); let base_image = image.with_exec(vec!["apt", "update"]).with_exec(vec![ "apt", "install", "-y", "libssl-dev", "pkg-config", "openssl", ]); Ok(base_image) } pub fn get_src( client: Arc, args: &GlobalArgs, ) -> eyre::Result { let directory = client.host().directory_opts( args.source .clone() .unwrap_or(PathBuf::from(".")) .display() .to_string(), dagger_sdk::HostDirectoryOptsBuilder::default() .exclude(vec!["node_modules/", ".git/", "target/"]) .build()?, ); Ok(directory) } pub async fn get_rust_dep_src( client: Arc, args: &GlobalArgs, ) -> eyre::Result { let directory = client.host().directory_opts( args.source .clone() .unwrap_or(PathBuf::from(".")) .display() .to_string(), dagger_sdk::HostDirectoryOptsBuilder::default() .include(vec!["**/Cargo.toml", "**/Cargo.lock"]) .build()?, ); return Ok(directory); } pub async fn get_rust_skeleton_files( client: Arc, args: &GlobalArgs, ) -> eyre::Result<(dagger_sdk::Directory, Vec)> { let mut rust_crates = vec![PathBuf::from("ci")]; let mut dirs = tokio::fs::read_dir("crates").await?; while let Some(entry) = dirs.next_entry().await? { if entry.metadata().await?.is_dir() { rust_crates.push(entry.path()) } } fn create_skeleton_files( directory: dagger_sdk::Directory, path: &Path, ) -> eyre::Result { println!("found crates: {}", path.display()); let main_content = r#" #[allow(dead_code)] fn main() { panic!("should never be executed"); }"#; let lib_content = r#" #[allow(dead_code)] fn some() { panic!("should never be executed"); }"#; let directory = directory.with_new_file( path.join("src").join("main.rs").display().to_string(), main_content, ); let directory = directory.with_new_file( path.join("src").join("lib.rs").display().to_string(), lib_content, ); Ok(directory) } let mut directory = client.directory(); let mut crate_names = Vec::new(); for rust_crate in rust_crates.iter() { if let Some(file_name) = rust_crate.file_name() { crate_names.push(file_name.to_str().unwrap().to_string()); } directory = create_skeleton_files(directory, &rust_crate)?; } Ok((directory, crate_names)) } pub async fn base_rust_image( client: Arc, args: &GlobalArgs, platform: Option, bin_name: String, ) -> eyre::Result { let dep_src = get_rust_dep_src(client.clone(), args).await?; let (skeleton_files, crates) = get_rust_skeleton_files(client.clone(), args).await?; let src = get_src(client.clone(), args)?; let client = client.pipeline("rust_base_image"); let rust_target = match platform.unwrap_or("linux/amd64".to_string()).as_str() { "linux/amd64" => "x86_64-unknown-linux-gnu", "linux/arm64" => "aarch64-unknown-linux-gnu", _ => eyre::bail!("architecture not supported"), }; let rust_build_image = client .container() .from( args.rust_builder_image .as_ref() .unwrap_or(&"rustlang/rust:nightly".into()), ) .with_exec(vec!["rustup", "target", "add", rust_target]); let target_cache = client.cache_volume("rust_target"); let rust_prebuild = rust_build_image .with_workdir("/mnt/src") .with_directory("/mnt/src", dep_src.id().await?) .with_directory("/mnt/src/", skeleton_files.id().await?) .with_exec(vec![ "cargo", "build", "--target", rust_target, "--release", "-p", &bin_name, ]) .with_mounted_cache("/mnt/src/target/", target_cache.id().await?); let exclude = crates .iter() .filter(|c| **c != "ci") .map(|c| format!("**/*{}*", c.replace("-", "_"))) .collect::>(); let exclude = exclude.iter().map(|c| c.as_str()).collect(); let incremental_dir = client.directory().with_directory_opts( ".", rust_prebuild.directory("target").id().await?, dagger_sdk::DirectoryWithDirectoryOpts { exclude: Some(exclude), include: None, }, ); 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( //format!("/mnt/src/target/{}/release/build", rust_target), "target", //rust_prebuild.id().await?, incremental_dir.id().await?, ) .with_directory("/mnt/src/", src.id().await?); Ok(rust_with_src) }