6 Commits

Author SHA1 Message Date
617ba85442 Added:
- Toolchain overrides via `[toolchain]` and env (`DCR_*`)
- ASM `.S` preprocessing via GCC/Clang (`-x assembler-with-cpp`)
2026-03-03 22:25:00 +08:00
9af91a0d9f 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
2026-03-01 20:04:08 +08:00
a247e33555 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
2026-03-01 19:58:43 +08:00
d1e3b0c384 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
2026-03-01 19:47:38 +08:00
98531c7335 Update TODO.md 2026-02-27 22:07:04 +08:00
75859a6a53 fix Cargo.toml - version 2026-02-27 21:36:13 +08:00
30 changed files with 921 additions and 45 deletions

View File

@@ -1,5 +1,18 @@
# Changelog # 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)
- 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.4" version = "0.2.7"
dependencies = [ dependencies = [
"reqwest", "reqwest",
"self-replace", "self-replace",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "dcr" name = "dcr"
version = "0.2.4" version = "0.2.7"
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,10 +140,20 @@ 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"]
[toolchain]
# Optional tool overrides:
# cc = "clang"
# cxx = "clang++"
# as = "as"
# ar = "ar"
# ld = "ld"
[dependencies] [dependencies]
``` ```

View File

@@ -1,6 +1,7 @@
# TODO # TODO
- Add incremental builds
- 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)
- Support building static libraries (ar .a) - Multi-target
- Linker script + direct linker control
- Custom targets/steps

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,22 +10,35 @@ 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.
## Toolchain section
```toml
[toolchain]
cc = "clang"
cxx = "clang++"
as = "as"
ar = "ar"
ld = "ld"
```
## Behavior notes ## Behavior notes
- 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,39 @@ 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`)
## 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 ## Linking
For `kind = "bin"`: For `kind = "bin"`:
@@ -28,3 +63,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,14 +8,22 @@ 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
[toolchain]
cc = "string" # optional
cxx = "string" # optional
as = "string" # optional
ar = "string" # optional
ld = "string" # optional
[dependencies] [dependencies]
name = { name = {
path = "string", # required for each dependency path = "string", # required for each dependency
@@ -29,7 +37,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,12 @@ 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 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") { let build_cflags = match get_config_list(&config, "build.cflags") {
Ok(v) => v, Ok(v) => v,
Err(msg) => { Err(msg) => {
@@ -47,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!( println!(
" Building project `{}`\n Profile: {}\n Compiler: {}\n", " Building project `{}`\n Profile: {}\n Compiler: {}\n",
colored(&project_name, BOLD_GREEN), colored(&project_name, BOLD_GREEN),
colored(&active_profile, 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)); ensure_target_dirs(&items, &active_profile, normalize_target(&build_target));
@@ -73,11 +89,14 @@ pub fn build(args: &[String]) -> i32 {
let ctx = BuildContext { let ctx = BuildContext {
profile: &active_profile, profile: &active_profile,
project_name: &project_name, project_name: &project_name,
compiler: &project_compiler, compiler: &resolved_compiler,
language: &build_language, language: &build_language,
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),
linker: resolved_linker.as_deref(),
archiver: resolved_archiver.as_deref(),
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,
@@ -124,6 +143,16 @@ fn get_config_str(config: &Config, key: &str) -> String {
.to_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> { fn get_config_list(config: &Config, key: &str) -> Result<Vec<String>, String> {
let value = match config.get(key) { let value = match config.get(key) {
Some(v) => v, Some(v) => v,
@@ -184,3 +213,85 @@ 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)
}
}
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

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

@@ -18,7 +18,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.kind == "staticlib" { if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); 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); cmd.arg("rcs").arg(&lib_path);
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
@@ -33,7 +33,14 @@ 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");
} 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,18 @@ 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(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
if let Some(platform) = ctx.platform
&& !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) {
@@ -161,6 +181,15 @@ fn build_objects(
Ok(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 { fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source); let src_path = Path::new(source);
let rel = src_path let rel = src_path

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

@@ -0,0 +1,168 @@
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 archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
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);
}
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(ctx.linker.unwrap_or("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

@@ -18,7 +18,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.kind == "staticlib" { if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); 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); cmd.arg("rcs").arg(&lib_path);
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(obj);
@@ -33,7 +33,14 @@ 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");
} 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,18 @@ 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(flag) = asm_lang_flag(source) {
cmd.arg("-x").arg(flag);
}
if let Some(platform) = ctx.platform
&& !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) {
@@ -161,6 +181,15 @@ fn build_objects(
Ok(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 { fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
let src_path = Path::new(source); let src_path = Path::new(source);
let rel = src_path let rel = src_path

View File

@@ -1,6 +1,8 @@
pub mod clang; pub mod clang;
pub mod gas;
pub mod gcc; pub mod gcc;
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,9 @@ 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 linker: Option<&'a str>,
pub archiver: 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 +27,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");
@@ -18,7 +21,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
if ctx.kind == "staticlib" { if ctx.kind == "staticlib" {
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); 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}")); cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
for obj in &objects { for obj in &objects {
cmd.arg(obj); cmd.arg(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);
} }

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

@@ -0,0 +1,209 @@
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 archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
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);
}
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(ctx.linker.unwrap_or("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 == "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")]
{
"elf64"
}
#[cfg(target_os = "macos")]
{
"macho64"
}
#[cfg(target_os = "windows")]
{
"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,26 @@ 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")
&& !platform
.as_str()
.map(|v| !v.trim().is_empty())
.unwrap_or(false)
{
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()) { 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()));
} }
} }
@@ -205,6 +222,7 @@ fn format_toml(value: &Value) -> Result<String, ConfigError> {
.get("dependencies") .get("dependencies")
.and_then(|v| v.as_table()) .and_then(|v| v.as_table())
.ok_or_else(|| ConfigError::Invalid("missing [dependencies]".into()))?; .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 name = package.get("name").and_then(|v| v.as_str()).unwrap_or("");
let version = package let version = package
@@ -243,6 +261,24 @@ fn format_toml(value: &Value) -> Result<String, ConfigError> {
} }
out.push('\n'); 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"); out.push_str("[dependencies]\n");
if !deps.is_empty() { if !deps.is_empty() {
let mut keys: Vec<&String> = deps.keys().collect(); let mut keys: Vec<&String> = deps.keys().collect();

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::Once;
use std::sync::atomic::{AtomicUsize, Ordering};
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");
}