From 15b0324ed8476be997cc73ce15d834f513f9be55 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Wed, 24 Jun 2026 22:41:22 -0400 Subject: [PATCH 01/13] feat(diagnostic): add --timing flag with per-phase timing output --- src/cli/args.rs | 7 +++- src/cli/build/detect.rs | 16 +++++++- src/cli/flags.rs | 7 ++++ src/cli/resolve.rs | 1 + src/cli/resolved.rs | 3 +- src/commands/build.rs | 17 ++++++-- src/commands/mono/mode.rs | 73 ++++++++++++++++++++++++++-------- src/commands/single.rs | 1 + tests/cli/build/detect.rs | 17 +++++--- tests/cli/resolve.rs | 2 + tests/commands/mono/resolve.rs | 2 + tests/interactive.rs | 2 + 12 files changed, 120 insertions(+), 28 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index ebec689..785cf06 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -1,6 +1,8 @@ use crate::{ cli::{ - flags::{BuildFlags, ConfigFlags, ConnectionFlags, MonoRepoFlags, ProfileFlags}, + flags::{ + BuildFlags, ConfigFlags, ConnectionFlags, DiagnosticFlags, MonoRepoFlags, ProfileFlags, + }, resolve::resolve_with_config, resolved::ResolvedArgs, }, @@ -37,6 +39,9 @@ pub struct Args { #[command(flatten)] pub profile: ProfileFlags, + + #[command(flatten)] + pub diagnostic: DiagnosticFlags, } impl Args { diff --git a/src/cli/build/detect.rs b/src/cli/build/detect.rs index 2928bc8..554d63d 100644 --- a/src/cli/build/detect.rs +++ b/src/cli/build/detect.rs @@ -11,10 +11,12 @@ pub fn detect_build_system( dir: &Path, input: &mut impl BufRead, output: &mut impl Write, + timing: bool, ) -> Result { + let t = std::time::Instant::now(); let has_cmake = dir.join("CMakeLists.txt").exists(); let has_meson = dir.join("meson.build").exists(); - match (has_cmake, has_meson) { + let result = match (has_cmake, has_meson) { (true, false) => Ok(BuildSystem::Cmake), (false, true) => Ok(BuildSystem::Meson), (true, true) => match ask_choice( @@ -27,7 +29,11 @@ pub fn detect_build_system( _ => Ok(BuildSystem::Meson), }, (false, false) => Err("No supported build system found".into()), + }; + if timing { + writeln!(output, " [timing] Detect: {:.2?}", t.elapsed()).ok(); } + result } /// Detects the build system consistently across all repo directories. @@ -37,11 +43,13 @@ pub fn detect_mono_build_system( dirs: &[PathBuf], input: &mut impl BufRead, output: &mut impl Write, + timing: bool, ) -> Result { writeln!(output, "Detecting build system\n").ok(); + let t = std::time::Instant::now(); let all_cmake = dirs.iter().all(|d| d.join("CMakeLists.txt").exists()); let all_meson = dirs.iter().all(|d| d.join("meson.build").exists()); - match (all_cmake, all_meson) { + let result = match (all_cmake, all_meson) { (true, false) => Ok(BuildSystem::Cmake), (false, true) => Ok(BuildSystem::Meson), (true, true) => match ask_choice( @@ -54,5 +62,9 @@ pub fn detect_mono_build_system( _ => Ok(BuildSystem::Meson), }, (false, false) => Err("Repositories have inconsistent or missing build systems".into()), + }; + if timing { + writeln!(output, " [timing] Detect: {:.2?}", t.elapsed()).ok(); } + result } diff --git a/src/cli/flags.rs b/src/cli/flags.rs index 7bcd8d2..782b1e1 100644 --- a/src/cli/flags.rs +++ b/src/cli/flags.rs @@ -109,3 +109,10 @@ pub struct ProfileFlags { #[arg(long)] pub list_profiles: bool, } + +#[derive(ClapArgs)] +pub struct DiagnosticFlags { + /// Show timing information for each phase + #[arg(long)] + pub timing: bool, +} diff --git a/src/cli/resolve.rs b/src/cli/resolve.rs index c2c8dcd..6119eed 100644 --- a/src/cli/resolve.rs +++ b/src/cli/resolve.rs @@ -72,6 +72,7 @@ pub fn resolve_with_config(mut args: Args, config: &SetupConfig) -> Result()? diff --git a/src/cli/resolved.rs b/src/cli/resolved.rs index 814c29d..5fb77cf 100644 --- a/src/cli/resolved.rs +++ b/src/cli/resolved.rs @@ -1,6 +1,6 @@ use crate::cli::{ build::BuildType, - flags::{ConfigFlags, ProfileFlags}, + flags::{ConfigFlags, DiagnosticFlags, ProfileFlags}, }; /// Resolved connection flags after applying config and CLI overrides. @@ -36,4 +36,5 @@ pub struct ResolvedArgs { pub mono: ResolvedMonoFlags, pub config: ConfigFlags, pub profile: ProfileFlags, + pub diagnostic: DiagnosticFlags, } diff --git a/src/commands/build.rs b/src/commands/build.rs index 57c2103..db20190 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -18,7 +18,9 @@ pub fn cmake_build( build_path: &Path, mono: bool, output: &mut impl Write, + timing: bool, ) -> Result<(), String> { + let t = std::time::Instant::now(); let build_type_flag = format!("-DCMAKE_BUILD_TYPE={}", args.build.build_type.to_cmake()); let mut cmake_cmd = if mono { vec!["cmake", "-DBUILD_LOCAL=ON", &build_type_flag, ".."] @@ -47,6 +49,9 @@ pub fn cmake_build( output, )?; } + if timing { + writeln!(output, " [timing] CMake build: {:.2?}", t.elapsed()).ok(); + } Ok(()) } @@ -58,7 +63,9 @@ pub fn meson_build( build_path: &Path, source_path: &Path, output: &mut impl Write, + timing: bool, ) -> Result<(), String> { + let t = std::time::Instant::now(); let buildtype_flag = format!("--buildtype={}", args.build.build_type.to_meson()); let mut meson_cmd = vec!["meson", "setup"]; meson_cmd.push(&buildtype_flag); @@ -80,6 +87,9 @@ pub fn meson_build( output, )?; } + if timing { + writeln!(output, " [timing] Meson build: {:.2?}", t.elapsed()).ok(); + } Ok(()) } @@ -93,9 +103,10 @@ pub fn build_project( mono: bool, input: &mut impl BufRead, output: &mut impl Write, + timing: bool, ) -> Result<(), String> { - match detect_build_system(source_path, input, output)? { - BuildSystem::Cmake => cmake_build(args, build_path, mono, output), - BuildSystem::Meson => meson_build(args, build_path, source_path, output), + match detect_build_system(source_path, input, output, timing)? { + BuildSystem::Cmake => cmake_build(args, build_path, mono, output, timing), + BuildSystem::Meson => meson_build(args, build_path, source_path, output, timing), } } diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index e2670ba..33abe69 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -4,10 +4,12 @@ use crate::{ ResolvedArgs, }, commands::{ - build::{cmake_build, meson_build}, - mono::config::{create_mono_repo_cmakelists, create_mono_repo_mesonbuild}, - mono::resolve::{resolve_repos_for_mono, resolve_test_repo}, - mono::wraps::hoist_wraps, + build::build_project, + mono::{ + config::{create_mono_repo_cmakelists, create_mono_repo_mesonbuild}, + resolve::{resolve_repos_for_mono, resolve_test_repo}, + wraps::hoist_wraps, + }, }, config::types::SetupConfig, repository::{clone_repository, repo_dir_name}, @@ -23,9 +25,11 @@ fn clone_mono_repos( repos_path: &std::path::Path, ssh: bool, verbose: bool, + timing: bool, output: &mut impl Write, ) -> Result<(), String> { writeln!(output, "Cloning repositories").ok(); + let t = std::time::Instant::now(); for repo in repos { clone_repository(repo, repos_path, ssh, verbose, output)?; } @@ -35,6 +39,9 @@ fn clone_mono_repos( repos.len() ) .ok(); + if timing { + writeln!(output, " [timing] Clone: {:.2?}", t.elapsed()).ok(); + } Ok(()) } @@ -80,6 +87,30 @@ pub fn build_repo_list(test_repo: &str, deps: &[String]) -> Vec { .collect() } +fn prepare_build_dir( + build_path: &std::path::Path, + clean: bool, + timing: bool, + output: &mut impl Write, +) -> Result<(), String> { + if clean && build_path.exists() { + let t = std::time::Instant::now(); + writeln!(output, "Cleaning build directory\n").ok(); + fs::remove_dir_all(build_path).map_err(|e| e.to_string())?; + if timing { + writeln!(output, " [timing] Clean: {:.2?}", t.elapsed()).ok(); + } + } + + let t = std::time::Instant::now(); + writeln!(output, "Creating build directory\n").ok(); + fs::create_dir_all(build_path).map_err(|e| e.to_string())?; + if timing { + writeln!(output, " [timing] Create build directory: {:.2?}", t.elapsed()).ok(); + } + Ok(()) +} + /// Clones and configures a mono-repo ecosystem from a profile or explicit repository list. /// # Errors /// Returns an error if no repository is specified, directory creation fails, or any build system command fails. @@ -89,6 +120,9 @@ pub fn mono_repo_mode( input: &mut impl BufRead, output: &mut impl Write, ) -> Result<(), String> { + let timing = args.diagnostic.timing; + let total = std::time::Instant::now(); + let repo_input = args.repo.as_deref().ok_or("No repository specified")?; let repo_input = repo_input.trim_end_matches('/'); @@ -97,9 +131,13 @@ pub fn mono_repo_mode( let repos = build_repo_list(&test_repo, &deps); writeln!(output, "Total repositories: {}\n", repos.len()).ok(); + let t = std::time::Instant::now(); let mono_repo_path = PathBuf::from(&args.mono.mono_dir); writeln!(output, "Creating directory: {}\n", mono_repo_path.display()).ok(); fs::create_dir_all(&mono_repo_path).map_err(|e| e.to_string())?; + if timing { + writeln!(output, " [timing] Create directory: {:.2?}", t.elapsed()).ok(); + } let repos_path = mono_repo_path.join("repos"); fs::create_dir_all(&repos_path).map_err(|e| e.to_string())?; @@ -109,6 +147,7 @@ pub fn mono_repo_mode( &repos_path, args.connection.ssh, args.connection.verbose, + timing, output, )?; @@ -117,7 +156,7 @@ pub fn mono_repo_mode( .map(|r| repos_path.join(repo_dir_name(r))) .collect(); - let build_system = detect_mono_build_system(&repo_dirs, input, output)?; + let build_system = detect_mono_build_system(&repo_dirs, input, output, timing)?; let canonical_map = generate_mono_config( &build_system, @@ -129,19 +168,18 @@ pub fn mono_repo_mode( )?; let build_path = mono_repo_path.join(&args.build.build_dir); - if args.build.clean && build_path.exists() { - writeln!(output, "Cleaning build directory\n").ok(); - fs::remove_dir_all(&build_path).map_err(|e| e.to_string())?; - } - - writeln!(output, "Creating build directory\n").ok(); - fs::create_dir_all(&build_path).map_err(|e| e.to_string())?; + prepare_build_dir(build_path.as_path(), args.build.clean, timing, output)?; writeln!(output, "Configuring project in {}\n", build_path.display()).ok(); - match &build_system { - BuildSystem::Cmake => cmake_build(args, build_path.as_path(), true, output)?, - BuildSystem::Meson => meson_build(args, build_path.as_path(), &mono_repo_path, output)?, - } + build_project( + args, + build_path.as_path(), + &mono_repo_path, + true, + input, + output, + timing, + )?; writeln!(output, "Setup complete").ok(); writeln!( @@ -181,5 +219,8 @@ pub fn mono_repo_mode( ) .ok(); } + if timing { + writeln!(output, "[timing] Total: {:.2?}", total.elapsed()).ok(); + } Ok(()) } diff --git a/src/commands/single.rs b/src/commands/single.rs index 63de4b1..895166d 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -83,6 +83,7 @@ pub fn single_repo_mode( false, input, output, + args.diagnostic.timing, )?; writeln!( diff --git a/tests/cli/build/detect.rs b/tests/cli/build/detect.rs index ad3e838..0083995 100644 --- a/tests/cli/build/detect.rs +++ b/tests/cli/build/detect.rs @@ -16,21 +16,21 @@ fn meson_dir() -> TempDir { #[test] fn test_detect_build_system_cmake() { let dir = cmake_dir(); - let result = detect_build_system(dir.path(), &mut b"".as_ref(), &mut Vec::new()).unwrap(); + let result = detect_build_system(dir.path(), &mut b"".as_ref(), &mut Vec::new(), false).unwrap(); assert!(matches!(result, BuildSystem::Cmake)); } #[test] fn test_detect_build_system_meson() { let dir = meson_dir(); - let result = detect_build_system(dir.path(), &mut b"".as_ref(), &mut Vec::new()).unwrap(); + let result = detect_build_system(dir.path(), &mut b"".as_ref(), &mut Vec::new(), false).unwrap(); assert!(matches!(result, BuildSystem::Meson)); } #[test] fn test_detect_build_system_none() { let dir = TempDir::new().unwrap(); - let result = detect_build_system(dir.path(), &mut b"".as_ref(), &mut Vec::new()); + let result = detect_build_system(dir.path(), &mut b"".as_ref(), &mut Vec::new(), false); assert!(result.is_err()); } @@ -38,7 +38,8 @@ fn test_detect_build_system_none() { fn test_detect_build_system_both_picks_cmake() { let dir = cmake_dir(); std::fs::write(dir.path().join("meson.build"), "").unwrap(); - let result = detect_build_system(dir.path(), &mut b"1\n".as_ref(), &mut Vec::new()).unwrap(); + let result = + detect_build_system(dir.path(), &mut b"1\n".as_ref(), &mut Vec::new(), false).unwrap(); assert!(matches!(result, BuildSystem::Cmake)); } @@ -46,7 +47,8 @@ fn test_detect_build_system_both_picks_cmake() { fn test_detect_build_system_both_picks_meson() { let dir = cmake_dir(); std::fs::write(dir.path().join("meson.build"), "").unwrap(); - let result = detect_build_system(dir.path(), &mut b"2\n".as_ref(), &mut Vec::new()).unwrap(); + let result = + detect_build_system(dir.path(), &mut b"2\n".as_ref(), &mut Vec::new(), false).unwrap(); assert!(matches!(result, BuildSystem::Meson)); } @@ -57,6 +59,7 @@ fn test_detect_mono_build_system_cmake() { &[dir.path().to_path_buf()], &mut b"".as_ref(), &mut Vec::new(), + false, ) .unwrap(); assert!(matches!(result, BuildSystem::Cmake)); @@ -69,6 +72,7 @@ fn test_detect_mono_build_system_meson() { &[dir.path().to_path_buf()], &mut b"".as_ref(), &mut Vec::new(), + false, ) .unwrap(); assert!(matches!(result, BuildSystem::Meson)); @@ -81,6 +85,7 @@ fn test_detect_mono_build_system_none() { &[dir.path().to_path_buf()], &mut b"".as_ref(), &mut Vec::new(), + false, ); assert!(result.is_err()); } @@ -93,6 +98,7 @@ fn test_detect_mono_build_system_both_picks_cmake() { &[dir.path().to_path_buf()], &mut b"1\n".as_ref(), &mut Vec::new(), + false, ) .unwrap(); assert!(matches!(result, BuildSystem::Cmake)); @@ -106,6 +112,7 @@ fn test_detect_mono_build_system_both_picks_meson() { &[dir.path().to_path_buf()], &mut b"2\n".as_ref(), &mut Vec::new(), + false, ) .unwrap(); assert!(matches!(result, BuildSystem::Meson)); diff --git a/tests/cli/resolve.rs b/tests/cli/resolve.rs index e67220a..550bf42 100644 --- a/tests/cli/resolve.rs +++ b/tests/cli/resolve.rs @@ -1,3 +1,4 @@ +use star_setup::cli::flags::DiagnosticFlags; use star_setup::cli::{ build::BuildType, resolve_bool, resolve_with_config, Args, BuildFlags, ConfigFlags, ConnectionFlags, MonoRepoFlags, ProfileFlags, @@ -73,6 +74,7 @@ fn default_args() -> Args { Args { repo: None, yes: false, + diagnostic: DiagnosticFlags { timing: false }, connection: ConnectionFlags { ssh: false, https: false, diff --git a/tests/commands/mono/resolve.rs b/tests/commands/mono/resolve.rs index 105b1c8..b331585 100644 --- a/tests/commands/mono/resolve.rs +++ b/tests/commands/mono/resolve.rs @@ -1,4 +1,5 @@ use super::common::sink; +use star_setup::cli::flags::DiagnosticFlags; use star_setup::cli::{ resolve_with_config, Args, BuildFlags, ConfigFlags, ConnectionFlags, MonoRepoFlags, ProfileFlags, }; @@ -50,6 +51,7 @@ fn default_resolved() -> star_setup::cli::ResolvedArgs { let args = Args { repo: Some("user/repo".to_string()), yes: false, + diagnostic: DiagnosticFlags { timing: false }, connection: ConnectionFlags { ssh: false, https: false, diff --git a/tests/interactive.rs b/tests/interactive.rs index 5463cfc..8733200 100644 --- a/tests/interactive.rs +++ b/tests/interactive.rs @@ -1,3 +1,4 @@ +use star_setup::cli::flags::DiagnosticFlags; use star_setup::cli::{ resolve_with_config, Args, BuildFlags, ConfigFlags, ConnectionFlags, MonoRepoFlags, ProfileFlags, }; @@ -8,6 +9,7 @@ fn default_resolved() -> star_setup::cli::ResolvedArgs { let args = Args { repo: None, yes: false, + diagnostic: DiagnosticFlags { timing: false }, connection: ConnectionFlags { ssh: false, https: false, From 6a125bce87931519e440ee88a914e8f2dffdf9f4 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Wed, 24 Jun 2026 22:44:16 -0400 Subject: [PATCH 02/13] test(config): add tests for meson_flags display branches --- tests/config/display.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/config/display.rs b/tests/config/display.rs index b8662cf..529b978 100644 --- a/tests/config/display.rs +++ b/tests/config/display.rs @@ -27,3 +27,21 @@ fn test_format_entry_multiple_cmake_flags() { assert!(output.contains("-DTEST=ON")); assert!(output.contains("-DDEBUG=OFF")); } + +#[test] +fn test_format_entry_single_meson_flag() { + let mut entry = sample_entry(); + entry.meson_flags = vec!["-Db_lto=true".to_string()]; + let output = format_entry(&entry); + assert!(output.contains("Meson argument: -Db_lto=true")); +} + +#[test] +fn test_format_entry_multiple_meson_flags() { + let mut entry = sample_entry(); + entry.meson_flags = vec!["-Db_lto=true".to_string(), "-Db_ndebug=true".to_string()]; + let output = format_entry(&entry); + assert!(output.contains("Meson arguments:")); + assert!(output.contains("-Db_lto=true")); + assert!(output.contains("-Db_ndebug=true")); +} From cd91c1758c5b5607e675c9c66c87a261ff086397 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Wed, 24 Jun 2026 22:47:38 -0400 Subject: [PATCH 03/13] test(cli): add timing output tests for detect_build_system and detect_mono_build_system --- src/commands/mono/mode.rs | 7 ++++++- tests/cli/build/detect.rs | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index 33abe69..0ef93d3 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -106,7 +106,12 @@ fn prepare_build_dir( writeln!(output, "Creating build directory\n").ok(); fs::create_dir_all(build_path).map_err(|e| e.to_string())?; if timing { - writeln!(output, " [timing] Create build directory: {:.2?}", t.elapsed()).ok(); + writeln!( + output, + " [timing] Create build directory: {:.2?}", + t.elapsed() + ) + .ok(); } Ok(()) } diff --git a/tests/cli/build/detect.rs b/tests/cli/build/detect.rs index 0083995..ea744b4 100644 --- a/tests/cli/build/detect.rs +++ b/tests/cli/build/detect.rs @@ -117,3 +117,27 @@ fn test_detect_mono_build_system_both_picks_meson() { .unwrap(); assert!(matches!(result, BuildSystem::Meson)); } + +#[test] +fn test_detect_build_system_timing_output() { + let dir = cmake_dir(); + let mut output = Vec::new(); + detect_build_system(dir.path(), &mut b"".as_ref(), &mut output, true).unwrap(); + let out = String::from_utf8(output).unwrap(); + assert!(out.contains("[timing] Detect:")); +} + +#[test] +fn test_detect_mono_build_system_timing_output() { + let dir = cmake_dir(); + let mut output = Vec::new(); + detect_mono_build_system( + &[dir.path().to_path_buf()], + &mut b"".as_ref(), + &mut output, + true, + ) + .unwrap(); + let out = String::from_utf8(output).unwrap(); + assert!(out.contains("[timing] Detect:")); +} From 76ef7080ac69d88c1ec19953e53e614f5879221f Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 25 Jun 2026 00:07:37 -0400 Subject: [PATCH 04/13] refactor: replace boilerplate timing logic with macro --- src/cli/build/detect.rs | 78 ++++++++++++++++++--------------------- src/commands/mono/mode.rs | 44 +++++++++------------- src/utils/mod.rs | 1 + src/utils/timing.rs | 14 +++++++ 4 files changed, 68 insertions(+), 69 deletions(-) create mode 100644 src/utils/timing.rs diff --git a/src/cli/build/detect.rs b/src/cli/build/detect.rs index 554d63d..e69fb50 100644 --- a/src/cli/build/detect.rs +++ b/src/cli/build/detect.rs @@ -13,27 +13,24 @@ pub fn detect_build_system( output: &mut impl Write, timing: bool, ) -> Result { - let t = std::time::Instant::now(); - let has_cmake = dir.join("CMakeLists.txt").exists(); - let has_meson = dir.join("meson.build").exists(); - let result = match (has_cmake, has_meson) { - (true, false) => Ok(BuildSystem::Cmake), - (false, true) => Ok(BuildSystem::Meson), - (true, true) => match ask_choice( - "Multiple build systems detected:", - &["CMake", "Meson"], - input, - output, - )? { - 0 => Ok(BuildSystem::Cmake), - _ => Ok(BuildSystem::Meson), - }, - (false, false) => Err("No supported build system found".into()), - }; - if timing { - writeln!(output, " [timing] Detect: {:.2?}", t.elapsed()).ok(); - } - result + crate::time!(timing, output, "Detect", { + let has_cmake = dir.join("CMakeLists.txt").exists(); + let has_meson = dir.join("meson.build").exists(); + match (has_cmake, has_meson) { + (true, false) => Ok(BuildSystem::Cmake), + (false, true) => Ok(BuildSystem::Meson), + (true, true) => match ask_choice( + "Multiple build systems detected:", + &["CMake", "Meson"], + input, + output, + )? { + 0 => Ok(BuildSystem::Cmake), + _ => Ok(BuildSystem::Meson), + }, + (false, false) => Err("No supported build system found".into()), + } + }) } /// Detects the build system consistently across all repo directories. @@ -46,25 +43,22 @@ pub fn detect_mono_build_system( timing: bool, ) -> Result { writeln!(output, "Detecting build system\n").ok(); - let t = std::time::Instant::now(); - let all_cmake = dirs.iter().all(|d| d.join("CMakeLists.txt").exists()); - let all_meson = dirs.iter().all(|d| d.join("meson.build").exists()); - let result = match (all_cmake, all_meson) { - (true, false) => Ok(BuildSystem::Cmake), - (false, true) => Ok(BuildSystem::Meson), - (true, true) => match ask_choice( - "Multiple build systems detected:", - &["CMake", "Meson"], - input, - output, - )? { - 0 => Ok(BuildSystem::Cmake), - _ => Ok(BuildSystem::Meson), - }, - (false, false) => Err("Repositories have inconsistent or missing build systems".into()), - }; - if timing { - writeln!(output, " [timing] Detect: {:.2?}", t.elapsed()).ok(); - } - result + crate::time!(timing, output, "Detect", { + let all_cmake = dirs.iter().all(|d| d.join("CMakeLists.txt").exists()); + let all_meson = dirs.iter().all(|d| d.join("meson.build").exists()); + match (all_cmake, all_meson) { + (true, false) => Ok(BuildSystem::Cmake), + (false, true) => Ok(BuildSystem::Meson), + (true, true) => match ask_choice( + "Multiple build systems detected:", + &["CMake", "Meson"], + input, + output, + )? { + 0 => Ok(BuildSystem::Cmake), + _ => Ok(BuildSystem::Meson), + }, + (false, false) => Err("Repositories have inconsistent or missing build systems".into()), + } + }) } diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index 0ef93d3..1bdb7ee 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -29,19 +29,18 @@ fn clone_mono_repos( output: &mut impl Write, ) -> Result<(), String> { writeln!(output, "Cloning repositories").ok(); - let t = std::time::Instant::now(); - for repo in repos { - clone_repository(repo, repos_path, ssh, verbose, output)?; - } + crate::time!(timing, output, "Clone", { + for repo in repos { + clone_repository(repo, repos_path, ssh, verbose, output)?; + } + Ok::<(), String>(()) + })?; writeln!( output, "\n Finished cloning ({} repositories)\n", repos.len() ) .ok(); - if timing { - writeln!(output, " [timing] Clone: {:.2?}", t.elapsed()).ok(); - } Ok(()) } @@ -94,25 +93,16 @@ fn prepare_build_dir( output: &mut impl Write, ) -> Result<(), String> { if clean && build_path.exists() { - let t = std::time::Instant::now(); writeln!(output, "Cleaning build directory\n").ok(); - fs::remove_dir_all(build_path).map_err(|e| e.to_string())?; - if timing { - writeln!(output, " [timing] Clean: {:.2?}", t.elapsed()).ok(); - } + crate::time!(timing, output, "Clean", { + fs::remove_dir_all(build_path).map_err(|e| e.to_string())?; + }); } - let t = std::time::Instant::now(); writeln!(output, "Creating build directory\n").ok(); - fs::create_dir_all(build_path).map_err(|e| e.to_string())?; - if timing { - writeln!( - output, - " [timing] Create build directory: {:.2?}", - t.elapsed() - ) - .ok(); - } + crate::time!(timing, output, "Create build directory", { + fs::create_dir_all(build_path).map_err(|e| e.to_string())?; + }); Ok(()) } @@ -136,13 +126,11 @@ pub fn mono_repo_mode( let repos = build_repo_list(&test_repo, &deps); writeln!(output, "Total repositories: {}\n", repos.len()).ok(); - let t = std::time::Instant::now(); let mono_repo_path = PathBuf::from(&args.mono.mono_dir); writeln!(output, "Creating directory: {}\n", mono_repo_path.display()).ok(); - fs::create_dir_all(&mono_repo_path).map_err(|e| e.to_string())?; - if timing { - writeln!(output, " [timing] Create directory: {:.2?}", t.elapsed()).ok(); - } + crate::time!(timing, output, "Create directory", { + fs::create_dir_all(&mono_repo_path).map_err(|e| e.to_string())?; + }); let repos_path = mono_repo_path.join("repos"); fs::create_dir_all(&repos_path).map_err(|e| e.to_string())?; @@ -195,6 +183,7 @@ pub fn mono_repo_mode( .display() ) .ok(); + if let Some(map) = canonical_map { let test_repo_name = repo_dir_name(&test_repo); if let Some((canonical, _)) = map.iter().find(|(_, v)| *v == &test_repo_name) { @@ -224,6 +213,7 @@ pub fn mono_repo_mode( ) .ok(); } + if timing { writeln!(output, "[timing] Total: {:.2?}", total.elapsed()).ok(); } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 246925a..02bee32 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,2 +1,3 @@ pub mod prerequisites; pub mod process; +pub mod timing; diff --git a/src/utils/timing.rs b/src/utils/timing.rs new file mode 100644 index 0000000..fe1b32b --- /dev/null +++ b/src/utils/timing.rs @@ -0,0 +1,14 @@ +#[macro_export] +macro_rules! time { + ($timing:expr, $output:expr, $msg:expr, $block:expr) => {{ + let t = std::time::Instant::now(); + let result = $block; + if $timing { + let _ = std::io::Write::write_fmt( + $output, + format_args!(" [timing] {}: {:.2?}\n", $msg, t.elapsed()), + ); + } + result + }}; +} From 93ee2976afbce1e6d346a2c3ae8a8681b62e193a Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 25 Jun 2026 00:14:14 -0400 Subject: [PATCH 05/13] feat: time prerequisite --- src/run.rs | 2 +- src/utils/prerequisites.rs | 38 +++++++++++++++++++++--------------- tests/utils/prerequisites.rs | 4 ++-- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/run.rs b/src/run.rs index bf40843..f4c80ea 100644 --- a/src/run.rs +++ b/src/run.rs @@ -86,7 +86,7 @@ pub fn run() -> Result<(), Box> { } } - check_prerequisites(args.connection.verbose, &mut stdout)?; + check_prerequisites(args.connection.verbose, &mut stdout, args.diagnostic.timing)?; if args.mono.mono_repo { mono_repo_mode(&args, &config, &mut stdin, &mut stdout)?; diff --git a/src/utils/prerequisites.rs b/src/utils/prerequisites.rs index 81b03c8..8fbb8f6 100644 --- a/src/utils/prerequisites.rs +++ b/src/utils/prerequisites.rs @@ -5,21 +5,27 @@ use std::process::Command; /// Returns Result. /// # Errors /// Returns an error if any required tool is missing from PATH. -pub fn check_prerequisites(verbose: bool, output: &mut impl Write) -> Result<(), String> { - let mut missing: Vec<&str> = Vec::new(); - for tool in &["git", "cmake", "meson"] { - if Command::new(tool) - .arg("--version") - .output() - .map_or(true, |o| !o.status.success()) - { - missing.push(tool); - } else if verbose { - writeln!(output, "Found {tool}").ok(); +pub fn check_prerequisites( + verbose: bool, + output: &mut impl Write, + timing: bool, +) -> Result<(), String> { + crate::time!(timing, output, "Check prerequisites", { + let mut missing: Vec<&str> = Vec::new(); + for tool in &["git", "cmake", "meson"] { + if Command::new(tool) + .arg("--version") + .output() + .map_or(true, |o| !o.status.success()) + { + missing.push(tool); + } else if verbose { + writeln!(output, " Found {tool}").ok(); + } } - } - if !missing.is_empty() { - return Err(format!("Missing required tools: {}", missing.join(", "))); - } - Ok(()) + if !missing.is_empty() { + return Err(format!("Missing required tools: {}", missing.join(", "))); + } + Ok(()) + }) } diff --git a/tests/utils/prerequisites.rs b/tests/utils/prerequisites.rs index 3daff91..1961576 100644 --- a/tests/utils/prerequisites.rs +++ b/tests/utils/prerequisites.rs @@ -2,14 +2,14 @@ use star_setup::utils::prerequisites::check_prerequisites; #[test] fn test_check_prerequisites_succeeds_with_tools_present() { - let result = check_prerequisites(false, &mut Vec::new()); + let result = check_prerequisites(false, &mut Vec::new(), false); assert!(result.is_ok()); } #[test] fn test_check_prerequisites_verbose_outputs_found() { let mut output = Vec::new(); - check_prerequisites(true, &mut output).unwrap(); + check_prerequisites(true, &mut output, false).unwrap(); let out = String::from_utf8(output).unwrap(); assert!(out.contains("Found git")); assert!(out.contains("Found cmake")); From 9621b51dd672d0f6de84bdcd825c259183b487bb Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 25 Jun 2026 00:23:29 -0400 Subject: [PATCH 06/13] feat: time single repo sequence --- src/commands/single.rs | 43 +++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/commands/single.rs b/src/commands/single.rs index 895166d..1009871 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -22,6 +22,7 @@ pub fn single_repo_mode( input: &mut impl BufRead, output: &mut impl Write, ) -> Result<(), String> { + let total = std::time::Instant::now(); let repo = args.repo.as_deref().ok_or("No repository specified")?; let repo_url = resolve_repo_url(repo, args.connection.ssh); let dir_name = repo_dir_name(repo); @@ -44,27 +45,33 @@ pub fn single_repo_mode( writeln!(output, "Repository {dir_name} already exists").ok(); if confirm("Update existing repository?", args.yes, input, output)? { writeln!(output, "Updating {dir_name}\n").ok(); + crate::time!(args.diagnostic.timing, output, "Update", { + run_command( + &["git", "pull"], + Some(Path::new(&dir_name)), + args.connection.verbose, + output, + )?; + }); + } + } else { + writeln!(output, "Cloning {dir_name}\n").ok(); + crate::time!(args.diagnostic.timing, output, "Clone", { run_command( - &["git", "pull"], - Some(Path::new(&dir_name)), + &["git", "clone", &repo_url, &dir_name], + None, args.connection.verbose, output, )?; - } - } else { - writeln!(output, "Cloning {dir_name}\n").ok(); - run_command( - &["git", "clone", &repo_url, &dir_name], - None, - args.connection.verbose, - output, - )?; + }); } let build_path = PathBuf::from(&dir_name).join(&args.build.build_dir); - if args.build.clean && build_path.exists() { - writeln!(output, "Cleaning build directory\n").ok(); - fs::remove_dir_all(&build_path).map_err(|e| e.to_string())?; + if args.build.clean && build_path.exists() { + writeln!(output, "Cleaning build directory\n").ok(); + crate::time!(args.diagnostic.timing, output, "Clean", { + fs::remove_dir_all(&build_path).map_err(|e| e.to_string())?; + }); } writeln!( @@ -73,7 +80,9 @@ pub fn single_repo_mode( args.build.build_dir ) .ok(); - fs::create_dir_all(&build_path).map_err(|e| e.to_string())?; + crate::time!(args.diagnostic.timing, output, "Create build directory", { + fs::create_dir_all(&build_path).map_err(|e| e.to_string())?; + }); writeln!(output, "Configuring project\n").ok(); build_project( @@ -92,5 +101,9 @@ pub fn single_repo_mode( args.build.build_dir ) .ok(); + + if args.diagnostic.timing { + writeln!(output, "[timing] Total: {:.2?}", total.elapsed()).ok(); + } Ok(()) } From d4b2d8fb9b4609bb807384cda1f1610ea5e78eb9 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 25 Jun 2026 00:27:12 -0400 Subject: [PATCH 07/13] feat: split configuration and build stages into timed blocks --- src/commands/build.rs | 77 ++++++++++++++++++++++-------------------- src/commands/single.rs | 10 +++--- 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/commands/build.rs b/src/commands/build.rs index db20190..f539e97 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -20,7 +20,6 @@ pub fn cmake_build( output: &mut impl Write, timing: bool, ) -> Result<(), String> { - let t = std::time::Instant::now(); let build_type_flag = format!("-DCMAKE_BUILD_TYPE={}", args.build.build_type.to_cmake()); let mut cmake_cmd = if mono { vec!["cmake", "-DBUILD_LOCAL=ON", &build_type_flag, ".."] @@ -28,29 +27,32 @@ pub fn cmake_build( vec!["cmake", "..", &build_type_flag] }; cmake_cmd.extend(args.build.cmake_flags.iter().map(String::as_str)); - run_command( - &cmake_cmd, - Some(build_path), - args.connection.verbose, - output, - )?; - if !args.build.no_build { - writeln!(output, "Building project\n").ok(); + + crate::time!(timing, output, "CMake configure", { run_command( - &[ - "cmake", - "--build", - ".", - "--config", - args.build.build_type.to_cmake(), - ], + &cmake_cmd, Some(build_path), args.connection.verbose, output, )?; - } - if timing { - writeln!(output, " [timing] CMake build: {:.2?}", t.elapsed()).ok(); + }); + + if !args.build.no_build { + writeln!(output, "Building project\n").ok(); + crate::time!(timing, output, "CMake build", { + run_command( + &[ + "cmake", + "--build", + ".", + "--config", + args.build.build_type.to_cmake(), + ], + Some(build_path), + args.connection.verbose, + output, + )?; + }); } Ok(()) } @@ -65,32 +67,33 @@ pub fn meson_build( output: &mut impl Write, timing: bool, ) -> Result<(), String> { - let t = std::time::Instant::now(); let buildtype_flag = format!("--buildtype={}", args.build.build_type.to_meson()); let mut meson_cmd = vec!["meson", "setup"]; meson_cmd.push(&buildtype_flag); meson_cmd.push(build_path.to_str().ok_or("Invalid build path")?); meson_cmd.push(source_path.to_str().ok_or("Invalid source path")?); meson_cmd.extend(args.build.meson_flags.iter().map(String::as_str)); - run_command(&meson_cmd, None, args.connection.verbose, output)?; + + crate::time!(timing, output, "Meson setup", { + run_command(&meson_cmd, None, args.connection.verbose, output)?; + }); if !args.build.no_build { writeln!(output, "Building project\n").ok(); - run_command( - &[ - "meson", - "compile", - "-C", - build_path.to_str().ok_or("Invalid build path")?, - ], - None, - args.connection.verbose, - output, - )?; - } - if timing { - writeln!(output, " [timing] Meson build: {:.2?}", t.elapsed()).ok(); - } - Ok(()) + crate::time!(timing, output, "Meson compile", { + run_command( + &[ + "meson", + "compile", + "-C", + build_path.to_str().ok_or("Invalid build path")?, + ], + None, + args.connection.verbose, + output, + )?; + }); + } + Ok(()) } /// Detects and dispatches to the appropriate build system. diff --git a/src/commands/single.rs b/src/commands/single.rs index 1009871..c1d1027 100644 --- a/src/commands/single.rs +++ b/src/commands/single.rs @@ -67,11 +67,11 @@ pub fn single_repo_mode( } let build_path = PathBuf::from(&dir_name).join(&args.build.build_dir); - if args.build.clean && build_path.exists() { - writeln!(output, "Cleaning build directory\n").ok(); - crate::time!(args.diagnostic.timing, output, "Clean", { - fs::remove_dir_all(&build_path).map_err(|e| e.to_string())?; - }); + if args.build.clean && build_path.exists() { + writeln!(output, "Cleaning build directory\n").ok(); + crate::time!(args.diagnostic.timing, output, "Clean", { + fs::remove_dir_all(&build_path).map_err(|e| e.to_string())?; + }); } writeln!( From e9728c82dd42f7401ba1b6a8b9909d08e795c40f Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 25 Jun 2026 12:33:06 -0400 Subject: [PATCH 08/13] feat: add timing to config generation --- src/commands/build.rs | 28 ++++++++++++++-------------- src/commands/mono/config.rs | 11 +++++++++-- src/commands/mono/mode.rs | 6 ++++-- src/utils/prerequisites.rs | 1 + tests/commands/mono/config.rs | 8 ++++---- 5 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/commands/build.rs b/src/commands/build.rs index f539e97..8e0dac6 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -80,20 +80,20 @@ pub fn meson_build( if !args.build.no_build { writeln!(output, "Building project\n").ok(); crate::time!(timing, output, "Meson compile", { - run_command( - &[ - "meson", - "compile", - "-C", - build_path.to_str().ok_or("Invalid build path")?, - ], - None, - args.connection.verbose, - output, - )?; - }); - } - Ok(()) + run_command( + &[ + "meson", + "compile", + "-C", + build_path.to_str().ok_or("Invalid build path")?, + ], + None, + args.connection.verbose, + output, + )?; + }); + } + Ok(()) } /// Detects and dispatches to the appropriate build system. diff --git a/src/commands/mono/config.rs b/src/commands/mono/config.rs index af6c85d..2378b15 100644 --- a/src/commands/mono/config.rs +++ b/src/commands/mono/config.rs @@ -6,16 +6,19 @@ fn write_mono_repo_config( mono_dir: &Path, repos: &[String], output: &mut impl Write, + timing: bool, filename: &str, format_modules: impl Fn(&[String]) -> String, render_template: impl Fn(&str) -> String, ) -> Result<(), String> { let module_names: Vec = repos.iter().map(|r| repo_dir_name(r)).collect(); let modules_str = format_modules(&module_names); - let content = render_template(&modules_str); let file_path = mono_dir.join(filename); - fs::write(&file_path, content).map_err(|e| e.to_string())?; + + crate::time!(timing, output, &format!("Generate {filename}"), { + fs::write(&file_path, content).map_err(|e| e.to_string())?; + }); // .to_string() is required to force an allocation and satisfy line coverage tracking #[allow(clippy::to_string_in_format_args)] @@ -37,12 +40,14 @@ pub fn create_mono_repo_cmakelists( mono_dir: &Path, repos: &[String], output: &mut impl Write, + timing: bool, ) -> Result<(), String> { writeln!(output, " Creating CMake configuration").ok(); write_mono_repo_config( mono_dir, repos, output, + timing, "CMakeLists.txt", |modules| modules.join("\n "), |modules_cmake| { @@ -79,12 +84,14 @@ pub fn create_mono_repo_mesonbuild( mono_dir: &Path, repos: &[String], output: &mut impl Write, + timing: bool, ) -> Result<(), String> { writeln!(output, " Creating Meson configuration").ok(); write_mono_repo_config( mono_dir, repos, output, + timing, "meson.build", |modules| { modules diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index 1bdb7ee..29a659d 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -51,11 +51,12 @@ fn generate_mono_config( repo_dirs: &[PathBuf], repos: &[String], output: &mut impl Write, + timing: bool, ) -> Result>, String> { writeln!(output, "Creating mono-repo configuration").ok(); match build_system { BuildSystem::Cmake => { - create_mono_repo_cmakelists(mono_repo_path, repos, output)?; + create_mono_repo_cmakelists(mono_repo_path, repos, output, timing)?; Ok(None) } BuildSystem::Meson => { @@ -71,7 +72,7 @@ fn generate_mono_config( .unwrap_or(dir) }) .collect(); - create_mono_repo_mesonbuild(mono_repo_path, &subproject_names, output)?; + create_mono_repo_mesonbuild(mono_repo_path, &subproject_names, output, timing)?; Ok(Some(map)) } } @@ -158,6 +159,7 @@ pub fn mono_repo_mode( &repo_dirs, &repos, output, + timing, )?; let build_path = mono_repo_path.join(&args.build.build_dir); diff --git a/src/utils/prerequisites.rs b/src/utils/prerequisites.rs index 8fbb8f6..32c9968 100644 --- a/src/utils/prerequisites.rs +++ b/src/utils/prerequisites.rs @@ -12,6 +12,7 @@ pub fn check_prerequisites( ) -> Result<(), String> { crate::time!(timing, output, "Check prerequisites", { let mut missing: Vec<&str> = Vec::new(); + for tool in &["git", "cmake", "meson"] { if Command::new(tool) .arg("--version") diff --git a/tests/commands/mono/config.rs b/tests/commands/mono/config.rs index c82c63e..d14a591 100644 --- a/tests/commands/mono/config.rs +++ b/tests/commands/mono/config.rs @@ -11,7 +11,7 @@ fn test_create_mono_repo_cmakelists_creates_file() { "user/lib1".to_string(), "user/lib2".to_string(), ]; - create_mono_repo_cmakelists(tmp.path(), &repos, &mut sink()).unwrap(); + create_mono_repo_cmakelists(tmp.path(), &repos, &mut sink(), false).unwrap(); let cmake_file = tmp.path().join("CMakeLists.txt"); assert!(cmake_file.exists()); @@ -26,7 +26,7 @@ fn test_create_mono_repo_cmakelists_creates_file() { fn test_create_mono_repo_cmakelists_empty_repos() { let tmp = tempfile::TempDir::new().unwrap(); let repos = vec!["user-testrepo".to_string()]; - create_mono_repo_cmakelists(tmp.path(), &repos, &mut sink()).unwrap(); + create_mono_repo_cmakelists(tmp.path(), &repos, &mut sink(), false).unwrap(); assert!(tmp.path().join("CMakeLists.txt").exists()); } @@ -39,7 +39,7 @@ fn test_create_mono_repo_mesonbuild_creates_file() { "user/lib1".to_string(), "user/lib2".to_string(), ]; - create_mono_repo_mesonbuild(tmp.path(), &repos, &mut sink()).unwrap(); + create_mono_repo_mesonbuild(tmp.path(), &repos, &mut sink(), false).unwrap(); let meson_file = tmp.path().join("meson.build"); assert!(meson_file.exists()); let content = std::fs::read_to_string(&meson_file).unwrap(); @@ -52,6 +52,6 @@ fn test_create_mono_repo_mesonbuild_creates_file() { fn test_create_mono_repo_mesonbuild_empty_repos() { let tmp = tempfile::TempDir::new().unwrap(); let repos = vec!["user-testrepo".to_string()]; - create_mono_repo_mesonbuild(tmp.path(), &repos, &mut sink()).unwrap(); + create_mono_repo_mesonbuild(tmp.path(), &repos, &mut sink(), false).unwrap(); assert!(tmp.path().join("meson.build").exists()); } From 2527c29160bf9a6ec1ba55dc6e98d634292ef5fa Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 25 Jun 2026 12:43:10 -0400 Subject: [PATCH 09/13] feat(wraps): add timing to hoist_wraps --- src/commands/mono/mode.rs | 2 +- src/commands/mono/wraps.rs | 89 +++++++++++++++++++----------------- tests/commands/mono/wraps.rs | 8 ++-- 3 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/commands/mono/mode.rs b/src/commands/mono/mode.rs index 29a659d..de381be 100644 --- a/src/commands/mono/mode.rs +++ b/src/commands/mono/mode.rs @@ -60,7 +60,7 @@ fn generate_mono_config( Ok(None) } BuildSystem::Meson => { - let map = hoist_wraps(repos_path, repo_dirs, output)?; + let map = hoist_wraps(repos_path, repo_dirs, output, timing)?; let subproject_names: Vec = repos .iter() .map(|r| { diff --git a/src/commands/mono/wraps.rs b/src/commands/mono/wraps.rs index d45662e..9b9f15b 100644 --- a/src/commands/mono/wraps.rs +++ b/src/commands/mono/wraps.rs @@ -68,58 +68,61 @@ pub fn hoist_wraps( repos_dir: &Path, repo_dirs: &[PathBuf], output: &mut impl Write, + timing: bool, ) -> Result, String> { - // normalized project name -> owner-prefixed dir name - let mut project_to_dir: HashMap = HashMap::new(); - for dir in repo_dirs { - let meson_build = dir.join("meson.build"); - if !meson_build.exists() { - continue; - } - let content = fs::read_to_string(&meson_build).map_err(|e| e.to_string())?; - if let Some(name) = parse_project_name(&content) { - if let Some(dir_name) = dir.file_name().and_then(|n| n.to_str()) { - project_to_dir.insert(name, dir_name.to_string()); + crate::time!(timing, output, "Hoist wraps", { + // normalized project name -> owner-prefixed dir name + let mut project_to_dir: HashMap = HashMap::new(); + for dir in repo_dirs { + let meson_build = dir.join("meson.build"); + if !meson_build.exists() { + continue; + } + let content = fs::read_to_string(&meson_build).map_err(|e| e.to_string())?; + if let Some(name) = parse_project_name(&content) { + if let Some(dir_name) = dir.file_name().and_then(|n| n.to_str()) { + project_to_dir.insert(name, dir_name.to_string()); + } } } - } - // collect [provide] pairs from each repo's subprojects/*.wrap - let mut provides: HashMap = HashMap::new(); - for dir in repo_dirs { - let subprojects_dir = dir.join("subprojects"); - if !subprojects_dir.exists() { - continue; - } - for entry in fs::read_dir(&subprojects_dir).map_err(|e| e.to_string())? { - let path = entry.map_err(|e| e.to_string())?.path(); - if path.extension().and_then(|e| e.to_str()) != Some("wrap") { + // collect [provide] pairs from each repo's subprojects/*.wrap + let mut provides: HashMap = HashMap::new(); + for dir in repo_dirs { + let subprojects_dir = dir.join("subprojects"); + if !subprojects_dir.exists() { continue; } - let content = fs::read_to_string(&path).map_err(|e| e.to_string())?; - for (key, val) in parse_provide_pairs(&content) { - if project_to_dir.contains_key(&key) { - provides.entry(key).or_insert(val); + for entry in fs::read_dir(&subprojects_dir).map_err(|e| e.to_string())? { + let path = entry.map_err(|e| e.to_string())?.path(); + if path.extension().and_then(|e| e.to_str()) != Some("wrap") { + continue; + } + let content = fs::read_to_string(&path).map_err(|e| e.to_string())?; + for (key, val) in parse_provide_pairs(&content) { + if project_to_dir.contains_key(&key) { + provides.entry(key).or_insert(val); + } } } } - } - // emit hoisted wraps - for (canonical_name, dir_name) in &project_to_dir { - let wrap_content = if let Some(dep_var) = provides.get(canonical_name) { - format!("[wrap-file]\ndirectory = {dir_name}\n\n[provide]\n{canonical_name} = {dep_var}\n") - } else { - format!("[wrap-file]\ndirectory = {dir_name}\n") - }; - let wrap_path = repos_dir.join(format!("{canonical_name}.wrap")); - fs::write(&wrap_path, &wrap_content).map_err(|e| e.to_string())?; - writeln!( - output, - " Generated wrap: {canonical_name}.wrap -> {dir_name}" - ) - .ok(); - } + // emit hoisted wraps + for (canonical_name, dir_name) in &project_to_dir { + let wrap_content = if let Some(dep_var) = provides.get(canonical_name) { + format!("[wrap-file]\ndirectory = {dir_name}\n\n[provide]\n{canonical_name} = {dep_var}\n") + } else { + format!("[wrap-file]\ndirectory = {dir_name}\n") + }; + let wrap_path = repos_dir.join(format!("{canonical_name}.wrap")); + fs::write(&wrap_path, &wrap_content).map_err(|e| e.to_string())?; + writeln!( + output, + " Generated wrap: {canonical_name}.wrap -> {dir_name}" + ) + .ok(); + } - Ok(project_to_dir) + Ok(project_to_dir) + }) } diff --git a/tests/commands/mono/wraps.rs b/tests/commands/mono/wraps.rs index 23618b9..b55ff39 100644 --- a/tests/commands/mono/wraps.rs +++ b/tests/commands/mono/wraps.rs @@ -92,7 +92,7 @@ fn make_repo(project_name: &str) -> TempDir { fn test_hoist_wraps_empty_repos() { let repos_dir = TempDir::new().unwrap(); let mut output = Vec::new(); - let result = hoist_wraps(repos_dir.path(), &[], &mut output).unwrap(); + let result = hoist_wraps(repos_dir.path(), &[], &mut output, false).unwrap(); assert!(result.is_empty()); } @@ -101,7 +101,7 @@ fn test_hoist_wraps_skips_repo_without_meson_build() { let repos_dir = TempDir::new().unwrap(); let repo = TempDir::new().unwrap(); let mut output = Vec::new(); - let result = hoist_wraps(repos_dir.path(), &[repo.path().to_path_buf()], &mut output).unwrap(); + let result = hoist_wraps(repos_dir.path(), &[repo.path().to_path_buf()], &mut output, false).unwrap(); assert!(result.is_empty()); } @@ -110,7 +110,7 @@ fn test_hoist_wraps_emits_wrap_without_provide() { let repos_dir = TempDir::new().unwrap(); let repo = make_repo("my-lib"); let mut output = Vec::new(); - let result = hoist_wraps(repos_dir.path(), &[repo.path().to_path_buf()], &mut output).unwrap(); + let result = hoist_wraps(repos_dir.path(), &[repo.path().to_path_buf()], &mut output, false).unwrap(); assert!(result.contains_key("my_lib")); let wrap = repos_dir.path().join("my_lib.wrap"); assert!(wrap.exists()); @@ -132,7 +132,7 @@ fn test_hoist_wraps_emits_wrap_with_provide() { .unwrap(); std::fs::write(subprojects.join("readme.txt"), "ignore me").unwrap(); let mut output = Vec::new(); - let result = hoist_wraps(repos_dir.path(), &[repo.path().to_path_buf()], &mut output).unwrap(); + let result = hoist_wraps(repos_dir.path(), &[repo.path().to_path_buf()], &mut output, false).unwrap(); assert!(result.contains_key("my_lib")); let wrap = repos_dir.path().join("my_lib.wrap"); let content = std::fs::read_to_string(&wrap).unwrap(); From 368d641e7c503cb460a30d54e4252cf55b3fb2c0 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 25 Jun 2026 12:47:58 -0400 Subject: [PATCH 10/13] fix(cli): export DiagnosticFlags from cli mod --- src/cli/mod.rs | 4 +++- tests/commands/mono/wraps.rs | 24 +++++++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index fc7c2e5..f02be2f 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -2,7 +2,9 @@ pub mod args; pub use args::Args; pub mod build; pub mod flags; -pub use flags::{BuildFlags, ConfigFlags, ConnectionFlags, MonoRepoFlags, ProfileFlags}; +pub use flags::{ + BuildFlags, ConfigFlags, ConnectionFlags, DiagnosticFlags, MonoRepoFlags, ProfileFlags, +}; pub mod resolve; pub use resolve::{resolve_bool, resolve_with_config}; pub mod resolved; diff --git a/tests/commands/mono/wraps.rs b/tests/commands/mono/wraps.rs index b55ff39..8f3ecec 100644 --- a/tests/commands/mono/wraps.rs +++ b/tests/commands/mono/wraps.rs @@ -101,7 +101,13 @@ fn test_hoist_wraps_skips_repo_without_meson_build() { let repos_dir = TempDir::new().unwrap(); let repo = TempDir::new().unwrap(); let mut output = Vec::new(); - let result = hoist_wraps(repos_dir.path(), &[repo.path().to_path_buf()], &mut output, false).unwrap(); + let result = hoist_wraps( + repos_dir.path(), + &[repo.path().to_path_buf()], + &mut output, + false, + ) + .unwrap(); assert!(result.is_empty()); } @@ -110,7 +116,13 @@ fn test_hoist_wraps_emits_wrap_without_provide() { let repos_dir = TempDir::new().unwrap(); let repo = make_repo("my-lib"); let mut output = Vec::new(); - let result = hoist_wraps(repos_dir.path(), &[repo.path().to_path_buf()], &mut output, false).unwrap(); + let result = hoist_wraps( + repos_dir.path(), + &[repo.path().to_path_buf()], + &mut output, + false, + ) + .unwrap(); assert!(result.contains_key("my_lib")); let wrap = repos_dir.path().join("my_lib.wrap"); assert!(wrap.exists()); @@ -132,7 +144,13 @@ fn test_hoist_wraps_emits_wrap_with_provide() { .unwrap(); std::fs::write(subprojects.join("readme.txt"), "ignore me").unwrap(); let mut output = Vec::new(); - let result = hoist_wraps(repos_dir.path(), &[repo.path().to_path_buf()], &mut output, false).unwrap(); + let result = hoist_wraps( + repos_dir.path(), + &[repo.path().to_path_buf()], + &mut output, + false, + ) + .unwrap(); assert!(result.contains_key("my_lib")); let wrap = repos_dir.path().join("my_lib.wrap"); let content = std::fs::read_to_string(&wrap).unwrap(); From 98583c61b1a1af4c1db2452e7fb895add16fa0ae Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 25 Jun 2026 12:49:48 -0400 Subject: [PATCH 11/13] chore: bump v0.2.1 and update description for multi-build system --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 40aa054..0ebb2e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "star-setup" -version = "0.2.0" +version = "0.2.1" edition = "2021" repository = "https://github.com/star-setup/core" -description = "Lightweight CLI to clone, configure, and wire single or multi-repo CMake ecosystems" +description = "Lightweight CLI to clone, configure, and wire single or multi-repo ecosystems" homepage = "https://github.com/star-setup/core" authors = ["Mason L'Etoile "] From 7ce289757c1e33d2439b849ed3c3d03f779dc246 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 25 Jun 2026 13:18:36 -0400 Subject: [PATCH 12/13] feat: add timing to config entry and interactive mode prompt --- src/cli/mod.rs | 5 ++++- src/cli/resolve.rs | 13 +++++++++++-- src/cli/resolved.rs | 9 +++++++-- src/config/crud.rs | 1 + src/config/display.rs | 1 + src/config/types.rs | 2 ++ src/interactive.rs | 3 +++ src/run.rs | 1 + tests/cli/resolve.rs | 5 +++++ tests/config/crud.rs | 1 + tests/config/display.rs | 1 + tests/config/fixtures.rs | 1 + tests/config/io.rs | 1 + tests/interactive.rs | 40 ++++++++++++++++++++-------------------- 14 files changed, 59 insertions(+), 25 deletions(-) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f02be2f..842f3bb 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -8,4 +8,7 @@ pub use flags::{ pub mod resolve; pub use resolve::{resolve_bool, resolve_with_config}; pub mod resolved; -pub use resolved::{ResolvedArgs, ResolvedBuildFlags, ResolvedConnectionFlags, ResolvedMonoFlags}; +pub use resolved::{ + ResolvedArgs, ResolvedBuildFlags, ResolvedConnectionFlags, ResolvedDiagnosticFlags, + ResolvedMonoFlags, +}; diff --git a/src/cli/resolve.rs b/src/cli/resolve.rs index 6119eed..dc252a6 100644 --- a/src/cli/resolve.rs +++ b/src/cli/resolve.rs @@ -2,7 +2,10 @@ use crate::{ cli::{ args::Args, build::BuildType, - resolved::{ResolvedArgs, ResolvedBuildFlags, ResolvedConnectionFlags, ResolvedMonoFlags}, + resolved::{ + ResolvedArgs, ResolvedBuildFlags, ResolvedConnectionFlags, ResolvedDiagnosticFlags, + ResolvedMonoFlags, + }, }, config::types::SetupConfig, }; @@ -45,6 +48,12 @@ pub fn resolve_with_config(mut args: Args, config: &SetupConfig) -> Result Result()? diff --git a/src/cli/resolved.rs b/src/cli/resolved.rs index 5fb77cf..6dce856 100644 --- a/src/cli/resolved.rs +++ b/src/cli/resolved.rs @@ -1,6 +1,6 @@ use crate::cli::{ build::BuildType, - flags::{ConfigFlags, DiagnosticFlags, ProfileFlags}, + flags::{ConfigFlags, ProfileFlags}, }; /// Resolved connection flags after applying config and CLI overrides. @@ -9,6 +9,11 @@ pub struct ResolvedConnectionFlags { pub verbose: bool, } +/// Resolved diagnostic flags after applying config and CLI overrides. +pub struct ResolvedDiagnosticFlags { + pub timing: bool, +} + /// Resolved build flags after applying config and CLI overrides. pub struct ResolvedBuildFlags { pub build_type: BuildType, @@ -32,9 +37,9 @@ pub struct ResolvedArgs { pub repo: Option, pub yes: bool, pub connection: ResolvedConnectionFlags, + pub diagnostic: ResolvedDiagnosticFlags, pub build: ResolvedBuildFlags, pub mono: ResolvedMonoFlags, pub config: ConfigFlags, pub profile: ProfileFlags, - pub diagnostic: DiagnosticFlags, } diff --git a/src/config/crud.rs b/src/config/crud.rs index b2ce294..9def32d 100644 --- a/src/config/crud.rs +++ b/src/config/crud.rs @@ -61,6 +61,7 @@ pub fn create_default_config( no_build: false, clean: false, verbose: false, + timing: false, cmake_flags: vec![], meson_flags: vec![], }, diff --git a/src/config/display.rs b/src/config/display.rs index 2beb792..3026e4d 100644 --- a/src/config/display.rs +++ b/src/config/display.rs @@ -12,6 +12,7 @@ pub fn format_entry(e: &ConfigEntry) -> String { writeln!(out, " No-build flag: {}", e.no_build).ok(); writeln!(out, " Clean flag: {}", e.clean).ok(); writeln!(out, " Verbose flag: {}", e.verbose).ok(); + writeln!(out, " Timing flag: {}", e.timing).ok(); if e.cmake_flags.is_empty() { out.push('\n'); } else if e.cmake_flags.len() == 1 { diff --git a/src/config/types.rs b/src/config/types.rs index c9e3223..627c128 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -20,6 +20,8 @@ pub struct ConfigEntry { pub clean: bool, /// Show detailed command output. pub verbose: bool, + /// Show timing information. + pub timing: bool, /// Additional `CMake` arguments. pub cmake_flags: Vec, /// Additional `Meson` arguments. diff --git a/src/interactive.rs b/src/interactive.rs index 083e1bc..0b7815d 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -32,6 +32,9 @@ pub fn interactive_mode( if !args.connection.verbose { args.connection.verbose = ask_yesno("Verbose?", false, input, output)?; } + if !args.diagnostic.timing { + args.diagnostic.timing = ask_yesno("Show timing?", false, input, output)?; + } if !args.build.clean { args.build.clean = ask_yesno("Clean build directory if exists?", false, input, output)?; } diff --git a/src/run.rs b/src/run.rs index f4c80ea..f8d0d86 100644 --- a/src/run.rs +++ b/src/run.rs @@ -61,6 +61,7 @@ pub fn run() -> Result<(), Box> { no_build: args.build.no_build, clean: args.build.clean, verbose: args.connection.verbose, + timing: args.diagnostic.timing, cmake_flags: args.build.cmake_flags.clone(), meson_flags: args.build.meson_flags.clone(), }; diff --git a/tests/cli/resolve.rs b/tests/cli/resolve.rs index 550bf42..612c553 100644 --- a/tests/cli/resolve.rs +++ b/tests/cli/resolve.rs @@ -139,6 +139,7 @@ fn test_resolve_with_config_applies_config_defaults() { mono_dir: "mono".to_string(), no_build: true, clean: true, + timing: false, cmake_flags: vec!["-DTEST=ON".to_string()], meson_flags: vec![], }, @@ -166,6 +167,7 @@ fn test_resolve_with_config_cli_overrides_config() { mono_dir: "build-mono".to_string(), no_build: false, clean: false, + timing: false, cmake_flags: vec![], meson_flags: vec![], }, @@ -218,6 +220,7 @@ fn test_resolve_with_config_named_config_pulls_correct_values() { mono_dir: "mono".to_string(), no_build: false, clean: true, + timing: false, cmake_flags: vec![], meson_flags: vec![], }, @@ -244,6 +247,7 @@ fn test_resolve_with_config_cli_cmake_flags_not_overwritten_by_config() { mono_dir: "build-mono".to_string(), no_build: false, clean: false, + timing: false, cmake_flags: vec!["-DCONFIG_FLAG=ON".to_string()], meson_flags: vec![], }, @@ -267,6 +271,7 @@ fn test_resolve_with_config_negative_flags_override_config() { mono_dir: "build-mono".to_string(), no_build: true, clean: true, + timing: false, cmake_flags: vec![], meson_flags: vec![], }, diff --git a/tests/config/crud.rs b/tests/config/crud.rs index 34ebae3..710e088 100644 --- a/tests/config/crud.rs +++ b/tests/config/crud.rs @@ -66,6 +66,7 @@ fn test_add_config_aborts_when_exists_and_not_confirmed() { no_build: false, clean: false, verbose: false, + timing: false, cmake_flags: vec![], meson_flags: vec![], }, diff --git a/tests/config/display.rs b/tests/config/display.rs index 529b978..05f8f95 100644 --- a/tests/config/display.rs +++ b/tests/config/display.rs @@ -8,6 +8,7 @@ fn test_format_entry_contains_fields() { assert!(output.contains("SSH: true")); assert!(output.contains("Build Type: Release")); assert!(output.contains("Clean flag: true")); + assert!(output.contains("Timing flag: false")); } #[test] diff --git a/tests/config/fixtures.rs b/tests/config/fixtures.rs index 20d4f40..8cc214e 100644 --- a/tests/config/fixtures.rs +++ b/tests/config/fixtures.rs @@ -9,6 +9,7 @@ pub fn sample_entry() -> ConfigEntry { no_build: false, clean: true, verbose: false, + timing: false, cmake_flags: vec![], meson_flags: vec![], } diff --git a/tests/config/io.rs b/tests/config/io.rs index 2dda436..39477b8 100644 --- a/tests/config/io.rs +++ b/tests/config/io.rs @@ -26,6 +26,7 @@ fn test_save_and_load_roundtrip() { no_build: false, clean: false, verbose: false, + timing: false, cmake_flags: vec![], meson_flags: vec![], }, diff --git a/tests/interactive.rs b/tests/interactive.rs index 8733200..e4fc5ad 100644 --- a/tests/interactive.rs +++ b/tests/interactive.rs @@ -56,7 +56,7 @@ fn input_with_suffix(prefix: &[u8]) -> Vec { #[test] fn test_interactive_mode_single_repo() { - let input = input_with_suffix(b"user/repo\nn\nn\nn\n1"); + let input = input_with_suffix(b"user/repo\nn\nn\nn\nn\n1"); let mut output = Vec::new(); let mut args = default_resolved(); interactive_mode(&mut args, &mut input.as_ref(), &mut output).unwrap(); @@ -67,7 +67,7 @@ fn test_interactive_mode_single_repo() { #[test] fn test_interactive_mode_ssh_enabled() { - let input = input_with_suffix(b"user/repo\ny\nn\nn\n1"); + let input = input_with_suffix(b"user/repo\ny\nn\nn\nn\n1"); let mut output = Vec::new(); let mut args = default_resolved(); interactive_mode(&mut args, &mut input.as_ref(), &mut output).unwrap(); @@ -76,7 +76,7 @@ fn test_interactive_mode_ssh_enabled() { #[test] fn test_interactive_mode_mono_repo_with_profile() { - let input = input_with_suffix(b"user/repo\nn\nn\nn\n2\n1\nmyprofile"); + let input = input_with_suffix(b"user/repo\nn\nn\nn\nn\n2\n1\nmyprofile"); let mut output = Vec::new(); let mut args = default_resolved(); interactive_mode(&mut args, &mut input.as_ref(), &mut output).unwrap(); @@ -86,7 +86,7 @@ fn test_interactive_mode_mono_repo_with_profile() { #[test] fn test_interactive_mode_mono_repo_with_manual_repos() { - let input = input_with_suffix(b"user/repo\nn\nn\nn\n2\n2\nuser/lib1 user/lib2"); + let input = input_with_suffix(b"user/repo\nn\nn\nn\nn\n2\n2\nuser/lib1 user/lib2"); let mut output = Vec::new(); let mut args = default_resolved(); interactive_mode(&mut args, &mut input.as_ref(), &mut output).unwrap(); @@ -99,7 +99,7 @@ fn test_interactive_mode_mono_repo_with_manual_repos() { #[test] fn test_interactive_mode_skips_repo_prompt_when_set() { - let input = input_with_suffix(b"n\nn\nn\n1"); + let input = input_with_suffix(b"n\nn\nn\nn\n1"); let mut output = Vec::new(); let mut args = default_resolved(); args.repo = Some("already/set".to_string()); @@ -109,7 +109,7 @@ fn test_interactive_mode_skips_repo_prompt_when_set() { #[test] fn test_interactive_mode_output_contains_header() { - let input = input_with_suffix(b"user/repo\nn\nn\nn\n1"); + let input = input_with_suffix(b"user/repo\nn\nn\nn\nn\n1"); let mut output = Vec::new(); let mut args = default_resolved(); interactive_mode(&mut args, &mut input.as_ref(), &mut output).unwrap(); @@ -118,20 +118,9 @@ fn test_interactive_mode_output_contains_header() { assert!(out_str.contains("Interactive mode complete")); } -#[test] -fn test_interactive_mode_errors_on_eof() { - let input = b""; - let mut output = Vec::new(); - let mut args = default_resolved(); - args.repo = None; - let result = interactive_mode(&mut args, &mut input.as_ref(), &mut output); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("unexpected end of input")); -} - #[test] fn test_interactive_mode_yes_word_not_accepted_for_ssh() { - let input = input_with_suffix(b"user/repo\nyes\nn\nn\n1"); + let input = input_with_suffix(b"user/repo\nyes\nn\nn\nn\n1"); let mut output = Vec::new(); let mut args = default_resolved(); interactive_mode(&mut args, &mut input.as_ref(), &mut output).unwrap(); @@ -140,7 +129,7 @@ fn test_interactive_mode_yes_word_not_accepted_for_ssh() { #[test] fn test_interactive_mode_invalid_mode_then_valid() { - let input = input_with_suffix(b"user/repo\nn\nn\nn\nfoo\n1"); + let input = input_with_suffix(b"user/repo\nn\nn\nn\nn\nfoo\n1"); let mut output = Vec::new(); let mut args = default_resolved(); interactive_mode(&mut args, &mut input.as_ref(), &mut output).unwrap(); @@ -149,10 +138,21 @@ fn test_interactive_mode_invalid_mode_then_valid() { #[test] fn test_interactive_mode_invalid_mono_choice_then_valid() { - let input = input_with_suffix(b"user/repo\nn\nn\nn\n2\nfoo\n1\nmyprofile"); + let input = input_with_suffix(b"user/repo\nn\nn\nn\nn\n2\nfoo\n1\nmyprofile"); let mut output = Vec::new(); let mut args = default_resolved(); interactive_mode(&mut args, &mut input.as_ref(), &mut output).unwrap(); assert!(args.mono.mono_repo); assert_eq!(args.mono.profile, Some("myprofile".to_string())); } + +#[test] +fn test_interactive_mode_errors_on_eof() { + let input = b""; + let mut output = Vec::new(); + let mut args = default_resolved(); + args.repo = None; + let result = interactive_mode(&mut args, &mut input.as_ref(), &mut output); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("unexpected end of input")); +} From 5db89a2c5e945056ec2f710bfedba74cfe8bcbc9 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Thu, 25 Jun 2026 13:21:25 -0400 Subject: [PATCH 13/13] chore: dist init --- wix/main.wxs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wix/main.wxs b/wix/main.wxs index e0dd997..83b1970 100644 --- a/wix/main.wxs +++ b/wix/main.wxs @@ -69,7 +69,7 @@