@@ -1842,26 +1842,122 @@ impl Config {
18421842 }
18431843 }
18441844
1845- /// Merge provision profile container args with CLI args, expanding environment variables
1846- /// Returns a new Vec containing provision profile args first, then CLI args
1845+ /// Merge SDK container args, provision profile container args, and CLI args
1846+ /// Returns a new Vec containing: SDK args first, then provision profile args, then CLI args
1847+ /// This ensures SDK defaults are used as a base, with provision profiles and CLI overriding
1848+ /// Duplicate args are removed (later args take precedence for flags with values)
18471849 pub fn merge_provision_container_args (
18481850 & self ,
18491851 provision_profile : Option < & str > ,
18501852 cli_args : Option < & Vec < String > > ,
18511853 ) -> Option < Vec < String > > {
1854+ let sdk_args = self . get_sdk_container_args ( ) ;
18521855 let profile_args = provision_profile
18531856 . and_then ( |profile| self . get_provision_profile_container_args ( profile) ) ;
18541857
1855- match ( profile_args, cli_args) {
1856- ( Some ( profile) , Some ( cli) ) => {
1857- let mut merged = Self :: process_container_args ( Some ( profile) ) . unwrap_or_default ( ) ;
1858- merged. extend ( Self :: process_container_args ( Some ( cli) ) . unwrap_or_default ( ) ) ;
1859- Some ( merged)
1858+ // Collect all args in order: SDK first, then provision profile, then CLI
1859+ let mut all_args: Vec < String > = Vec :: new ( ) ;
1860+
1861+ if let Some ( sdk) = sdk_args {
1862+ all_args. extend ( Self :: process_container_args ( Some ( sdk) ) . unwrap_or_default ( ) ) ;
1863+ }
1864+
1865+ if let Some ( profile) = profile_args {
1866+ all_args. extend ( Self :: process_container_args ( Some ( profile) ) . unwrap_or_default ( ) ) ;
1867+ }
1868+
1869+ if let Some ( cli) = cli_args {
1870+ all_args. extend ( Self :: process_container_args ( Some ( cli) ) . unwrap_or_default ( ) ) ;
1871+ }
1872+
1873+ if all_args. is_empty ( ) {
1874+ return None ;
1875+ }
1876+
1877+ // Deduplicate args, keeping the last occurrence for flags with values
1878+ // This allows provision profile and CLI to override SDK defaults
1879+ let deduped = Self :: deduplicate_container_args ( all_args) ;
1880+
1881+ if deduped. is_empty ( ) {
1882+ None
1883+ } else {
1884+ Some ( deduped)
1885+ }
1886+ }
1887+
1888+ /// Deduplicate container args, keeping the last occurrence for each unique arg or flag
1889+ /// Handles both standalone flags (--privileged) and flag-value pairs (-v /dev:/dev, --network=host)
1890+ fn deduplicate_container_args ( args : Vec < String > ) -> Vec < String > {
1891+ use std:: collections:: HashSet ;
1892+
1893+ // First pass: identify which args are flags that take a separate value argument
1894+ // (e.g., -v, -e, --volume, --env, etc.)
1895+ let flags_with_separate_values: HashSet < & str > = [
1896+ "-v" , "--volume" ,
1897+ "-e" , "--env" ,
1898+ "-p" , "--publish" ,
1899+ "-w" , "--workdir" ,
1900+ "-u" , "--user" ,
1901+ "-l" , "--label" ,
1902+ "--mount" ,
1903+ "--device" ,
1904+ "--add-host" ,
1905+ "--dns" ,
1906+ "--cap-add" ,
1907+ "--cap-drop" ,
1908+ "--security-opt" ,
1909+ "--ulimit" ,
1910+ ]
1911+ . iter ( )
1912+ . cloned ( )
1913+ . collect ( ) ;
1914+
1915+ // Parse args into (key, full_representation) pairs for deduplication
1916+ // key is used for deduplication, full_representation is what we keep
1917+ let mut parsed_args: Vec < ( String , Vec < String > ) > = Vec :: new ( ) ;
1918+ let mut i = 0 ;
1919+
1920+ while i < args. len ( ) {
1921+ let arg = & args[ i] ;
1922+
1923+ if flags_with_separate_values. contains ( arg. as_str ( ) ) && i + 1 < args. len ( ) {
1924+ // Flag with separate value: combine flag and value as key
1925+ let value = & args[ i + 1 ] ;
1926+ let key = format ! ( "{} {}" , arg, value) ;
1927+ parsed_args. push ( ( key, vec ! [ arg. clone( ) , value. clone( ) ] ) ) ;
1928+ i += 2 ;
1929+ } else if arg. starts_with ( '-' ) && arg. contains ( '=' ) {
1930+ // Flag with inline value (e.g., --network=host)
1931+ // Use just the flag name as key for network/other single-value flags
1932+ let flag_name = arg. split ( '=' ) . next ( ) . unwrap_or ( arg) ;
1933+ let key = flag_name. to_string ( ) ;
1934+ parsed_args. push ( ( key, vec ! [ arg. clone( ) ] ) ) ;
1935+ i += 1 ;
1936+ } else if arg. starts_with ( '-' ) {
1937+ // Standalone flag (e.g., --privileged, --rm)
1938+ parsed_args. push ( ( arg. clone ( ) , vec ! [ arg. clone( ) ] ) ) ;
1939+ i += 1 ;
1940+ } else {
1941+ // Non-flag argument (shouldn't happen normally, but handle it)
1942+ parsed_args. push ( ( arg. clone ( ) , vec ! [ arg. clone( ) ] ) ) ;
1943+ i += 1 ;
18601944 }
1861- ( Some ( profile) , None ) => Self :: process_container_args ( Some ( profile) ) ,
1862- ( None , Some ( cli) ) => Self :: process_container_args ( Some ( cli) ) ,
1863- ( None , None ) => None ,
18641945 }
1946+
1947+ // Deduplicate by key, keeping the last occurrence
1948+ let mut seen_keys: HashSet < String > = HashSet :: new ( ) ;
1949+ let mut result: Vec < Vec < String > > = Vec :: new ( ) ;
1950+
1951+ // Iterate in reverse to keep last occurrence, then reverse the result
1952+ for ( key, values) in parsed_args. into_iter ( ) . rev ( ) {
1953+ if !seen_keys. contains ( & key) {
1954+ seen_keys. insert ( key) ;
1955+ result. push ( values) ;
1956+ }
1957+ }
1958+
1959+ result. reverse ( ) ;
1960+ result. into_iter ( ) . flatten ( ) . collect ( )
18651961 }
18661962
18671963 /// Get compile section dependencies
@@ -3239,6 +3335,97 @@ image = "docker.io/avocadolinux/sdk:apollo-edge"
32393335 assert ! ( merged. is_none( ) ) ;
32403336 }
32413337
3338+ #[ test]
3339+ fn test_merge_provision_container_args_with_sdk_defaults ( ) {
3340+ // Test that SDK container_args are included as base defaults
3341+ let config_content = r#"
3342+ [sdk]
3343+ image = "docker.io/avocadolinux/sdk:apollo-edge"
3344+ container_args = ["--privileged", "--network=host"]
3345+
3346+ [provision.usb]
3347+ container_args = ["-v", "/dev:/dev"]
3348+ "# ;
3349+
3350+ let config = Config :: load_from_str ( config_content) . unwrap ( ) ;
3351+
3352+ // Test merging SDK + provision profile + CLI args
3353+ let cli_args = vec ! [ "--rm" . to_string( ) ] ;
3354+ let merged = config. merge_provision_container_args ( Some ( "usb" ) , Some ( & cli_args) ) ;
3355+
3356+ assert ! ( merged. is_some( ) ) ;
3357+ let merged_args = merged. unwrap ( ) ;
3358+ // Should have SDK args first, then provision profile args, then CLI args
3359+ assert_eq ! ( merged_args. len( ) , 5 ) ;
3360+ assert_eq ! ( merged_args[ 0 ] , "--privileged" ) ;
3361+ assert_eq ! ( merged_args[ 1 ] , "--network=host" ) ;
3362+ assert_eq ! ( merged_args[ 2 ] , "-v" ) ;
3363+ assert_eq ! ( merged_args[ 3 ] , "/dev:/dev" ) ;
3364+ assert_eq ! ( merged_args[ 4 ] , "--rm" ) ;
3365+ }
3366+
3367+ #[ test]
3368+ fn test_merge_provision_container_args_sdk_defaults_only ( ) {
3369+ // Test that SDK container_args are used when no provision profile or CLI args
3370+ let config_content = r#"
3371+ [sdk]
3372+ image = "docker.io/avocadolinux/sdk:apollo-edge"
3373+ container_args = ["--privileged", "-v", "/dev:/dev"]
3374+ "# ;
3375+
3376+ let config = Config :: load_from_str ( config_content) . unwrap ( ) ;
3377+
3378+ let merged = config. merge_provision_container_args ( None , None ) ;
3379+
3380+ assert ! ( merged. is_some( ) ) ;
3381+ let merged_args = merged. unwrap ( ) ;
3382+ assert_eq ! ( merged_args. len( ) , 3 ) ;
3383+ assert_eq ! ( merged_args[ 0 ] , "--privileged" ) ;
3384+ assert_eq ! ( merged_args[ 1 ] , "-v" ) ;
3385+ assert_eq ! ( merged_args[ 2 ] , "/dev:/dev" ) ;
3386+ }
3387+
3388+ #[ test]
3389+ fn test_merge_provision_container_args_deduplication ( ) {
3390+ // Test that duplicate args are removed (keeping the last occurrence)
3391+ let config_content = r#"
3392+ [sdk]
3393+ image = "docker.io/avocadolinux/sdk:apollo-edge"
3394+ container_args = ["--privileged", "--network=host", "-v", "/dev:/dev"]
3395+
3396+ [provision.tegraflash]
3397+ container_args = ["--privileged", "--network=host", "-v", "/dev:/dev", "-v", "/sys:/sys"]
3398+ "# ;
3399+
3400+ let config = Config :: load_from_str ( config_content) . unwrap ( ) ;
3401+
3402+ // Test that duplicates are removed
3403+ let merged = config. merge_provision_container_args ( Some ( "tegraflash" ) , None ) ;
3404+
3405+ assert ! ( merged. is_some( ) ) ;
3406+ let merged_args = merged. unwrap ( ) ;
3407+ // Should only have unique args: --privileged, --network=host, -v /dev:/dev, -v /sys:/sys
3408+ // Note: --network=host keeps last occurrence (same value), -v /dev:/dev and -v /sys:/sys are different
3409+ assert_eq ! ( merged_args. len( ) , 6 ) ; // --privileged, --network=host, -v, /dev:/dev, -v, /sys:/sys
3410+ assert ! ( merged_args. contains( & "--privileged" . to_string( ) ) ) ;
3411+ assert ! ( merged_args. contains( & "--network=host" . to_string( ) ) ) ;
3412+ // Count occurrences of --privileged and --network=host - should be 1 each
3413+ assert_eq ! (
3414+ merged_args
3415+ . iter( )
3416+ . filter( |a| * a == "--privileged" )
3417+ . count( ) ,
3418+ 1
3419+ ) ;
3420+ assert_eq ! (
3421+ merged_args
3422+ . iter( )
3423+ . filter( |a| * a == "--network=host" )
3424+ . count( ) ,
3425+ 1
3426+ ) ;
3427+ }
3428+
32423429 #[ test]
32433430 fn test_provision_state_file_default ( ) {
32443431 // Test that state_file defaults to .avocado/provision-{profile}.state when not configured
0 commit comments