- Toolchain overrides via `[toolchain]` and env (`DCR_*`)
- ASM `.S` preprocessing via GCC/Clang (`-x assembler-with-cpp`)
This commit is contained in:
2026-03-03 22:25:00 +08:00
parent 9af91a0d9f
commit 617ba85442
16 changed files with 215 additions and 19 deletions

View File

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

2
Cargo.lock generated
View File

@@ -90,7 +90,7 @@ dependencies = [
[[package]]
name = "dcr"
version = "0.2.6"
version = "0.2.7"
dependencies = [
"reqwest",
"self-replace",

View File

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

View File

@@ -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]
```

View File

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

View File

@@ -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`).

View File

@@ -39,6 +39,18 @@ If `build.platform` is set, DCR passes it as an architecture hint:
- GCC/Clang: `-march=<platform>`
- 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"`:

View File

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

View File

@@ -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<String> {
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<Vec<String>, 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<String> {
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<String> {
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<String> {
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())
}

View File

@@ -18,7 +18,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
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<f64, String> {
}
}
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

View File

@@ -21,12 +21,13 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
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<f64, String> {
}
}
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");

View File

@@ -18,7 +18,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
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<f64, String> {
}
}
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

View File

@@ -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],

View File

@@ -21,7 +21,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
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);

View File

@@ -21,12 +21,13 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
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<f64, String> {
}
}
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");

View File

@@ -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<String, ConfigError> {
.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<String, ConfigError> {
}
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();