@@ -55,6 +55,15 @@ impl TryFrom<ProgressOptions> for ProgressWriter {
55
55
}
56
56
}
57
57
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
+
58
67
/// Perform an upgrade operation
59
68
#[ derive( Debug , Parser , PartialEq , Eq ) ]
60
69
pub ( crate ) struct UpgradeOpts {
@@ -460,10 +469,19 @@ impl InternalsOpts {
460
469
/// whether directly via `bootc install` (executed as part of a container)
461
470
/// or via another mechanism such as an OS installer tool, further
462
471
/// updates can be pulled and `bootc upgrade`.
463
- #[ derive( Debug , Parser , PartialEq , Eq ) ]
472
+ #[ derive( Debug , Parser ) ]
464
473
#[ clap( name = "bootc" ) ]
465
474
#[ clap( rename_all = "kebab-case" ) ]
466
475
#[ 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 ) ]
467
485
#[ allow( clippy:: large_enum_variant) ]
468
486
pub ( crate ) enum Opt {
469
487
/// Download and queue an updated container image to apply.
@@ -988,7 +1006,7 @@ where
988
1006
I : IntoIterator ,
989
1007
I :: Item : Into < OsString > + Clone ,
990
1008
{
991
- run_from_opt ( Opt :: parse_including_static ( args) ) . await
1009
+ run_from_opt ( Cli :: parse_including_static ( args) . opt ) . await
992
1010
}
993
1011
994
1012
/// 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 {
1003
1021
. unwrap_or ( default)
1004
1022
}
1005
1023
1006
- impl Opt {
1024
+ impl Cli {
1007
1025
/// In some cases (e.g. systemd generator) we dispatch specifically on argv0. This
1008
1026
/// requires some special handling in clap.
1009
1027
fn parse_including_static < I > ( args : I ) -> Self
@@ -1027,13 +1045,26 @@ impl Opt {
1027
1045
} ;
1028
1046
if let Some ( base_args) = mapped {
1029
1047
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 ( ) ) ) ) ;
1031
1049
}
1032
1050
Some ( first)
1033
1051
} else {
1034
1052
None
1035
1053
} ;
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
1037
1068
}
1038
1069
}
1039
1070
@@ -1240,14 +1271,15 @@ mod tests {
1240
1271
#[ test]
1241
1272
fn test_parse_install_args ( ) {
1242
1273
// Verify we still process the legacy --target-no-signature-verification
1243
- let o = Opt :: try_parse_from ( [
1274
+ let o = Cli :: try_parse_from ( [
1244
1275
"bootc" ,
1245
1276
"install" ,
1246
1277
"to-filesystem" ,
1247
1278
"--target-no-signature-verification" ,
1248
1279
"/target" ,
1249
1280
] )
1250
- . unwrap ( ) ;
1281
+ . unwrap ( )
1282
+ . opt ;
1251
1283
let o = match o {
1252
1284
Opt :: Install ( InstallOpts :: ToFilesystem ( fsopts) ) => fsopts,
1253
1285
o => panic ! ( "Expected filesystem opts, not {o:?}" ) ,
@@ -1264,7 +1296,7 @@ mod tests {
1264
1296
#[ test]
1265
1297
fn test_parse_opts ( ) {
1266
1298
assert ! ( matches!(
1267
- Opt :: parse_including_static( [ "bootc" , "status" ] ) ,
1299
+ Cli :: parse_including_static( [ "bootc" , "status" ] ) . opt ,
1268
1300
Opt :: Status ( StatusOpts {
1269
1301
json: false ,
1270
1302
format: None ,
@@ -1273,7 +1305,7 @@ mod tests {
1273
1305
} )
1274
1306
) ) ;
1275
1307
assert ! ( matches!(
1276
- Opt :: parse_including_static( [ "bootc" , "status" , "--format-version=0" ] ) ,
1308
+ Cli :: parse_including_static( [ "bootc" , "status" , "--format-version=0" ] ) . opt ,
1277
1309
Opt :: Status ( StatusOpts {
1278
1310
format_version: Some ( 0 ) ,
1279
1311
..
@@ -1284,18 +1316,18 @@ mod tests {
1284
1316
#[ test]
1285
1317
fn test_parse_generator ( ) {
1286
1318
assert ! ( matches!(
1287
- Opt :: parse_including_static( [
1319
+ Cli :: parse_including_static( [
1288
1320
"/usr/lib/systemd/system/bootc-systemd-generator" ,
1289
1321
"/run/systemd/system"
1290
- ] ) ,
1322
+ ] ) . opt ,
1291
1323
Opt :: Internals ( InternalsOpts :: SystemdGenerator { normal_dir, .. } ) if normal_dir == "/run/systemd/system"
1292
1324
) ) ;
1293
1325
}
1294
1326
1295
1327
#[ test]
1296
1328
fn test_parse_ostree_ext ( ) {
1297
1329
assert ! ( matches!(
1298
- Opt :: parse_including_static( [ "bootc" , "internals" , "ostree-container" ] ) ,
1330
+ Cli :: parse_including_static( [ "bootc" , "internals" , "ostree-container" ] ) . opt ,
1299
1331
Opt :: Internals ( InternalsOpts :: OstreeContainer { .. } )
1300
1332
) ) ;
1301
1333
@@ -1305,25 +1337,147 @@ mod tests {
1305
1337
o => panic ! ( "unexpected {o:?}" ) ,
1306
1338
}
1307
1339
}
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
+ ) ;
1316
1351
assert_eq ! (
1317
1352
args. as_slice( ) ,
1318
1353
[ "ima-sign" , "--repo=foo" , "foo" , "bar" , "baz" ]
1319
1354
) ;
1320
1355
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
+ ) ;
1327
1365
assert_eq ! ( args. as_slice( ) , [ "container" , "image" , "pull" ] ) ;
1328
1366
}
1329
1367
}
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