//! Tasks related to zellij CI use crate::{ build, flags::{self, BuildRelease, CiCmd, Cross, E2e}, metadata, }; use anyhow::Context; use std::{ ffi::OsString, path::{Path, PathBuf}, }; use xshell::{cmd, Shell}; pub fn main(sh: &Shell, flags: flags::Ci) -> anyhow::Result<()> { let err_context = "failed to run CI task"; match flags.subcommand { CiCmd::E2e(E2e { build: false, test: false, .. }) => Err(anyhow::anyhow!( "either '--build' or '--test' must be provided!" )), CiCmd::E2e(E2e { build: true, test: true, .. }) => Err(anyhow::anyhow!( "flags '--build' and '--test' are mutually exclusive!" )), CiCmd::E2e(E2e { build: true, test: false, .. }) => e2e_build(sh), CiCmd::E2e(E2e { build: false, test: true, args, }) => e2e_test(sh, args), CiCmd::Cross(Cross { triple, no_web }) => cross_compile(sh, &triple, no_web), CiCmd::BuildRelease(BuildRelease { no_web }) => build_release(sh, no_web), } .context(err_context) } fn e2e_build(sh: &Shell) -> anyhow::Result<()> { let err_context = "failed to build E2E binary"; build::build( sh, flags::Build { release: true, no_plugins: false, plugins_only: true, no_web: false, args: vec![], }, ) .context(err_context)?; // Copy plugins to e2e data-dir let plugin_dir = crate::asset_dir().join("plugins"); let project_root = crate::project_root(); let data_dir = crate::target_dir().join("e2e-data"); let plugins: Vec<_> = std::fs::read_dir(plugin_dir) .context(err_context)? .filter_map(|dir_entry| { if let Ok(entry) = dir_entry { entry .file_name() .to_string_lossy() .ends_with(".wasm") .then_some(entry.path()) } else { None } }) .collect(); sh.remove_path(&data_dir) .and_then(|_| sh.create_dir(&data_dir)) .and_then(|_| sh.create_dir(data_dir.join("plugins"))) .context(err_context)?; for plugin in plugins { sh.copy_file(plugin, data_dir.join("plugins")) .context(err_context)?; } let _pd = sh.push_dir(project_root); // On Windows, build natively (MSVC). // On macOS: use cargo-zigbuild which handles musl cross-compilation for any arch without // needing a separate linker toolchain. The target arch matches the host so the Docker // container runs natively without emulation (arm64 on Apple Silicon, amd64 on Intel). // Install with: cargo install cargo-zigbuild && brew install zig // On Linux: cross-compile for musl. if cfg!(windows) { crate::cargo() .and_then(|cargo| { cmd!(sh, "{cargo} build --release") .run() .map_err(anyhow::Error::new) }) .context(err_context) } else if cfg!(target_os = "macos") { let target = if cfg!(target_arch = "aarch64") { "aarch64-unknown-linux-musl" } else { "x86_64-unknown-linux-musl" }; crate::cargo() .and_then(|cargo| { if which::which("cargo-zigbuild").is_err() { eprintln!("!! 'cargo-zigbuild' wasn't found but is needed on macOS."); eprintln!("!! Please install it with:"); eprintln!("!! cargo install cargo-zigbuild"); eprintln!("!! brew install zig"); return Err(anyhow::anyhow!("couldn't find 'cargo-zigbuild'")); } cmd!(sh, "rustup target add {target}") .run() .map_err(anyhow::Error::new)?; cmd!(sh, "{cargo} zigbuild --release --target {target}") .run() .map_err(anyhow::Error::new) }) .context(err_context) } else { crate::cargo() .and_then(|cargo| { cmd!( sh, "{cargo} build --release --target x86_64-unknown-linux-musl" ) .run() .map_err(anyhow::Error::new) }) .context(err_context) } } /// Native release build: builds plugins, generates protobufs, and runs /// `cargo build --release` without cross-compilation. fn build_release(sh: &Shell, no_web: bool) -> anyhow::Result<()> { let err_context = "failed to perform native release build"; // Build plugins and generate protobufs build::build( sh, flags::Build { release: true, no_plugins: false, plugins_only: true, no_web, args: vec![], }, ) .context(err_context)?; // Build the main binary natively let _pd = sh.push_dir(crate::project_root()); crate::cargo() .and_then(|cargo| { if no_web { match metadata::get_no_web_features(sh, ".") .context("Failed to check web features for build-release")? { Some(features) => { let mut cmd = cmd!( sh, "{cargo} build --verbose --release --no-default-features" ); if !features.is_empty() { cmd = cmd.arg("--features").arg(features); } cmd.run().map_err(anyhow::Error::new) }, None => cmd!(sh, "{cargo} build --verbose --release") .run() .map_err(anyhow::Error::new), } } else { cmd!(sh, "{cargo} build --verbose --release") .run() .map_err(anyhow::Error::new) } }) .context(err_context) } fn e2e_test(sh: &Shell, args: Vec) -> anyhow::Result<()> { let err_context = "failed to run E2E tests"; e2e_build(sh).context(err_context)?; let _pd = sh.push_dir(crate::project_root()); // set --no-default-features so the test binary gets built with the plugins from assets/plugins that just got built crate::cargo() .and_then(|cargo| { // e2e tests cmd!( sh, "{cargo} test --no-default-features -- --ignored --nocapture --test-threads 1" ) .args(args.clone()) .run() .map_err(anyhow::Error::new)?; // plugin system tests are run here because they're medium-slow let _pd = sh.push_dir(Path::new("zellij-server")); println!(); let msg = ">> Testing Plugin System".to_string(); crate::status(&msg); println!("{}", msg); cmd!(sh, "{cargo} test -- --ignored --nocapture --test-threads 1") .args(args.clone()) .run() .with_context(|| "Failed to run tests for the Plugin System".to_string())?; Ok(()) }) .context(err_context) } fn cross_compile(sh: &Shell, target: &OsString, no_web: bool) -> anyhow::Result<()> { let err_context = || format!("failed to cross-compile for {target:?}"); crate::cargo() .and_then(|cargo| { cmd!(sh, "{cargo} install cross") .run() .map_err(anyhow::Error::new) }) .with_context(err_context)?; build::build( sh, flags::Build { release: true, no_plugins: false, plugins_only: true, no_web, args: vec![], }, ) .with_context(err_context)?; cross() .and_then(|cross| { if no_web { match metadata::get_no_web_features(sh, ".") .context("Failed to check web features for cross compilation")? { Some(features) => { let mut cmd = cmd!(sh, "{cross} build --verbose --release --target {target} --no-default-features"); if !features.is_empty() { cmd = cmd.arg("--features").arg(features); } cmd.run().map_err(anyhow::Error::new) }, None => { // Main crate doesn't have web_server_capability, build normally cmd!(sh, "{cross} build --verbose --release --target {target}") .run() .map_err(anyhow::Error::new) }, } } else { cmd!(sh, "{cross} build --verbose --release --target {target}") .run() .map_err(anyhow::Error::new) } }) .with_context(err_context) } fn cross() -> anyhow::Result { match which::which("cross") { Ok(path) => Ok(path), Err(e) => { eprintln!("!! 'cross' wasn't found but is needed for this build step."); eprintln!("!! Please install it with: `cargo install cross`"); Err(e).context("couldn't find 'cross' executable") }, } }