mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 19:59:36 +00:00
feat: add native Qt support and dcr lint command; refactor architecture and integration tests
This commit is contained in:
30
CHANGELOG.md
30
CHANGELOG.md
@@ -1,11 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## [0.7.4] - 2026-06-16 "Bare-Metal и Оптимизация Сборки / Bare-Metal & Build Optimization"
|
||||
## [0.7.4] - 2026-06-17 "Qt Поддержка, Linting и Рефакторинг Архитектуры / Qt Support, Linting, & Architecture Refactor"
|
||||
|
||||
### RU
|
||||
|
||||
**Добавлено:**
|
||||
|
||||
- **Нативная поддержка Qt** — добавлена автоматическая обработка мета-объектов (MOC, UIC, RCC) при включении `build.qt = true` в `dcr.toml`.
|
||||
- **Команда `dcr lint`** — статический анализ C/C++ через `clang-tidy`. Поддерживает `--fix` для автоматического исправления. Уважает `build.roots` и `build.src_disable` из конфига.
|
||||
- **Три новых ассемблерных бэкенда**:
|
||||
- **MASM** (`compiler = "ml"` / `"ml64"`) — Microsoft Macro Assembler.
|
||||
- **FASM** (`compiler = "fasm"`) — Flat Assembler.
|
||||
- **LLVM IR** (`compiler = "llc"`) — компиляция `.ll` → объектный файл.
|
||||
- **Общий ASM-пайплайн** — линковка и архивирование вынесены в `core/builder/asm.rs`. Новый ассемблерный бэкенд теперь пишется за ~30 строк.
|
||||
- **Утилиты для файловой системы** — `to_hex()` и `home_dir()` вынесены в `utils/fs.rs`.
|
||||
- **Утилиты для сборки** — `normalize_target()`, `normalize_kind()`, `normalize_platform()`, `default_profile_flags()` вынесены в `utils/build.rs`.
|
||||
- **Утилиты для бэкендов** — `asm_lang_flag()`, `source_extensions()`, `elapsed_secs()` добавлены в `core/builder/common.rs`.
|
||||
|
||||
- **Полноценная автоматизация Bare-Metal / Freestanding** — добавлен конфигурационный флаг `build.freestanding = true`. При его включении (или при обнаружении bare-metal таргета) DCR автоматически инжектирует флаг `-ffreestanding` на этапе компиляции, а также `-nostdlib` и `-static` на этапе линковки.
|
||||
- **Оптимизация артефактов (LTO и Strip)** — в `dcr.toml` добавлены параметры `build.lto` (авто-инжекция флага `-flto` для компилятора и линкера) и `build.strip` (автоматическое отсекание отладочной информации через флаг `-s` линкера).
|
||||
- **Контроль многопоточности сборки** — добавлен параметр `build.codegen-units`, позволяющий жестко ограничить максимальное количество параллельных потоков сборки для кастомного движка `parallel_build`.
|
||||
@@ -15,11 +26,25 @@
|
||||
|
||||
- **Интеллектуальные дефолтные флаги (DCR Defaults)** — генерация дефолтных флагов оптимизации (`-O3`/`-O0`), отладки (`-g`) и предупреждений (`-Wall -Wextra`) теперь автоматически отключается для bare-metal и freestanding целей, если пользователь переопределил `build.cflags`.
|
||||
- **Глобальный вынос bare-metal детектора** — функция `is_bare_metal_target` вынесена в общие утилиты `src/utils/build.rs` для сквозного использования в конвейере сборки.
|
||||
- **Рефакторинг архитектуры**:
|
||||
- `src/config.rs` переименован в `src/templates.rs`.
|
||||
- Начата консолидация конфигурационной логики и оркестрации сборки.
|
||||
|
||||
### EN
|
||||
|
||||
**Added:**
|
||||
|
||||
- **Native Qt support** — automatic meta-object handling (MOC, UIC, RCC) added when `build.qt = true` is set in `dcr.toml`.
|
||||
- **`dcr lint` command** — C/C++ static analysis via `clang-tidy`. Supports `--fix` for automatic fixes. Respects `build.roots` and `build.src_disable` from config.
|
||||
- **Three new assembler backends**:
|
||||
- **MASM** (`compiler = "ml"` / `"ml64"`) — Microsoft Macro Assembler.
|
||||
- **FASM** (`compiler = "fasm"`) — Flat Assembler.
|
||||
- **LLVM IR** (`compiler = "llc"`) — compiles `.ll` files via `llc -filetype=obj`.
|
||||
- **Shared ASM pipeline** — linking and archiving logic consolidated into `core/builder/asm.rs`. Adding a new assembler backend now takes ~30 lines.
|
||||
- **Filesystem utilities** — `to_hex()` and `home_dir()` extracted to `utils/fs.rs`.
|
||||
- **Build utilities** — `normalize_target()`, `normalize_kind()`, `normalize_platform()`, `default_profile_flags()` extracted to `utils/build.rs`.
|
||||
- **Backend helpers** — `asm_lang_flag()`, `source_extensions()`, `elapsed_secs()` added to `core/builder/common.rs`.
|
||||
|
||||
- **Full Bare-Metal / Freestanding Automation** — introduced the `build.freestanding = true` configuration option. When enabled (or when a bare-metal target is detected), DCR automatically injects `-ffreestanding` during compilation and both `-nostdlib` and `-static` during linking.
|
||||
- **Artifact Optimization (LTO & Strip)** — added `build.lto` (auto-injects `-flto` for compiler and linker) and `build.strip` (automatically strips debug symbols via linker `-s` flag) options to `dcr.toml`.
|
||||
- **Compilation Thread Control** — added the `build.codegen-units` option to strictly limit the maximum number of parallel jobs utilized by the custom `parallel_build` worker pool.
|
||||
@@ -29,6 +54,9 @@
|
||||
|
||||
- **Intelligent Default Flags Generation** — automated injection of fallback optimization (`-O3`/`-O0`), debug (`-g`), and warning (`-Wall -Wextra`) flags is now suppressed for bare-metal and freestanding builds if `build.cflags` are overridden.
|
||||
- **Centralized Bare-Metal Detection** — relocated the `is_bare_metal_target` helper to common build utilities (`src/utils/build.rs`) for unified access across the building core.
|
||||
- **Architectural Refactoring**:
|
||||
- Renamed `src/config.rs` to `src/templates.rs`.
|
||||
- Started consolidation of configuration and build orchestration logic.
|
||||
|
||||
## [0.7.3] - 2026-06-13 "Оптимизация Зависимостей и VCS / Dependency Optimization & VCS"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
135
src/cli/build.rs
135
src/cli/build.rs
@@ -17,16 +17,16 @@
|
||||
|
||||
use crate::cli::clean::clean;
|
||||
use crate::cli::flags::parse_build_run_flags;
|
||||
use crate::core::build_config::Config;
|
||||
use crate::core::builder::common;
|
||||
use crate::core::builder::{BuildContext, build as build_project, collect_sources};
|
||||
use crate::core::config::Config;
|
||||
use crate::core::deps::{register, resolve_deps};
|
||||
use crate::core::workspace::parse_workspace;
|
||||
use crate::utils::build::{
|
||||
default_target_triple, get_bool_with_profile, get_config_opt, get_config_str,
|
||||
get_language_with_profile, is_bare_metal_target, normalize_target_os, parse_version_info,
|
||||
prepend_clang_target_flag, profile_table, resolve_compiler, resolve_pkg_config_flags,
|
||||
resolve_tool,
|
||||
get_language_with_profile, is_bare_metal_target, normalize_kind, normalize_platform,
|
||||
normalize_target, normalize_target_os, parse_version_info, prepend_clang_target_flag,
|
||||
profile_table, resolve_compiler, resolve_pkg_config_flags, resolve_tool, substitute_vars,
|
||||
};
|
||||
use crate::utils::fs::{check_dir, find_project_root, with_dir};
|
||||
use crate::utils::log::error;
|
||||
@@ -633,6 +633,7 @@ fn build_project_at(
|
||||
let project_compiler =
|
||||
get_string_with_profile_and_target(&config, "compiler", profile, build_target);
|
||||
let build_language = get_language_with_profile(&config, profile)?;
|
||||
let build_qt = get_bool_with_profile(&config, "qt", profile, false);
|
||||
let build_standard =
|
||||
get_string_with_profile_and_target(&config, "standard", profile, build_target);
|
||||
let build_cxx_standard =
|
||||
@@ -930,21 +931,19 @@ fn build_project_at(
|
||||
}
|
||||
|
||||
let version_info = parse_version_info(&project_version);
|
||||
let substitute = |s: &str| -> String {
|
||||
s.replace("{profile}", profile)
|
||||
.replace("{name}", &project_name)
|
||||
.replace("{version}", &version_info.full)
|
||||
.replace("{version_major}", &version_info.major)
|
||||
.replace("{version_minor}", &version_info.minor)
|
||||
.replace("{version_patch}", &version_info.patch)
|
||||
.replace("{version_suffix}", &version_info.suffix)
|
||||
.replace("{version_suffix_dash}", &version_info.suffix_dash)
|
||||
};
|
||||
let resolved_cflags: Vec<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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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('"');
|
||||
|
||||
@@ -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
189
src/cli/lint.rs
Normal 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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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, "")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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
170
src/core/builder/asm.rs
Normal 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}")),
|
||||
}
|
||||
}
|
||||
@@ -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
47
src/core/builder/fasm.rs
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
43
src/core/builder/llvm_ir.rs
Normal file
43
src/core/builder/llvm_ir.rs
Normal 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
51
src/core/builder/masm.rs
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
141
src/core/builder/qt.rs
Normal 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)),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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" => {
|
||||
|
||||
@@ -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
31
src/platform/posix.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
130
tests/cli_build.rs
Normal 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
176
tests/cli_deps.rs
Normal 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
66
tests/cli_lint.rs
Normal 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
75
tests/cli_new.rs
Normal 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
79
tests/cli_qt.rs
Normal 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
30
tests/cli_test.rs
Normal 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
58
tests/cli_workspace.rs
Normal 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
101
tests/common/mod.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user