From 7969209eae50eb435577f0ee86e7a1ae941dc997 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Tue, 26 May 2026 16:38:23 +0800 Subject: [PATCH] Added: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `build.kind = "efi"` — UEFI PE32+ executable support. Links with `-shared -nostdlib -Wl,-dll -Wl,--subsystem,10`, output via `efi_path()` with `.efi` extension, rejected by `dcr run`. - `build.kind = "elf"` — bare-metal ELF executable support. Uses `elf_path()` output path, rejected by `dcr run`, no extra linker flags. Fixed: - `dcr run` with `--target` or `build.target` now correctly finds the binary in the arch-specific target directory. - `dcr run` checks profile-specific `build.{profile}.kind` before rejecting library builds (previously only checked `build.kind`). - `dcr clean` now reads `build.target` from `dcr.toml` when no `--target` flag is given. - Linux: target directory is now consistent — `target/-unknown-linux-gnu//` (no stale `target//`). - `dcr.lock` is now populated with all resolved dependencies (registry, path, git) instead of being always empty. - Git dependencies are now properly recognized and recorded in `dcr.lock`. - `is_registry_dep` correctly identifies version-based registry dependency tables (`{ version = "..." }`) and rejects tables with unknown keys. - `dcr test` accepts `--debug`/`--release` flags (defaults to `debug` profile instead of always `release`). - Build fingerprint cache (`build_cache_path`) now includes `target_dir` — different targets no longer share a single cache entry. - `object_path` no longer hardcodes the `src/` prefix — works correctly with custom `build.roots` (e.g. `roots = ["lib"]`). - `dcr update` now generates `.exe` candidates for all Windows targets, not just `x86_64-pc-windows-msvc`. - Compiler existence is verified before starting the build (clear error if compiler is not found in PATH). - Consolidated duplicate `OUTPUT_MUTEX` definitions — MSVC backend now uses the shared mutex from `common.rs`. - Registry cache root directory is created if missing (prevents cryptic errors when `~/.dcr/` does not exist). --- CHANGELOG.md | 18 +++++++- README.md | 8 ++-- docs/commands/dcr-build.md | 4 +- docs/commands/dcr-clean.md | 2 +- docs/commands/dcr-run.md | 2 +- docs/commands/dcr-test.md | 3 +- docs/configuration/build-section.md | 4 +- docs/getting-started/build-and-run.md | 2 +- src/cli/build.rs | 23 +++++++--- src/cli/clean.rs | 30 ++++++++----- src/cli/flag_update.rs | 4 +- src/cli/run.rs | 31 ++++++++----- src/cli/test.rs | 16 ++++--- src/core/builder/common.rs | 18 ++++++-- src/core/builder/gas.rs | 7 ++- src/core/builder/mod.rs | 14 ++++++ src/core/builder/msvc.rs | 11 +++-- src/core/builder/nasm.rs | 7 ++- src/core/builder/unix_cc.rs | 15 ++++++- src/core/config.rs | 19 ++++---- src/core/deps/mod.rs | 65 ++++++++++++++++++++++++++- src/core/deps/register.rs | 30 +++++++++---- src/platform/linux.rs | 48 +++++++++++++++----- src/platform/macos.rs | 14 ++++++ src/platform/mod.rs | 44 ++++++++++++++++++ src/platform/windows.rs | 14 ++++++ 26 files changed, 368 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f34f3e2..455c7b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ # Changelog -## [0.6.7] - 2026-05-21 +## [0.6.7] - 2026-05-26 Added: +- `build.kind = "efi"` — UEFI PE32+ executable support. Links with `-shared -nostdlib -Wl,-dll -Wl,--subsystem,10`, output via `efi_path()` with `.efi` extension, rejected by `dcr run`. +- `build.kind = "elf"` — bare-metal ELF executable support. Uses `elf_path()` output path, rejected by `dcr run`, no extra linker flags. - `build.filename` and `build.extension` in `dcr.toml` — complete control over the final artifact name without relying on `package.name`. Example: ```toml @@ -21,6 +23,20 @@ Added: Fixed: - `build.target` declared in `dcr.toml` is now correctly used as the default target even when `--target` is not passed on the command line (previously the host target was always forced for config resolution). +- `dcr run` with `--target` or `build.target` now correctly finds the binary in the arch-specific target directory. +- `dcr run` checks profile-specific `build.{profile}.kind` before rejecting library builds (previously only checked `build.kind`). +- `dcr clean` now reads `build.target` from `dcr.toml` when no `--target` flag is given. +- Linux: target directory is now consistent — `target/-unknown-linux-gnu//` (no stale `target//`). +- `dcr.lock` is now populated with all resolved dependencies (registry, path, git) instead of being always empty. +- Git dependencies are now properly recognized and recorded in `dcr.lock`. +- `is_registry_dep` correctly identifies version-based registry dependency tables (`{ version = "..." }`) and rejects tables with unknown keys. +- `dcr test` accepts `--debug`/`--release` flags (defaults to `debug` profile instead of always `release`). +- Build fingerprint cache (`build_cache_path`) now includes `target_dir` — different targets no longer share a single cache entry. +- `object_path` no longer hardcodes the `src/` prefix — works correctly with custom `build.roots` (e.g. `roots = ["lib"]`). +- `dcr update` now generates `.exe` candidates for all Windows targets, not just `x86_64-pc-windows-msvc`. +- Compiler existence is verified before starting the build (clear error if compiler is not found in PATH). +- Consolidated duplicate `OUTPUT_MUTEX` definitions — MSVC backend now uses the shared mutex from `common.rs`. +- Registry cache root directory is created if missing (prevents cryptic errors when `~/.dcr/` does not exist). ## [0.6.6] - 2026-05-18 diff --git a/README.md b/README.md index 58f14e9..ca201c9 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The current implementation is written in Rust. - Clean build artifacts - Generate IDE integration files (VS Code, CLion) and compilation databases - Generate a minimal C project template -- Build static and shared libraries +- Build static, shared libraries, UEFI PE32+ executables, and bare-metal ELF executables - Dependency management: - Git and Path dependencies with `dcr.lock` - Registry-based dependency resolution @@ -154,7 +154,9 @@ You can also set `build.target` to override the output directory (profile-indepe 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. 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. +Use `build.kind = "efi"` to build a UEFI PE32+ executable (`.efi`). +Use `build.kind = "elf"` to build a bare-metal ELF executable. +`dcr run` is only for `build.kind = "bin"` and will fail for libraries, EFI, and ELF. Use `build.exclude`/`build.include` to control source/header collection. Use `build.roots` and `build.src_disable` to replace the default `src/` root. Use `build.steps` and `build.post_steps` to run custom commands. @@ -203,7 +205,7 @@ type = "app" # or "lib", "none" language = "c" standard = "c11" compiler = "clang" -kind = "bin" # "bin", "staticlib", "sharedlib" +kind = "bin" # "bin", "staticlib", "sharedlib", "efi", "elf" # Optional platform hint # platform = "x86_64" # Optional custom flags diff --git a/docs/commands/dcr-build.md b/docs/commands/dcr-build.md index d712c26..d812bd8 100644 --- a/docs/commands/dcr-build.md +++ b/docs/commands/dcr-build.md @@ -64,7 +64,9 @@ dcr build --debug --clean - Incremental rebuild checks source/object mtime and tracked header dependencies. - `--force` skips build cache checks and recompiles. - `--clean` removes `target/` and `build.clean` paths before building. -- `build.filename` + `build.extension` allow full control over the final artifact name (e.g. `KERNEL.BIN` instead of using `package.name`). +- `build.filename` + `build.extension` allow full control over the final artifact name (e.g. `KERNEL.BIN` instead of using `package.name`). When `build.kind = "efi"`, the output is a PE32+ executable with `.efi` extension. +- `build.kind = "efi"` links with `-shared -nostdlib -Wl,-dll -Wl,--subsystem,10` for UEFI PE32+ format. Use `build.filename` and `build.extension` for full control over the artifact name. +- `build.kind = "elf"` produces a bare-metal ELF executable. Linking flags are user-controlled via `build.ldflags` (no PIE/CRT defaults are added). - Default GCC/Clang profile flags: `debug` -> `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG`, `release` -> `-O3 -DNDEBUG`. These defaults are **not applied** for bare-metal targets (e.g. `aarch64-none-elf`, targets containing `none` / `-elf` / `eabi`). - For `language = "asm"` with `compiler = "as"`/`"gas"`, use `.s` files (no preprocessing). For `.S`, use `gcc` or `clang`. diff --git a/docs/commands/dcr-clean.md b/docs/commands/dcr-clean.md index dce8d8f..81e8254 100644 --- a/docs/commands/dcr-clean.md +++ b/docs/commands/dcr-clean.md @@ -15,7 +15,7 @@ dcr clean --release --all ## Behavior - Without arguments: removes entire `target/`. -- With profile: removes only `target//`. +- With profile: removes only the profile-specific directory. Respects `build.target` from config: if `build.target = "aarch64-unknown-linux-gnu"`, cleans `target/aarch64-unknown-linux-gnu//`. - With `--all`: also cleans all workspace members. - Additionally removes paths from `build.clean` if configured. - `build.clean` supports `{profile}` and version placeholders. diff --git a/docs/commands/dcr-run.md b/docs/commands/dcr-run.md index b5f09f7..f5902db 100644 --- a/docs/commands/dcr-run.md +++ b/docs/commands/dcr-run.md @@ -23,7 +23,7 @@ dcr run --debug --clean ## Restrictions -- If `build.kind = "staticlib"` or `build.kind = "sharedlib"`, `run` exits with error. +- If `build.kind` (or `build.{profile}.kind`) is `"staticlib"` or `"sharedlib"`, `run` exits with error. - The profile is selected from the first argument. - `--target ` runs the binary for specified target (short names: `linux`, `macos`, `windows`). - `--force` skips build cache checks and recompiles. diff --git a/docs/commands/dcr-test.md b/docs/commands/dcr-test.md index 01d9011..19766c1 100644 --- a/docs/commands/dcr-test.md +++ b/docs/commands/dcr-test.md @@ -6,6 +6,7 @@ Runs the project's test suite. ```sh dcr test +dcr test --debug dcr test --release dcr test --target linux dcr test --init @@ -39,6 +40,6 @@ DCR uses a simple test framework: - Test binary is linked with the main project (for lib projects) or standalone. - `--init` creates `tests/dcr_test.h` and `tests/test.c` if missing. -- Tests run in release mode by default for performance. +- Tests run in debug mode by default. Use `--release` for release mode. - Custom test logic can be added by editing `tests/test.c`. - Fails if no `tests/test.c` exists (run `dcr test --init` first). \ No newline at end of file diff --git a/docs/configuration/build-section.md b/docs/configuration/build-section.md index a556406..dd5f03e 100644 --- a/docs/configuration/build-section.md +++ b/docs/configuration/build-section.md @@ -33,7 +33,7 @@ kind = "bin" Mixed languages can be specified as an array, for example `["c", "c++", "asm"]`. - `standard` (string, required): language standard passed to compiler. - `compiler` (string, required): compiler command (for example `clang`, `gcc`, `cl`). -- `kind` (string, required): `bin`, `staticlib`, or `sharedlib`. +- `kind` (string, required): `bin`, `staticlib`, `sharedlib`, `efi`, or `elf`. - `filename` (string, optional): custom output filename (without extension). Overrides `package.name` for the final artifact. - `extension` (string, optional): custom file extension without the leading dot (e.g. `BIN`, `efi`, `EFI`). Combined with `filename`. - `inherit` (bool, optional): inherit settings from base `[build]` (default `true`). If `false`, only use target/profile specific settings. @@ -151,4 +151,4 @@ rcc = "rcc" - 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 library kinds (`staticlib`, `sharedlib`). +- `run` is not allowed for library kinds (`staticlib`, `sharedlib`), `efi`, or `elf`. diff --git a/docs/getting-started/build-and-run.md b/docs/getting-started/build-and-run.md index 470a255..731cf68 100644 --- a/docs/getting-started/build-and-run.md +++ b/docs/getting-started/build-and-run.md @@ -49,7 +49,7 @@ dcr clean --release --all ## Notes -- `run` is unavailable for `build.kind = "staticlib"` and `build.kind = "sharedlib"`. +- `run` is unavailable for `build.kind = "staticlib"`, `build.kind = "sharedlib"`, `build.kind = "efi"`, and `build.kind = "elf"`. - Build profile flag (`--debug` / `--release`) can be passed in any argument position. - Unknown profile flags produce an error. - In workspace root, `clean --all` cleans all member projects. diff --git a/src/cli/build.rs b/src/cli/build.rs index 3a1f23c..c190b6d 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -355,9 +355,15 @@ fn ensure_target_dirs(items: &[String], profile: &str, target_dir: Option Result<(), } fn build_cache_path(profile: &str, target_dir: Option<&str>) -> std::path::PathBuf { - let _ = target_dir; - Path::new("./target").join(profile).join(".dcr-build.hash") + match target_dir { + Some(dir) => Path::new(dir).join(".dcr-build.hash"), + None => Path::new("./target").join(profile).join(".dcr-build.hash"), + } } fn build_output_path(ctx: &BuildContext) -> String { @@ -1332,12 +1340,17 @@ fn build_output_path(ctx: &BuildContext) -> String { }; if ctx.kind == "staticlib" { - // Для библиотек добавляем lib префикс если нужно, но упростим return crate::platform::lib_path(ctx.profile, &final_name, ctx.target_dir); } if ctx.kind == "sharedlib" { return crate::platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir); } + if ctx.kind == "efi" { + return crate::platform::efi_path(ctx.profile, &final_name, ctx.target_dir); + } + if ctx.kind == "elf" { + return crate::platform::elf_path(ctx.profile, &final_name, ctx.target_dir); + } crate::platform::bin_path(ctx.profile, &final_name, ctx.target_dir) } diff --git a/src/cli/clean.rs b/src/cli/clean.rs index b86f589..43e772e 100644 --- a/src/cli/clean.rs +++ b/src/cli/clean.rs @@ -91,17 +91,27 @@ fn parse_clean_flags(args: &[String]) -> Result { fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> { let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?; - let target = flags.target.clone().or_else(|| { - Some(if cfg!(target_os = "linux") { - "x86_64-unknown-linux-gnu".to_string() - } else if cfg!(target_os = "macos") { - "x86_64-apple-darwin".to_string() - } else if cfg!(target_os = "windows") { - "x86_64-pc-windows-msvc".to_string() - } else { - "unknown".to_string() + let target = flags + .target + .clone() + .or_else(|| { + config + .get("build.target") + .and_then(|v| v.as_str()) + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) }) - }); + .or_else(|| { + Some(if cfg!(target_os = "linux") { + "x86_64-unknown-linux-gnu".to_string() + } else if cfg!(target_os = "macos") { + "x86_64-apple-darwin".to_string() + } else if cfg!(target_os = "windows") { + "x86_64-pc-windows-msvc".to_string() + } else { + "unknown".to_string() + }) + }); if flags.all && let Some(workspace) = parse_workspace( diff --git a/src/cli/flag_update.rs b/src/cli/flag_update.rs index 1111f4b..8c2e986 100644 --- a/src/cli/flag_update.rs +++ b/src/cli/flag_update.rs @@ -138,8 +138,8 @@ fn download_asset(client: &Client, url: &str) -> Result, String> { fn asset_candidates(target: &str) -> Vec { let mut names = vec![format!("dcr-{target}")]; - if target == "x86_64-pc-windows-msvc" { - names.push("dcr-x86_64-pc-windows-msvc.exe".to_string()); + if target.contains("-windows-") || target.ends_with("-windows") { + names.push(format!("dcr-{target}.exe")); } names diff --git a/src/cli/run.rs b/src/cli/run.rs index 20a23bc..3a23970 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -69,15 +69,6 @@ pub fn run(args: &[String]) -> i32 { .get("package.name") .and_then(|v| v.as_str()) .unwrap_or(""); - let build_kind = config - .get("build.kind") - .and_then(|v| v.as_str()) - .unwrap_or(""); - let target_dir = config - .get("build.target") - .and_then(|v| v.as_str()) - .map(|s| s.trim()) - .filter(|s| !s.is_empty()); let mut flags = match parse_build_run_flags(args) { Ok(v) => v, @@ -98,6 +89,12 @@ pub fn run(args: &[String]) -> i32 { flags.target = Some(default_target.to_string()); } + let build_kind = config + .get(&format!("build.{}.kind", flags.profile)) + .and_then(|v| v.as_str()) + .or_else(|| config.get("build.kind").and_then(|v| v.as_str())) + .unwrap_or(""); + let normalized_target_dir = flags .target .as_ref() @@ -110,7 +107,9 @@ pub fn run(args: &[String]) -> i32 { let run_cmd = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), version); let kind = build_kind.trim(); - if run_cmd.is_none() && (kind == "staticlib" || kind == "sharedlib") { + if run_cmd.is_none() + && (kind == "staticlib" || kind == "sharedlib" || kind == "efi" || kind == "elf") + { error("Cannot run library build"); return 1; } @@ -128,13 +127,21 @@ pub fn run(args: &[String]) -> i32 { } println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path); println!("--------------------------------"); - return run_binary(project_name, &flags.profile, target_dir); + return run_binary( + project_name, + &flags.profile, + normalized_target_dir.as_deref(), + ); } let fallback_code = if let Some(cmd) = run_cmd { run_shell(&cmd) } else { - run_binary(project_name, &flags.profile, target_dir) + run_binary( + project_name, + &flags.profile, + normalized_target_dir.as_deref(), + ) }; if fallback_code != 1 { return fallback_code; diff --git a/src/cli/test.rs b/src/cli/test.rs index 127424e..83855fa 100644 --- a/src/cli/test.rs +++ b/src/cli/test.rs @@ -16,10 +16,11 @@ const BOLD_BLUE: &str = "\x1b[1m\x1b[94m"; pub fn test(args: &[String]) -> i32 { let mut init_header = false; + let mut profile = "debug"; for arg in args { if arg == "--help" { println!("USAGE:"); - println!(" dcr test [--init]"); + println!(" dcr test [--init] [--debug | --release]"); println!(); println!("ALIASES:"); println!(" dcr tests"); @@ -29,12 +30,18 @@ pub fn test(args: &[String]) -> i32 { println!(); println!("OPTIONS:"); println!(" --init Create tests/dcr_test.h in current project"); + println!(" --debug Build and run tests with debug profile (default)"); + println!(" --release Build and run tests with release profile"); return 0; } if arg == "--init" { init_header = true; continue; } + if arg == "--debug" || arg == "--release" { + profile = arg.trim_start_matches("--"); + continue; + } error("Unknown argument"); return 1; } @@ -68,7 +75,7 @@ pub fn test(args: &[String]) -> i32 { } } - match with_dir(&root, run_testsuite) { + match with_dir(&root, || run_testsuite(profile)) { Ok(code) => code, Err(msg) => { error(&msg); @@ -77,13 +84,12 @@ pub fn test(args: &[String]) -> i32 { } } -fn run_testsuite() -> Result { - if build::build(&[String::from("--release")]) != 0 { +fn run_testsuite(profile: &str) -> Result { + if build::build(&[format!("--{profile}")]) != 0 { return Ok(1); } let config = Config::open("./dcr.toml").map_err(|_| "Failed to read dcr.toml".to_string())?; - let profile = "release"; let name = get_config_str(&config, "package.name"); let language = get_language_with_profile(&config, profile)?; let compiler_cfg = get_string_with_profile(&config, "compiler", profile); diff --git a/src/core/builder/common.rs b/src/core/builder/common.rs index 0575e0d..b0dd391 100644 --- a/src/core/builder/common.rs +++ b/src/core/builder/common.rs @@ -5,6 +5,10 @@ use std::sync::{Mutex, OnceLock}; static OUTPUT_MUTEX: OnceLock> = OnceLock::new(); +pub fn get_output_lock() -> &'static Mutex<()> { + OUTPUT_MUTEX.get_or_init(|| Mutex::new(())) +} + pub fn collect_sources( roots: &[PathBuf], extensions: &[&str], @@ -230,10 +234,16 @@ pub fn normalize_source_path(path: &Path) -> String { pub 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 stripped = src_path.strip_prefix("./").unwrap_or(src_path); + let rel = stripped + .components() + .skip(1) + .collect::(); + let rel = if rel.components().next().is_some() { + rel + } else { + stripped.to_path_buf() + }; let mut out = obj_dir.join(rel); out.set_extension(obj_ext.trim_start_matches('.')); out.to_string_lossy().to_string() diff --git a/src/core/builder/gas.rs b/src/core/builder/gas.rs index f120187..bb25249 100644 --- a/src/core/builder/gas.rs +++ b/src/core/builder/gas.rs @@ -18,7 +18,10 @@ pub fn build(ctx: &BuildContext) -> Result { }; let start_time = Instant::now(); let sources = collect_sources(ctx)?; - let obj_dir = Path::new("./target").join(ctx.profile).join("obj"); + let obj_dir = match ctx.target_dir { + Some(dir) => Path::new(dir).join("obj"), + None => Path::new("./target").join(ctx.profile).join("obj"), + }; let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?; if ctx.kind == "staticlib" { @@ -80,6 +83,8 @@ pub fn build(ctx: &BuildContext) -> Result { let out_path = if ctx.kind == "sharedlib" { platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir) + } else if ctx.kind == "elf" { + platform::elf_path(ctx.profile, &final_name, ctx.target_dir) } else { platform::bin_path(ctx.profile, &final_name, ctx.target_dir) }; diff --git a/src/core/builder/mod.rs b/src/core/builder/mod.rs index fa7e446..4a5d1f9 100644 --- a/src/core/builder/mod.rs +++ b/src/core/builder/mod.rs @@ -33,6 +33,12 @@ pub struct BuildContext<'a> { pub fn build(ctx: &BuildContext) -> Result { let compiler = ctx.compiler.to_lowercase(); + if !check_compiler_exists(ctx.compiler) { + return Err(format!( + "Compiler not found: {}. Make sure it is installed and available in PATH.", + ctx.compiler + )); + } if compiler.contains("clang-cl") { return msvc::build(ctx); } @@ -48,6 +54,14 @@ pub fn build(ctx: &BuildContext) -> Result { unix_cc::build(ctx) } +fn check_compiler_exists(compiler: &str) -> bool { + let name = if compiler.is_empty() { "cc" } else { compiler }; + std::process::Command::new(name) + .arg("--version") + .output() + .is_ok() +} + pub fn collect_sources(ctx: &BuildContext) -> Result, String> { let compiler = ctx.compiler.to_lowercase(); if compiler.contains("clang-cl") || compiler == "cl" || compiler.contains("msvc") { diff --git a/src/core/builder/msvc.rs b/src/core/builder/msvc.rs index 49b3c96..3cca499 100644 --- a/src/core/builder/msvc.rs +++ b/src/core/builder/msvc.rs @@ -18,7 +18,10 @@ pub fn build(ctx: &BuildContext) -> Result { } let start_time = Instant::now(); let sources = collect_sources(ctx)?; - let obj_dir = Path::new("./target").join(ctx.profile).join("obj"); + let obj_dir = match ctx.target_dir { + Some(dir) => Path::new(dir).join("obj"), + None => Path::new("./target").join(ctx.profile).join("obj"), + }; let objects = build_objects(compiler, &sources, &obj_dir, ctx, "obj")?; if ctx.kind == "staticlib" { @@ -92,6 +95,8 @@ pub fn build(ctx: &BuildContext) -> Result { } let out_path = if ctx.kind == "sharedlib" { platform::shared_lib_path(ctx.profile, ctx.project_name, ctx.target_dir) + } else if ctx.kind == "elf" { + platform::elf_path(ctx.profile, ctx.project_name, ctx.target_dir) } else { platform::bin_path(ctx.profile, ctx.project_name, ctx.target_dir) }; @@ -185,8 +190,6 @@ fn default_flags(profile: &str) -> &'static [&'static str] { } } -static OUTPUT_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); - fn build_objects( compiler: &str, sources: &[String], @@ -282,7 +285,7 @@ fn build_object( } } - let _lock = OUTPUT_MUTEX.lock().unwrap(); + let _lock = common::get_output_lock().lock().unwrap(); if !output.status.success() { eprint!("{}", clean_stdout); diff --git a/src/core/builder/nasm.rs b/src/core/builder/nasm.rs index a44f8c9..35a522f 100644 --- a/src/core/builder/nasm.rs +++ b/src/core/builder/nasm.rs @@ -18,7 +18,10 @@ pub fn build(ctx: &BuildContext) -> Result { }; let start_time = Instant::now(); let sources = collect_sources(ctx)?; - let obj_dir = Path::new("./target").join(ctx.profile).join("obj"); + let obj_dir = match ctx.target_dir { + Some(dir) => Path::new(dir).join("obj"), + None => Path::new("./target").join(ctx.profile).join("obj"), + }; let objects = build_objects(assembler, &sources, &obj_dir, ctx, "o")?; if ctx.kind == "staticlib" { @@ -80,6 +83,8 @@ pub fn build(ctx: &BuildContext) -> Result { let out_path = if ctx.kind == "sharedlib" { platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir) + } else if ctx.kind == "elf" { + platform::elf_path(ctx.profile, &final_name, ctx.target_dir) } else { platform::bin_path(ctx.profile, &final_name, ctx.target_dir) }; diff --git a/src/core/builder/unix_cc.rs b/src/core/builder/unix_cc.rs index 25c04d7..1e9d8db 100644 --- a/src/core/builder/unix_cc.rs +++ b/src/core/builder/unix_cc.rs @@ -20,7 +20,10 @@ pub fn build(ctx: &BuildContext) -> Result { ctx.exclude_dirs, ctx.include_paths, )?; - let obj_dir = Path::new("./target").join(ctx.profile).join("obj"); + let obj_dir = match ctx.target_dir { + Some(dir) => Path::new(dir).join("obj"), + None => Path::new("./target").join(ctx.profile).join("obj"), + }; let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o")?; if ctx.kind == "staticlib" { @@ -51,6 +54,12 @@ pub fn build(ctx: &BuildContext) -> Result { cmd.arg("-shared"); } } + if ctx.kind == "efi" { + cmd.arg("-shared"); + cmd.arg("-nostdlib"); + cmd.arg("-Wl,-dll"); + cmd.arg("-Wl,--subsystem,10"); + } for obj in &objects { cmd.arg(obj); } @@ -73,6 +82,10 @@ pub fn build(ctx: &BuildContext) -> Result { let out_path = if ctx.kind == "sharedlib" { platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir) + } else if ctx.kind == "efi" { + platform::efi_path(ctx.profile, &final_name, ctx.target_dir) + } else if ctx.kind == "elf" { + platform::elf_path(ctx.profile, &final_name, ctx.target_dir) } else { platform::bin_path(ctx.profile, &final_name, ctx.target_dir) }; diff --git a/src/core/config.rs b/src/core/config.rs index 96a0afc..393f17b 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -10,6 +10,7 @@ const DEFAULT_LANGUAGE: &str = "c"; const DEFAULT_STANDARD: &str = "c11"; const DEFAULT_COMPILER: &str = "clang"; const DEFAULT_KIND: &str = "bin"; +const VALID_KINDS: &[&str] = &["bin", "staticlib", "sharedlib", "efi", "elf"]; #[derive(Debug)] pub enum ConfigError { @@ -242,7 +243,7 @@ impl Config { validate_toolchain(self.typed.toolchain.as_ref())?; if let Some(kind) = &self.typed.build.kind { let kind = kind.trim(); - if !kind.is_empty() && kind != "bin" && kind != "staticlib" && kind != "sharedlib" { + if !kind.is_empty() && !VALID_KINDS.contains(&kind) { return Err(ConfigError::Invalid("build.kind is invalid".into())); } } @@ -410,7 +411,7 @@ impl Config { } if let Some(kind) = table.get("kind").and_then(|v| v.as_str()) { let kind = kind.trim(); - if !kind.is_empty() && kind != "bin" && kind != "staticlib" && kind != "sharedlib" { + if !kind.is_empty() && !VALID_KINDS.contains(&kind) { return Err(ConfigError::Invalid(format!( "build.{profile}.kind is invalid" ))); @@ -919,14 +920,16 @@ mod tests { #[test] fn validate_valid_kinds() { - for kind in ["bin", "staticlib", "sharedlib"] { - let dir = temp_dir(&format!("kind_{kind}")); - let content = format!( + for &kind in VALID_KINDS { + let dir = temp_dir("valid_kind"); + let toml = format!( "[package]\nname = \"test\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"{kind}\"\n\n[dependencies]\n" ); - let path = write_toml_file(&dir, &content); - let result = Config::open(&path.to_string_lossy()); - assert!(result.is_ok(), "kind '{kind}' should be valid"); + let path = write_toml_file(&dir, &toml); + assert!( + Config::open(&path.to_string_lossy()).is_ok(), + "kind '{kind}' should be valid" + ); } } diff --git a/src/core/deps/mod.rs b/src/core/deps/mod.rs index 1413b35..d176d27 100644 --- a/src/core/deps/mod.rs +++ b/src/core/deps/mod.rs @@ -5,8 +5,20 @@ pub mod register; use crate::core::config::Config; use crate::core::deps::common::ResolvedDeps; +use crate::core::deps::lock::{DepLock, write_lock}; use std::path::Path; +fn dep_version(path: &Path) -> String { + Config::open(&path.join("dcr.toml").to_string_lossy()) + .ok() + .and_then(|c| { + c.get("package.version") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + }) + .unwrap_or_default() +} + pub fn resolve_deps( config: &Config, _profile: &str, @@ -26,7 +38,7 @@ pub fn resolve_deps( .to_string(); let deps_table = config.get("dependencies").and_then(|v| v.as_table()); - let lock_packages = Vec::new(); + let mut lock_packages: Vec = Vec::new(); if let Some(deps) = deps_table { for (name, value) in deps { @@ -45,6 +57,23 @@ pub fn resolve_deps( .to_string(), ); resolved.libs.push(name.clone()); + + lock_packages.push(DepLock { + name: name.clone(), + version: pkg_info + .get("version") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(), + checksum: String::new(), + source: format!( + "registry+{}", + pkg_info + .get("registry_url") + .and_then(|v| v.as_str()) + .unwrap_or("https://dcr-registry.pages.dev") + ), + }); } else if let Some(path) = path_dep_path(value) { let dep_root = project_root.join(path); if let Some(table) = value.as_table() { @@ -86,11 +115,25 @@ pub fn resolve_deps( push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root); resolved.libs.push(name.clone()); } + + lock_packages.push(DepLock { + name: name.clone(), + version: dep_version(&dep_root), + checksum: String::new(), + source: format!("path+{}", dep_root.display()), + }); + } else if let Some(git_info) = git_dep(value) { + lock_packages.push(DepLock { + name: name.clone(), + version: git_info.version.unwrap_or_default(), + checksum: String::new(), + source: format!("git+{}", git_info.url), + }); } } } - crate::core::deps::lock::write_lock( + write_lock( project_root, &project_name, &project_version, @@ -100,6 +143,24 @@ pub fn resolve_deps( Ok(resolved) } +struct GitDep<'a> { + url: &'a str, + version: Option, +} + +fn git_dep(value: &toml::Value) -> Option> { + if let Some(table) = value.as_table() + && let Some(url) = table.get("git").and_then(|v| v.as_str()) + { + let version = table + .get("version") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + return Some(GitDep { url, version }); + } + None +} + fn path_dep_path(value: &toml::Value) -> Option<&str> { if let Some(table) = value.as_table() { return table.get("path").and_then(|v| v.as_str()); diff --git a/src/core/deps/register.rs b/src/core/deps/register.rs index a3d1d4a..aed0463 100644 --- a/src/core/deps/register.rs +++ b/src/core/deps/register.rs @@ -51,10 +51,18 @@ pub fn get_index_path() -> PathBuf { } pub fn get_registry_cache_root() -> PathBuf { - get_index_path() - .parent() - .map(Path::to_path_buf) - .unwrap_or_else(|| PathBuf::from(".")) + let index_path = get_index_path(); + if let Some(parent) = index_path.parent() { + if parent.exists() { + return parent.to_path_buf(); + } + if let Err(e) = std::fs::create_dir_all(parent) { + eprintln!("Warning: failed to create registry cache directory: {e}"); + } + parent.to_path_buf() + } else { + PathBuf::from(".") + } } pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result { @@ -124,12 +132,16 @@ pub fn is_registry_dep(value: &TomlValue) -> bool { return !is_path_like_string(raw) && !is_git_like_string(raw); } if let Some(table) = value.as_table() { - return !table.contains_key("git") - && !table.contains_key("path") - && !table.contains_key("url") - && !table.contains_key("version"); + if table.contains_key("git") || table.contains_key("path") || table.contains_key("url") { + return false; + } + table.contains_key("version") + || table.contains_key("features") + || table.contains_key("optional") + || table.contains_key("registry") + } else { + false } - false } pub fn path_from_string_dep(value: &TomlValue) -> Option<&str> { diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 2f4b802..2768e72 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -12,14 +12,44 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { match target_dir { Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name), - None => format!("./target/{profile}/lib{name}.a"), + None => { + let arch = std::env::consts::ARCH; + let target = format!("{arch}-unknown-linux-gnu"); + format!("./target/{target}/{profile}/lib{name}.a") + } + } +} + +pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + match target_dir { + Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name), + None => { + let arch = std::env::consts::ARCH; + let target = format!("{arch}-unknown-linux-gnu"); + format!("./target/{target}/{profile}/{name}") + } + } +} + +pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + match target_dir { + Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name), + None => { + let arch = std::env::consts::ARCH; + let target = format!("{arch}-unknown-linux-gnu"); + format!("./target/{target}/{profile}/{name}.efi") + } } } 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"), + None => { + let arch = std::env::consts::ARCH; + let target = format!("{arch}-unknown-linux-gnu"); + format!("./target/{target}/{profile}/lib{name}.so") + } } } @@ -53,10 +83,9 @@ mod tests { #[test] fn lib_path_default() { - assert_eq!( - lib_path("debug", "mylib", None), - "./target/debug/libmylib.a" - ); + let arch = std::env::consts::ARCH; + let expected = format!("./target/{arch}-unknown-linux-gnu/debug/libmylib.a"); + assert_eq!(lib_path("debug", "mylib", None), expected); } #[test] @@ -66,10 +95,9 @@ mod tests { #[test] fn shared_lib_path_default() { - assert_eq!( - shared_lib_path("debug", "mylib", None), - "./target/debug/libmylib.so" - ); + let arch = std::env::consts::ARCH; + let expected = format!("./target/{arch}-unknown-linux-gnu/debug/libmylib.so"); + assert_eq!(shared_lib_path("debug", "mylib", None), expected); } #[test] diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 339529d..bb651a2 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -12,6 +12,20 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + match target_dir { + Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name), + None => format!("./target/{profile}/{name}"), + } +} + +pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + match target_dir { + Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name), + None => format!("./target/{profile}/{name}.efi"), + } +} + 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), diff --git a/src/platform/mod.rs b/src/platform/mod.rs index c082f79..a0741b3 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -27,6 +27,50 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + #[cfg(target_os = "linux")] + { + linux::elf_path(profile, name, target_dir) + } + #[cfg(target_os = "macos")] + { + return macos::elf_path(profile, name, target_dir); + } + #[cfg(target_os = "windows")] + { + return windows::elf_path(profile, name, target_dir); + } + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + { + match target_dir { + Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name), + None => format!("./target/{profile}/{name}"), + } + } +} + +pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + #[cfg(target_os = "linux")] + { + linux::efi_path(profile, name, target_dir) + } + #[cfg(target_os = "macos")] + { + return macos::efi_path(profile, name, target_dir); + } + #[cfg(target_os = "windows")] + { + return windows::efi_path(profile, name, target_dir); + } + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + { + match target_dir { + Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name), + None => format!("./target/{profile}/{name}.efi"), + } + } +} + pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { #[cfg(target_os = "linux")] { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 71707aa..d4fa9f3 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -12,6 +12,20 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { } } +pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + match target_dir { + Some(dir) => format!("{}/{}.exe", dir.trim_end_matches('/'), name), + None => format!("./target/{profile}/{name}.exe"), + } +} + +pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + match target_dir { + Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name), + None => format!("./target/{profile}/{name}.efi"), + } +} + 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),