mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 16:19:37 +00:00
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
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
2
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
1
TODO.md
1
TODO.md
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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`).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
159
src/core/builder/gas.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
201
src/core/builder/nasm.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
94
tests/cli_basic.rs
Normal 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");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user