mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 19:19:41 +00:00
Compare commits
7 Commits
v0.8.2
...
bc308d6996
| Author | SHA1 | Date | |
|---|---|---|---|
| bc308d6996 | |||
| 09e7f5b880 | |||
| 3f925f437f | |||
| b9cbef43e9 | |||
| 794d571d58 | |||
| d2df85a064 | |||
| 5e8037da97 |
141
CHANGELOG.md
141
CHANGELOG.md
@@ -1,5 +1,111 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.8.4] - 2026-08-04 "Инфраструктура .dcr/ и интеграция с IDE / Internal .dcr/ Layout & IDE Integration"
|
||||||
|
|
||||||
|
### RU
|
||||||
|
|
||||||
|
**Добавлено:**
|
||||||
|
|
||||||
|
- **Директория `.dcr/`** — единый machine-readable источник фактов о сборке для IDE/расширений:
|
||||||
|
- `.dcr/compile_commands.json`
|
||||||
|
- `.dcr/build-info.json` (`schemaVersion`, `artifact_path`, `profile`, …)
|
||||||
|
- `.dcr/toolchain.json` (compiler/debugger paths, moc/uic/rcc)
|
||||||
|
- `.dcr/ide/` (заготовки lock-файлов для расширений)
|
||||||
|
- **`dcr gen project-info`** — поля `target`, `target_dir`, `artifact_path`, `artifact_kind`, `source_roots`, `include_globs`, `exclude_globs`; пути абсолютные/канонические.
|
||||||
|
- **`dcr build --print-artifact-path`** — после успешной сборки одна строка absolute path на stdout.
|
||||||
|
- **`dcr gen … --quiet` / `-q`** — без human-лога на stdout для `compile-commands` / `project-info`.
|
||||||
|
- **`.clangd`** — при первом `gen` создаётся с `CompilationDatabase: .dcr` (маркер `# managed by dcr gen`); ручной `.clangd` не перезаписывается.
|
||||||
|
- **Хелперы** `canonicalize_path`, `atomic_write`, `ensure_dcr_dir`; pure `resolve_artifact_path` через `platform::*`.
|
||||||
|
- **`dcr new` / `dcr init`** — `.dcr/` в `.gitignore`.
|
||||||
|
|
||||||
|
**Изменено (breaking):**
|
||||||
|
|
||||||
|
- **`compile_commands.json` перенесён** из корня проекта в **`.dcr/compile_commands.json`**.
|
||||||
|
clangd находит базу через `.clangd` / `clangd.arguments --compile-commands-dir=.dcr`.
|
||||||
|
CLion `misc.xml` и VSCode `settings.json` обновлены под новый путь.
|
||||||
|
- **Атомарная запись** `compile_commands.json` (tmp + rename).
|
||||||
|
- **`gen vscode` / `gen clion` launch/run** — `program`/`executable` через тот же artifact resolver, что `project-info` / `build-info.json` (Linux triple, Windows `.exe`, custom `target`/`filename`).
|
||||||
|
|
||||||
|
**Исправлено:**
|
||||||
|
|
||||||
|
- Launch-конфиги больше не указывают `target/<profile>/<name>` без triple на Linux.
|
||||||
|
- Симлинки/mount: root и пути в compile database канонизируются best-effort.
|
||||||
|
|
||||||
|
### EN
|
||||||
|
|
||||||
|
**Added:**
|
||||||
|
|
||||||
|
- **`.dcr/` directory** — shared machine-readable build facts for IDE integrations:
|
||||||
|
- `.dcr/compile_commands.json`
|
||||||
|
- `.dcr/build-info.json` (`schemaVersion`, `artifact_path`, `profile`, …)
|
||||||
|
- `.dcr/toolchain.json` (compiler/debugger paths, moc/uic/rcc)
|
||||||
|
- `.dcr/ide/` (placeholder for IDE lock files)
|
||||||
|
- **`dcr gen project-info`** — `target`, `target_dir`, `artifact_path`, `artifact_kind`, `source_roots`, `include_globs`, `exclude_globs`; absolute/canonical paths.
|
||||||
|
- **`dcr build --print-artifact-path`** — one absolute artifact path line on stdout after a successful build.
|
||||||
|
- **`dcr gen … --quiet` / `-q`** — suppress human success logs for gen subcommands.
|
||||||
|
- **`.clangd`** — created on first gen with `CompilationDatabase: .dcr` (`# managed by dcr gen`); custom `.clangd` is not overwritten.
|
||||||
|
- **Helpers** `canonicalize_path`, `atomic_write`, `ensure_dcr_dir`; pure `resolve_artifact_path` via `platform::*`.
|
||||||
|
- **`dcr new` / `dcr init`** — add `.dcr/` to `.gitignore`.
|
||||||
|
|
||||||
|
**Changed (breaking):**
|
||||||
|
|
||||||
|
- **`compile_commands.json` moved** from project root to **`.dcr/compile_commands.json`**.
|
||||||
|
clangd discovers it via `.clangd` / `--compile-commands-dir=.dcr`.
|
||||||
|
CLion and VSCode generators updated accordingly.
|
||||||
|
- **Atomic write** for compile_commands (tmp + rename).
|
||||||
|
- **`gen vscode` / `gen clion` launch/run** — use the same artifact path resolver as `project-info` / `build-info.json`.
|
||||||
|
|
||||||
|
**Fixed:**
|
||||||
|
|
||||||
|
- Launch configs no longer hardcode `target/<profile>/<name>` without the Linux triple segment.
|
||||||
|
- Symlink/mount roots: best-effort canonicalize for root and compile database paths.
|
||||||
|
|
||||||
|
## [0.8.3] - 2026-08-03 "Конфигурация host target, аргументы run и нативные пути артефактов / Host Target Config, Run Args & Native Artifact Paths"
|
||||||
|
|
||||||
|
### RU
|
||||||
|
|
||||||
|
**Добавлено:**
|
||||||
|
|
||||||
|
- **`dcr run -- <args>...`** — cargo-style: всё после bare `--` передаётся собранному бинарнику
|
||||||
|
(`dcr run --release -- --test_help`). Работает и для прямого запуска, и для `[run].cmd`.
|
||||||
|
- **Интеграционные / unit-тесты** — host target sections применяют `ldflags` без `--target`; проброс аргументов после `--`; парсер `split_double_dash`.
|
||||||
|
|
||||||
|
**Изменено:**
|
||||||
|
|
||||||
|
- **Native-сборка резолвит host triple** — без `--target` / без `build.target` конфиг-секции вида
|
||||||
|
`[build.x86_64-unknown-linux-gnu]` (и аналоги host) снова применяются (`cflags` / `ldflags` / toolchain).
|
||||||
|
Раньше target был «пустым», секции молча игнорировались, а артефакты уже лежали под host triple — линковка без `-L`/`-l`.
|
||||||
|
- **Пустой target** — `normalize_target_os("")` больше не warn'ит `Unknown target ''`.
|
||||||
|
- **`dcr run --help` / man `dcr-run`** — документирован разделитель `--` и forwarding args.
|
||||||
|
|
||||||
|
**Исправлено:**
|
||||||
|
|
||||||
|
- **Нативные пути артефактов на Windows и macOS** — без явного `--target` / `build.target` артефакты снова сохраняются в `target/<profile>`, поэтому `dcr run` и flat-bin находят результат сборки.
|
||||||
|
- Target-specific `ldflags` на native Linux/macOS/Windows (host triple) не доходили до линкера.
|
||||||
|
- Ложный warning на пустой target string.
|
||||||
|
|
||||||
|
### EN
|
||||||
|
|
||||||
|
**Added:**
|
||||||
|
|
||||||
|
- **`dcr run -- <args>...`** — cargo-style: arguments after a bare `--` are forwarded to the built binary
|
||||||
|
(`dcr run --release -- --test_help`). Works for direct binary runs and `[run].cmd`.
|
||||||
|
- **Tests** — host target sections apply `ldflags` without CLI `--target`; run-arg forwarding; `split_double_dash` unit tests.
|
||||||
|
|
||||||
|
**Changed:**
|
||||||
|
|
||||||
|
- **Native builds resolve the host triple** — without `--target` / without package `build.target`, host sections such as
|
||||||
|
`[build.x86_64-unknown-linux-gnu]` (and host equivalents) apply again (`cflags` / `ldflags` / toolchain).
|
||||||
|
Previously the target was empty, those sections were silently ignored while artifacts already used the host triple path — link failed without `-L`/`-l` (e.g. sandbox micro-lang + `libsct-elf`).
|
||||||
|
- **Empty target** — `normalize_target_os("")` no longer warns `Unknown target ''`.
|
||||||
|
- **`dcr run --help` / man `dcr-run`** — document `--` and argument forwarding.
|
||||||
|
|
||||||
|
**Fixed:**
|
||||||
|
|
||||||
|
- **Native artifact paths on Windows and macOS** — without an explicit `--target` / `build.target`, artifacts again use `target/<profile>`, so `dcr run` and flat-bin find the build output.
|
||||||
|
- Target-specific `ldflags` on native host builds not reaching the linker.
|
||||||
|
- Spurious warning for an empty target string.
|
||||||
|
|
||||||
## [0.8.2] - 2026-07-22 "CLI Progress, archive Feature, Workspace Path Fixes / CLI Progress, archive Feature, Workspace Path Fixes"
|
## [0.8.2] - 2026-07-22 "CLI Progress, archive Feature, Workspace Path Fixes / CLI Progress, archive Feature, Workspace Path Fixes"
|
||||||
|
|
||||||
### RU
|
### RU
|
||||||
@@ -215,25 +321,31 @@
|
|||||||
### RU
|
### RU
|
||||||
|
|
||||||
**Добавлено:**
|
**Добавлено:**
|
||||||
|
|
||||||
- Учтён `target_env` в `default_target_triple()` — Linux (gnu/musl) и Windows (msvc/gnu) теперь используют правильное окружение, а не хардкод.
|
- Учтён `target_env` в `default_target_triple()` — Linux (gnu/musl) и Windows (msvc/gnu) теперь используют правильное окружение, а не хардкод.
|
||||||
|
|
||||||
**Исправлено:**
|
**Исправлено:**
|
||||||
|
|
||||||
- **Сборка на macOS Apple Silicon падала с `_main` undefined** — `--target=` теперь передаётся и в ldflags, не только в cflags. `default_target_triple()` на macOS использует `std::env::consts::ARCH` вместо хардкода `x86_64`.
|
- **Сборка на macOS Apple Silicon падала с `_main` undefined** — `--target=` теперь передаётся и в ldflags, не только в cflags. `default_target_triple()` на macOS использует `std::env::consts::ARCH` вместо хардкода `x86_64`.
|
||||||
- **Бейджи GitHub Stars и GPL-3.0 не рендерились в README** — блок бейджей переведён на чистый HTML.
|
- **Бейджи GitHub Stars и GPL-3.0 не рендерились в README** — блок бейджей переведён на чистый HTML.
|
||||||
|
|
||||||
**Изменено:**
|
**Изменено:**
|
||||||
|
|
||||||
- Вычисление таргета по умолчанию вынесено в общую функцию `default_target_triple()` — устранено дублирование в build, run, clean.
|
- Вычисление таргета по умолчанию вынесено в общую функцию `default_target_triple()` — устранено дублирование в build, run, clean.
|
||||||
|
|
||||||
### EN
|
### EN
|
||||||
|
|
||||||
**Added:**
|
**Added:**
|
||||||
|
|
||||||
- `default_target_triple()` now respects `target_env` — Linux (gnu/musl) and Windows (msvc/gnu) use the correct environment instead of hardcoded values.
|
- `default_target_triple()` now respects `target_env` — Linux (gnu/musl) and Windows (msvc/gnu) use the correct environment instead of hardcoded values.
|
||||||
|
|
||||||
**Fixed:**
|
**Fixed:**
|
||||||
|
|
||||||
- **macOS Apple Silicon builds failed with `_main` undefined** — `--target=` is now passed to ldflags, not just cflags. `default_target_triple()` uses `std::env::consts::ARCH` on macOS instead of hardcoded `x86_64`.
|
- **macOS Apple Silicon builds failed with `_main` undefined** — `--target=` is now passed to ldflags, not just cflags. `default_target_triple()` uses `std::env::consts::ARCH` on macOS instead of hardcoded `x86_64`.
|
||||||
- **GitHub Stars and GPL-3.0 badges not rendering in README** — badge block converted to pure HTML.
|
- **GitHub Stars and GPL-3.0 badges not rendering in README** — badge block converted to pure HTML.
|
||||||
|
|
||||||
**Changed:**
|
**Changed:**
|
||||||
|
|
||||||
- Default target resolution extracted into shared `default_target_triple()` — removed duplication across build, run, clean.
|
- Default target resolution extracted into shared `default_target_triple()` — removed duplication across build, run, clean.
|
||||||
|
|
||||||
## [0.7.1] - 2026-06-02 "Мультиархитектурное расширение / The Multi-Arch Expansion"
|
## [0.7.1] - 2026-06-02 "Мультиархитектурное расширение / The Multi-Arch Expansion"
|
||||||
@@ -241,6 +353,7 @@
|
|||||||
### RU
|
### RU
|
||||||
|
|
||||||
**Добавлено:**
|
**Добавлено:**
|
||||||
|
|
||||||
- Вывод `--help` для всех команд с цветным форматированием.
|
- Вывод `--help` для всех команд с цветным форматированием.
|
||||||
- man-страницы (12 штук в `man/man1/`) и их установка во всех пакетах.
|
- man-страницы (12 штук в `man/man1/`) и их установка во всех пакетах.
|
||||||
- Валидация имен проектов в `dcr init` и `dcr new` (только ASCII буквы, цифры, `_` и `-`).
|
- Валидация имен проектов в `dcr init` и `dcr new` (только ASCII буквы, цифры, `_` и `-`).
|
||||||
@@ -251,6 +364,7 @@
|
|||||||
- Пакеты AUR (`dcr-dev`, `dcr-dev-bin`) и сборка Snap в CI.
|
- Пакеты AUR (`dcr-dev`, `dcr-dev-bin`) и сборка Snap в CI.
|
||||||
|
|
||||||
**Изменено:**
|
**Изменено:**
|
||||||
|
|
||||||
- Функция `validate_package_name` стала публичной.
|
- Функция `validate_package_name` стала публичной.
|
||||||
- Команды `gen`, `tree`, `fmt`, `setup` больше не игнорируют аргументы и используют цветной вывод.
|
- Команды `gen`, `tree`, `fmt`, `setup` больше не игнорируют аргументы и используют цветной вывод.
|
||||||
- Инструкция в README изменена с `| bash` на `| sh` для POSIX.
|
- Инструкция в README изменена с `| bash` на `| sh` для POSIX.
|
||||||
@@ -268,6 +382,7 @@
|
|||||||
### EN
|
### EN
|
||||||
|
|
||||||
**Added:**
|
**Added:**
|
||||||
|
|
||||||
- `--help` for all commands with styled headers and usage lines.
|
- `--help` for all commands with styled headers and usage lines.
|
||||||
- Man pages (12 troff man pages in `man/man1/`) and their installation across all packaging formats.
|
- Man pages (12 troff man pages in `man/man1/`) and their installation across all packaging formats.
|
||||||
- Project name validation in `dcr init` and `dcr new` (ASCII letters, digits, `_`, `-` only).
|
- Project name validation in `dcr init` and `dcr new` (ASCII letters, digits, `_`, `-` only).
|
||||||
@@ -278,6 +393,7 @@
|
|||||||
- AUR packages (`dcr-dev`, `dcr-dev-bin`) and Snap build in CI.
|
- AUR packages (`dcr-dev`, `dcr-dev-bin`) and Snap build in CI.
|
||||||
|
|
||||||
**Changed:**
|
**Changed:**
|
||||||
|
|
||||||
- `validate_package_name` made public and callable from CLI commands directly.
|
- `validate_package_name` made public and callable from CLI commands directly.
|
||||||
- `gen.rs`, `tree.rs`, `fmt.rs`, `setup.rs` no longer ignore arguments and use styled output.
|
- `gen.rs`, `tree.rs`, `fmt.rs`, `setup.rs` no longer ignore arguments and use styled output.
|
||||||
- README install instructions changed from `| bash` to `| sh` for POSIX compatibility.
|
- README install instructions changed from `| bash` to `| sh` for POSIX compatibility.
|
||||||
@@ -454,6 +570,7 @@ Added:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Produces `KERNEL.EFI` (works for `bin`, `staticlib`, and `sharedlib`).
|
Produces `KERNEL.EFI` (works for `bin`, `staticlib`, and `sharedlib`).
|
||||||
|
|
||||||
- Automatic injection of `--target=<build.target>` into compiler flags when `build.target` is set in `dcr.toml`. This greatly simplifies clang-based cross-compilation (especially for bare-metal targets like `aarch64-none-elf`).
|
- Automatic injection of `--target=<build.target>` into compiler flags when `build.target` is set in `dcr.toml`. This greatly simplifies clang-based cross-compilation (especially for bare-metal targets like `aarch64-none-elf`).
|
||||||
- Bare-metal targets (containing `none`, `-elf`, `eabi`, `baremetal`) no longer receive DCR's internal default flags (`-g`, `-Wall`, `-Wextra`, `-fno-omit-frame-pointer`, `-DDCR_DEBUG`, etc.). Prevents unwanted sections (`.comment`, debug info, etc.) that break custom linker scripts when `inherit = true`.
|
- Bare-metal targets (containing `none`, `-elf`, `eabi`, `baremetal`) no longer receive DCR's internal default flags (`-g`, `-Wall`, `-Wextra`, `-fno-omit-frame-pointer`, `-DDCR_DEBUG`, etc.). Prevents unwanted sections (`.comment`, debug info, etc.) that break custom linker scripts when `inherit = true`.
|
||||||
- `build.ldscript` in `dcr.toml` — linker script path passed as `-T <path>` to the linker. Essential for bare-metal/embedded/freestanding targets.
|
- `build.ldscript` in `dcr.toml` — linker script path passed as `-T <path>` to the linker. Essential for bare-metal/embedded/freestanding targets.
|
||||||
@@ -488,28 +605,28 @@ Fixed:
|
|||||||
|
|
||||||
- Path dependencies now work correctly: `dcr add` stores them as `{ path =
|
- Path dependencies now work correctly: `dcr add` stores them as `{ path =
|
||||||
"./..." }` tables, `is_registry_dep` properly distinguishes registry strings
|
"./..." }` tables, `is_registry_dep` properly distinguishes registry strings
|
||||||
from path/git strings, and `deps/mod.rs` resolves both table-form and
|
from path/git strings, and `deps/mod.rs` resolves both table-form and
|
||||||
legacy string-form path dependencies
|
legacy string-form path dependencies
|
||||||
- Registry dependency paths no longer hardcoded to `project_root/dcr-index`;
|
- Registry dependency paths no longer hardcoded to `project_root/dcr-index`;
|
||||||
now use `package_root_from_registry_info()` which resolves relative to the
|
now use `package_root_from_registry_info()` which resolves relative to the
|
||||||
registry cache root (`~/.dcr/`)
|
registry cache root (`~/.dcr/`)
|
||||||
- Registry dependencies are now actually built: `build_project_at()` is
|
- Registry dependencies are now actually built: `build_project_at()` is
|
||||||
called when `include_dir` or `lib_dir` is missing
|
called when `include_dir` or `lib_dir` is missing
|
||||||
|
|
||||||
Added:
|
Added:
|
||||||
|
|
||||||
- `SIGINT`/`Ctrl+C` handler via `ctrlc` crate — `dcr build` now checks
|
- `SIGINT`/`Ctrl+C` handler via `ctrlc` crate — `dcr build` now checks
|
||||||
`BUILD_INTERRUPTED` flag at key points and aborts cleanly
|
`BUILD_INTERRUPTED` flag at key points and aborts cleanly
|
||||||
- `utils/build.rs` — extracted shared utilities: `parse_version_info`,
|
- `utils/build.rs` — extracted shared utilities: `parse_version_info`,
|
||||||
`normalize_target_os`, `resolve_compiler`, `primary_language`,
|
`normalize_target_os`, `resolve_compiler`, `primary_language`,
|
||||||
`resolve_pkg_config_flags` and config helpers. Eliminates code duplication
|
`resolve_pkg_config_flags` and config helpers. Eliminates code duplication
|
||||||
between `cli/build.rs`, `cli/run.rs`, `cli/clean.rs`, `cli/gen.rs`
|
between `cli/build.rs`, `cli/run.rs`, `cli/clean.rs`, `cli/gen.rs`
|
||||||
- `utils/fs.rs::with_dir` — extracted common directory-scoped execution
|
- `utils/fs.rs::with_dir` — extracted common directory-scoped execution
|
||||||
- `run_command_sync_output` in `builder/common.rs` with global `OUTPUT_MUTEX`
|
- `run_command_sync_output` in `builder/common.rs` with global `OUTPUT_MUTEX`
|
||||||
— synchronized compiler output across all backends (unix_cc, gas, nasm, msvc)
|
— synchronized compiler output across all backends (unix_cc, gas, nasm, msvc)
|
||||||
- New helper functions in `deps/register.rs`: `get_registry_cache_root`,
|
- New helper functions in `deps/register.rs`: `get_registry_cache_root`,
|
||||||
`package_root_from_registry_info`, `registry_include_dir`, `registry_lib_dir`,
|
`package_root_from_registry_info`, `registry_include_dir`, `registry_lib_dir`,
|
||||||
`path_from_string_dep`
|
`path_from_string_dep`
|
||||||
- Unit tests for `register.rs` (`is_registry_dep`, `package_root_from_registry_info`),
|
- Unit tests for `register.rs` (`is_registry_dep`, `package_root_from_registry_info`),
|
||||||
`deps/mod.rs` (`path_dep_path`, `push_default_lib_dirs`),
|
`deps/mod.rs` (`path_dep_path`, `push_default_lib_dirs`),
|
||||||
`utils/build.rs` (`normalize_target_os`, `parse_version_info`)
|
`utils/build.rs` (`normalize_target_os`, `parse_version_info`)
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -154,7 +154,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dcr"
|
name = "dcr"
|
||||||
version = "0.8.2"
|
version = "0.8.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"fatfs",
|
"fatfs",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dcr"
|
name = "dcr"
|
||||||
version = "0.8.2"
|
version = "0.8.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
|
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
|
||||||
|
|||||||
@@ -4,6 +4,57 @@ sidebar_label: Changelog
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.8.4 (2026-08-04)
|
||||||
|
|
||||||
|
**Added:**
|
||||||
|
|
||||||
|
- **`.dcr/` directory** — shared machine-readable build facts for IDE integrations:
|
||||||
|
- `.dcr/compile_commands.json`
|
||||||
|
- `.dcr/build-info.json` (`schemaVersion`, `artifact_path`, `profile`, …)
|
||||||
|
- `.dcr/toolchain.json` (compiler/debugger paths, moc/uic/rcc)
|
||||||
|
- `.dcr/ide/` (placeholder for IDE lock files)
|
||||||
|
- **`dcr gen project-info`** — `target`, `target_dir`, `artifact_path`, `artifact_kind`, `source_roots`, `include_globs`, `exclude_globs`; absolute/canonical paths.
|
||||||
|
- **`dcr build --print-artifact-path`** — one absolute artifact path line on stdout after a successful build.
|
||||||
|
- **`dcr gen … --quiet` / `-q`** — suppress human success logs for gen subcommands.
|
||||||
|
- **`.clangd`** — created on first gen with `CompilationDatabase: .dcr` (`# managed by dcr gen`); custom `.clangd` is not overwritten.
|
||||||
|
- **Helpers** `canonicalize_path`, `atomic_write`, `ensure_dcr_dir`; pure `resolve_artifact_path` via `platform::*`.
|
||||||
|
- **`dcr new` / `dcr init`** — add `.dcr/` to `.gitignore`.
|
||||||
|
|
||||||
|
**Changed (breaking):**
|
||||||
|
|
||||||
|
- **`compile_commands.json` moved** from project root to **`.dcr/compile_commands.json`**.
|
||||||
|
clangd discovers it via `.clangd` / `--compile-commands-dir=.dcr`.
|
||||||
|
CLion and VSCode generators updated accordingly.
|
||||||
|
- **Atomic write** for compile_commands (tmp + rename).
|
||||||
|
- **`gen vscode` / `gen clion` launch/run** — use the same artifact path resolver as `project-info` / `build-info.json`.
|
||||||
|
|
||||||
|
**Fixed:**
|
||||||
|
|
||||||
|
- Launch configs no longer hardcode `target/<profile>/<name>` without the Linux triple segment.
|
||||||
|
- Symlink/mount roots: best-effort canonicalize for root and compile database paths.
|
||||||
|
|
||||||
|
## 0.8.3 (2026-08-03)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`dcr run -- <args>...`** — cargo-style argument forwarding after a bare `--`
|
||||||
|
(`dcr run --release -- --test_help`). Applies to direct binary runs and `[run].cmd`.
|
||||||
|
- **Tests** — host target section `ldflags` without CLI `--target`; run-arg forwarding; `split_double_dash` unit tests.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Native builds resolve the host triple** — without `--target` / package `build.target`, host sections such as
|
||||||
|
`[build.x86_64-unknown-linux-gnu]` apply again (`cflags` / `ldflags` / toolchain).
|
||||||
|
Previously the target was empty and those sections were ignored while artifacts already used the host triple path.
|
||||||
|
- **Empty target** — no more `Unknown target ''` warning.
|
||||||
|
- **`dcr run --help` / man `dcr-run`** — document `--` forwarding.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Native artifact paths on Windows and macOS** — without an explicit `--target` / `build.target`, artifacts use `target/<profile>`, so `dcr run` and flat-bin find the build output.
|
||||||
|
- Target-specific `ldflags` missing on native host builds (e.g. micro-lang + `libsct-elf`).
|
||||||
|
- Spurious empty-target warning.
|
||||||
|
|
||||||
## 0.8.2 (2026-07-22)
|
## 0.8.2 (2026-07-22)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.TH DCR-BUILD 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
|
.TH DCR-BUILD 1 "2026-08-04" "dcr 0.8.4" "DCR Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
dcr-build \- Compile a DCR project
|
dcr-build \- Compile a DCR project
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -9,8 +9,15 @@ dcr-build \- Compile a DCR project
|
|||||||
[\fB\-\-clean\fR]
|
[\fB\-\-clean\fR]
|
||||||
[\fB\-\-verbose\fR]
|
[\fB\-\-verbose\fR]
|
||||||
[\fB\-\-workspace\fR \fI<name>\fR]
|
[\fB\-\-workspace\fR \fI<name>\fR]
|
||||||
|
[\fB\-\-print\-artifact\-path\fR]
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
Compiles the project. Default profile is \fB\-\-debug\fR.
|
Compiles the project. Default profile is
|
||||||
|
.B \-\-debug .
|
||||||
|
On success, updates
|
||||||
|
.B .dcr/build-info.json
|
||||||
|
and
|
||||||
|
.B .dcr/toolchain.json
|
||||||
|
when applicable.
|
||||||
.SH OPTIONS
|
.SH OPTIONS
|
||||||
.TP
|
.TP
|
||||||
.B \-\-debug
|
.B \-\-debug
|
||||||
@@ -33,5 +40,9 @@ Print detailed build output including compiler commands.
|
|||||||
.TP
|
.TP
|
||||||
.B \-\-workspace \fI<name>\fR
|
.B \-\-workspace \fI<name>\fR
|
||||||
Build a specific workspace member.
|
Build a specific workspace member.
|
||||||
|
.TP
|
||||||
|
.B \-\-print\-artifact\-path
|
||||||
|
After a successful build, print the absolute path of the main artifact as a single line on stdout.
|
||||||
.SH SEE ALSO
|
.SH SEE ALSO
|
||||||
.BR dcr (1)
|
.BR dcr (1),
|
||||||
|
.BR dcr-gen (1)
|
||||||
|
|||||||
@@ -1,23 +1,67 @@
|
|||||||
.TH DCR-GEN 1 "2026-06-02" "dcr 0.7.0" "DCR Manual"
|
.TH DCR-GEN 1 "2026-08-04" "dcr 0.8.4" "DCR Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
dcr-gen \- Generate IDE and tooling integration files
|
dcr-gen \- Generate IDE and tooling integration files
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
.B dcr gen \fI<subcommand>\fR
|
.B dcr gen \fI<subcommand>\fR
|
||||||
|
[\fB\-\-debug\fR | \fB\-\-release\fR]
|
||||||
|
[\fB\-\-quiet\fR | \fB\-q\fR]
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
Generates IDE and tooling integration files for DCR projects.
|
Generates IDE and tooling integration files for DCR projects.
|
||||||
|
Machine-readable build facts are written under
|
||||||
|
.B .dcr/
|
||||||
|
(compile database, build-info, toolchain).
|
||||||
.SH SUBCOMMANDS
|
.SH SUBCOMMANDS
|
||||||
.TP
|
.TP
|
||||||
.B project-info
|
.B project-info
|
||||||
Print project metadata as JSON to stdout.
|
Print project metadata as JSON to stdout (absolute paths, artifact_path, target_dir).
|
||||||
|
Also updates
|
||||||
|
.B .dcr/build-info.json
|
||||||
|
and
|
||||||
|
.B .dcr/toolchain.json .
|
||||||
.TP
|
.TP
|
||||||
.B compile-commands
|
.B compile-commands
|
||||||
Generate \fBcompile_commands.json\fR for IDE tooling.
|
Generate
|
||||||
|
.B .dcr/compile_commands.json
|
||||||
|
(atomic write) and ensure
|
||||||
|
.B .clangd
|
||||||
|
points at
|
||||||
|
.B .dcr
|
||||||
|
as CompilationDatabase.
|
||||||
.TP
|
.TP
|
||||||
.B vscode
|
.B vscode
|
||||||
Generate \fB.vscode/\fR integration files (tasks, launch configurations).
|
Generate
|
||||||
|
.B .vscode/
|
||||||
|
integration files (tasks, launch configurations) and compile commands under
|
||||||
|
.B .dcr/ .
|
||||||
.TP
|
.TP
|
||||||
.B clion
|
.B clion
|
||||||
Generate \fB.idea/\fR integration files for CLion.
|
Generate
|
||||||
|
.B .idea/
|
||||||
|
integration files for CLion and compile commands under
|
||||||
|
.B .dcr/ .
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-\-debug
|
||||||
|
Use debug profile (default).
|
||||||
|
.TP
|
||||||
|
.B \-\-release
|
||||||
|
Use release profile.
|
||||||
|
.TP
|
||||||
|
.B \-\-quiet\fR, \fB\-q
|
||||||
|
Suppress human-readable success messages on stdout.
|
||||||
|
.SH FILES
|
||||||
|
.TP
|
||||||
|
.B .dcr/compile_commands.json
|
||||||
|
JSON Compilation Database (moved from project root; breaking change in 0.8.4).
|
||||||
|
.TP
|
||||||
|
.B .dcr/build-info.json
|
||||||
|
Last known artifact path, profile, compiler.
|
||||||
|
.TP
|
||||||
|
.B .dcr/toolchain.json
|
||||||
|
Resolved compiler/debugger (and Qt tools if configured).
|
||||||
|
.TP
|
||||||
|
.B .clangd
|
||||||
|
Created once with CompilationDatabase: .dcr; not overwritten if customized.
|
||||||
.SH SEE ALSO
|
.SH SEE ALSO
|
||||||
.BR dcr (1),
|
.BR dcr (1),
|
||||||
.BR dcr-build (1)
|
.BR dcr-build (1)
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ dcr-run \- Build and run a DCR project
|
|||||||
[\fB\-\-force\fR]
|
[\fB\-\-force\fR]
|
||||||
[\fB\-\-clean\fR]
|
[\fB\-\-clean\fR]
|
||||||
[\fB\-\-verbose\fR]
|
[\fB\-\-verbose\fR]
|
||||||
|
[\fB\-\-\fR \fI<args>...\fR]
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
Builds and runs the project. Only available for projects with
|
Builds and runs the project. Only available for projects with
|
||||||
\fBkind = "bin"\fR.
|
\fBkind = "bin"\fR.
|
||||||
|
Arguments after a bare
|
||||||
|
.B \-\-
|
||||||
|
are forwarded to the built binary (cargo-style).
|
||||||
.SH OPTIONS
|
.SH OPTIONS
|
||||||
.TP
|
.TP
|
||||||
.B \-\-debug
|
.B \-\-debug
|
||||||
@@ -30,6 +34,14 @@ Clean before building.
|
|||||||
.TP
|
.TP
|
||||||
.B \-\-verbose
|
.B \-\-verbose
|
||||||
Print detailed build output.
|
Print detailed build output.
|
||||||
|
.TP
|
||||||
|
.B \-\- \fI<args>...\fR
|
||||||
|
Arguments passed to the built binary.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.nf
|
||||||
|
dcr run --release -- --test_help
|
||||||
|
dcr run -- --foo bar
|
||||||
|
.fi
|
||||||
.SH SEE ALSO
|
.SH SEE ALSO
|
||||||
.BR dcr (1),
|
.BR dcr (1),
|
||||||
.BR dcr-build (1)
|
.BR dcr-build (1)
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ Version from registry (e.g. \fB1.2.3\fR).
|
|||||||
Print project metadata as JSON to stdout.
|
Print project metadata as JSON to stdout.
|
||||||
.TP
|
.TP
|
||||||
.B compile-commands
|
.B compile-commands
|
||||||
Generate \fBcompile_commands.json\fR for IDE tooling.
|
Generate \fB.dcr/compile_commands.json\fR for IDE tooling.
|
||||||
.TP
|
.TP
|
||||||
.B vscode
|
.B vscode
|
||||||
Generate \fB.vscode/\fR integration files (tasks, launch configurations).
|
Generate \fB.vscode/\fR integration files (tasks, launch configurations).
|
||||||
|
|||||||
160
src/cli/build.rs
160
src/cli/build.rs
@@ -17,13 +17,20 @@
|
|||||||
|
|
||||||
use crate::cli::clean::clean;
|
use crate::cli::clean::clean;
|
||||||
use crate::cli::flags::parse_build_run_flags;
|
use crate::cli::flags::parse_build_run_flags;
|
||||||
|
use crate::cli::r#gen::{ProjectInfo, write_dcr_metadata};
|
||||||
|
use crate::core::build::builder::artifact::{absolute_artifact_path, resolve_artifact_path};
|
||||||
use crate::core::build::common;
|
use crate::core::build::common;
|
||||||
use crate::core::build::{BuildEvent, BuildReporter, BuildRequest, run_build};
|
use crate::core::build::{BuildEvent, BuildReporter, BuildRequest, run_build};
|
||||||
use crate::core::build_config::Config;
|
use crate::core::build_config::Config;
|
||||||
use crate::utils::fs::find_project_root;
|
use crate::utils::build::{
|
||||||
|
get_config_opt, get_config_str, get_language_with_profile_or_default, get_string_with_profile,
|
||||||
|
normalize_kind, resolve_artifact_target_dir, resolve_compiler,
|
||||||
|
};
|
||||||
|
use crate::utils::fs::{canonicalize_path, find_project_root};
|
||||||
use crate::utils::log::error;
|
use crate::utils::log::error;
|
||||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
|
||||||
use std::io::IsTerminal;
|
use std::io::IsTerminal;
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::{Arc, atomic::AtomicBool};
|
use std::sync::{Arc, atomic::AtomicBool};
|
||||||
|
|
||||||
pub use crate::core::build::get_build_string_with_profile;
|
pub use crate::core::build::get_build_string_with_profile;
|
||||||
@@ -109,7 +116,7 @@ pub fn build(args: &[String]) -> i32 {
|
|||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
printc(
|
printc(
|
||||||
" dcr build [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose]",
|
" dcr build [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [--print-artifact-path]",
|
||||||
BOLD_CYAN,
|
BOLD_CYAN,
|
||||||
);
|
);
|
||||||
println!();
|
println!();
|
||||||
@@ -124,6 +131,9 @@ pub fn build(args: &[String]) -> i32 {
|
|||||||
println!(" --clean Clean before building");
|
println!(" --clean Clean before building");
|
||||||
println!(" --verbose Print detailed build output");
|
println!(" --verbose Print detailed build output");
|
||||||
println!(" --workspace <name> Build a specific workspace member");
|
println!(" --workspace <name> Build a specific workspace member");
|
||||||
|
println!(
|
||||||
|
" --print-artifact-path Print absolute main artifact path on stdout after success"
|
||||||
|
);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +151,7 @@ pub fn build(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let root = match find_project_root(&start_dir) {
|
let root = match find_project_root(&start_dir) {
|
||||||
Ok(Some(dir)) => dir,
|
Ok(Some(dir)) => canonicalize_path(&dir),
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
error("dcr.toml file not found");
|
error("dcr.toml file not found");
|
||||||
return 1;
|
return 1;
|
||||||
@@ -174,16 +184,33 @@ pub fn build(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let req = BuildRequest {
|
let req = BuildRequest {
|
||||||
profile: flags.profile,
|
profile: flags.profile.clone(),
|
||||||
target: flags.target,
|
target: flags.target.clone(),
|
||||||
force: flags.force,
|
force: flags.force,
|
||||||
verbose: flags.verbose,
|
verbose: flags.verbose,
|
||||||
workspace: flags.workspace,
|
workspace: flags.workspace.clone(),
|
||||||
cancel,
|
cancel,
|
||||||
};
|
};
|
||||||
let mut reporter = CliReporter;
|
let mut reporter = CliReporter;
|
||||||
match run_build(&root, &req, &mut reporter) {
|
match run_build(&root, &req, &mut reporter) {
|
||||||
Ok(_) => 0,
|
Ok(_) => {
|
||||||
|
if let Some(info) =
|
||||||
|
resolve_post_build_info(&root, &flags.profile, flags.target.as_deref())
|
||||||
|
{
|
||||||
|
let _ = write_dcr_metadata(&root, &info);
|
||||||
|
if flags.print_artifact_path
|
||||||
|
&& let Some(ref path) = info.artifact_path
|
||||||
|
{
|
||||||
|
println!("{path}");
|
||||||
|
}
|
||||||
|
} else if flags.print_artifact_path
|
||||||
|
&& let Some(path) =
|
||||||
|
fallback_artifact_path(&root, &flags.profile, flags.target.as_deref())
|
||||||
|
{
|
||||||
|
println!("{path}");
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
common::finish_progress_line();
|
common::finish_progress_line();
|
||||||
error(&err.message);
|
error(&err.message);
|
||||||
@@ -191,3 +218,122 @@ pub fn build(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_post_build_info(
|
||||||
|
root: &Path,
|
||||||
|
profile: &str,
|
||||||
|
cli_target: Option<&str>,
|
||||||
|
) -> Option<ProjectInfo> {
|
||||||
|
let config = Config::open(root.join("dcr.toml").to_str()?).ok()?;
|
||||||
|
if config.is_workspace_only() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let name = get_config_str(&config, "package.name");
|
||||||
|
let version = get_config_str(&config, "package.version");
|
||||||
|
let language = get_language_with_profile_or_default(&config, profile);
|
||||||
|
let standard = get_string_with_profile(&config, "standard", profile);
|
||||||
|
let cxx_standard = get_string_with_profile(&config, "cxx_standard", profile);
|
||||||
|
let compiler_s = get_string_with_profile(&config, "compiler", profile);
|
||||||
|
let kind = normalize_kind(&get_string_with_profile(&config, "kind", profile)).to_string();
|
||||||
|
let build_target = cli_target
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.unwrap_or_else(|| get_string_with_profile(&config, "target", profile));
|
||||||
|
let output_filename_s = get_string_with_profile(&config, "filename", profile);
|
||||||
|
let output_extension_s = get_string_with_profile(&config, "extension", profile);
|
||||||
|
let output_filename = if output_filename_s.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(output_filename_s)
|
||||||
|
};
|
||||||
|
let output_extension = if output_extension_s.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(output_extension_s)
|
||||||
|
};
|
||||||
|
let tc_cc = get_config_opt(&config, "toolchain.cc");
|
||||||
|
let tc_cxx = get_config_opt(&config, "toolchain.cxx");
|
||||||
|
let tc_as = get_config_opt(&config, "toolchain.as");
|
||||||
|
let resolved_compiler = resolve_compiler(
|
||||||
|
&language,
|
||||||
|
&compiler_s,
|
||||||
|
tc_cc.as_deref(),
|
||||||
|
tc_cxx.as_deref(),
|
||||||
|
tc_as.as_deref(),
|
||||||
|
);
|
||||||
|
let out_dir = get_string_with_profile(&config, "out_dir", profile);
|
||||||
|
let has_explicit = cli_target.is_some_and(|t| !t.is_empty()) || !build_target.trim().is_empty();
|
||||||
|
let target_dir =
|
||||||
|
resolve_artifact_target_dir(root, None, profile, &build_target, &out_dir, has_explicit);
|
||||||
|
let rel = resolve_artifact_path(
|
||||||
|
&kind,
|
||||||
|
profile,
|
||||||
|
&name,
|
||||||
|
Some(&target_dir),
|
||||||
|
output_filename.as_deref(),
|
||||||
|
output_extension.as_deref(),
|
||||||
|
)?;
|
||||||
|
let artifact_path = absolute_artifact_path(root, &rel)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
let abs_target_dir = {
|
||||||
|
let p = Path::new(&target_dir);
|
||||||
|
let path = if p.is_absolute() {
|
||||||
|
canonicalize_path(p)
|
||||||
|
} else {
|
||||||
|
canonicalize_path(&root.join(p))
|
||||||
|
};
|
||||||
|
path.to_string_lossy().into_owned()
|
||||||
|
};
|
||||||
|
Some(ProjectInfo {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
root: root.to_path_buf(),
|
||||||
|
profile: profile.to_string(),
|
||||||
|
language,
|
||||||
|
standard,
|
||||||
|
cxx_standard,
|
||||||
|
compiler: resolved_compiler,
|
||||||
|
kind: kind.clone(),
|
||||||
|
target: build_target,
|
||||||
|
target_dir: Some(abs_target_dir),
|
||||||
|
artifact_path: Some(artifact_path),
|
||||||
|
artifact_kind: kind,
|
||||||
|
sources: vec![],
|
||||||
|
include_dirs: vec![],
|
||||||
|
lib_dirs: vec![],
|
||||||
|
libs: vec![],
|
||||||
|
cflags: vec![],
|
||||||
|
ldflags: vec![],
|
||||||
|
source_roots: vec![],
|
||||||
|
include_globs: vec![],
|
||||||
|
exclude_globs: vec![],
|
||||||
|
output_filename,
|
||||||
|
output_extension,
|
||||||
|
out_dir,
|
||||||
|
debugger: "lldb".to_string(),
|
||||||
|
moc: None,
|
||||||
|
uic: None,
|
||||||
|
rcc: None,
|
||||||
|
workspace_root: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_artifact_path(root: &Path, profile: &str, target: Option<&str>) -> Option<String> {
|
||||||
|
let config = Config::open(root.join("dcr.toml").to_str()?).ok()?;
|
||||||
|
let name = get_config_str(&config, "package.name");
|
||||||
|
if name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let build_target = target.unwrap_or("").to_string();
|
||||||
|
let out_dir = get_string_with_profile(&config, "out_dir", profile);
|
||||||
|
let has_explicit = !build_target.trim().is_empty();
|
||||||
|
let target_dir =
|
||||||
|
resolve_artifact_target_dir(root, None, profile, &build_target, &out_dir, has_explicit);
|
||||||
|
let rel = resolve_artifact_path("bin", profile, &name, Some(&target_dir), None, None)?;
|
||||||
|
Some(
|
||||||
|
absolute_artifact_path(root, &rel)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,16 +25,27 @@ pub struct BuildRunFlags {
|
|||||||
pub force: bool,
|
pub force: bool,
|
||||||
pub clean: bool,
|
pub clean: bool,
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
|
pub print_artifact_path: bool,
|
||||||
|
pub bin_args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn split_double_dash(args: &[String]) -> (&[String], &[String]) {
|
||||||
|
match args.iter().position(|a| a == "--") {
|
||||||
|
Some(i) => (&args[..i], &args[i + 1..]),
|
||||||
|
None => (args, &[]),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
||||||
|
let (dcr_args, bin_args_slice) = split_double_dash(args);
|
||||||
let mut profile = PROFILE.to_string();
|
let mut profile = PROFILE.to_string();
|
||||||
let mut target = None;
|
let mut target = None;
|
||||||
let mut workspace = None;
|
let mut workspace = None;
|
||||||
let mut force = false;
|
let mut force = false;
|
||||||
let mut clean = false;
|
let mut clean = false;
|
||||||
let mut verbose = false;
|
let mut verbose = false;
|
||||||
let mut iter = args.iter();
|
let mut print_artifact_path = false;
|
||||||
|
let mut iter = dcr_args.iter();
|
||||||
|
|
||||||
while let Some(arg) = iter.next() {
|
while let Some(arg) = iter.next() {
|
||||||
if !arg.starts_with("--") {
|
if !arg.starts_with("--") {
|
||||||
@@ -54,6 +65,10 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
|||||||
verbose = true;
|
verbose = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if candidate == "print-artifact-path" {
|
||||||
|
print_artifact_path = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if candidate == "workspace" {
|
if candidate == "workspace" {
|
||||||
if let Some(w) = iter.next() {
|
if let Some(w) = iter.next() {
|
||||||
workspace = Some(w.clone());
|
workspace = Some(w.clone());
|
||||||
@@ -91,5 +106,39 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
|||||||
force,
|
force,
|
||||||
clean,
|
clean,
|
||||||
verbose,
|
verbose,
|
||||||
|
print_artifact_path,
|
||||||
|
bin_args: bin_args_slice.to_vec(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn s(args: &[&str]) -> Vec<String> {
|
||||||
|
args.iter().map(|a| a.to_string()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_double_dash_empty_tail() {
|
||||||
|
let args = s(&["--release", "--force"]);
|
||||||
|
let (left, right) = split_double_dash(&args);
|
||||||
|
assert_eq!(left, &s(&["--release", "--force"]));
|
||||||
|
assert!(right.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_double_dash_with_bin_args() {
|
||||||
|
let args = s(&["--release", "--", "--test_help", "x"]);
|
||||||
|
let (left, right) = split_double_dash(&args);
|
||||||
|
assert_eq!(left, &s(&["--release"]));
|
||||||
|
assert_eq!(right, &s(&["--test_help", "x"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_run_flags_forwards_after_double_dash() {
|
||||||
|
let flags = parse_build_run_flags(&s(&["--release", "--", "--test_help"])).unwrap();
|
||||||
|
assert_eq!(flags.profile, "release");
|
||||||
|
assert_eq!(flags.bin_args, s(&["--test_help"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
595
src/cli/gen.rs
595
src/cli/gen.rs
@@ -16,6 +16,7 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::core::build::builder::BuildContext;
|
use crate::core::build::builder::BuildContext;
|
||||||
|
use crate::core::build::builder::artifact::{absolute_artifact_path, resolve_artifact_path};
|
||||||
use crate::core::build::builder::collect_sources;
|
use crate::core::build::builder::collect_sources;
|
||||||
use crate::core::build::common;
|
use crate::core::build::common;
|
||||||
use crate::core::build_config::Config;
|
use crate::core::build_config::Config;
|
||||||
@@ -23,12 +24,15 @@ use crate::core::workspace::parse_workspace;
|
|||||||
use crate::utils::build::{
|
use crate::utils::build::{
|
||||||
get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile_or_default,
|
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_kind, normalize_platform,
|
get_list_with_profile, get_string_with_profile, normalize_kind, normalize_platform,
|
||||||
normalize_target, resolve_compiler, resolve_pkg_config_flags_lossy,
|
resolve_artifact_target_dir, resolve_compiler, resolve_pkg_config_flags_lossy,
|
||||||
|
};
|
||||||
|
use crate::utils::fs::{
|
||||||
|
absolute_join, atomic_write, canonicalize_path, ensure_dcr_dir, find_project_root,
|
||||||
};
|
};
|
||||||
use crate::utils::fs::find_project_root;
|
|
||||||
use crate::utils::log::error;
|
use crate::utils::log::error;
|
||||||
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
/// Like `deps::resolve_deps` but does NOT require lib directories to exist.
|
/// Like `deps::resolve_deps` but does NOT require lib directories to exist.
|
||||||
/// Used by `gen` commands where the project may not have been built yet.
|
/// Used by `gen` commands where the project may not have been built yet.
|
||||||
@@ -164,7 +168,6 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) ->
|
|||||||
|
|
||||||
// ── public API ───────────────────────────────────────────────────────────────
|
// ── public API ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Everything needed to generate output for one project member.
|
|
||||||
pub struct ProjectInfo {
|
pub struct ProjectInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
@@ -175,12 +178,27 @@ pub struct ProjectInfo {
|
|||||||
pub cxx_standard: String,
|
pub cxx_standard: String,
|
||||||
pub compiler: String,
|
pub compiler: String,
|
||||||
pub kind: String,
|
pub kind: String,
|
||||||
|
pub target: String,
|
||||||
|
pub target_dir: Option<String>,
|
||||||
|
pub artifact_path: Option<String>,
|
||||||
|
pub artifact_kind: String,
|
||||||
pub sources: Vec<String>,
|
pub sources: Vec<String>,
|
||||||
pub include_dirs: Vec<String>,
|
pub include_dirs: Vec<String>,
|
||||||
pub lib_dirs: Vec<String>,
|
pub lib_dirs: Vec<String>,
|
||||||
pub libs: Vec<String>,
|
pub libs: Vec<String>,
|
||||||
pub cflags: Vec<String>,
|
pub cflags: Vec<String>,
|
||||||
pub ldflags: Vec<String>,
|
pub ldflags: Vec<String>,
|
||||||
|
pub source_roots: Vec<String>,
|
||||||
|
pub include_globs: Vec<String>,
|
||||||
|
pub exclude_globs: Vec<String>,
|
||||||
|
pub output_filename: Option<String>,
|
||||||
|
pub output_extension: Option<String>,
|
||||||
|
pub out_dir: String,
|
||||||
|
pub debugger: String,
|
||||||
|
pub moc: Option<String>,
|
||||||
|
pub uic: Option<String>,
|
||||||
|
pub rcc: Option<String>,
|
||||||
|
pub workspace_root: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── entry-point for `dcr gen` ────────────────────────────────────────────────
|
// ── entry-point for `dcr gen` ────────────────────────────────────────────────
|
||||||
@@ -196,9 +214,13 @@ pub fn r#gen(args: &[String]) -> i32 {
|
|||||||
println!();
|
println!();
|
||||||
printc("SUBCOMMANDS:", BOLD_GREEN);
|
printc("SUBCOMMANDS:", BOLD_GREEN);
|
||||||
println!(" project-info Print project metadata as JSON");
|
println!(" project-info Print project metadata as JSON");
|
||||||
println!(" compile-commands Generate compile_commands.json");
|
println!(" compile-commands Generate .dcr/compile_commands.json");
|
||||||
println!(" vscode Generate .vscode/ integration files");
|
println!(" vscode Generate .vscode/ integration files");
|
||||||
println!(" clion Generate .idea/ integration files");
|
println!(" clion Generate .idea/ integration files");
|
||||||
|
println!();
|
||||||
|
printc("OPTIONS:", BOLD_GREEN);
|
||||||
|
println!(" --debug | --release Profile (default: debug)");
|
||||||
|
println!(" --quiet | -q Suppress success messages on stdout");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
Some(s) => s.as_str(),
|
Some(s) => s.as_str(),
|
||||||
@@ -208,7 +230,7 @@ pub fn r#gen(args: &[String]) -> i32 {
|
|||||||
println!();
|
println!();
|
||||||
printc("SUBCOMMANDS:", BOLD_GREEN);
|
printc("SUBCOMMANDS:", BOLD_GREEN);
|
||||||
println!(" project-info Print project metadata as JSON");
|
println!(" project-info Print project metadata as JSON");
|
||||||
println!(" compile-commands Generate compile_commands.json");
|
println!(" compile-commands Generate .dcr/compile_commands.json");
|
||||||
println!(" vscode Generate .vscode/ integration files");
|
println!(" vscode Generate .vscode/ integration files");
|
||||||
println!(" clion Generate .idea/ integration files");
|
println!(" clion Generate .idea/ integration files");
|
||||||
return 1;
|
return 1;
|
||||||
@@ -231,16 +253,24 @@ pub fn r#gen(args: &[String]) -> i32 {
|
|||||||
|
|
||||||
// ── shared: collect per-project data ─────────────────────────────────────────
|
// ── shared: collect per-project data ─────────────────────────────────────────
|
||||||
|
|
||||||
fn collect_project_info(root: &Path, profile: &str) -> Result<ProjectInfo, String> {
|
fn collect_project_info(
|
||||||
// Run from the project root so relative paths in dcr.toml resolve correctly.
|
root: &Path,
|
||||||
|
profile: &str,
|
||||||
|
workspace_root: Option<&Path>,
|
||||||
|
) -> Result<ProjectInfo, String> {
|
||||||
let prev = std::env::current_dir().map_err(|e| e.to_string())?;
|
let prev = std::env::current_dir().map_err(|e| e.to_string())?;
|
||||||
std::env::set_current_dir(root).map_err(|e| e.to_string())?;
|
std::env::set_current_dir(root).map_err(|e| e.to_string())?;
|
||||||
let result = collect_project_info_inner(root, profile);
|
let result = collect_project_info_inner(root, profile, workspace_root);
|
||||||
let _ = std::env::set_current_dir(prev);
|
let _ = std::env::set_current_dir(prev);
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo, String> {
|
fn collect_project_info_inner(
|
||||||
|
root: &Path,
|
||||||
|
profile: &str,
|
||||||
|
workspace_root: Option<&Path>,
|
||||||
|
) -> Result<ProjectInfo, String> {
|
||||||
|
let root = canonicalize_path(root);
|
||||||
let config = Config::open("./dcr.toml").map_err(|e| e.to_string())?;
|
let config = Config::open("./dcr.toml").map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let name = get_config_str(&config, "package.name");
|
let name = get_config_str(&config, "package.name");
|
||||||
@@ -249,15 +279,32 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
|
|||||||
let standard = get_string_with_profile(&config, "standard", profile);
|
let standard = get_string_with_profile(&config, "standard", profile);
|
||||||
let cxx_standard = get_string_with_profile(&config, "cxx_standard", profile);
|
let cxx_standard = get_string_with_profile(&config, "cxx_standard", profile);
|
||||||
let compiler_s = get_string_with_profile(&config, "compiler", profile);
|
let compiler_s = get_string_with_profile(&config, "compiler", profile);
|
||||||
let kind = get_string_with_profile(&config, "kind", profile);
|
let kind_raw = get_string_with_profile(&config, "kind", profile);
|
||||||
|
let kind = normalize_kind(&kind_raw).to_string();
|
||||||
let build_target = get_string_with_profile(&config, "target", profile);
|
let build_target = get_string_with_profile(&config, "target", profile);
|
||||||
let platform = get_string_with_profile(&config, "platform", profile);
|
let platform = get_string_with_profile(&config, "platform", profile);
|
||||||
|
let output_filename_s = get_string_with_profile(&config, "filename", profile);
|
||||||
|
let output_extension_s = get_string_with_profile(&config, "extension", profile);
|
||||||
|
let output_filename = if output_filename_s.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(output_filename_s)
|
||||||
|
};
|
||||||
|
let output_extension = if output_extension_s.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(output_extension_s)
|
||||||
|
};
|
||||||
|
|
||||||
let tc_cc = get_config_opt(&config, "toolchain.cc");
|
let tc_cc = get_config_opt(&config, "toolchain.cc");
|
||||||
let tc_cxx = get_config_opt(&config, "toolchain.cxx");
|
let tc_cxx = get_config_opt(&config, "toolchain.cxx");
|
||||||
let tc_as = get_config_opt(&config, "toolchain.as");
|
let tc_as = get_config_opt(&config, "toolchain.as");
|
||||||
let tc_ar = get_config_opt(&config, "toolchain.ar");
|
let tc_ar = get_config_opt(&config, "toolchain.ar");
|
||||||
let tc_ld = get_config_opt(&config, "toolchain.ld");
|
let tc_ld = get_config_opt(&config, "toolchain.ld");
|
||||||
|
let tc_moc = get_config_opt(&config, "toolchain.moc");
|
||||||
|
let tc_uic = get_config_opt(&config, "toolchain.uic");
|
||||||
|
let tc_rcc = get_config_opt(&config, "toolchain.rcc");
|
||||||
|
let tc_debugger = get_config_opt(&config, "toolchain.debugger");
|
||||||
|
|
||||||
let base_cflags = get_list_with_profile(&config, "cflags", profile);
|
let base_cflags = get_list_with_profile(&config, "cflags", profile);
|
||||||
let base_ldflags = get_list_with_profile(&config, "ldflags", profile);
|
let base_ldflags = get_list_with_profile(&config, "ldflags", profile);
|
||||||
@@ -286,7 +333,7 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
|
|||||||
.filter(|v| !v.trim().is_empty())
|
.filter(|v| !v.trim().is_empty())
|
||||||
});
|
});
|
||||||
|
|
||||||
let resolved = resolve_deps_for_gen(&config, profile, root);
|
let resolved = resolve_deps_for_gen(&config, profile, &root);
|
||||||
let (resolved_cflags, resolved_ldflags) =
|
let (resolved_cflags, resolved_ldflags) =
|
||||||
resolve_pkg_config_flags_lossy(&pkg_configs, &base_cflags, &base_ldflags);
|
resolve_pkg_config_flags_lossy(&pkg_configs, &base_cflags, &base_ldflags);
|
||||||
|
|
||||||
@@ -352,7 +399,22 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let target_dir_binding = normalize_target(&build_target, profile);
|
let out_dir_config = get_string_with_profile(&config, "out_dir", profile);
|
||||||
|
let has_explicit = !build_target.trim().is_empty();
|
||||||
|
let target_dir_binding = resolve_artifact_target_dir(
|
||||||
|
&root,
|
||||||
|
workspace_root,
|
||||||
|
profile,
|
||||||
|
&build_target,
|
||||||
|
&out_dir_config,
|
||||||
|
has_explicit,
|
||||||
|
);
|
||||||
|
let target_dir_opt = if target_dir_binding.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(target_dir_binding.as_str())
|
||||||
|
};
|
||||||
|
|
||||||
let ctx = BuildContext {
|
let ctx = BuildContext {
|
||||||
profile,
|
profile,
|
||||||
project_name: &name,
|
project_name: &name,
|
||||||
@@ -360,15 +422,19 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
|
|||||||
language: &language,
|
language: &language,
|
||||||
standard: &standard,
|
standard: &standard,
|
||||||
cxx_standard: &cxx_standard,
|
cxx_standard: &cxx_standard,
|
||||||
target: Some(build_target.as_str()),
|
target: if build_target.is_empty() {
|
||||||
target_dir: target_dir_binding.as_deref(),
|
None
|
||||||
kind: normalize_kind(&kind),
|
} else {
|
||||||
|
Some(build_target.as_str())
|
||||||
|
},
|
||||||
|
target_dir: target_dir_opt,
|
||||||
|
kind: kind.as_str(),
|
||||||
platform: normalize_platform(&platform),
|
platform: normalize_platform(&platform),
|
||||||
linker: resolved_linker.as_deref(),
|
linker: resolved_linker.as_deref(),
|
||||||
archiver: resolved_archiver.as_deref(),
|
archiver: resolved_archiver.as_deref(),
|
||||||
moc: None,
|
moc: tc_moc.as_deref(),
|
||||||
uic: None,
|
uic: tc_uic.as_deref(),
|
||||||
rcc: None,
|
rcc: tc_rcc.as_deref(),
|
||||||
package_type: None,
|
package_type: None,
|
||||||
freestanding: false,
|
freestanding: false,
|
||||||
panic_abort: false,
|
panic_abort: false,
|
||||||
@@ -381,8 +447,8 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
|
|||||||
libs: &resolved.libs,
|
libs: &resolved.libs,
|
||||||
cflags: &resolved_cflags,
|
cflags: &resolved_cflags,
|
||||||
ldflags: &resolved_ldflags,
|
ldflags: &resolved_ldflags,
|
||||||
output_filename: None,
|
output_filename: output_filename.as_deref(),
|
||||||
output_extension: None,
|
output_extension: output_extension.as_deref(),
|
||||||
verbose: false,
|
verbose: false,
|
||||||
qt: false,
|
qt: false,
|
||||||
};
|
};
|
||||||
@@ -393,62 +459,317 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result<ProjectInfo,
|
|||||||
let abs_sources: Vec<String> = sources
|
let abs_sources: Vec<String> = sources
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
let p = Path::new(s);
|
absolute_join(&root, Path::new(s))
|
||||||
if p.is_absolute() {
|
.to_string_lossy()
|
||||||
s.clone()
|
.to_string()
|
||||||
} else {
|
|
||||||
root.join(s).to_string_lossy().to_string()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let abs_include_dirs: Vec<String> = merged_include_dirs
|
||||||
|
.iter()
|
||||||
|
.map(|d| {
|
||||||
|
absolute_join(&root, Path::new(d))
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let abs_lib_dirs: Vec<String> = resolved
|
||||||
|
.lib_dirs
|
||||||
|
.iter()
|
||||||
|
.map(|d| {
|
||||||
|
absolute_join(&root, Path::new(d))
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let abs_source_roots: Vec<String> = source_roots
|
||||||
|
.iter()
|
||||||
|
.map(|p| canonicalize_path(p).to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let abs_target_dir = {
|
||||||
|
let p = Path::new(&target_dir_binding);
|
||||||
|
let path = if p.is_absolute() {
|
||||||
|
canonicalize_path(p)
|
||||||
|
} else {
|
||||||
|
absolute_join(&root, p)
|
||||||
|
};
|
||||||
|
path.to_string_lossy().into_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let relative_artifact = resolve_artifact_path(
|
||||||
|
&kind,
|
||||||
|
profile,
|
||||||
|
&name,
|
||||||
|
Some(&abs_target_dir),
|
||||||
|
output_filename.as_deref(),
|
||||||
|
output_extension.as_deref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let artifact_path = relative_artifact.map(|rel| {
|
||||||
|
absolute_artifact_path(&root, &rel)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
});
|
||||||
|
|
||||||
|
let debugger = resolve_debugger(tc_debugger.as_deref());
|
||||||
|
let ws_root = workspace_root.map(canonicalize_path);
|
||||||
|
|
||||||
Ok(ProjectInfo {
|
Ok(ProjectInfo {
|
||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
root: root.to_path_buf(),
|
root,
|
||||||
profile: profile.to_string(),
|
profile: profile.to_string(),
|
||||||
language,
|
language,
|
||||||
standard,
|
standard,
|
||||||
cxx_standard,
|
cxx_standard,
|
||||||
compiler: resolved_compiler,
|
compiler: resolved_compiler,
|
||||||
kind: normalize_kind(&kind).to_string(),
|
kind: kind.clone(),
|
||||||
|
target: build_target,
|
||||||
|
target_dir: Some(abs_target_dir),
|
||||||
|
artifact_path,
|
||||||
|
artifact_kind: kind,
|
||||||
sources: abs_sources,
|
sources: abs_sources,
|
||||||
include_dirs: merged_include_dirs,
|
include_dirs: abs_include_dirs,
|
||||||
lib_dirs: resolved.lib_dirs,
|
lib_dirs: abs_lib_dirs,
|
||||||
libs: resolved.libs,
|
libs: resolved.libs,
|
||||||
cflags: resolved_cflags,
|
cflags: resolved_cflags,
|
||||||
ldflags: resolved_ldflags,
|
ldflags: resolved_ldflags,
|
||||||
|
source_roots: abs_source_roots,
|
||||||
|
include_globs: build_includes,
|
||||||
|
exclude_globs: build_excludes,
|
||||||
|
output_filename,
|
||||||
|
output_extension,
|
||||||
|
out_dir: out_dir_config,
|
||||||
|
debugger,
|
||||||
|
moc: tc_moc.and_then(|v| resolve_tool_path(&v)),
|
||||||
|
uic: tc_uic.and_then(|v| resolve_tool_path(&v)),
|
||||||
|
rcc: tc_rcc.and_then(|v| resolve_tool_path(&v)),
|
||||||
|
workspace_root: ws_root,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collect info for root project + all workspace members.
|
fn find_in_path(name: &str) -> Option<String> {
|
||||||
|
if name.contains('/') || name.contains('\\') {
|
||||||
|
let p = Path::new(name);
|
||||||
|
if p.exists() {
|
||||||
|
return Some(canonicalize_path(p).to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
return Some(name.to_string());
|
||||||
|
}
|
||||||
|
let path_var = std::env::var_os("PATH")?;
|
||||||
|
for dir in std::env::split_paths(&path_var) {
|
||||||
|
let candidate = dir.join(name);
|
||||||
|
if candidate.is_file() {
|
||||||
|
return Some(canonicalize_path(&candidate).to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let with_exe = dir.join(format!("{name}.exe"));
|
||||||
|
if with_exe.is_file() {
|
||||||
|
return Some(canonicalize_path(&with_exe).to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_tool_path(name: &str) -> Option<String> {
|
||||||
|
let t = name.trim();
|
||||||
|
if t.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
find_in_path(t).or_else(|| Some(t.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_debugger(configured: Option<&str>) -> String {
|
||||||
|
if let Some(d) = configured {
|
||||||
|
let t = d.trim();
|
||||||
|
if !t.is_empty() {
|
||||||
|
return t.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for candidate in ["lldb", "gdb"] {
|
||||||
|
if find_in_path(candidate).is_some() {
|
||||||
|
return candidate.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg!(windows) {
|
||||||
|
"cppvsdbg".to_string()
|
||||||
|
} else {
|
||||||
|
"lldb".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn utc_now_iso() -> String {
|
||||||
|
let secs = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let days = secs / 86400;
|
||||||
|
let tod = secs % 86400;
|
||||||
|
let hour = tod / 3600;
|
||||||
|
let min = (tod % 3600) / 60;
|
||||||
|
let sec = tod % 60;
|
||||||
|
let (year, month, day) = civil_from_days(days as i64);
|
||||||
|
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}Z")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn civil_from_days(days: i64) -> (i32, u32, u32) {
|
||||||
|
let z = days + 719468;
|
||||||
|
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
||||||
|
let doe = (z - era * 146097) as u32;
|
||||||
|
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||||
|
let y = (yoe as i64) + era * 400;
|
||||||
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||||
|
let mp = (5 * doy + 2) / 153;
|
||||||
|
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||||
|
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||||
|
let y = if m <= 2 { y + 1 } else { y };
|
||||||
|
(y as i32, m, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_dcr_metadata(root: &Path, info: &ProjectInfo) -> Result<(), String> {
|
||||||
|
let dcr = ensure_dcr_dir(root).map_err(|e| format!("Failed to create .dcr/: {e}"))?;
|
||||||
|
|
||||||
|
let artifact_json = match &info.artifact_path {
|
||||||
|
Some(p) => json_str(p),
|
||||||
|
None => "null".to_string(),
|
||||||
|
};
|
||||||
|
let target_dir_json = match &info.target_dir {
|
||||||
|
Some(p) => json_str(p),
|
||||||
|
None => "null".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let build_info = format!(
|
||||||
|
r#"{{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"generatedAt": {generated},
|
||||||
|
"profile": {profile},
|
||||||
|
"compiler": {compiler},
|
||||||
|
"target": {target},
|
||||||
|
"target_dir": {target_dir},
|
||||||
|
"artifact_path": {artifact},
|
||||||
|
"artifact_kind": {kind}
|
||||||
|
}}
|
||||||
|
"#,
|
||||||
|
generated = json_str(&utc_now_iso()),
|
||||||
|
profile = json_str(&info.profile),
|
||||||
|
compiler = json_str(&info.compiler),
|
||||||
|
target = json_str(&info.target),
|
||||||
|
target_dir = target_dir_json,
|
||||||
|
artifact = artifact_json,
|
||||||
|
kind = json_str(&info.artifact_kind),
|
||||||
|
);
|
||||||
|
atomic_write(&dcr.join("build-info.json"), build_info.as_bytes())
|
||||||
|
.map_err(|e| format!("Failed to write build-info.json: {e}"))?;
|
||||||
|
|
||||||
|
let compiler_path = find_in_path(&info.compiler).unwrap_or_else(|| info.compiler.clone());
|
||||||
|
let debugger_path = find_in_path(&info.debugger).unwrap_or_else(|| info.debugger.clone());
|
||||||
|
|
||||||
|
let mut toolchain = String::from("{\n");
|
||||||
|
toolchain.push_str(" \"schemaVersion\": 1,\n");
|
||||||
|
toolchain.push_str(&format!(" \"compiler\": {},\n", json_str(&info.compiler)));
|
||||||
|
toolchain.push_str(&format!(
|
||||||
|
" \"compilerPath\": {},\n",
|
||||||
|
json_str(&compiler_path)
|
||||||
|
));
|
||||||
|
toolchain.push_str(&format!(" \"debugger\": {},\n", json_str(&info.debugger)));
|
||||||
|
toolchain.push_str(&format!(" \"debuggerPath\": {}", json_str(&debugger_path)));
|
||||||
|
if let Some(ref m) = info.moc {
|
||||||
|
toolchain.push_str(",\n");
|
||||||
|
toolchain.push_str(&format!(" \"moc\": {}", json_str(m)));
|
||||||
|
}
|
||||||
|
if let Some(ref u) = info.uic {
|
||||||
|
toolchain.push_str(",\n");
|
||||||
|
toolchain.push_str(&format!(" \"uic\": {}", json_str(u)));
|
||||||
|
}
|
||||||
|
if let Some(ref r) = info.rcc {
|
||||||
|
toolchain.push_str(",\n");
|
||||||
|
toolchain.push_str(&format!(" \"rcc\": {}", json_str(r)));
|
||||||
|
}
|
||||||
|
toolchain.push_str("\n}\n");
|
||||||
|
atomic_write(&dcr.join("toolchain.json"), toolchain.as_bytes())
|
||||||
|
.map_err(|e| format!("Failed to write toolchain.json: {e}"))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_clangd(root: &Path, quiet: bool) {
|
||||||
|
let path = root.join(".clangd");
|
||||||
|
let marker = "# managed by dcr gen";
|
||||||
|
let desired = format!("{marker}\nCompileFlags:\n CompilationDatabase: .dcr\n");
|
||||||
|
if path.exists() {
|
||||||
|
match std::fs::read_to_string(&path) {
|
||||||
|
Ok(content)
|
||||||
|
if content.contains(marker) || content.contains("CompilationDatabase: .dcr") =>
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
if !quiet {
|
||||||
|
eprintln!(
|
||||||
|
"Warning: .clangd already exists and is not managed by dcr; \
|
||||||
|
set CompilationDatabase to .dcr manually if needed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(_) => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = std::fs::write(&path, desired);
|
||||||
|
}
|
||||||
|
|
||||||
fn collect_all(root: &Path, profile: &str) -> Result<Vec<ProjectInfo>, String> {
|
fn collect_all(root: &Path, profile: &str) -> Result<Vec<ProjectInfo>, String> {
|
||||||
// Check for workspace
|
let root = canonicalize_path(root);
|
||||||
let config = {
|
let config = {
|
||||||
let prev = std::env::current_dir().map_err(|e| e.to_string())?;
|
let prev = std::env::current_dir().map_err(|e| e.to_string())?;
|
||||||
std::env::set_current_dir(root).map_err(|e| e.to_string())?;
|
std::env::set_current_dir(&root).map_err(|e| e.to_string())?;
|
||||||
let cfg = Config::open("./dcr.toml").map_err(|e| e.to_string());
|
let cfg = Config::open("./dcr.toml").map_err(|e| e.to_string());
|
||||||
let _ = std::env::set_current_dir(prev);
|
let _ = std::env::set_current_dir(prev);
|
||||||
cfg?
|
cfg?
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut all = Vec::new();
|
let mut all = Vec::new();
|
||||||
|
let is_workspace = parse_workspace(&config, profile, None, &root)
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.is_some();
|
||||||
|
let ws_root = if is_workspace {
|
||||||
|
Some(root.as_path())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
if let Ok(Some(ws)) = parse_workspace(&config, profile, None, root) {
|
if let Ok(Some(ws)) = parse_workspace(&config, profile, None, &root) {
|
||||||
for member in &ws.members {
|
for member in &ws.members {
|
||||||
match collect_project_info(&member.path, profile) {
|
let member_path = canonicalize_path(&member.path);
|
||||||
|
match collect_project_info(&member_path, profile, ws_root) {
|
||||||
Ok(info) => all.push(info),
|
Ok(info) => all.push(info),
|
||||||
Err(e) => eprintln!(
|
Err(e) => eprintln!(
|
||||||
"Warning: skipping workspace member {}: {e}",
|
"Warning: skipping workspace member {}: {e}",
|
||||||
member.path.display()
|
member_path.display()
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Root project itself
|
if !is_workspace || !config.is_workspace_only() {
|
||||||
let root_info = collect_project_info(root, profile)?;
|
match collect_project_info(&root, profile, ws_root) {
|
||||||
all.push(root_info);
|
Ok(info) => all.push(info),
|
||||||
|
Err(e) if is_workspace => {
|
||||||
|
eprintln!("Warning: skipping workspace root package: {e}");
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if all.is_empty() {
|
||||||
|
return Err("No project members found".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(all)
|
Ok(all)
|
||||||
}
|
}
|
||||||
@@ -456,7 +777,7 @@ fn collect_all(root: &Path, profile: &str) -> Result<Vec<ProjectInfo>, String> {
|
|||||||
// ── dcr gen project-info ─────────────────────────────────────────────────────
|
// ── dcr gen project-info ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn gen_project_info(args: &[String]) -> i32 {
|
fn gen_project_info(args: &[String]) -> i32 {
|
||||||
let (root, profile) = match parse_gen_args(args) {
|
let (root, profile, quiet) = match parse_gen_args(args) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(code) => return code,
|
Err(code) => return code,
|
||||||
};
|
};
|
||||||
@@ -469,6 +790,14 @@ fn gen_project_info(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(info) = all.last()
|
||||||
|
&& let Err(e) = write_dcr_metadata(&root, info)
|
||||||
|
&& !quiet
|
||||||
|
{
|
||||||
|
eprintln!("Warning: {e}");
|
||||||
|
}
|
||||||
|
ensure_clangd(&root, quiet);
|
||||||
|
|
||||||
print!("[");
|
print!("[");
|
||||||
for (i, info) in all.iter().enumerate() {
|
for (i, info) in all.iter().enumerate() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@@ -509,6 +838,25 @@ fn project_info_to_json(info: &ProjectInfo) -> String {
|
|||||||
json_str(&info.compiler)
|
json_str(&info.compiler)
|
||||||
));
|
));
|
||||||
out.push_str(&format!(" \"kind\": {},\n", json_str(&info.kind)));
|
out.push_str(&format!(" \"kind\": {},\n", json_str(&info.kind)));
|
||||||
|
out.push_str(&format!(" \"target\": {},\n", json_str(&info.target)));
|
||||||
|
out.push_str(&format!(
|
||||||
|
" \"target_dir\": {},\n",
|
||||||
|
match &info.target_dir {
|
||||||
|
Some(p) => json_str(p),
|
||||||
|
None => "null".to_string(),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
out.push_str(&format!(
|
||||||
|
" \"artifact_path\": {},\n",
|
||||||
|
match &info.artifact_path {
|
||||||
|
Some(p) => json_str(p),
|
||||||
|
None => "null".to_string(),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
out.push_str(&format!(
|
||||||
|
" \"artifact_kind\": {},\n",
|
||||||
|
json_str(&info.artifact_kind)
|
||||||
|
));
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
" \"sources\": {},\n",
|
" \"sources\": {},\n",
|
||||||
json_str_array(&info.sources)
|
json_str_array(&info.sources)
|
||||||
@@ -527,9 +875,21 @@ fn project_info_to_json(info: &ProjectInfo) -> String {
|
|||||||
json_str_array(&info.cflags)
|
json_str_array(&info.cflags)
|
||||||
));
|
));
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
" \"ldflags\": {}\n",
|
" \"ldflags\": {},\n",
|
||||||
json_str_array(&info.ldflags)
|
json_str_array(&info.ldflags)
|
||||||
));
|
));
|
||||||
|
out.push_str(&format!(
|
||||||
|
" \"source_roots\": {},\n",
|
||||||
|
json_str_array(&info.source_roots)
|
||||||
|
));
|
||||||
|
out.push_str(&format!(
|
||||||
|
" \"include_globs\": {},\n",
|
||||||
|
json_str_array(&info.include_globs)
|
||||||
|
));
|
||||||
|
out.push_str(&format!(
|
||||||
|
" \"exclude_globs\": {}\n",
|
||||||
|
json_str_array(&info.exclude_globs)
|
||||||
|
));
|
||||||
out.push_str(" }");
|
out.push_str(" }");
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
@@ -537,7 +897,7 @@ fn project_info_to_json(info: &ProjectInfo) -> String {
|
|||||||
// ── dcr gen compile-commands ─────────────────────────────────────────────────
|
// ── dcr gen compile-commands ─────────────────────────────────────────────────
|
||||||
|
|
||||||
fn gen_compile_commands(args: &[String]) -> i32 {
|
fn gen_compile_commands(args: &[String]) -> i32 {
|
||||||
let (root, profile) = match parse_gen_args(args) {
|
let (root, profile, quiet) = match parse_gen_args(args) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(code) => return code,
|
Err(code) => return code,
|
||||||
};
|
};
|
||||||
@@ -550,16 +910,29 @@ fn gen_compile_commands(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
gen_compile_commands_inner(&root, &profile, &all)
|
gen_compile_commands_inner(&root, &profile, &all, quiet)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo]) -> i32 {
|
fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo], quiet: bool) -> i32 {
|
||||||
let entries = build_compile_commands(all, profile);
|
if let Err(e) = ensure_dcr_dir(root) {
|
||||||
|
error(&format!("Failed to create .dcr/: {e}"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
let out_path = root.join("compile_commands.json");
|
let entries = build_compile_commands(all, profile);
|
||||||
match std::fs::write(&out_path, &entries) {
|
let out_path = root.join(".dcr").join("compile_commands.json");
|
||||||
|
match atomic_write(&out_path, entries.as_bytes()) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
println!("Generated {}", out_path.display());
|
if let Some(info) = all.last()
|
||||||
|
&& let Err(e) = write_dcr_metadata(root, info)
|
||||||
|
&& !quiet
|
||||||
|
{
|
||||||
|
eprintln!("Warning: {e}");
|
||||||
|
}
|
||||||
|
ensure_clangd(root, quiet);
|
||||||
|
if !quiet {
|
||||||
|
println!("Generated {}", out_path.display());
|
||||||
|
}
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -615,16 +988,21 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec
|
|||||||
cmd.push(flag.to_string());
|
cmd.push(flag.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.push(source.to_string());
|
let abs_source = absolute_join(&info.root, Path::new(source));
|
||||||
|
cmd.push(abs_source.to_string_lossy().to_string());
|
||||||
|
|
||||||
// Object path (for -o, approximate — not critical for IntelliSense)
|
// Object path (for -o, approximate — not critical for IntelliSense)
|
||||||
let obj_dir = info.root.join("target").join(profile).join("obj");
|
|
||||||
|
let obj_base = info
|
||||||
|
.target_dir
|
||||||
|
.as_ref()
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| info.root.join("target").join(profile));
|
||||||
|
let obj_dir = obj_base.join("obj");
|
||||||
let obj_path = {
|
let obj_path = {
|
||||||
let p = Path::new(source);
|
let p = Path::new(source);
|
||||||
let rel = strip_src_prefix(p);
|
let rel = strip_src_prefix(p);
|
||||||
obj_dir
|
absolute_join(&info.root, &obj_dir.join(rel).with_extension("o"))
|
||||||
.join(rel)
|
|
||||||
.with_extension("o")
|
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string()
|
.to_string()
|
||||||
};
|
};
|
||||||
@@ -670,19 +1048,15 @@ fn build_compile_command(info: &ProjectInfo, source: &str, profile: &str) -> Vec
|
|||||||
// Expand relative -I paths to absolute so clangd/cpptools work
|
// Expand relative -I paths to absolute so clangd/cpptools work
|
||||||
// regardless of their working directory.
|
// regardless of their working directory.
|
||||||
if let Some(rel) = flag.strip_prefix("-I") {
|
if let Some(rel) = flag.strip_prefix("-I") {
|
||||||
let p = Path::new(rel);
|
let abs = absolute_join(&info.root, Path::new(rel));
|
||||||
let abs = if p.is_absolute() {
|
|
||||||
p.to_path_buf()
|
|
||||||
} else {
|
|
||||||
info.root.join(p)
|
|
||||||
};
|
|
||||||
cmd.push(format!("-I{}", abs.to_string_lossy()));
|
cmd.push(format!("-I{}", abs.to_string_lossy()));
|
||||||
} else {
|
} else {
|
||||||
cmd.push(flag.clone());
|
cmd.push(flag.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for dir in &info.include_dirs {
|
for dir in &info.include_dirs {
|
||||||
cmd.push(format!("-I{dir}"));
|
let abs = absolute_join(&info.root, Path::new(dir));
|
||||||
|
cmd.push(format!("-I{}", abs.to_string_lossy()));
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd
|
cmd
|
||||||
@@ -703,7 +1077,7 @@ fn strip_src_prefix(p: &Path) -> PathBuf {
|
|||||||
// ── dcr gen vscode ───────────────────────────────────────────────────────────
|
// ── dcr gen vscode ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn gen_vscode(args: &[String]) -> i32 {
|
fn gen_vscode(args: &[String]) -> i32 {
|
||||||
let (root, profile) = match parse_gen_args(args) {
|
let (root, profile, quiet) = match parse_gen_args(args) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(code) => return code,
|
Err(code) => return code,
|
||||||
};
|
};
|
||||||
@@ -718,7 +1092,7 @@ fn gen_vscode(args: &[String]) -> i32 {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 1. Generate compile_commands.json
|
// 1. Generate compile_commands.json
|
||||||
let cc_code = gen_compile_commands_inner(&root, &profile, &all);
|
let cc_code = gen_compile_commands_inner(&root, &profile, &all, quiet);
|
||||||
if cc_code != 0 {
|
if cc_code != 0 {
|
||||||
return cc_code;
|
return cc_code;
|
||||||
}
|
}
|
||||||
@@ -842,6 +1216,58 @@ fn gen_tasks_json() -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_launch_program(info: &ProjectInfo, profile: &str) -> String {
|
||||||
|
if info.profile == profile
|
||||||
|
&& let Some(ref p) = info.artifact_path
|
||||||
|
{
|
||||||
|
return p.clone();
|
||||||
|
}
|
||||||
|
let has_explicit = !info.target.trim().is_empty();
|
||||||
|
let target_dir = resolve_artifact_target_dir(
|
||||||
|
&info.root,
|
||||||
|
info.workspace_root.as_deref(),
|
||||||
|
profile,
|
||||||
|
&info.target,
|
||||||
|
&info.out_dir,
|
||||||
|
has_explicit,
|
||||||
|
);
|
||||||
|
let rel = resolve_artifact_path(
|
||||||
|
&info.kind,
|
||||||
|
profile,
|
||||||
|
&info.name,
|
||||||
|
Some(&target_dir),
|
||||||
|
info.output_filename.as_deref(),
|
||||||
|
info.output_extension.as_deref(),
|
||||||
|
);
|
||||||
|
match rel {
|
||||||
|
Some(r) => absolute_artifact_path(&info.root, &r)
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
None => {
|
||||||
|
let name = if cfg!(windows) && !info.name.to_ascii_lowercase().ends_with(".exe") {
|
||||||
|
format!("{}.exe", info.name)
|
||||||
|
} else {
|
||||||
|
info.name.clone()
|
||||||
|
};
|
||||||
|
Path::new(&target_dir)
|
||||||
|
.join(name)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_type_for(info: &ProjectInfo) -> &'static str {
|
||||||
|
let d = info.debugger.to_lowercase();
|
||||||
|
if d.contains("gdb") || d == "cppdbg" {
|
||||||
|
"cppdbg"
|
||||||
|
} else if cfg!(windows) && (d.contains("vs") || d == "cppvsdbg") {
|
||||||
|
"cppvsdbg"
|
||||||
|
} else {
|
||||||
|
"lldb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String {
|
fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String {
|
||||||
let mut configs = Vec::new();
|
let mut configs = Vec::new();
|
||||||
|
|
||||||
@@ -851,25 +1277,14 @@ fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// binary expected at info.root/target/<profile>/<name> (account for member projects)
|
// binary expected at info.root/target/<profile>/<name> (account for member projects)
|
||||||
let debug_bin = info
|
let debug_bin = resolve_launch_program(info, "debug");
|
||||||
.root
|
let release_bin = resolve_launch_program(info, "release");
|
||||||
.join("target")
|
let dtype = debug_type_for(info);
|
||||||
.join("debug")
|
|
||||||
.join(&info.name)
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string();
|
|
||||||
let release_bin = info
|
|
||||||
.root
|
|
||||||
.join("target")
|
|
||||||
.join("release")
|
|
||||||
.join(&info.name)
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let debug_entry = format!(
|
let debug_entry = format!(
|
||||||
r#" {{
|
r#" {{
|
||||||
"name": {name},
|
"name": {name},
|
||||||
"type": "lldb",
|
"type": {dtype},
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": {prog},
|
"program": {prog},
|
||||||
"args": [],
|
"args": [],
|
||||||
@@ -879,6 +1294,7 @@ fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String {
|
|||||||
"preLaunchTask": "dcr: build (debug)"
|
"preLaunchTask": "dcr: build (debug)"
|
||||||
}}"#,
|
}}"#,
|
||||||
name = json_str(&format!("{} (debug)", info.name)),
|
name = json_str(&format!("{} (debug)", info.name)),
|
||||||
|
dtype = json_str(dtype),
|
||||||
prog = json_str(&debug_bin),
|
prog = json_str(&debug_bin),
|
||||||
cwd = json_str(&info.root.to_string_lossy()),
|
cwd = json_str(&info.root.to_string_lossy()),
|
||||||
);
|
);
|
||||||
@@ -886,7 +1302,7 @@ fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String {
|
|||||||
let release_entry = format!(
|
let release_entry = format!(
|
||||||
r#" {{
|
r#" {{
|
||||||
"name": {name},
|
"name": {name},
|
||||||
"type": "lldb",
|
"type": {dtype},
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": {prog},
|
"program": {prog},
|
||||||
"args": [],
|
"args": [],
|
||||||
@@ -896,6 +1312,7 @@ fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String {
|
|||||||
"preLaunchTask": "dcr: build (release)"
|
"preLaunchTask": "dcr: build (release)"
|
||||||
}}"#,
|
}}"#,
|
||||||
name = json_str(&format!("{} (release)", info.name)),
|
name = json_str(&format!("{} (release)", info.name)),
|
||||||
|
dtype = json_str(dtype),
|
||||||
prog = json_str(&release_bin),
|
prog = json_str(&release_bin),
|
||||||
cwd = json_str(&info.root.to_string_lossy()),
|
cwd = json_str(&info.root.to_string_lossy()),
|
||||||
);
|
);
|
||||||
@@ -925,7 +1342,7 @@ fn gen_launch_json(projects: &[ProjectInfo], _root: &Path) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn gen_settings_json(root: &Path) -> String {
|
fn gen_settings_json(root: &Path) -> String {
|
||||||
let cc_dir = root.to_string_lossy();
|
let cc_dir = root.join(".dcr").to_string_lossy().to_string();
|
||||||
format!(
|
format!(
|
||||||
r#"{{
|
r#"{{
|
||||||
"clangd.arguments": [
|
"clangd.arguments": [
|
||||||
@@ -945,7 +1362,7 @@ fn gen_settings_json(root: &Path) -> String {
|
|||||||
// ── dcr gen clion ─────────────────────────────────────────────────────────────
|
// ── dcr gen clion ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn gen_clion(args: &[String]) -> i32 {
|
fn gen_clion(args: &[String]) -> i32 {
|
||||||
let (root, profile) = match parse_gen_args(args) {
|
let (root, profile, quiet) = match parse_gen_args(args) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(code) => return code,
|
Err(code) => return code,
|
||||||
};
|
};
|
||||||
@@ -960,7 +1377,7 @@ fn gen_clion(args: &[String]) -> i32 {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 1. compile_commands.json
|
// 1. compile_commands.json
|
||||||
let cc_code = gen_compile_commands_inner(&root, &profile, &all);
|
let cc_code = gen_compile_commands_inner(&root, &profile, &all, quiet);
|
||||||
if cc_code != 0 {
|
if cc_code != 0 {
|
||||||
return cc_code;
|
return cc_code;
|
||||||
}
|
}
|
||||||
@@ -1140,7 +1557,7 @@ fn gen_clion_custom_targets() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn gen_clion_misc_xml(root: &Path) -> String {
|
fn gen_clion_misc_xml(root: &Path) -> String {
|
||||||
let cc_path = root.join("compile_commands.json");
|
let cc_path = root.join(".dcr").join("compile_commands.json");
|
||||||
let cc = xml_escape(&cc_path.to_string_lossy());
|
let cc = xml_escape(&cc_path.to_string_lossy());
|
||||||
format!(
|
format!(
|
||||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@@ -1154,9 +1571,9 @@ fn gen_clion_misc_xml(root: &Path) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gen_clion_run_config(info: &ProjectInfo, root: &Path, profile: &str) -> String {
|
fn gen_clion_run_config(info: &ProjectInfo, _root: &Path, profile: &str) -> String {
|
||||||
let bin_path = root.join("target").join(profile).join(&info.name);
|
let bin_path = resolve_launch_program(info, profile);
|
||||||
let bin = xml_escape(&bin_path.to_string_lossy());
|
let bin = xml_escape(&bin_path);
|
||||||
let target = if profile == "release" {
|
let target = if profile == "release" {
|
||||||
"dcr: build (release)"
|
"dcr: build (release)"
|
||||||
} else {
|
} else {
|
||||||
@@ -1206,12 +1623,14 @@ fn xml_escape(s: &str) -> String {
|
|||||||
|
|
||||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String), i32> {
|
fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String, bool), i32> {
|
||||||
let mut profile = "debug".to_string();
|
let mut profile = "debug".to_string();
|
||||||
|
let mut quiet = false;
|
||||||
for arg in args {
|
for arg in args {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"--debug" => profile = "debug".to_string(),
|
"--debug" => profile = "debug".to_string(),
|
||||||
"--release" => profile = "release".to_string(),
|
"--release" => profile = "release".to_string(),
|
||||||
|
"--quiet" | "-q" => quiet = true,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1222,7 +1641,7 @@ fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String), i32> {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let root = match find_project_root(&start) {
|
let root = match find_project_root(&start) {
|
||||||
Ok(Some(r)) => r,
|
Ok(Some(r)) => canonicalize_path(&r),
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
error("dcr.toml not found");
|
error("dcr.toml not found");
|
||||||
return Err(1);
|
return Err(1);
|
||||||
@@ -1233,7 +1652,7 @@ fn parse_gen_args(args: &[String]) -> Result<(PathBuf, String), i32> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((root, profile))
|
Ok((root, profile, quiet))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn json_str(s: &str) -> String {
|
fn json_str(s: &str) -> String {
|
||||||
|
|||||||
@@ -159,6 +159,10 @@ pub fn init(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Err(e) = crate::utils::fs::ensure_gitignore_has_dcr(std::path::Path::new(".")) {
|
||||||
|
warn(&format!("Failed to update .gitignore: {e}"));
|
||||||
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Project `{}` successfully created\n",
|
"Project `{}` successfully created\n",
|
||||||
colored(project_name.as_str(), BOLD_GREEN)
|
colored(project_name.as_str(), BOLD_GREEN)
|
||||||
|
|||||||
@@ -180,6 +180,11 @@ pub fn new(args: &[String]) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let project_path = std::path::Path::new(project_name);
|
||||||
|
if let Err(e) = crate::utils::fs::ensure_gitignore_has_dcr(project_path) {
|
||||||
|
warn(&format!("Failed to update .gitignore: {e}"));
|
||||||
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Project `{}` successfully created\n",
|
"Project `{}` successfully created\n",
|
||||||
colored(project_name, BOLD_GREEN)
|
colored(project_name, BOLD_GREEN)
|
||||||
|
|||||||
101
src/cli/run.rs
101
src/cli/run.rs
@@ -59,12 +59,13 @@ pub fn run(args: &[String]) -> i32 {
|
|||||||
if args.first().is_some_and(|a| a == "--help") {
|
if args.first().is_some_and(|a| a == "--help") {
|
||||||
printc("USAGE:", BOLD_GREEN);
|
printc("USAGE:", BOLD_GREEN);
|
||||||
printc(
|
printc(
|
||||||
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose]",
|
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]",
|
||||||
BOLD_CYAN,
|
BOLD_CYAN,
|
||||||
);
|
);
|
||||||
println!();
|
println!();
|
||||||
printc("DESCRIPTION:", BOLD_GREEN);
|
printc("DESCRIPTION:", BOLD_GREEN);
|
||||||
println!(" Builds and runs the project. Only available for kind = \"bin\".");
|
println!(" Builds and runs the project. Only available for kind = \"bin\".");
|
||||||
|
println!(" Arguments after `--` are passed to the built binary (cargo-style).");
|
||||||
println!();
|
println!();
|
||||||
printc("OPTIONS:", BOLD_GREEN);
|
printc("OPTIONS:", BOLD_GREEN);
|
||||||
println!(" --debug Run with debug profile (default)");
|
println!(" --debug Run with debug profile (default)");
|
||||||
@@ -73,6 +74,7 @@ pub fn run(args: &[String]) -> i32 {
|
|||||||
println!(" --force Force a full rebuild");
|
println!(" --force Force a full rebuild");
|
||||||
println!(" --clean Clean before building");
|
println!(" --clean Clean before building");
|
||||||
println!(" --verbose Print detailed build output");
|
println!(" --verbose Print detailed build output");
|
||||||
|
println!(" -- <args>... Arguments forwarded to the binary");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,12 +114,13 @@ pub fn run(args: &[String]) -> i32 {
|
|||||||
if let Some(cmd) = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), "") {
|
if let Some(cmd) = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), "") {
|
||||||
let build_status = build(&args_for_build(&flags));
|
let build_status = build(&args_for_build(&flags));
|
||||||
if build_status == 0 {
|
if build_status == 0 {
|
||||||
|
let display = display_run_cmd(&cmd, &flags.bin_args);
|
||||||
println!(
|
println!(
|
||||||
" {} {}",
|
" {} {}",
|
||||||
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
||||||
cmd
|
display
|
||||||
);
|
);
|
||||||
return run_shell(&cmd);
|
return run_shell_with_args(&cmd, &flags.bin_args);
|
||||||
}
|
}
|
||||||
return build_status;
|
return build_status;
|
||||||
}
|
}
|
||||||
@@ -204,19 +207,17 @@ fn run_project(
|
|||||||
|
|
||||||
let out_dir =
|
let out_dir =
|
||||||
crate::cli::build::get_build_string_with_profile(&config, "out_dir", &flags.profile);
|
crate::cli::build::get_build_string_with_profile(&config, "out_dir", &flags.profile);
|
||||||
let normalized_target_dir = if !out_dir.is_empty() {
|
let build_target_str = target.clone().unwrap_or_default();
|
||||||
Some(out_dir)
|
let has_explicit = target.as_ref().is_some_and(|t| !t.trim().is_empty());
|
||||||
} else {
|
|
||||||
match workspace_root {
|
let normalized_target_dir = Some(crate::utils::build::resolve_artifact_target_dir(
|
||||||
Some(wr) => target
|
root,
|
||||||
.as_ref()
|
workspace_root,
|
||||||
.and_then(|t| crate::utils::build::normalize_target(t, &flags.profile))
|
&flags.profile,
|
||||||
.map(|rel| wr.join(&rel).to_string_lossy().to_string()),
|
&build_target_str,
|
||||||
None => target
|
&out_dir,
|
||||||
.as_ref()
|
has_explicit,
|
||||||
.and_then(|t| crate::utils::build::normalize_target(t, &flags.profile)),
|
));
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let version = config
|
let version = config
|
||||||
.get("package.version")
|
.get("package.version")
|
||||||
@@ -250,32 +251,36 @@ fn run_project(
|
|||||||
);
|
);
|
||||||
if build_status == 0 {
|
if build_status == 0 {
|
||||||
if let Some(cmd) = run_cmd {
|
if let Some(cmd) = run_cmd {
|
||||||
|
let display = display_run_cmd(&cmd, &flags.bin_args);
|
||||||
println!(
|
println!(
|
||||||
" {} {}",
|
" {} {}",
|
||||||
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
||||||
cmd
|
display
|
||||||
);
|
);
|
||||||
return Ok(run_shell(&cmd));
|
return Ok(run_shell_with_args(&cmd, &flags.bin_args));
|
||||||
}
|
}
|
||||||
|
let display = display_bin_run(&bin_path, &flags.bin_args);
|
||||||
println!(
|
println!(
|
||||||
" {} {}",
|
" {} {}",
|
||||||
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
||||||
bin_path
|
display
|
||||||
);
|
);
|
||||||
return Ok(run_binary(
|
return Ok(run_binary(
|
||||||
project_name,
|
project_name,
|
||||||
&flags.profile,
|
&flags.profile,
|
||||||
normalized_target_dir.as_deref(),
|
normalized_target_dir.as_deref(),
|
||||||
|
&flags.bin_args,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let fallback_code = if let Some(cmd) = run_cmd {
|
let fallback_code = if let Some(cmd) = run_cmd {
|
||||||
run_shell(&cmd)
|
run_shell_with_args(&cmd, &flags.bin_args)
|
||||||
} else {
|
} else {
|
||||||
run_binary(
|
run_binary(
|
||||||
project_name,
|
project_name,
|
||||||
&flags.profile,
|
&flags.profile,
|
||||||
normalized_target_dir.as_deref(),
|
normalized_target_dir.as_deref(),
|
||||||
|
&flags.bin_args,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
if fallback_code != 1 {
|
if fallback_code != 1 {
|
||||||
@@ -345,6 +350,62 @@ fn run_shell(cmd: &str) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_shell_with_args(cmd: &str, bin_args: &[String]) -> i32 {
|
||||||
|
if bin_args.is_empty() {
|
||||||
|
return run_shell(cmd);
|
||||||
|
}
|
||||||
|
let mut full = cmd.to_string();
|
||||||
|
for arg in bin_args {
|
||||||
|
full.push(' ');
|
||||||
|
full.push_str(&shell_escape(arg));
|
||||||
|
}
|
||||||
|
run_shell(&full)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_run_cmd(cmd: &str, bin_args: &[String]) -> String {
|
||||||
|
if bin_args.is_empty() {
|
||||||
|
return cmd.to_string();
|
||||||
|
}
|
||||||
|
let mut display = cmd.to_string();
|
||||||
|
for arg in bin_args {
|
||||||
|
display.push(' ');
|
||||||
|
display.push_str(arg);
|
||||||
|
}
|
||||||
|
display
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_bin_run(bin_path: &str, bin_args: &[String]) -> String {
|
||||||
|
if bin_args.is_empty() {
|
||||||
|
return bin_path.to_string();
|
||||||
|
}
|
||||||
|
let mut display = bin_path.to_string();
|
||||||
|
for arg in bin_args {
|
||||||
|
display.push(' ');
|
||||||
|
display.push_str(arg);
|
||||||
|
}
|
||||||
|
display
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shell_escape(arg: &str) -> String {
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
// cmd.exe: wrap in double quotes and escape embedded quotes.
|
||||||
|
let escaped = arg.replace('"', "\\\"");
|
||||||
|
format!("\"{escaped}\"")
|
||||||
|
} else {
|
||||||
|
// POSIX sh: single-quote and escape embedded single quotes as '\''.
|
||||||
|
let mut out = String::from("'");
|
||||||
|
for ch in arg.chars() {
|
||||||
|
if ch == '\'' {
|
||||||
|
out.push_str("'\\''");
|
||||||
|
} else {
|
||||||
|
out.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push('\'');
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
|
fn substitute_run_vars(cmd: &str, profile: &str, version: &str) -> String {
|
||||||
let info = parse_version_info(version);
|
let info = parse_version_info(version);
|
||||||
substitute_vars(cmd, &info, profile, "")
|
substitute_vars(cmd, &info, profile, "")
|
||||||
|
|||||||
@@ -30,6 +30,64 @@ pub(crate) fn artifact_dir(ctx: &BuildContext) -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn resolve_artifact_path(
|
||||||
|
kind: &str,
|
||||||
|
profile: &str,
|
||||||
|
project_name: &str,
|
||||||
|
target_dir: Option<&str>,
|
||||||
|
output_filename: Option<&str>,
|
||||||
|
output_extension: Option<&str>,
|
||||||
|
) -> Option<String> {
|
||||||
|
if crate::utils::build::is_flat_bin(kind) {
|
||||||
|
let name = output_filename.unwrap_or(project_name);
|
||||||
|
let ext = output_extension.unwrap_or("bin");
|
||||||
|
let file = if ext.is_empty() {
|
||||||
|
name.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{name}.{ext}")
|
||||||
|
};
|
||||||
|
let dir = match target_dir {
|
||||||
|
Some(dir) => Path::new(dir).to_path_buf(),
|
||||||
|
None => Path::new("./target").join(profile),
|
||||||
|
};
|
||||||
|
return Some(dir.join(file).to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(kind, "none" | "custom") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = output_filename.unwrap_or(project_name);
|
||||||
|
let ext = output_extension.unwrap_or("");
|
||||||
|
let final_name = if ext.is_empty() {
|
||||||
|
name.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}.{}", name, ext)
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = match kind {
|
||||||
|
"staticlib" => platform::lib_path(profile, &final_name, target_dir),
|
||||||
|
"sharedlib" => platform::shared_lib_path(profile, &final_name, target_dir),
|
||||||
|
"efi" => platform::efi_path(profile, &final_name, target_dir),
|
||||||
|
"elf" => platform::elf_path(profile, &final_name, target_dir),
|
||||||
|
_ => platform::bin_path(profile, &final_name, target_dir),
|
||||||
|
};
|
||||||
|
Some(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn absolute_artifact_path(project_root: &Path, relative_or_abs: &str) -> PathBuf {
|
||||||
|
let p = Path::new(relative_or_abs);
|
||||||
|
let joined = if p.is_absolute() {
|
||||||
|
p.to_path_buf()
|
||||||
|
} else {
|
||||||
|
let stripped = relative_or_abs
|
||||||
|
.strip_prefix("./")
|
||||||
|
.unwrap_or(relative_or_abs);
|
||||||
|
project_root.join(stripped)
|
||||||
|
};
|
||||||
|
crate::utils::fs::canonicalize_path(&joined)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn flat_output_path(ctx: &BuildContext) -> String {
|
pub(crate) fn flat_output_path(ctx: &BuildContext) -> String {
|
||||||
let name = ctx.output_filename.unwrap_or(ctx.project_name);
|
let name = ctx.output_filename.unwrap_or(ctx.project_name);
|
||||||
let ext = ctx.output_extension.unwrap_or("bin");
|
let ext = ctx.output_extension.unwrap_or("bin");
|
||||||
@@ -282,3 +340,59 @@ fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result<f64,
|
|||||||
Err(format!("Build failed:\nstdout: {stdout}\nstderr: {stderr}"))
|
Err(format!("Build failed:\nstdout: {stdout}\nstderr: {stderr}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_bin_default() {
|
||||||
|
let p = resolve_artifact_path("bin", "debug", "hello", None, None, None).unwrap();
|
||||||
|
assert_eq!(p, platform::bin_path("debug", "hello", None));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_bin_custom_target_dir() {
|
||||||
|
let p = resolve_artifact_path("bin", "debug", "hello", Some("/out"), None, None).unwrap();
|
||||||
|
let name = if cfg!(windows) { "hello.exe" } else { "hello" };
|
||||||
|
let expected = Path::new("/out").join(name);
|
||||||
|
assert_eq!(p, expected.to_string_lossy());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_staticlib() {
|
||||||
|
let p = resolve_artifact_path("staticlib", "release", "mylib", Some("dist"), None, None)
|
||||||
|
.unwrap();
|
||||||
|
let name = if cfg!(windows) {
|
||||||
|
"mylib.lib"
|
||||||
|
} else {
|
||||||
|
"libmylib.a"
|
||||||
|
};
|
||||||
|
let expected = Path::new("dist").join(name);
|
||||||
|
assert_eq!(p, expected.to_string_lossy());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_with_filename_ext() {
|
||||||
|
let p = resolve_artifact_path(
|
||||||
|
"bin",
|
||||||
|
"debug",
|
||||||
|
"pkg",
|
||||||
|
Some("/out"),
|
||||||
|
Some("KERNEL"),
|
||||||
|
Some("ELF"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
p,
|
||||||
|
std::path::Path::new("/out")
|
||||||
|
.join("KERNEL.ELF")
|
||||||
|
.to_string_lossy()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_none_kind() {
|
||||||
|
assert!(resolve_artifact_path("none", "debug", "x", None, None, None).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -106,33 +106,22 @@ fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_output_path(ctx: &BuildContext) -> String {
|
pub fn build_output_path(ctx: &BuildContext) -> String {
|
||||||
if crate::utils::build::is_flat_bin(ctx.kind) {
|
crate::core::build::builder::artifact::resolve_artifact_path(
|
||||||
return crate::core::build::builder::artifact::flat_output_path(ctx);
|
ctx.kind,
|
||||||
}
|
ctx.profile,
|
||||||
|
ctx.project_name,
|
||||||
let name = ctx.output_filename.unwrap_or(ctx.project_name);
|
ctx.target_dir,
|
||||||
let ext = ctx.output_extension.unwrap_or("");
|
ctx.output_filename,
|
||||||
|
ctx.output_extension,
|
||||||
let final_name = if ext.is_empty() {
|
)
|
||||||
name.to_string()
|
.unwrap_or_else(|| {
|
||||||
} else {
|
crate::platform::bin_path(
|
||||||
format!("{}.{}", name, ext)
|
ctx.profile,
|
||||||
};
|
ctx.output_filename.unwrap_or(ctx.project_name),
|
||||||
|
ctx.target_dir,
|
||||||
if ctx.kind == "staticlib" {
|
)
|
||||||
return crate::platform::lib_path(ctx.profile, &final_name, ctx.target_dir);
|
})
|
||||||
}
|
|
||||||
if ctx.kind == "sharedlib" {
|
|
||||||
return crate::platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir);
|
|
||||||
}
|
|
||||||
if ctx.kind == "efi" {
|
|
||||||
return crate::platform::efi_path(ctx.profile, &final_name, ctx.target_dir);
|
|
||||||
}
|
|
||||||
if ctx.kind == "elf" {
|
|
||||||
return crate::platform::elf_path(ctx.profile, &final_name, ctx.target_dir);
|
|
||||||
}
|
|
||||||
crate::platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn collect_header_files(
|
pub(crate) fn collect_header_files(
|
||||||
|
|||||||
@@ -36,10 +36,11 @@ use crate::core::build_config::Config;
|
|||||||
use crate::core::deps::{register, resolve_deps};
|
use crate::core::deps::{register, resolve_deps};
|
||||||
use crate::core::workspace::parse_workspace;
|
use crate::core::workspace::parse_workspace;
|
||||||
use crate::utils::build::{
|
use crate::utils::build::{
|
||||||
get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile,
|
default_target_triple, get_bool_with_profile, get_config_opt, get_config_str,
|
||||||
is_bare_metal_target, normalize_kind, normalize_platform, normalize_target,
|
get_language_with_profile, is_bare_metal_target, normalize_kind, normalize_platform,
|
||||||
normalize_target_os, parse_version_info, prepend_clang_target_flag, primary_language,
|
normalize_target_os, parse_version_info, prepend_clang_target_flag, primary_language,
|
||||||
resolve_compiler, resolve_pkg_config_flags, resolve_tool, substitute_vars,
|
resolve_artifact_target_dir, resolve_compiler, resolve_pkg_config_flags, resolve_tool,
|
||||||
|
substitute_vars,
|
||||||
};
|
};
|
||||||
use crate::utils::fs::check_dir;
|
use crate::utils::fs::check_dir;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -61,26 +62,9 @@ fn ensure_target_dirs(
|
|||||||
if let Some(dir) = &target_dir {
|
if let Some(dir) = &target_dir {
|
||||||
let _ = fs::create_dir_all(dir);
|
let _ = fs::create_dir_all(dir);
|
||||||
} else {
|
} else {
|
||||||
let default_dir = if cfg!(target_os = "linux") {
|
let _ = fs::create_dir_all(
|
||||||
let arch = std::env::consts::ARCH;
|
project_root.join(crate::utils::build::native_host_target_rel(profile)),
|
||||||
format!("{arch}-unknown-linux-gnu/{profile}")
|
);
|
||||||
} else if cfg!(any(
|
|
||||||
target_os = "freebsd",
|
|
||||||
target_os = "openbsd",
|
|
||||||
target_os = "netbsd",
|
|
||||||
target_os = "dragonfly"
|
|
||||||
)) {
|
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let os = std::env::consts::OS;
|
|
||||||
format!("{arch}-unknown-{os}/{profile}")
|
|
||||||
} else {
|
|
||||||
profile.to_string()
|
|
||||||
};
|
|
||||||
let target_path = project_root.join("target");
|
|
||||||
let target_items = check_dir(target_path.to_str()).unwrap_or_default();
|
|
||||||
if !target_items.contains(&default_dir) {
|
|
||||||
let _ = fs::create_dir_all(target_path.join(&default_dir));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +143,7 @@ fn build_all(
|
|||||||
&member.path,
|
&member.path,
|
||||||
profile,
|
profile,
|
||||||
target,
|
target,
|
||||||
|
target.is_some_and(|t| !t.trim().is_empty()),
|
||||||
&[],
|
&[],
|
||||||
force,
|
force,
|
||||||
verbose,
|
verbose,
|
||||||
@@ -186,22 +171,34 @@ fn build_all(
|
|||||||
let project_name = get_config_str(&config, "package.name");
|
let project_name = get_config_str(&config, "package.name");
|
||||||
let project_version = get_config_str(&config, "package.version");
|
let project_version = get_config_str(&config, "package.version");
|
||||||
|
|
||||||
let targets_to_build: Vec<Option<String>> = if let Some(t) = target {
|
let targets_to_build: Vec<(Option<String>, bool)> = if let Some(t) = target {
|
||||||
vec![Some(normalize_target_os(t).to_string())]
|
let normalized = normalize_target_os(t);
|
||||||
|
if normalized.is_empty() {
|
||||||
|
vec![(Some(default_target_triple()), true)]
|
||||||
|
} else {
|
||||||
|
vec![(Some(normalized.to_string()), true)]
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let config_targets = get_targets(&config, profile)?;
|
let config_targets = get_targets(&config, profile)?;
|
||||||
if config_targets.is_empty() {
|
if config_targets.is_empty() {
|
||||||
vec![None]
|
vec![(Some(default_target_triple()), false)]
|
||||||
} else {
|
} else {
|
||||||
config_targets
|
config_targets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|t| Some(normalize_target_os(&t).to_string()))
|
.map(|t| {
|
||||||
|
let normalized = normalize_target_os(&t);
|
||||||
|
if normalized.is_empty() {
|
||||||
|
(Some(default_target_triple()), true)
|
||||||
|
} else {
|
||||||
|
(Some(normalized.to_string()), true)
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
for (i, build_target) in targets_to_build.iter().enumerate() {
|
for (i, (build_target, explicit_target)) in targets_to_build.iter().enumerate() {
|
||||||
if cancel.load(Ordering::SeqCst) {
|
if cancel.load(Ordering::SeqCst) {
|
||||||
return Err("Build interrupted".to_string());
|
return Err("Build interrupted".to_string());
|
||||||
}
|
}
|
||||||
@@ -226,6 +223,7 @@ fn build_all(
|
|||||||
&member.path,
|
&member.path,
|
||||||
profile,
|
profile,
|
||||||
build_target.as_deref(),
|
build_target.as_deref(),
|
||||||
|
*explicit_target,
|
||||||
&[],
|
&[],
|
||||||
force,
|
force,
|
||||||
verbose,
|
verbose,
|
||||||
@@ -239,6 +237,7 @@ fn build_all(
|
|||||||
&ws,
|
&ws,
|
||||||
profile,
|
profile,
|
||||||
build_target.as_deref(),
|
build_target.as_deref(),
|
||||||
|
*explicit_target,
|
||||||
force,
|
force,
|
||||||
verbose,
|
verbose,
|
||||||
Some(root),
|
Some(root),
|
||||||
@@ -251,6 +250,7 @@ fn build_all(
|
|||||||
root,
|
root,
|
||||||
profile,
|
profile,
|
||||||
build_target.as_deref(),
|
build_target.as_deref(),
|
||||||
|
*explicit_target,
|
||||||
&excludes,
|
&excludes,
|
||||||
force,
|
force,
|
||||||
verbose,
|
verbose,
|
||||||
@@ -264,6 +264,7 @@ fn build_all(
|
|||||||
root,
|
root,
|
||||||
profile,
|
profile,
|
||||||
build_target.as_deref(),
|
build_target.as_deref(),
|
||||||
|
*explicit_target,
|
||||||
&[],
|
&[],
|
||||||
force,
|
force,
|
||||||
verbose,
|
verbose,
|
||||||
@@ -283,6 +284,7 @@ fn build_workspace(
|
|||||||
workspace: &crate::core::workspace::Workspace,
|
workspace: &crate::core::workspace::Workspace,
|
||||||
profile: &str,
|
profile: &str,
|
||||||
target: Option<&str>,
|
target: Option<&str>,
|
||||||
|
explicit_target: bool,
|
||||||
force: bool,
|
force: bool,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
workspace_root: Option<&Path>,
|
workspace_root: Option<&Path>,
|
||||||
@@ -294,6 +296,7 @@ fn build_workspace(
|
|||||||
&member.path,
|
&member.path,
|
||||||
profile,
|
profile,
|
||||||
target,
|
target,
|
||||||
|
explicit_target,
|
||||||
&[],
|
&[],
|
||||||
force,
|
force,
|
||||||
verbose,
|
verbose,
|
||||||
@@ -310,6 +313,7 @@ fn build_project_at(
|
|||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
profile: &str,
|
profile: &str,
|
||||||
target: Option<&str>,
|
target: Option<&str>,
|
||||||
|
explicit_target: bool,
|
||||||
exclude_dirs: &[std::path::PathBuf],
|
exclude_dirs: &[std::path::PathBuf],
|
||||||
force: bool,
|
force: bool,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
@@ -344,12 +348,43 @@ fn build_project_at(
|
|||||||
}
|
}
|
||||||
let project_name = get_config_str(&config, "package.name");
|
let project_name = get_config_str(&config, "package.name");
|
||||||
let project_version = get_config_str(&config, "package.version");
|
let project_version = get_config_str(&config, "package.version");
|
||||||
|
let host_target = default_target_triple();
|
||||||
let build_target_config = get_build_string_with_profile(&config, "target", profile);
|
let build_target_config = get_build_string_with_profile(&config, "target", profile);
|
||||||
let build_target = target.or(if build_target_config.is_empty() {
|
let has_explicit_target = explicit_target || !build_target_config.is_empty();
|
||||||
None
|
let explicit_build_target = explicit_target
|
||||||
} else {
|
.then_some(target)
|
||||||
Some(build_target_config.as_str())
|
.flatten()
|
||||||
});
|
.filter(|t| !t.trim().is_empty())
|
||||||
|
.map(|t| {
|
||||||
|
let normalized = normalize_target_os(t);
|
||||||
|
if normalized.is_empty() {
|
||||||
|
host_target.clone()
|
||||||
|
} else {
|
||||||
|
normalized.to_string()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let configured_build_target = || {
|
||||||
|
if build_target_config.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let normalized = normalize_target_os(&build_target_config);
|
||||||
|
if normalized.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(normalized.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let implicit_build_target = || {
|
||||||
|
target
|
||||||
|
.filter(|t| !t.trim().is_empty())
|
||||||
|
.map(|t| normalize_target_os(t).to_string())
|
||||||
|
};
|
||||||
|
let build_target_owned = explicit_build_target
|
||||||
|
.or_else(configured_build_target)
|
||||||
|
.or_else(implicit_build_target)
|
||||||
|
.unwrap_or(host_target);
|
||||||
|
let build_target = Some(build_target_owned.as_str());
|
||||||
let build_language = get_language_with_profile(&config, profile)?;
|
let build_language = get_language_with_profile(&config, profile)?;
|
||||||
let build_qt = get_bool_with_profile(&config, "qt", profile, false);
|
let build_qt = get_bool_with_profile(&config, "qt", profile, false);
|
||||||
let lang_key = primary_language(&build_language);
|
let lang_key = primary_language(&build_language);
|
||||||
@@ -493,68 +528,14 @@ fn build_project_at(
|
|||||||
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());
|
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());
|
||||||
|
|
||||||
let out_dir_config = get_build_string_with_profile(&config, "out_dir", profile);
|
let out_dir_config = get_build_string_with_profile(&config, "out_dir", profile);
|
||||||
let target_dir_binding = if !out_dir_config.is_empty() {
|
let target_dir_binding = resolve_artifact_target_dir(
|
||||||
let p = Path::new(&out_dir_config);
|
project_root,
|
||||||
if p.is_absolute() {
|
workspace_root,
|
||||||
out_dir_config
|
profile,
|
||||||
} else {
|
&build_target_owned,
|
||||||
project_root.join(p).to_string_lossy().to_string()
|
&out_dir_config,
|
||||||
}
|
has_explicit_target,
|
||||||
} else {
|
);
|
||||||
match workspace_root {
|
|
||||||
Some(root) => {
|
|
||||||
let target_str = build_target
|
|
||||||
.map(normalize_target_os)
|
|
||||||
.filter(|t| !t.is_empty());
|
|
||||||
let rel: PathBuf = match target_str {
|
|
||||||
Some(ref t) => Path::new("target").join(t).join(profile),
|
|
||||||
None => {
|
|
||||||
let default_dir = if cfg!(target_os = "linux") {
|
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
format!("{arch}-unknown-linux-gnu/{profile}")
|
|
||||||
} else if cfg!(any(
|
|
||||||
target_os = "freebsd",
|
|
||||||
target_os = "openbsd",
|
|
||||||
target_os = "netbsd",
|
|
||||||
target_os = "dragonfly"
|
|
||||||
)) {
|
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let os = std::env::consts::OS;
|
|
||||||
format!("{arch}-unknown-{os}/{profile}")
|
|
||||||
} else {
|
|
||||||
profile.to_string()
|
|
||||||
};
|
|
||||||
Path::new("target").join(&default_dir)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
root.join(&rel).to_string_lossy().to_string()
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let base_dir =
|
|
||||||
if let Some(rel) = normalize_target(build_target.unwrap_or(""), profile) {
|
|
||||||
PathBuf::from(rel)
|
|
||||||
} else {
|
|
||||||
let default_dir = if cfg!(target_os = "linux") {
|
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
format!("{arch}-unknown-linux-gnu/{profile}")
|
|
||||||
} else if cfg!(any(
|
|
||||||
target_os = "freebsd",
|
|
||||||
target_os = "openbsd",
|
|
||||||
target_os = "netbsd",
|
|
||||||
target_os = "dragonfly"
|
|
||||||
)) {
|
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let os = std::env::consts::OS;
|
|
||||||
format!("{arch}-unknown-{os}/{profile}")
|
|
||||||
} else {
|
|
||||||
profile.to_string()
|
|
||||||
};
|
|
||||||
Path::new("target").join(&default_dir)
|
|
||||||
};
|
|
||||||
project_root.join(base_dir).to_string_lossy().to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let target_dir = if target_dir_binding.is_empty() {
|
let target_dir = if target_dir_binding.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -612,24 +593,9 @@ fn build_project_at(
|
|||||||
let target_str = build_target
|
let target_str = build_target
|
||||||
.map(normalize_target_os)
|
.map(normalize_target_os)
|
||||||
.filter(|t| !t.is_empty());
|
.filter(|t| !t.is_empty());
|
||||||
let default_dir = if cfg!(target_os = "linux") {
|
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
format!("{arch}-unknown-linux-gnu/{profile}")
|
|
||||||
} else if cfg!(any(
|
|
||||||
target_os = "freebsd",
|
|
||||||
target_os = "openbsd",
|
|
||||||
target_os = "netbsd",
|
|
||||||
target_os = "dragonfly"
|
|
||||||
)) {
|
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let os = std::env::consts::OS;
|
|
||||||
format!("{arch}-unknown-{os}/{profile}")
|
|
||||||
} else {
|
|
||||||
profile.to_string()
|
|
||||||
};
|
|
||||||
let rel = match target_str {
|
let rel = match target_str {
|
||||||
Some(ref t) => Path::new("target").join(t).join(profile),
|
Some(ref t) => Path::new("target").join(t).join(profile),
|
||||||
None => Path::new("target").join(&default_dir),
|
None => crate::utils::build::native_host_target_rel(profile),
|
||||||
};
|
};
|
||||||
let dep_build_lib1 = dep_member.path.join(&rel);
|
let dep_build_lib1 = dep_member.path.join(&rel);
|
||||||
let dep_build_lib2 = wroot.join(&rel);
|
let dep_build_lib2 = wroot.join(&rel);
|
||||||
@@ -694,6 +660,7 @@ fn build_project_at(
|
|||||||
&dep_root,
|
&dep_root,
|
||||||
profile,
|
profile,
|
||||||
build_target,
|
build_target,
|
||||||
|
has_explicit_target,
|
||||||
&[],
|
&[],
|
||||||
force,
|
force,
|
||||||
verbose,
|
verbose,
|
||||||
|
|||||||
@@ -19,10 +19,15 @@ use crate::platform;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -> i32 {
|
pub fn run_binary(
|
||||||
|
project_name: &str,
|
||||||
|
profile: &str,
|
||||||
|
target_dir: Option<&str>,
|
||||||
|
bin_args: &[String],
|
||||||
|
) -> i32 {
|
||||||
let bin_path = platform::bin_path(profile, project_name, target_dir);
|
let bin_path = platform::bin_path(profile, project_name, target_dir);
|
||||||
if Path::new(&bin_path).exists() {
|
if Path::new(&bin_path).exists() {
|
||||||
let status = Command::new(&bin_path).status();
|
let status = Command::new(&bin_path).args(bin_args).status();
|
||||||
match status {
|
match status {
|
||||||
Ok(s) => {
|
Ok(s) => {
|
||||||
return s.code().unwrap_or(0);
|
return s.code().unwrap_or(0);
|
||||||
|
|||||||
@@ -24,8 +24,11 @@ pub fn init(path: &Path) -> Result<(), String> {
|
|||||||
|
|
||||||
let gitignore_path = path.join(".gitignore");
|
let gitignore_path = path.join(".gitignore");
|
||||||
if !gitignore_path.exists() {
|
if !gitignore_path.exists() {
|
||||||
fs::write(&gitignore_path, "/target\n")
|
fs::write(&gitignore_path, "/target\n.dcr/\n")
|
||||||
.map_err(|e| format!("failed to create .gitignore: {}", e))?;
|
.map_err(|e| format!("failed to create .gitignore: {}", e))?;
|
||||||
|
} else {
|
||||||
|
crate::utils::fs::ensure_gitignore_has_dcr(path)
|
||||||
|
.map_err(|e| format!("failed to update .gitignore: {}", e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -15,63 +15,45 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
||||||
|
use crate::utils::build::default_target_triple;
|
||||||
|
|
||||||
|
fn host_triple() -> String {
|
||||||
|
default_target_triple()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, name),
|
||||||
None => {
|
None => default_bin_rel(profile, name, Some(&host_triple())),
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let os = std::env::consts::OS;
|
|
||||||
let target = format!("{arch}-unknown-{os}");
|
|
||||||
format!("./target/{target}/{profile}/{name}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = format!("lib{name}.a");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => {
|
None => default_lib_rel(profile, &file, Some(&host_triple())),
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let os = std::env::consts::OS;
|
|
||||||
let target = format!("{arch}-unknown-{os}");
|
|
||||||
format!("./target/{target}/{profile}/lib{name}.a")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
match target_dir {
|
bin_path(profile, name, target_dir)
|
||||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
|
||||||
None => {
|
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let os = std::env::consts::OS;
|
|
||||||
let target = format!("{arch}-unknown-{os}");
|
|
||||||
format!("./target/{target}/{profile}/{name}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = format!("{name}.efi");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => {
|
None => default_lib_rel(profile, &file, Some(&host_triple())),
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let os = std::env::consts::OS;
|
|
||||||
let target = format!("{arch}-unknown-{os}");
|
|
||||||
format!("./target/{target}/{profile}/{name}.efi")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = format!("lib{name}.so");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => {
|
None => default_lib_rel(profile, &file, Some(&host_triple())),
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let os = std::env::consts::OS;
|
|
||||||
let target = format!("{arch}-unknown-{os}");
|
|
||||||
format!("./target/{target}/{profile}/lib{name}.so")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,25 +63,19 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bin_path_default() {
|
fn bin_path_default() {
|
||||||
let arch = std::env::consts::ARCH;
|
let expected = default_bin_rel("debug", "hello", Some(&host_triple()));
|
||||||
let os = std::env::consts::OS;
|
|
||||||
let expected = format!("./target/{arch}-unknown-{os}/debug/hello");
|
|
||||||
assert_eq!(bin_path("debug", "hello", None), expected);
|
assert_eq!(bin_path("debug", "hello", None), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lib_path_default() {
|
fn lib_path_default() {
|
||||||
let arch = std::env::consts::ARCH;
|
let expected = default_lib_rel("debug", "libmylib.a", Some(&host_triple()));
|
||||||
let os = std::env::consts::OS;
|
|
||||||
let expected = format!("./target/{arch}-unknown-{os}/debug/libmylib.a");
|
|
||||||
assert_eq!(lib_path("debug", "mylib", None), expected);
|
assert_eq!(lib_path("debug", "mylib", None), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shared_lib_path_default() {
|
fn shared_lib_path_default() {
|
||||||
let arch = std::env::consts::ARCH;
|
let expected = default_lib_rel("debug", "libmylib.so", Some(&host_triple()));
|
||||||
let os = std::env::consts::OS;
|
|
||||||
let expected = format!("./target/{arch}-unknown-{os}/debug/libmylib.so");
|
|
||||||
assert_eq!(shared_lib_path("debug", "mylib", None), expected);
|
assert_eq!(shared_lib_path("debug", "mylib", None), expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,58 +15,45 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
||||||
|
use crate::utils::build::default_target_triple;
|
||||||
|
|
||||||
|
fn host_triple() -> String {
|
||||||
|
default_target_triple()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, name),
|
||||||
None => {
|
None => default_bin_rel(profile, name, Some(&host_triple())),
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let target = format!("{arch}-unknown-linux-gnu");
|
|
||||||
format!("./target/{target}/{profile}/{name}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = format!("lib{name}.a");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => {
|
None => default_lib_rel(profile, &file, Some(&host_triple())),
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let target = format!("{arch}-unknown-linux-gnu");
|
|
||||||
format!("./target/{target}/{profile}/lib{name}.a")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
match target_dir {
|
bin_path(profile, name, target_dir)
|
||||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
|
||||||
None => {
|
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let target = format!("{arch}-unknown-linux-gnu");
|
|
||||||
format!("./target/{target}/{profile}/{name}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = format!("{name}.efi");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => {
|
None => default_lib_rel(profile, &file, Some(&host_triple())),
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let target = format!("{arch}-unknown-linux-gnu");
|
|
||||||
format!("./target/{target}/{profile}/{name}.efi")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = format!("lib{name}.so");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => {
|
None => default_lib_rel(profile, &file, Some(&host_triple())),
|
||||||
let arch = std::env::consts::ARCH;
|
|
||||||
let target = format!("{arch}-unknown-linux-gnu");
|
|
||||||
format!("./target/{target}/{profile}/lib{name}.so")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,44 +63,49 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bin_path_default() {
|
fn bin_path_default() {
|
||||||
let arch = std::env::consts::ARCH;
|
let expected = default_bin_rel("debug", "hello", Some(&host_triple()));
|
||||||
let expected = format!("./target/{arch}-unknown-linux-gnu/debug/hello");
|
|
||||||
assert_eq!(bin_path("debug", "hello", None), expected);
|
assert_eq!(bin_path("debug", "hello", None), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bin_path_release() {
|
fn bin_path_release() {
|
||||||
let arch = std::env::consts::ARCH;
|
let expected = default_bin_rel("release", "hello", Some(&host_triple()));
|
||||||
let expected = format!("./target/{arch}-unknown-linux-gnu/release/hello");
|
|
||||||
assert_eq!(bin_path("release", "hello", None), expected);
|
assert_eq!(bin_path("release", "hello", None), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bin_path_custom_target() {
|
fn bin_path_custom_target() {
|
||||||
assert_eq!(bin_path("debug", "hello", Some("out")), "out/hello");
|
assert_eq!(
|
||||||
|
bin_path("debug", "hello", Some("out")),
|
||||||
|
join_dir("out", "hello")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bin_path_custom_target_trailing_slash() {
|
fn bin_path_custom_target_trailing_slash() {
|
||||||
assert_eq!(bin_path("debug", "hello", Some("out/")), "out/hello");
|
assert_eq!(
|
||||||
|
bin_path("debug", "hello", Some("out/")),
|
||||||
|
join_dir("out/", "hello")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lib_path_default() {
|
fn lib_path_default() {
|
||||||
let arch = std::env::consts::ARCH;
|
let expected = default_lib_rel("debug", "libmylib.a", Some(&host_triple()));
|
||||||
let expected = format!("./target/{arch}-unknown-linux-gnu/debug/libmylib.a");
|
|
||||||
assert_eq!(lib_path("debug", "mylib", None), expected);
|
assert_eq!(lib_path("debug", "mylib", None), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lib_path_custom_target() {
|
fn lib_path_custom_target() {
|
||||||
assert_eq!(lib_path("debug", "mylib", Some("out")), "out/libmylib.a");
|
assert_eq!(
|
||||||
|
lib_path("debug", "mylib", Some("out")),
|
||||||
|
join_dir("out", "libmylib.a")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shared_lib_path_default() {
|
fn shared_lib_path_default() {
|
||||||
let arch = std::env::consts::ARCH;
|
let expected = default_lib_rel("debug", "libmylib.so", Some(&host_triple()));
|
||||||
let expected = format!("./target/{arch}-unknown-linux-gnu/debug/libmylib.so");
|
|
||||||
assert_eq!(shared_lib_path("debug", "mylib", None), expected);
|
assert_eq!(shared_lib_path("debug", "mylib", None), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +113,7 @@ mod tests {
|
|||||||
fn shared_lib_path_custom_target() {
|
fn shared_lib_path_custom_target() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
shared_lib_path("release", "mylib", Some("dist")),
|
shared_lib_path("release", "mylib", Some("dist")),
|
||||||
"dist/libmylib.so"
|
join_dir("dist", "libmylib.so")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,37 +15,39 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
||||||
|
|
||||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, name),
|
||||||
None => format!("./target/{profile}/{name}"),
|
None => default_bin_rel(profile, name, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = format!("lib{name}.a");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => format!("./target/{profile}/lib{name}.a"),
|
None => default_lib_rel(profile, &file, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
match target_dir {
|
bin_path(profile, name, target_dir)
|
||||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
|
||||||
None => format!("./target/{profile}/{name}"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = format!("{name}.efi");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => format!("./target/{profile}/{name}.efi"),
|
None => default_lib_rel(profile, &file, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = format!("lib{name}.dylib");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/lib{}.dylib", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => format!("./target/{profile}/lib{name}.dylib"),
|
None => default_lib_rel(profile, &file, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ pub mod bsd;
|
|||||||
pub mod linux;
|
pub mod linux;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub mod macos;
|
pub mod macos;
|
||||||
|
mod path_util;
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(
|
||||||
target_os = "linux",
|
target_os = "linux",
|
||||||
target_os = "macos",
|
target_os = "macos",
|
||||||
|
|||||||
80
src/platform/path_util.rs
Normal file
80
src/platform/path_util.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// 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, PathBuf};
|
||||||
|
|
||||||
|
pub fn join_dir(dir: &str, file: &str) -> String {
|
||||||
|
let trimmed = dir.trim_end_matches(['/', '\\']);
|
||||||
|
Path::new(trimmed).join(file).to_string_lossy().into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(not(windows), allow(dead_code))]
|
||||||
|
pub fn with_exe_suffix(name: &str) -> String {
|
||||||
|
let lower = name.to_ascii_lowercase();
|
||||||
|
if lower.ends_with(".exe")
|
||||||
|
|| lower.ends_with(".dll")
|
||||||
|
|| lower.ends_with(".sys")
|
||||||
|
|| lower.ends_with(".com")
|
||||||
|
|| lower.ends_with(".efi")
|
||||||
|
|| Path::new(name).extension().is_some()
|
||||||
|
{
|
||||||
|
name.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{name}.exe")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_bin_rel(profile: &str, name: &str, triple_segment: Option<&str>) -> String {
|
||||||
|
let mut p = PathBuf::from("./target");
|
||||||
|
if let Some(t) = triple_segment {
|
||||||
|
p = p.join(t);
|
||||||
|
}
|
||||||
|
p.join(profile).join(name).to_string_lossy().into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_lib_rel(profile: &str, file: &str, triple_segment: Option<&str>) -> String {
|
||||||
|
let mut p = PathBuf::from("./target");
|
||||||
|
if let Some(t) = triple_segment {
|
||||||
|
p = p.join(t);
|
||||||
|
}
|
||||||
|
p.join(profile).join(file).to_string_lossy().into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn join_dir_trims_both_slashes() {
|
||||||
|
assert_eq!(
|
||||||
|
join_dir("out/", "a"),
|
||||||
|
Path::new("out").join("a").to_string_lossy()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
join_dir(r"out\", "a"),
|
||||||
|
Path::new("out").join("a").to_string_lossy()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn with_exe_no_double() {
|
||||||
|
assert_eq!(with_exe_suffix("hello"), "hello.exe");
|
||||||
|
assert_eq!(with_exe_suffix("hello.exe"), "hello.exe");
|
||||||
|
assert_eq!(with_exe_suffix("KERNEL.ELF"), "KERNEL.ELF");
|
||||||
|
assert_eq!(with_exe_suffix("x.dll"), "x.dll");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
|
use super::path_util::{default_bin_rel, default_lib_rel, join_dir};
|
||||||
|
|
||||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, name),
|
||||||
None => format!("./target/{profile}/{name}"),
|
None => default_bin_rel(profile, name, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = format!("lib{name}.a");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/lib{}.a", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => format!("./target/{profile}/lib{name}.a"),
|
None => default_lib_rel(profile, &file, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,15 +20,17 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = format!("{name}.efi");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => format!("./target/{profile}/{name}.efi"),
|
None => default_lib_rel(profile, &file, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = format!("lib{name}.so");
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/lib{}.so", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => format!("./target/{profile}/lib{name}.so"),
|
None => default_lib_rel(profile, &file, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,37 +15,52 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use super::path_util::{default_bin_rel, default_lib_rel, join_dir, with_exe_suffix};
|
||||||
|
|
||||||
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = with_exe_suffix(name);
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/{}.exe", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => format!("./target/{profile}/{name}.exe"),
|
None => default_bin_rel(profile, &file, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = if name.to_ascii_lowercase().ends_with(".lib") {
|
||||||
|
name.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{name}.lib")
|
||||||
|
};
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/{}.lib", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => format!("./target/{profile}/{name}.lib"),
|
None => default_lib_rel(profile, &file, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
match target_dir {
|
bin_path(profile, name, target_dir)
|
||||||
Some(dir) => format!("{}/{}.exe", dir.trim_end_matches('/'), name),
|
|
||||||
None => format!("./target/{profile}/{name}.exe"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = if name.to_ascii_lowercase().ends_with(".efi") {
|
||||||
|
name.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{name}.efi")
|
||||||
|
};
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/{}.efi", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => format!("./target/{profile}/{name}.efi"),
|
None => default_lib_rel(profile, &file, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
|
||||||
|
let file = if name.to_ascii_lowercase().ends_with(".dll") {
|
||||||
|
name.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{name}.dll")
|
||||||
|
};
|
||||||
match target_dir {
|
match target_dir {
|
||||||
Some(dir) => format!("{}/{}.dll", dir.trim_end_matches('/'), name),
|
Some(dir) => join_dir(dir, &file),
|
||||||
None => format!("./target/{profile}/{name}.dll"),
|
None => default_lib_rel(profile, &file, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
use crate::core::build_config::Config;
|
use crate::core::build_config::Config;
|
||||||
use crate::utils::log::warn;
|
use crate::utils::log::warn;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub struct VersionInfo {
|
pub struct VersionInfo {
|
||||||
pub full: String,
|
pub full: String,
|
||||||
@@ -81,6 +82,69 @@ pub fn normalize_target(target: &str, profile: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn native_host_target_rel(profile: &str) -> PathBuf {
|
||||||
|
if cfg!(any(
|
||||||
|
target_os = "linux",
|
||||||
|
target_os = "freebsd",
|
||||||
|
target_os = "openbsd",
|
||||||
|
target_os = "netbsd",
|
||||||
|
target_os = "dragonfly"
|
||||||
|
)) {
|
||||||
|
Path::new("target")
|
||||||
|
.join(default_target_triple())
|
||||||
|
.join(profile)
|
||||||
|
} else {
|
||||||
|
Path::new("target").join(profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_artifact_target_dir(
|
||||||
|
project_root: &Path,
|
||||||
|
workspace_root: Option<&Path>,
|
||||||
|
profile: &str,
|
||||||
|
build_target: &str,
|
||||||
|
out_dir: &str,
|
||||||
|
has_explicit_target: bool,
|
||||||
|
) -> String {
|
||||||
|
if !out_dir.trim().is_empty() {
|
||||||
|
let p = Path::new(out_dir.trim());
|
||||||
|
return if p.is_absolute() {
|
||||||
|
out_dir.trim().to_string()
|
||||||
|
} else {
|
||||||
|
project_root.join(p).to_string_lossy().into_owned()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ws) = workspace_root {
|
||||||
|
let triple = if build_target.trim().is_empty() {
|
||||||
|
default_target_triple()
|
||||||
|
} else {
|
||||||
|
let n = normalize_target_os(build_target.trim());
|
||||||
|
if n.is_empty() {
|
||||||
|
default_target_triple()
|
||||||
|
} else {
|
||||||
|
n.to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return ws
|
||||||
|
.join("target")
|
||||||
|
.join(triple)
|
||||||
|
.join(profile)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
let base = if has_explicit_target {
|
||||||
|
match normalize_target(build_target, profile) {
|
||||||
|
Some(rel) => PathBuf::from(rel),
|
||||||
|
None => native_host_target_rel(profile),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
native_host_target_rel(profile)
|
||||||
|
};
|
||||||
|
project_root.join(base).to_string_lossy().into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn normalize_kind(kind: &str) -> &str {
|
pub fn normalize_kind(kind: &str) -> &str {
|
||||||
let trimmed = kind.trim();
|
let trimmed = kind.trim();
|
||||||
if trimmed.is_empty() { "bin" } else { trimmed }
|
if trimmed.is_empty() { "bin" } else { trimmed }
|
||||||
@@ -105,6 +169,7 @@ pub fn normalize_platform(platform: &str) -> Option<&str> {
|
|||||||
|
|
||||||
pub fn normalize_target_os(target: &str) -> &str {
|
pub fn normalize_target_os(target: &str) -> &str {
|
||||||
match target {
|
match target {
|
||||||
|
"" => "",
|
||||||
"linux" => "x86_64-unknown-linux-gnu",
|
"linux" => "x86_64-unknown-linux-gnu",
|
||||||
"macos" => "x86_64-apple-darwin",
|
"macos" => "x86_64-apple-darwin",
|
||||||
"windows" => "x86_64-pc-windows-msvc",
|
"windows" => "x86_64-pc-windows-msvc",
|
||||||
@@ -515,6 +580,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normalize_target_short_names() {
|
fn normalize_target_short_names() {
|
||||||
|
assert_eq!(normalize_target_os(""), "");
|
||||||
assert_eq!(normalize_target_os("linux"), "x86_64-unknown-linux-gnu");
|
assert_eq!(normalize_target_os("linux"), "x86_64-unknown-linux-gnu");
|
||||||
assert_eq!(normalize_target_os("macos"), "x86_64-apple-darwin");
|
assert_eq!(normalize_target_os("macos"), "x86_64-apple-darwin");
|
||||||
assert_eq!(normalize_target_os("windows"), "x86_64-pc-windows-msvc");
|
assert_eq!(normalize_target_os("windows"), "x86_64-pc-windows-msvc");
|
||||||
@@ -529,16 +595,26 @@ mod tests {
|
|||||||
fn default_target_triple_uses_host_arch() {
|
fn default_target_triple_uses_host_arch() {
|
||||||
let target = default_target_triple();
|
let target = default_target_triple();
|
||||||
if cfg!(target_os = "linux") {
|
if cfg!(target_os = "linux") {
|
||||||
|
let env = if cfg!(target_env = "musl") {
|
||||||
|
"musl"
|
||||||
|
} else {
|
||||||
|
"gnu"
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
target,
|
target,
|
||||||
format!("{}-unknown-linux-gnu", std::env::consts::ARCH)
|
format!("{}-unknown-linux-{env}", std::env::consts::ARCH)
|
||||||
);
|
);
|
||||||
} else if cfg!(target_os = "macos") {
|
} else if cfg!(target_os = "macos") {
|
||||||
assert_eq!(target, format!("{}-apple-darwin", std::env::consts::ARCH));
|
assert_eq!(target, format!("{}-apple-darwin", std::env::consts::ARCH));
|
||||||
} else if cfg!(target_os = "windows") {
|
} else if cfg!(target_os = "windows") {
|
||||||
|
let env = if cfg!(target_env = "gnu") {
|
||||||
|
"gnu"
|
||||||
|
} else {
|
||||||
|
"msvc"
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
target,
|
target,
|
||||||
format!("{}-pc-windows-msvc", std::env::consts::ARCH)
|
format!("{}-pc-windows-{env}", std::env::consts::ARCH)
|
||||||
);
|
);
|
||||||
} else if cfg!(any(
|
} else if cfg!(any(
|
||||||
target_os = "freebsd",
|
target_os = "freebsd",
|
||||||
@@ -559,6 +635,26 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_artifact_target_dir_native_and_workspace() {
|
||||||
|
let root = Path::new("/proj");
|
||||||
|
let ws = Path::new("/ws");
|
||||||
|
let host = default_target_triple();
|
||||||
|
let native = resolve_artifact_target_dir(root, None, "debug", &host, "", false);
|
||||||
|
let expected_native = root.join(native_host_target_rel("debug"));
|
||||||
|
assert_eq!(native, expected_native.to_string_lossy());
|
||||||
|
|
||||||
|
let explicit =
|
||||||
|
resolve_artifact_target_dir(root, None, "debug", "x86_64-unknown-linux-gnu", "", true);
|
||||||
|
assert!(explicit.contains("x86_64-unknown-linux-gnu"));
|
||||||
|
|
||||||
|
let ws_dir = resolve_artifact_target_dir(root, Some(ws), "debug", &host, "", false);
|
||||||
|
assert!(ws_dir.starts_with("/ws") || ws_dir.contains("ws"));
|
||||||
|
assert!(ws_dir.contains(&host) || cfg!(windows));
|
||||||
|
let out = resolve_artifact_target_dir(root, None, "debug", "", "dist", false);
|
||||||
|
assert!(out.contains("dist"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn prepend_clang_target_flag_adds_once_for_clang() {
|
fn prepend_clang_target_flag_adds_once_for_clang() {
|
||||||
let mut flags = vec!["-O2".to_string()];
|
let mut flags = vec!["-O2".to_string()];
|
||||||
@@ -593,13 +689,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normalize_target_with_profile() {
|
fn normalize_target_with_profile() {
|
||||||
|
let linux = normalize_target_os("linux");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
normalize_target("linux", "debug"),
|
normalize_target("linux", "debug"),
|
||||||
Some("target/x86_64-unknown-linux-gnu/debug".into())
|
Some(format!("target/{linux}/debug"))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
normalize_target("x86_64-unknown-linux-gnu", "release"),
|
normalize_target(linux, "release"),
|
||||||
Some("target/x86_64-unknown-linux-gnu/release".into())
|
Some(format!("target/{linux}/release"))
|
||||||
);
|
);
|
||||||
assert_eq!(normalize_target("", "debug"), None);
|
assert_eq!(normalize_target("", "debug"), None);
|
||||||
}
|
}
|
||||||
|
|||||||
152
src/utils/fs.rs
152
src/utils/fs.rs
@@ -16,6 +16,7 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub fn home_dir() -> Option<PathBuf> {
|
pub fn home_dir() -> Option<PathBuf> {
|
||||||
@@ -75,3 +76,154 @@ where
|
|||||||
let _ = std::env::set_current_dir(prev);
|
let _ = std::env::set_current_dir(prev);
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn strip_verbatim_prefix(path: PathBuf) -> PathBuf {
|
||||||
|
let s = path.to_string_lossy();
|
||||||
|
let mut cleaned = s.replace('\\', "/");
|
||||||
|
if let Some(rest) = cleaned.strip_prefix("//?/") {
|
||||||
|
cleaned = rest.to_string();
|
||||||
|
} else if let Some(rest) = cleaned.strip_prefix("//./") {
|
||||||
|
cleaned = rest.to_string();
|
||||||
|
} else if let Some(rest) = cleaned.strip_prefix(r"\\?\") {
|
||||||
|
cleaned = rest.replace('\\', "/");
|
||||||
|
} else if let Some(rest) = cleaned.strip_prefix(r"\\.\") {
|
||||||
|
cleaned = rest.replace('\\', "/");
|
||||||
|
}
|
||||||
|
if cleaned.len() >= 3 && cleaned.as_bytes()[0] == b'/' && cleaned.as_bytes()[2] == b':' {
|
||||||
|
cleaned = cleaned[1..].to_string();
|
||||||
|
}
|
||||||
|
if cfg!(windows) {
|
||||||
|
PathBuf::from(cleaned.replace('/', "\\"))
|
||||||
|
} else {
|
||||||
|
PathBuf::from(cleaned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn canonicalize_path(path: &Path) -> PathBuf {
|
||||||
|
let raw = std::fs::canonicalize(path).unwrap_or_else(|_| {
|
||||||
|
if path.is_absolute() {
|
||||||
|
path.to_path_buf()
|
||||||
|
} else {
|
||||||
|
std::env::current_dir()
|
||||||
|
.map(|cwd| cwd.join(path))
|
||||||
|
.unwrap_or_else(|_| path.to_path_buf())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
strip_verbatim_prefix(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn absolute_join(root: &Path, path: &Path) -> PathBuf {
|
||||||
|
let joined = if path.is_absolute() {
|
||||||
|
path.to_path_buf()
|
||||||
|
} else {
|
||||||
|
root.join(path)
|
||||||
|
};
|
||||||
|
canonicalize_path(&joined)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_dcr_dir(root: &Path) -> io::Result<PathBuf> {
|
||||||
|
let dcr = root.join(".dcr");
|
||||||
|
std::fs::create_dir_all(dcr.join("ide"))?;
|
||||||
|
Ok(dcr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn atomic_write(path: &Path, contents: impl AsRef<[u8]>) -> io::Result<()> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let pid = std::process::id();
|
||||||
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "tmp".to_string());
|
||||||
|
let tmp = path.with_file_name(format!("{file_name}.tmp.{pid}"));
|
||||||
|
{
|
||||||
|
let mut f = std::fs::File::create(&tmp)?;
|
||||||
|
f.write_all(contents.as_ref())?;
|
||||||
|
let _ = f.sync_all();
|
||||||
|
}
|
||||||
|
match std::fs::rename(&tmp, path) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
if path.exists() {
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
match std::fs::rename(&tmp, path) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(e2) => {
|
||||||
|
let _ = std::fs::remove_file(&tmp);
|
||||||
|
Err(e2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let _ = std::fs::remove_file(&tmp);
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_gitignore_has_dcr(project_dir: &Path) -> io::Result<()> {
|
||||||
|
let path = project_dir.join(".gitignore");
|
||||||
|
if path.exists() {
|
||||||
|
let content = std::fs::read_to_string(&path)?;
|
||||||
|
let has = content.lines().any(|l| {
|
||||||
|
let t = l.trim();
|
||||||
|
t == ".dcr/" || t == ".dcr" || t == "/.dcr/" || t == "/.dcr"
|
||||||
|
});
|
||||||
|
if !has {
|
||||||
|
let mut new_content = content;
|
||||||
|
if !new_content.ends_with('\n') && !new_content.is_empty() {
|
||||||
|
new_content.push('\n');
|
||||||
|
}
|
||||||
|
new_content.push_str(".dcr/\n");
|
||||||
|
std::fs::write(&path, new_content)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::fs::write(&path, "/target\n.dcr/\n")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn canonicalize_path_absolute() {
|
||||||
|
let cwd = std::env::current_dir().unwrap();
|
||||||
|
let got = canonicalize_path(&cwd);
|
||||||
|
assert!(got.is_absolute());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn canonicalize_path_relative_fallback() {
|
||||||
|
let p = Path::new("does-not-exist-xyz-12345");
|
||||||
|
let got = canonicalize_path(p);
|
||||||
|
assert!(got.is_absolute());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn atomic_write_roundtrip() {
|
||||||
|
let dir = std::env::temp_dir().join(format!("dcr_atomic_{}", std::process::id()));
|
||||||
|
let _ = fs::remove_dir_all(&dir);
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
let path = dir.join("out.json");
|
||||||
|
atomic_write(&path, b"hello").unwrap();
|
||||||
|
assert_eq!(fs::read_to_string(&path).unwrap(), "hello");
|
||||||
|
atomic_write(&path, b"world").unwrap();
|
||||||
|
assert_eq!(fs::read_to_string(&path).unwrap(), "world");
|
||||||
|
let _ = fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_dcr_dir_creates_ide() {
|
||||||
|
let dir = std::env::temp_dir().join(format!("dcr_dir_{}", std::process::id()));
|
||||||
|
let _ = fs::remove_dir_all(&dir);
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
let dcr = ensure_dcr_dir(&dir).unwrap();
|
||||||
|
assert!(dcr.is_dir());
|
||||||
|
assert!(dcr.join("ide").is_dir());
|
||||||
|
let _ = fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -87,6 +87,33 @@ fn build_run_clean_flags_normal_project() {
|
|||||||
"dcr run should start"
|
"dcr run should start"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
dir.join("src/main.c"),
|
||||||
|
r#"#include <stdio.h>
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
printf("ARG:%s\n", argv[i]);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect("rewrite main.c");
|
||||||
|
let out = run_dcr_env(&["run", "--force", "--", "--test_help", "foo"], &dir, &envs);
|
||||||
|
let combined = format!(
|
||||||
|
"{}{}",
|
||||||
|
String::from_utf8_lossy(&out.stdout),
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
out.status.success(),
|
||||||
|
"dcr run -- <args> should succeed:\n{combined}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
combined.contains("ARG:--test_help") && combined.contains("ARG:foo"),
|
||||||
|
"binary should receive args after `--`:\n{combined}"
|
||||||
|
);
|
||||||
|
|
||||||
let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
|
let out = run_dcr_env(&["clean", "--release"], &dir, &envs);
|
||||||
assert!(out.status.success(), "dcr clean --release should succeed");
|
assert!(out.status.success(), "dcr clean --release should succeed");
|
||||||
let release_dir = host_profile_dir(&dir, "release");
|
let release_dir = host_profile_dir(&dir, "release");
|
||||||
@@ -129,15 +156,9 @@ fn build_with_target_config() {
|
|||||||
if !out.status.success() {
|
if !out.status.success() {
|
||||||
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
|
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
|
||||||
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
|
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||||
if cfg!(windows) {
|
eprintln!("skipping linux-target path assert: build failed (no suitable toolchain)");
|
||||||
eprintln!("skipping linux-target path assert: build failed (no linux cross toolchain)");
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
assert!(
|
|
||||||
out.status.success(),
|
|
||||||
"dcr build with target = \"linux\" should succeed"
|
|
||||||
);
|
|
||||||
|
|
||||||
let out_dir = dir
|
let out_dir = dir
|
||||||
.join("target")
|
.join("target")
|
||||||
@@ -237,13 +258,11 @@ roots = ["src"]
|
|||||||
}
|
}
|
||||||
assert!(out.status.success(), "flat-bin nasm build should succeed");
|
assert!(out.status.success(), "flat-bin nasm build should succeed");
|
||||||
|
|
||||||
let candidates = [
|
let boot = host_profile_dir(&dir, "debug").join("boot.bin");
|
||||||
host_profile_dir(&dir, "debug").join("boot.bin"),
|
|
||||||
dir.join("target/debug/boot.bin"),
|
|
||||||
];
|
|
||||||
assert!(
|
assert!(
|
||||||
candidates.iter().any(|p| p.is_file()),
|
boot.is_file(),
|
||||||
"expected boot.bin under host profile dir, candidates: {candidates:?}"
|
"expected boot.bin under host profile dir: {}",
|
||||||
|
boot.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,12 +288,9 @@ fn package_build_target_used_without_cli_flag() {
|
|||||||
if !out.status.success() {
|
if !out.status.success() {
|
||||||
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
|
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
|
||||||
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
|
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||||
if cfg!(windows) {
|
eprintln!("skipping package-target path assert: build failed (no suitable toolchain)");
|
||||||
eprintln!("skipping linux-target path assert: build failed (no linux cross toolchain)");
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
assert!(out.status.success(), "build should use package target");
|
|
||||||
|
|
||||||
let out_dir = dir
|
let out_dir = dir
|
||||||
.join("target")
|
.join("target")
|
||||||
@@ -286,3 +302,57 @@ fn package_build_target_used_without_cli_flag() {
|
|||||||
"artifact should be under package build.target path",
|
"artifact should be under package build.target path",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_target_section_ldflags_apply_without_cli_target() {
|
||||||
|
let Some(compiler) = available_compiler() else {
|
||||||
|
eprintln!("no compiler found; skipping host target ldflags test");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let dir = unique_sandbox_dir("host_ldflags");
|
||||||
|
let out = run_dcr(&["init"], &dir);
|
||||||
|
assert!(out.status.success(), "dcr init should succeed");
|
||||||
|
|
||||||
|
let host = if cfg!(target_os = "linux") {
|
||||||
|
let arch = std::env::consts::ARCH;
|
||||||
|
let env = if cfg!(target_env = "musl") {
|
||||||
|
"musl"
|
||||||
|
} else {
|
||||||
|
"gnu"
|
||||||
|
};
|
||||||
|
format!("{arch}-unknown-linux-{env}")
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
format!("{}-apple-darwin", std::env::consts::ARCH)
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
let env = if cfg!(target_env = "gnu") {
|
||||||
|
"gnu"
|
||||||
|
} else {
|
||||||
|
"msvc"
|
||||||
|
};
|
||||||
|
format!("{}-pc-windows-{env}", std::env::consts::ARCH)
|
||||||
|
} else {
|
||||||
|
eprintln!("unsupported host for host_target_section test; skipping");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let toml_path = dir.join("dcr.toml");
|
||||||
|
let toml = std::fs::read_to_string(&toml_path).expect("read dcr.toml");
|
||||||
|
let updated = format!(
|
||||||
|
"{toml}\n[build.{host}]\nldflags = [\"-Wl,-rpath,/nonexistent-dcr-host-ldflags\"]\n"
|
||||||
|
);
|
||||||
|
std::fs::write(&toml_path, updated).expect("write dcr.toml");
|
||||||
|
|
||||||
|
let envs = [("DCR_COMPILER", compiler), ("DCR_DEBUG", "1")];
|
||||||
|
let out = run_dcr_env(&["build", "--force"], &dir, &envs);
|
||||||
|
let combined = format!(
|
||||||
|
"{}{}",
|
||||||
|
String::from_utf8_lossy(&out.stdout),
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
combined.contains("-Wl,-rpath,/nonexistent-dcr-host-ldflags")
|
||||||
|
|| combined.contains("rpath,/nonexistent-dcr-host-ldflags"),
|
||||||
|
"host target section ldflags must be applied on native build:\n{combined}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,25 +76,25 @@ fn available_compiler() -> Option<&'static str> {
|
|||||||
|
|
||||||
fn host_profile_dir(project_root: &Path, profile: &str) -> PathBuf {
|
fn host_profile_dir(project_root: &Path, profile: &str) -> PathBuf {
|
||||||
let target = project_root.join("target");
|
let target = project_root.join("target");
|
||||||
if cfg!(target_os = "linux") {
|
if cfg!(any(
|
||||||
let arch = std::env::consts::ARCH;
|
target_os = "linux",
|
||||||
let env = if cfg!(target_env = "musl") {
|
|
||||||
"musl"
|
|
||||||
} else {
|
|
||||||
"gnu"
|
|
||||||
};
|
|
||||||
target
|
|
||||||
.join(format!("{arch}-unknown-linux-{env}"))
|
|
||||||
.join(profile)
|
|
||||||
} else if cfg!(any(
|
|
||||||
target_os = "freebsd",
|
target_os = "freebsd",
|
||||||
target_os = "openbsd",
|
target_os = "openbsd",
|
||||||
target_os = "netbsd",
|
target_os = "netbsd",
|
||||||
target_os = "dragonfly"
|
target_os = "dragonfly"
|
||||||
)) {
|
)) {
|
||||||
let arch = std::env::consts::ARCH;
|
let arch = std::env::consts::ARCH;
|
||||||
let os = std::env::consts::OS;
|
let triple = if cfg!(target_os = "linux") {
|
||||||
target.join(format!("{arch}-unknown-{os}")).join(profile)
|
let env = if cfg!(target_env = "musl") {
|
||||||
|
"musl"
|
||||||
|
} else {
|
||||||
|
"gnu"
|
||||||
|
};
|
||||||
|
format!("{arch}-unknown-linux-{env}")
|
||||||
|
} else {
|
||||||
|
format!("{arch}-unknown-{}", std::env::consts::OS)
|
||||||
|
};
|
||||||
|
target.join(triple).join(profile)
|
||||||
} else {
|
} else {
|
||||||
target.join(profile)
|
target.join(profile)
|
||||||
}
|
}
|
||||||
|
|||||||
151
tests/cli_gen.rs
Normal file
151
tests/cli_gen.rs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
mod common;
|
||||||
|
use common::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_project_info_has_artifact_path() {
|
||||||
|
let dir = unique_sandbox_dir("gen_pi");
|
||||||
|
let out = run_dcr(&["new", "app", "--vcs", "none"], &dir);
|
||||||
|
assert!(out.status.success());
|
||||||
|
let project = dir.join("app");
|
||||||
|
|
||||||
|
let out = run_dcr(&["gen", "project-info", "-q"], &project);
|
||||||
|
assert!(
|
||||||
|
out.status.success(),
|
||||||
|
"project-info failed: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
|
assert!(
|
||||||
|
stdout.contains("\"artifact_path\""),
|
||||||
|
"missing artifact_path: {stdout}"
|
||||||
|
);
|
||||||
|
assert!(stdout.contains("\"target_dir\""), "missing target_dir");
|
||||||
|
assert!(
|
||||||
|
stdout.contains("\"artifact_kind\""),
|
||||||
|
"missing artifact_kind"
|
||||||
|
);
|
||||||
|
assert!(stdout.contains("\"source_roots\""), "missing source_roots");
|
||||||
|
|
||||||
|
let root_canon = fs::canonicalize(&project).unwrap();
|
||||||
|
assert!(
|
||||||
|
stdout.contains(&root_canon.to_string_lossy().to_string()) || stdout.contains("app"),
|
||||||
|
"root should be absolute-ish: {stdout}"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(project.join(".dcr").join("build-info.json").is_file());
|
||||||
|
assert!(project.join(".dcr").join("toolchain.json").is_file());
|
||||||
|
assert!(project.join(".clangd").is_file());
|
||||||
|
let clangd = fs::read_to_string(project.join(".clangd")).unwrap();
|
||||||
|
assert!(clangd.contains("CompilationDatabase: .dcr"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_compile_commands_in_dcr_dir() {
|
||||||
|
let dir = unique_sandbox_dir("gen_cc");
|
||||||
|
let out = run_dcr(&["new", "app", "--vcs", "none"], &dir);
|
||||||
|
assert!(out.status.success());
|
||||||
|
let project = dir.join("app");
|
||||||
|
|
||||||
|
let out = run_dcr(&["gen", "compile-commands", "-q"], &project);
|
||||||
|
assert!(
|
||||||
|
out.status.success(),
|
||||||
|
"compile-commands failed: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!project.join("compile_commands.json").exists(),
|
||||||
|
"compile_commands.json must not be in project root"
|
||||||
|
);
|
||||||
|
let cc = project.join(".dcr").join("compile_commands.json");
|
||||||
|
assert!(cc.is_file(), "expected .dcr/compile_commands.json");
|
||||||
|
let content = fs::read_to_string(&cc).unwrap();
|
||||||
|
assert!(content.contains("\"directory\""), "missing directory field");
|
||||||
|
assert!(content.contains("\"file\""), "missing file field");
|
||||||
|
assert!(content.starts_with('['));
|
||||||
|
|
||||||
|
assert!(project.join(".clangd").is_file());
|
||||||
|
assert!(project.join(".dcr").join("ide").is_dir());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_clangd_not_overwritten_if_custom() {
|
||||||
|
let dir = unique_sandbox_dir("gen_clangd");
|
||||||
|
let out = run_dcr(&["new", "app", "--vcs", "none"], &dir);
|
||||||
|
assert!(out.status.success());
|
||||||
|
let project = dir.join("app");
|
||||||
|
|
||||||
|
let custom = "CompileFlags:\n Add: [-Wall]\n";
|
||||||
|
fs::write(project.join(".clangd"), custom).unwrap();
|
||||||
|
|
||||||
|
let out = run_dcr(&["gen", "compile-commands", "-q"], &project);
|
||||||
|
assert!(out.status.success());
|
||||||
|
let after = fs::read_to_string(project.join(".clangd")).unwrap();
|
||||||
|
assert_eq!(after, custom, "custom .clangd must not be overwritten");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gen_vscode_launch_uses_resolver() {
|
||||||
|
let dir = unique_sandbox_dir("gen_vscode");
|
||||||
|
let out = run_dcr(&["new", "app", "--vcs", "none"], &dir);
|
||||||
|
assert!(out.status.success());
|
||||||
|
let project = dir.join("app");
|
||||||
|
|
||||||
|
let out = run_dcr(&["gen", "vscode"], &project);
|
||||||
|
assert!(
|
||||||
|
out.status.success(),
|
||||||
|
"gen vscode failed: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
let launch = fs::read_to_string(project.join(".vscode").join("launch.json")).unwrap();
|
||||||
|
assert!(launch.contains("\"program\""), "launch missing program");
|
||||||
|
let normalized = launch.replace("\\\\", "/").replace('\\', "/");
|
||||||
|
let expected_rel = host_profile_dir(&project, "debug")
|
||||||
|
.join(bin_name("app"))
|
||||||
|
.strip_prefix(&project)
|
||||||
|
.unwrap_or_else(|_| Path::new("target"))
|
||||||
|
.to_string_lossy()
|
||||||
|
.replace('\\', "/");
|
||||||
|
assert!(
|
||||||
|
normalized.contains(&expected_rel),
|
||||||
|
"program should use host profile layout ({expected_rel}): {launch}"
|
||||||
|
);
|
||||||
|
let settings = fs::read_to_string(project.join(".vscode").join("settings.json")).unwrap();
|
||||||
|
assert!(
|
||||||
|
settings.contains(".dcr") || settings.contains("compile-commands-dir"),
|
||||||
|
"settings should point at .dcr: {settings}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_print_artifact_path() {
|
||||||
|
if available_compiler().is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dir = unique_sandbox_dir("build_print");
|
||||||
|
let out = run_dcr(&["new", "app", "--vcs", "none"], &dir);
|
||||||
|
assert!(out.status.success());
|
||||||
|
let project = dir.join("app");
|
||||||
|
|
||||||
|
let out = run_dcr(&["build", "--print-artifact-path"], &project);
|
||||||
|
assert!(
|
||||||
|
out.status.success(),
|
||||||
|
"build failed: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let path_line = stdout
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.rfind(|l| !l.is_empty())
|
||||||
|
.unwrap_or("");
|
||||||
|
assert!(
|
||||||
|
Path::new(path_line).is_absolute() || path_line.contains("app"),
|
||||||
|
"expected artifact path on stdout, got: {stdout}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
project.join(".dcr").join("build-info.json").is_file(),
|
||||||
|
"build-info.json should be written after build"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -61,6 +61,20 @@ fn lint_command_runs() {
|
|||||||
assert!(out.status.success(), "dcr init should succeed");
|
assert!(out.status.success(), "dcr init should succeed");
|
||||||
|
|
||||||
// Linting a clean project should succeed or at least run without crashing
|
// Linting a clean project should succeed or at least run without crashing
|
||||||
|
let tidy_ok = Command::new("clang-tidy")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !tidy_ok {
|
||||||
|
eprintln!("clang-tidy not found; skipping lint test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
let out = run_dcr_env(&["lint"], &dir, &[]);
|
let out = run_dcr_env(&["lint"], &dir, &[]);
|
||||||
assert!(out.status.success(), "dcr lint should run on clean project");
|
assert!(
|
||||||
|
out.status.success(),
|
||||||
|
"dcr lint should run on clean project:\nstdout: {}\nstderr: {}",
|
||||||
|
String::from_utf8_lossy(&out.stdout),
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ fn new_vcs_options_work() {
|
|||||||
let project_dir = dir.join("hello_none");
|
let project_dir = dir.join("hello_none");
|
||||||
assert!(project_dir.is_dir());
|
assert!(project_dir.is_dir());
|
||||||
assert!(!project_dir.join(".git").exists());
|
assert!(!project_dir.join(".git").exists());
|
||||||
assert!(!project_dir.join(".gitignore").exists());
|
assert!(project_dir.join(".gitignore").is_file());
|
||||||
|
let gi = std::fs::read_to_string(project_dir.join(".gitignore")).unwrap();
|
||||||
|
assert!(gi.contains(".dcr/"));
|
||||||
|
|
||||||
// 2. Test --vcs git
|
// 2. Test --vcs git
|
||||||
if is_git_in_path() {
|
if is_git_in_path() {
|
||||||
@@ -50,6 +52,7 @@ fn new_vcs_options_work() {
|
|||||||
assert!(project_dir.join(".gitignore").is_file());
|
assert!(project_dir.join(".gitignore").is_file());
|
||||||
let gitignore = std::fs::read_to_string(project_dir.join(".gitignore")).unwrap();
|
let gitignore = std::fs::read_to_string(project_dir.join(".gitignore")).unwrap();
|
||||||
assert!(gitignore.contains("/target"));
|
assert!(gitignore.contains("/target"));
|
||||||
|
assert!(gitignore.contains(".dcr/"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +63,9 @@ fn init_vcs_options_work() {
|
|||||||
let out = run_dcr(&["init", "--vcs", "none"], &dir);
|
let out = run_dcr(&["init", "--vcs", "none"], &dir);
|
||||||
assert!(out.status.success(), "dcr init --vcs none should succeed");
|
assert!(out.status.success(), "dcr init --vcs none should succeed");
|
||||||
assert!(!dir.join(".git").exists());
|
assert!(!dir.join(".git").exists());
|
||||||
assert!(!dir.join(".gitignore").exists());
|
assert!(dir.join(".gitignore").is_file());
|
||||||
|
let gi = std::fs::read_to_string(dir.join(".gitignore")).unwrap();
|
||||||
|
assert!(gi.contains(".dcr/"));
|
||||||
|
|
||||||
// 2. Test --vcs git
|
// 2. Test --vcs git
|
||||||
if is_git_in_path() {
|
if is_git_in_path() {
|
||||||
@@ -71,5 +76,6 @@ fn init_vcs_options_work() {
|
|||||||
assert!(dir.join(".gitignore").is_file());
|
assert!(dir.join(".gitignore").is_file());
|
||||||
let gitignore = std::fs::read_to_string(dir.join(".gitignore")).unwrap();
|
let gitignore = std::fs::read_to_string(dir.join(".gitignore")).unwrap();
|
||||||
assert!(gitignore.contains("/target"));
|
assert!(gitignore.contains("/target"));
|
||||||
|
assert!(gitignore.contains(".dcr/"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,25 +94,25 @@ pub fn parse_project_name(toml: &str) -> String {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn host_profile_dir(project_root: &Path, profile: &str) -> PathBuf {
|
pub fn host_profile_dir(project_root: &Path, profile: &str) -> PathBuf {
|
||||||
let target = project_root.join("target");
|
let target = project_root.join("target");
|
||||||
if cfg!(target_os = "linux") {
|
if cfg!(any(
|
||||||
let arch = std::env::consts::ARCH;
|
target_os = "linux",
|
||||||
let env = if cfg!(target_env = "musl") {
|
|
||||||
"musl"
|
|
||||||
} else {
|
|
||||||
"gnu"
|
|
||||||
};
|
|
||||||
target
|
|
||||||
.join(format!("{arch}-unknown-linux-{env}"))
|
|
||||||
.join(profile)
|
|
||||||
} else if cfg!(any(
|
|
||||||
target_os = "freebsd",
|
target_os = "freebsd",
|
||||||
target_os = "openbsd",
|
target_os = "openbsd",
|
||||||
target_os = "netbsd",
|
target_os = "netbsd",
|
||||||
target_os = "dragonfly"
|
target_os = "dragonfly"
|
||||||
)) {
|
)) {
|
||||||
let arch = std::env::consts::ARCH;
|
let arch = std::env::consts::ARCH;
|
||||||
let os = std::env::consts::OS;
|
let triple = if cfg!(target_os = "linux") {
|
||||||
target.join(format!("{arch}-unknown-{os}")).join(profile)
|
let env = if cfg!(target_env = "musl") {
|
||||||
|
"musl"
|
||||||
|
} else {
|
||||||
|
"gnu"
|
||||||
|
};
|
||||||
|
format!("{arch}-unknown-linux-{env}")
|
||||||
|
} else {
|
||||||
|
format!("{arch}-unknown-{}", std::env::consts::OS)
|
||||||
|
};
|
||||||
|
target.join(triple).join(profile)
|
||||||
} else {
|
} else {
|
||||||
target.join(profile)
|
target.join(profile)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user