diff --git a/CHANGELOG.md b/CHANGELOG.md index 31c19a7..2f04a22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 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 Added: - Incremental builds (object caching by mtime) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7db336f..1f6fd39 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,7 @@ Before PR, run: cargo fmt --all cargo clippy --all-targets --all-features -- -D warnings cargo check +cargo test ``` ## CI/CD and Releases diff --git a/Cargo.lock b/Cargo.lock index 807c344..280bee9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,7 +90,7 @@ dependencies = [ [[package]] name = "dcr" -version = "0.2.5" +version = "0.2.6" dependencies = [ "reqwest", "self-replace", diff --git a/Cargo.toml b/Cargo.toml index 7763d24..5bfe965 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dcr" -version = "0.2.5" +version = "0.2.6" edition = "2024" build = "build.rs" diff --git a/README.md b/README.md index 9f04939..a5301fb 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ The current implementation is written in Rust. - Run the compiled binary - Clean build artifacts - 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) ## Supported Platforms @@ -119,8 +121,10 @@ Two profiles are supported: 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 set `build.platform` to pass `-march=` (GCC/Clang) or `/arch:*` (MSVC) where supported. 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 The main project file is `dcr.toml`. @@ -136,6 +140,8 @@ version = "0.1.0" language = "c" standard = "c11" compiler = "clang" +# Optional platform hint +# platform = "x86_64" # Optional custom flags cflags = ["-Wall", "-Wextra"] ldflags = ["-lm"] diff --git a/TODO.md b/TODO.md index 80d8b2b..fc3c927 100644 --- a/TODO.md +++ b/TODO.md @@ -2,7 +2,6 @@ - Add header dependency tracking for incremental builds (GCC/Clang: -MMD, MSVC: /showIncludes) - Add external package managers (Conan, vcpkg) -- Object library - Multi-target - ASM support - Linker script + direct linker control diff --git a/docs/README.md b/docs/README.md index 8e5b8d7..c6a0642 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,10 +17,13 @@ Current DCR focuses on a simple, predictable workflow: - C: `*.c` - C++: `*.cpp`, `*.cxx`, `*.cc` - 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) - `staticlib` + - `sharedlib` - 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. ## Important behavior notes diff --git a/docs/commands/dcr-build.md b/docs/commands/dcr-build.md index fe2c852..9cbdaf3 100644 --- a/docs/commands/dcr-build.md +++ b/docs/commands/dcr-build.md @@ -17,7 +17,7 @@ dcr build --release 3. Resolves path dependencies from `[dependencies]`. 4. Creates required output directories. 5. Recursively compiles sources from `src/`. -6. Links final artifact (binary or static library). +6. Links final artifact (binary or library). ## Config values used @@ -27,6 +27,7 @@ dcr build --release - `build.standard` - `build.kind` - `build.target` +- `build.platform` - `build.cflags` - `build.ldflags` @@ -34,9 +35,11 @@ dcr build --release - `language = "c"` -> `*.c` - `language = "c++" | "cpp" | "cxx"` -> `*.cpp`, `*.cxx`, `*.cc` +- `language = "asm"` -> `*.s`, `*.S`, `*.asm` ## Notes - The profile is selected from the first argument only. - Unknown profile flags return an error. - 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`. diff --git a/docs/commands/dcr-run.md b/docs/commands/dcr-run.md index 89ebfa8..842dccc 100644 --- a/docs/commands/dcr-run.md +++ b/docs/commands/dcr-run.md @@ -19,7 +19,7 @@ dcr run --release ## 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. ## On build failure diff --git a/docs/configuration/build-profiles.md b/docs/configuration/build-profiles.md index 96eb107..fcb9b0f 100644 --- a/docs/configuration/build-profiles.md +++ b/docs/configuration/build-profiles.md @@ -33,7 +33,25 @@ By default: - `target/debug/` (or `.exe` on Windows) - `target/release/` (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"`): - Linux/macOS: `lib.a` - Windows: `.lib` + +Shared library mode (`kind = "sharedlib"`): + +- Linux: `lib.so` +- macOS: `lib.dylib` +- Windows: `.dll` diff --git a/docs/configuration/build-section.md b/docs/configuration/build-section.md index 08e9617..eda81f9 100644 --- a/docs/configuration/build-section.md +++ b/docs/configuration/build-section.md @@ -10,17 +10,19 @@ compiler = "clang" kind = "bin" # optional: # target = "./dist" +# platform = "x86_64" # cflags = ["-Wall"] # ldflags = ["-lm"] ``` ## 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. - `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. +- `platform` (string, optional): architecture hint for compiler (used for `-march` or `/arch`). - `cflags` (string array, optional): extra compile 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`). - 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`). diff --git a/docs/configuration/dcr.toml-overview.md b/docs/configuration/dcr.toml-overview.md index bafa7f8..0383ec5 100644 --- a/docs/configuration/dcr.toml-overview.md +++ b/docs/configuration/dcr.toml-overview.md @@ -35,7 +35,7 @@ kind = "bin" - `build.compiler` - `build.kind` -`build.kind` must be either `bin` or `staticlib`. +`build.kind` must be either `bin`, `staticlib`, or `sharedlib`. ## Optional keys diff --git a/docs/dependencies-and-build/compilers-and-linking.md b/docs/dependencies-and-build/compilers-and-linking.md index 592510a..c07a989 100644 --- a/docs/dependencies-and-build/compilers-and-linking.md +++ b/docs/dependencies-and-build/compilers-and-linking.md @@ -5,9 +5,11 @@ DCR picks a backend from `build.compiler`: - contains `clang-cl` -> MSVC backend +- equals `as` or contains `gas` -> GAS backend (standalone assembler) - contains `gcc` or `g++` -> GCC backend - contains `clang` or `clang++` -> Clang backend - equals `cl` or contains `msvc` -> MSVC backend +- contains `nasm` -> NASM backend - otherwise -> Clang backend fallback ## Compilation model @@ -16,6 +18,27 @@ DCR picks a backend from `build.compiler`: - Each source is compiled to `target//obj/.o` (`.obj` on MSVC). - 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=` +- MSVC: maps to `/arch:*` for known values (`x86`, `i386`, `i486`, `i586`, `i686`, `sse2`, `avx`, `avx2`) + ## Linking For `kind = "bin"`: @@ -28,3 +51,9 @@ For `kind = "staticlib"`: - Linux/macOS (GCC/Clang): `ar rcs` creates `.a` 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`. diff --git a/docs/getting-started/build-and-run.md b/docs/getting-started/build-and-run.md index bb4b6fa..ae2c361 100644 --- a/docs/getting-started/build-and-run.md +++ b/docs/getting-started/build-and-run.md @@ -41,6 +41,6 @@ dcr clean --release ## 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. - Unknown profile flags produce an error. diff --git a/docs/getting-started/project-structure.md b/docs/getting-started/project-structure.md index 0550d99..a077cc5 100644 --- a/docs/getting-started/project-structure.md +++ b/docs/getting-started/project-structure.md @@ -35,5 +35,6 @@ project/ - `target//deps/`: synchronized copies of path dependencies. - `target//` (or `.exe` on Windows): output for `kind = "bin"`. - `target//lib.a` (or `.lib` on Windows): output for `kind = "staticlib"`. +- `target//lib.so` (or `.dylib` on macOS, `.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//obj`. diff --git a/docs/reference-and-help/config-reference.md b/docs/reference-and-help/config-reference.md index 5458d77..604e4b1 100644 --- a/docs/reference-and-help/config-reference.md +++ b/docs/reference-and-help/config-reference.md @@ -8,11 +8,12 @@ name = "string" # required version = "string" # required [build] -language = "c|c++|cpp|cxx" # required +language = "c|c++|cpp|cxx|asm" # required standard = "string" # required compiler = "string" # required -kind = "bin|staticlib" # required +kind = "bin|staticlib|sharedlib" # required target = "string" # optional +platform = "string" # optional cflags = ["string", "..."] # optional ldflags = ["string", "..."] # optional @@ -29,7 +30,7 @@ name = { - `[package]`, `[build]`, `[dependencies]` must exist. - 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. ## Generated defaults (`dcr new` / `dcr init`) diff --git a/src/cli/build.rs b/src/cli/build.rs index f5e852d..fec8b69 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -32,6 +32,7 @@ pub fn build(args: &[String]) -> i32 { let build_standard = get_config_str(&config, "build.standard"); let build_target = get_config_str(&config, "build.target"); let build_kind = get_config_str(&config, "build.kind"); + let build_platform = get_config_str(&config, "build.platform"); let build_cflags = match get_config_list(&config, "build.cflags") { Ok(v) => v, Err(msg) => { @@ -78,6 +79,7 @@ pub fn build(args: &[String]) -> i32 { standard: &build_standard, target_dir: normalize_target(&build_target), kind: normalize_kind(&build_kind), + platform: normalize_platform(&build_platform), include_dirs: &resolved.include_dirs, lib_dirs: &resolved.lib_dirs, libs: &resolved.libs, @@ -184,3 +186,8 @@ fn normalize_kind(kind: &str) -> &str { let trimmed = kind.trim(); 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) } +} diff --git a/src/cli/run.rs b/src/cli/run.rs index 739c899..5b1fe31 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -51,8 +51,9 @@ pub fn run(args: &[String]) -> i32 { } } - if build_kind.trim() == "staticlib" { - error("Cannot run static library"); + let kind = build_kind.trim(); + if kind == "staticlib" || kind == "sharedlib" { + error("Cannot run library build"); return 1; } let build_status = build(args); diff --git a/src/core/builder/clang.rs b/src/core/builder/clang.rs index 30e23b2..c80333b 100644 --- a/src/core/builder/clang.rs +++ b/src/core/builder/clang.rs @@ -34,6 +34,13 @@ pub fn build(ctx: &BuildContext) -> Result { } 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 { cmd.arg(obj); } @@ -52,11 +59,12 @@ pub fn build(ctx: &BuildContext) -> Result { for flag in ctx.ldflags { cmd.arg(flag); } - cmd.arg("-o").arg(platform::bin_path( - ctx.profile, - ctx.project_name, - ctx.target_dir, - )); + 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() => { @@ -99,7 +107,8 @@ fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec) -> Result<( let file = path.to_string_lossy().to_string(); let allowed = (lang == "c" && ext == "c") || ((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 { out.push(file); } @@ -138,7 +147,15 @@ fn build_objects( if needs_rebuild(source, &obj_path) { let mut cmd = Command::new(compiler); 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)); } for flag in default_flags(ctx.profile) { diff --git a/src/core/builder/gas.rs b/src/core/builder/gas.rs new file mode 100644 index 0000000..7c56045 --- /dev/null +++ b/src/core/builder/gas.rs @@ -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 { + 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, 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) -> 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, 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, + } +} diff --git a/src/core/builder/gcc.rs b/src/core/builder/gcc.rs index 30e23b2..c80333b 100644 --- a/src/core/builder/gcc.rs +++ b/src/core/builder/gcc.rs @@ -34,6 +34,13 @@ pub fn build(ctx: &BuildContext) -> Result { } 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 { cmd.arg(obj); } @@ -52,11 +59,12 @@ pub fn build(ctx: &BuildContext) -> Result { for flag in ctx.ldflags { cmd.arg(flag); } - cmd.arg("-o").arg(platform::bin_path( - ctx.profile, - ctx.project_name, - ctx.target_dir, - )); + 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() => { @@ -99,7 +107,8 @@ fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec) -> Result<( let file = path.to_string_lossy().to_string(); let allowed = (lang == "c" && ext == "c") || ((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 { out.push(file); } @@ -138,7 +147,15 @@ fn build_objects( if needs_rebuild(source, &obj_path) { let mut cmd = Command::new(compiler); 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)); } for flag in default_flags(ctx.profile) { diff --git a/src/core/builder/mod.rs b/src/core/builder/mod.rs index c7ebdf7..fb777bc 100644 --- a/src/core/builder/mod.rs +++ b/src/core/builder/mod.rs @@ -1,6 +1,8 @@ pub mod clang; pub mod gcc; +pub mod gas; pub mod msvc; +pub mod nasm; pub struct BuildContext<'a> { pub profile: &'a str, @@ -10,6 +12,7 @@ pub struct BuildContext<'a> { pub standard: &'a str, pub target_dir: Option<&'a str>, pub kind: &'a str, + pub platform: Option<&'a str>, pub include_dirs: &'a [String], pub lib_dirs: &'a [String], pub libs: &'a [String], @@ -22,6 +25,12 @@ pub fn build(ctx: &BuildContext) -> Result { if compiler.contains("clang-cl") { 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++") { return gcc::build(ctx); } diff --git a/src/core/builder/msvc.rs b/src/core/builder/msvc.rs index 0aa1117..199a847 100644 --- a/src/core/builder/msvc.rs +++ b/src/core/builder/msvc.rs @@ -11,6 +11,9 @@ pub fn build(ctx: &BuildContext) -> Result { } else { 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 sources = collect_sources(ctx.language)?; let obj_dir = Path::new("./target").join(ctx.profile).join("obj"); @@ -35,6 +38,9 @@ pub fn build(ctx: &BuildContext) -> Result { let mut cmd = Command::new(compiler); cmd.arg("/nologo"); + if ctx.kind == "sharedlib" { + cmd.arg("/LD"); + } match ctx.language.to_lowercase().as_str() { "c" => { cmd.arg("/TC"); @@ -77,10 +83,12 @@ pub fn build(ctx: &BuildContext) -> Result { cmd.arg(flag); } } - cmd.arg(format!( - "/Fe:{}", + 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(format!("/Fe:{out_path}")); match cmd.status() { Ok(status) if status.success() => { @@ -123,7 +131,8 @@ fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec) -> Result<( let file = path.to_string_lossy().to_string(); let allowed = (lang == "c" && ext == "c") || ((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 { out.push(file); } @@ -154,6 +163,23 @@ fn msvc_standard_flag(language: &str, standard: &str) -> Result 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] { match profile { "release" => &["/O2", "/DNDEBUG"], @@ -189,10 +215,13 @@ fn build_objects( 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)?; cmd.arg(std_flag); } + if let Some(flag) = msvc_arch_flag(ctx.platform) { + cmd.arg(flag); + } for flag in default_flags(ctx.profile) { cmd.arg(flag); } diff --git a/src/core/builder/nasm.rs b/src/core/builder/nasm.rs new file mode 100644 index 0000000..b85c523 --- /dev/null +++ b/src/core/builder/nasm.rs @@ -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 { + 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, 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) -> 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, 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, + } +} diff --git a/src/core/config.rs b/src/core/config.rs index 95ded8e..bfab2ec 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -121,9 +121,14 @@ impl Config { 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()) { 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())); } } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 250339b..74039a2 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -11,3 +11,10 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { 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"), + } +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 250339b..339529d 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -11,3 +11,10 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { 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"), + } +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 25a1b8a..c082f79 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -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"), + } + } +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index c7f0945..71707aa 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -11,3 +11,10 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { 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"), + } +} diff --git a/tests/cli_basic.rs b/tests/cli_basic.rs new file mode 100644 index 0000000..bab020b --- /dev/null +++ b/tests/cli_basic.rs @@ -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"); +}