From 5e8037da970e616232b35f9721dca823734898b8 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Mon, 3 Aug 2026 00:40:51 +0800 Subject: [PATCH] feat: host-target config resolution and cargo-style run args --- Cargo.lock | 2 +- Cargo.toml | 2 +- man/man1/dcr-run.1 | 12 ++++++ src/cli/flags.rs | 44 ++++++++++++++++++- src/cli/run.rs | 77 ++++++++++++++++++++++++++++++--- src/core/build/engine.rs | 93 ++++++++++++++++++++++++++-------------- src/core/runner.rs | 9 +++- src/utils/build.rs | 2 + tests/cli_build.rs | 81 ++++++++++++++++++++++++++++++++++ 9 files changed, 279 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a44f4c..cccfe4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,7 +154,7 @@ dependencies = [ [[package]] name = "dcr" -version = "0.8.2" +version = "0.8.3" dependencies = [ "ctrlc", "fatfs", diff --git a/Cargo.toml b/Cargo.toml index 35cd7c1..bf160b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dcr" -version = "0.8.2" +version = "0.8.3" edition = "2024" build = "build.rs" description = "DCR is a utility for managing C/C++ projects in a Cargo-like style." diff --git a/man/man1/dcr-run.1 b/man/man1/dcr-run.1 index c5939b3..c113d5f 100644 --- a/man/man1/dcr-run.1 +++ b/man/man1/dcr-run.1 @@ -8,9 +8,13 @@ dcr-run \- Build and run a DCR project [\fB\-\-force\fR] [\fB\-\-clean\fR] [\fB\-\-verbose\fR] +[\fB\-\-\fR \fI...\fR] .SH DESCRIPTION Builds and runs the project. Only available for projects with \fBkind = "bin"\fR. +Arguments after a bare +.B \-\- +are forwarded to the built binary (cargo-style). .SH OPTIONS .TP .B \-\-debug @@ -30,6 +34,14 @@ Clean before building. .TP .B \-\-verbose Print detailed build output. +.TP +.B \-\- \fI...\fR +Arguments passed to the built binary. +.SH EXAMPLES +.nf +dcr run --release -- --test_help +dcr run -- --foo bar +.fi .SH SEE ALSO .BR dcr (1), .BR dcr-build (1) diff --git a/src/cli/flags.rs b/src/cli/flags.rs index 0ecb458..c8c68b4 100644 --- a/src/cli/flags.rs +++ b/src/cli/flags.rs @@ -25,16 +25,25 @@ pub struct BuildRunFlags { pub force: bool, pub clean: bool, pub verbose: bool, + pub bin_args: Vec, +} + +pub fn split_double_dash(args: &[String]) -> (&[String], &[String]) { + match args.iter().position(|a| a == "--") { + Some(i) => (&args[..i], &args[i + 1..]), + None => (args, &[]), + } } pub fn parse_build_run_flags(args: &[String]) -> Result { + let (dcr_args, bin_args_slice) = split_double_dash(args); let mut profile = PROFILE.to_string(); let mut target = None; let mut workspace = None; let mut force = false; let mut clean = false; let mut verbose = false; - let mut iter = args.iter(); + let mut iter = dcr_args.iter(); while let Some(arg) = iter.next() { if !arg.starts_with("--") { @@ -91,5 +100,38 @@ pub fn parse_build_run_flags(args: &[String]) -> Result { force, clean, verbose, + bin_args: bin_args_slice.to_vec(), }) } + +#[cfg(test)] +mod tests { + use super::*; + + fn s(args: &[&str]) -> Vec { + args.iter().map(|a| a.to_string()).collect() + } + + #[test] + fn split_double_dash_empty_tail() { + let args = s(&["--release", "--force"]); + let (left, right) = split_double_dash(&args); + assert_eq!(left, &s(&["--release", "--force"])); + assert!(right.is_empty()); + } + + #[test] + fn split_double_dash_with_bin_args() { + let args = s(&["--release", "--", "--test_help", "x"]); + let (left, right) = split_double_dash(&args); + assert_eq!(left, &s(&["--release"])); + assert_eq!(right, &s(&["--test_help", "x"])); + } + + #[test] + fn parse_run_flags_forwards_after_double_dash() { + let flags = parse_build_run_flags(&s(&["--release", "--", "--test_help"])).unwrap(); + assert_eq!(flags.profile, "release"); + assert_eq!(flags.bin_args, s(&["--test_help"])); + } +} diff --git a/src/cli/run.rs b/src/cli/run.rs index 9bcc731..1c99357 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -59,12 +59,13 @@ pub fn run(args: &[String]) -> i32 { if args.first().is_some_and(|a| a == "--help") { printc("USAGE:", BOLD_GREEN); printc( - " dcr run [--debug | --release] [--target ] [--force] [--clean] [--verbose]", + " dcr run [--debug | --release] [--target ] [--force] [--clean] [--verbose] [-- ...]", BOLD_CYAN, ); println!(); printc("DESCRIPTION:", BOLD_GREEN); println!(" Builds and runs the project. Only available for kind = \"bin\"."); + println!(" Arguments after `--` are passed to the built binary (cargo-style)."); println!(); printc("OPTIONS:", BOLD_GREEN); println!(" --debug Run with debug profile (default)"); @@ -73,6 +74,7 @@ pub fn run(args: &[String]) -> i32 { println!(" --force Force a full rebuild"); println!(" --clean Clean before building"); println!(" --verbose Print detailed build output"); + println!(" -- ... Arguments forwarded to the binary"); return 0; } @@ -112,12 +114,13 @@ pub fn run(args: &[String]) -> i32 { if let Some(cmd) = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), "") { let build_status = build(&args_for_build(&flags)); if build_status == 0 { + let display = display_run_cmd(&cmd, &flags.bin_args); println!( " {} {}", colored(&format!("{:<9}", "run"), BOLD_GREEN), - cmd + display ); - return run_shell(&cmd); + return run_shell_with_args(&cmd, &flags.bin_args); } return build_status; } @@ -250,32 +253,36 @@ fn run_project( ); if build_status == 0 { if let Some(cmd) = run_cmd { + let display = display_run_cmd(&cmd, &flags.bin_args); println!( " {} {}", colored(&format!("{:<9}", "run"), BOLD_GREEN), - cmd + display ); - return Ok(run_shell(&cmd)); + return Ok(run_shell_with_args(&cmd, &flags.bin_args)); } + let display = display_bin_run(&bin_path, &flags.bin_args); println!( " {} {}", colored(&format!("{:<9}", "run"), BOLD_GREEN), - bin_path + display ); return Ok(run_binary( project_name, &flags.profile, normalized_target_dir.as_deref(), + &flags.bin_args, )); } let fallback_code = if let Some(cmd) = run_cmd { - run_shell(&cmd) + run_shell_with_args(&cmd, &flags.bin_args) } else { run_binary( project_name, &flags.profile, normalized_target_dir.as_deref(), + &flags.bin_args, ) }; if fallback_code != 1 { @@ -345,6 +352,62 @@ fn run_shell(cmd: &str) -> i32 { } } +fn run_shell_with_args(cmd: &str, bin_args: &[String]) -> i32 { + if bin_args.is_empty() { + return run_shell(cmd); + } + let mut full = cmd.to_string(); + for arg in bin_args { + full.push(' '); + full.push_str(&shell_escape(arg)); + } + run_shell(&full) +} + +fn display_run_cmd(cmd: &str, bin_args: &[String]) -> String { + if bin_args.is_empty() { + return cmd.to_string(); + } + let mut display = cmd.to_string(); + for arg in bin_args { + display.push(' '); + display.push_str(arg); + } + display +} + +fn display_bin_run(bin_path: &str, bin_args: &[String]) -> String { + if bin_args.is_empty() { + return bin_path.to_string(); + } + let mut display = bin_path.to_string(); + for arg in bin_args { + display.push(' '); + display.push_str(arg); + } + display +} + +fn shell_escape(arg: &str) -> String { + if cfg!(target_os = "windows") { + // cmd.exe: wrap in double quotes and escape embedded quotes. + let escaped = arg.replace('"', "\\\""); + format!("\"{escaped}\"") + } else { + // POSIX sh: single-quote and escape embedded single quotes as '\''. + let mut out = String::from("'"); + for ch in arg.chars() { + if ch == '\'' { + out.push_str("'\\''"); + } else { + out.push(ch); + } + } + out.push('\''); + out + } +} + fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String { let info = parse_version_info(version); substitute_vars(cmd, &info, profile, "") diff --git a/src/core/build/engine.rs b/src/core/build/engine.rs index f0d1ee4..9a1d6c8 100644 --- a/src/core/build/engine.rs +++ b/src/core/build/engine.rs @@ -36,10 +36,10 @@ use crate::core::build_config::Config; use crate::core::deps::{register, resolve_deps}; use crate::core::workspace::parse_workspace; use crate::utils::build::{ - get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile, - is_bare_metal_target, normalize_kind, normalize_platform, normalize_target, - normalize_target_os, parse_version_info, prepend_clang_target_flag, primary_language, - resolve_compiler, resolve_pkg_config_flags, resolve_tool, substitute_vars, + default_target_triple, get_bool_with_profile, get_config_opt, get_config_str, + get_language_with_profile, is_bare_metal_target, normalize_kind, normalize_platform, + normalize_target, normalize_target_os, parse_version_info, prepend_clang_target_flag, + primary_language, resolve_compiler, resolve_pkg_config_flags, resolve_tool, substitute_vars, }; use crate::utils::fs::check_dir; use std::fs; @@ -187,15 +187,27 @@ fn build_all( let project_version = get_config_str(&config, "package.version"); let targets_to_build: Vec> = if let Some(t) = target { - vec![Some(normalize_target_os(t).to_string())] + let normalized = normalize_target_os(t); + if normalized.is_empty() { + vec![Some(default_target_triple())] + } else { + vec![Some(normalized.to_string())] + } } else { let config_targets = get_targets(&config, profile)?; if config_targets.is_empty() { - vec![None] + vec![Some(default_target_triple())] } else { config_targets .into_iter() - .map(|t| Some(normalize_target_os(&t).to_string())) + .map(|t| { + let normalized = normalize_target_os(&t); + if normalized.is_empty() { + Some(default_target_triple()) + } else { + Some(normalized.to_string()) + } + }) .collect() } }; @@ -344,12 +356,32 @@ fn build_project_at( } let project_name = get_config_str(&config, "package.name"); let project_version = get_config_str(&config, "package.version"); + let host_target = default_target_triple(); let build_target_config = get_build_string_with_profile(&config, "target", profile); - let build_target = target.or(if build_target_config.is_empty() { - None - } else { - Some(build_target_config.as_str()) - }); + let build_target_owned = target + .filter(|t| !t.trim().is_empty()) + .map(|t| { + let normalized = normalize_target_os(t); + if normalized.is_empty() { + host_target.clone() + } else { + normalized.to_string() + } + }) + .or_else(|| { + if build_target_config.is_empty() { + None + } else { + let normalized = normalize_target_os(&build_target_config); + if normalized.is_empty() { + None + } else { + Some(normalized.to_string()) + } + } + }) + .unwrap_or(host_target); + let build_target = Some(build_target_owned.as_str()); let build_language = get_language_with_profile(&config, profile)?; let build_qt = get_bool_with_profile(&config, "qt", profile, false); let lang_key = primary_language(&build_language); @@ -530,27 +562,26 @@ fn build_project_at( root.join(&rel).to_string_lossy().to_string() } None => { - let base_dir = - if let Some(rel) = normalize_target(build_target.unwrap_or(""), profile) { - PathBuf::from(rel) + let base_dir = if let Some(rel) = normalize_target(&build_target_owned, profile) { + PathBuf::from(rel) + } else { + let default_dir = if cfg!(target_os = "linux") { + let arch = std::env::consts::ARCH; + format!("{arch}-unknown-linux-gnu/{profile}") + } else if cfg!(any( + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "dragonfly" + )) { + let arch = std::env::consts::ARCH; + let os = std::env::consts::OS; + format!("{arch}-unknown-{os}/{profile}") } else { - let default_dir = if cfg!(target_os = "linux") { - let arch = std::env::consts::ARCH; - format!("{arch}-unknown-linux-gnu/{profile}") - } else if cfg!(any( - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd", - target_os = "dragonfly" - )) { - let arch = std::env::consts::ARCH; - let os = std::env::consts::OS; - format!("{arch}-unknown-{os}/{profile}") - } else { - profile.to_string() - }; - Path::new("target").join(&default_dir) + profile.to_string() }; + Path::new("target").join(&default_dir) + }; project_root.join(base_dir).to_string_lossy().to_string() } } diff --git a/src/core/runner.rs b/src/core/runner.rs index 68d2778..32fc08d 100644 --- a/src/core/runner.rs +++ b/src/core/runner.rs @@ -19,10 +19,15 @@ use crate::platform; use std::path::Path; use std::process::Command; -pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -> i32 { +pub fn run_binary( + project_name: &str, + profile: &str, + target_dir: Option<&str>, + bin_args: &[String], +) -> i32 { let bin_path = platform::bin_path(profile, project_name, target_dir); if Path::new(&bin_path).exists() { - let status = Command::new(&bin_path).status(); + let status = Command::new(&bin_path).args(bin_args).status(); match status { Ok(s) => { return s.code().unwrap_or(0); diff --git a/src/utils/build.rs b/src/utils/build.rs index 0f70a6b..155d8b8 100644 --- a/src/utils/build.rs +++ b/src/utils/build.rs @@ -105,6 +105,7 @@ pub fn normalize_platform(platform: &str) -> Option<&str> { pub fn normalize_target_os(target: &str) -> &str { match target { + "" => "", "linux" => "x86_64-unknown-linux-gnu", "macos" => "x86_64-apple-darwin", "windows" => "x86_64-pc-windows-msvc", @@ -515,6 +516,7 @@ mod tests { #[test] fn normalize_target_short_names() { + assert_eq!(normalize_target_os(""), ""); assert_eq!(normalize_target_os("linux"), "x86_64-unknown-linux-gnu"); assert_eq!(normalize_target_os("macos"), "x86_64-apple-darwin"); assert_eq!(normalize_target_os("windows"), "x86_64-pc-windows-msvc"); diff --git a/tests/cli_build.rs b/tests/cli_build.rs index 9543158..8b45f5b 100644 --- a/tests/cli_build.rs +++ b/tests/cli_build.rs @@ -87,6 +87,33 @@ fn build_run_clean_flags_normal_project() { "dcr run should start" ); + std::fs::write( + dir.join("src/main.c"), + r#"#include +int main(int argc, char **argv) { + for (int i = 1; i < argc; i++) { + printf("ARG:%s\n", argv[i]); + } + return 0; +} +"#, + ) + .expect("rewrite main.c"); + let out = run_dcr_env(&["run", "--force", "--", "--test_help", "foo"], &dir, &envs); + let combined = format!( + "{}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + assert!( + out.status.success(), + "dcr run -- should succeed:\n{combined}" + ); + assert!( + combined.contains("ARG:--test_help") && combined.contains("ARG:foo"), + "binary should receive args after `--`:\n{combined}" + ); + let out = run_dcr_env(&["clean", "--release"], &dir, &envs); assert!(out.status.success(), "dcr clean --release should succeed"); let release_dir = host_profile_dir(&dir, "release"); @@ -286,3 +313,57 @@ fn package_build_target_used_without_cli_flag() { "artifact should be under package build.target path", ); } + +#[test] +fn host_target_section_ldflags_apply_without_cli_target() { + let Some(compiler) = available_compiler() else { + eprintln!("no compiler found; skipping host target ldflags test"); + return; + }; + + let dir = unique_sandbox_dir("host_ldflags"); + let out = run_dcr(&["init"], &dir); + assert!(out.status.success(), "dcr init should succeed"); + + let host = if cfg!(target_os = "linux") { + let arch = std::env::consts::ARCH; + let env = if cfg!(target_env = "musl") { + "musl" + } else { + "gnu" + }; + format!("{arch}-unknown-linux-{env}") + } else if cfg!(target_os = "macos") { + format!("{}-apple-darwin", std::env::consts::ARCH) + } else if cfg!(target_os = "windows") { + let env = if cfg!(target_env = "gnu") { + "gnu" + } else { + "msvc" + }; + format!("{}-pc-windows-{env}", std::env::consts::ARCH) + } else { + eprintln!("unsupported host for host_target_section test; skipping"); + return; + }; + + let toml_path = dir.join("dcr.toml"); + let toml = std::fs::read_to_string(&toml_path).expect("read dcr.toml"); + let updated = format!( + "{toml}\n[build.{host}]\nldflags = [\"-Wl,-rpath,/nonexistent-dcr-host-ldflags\"]\n" + ); + std::fs::write(&toml_path, updated).expect("write dcr.toml"); + + let envs = [("DCR_COMPILER", compiler), ("DCR_DEBUG", "1")]; + let out = run_dcr_env(&["build", "--force"], &dir, &envs); + let combined = format!( + "{}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + assert!( + combined.contains("-Wl,-rpath,/nonexistent-dcr-host-ldflags") + || combined.contains("rpath,/nonexistent-dcr-host-ldflags"), + "host target section ldflags must be applied on native build:\n{combined}" + ); +}