From 21932ac94a749231c1bc67eb8ddd9b9ece567fd7 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Tue, 16 Jun 2026 16:42:56 +0800 Subject: [PATCH 1/2] feat(build): add freestanding, lto, strip, panic-abort, and codegen-units support in dcr.toml --- .github/workflows/release.yml | 14 +++--- CHANGELOG.md | 30 +++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- TODO.md | 60 ++++++++++++------------- docs/reference/build-profiles.mdx | 17 ++++++++ docs/reference/build-system.mdx | 4 ++ docs/reference/cross-compilation.mdx | 14 +++++- docs/reference/dcr-toml.mdx | 10 ++++- src/cli/build.rs | 65 +++++++++++++++++++++++++++- src/cli/gen.rs | 3 ++ src/core/builder/common.rs | 9 +++- src/core/builder/gas.rs | 8 ++-- src/core/builder/mod.rs | 3 ++ src/core/builder/msvc.rs | 8 ++-- src/core/builder/nasm.rs | 8 ++-- src/core/builder/unix_cc.rs | 45 +++++++++++-------- src/utils/build.rs | 13 ++++++ 18 files changed, 242 insertions(+), 73 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7098bcf..88e4fe1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,20 +93,22 @@ jobs: set -euo pipefail tag="${{ needs.setup.outputs.version }}" notes_file=".github/release-notes.md" - if grep -q "^## \\[${tag}\\]" CHANGELOG.md; then + # Strip -dev suffix to match base version in CHANGELOG + changelog_tag="${tag%-dev}" + if grep -q "^## \\[${changelog_tag}\\]" CHANGELOG.md; then # Extract release name from header: ## [0.7.1] - 2026-06-02 "Title" - release_name=$(awk -v ver="$tag" ' - $0 ~ "^## \\[" ver "\\]" { + release_name=$(awk -v search_ver="$changelog_tag" -v display_ver="$tag" ' + $0 ~ "^## \\[" search_ver "\\]" { match($0, /"([^"]+)"/, arr) if (arr[1] != "") { - print "v" ver " - " arr[1] + print "v" display_ver " - " arr[1] } else { - print "v" ver + print "v" display_ver } } ' CHANGELOG.md) echo "release_name=${release_name}" >> "$GITHUB_OUTPUT" - awk -v ver="$tag" ' + awk -v ver="$changelog_tag" ' $0 ~ "^## \\[" ver "\\]" {found=1; next} found && $0 ~ "^## \\[" {exit} found {print} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b3c040..e8f21e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## [0.7.4] - 2026-06-16 "Bare-Metal и Оптимизация Сборки / Bare-Metal & Build Optimization" + +### RU + +**Добавлено:** + +- **Полноценная автоматизация Bare-Metal / Freestanding** — добавлен конфигурационный флаг `build.freestanding = true`. При его включении (или при обнаружении bare-metal таргета) DCR автоматически инжектирует флаг `-ffreestanding` на этапе компиляции, а также `-nostdlib` и `-static` на этапе линковки. +- **Оптимизация артефактов (LTO и Strip)** — в `dcr.toml` добавлены параметры `build.lto` (авто-инжекция флага `-flto` для компилятора и линкера) и `build.strip` (автоматическое отсекание отладочной информации через флаг `-s` линкера). +- **Контроль многопоточности сборки** — добавлен параметр `build.codegen-units`, позволяющий жестко ограничить максимальное количество параллельных потоков сборки для кастомного движка `parallel_build`. +- **Управление паникой (Panic Abort)** — добавлен параметр `build.panic = "abort"`. Для C++ проектов он автоматически отключает генерацию таблиц раскрутки стека и исключений через флаги `-fno-exceptions`, `-fno-unwind-tables` и `-fno-asynchronous-unwind-tables`. + +**Изменено:** + +- **Интеллектуальные дефолтные флаги (DCR Defaults)** — генерация дефолтных флагов оптимизации (`-O3`/`-O0`), отладки (`-g`) и предупреждений (`-Wall -Wextra`) теперь автоматически отключается для bare-metal и freestanding целей, если пользователь переопределил `build.cflags`. +- **Глобальный вынос bare-metal детектора** — функция `is_bare_metal_target` вынесена в общие утилиты `src/utils/build.rs` для сквозного использования в конвейере сборки. + +### EN + +**Added:** + +- **Full Bare-Metal / Freestanding Automation** — introduced the `build.freestanding = true` configuration option. When enabled (or when a bare-metal target is detected), DCR automatically injects `-ffreestanding` during compilation and both `-nostdlib` and `-static` during linking. +- **Artifact Optimization (LTO & Strip)** — added `build.lto` (auto-injects `-flto` for compiler and linker) and `build.strip` (automatically strips debug symbols via linker `-s` flag) options to `dcr.toml`. +- **Compilation Thread Control** — added the `build.codegen-units` option to strictly limit the maximum number of parallel jobs utilized by the custom `parallel_build` worker pool. +- **Panic Behavior Management** — added `build.panic = "abort"` support. For C++ targets, it automatically strips exception handling and unwind tables via `-fno-exceptions`, `-fno-unwind-tables`, and `-fno-asynchronous-unwind-tables`. + +**Changed:** + +- **Intelligent Default Flags Generation** — automated injection of fallback optimization (`-O3`/`-O0`), debug (`-g`), and warning (`-Wall -Wextra`) flags is now suppressed for bare-metal and freestanding builds if `build.cflags` are overridden. +- **Centralized Bare-Metal Detection** — relocated the `is_bare_metal_target` helper to common build utilities (`src/utils/build.rs`) for unified access across the building core. + ## [0.7.3] - 2026-06-13 "Оптимизация Зависимостей и VCS / Dependency Optimization & VCS" ### RU diff --git a/Cargo.lock b/Cargo.lock index 5a5eecf..3a41b9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,7 +98,7 @@ dependencies = [ [[package]] name = "dcr" -version = "0.7.3" +version = "0.7.4" dependencies = [ "ctrlc", "glob", diff --git a/Cargo.toml b/Cargo.toml index c599bce..5a85834 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dcr" -version = "0.7.3" +version = "0.7.4" edition = "2024" build = "build.rs" description = "DCR is a utility for managing C/C++ projects in a Cargo-like style." diff --git a/TODO.md b/TODO.md index 3d61f14..964cb6c 100644 --- a/TODO.md +++ b/TODO.md @@ -2,38 +2,35 @@ Priority list of tasks to turn DCR into a true Cargo killer for C/C++ developers. -## 🔥 High Priority (Next Releases) +**Legend:** `[ ]` — not implemented | `[-]` — partially implemented | `[x]` — implemented -- [x] **Git dependencies support** - `git = "https://github.com/user/repo.git#branch"` and `git = "..."@tag` -- [ ] **Conan and vcpkg integration** - Automatic download, installation and linking of packages -- [ ] **Full freestanding / embedded support** - `-nostdlib`, linker scripts, bare-metal targets (i686-elf, aarch64-none-elf, etc.) -- [ ] **EFI support** - Dedicated platform configuration for EFI targets -- [ ] **Custom project name** - Override binary name in `dcr.toml` -- [ ] **Custom build steps / targets** - Ability to define custom pre-build, post-build, codegen steps in `dcr.toml` -- [ ] **Improved mixed C/C++/ASM projects** - Better handling of `.S` and `.asm` files with correct flags -- [ ] **dcr publish + official package registry** - Publish and fetch your own libraries easily +## 🔥 High Priority + +- [ ] **CLI Output Refactoring & Cleanup** + - [ ] Move all terminal strings to a dedicated module (`src/ui/messages.rs`) + - [ ] Standardize logging formats (`error:`, `warning:`, `status:`) + - [x] Add build execution time statistics to the final output (e.g., `Finished in 4.23s`) +- [ ] **Internationalization (i18n) / Localization** + - [ ] Integrate a lightweight localization engine (`fluent` or `gettext` pure-Rust port) + - [ ] Create initial translation catalogs (`en.ftl`, `ru.ftl`) +- [x] **Self-Update Stability Validation** + - [x] Self-update implemented via `self-replace` + `ureq` in `flag_update.rs` +- [-] **Freestanding / Embedded Automation** + - *`-ffreestanding` injected at compile time, `-nostdlib -static` at link time for bare-metal targets or `build.freestanding = true`. Config option `build.freestanding` (bool) added.* +- [ ] **Conan and vcpkg integration** Automatic download, installation and linking of packages ## ✅ Medium Priority - [ ] **Static analysis** (`dcr check`) Integration with clang-tidy, cppcheck, include-what-you-use -- [ ] **Linter + Formatter** (`dcr fmt`, `dcr lint`) +- [-] **Linter + Formatter** (`dcr fmt`, `dcr lint`) + *`dcr fmt` (clang-format) implemented; `dcr lint` does not exist yet* - [ ] **Hot reload / Live reload** for applications (raylib, SFML, SDL, etc.) -- [ ] **Better cross-compilation experience** - Ready-to-use toolchains + automatic download -- [ ] **Linker script support** + direct linker control -- [ ] **Workspaces improvements** - Per-member include directories, selective builds, better dependency graph -- [ ] **Build-time code generation** - Support for protobuf, flatbuffers, Qt moc, custom generators +- [-] **Workspaces improvements** Per-member include directories, selective builds, better dependency graph + *Basic workspaces with dep ordering + topological sort work; advanced features missing* +- [-] **Build-time code generation** Support for protobuf, flatbuffers, Qt moc, custom generators + *Generic `build.steps` machinery + Qt toolchain (uic/moc/rcc) work; no dedicated protobuf/flatbuffers generators* +- [-] **dcr publish + official package registry** *Registry consumption (fetch, resolve, link) works; `dcr publish` command / local packing missing* ## 📌 Low Priority / Nice to Have @@ -57,13 +54,12 @@ Priority list of tasks to turn DCR into a true Cargo killer for C/C++ developers --- **Currently in progress:** - -- ... +- UI/UX terminal output unification and benchmarking metrics. **Completed in recent releases:** - -- Workspaces -- Cross-compilation +- Extreme binary size optimization (-50% weight reduction via `ureq` + `native-certs`) +- Complete removal of `git2` and `openssl` in favor of system Git execution +- Workspaces & Cross-compilation (`--target `) - IDE config generation (VS Code, CLion, compile_commands.json) -- Self-update (`dcr --update`) -- Header tracking +- EFI support & Linker script support (`build.ldscript`) +- Mixed C/C++/ASM compilation with custom `std::thread::scope` work-stealing engine \ No newline at end of file diff --git a/docs/reference/build-profiles.mdx b/docs/reference/build-profiles.mdx index 140e0fe..54a6d42 100644 --- a/docs/reference/build-profiles.mdx +++ b/docs/reference/build-profiles.mdx @@ -37,6 +37,23 @@ Set `inherit = false` to disable array inheritance (only profile's own arrays ar | `debug` | `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG` | | `release` | `-O3 -DNDEBUG` | +These defaults are composed from the `opt_level`, `debug`, and `warnings` fields and can +be overridden per-profile: + +```toml +[build.release] +opt_level = "z" +lto = true +strip = true +panic = "abort" +codegen-units = "1" + +[build.debug] +opt_level = "1" +debug = false +warnings = ["all", "error"] +``` + ## Target-specific profiles ```toml diff --git a/docs/reference/build-system.mdx b/docs/reference/build-system.mdx index 7786944..c21d726 100644 --- a/docs/reference/build-system.mdx +++ b/docs/reference/build-system.mdx @@ -20,6 +20,10 @@ DCR supports 4 compilation backends. Selection is automatic based on file extens - Compiler resolved via `resolve_compiler()`: `DCR_COMPILER` > `DCR_CC` > `[toolchain]` > `build.compiler` > `PATH` - Supports `.d` files for header dependency tracking - Flags: `-std=`, `-MMD -MF`, `-c -o`, `-I`, `-L`, `-l` +- Conditional flags based on config: + - `freestanding` or bare-metal target: `-ffreestanding` (compile), `-nostdlib -static` (link) + - `lto`: `-flto` (compile + link) + - `panic = "abort"`: `-fno-exceptions` (C++ only), `-fno-unwind-tables`, `-fno-asynchronous-unwind-tables` ### MSVC diff --git a/docs/reference/cross-compilation.mdx b/docs/reference/cross-compilation.mdx index 24b1c2e..ac3a321 100644 --- a/docs/reference/cross-compilation.mdx +++ b/docs/reference/cross-compilation.mdx @@ -37,9 +37,19 @@ target = "aarch64-unknown-linux-gnu" # Auto: cflags += ["--target=aarch64-unknown-linux-gnu"] ``` -## Bare-Metal +## Bare-Metal / Freestanding -For targets containing `none`, `-elf`, `eabi`, or `baremetal`, DCR default flags are disabled (no system include paths, no `-l` libc). +For targets containing `none`, `-elf`, `eabi`, or `baremetal`, DCR automatically: + +1. **Disables default flags** — no system include paths, no `-l` libc +2. **Injects `-ffreestanding`** at compile time and `-nostdlib -static` at link time + +You can also enable freestanding mode explicitly: + +```toml +[build] +freestanding = true +``` ```bash dcr build --target aarch64-none-elf diff --git a/docs/reference/dcr-toml.mdx b/docs/reference/dcr-toml.mdx index e127923..7f4c804 100644 --- a/docs/reference/dcr-toml.mdx +++ b/docs/reference/dcr-toml.mdx @@ -64,7 +64,7 @@ author = "John Doe" | `compiler` | `"clang"` | Preferred compiler | | `kind` | `"bin"` | `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom` | | `target` | host | Target triple for cross-compilation | -| `platform` | `"native"` | `native`, `efi`, `freestanding` | +| `platform` | `"native"` | `native`, `efi` | | `cflags` | `[]` | Additional C/C++/ASM flags | | `ldflags` | `[]` | Additional linker flags | | `filename` | `""` | Custom output file name | @@ -77,6 +77,14 @@ author = "John Doe" | `clean` | `[]` | Glob patterns for custom clean paths | | `out_dir` | `""` | Custom output directory | | `workspace_only` | `false` | Workspace-only, not built standalone | +| `freestanding` | `false` | Compile in freestanding mode (`-ffreestanding` + `-nostdlib -static`) | +| `opt_level` | `"0"` (debug) / `"3"` (release) | Optimization level: `0`-`3`, `"s"`, `"z"` | +| `debug` | `true` (debug) / `false` (release) | Emit debug symbols (`-g`) | +| `lto` | `false` | Link-time optimization (`-flto` for both compiler and linker) | +| `strip` | `false` | Strip symbols from output (`-s` in ldflags) | +| `warnings` | `["all", "extra"]` (debug) / `[]` (release) | Warning flags (e.g. `"all"`, `"extra"`, `"pedantic"`) | +| `panic` | `""` | Panic strategy: `"abort"` disables exceptions and unwind tables | +| `codegen-units` | `""` | Max parallel compilation jobs (`"0"` = auto) | Settings from raw config (not in typed struct): - `pkg_config` — list of pkg-config packages diff --git a/src/cli/build.rs b/src/cli/build.rs index ed39098..e45ac54 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -24,8 +24,9 @@ use crate::core::deps::{register, resolve_deps}; use crate::core::workspace::parse_workspace; use crate::utils::build::{ default_target_triple, get_bool_with_profile, get_config_opt, get_config_str, - get_language_with_profile, normalize_target_os, parse_version_info, prepend_clang_target_flag, - profile_table, resolve_compiler, resolve_pkg_config_flags, resolve_tool, + get_language_with_profile, is_bare_metal_target, normalize_target_os, parse_version_info, + prepend_clang_target_flag, profile_table, resolve_compiler, resolve_pkg_config_flags, + resolve_tool, }; use crate::utils::fs::{check_dir, find_project_root, with_dir}; use crate::utils::log::error; @@ -682,6 +683,63 @@ fn build_project_at( let build_roots = get_list_with_profile_and_target(&config, "roots", profile, build_target)?; let src_disable = get_bool_with_profile(&config, "src_disable", profile, false); + let freestanding = get_bool_with_profile(&config, "freestanding", profile, false); + let lto = get_bool_with_profile(&config, "lto", profile, false); + let strip = get_bool_with_profile(&config, "strip", profile, false); + let opt_level = + get_string_with_profile_and_target(&config, "opt_level", profile, build_target); + let debug = get_bool_with_profile(&config, "debug", profile, profile != "release"); + let warnings = + get_list_with_profile_and_target(&config, "warnings", profile, build_target)?; + let panic_val = get_string_with_profile_and_target(&config, "panic", profile, build_target); + let panic_abort = panic_val == "abort"; + let codegen_units: usize = + get_string_with_profile_and_target(&config, "codegen-units", profile, build_target) + .parse() + .unwrap_or(0); + + if build_cflags.is_empty() && !freestanding && !is_bare_metal_target(build_target) { + if !opt_level.is_empty() { + build_cflags.push(format!("-O{opt_level}")); + } else { + build_cflags.push(match profile { + "release" => "-O3".to_string(), + _ => "-O0".to_string(), + }); + } + if debug { + build_cflags.push("-g".to_string()); + } + if warnings.is_empty() { + if profile == "debug" { + build_cflags.push("-Wall".to_string()); + build_cflags.push("-Wextra".to_string()); + } + } else { + for w in &warnings { + build_cflags.push(format!("-W{w}")); + } + } + match profile { + "debug" => { + build_cflags.push("-fno-omit-frame-pointer".to_string()); + build_cflags.push("-DDCR_DEBUG".to_string()); + } + "release" => { + build_cflags.push("-DNDEBUG".to_string()); + } + _ => {} + } + } + + if lto { + build_cflags.push("-flto".to_string()); + build_ldflags.push("-flto".to_string()); + } + if strip { + build_ldflags.push("-s".to_string()); + } + let build_expects = get_list_with_profile_and_target(&config, "expect", profile, build_target)?; let pkg_configs = @@ -909,6 +967,9 @@ fn build_project_at( } else { Some(build_type.as_str()) }, + freestanding, + panic_abort, + codegen_units, source_roots: &source_roots, exclude_dirs: &combined_excludes, include_paths: &combined_includes, diff --git a/src/cli/gen.rs b/src/cli/gen.rs index c879a9e..257ae47 100644 --- a/src/cli/gen.rs +++ b/src/cli/gen.rs @@ -367,6 +367,9 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result bool { value.chars().any(|c| matches!(c, '*' | '?' | '[')) } -pub fn parallel_build(total_tasks: usize, task_fn: F) -> Result<(), String> +pub fn parallel_build(total_tasks: usize, task_fn: F, max_jobs: usize) -> Result<(), String> where F: Fn(usize) -> Result<(), String> + Sync + Send, { - let num_threads = std::thread::available_parallelism() + let available = std::thread::available_parallelism() .map(|n| n.get()) .unwrap_or(1); + let num_threads = if max_jobs > 0 { + max_jobs.min(available) + } else { + available + }; let counter = std::sync::atomic::AtomicUsize::new(0); let err_msg = std::sync::Mutex::new(None); diff --git a/src/core/builder/gas.rs b/src/core/builder/gas.rs index 23406d5..502e121 100644 --- a/src/core/builder/gas.rs +++ b/src/core/builder/gas.rs @@ -151,9 +151,11 @@ fn build_objects( .map(|s| common::object_path(obj_dir, s, obj_ext)) .collect(); - common::parallel_build(sources.len(), |i| { - build_object(assembler, &sources[i], &objects[i], ctx) - })?; + common::parallel_build( + sources.len(), + |i| build_object(assembler, &sources[i], &objects[i], ctx), + ctx.codegen_units, + )?; Ok(objects) } diff --git a/src/core/builder/mod.rs b/src/core/builder/mod.rs index cfbaaf0..fda501d 100644 --- a/src/core/builder/mod.rs +++ b/src/core/builder/mod.rs @@ -35,6 +35,9 @@ pub struct BuildContext<'a> { pub linker: Option<&'a str>, pub archiver: Option<&'a str>, pub package_type: Option<&'a str>, + pub freestanding: bool, + pub panic_abort: bool, + pub codegen_units: usize, pub source_roots: &'a [std::path::PathBuf], pub exclude_dirs: &'a [std::path::PathBuf], pub include_paths: &'a [String], diff --git a/src/core/builder/msvc.rs b/src/core/builder/msvc.rs index 110d61b..fc218a0 100644 --- a/src/core/builder/msvc.rs +++ b/src/core/builder/msvc.rs @@ -228,9 +228,11 @@ fn build_objects( .map(|s| common::object_path(obj_dir, s, obj_ext)) .collect(); - common::parallel_build(sources.len(), |i| { - build_object(compiler, &sources[i], &objects[i], ctx) - })?; + common::parallel_build( + sources.len(), + |i| build_object(compiler, &sources[i], &objects[i], ctx), + ctx.codegen_units, + )?; Ok(objects) } diff --git a/src/core/builder/nasm.rs b/src/core/builder/nasm.rs index 0e4dc05..6dff1af 100644 --- a/src/core/builder/nasm.rs +++ b/src/core/builder/nasm.rs @@ -153,9 +153,11 @@ fn build_objects( let format = nasm_format(ctx.platform); - common::parallel_build(sources.len(), |i| { - build_object(assembler, &sources[i], &objects[i], format, ctx) - })?; + common::parallel_build( + sources.len(), + |i| build_object(assembler, &sources[i], &objects[i], format, ctx), + ctx.codegen_units, + )?; Ok(objects) } diff --git a/src/core/builder/unix_cc.rs b/src/core/builder/unix_cc.rs index eacc37f..928209d 100644 --- a/src/core/builder/unix_cc.rs +++ b/src/core/builder/unix_cc.rs @@ -18,6 +18,7 @@ use crate::core::builder::BuildContext; use crate::core::builder::common; use crate::platform; +use crate::utils::build::is_bare_metal_target; use std::fs; use std::path::Path; use std::process::Command; @@ -80,6 +81,9 @@ pub fn build(ctx: &BuildContext) -> Result { cmd.arg("-nostdlib"); cmd.arg("-Wl,-dll"); cmd.arg("-Wl,--subsystem,10"); + } else if ctx.freestanding || is_bare_metal_target(ctx.target) { + cmd.arg("-nostdlib"); + cmd.arg("-static"); } for obj in &objects { cmd.arg(obj); @@ -172,19 +176,6 @@ fn default_flags(profile: &str) -> &'static [&'static str] { } } -fn is_bare_metal_target(target: Option<&str>) -> bool { - if let Some(t) = target { - let lower = t.to_lowercase(); - lower.contains("none") - || lower.contains("-elf") - || lower.contains("eabi") - || lower.contains("baremetal") - || lower.contains("bare-metal") - } else { - false - } -} - fn build_objects( compiler: &str, sources: &[String], @@ -197,9 +188,11 @@ fn build_objects( .map(|s| common::object_path(obj_dir, s, obj_ext)) .collect(); - common::parallel_build(sources.len(), |i| { - build_object(compiler, &sources[i], &objects[i], ctx) - })?; + common::parallel_build( + sources.len(), + |i| build_object(compiler, &sources[i], &objects[i], ctx), + ctx.codegen_units, + )?; Ok(objects) } @@ -231,6 +224,23 @@ fn build_object( cmd.arg("-fPIC"); } + if (ctx.freestanding || is_bare_metal_target(ctx.target)) + && ctx.language.to_lowercase() != "asm" + { + cmd.arg("-ffreestanding"); + } + + if ctx.panic_abort && ctx.language.to_lowercase() != "asm" { + if ctx.language.contains("++") + || ctx.language.contains("cpp") + || ctx.language.contains("cxx") + { + cmd.arg("-fno-exceptions"); + } + cmd.arg("-fno-unwind-tables"); + cmd.arg("-fno-asynchronous-unwind-tables"); + } + if let Some(platform) = ctx.platform && !platform.trim().is_empty() { @@ -250,7 +260,8 @@ fn build_object( } } - let use_dcr_defaults = ctx.cflags.is_empty() && !is_bare_metal_target(ctx.target); + let use_dcr_defaults = + ctx.cflags.is_empty() && !(ctx.freestanding || is_bare_metal_target(ctx.target)); if use_dcr_defaults { for flag in default_flags(ctx.profile) { cmd.arg(flag); diff --git a/src/utils/build.rs b/src/utils/build.rs index 83e99bf..c32a5a4 100644 --- a/src/utils/build.rs +++ b/src/utils/build.rs @@ -187,6 +187,19 @@ pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec out } +pub fn is_bare_metal_target(target: Option<&str>) -> bool { + if let Some(t) = target { + let lower = t.to_lowercase(); + lower.contains("none") + || lower.contains("-elf") + || lower.contains("eabi") + || lower.contains("baremetal") + || lower.contains("bare-metal") + } else { + false + } +} + pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool { let base = config .get(&format!("build.{field}")) From 9326e23a53d0e7028c6c38541dda7b24f08c9691 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 17 Jun 2026 12:39:22 +0800 Subject: [PATCH 2/2] feat: add native Qt support and dcr lint command; refactor architecture and integration tests --- CHANGELOG.md | 30 +- Cargo.toml | 4 +- docs/commands/quality-commands.mdx | 15 + docs/reference/build-profiles.mdx | 15 +- docs/reference/build-system.mdx | 34 +- docs/reference/dcr-toml.mdx | 10 + src/cli/add.rs | 2 +- src/cli/build.rs | 135 ++---- src/cli/clean.rs | 19 +- src/cli/gen.rs | 40 +- src/cli/init.rs | 2 +- src/cli/lint.rs | 189 ++++++++ src/cli/mod.rs | 1 + src/cli/new.rs | 2 +- src/cli/run.rs | 18 +- src/cli/test.rs | 2 +- src/cli/tree.rs | 2 +- src/config.rs | 14 +- src/core/{config.rs => build_config.rs} | 2 + src/core/builder/asm.rs | 170 ++++++++ src/core/builder/common.rs | 71 +++ src/core/builder/fasm.rs | 47 ++ src/core/builder/gas.rs | 129 +----- src/core/builder/llvm_ir.rs | 43 ++ src/core/builder/masm.rs | 51 +++ src/core/builder/mod.rs | 27 ++ src/core/builder/msvc.rs | 27 +- src/core/builder/nasm.rs | 133 +----- src/core/builder/qt.rs | 141 ++++++ src/core/builder/unix_cc.rs | 90 ++-- src/core/deps/common.rs | 122 ------ src/core/deps/git.rs | 33 -- src/core/deps/lock.rs | 64 --- src/core/deps/mod.rs | 3 +- src/core/deps/register.rs | 8 +- src/core/mod.rs | 2 +- src/core/registry.rs | 8 +- src/core/workspace.rs | 2 +- src/main.rs | 1 + src/platform/mod.rs | 35 +- src/platform/posix.rs | 31 ++ src/utils/build.rs | 145 +++++- src/utils/fs.rs | 18 + src/utils/git.rs | 84 ---- tests/cli_basic.rs | 556 ------------------------ tests/cli_build.rs | 130 ++++++ tests/cli_deps.rs | 176 ++++++++ tests/cli_lint.rs | 66 +++ tests/cli_new.rs | 75 ++++ tests/cli_qt.rs | 79 ++++ tests/cli_test.rs | 30 ++ tests/cli_workspace.rs | 58 +++ tests/common/mod.rs | 101 +++++ 53 files changed, 1885 insertions(+), 1407 deletions(-) create mode 100644 src/cli/lint.rs rename src/core/{config.rs => build_config.rs} (99%) create mode 100644 src/core/builder/asm.rs create mode 100644 src/core/builder/fasm.rs create mode 100644 src/core/builder/llvm_ir.rs create mode 100644 src/core/builder/masm.rs create mode 100644 src/core/builder/qt.rs delete mode 100644 src/core/deps/git.rs create mode 100644 src/platform/posix.rs delete mode 100644 tests/cli_basic.rs create mode 100644 tests/cli_build.rs create mode 100644 tests/cli_deps.rs create mode 100644 tests/cli_lint.rs create mode 100644 tests/cli_new.rs create mode 100644 tests/cli_qt.rs create mode 100644 tests/cli_test.rs create mode 100644 tests/cli_workspace.rs create mode 100644 tests/common/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e8f21e4..494cee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,22 @@ # Changelog -## [0.7.4] - 2026-06-16 "Bare-Metal и Оптимизация Сборки / Bare-Metal & Build Optimization" +## [0.7.4] - 2026-06-17 "Qt Поддержка, Linting и Рефакторинг Архитектуры / Qt Support, Linting, & Architecture Refactor" ### RU **Добавлено:** +- **Нативная поддержка Qt** — добавлена автоматическая обработка мета-объектов (MOC, UIC, RCC) при включении `build.qt = true` в `dcr.toml`. +- **Команда `dcr lint`** — статический анализ C/C++ через `clang-tidy`. Поддерживает `--fix` для автоматического исправления. Уважает `build.roots` и `build.src_disable` из конфига. +- **Три новых ассемблерных бэкенда**: + - **MASM** (`compiler = "ml"` / `"ml64"`) — Microsoft Macro Assembler. + - **FASM** (`compiler = "fasm"`) — Flat Assembler. + - **LLVM IR** (`compiler = "llc"`) — компиляция `.ll` → объектный файл. +- **Общий ASM-пайплайн** — линковка и архивирование вынесены в `core/builder/asm.rs`. Новый ассемблерный бэкенд теперь пишется за ~30 строк. +- **Утилиты для файловой системы** — `to_hex()` и `home_dir()` вынесены в `utils/fs.rs`. +- **Утилиты для сборки** — `normalize_target()`, `normalize_kind()`, `normalize_platform()`, `default_profile_flags()` вынесены в `utils/build.rs`. +- **Утилиты для бэкендов** — `asm_lang_flag()`, `source_extensions()`, `elapsed_secs()` добавлены в `core/builder/common.rs`. + - **Полноценная автоматизация Bare-Metal / Freestanding** — добавлен конфигурационный флаг `build.freestanding = true`. При его включении (или при обнаружении bare-metal таргета) DCR автоматически инжектирует флаг `-ffreestanding` на этапе компиляции, а также `-nostdlib` и `-static` на этапе линковки. - **Оптимизация артефактов (LTO и Strip)** — в `dcr.toml` добавлены параметры `build.lto` (авто-инжекция флага `-flto` для компилятора и линкера) и `build.strip` (автоматическое отсекание отладочной информации через флаг `-s` линкера). - **Контроль многопоточности сборки** — добавлен параметр `build.codegen-units`, позволяющий жестко ограничить максимальное количество параллельных потоков сборки для кастомного движка `parallel_build`. @@ -15,11 +26,25 @@ - **Интеллектуальные дефолтные флаги (DCR Defaults)** — генерация дефолтных флагов оптимизации (`-O3`/`-O0`), отладки (`-g`) и предупреждений (`-Wall -Wextra`) теперь автоматически отключается для bare-metal и freestanding целей, если пользователь переопределил `build.cflags`. - **Глобальный вынос bare-metal детектора** — функция `is_bare_metal_target` вынесена в общие утилиты `src/utils/build.rs` для сквозного использования в конвейере сборки. +- **Рефакторинг архитектуры**: + - `src/config.rs` переименован в `src/templates.rs`. + - Начата консолидация конфигурационной логики и оркестрации сборки. ### EN **Added:** +- **Native Qt support** — automatic meta-object handling (MOC, UIC, RCC) added when `build.qt = true` is set in `dcr.toml`. +- **`dcr lint` command** — C/C++ static analysis via `clang-tidy`. Supports `--fix` for automatic fixes. Respects `build.roots` and `build.src_disable` from config. +- **Three new assembler backends**: + - **MASM** (`compiler = "ml"` / `"ml64"`) — Microsoft Macro Assembler. + - **FASM** (`compiler = "fasm"`) — Flat Assembler. + - **LLVM IR** (`compiler = "llc"`) — compiles `.ll` files via `llc -filetype=obj`. +- **Shared ASM pipeline** — linking and archiving logic consolidated into `core/builder/asm.rs`. Adding a new assembler backend now takes ~30 lines. +- **Filesystem utilities** — `to_hex()` and `home_dir()` extracted to `utils/fs.rs`. +- **Build utilities** — `normalize_target()`, `normalize_kind()`, `normalize_platform()`, `default_profile_flags()` extracted to `utils/build.rs`. +- **Backend helpers** — `asm_lang_flag()`, `source_extensions()`, `elapsed_secs()` added to `core/builder/common.rs`. + - **Full Bare-Metal / Freestanding Automation** — introduced the `build.freestanding = true` configuration option. When enabled (or when a bare-metal target is detected), DCR automatically injects `-ffreestanding` during compilation and both `-nostdlib` and `-static` during linking. - **Artifact Optimization (LTO & Strip)** — added `build.lto` (auto-injects `-flto` for compiler and linker) and `build.strip` (automatically strips debug symbols via linker `-s` flag) options to `dcr.toml`. - **Compilation Thread Control** — added the `build.codegen-units` option to strictly limit the maximum number of parallel jobs utilized by the custom `parallel_build` worker pool. @@ -29,6 +54,9 @@ - **Intelligent Default Flags Generation** — automated injection of fallback optimization (`-O3`/`-O0`), debug (`-g`), and warning (`-Wall -Wextra`) flags is now suppressed for bare-metal and freestanding builds if `build.cflags` are overridden. - **Centralized Bare-Metal Detection** — relocated the `is_bare_metal_target` helper to common build utilities (`src/utils/build.rs`) for unified access across the building core. +- **Architectural Refactoring**: + - Renamed `src/config.rs` to `src/templates.rs`. + - Started consolidation of configuration and build orchestration logic. ## [0.7.3] - 2026-06-13 "Оптимизация Зависимостей и VCS / Dependency Optimization & VCS" diff --git a/Cargo.toml b/Cargo.toml index 5a85834..50c7666 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,8 @@ build = "build.rs" description = "DCR is a utility for managing C/C++ projects in a Cargo-like style." license = "GPL-3.0-or-later" repository = "https://github.com/dexoron/dcr" -documentation = "https://dcr.dexoron.su/docs" -homepage = "https://dcr.dexoron.su" +documentation = "https://dcr-tool.ru/docs" +homepage = "https://dcr-tool.ru" [package.metadata.binstall] pkg-fmt = "bin" diff --git a/docs/commands/quality-commands.mdx b/docs/commands/quality-commands.mdx index c8a702b..75a0be4 100644 --- a/docs/commands/quality-commands.mdx +++ b/docs/commands/quality-commands.mdx @@ -42,3 +42,18 @@ dcr fmt Processes: `src/**/*.{c,cpp,cxx,cc,h,hpp,hxx,hh}` and `tests/**/*.{c,cpp,cxx,cc,h,hpp,hxx,hh}`. Uses `.clang-format` at the project root (if present), otherwise default clang-format style. + +## `dcr lint` + +Runs `clang-tidy` on C/C++ source files for static analysis. + +```bash +dcr lint # show diagnostics +dcr lint --fix # apply fixes automatically +dcr lint --help # detailed help +``` + +Processes: `src/**/*.{c,cpp,cxx,cc}` and `tests/**/*.{c,cpp,cxx,cc}`. + +Without `--fix`, clang-tidy reports warnings and errors without modifying files. +With `--fix`, clang-tidy applies automatic suggestions in place. diff --git a/docs/reference/build-profiles.mdx b/docs/reference/build-profiles.mdx index 54a6d42..1079547 100644 --- a/docs/reference/build-profiles.mdx +++ b/docs/reference/build-profiles.mdx @@ -32,13 +32,22 @@ Set `inherit = false` to disable array inheritance (only profile's own arrays ar ## Built-in profiles -| Profile | Default cflags | +The default flags for each profile are composed from three config fields: + +| Field | debug default | release default | +|-------|--------------|----------------| +| `opt_level` | `"0"` | `"3"` | +| `debug` | `true` | `false` | +| `warnings` | `["all", "extra"]` | `[]` | + +Which produce the equivalent compiler flags: + +| Profile | Effective flags | |---------|---------------| | `debug` | `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDCR_DEBUG` | | `release` | `-O3 -DNDEBUG` | -These defaults are composed from the `opt_level`, `debug`, and `warnings` fields and can -be overridden per-profile: +Additional build options can be toggled per-profile: ```toml [build.release] diff --git a/docs/reference/build-system.mdx b/docs/reference/build-system.mdx index c21d726..e1b3ee9 100644 --- a/docs/reference/build-system.mdx +++ b/docs/reference/build-system.mdx @@ -6,14 +6,37 @@ sidebar_label: Build system ## Compiler Backends -DCR supports 4 compilation backends. Selection is automatic based on file extension. +DCR supports 7 compilation backends. Selection is automatic based on file extension and `build.compiler` value. | Backend | Files | When used | |---------|-------|-----------| | `unix_cc` | `.c`, `.cpp`, `.cxx`, `.cc`, `.S` | gcc/clang on Linux/macOS/BSD | | `msvc` | `.c`, `.cpp`, `.cxx`, `.cc` | Windows (cl, clang-cl) | | `gas` | `.s` | ARM/ARM64 assembler (no preprocessor) | -| `nasm` | `.asm` | x86/x86_64 assembler | +| `nasm` | `.asm`, `.s` | x86/x86_64 NASM assembler | +| `masm` | `.asm` | MASM (ml/ml64) on Windows | +| `fasm` | `.asm`, `.fasm` | Flat Assembler | +| `llvm_ir` | `.ll` | LLVM IR via `llc -filetype=obj` | + +## Qt Support + +DCR provides native Qt support for automatic meta-object handling (MOC, UIC, RCC). + +Enable it by setting `build.qt = true` in `dcr.toml`. DCR will automatically detect Qt-related files (`.ui`, `.qrc`, `.h` with `Q_OBJECT`) and process them. + +```toml +[build] +qt = true +``` + +*Note: Requires `qt6` modules (Core, Widgets, Gui, Svg) installed via `pkg-config`.* + +Advanced customization is still possible via `build.steps` if special handling is needed: + +```toml +[build.steps] +moc = "moc {in} -o {out}" +``` ### Unix CC @@ -30,10 +53,13 @@ DCR supports 4 compilation backends. Selection is automatic based on file extens - Supports cl.exe and clang-cl.exe - Flags: `/std:`, `/Fo:`, `/Fe:`, `/I`, `/link` -### GAS / NASM +### GAS / NASM / MASM / FASM / LLVM-IR - GAS: `-I`, `-c -o`, `--defsym` - NASM: `-I`, `-o`, `-D`, `-f` (format: win64/elf64/macho64/macho32/elf32) +- MASM: `/nologo /c /Fo ` +- FASM: ` ` (output path is the object file) +- LLVM IR: `-filetype=obj -o ` ## Incremental Builds @@ -48,7 +74,7 @@ Three levels of incrementality: - `thread::scope` for thread pool - Atomic task queue (`AtomicU64`) - Mutex on stdout (`OUTPUT_MUTEX`) -- Thread count = `available_parallelism()` +- Thread count = `available_parallelism()`, capped by `build.codegen-units` if set ## Build Kinds diff --git a/docs/reference/dcr-toml.mdx b/docs/reference/dcr-toml.mdx index 7f4c804..829b51c 100644 --- a/docs/reference/dcr-toml.mdx +++ b/docs/reference/dcr-toml.mdx @@ -56,6 +56,10 @@ author = "John Doe" ## [build] +... + +- `build.qt` — (bool) Enable automatic Qt meta-object handling (MOC, UIC, RCC). Requires `qt6` modules installed via `pkg-config`. + | Field | Default | Description | |-------|---------|-------------| | `language` | `"c"` | `"c"`, `"c++"`, `"cpp"`, `"cxx"`, `"asm"` | @@ -85,6 +89,7 @@ author = "John Doe" | `warnings` | `["all", "extra"]` (debug) / `[]` (release) | Warning flags (e.g. `"all"`, `"extra"`, `"pedantic"`) | | `panic` | `""` | Panic strategy: `"abort"` disables exceptions and unwind tables | | `codegen-units` | `""` | Max parallel compilation jobs (`"0"` = auto) | +| `qt` | `false` | Enable automatic Qt meta-object handling (MOC, UIC, RCC) | Settings from raw config (not in typed struct): - `pkg_config` — list of pkg-config packages @@ -101,6 +106,11 @@ cxx_standard = "c++23" compiler = "clang" kind = "sharedlib" cflags = ["-Wall", "-Wextra"] +opt_level = "z" +lto = true +strip = true +panic = "abort" +codegen-units = "2" ``` ## [toolchain] diff --git a/src/cli/add.rs b/src/cli/add.rs index 2f9a3e9..e4a5339 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::core::config::Config; +use crate::core::build_config::Config; use crate::core::deps::register; use crate::utils::fs::find_project_root; use crate::utils::log::error; diff --git a/src/cli/build.rs b/src/cli/build.rs index e45ac54..c30f2d4 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -17,16 +17,16 @@ use crate::cli::clean::clean; use crate::cli::flags::parse_build_run_flags; +use crate::core::build_config::Config; use crate::core::builder::common; use crate::core::builder::{BuildContext, build as build_project, collect_sources}; -use crate::core::config::Config; use crate::core::deps::{register, resolve_deps}; use crate::core::workspace::parse_workspace; use crate::utils::build::{ default_target_triple, get_bool_with_profile, get_config_opt, get_config_str, - get_language_with_profile, is_bare_metal_target, normalize_target_os, parse_version_info, - prepend_clang_target_flag, profile_table, resolve_compiler, resolve_pkg_config_flags, - resolve_tool, + get_language_with_profile, is_bare_metal_target, normalize_kind, normalize_platform, + normalize_target, normalize_target_os, parse_version_info, prepend_clang_target_flag, + profile_table, resolve_compiler, resolve_pkg_config_flags, resolve_tool, substitute_vars, }; use crate::utils::fs::{check_dir, find_project_root, with_dir}; use crate::utils::log::error; @@ -633,6 +633,7 @@ fn build_project_at( let project_compiler = get_string_with_profile_and_target(&config, "compiler", profile, build_target); let build_language = get_language_with_profile(&config, profile)?; + let build_qt = get_bool_with_profile(&config, "qt", profile, false); let build_standard = get_string_with_profile_and_target(&config, "standard", profile, build_target); let build_cxx_standard = @@ -930,21 +931,19 @@ fn build_project_at( } let version_info = parse_version_info(&project_version); - let substitute = |s: &str| -> String { - s.replace("{profile}", profile) - .replace("{name}", &project_name) - .replace("{version}", &version_info.full) - .replace("{version_major}", &version_info.major) - .replace("{version_minor}", &version_info.minor) - .replace("{version_patch}", &version_info.patch) - .replace("{version_suffix}", &version_info.suffix) - .replace("{version_suffix_dash}", &version_info.suffix_dash) - }; - let resolved_cflags: Vec = resolved_cflags.iter().map(|f| substitute(f)).collect(); - let modified_ldflags: Vec = - resolved_ldflags.iter().map(|f| substitute(f)).collect(); - let merged_include_dirs: Vec = - merged_include_dirs.iter().map(|f| substitute(f)).collect(); + let resolved_cflags: Vec = resolved_cflags + .iter() + .map(|f| substitute_vars(f, &version_info, profile, &project_name)) + .collect(); + let modified_ldflags: Vec = resolved_ldflags + .iter() + .map(|f| substitute_vars(f, &version_info, profile, &project_name)) + .collect(); + let merged_include_dirs: Vec = merged_include_dirs + .iter() + .map(|f| substitute_vars(f, &version_info, profile, &project_name)) + .collect(); + let tool_execs = resolve_toolchain_execs(&tc_uic, &tc_moc, &tc_rcc, &pkg_configs); let ctx = BuildContext { profile, project_name: &project_name, @@ -962,6 +961,9 @@ fn build_project_at( platform: normalize_platform(&build_platform), linker: resolved_linker.as_deref(), archiver: resolved_archiver.as_deref(), + moc: Some(&tool_execs.moc), + uic: Some(&tool_execs.uic), + rcc: Some(&tool_execs.rcc), package_type: if build_type.is_empty() { None } else { @@ -989,15 +991,8 @@ fn build_project_at( Some(output_extension.as_str()) }, verbose, + qt: build_qt, }; - if std::env::var("DCR_DEBUG").is_ok() { - eprintln!("[dcr] debug: compiler={}", ctx.compiler); - eprintln!("[dcr] debug: cflags={:?}", ctx.cflags); - eprintln!("[dcr] debug: ldflags={:?}", ctx.ldflags); - eprintln!("[dcr] debug: lib_dirs={:?}", ctx.lib_dirs); - eprintln!("[dcr] debug: libs={:?}", ctx.libs); - } - let tool_execs = resolve_toolchain_execs(&tc_uic, &tc_moc, &tc_rcc, &pkg_configs); let step_flags = build_step_flags(&resolved_cflags, &resolved.include_dirs, &resolved_compiler); let step_vars = StepVars { @@ -1105,29 +1100,6 @@ fn package_library(ctx: &BuildContext, headers: &[PathBuf]) -> Result<(), String Ok(()) } -pub fn normalize_target(target: &str, profile: &str) -> Option { - let trimmed = normalize_target_os(target.trim()); - if trimmed.is_empty() { - None - } else { - Some(format!("target/{trimmed}/{profile}")) - } -} - -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) - } -} - struct ToolchainExecs { uic: String, moc: String, @@ -1242,12 +1214,11 @@ fn detect_qt6_tool_variant(tool: &str) -> Option { } fn is_on_path(cmd: &str) -> bool { - std::process::Command::new("sh") - .arg("-c") - .arg(format!("command -v {cmd}")) - .output() - .map(|o| o.status.success()) - .unwrap_or(false) + if Path::new(cmd).is_file() { + return true; + } + std::env::var_os("PATH") + .is_some_and(|paths| std::env::split_paths(&paths).any(|dir| dir.join(cmd).is_file())) } #[derive(Clone)] @@ -1370,21 +1341,15 @@ fn substitute_step_cmd( stem: &str, vars: &StepVars, ) -> String { - template - .replace("{in}", &input.to_string_lossy()) + let info = make_version_info(vars); + let s = substitute_vars(template, &info, vars.profile, ""); + s.replace("{in}", &input.to_string_lossy()) .replace("{out}", &output.to_string_lossy()) .replace("{uic}", &tools.uic) .replace("{moc}", &tools.moc) .replace("{rcc}", &tools.rcc) .replace("{cflags}", step_flags) .replace("{stem}", stem) - .replace("{profile}", vars.profile) - .replace("{version}", vars.version) - .replace("{version_major}", vars.version_major) - .replace("{version_minor}", vars.version_minor) - .replace("{version_patch}", vars.version_patch) - .replace("{version_suffix}", vars.version_suffix) - .replace("{version_suffix_dash}", vars.version_suffix_dash) } fn build_step_flags(cflags: &[String], include_dirs: &[String], compiler: &str) -> String { @@ -1407,14 +1372,9 @@ fn build_step_flags(cflags: &[String], include_dirs: &[String], compiler: &str) out.push(format!("/I{dir}")); } } - let mut dedup = Vec::new(); - for item in out { - if !dedup.contains(&item) { - dedup.push(item); - } - } - dedup - .into_iter() + out.sort(); + out.dedup(); + out.into_iter() .map(quote_step_arg) .collect::>() .join(" ") @@ -1475,16 +1435,21 @@ fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Result crate::utils::build::VersionInfo { + crate::utils::build::VersionInfo { + full: vars.version.to_string(), + major: vars.version_major.to_string(), + minor: vars.version_minor.to_string(), + patch: vars.version_patch.to_string(), + suffix: vars.version_suffix.to_string(), + suffix_dash: vars.version_suffix_dash.to_string(), + } +} + fn expand_step_value(template: &str, stem: &str, vars: &StepVars) -> String { - template - .replace("{stem}", stem) - .replace("{profile}", vars.profile) - .replace("{version}", vars.version) - .replace("{version_major}", vars.version_major) - .replace("{version_minor}", vars.version_minor) - .replace("{version_patch}", vars.version_patch) - .replace("{version_suffix}", vars.version_suffix) - .replace("{version_suffix_dash}", vars.version_suffix_dash) + let info = make_version_info(vars); + let s = substitute_vars(template, &info, vars.profile, ""); + s.replace("{stem}", stem) } struct StepVars<'a> { @@ -1716,9 +1681,5 @@ fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), Strin } fn to_hex(bytes: &[u8]) -> String { - let mut out = String::with_capacity(bytes.len() * 2); - for b in bytes { - out.push_str(&format!("{:02x}", b)); - } - out + crate::utils::fs::to_hex(bytes) } diff --git a/src/cli/clean.rs b/src/cli/clean.rs index 151f6bb..296b038 100644 --- a/src/cli/clean.rs +++ b/src/cli/clean.rs @@ -15,10 +15,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::config::flags; -use crate::core::config::Config; +use crate::core::build_config::Config; use crate::core::workspace::parse_workspace; -use crate::utils::build::{default_target_triple, parse_version_info}; +use crate::utils::build::{default_profile_flags, default_target_triple, parse_version_info}; use crate::utils::fs::{check_dir, find_project_root, with_dir}; use crate::utils::log::{error, warn}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; @@ -106,7 +105,7 @@ fn parse_clean_flags(args: &[String]) -> Result { } if arg.starts_with("--") { let candidate = arg.trim_start_matches("--").to_string(); - if flags(&candidate).is_some() { + if !default_profile_flags(&candidate).is_empty() { if profile.is_some() { return Err("Duplicate profile flag".to_string()); } @@ -227,7 +226,7 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> { .filter_map(|item| item.as_str()) .map(|s| { let value = s.replace("{profile}", profile); - substitute_version_vars(&value, config) + substitute_clean_vars(&value, config) }) .collect::>(), None => Vec::new(), @@ -248,17 +247,11 @@ fn clean_custom_paths(config: &Config, profile: &str) -> Result<(), String> { Ok(()) } -fn substitute_version_vars(value: &str, config: &Config) -> String { +fn substitute_clean_vars(value: &str, config: &Config) -> String { let version = config .get("package.version") .and_then(|v| v.as_str()) .unwrap_or(""); let info = parse_version_info(version); - value - .replace("{version}", &info.full) - .replace("{version_major}", &info.major) - .replace("{version_minor}", &info.minor) - .replace("{version_patch}", &info.patch) - .replace("{version_suffix}", &info.suffix) - .replace("{version_suffix_dash}", &info.suffix_dash) + crate::utils::build::substitute_version_vars(value, &info) } diff --git a/src/cli/gen.rs b/src/cli/gen.rs index 257ae47..8cb1e28 100644 --- a/src/cli/gen.rs +++ b/src/cli/gen.rs @@ -15,15 +15,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use crate::core::build_config::Config; use crate::core::builder::BuildContext; use crate::core::builder::collect_sources; use crate::core::builder::common; -use crate::core::config::Config; use crate::core::workspace::parse_workspace; use crate::utils::build::{ get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile_or_default, - get_list_with_profile, get_string_with_profile, normalize_target_os, resolve_compiler, - resolve_pkg_config_flags_lossy, + get_list_with_profile, get_string_with_profile, normalize_kind, normalize_platform, + normalize_target, resolve_compiler, resolve_pkg_config_flags_lossy, }; use crate::utils::fs::find_project_root; use crate::utils::log::error; @@ -366,6 +366,9 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result Result Vec cmd.push("-c".to_string()); // ASM x flag — must be before source file - if let Some(flag) = asm_lang_flag(source) { + if let Some(flag) = common::asm_lang_flag(source) { cmd.push("-x".to_string()); cmd.push(flag.to_string()); } @@ -684,15 +688,6 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec cmd } -fn asm_lang_flag(source: &str) -> Option<&'static str> { - let ext = Path::new(source).extension().and_then(|v| v.to_str())?; - match ext { - "S" => Some("assembler-with-cpp"), - "s" | "asm" => Some("assembler"), - _ => None, - } -} - fn strip_src_prefix(p: &Path) -> PathBuf { // Try to strip leading ./src or src let s = p.to_string_lossy(); @@ -1241,25 +1236,6 @@ fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String), i32> { Ok((root, profile)) } -fn normalize_target(s: &str, profile: &str) -> Option { - let trimmed = normalize_target_os(s.trim()); - if trimmed.is_empty() { - None - } else { - Some(format!("target/{trimmed}/{profile}")) - } -} - -fn normalize_kind(s: &str) -> &str { - let t = s.trim(); - if t.is_empty() { "bin" } else { t } -} - -fn normalize_platform(s: &str) -> Option<&str> { - let t = s.trim(); - if t.is_empty() { None } else { Some(t) } -} - fn json_str(s: &str) -> String { let mut result = String::with_capacity(s.len() + 2); result.push('"'); diff --git a/src/cli/init.rs b/src/cli/init.rs index 2183131..839bdd2 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -16,7 +16,7 @@ // along with this program. If not, see . use crate::config::FILE_MAIN_C; -use crate::core::config::{Config, validate_package_name}; +use crate::core::build_config::{Config, validate_package_name}; use crate::core::vcs::VcsKind; use crate::utils::fs::check_dir; use crate::utils::log::{error, warn}; diff --git a/src/cli/lint.rs b/src/cli/lint.rs new file mode 100644 index 0000000..118a687 --- /dev/null +++ b/src/cli/lint.rs @@ -0,0 +1,189 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::core::build_config::Config; +use crate::core::builder::common; +use crate::utils::build::{get_bool_with_profile, get_list_with_profile}; +use crate::utils::fs::find_project_root; +use crate::utils::log::error; +use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +pub fn lint(args: &[String]) -> i32 { + if args.first().is_some_and(|a| a == "--help") { + printc("USAGE:", BOLD_GREEN); + printc(" dcr lint [--fix]", BOLD_CYAN); + println!(); + printc("DESCRIPTION:", BOLD_GREEN); + println!(" Runs clang-tidy on all C/C++ source files."); + println!(" Scans source roots (configurable via build.roots) and tests/ directory."); + println!(); + printc("OPTIONS:", BOLD_GREEN); + println!(" --fix Apply clang-tidy suggestions automatically"); + return 0; + } + + let do_fix = args.iter().any(|a| a == "--fix"); + + let start_dir = match std::env::current_dir() { + Ok(dir) => dir, + Err(_) => { + error("Failed to determine current directory"); + return 1; + } + }; + + let root = match find_project_root(&start_dir) { + Ok(Some(r)) => r, + Ok(None) => { + error("dcr.toml not found"); + return 1; + } + Err(_) => { + error("Failed to find project root"); + return 1; + } + }; + + let config = match Config::open(&root.join("dcr.toml").to_string_lossy()) { + Ok(c) => c, + Err(e) => { + error(&format!("Failed to load dcr.toml: {e}")); + return 1; + } + }; + + let profile = "debug"; + let build_roots = get_list_with_profile(&config, "roots", profile); + let src_disable = get_bool_with_profile(&config, "src_disable", profile, false); + + let mut source_roots: Vec = Vec::new(); + for raw in &build_roots { + let p = Path::new(raw); + if p.is_absolute() { + source_roots.push(raw.clone()); + } else { + source_roots.push(root.join(p).to_string_lossy().to_string()); + } + } + if !src_disable && source_roots.is_empty() { + source_roots.push(root.join("src").to_string_lossy().to_string()); + } + + let cxx_extensions = ["c", "cpp", "cxx", "cc"]; + let exclude_dirs: Vec = Vec::new(); + let include_paths: Vec = Vec::new(); + + let roots: Vec = source_roots.iter().map(PathBuf::from).collect(); + let mut files = + match common::collect_sources(&roots, &cxx_extensions, &exclude_dirs, &include_paths) { + Ok(f) => f, + Err(e) => { + error(&format!("Failed to collect source files: {e}")); + return 1; + } + }; + + let tests_dir = root.join("tests"); + if tests_dir.is_dir() { + let test_roots = vec![tests_dir]; + if let Ok(mut test_files) = + common::collect_sources(&test_roots, &cxx_extensions, &exclude_dirs, &include_paths) + { + files.append(&mut test_files); + } + } + + files.sort(); + + if files.is_empty() { + println!(" {} No files to lint", colored("Lint", BOLD_GREEN)); + return 0; + } + + println!( + " {} {} files {}", + colored("Linting", BOLD_GREEN), + files.len(), + if do_fix { "(with --fix)" } else { "" } + ); + + let mut total_issues = 0u32; + let mut failed = 0u32; + + for file in &files { + let mut cmd = Command::new("clang-tidy"); + if do_fix { + cmd.arg("--fix"); + } + cmd.arg("--quiet"); + cmd.arg(file); + + let output = match cmd.output() { + Ok(o) => o, + Err(e) => { + error(&format!("Failed to execute clang-tidy: {}", e)); + return 1; + } + }; + + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + let combined = format!("{}{}", stdout, stderr); + + let issues: Vec<&str> = combined + .lines() + .filter(|l| l.contains("warning:") || l.contains("error:")) + .collect(); + let issue_count = issues.len(); + + if issue_count > 0 { + total_issues += issue_count as u32; + failed += 1; + print!(" {} ", colored("×", BOLD_YELLOW)); + println!("{} ({} issues)", file, issue_count); + for issue in &issues { + println!(" {}", issue); + } + } else { + print!(" {} ", colored("✓", BOLD_GREEN)); + println!("{}", file); + } + } + + let total = files.len(); + let passed = total - failed as usize; + + if failed == 0 { + println!( + " {} {} files, no issues", + colored("Lint", BOLD_GREEN), + total + ); + } else { + println!( + " {} {}/{} files passed, {} issues found", + colored("Lint", BOLD_YELLOW), + passed, + total, + total_issues + ); + } + + 0 +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 389d98c..08cfe4b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -24,6 +24,7 @@ pub mod fmt; pub mod r#gen; pub mod help; pub mod init; +pub mod lint; pub mod new; pub mod run; pub mod setup; diff --git a/src/cli/new.rs b/src/cli/new.rs index 599af78..c9d7651 100644 --- a/src/cli/new.rs +++ b/src/cli/new.rs @@ -16,7 +16,7 @@ // along with this program. If not, see . use crate::config::FILE_MAIN_C; -use crate::core::config::{Config, validate_package_name}; +use crate::core::build_config::{Config, validate_package_name}; use crate::core::vcs::VcsKind; use crate::utils::fs::check_dir; use crate::utils::log::{error, warn}; diff --git a/src/cli/run.rs b/src/cli/run.rs index 942e0c1..b8f1aac 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -17,9 +17,11 @@ use crate::cli::build::build; use crate::cli::flags::parse_build_run_flags; -use crate::core::config::Config; +use crate::core::build_config::Config; use crate::core::runner::run_binary; -use crate::utils::build::{default_target_triple, normalize_target_os, parse_version_info}; +use crate::utils::build::{ + default_target_triple, normalize_target_os, parse_version_info, substitute_vars, +}; use crate::utils::fs::find_project_root; use crate::utils::fs::with_dir; use crate::utils::log::error; @@ -195,11 +197,11 @@ fn run_project( match workspace_root { Some(wr) => target .as_ref() - .and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)) + .and_then(|t| crate::utils::build::normalize_target(t, &flags.profile)) .map(|rel| wr.join(&rel).to_string_lossy().to_string()), None => target .as_ref() - .and_then(|t| crate::cli::build::normalize_target(t, &flags.profile)), + .and_then(|t| crate::utils::build::normalize_target(t, &flags.profile)), } }; @@ -285,11 +287,5 @@ fn run_shell(cmd: &str) -> i32 { fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String { let info = parse_version_info(version); - cmd.replace("{profile}", profile) - .replace("{version}", &info.full) - .replace("{version_major}", &info.major) - .replace("{version_minor}", &info.minor) - .replace("{version_patch}", &info.patch) - .replace("{version_suffix}", &info.suffix) - .replace("{version_suffix_dash}", &info.suffix_dash) + substitute_vars(cmd, &info, profile, "") } diff --git a/src/cli/test.rs b/src/cli/test.rs index 335ebb1..412921b 100644 --- a/src/cli/test.rs +++ b/src/cli/test.rs @@ -17,7 +17,7 @@ use crate::cli::build; use crate::config::{FILE_DCR_TEST_H, FILE_TEST_C, flags}; -use crate::core::config::Config; +use crate::core::build_config::Config; use crate::utils::build::{ get_config_opt, get_config_str, get_language_with_profile, get_list_with_profile, get_string_with_profile, resolve_compiler, resolve_pkg_config_flags, diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 724bb16..eaff5b3 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::core::config::Config; +use crate::core::build_config::Config; use crate::utils::log::error; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc}; use std::collections::HashSet; diff --git a/src/config.rs b/src/config.rs index d574b6b..c98cb03 100644 --- a/src/config.rs +++ b/src/config.rs @@ -97,16 +97,6 @@ int main(int argc, char **argv) { "#; pub fn flags(profile: &str) -> Option<&'static [&'static str]> { - match profile { - "release" => Some(&["-O3", "-DNDEBUG"]), - "debug" => Some(&[ - "-O0", - "-g", - "-Wall", - "-Wextra", - "-fno-omit-frame-pointer", - "-DDCR_DEBUG", - ]), - _ => None, - } + let f = crate::utils::build::default_profile_flags(profile); + if f.is_empty() { None } else { Some(f) } } diff --git a/src/core/config.rs b/src/core/build_config.rs similarity index 99% rename from src/core/config.rs rename to src/core/build_config.rs index 25ee1f2..3f229ca 100644 --- a/src/core/config.rs +++ b/src/core/build_config.rs @@ -138,6 +138,8 @@ pub struct BuildConfig { pub src_disable: Option, #[serde(default)] pub workspace_only: bool, + #[serde(default)] + pub qt: Option, } #[derive(Debug, Clone, Deserialize)] diff --git a/src/core/builder/asm.rs b/src/core/builder/asm.rs new file mode 100644 index 0000000..9e04c77 --- /dev/null +++ b/src/core/builder/asm.rs @@ -0,0 +1,170 @@ +use crate::core::builder::BuildContext; +use crate::core::builder::common; +use crate::platform; +use std::path::Path; +use std::process::Command; +use std::time::Instant; + +pub fn build_assembly( + ctx: &BuildContext, + backend_name: &str, + default_assembler: &str, + extensions: &[&str], + build_object: F, +) -> Result +where + F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync, +{ + let lang = ctx.language.to_lowercase(); + if lang.split(',').any(|p| p.trim() != "asm") { + return Err(format!( + "{backend_name} backend requires build.language = \"asm\"" + )); + } + + let assembler = if ctx.compiler.is_empty() { + default_assembler + } else { + ctx.compiler + }; + + let start_time = Instant::now(); + let sources = common::collect_sources( + ctx.source_roots, + extensions, + ctx.exclude_dirs, + ctx.include_paths, + )?; + 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_common(assembler, &sources, &obj_dir, ctx, &build_object)?; + + if ctx.kind == "staticlib" { + return archive_staticlib(ctx, &objects, start_time); + } + + link_objects(ctx, &objects, start_time) +} + +fn build_objects_common( + assembler: &str, + sources: &[String], + obj_dir: &Path, + ctx: &BuildContext, + build_object: &F, +) -> Result, String> +where + F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync, +{ + let objects: Vec = sources + .iter() + .map(|s| common::object_path(obj_dir, s, "o")) + .collect(); + + common::parallel_build( + sources.len(), + |i| build_object(assembler, &sources[i], &objects[i], ctx), + ctx.codegen_units, + )?; + + Ok(objects) +} + +fn archive_staticlib( + ctx: &BuildContext, + objects: &[String], + start_time: Instant, +) -> Result { + let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); + if !common::needs_link(objects, &lib_path) { + return Ok(common::elapsed_secs(start_time)); + } + + let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") { + "lib" + } else { + "ar" + }); + + let mut cmd = Command::new(archiver); + if cfg!(target_os = "windows") && archiver == "lib" { + cmd.arg("/nologo").arg(format!("/OUT:{lib_path}")); + } else { + cmd.arg("rcs").arg(&lib_path); + } + for obj in objects { + cmd.arg(obj); + } + + if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { + eprintln!("[dcr] {:?}", cmd); + } + + match cmd.status() { + Ok(status) if status.success() => Ok(common::elapsed_secs(start_time)), + Ok(_) => Err("Build failed".to_string()), + Err(err) => Err(format!("Build failed: {err}")), + } +} + +fn link_objects( + ctx: &BuildContext, + objects: &[String], + start_time: Instant, +) -> Result { + let mut cmd = Command::new(ctx.linker.unwrap_or("cc")); + if ctx.kind == "sharedlib" { + if cfg!(target_os = "macos") { + cmd.arg("-dynamiclib"); + } else { + cmd.arg("-shared"); + } + } + + for obj in objects { + cmd.arg(obj); + } + for dir in ctx.lib_dirs { + cmd.arg(format!("-L{dir}")); + } + for lib in ctx.libs { + cmd.arg(format!("-l{lib}")); + } + for flag in ctx.ldflags { + cmd.arg(flag); + } + + let name = ctx.output_filename.unwrap_or(ctx.project_name); + let ext = ctx.output_extension.unwrap_or(""); + let final_name = if ext.is_empty() { + name.to_string() + } else { + format!("{}.{}", name, ext) + }; + + 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) + }; + + if !common::needs_link(objects, &out_path) { + return Ok(common::elapsed_secs(start_time)); + } + + cmd.arg("-o").arg(out_path); + + if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { + eprintln!("[dcr] {:?}", cmd); + } + + match cmd.status() { + Ok(status) if status.success() => Ok(common::elapsed_secs(start_time)), + Ok(_) => Err("Build failed".to_string()), + Err(err) => Err(format!("Build failed: {err}")), + } +} diff --git a/src/core/builder/common.rs b/src/core/builder/common.rs index 2f5175e..ab13fc9 100644 --- a/src/core/builder/common.rs +++ b/src/core/builder/common.rs @@ -19,6 +19,7 @@ use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::{Mutex, OnceLock}; +use std::time::Instant; static OUTPUT_MUTEX: OnceLock> = OnceLock::new(); @@ -164,6 +165,36 @@ pub fn has_glob_magic(value: &str) -> bool { value.chars().any(|c| matches!(c, '*' | '?' | '[')) } +pub fn asm_lang_flag(source: &str) -> Option<&'static str> { + let ext = Path::new(source).extension().and_then(|v| v.to_str())?; + match ext { + "S" => Some("assembler-with-cpp"), + "s" | "asm" => Some("assembler"), + _ => None, + } +} + +pub fn source_extensions(language: &str) -> Vec<&'static str> { + let mut out = Vec::new(); + for part in language.split(',').map(|s| s.trim()) { + let lang = part.to_lowercase(); + match lang.as_str() { + "c" => out.extend(["c"]), + "c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]), + "asm" => out.extend(["s", "S", "asm"]), + _ => {} + } + } + if out.is_empty() { + out.extend(["c"]); + } + out +} + +pub fn elapsed_secs(start: Instant) -> f64 { + ((start.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0 +} + pub fn parallel_build(total_tasks: usize, task_fn: F, max_jobs: usize) -> Result<(), String> where F: Fn(usize) -> Result<(), String> + Sync + Send, @@ -796,4 +827,44 @@ mod tests { "changed header should trigger rebuild" ); } + + #[test] + fn asm_lang_flag_returns_correct_flag() { + assert_eq!(asm_lang_flag("foo.S"), Some("assembler-with-cpp")); + assert_eq!(asm_lang_flag("bar.s"), Some("assembler")); + assert_eq!(asm_lang_flag("baz.asm"), Some("assembler")); + assert_eq!(asm_lang_flag("main.c"), None); + } + + #[test] + fn source_extensions_parses_language_string() { + let c = source_extensions("c"); + assert!(c.contains(&"c")); + assert!(!c.contains(&"cpp")); + + let cpp = source_extensions("c++"); + assert!(cpp.contains(&"cpp")); + assert!(cpp.contains(&"cxx")); + assert!(cpp.contains(&"cc")); + + let asm = source_extensions("asm"); + assert!(asm.contains(&"s")); + assert!(asm.contains(&"S")); + assert!(asm.contains(&"asm")); + + let mixed = source_extensions("c, c++"); + assert!(mixed.contains(&"c")); + assert!(mixed.contains(&"cpp")); + + let unknown = source_extensions("rust"); + assert_eq!(unknown, vec!["c"]); + } + + #[test] + fn elapsed_secs_returns_positive() { + let start = std::time::Instant::now(); + std::thread::sleep(std::time::Duration::from_millis(50)); + let elapsed = elapsed_secs(start); + assert!(elapsed > 0.0); + } } diff --git a/src/core/builder/fasm.rs b/src/core/builder/fasm.rs new file mode 100644 index 0000000..51daee3 --- /dev/null +++ b/src/core/builder/fasm.rs @@ -0,0 +1,47 @@ +use crate::core::builder::BuildContext; +use crate::core::builder::asm; +use crate::core::builder::common; +use std::fs; +use std::path::Path; +use std::process::Command; + +pub fn build(ctx: &BuildContext) -> Result { + asm::build_assembly(ctx, "FASM", "fasm", &["asm", "fasm"], build_object) +} + +pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> { + common::collect_sources( + ctx.source_roots, + &["asm", "fasm"], + ctx.exclude_dirs, + ctx.include_paths, + ) +} + +fn build_object( + assembler: &str, + source: &str, + obj_path: &str, + ctx: &BuildContext, +) -> Result<(), String> { + if let Some(parent) = Path::new(obj_path).parent() { + fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; + } + + if !common::needs_rebuild(source, obj_path) { + return Ok(()); + } + + let mut cmd = Command::new(assembler); + cmd.arg(source).arg(obj_path); + + for flag in ctx.cflags { + cmd.arg(flag); + } + + if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { + eprintln!("[dcr] {:?}", cmd); + } + + common::run_command_sync_output(&mut cmd) +} diff --git a/src/core/builder/gas.rs b/src/core/builder/gas.rs index 502e121..51522d1 100644 --- a/src/core/builder/gas.rs +++ b/src/core/builder/gas.rs @@ -16,121 +16,17 @@ // along with this program. If not, see . use crate::core::builder::BuildContext; +use crate::core::builder::asm; use crate::core::builder::common; -use crate::platform; use std::fs; use std::path::Path; use std::process::Command; -use std::time::Instant; pub fn build(ctx: &BuildContext) -> Result { - let lang = ctx.language.to_lowercase(); - if lang.split(',').any(|p| p.trim() != "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)?; - 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" { - let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); - if !common::needs_link(&objects, &lib_path) { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; - return Ok(elapsed); - } - let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") { - "lib" - } else { - "ar" - }); - let mut cmd = Command::new(archiver); - if cfg!(target_os = "windows") && archiver == "lib" { - cmd.arg("/nologo").arg(format!("/OUT:{lib_path}")); - } else { - cmd.arg("rcs").arg(&lib_path); - } - for obj in &objects { - cmd.arg(obj); - } - if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { - eprintln!("[dcr] {:?}", cmd); - } - match cmd.status() { - Ok(status) if status.success() => { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; - return Ok(elapsed); - } - Ok(_) => return Err("Build failed".to_string()), - Err(err) => return Err(format!("Build failed: {err}")), - } - } - - let mut cmd = Command::new(ctx.linker.unwrap_or("cc")); - if ctx.kind == "sharedlib" { - if cfg!(target_os = "macos") { - cmd.arg("-dynamiclib"); - } else { - cmd.arg("-shared"); - } - } - for obj in &objects { - cmd.arg(obj); - } - for dir in ctx.lib_dirs { - cmd.arg(format!("-L{dir}")); - } - for lib in ctx.libs { - cmd.arg(format!("-l{lib}")); - } - for flag in ctx.ldflags { - cmd.arg(flag); - } - let name = ctx.output_filename.unwrap_or(ctx.project_name); - let ext = ctx.output_extension.unwrap_or(""); - let final_name = if ext.is_empty() { - name.to_string() - } else { - format!("{}.{}", name, ext) - }; - - 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) - }; - - if !common::needs_link(&objects, &out_path) { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; - return Ok(elapsed); - } - cmd.arg("-o").arg(out_path); - - if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { - eprintln!("[dcr] {:?}", cmd); - } - 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}")), - } + asm::build_assembly(ctx, "GAS", "as", &["s"], build_object) } pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> { - // GAS handles only lowercase .s files common::collect_sources( ctx.source_roots, &["s"], @@ -139,27 +35,6 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> ) } -fn build_objects( - assembler: &str, - sources: &[String], - obj_dir: &Path, - ctx: &BuildContext, - obj_ext: &str, -) -> Result, String> { - let objects: Vec = sources - .iter() - .map(|s| common::object_path(obj_dir, s, obj_ext)) - .collect(); - - common::parallel_build( - sources.len(), - |i| build_object(assembler, &sources[i], &objects[i], ctx), - ctx.codegen_units, - )?; - - Ok(objects) -} - fn build_object( assembler: &str, source: &str, diff --git a/src/core/builder/llvm_ir.rs b/src/core/builder/llvm_ir.rs new file mode 100644 index 0000000..62135f8 --- /dev/null +++ b/src/core/builder/llvm_ir.rs @@ -0,0 +1,43 @@ +use crate::core::builder::BuildContext; +use crate::core::builder::asm; +use crate::core::builder::common; +use std::fs; +use std::path::Path; +use std::process::Command; + +pub fn build(ctx: &BuildContext) -> Result { + asm::build_assembly(ctx, "LLVM-IR", "llc", &["ll"], build_object) +} + +pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> { + common::collect_sources( + ctx.source_roots, + &["ll"], + ctx.exclude_dirs, + ctx.include_paths, + ) +} + +fn build_object( + assembler: &str, + source: &str, + obj_path: &str, + ctx: &BuildContext, +) -> Result<(), String> { + if let Some(parent) = Path::new(obj_path).parent() { + fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; + } + + if !common::needs_rebuild(source, obj_path) { + return Ok(()); + } + + let mut cmd = Command::new(assembler); + cmd.arg("-filetype=obj").arg(source).arg("-o").arg(obj_path); + + for flag in ctx.cflags { + cmd.arg(flag); + } + + common::run_command_sync_output(&mut cmd) +} diff --git a/src/core/builder/masm.rs b/src/core/builder/masm.rs new file mode 100644 index 0000000..63b8d18 --- /dev/null +++ b/src/core/builder/masm.rs @@ -0,0 +1,51 @@ +use crate::core::builder::BuildContext; +use crate::core::builder::asm; +use crate::core::builder::common; +use std::fs; +use std::path::Path; +use std::process::Command; + +pub fn build(ctx: &BuildContext) -> Result { + asm::build_assembly(ctx, "MASM", "ml", &["asm"], build_object) +} + +pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> { + common::collect_sources( + ctx.source_roots, + &["asm"], + ctx.exclude_dirs, + ctx.include_paths, + ) +} + +fn build_object( + assembler: &str, + source: &str, + obj_path: &str, + ctx: &BuildContext, +) -> Result<(), String> { + if let Some(parent) = Path::new(obj_path).parent() { + fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; + } + + if !common::needs_rebuild(source, obj_path) { + return Ok(()); + } + + let mut cmd = Command::new(assembler); + cmd.arg("/nologo") + .arg("/c") + .arg("/Fo") + .arg(obj_path) + .arg(source); + + for flag in ctx.cflags { + cmd.arg(flag); + } + + if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { + eprintln!("[dcr] {:?}", cmd); + } + + common::run_command_sync_output(&mut cmd) +} diff --git a/src/core/builder/mod.rs b/src/core/builder/mod.rs index fda501d..257498b 100644 --- a/src/core/builder/mod.rs +++ b/src/core/builder/mod.rs @@ -15,10 +15,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +pub mod asm; pub mod common; +pub mod fasm; pub mod gas; +pub mod llvm_ir; +pub mod masm; pub mod msvc; pub mod nasm; +pub mod qt; pub mod unix_cc; pub struct BuildContext<'a> { @@ -34,6 +39,9 @@ pub struct BuildContext<'a> { pub platform: Option<&'a str>, pub linker: Option<&'a str>, pub archiver: Option<&'a str>, + pub moc: Option<&'a str>, + pub uic: Option<&'a str>, + pub rcc: Option<&'a str>, pub package_type: Option<&'a str>, pub freestanding: bool, pub panic_abort: bool, @@ -49,6 +57,7 @@ pub struct BuildContext<'a> { pub output_filename: Option<&'a str>, pub output_extension: Option<&'a str>, pub verbose: bool, + pub qt: bool, } pub fn build(ctx: &BuildContext) -> Result { @@ -65,9 +74,18 @@ pub fn build(ctx: &BuildContext) -> Result { if compiler == "as" || compiler.contains("gas") { return gas::build(ctx); } + if compiler.contains("llc") { + return llvm_ir::build(ctx); + } + if compiler.contains("fasm") { + return fasm::build(ctx); + } if compiler.contains("nasm") { return nasm::build(ctx); } + if compiler == "ml" || compiler == "ml64" || compiler.contains("masm") { + return masm::build(ctx); + } if compiler == "cl" || compiler.contains("msvc") { return msvc::build(ctx); } @@ -90,8 +108,17 @@ pub fn collect_sources(ctx: &BuildContext) -> Result, String> { if compiler == "as" || compiler.contains("gas") { return gas::collect_sources(ctx); } + if compiler.contains("llc") { + return llvm_ir::collect_sources(ctx); + } + if compiler.contains("fasm") { + return fasm::collect_sources(ctx); + } if compiler.contains("nasm") { return nasm::collect_sources(ctx); } + if compiler == "ml" || compiler == "ml64" || compiler.contains("masm") { + return masm::collect_sources(ctx); + } unix_cc::collect_sources(ctx) } diff --git a/src/core/builder/msvc.rs b/src/core/builder/msvc.rs index fc218a0..b3893c6 100644 --- a/src/core/builder/msvc.rs +++ b/src/core/builder/msvc.rs @@ -44,7 +44,7 @@ pub fn build(ctx: &BuildContext) -> Result { if ctx.kind == "staticlib" { let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); if !common::needs_link(&objects, &lib_path) { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; + let elapsed = common::elapsed_secs(start_time); return Ok(elapsed); } let mut cmd = Command::new(ctx.archiver.unwrap_or("lib")); @@ -57,7 +57,7 @@ pub fn build(ctx: &BuildContext) -> Result { } match cmd.status() { Ok(status) if status.success() => { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; + let elapsed = common::elapsed_secs(start_time); return Ok(elapsed); } Ok(_) => return Err("Build failed".to_string()), @@ -123,17 +123,17 @@ pub fn build(ctx: &BuildContext) -> Result { }; if !common::needs_link(&objects, &out_path) { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; + let elapsed = common::elapsed_secs(start_time); return Ok(elapsed); } - cmd.arg(format!("/Fe:{out_path}")); + cmd.arg("-o").arg(out_path); if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { eprintln!("[dcr] {:?}", cmd); } match cmd.status() { Ok(status) if status.success() => { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; + let elapsed = common::elapsed_secs(start_time); Ok(elapsed) } Ok(_) => Err("Build failed".to_string()), @@ -151,21 +151,8 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> ) } -fn source_extensions(language: &str) -> Vec<&str> { - let mut out = Vec::new(); - for part in language.split(',').map(|s| s.trim()) { - let lang = part.to_lowercase(); - match lang.as_str() { - "c" => out.extend(["c"]), - "c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]), - "asm" => {} - _ => {} - } - } - if out.is_empty() { - out.extend(["c"]); - } - out +fn source_extensions(language: &str) -> Vec<&'static str> { + crate::core::builder::common::source_extensions(language) } fn msvc_standard_flag(language: &str, standard: &str) -> Result { diff --git a/src/core/builder/nasm.rs b/src/core/builder/nasm.rs index 6dff1af..53ea53c 100644 --- a/src/core/builder/nasm.rs +++ b/src/core/builder/nasm.rs @@ -16,117 +16,14 @@ // along with this program. If not, see . use crate::core::builder::BuildContext; +use crate::core::builder::asm; use crate::core::builder::common; -use crate::platform; use std::fs; use std::path::Path; use std::process::Command; -use std::time::Instant; pub fn build(ctx: &BuildContext) -> Result { - let lang = ctx.language.to_lowercase(); - if lang.split(',').any(|p| p.trim() != "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)?; - 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" { - let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); - if !common::needs_link(&objects, &lib_path) { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; - return Ok(elapsed); - } - let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") { - "lib" - } else { - "ar" - }); - let mut cmd = Command::new(archiver); - if cfg!(target_os = "windows") && archiver == "lib" { - cmd.arg("/nologo").arg(format!("/OUT:{lib_path}")); - } else { - cmd.arg("rcs").arg(&lib_path); - } - for obj in &objects { - cmd.arg(obj); - } - if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { - eprintln!("[dcr] {:?}", cmd); - } - match cmd.status() { - Ok(status) if status.success() => { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; - return Ok(elapsed); - } - Ok(_) => return Err("Build failed".to_string()), - Err(err) => return Err(format!("Build failed: {err}")), - } - } - - let mut cmd = Command::new(ctx.linker.unwrap_or("cc")); - if ctx.kind == "sharedlib" { - if cfg!(target_os = "macos") { - cmd.arg("-dynamiclib"); - } else { - cmd.arg("-shared"); - } - } - for obj in &objects { - cmd.arg(obj); - } - for dir in ctx.lib_dirs { - cmd.arg(format!("-L{dir}")); - } - for lib in ctx.libs { - cmd.arg(format!("-l{lib}")); - } - for flag in ctx.ldflags { - cmd.arg(flag); - } - let name = ctx.output_filename.unwrap_or(ctx.project_name); - let ext = ctx.output_extension.unwrap_or(""); - let final_name = if ext.is_empty() { - name.to_string() - } else { - format!("{}.{}", name, ext) - }; - - 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) - }; - - if !common::needs_link(&objects, &out_path) { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; - return Ok(elapsed); - } - cmd.arg("-o").arg(out_path); - - if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() { - eprintln!("[dcr] {:?}", cmd); - } - 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}")), - } + asm::build_assembly(ctx, "NASM", "nasm", &["asm", "s"], build_object) } pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> { @@ -139,34 +36,10 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> ) } -fn build_objects( - assembler: &str, - sources: &[String], - obj_dir: &Path, - ctx: &BuildContext, - obj_ext: &str, -) -> Result, String> { - let objects: Vec = sources - .iter() - .map(|s| common::object_path(obj_dir, s, obj_ext)) - .collect(); - - let format = nasm_format(ctx.platform); - - common::parallel_build( - sources.len(), - |i| build_object(assembler, &sources[i], &objects[i], format, ctx), - ctx.codegen_units, - )?; - - Ok(objects) -} - fn build_object( assembler: &str, source: &str, obj_path: &str, - format: &str, ctx: &BuildContext, ) -> Result<(), String> { if let Some(parent) = Path::new(obj_path).parent() { @@ -177,6 +50,8 @@ fn build_object( return Ok(()); } + let format = nasm_format(ctx.platform); + let mut cmd = Command::new(assembler); cmd.arg("-f") .arg(format) diff --git a/src/core/builder/qt.rs b/src/core/builder/qt.rs new file mode 100644 index 0000000..d583320 --- /dev/null +++ b/src/core/builder/qt.rs @@ -0,0 +1,141 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::core::builder::BuildContext; +use crate::core::builder::common; +use crate::utils::build::run_pkg_config; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result, String> { + let qt_dir = build_dir.join("qt"); + fs::create_dir_all(&qt_dir).map_err(|e| format!("Failed to create qt dir: {e}"))?; + + let extensions = vec!["ui", "qrc", "h", "hpp", "hxx"]; + let files = common::collect_sources( + ctx.source_roots, + &extensions, + ctx.exclude_dirs, + ctx.include_paths, + )?; + + // Fetch Qt system include paths and defines for MOC + let mut qt_moc_args = Vec::new(); + let qt_modules = vec!["Qt6Core", "Qt6Widgets", "Qt6Gui", "Qt6Svg"]; + + for module in qt_modules { + if let Ok(cflags) = run_pkg_config(module, "--cflags") { + for flag in cflags.split_whitespace() { + // Pass both -I and -D flags to moc + if flag.starts_with("-I") || flag.starts_with("-D") { + qt_moc_args.push(flag.to_string()); + } + } + } + } + + let mut generated_any = false; + + for file in files { + let path = Path::new(&file); + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + + match ext { + "ui" => { + if let Some(uic) = ctx.uic { + let out_name = path.file_stem().unwrap().to_str().unwrap(); + let out_file = qt_dir.join(format!("ui_{out_name}.h")); + run_command( + uic, + &[path.to_str().unwrap(), "-o", out_file.to_str().unwrap()], + ctx.verbose, + )?; + generated_any = true; + } + } + "qrc" => { + if let Some(rcc) = ctx.rcc { + let out_name = path.file_stem().unwrap().to_str().unwrap(); + let out_file = qt_dir.join(format!("qrc_{out_name}.cpp")); + run_command( + rcc, + &[ + path.to_str().unwrap(), + "-name", + out_name, + "-o", + out_file.to_str().unwrap(), + ], + ctx.verbose, + )?; + generated_any = true; + } + } + "h" | "hpp" | "hxx" => { + if let Some(moc) = ctx.moc { + let content = fs::read_to_string(path) + .map_err(|e| format!("Failed to read {}: {e}", path.display()))?; + if content.contains("Q_OBJECT") { + let out_name = path.file_stem().unwrap().to_str().unwrap(); + let out_file = qt_dir.join(format!("moc_{out_name}.cpp")); + + let mut moc_args = Vec::new(); + + // Add Qt system cflags (includes + defines) + for arg in &qt_moc_args { + moc_args.push(arg.as_str()); + } + + // Add project include paths + for inc in ctx.include_paths.iter().chain(ctx.include_dirs.iter()) { + moc_args.push("-I"); + moc_args.push(inc); + } + + moc_args.push(path.to_str().unwrap()); + moc_args.push("-o"); + moc_args.push(out_file.to_str().unwrap()); + + run_command(moc, &moc_args, ctx.verbose)?; + generated_any = true; + } + } + } + _ => {} + } + } + + if generated_any { + Ok(Some(qt_dir)) + } else { + Ok(None) + } +} + +fn run_command(cmd_path: &str, args: &[&str], verbose: bool) -> Result<(), String> { + let mut cmd = Command::new(cmd_path); + cmd.args(args); + if verbose || std::env::var("DCR_DEBUG").is_ok() { + eprintln!("[dcr] {:?}", cmd); + } + match cmd.status() { + Ok(s) if s.success() => Ok(()), + Ok(s) => Err(format!("Command {} failed with status {}", cmd_path, s)), + Err(e) => Err(format!("Failed to execute {}: {}", cmd_path, e)), + } +} diff --git a/src/core/builder/unix_cc.rs b/src/core/builder/unix_cc.rs index 928209d..eb6d8a9 100644 --- a/src/core/builder/unix_cc.rs +++ b/src/core/builder/unix_cc.rs @@ -20,7 +20,7 @@ use crate::core::builder::common; use crate::platform; use crate::utils::build::is_bare_metal_target; use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; use std::time::Instant; @@ -31,23 +31,34 @@ pub fn build(ctx: &BuildContext) -> Result { ctx.compiler }; let start_time = Instant::now(); - let extensions = source_extensions(ctx.language); - let sources = common::collect_sources( - ctx.source_roots, - &extensions, - ctx.exclude_dirs, - ctx.include_paths, - )?; 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")?; + let qt_include_path = if ctx.qt { + crate::core::builder::qt::process_qt(ctx, &obj_dir)? + } else { + None + }; + + let mut all_source_roots = ctx.source_roots.to_vec(); + if let Some(qt_path) = &qt_include_path { + all_source_roots.push(qt_path.clone()); + } + + let extensions = source_extensions(ctx.language); + let sources = common::collect_sources( + &all_source_roots, + &extensions, + ctx.exclude_dirs, + ctx.include_paths, + )?; + let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o", qt_include_path)?; if ctx.kind == "staticlib" { let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir); if !common::needs_link(&objects, &lib_path) { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; + let elapsed = common::elapsed_secs(start_time); return Ok(elapsed); } let mut cmd = Command::new(ctx.archiver.unwrap_or("ar")); @@ -60,7 +71,7 @@ pub fn build(ctx: &BuildContext) -> Result { } match cmd.status() { Ok(status) if status.success() => { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; + let elapsed = common::elapsed_secs(start_time); return Ok(elapsed); } Ok(_) => return Err("Build failed".to_string()), @@ -116,7 +127,7 @@ pub fn build(ctx: &BuildContext) -> Result { }; if !common::needs_link(&objects, &out_path) { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; + let elapsed = common::elapsed_secs(start_time); return Ok(elapsed); } cmd.arg("-o").arg(out_path); @@ -126,7 +137,7 @@ pub fn build(ctx: &BuildContext) -> Result { } match cmd.status() { Ok(status) if status.success() => { - let elapsed = ((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0; + let elapsed = common::elapsed_secs(start_time); Ok(elapsed) } Ok(_) => Err("Build failed".to_string()), @@ -144,36 +155,12 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result, String> ) } -fn source_extensions(language: &str) -> Vec<&str> { - let mut out = Vec::new(); - for part in language.split(',').map(|s| s.trim()) { - let lang = part.to_lowercase(); - match lang.as_str() { - "c" => out.extend(["c"]), - "c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]), - "asm" => out.extend(["s", "S", "asm"]), - _ => {} - } - } - if out.is_empty() { - out.extend(["c"]); - } - out +fn source_extensions(language: &str) -> Vec<&'static str> { + crate::core::builder::common::source_extensions(language) } fn default_flags(profile: &str) -> &'static [&'static str] { - match profile { - "release" => &["-O3", "-DNDEBUG"], - "debug" => &[ - "-O0", - "-g", - "-Wall", - "-Wextra", - "-fno-omit-frame-pointer", - "-DDCR_DEBUG", - ], - _ => &[], - } + crate::utils::build::default_profile_flags(profile) } fn build_objects( @@ -182,6 +169,7 @@ fn build_objects( obj_dir: &Path, ctx: &BuildContext, obj_ext: &str, + qt_include_dir: Option, ) -> Result, String> { let objects: Vec = sources .iter() @@ -190,7 +178,15 @@ fn build_objects( common::parallel_build( sources.len(), - |i| build_object(compiler, &sources[i], &objects[i], ctx), + |i| { + build_object( + compiler, + &sources[i], + &objects[i], + ctx, + qt_include_dir.as_deref(), + ) + }, ctx.codegen_units, )?; @@ -202,6 +198,7 @@ fn build_object( source: &str, obj_path: &str, ctx: &BuildContext, + qt_include_dir: Option<&Path>, ) -> Result<(), String> { if let Some(parent) = Path::new(obj_path).parent() { fs::create_dir_all(parent).map_err(|err| format!("obj dir error: {err}"))?; @@ -276,6 +273,10 @@ fn build_object( cmd.arg(format!("-I{dir}")); } + if let Some(path) = qt_include_dir { + cmd.arg(format!("-I{}", path.display())); + } + let d_path = Path::new(obj_path).with_extension("d"); cmd.arg("-MMD").arg("-MF").arg(&d_path); @@ -287,10 +288,5 @@ fn build_object( } fn asm_lang_flag(source: &str) -> Option<&'static str> { - let ext = Path::new(source).extension().and_then(|v| v.to_str())?; - match ext { - "S" => Some("assembler-with-cpp"), - "s" | "asm" => Some("assembler"), - _ => None, - } + crate::core::builder::common::asm_lang_flag(source) } diff --git a/src/core/deps/common.rs b/src/core/deps/common.rs index 356b84d..97811d0 100644 --- a/src/core/deps/common.rs +++ b/src/core/deps/common.rs @@ -1,128 +1,6 @@ -// DCR — Cargo-like C/C++ project manager. -// -// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#[allow(dead_code)] -use std::fs; -use std::path::{Path, PathBuf}; - -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub enum DepSource { - Path(String), - Git { - url: String, - branch: Option, - tag: Option, - rev: Option, - }, - Registry { - version: String, - features: Option>, - }, -} - -#[allow(dead_code)] -#[derive(Debug, Clone)] -pub struct DepSpec { - pub name: String, - pub source: DepSource, - pub include_raw: Option>, - pub lib_raw: Option>, - pub libs_raw: Option>, -} - #[derive(Debug, Clone, Default)] pub struct ResolvedDeps { pub include_dirs: Vec, pub lib_dirs: Vec, pub libs: Vec, } - -#[allow(dead_code)] -pub fn resolve_path(project_root: &Path, raw: &str) -> Result { - let p = Path::new(raw); - let full = if p.is_absolute() { - p.to_path_buf() - } else { - project_root.join(p) - }; - Ok(full) -} - -#[allow(dead_code)] -pub fn resolve_paths( - base: &Path, - raw: Option<&[String]>, - defaults: &[&str], - profile: &str, -) -> Result, String> { - let mut out = Vec::new(); - if let Some(raw) = raw { - for r in raw { - let expanded = expand_profile(r, profile); - let p = Path::new(&expanded); - let full = if p.is_absolute() { - p.to_path_buf() - } else { - base.join(p) - }; - if !full.exists() { - return Err(format!("Path does not exist: {}", full.display())); - } - out.push(full); - } - return Ok(out); - } - - for d in defaults { - let candidate = base.join(d); - if candidate.exists() { - out.push(candidate); - } - } - Ok(out) -} - -#[allow(dead_code)] -pub fn expand_profile(raw: &str, profile: &str) -> String { - raw.replace("{profile}", profile) -} - -#[allow(dead_code)] -pub fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> { - if dst.exists() { - fs::remove_dir_all(dst)?; - } - copy_dir_all(src, dst) -} - -#[allow(dead_code)] -pub fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> { - fs::create_dir_all(dst)?; - for entry in fs::read_dir(src)? { - let entry = entry?; - let ty = entry.file_type()?; - let from = entry.path(); - let to = dst.join(entry.file_name()); - if ty.is_dir() { - copy_dir_all(&from, &to)?; - } else { - fs::copy(&from, &to)?; - } - } - Ok(()) -} diff --git a/src/core/deps/git.rs b/src/core/deps/git.rs deleted file mode 100644 index 2ca84e8..0000000 --- a/src/core/deps/git.rs +++ /dev/null @@ -1,33 +0,0 @@ -// DCR — Cargo-like C/C++ project manager. -// -// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use std::path::Path; - -#[allow(dead_code)] -pub fn fetch_git_dep( - url: &str, - target_dir: &Path, - branch: Option<&str>, - tag: Option<&str>, - rev: Option<&str>, -) -> Result<(), String> { - if target_dir.exists() { - return Ok(()); - } - - crate::utils::git::git_clone_and_checkout(url, target_dir, branch, tag, rev) -} diff --git a/src/core/deps/lock.rs b/src/core/deps/lock.rs index 0ba3126..1ac1db0 100644 --- a/src/core/deps/lock.rs +++ b/src/core/deps/lock.rs @@ -15,10 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use sha2::{Digest, Sha256}; use std::fs; use std::path::Path; -use toml::Value; #[derive(Debug, Clone)] pub struct DepLock { @@ -73,65 +71,3 @@ fn quote_list(items: &[String]) -> String { fn escape_value(input: &str) -> String { input.replace('\\', "\\\\").replace('"', "\\\"") } - -#[allow(dead_code)] -pub fn read_dep_version(dep_path: &Path) -> Option { - let path = dep_path.join("dcr.toml"); - let content = fs::read_to_string(path).ok()?; - let value: Value = content.parse().ok()?; - value - .get("package") - .and_then(|v| v.as_table()) - .and_then(|t| t.get("version")) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) -} - -#[allow(dead_code)] -pub fn compute_checksum(root: &Path) -> Result { - let mut files = Vec::new(); - collect_files(root, root, &mut files)?; - files.sort(); - let mut hasher = Sha256::new(); - for rel in files { - hasher.update(rel.as_bytes()); - let data = - fs::read(root.join(&rel)).map_err(|err| format!("failed to read {}: {err}", rel))?; - hasher.update(&data); - } - let hash = hasher.finalize(); - Ok(to_hex(&hash)) -} - -#[allow(dead_code)] -fn collect_files(root: &Path, dir: &Path, out: &mut Vec) -> Result<(), String> { - for entry in fs::read_dir(dir).map_err(|err| format!("read_dir failed: {err}"))? { - let entry = entry.map_err(|err| format!("read_dir failed: {err}"))?; - let path = entry.path(); - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - if name_str == "target" { - continue; - } - if path.is_dir() { - collect_files(root, &path, out)?; - } else if path.is_file() { - let rel = path - .strip_prefix(root) - .unwrap_or(&path) - .to_string_lossy() - .to_string(); - out.push(rel); - } - } - Ok(()) -} - -#[allow(dead_code)] -fn to_hex(bytes: &[u8]) -> String { - let mut out = String::with_capacity(bytes.len() * 2); - for b in bytes { - out.push_str(&format!("{:02x}", b)); - } - out -} diff --git a/src/core/deps/mod.rs b/src/core/deps/mod.rs index 6e84b47..12aa565 100644 --- a/src/core/deps/mod.rs +++ b/src/core/deps/mod.rs @@ -16,11 +16,10 @@ // along with this program. If not, see . pub mod common; -pub mod git; pub mod lock; pub mod register; -use crate::core::config::Config; +use crate::core::build_config::Config; use crate::core::deps::common::ResolvedDeps; use crate::core::deps::lock::{DepLock, write_lock}; use std::path::Path; diff --git a/src/core/deps/register.rs b/src/core/deps/register.rs index 4affc67..c9e3789 100644 --- a/src/core/deps/register.rs +++ b/src/core/deps/register.rs @@ -33,13 +33,7 @@ pub struct DcrConfig { } fn home_dir() -> Option { - if let Ok(home) = std::env::var("HOME") { - return Some(PathBuf::from(home)); - } - if let Ok(profile) = std::env::var("USERPROFILE") { - return Some(PathBuf::from(profile)); - } - None + crate::utils::fs::home_dir() } fn dcr_config_dir() -> Option { diff --git a/src/core/mod.rs b/src/core/mod.rs index ed33f0d..b99bcd9 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +pub mod build_config; pub mod builder; -pub mod config; pub mod deps; pub mod registry; pub mod runner; diff --git a/src/core/registry.rs b/src/core/registry.rs index fcafd46..50a2814 100644 --- a/src/core/registry.rs +++ b/src/core/registry.rs @@ -38,13 +38,7 @@ pub struct RegistryManager { } fn home_dir() -> Option { - if let Ok(home) = std::env::var("HOME") { - return Some(PathBuf::from(home)); - } - if let Ok(profile) = std::env::var("USERPROFILE") { - return Some(PathBuf::from(profile)); - } - None + crate::utils::fs::home_dir() } impl RegistryManager { diff --git a/src/core/workspace.rs b/src/core/workspace.rs index 440c7b4..fdf1d60 100644 --- a/src/core/workspace.rs +++ b/src/core/workspace.rs @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::core::config::Config; +use crate::core::build_config::Config; use crate::utils::build::normalize_target_os; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; diff --git a/src/main.rs b/src/main.rs index 361029e..c238e76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,6 +41,7 @@ fn main() { "tree" => cli::tree::tree(rest), "test" | "tests" => cli::test::test(rest), "fmt" => cli::fmt::fmt(rest), + "lint" => cli::lint::lint(rest), "clean" => cli::clean::clean(rest), "gen" => cli::r#gen::r#gen(rest), "--version" => { diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 5465db5..146231c 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -26,6 +26,16 @@ pub mod bsd; pub mod linux; #[cfg(target_os = "macos")] pub mod macos; +#[cfg(not(any( + target_os = "linux", + target_os = "macos", + target_os = "windows", + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd", + target_os = "dragonfly" +)))] +pub mod posix; #[cfg(target_os = "windows")] pub mod windows; @@ -61,10 +71,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { target_os = "dragonfly" )))] { - match target_dir { - Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name), - None => format!("./target/{profile}/{name}"), - } + return posix::bin_path(profile, name, target_dir); } } @@ -100,10 +107,7 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { target_os = "dragonfly" )))] { - match target_dir { - Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name), - None => format!("./target/{profile}/{name}"), - } + return posix::elf_path(profile, name, target_dir); } } @@ -139,10 +143,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { target_os = "dragonfly" )))] { - match target_dir { - Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name), - None => format!("./target/{profile}/{name}.efi"), - } + return posix::efi_path(profile, name, target_dir); } } @@ -178,10 +179,7 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { target_os = "dragonfly" )))] { - match target_dir { - Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name), - None => format!("./target/{profile}/lib{name}.a"), - } + return posix::lib_path(profile, name, target_dir); } } @@ -217,9 +215,6 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S target_os = "dragonfly" )))] { - match target_dir { - Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name), - None => format!("./target/{profile}/lib{name}.so"), - } + return posix::shared_lib_path(profile, name, target_dir); } } diff --git a/src/platform/posix.rs b/src/platform/posix.rs new file mode 100644 index 0000000..74c854e --- /dev/null +++ b/src/platform/posix.rs @@ -0,0 +1,31 @@ +pub fn bin_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 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"), + } +} + +pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { + bin_path(profile, name, target_dir) +} + +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{}.so", dir.trim_end_matches('/'), name), + None => format!("./target/{profile}/lib{name}.so"), + } +} diff --git a/src/utils/build.rs b/src/utils/build.rs index c32a5a4..a3f7b71 100644 --- a/src/utils/build.rs +++ b/src/utils/build.rs @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::core::config::Config; +use crate::core::build_config::Config; use crate::utils::log::warn; pub struct VersionInfo { @@ -32,17 +32,16 @@ pub fn parse_version_info(version: &str) -> VersionInfo { if full.is_empty() { full = "0.0.0".to_string(); } - let full_clone = full.clone(); - let (base, suffix) = match full_clone.split_once('-') { - Some((head, tail)) => (head, tail), - None => (full_clone.as_str(), ""), + let (base, suffix) = match full.split_once('-') { + Some((head, tail)) => (head.to_string(), tail.to_string()), + None => (full.clone(), String::new()), }; let mut parts = base.split('.'); let major = parts.next().unwrap_or("0").to_string(); let minor = parts.next().unwrap_or("0").to_string(); let patch = parts.next().unwrap_or("0").to_string(); let suffix_dash = if suffix.is_empty() { - "".to_string() + String::new() } else { format!("-{suffix}") }; @@ -51,11 +50,51 @@ pub fn parse_version_info(version: &str) -> VersionInfo { major, minor, patch, - suffix: suffix.to_string(), + suffix, suffix_dash, } } +pub fn substitute_vars(template: &str, info: &VersionInfo, profile: &str, name: &str) -> String { + let s = template + .replace("{profile}", profile) + .replace("{name}", name); + substitute_version_vars(&s, info) +} + +pub fn substitute_version_vars(template: &str, info: &VersionInfo) -> String { + template + .replace("{version}", &info.full) + .replace("{version_major}", &info.major) + .replace("{version_minor}", &info.minor) + .replace("{version_patch}", &info.patch) + .replace("{version_suffix}", &info.suffix) + .replace("{version_suffix_dash}", &info.suffix_dash) +} + +pub fn normalize_target(target: &str, profile: &str) -> Option { + let trimmed = normalize_target_os(target.trim()); + if trimmed.is_empty() { + None + } else { + Some(format!("target/{trimmed}/{profile}")) + } +} + +pub fn normalize_kind(kind: &str) -> &str { + let trimmed = kind.trim(); + if trimmed.is_empty() { "bin" } else { trimmed } +} + +pub fn normalize_platform(platform: &str) -> Option<&str> { + let trimmed = platform.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed) + } +} + pub fn normalize_target_os(target: &str) -> &str { match target { "linux" => "x86_64-unknown-linux-gnu", @@ -187,16 +226,40 @@ pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec out } +fn contains_ignore_case(text: &str, pattern: &str) -> bool { + if pattern.len() > text.len() { + return false; + } + text.as_bytes() + .windows(pattern.len()) + .any(|w| w.eq_ignore_ascii_case(pattern.as_bytes())) +} + pub fn is_bare_metal_target(target: Option<&str>) -> bool { - if let Some(t) = target { - let lower = t.to_lowercase(); - lower.contains("none") - || lower.contains("-elf") - || lower.contains("eabi") - || lower.contains("baremetal") - || lower.contains("bare-metal") - } else { - false + match target { + Some(t) => { + contains_ignore_case(t, "none") + || contains_ignore_case(t, "-elf") + || contains_ignore_case(t, "eabi") + || contains_ignore_case(t, "baremetal") + || contains_ignore_case(t, "bare-metal") + } + None => false, + } +} + +pub fn default_profile_flags(profile: &str) -> &'static [&'static str] { + match profile { + "release" => &["-O3", "-DNDEBUG"], + "debug" => &[ + "-O0", + "-g", + "-Wall", + "-Wextra", + "-fno-omit-frame-pointer", + "-DDCR_DEBUG", + ], + _ => &[], } } @@ -376,7 +439,7 @@ pub fn resolve_pkg_config_flags_lossy( } } -fn run_pkg_config(pkg: &str, arg: &str) -> Result { +pub fn run_pkg_config(pkg: &str, arg: &str) -> Result { let output = std::process::Command::new("pkg-config") .arg(arg) .arg(pkg) @@ -505,4 +568,52 @@ mod tests { assert_eq!(info.suffix, "beta"); assert_eq!(info.suffix_dash, "-beta"); } + + #[test] + fn normalize_target_with_profile() { + assert_eq!( + normalize_target("linux", "debug"), + Some("target/x86_64-unknown-linux-gnu/debug".into()) + ); + assert_eq!( + normalize_target("x86_64-unknown-linux-gnu", "release"), + Some("target/x86_64-unknown-linux-gnu/release".into()) + ); + assert_eq!(normalize_target("", "debug"), None); + } + + #[test] + fn normalize_kind_empty_defaults_to_bin() { + assert_eq!(normalize_kind(""), "bin"); + assert_eq!(normalize_kind(" "), "bin"); + assert_eq!(normalize_kind("staticlib"), "staticlib"); + assert_eq!(normalize_kind(" elf "), "elf"); + } + + #[test] + fn normalize_platform_empty_is_none() { + assert_eq!(normalize_platform(""), None); + assert_eq!(normalize_platform(" "), None); + assert_eq!(normalize_platform("native"), Some("native")); + assert_eq!(normalize_platform(" efi "), Some("efi")); + } + + #[test] + fn default_profile_flags_by_profile() { + assert!(default_profile_flags("debug").contains(&"-O0")); + assert!(default_profile_flags("debug").contains(&"-g")); + assert!(default_profile_flags("release").contains(&"-O3")); + assert!(default_profile_flags("release").contains(&"-DNDEBUG")); + assert!(default_profile_flags("unknown").is_empty()); + } + + #[test] + fn is_bare_metal_detects_embedded_targets() { + assert!(is_bare_metal_target(Some("aarch64-none-elf"))); + assert!(is_bare_metal_target(Some("i686-elf"))); + assert!(is_bare_metal_target(Some("armv7e-m-none-eabi"))); + assert!(is_bare_metal_target(Some("riscv32-unknown-none-elf"))); + assert!(!is_bare_metal_target(Some("x86_64-unknown-linux-gnu"))); + assert!(!is_bare_metal_target(None)); + } } diff --git a/src/utils/fs.rs b/src/utils/fs.rs index 2212bca..c1f142d 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -18,6 +18,24 @@ use std::io; use std::path::{Path, PathBuf}; +pub fn home_dir() -> Option { + if let Ok(home) = std::env::var("HOME") { + return Some(PathBuf::from(home)); + } + if let Ok(profile) = std::env::var("USERPROFILE") { + return Some(PathBuf::from(profile)); + } + None +} + +pub fn to_hex(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len() * 2); + for b in bytes { + out.push_str(&format!("{:02x}", b)); + } + out +} + #[allow(dead_code)] pub fn check_dir(dir: Option<&str>) -> io::Result> { let path: PathBuf = match dir { diff --git a/src/utils/git.rs b/src/utils/git.rs index ed83dc1..00b6cfe 100644 --- a/src/utils/git.rs +++ b/src/utils/git.rs @@ -1,24 +1,6 @@ -// DCR — Cargo-like C/C++ project manager. -// -// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - use std::path::Path; use std::process::Command; -// checking installed git on system pub fn is_git_available() -> bool { Command::new("git") .arg("--version") @@ -27,17 +9,6 @@ pub fn is_git_available() -> bool { .unwrap_or(false) } -// warning of git not in system -#[allow(dead_code)] -pub fn warn_if_git_missing() { - if !is_git_available() { - crate::utils::log::warn( - "Git is not installed or not found in PATH. Some features (like dependency cloning and VCS initialization) may fail.", - ); - } -} - -// init repo in folder pub fn git_init(path: &Path) -> Result<(), String> { if !is_git_available() { return Err("git executable not found in PATH".to_string()); @@ -55,58 +26,3 @@ pub fn git_init(path: &Path) -> Result<(), String> { Ok(()) } - -// Clone the dependency and switch to the branch/tag/commit -#[allow(dead_code)] -pub fn git_clone_and_checkout( - url: &str, - target_dir: &Path, - branch: Option<&str>, - tag: Option<&str>, - rev: Option<&str>, -) -> Result<(), String> { - if !is_git_available() { - return Err("git executable not found in PATH".to_string()); - } - - let mut args = vec!["clone"]; - - if let Some(b) = branch { - args.push("--branch"); - args.push(b); - args.push("--single-branch"); - } else if let Some(t) = tag { - args.push("--branch"); - args.push(t); - args.push("--single-branch"); - } - - args.push(url); - - let target_dir_str = target_dir.to_string_lossy().to_string(); - args.push(&target_dir_str); - - let status = Command::new("git") - .args(&args) - .status() - .map_err(|e| format!("failed to execute git clone: {}", e))?; - - if !status.success() { - return Err(format!("git clone of {} failed", url)); - } - - if let Some(r) = rev { - let checkout_status = Command::new("git") - .arg("checkout") - .arg(r) - .current_dir(target_dir) - .status() - .map_err(|e| format!("failed to execute git checkout: {}", e))?; - - if !checkout_status.success() { - return Err(format!("git checkout to revision {} failed", r)); - } - } - - Ok(()) -} diff --git a/tests/cli_basic.rs b/tests/cli_basic.rs deleted file mode 100644 index 9fc3704..0000000 --- a/tests/cli_basic.rs +++ /dev/null @@ -1,556 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::sync::Once; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::time::{SystemTime, UNIX_EPOCH}; - -static COUNTER: AtomicUsize = AtomicUsize::new(0); -static BUILD_ONCE: Once = Once::new(); - -fn bin_path() -> PathBuf { - if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") { - return PathBuf::from(exe); - } - ensure_bin_built(); - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("target"); - path.push("debug"); - path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX)); - path -} - -fn unique_sandbox_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 = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("sandbox"); - path.push("cli-tests"); - 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 { - run_dcr_env(args, cwd, &[]) -} - -fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output { - let mut cmd = Command::new(bin_path()); - cmd.args(args).current_dir(cwd); - for (k, v) in envs { - cmd.env(k, v); - } - cmd.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"); - }); -} - -fn available_compiler() -> Option<&'static str> { - for candidate in ["gcc", "clang", "cc"] { - let ok = Command::new(candidate) - .arg("--version") - .output() - .map(|o| o.status.success()) - .unwrap_or(false); - if ok { - return Some(candidate); - } - } - None -} - -#[test] -fn help_and_version_work() { - let dir = unique_sandbox_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_sandbox_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" - ); -} - -fn is_git_in_path() -> bool { - std::process::Command::new("git") - .arg("--version") - .output() - .map(|o| o.status.success()) - .unwrap_or(false) -} - -#[test] -fn new_vcs_options_work() { - // 1. Test --vcs none - let dir = unique_sandbox_dir("new_vcs_none"); - let out = run_dcr(&["new", "hello_none", "--vcs", "none"], &dir); - assert!(out.status.success(), "dcr new --vcs none should succeed"); - let project_dir = dir.join("hello_none"); - assert!(project_dir.is_dir()); - assert!(!project_dir.join(".git").exists()); - assert!(!project_dir.join(".gitignore").exists()); - - // 2. Test --vcs git - if is_git_in_path() { - let dir = unique_sandbox_dir("new_vcs_git"); - let out = run_dcr(&["new", "hello_git", "--vcs", "git"], &dir); - assert!(out.status.success(), "dcr new --vcs git should succeed"); - let project_dir = dir.join("hello_git"); - assert!(project_dir.is_dir()); - assert!(project_dir.join(".git").is_dir()); - assert!(project_dir.join(".gitignore").is_file()); - let gitignore = std::fs::read_to_string(project_dir.join(".gitignore")).unwrap(); - assert!(gitignore.contains("/target")); - } -} - -#[test] -fn init_vcs_options_work() { - // 1. Test --vcs none - let dir = unique_sandbox_dir("init_vcs_none"); - let out = run_dcr(&["init", "--vcs", "none"], &dir); - assert!(out.status.success(), "dcr init --vcs none should succeed"); - assert!(!dir.join(".git").exists()); - assert!(!dir.join(".gitignore").exists()); - - // 2. Test --vcs git - if is_git_in_path() { - let dir = unique_sandbox_dir("init_vcs_git"); - let out = run_dcr(&["init", "--vcs", "git"], &dir); - assert!(out.status.success(), "dcr init --vcs git should succeed"); - assert!(dir.join(".git").is_dir()); - assert!(dir.join(".gitignore").is_file()); - let gitignore = std::fs::read_to_string(dir.join(".gitignore")).unwrap(); - assert!(gitignore.contains("/target")); - } -} - -#[test] -fn init_and_clean_remove_target() { - let dir = unique_sandbox_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"); -} - -#[test] -fn build_run_clean_flags_normal_project() { - let Some(compiler) = available_compiler() else { - eprintln!("no compiler found; skipping build/run test"); - return; - }; - - let dir = unique_sandbox_dir("normal"); - let out = run_dcr(&["init"], &dir); - assert!(out.status.success(), "dcr init should succeed"); - - let envs = [("DCR_COMPILER", compiler)]; - let out = run_dcr_env(&["build"], &dir, &envs); - assert!(out.status.success(), "dcr build should succeed"); - - let out = run_dcr_env(&["build", "--release"], &dir, &envs); - assert!(out.status.success(), "dcr build --release should succeed"); - - let out = run_dcr_env(&["run"], &dir, &envs); - let stdout = String::from_utf8_lossy(&out.stdout); - assert!(stdout.contains("Running"), "dcr run should start"); - - let out = run_dcr_env(&["clean", "--release"], &dir, &envs); - assert!(out.status.success(), "dcr clean --release should succeed"); - let target_dir = "target/x86_64-unknown-linux-gnu".to_string(); - assert!( - !dir.join(&target_dir).join("release").exists(), - "target/x86_64-unknown-linux-gnu/release should be removed" - ); - assert!( - dir.join(&target_dir).join("debug").is_dir(), - "target/x86_64-unknown-linux-gnu/debug should remain" - ); - - let out = run_dcr_env(&["clean"], &dir, &envs); - assert!(out.status.success(), "dcr clean should succeed"); - assert!(!dir.join("target").exists(), "target should be removed"); -} - -#[test] -fn workspace_build_and_clean_all() { - let Some(compiler) = available_compiler() else { - eprintln!("no compiler found; skipping workspace test"); - return; - }; - - let root = unique_sandbox_dir("workspace"); - let out = run_dcr(&["init"], &root); - assert!(out.status.success(), "root init should succeed"); - - let members = [ - ("userspace", &[][..]), - ("core", &["userspace"][..]), - ("kernel", &["core"][..]), - ]; - for (name, _) in &members { - let member_dir = root.join("src").join(name); - std::fs::create_dir_all(&member_dir).expect("failed to create member dir"); - let out = run_dcr(&["init"], &member_dir); - assert!(out.status.success(), "member init should succeed"); - } - - let workspace_toml = "[package]\nname = \"ws-root\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[workspace]\nuserspace = { path = \"src/userspace\", deps = [] }\ncore = { path = \"src/core\", deps = [\"userspace\"] }\nkernel = { path = \"src/kernel\", deps = [\"core\"] }\n\n[dependencies]\n"; - std::fs::write(root.join("dcr.toml"), workspace_toml).expect("failed to write root dcr.toml"); - - let envs = [("DCR_COMPILER", compiler)]; - let out = run_dcr_env(&["build"], &root, &envs); - assert!(out.status.success(), "workspace build should succeed"); - - let out = run_dcr_env(&["build"], &root, &envs); - assert!(out.status.success(), "workspace build should succeed"); - - let out = run_dcr_env(&["build", "--release"], &root, &envs); - assert!( - out.status.success(), - "workspace build --release should succeed" - ); - - let out = run_dcr_env(&["clean", "--release", "--all"], &root, &envs); - assert!( - out.status.success(), - "workspace clean --all --release should succeed" - ); - - let target_dir = "target/x86_64-unknown-linux-gnu"; - assert!( - !root.join(target_dir).join("release").exists(), - "root target/x86_64-unknown-linux-gnu/release should be removed" - ); - assert!( - root.join(target_dir).join("debug").exists(), - "root target/x86_64-unknown-linux-gnu/debug should remain" - ); -} - -#[test] -fn dcr_add_dependencies() { - let dir = unique_sandbox_dir("add_dep"); - let out = run_dcr(&["init"], &dir); - assert!(out.status.success(), "dcr init should succeed"); - - // Test path: prefix - let out = run_dcr(&["add", "mylib", "path:./libs/mylib"], &dir); - assert!(out.status.success(), "dcr add path should succeed"); - let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); - assert!( - toml.contains("mylib = { path = \"./libs/mylib\" }"), - "path dep not found in toml" - ); - - // Test github: prefix - let out = run_dcr(&["add", "gh_lib", "github:user/repo"], &dir); - assert!(out.status.success(), "dcr add github should succeed"); - let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); - assert!( - toml.contains("gh_lib = { git = \"https://github.com/user/repo\" }"), - "github dep not found in toml" - ); - - // Test git: prefix (generic) - let out = run_dcr(&["add", "custom_git", "git:host.com/user/repo"], &dir); - assert!(out.status.success(), "dcr add custom git should succeed"); - let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); - assert!( - toml.contains("custom_git = { git = \"https://host.com/user/repo\" }"), - "custom git dep not found in toml" - ); - - // Test git: prefix (github default) - let out = run_dcr(&["add", "git_short", "git:user/repo"], &dir); - assert!(out.status.success(), "dcr add git short should succeed"); - let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); - assert!( - toml.contains("git_short = { git = \"https://github.com/user/repo\" }"), - "git short dep not found in toml" - ); - - // Test flags (branch) - let out = run_dcr( - &["add", "branch_lib", "github:user/repo", "--branch", "dev"], - &dir, - ); - assert!(out.status.success(), "dcr add with branch should succeed"); - let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); - assert!( - toml.contains("branch_lib = { git = \"https://github.com/user/repo\", branch = \"dev\" }"), - "branch lib not found in toml" - ); - - // Test failure on no prefix - let out = run_dcr(&["add", "fail_lib", "user/repo"], &dir); - assert!(!out.status.success(), "dcr add without prefix should fail"); -} - -#[test] -fn dcr_builds_lib_package() { - let Some(compiler) = available_compiler() else { - eprintln!("no compiler found; skipping lib package test"); - return; - }; - - let dir = unique_sandbox_dir("lib_package"); - let out = run_dcr(&["init"], &dir); - assert!(out.status.success(), "dcr init should succeed"); - - let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); - let updated_toml = toml - .replace("kind = \"bin\"", "kind = \"staticlib\"") - .replace("type = \"none\"", "type = \"lib\""); - std::fs::write(dir.join("dcr.toml"), updated_toml).expect("failed to write toml"); - - std::fs::write(dir.join("src").join("my_lib.h"), "void hello();") - .expect("failed to write header"); - - let envs = [("DCR_COMPILER", compiler)]; - let out = run_dcr_env(&["build"], &dir, &envs); - if !out.status.success() { - eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout)); - eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr)); - } - assert!(out.status.success(), "dcr build should succeed"); - - let target_dir = dir.join("target"); - assert!( - target_dir.join("include").join("my_lib.h").is_file(), - "include/my_lib.h missing" - ); - assert!(target_dir.join("lib").exists(), "lib directory missing"); -} - -#[test] -fn registry_dependency_is_built_from_cache() { - let Some(compiler) = available_compiler() else { - eprintln!("no compiler found; skipping registry dependency build test"); - return; - }; - - let root = unique_sandbox_dir("registry_dep"); - let home = root.join("home"); - let dcr_home = home.join(".dcr"); - let dep = root.join("cache").join("mylib"); - let app = root.join("app"); - std::fs::create_dir_all(dcr_home.as_path()).expect("failed to create dcr home"); - std::fs::create_dir_all(dep.join("src")).expect("failed to create dep src"); - std::fs::create_dir_all(app.join("src")).expect("failed to create app src"); - - std::fs::write( - dcr_home.join("config.toml"), - "[registry.local]\nurl = \"file://local\"\npriority = 1\n", - ) - .expect("failed to write registry config"); - std::fs::write( - dcr_home.join("index.json"), - format!( - "{{\"packages\":[{{\"name\":\"mylib\",\"latest_version\":\"0.1.0\",\"path\":\"{}\"}}]}}", - dep.display() - ), - ) - .expect("failed to write registry index"); - - std::fs::write( - dep.join("dcr.toml"), - "[package]\nname = \"mylib\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n\n[dependencies]\n", - ) - .expect("failed to write dep dcr.toml"); - std::fs::write(dep.join("src").join("mylib.h"), "int answer(void);\n") - .expect("failed to write header"); - std::fs::write( - dep.join("src").join("mylib.c"), - "int answer(void) { return 42; }\n", - ) - .expect("failed to write dep source"); - - std::fs::write( - app.join("dcr.toml"), - "[package]\nname = \"app\"\nversion = \"0.1.0\"\ntype = \"none\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nmylib = \"0.1.0\"\n", - ) - .expect("failed to write app dcr.toml"); - std::fs::write( - app.join("src").join("main.c"), - "#include \"mylib.h\"\nint main(void) { return answer() == 42 ? 0 : 1; }\n", - ) - .expect("failed to write app source"); - - let index_path = dcr_home.join("index.json"); - let envs = [ - ("DCR_COMPILER", compiler), - ("HOME", home.to_str().unwrap()), - ("DCR_INDEX_PATH", index_path.to_str().unwrap()), - ]; - let out = run_dcr_env(&["build"], &app, &envs); - if !out.status.success() { - eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout)); - eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr)); - } - assert!( - out.status.success(), - "registry dependency build should succeed" - ); - assert!( - dep.join("target").join("include").join("mylib.h").is_file(), - "registry dependency headers were not packaged" - ); - assert!( - dep.join("target").join("lib").exists(), - "registry dependency library directory missing" - ); -} - -fn parse_project_name(toml: &str) -> String { - toml.lines() - .find(|l| l.trim().starts_with("name =")) - .and_then(|l| l.split('=').nth(1)) - .map(|s| s.trim().trim_matches('"').to_string()) - .expect("could not parse project name from dcr.toml") -} - -fn default_artifact_path(project_root: &Path, project_name: &str) -> PathBuf { - project_root - .join("target") - .join("x86_64-unknown-linux-gnu") - .join("debug") - .join(project_name) -} - -#[test] -fn build_with_target_config() { - let Some(compiler) = available_compiler() else { - eprintln!("no compiler found; skipping target config test"); - return; - }; - - let dir = unique_sandbox_dir("target_cfg"); - let out = run_dcr(&["init"], &dir); - assert!(out.status.success(), "dcr init should succeed"); - - let toml_path = dir.join("dcr.toml"); - let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml"); - let project_name = parse_project_name(&toml); - let updated = toml.replace("[build]", "[build]\ntarget = \"linux\""); - std::fs::write(&toml_path, updated).expect("failed to write dcr.toml"); - - let envs = [("DCR_COMPILER", compiler)]; - let out = run_dcr_env(&["build"], &dir, &envs); - assert!( - out.status.success(), - "dcr build with target = \"linux\" should succeed" - ); - - let artifact = default_artifact_path(&dir, &project_name); - assert!( - artifact.is_file(), - "artifact should be at default path target/x86_64-unknown-linux-gnu/debug/{}", - project_name - ); -} - -#[test] -fn build_with_out_dir() { - let Some(compiler) = available_compiler() else { - eprintln!("no compiler found; skipping out_dir test"); - return; - }; - - let dir = unique_sandbox_dir("out_dir"); - let out = run_dcr(&["init"], &dir); - assert!(out.status.success(), "dcr init should succeed"); - - let toml_path = dir.join("dcr.toml"); - let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml"); - let project_name = parse_project_name(&toml); - let updated = toml.replace("[build]", "[build]\nout_dir = \"./_BUILD\""); - std::fs::write(&toml_path, updated).expect("failed to write dcr.toml"); - - let envs = [("DCR_COMPILER", compiler)]; - let out = run_dcr_env(&["build"], &dir, &envs); - if !out.status.success() { - eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout)); - eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr)); - } - assert!( - out.status.success(), - "dcr build with out_dir should succeed" - ); - - let artifact = dir.join("_BUILD").join(&project_name); - assert!( - artifact.is_file(), - "artifact should be at _BUILD/{} (custom out_dir)", - project_name - ); - - let default_path = default_artifact_path(&dir, &project_name); - assert!( - !default_path.exists(), - "artifact should NOT be at default path when out_dir is set" - ); -} - -#[test] -fn dcr_test_runs_without_sandbox_dependency() { - let Some(compiler) = available_compiler() else { - eprintln!("no compiler found; skipping dcr test integration"); - return; - }; - - let dir = unique_sandbox_dir("dcr_test_independent"); - let out = run_dcr(&["init"], &dir); - assert!(out.status.success(), "dcr init should succeed"); - - let envs = [("DCR_CC", compiler)]; - let out_init = run_dcr_env(&["test", "--init"], &dir, &envs); - assert!(out_init.status.success(), "dcr test --init should succeed"); - - let out = run_dcr_env(&["test"], &dir, &envs); - assert!(out.status.success(), "dcr test should succeed"); - let stdout = String::from_utf8_lossy(&out.stdout); - let stderr = String::from_utf8_lossy(&out.stderr); - assert!(stdout.contains("TOTAL: 1"), "TOTAL summary line missing"); - assert!( - stdout.contains("PASS: 1"), - "PASS summary line missing\nstdout:\n{}\nstderr:\n{}", - stdout, - stderr - ); -} diff --git a/tests/cli_build.rs b/tests/cli_build.rs new file mode 100644 index 0000000..530c66b --- /dev/null +++ b/tests/cli_build.rs @@ -0,0 +1,130 @@ +mod common; +use common::*; + +#[test] +fn init_and_clean_remove_target() { + let dir = unique_sandbox_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"); +} + +#[test] +fn build_run_clean_flags_normal_project() { + let Some(compiler) = available_compiler() else { + eprintln!("no compiler found; skipping build/run test"); + return; + }; + + let dir = unique_sandbox_dir("normal"); + let out = run_dcr(&["init"], &dir); + assert!(out.status.success(), "dcr init should succeed"); + + let envs = [("DCR_COMPILER", compiler)]; + let out = run_dcr_env(&["build"], &dir, &envs); + assert!(out.status.success(), "dcr build should succeed"); + + let out = run_dcr_env(&["build", "--release"], &dir, &envs); + assert!(out.status.success(), "dcr build --release should succeed"); + + let out = run_dcr_env(&["run"], &dir, &envs); + let stdout = String::from_utf8_lossy(&out.stdout); + assert!(stdout.contains("Running"), "dcr run should start"); + + let out = run_dcr_env(&["clean", "--release"], &dir, &envs); + assert!(out.status.success(), "dcr clean --release should succeed"); + let target_dir = "target/x86_64-unknown-linux-gnu".to_string(); + assert!( + !dir.join(&target_dir).join("release").exists(), + "target/x86_64-unknown-linux-gnu/release should be removed" + ); + assert!( + dir.join(&target_dir).join("debug").is_dir(), + "target/x86_64-unknown-linux-gnu/debug should remain" + ); + + let out = run_dcr_env(&["clean"], &dir, &envs); + assert!(out.status.success(), "dcr clean should succeed"); + assert!(!dir.join("target").exists(), "target should be removed"); +} + +#[test] +fn build_with_target_config() { + let Some(compiler) = available_compiler() else { + eprintln!("no compiler found; skipping target config test"); + return; + }; + + let dir = unique_sandbox_dir("target_cfg"); + let out = run_dcr(&["init"], &dir); + assert!(out.status.success(), "dcr init should succeed"); + + let toml_path = dir.join("dcr.toml"); + let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml"); + let project_name = parse_project_name(&toml); + let updated = toml.replace("[build]", "[build]\ntarget = \"linux\""); + std::fs::write(&toml_path, updated).expect("failed to write dcr.toml"); + + let envs = [("DCR_COMPILER", compiler)]; + let out = run_dcr_env(&["build"], &dir, &envs); + assert!( + out.status.success(), + "dcr build with target = \"linux\" should succeed" + ); + + let artifact = default_artifact_path(&dir, &project_name); + assert!( + artifact.is_file(), + "artifact should be at default path target/x86_64-unknown-linux-gnu/debug/{}", + project_name + ); +} + +#[test] +fn build_with_out_dir() { + let Some(compiler) = available_compiler() else { + eprintln!("no compiler found; skipping out_dir test"); + return; + }; + + let dir = unique_sandbox_dir("out_dir"); + let out = run_dcr(&["init"], &dir); + assert!(out.status.success(), "dcr init should succeed"); + + let toml_path = dir.join("dcr.toml"); + let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml"); + let project_name = parse_project_name(&toml); + let updated = toml.replace("[build]", "[build]\nout_dir = \"./_BUILD\""); + std::fs::write(&toml_path, updated).expect("failed to write dcr.toml"); + + let envs = [("DCR_COMPILER", compiler)]; + let out = run_dcr_env(&["build"], &dir, &envs); + if !out.status.success() { + eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout)); + eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr)); + } + assert!( + out.status.success(), + "dcr build with out_dir should succeed" + ); + + let artifact = dir.join("_BUILD").join(&project_name); + assert!( + artifact.is_file(), + "artifact should be at _BUILD/{} (custom out_dir)", + project_name + ); + + let default_path = default_artifact_path(&dir, &project_name); + assert!( + !default_path.exists(), + "artifact should NOT be at default path when out_dir is set" + ); +} diff --git a/tests/cli_deps.rs b/tests/cli_deps.rs new file mode 100644 index 0000000..5acb361 --- /dev/null +++ b/tests/cli_deps.rs @@ -0,0 +1,176 @@ +mod common; +use common::*; + +#[test] +fn dcr_add_dependencies() { + let dir = unique_sandbox_dir("add_dep"); + let out = run_dcr(&["init"], &dir); + assert!(out.status.success(), "dcr init should succeed"); + + // Test path: prefix + let out = run_dcr(&["add", "mylib", "path:./libs/mylib"], &dir); + assert!(out.status.success(), "dcr add path should succeed"); + let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); + assert!( + toml.contains("mylib = { path = \"./libs/mylib\" }"), + "path dep not found in toml" + ); + + // Test github: prefix + let out = run_dcr(&["add", "gh_lib", "github:user/repo"], &dir); + assert!(out.status.success(), "dcr add github should succeed"); + let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); + assert!( + toml.contains("gh_lib = { git = \"https://github.com/user/repo\" }"), + "github dep not found in toml" + ); + + // Test git: prefix (generic) + let out = run_dcr(&["add", "custom_git", "git:host.com/user/repo"], &dir); + assert!(out.status.success(), "dcr add custom git should succeed"); + let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); + assert!( + toml.contains("custom_git = { git = \"https://host.com/user/repo\" }"), + "custom git dep not found in toml" + ); + + // Test git: prefix (github default) + let out = run_dcr(&["add", "git_short", "git:user/repo"], &dir); + assert!(out.status.success(), "dcr add git short should succeed"); + let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); + assert!( + toml.contains("git_short = { git = \"https://github.com/user/repo\" }"), + "git short dep not found in toml" + ); + + // Test flags (branch) + let out = run_dcr( + &["add", "branch_lib", "github:user/repo", "--branch", "dev"], + &dir, + ); + assert!(out.status.success(), "dcr add with branch should succeed"); + let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); + assert!( + toml.contains("branch_lib = { git = \"https://github.com/user/repo\", branch = \"dev\" }"), + "branch lib not found in toml" + ); + + // Test failure on no prefix + let out = run_dcr(&["add", "fail_lib", "user/repo"], &dir); + assert!(!out.status.success(), "dcr add without prefix should fail"); +} + +#[test] +fn dcr_builds_lib_package() { + let Some(compiler) = available_compiler() else { + eprintln!("no compiler found; skipping lib package test"); + return; + }; + + let dir = unique_sandbox_dir("lib_package"); + let out = run_dcr(&["init"], &dir); + assert!(out.status.success(), "dcr init should succeed"); + + let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); + let updated_toml = toml + .replace("kind = \"bin\"", "kind = \"staticlib\"") + .replace("type = \"none\"", "type = \"lib\""); + std::fs::write(dir.join("dcr.toml"), updated_toml).expect("failed to write toml"); + + std::fs::write(dir.join("src").join("my_lib.h"), "void hello();") + .expect("failed to write header"); + + let envs = [("DCR_COMPILER", compiler)]; + let out = run_dcr_env(&["build"], &dir, &envs); + if !out.status.success() { + eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout)); + eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr)); + } + assert!(out.status.success(), "dcr build should succeed"); + + let target_dir = dir.join("target"); + assert!( + target_dir.join("include").join("my_lib.h").is_file(), + "include/my_lib.h missing" + ); + assert!(target_dir.join("lib").exists(), "lib directory missing"); +} + +#[test] +fn registry_dependency_is_built_from_cache() { + let Some(compiler) = available_compiler() else { + eprintln!("no compiler found; skipping registry dependency build test"); + return; + }; + + let root = unique_sandbox_dir("registry_dep"); + let home = root.join("home"); + let dcr_home = home.join(".dcr"); + let dep = root.join("cache").join("mylib"); + let app = root.join("app"); + std::fs::create_dir_all(dcr_home.as_path()).expect("failed to create dcr home"); + std::fs::create_dir_all(dep.join("src")).expect("failed to create dep src"); + std::fs::create_dir_all(app.join("src")).expect("failed to create app src"); + + std::fs::write( + dcr_home.join("config.toml"), + "[registry.local]\nurl = \"file://local\"\npriority = 1\n", + ) + .expect("failed to write registry config"); + std::fs::write( + dcr_home.join("index.json"), + format!( + "{{\"packages\":[{{\"name\":\"mylib\",\"latest_version\":\"0.1.0\",\"path\":\"{}\"}}]}}", + dep.display() + ), + ) + .expect("failed to write registry index"); + + std::fs::write( + dep.join("dcr.toml"), + "[package]\nname = \"mylib\"\nversion = \"0.1.0\"\ntype = \"lib\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"staticlib\"\n\n[dependencies]\n", + ) + .expect("failed to write dep dcr.toml"); + std::fs::write(dep.join("src").join("mylib.h"), "int answer(void);\n") + .expect("failed to write header"); + std::fs::write( + dep.join("src").join("mylib.c"), + "int answer(void) { return 42; }\n", + ) + .expect("failed to write dep source"); + + std::fs::write( + app.join("dcr.toml"), + "[package]\nname = \"app\"\nversion = \"0.1.0\"\ntype = \"none\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nmylib = \"0.1.0\"\n", + ) + .expect("failed to write app dcr.toml"); + std::fs::write( + app.join("src").join("main.c"), + "#include \"mylib.h\"\nint main(void) { return answer() == 42 ? 0 : 1; }\n", + ) + .expect("failed to write app source"); + + let index_path = dcr_home.join("index.json"); + let envs = [ + ("DCR_COMPILER", compiler), + ("HOME", home.to_str().unwrap()), + ("DCR_INDEX_PATH", index_path.to_str().unwrap()), + ]; + let out = run_dcr_env(&["build"], &app, &envs); + if !out.status.success() { + eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout)); + eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr)); + } + assert!( + out.status.success(), + "registry dependency build should succeed" + ); + assert!( + dep.join("target").join("include").join("mylib.h").is_file(), + "registry dependency headers were not packaged" + ); + assert!( + dep.join("target").join("lib").exists(), + "registry dependency library directory missing" + ); +} diff --git a/tests/cli_lint.rs b/tests/cli_lint.rs new file mode 100644 index 0000000..44e4e57 --- /dev/null +++ b/tests/cli_lint.rs @@ -0,0 +1,66 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::Once; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +static COUNTER: AtomicUsize = AtomicUsize::new(0); +static BUILD_ONCE: Once = Once::new(); + +fn bin_path() -> PathBuf { + if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") { + return PathBuf::from(exe); + } + ensure_bin_built(); + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("target"); + path.push("debug"); + path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX)); + path +} + +fn unique_sandbox_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 = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("sandbox"); + path.push("cli-tests"); + path.push(format!("dcr_{prefix}_{pid}_{n}_{now}")); + std::fs::create_dir_all(&path).expect("failed to create temp dir"); + path +} + +fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output { + let mut cmd = Command::new(bin_path()); + cmd.args(args).current_dir(cwd); + for (k, v) in envs { + cmd.env(k, v); + } + cmd.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 lint_command_runs() { + let dir = unique_sandbox_dir("lint"); + let out = run_dcr_env(&["init"], &dir, &[]); + assert!(out.status.success(), "dcr init should succeed"); + + // Linting a clean project should succeed or at least run without crashing + let out = run_dcr_env(&["lint"], &dir, &[]); + assert!(out.status.success(), "dcr lint should run on clean project"); +} diff --git a/tests/cli_new.rs b/tests/cli_new.rs new file mode 100644 index 0000000..b628106 --- /dev/null +++ b/tests/cli_new.rs @@ -0,0 +1,75 @@ +mod common; +use common::*; + +#[test] +fn help_and_version_work() { + let dir = unique_sandbox_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_sandbox_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 new_vcs_options_work() { + // 1. Test --vcs none + let dir = unique_sandbox_dir("new_vcs_none"); + let out = run_dcr(&["new", "hello_none", "--vcs", "none"], &dir); + assert!(out.status.success(), "dcr new --vcs none should succeed"); + let project_dir = dir.join("hello_none"); + assert!(project_dir.is_dir()); + assert!(!project_dir.join(".git").exists()); + assert!(!project_dir.join(".gitignore").exists()); + + // 2. Test --vcs git + if is_git_in_path() { + let dir = unique_sandbox_dir("new_vcs_git"); + let out = run_dcr(&["new", "hello_git", "--vcs", "git"], &dir); + assert!(out.status.success(), "dcr new --vcs git should succeed"); + let project_dir = dir.join("hello_git"); + assert!(project_dir.is_dir()); + assert!(project_dir.join(".git").is_dir()); + assert!(project_dir.join(".gitignore").is_file()); + let gitignore = std::fs::read_to_string(project_dir.join(".gitignore")).unwrap(); + assert!(gitignore.contains("/target")); + } +} + +#[test] +fn init_vcs_options_work() { + // 1. Test --vcs none + let dir = unique_sandbox_dir("init_vcs_none"); + let out = run_dcr(&["init", "--vcs", "none"], &dir); + assert!(out.status.success(), "dcr init --vcs none should succeed"); + assert!(!dir.join(".git").exists()); + assert!(!dir.join(".gitignore").exists()); + + // 2. Test --vcs git + if is_git_in_path() { + let dir = unique_sandbox_dir("init_vcs_git"); + let out = run_dcr(&["init", "--vcs", "git"], &dir); + assert!(out.status.success(), "dcr init --vcs git should succeed"); + assert!(dir.join(".git").is_dir()); + assert!(dir.join(".gitignore").is_file()); + let gitignore = std::fs::read_to_string(dir.join(".gitignore")).unwrap(); + assert!(gitignore.contains("/target")); + } +} diff --git a/tests/cli_qt.rs b/tests/cli_qt.rs new file mode 100644 index 0000000..7cf15a9 --- /dev/null +++ b/tests/cli_qt.rs @@ -0,0 +1,79 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::Once; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +static COUNTER: AtomicUsize = AtomicUsize::new(0); +static BUILD_ONCE: Once = Once::new(); + +fn bin_path() -> PathBuf { + if let Ok(exe) = std::env::var("CARGO_BIN_EXE_dcr") { + return PathBuf::from(exe); + } + ensure_bin_built(); + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("target"); + path.push("debug"); + path.push(format!("dcr{}", std::env::consts::EXE_SUFFIX)); + path +} + +fn unique_sandbox_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 = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("sandbox"); + path.push("cli-tests"); + path.push(format!("dcr_{prefix}_{pid}_{n}_{now}")); + std::fs::create_dir_all(&path).expect("failed to create temp dir"); + path +} + +fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output { + let mut cmd = Command::new(bin_path()); + cmd.args(args).current_dir(cwd); + for (k, v) in envs { + cmd.env(k, v); + } + cmd.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 qt_support_config_detected() { + let dir = unique_sandbox_dir("qt"); + let out = run_dcr_env(&["init"], &dir, &[]); + assert!(out.status.success(), "dcr init should succeed"); + + let toml_path = dir.join("dcr.toml"); + let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml"); + let updated = toml.replace("[build]", "[build]\nqt = true\nlanguage = \"cpp\""); + std::fs::write(&toml_path, updated).expect("failed to write dcr.toml"); + + // This might fail if Qt is not installed, but it should at least try to run Qt processing + // We just check if it doesn't crash immediately due to bad config + let out = run_dcr_env(&["build"], &dir, &[]); + + // Depending on environment, this might be success or failure, but it should not panic. + // If it fails with "qt not found", it means it parsed the config and attempted to use it. + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + !stderr.contains("panicked"), + "dcr should not panic on Qt config" + ); +} diff --git a/tests/cli_test.rs b/tests/cli_test.rs new file mode 100644 index 0000000..774ab7f --- /dev/null +++ b/tests/cli_test.rs @@ -0,0 +1,30 @@ +mod common; +use common::*; + +#[test] +fn dcr_test_runs_without_sandbox_dependency() { + let Some(compiler) = available_compiler() else { + eprintln!("no compiler found; skipping dcr test integration"); + return; + }; + + let dir = unique_sandbox_dir("dcr_test_independent"); + let out = run_dcr(&["init"], &dir); + assert!(out.status.success(), "dcr init should succeed"); + + let envs = [("DCR_CC", compiler)]; + let out_init = run_dcr_env(&["test", "--init"], &dir, &envs); + assert!(out_init.status.success(), "dcr test --init should succeed"); + + let out = run_dcr_env(&["test"], &dir, &envs); + assert!(out.status.success(), "dcr test should succeed"); + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stdout.contains("TOTAL: 1"), "TOTAL summary line missing"); + assert!( + stdout.contains("PASS: 1"), + "PASS summary line missing\nstdout:\n{}\nstderr:\n{}", + stdout, + stderr + ); +} diff --git a/tests/cli_workspace.rs b/tests/cli_workspace.rs new file mode 100644 index 0000000..c16d8ab --- /dev/null +++ b/tests/cli_workspace.rs @@ -0,0 +1,58 @@ +mod common; +use common::*; + +#[test] +fn workspace_build_and_clean_all() { + let Some(compiler) = available_compiler() else { + eprintln!("no compiler found; skipping workspace test"); + return; + }; + + let root = unique_sandbox_dir("workspace"); + let out = run_dcr(&["init"], &root); + assert!(out.status.success(), "root init should succeed"); + + let members = [ + ("userspace", &[][..]), + ("core", &["userspace"][..]), + ("kernel", &["core"][..]), + ]; + for (name, _) in &members { + let member_dir = root.join("src").join(name); + std::fs::create_dir_all(&member_dir).expect("failed to create member dir"); + let out = run_dcr(&["init"], &member_dir); + assert!(out.status.success(), "member init should succeed"); + } + + let workspace_toml = "[package]\nname = \"ws-root\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\nstandard = \"c11\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[workspace]\nuserspace = { path = \"src/userspace\", deps = [] }\ncore = { path = \"src/core\", deps = [\"userspace\"] }\nkernel = { path = \"src/kernel\", deps = [\"core\"] }\n\n[dependencies]\n"; + std::fs::write(root.join("dcr.toml"), workspace_toml).expect("failed to write root dcr.toml"); + + let envs = [("DCR_COMPILER", compiler)]; + let out = run_dcr_env(&["build"], &root, &envs); + assert!(out.status.success(), "workspace build should succeed"); + + let out = run_dcr_env(&["build"], &root, &envs); + assert!(out.status.success(), "workspace build should succeed"); + + let out = run_dcr_env(&["build", "--release"], &root, &envs); + assert!( + out.status.success(), + "workspace build --release should succeed" + ); + + let out = run_dcr_env(&["clean", "--release", "--all"], &root, &envs); + assert!( + out.status.success(), + "workspace clean --all --release should succeed" + ); + + let target_dir = "target/x86_64-unknown-linux-gnu"; + assert!( + !root.join(target_dir).join("release").exists(), + "root target/x86_64-unknown-linux-gnu/release should be removed" + ); + assert!( + root.join(target_dir).join("debug").exists(), + "root target/x86_64-unknown-linux-gnu/debug should remain" + ); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..3019fc1 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,101 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::Once; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +static COUNTER: AtomicUsize = AtomicUsize::new(0); +static BUILD_ONCE: Once = Once::new(); + +pub 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 +} + +pub fn unique_sandbox_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 = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("sandbox"); + path.push("cli-tests"); + path.push(format!("dcr_{prefix}_{pid}_{n}_{now}")); + std::fs::create_dir_all(&path).expect("failed to create temp dir"); + path +} + +pub fn run_dcr(args: &[&str], cwd: &Path) -> std::process::Output { + run_dcr_env(args, cwd, &[]) +} + +pub fn run_dcr_env(args: &[&str], cwd: &Path, envs: &[(&str, &str)]) -> std::process::Output { + let mut cmd = Command::new(bin_path()); + cmd.args(args).current_dir(cwd); + for (k, v) in envs { + cmd.env(k, v); + } + cmd.output().expect("failed to run dcr") +} + +pub 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"); + }); +} + +#[allow(dead_code)] +pub fn available_compiler() -> Option<&'static str> { + for candidate in ["gcc", "clang", "cc"] { + let ok = Command::new(candidate) + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false); + if ok { + return Some(candidate); + } + } + None +} + +#[allow(dead_code)] +pub fn is_git_in_path() -> bool { + std::process::Command::new("git") + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +#[allow(dead_code)] +pub fn parse_project_name(toml: &str) -> String { + toml.lines() + .find(|l| l.trim().starts_with("name =")) + .and_then(|l| l.split('=').nth(1)) + .map(|s| s.trim().trim_matches('"').to_string()) + .expect("could not parse project name from dcr.toml") +} + +#[allow(dead_code)] +pub fn default_artifact_path(project_root: &Path, project_name: &str) -> PathBuf { + project_root + .join("target") + .join("x86_64-unknown-linux-gnu") + .join("debug") + .join(project_name) +}