mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 14:18:36 +00:00
feat: Implement cross-compilation with target inheritance and host defaults
This commit is contained in:
16
CHANGELOG.md
16
CHANGELOG.md
@@ -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
2
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user