- `sharedlib` build kind (shared library output)
- ASM projects (`build.language = "asm"`)
- NASM and GAS backends
- `build.platform` architecture hint for `-march` / `/arch`
- Basic CLI integration tests
This commit is contained in:
2026-03-01 19:47:38 +08:00
parent 98531c7335
commit d1e3b0c384
30 changed files with 690 additions and 37 deletions

View File

@@ -1,5 +1,13 @@
# Changelog # Changelog
## [0.2.6] - 2026-03-01
Added:
- `sharedlib` build kind (shared library output)
- ASM projects (`build.language = "asm"`)
- NASM and GAS backends
- `build.platform` architecture hint for `-march` / `/arch`
- Basic CLI integration tests
## [0.2.5] - 2026-02-26 ## [0.2.5] - 2026-02-26
Added: Added:
- Incremental builds (object caching by mtime) - Incremental builds (object caching by mtime)

View File

@@ -30,6 +30,7 @@ Before PR, run:
cargo fmt --all cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings cargo clippy --all-targets --all-features -- -D warnings
cargo check cargo check
cargo test
``` ```
## CI/CD and Releases ## CI/CD and Releases

2
Cargo.lock generated
View File

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

View File

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

View File

@@ -15,6 +15,8 @@ The current implementation is written in Rust.
- Run the compiled binary - Run the compiled binary
- Clean build artifacts - Clean build artifacts
- Generate a minimal C project template - Generate a minimal C project template
- Build static and shared libraries
- ASM projects with NASM/GAS or via GCC/Clang
- Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs) - Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs)
## Supported Platforms ## Supported Platforms
@@ -119,8 +121,10 @@ Two profiles are supported:
Custom flags can be added in `dcr.toml` via `build.cflags` and `build.ldflags`. Custom flags can be added in `dcr.toml` via `build.cflags` and `build.ldflags`.
You can also set `build.target` to override the output directory (profile-independent). You can also set `build.target` to override the output directory (profile-independent).
You can set `build.platform` to pass `-march=<platform>` (GCC/Clang) or `/arch:*` (MSVC) where supported.
Use `build.kind = "staticlib"` to build a static library instead of a binary. Use `build.kind = "staticlib"` to build a static library instead of a binary.
`dcr run` is only for `build.kind = "bin"` and will fail for `staticlib`. Use `build.kind = "sharedlib"` to build a shared library (`.so`/`.dylib`/`.dll`).
`dcr run` is only for `build.kind = "bin"` and will fail for libraries.
## Configuration ## Configuration
The main project file is `dcr.toml`. The main project file is `dcr.toml`.
@@ -136,6 +140,8 @@ version = "0.1.0"
language = "c" language = "c"
standard = "c11" standard = "c11"
compiler = "clang" compiler = "clang"
# Optional platform hint
# platform = "x86_64"
# Optional custom flags # Optional custom flags
cflags = ["-Wall", "-Wextra"] cflags = ["-Wall", "-Wextra"]
ldflags = ["-lm"] ldflags = ["-lm"]

View File

@@ -2,7 +2,6 @@
- Add header dependency tracking for incremental builds (GCC/Clang: -MMD, MSVC: /showIncludes) - Add header dependency tracking for incremental builds (GCC/Clang: -MMD, MSVC: /showIncludes)
- Add external package managers (Conan, vcpkg) - Add external package managers (Conan, vcpkg)
- Object library
- Multi-target - Multi-target
- ASM support - ASM support
- Linker script + direct linker control - Linker script + direct linker control

View File

@@ -17,10 +17,13 @@ Current DCR focuses on a simple, predictable workflow:
- C: `*.c` - C: `*.c`
- C++: `*.cpp`, `*.cxx`, `*.cc` - C++: `*.cpp`, `*.cxx`, `*.cc`
- Compiler selection from `dcr.toml` (`clang`, `gcc`, `cl`, `clang-cl`, etc.). - Compiler selection from `dcr.toml` (`clang`, `gcc`, `cl`, `clang-cl`, etc.).
- Two build kinds: - ASM support via `language = "asm"` (GCC/Clang, NASM, GAS).
- Three build kinds:
- `bin` (default) - `bin` (default)
- `staticlib` - `staticlib`
- `sharedlib`
- Optional custom output directory via `build.target`. - Optional custom output directory via `build.target`.
- Optional `build.platform` to pass architecture hints (`-march` / `/arch`).
- Path dependencies with include/lib resolution and `dcr.lock` generation. - Path dependencies with include/lib resolution and `dcr.lock` generation.
## Important behavior notes ## Important behavior notes

View File

@@ -17,7 +17,7 @@ dcr build --release
3. Resolves path dependencies from `[dependencies]`. 3. Resolves path dependencies from `[dependencies]`.
4. Creates required output directories. 4. Creates required output directories.
5. Recursively compiles sources from `src/`. 5. Recursively compiles sources from `src/`.
6. Links final artifact (binary or static library). 6. Links final artifact (binary or library).
## Config values used ## Config values used
@@ -27,6 +27,7 @@ dcr build --release
- `build.standard` - `build.standard`
- `build.kind` - `build.kind`
- `build.target` - `build.target`
- `build.platform`
- `build.cflags` - `build.cflags`
- `build.ldflags` - `build.ldflags`
@@ -34,9 +35,11 @@ dcr build --release
- `language = "c"` -> `*.c` - `language = "c"` -> `*.c`
- `language = "c++" | "cpp" | "cxx"` -> `*.cpp`, `*.cxx`, `*.cc` - `language = "c++" | "cpp" | "cxx"` -> `*.cpp`, `*.cxx`, `*.cc`
- `language = "asm"` -> `*.s`, `*.S`, `*.asm`
## Notes ## Notes
- The profile is selected from the first argument only. - The profile is selected from the first argument only.
- Unknown profile flags return an error. - Unknown profile flags return an error.
- Incremental rebuild is based on source/object mtime comparison. - Incremental rebuild is based on source/object mtime comparison.
- For `language = "asm"` with `compiler = "as"`/`"gas"`, use `.s` files (no preprocessing). For `.S`, use `gcc` or `clang`.

View File

@@ -19,7 +19,7 @@ dcr run --release
## Restrictions ## Restrictions
- If `build.kind = "staticlib"`, `run` exits with error. - If `build.kind = "staticlib"` or `build.kind = "sharedlib"`, `run` exits with error.
- The profile is selected from the first argument. - The profile is selected from the first argument.
## On build failure ## On build failure

View File

@@ -33,7 +33,25 @@ By default:
- `target/debug/<name>` (or `.exe` on Windows) - `target/debug/<name>` (or `.exe` on Windows)
- `target/release/<name>` (or `.exe` on Windows) - `target/release/<name>` (or `.exe` on Windows)
Default compiler flags:
GCC/Clang:
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG`
- `release`: `-O3 -DNDEBUG -march=native`
MSVC:
- `debug`: `/Od /Zi /W4 /DDEBUG /Oy-`
- `release`: `/O2 /DNDEBUG`
Static library mode (`kind = "staticlib"`): Static library mode (`kind = "staticlib"`):
- Linux/macOS: `lib<name>.a` - Linux/macOS: `lib<name>.a`
- Windows: `<name>.lib` - Windows: `<name>.lib`
Shared library mode (`kind = "sharedlib"`):
- Linux: `lib<name>.so`
- macOS: `lib<name>.dylib`
- Windows: `<name>.dll`

View File

@@ -10,17 +10,19 @@ compiler = "clang"
kind = "bin" kind = "bin"
# optional: # optional:
# target = "./dist" # target = "./dist"
# platform = "x86_64"
# cflags = ["-Wall"] # cflags = ["-Wall"]
# ldflags = ["-lm"] # ldflags = ["-lm"]
``` ```
## Fields ## Fields
- `language` (string, required): `c`, `c++`, `cpp`, or `cxx`. - `language` (string, required): `c`, `c++`, `cpp`, `cxx`, or `asm`.
- `standard` (string, required): language standard passed to compiler. - `standard` (string, required): language standard passed to compiler.
- `compiler` (string, required): compiler command (for example `clang`, `gcc`, `cl`). - `compiler` (string, required): compiler command (for example `clang`, `gcc`, `cl`).
- `kind` (string, required): `bin` or `staticlib`. - `kind` (string, required): `bin`, `staticlib`, or `sharedlib`.
- `target` (string, optional): custom output directory for final artifact. - `target` (string, optional): custom output directory for final artifact.
- `platform` (string, optional): architecture hint for compiler (used for `-march` or `/arch`).
- `cflags` (string array, optional): extra compile flags. - `cflags` (string array, optional): extra compile flags.
- `ldflags` (string array, optional): extra link flags. - `ldflags` (string array, optional): extra link flags.
@@ -28,4 +30,4 @@ kind = "bin"
- Compiler backend is selected by compiler string (`gcc`, `clang`, `cl`, `clang-cl`). - Compiler backend is selected by compiler string (`gcc`, `clang`, `cl`, `clang-cl`).
- Empty or unknown compiler value falls back to clang-like build path. - Empty or unknown compiler value falls back to clang-like build path.
- `run` is not allowed for `kind = "staticlib"`. - `run` is not allowed for library kinds (`staticlib`, `sharedlib`).

View File

@@ -35,7 +35,7 @@ kind = "bin"
- `build.compiler` - `build.compiler`
- `build.kind` - `build.kind`
`build.kind` must be either `bin` or `staticlib`. `build.kind` must be either `bin`, `staticlib`, or `sharedlib`.
## Optional keys ## Optional keys

View File

@@ -5,9 +5,11 @@
DCR picks a backend from `build.compiler`: DCR picks a backend from `build.compiler`:
- contains `clang-cl` -> MSVC backend - contains `clang-cl` -> MSVC backend
- equals `as` or contains `gas` -> GAS backend (standalone assembler)
- contains `gcc` or `g++` -> GCC backend - contains `gcc` or `g++` -> GCC backend
- contains `clang` or `clang++` -> Clang backend - contains `clang` or `clang++` -> Clang backend
- equals `cl` or contains `msvc` -> MSVC backend - equals `cl` or contains `msvc` -> MSVC backend
- contains `nasm` -> NASM backend
- otherwise -> Clang backend fallback - otherwise -> Clang backend fallback
## Compilation model ## Compilation model
@@ -16,6 +18,27 @@ DCR picks a backend from `build.compiler`:
- Each source is compiled to `target/<profile>/obj/<relative-path>.o` (`.obj` on MSVC). - Each source is compiled to `target/<profile>/obj/<relative-path>.o` (`.obj` on MSVC).
- Recompile happens when source mtime is newer than object mtime. - Recompile happens when source mtime is newer than object mtime.
## Default build flags
These flags are applied automatically based on profile and compiler backend.
GCC/Clang:
- `debug`: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG`
- `release`: `-O3 -DNDEBUG -march=native`
MSVC:
- `debug`: `/Od /Zi /W4 /DDEBUG /Oy-`
- `release`: `/O2 /DNDEBUG`
## Platform hint
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`)
## Linking ## Linking
For `kind = "bin"`: For `kind = "bin"`:
@@ -28,3 +51,9 @@ For `kind = "staticlib"`:
- Linux/macOS (GCC/Clang): `ar rcs` creates `.a` archive. - Linux/macOS (GCC/Clang): `ar rcs` creates `.a` archive.
- Windows (MSVC): `lib` creates `.lib` archive. - Windows (MSVC): `lib` creates `.lib` archive.
For `kind = "sharedlib"`:
- Linux (GCC/Clang): `-shared` with output `.so`.
- macOS (Clang): `-dynamiclib` with output `.dylib`.
- Windows (MSVC): `/LD` with output `.dll`.

View File

@@ -41,6 +41,6 @@ dcr clean --release
## Notes ## Notes
- `run` is unavailable for `build.kind = "staticlib"`. - `run` is unavailable for `build.kind = "staticlib"` and `build.kind = "sharedlib"`.
- Build profile flag is parsed from the first command argument. - Build profile flag is parsed from the first command argument.
- Unknown profile flags produce an error. - Unknown profile flags produce an error.

View File

@@ -35,5 +35,6 @@ project/
- `target/<profile>/deps/`: synchronized copies of path dependencies. - `target/<profile>/deps/`: synchronized copies of path dependencies.
- `target/<profile>/<name>` (or `.exe` on Windows): output for `kind = "bin"`. - `target/<profile>/<name>` (or `.exe` on Windows): output for `kind = "bin"`.
- `target/<profile>/lib<name>.a` (or `<name>.lib` on Windows): output for `kind = "staticlib"`. - `target/<profile>/lib<name>.a` (or `<name>.lib` on Windows): output for `kind = "staticlib"`.
- `target/<profile>/lib<name>.so` (or `.dylib` on macOS, `<name>.dll` on Windows): output for `kind = "sharedlib"`.
If `build.target` is set, the final artifact is written to that custom directory, while object cache still stays in `target/<profile>/obj`. If `build.target` is set, the final artifact is written to that custom directory, while object cache still stays in `target/<profile>/obj`.

View File

@@ -8,11 +8,12 @@ name = "string" # required
version = "string" # required version = "string" # required
[build] [build]
language = "c|c++|cpp|cxx" # required language = "c|c++|cpp|cxx|asm" # required
standard = "string" # required standard = "string" # required
compiler = "string" # required compiler = "string" # required
kind = "bin|staticlib" # required kind = "bin|staticlib|sharedlib" # required
target = "string" # optional target = "string" # optional
platform = "string" # optional
cflags = ["string", "..."] # optional cflags = ["string", "..."] # optional
ldflags = ["string", "..."] # optional ldflags = ["string", "..."] # optional
@@ -29,7 +30,7 @@ name = {
- `[package]`, `[build]`, `[dependencies]` must exist. - `[package]`, `[build]`, `[dependencies]` must exist.
- Required string fields must be non-empty. - Required string fields must be non-empty.
- `build.kind` must be `bin` or `staticlib`. - `build.kind` must be `bin`, `staticlib`, or `sharedlib`.
- Dependency fields `include/lib/libs` must be string arrays when provided. - Dependency fields `include/lib/libs` must be string arrays when provided.
## Generated defaults (`dcr new` / `dcr init`) ## Generated defaults (`dcr new` / `dcr init`)

View File

@@ -32,6 +32,7 @@ pub fn build(args: &[String]) -> i32 {
let build_standard = get_config_str(&config, "build.standard"); let build_standard = get_config_str(&config, "build.standard");
let build_target = get_config_str(&config, "build.target"); let build_target = get_config_str(&config, "build.target");
let build_kind = get_config_str(&config, "build.kind"); let build_kind = get_config_str(&config, "build.kind");
let build_platform = get_config_str(&config, "build.platform");
let build_cflags = match get_config_list(&config, "build.cflags") { let build_cflags = match get_config_list(&config, "build.cflags") {
Ok(v) => v, Ok(v) => v,
Err(msg) => { Err(msg) => {
@@ -78,6 +79,7 @@ pub fn build(args: &[String]) -> i32 {
standard: &build_standard, standard: &build_standard,
target_dir: normalize_target(&build_target), target_dir: normalize_target(&build_target),
kind: normalize_kind(&build_kind), kind: normalize_kind(&build_kind),
platform: normalize_platform(&build_platform),
include_dirs: &resolved.include_dirs, include_dirs: &resolved.include_dirs,
lib_dirs: &resolved.lib_dirs, lib_dirs: &resolved.lib_dirs,
libs: &resolved.libs, libs: &resolved.libs,
@@ -184,3 +186,8 @@ fn normalize_kind(kind: &str) -> &str {
let trimmed = kind.trim(); let trimmed = kind.trim();
if trimmed.is_empty() { "bin" } else { trimmed } if trimmed.is_empty() { "bin" } else { trimmed }
} }
fn normalize_platform(platform: &str) -> Option<&str> {
let trimmed = platform.trim();
if trimmed.is_empty() { None } else { Some(trimmed) }
}

View File

@@ -51,8 +51,9 @@ pub fn run(args: &[String]) -> i32 {
} }
} }
if build_kind.trim() == "staticlib" { let kind = build_kind.trim();
error("Cannot run static library"); if kind == "staticlib" || kind == "sharedlib" {
error("Cannot run library build");
return 1; return 1;
} }
let build_status = build(args); let build_status = build(args);

View File

@@ -34,6 +34,13 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} }
let mut cmd = Command::new(compiler); let mut cmd = Command::new(compiler);
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
} }
@@ -52,11 +59,12 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for flag in ctx.ldflags { for flag in ctx.ldflags {
cmd.arg(flag); cmd.arg(flag);
} }
cmd.arg("-o").arg(platform::bin_path( let out_path = if ctx.kind == "sharedlib" {
ctx.profile, platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
ctx.project_name, } else {
ctx.target_dir, platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
)); };
cmd.arg("-o").arg(out_path);
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
@@ -99,7 +107,8 @@ fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec<String>) -> Result<(
let file = path.to_string_lossy().to_string(); let file = path.to_string_lossy().to_string();
let allowed = (lang == "c" && ext == "c") let allowed = (lang == "c" && ext == "c")
|| ((lang == "c++" || lang == "cpp" || lang == "cxx") || ((lang == "c++" || lang == "cpp" || lang == "cxx")
&& (ext == "cpp" || ext == "cxx" || ext == "cc")); && (ext == "cpp" || ext == "cxx" || ext == "cc"))
|| (lang == "asm" && (ext == "s" || ext == "asm"));
if allowed { if allowed {
out.push(file); out.push(file);
} }
@@ -138,7 +147,15 @@ fn build_objects(
if needs_rebuild(source, &obj_path) { if needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(compiler); let mut cmd = Command::new(compiler);
cmd.arg("-c").arg(source).arg("-o").arg(&obj_path); cmd.arg("-c").arg(source).arg("-o").arg(&obj_path);
if !ctx.standard.is_empty() { if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
if let Some(platform) = ctx.platform {
if !platform.trim().is_empty() {
cmd.arg(format!("-march={}", platform));
}
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
cmd.arg(format!("-std={}", ctx.standard)); cmd.arg(format!("-std={}", ctx.standard));
} }
for flag in default_flags(ctx.profile) { for flag in default_flags(ctx.profile) {

159
src/core/builder/gas.rs Normal file
View File

@@ -0,0 +1,159 @@
use crate::core::builder::BuildContext;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.language.to_lowercase().as_str() != "asm" {
return Err("GAS backend requires build.language = \"asm\"".to_string());
}
let assembler = if ctx.compiler.is_empty() { "as" } else { ctx.compiler };
let start_time = Instant::now();
let sources = collect_sources(ctx.language)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?;
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") { "lib" } else { "ar" });
if cfg!(target_os = "windows") {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in &objects {
cmd.arg(obj);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let mut cmd = Command::new("cc");
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in &objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
};
cmd.arg("-o").arg(out_path);
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
fn collect_sources(language: &str) -> Result<Vec<String>, String> {
let lang = language.to_lowercase();
let mut sources = Vec::new();
collect_sources_rec("./src", &lang, &mut sources)?;
sources.sort();
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
}
fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec<String>) -> Result<(), String> {
let entries = fs::read_dir(dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
collect_sources_rec(&path.to_string_lossy(), lang, out)?;
continue;
}
if !path.is_file() {
continue;
}
let ext_raw = path.extension().and_then(|v| v.to_str()).unwrap_or("");
let ext = ext_raw.to_lowercase();
let file = path.to_string_lossy().to_string();
let allowed = lang == "asm" && ext == "s" && ext_raw == "s";
if allowed {
out.push(file);
}
}
Ok(())
}
fn build_objects(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
for source in sources {
let obj_path = object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(assembler);
cmd.arg(source).arg("-o").arg(&obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
Ok(objects)
}
fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
match (src_time, obj_time) {
(Ok(s), Ok(o)) => s > o,
(Ok(_), Err(_)) => true,
_ => true,
}
}

View File

@@ -34,6 +34,13 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} }
let mut cmd = Command::new(compiler); let mut cmd = Command::new(compiler);
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
} }
@@ -52,11 +59,12 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
for flag in ctx.ldflags { for flag in ctx.ldflags {
cmd.arg(flag); cmd.arg(flag);
} }
cmd.arg("-o").arg(platform::bin_path( let out_path = if ctx.kind == "sharedlib" {
ctx.profile, platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
ctx.project_name, } else {
ctx.target_dir, platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
)); };
cmd.arg("-o").arg(out_path);
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
@@ -99,7 +107,8 @@ fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec<String>) -> Result<(
let file = path.to_string_lossy().to_string(); let file = path.to_string_lossy().to_string();
let allowed = (lang == "c" && ext == "c") let allowed = (lang == "c" && ext == "c")
|| ((lang == "c++" || lang == "cpp" || lang == "cxx") || ((lang == "c++" || lang == "cpp" || lang == "cxx")
&& (ext == "cpp" || ext == "cxx" || ext == "cc")); && (ext == "cpp" || ext == "cxx" || ext == "cc"))
|| (lang == "asm" && (ext == "s" || ext == "asm"));
if allowed { if allowed {
out.push(file); out.push(file);
} }
@@ -138,7 +147,15 @@ fn build_objects(
if needs_rebuild(source, &obj_path) { if needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(compiler); let mut cmd = Command::new(compiler);
cmd.arg("-c").arg(source).arg("-o").arg(&obj_path); cmd.arg("-c").arg(source).arg("-o").arg(&obj_path);
if !ctx.standard.is_empty() { if ctx.kind == "sharedlib" {
cmd.arg("-fPIC");
}
if let Some(platform) = ctx.platform {
if !platform.trim().is_empty() {
cmd.arg(format!("-march={}", platform));
}
}
if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
cmd.arg(format!("-std={}", ctx.standard)); cmd.arg(format!("-std={}", ctx.standard));
} }
for flag in default_flags(ctx.profile) { for flag in default_flags(ctx.profile) {

View File

@@ -1,6 +1,8 @@
pub mod clang; pub mod clang;
pub mod gcc; pub mod gcc;
pub mod gas;
pub mod msvc; pub mod msvc;
pub mod nasm;
pub struct BuildContext<'a> { pub struct BuildContext<'a> {
pub profile: &'a str, pub profile: &'a str,
@@ -10,6 +12,7 @@ pub struct BuildContext<'a> {
pub standard: &'a str, pub standard: &'a str,
pub target_dir: Option<&'a str>, pub target_dir: Option<&'a str>,
pub kind: &'a str, pub kind: &'a str,
pub platform: Option<&'a str>,
pub include_dirs: &'a [String], pub include_dirs: &'a [String],
pub lib_dirs: &'a [String], pub lib_dirs: &'a [String],
pub libs: &'a [String], pub libs: &'a [String],
@@ -22,6 +25,12 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if compiler.contains("clang-cl") { if compiler.contains("clang-cl") {
return msvc::build(ctx); return msvc::build(ctx);
} }
if compiler == "as" || compiler.contains("gas") {
return gas::build(ctx);
}
if compiler.contains("nasm") {
return nasm::build(ctx);
}
if compiler.contains("gcc") || compiler.contains("g++") { if compiler.contains("gcc") || compiler.contains("g++") {
return gcc::build(ctx); return gcc::build(ctx);
} }

View File

@@ -11,6 +11,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
} else { } else {
ctx.compiler ctx.compiler
}; };
if ctx.language.to_lowercase() == "asm" {
return Err("MSVC backend does not support build.language = \"asm\"".to_string());
}
let start_time = Instant::now(); let start_time = Instant::now();
let sources = collect_sources(ctx.language)?; let sources = collect_sources(ctx.language)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj"); let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
@@ -35,6 +38,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
let mut cmd = Command::new(compiler); let mut cmd = Command::new(compiler);
cmd.arg("/nologo"); cmd.arg("/nologo");
if ctx.kind == "sharedlib" {
cmd.arg("/LD");
}
match ctx.language.to_lowercase().as_str() { match ctx.language.to_lowercase().as_str() {
"c" => { "c" => {
cmd.arg("/TC"); cmd.arg("/TC");
@@ -77,10 +83,12 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
cmd.arg(flag); cmd.arg(flag);
} }
} }
cmd.arg(format!( let out_path = if ctx.kind == "sharedlib" {
"/Fe:{}", platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir) platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
)); };
cmd.arg(format!("/Fe:{out_path}"));
match cmd.status() { match cmd.status() {
Ok(status) if status.success() => { Ok(status) if status.success() => {
@@ -123,7 +131,8 @@ fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec<String>) -> Result<(
let file = path.to_string_lossy().to_string(); let file = path.to_string_lossy().to_string();
let allowed = (lang == "c" && ext == "c") let allowed = (lang == "c" && ext == "c")
|| ((lang == "c++" || lang == "cpp" || lang == "cxx") || ((lang == "c++" || lang == "cpp" || lang == "cxx")
&& (ext == "cpp" || ext == "cxx" || ext == "cc")); && (ext == "cpp" || ext == "cxx" || ext == "cc"))
|| (lang == "asm" && (ext == "s" || ext == "asm"));
if allowed { if allowed {
out.push(file); out.push(file);
} }
@@ -154,6 +163,23 @@ fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String>
Err("Unsupported language".to_string()) Err("Unsupported language".to_string())
} }
fn msvc_arch_flag(platform: Option<&str>) -> Option<&'static str> {
let raw = platform?.trim();
if raw.is_empty() {
return None;
}
let p = raw.to_lowercase().replace('-', "_");
if p == "x86" || (p.starts_with('i') && p.ends_with("86") && p.len() == 4) {
return Some("/arch:IA32");
}
match p.as_str() {
"sse2" => Some("/arch:SSE2"),
"avx" => Some("/arch:AVX"),
"avx2" => Some("/arch:AVX2"),
_ => None,
}
}
fn default_flags(profile: &str) -> &'static [&'static str] { fn default_flags(profile: &str) -> &'static [&'static str] {
match profile { match profile {
"release" => &["/O2", "/DNDEBUG"], "release" => &["/O2", "/DNDEBUG"],
@@ -189,10 +215,13 @@ fn build_objects(
return Err("Unsupported language".to_string()); return Err("Unsupported language".to_string());
} }
} }
if !ctx.standard.is_empty() { if !ctx.standard.is_empty() && ctx.language.to_lowercase() != "asm" {
let std_flag = msvc_standard_flag(ctx.language, ctx.standard)?; let std_flag = msvc_standard_flag(ctx.language, ctx.standard)?;
cmd.arg(std_flag); cmd.arg(std_flag);
} }
if let Some(flag) = msvc_arch_flag(ctx.platform) {
cmd.arg(flag);
}
for flag in default_flags(ctx.profile) { for flag in default_flags(ctx.profile) {
cmd.arg(flag); cmd.arg(flag);
} }

201
src/core/builder/nasm.rs Normal file
View File

@@ -0,0 +1,201 @@
use crate::core::builder::BuildContext;
use crate::platform;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.language.to_lowercase().as_str() != "asm" {
return Err("NASM backend requires build.language = \"asm\"".to_string());
}
let assembler = if ctx.compiler.is_empty() {
"nasm"
} else {
ctx.compiler
};
let start_time = Instant::now();
let sources = collect_sources(ctx.language)?;
let obj_dir = Path::new("./target").join(ctx.profile).join("obj");
let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?;
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") { "lib" } else { "ar" });
if cfg!(target_os = "windows") {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
}
for obj in &objects {
cmd.arg(obj);
}
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
return Ok(elapsed);
}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
let linker = if cfg!(target_os = "windows") { "cc" } else { "cc" };
let mut cmd = Command::new(linker);
if ctx.kind == "sharedlib" {
if cfg!(target_os = "macos") {
cmd.arg("-dynamiclib");
} else {
cmd.arg("-shared");
}
}
for obj in &objects {
cmd.arg(obj);
}
for dir in ctx.lib_dirs {
cmd.arg(format!("-L{dir}"));
}
for lib in ctx.libs {
cmd.arg(format!("-l{lib}"));
}
for flag in ctx.ldflags {
cmd.arg(flag);
}
let out_path = if ctx.kind == "sharedlib" {
platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir)
} else {
platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir)
};
cmd.arg("-o").arg(out_path);
match cmd.status() {
Ok(status) if status.success() => {
let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0;
Ok(elapsed)
}
Ok(_) => Err("Build failed".to_string()),
Err(err) => Err(format!("Build failed: {err}")),
}
}
fn collect_sources(language: &str) -> Result<Vec<String>, String> {
let lang = language.to_lowercase();
let mut sources = Vec::new();
collect_sources_rec("./src", &lang, &mut sources)?;
sources.sort();
if sources.is_empty() {
return Err("No source files found in ./src".to_string());
}
Ok(sources)
}
fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec<String>) -> Result<(), String> {
let entries = fs::read_dir(dir).map_err(|err| format!("src dir error: {err}"))?;
for entry in entries {
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
let path = entry.path();
if path.is_dir() {
collect_sources_rec(&path.to_string_lossy(), lang, out)?;
continue;
}
if !path.is_file() {
continue;
}
let ext_raw = path.extension().and_then(|v| v.to_str()).unwrap_or("");
let ext = ext_raw.to_lowercase();
let file = path.to_string_lossy().to_string();
let allowed = lang == "asm" && (ext == "asm" || (ext == "s" && ext_raw == "s"));
if allowed {
out.push(file);
}
}
Ok(())
}
fn build_objects(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let mut objects = Vec::new();
let format = nasm_format(ctx.platform);
for source in sources {
let obj_path = object_path(obj_dir, source, obj_ext);
if let Some(parent) = Path::new(&obj_path).parent() {
fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?;
}
if needs_rebuild(source, &obj_path) {
let mut cmd = Command::new(assembler);
cmd.arg("-f").arg(format).arg(source).arg("-o").arg(&obj_path);
for flag in ctx.cflags {
cmd.arg(flag);
}
match cmd.status() {
Ok(status) if status.success() => {}
Ok(_) => return Err("Build failed".to_string()),
Err(err) => return Err(format!("Build failed: {err}")),
}
}
objects.push(obj_path);
}
Ok(objects)
}
fn nasm_format(platform: Option<&str>) -> &'static str {
if let Some(p) = platform {
let p = p.to_lowercase().replace('-', "_");
if p == "x86" || (p.starts_with('i') && p.ends_with("86") && p.len() == 4) {
#[cfg(target_os = "macos")]
{
return "macho32";
}
#[cfg(target_os = "windows")]
{
return "win32";
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
return "elf32";
}
}
}
#[cfg(target_os = "linux")]
{
return "elf64";
}
#[cfg(target_os = "macos")]
{
return "macho64";
}
#[cfg(target_os = "windows")]
{
return "win64";
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
"elf64"
}
}
fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source);
let rel = src_path
.strip_prefix("./src")
.or_else(|_| src_path.strip_prefix("src"))
.unwrap_or(src_path);
let mut out = obj_dir.join(rel);
out.set_extension(obj_ext.trim_start_matches('.'));
out.to_string_lossy().to_string()
}
fn needs_rebuild(source: &str, object: &str) -> bool {
let src_time = fs::metadata(source).and_then(|m| m.modified());
let obj_time = fs::metadata(object).and_then(|m| m.modified());
match (src_time, obj_time) {
(Ok(s), Ok(o)) => s > o,
(Ok(_), Err(_)) => true,
_ => true,
}
}

View File

@@ -121,9 +121,14 @@ impl Config {
return Err(ConfigError::Invalid(format!("build.{key} is empty"))); return Err(ConfigError::Invalid(format!("build.{key} is empty")));
} }
} }
if let Some(platform) = build.get("platform") {
if !platform.as_str().map(|v| !v.trim().is_empty()).unwrap_or(false) {
return Err(ConfigError::Invalid("build.platform is empty".into()));
}
}
if let Some(kind) = build.get("kind").and_then(|v| v.as_str()) { if let Some(kind) = build.get("kind").and_then(|v| v.as_str()) {
let kind = kind.trim(); let kind = kind.trim();
if kind != "bin" && kind != "staticlib" { if kind != "bin" && kind != "staticlib" && kind != "sharedlib" {
return Err(ConfigError::Invalid("build.kind is invalid".into())); return Err(ConfigError::Invalid("build.kind is invalid".into()));
} }
} }

View File

@@ -11,3 +11,10 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
None => format!("./target/{profile}/lib{name}.a"), None => format!("./target/{profile}/lib{name}.a"),
} }
} }
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.so"),
}
}

View File

@@ -11,3 +11,10 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
None => format!("./target/{profile}/lib{name}.a"), None => format!("./target/{profile}/lib{name}.a"),
} }
} }
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/lib{}.dylib", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.dylib"),
}
}

View File

@@ -48,3 +48,25 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
} }
} }
} }
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
#[cfg(target_os = "linux")]
{
linux::shared_lib_path(profile, name, target_dir)
}
#[cfg(target_os = "macos")]
{
return macos::shared_lib_path(profile, name, target_dir);
}
#[cfg(target_os = "windows")]
{
return windows::shared_lib_path(profile, name, target_dir);
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
match target_dir {
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/lib{name}.so"),
}
}
}

View File

@@ -11,3 +11,10 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
None => format!("./target/{profile}/{name}.lib"), None => format!("./target/{profile}/{name}.lib"),
} }
} }
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
match target_dir {
Some(dir) => format!("{}/{}.dll", dir.trim_end_matches('/'), name),
None => format!("./target/{profile}/{name}.dll"),
}
}

94
tests/cli_basic.rs Normal file
View File

@@ -0,0 +1,94 @@
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Once;
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static BUILD_ONCE: Once = Once::new();
fn bin_path() -> PathBuf {
if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") {
return PathBuf::from(exe);
}
ensure_bin_built();
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("target");
path.push("debug");
path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX));
path
}
fn unique_temp_dir(prefix: &str) -> PathBuf {
let pid = std::process::id();
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut path = std::env::temp_dir();
path.push(format!("dcr_{prefix}_{pid}_{n}_{now}"));
std::fs::create_dir_all(&path).expect("failed to create temp dir");
path
}
fn run_dcr(args: &[&str], cwd: &Path) -> std::process::Output {
Command::new(bin_path())
.args(args)
.current_dir(cwd)
.output()
.expect("failed to run dcr")
}
fn ensure_bin_built() {
BUILD_ONCE.call_once(|| {
let status = Command::new("cargo")
.arg("build")
.current_dir(env!("CARGO_MANIFEST_DIR"))
.status()
.expect("failed to run cargo build");
assert!(status.success(), "cargo build failed");
});
}
#[test]
fn help_and_version_work() {
let dir = unique_temp_dir("help");
let out = run_dcr(&["--help"], &dir);
assert!(out.status.success(), "--help should succeed");
let out = run_dcr(&["--version"], &dir);
assert!(out.status.success(), "--version should succeed");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("dcr"), "version output should mention dcr");
}
#[test]
fn new_creates_project_layout() {
let dir = unique_temp_dir("new");
let out = run_dcr(&["new", "hello"], &dir);
assert!(out.status.success(), "dcr new should succeed");
let project_dir = dir.join("hello");
assert!(project_dir.is_dir(), "project dir should exist");
assert!(project_dir.join("dcr.toml").is_file(), "dcr.toml missing");
assert!(
project_dir.join("src").join("main.c").is_file(),
"src/main.c missing"
);
}
#[test]
fn init_and_clean_remove_target() {
let dir = unique_temp_dir("init");
let out = run_dcr(&["init"], &dir);
assert!(out.status.success(), "dcr init should succeed");
let target_debug = dir.join("target").join("debug");
std::fs::create_dir_all(&target_debug).expect("failed to create target/debug");
std::fs::write(target_debug.join("dummy.o"), "x").expect("failed to write dummy file");
let out = run_dcr(&["clean"], &dir);
assert!(out.status.success(), "dcr clean should succeed");
assert!(!dir.join("target").exists(), "target should be removed");
}