From 617ba8544235a6a0a1ac9bca0eee7fec4dd68b17 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Tue, 3 Mar 2026 22:25:00 +0800 Subject: [PATCH] Added: - Toolchain overrides via `[toolchain]` and env (`DCR_*`) - ASM `.S` preprocessing via GCC/Clang (`-x assembler-with-cpp`) --- CHANGELOG.md | 5 + Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 8 ++ TODO.md | 4 +- docs/configuration/build-section.md | 11 ++ .../compilers-and-linking.md | 12 ++ docs/reference-and-help/config-reference.md | 7 ++ src/cli/build.rs | 104 +++++++++++++++++- src/core/builder/clang.rs | 16 ++- src/core/builder/gas.rs | 7 +- src/core/builder/gcc.rs | 16 ++- src/core/builder/mod.rs | 2 + src/core/builder/msvc.rs | 2 +- src/core/builder/nasm.rs | 8 +- src/core/config.rs | 28 +++++ 16 files changed, 215 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f04a22..9d9396a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.2.7] - 2026-03-03 +Added: +- Toolchain overrides via `[toolchain]` and env (`DCR_*`) +- ASM `.S` preprocessing via GCC/Clang (`-x assembler-with-cpp`) + ## [0.2.6] - 2026-03-01 Added: - `sharedlib` build kind (shared library output) diff --git a/Cargo.lock b/Cargo.lock index 280bee9..0823ec7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,7 +90,7 @@ dependencies = [ [[package]] name = "dcr" -version = "0.2.6" +version = "0.2.7" dependencies = [ "reqwest", "self-replace", diff --git a/Cargo.toml b/Cargo.toml index 5bfe965..ea2030b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dcr" -version = "0.2.6" +version = "0.2.7" edition = "2024" build = "build.rs" diff --git a/README.md b/README.md index a5301fb..4dc0d16 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,14 @@ compiler = "clang" cflags = ["-Wall", "-Wextra"] ldflags = ["-lm"] +[toolchain] +# Optional tool overrides: +# cc = "clang" +# cxx = "clang++" +# as = "as" +# ar = "ar" +# ld = "ld" + [dependencies] ``` diff --git a/TODO.md b/TODO.md index fc3c927..cc73861 100644 --- a/TODO.md +++ b/TODO.md @@ -3,7 +3,5 @@ - Add header dependency tracking for incremental builds (GCC/Clang: -MMD, MSVC: /showIncludes) - Add external package managers (Conan, vcpkg) - Multi-target -- ASM support - Linker script + direct linker control -- Custom targets/steps -- Tool overrides and env vars +- Custom targets/steps \ No newline at end of file diff --git a/docs/configuration/build-section.md b/docs/configuration/build-section.md index eda81f9..58490f9 100644 --- a/docs/configuration/build-section.md +++ b/docs/configuration/build-section.md @@ -26,6 +26,17 @@ kind = "bin" - `cflags` (string array, optional): extra compile flags. - `ldflags` (string array, optional): extra link flags. +## Toolchain section + +```toml +[toolchain] +cc = "clang" +cxx = "clang++" +as = "as" +ar = "ar" +ld = "ld" +``` + ## Behavior notes - Compiler backend is selected by compiler string (`gcc`, `clang`, `cl`, `clang-cl`). diff --git a/docs/dependencies-and-build/compilers-and-linking.md b/docs/dependencies-and-build/compilers-and-linking.md index c07a989..e905a61 100644 --- a/docs/dependencies-and-build/compilers-and-linking.md +++ b/docs/dependencies-and-build/compilers-and-linking.md @@ -39,6 +39,18 @@ If `build.platform` is set, DCR passes it as an architecture hint: - GCC/Clang: `-march=` - MSVC: maps to `/arch:*` for known values (`x86`, `i386`, `i486`, `i586`, `i686`, `sse2`, `avx`, `avx2`) +## Toolchain overrides + +If `[toolchain]` is set, DCR uses its values to override compiler/linker tools: + +- `cc`, `cxx`, `as` override compilers for C/C++/ASM +- `ar` overrides the archiver (used for `staticlib`) +- `ld` overrides the linker (used by GCC/Clang/NASM/GAS backends) + +Environment overrides (highest priority): +- `DCR_COMPILER` (all languages) +- `DCR_CC`, `DCR_CXX`, `DCR_AS`, `DCR_AR`, `DCR_LD` + ## Linking For `kind = "bin"`: diff --git a/docs/reference-and-help/config-reference.md b/docs/reference-and-help/config-reference.md index 604e4b1..9566218 100644 --- a/docs/reference-and-help/config-reference.md +++ b/docs/reference-and-help/config-reference.md @@ -17,6 +17,13 @@ platform = "string" # optional cflags = ["string", "..."] # optional ldflags = ["string", "..."] # optional +[toolchain] +cc = "string" # optional +cxx = "string" # optional +as = "string" # optional +ar = "string" # optional +ld = "string" # optional + [dependencies] name = { path = "string", # required for each dependency diff --git a/src/cli/build.rs b/src/cli/build.rs index 56719e5..133beea 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -33,6 +33,11 @@ pub fn build(args: &[String]) -> i32 { let build_target = get_config_str(&config, "build.target"); let build_kind = get_config_str(&config, "build.kind"); let build_platform = get_config_str(&config, "build.platform"); + let tc_cc = get_config_opt(&config, "toolchain.cc"); + let tc_cxx = get_config_opt(&config, "toolchain.cxx"); + let tc_as = get_config_opt(&config, "toolchain.as"); + let tc_ar = get_config_opt(&config, "toolchain.ar"); + let tc_ld = get_config_opt(&config, "toolchain.ld"); let build_cflags = match get_config_list(&config, "build.cflags") { Ok(v) => v, Err(msg) => { @@ -48,11 +53,21 @@ pub fn build(args: &[String]) -> i32 { } }; + let resolved_compiler = resolve_compiler( + &build_language, + &project_compiler, + tc_cc.as_deref(), + tc_cxx.as_deref(), + tc_as.as_deref(), + ); + let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref()); + let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref()); + println!( " Building project `{}`\n Profile: {}\n Compiler: {}\n", colored(&project_name, BOLD_GREEN), colored(&active_profile, BOLD_GREEN), - colored(&project_compiler, BOLD_GREEN) + colored(&resolved_compiler, BOLD_GREEN) ); ensure_target_dirs(&items, &active_profile, normalize_target(&build_target)); @@ -74,12 +89,14 @@ pub fn build(args: &[String]) -> i32 { let ctx = BuildContext { profile: &active_profile, project_name: &project_name, - compiler: &project_compiler, + compiler: &resolved_compiler, language: &build_language, standard: &build_standard, target_dir: normalize_target(&build_target), kind: normalize_kind(&build_kind), platform: normalize_platform(&build_platform), + linker: resolved_linker.as_deref(), + archiver: resolved_archiver.as_deref(), include_dirs: &resolved.include_dirs, lib_dirs: &resolved.lib_dirs, libs: &resolved.libs, @@ -126,6 +143,16 @@ fn get_config_str(config: &Config, key: &str) -> String { .to_string() } +fn get_config_opt(config: &Config, key: &str) -> Option { + let value = config.get(key)?.as_str()?; + let trimmed = value.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } +} + fn get_config_list(config: &Config, key: &str) -> Result, String> { let value = match config.get(key) { Some(v) => v, @@ -195,3 +222,76 @@ fn normalize_platform(platform: &str) -> Option<&str> { Some(trimmed) } } + +fn resolve_compiler( + language: &str, + compiler: &str, + tc_cc: Option<&str>, + tc_cxx: Option<&str>, + tc_as: Option<&str>, +) -> String { + let lang = language.to_lowercase(); + env_override_compiler(&lang) + .or_else(|| toolchain_override_compiler(&lang, tc_cc, tc_cxx, tc_as)) + .unwrap_or_else(|| compiler.to_string()) +} + +fn env_override_compiler(lang: &str) -> Option { + if let Ok(value) = std::env::var("DCR_COMPILER") { + let trimmed = value.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + if lang == "asm" { + if let Ok(value) = std::env::var("DCR_AS") { + let trimmed = value.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + return None; + } + if (lang == "c++" || lang == "cpp" || lang == "cxx") + && let Ok(value) = std::env::var("DCR_CXX") + { + let trimmed = value.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + if let Ok(value) = std::env::var("DCR_CC") { + let trimmed = value.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + None +} + +fn toolchain_override_compiler( + lang: &str, + tc_cc: Option<&str>, + tc_cxx: Option<&str>, + tc_as: Option<&str>, +) -> Option { + if lang == "asm" { + return tc_as.map(|v| v.to_string()); + } + if (lang == "c++" || lang == "cpp" || lang == "cxx") + && let Some(v) = tc_cxx + { + return Some(v.to_string()); + } + tc_cc.map(|v| v.to_string()) +} + +fn resolve_tool(env_key: &str, fallback: Option<&str>) -> Option { + if let Ok(value) = std::env::var(env_key) { + let trimmed = value.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + fallback.map(|v| v.to_string()) +} diff --git a/src/core/builder/clang.rs b/src/core/builder/clang.rs index 9d4f4ca..9caefa2 100644 --- a/src/core/builder/clang.rs +++ b/src/core/builder/clang.rs @@ -18,7 +18,7 @@ pub fn build(ctx: &BuildContext) -> Result { if ctx.kind == "staticlib" { let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); - let mut cmd = Command::new("ar"); + let mut cmd = Command::new(ctx.archiver.unwrap_or("ar")); cmd.arg("rcs").arg(&lib_path); for obj in &objects { cmd.arg(obj); @@ -33,7 +33,7 @@ pub fn build(ctx: &BuildContext) -> Result { } } - let mut cmd = Command::new(compiler); + let mut cmd = Command::new(ctx.linker.unwrap_or(compiler)); if ctx.kind == "sharedlib" { if cfg!(target_os = "macos") { cmd.arg("-dynamiclib"); @@ -150,6 +150,9 @@ fn build_objects( if ctx.kind == "sharedlib" { cmd.arg("-fPIC"); } + if let Some(flag) = asm_lang_flag(source) { + cmd.arg("-x").arg(flag); + } if let Some(platform) = ctx.platform && !platform.trim().is_empty() { @@ -178,6 +181,15 @@ fn build_objects( Ok(objects) } +fn asm_lang_flag(source: &str) -> Option<&'static str> { + let ext = Path::new(source).extension().and_then(|v| v.to_str())?; + match ext { + "S" => Some("assembler-with-cpp"), + "s" | "asm" => Some("assembler"), + _ => None, + } +} + fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String { let src_path = Path::new(source); let rel = src_path diff --git a/src/core/builder/gas.rs b/src/core/builder/gas.rs index d4ac729..cdd4ea6 100644 --- a/src/core/builder/gas.rs +++ b/src/core/builder/gas.rs @@ -21,12 +21,13 @@ pub fn build(ctx: &BuildContext) -> Result { if ctx.kind == "staticlib" { let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); - let mut cmd = Command::new(if cfg!(target_os = "windows") { + let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") { "lib" } else { "ar" }); - if cfg!(target_os = "windows") { + let mut cmd = Command::new(archiver); + if cfg!(target_os = "windows") && archiver == "lib" { cmd.arg("/nologo").arg(format!("/OUT:{lib_path}")); } else { cmd.arg("rcs").arg(&lib_path); @@ -44,7 +45,7 @@ pub fn build(ctx: &BuildContext) -> Result { } } - let mut cmd = Command::new("cc"); + let mut cmd = Command::new(ctx.linker.unwrap_or("cc")); if ctx.kind == "sharedlib" { if cfg!(target_os = "macos") { cmd.arg("-dynamiclib"); diff --git a/src/core/builder/gcc.rs b/src/core/builder/gcc.rs index 9d4f4ca..9caefa2 100644 --- a/src/core/builder/gcc.rs +++ b/src/core/builder/gcc.rs @@ -18,7 +18,7 @@ pub fn build(ctx: &BuildContext) -> Result { if ctx.kind == "staticlib" { let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); - let mut cmd = Command::new("ar"); + let mut cmd = Command::new(ctx.archiver.unwrap_or("ar")); cmd.arg("rcs").arg(&lib_path); for obj in &objects { cmd.arg(obj); @@ -33,7 +33,7 @@ pub fn build(ctx: &BuildContext) -> Result { } } - let mut cmd = Command::new(compiler); + let mut cmd = Command::new(ctx.linker.unwrap_or(compiler)); if ctx.kind == "sharedlib" { if cfg!(target_os = "macos") { cmd.arg("-dynamiclib"); @@ -150,6 +150,9 @@ fn build_objects( if ctx.kind == "sharedlib" { cmd.arg("-fPIC"); } + if let Some(flag) = asm_lang_flag(source) { + cmd.arg("-x").arg(flag); + } if let Some(platform) = ctx.platform && !platform.trim().is_empty() { @@ -178,6 +181,15 @@ fn build_objects( Ok(objects) } +fn asm_lang_flag(source: &str) -> Option<&'static str> { + let ext = Path::new(source).extension().and_then(|v| v.to_str())?; + match ext { + "S" => Some("assembler-with-cpp"), + "s" | "asm" => Some("assembler"), + _ => None, + } +} + fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String { let src_path = Path::new(source); let rel = src_path diff --git a/src/core/builder/mod.rs b/src/core/builder/mod.rs index 0d24476..4ecd2ff 100644 --- a/src/core/builder/mod.rs +++ b/src/core/builder/mod.rs @@ -13,6 +13,8 @@ pub struct BuildContext<'a> { pub target_dir: Option<&'a str>, pub kind: &'a str, pub platform: Option<&'a str>, + pub linker: Option<&'a str>, + pub archiver: Option<&'a str>, pub include_dirs: &'a [String], pub lib_dirs: &'a [String], pub libs: &'a [String], diff --git a/src/core/builder/msvc.rs b/src/core/builder/msvc.rs index 199a847..9c18d39 100644 --- a/src/core/builder/msvc.rs +++ b/src/core/builder/msvc.rs @@ -21,7 +21,7 @@ pub fn build(ctx: &BuildContext) -> Result { if ctx.kind == "staticlib" { let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); - let mut cmd = Command::new("lib"); + let mut cmd = Command::new(ctx.archiver.unwrap_or("lib")); cmd.arg("/nologo").arg(format!("/OUT:{lib_path}")); for obj in &objects { cmd.arg(obj); diff --git a/src/core/builder/nasm.rs b/src/core/builder/nasm.rs index dbd8778..57aa131 100644 --- a/src/core/builder/nasm.rs +++ b/src/core/builder/nasm.rs @@ -21,12 +21,13 @@ pub fn build(ctx: &BuildContext) -> Result { if ctx.kind == "staticlib" { let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); - let mut cmd = Command::new(if cfg!(target_os = "windows") { + let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") { "lib" } else { "ar" }); - if cfg!(target_os = "windows") { + let mut cmd = Command::new(archiver); + if cfg!(target_os = "windows") && archiver == "lib" { cmd.arg("/nologo").arg(format!("/OUT:{lib_path}")); } else { cmd.arg("rcs").arg(&lib_path); @@ -44,8 +45,7 @@ pub fn build(ctx: &BuildContext) -> Result { } } - let linker = "cc"; - let mut cmd = Command::new(linker); + let mut cmd = Command::new(ctx.linker.unwrap_or("cc")); if ctx.kind == "sharedlib" { if cfg!(target_os = "macos") { cmd.arg("-dynamiclib"); diff --git a/src/core/config.rs b/src/core/config.rs index 81cda4c..faf63ee 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -129,6 +129,15 @@ impl Config { { return Err(ConfigError::Invalid("build.platform is empty".into())); } + if let Some(toolchain) = self.get("toolchain").and_then(|v| v.as_table()) { + for key in ["cc", "cxx", "as", "ar", "ld"] { + if let Some(v) = toolchain.get(key) + && !v.as_str().map(|s| !s.trim().is_empty()).unwrap_or(false) + { + return Err(ConfigError::Invalid(format!("toolchain.{key} is invalid"))); + } + } + } if let Some(kind) = build.get("kind").and_then(|v| v.as_str()) { let kind = kind.trim(); if kind != "bin" && kind != "staticlib" && kind != "sharedlib" { @@ -213,6 +222,7 @@ fn format_toml(value: &Value) -> Result { .get("dependencies") .and_then(|v| v.as_table()) .ok_or_else(|| ConfigError::Invalid("missing [dependencies]".into()))?; + let toolchain = root.get("toolchain").and_then(|v| v.as_table()); let name = package.get("name").and_then(|v| v.as_str()).unwrap_or(""); let version = package @@ -251,6 +261,24 @@ fn format_toml(value: &Value) -> Result { } out.push('\n'); + if let Some(toolchain) = toolchain { + let mut lines = Vec::new(); + for key in ["cc", "cxx", "as", "ar", "ld"] { + if let Some(value) = toolchain.get(key).and_then(|v| v.as_str()) + && !value.trim().is_empty() + { + lines.push(format!("{key} = \"{value}\"")); + } + } + if !lines.is_empty() { + out.push_str("[toolchain]\n"); + for line in lines { + out.push_str(&format!("{line}\n")); + } + out.push('\n'); + } + } + out.push_str("[dependencies]\n"); if !deps.is_empty() { let mut keys: Vec<&String> = deps.keys().collect();