use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use build::build_and_deploy; 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; use tokio::sync::Mutex; use tokio::task::JoinHandle; use crate::please_release::run_release_please; #[derive(Parser, Clone)] #[command(author, version, about, long_about = None, subcommand_required = true)] pub struct Command { #[command(subcommand)] commands: Commands, #[command(flatten)] global: GlobalArgs, } #[derive(Subcommand, Clone)] pub enum Commands { #[command(subcommand_required = true)] Local { #[command(subcommand)] command: LocalCommands, }, PullRequest, Main { #[arg(long)] image: String, #[arg(long)] tag: String, #[arg(long)] bin_name: String, }, Release, } #[derive(Subcommand, Clone)] 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, BuildDocs {}, } #[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")] mkdocs_image: Option, #[arg(long, global = true, help_heading = "Global")] caddy_image: Option, #[arg(long, global = true, help_heading = "Global")] source: Option, #[arg(long, global = true, help_heading = "Global")] docs_image: Option, #[arg(long, global = true, help_heading = "Global")] docs_image_tag: Option, } #[tokio::main] async fn main() -> eyre::Result<()> { let _ = dotenv::dotenv(); 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(), &"release".into(), ) .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(), &"debug".into(), ) .await?; test::execute(client, &cli.global, base_image).await?; } LocalCommands::DockerImage { tag, image, bin_name, } => { build::build_and_deploy(client, &cli.global, &bin_name, &image, &tag).await?; } LocalCommands::PleaseRelease => todo!(), LocalCommands::BuildDocs {} => { let image = docs::execute( client.clone(), &cli.global, &Some("linux/amd64".to_string()), ) .await?; } }, Commands::PullRequest => todo!(), Commands::Main { image, tag, bin_name, } => { async fn test(client: Arc, cli: &Command, bin_name: &String) { let args = &cli.global; let base_image = base_rust_image(client.clone(), &args, &None, bin_name, &"debug".into()) .await .unwrap(); test::execute(client.clone(), &args, base_image) .await .unwrap(); } async fn build( client: Arc, cli: &Command, bin_name: &String, image: &String, tag: &String, ) { let args = &cli.global; build::build_and_deploy(client.clone(), &args, bin_name, image, tag) .await .unwrap(); } async fn cuddle_please(client: Arc, cli: &Command) { run_release_please(client.clone(), &cli.global) .await .unwrap(); } tokio::join!( test(client.clone(), &cli, &bin_name), build(client.clone(), &cli, &bin_name, &image, &tag), cuddle_please(client.clone(), &cli) ); } Commands::Release => todo!(), } Ok(()) } mod please_release { use std::sync::Arc; use dagger_sdk::Container; use crate::{base_rust_image, build, get_base_debian_image, get_rust_dep_src, GlobalArgs}; pub async fn run_release_please( client: Arc, args: &GlobalArgs, ) -> eyre::Result<()> { let base_image = base_rust_image( client.clone(), args, &Some("linux/amd64".to_string()), &"cuddle-please".to_string(), &"release".into(), ) .await?; let build_image = base_image.with_exec(vec![ "cargo", "install", "--target", "x86_64-unknown-linux-gnu", "--path=crates/cuddle-please", ]); let src = client .git_opts( "https://git.front.kjuulh.io/kjuulh/cuddle-please", dagger_sdk::QueryGitOpts { experimental_service_host: None, keep_git_dir: Some(true), }, ) .branch("main") .tree(); let res = build_image .with_secret_variable( "CUDDLE_PLEASE_TOKEN", client .set_secret("CUDDLE_PLEASE_TOKEN", std::env::var("CUDDLE_PLEASE_TOKEN")?) .id() .await?, ) .with_workdir("/mnt/app") .with_directory(".", src.id().await?) .with_exec(vec![ "git", "remote", "set-url", "origin", &format!( "https://git:{}@git.front.kjuulh.io/kjuulh/cuddle-please.git", std::env::var("CUDDLE_PLEASE_TOKEN")? ), ]) .with_exec(vec![ "git", "config", "http.extraheader", "'Authorization: token b52c18cab8a95d33f34b0d081440f77a2b156886'", ]) .with_exec(vec![ "cuddle-please", "release", "--engine=gitea", "--owner=kjuulh", "--repo=cuddle-please", "--branch=main", "--api-url=https://git.front.kjuulh.io", "--log-level=debug", ]); let exit_code = res.exit_code().await?; if exit_code != 0 { eyre::bail!("failed to run cuddle-please"); } let please_out = res.stdout().await?; println!("{please_out}"); let please_out = res.stderr().await?; println!("{please_out}"); Ok(()) } } mod docs { use std::sync::Arc; use dagger_sdk::Container; use crate::GlobalArgs; pub fn get_docs_src(client: Arc) -> eyre::Result { let docs_content = client.host().directory_opts( ".", dagger_sdk::HostDirectoryOpts { exclude: None, include: Some(vec!["mkdocs.yml", "docs/"]), }, ); Ok(docs_content) } pub async fn execute( client: Arc, args: &GlobalArgs, platform: &Option, ) -> eyre::Result { let mkdocs_container = client.container().from( args.mkdocs_image .as_ref() .expect("--mkdocs-image to be set"), ); let built_mkdocs_container = mkdocs_container .with_directory("/docs", get_docs_src(client.clone())?.id().await?) .with_exec(vec!["build"]); let site_output = built_mkdocs_container.directory("/docs/site").id().await?; let caddy_file = client.host().directory("templates").file("Caddyfile"); let dep_image = client .container() .from(args.caddy_image.as_ref().expect("--caddy-image to be set")) .with_directory("/usr/share/caddy", site_output) .with_file("/etc/caddy/Caddyfile", caddy_file.id().await?) .with_exec(vec!["echo", "caddy"]); Ok(dep_image) } pub async fn publish( client: Arc, args: &GlobalArgs, containers: &Vec, ) -> eyre::Result<()> { let container_ids = futures::future::join_all(containers.iter().map(|c| c.id()).collect::>()).await; let container_ids = container_ids .into_iter() .collect::>>()?; client .container() .publish_opts( format!( "{}:{}", args.docs_image.as_ref().expect("--docs-image to be set"), args.docs_image_tag .as_ref() .expect("--docs-image-tag to be set") ), dagger_sdk::ContainerPublishOpts { platform_variants: Some(container_ids), }, ) .await?; Ok(()) } } mod build { use std::sync::Arc; use dagger_sdk::Container; use futures::StreamExt; use tokio::sync::Mutex; use tokio::task::JoinHandle; use crate::{base_rust_image, get_base_debian_image, GlobalArgs}; pub async fn build_and_deploy( client: Arc, args: &GlobalArgs, bin_name: &String, image: &String, tag: &String, ) -> eyre::Result<()> { // let containers = vec!["linux/amd64", "linux/arm64"]; let base_image = get_base_debian_image( client.clone(), &args.clone(), Some("linux/amd64".to_string()), ) .await?; let container = base_rust_image( client.clone(), &args, &Some("linux/amd64".to_string()), &bin_name.clone(), &"release".into(), ) .await?; let build_image = execute( client.clone(), &args, &container, &base_image, &bin_name, &Some("linux/amd64".to_string()), ) .await?; let build_id = build_image.id().await?; let _container = client .clone() .container() .publish_opts( format!("{image}:{tag}"), dagger_sdk::ContainerPublishOpts { platform_variants: Some(vec![build_id]), }, ) .await?; Ok(()) } 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 .clone() .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!["apt", "update"]) .with_exec(vec!["apt", "install", "-y", "git"]) .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", "git", ]); 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, profile: &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 .clone() .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(format!("rust_target_{}", profile)); let mut build_options = vec!["cargo", "build", "--target", rust_target, "-p", &bin_name]; if profile == "release" { build_options.push("--release"); } 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(build_options) .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("target", incremental_dir.id().await?) .with_directory("/mnt/src/", src.id().await?); Ok(rust_with_src) }