@@ -55,6 +55,15 @@ impl TryFrom<ProgressOptions> for ProgressWriter {
5555 }
5656}
5757
58+ /// Global args that apply to all subcommands
59+ #[ derive( clap:: Args , Debug , Clone , Copy , Default ) ]
60+ #[ command( about = None , long_about = None ) ]
61+ pub ( crate ) struct GlobalArgs {
62+ /// Increase logging verbosity
63+ #[ arg( short = 'v' , long = "verbose" , action = clap:: ArgAction :: Count , global = true ) ]
64+ pub ( crate ) verbose : u8 , // Custom verbosity, counts occurrences of -v
65+ }
66+
5867/// Perform an upgrade operation
5968#[ derive( Debug , Parser , PartialEq , Eq ) ]
6069pub ( crate ) struct UpgradeOpts {
@@ -460,10 +469,19 @@ impl InternalsOpts {
460469/// whether directly via `bootc install` (executed as part of a container)
461470/// or via another mechanism such as an OS installer tool, further
462471/// updates can be pulled and `bootc upgrade`.
463- #[ derive( Debug , Parser , PartialEq , Eq ) ]
472+ #[ derive( Debug , Parser ) ]
464473#[ clap( name = "bootc" ) ]
465474#[ clap( rename_all = "kebab-case" ) ]
466475#[ clap( version, long_version=clap:: crate_version!( ) ) ]
476+ pub ( crate ) struct Cli {
477+ #[ clap( flatten) ]
478+ pub ( crate ) global_args : GlobalArgs ,
479+
480+ #[ clap( subcommand) ]
481+ pub ( crate ) opt : Opt , // Wrap Opt inside Cli
482+ }
483+
484+ #[ derive( Debug , clap:: Subcommand , PartialEq , Eq ) ]
467485#[ allow( clippy:: large_enum_variant) ]
468486pub ( crate ) enum Opt {
469487 /// Download and queue an updated container image to apply.
@@ -988,7 +1006,7 @@ where
9881006 I : IntoIterator ,
9891007 I :: Item : Into < OsString > + Clone ,
9901008{
991- run_from_opt ( Opt :: parse_including_static ( args) ) . await
1009+ run_from_opt ( Cli :: parse_including_static ( args) . opt ) . await
9921010}
9931011
9941012/// Find the base binary name from argv0 (without a full path). The empty string
@@ -1003,7 +1021,7 @@ fn callname_from_argv0(argv0: &OsStr) -> &str {
10031021 . unwrap_or ( default)
10041022}
10051023
1006- impl Opt {
1024+ impl Cli {
10071025 /// In some cases (e.g. systemd generator) we dispatch specifically on argv0. This
10081026 /// requires some special handling in clap.
10091027 fn parse_including_static < I > ( args : I ) -> Self
@@ -1027,13 +1045,26 @@ impl Opt {
10271045 } ;
10281046 if let Some ( base_args) = mapped {
10291047 let base_args = base_args. iter ( ) . map ( OsString :: from) ;
1030- return Opt :: parse_from ( base_args. chain ( args. map ( |i| i. into ( ) ) ) ) ;
1048+ return Cli :: parse_from ( base_args. chain ( args. map ( |i| i. into ( ) ) ) ) ;
10311049 }
10321050 Some ( first)
10331051 } else {
10341052 None
10351053 } ;
1036- Opt :: parse_from ( first. into_iter ( ) . chain ( args. map ( |i| i. into ( ) ) ) )
1054+ // Parse CLI to extract verbosity level
1055+ let cli = Cli :: parse_from ( first. into_iter ( ) . chain ( args. map ( |i| i. into ( ) ) ) ) ;
1056+
1057+ // Set log level based on `-v` occurrences
1058+ let log_level = match cli. global_args . verbose {
1059+ 0 => tracing:: Level :: WARN , // Default (no -v)
1060+ 1 => tracing:: Level :: INFO , // -v
1061+ 2 => tracing:: Level :: DEBUG , // -vv
1062+ _ => tracing:: Level :: TRACE , // -vvv or more
1063+ } ;
1064+
1065+ bootc_utils:: update_tracing ( log_level) ;
1066+
1067+ cli
10371068 }
10381069}
10391070
@@ -1240,14 +1271,15 @@ mod tests {
12401271 #[ test]
12411272 fn test_parse_install_args ( ) {
12421273 // Verify we still process the legacy --target-no-signature-verification
1243- let o = Opt :: try_parse_from ( [
1274+ let o = Cli :: try_parse_from ( [
12441275 "bootc" ,
12451276 "install" ,
12461277 "to-filesystem" ,
12471278 "--target-no-signature-verification" ,
12481279 "/target" ,
12491280 ] )
1250- . unwrap ( ) ;
1281+ . unwrap ( )
1282+ . opt ;
12511283 let o = match o {
12521284 Opt :: Install ( InstallOpts :: ToFilesystem ( fsopts) ) => fsopts,
12531285 o => panic ! ( "Expected filesystem opts, not {o:?}" ) ,
@@ -1264,7 +1296,7 @@ mod tests {
12641296 #[ test]
12651297 fn test_parse_opts ( ) {
12661298 assert ! ( matches!(
1267- Opt :: parse_including_static( [ "bootc" , "status" ] ) ,
1299+ Cli :: parse_including_static( [ "bootc" , "status" ] ) . opt ,
12681300 Opt :: Status ( StatusOpts {
12691301 json: false ,
12701302 format: None ,
@@ -1273,7 +1305,7 @@ mod tests {
12731305 } )
12741306 ) ) ;
12751307 assert ! ( matches!(
1276- Opt :: parse_including_static( [ "bootc" , "status" , "--format-version=0" ] ) ,
1308+ Cli :: parse_including_static( [ "bootc" , "status" , "--format-version=0" ] ) . opt ,
12771309 Opt :: Status ( StatusOpts {
12781310 format_version: Some ( 0 ) ,
12791311 ..
@@ -1284,18 +1316,18 @@ mod tests {
12841316 #[ test]
12851317 fn test_parse_generator ( ) {
12861318 assert ! ( matches!(
1287- Opt :: parse_including_static( [
1319+ Cli :: parse_including_static( [
12881320 "/usr/lib/systemd/system/bootc-systemd-generator" ,
12891321 "/run/systemd/system"
1290- ] ) ,
1322+ ] ) . opt ,
12911323 Opt :: Internals ( InternalsOpts :: SystemdGenerator { normal_dir, .. } ) if normal_dir == "/run/systemd/system"
12921324 ) ) ;
12931325 }
12941326
12951327 #[ test]
12961328 fn test_parse_ostree_ext ( ) {
12971329 assert ! ( matches!(
1298- Opt :: parse_including_static( [ "bootc" , "internals" , "ostree-container" ] ) ,
1330+ Cli :: parse_including_static( [ "bootc" , "internals" , "ostree-container" ] ) . opt ,
12991331 Opt :: Internals ( InternalsOpts :: OstreeContainer { .. } )
13001332 ) ) ;
13011333
@@ -1305,25 +1337,147 @@ mod tests {
13051337 o => panic ! ( "unexpected {o:?}" ) ,
13061338 }
13071339 }
1308- let args = peel ( Opt :: parse_including_static ( [
1309- "/usr/libexec/libostree/ext/ostree-ima-sign" ,
1310- "ima-sign" ,
1311- "--repo=foo" ,
1312- "foo" ,
1313- "bar" ,
1314- "baz" ,
1315- ] ) ) ;
1340+ let args = peel (
1341+ Cli :: parse_including_static ( [
1342+ "/usr/libexec/libostree/ext/ostree-ima-sign" ,
1343+ "ima-sign" ,
1344+ "--repo=foo" ,
1345+ "foo" ,
1346+ "bar" ,
1347+ "baz" ,
1348+ ] )
1349+ . opt ,
1350+ ) ;
13161351 assert_eq ! (
13171352 args. as_slice( ) ,
13181353 [ "ima-sign" , "--repo=foo" , "foo" , "bar" , "baz" ]
13191354 ) ;
13201355
1321- let args = peel ( Opt :: parse_including_static ( [
1322- "/usr/libexec/libostree/ext/ostree-container" ,
1323- "container" ,
1324- "image" ,
1325- "pull" ,
1326- ] ) ) ;
1356+ let args = peel (
1357+ Cli :: parse_including_static ( [
1358+ "/usr/libexec/libostree/ext/ostree-container" ,
1359+ "container" ,
1360+ "image" ,
1361+ "pull" ,
1362+ ] )
1363+ . opt ,
1364+ ) ;
13271365 assert_eq ! ( args. as_slice( ) , [ "container" , "image" , "pull" ] ) ;
13281366 }
13291367}
1368+
1369+ #[ cfg( test) ]
1370+ mod tracing_tests {
1371+ #![ allow( unsafe_code) ]
1372+
1373+ use bootc_utils:: { initialize_tracing, update_tracing} ;
1374+ use nix:: unistd:: { close, dup, dup2, pipe} ;
1375+ use std:: fs:: File ;
1376+ use std:: io:: { self , Read } ;
1377+ use std:: os:: unix:: io:: { AsRawFd , FromRawFd } ;
1378+ use std:: sync:: Once ;
1379+
1380+ // Ensure logging is initialized once to prevent conflicts across tests
1381+ static INIT : Once = Once :: new ( ) ;
1382+
1383+ /// Helper function to initialize tracing for tests
1384+ fn init_tracing_for_tests ( ) {
1385+ INIT . call_once ( || {
1386+ std:: env:: remove_var ( "RUST_LOG" ) ;
1387+ initialize_tracing ( ) ;
1388+ } ) ;
1389+ }
1390+
1391+ /// Captures `stderr` output using a pipe
1392+ fn capture_stderr < F : FnOnce ( ) > ( test_fn : F ) -> String {
1393+ let ( read_fd, write_fd) = pipe ( ) . expect ( "Failed to create pipe" ) ;
1394+
1395+ // Duplicate original stderr
1396+ let original_stderr = dup ( io:: stderr ( ) . as_raw_fd ( ) ) . expect ( "Failed to duplicate stderr" ) ;
1397+
1398+ // Redirect stderr to the write end of the pipe
1399+ dup2 ( write_fd, io:: stderr ( ) . as_raw_fd ( ) ) . expect ( "Failed to redirect stderr" ) ;
1400+
1401+ // Close the write end in the parent to prevent deadlocks
1402+ close ( write_fd) . expect ( "Failed to close write end" ) ;
1403+
1404+ // Run the test function that produces logs
1405+ test_fn ( ) ;
1406+
1407+ // Restore original stderr
1408+ dup2 ( original_stderr, io:: stderr ( ) . as_raw_fd ( ) ) . expect ( "Failed to restore stderr" ) ;
1409+ close ( original_stderr) . expect ( "Failed to close original stderr" ) ;
1410+
1411+ // Read from the pipe
1412+ let mut buffer = String :: new ( ) ;
1413+ // File::from_raw_fd() is considered unsafe in Rust, as it takes ownership of a raw file descriptor.
1414+ // However, in this case, it's safe because we're using a valid file descriptor obtained from pipe().
1415+ let mut file = unsafe { File :: from_raw_fd ( read_fd) } ;
1416+ file. read_to_string ( & mut buffer)
1417+ . expect ( "Failed to read from pipe" ) ;
1418+
1419+ buffer
1420+ }
1421+
1422+ #[ test]
1423+ fn test_default_tracing ( ) {
1424+ init_tracing_for_tests ( ) ;
1425+
1426+ let output = capture_stderr ( || {
1427+ tracing:: warn!( "Test log message to stderr" ) ;
1428+ } ) ;
1429+
1430+ assert ! (
1431+ output. contains( "Test log message to stderr" ) ,
1432+ "Expected log message not found in stderr"
1433+ ) ;
1434+ }
1435+
1436+ #[ test]
1437+ fn test_update_tracing ( ) {
1438+ init_tracing_for_tests ( ) ;
1439+ std:: env:: remove_var ( "RUST_LOG" ) ;
1440+ update_tracing ( tracing:: Level :: TRACE ) ;
1441+
1442+ let output = capture_stderr ( || {
1443+ tracing:: info!( "Info message to stderr" ) ;
1444+ tracing:: debug!( "Debug message to stderr" ) ;
1445+ tracing:: trace!( "Trace message to stderr" ) ;
1446+ } ) ;
1447+
1448+ assert ! (
1449+ output. contains( "Info message to stderr" ) ,
1450+ "Expected INFO message not found"
1451+ ) ;
1452+ assert ! (
1453+ output. contains( "Debug message to stderr" ) ,
1454+ "Expected DEBUG message not found"
1455+ ) ;
1456+ assert ! (
1457+ output. contains( "Trace message to stderr" ) ,
1458+ "Expected TRACE message not found"
1459+ ) ;
1460+ }
1461+
1462+ #[ test]
1463+ fn test_update_tracing_respects_rust_log ( ) {
1464+ init_tracing_for_tests ( ) ;
1465+ // Set RUST_LOG before initializing(not possible in this test) or after updating tracing
1466+ std:: env:: set_var ( "RUST_LOG" , "info" ) ;
1467+ update_tracing ( tracing:: Level :: DEBUG ) ;
1468+
1469+ let output = capture_stderr ( || {
1470+ tracing:: info!( "Info message to stderr" ) ;
1471+ tracing:: debug!( "Debug message to stderr" ) ;
1472+ } ) ;
1473+
1474+ assert ! (
1475+ output. contains( "Info message to stderr" ) ,
1476+ "Expected INFO message not found"
1477+ ) ;
1478+ assert ! (
1479+ !output. contains( "Debug message to stderr" ) ,
1480+ "Expected DEBUG message found"
1481+ ) ;
1482+ }
1483+ }
0 commit comments