From 2115aab62daf6fcbc8f52a6b55ace9dd9dbaccf5 Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Mon, 22 Dec 2025 15:31:54 -0500 Subject: [PATCH 1/4] bump to 0.19.1-dev --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0edb47..f91afeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,7 +130,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "avocado-cli" -version = "0.18.0" +version = "0.19.1-dev" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index e6dcf98..2306fb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "avocado-cli" -version = "0.18.0" +version = "0.19.1-dev" edition = "2021" description = "Command line interface for Avocado." authors = ["Avocado"] From ab02d93dc43ceedade43a7f0a51732d7e5b06899 Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Mon, 22 Dec 2025 15:32:07 -0500 Subject: [PATCH 2/4] fix windows builds --- src/commands/runtime/provision.rs | 24 ++++++++++++++++++++++-- src/utils/mod.rs | 1 + tests/signing_integration.rs | 1 + 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/commands/runtime/provision.rs b/src/commands/runtime/provision.rs index 6d4667c..ab70267 100644 --- a/src/commands/runtime/provision.rs +++ b/src/commands/runtime/provision.rs @@ -1,8 +1,9 @@ +#[cfg(unix)] +use crate::utils::signing_service::{generate_helper_script, SigningService, SigningServiceConfig}; use crate::utils::{ config::load_config, container::{RunConfig, SdkContainer}, output::{print_info, print_success, OutputLevel}, - signing_service::{generate_helper_script, SigningService, SigningServiceConfig}, stamps::{ generate_batch_read_stamps_script, generate_write_stamp_script, resolve_required_stamps, validate_stamps_batch, Stamp, StampCommand, StampComponent, StampInputs, StampOutputs, @@ -34,6 +35,7 @@ pub struct RuntimeProvisionConfig { pub struct RuntimeProvisionCommand { config: RuntimeProvisionConfig, + #[cfg(unix)] signing_service: Option, } @@ -41,6 +43,7 @@ impl RuntimeProvisionCommand { pub fn new(config: RuntimeProvisionConfig) -> Self { Self { config, + #[cfg(unix)] signing_service: None, } } @@ -370,6 +373,7 @@ impl RuntimeProvisionCommand { /// Setup signing service if signing is configured for the runtime /// /// Returns Some((socket_path, helper_script_path, key_name, checksum_algorithm)) if signing is enabled + #[cfg(unix)] async fn setup_signing_service( &mut self, config: &crate::utils::config::Config, @@ -423,7 +427,6 @@ impl RuntimeProvisionCommand { .context("Failed to write helper script")?; // Make helper script executable - #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let perms = std::fs::Permissions::from_mode(0o755); @@ -464,7 +467,18 @@ impl RuntimeProvisionCommand { ))) } + /// Setup signing service stub for non-Unix platforms + /// Signing service requires Unix domain sockets and is not available on Windows + #[cfg(not(unix))] + async fn setup_signing_service( + &mut self, + _config: &crate::utils::config::Config, + ) -> Result> { + Ok(None) + } + /// Cleanup signing service resources + #[cfg(unix)] async fn cleanup_signing_service(&mut self) -> Result<()> { if let Some(service) = self.signing_service.take() { service.shutdown().await?; @@ -472,6 +486,12 @@ impl RuntimeProvisionCommand { Ok(()) } + /// Cleanup signing service stub for non-Unix platforms + #[cfg(not(unix))] + async fn cleanup_signing_service(&mut self) -> Result<()> { + Ok(()) + } + /// Fix file ownership of output directory to match calling user async fn fix_output_permissions(&self, out_path: &str) -> Result<()> { // Get the absolute path to the output directory diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 91cec22..52c78ad 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -5,6 +5,7 @@ pub mod interpolation; pub mod output; pub mod pkcs11_devices; pub mod signing_keys; +#[cfg(unix)] pub mod signing_service; pub mod stamps; pub mod target; diff --git a/tests/signing_integration.rs b/tests/signing_integration.rs index b873702..97327e8 100644 --- a/tests/signing_integration.rs +++ b/tests/signing_integration.rs @@ -37,6 +37,7 @@ mod tests { } #[test] + #[cfg(unix)] fn test_helper_script_contains_required_elements() { use avocado_cli::utils::signing_service::generate_helper_script; From a0fd1c3746a41d023faffe53cd665a30b1f0dda1 Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Tue, 23 Dec 2025 10:57:49 -0500 Subject: [PATCH 3/4] use sdk container_args for provision commands --- src/commands/provision.rs | 10 +- src/utils/config.rs | 207 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 200 insertions(+), 17 deletions(-) diff --git a/src/commands/provision.rs b/src/commands/provision.rs index 9d4dd26..02ada8e 100644 --- a/src/commands/provision.rs +++ b/src/commands/provision.rs @@ -47,12 +47,6 @@ impl ProvisionCommand { // Load config to access provision profiles let config = crate::utils::config::Config::load(&self.config.config_path)?; - // Merge provision profile container args with CLI container args - let merged_container_args = config.merge_provision_container_args( - self.config.provision_profile.as_deref(), - self.config.container_args.as_ref(), - ); - // Get state file path from provision profile if available let state_file = self .config @@ -60,6 +54,8 @@ impl ProvisionCommand { .as_ref() .map(|profile| config.get_provision_state_file(profile)); + // Pass raw CLI container_args - RuntimeProvisionCommand will handle merging + // with SDK and provision profile args to avoid double-merging let mut runtime_provision_cmd = RuntimeProvisionCommand::new( crate::commands::runtime::provision::RuntimeProvisionConfig { runtime_name: self.config.runtime.clone(), @@ -70,7 +66,7 @@ impl ProvisionCommand { provision_profile: self.config.provision_profile.clone(), env_vars: self.config.env_vars.clone(), out: self.config.out.clone(), - container_args: merged_container_args, + container_args: self.config.container_args.clone(), dnf_args: self.config.dnf_args.clone(), state_file, no_stamps: self.config.no_stamps, diff --git a/src/utils/config.rs b/src/utils/config.rs index 7ea85fb..8884c97 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -1842,26 +1842,122 @@ impl Config { } } - /// Merge provision profile container args with CLI args, expanding environment variables - /// Returns a new Vec containing provision profile args first, then CLI args + /// Merge SDK container args, provision profile container args, and CLI args + /// Returns a new Vec containing: SDK args first, then provision profile args, then CLI args + /// This ensures SDK defaults are used as a base, with provision profiles and CLI overriding + /// Duplicate args are removed (later args take precedence for flags with values) pub fn merge_provision_container_args( &self, provision_profile: Option<&str>, cli_args: Option<&Vec>, ) -> Option> { + let sdk_args = self.get_sdk_container_args(); let profile_args = provision_profile .and_then(|profile| self.get_provision_profile_container_args(profile)); - match (profile_args, cli_args) { - (Some(profile), Some(cli)) => { - let mut merged = Self::process_container_args(Some(profile)).unwrap_or_default(); - merged.extend(Self::process_container_args(Some(cli)).unwrap_or_default()); - Some(merged) + // Collect all args in order: SDK first, then provision profile, then CLI + let mut all_args: Vec = Vec::new(); + + if let Some(sdk) = sdk_args { + all_args.extend(Self::process_container_args(Some(sdk)).unwrap_or_default()); + } + + if let Some(profile) = profile_args { + all_args.extend(Self::process_container_args(Some(profile)).unwrap_or_default()); + } + + if let Some(cli) = cli_args { + all_args.extend(Self::process_container_args(Some(cli)).unwrap_or_default()); + } + + if all_args.is_empty() { + return None; + } + + // Deduplicate args, keeping the last occurrence for flags with values + // This allows provision profile and CLI to override SDK defaults + let deduped = Self::deduplicate_container_args(all_args); + + if deduped.is_empty() { + None + } else { + Some(deduped) + } + } + + /// Deduplicate container args, keeping the last occurrence for each unique arg or flag + /// Handles both standalone flags (--privileged) and flag-value pairs (-v /dev:/dev, --network=host) + fn deduplicate_container_args(args: Vec) -> Vec { + use std::collections::HashSet; + + // First pass: identify which args are flags that take a separate value argument + // (e.g., -v, -e, --volume, --env, etc.) + let flags_with_separate_values: HashSet<&str> = [ + "-v", "--volume", + "-e", "--env", + "-p", "--publish", + "-w", "--workdir", + "-u", "--user", + "-l", "--label", + "--mount", + "--device", + "--add-host", + "--dns", + "--cap-add", + "--cap-drop", + "--security-opt", + "--ulimit", + ] + .iter() + .cloned() + .collect(); + + // Parse args into (key, full_representation) pairs for deduplication + // key is used for deduplication, full_representation is what we keep + let mut parsed_args: Vec<(String, Vec)> = Vec::new(); + let mut i = 0; + + while i < args.len() { + let arg = &args[i]; + + if flags_with_separate_values.contains(arg.as_str()) && i + 1 < args.len() { + // Flag with separate value: combine flag and value as key + let value = &args[i + 1]; + let key = format!("{} {}", arg, value); + parsed_args.push((key, vec![arg.clone(), value.clone()])); + i += 2; + } else if arg.starts_with('-') && arg.contains('=') { + // Flag with inline value (e.g., --network=host) + // Use just the flag name as key for network/other single-value flags + let flag_name = arg.split('=').next().unwrap_or(arg); + let key = flag_name.to_string(); + parsed_args.push((key, vec![arg.clone()])); + i += 1; + } else if arg.starts_with('-') { + // Standalone flag (e.g., --privileged, --rm) + parsed_args.push((arg.clone(), vec![arg.clone()])); + i += 1; + } else { + // Non-flag argument (shouldn't happen normally, but handle it) + parsed_args.push((arg.clone(), vec![arg.clone()])); + i += 1; } - (Some(profile), None) => Self::process_container_args(Some(profile)), - (None, Some(cli)) => Self::process_container_args(Some(cli)), - (None, None) => None, } + + // Deduplicate by key, keeping the last occurrence + let mut seen_keys: HashSet = HashSet::new(); + let mut result: Vec> = Vec::new(); + + // Iterate in reverse to keep last occurrence, then reverse the result + for (key, values) in parsed_args.into_iter().rev() { + if !seen_keys.contains(&key) { + seen_keys.insert(key); + result.push(values); + } + } + + result.reverse(); + result.into_iter().flatten().collect() } /// Get compile section dependencies @@ -3239,6 +3335,97 @@ image = "docker.io/avocadolinux/sdk:apollo-edge" assert!(merged.is_none()); } + #[test] + fn test_merge_provision_container_args_with_sdk_defaults() { + // Test that SDK container_args are included as base defaults + let config_content = r#" +[sdk] +image = "docker.io/avocadolinux/sdk:apollo-edge" +container_args = ["--privileged", "--network=host"] + +[provision.usb] +container_args = ["-v", "/dev:/dev"] +"#; + + let config = Config::load_from_str(config_content).unwrap(); + + // Test merging SDK + provision profile + CLI args + let cli_args = vec!["--rm".to_string()]; + let merged = config.merge_provision_container_args(Some("usb"), Some(&cli_args)); + + assert!(merged.is_some()); + let merged_args = merged.unwrap(); + // Should have SDK args first, then provision profile args, then CLI args + assert_eq!(merged_args.len(), 5); + assert_eq!(merged_args[0], "--privileged"); + assert_eq!(merged_args[1], "--network=host"); + assert_eq!(merged_args[2], "-v"); + assert_eq!(merged_args[3], "/dev:/dev"); + assert_eq!(merged_args[4], "--rm"); + } + + #[test] + fn test_merge_provision_container_args_sdk_defaults_only() { + // Test that SDK container_args are used when no provision profile or CLI args + let config_content = r#" +[sdk] +image = "docker.io/avocadolinux/sdk:apollo-edge" +container_args = ["--privileged", "-v", "/dev:/dev"] +"#; + + let config = Config::load_from_str(config_content).unwrap(); + + let merged = config.merge_provision_container_args(None, None); + + assert!(merged.is_some()); + let merged_args = merged.unwrap(); + assert_eq!(merged_args.len(), 3); + assert_eq!(merged_args[0], "--privileged"); + assert_eq!(merged_args[1], "-v"); + assert_eq!(merged_args[2], "/dev:/dev"); + } + + #[test] + fn test_merge_provision_container_args_deduplication() { + // Test that duplicate args are removed (keeping the last occurrence) + let config_content = r#" +[sdk] +image = "docker.io/avocadolinux/sdk:apollo-edge" +container_args = ["--privileged", "--network=host", "-v", "/dev:/dev"] + +[provision.tegraflash] +container_args = ["--privileged", "--network=host", "-v", "/dev:/dev", "-v", "/sys:/sys"] +"#; + + let config = Config::load_from_str(config_content).unwrap(); + + // Test that duplicates are removed + let merged = config.merge_provision_container_args(Some("tegraflash"), None); + + assert!(merged.is_some()); + let merged_args = merged.unwrap(); + // Should only have unique args: --privileged, --network=host, -v /dev:/dev, -v /sys:/sys + // Note: --network=host keeps last occurrence (same value), -v /dev:/dev and -v /sys:/sys are different + assert_eq!(merged_args.len(), 6); // --privileged, --network=host, -v, /dev:/dev, -v, /sys:/sys + assert!(merged_args.contains(&"--privileged".to_string())); + assert!(merged_args.contains(&"--network=host".to_string())); + // Count occurrences of --privileged and --network=host - should be 1 each + assert_eq!( + merged_args + .iter() + .filter(|a| *a == "--privileged") + .count(), + 1 + ); + assert_eq!( + merged_args + .iter() + .filter(|a| *a == "--network=host") + .count(), + 1 + ); + } + #[test] fn test_provision_state_file_default() { // Test that state_file defaults to .avocado/provision-{profile}.state when not configured From 6b260a7da202ba3e0b22462b390dba89ed26b48b Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Tue, 23 Dec 2025 10:59:09 -0500 Subject: [PATCH 4/4] 0.19.0 release --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/utils/config.rs | 23 +++++++++++++---------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f91afeb..5b8bb17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,7 +130,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "avocado-cli" -version = "0.19.1-dev" +version = "0.19.1" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 2306fb6..5fd1114 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "avocado-cli" -version = "0.19.1-dev" +version = "0.19.1" edition = "2021" description = "Command line interface for Avocado." authors = ["Avocado"] diff --git a/src/utils/config.rs b/src/utils/config.rs index 8884c97..762aa26 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -1893,12 +1893,18 @@ impl Config { // First pass: identify which args are flags that take a separate value argument // (e.g., -v, -e, --volume, --env, etc.) let flags_with_separate_values: HashSet<&str> = [ - "-v", "--volume", - "-e", "--env", - "-p", "--publish", - "-w", "--workdir", - "-u", "--user", - "-l", "--label", + "-v", + "--volume", + "-e", + "--env", + "-p", + "--publish", + "-w", + "--workdir", + "-u", + "--user", + "-l", + "--label", "--mount", "--device", "--add-host", @@ -3411,10 +3417,7 @@ container_args = ["--privileged", "--network=host", "-v", "/dev:/dev", "-v", "/s assert!(merged_args.contains(&"--network=host".to_string())); // Count occurrences of --privileged and --network=host - should be 1 each assert_eq!( - merged_args - .iter() - .filter(|a| *a == "--privileged") - .count(), + merged_args.iter().filter(|a| *a == "--privileged").count(), 1 ); assert_eq!(