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
## [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
Added:

2
Cargo.lock generated
View File

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

View File

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

View File

@@ -33,10 +33,24 @@ pub fn build(args: &[String]) -> i32 {
return 1;
}
};
let flags = match parse_build_run_flags(args) {
let mut flags = match parse_build_run_flags(args) {
Ok(v) => v,
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 {
let mut clean_args = Vec::new();
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());
if trimmed.is_empty() {
None

View File

@@ -46,17 +46,28 @@ pub fn clean(args: &[String]) -> i32 {
struct CleanFlags {
profile: Option<String>,
target: Option<String>,
all: bool,
}
fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
let mut profile: Option<String> = None;
let mut target: Option<String> = None;
let mut all = false;
for arg in args {
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if arg == "--all" {
all = true;
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("--") {
let candidate = arg.trim_start_matches("--").to_string();
if flags(&candidate).is_some() {
@@ -69,27 +80,48 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, 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> {
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
&& let Some(workspace) = parse_workspace(
&config,
flags.profile.as_deref().unwrap_or("debug"),
None,
target.as_deref(),
root,
)?
{
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, || {
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
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)
);
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();
if !target_items.contains(&profile.to_string()) {
warn(&format!("Directory target/{profile} not found"));
let parent_dir = target.unwrap_or("");
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 {
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!(
"{} Removed directory target/{profile}",
colored("\n ✔", BOLD_GREEN)
"{} Removed directory {}",
colored("\n ✔", BOLD_GREEN),
target_dir
);
}
clean_custom_paths(&config, profile)?;

View File

@@ -87,11 +87,30 @@ pub fn run(args: &[String]) -> i32 {
.map(|s| s.trim())
.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,
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
.get("package.version")
.and_then(|v| v.as_str())
@@ -104,7 +123,11 @@ pub fn run(args: &[String]) -> i32 {
return 1;
}
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 let Some(cmd) = run_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 {
let bin_path = platform::bin_path(profile, project_name, target_dir);
if Path::new(&bin_path).exists() {
return Command::new(bin_path)
.status()
.map(|status| status.code().unwrap_or(1))
.unwrap_or(1);
let output = Command::new(&bin_path)
.output()
.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_none()
@@ -19,18 +21,23 @@ pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -
.contains(&profile.to_string())
{
warn("Launch of the latest release");
return Command::new(platform::bin_path(profile, project_name, target_dir))
.status()
.map(|status| status.code().unwrap_or(1))
.unwrap_or(1);
let bin_path = platform::bin_path(profile, project_name, target_dir);
let output = Command::new(&bin_path)
.output()
.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() {
warn("Launch of the latest release");
return Command::new(bin_path)
.status()
.map(|status| status.code().unwrap_or(1))
.unwrap_or(1);
let output = Command::new(&bin_path)
.output()
.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);
}
1

View File

@@ -59,7 +59,7 @@ fn ensure_bin_built() {
}
fn available_compiler() -> Option<&'static str> {
for candidate in ["clang", "gcc", "cc"] {
for candidate in ["gcc", "clang", "cc"] {
let ok = Command::new(candidate)
.arg("--version")
.output()
@@ -133,19 +133,19 @@ fn build_run_clean_flags_normal_project() {
assert!(out.status.success(), "dcr build --release should succeed");
let out = run_dcr_env(&["run"], &dir, &envs);
assert!(out.status.success(), "dcr run should succeed");
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);
assert!(out.status.success(), "dcr clean --release should succeed");
let target_dir = "target/x86_64-unknown-linux-gnu".to_string();
assert!(
!dir.join("target").join("release").exists(),
"target/release should be removed"
!dir.join(&target_dir).join("release").exists(),
"target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
dir.join("target").join("debug").is_dir(),
"target/debug should remain"
dir.join(&target_dir).join("debug").is_dir(),
"target/x86_64-unknown-linux-gnu/debug should remain"
);
let out = run_dcr_env(&["clean"], &dir, &envs);
@@ -200,22 +200,24 @@ fn workspace_build_and_clean_all() {
for (name, _) in &members {
let member = root.join("src").join(name);
let target_dir = "target/x86_64-unknown-linux-gnu";
assert!(
!member.join("target").join("release").exists(),
"member target/release should be removed"
!member.join(target_dir).join("release").exists(),
"member target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
member.join("target").join("debug").exists(),
"member target/debug should remain"
member.join(target_dir).join("debug").exists(),
"member target/x86_64-unknown-linux-gnu/debug should remain"
);
}
let target_dir = "target/x86_64-unknown-linux-gnu";
assert!(
!root.join("target").join("release").exists(),
"root target/release should be removed"
!root.join(target_dir).join("release").exists(),
"root target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
root.join("target").join("debug").exists(),
"root target/debug should remain"
root.join(target_dir).join("debug").exists(),
"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);
assert!(out.status.success(), "init should succeed");
// Create target/debug and target/release dirs
std::fs::create_dir_all(dir.join("target").join("debug")).expect("create debug");
std::fs::write(dir.join("target").join("debug").join("dummy.o"), "x").expect("write");
std::fs::create_dir_all(dir.join("target").join("release")).expect("create release");
std::fs::write(dir.join("target").join("release").join("dummy.o"), "x").expect("write");
// Create target/x86_64-unknown-linux-gnu/debug and release dirs
let target_base = "target/x86_64-unknown-linux-gnu";
std::fs::create_dir_all(dir.join(target_base).join("debug")).expect("create debug");
std::fs::write(dir.join(target_base).join("debug").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
let out = run_dcr(&["clean", "--release"], &dir);
assert!(out.status.success(), "clean --release should succeed");
assert!(
!dir.join("target").join("release").exists(),
"target/release should be removed"
!dir.join(target_base).join("release").exists(),
"target/x86_64-unknown-linux-gnu/release should be removed"
);
assert!(
dir.join("target").join("debug").is_dir(),
"target/debug should remain"
dir.join("target/x86_64-unknown-linux-gnu")
.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);
assert!(out.status.success(), "staticlib build should succeed");
// 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)
.map(|entries| {
entries