From 40560e59604d963e757bc691acfae9c6b895d672 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Sat, 26 Jul 2025 14:18:30 +0200 Subject: [PATCH] Add `xtask` for build and test automation, replacing legacy scripts, and integrate commands for environment setup, build, test, and formatting. Update documentation for cargo-xtask usage. --- .junie/guidelines.md | 57 ++++++++--- Cargo.lock | 12 ++- Cargo.toml | 1 + xtask/Cargo.toml | 10 ++ xtask/src/main.rs | 233 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 298 insertions(+), 15 deletions(-) create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/main.rs diff --git a/.junie/guidelines.md b/.junie/guidelines.md index c406a2c..ba3e85e 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -130,40 +130,41 @@ cargo test -p ### Setting Up Test Environment -The project includes a script to set up the test environment for repository tests: +The project uses cargo-xtask for automation tasks, including setting up the test environment: ```bash -./setup_test_env.sh +cargo xtask setup-test-env ``` -This script: +This command: 1. Creates test directories in `/tmp/pkg6_test` 2. Compiles the applications 3. Creates a prototype directory structure with sample files 4. Creates package manifests for testing +The legacy script `./setup_test_env.sh` is still available but is being phased out in favor of cargo-xtask. + ### Writing Tests - Unit tests should be placed in the same file as the code they're testing, in a `mod tests` block - Integration tests should be placed in the `tests` directory of each crate -- End-to-end tests should use the test environment set up by `setup_test_env.sh` +- End-to-end tests should use the test environment set up by `cargo xtask setup-test-env` ## Build Guidelines ### Building the Project -To build the entire project: +Using cargo directly: ```bash -cargo build +cargo build # Build the entire project +cargo build -p # Build a specific crate +cargo build --release # Build with optimizations for release ``` -To build a specific crate: +Using cargo-xtask: ```bash -cargo build -p -``` - -To build with optimizations for release: -```bash -cargo build --release +cargo xtask build # Build the entire project +cargo xtask build -p # Build a specific crate +cargo xtask build -r # Build with optimizations for release ``` ### Build Order @@ -180,6 +181,36 @@ The crates are built in the following order (as specified in the workspace Cargo This order is important as it reflects the dependency hierarchy, with `libips` being the foundation that other crates build upon. +## Cargo-xtask + +The project uses [cargo-xtask](https://github.com/matklad/cargo-xtask) for automation of tests and builds. This approach allows us to write build scripts and automation tasks in Rust instead of shell scripts, making them more maintainable and cross-platform. + +### Available Commands + +The following commands are available through cargo-xtask: + +```bash +cargo xtask setup-test-env # Set up the test environment for repository tests +cargo xtask build # Build the project +cargo xtask build -r # Build with release optimizations +cargo xtask build -p # Build a specific crate +cargo xtask test # Run tests +cargo xtask test -r # Run tests with release optimizations +cargo xtask test -p # Run tests for a specific crate +cargo xtask fmt # Format code using cargo fmt +cargo xtask clippy # Run clippy for code quality checks +cargo xtask clean # Clean build artifacts +``` + +### Adding New Commands + +To add a new command to cargo-xtask: + +1. Edit the `xtask/src/main.rs` file +2. Add a new variant to the `Commands` enum +3. Implement a function for the new command +4. Add a match arm in the `main` function to call your new function + ## Code Style Guidelines ### General Guidelines diff --git a/Cargo.lock b/Cargo.lock index 89efeee..554df2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,9 +99,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.70" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arc-swap" @@ -2987,6 +2987,14 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.5.41", +] + [[package]] name = "zerocopy" version = "0.8.26" diff --git a/Cargo.toml b/Cargo.toml index 3b668f2..5d8d4d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "specfile", "ports", "crates/*", + "xtask", ] resolver = "2" \ No newline at end of file diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000..525adc2 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" +description = "Build and test automation for IPS" +publish = false + +[dependencies] +anyhow = "1.0.75" +clap = { version = "4.4.6", features = ["derive"] } diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..f3f69ed --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,233 @@ +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; +use std::fs::{self, File}; +use std::io::Write; +use std::path::Path; +use std::process::Command; + +// Constants +const TEST_BASE_DIR: &str = "/tmp/pkg6_test"; +const PROTOTYPE_DIR: &str = "/tmp/pkg6_test/prototype"; +const MANIFEST_DIR: &str = "/tmp/pkg6_test/manifests"; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Set up the test environment for repository tests + SetupTestEnv, + + /// Build the project + Build { + /// Build with release optimizations + #[arg(short, long)] + release: bool, + + /// Specific crate to build + #[arg(short, long)] + package: Option, + }, + + /// Run tests + Test { + /// Run tests with release optimizations + #[arg(short, long)] + release: bool, + + /// Specific crate to test + #[arg(short, long)] + package: Option, + }, + + /// Format code + Fmt, + + /// Run clippy + Clippy, + + /// Clean build artifacts + Clean, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + match &cli.command { + Commands::SetupTestEnv => setup_test_env(), + Commands::Build { release, package } => build(release, package), + Commands::Test { release, package } => test(release, package), + Commands::Fmt => fmt(), + Commands::Clippy => clippy(), + Commands::Clean => clean(), + } +} + +/// Set up the test environment for repository tests +fn setup_test_env() -> Result<()> { + println!("Setting up test environment..."); + + // Clean up any existing test directories + if Path::new(TEST_BASE_DIR).exists() { + println!("Cleaning up existing test directory..."); + fs::remove_dir_all(TEST_BASE_DIR).context("Failed to remove existing test directory")?; + } + + // Create test directories + println!("Creating test directories..."); + fs::create_dir_all(PROTOTYPE_DIR).context("Failed to create prototype directory")?; + fs::create_dir_all(MANIFEST_DIR).context("Failed to create manifest directory")?; + + // Compile the applications + println!("Compiling applications..."); + Command::new("cargo") + .arg("build") + .status() + .context("Failed to compile applications")?; + + // Create a simple prototype directory structure with some files + println!("Creating prototype directory structure..."); + + // Create some directories + fs::create_dir_all(format!("{}/usr/bin", PROTOTYPE_DIR)).context("Failed to create usr/bin directory")?; + fs::create_dir_all(format!("{}/usr/share/doc/example", PROTOTYPE_DIR)).context("Failed to create usr/share/doc/example directory")?; + fs::create_dir_all(format!("{}/etc/config", PROTOTYPE_DIR)).context("Failed to create etc/config directory")?; + + // Create some files + let hello_script = "#!/bin/sh\necho 'Hello, World!'"; + let mut hello_file = File::create(format!("{}/usr/bin/hello", PROTOTYPE_DIR)).context("Failed to create hello script")?; + hello_file.write_all(hello_script.as_bytes()).context("Failed to write hello script")?; + + // Make the script executable + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(format!("{}/usr/bin/hello", PROTOTYPE_DIR)) + .context("Failed to get hello script metadata")? + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(format!("{}/usr/bin/hello", PROTOTYPE_DIR), perms) + .context("Failed to set hello script permissions")?; + } + + let readme_content = "This is an example document."; + let mut readme_file = File::create(format!("{}/usr/share/doc/example/README.txt", PROTOTYPE_DIR)) + .context("Failed to create README.txt")?; + readme_file.write_all(readme_content.as_bytes()).context("Failed to write README.txt")?; + + let config_content = "# Example configuration file\nvalue=42"; + let mut config_file = File::create(format!("{}/etc/config/example.conf", PROTOTYPE_DIR)) + .context("Failed to create example.conf")?; + config_file.write_all(config_content.as_bytes()).context("Failed to write example.conf")?; + + // Create a simple manifest + println!("Creating package manifest..."); + let example_manifest = r#"set name=pkg.fmri value=pkg://test/example@1.0.0 +set name=pkg.summary value="Example package for testing" +set name=pkg.description value="This is an example package used for testing the repository implementation." +set name=info.classification value="org.opensolaris.category.2008:System/Core" +set name=variant.arch value=i386 value=sparc +file path=usr/bin/hello mode=0755 owner=root group=bin +file path=usr/share/doc/example/README.txt mode=0644 owner=root group=bin +file path=etc/config/example.conf mode=0644 owner=root group=bin preserve=true +dir path=usr/bin mode=0755 owner=root group=bin +dir path=usr/share/doc/example mode=0755 owner=root group=bin +dir path=etc/config mode=0755 owner=root group=sys +"#; + + let mut example_file = File::create(format!("{}/example.p5m", MANIFEST_DIR)) + .context("Failed to create example.p5m")?; + example_file.write_all(example_manifest.as_bytes()).context("Failed to write example.p5m")?; + + // Create a second manifest for testing multiple packages + let example2_manifest = r#"set name=pkg.fmri value=pkg://test/example2@1.0.0 +set name=pkg.summary value="Second example package for testing" +set name=pkg.description value="This is a second example package used for testing the repository implementation." +set name=info.classification value="org.opensolaris.category.2008:System/Core" +set name=variant.arch value=i386 value=sparc +file path=usr/bin/hello mode=0755 owner=root group=bin +file path=usr/share/doc/example/README.txt mode=0644 owner=root group=bin +dir path=usr/bin mode=0755 owner=root group=bin +dir path=usr/share/doc/example mode=0755 owner=root group=bin +"#; + + let mut example2_file = File::create(format!("{}/example2.p5m", MANIFEST_DIR)) + .context("Failed to create example2.p5m")?; + example2_file.write_all(example2_manifest.as_bytes()).context("Failed to write example2.p5m")?; + + println!("Test environment setup complete!"); + println!("Prototype directory: {}", PROTOTYPE_DIR); + println!("Manifest directory: {}", MANIFEST_DIR); + + Ok(()) +} + +/// Build the project +fn build(release: &bool, package: &Option) -> Result<()> { + let mut cmd = Command::new("cargo"); + cmd.arg("build"); + + if *release { + cmd.arg("--release"); + } + + if let Some(pkg) = package { + cmd.args(["--package", pkg]); + } + + cmd.status().context("Failed to build project")?; + + Ok(()) +} + +/// Run tests +fn test(release: &bool, package: &Option) -> Result<()> { + let mut cmd = Command::new("cargo"); + cmd.arg("test"); + + if *release { + cmd.arg("--release"); + } + + if let Some(pkg) = package { + cmd.args(["--package", pkg]); + } + + cmd.status().context("Failed to run tests")?; + + Ok(()) +} + +/// Format code +fn fmt() -> Result<()> { + Command::new("cargo") + .arg("fmt") + .status() + .context("Failed to format code")?; + + Ok(()) +} + +/// Run clippy +fn clippy() -> Result<()> { + Command::new("cargo") + .args(["clippy", "--all-targets", "--all-features", "--", "-D", "warnings"]) + .status() + .context("Failed to run clippy")?; + + Ok(()) +} + +/// Clean build artifacts +fn clean() -> Result<()> { + Command::new("cargo") + .arg("clean") + .status() + .context("Failed to clean build artifacts")?; + + Ok(()) +} \ No newline at end of file