diff --git a/Cargo.lock b/Cargo.lock index f8d4774..41e77da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,6 +326,7 @@ version = "0.2.0" dependencies = [ "async-trait", "clap", + "dagger-rust", "dagger-sdk", "eyre", "futures", diff --git a/ci/src/main.rs b/ci/src/main.rs index f3e0122..59d8515 100644 --- a/ci/src/main.rs +++ b/ci/src/main.rs @@ -1,6 +1,5 @@ use std::path::PathBuf; - use clap::Args; use clap::Parser; use clap::Subcommand; @@ -62,10 +61,10 @@ async fn main() -> eyre::Result<()> { let _ = dotenv::dotenv(); let _ = color_eyre::install(); - let client = dagger_sdk::connect().await?; - let cli = Command::parse(); + let client = dagger_sdk::connect().await?; + match &cli.commands { Commands::Local { command } => match command { LocalCommands::Test => { @@ -107,7 +106,6 @@ async fn main() -> eyre::Result<()> { } mod please_release { - use dagger_cuddle_please::{models::CuddlePleaseSrcArgs, DaggerCuddlePleaseAction}; @@ -136,7 +134,7 @@ mod please_release { } mod test { - use std::{path::PathBuf}; + use std::path::PathBuf; use dagger_rust::build::RustVersion; diff --git a/crates/cuddle-ci/Cargo.toml b/crates/cuddle-ci/Cargo.toml index a8b3b33..077f069 100644 --- a/crates/cuddle-ci/Cargo.toml +++ b/crates/cuddle-ci/Cargo.toml @@ -10,6 +10,8 @@ repository.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +dagger-rust.workspace = true + dagger-sdk.workspace = true eyre.workspace = true clap.workspace = true diff --git a/crates/cuddle-ci/src/dagger_middleware.rs b/crates/cuddle-ci/src/dagger_middleware.rs index 4950f31..e059155 100644 --- a/crates/cuddle-ci/src/dagger_middleware.rs +++ b/crates/cuddle-ci/src/dagger_middleware.rs @@ -5,7 +5,7 @@ use std::{future::Future, pin::Pin}; #[async_trait] pub trait DaggerMiddleware { async fn handle( - &mut self, + &self, container: dagger_sdk::Container, ) -> eyre::Result { Ok(container) @@ -14,14 +14,14 @@ pub trait DaggerMiddleware { pub struct DaggerMiddlewareFn where - F: FnMut(Container) -> Pin> + Send>>, + F: Fn(Container) -> Pin> + Send>>, { pub func: F, } pub fn middleware(func: F) -> Box> where - F: FnMut(Container) -> Pin> + Send>>, + F: Fn(Container) -> Pin> + Send>>, { Box::new(DaggerMiddlewareFn { func }) } @@ -29,11 +29,9 @@ where #[async_trait] impl DaggerMiddleware for DaggerMiddlewareFn where - F: FnMut(Container) -> Pin> + Send>> - + Send - + Sync, + F: Fn(Container) -> Pin> + Send>> + Send + Sync, { - async fn handle(&mut self, container: Container) -> eyre::Result { + async fn handle(&self, container: Container) -> eyre::Result { // Call the closure stored in the struct (self.func)(container).await } diff --git a/crates/cuddle-ci/src/rust_service.rs b/crates/cuddle-ci/src/rust_service.rs index 9956263..05d0884 100644 --- a/crates/cuddle-ci/src/rust_service.rs +++ b/crates/cuddle-ci/src/rust_service.rs @@ -1,5 +1,9 @@ +use std::path::PathBuf; + use async_trait::async_trait; +use dagger_rust::source::RustSource; use dagger_sdk::Container; +use futures::{stream, StreamExt}; use crate::{dagger_middleware::DaggerMiddleware, MainAction, PullRequestAction}; @@ -10,16 +14,22 @@ pub enum RustServiceStage { AfterDeps(DynMiddleware), BeforeBase(DynMiddleware), AfterBase(DynMiddleware), + BeforeBuild(DynMiddleware), + AfterBuild(DynMiddleware), + BeforePackage(DynMiddleware), + AfterPackage(DynMiddleware), BeforeRelease(DynMiddleware), AfterRelease(DynMiddleware), } pub struct RustService { client: dagger_sdk::Query, - base_image: Option, - + final_image: Option, stages: Vec, + source: Option, + crates: Vec, + bin_name: String, } impl From for RustService { @@ -27,7 +37,11 @@ impl From for RustService { Self { client: value, base_image: None, + final_image: None, stages: Vec::new(), + source: None, + crates: Vec::new(), + bin_name: String::new(), } } } @@ -37,7 +51,11 @@ impl RustService { Ok(Self { client: dagger_sdk::connect().await?, base_image: None, + final_image: None, stages: Vec::new(), + source: None, + crates: Vec::new(), + bin_name: String::new(), }) } @@ -47,23 +65,206 @@ impl RustService { self } - pub fn with_stage(&mut self, _stage: RustServiceStage) -> &mut Self { + pub fn with_stage(&mut self, stage: RustServiceStage) -> &mut Self { + self.stages.push(stage); + self } - pub fn with_sqlx(&mut self) -> &mut Self { + pub fn with_source(&mut self, path: impl Into) -> &mut Self { + self.source = Some(path.into()); + self } - pub async fn build_release(&self) -> eyre::Result> { - Ok(Vec::new()) + pub fn with_bin_name(&mut self, bin_name: impl Into) -> &mut Self { + self.bin_name = bin_name.into(); + + self + } + + pub fn with_crates( + &mut self, + crates: impl IntoIterator>, + ) -> &mut Self { + self.crates = crates.into_iter().map(|c| c.into()).collect(); + + self + } + + fn get_src(&self) -> PathBuf { + self.source + .clone() + .unwrap_or(std::env::current_dir().unwrap()) + } + + async fn run_stage( + &self, + stages: impl IntoIterator>, + container: Container, + ) -> eyre::Result { + let before_deps_stream = stream::iter(stages.into_iter().map(Ok)); + let res = StreamExt::fold(before_deps_stream, Ok(container), |base, m| async move { + match (base, m) { + (Ok(base), Ok(m)) => m.handle(base).await, + (_, Err(e)) | (Err(e), _) => eyre::bail!("failed with {e}"), + } + }) + .await?; + + Ok(res) + } + + pub async fn build_base(&self) -> eyre::Result { + let rust_src = RustSource::new(self.client.clone()); + + let (src, dep_src) = rust_src + .get_rust_src(Some(&self.get_src()), self.crates.clone()) + .await?; + + let base_image = self + .base_image + .clone() + .unwrap_or(self.client.container().from("rustlang/rust:nightly")); + + let before_deps = self + .stages + .iter() + .filter_map(|s| match s { + RustServiceStage::BeforeDeps(middleware) => Some(middleware), + _ => None, + }) + .collect::>(); + let image = self.run_stage(before_deps, base_image).await?; + + let after_deps = self + .stages + .iter() + .filter_map(|s| match s { + RustServiceStage::AfterDeps(m) => Some(m), + _ => None, + }) + .collect::>(); + let image = self.run_stage(after_deps, image).await?; + + let before_base = self + .stages + .iter() + .filter_map(|s| match s { + RustServiceStage::BeforeBase(m) => Some(m), + _ => None, + }) + .collect::>(); + let image = self.run_stage(before_base, image).await?; + + let cache = self.client.cache_volume("rust_target_cache"); + + let rust_prebuild = image + .with_workdir("/mnt/src") + .with_directory("/mnt/src", dep_src) + .with_exec(vec!["cargo", "build", "--release", "--bin", &self.bin_name]) + .with_mounted_cache("/mnt/src/target/", cache); + + let incremental_dir = rust_src + .get_rust_target_src(&self.get_src(), rust_prebuild.clone(), self.crates.clone()) + .await?; + + let rust_with_src = image + .with_workdir("/mnt/src") + .with_directory( + "/usr/local/cargo", + rust_prebuild.directory("/usr/local/cargo"), + ) + .with_directory("/mnt/src/target", incremental_dir) + .with_directory("/mnt/src/", src); + + let after_base = self + .stages + .iter() + .filter_map(|s| match s { + RustServiceStage::AfterBase(m) => Some(m), + _ => None, + }) + .collect::>(); + let image = self.run_stage(after_base, rust_with_src).await?; + + Ok(image) + } + + pub async fn build_release(&self) -> eyre::Result { + let base = self.build_base().await?; + + let before_build = self + .stages + .iter() + .filter_map(|s| match s { + RustServiceStage::BeforeBuild(m) => Some(m), + _ => None, + }) + .collect::>(); + let base = self.run_stage(before_build, base).await?; + + let binary_build = + base.with_exec(vec!["cargo", "build", "--release", "--bin", &self.bin_name]); + + let after_build = self + .stages + .iter() + .filter_map(|s| match s { + RustServiceStage::AfterBuild(m) => Some(m), + _ => None, + }) + .collect::>(); + let binary_build = self.run_stage(after_build, binary_build).await?; + + let dest = self + .final_image + .clone() + .unwrap_or(self.client.container().from("debian:bullseye")); + + let before_package = self + .stages + .iter() + .filter_map(|s| match s { + RustServiceStage::BeforePackage(m) => Some(m), + _ => None, + }) + .collect::>(); + let dest = self.run_stage(before_package, dest).await?; + + let final_image = dest.with_workdir("/mnt/app").with_file( + format!("/usr/local/bin/{}", self.bin_name), + binary_build.file(format!("/mnt/src/target/release/{}", self.bin_name)), + ); + + let after_package = self + .stages + .iter() + .filter_map(|s| match s { + RustServiceStage::AfterPackage(m) => Some(m), + _ => None, + }) + .collect::>(); + let final_image = self.run_stage(after_package, final_image).await?; + + Ok(final_image) + } + + pub async fn build_test(&self) -> eyre::Result<()> { + let base = self.build_base().await?; + + base.with_exec(vec!["cargo", "test", "--release"]) + .sync() + .await?; + + Ok(()) } } #[async_trait] impl PullRequestAction for RustService { async fn execute_pull_request(&self) -> eyre::Result<()> { - self.build_release().await?; + let _container = self.build_release().await?; Ok(()) } @@ -76,26 +277,68 @@ impl MainAction for RustService { } } +pub mod architecture { + #[derive(Debug)] + pub enum Architecture { + Amd64, + Arm64, + } + + #[derive(Debug)] + pub enum Os { + Linux, + MacOS, + } +} + +pub mod apt; +pub mod cargo_binstall; +pub mod clap_sanity_test; +pub mod mold; +pub mod sqlx; + #[cfg(test)] mod test { use futures::FutureExt; use crate::{ dagger_middleware::middleware, - rust_service::{RustService, RustServiceStage}, + rust_service::{ + apt::AptExt, + architecture::{Architecture, Os}, + cargo_binstall::CargoBInstallExt, + clap_sanity_test::ClapSanityTestExt, + mold::MoldActionExt, + RustService, RustServiceStage, + }, }; #[tokio::test] - async fn can_build_rust() -> eyre::Result<()> { + async fn test_can_build_rust() -> eyre::Result<()> { let client = dagger_sdk::connect().await?; - RustService::from(client.clone()) - .with_base_image(client.container().from("rustlang/rust:nightly")) - .with_sqlx() - .with_stage(RustServiceStage::BeforeDeps(middleware(|c| async move { Ok(c) }.boxed()))) + let root_dir = std::path::PathBuf::from("../../").canonicalize()?; + + let container = RustService::from(client.clone()) + .with_apt(&["git"]) + .with_cargo_binstall(Architecture::Amd64, Os::Linux, "latest", ["sqlx-cli"]) + .with_source(root_dir) + .with_bin_name("ci") + .with_crates(["crates/*", "examples/*", "ci"]) + .with_mold(Architecture::Amd64, Os::Linux, "2.3.3") + .with_stage(RustServiceStage::BeforeDeps(middleware(|c| { + async move { + // Noop + Ok(c) + } + .boxed() + }))) + .with_clap_sanity_test() .build_release() .await?; + container.sync().await?; + Ok(()) } } diff --git a/crates/cuddle-ci/src/rust_service/apt.rs b/crates/cuddle-ci/src/rust_service/apt.rs new file mode 100644 index 0000000..3fcbb47 --- /dev/null +++ b/crates/cuddle-ci/src/rust_service/apt.rs @@ -0,0 +1,52 @@ +use async_trait::async_trait; +use dagger_sdk::Container; + +use crate::dagger_middleware::DaggerMiddleware; + +use super::RustService; + +pub struct Apt { + deps: Vec, +} + +impl Apt { + pub fn new() -> Self { + Self { deps: Vec::new() } + } + + pub fn add(mut self, dep_name: impl Into) -> Self { + self.deps.push(dep_name.into()); + + self + } + + pub fn extend(mut self, deps: &[&str]) -> Self { + self.deps.extend(deps.iter().map(|s| s.to_string())); + + self + } +} + +#[async_trait] +impl DaggerMiddleware for Apt { + async fn handle(&self, container: Container) -> eyre::Result { + let mut deps = vec!["apt", "install", "-y"]; + deps.extend(self.deps.iter().map(|s| s.as_str())); + + let c = container.with_exec(vec!["apt", "update"]).with_exec(deps); + + Ok(c) + } +} + +pub trait AptExt { + fn with_apt(&mut self, deps: &[&str]) -> &mut Self { + self + } +} + +impl AptExt for RustService { + fn with_apt(&mut self, deps: &[&str]) -> &mut Self { + self.with_stage(super::RustServiceStage::BeforeDeps(Box::new(Apt::new().extend(deps)))) + } +} diff --git a/crates/cuddle-ci/src/rust_service/cargo_binstall.rs b/crates/cuddle-ci/src/rust_service/cargo_binstall.rs new file mode 100644 index 0000000..0c9e9e6 --- /dev/null +++ b/crates/cuddle-ci/src/rust_service/cargo_binstall.rs @@ -0,0 +1,109 @@ +use async_trait::async_trait; +use dagger_sdk::Container; + +use crate::dagger_middleware::DaggerMiddleware; + +use super::{ + architecture::{Architecture, Os}, + RustService, +}; + +pub struct CargoBInstall { + arch: Architecture, + os: Os, + version: String, + crates: Vec, +} + +impl CargoBInstall { + pub fn new( + arch: Architecture, + os: Os, + version: impl Into, + crates: impl Into>, + ) -> Self { + Self { + arch, + os, + version: version.into(), + crates: crates.into(), + } + } + + fn get_arch(&self) -> String { + match self.arch { + Architecture::Amd64 => "x86_64", + Architecture::Arm64 => "armv7", + } + .into() + } + + fn get_os(&self) -> String { + match self.os { + Os::Linux => "linux", + Os::MacOS => "darwin", + } + .into() + } + + pub fn get_download_url(&self) -> String { + format!("https://github.com/cargo-bins/cargo-binstall/releases/{}/download/cargo-binstall-{}-unknown-{}-musl.tgz", self.version, self.get_arch(), self.get_os()) + } + + pub fn get_archive(&self) -> String { + format!( + "cargo-binstall-{}-unknown-{}-musl.tgz", + self.get_arch(), + self.get_os() + ) + } +} + +#[async_trait] +impl DaggerMiddleware for CargoBInstall { + async fn handle(&self, container: Container) -> eyre::Result { + let c = + container + .with_exec(vec!["wget", &self.get_download_url()]) + .with_exec(vec!["tar", "-xvf", &self.get_archive()]) + .with_exec( + "mv cargo-binstall /usr/local/cargo/bin" + .split_whitespace() + .collect(), + ); + + let c = self.crates.iter().cloned().fold(c, |acc, item| { + acc.with_exec(vec!["cargo", "binstall", &item, "-y"]) + }); + + Ok(c) + } +} + +pub trait CargoBInstallExt { + fn with_cargo_binstall( + &mut self, + arch: Architecture, + os: Os, + version: impl Into, + crates: impl IntoIterator>, + ) -> &mut Self { + self + } +} + +impl CargoBInstallExt for RustService { + fn with_cargo_binstall( + &mut self, + arch: Architecture, + os: Os, + version: impl Into, + crates: impl IntoIterator>, + ) -> &mut Self { + let crates: Vec = crates.into_iter().map(|s| s.into()).collect(); + + self.with_stage(super::RustServiceStage::BeforeDeps( + Box::new(CargoBInstall::new(arch, os, version, crates)) + )) + } +} diff --git a/crates/cuddle-ci/src/rust_service/clap_sanity_test.rs b/crates/cuddle-ci/src/rust_service/clap_sanity_test.rs new file mode 100644 index 0000000..5e9ac60 --- /dev/null +++ b/crates/cuddle-ci/src/rust_service/clap_sanity_test.rs @@ -0,0 +1,41 @@ +use async_trait::async_trait; +use dagger_sdk::Container; + +use crate::dagger_middleware::DaggerMiddleware; + +use super::RustService; + +pub struct ClapSanityTest { + bin_name: String, +} + +impl ClapSanityTest { + pub fn new(bin_name: impl Into) -> Self { + Self { + bin_name: bin_name.into(), + } + } +} + +#[async_trait] +impl DaggerMiddleware for ClapSanityTest { + async fn handle(&self, container: Container) -> eyre::Result { + Ok(container.with_exec(vec![&self.bin_name, "--help"])) + } +} + +pub trait ClapSanityTestExt { + fn with_clap_sanity_test(&mut self) -> &mut Self { + self + } +} + +impl ClapSanityTestExt for RustService { + fn with_clap_sanity_test(&mut self) -> &mut Self { + self.with_stage( + super::RustServiceStage::AfterPackage(Box::new(ClapSanityTest::new(&self.bin_name))) + ); + + self + } +} diff --git a/crates/cuddle-ci/src/rust_service/mold.rs b/crates/cuddle-ci/src/rust_service/mold.rs new file mode 100644 index 0000000..7df1ea6 --- /dev/null +++ b/crates/cuddle-ci/src/rust_service/mold.rs @@ -0,0 +1,112 @@ +use async_trait::async_trait; + +use crate::dagger_middleware::DaggerMiddleware; + +use super::{ + architecture::{Architecture, Os}, + RustService, +}; + +pub struct MoldInstall { + arch: Architecture, + os: Os, + version: String, +} + +impl MoldInstall { + pub fn new(arch: Architecture, os: Os, version: impl Into) -> Self { + Self { + arch, + os, + version: version.into(), + } + } + + fn get_arch(&self) -> String { + match self.arch { + Architecture::Amd64 => "x86_64", + Architecture::Arm64 => "arm", + } + .into() + } + fn get_os(&self) -> String { + match &self.os { + Os::Linux => "linux", + o => todo!("os not implemented for mold: {:?}", o), + } + .into() + } + + pub fn get_download_url(&self) -> String { + format!( + "https://github.com/rui314/mold/releases/download/v{}/mold-{}-{}-{}.tar.gz", + self.version, + self.version, + self.get_arch(), + self.get_os() + ) + } + + pub fn get_folder(&self) -> String { + format!( + "mold-{}-{}-{}", + self.version, + self.get_arch(), + self.get_os() + ) + } + + pub fn get_archive_name(&self) -> String { + format!( + "mold-{}-{}-{}.tar.gz", + self.version, + self.get_arch(), + self.get_os() + ) + } +} + +#[async_trait] +impl DaggerMiddleware for MoldInstall { + async fn handle( + &self, + container: dagger_sdk::Container, + ) -> eyre::Result { + println!("installing mold"); + + let c = container + .with_exec(vec!["wget", &self.get_download_url()]) + .with_exec(vec!["tar", "-xvf", &self.get_archive_name()]) + .with_exec(vec![ + "mv", + &format!("{}/bin/mold", self.get_folder()), + "/usr/bin/mold", + ]); + + Ok(c) + } +} + +pub trait MoldActionExt { + fn with_mold( + &mut self, + architecture: Architecture, + os: Os, + version: impl Into, + ) -> &mut Self { + self + } +} + +impl MoldActionExt for RustService { + fn with_mold( + &mut self, + architecture: Architecture, + os: Os, + version: impl Into, + ) -> &mut Self { + self.with_stage(super::RustServiceStage::AfterDeps( + Box::new(MoldInstall::new(architecture, os, version)) + )) + } +} diff --git a/crates/cuddle-ci/src/rust_service/sqlx.rs b/crates/cuddle-ci/src/rust_service/sqlx.rs new file mode 100644 index 0000000..104613b --- /dev/null +++ b/crates/cuddle-ci/src/rust_service/sqlx.rs @@ -0,0 +1,12 @@ +use async_trait::async_trait; +use dagger_sdk::Container; + +use crate::dagger_middleware::DaggerMiddleware; + +pub struct Sqlx {} +#[async_trait] +impl DaggerMiddleware for Sqlx { + async fn handle(&self, container: Container) -> eyre::Result { + Ok(container.with_env_variable("SQLX_OFFLINE", "true")) + } +}