feat: add native Qt support and dcr lint command; refactor architecture and integration tests

This commit is contained in:
2026-06-17 12:39:22 +08:00
parent 21932ac94a
commit 9326e23a53
53 changed files with 1885 additions and 1407 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<obj> <source>`
- FASM: `<source> <output>` (output path is the object file)
- LLVM IR: `-filetype=obj <source> -o <output>`
## 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

View File

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

View File

@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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;

View File

@@ -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<String> = resolved_cflags.iter().map(|f| substitute(f)).collect();
let modified_ldflags: Vec<String> =
resolved_ldflags.iter().map(|f| substitute(f)).collect();
let merged_include_dirs: Vec<String> =
merged_include_dirs.iter().map(|f| substitute(f)).collect();
let resolved_cflags: Vec<String> = resolved_cflags
.iter()
.map(|f| substitute_vars(f, &version_info, profile, &project_name))
.collect();
let modified_ldflags: Vec<String> = resolved_ldflags
.iter()
.map(|f| substitute_vars(f, &version_info, profile, &project_name))
.collect();
let merged_include_dirs: Vec<String> = 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<String> {
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<String> {
}
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::<Vec<_>>()
.join(" ")
@@ -1475,16 +1435,21 @@ fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Result<bool, St
Ok(false)
}
fn make_version_info(vars: &StepVars) -> 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)
}

View File

@@ -15,10 +15,9 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<CleanFlags, String> {
}
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::<Vec<String>>(),
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)
}

View File

@@ -15,15 +15,15 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<ProjectInfo,
platform: normalize_platform(&platform),
linker: resolved_linker.as_deref(),
archiver: resolved_archiver.as_deref(),
moc: None,
uic: None,
rcc: None,
package_type: None,
freestanding: false,
panic_abort: false,
@@ -381,6 +384,7 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
output_filename: None,
output_extension: None,
verbose: false,
qt: false,
};
let sources = collect_sources(&ctx).map_err(|e| format!("Failed to collect sources: {e}"))?;
@@ -606,7 +610,7 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> 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<String> {
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('"');

View File

@@ -16,7 +16,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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};

189
src/cli/lint.rs Normal file
View File

@@ -0,0 +1,189 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// 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 <https://www.gnu.org/licenses/>.
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<String> = 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<PathBuf> = Vec::new();
let include_paths: Vec<String> = Vec::new();
let roots: Vec<PathBuf> = 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
}

View File

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

View File

@@ -16,7 +16,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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};

View File

@@ -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, "")
}

View File

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

View File

@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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;

View File

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

View File

@@ -138,6 +138,8 @@ pub struct BuildConfig {
pub src_disable: Option<bool>,
#[serde(default)]
pub workspace_only: bool,
#[serde(default)]
pub qt: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]

170
src/core/builder/asm.rs Normal file
View File

@@ -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<F>(
ctx: &BuildContext,
backend_name: &str,
default_assembler: &str,
extensions: &[&str],
build_object: F,
) -> Result<f64, String>
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<F>(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
build_object: &F,
) -> Result<Vec<String>, String>
where
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
{
let objects: Vec<String> = 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<f64, String> {
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<f64, String> {
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}")),
}
}

View File

@@ -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<Mutex<()>> = 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<F>(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);
}
}

47
src/core/builder/fasm.rs Normal file
View File

@@ -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<f64, String> {
asm::build_assembly(ctx, "FASM", "fasm", &["asm", "fasm"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, 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)
}

View File

@@ -16,121 +16,17 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<f64, String> {
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<Vec<String>, 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<Vec<String>, String>
)
}
fn build_objects(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = 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,

View File

@@ -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<f64, String> {
asm::build_assembly(ctx, "LLVM-IR", "llc", &["ll"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, 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)
}

51
src/core/builder/masm.rs Normal file
View File

@@ -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<f64, String> {
asm::build_assembly(ctx, "MASM", "ml", &["asm"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, 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)
}

View File

@@ -15,10 +15,15 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<f64, String> {
@@ -65,9 +74,18 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
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<Vec<String>, 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)
}

View File

@@ -44,7 +44,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
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<f64, String> {
}
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<f64, String> {
};
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<Vec<String>, 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<String, String> {

View File

@@ -16,117 +16,14 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<f64, String> {
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<Vec<String>, String> {
@@ -139,34 +36,10 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
)
}
fn build_objects(
assembler: &str,
sources: &[String],
obj_dir: &Path,
ctx: &BuildContext,
obj_ext: &str,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = 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)

141
src/core/builder/qt.rs Normal file
View File

@@ -0,0 +1,141 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// 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 <https://www.gnu.org/licenses/>.
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<Option<PathBuf>, 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)),
}
}

View File

@@ -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<f64, String> {
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<f64, String> {
}
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<f64, String> {
};
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<f64, String> {
}
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<Vec<String>, 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<PathBuf>,
) -> Result<Vec<String>, String> {
let objects: Vec<String> = 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)
}

View File

@@ -1,128 +1,6 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// 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 <https://www.gnu.org/licenses/>.
#[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<String>,
tag: Option<String>,
rev: Option<String>,
},
Registry {
version: String,
features: Option<Vec<String>>,
},
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct DepSpec {
pub name: String,
pub source: DepSource,
pub include_raw: Option<Vec<String>>,
pub lib_raw: Option<Vec<String>>,
pub libs_raw: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default)]
pub struct ResolvedDeps {
pub include_dirs: Vec<String>,
pub lib_dirs: Vec<String>,
pub libs: Vec<String>,
}
#[allow(dead_code)]
pub fn resolve_path(project_root: &Path, raw: &str) -> Result<PathBuf, String> {
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<Vec<PathBuf>, 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(())
}

View File

@@ -1,33 +0,0 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// 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 <https://www.gnu.org/licenses/>.
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)
}

View File

@@ -15,10 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<String> {
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<String, String> {
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<String>) -> 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
}

View File

@@ -16,11 +16,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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;

View File

@@ -33,13 +33,7 @@ pub struct DcrConfig {
}
fn home_dir() -> Option<PathBuf> {
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<PathBuf> {

View File

@@ -15,8 +15,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod build_config;
pub mod builder;
pub mod config;
pub mod deps;
pub mod registry;
pub mod runner;

View File

@@ -38,13 +38,7 @@ pub struct RegistryManager {
}
fn home_dir() -> Option<PathBuf> {
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 {

View File

@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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};

View File

@@ -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" => {

View File

@@ -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);
}
}

31
src/platform/posix.rs Normal file
View File

@@ -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"),
}
}

View File

@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<String> {
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<String, String> {
pub fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
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));
}
}

View File

@@ -18,6 +18,24 @@
use std::io;
use std::path::{Path, PathBuf};
pub fn home_dir() -> Option<PathBuf> {
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<Vec<String>> {
let path: PathBuf = match dir {

View File

@@ -1,24 +1,6 @@
// DCR — Cargo-like C/C++ project manager.
//
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
//
// 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 <https://www.gnu.org/licenses/>.
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(())
}

View File

@@ -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
);
}

130
tests/cli_build.rs Normal file
View File

@@ -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"
);
}

176
tests/cli_deps.rs Normal file
View File

@@ -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"
);
}

66
tests/cli_lint.rs Normal file
View File

@@ -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");
}

75
tests/cli_new.rs Normal file
View File

@@ -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"));
}
}

79
tests/cli_qt.rs Normal file
View File

@@ -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"
);
}

30
tests/cli_test.rs Normal file
View File

@@ -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
);
}

58
tests/cli_workspace.rs Normal file
View File

@@ -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"
);
}

101
tests/common/mod.rs Normal file
View File

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