feat: host-target config resolution and cargo-style run args

This commit is contained in:
2026-08-03 00:40:51 +08:00
parent 1a6a261cac
commit 5e8037da97
9 changed files with 279 additions and 43 deletions

View File

@@ -25,16 +25,25 @@ pub struct BuildRunFlags {
pub force: bool,
pub clean: bool,
pub verbose: bool,
pub bin_args: Vec<String>,
}
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<BuildRunFlags, i32> {
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<BuildRunFlags, i32> {
force,
clean,
verbose,
bin_args: bin_args_slice.to_vec(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn s(args: &[&str]) -> Vec<String> {
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"]));
}
}

View File

@@ -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 <triple>] [--force] [--clean] [--verbose]",
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]",
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!(" -- <args>... 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, "")

View File

@@ -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<Option<String>> = 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()
}
}

View File

@@ -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);

View File

@@ -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");