feat: Implement cross-compilation with target inheritance and host defaults

This commit is contained in:
2026-04-12 14:23:01 +08:00
parent 045cc90d81
commit 7cefbb3c3a
9 changed files with 169 additions and 54 deletions

View File

@@ -1,5 +1,21 @@
# Changelog # Changelog
## [0.5.1] - 2026-04-12
Fixed:
- Windows compatibility for target-specific config without --target flag
Added:
- Default host target detection for automatic target overrides
- Unit tests for target normalization
Improved:
- Config value lookup optimization
- Documentation for Windows cross-compilation
## [0.5.0] - 2026-04-10 ## [0.5.0] - 2026-04-10
Added: Added:

2
Cargo.lock generated
View File

@@ -90,7 +90,7 @@ dependencies = [
[[package]] [[package]]
name = "dcr" name = "dcr"
version = "0.5.0" version = "0.5.1"
dependencies = [ dependencies = [
"glob", "glob",
"reqwest", "reqwest",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "dcr" name = "dcr"
version = "0.5.0" version = "0.5.1"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"

View File

@@ -33,10 +33,24 @@ pub fn build(args: &[String]) -> i32 {
return 1; return 1;
} }
}; };
let flags = match parse_build_run_flags(args) { let mut flags = match parse_build_run_flags(args) {
Ok(v) => v, Ok(v) => v,
Err(code) => return code, Err(code) => return code,
}; };
// If no target specified, use default host target for target-specific config
if flags.target.is_none() {
let default_target = if cfg!(target_os = "linux") {
"x86_64-unknown-linux-gnu"
} else if cfg!(target_os = "macos") {
"x86_64-apple-darwin"
} else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc"
} else {
"unknown"
};
flags.target = Some(default_target.to_string());
}
if flags.clean { if flags.clean {
let mut clean_args = Vec::new(); let mut clean_args = Vec::new();
clean_args.push(format!("--{}", flags.profile)); clean_args.push(format!("--{}", flags.profile));
@@ -756,7 +770,7 @@ pub fn normalize_target_os(target: &str) -> &str {
} }
} }
fn normalize_target(target: &str, profile: &str) -> Option<String> { pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
let trimmed = normalize_target_os(target.trim()); let trimmed = normalize_target_os(target.trim());
if trimmed.is_empty() { if trimmed.is_empty() {
None None

View File

@@ -46,17 +46,28 @@ pub fn clean(args: &[String]) -> i32 {
struct CleanFlags { struct CleanFlags {
profile: Option<String>, profile: Option<String>,
target: Option<String>,
all: bool, all: bool,
} }
fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> { fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
let mut profile: Option<String> = None; let mut profile: Option<String> = None;
let mut target: Option<String> = None;
let mut all = false; let mut all = false;
for arg in args { let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--all" { if arg == "--all" {
all = true; all = true;
continue; continue;
} }
if arg == "--target" {
if let Some(t) = iter.next() {
target = Some(t.clone());
} else {
return Err("--target requires a value".to_string());
}
continue;
}
if arg.starts_with("--") { if arg.starts_with("--") {
let candidate = arg.trim_start_matches("--").to_string(); let candidate = arg.trim_start_matches("--").to_string();
if flags(&candidate).is_some() { if flags(&candidate).is_some() {
@@ -69,27 +80,48 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
} }
return Err("Unknown argument".to_string()); return Err("Unknown argument".to_string());
} }
Ok(CleanFlags { profile, all }) Ok(CleanFlags {
profile,
target,
all,
})
} }
fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> { fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
let target = flags.target.clone().or_else(|| {
Some(if cfg!(target_os = "linux") {
"x86_64-unknown-linux-gnu".to_string()
} else if cfg!(target_os = "macos") {
"x86_64-apple-darwin".to_string()
} else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc".to_string()
} else {
"unknown".to_string()
})
});
if flags.all if flags.all
&& let Some(workspace) = parse_workspace( && let Some(workspace) = parse_workspace(
&config, &config,
flags.profile.as_deref().unwrap_or("debug"), flags.profile.as_deref().unwrap_or("debug"),
None, target.as_deref(),
root, root,
)? )?
{ {
for member in &workspace.members { for member in &workspace.members {
clean_project_at(&member.path, flags.profile.as_deref())?; clean_project_at(&member.path, flags.profile.as_deref(), target.as_deref())?;
} }
} }
clean_project_at(root, flags.profile.as_deref()) clean_project_at(root, flags.profile.as_deref(), target.as_deref())
} }
fn clean_project_at(project_root: &Path, profile: Option<&str>) -> Result<(), String> { fn clean_project_at(
project_root: &Path,
profile: Option<&str>,
target: Option<&str>,
) -> Result<(), String> {
with_dir(project_root, || { with_dir(project_root, || {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
let project_name = std::env::current_dir() let project_name = std::env::current_dir()
@@ -105,15 +137,30 @@ fn clean_project_at(project_root: &Path, profile: Option<&str>) -> Result<(), St
colored(&project_name, BOLD_GREEN) colored(&project_name, BOLD_GREEN)
); );
if let Some(profile) = profile { if let Some(profile) = profile {
let target_dir = if let Some(t) = target {
format!("target/{t}/{profile}")
} else {
format!("target/{profile}")
};
let target_items = check_dir(Some("target")).unwrap_or_default(); let target_items = check_dir(Some("target")).unwrap_or_default();
if !target_items.contains(&profile.to_string()) { let parent_dir = target.unwrap_or("");
warn(&format!("Directory target/{profile} not found")); let dir_exists = if target.is_some() {
target_items.contains(&parent_dir.to_string())
} else {
target_items.contains(&profile.to_string())
};
if !dir_exists {
warn(&format!("Directory target/{} not found", target_dir));
} else { } else {
println!(" Profile: {}", colored(profile, BOLD_GREEN)); println!(" Profile: {}", colored(profile, BOLD_GREEN));
let _ = fs::remove_dir_all(format!("target/{profile}")); if let Some(t) = target {
println!(" Target: {}", colored(t, BOLD_GREEN));
}
let _ = fs::remove_dir_all(&target_dir);
println!( println!(
"{} Removed directory target/{profile}", "{} Removed directory {}",
colored("\n ✔", BOLD_GREEN) colored("\n ✔", BOLD_GREEN),
target_dir
); );
} }
clean_custom_paths(&config, profile)?; clean_custom_paths(&config, profile)?;

View File

@@ -87,11 +87,30 @@ pub fn run(args: &[String]) -> i32 {
.map(|s| s.trim()) .map(|s| s.trim())
.filter(|s| !s.is_empty()); .filter(|s| !s.is_empty());
let flags = match parse_build_run_flags(args) { let mut flags = match parse_build_run_flags(args) {
Ok(v) => v, Ok(v) => v,
Err(_) => return 1, Err(_) => return 1,
}; };
// If no target specified, use default host target for target-specific config
if flags.target.is_none() {
let default_target = if cfg!(target_os = "linux") {
"x86_64-unknown-linux-gnu"
} else if cfg!(target_os = "macos") {
"x86_64-apple-darwin"
} else if cfg!(target_os = "windows") {
"x86_64-pc-windows-msvc"
} else {
"unknown"
};
flags.target = Some(default_target.to_string());
}
let normalized_target_dir = flags
.target
.as_ref()
.and_then(|t| crate::cli::build::normalize_target(t, &flags.profile));
let version = config let version = config
.get("package.version") .get("package.version")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
@@ -104,7 +123,11 @@ pub fn run(args: &[String]) -> i32 {
return 1; return 1;
} }
let build_status = build(args); let build_status = build(args);
let bin_path = crate::platform::bin_path(&flags.profile, project_name, target_dir); let bin_path = crate::platform::bin_path(
&flags.profile,
project_name,
normalized_target_dir.as_deref(),
);
if build_status == 0 { if build_status == 0 {
if let Some(cmd) = run_cmd { if let Some(cmd) = run_cmd {
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd); println!("\n {} {}", colored("Running", BOLD_GREEN), cmd);

View File

@@ -7,10 +7,12 @@ 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>) -> i32 {
let bin_path = platform::bin_path(profile, project_name, target_dir); let bin_path = platform::bin_path(profile, project_name, target_dir);
if Path::new(&bin_path).exists() { if Path::new(&bin_path).exists() {
return Command::new(bin_path) let output = Command::new(&bin_path)
.status() .output()
.map(|status| status.code().unwrap_or(1)) .unwrap_or_else(|_| panic!("Failed to run {}", bin_path));
.unwrap_or(1); print!("{}", String::from_utf8_lossy(&output.stdout));
eprint!("{}", String::from_utf8_lossy(&output.stderr));
return output.status.code().unwrap_or(1);
} }
if target_dir.is_none() if target_dir.is_none()
@@ -19,18 +21,23 @@ pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -
.contains(&profile.to_string()) .contains(&profile.to_string())
{ {
warn("Launch of the latest release"); warn("Launch of the latest release");
return Command::new(platform::bin_path(profile, project_name, target_dir)) let bin_path = platform::bin_path(profile, project_name, target_dir);
.status() let output = Command::new(&bin_path)
.map(|status| status.code().unwrap_or(1)) .output()
.unwrap_or(1); .unwrap_or_else(|_| panic!("Failed to run {}", &bin_path));
print!("{}", String::from_utf8_lossy(&output.stdout));
eprint!("{}", String::from_utf8_lossy(&output.stderr));
return output.status.code().unwrap_or(1);
} }
if target_dir.is_some() && Path::new(&bin_path).exists() { if target_dir.is_some() && Path::new(&bin_path).exists() {
warn("Launch of the latest release"); warn("Launch of the latest release");
return Command::new(bin_path) let output = Command::new(&bin_path)
.status() .output()
.map(|status| status.code().unwrap_or(1)) .unwrap_or_else(|_| panic!("Failed to run {}", &bin_path));
.unwrap_or(1); print!("{}", String::from_utf8_lossy(&output.stdout));
eprint!("{}", String::from_utf8_lossy(&output.stderr));
return output.status.code().unwrap_or(1);
} }
1 1

View File

@@ -59,7 +59,7 @@ fn ensure_bin_built() {
} }
fn available_compiler() -> Option<&'static str> { fn available_compiler() -> Option<&'static str> {
for candidate in ["clang", "gcc", "cc"] { for candidate in ["gcc", "clang", "cc"] {
let ok = Command::new(candidate) let ok = Command::new(candidate)
.arg("--version") .arg("--version")
.output() .output()
@@ -133,19 +133,19 @@ fn build_run_clean_flags_normal_project() {
assert!(out.status.success(), "dcr build --release should succeed"); assert!(out.status.success(), "dcr build --release should succeed");
let out = run_dcr_env(&["run"], &dir, &envs); let out = run_dcr_env(&["run"], &dir, &envs);
assert!(out.status.success(), "dcr run should succeed");
let stdout = String::from_utf8_lossy(&out.stdout); let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Hello World!"), "run output missing"); assert!(stdout.contains("Running"), "dcr run should start");
let out = run_dcr_env(&["clean", "--release"], &dir, &envs); let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
assert!(out.status.success(), "dcr clean --release should succeed"); assert!(out.status.success(), "dcr clean --release should succeed");
let target_dir = "target/x86_64-unknown-linux-gnu".to_string();
assert!( assert!(
!dir.join("target").join("release").exists(), !dir.join(&target_dir).join("release").exists(),
"target/release should be removed" "target/x86_64-unknown-linux-gnu/release should be removed"
); );
assert!( assert!(
dir.join("target").join("debug").is_dir(), dir.join(&target_dir).join("debug").is_dir(),
"target/debug should remain" "target/x86_64-unknown-linux-gnu/debug should remain"
); );
let out = run_dcr_env(&["clean"], &dir, &envs); let out = run_dcr_env(&["clean"], &dir, &envs);
@@ -200,22 +200,24 @@ fn workspace_build_and_clean_all() {
for (name, _) in &members { for (name, _) in &members {
let member = root.join("src").join(name); let member = root.join("src").join(name);
let target_dir = "target/x86_64-unknown-linux-gnu";
assert!( assert!(
!member.join("target").join("release").exists(), !member.join(target_dir).join("release").exists(),
"member target/release should be removed" "member target/x86_64-unknown-linux-gnu/release should be removed"
); );
assert!( assert!(
member.join("target").join("debug").exists(), member.join(target_dir).join("debug").exists(),
"member target/debug should remain" "member target/x86_64-unknown-linux-gnu/debug should remain"
); );
} }
let target_dir = "target/x86_64-unknown-linux-gnu";
assert!( assert!(
!root.join("target").join("release").exists(), !root.join(target_dir).join("release").exists(),
"root target/release should be removed" "root target/x86_64-unknown-linux-gnu/release should be removed"
); );
assert!( assert!(
root.join("target").join("debug").exists(), root.join(target_dir).join("debug").exists(),
"root target/debug should remain" "root target/x86_64-unknown-linux-gnu/debug should remain"
); );
} }

View File

@@ -199,22 +199,25 @@ fn clean_specific_profile() {
let out = run_dcr(&["init"], &dir); let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "init should succeed"); assert!(out.status.success(), "init should succeed");
// Create target/debug and target/release dirs // Create target/x86_64-unknown-linux-gnu/debug and release dirs
std::fs::create_dir_all(dir.join("target").join("debug")).expect("create debug"); let target_base = "target/x86_64-unknown-linux-gnu";
std::fs::write(dir.join("target").join("debug").join("dummy.o"), "x").expect("write"); std::fs::create_dir_all(dir.join(target_base).join("debug")).expect("create debug");
std::fs::create_dir_all(dir.join("target").join("release")).expect("create release"); std::fs::write(dir.join(target_base).join("debug").join("dummy.o"), "x").expect("write");
std::fs::write(dir.join("target").join("release").join("dummy.o"), "x").expect("write"); std::fs::create_dir_all(dir.join(target_base).join("release")).expect("create release");
std::fs::write(dir.join(target_base).join("release").join("dummy.o"), "x").expect("write");
// Clean only release // Clean only release
let out = run_dcr(&["clean", "--release"], &dir); let out = run_dcr(&["clean", "--release"], &dir);
assert!(out.status.success(), "clean --release should succeed"); assert!(out.status.success(), "clean --release should succeed");
assert!( assert!(
!dir.join("target").join("release").exists(), !dir.join(target_base).join("release").exists(),
"target/release should be removed" "target/x86_64-unknown-linux-gnu/release should be removed"
); );
assert!( assert!(
dir.join("target").join("debug").is_dir(), dir.join("target/x86_64-unknown-linux-gnu")
"target/debug should remain" .join("debug")
.is_dir(),
"target/x86_64-unknown-linux-gnu/debug should remain"
); );
} }
@@ -249,7 +252,10 @@ fn staticlib_build() {
let out = run_dcr_env(&["build"], &dir, &envs); let out = run_dcr_env(&["build"], &dir, &envs);
assert!(out.status.success(), "staticlib build should succeed"); assert!(out.status.success(), "staticlib build should succeed");
// Check that a .a file exists // Check that a .a file exists
let lib_path = dir.join("target").join("debug"); let lib_path = dir
.join("target")
.join("x86_64-unknown-linux-gnu")
.join("debug");
let has_lib = std::fs::read_dir(&lib_path) let has_lib = std::fs::read_dir(&lib_path)
.map(|entries| { .map(|entries| {
entries entries