mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 16:09:41 +00:00
Compare commits
21 Commits
01f76fe052
...
v0.8.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bf1346d1b | |||
| 794d571d58 | |||
| d2df85a064 | |||
| 5e8037da97 | |||
| f29322a547 | |||
| 1a6a261cac | |||
| f92cc6ae91 | |||
| 5aefbc6498 | |||
| 594007f022 | |||
| d10ec84004 | |||
| 574ba8b6d0 | |||
| 75eace9d73 | |||
| 2446cb7970 | |||
| 588e4dd13f | |||
| 689fea7465 | |||
| a9dd8c1631 | |||
| 9d030cba0c | |||
| 1dcc8e614a | |||
| 9eaa4635d5 | |||
| a3f8444c20 | |||
| 1f3fc62cb6 |
130
.github/workflows/ci.yml
vendored
130
.github/workflows/ci.yml
vendored
@@ -5,10 +5,18 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTFLAGS: -Dwarnings
|
||||
CARGO_INCREMENTAL: "0"
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
name: fmt / clippy / check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -18,11 +26,129 @@ jobs:
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: dcr-ci
|
||||
cache-all-crates: true
|
||||
|
||||
- name: Format
|
||||
run: cargo fmt --all --check
|
||||
|
||||
- name: Lint
|
||||
- name: Clippy (default features)
|
||||
run: cargo clippy --all-targets -- -D warnings
|
||||
|
||||
- name: Clippy (all features)
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
- name: Check
|
||||
- name: Check (all features)
|
||||
run: cargo check --all-targets --all-features
|
||||
|
||||
test:
|
||||
name: tests (${{ matrix.os }})
|
||||
needs: checks
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: dcr-ci
|
||||
cache-all-crates: true
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Cache apt packages
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /var/cache/apt/archives
|
||||
key: apt-${{ runner.os }}-${{ hashFiles('.github/workflows/ci.yml') }}
|
||||
restore-keys: apt-${{ runner.os }}-
|
||||
|
||||
- name: Install Linux tools
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
clang \
|
||||
nasm \
|
||||
clang-format \
|
||||
clang-tidy \
|
||||
git \
|
||||
llvm
|
||||
|
||||
- name: Install macOS tools
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew install nasm llvm || true
|
||||
echo "$(brew --prefix llvm)/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install Windows tools
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
choco install nasm llvm -y
|
||||
echo "C:\Program Files\NASM" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
if (Test-Path "C:\Program Files\LLVM\bin") {
|
||||
echo "C:\Program Files\LLVM\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
}
|
||||
|
||||
- name: Unit tests (binary crate)
|
||||
run: cargo test --bins -- --nocapture
|
||||
|
||||
- name: Integration tests (default features)
|
||||
run: cargo test --tests -- --nocapture
|
||||
|
||||
- name: Integration tests (archive feature)
|
||||
if: runner.os != 'Windows'
|
||||
run: cargo test --tests --features archive -- --nocapture
|
||||
|
||||
- name: sccache stats
|
||||
if: always()
|
||||
run: sccache --show-stats || true
|
||||
|
||||
test-archive-linux:
|
||||
name: archive feature smoke
|
||||
needs: checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: dcr-ci
|
||||
cache-all-crates: true
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential clang nasm llvm
|
||||
|
||||
- name: Build with archive
|
||||
run: cargo build --features archive
|
||||
|
||||
- name: Flat-bin + unit suite with archive
|
||||
run: cargo test --features archive --test cli_build --test cli_workspace --test cli_errors -- --nocapture
|
||||
|
||||
- name: sccache stats
|
||||
if: always()
|
||||
run: sccache --show-stats || true
|
||||
|
||||
30
.github/workflows/release.yml
vendored
30
.github/workflows/release.yml
vendored
@@ -223,9 +223,9 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then
|
||||
cargo zigbuild --release --target ${{ matrix.target }}
|
||||
cargo zigbuild --release --features archive --target ${{ matrix.target }}
|
||||
else
|
||||
cargo build --release --target ${{ matrix.target }}
|
||||
cargo build --release --features archive --target ${{ matrix.target }}
|
||||
fi
|
||||
|
||||
- name: Prepare Artifact
|
||||
@@ -298,9 +298,9 @@ jobs:
|
||||
run: |
|
||||
cargo install cargo-deb
|
||||
if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then
|
||||
cargo zigbuild --release --target ${{ matrix.target }}
|
||||
cargo zigbuild --release --features archive --target ${{ matrix.target }}
|
||||
else
|
||||
cargo build --release --target ${{ matrix.target }}
|
||||
cargo build --release --features archive --target ${{ matrix.target }}
|
||||
fi
|
||||
cargo deb --no-build --target ${{ matrix.target }}
|
||||
mkdir -p dist-packages && cp target/${{ matrix.target }}/debian/*.deb dist-packages/
|
||||
@@ -311,9 +311,9 @@ jobs:
|
||||
run: |
|
||||
cargo install cargo-generate-rpm
|
||||
if [ "${{ matrix.target }}" != "x86_64-unknown-linux-gnu" ]; then
|
||||
cargo zigbuild --release --target ${{ matrix.target }}
|
||||
cargo zigbuild --release --features archive --target ${{ matrix.target }}
|
||||
else
|
||||
cargo build --release --target ${{ matrix.target }}
|
||||
cargo build --release --features archive --target ${{ matrix.target }}
|
||||
fi
|
||||
cargo generate-rpm --target ${{ matrix.target }}
|
||||
mkdir -p dist-packages && cp target/${{ matrix.target }}/generate-rpm/*.rpm dist-packages/
|
||||
@@ -322,7 +322,7 @@ jobs:
|
||||
- name: Build Snap Package
|
||||
if: matrix.package_type == 'snap'
|
||||
run: |
|
||||
cargo build --release
|
||||
cargo build --release --features archive
|
||||
mkdir -p packaging/snap/dist/bin
|
||||
cp target/release/dcr packaging/snap/dist/bin/
|
||||
cp -r man packaging/snap/dist/man
|
||||
@@ -364,7 +364,7 @@ jobs:
|
||||
cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
|
||||
unset CFLAGS CXXFLAGS LDFLAGS
|
||||
export RUSTFLAGS="-C linker=cc"
|
||||
cargo build --release --locked
|
||||
cargo build --release --locked --features archive
|
||||
}
|
||||
package() {
|
||||
cd "\$srcdir/dcr-${PKGVER_ORIG}"
|
||||
@@ -454,7 +454,7 @@ jobs:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
run: cargo build --release --features archive --target ${{ matrix.target }}
|
||||
|
||||
- name: Prepare Artifact
|
||||
run: |
|
||||
@@ -499,7 +499,7 @@ jobs:
|
||||
run: |
|
||||
export PATH="/usr/local/bin:/usr/local/sbin:$PATH"
|
||||
pkg install -y rust perl5 gmake
|
||||
cargo build --release
|
||||
cargo build --release --features archive
|
||||
|
||||
- name: Prepare FreeBSD Artifact
|
||||
if: matrix.system == 'freebsd'
|
||||
@@ -517,7 +517,7 @@ jobs:
|
||||
run: |
|
||||
export PATH="/usr/local/bin:$PATH"
|
||||
pkg_add rust perl gmake
|
||||
cargo build --release
|
||||
cargo build --release --features archive
|
||||
|
||||
- name: Prepare OpenBSD Artifact
|
||||
if: matrix.system == 'openbsd'
|
||||
@@ -537,7 +537,7 @@ jobs:
|
||||
mkdir -p /tmp/bin
|
||||
ln -sf /usr/pkg/bin/gmake /tmp/bin/make
|
||||
export PATH="/tmp/bin:/usr/pkg/bin:/usr/pkg/sbin:$PATH"
|
||||
cargo build --release
|
||||
cargo build --release --features archive
|
||||
|
||||
- name: Prepare NetBSD Artifact
|
||||
if: matrix.system == 'netbsd'
|
||||
@@ -585,7 +585,7 @@ jobs:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
run: cargo build --release --features archive --target ${{ matrix.target }}
|
||||
|
||||
- name: Prepare Artifact
|
||||
shell: pwsh
|
||||
@@ -654,7 +654,7 @@ jobs:
|
||||
cd "\$srcdir/\$pkgname-${PKGVER_ORIG}"
|
||||
unset CFLAGS CXXFLAGS LDFLAGS
|
||||
export RUSTFLAGS="-C linker=cc"
|
||||
cargo build --release --locked
|
||||
cargo build --release --locked --features archive
|
||||
}
|
||||
|
||||
package() {
|
||||
@@ -919,7 +919,7 @@ jobs:
|
||||
cd "\$srcdir/dcr-${PKGVER_ORIG}"
|
||||
unset CFLAGS CXXFLAGS LDFLAGS
|
||||
export RUSTFLAGS="-C linker=cc"
|
||||
cargo build --release --locked
|
||||
cargo build --release --locked --features archive
|
||||
}
|
||||
|
||||
package() {
|
||||
|
||||
14
.github/workflows/trigger-site-build.yml
vendored
Normal file
14
.github/workflows/trigger-site-build.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: Trigger site rebuild
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
|
||||
jobs:
|
||||
trigger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger Cloudflare Pages build
|
||||
run: curl -sS -X POST "${{ secrets.CF_PAGES_DEPLOY_HOOK }}"
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,4 +5,6 @@ sandbox/**
|
||||
.cargo/
|
||||
.env
|
||||
|
||||
personal/
|
||||
personal/
|
||||
.*
|
||||
!.gitignore
|
||||
56
AGENTS.md
56
AGENTS.md
@@ -1,56 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
cargo build --release && ./target/release/dcr setup
|
||||
```
|
||||
|
||||
## Pre-PR (CI) gate
|
||||
|
||||
```bash
|
||||
cargo fmt --all --check
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
cargo check --all-targets --all-features
|
||||
cargo test
|
||||
```
|
||||
|
||||
CI (`.github/workflows/ci.yml`) runs fmt, clippy, check — **not** tests.
|
||||
|
||||
## Tests
|
||||
|
||||
- Integration tests in `tests/` build the dcr binary and run it in temp dirs under `sandbox/cli-tests/` (gitignored).
|
||||
- Some tests **skip** if no C compiler (`gcc`/`clang`/`cc`) is found at runtime.
|
||||
- Compiler can be overridden via `DCR_COMPILER` or `DCR_CC` env vars.
|
||||
- Tests use `$CARGO_BIN_EXE_dcr` when available, else `target/debug/dcr`.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Single Rust package** (edition 2024), not a workspace. Only one binary: `dcr`.
|
||||
- **Entrypoint**: `src/main.rs` — matches `args[1]` against command names.
|
||||
- **Compiler backends** in `src/core/builder/`: `unix_cc` (gcc/clang), `msvc` (cl/clang-cl), `gas`, `nasm`. Dispatched by matching compiler name string.
|
||||
- **Config**: `dcr.toml` (per-project), `~/.dcr/config.toml` (registry), `~/.dcr/index.json` (index). Defaults: `language=c`, `standard=c11`, `compiler=clang`, `kind=bin`.
|
||||
- **`build.rs`** embeds `TARGET` as `DCR_TARGET` env var (shown in `--version`).
|
||||
- **CLI output** must be in English. Use `utils::log` for messaging.
|
||||
|
||||
## Key commands
|
||||
|
||||
| Command | Notes |
|
||||
|---|---|
|
||||
| `dcr new <name>` | Creates project dir |
|
||||
| `dcr init` | Init from CWD (must be empty) |
|
||||
| `dcr build [--debug\|--release] [--target <triple>]` | Default profile: `--debug` |
|
||||
| `dcr run [--debug\|--release]` | Only for `kind = "bin"` |
|
||||
| `dcr clean [--debug\|--release] [--all]` | `--all` for workspace roots |
|
||||
| `dcr tree` | Dependency tree viewer |
|
||||
| `dcr test [--init]` | Runs/test project tests |
|
||||
| `dcr gen <vscode\|clion\|compile-commands>` | IDE integration |
|
||||
| `dcr --update` | Self-update from GitHub Releases |
|
||||
|
||||
## Cross-compilation
|
||||
|
||||
`--target` accepts short names (`linux`, `macos`, `windows`) or full triples. In CI, Linux cross-builds use `cargo-zigbuild`. When `build.target` is set in `dcr.toml`, DCR auto-injects `--target=<value>` into cflags (clang). Bare-metal targets (containing `none`/`-elf`/`eabi`/`baremetal`) skip DCR's default compiler flags.
|
||||
|
||||
## Dependencies (Cargo)
|
||||
|
||||
reqwest (blocking+json+rustls-tls), serde, toml, git2, openssl (vendored), self-replace, sha2, ctrlc.
|
||||
201
CHANGELOG.md
201
CHANGELOG.md
@@ -1,5 +1,171 @@
|
||||
# Changelog
|
||||
|
||||
## [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"
|
||||
|
||||
### RU
|
||||
|
||||
**Добавлено:**
|
||||
|
||||
- **Прогресс компиляции `[N/M]`** — live-строка `compile pkg v0.1.0 [12/41]` на TTY (stderr), чтобы длинные mono-package сборки не выглядели «зависшими».
|
||||
- **Свой стиль статуса build** — фиксированная колонка глаголов: `project`, `compile`, `dep`, `ready`, `pack`, `done`, `run` (не cargo-clone).
|
||||
- **Cargo feature `archive`** — `fatfs` и упаковка FAT-образов опциональны (`cargo build --features archive`). Release CI собирает с feature; без feature `[archive]` даёт явную ошибку.
|
||||
- **Интеграционные тесты** — `flat_bin_nasm_build`, `package_build_target_used_without_cli_flag`, усилен workspace `clean --all`.
|
||||
- **Расширенный CI** — unit + integration на Linux/macOS/Windows, отдельный job с `--features archive`, rust-cache, clippy default + all-features.
|
||||
|
||||
**Изменено:**
|
||||
|
||||
- **CLI host triple больше не форсится** — без `--target` используется `build.target` пакета/member (важно для bare-metal / post_steps ISO).
|
||||
- **Relative `-I`/`-L`/`-T` (и `-isystem`/`-idirafter`)** в cflags/ldflags абсолютизируются относительно корня пакета (workspace CWD = root).
|
||||
- **`dcr clean --all`** — не спамит `target not found` по members без локального `target/` (общий root `target/`).
|
||||
- **`error` / `warn`** — красный / жёлтый, вывод на stderr; `Подсказка:` → `Hint:`.
|
||||
- **`dcr --help` / man** — добавлены `add`, `lint`, `setup`; unknown command → exit code 1.
|
||||
- **Сообщения NotFound** — `linker not found: ld.lld …` / `{tool} not found …` вместо сырого `os error 2`.
|
||||
|
||||
### EN
|
||||
|
||||
**Added:**
|
||||
|
||||
- **Compile progress `[N/M]`** — live TTY line `compile pkg v0.1.0 [12/41]` so long mono-package builds do not look hung.
|
||||
- **DCR status verbs** — fixed-width column: `project`, `compile`, `dep`, `ready`, `pack`, `done`, `run`.
|
||||
- **Cargo feature `archive`** — optional `fatfs` / FAT packing (`cargo build --features archive`). Release CI builds with the feature; without it `[archive]` errors clearly.
|
||||
- **Integration tests** — `flat_bin_nasm_build`, package `build.target` without CLI flag, stricter workspace `clean --all`.
|
||||
- **Expanded CI** — unit + integration on Linux/macOS/Windows, archive feature job, rust-cache, clippy default + all-features.
|
||||
|
||||
**Changed:**
|
||||
|
||||
- **CLI no longer forces host triple** — without `--target`, package/member `build.target` is used (bare-metal / ISO post_steps).
|
||||
- **Relative `-I`/`-L`/`-T` (and `-isystem`/`-idirafter`)** in flags are absolutized against the package root when workspace CWD is the root.
|
||||
- **`dcr clean --all`** — no spam for members without a local `target/` (shared root `target/`).
|
||||
- **`error` / `warn`** — red / yellow on stderr; Russian `Подсказка:` → `Hint:`.
|
||||
- **`dcr --help` / man** — list `add`, `lint`, `setup`; unknown command exits 1.
|
||||
- **NotFound messages** — `linker not found: ld.lld …` / `{tool} not found …` instead of raw `os error 2`.
|
||||
|
||||
## [0.8.1] - 2026-07-20 "OS-Dev: flat-bin, FAT-образы / OS-Dev: flat-bin & FAT Images"
|
||||
|
||||
### RU
|
||||
|
||||
**Добавлено:**
|
||||
|
||||
- **`build.kind = "flat-bin"`** — сырой бинарный артефакт (расширение по умолчанию `.bin`):
|
||||
- **NASM** — `-f bin`, прямой вывод `<stem>.bin`
|
||||
- **FASM** — прямой вывод (ожидается `format binary` в исходнике)
|
||||
- **GAS / MASM / LLVM IR (`llc`)** — объект → `objcopy -O binary` → `<stem>.bin`
|
||||
- **C/C++ (gcc/clang/MSVC)** — compile → link (nostdlib/static) → `objcopy -O binary` → `<name>.bin`
|
||||
- Требуется `llvm-objcopy` / `objcopy` / `gobjcopy` в PATH (кроме NASM/FASM direct)
|
||||
- **Секция `[archive]`** — после успешной сборки DCR собирает образ диска (FAT12/16/32): размер, offset, volume label, опциональный bootsector и `layout`. С 0.8.2 — за feature `archive`.
|
||||
- **Одиночные файлы в `build.roots`** — корень может быть файлом (`.c`, `.asm`, header), а не только каталогом.
|
||||
|
||||
**Изменено:**
|
||||
|
||||
- **`dcr build --force` / `dcr run --force`** — force также перезапускает `build.steps` и `build.post_steps`.
|
||||
- **`dcr run` на workspace-only root** — если задан `[run].cmd`, сначала build + run по конфигу корня; иначе делегирование члену workspace.
|
||||
|
||||
### EN
|
||||
|
||||
**Added:**
|
||||
|
||||
- **`build.kind = "flat-bin"`** — raw binary (default `.bin`) across NASM/FASM/GAS/MASM/LLC and C/C++ (objcopy where needed).
|
||||
- **`[archive]` section** — FAT12/16/32 disk images after build (from 0.8.2 gated by feature `archive`).
|
||||
- **Single-file `build.roots`**.
|
||||
|
||||
**Changed:**
|
||||
|
||||
- **`--force`** re-runs steps/post_steps; workspace-only **`dcr run`** prefers root `[run].cmd` when set.
|
||||
|
||||
## [0.8.0] - 2026-07-18 "Модульный Движок Сборки, Новая Архитектура Ядра и Улучшение Workspaces / Modular Build Engine, New Core Architecture, & Workspace Improvements"
|
||||
|
||||
### RU
|
||||
|
||||
**Добавлено:**
|
||||
|
||||
- **Выделенное ядро сборки (Build Engine extraction)** — вся логика оркестрации компиляции и сборки полностью вынесена из интерфейсного модуля `src/cli/build.rs` в изолированный, переиспользуемый модуль ядра `src/core/build/engine.rs`.
|
||||
- **Полиморфная модель языков (Трейт `Language`)** — введение абстрактного трейта `Language` (`src/core/build/language/mod.rs`). Логика поиска файлов, сопоставления расширений и компиляторов распределена по выделенным языковым бэкендам для C, C++ (включая шаги генерации Qt), LLVM IR и ассемблера (GAS, NASM, FASM, MASM).
|
||||
- **Абстракция сборщиков (Трейт `Builder`)** — введение трейта `Builder` для полиморфного управления процессами компиляции (`src/core/build/builder/mod.rs`). Логика вызова компиляторов семейства GCC/Clang вынесена в общий модуль `builder/cc_common.rs`, а MSVC — в `builder/msvc/`.
|
||||
- **Поязыковая конфигурация (`[build.<lang>]`)** — добавлена поддержка индивидуальных настроек для каждого языка в `dcr.toml` (например, `[build.c]`, `[build.cpp]`, `[build.asm]`), позволяя независимо настраивать компилятор, стандарт и флаги сборки.
|
||||
- **Интеграция путей зависимостей в Workspaces** — сборщик теперь автоматически инжектирует пути заголовков (включая исходники `src/` членов, папки `include/` и директорию `target/include`) и библиотек (`target/lib`, а также специфичные для целевой платформы папки сборки) зависимых членов workspace при сборке.
|
||||
- **Система событий `BuildReporter`** — весь вывод сборщика переведён на событийную модель (`src/core/build/report.rs`), что делает движок пригодным для программного встраивания (например, в IDE) без необходимости перехвата stdout/stderr.
|
||||
- **Поддержка отмены сборки** — добавлен потокобезопасный токен отмены (`Arc<AtomicBool>`), позволяющий безопасно прервать выполнение компиляции на любом этапе.
|
||||
- **Сохранение кастомных ключей TOML** — парсер `dcr.toml` переведен на `toml_edit`, что гарантирует сохранение всех неизвестных/пользовательских ключей, комментариев и форматирования при перезаписи файла (например, через `dcr add`).
|
||||
- **Разрешение заголовков для локальных path-зависимостей** — исправлена проблема, при которой потребители не могли найти заголовочные файлы статических/динамических библиотек, лежащие в упакованной папке `target/include` локальных path-зависимостей.
|
||||
- **Новая система интеграционных тестов** — старый громоздкий файл `tests/cli_basic.rs` разделен на изолированные и структурированные интеграционные тесты для каждой команды и фичи: `cli_build.rs`, `cli_deps.rs`, `cli_lint.rs`, `cli_new.rs`, `cli_qt.rs`, `cli_test.rs`, `cli_workspace.rs`.
|
||||
|
||||
**Изменено:**
|
||||
|
||||
- **Дедупликация генерации артефактов** — логика линковки бинарных файлов (ELF, PE, EXE), динамических библиотек (`.so`, `.dll`, `.dylib`) и архивации статических библиотек (`.a`, `.lib`) полностью перенесена в выделенный модуль `src/core/build/builder/artifact.rs`.
|
||||
- **Запуск членов workspace из корня** — команда `dcr run` при вызове из корня workspace-only проекта теперь запускает сборку выбранного пакета в контексте workspace (через флаг `--workspace`), исключая изолированные ошибки сборки.
|
||||
- **Отказ от глобального состояния** — все операции сборки изолированы внутри структуры `BuildContext`, обеспечивая многопоточную безопасность сборщика.
|
||||
- **Вынос логики кэширования** — проверка инкрементальных изменений, отслеживание заголовков (`#include`) и вычисление хэшей сборки перенесено в изолированный модуль `src/core/build/cache.rs`.
|
||||
- **Модуляризация кастомных шагов** — обработка этапа `build.steps` для препроцессоров вынесена в `src/core/build/steps.rs`.
|
||||
|
||||
### EN
|
||||
|
||||
**Added:**
|
||||
|
||||
- **Dedicated Build Engine (`src/core/build/engine.rs`)** — decoupled the build orchestration logic completely from the CLI front-end wrapper in `src/cli/build.rs` to a reusable, decoupled core build engine.
|
||||
- **Polymorphic Language Model (`Language` trait)** — introduced the abstract `Language` trait (`src/core/build/language/mod.rs`). File scanning, compiler resolution, and flags handling are now encapsulated in dedicated language modules for C, C++ (including Qt code generation), LLVM IR, and ASM (GAS, NASM, FASM, MASM).
|
||||
- **Polymorphic Compilation Dispatch (`Builder` trait)** — introduced the `Builder` trait to generalize builder invocations (`src/core/build/builder/mod.rs`). Consolidated GCC/Clang logic into `builder/cc_common.rs` and Microsoft Visual C++ logic into `builder/msvc/`.
|
||||
- **Per-Language Configuration overrides (`[build.<lang>]`)** — introduced support for configuring language-specific compiler, standard, and flags (e.g. `[build.c]`, `[build.cpp]`, `[build.asm]`) independently in `dcr.toml`.
|
||||
- **Automatic Workspace Dependency Injection** — during workspace builds, include/lib paths of dependent workspace members (including their source headers, local `include/` folders, and build target `target/include` / `target/lib` folders) are automatically resolved and injected.
|
||||
- **`BuildReporter` Event System** — decoupled build orchestration output into a structured event-driven reporter model (`src/core/build/report.rs`), making DCR suitable for library embedding and IDE integrations without stderr capturing.
|
||||
- **Build Cancellation Support** — introduced thread-safe cancellation tokens (`Arc<AtomicBool>`) allowing clients to safely abort compile runs mid-execution.
|
||||
- **TOML Formatting & Custom Keys Preservation** — migrated the config editor to `toml_edit`, preserving all unknown/user-defined TOML keys, structures, and comments during file write operations (e.g., in `dcr add`).
|
||||
- **Path Dependencies Target Include Resolution** — local path dependencies configured via tables now correctly expose headers built and packaged into their `target/include` folder to consumer packages.
|
||||
- **Modular Integration Test Suite** — refactored the monolith `tests/cli_basic.rs` file into clean, specialized integration tests: `cli_build.rs`, `cli_deps.rs`, `cli_lint.rs`, `cli_new.rs`, `cli_qt.rs`, `cli_test.rs`, `cli_workspace.rs`.
|
||||
|
||||
**Changed:**
|
||||
|
||||
- **Consolidated Linking and Archiving** — artifact generation (linking executables/shared libraries and archiving static libraries) consolidated into a single `src/core/build/builder/artifact.rs` module.
|
||||
- **Workspace-Aware member execution** — `dcr run` executed at the workspace root of a `workspace_only` project now triggers the member build inside the workspace context using the `--workspace` parameter, preventing standalone build issues.
|
||||
- **Elimination of Global State** — removed global variables and states from the build core, encapsulating build logic inside isolated `BuildContext` structs for thread safety.
|
||||
- **Build Cache Separation** — relocated mtime tracking, incremental caching, and header-dependency checking logic to `src/core/build/cache.rs`.
|
||||
- **Modularized Pre-build Steps** — decoupled generator scripts and pre-build commands parsing into `src/core/build/steps.rs`.
|
||||
|
||||
## [0.7.4] - 2026-06-17 "Qt Поддержка, Linting и Рефакторинг Архитектуры / Qt Support, Linting, & Architecture Refactor"
|
||||
|
||||
### RU
|
||||
@@ -95,25 +261,31 @@
|
||||
### RU
|
||||
|
||||
**Добавлено:**
|
||||
|
||||
- Учтён `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`.
|
||||
- **Бейджи GitHub Stars и GPL-3.0 не рендерились в README** — блок бейджей переведён на чистый HTML.
|
||||
|
||||
**Изменено:**
|
||||
|
||||
- Вычисление таргета по умолчанию вынесено в общую функцию `default_target_triple()` — устранено дублирование в build, run, clean.
|
||||
|
||||
### EN
|
||||
|
||||
**Added:**
|
||||
|
||||
- `default_target_triple()` now respects `target_env` — Linux (gnu/musl) and Windows (msvc/gnu) use the correct environment instead of hardcoded values.
|
||||
|
||||
**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`.
|
||||
- **GitHub Stars and GPL-3.0 badges not rendering in README** — badge block converted to pure HTML.
|
||||
|
||||
**Changed:**
|
||||
|
||||
- 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"
|
||||
@@ -121,6 +293,7 @@
|
||||
### RU
|
||||
|
||||
**Добавлено:**
|
||||
|
||||
- Вывод `--help` для всех команд с цветным форматированием.
|
||||
- man-страницы (12 штук в `man/man1/`) и их установка во всех пакетах.
|
||||
- Валидация имен проектов в `dcr init` и `dcr new` (только ASCII буквы, цифры, `_` и `-`).
|
||||
@@ -131,6 +304,7 @@
|
||||
- Пакеты AUR (`dcr-dev`, `dcr-dev-bin`) и сборка Snap в CI.
|
||||
|
||||
**Изменено:**
|
||||
|
||||
- Функция `validate_package_name` стала публичной.
|
||||
- Команды `gen`, `tree`, `fmt`, `setup` больше не игнорируют аргументы и используют цветной вывод.
|
||||
- Инструкция в README изменена с `| bash` на `| sh` для POSIX.
|
||||
@@ -148,6 +322,7 @@
|
||||
### EN
|
||||
|
||||
**Added:**
|
||||
|
||||
- `--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.
|
||||
- Project name validation in `dcr init` and `dcr new` (ASCII letters, digits, `_`, `-` only).
|
||||
@@ -158,6 +333,7 @@
|
||||
- AUR packages (`dcr-dev`, `dcr-dev-bin`) and Snap build in CI.
|
||||
|
||||
**Changed:**
|
||||
|
||||
- `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.
|
||||
- README install instructions changed from `| bash` to `| sh` for POSIX compatibility.
|
||||
@@ -334,6 +510,7 @@ Added:
|
||||
```
|
||||
|
||||
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`).
|
||||
- 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.
|
||||
@@ -368,28 +545,28 @@ Fixed:
|
||||
|
||||
- Path dependencies now work correctly: `dcr add` stores them as `{ path =
|
||||
"./..." }` tables, `is_registry_dep` properly distinguishes registry strings
|
||||
from path/git strings, and `deps/mod.rs` resolves both table-form and
|
||||
legacy string-form path dependencies
|
||||
from path/git strings, and `deps/mod.rs` resolves both table-form and
|
||||
legacy string-form path dependencies
|
||||
- Registry dependency paths no longer hardcoded to `project_root/dcr-index`;
|
||||
now use `package_root_from_registry_info()` which resolves relative to the
|
||||
registry cache root (`~/.dcr/`)
|
||||
now use `package_root_from_registry_info()` which resolves relative to the
|
||||
registry cache root (`~/.dcr/`)
|
||||
- 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:
|
||||
|
||||
- `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`,
|
||||
`normalize_target_os`, `resolve_compiler`, `primary_language`,
|
||||
`resolve_pkg_config_flags` and config helpers. Eliminates code duplication
|
||||
between `cli/build.rs`, `cli/run.rs`, `cli/clean.rs`, `cli/gen.rs`
|
||||
`normalize_target_os`, `resolve_compiler`, `primary_language`,
|
||||
`resolve_pkg_config_flags` and config helpers. Eliminates code duplication
|
||||
between `cli/build.rs`, `cli/run.rs`, `cli/clean.rs`, `cli/gen.rs`
|
||||
- `utils/fs.rs::with_dir` — extracted common directory-scoped execution
|
||||
- `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`,
|
||||
`package_root_from_registry_info`, `registry_include_dir`, `registry_lib_dir`,
|
||||
`path_from_string_dep`
|
||||
`package_root_from_registry_info`, `registry_include_dir`, `registry_lib_dir`,
|
||||
`path_from_string_dep`
|
||||
- Unit tests for `register.rs` (`is_registry_dep`, `package_root_from_registry_info`),
|
||||
`deps/mod.rs` (`path_dep_path`, `push_default_lib_dirs`),
|
||||
`utils/build.rs` (`normalize_target_os`, `parse_version_info`)
|
||||
|
||||
319
Cargo.lock
generated
319
Cargo.lock
generated
@@ -2,18 +2,39 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
@@ -38,6 +59,28 @@ dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
@@ -50,6 +93,19 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -98,15 +154,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dcr"
|
||||
version = "0.7.4"
|
||||
version = "0.8.3"
|
||||
dependencies = [
|
||||
"ctrlc",
|
||||
"fatfs",
|
||||
"glob",
|
||||
"self-replace",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"toml",
|
||||
"toml_edit",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
@@ -126,7 +184,7 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
@@ -165,6 +223,24 @@ version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "fatfs"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05669f8e7e2d7badc545c513710f0eba09c2fbef683eb859fd79c46c355048e0"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
@@ -180,6 +256,30 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -230,6 +330,30 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.2.0"
|
||||
@@ -357,6 +481,17 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
@@ -399,12 +534,21 @@ version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.1",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
@@ -438,6 +582,12 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.5"
|
||||
@@ -487,7 +637,7 @@ version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.1",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -525,6 +675,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.29"
|
||||
@@ -540,7 +696,7 @@ version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.1",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -637,6 +793,18 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
@@ -703,12 +871,18 @@ dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
"winnow 1.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
@@ -718,15 +892,33 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_write",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
"winnow 1.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
@@ -808,6 +1000,51 @@ dependencies = [
|
||||
"wit-bindgen 0.51.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.126"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
@@ -836,18 +1073,71 @@ version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.1",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -930,6 +1220,15 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.3"
|
||||
@@ -1000,7 +1299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"bitflags 2.11.1",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dcr"
|
||||
version = "0.7.4"
|
||||
version = "0.8.3"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
|
||||
@@ -59,6 +59,10 @@ assets = [
|
||||
{ source = "man/man1/dcr-tree.1", dest = "/usr/share/man/man1/dcr-tree.1", mode = "644" },
|
||||
]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
archive = ["dep:fatfs"]
|
||||
|
||||
[dependencies]
|
||||
glob = "0.3"
|
||||
ureq = { version = "2.10", default-features = false, features = ["json", "native-certs"] }
|
||||
@@ -66,8 +70,10 @@ serde = { version = "1", features = ["derive"] }
|
||||
self-replace = "1"
|
||||
sha2 = "0.10"
|
||||
toml = "1.0.3"
|
||||
toml_edit = "0.22"
|
||||
serde_json = "1"
|
||||
ctrlc = "3"
|
||||
fatfs = { version = "0.3", optional = true }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
|
||||
55
README.md
55
README.md
@@ -34,32 +34,6 @@
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><b>Linux (Dexoron Packages)</b></td>
|
||||
<td>
|
||||
|
||||
See https://pkg.dexoron.su for settings youre packages manager
|
||||
|
||||
**Arch Linux:**
|
||||
|
||||
```sh
|
||||
sudo pacman -Sy dcr
|
||||
```
|
||||
|
||||
**Debian/Ubuntu:**
|
||||
|
||||
```sh
|
||||
sudo apt update && sudo apt install dcr
|
||||
```
|
||||
|
||||
**Fedora/RHEL:**
|
||||
|
||||
```sh
|
||||
sudo dnf install dcr
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Arch Linux (AUR)</b></td>
|
||||
<td>
|
||||
|
||||
@@ -103,21 +77,28 @@ cargo install dcr
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Linux / macOS (script)</b></td>
|
||||
<td><b>dcrup (Linux / macOS / BSD / Windows(Bash))</b></td>
|
||||
<td>
|
||||
|
||||
```sh
|
||||
curl -fsSL https://dcr.dexoron.su/install.sh | sh
|
||||
curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
|
||||
export PATH="$HOME/.dcr/bin:$PATH"
|
||||
dcrup install stable
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>BSD (script)</b></td>
|
||||
<td><b>dcrup (Windows PowerShell)</b></td>
|
||||
<td>
|
||||
|
||||
```sh
|
||||
curl -fsSL https://dcr.dexoron.su/install_bsd.sh | sh
|
||||
```powershell
|
||||
irm https://ps1.dcr-tool.ru | iex
|
||||
# or:
|
||||
# irm https://ps1.dcr-tool.ru -OutFile dcrup.ps1
|
||||
# powershell -File .\dcrup.ps1 self-install
|
||||
# $env:Path += ";$env:USERPROFILE\.dcr\bin"
|
||||
dcrup install stable
|
||||
```
|
||||
|
||||
</td>
|
||||
@@ -133,16 +114,6 @@ curl -fsSL https://dcr.dexoron.su/install_bsd.sh | sh
|
||||
sudo snap install --classic --dangerous dcrup_*.snap
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Windows (PowerShell)</b></td>
|
||||
<td>
|
||||
|
||||
```powershell
|
||||
irm https://dcr.dexoron.su/install.ps1 | iex
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -151,7 +122,7 @@ irm https://dcr.dexoron.su/install.ps1 | iex
|
||||
|
||||
```sh
|
||||
git clone https://github.com/dexoron/dcr.git
|
||||
cd dcr && cargo build --release
|
||||
cd dcr && cargo build --release --features archive
|
||||
ln -sf "$PWD/target/release/dcr" ~/.local/bin/dcr
|
||||
```
|
||||
|
||||
|
||||
65
TODO.md
65
TODO.md
@@ -1,65 +0,0 @@
|
||||
# DCR TODO.md
|
||||
|
||||
Priority list of tasks to turn DCR into a true Cargo killer for C/C++ developers.
|
||||
|
||||
**Legend:** `[ ]` — not implemented | `[-]` — partially implemented | `[x]` — implemented
|
||||
|
||||
## 🔥 High Priority
|
||||
|
||||
- [ ] **CLI Output Refactoring & Cleanup**
|
||||
- [ ] Move all terminal strings to a dedicated module (`src/ui/messages.rs`)
|
||||
- [ ] Standardize logging formats (`error:`, `warning:`, `status:`)
|
||||
- [x] Add build execution time statistics to the final output (e.g., `Finished in 4.23s`)
|
||||
- [ ] **Internationalization (i18n) / Localization**
|
||||
- [ ] Integrate a lightweight localization engine (`fluent` or `gettext` pure-Rust port)
|
||||
- [ ] Create initial translation catalogs (`en.ftl`, `ru.ftl`)
|
||||
- [x] **Self-Update Stability Validation**
|
||||
- [x] Self-update implemented via `self-replace` + `ureq` in `flag_update.rs`
|
||||
- [-] **Freestanding / Embedded Automation**
|
||||
- *`-ffreestanding` injected at compile time, `-nostdlib -static` at link time for bare-metal targets or `build.freestanding = true`. Config option `build.freestanding` (bool) added.*
|
||||
- [ ] **Conan and vcpkg integration** Automatic download, installation and linking of packages
|
||||
|
||||
## ✅ Medium Priority
|
||||
|
||||
- [ ] **Static analysis** (`dcr check`)
|
||||
Integration with clang-tidy, cppcheck, include-what-you-use
|
||||
- [-] **Linter + Formatter** (`dcr fmt`, `dcr lint`)
|
||||
*`dcr fmt` (clang-format) implemented; `dcr lint` does not exist yet*
|
||||
- [ ] **Hot reload / Live reload** for applications (raylib, SFML, SDL, etc.)
|
||||
- [-] **Workspaces improvements** Per-member include directories, selective builds, better dependency graph
|
||||
*Basic workspaces with dep ordering + topological sort work; advanced features missing*
|
||||
- [-] **Build-time code generation** Support for protobuf, flatbuffers, Qt moc, custom generators
|
||||
*Generic `build.steps` machinery + Qt toolchain (uic/moc/rcc) work; no dedicated protobuf/flatbuffers generators*
|
||||
- [-] **dcr publish + official package registry** *Registry consumption (fetch, resolve, link) works; `dcr publish` command / local packing missing*
|
||||
|
||||
## 📌 Low Priority / Nice to Have
|
||||
|
||||
- [ ] Support for Meson and Ninja as alternative backends
|
||||
- [ ] Dockerfile generation (`dcr docker init`)
|
||||
- [ ] First-class Windows MSVC support (currently works but rough)
|
||||
- [ ] Documentation generation (`dcr doc`)
|
||||
- [ ] Sanitizers integration (`dcr sanitize`)
|
||||
- [ ] Benchmarks and comparison with CMake/Meson
|
||||
- [ ] C++20 modules support (when it becomes stable)
|
||||
|
||||
## 🧠 Future Ideas
|
||||
|
||||
- Official DCR package registry (dcrhub)
|
||||
- GUI wrapper (dcr-gui)
|
||||
- Plugin system
|
||||
- Toolchain manager (like rustup)
|
||||
- Zig as alternative compiler
|
||||
- deb/rpm/flatpak/AppImage packaging support
|
||||
|
||||
---
|
||||
|
||||
**Currently in progress:**
|
||||
- UI/UX terminal output unification and benchmarking metrics.
|
||||
|
||||
**Completed in recent releases:**
|
||||
- Extreme binary size optimization (-50% weight reduction via `ureq` + `native-certs`)
|
||||
- Complete removal of `git2` and `openssl` in favor of system Git execution
|
||||
- Workspaces & Cross-compilation (`--target <triple>`)
|
||||
- IDE config generation (VS Code, CLion, compile_commands.json)
|
||||
- EFI support & Linker script support (`build.ldscript`)
|
||||
- Mixed C/C++/ASM compilation with custom `std::thread::scope` work-stealing engine
|
||||
13
dcrup.cmd
Executable file
13
dcrup.cmd
Executable file
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
if exist "%SCRIPT_DIR%dcrup.ps1" (
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%dcrup.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
if exist "%USERPROFILE%\.dcr\bin\dcrup.ps1" (
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\.dcr\bin\dcrup.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
echo [error] dcrup.ps1 not found near dcrup.cmd >&2
|
||||
exit /b 1
|
||||
367
dcrup.ps1
Normal file
367
dcrup.ps1
Normal file
@@ -0,0 +1,367 @@
|
||||
#Requires -Version 5.1
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$DcrupHome = if ($env:DCRUP_HOME) { $env:DCRUP_HOME } else { Join-Path $env:USERPROFILE '.dcr' }
|
||||
$DcrupBin = if ($env:DCRUP_BIN) { $env:DCRUP_BIN } else { Join-Path $DcrupHome 'bin' }
|
||||
$DcrupTc = Join-Path $DcrupHome 'toolchains'
|
||||
$DcrupMeta = Join-Path $DcrupHome 'meta'
|
||||
$RepoUrl = if ($env:DCRUP_REPO) { $env:DCRUP_REPO } else { 'https://github.com/dexoron/dcr' }
|
||||
$ApiLatest = 'https://api.github.com/repos/dexoron/dcr/releases/latest'
|
||||
$ApiAll = 'https://api.github.com/repos/dexoron/dcr/releases'
|
||||
$Features = if ($env:DCR_FEATURES) { $env:DCR_FEATURES } else { 'archive' }
|
||||
|
||||
function Info($m) { Write-Host "[dcrup] $m" -ForegroundColor Cyan }
|
||||
function Ok($m) { Write-Host "[ok] $m" -ForegroundColor Green }
|
||||
function Warn($m) { Write-Host "[warn] $m" -ForegroundColor Yellow }
|
||||
function Die($m) { Write-Host "[error] $m" -ForegroundColor Red; exit 1 }
|
||||
|
||||
function Show-Usage {
|
||||
@'
|
||||
dcrup — install and switch DCR versions (Windows)
|
||||
|
||||
Usage:
|
||||
dcrup install <spec> [--build|--release] [--force]
|
||||
dcrup default <spec>
|
||||
dcrup update
|
||||
dcrup list
|
||||
dcrup show
|
||||
dcrup which
|
||||
dcrup uninstall [<spec>|--all]
|
||||
dcrup self-install [--to DIR]
|
||||
dcrup help
|
||||
|
||||
Spec:
|
||||
stable | dev | night
|
||||
VERSION → VERSION@stable (e.g. 0.8.2)
|
||||
VERSION@stable
|
||||
VERSION@dev
|
||||
night → always build from branch "dev" HEAD
|
||||
|
||||
Modes:
|
||||
--release download GitHub Release binary (default for stable/dev)
|
||||
--build cargo build --release --features archive
|
||||
night always --build (prebuilt not available)
|
||||
|
||||
Env:
|
||||
DCRUP_HOME default ~/.dcr
|
||||
DCRUP_BIN default $DCRUP_HOME/bin
|
||||
DCR_FEATURES default archive
|
||||
DCRUP_REPO git/GitHub repo URL
|
||||
'@ | Write-Host
|
||||
}
|
||||
|
||||
function Ensure-Dirs {
|
||||
New-Item -ItemType Directory -Force -Path $DcrupTc, $DcrupBin, $DcrupMeta | Out-Null
|
||||
}
|
||||
|
||||
function Get-Target {
|
||||
$arch = $env:PROCESSOR_ARCHITECTURE
|
||||
if ($env:PROCESSOR_ARCHITEW6432) { $arch = $env:PROCESSOR_ARCHITEW6432 }
|
||||
switch ($arch) {
|
||||
'AMD64' { return 'x86_64-pc-windows-msvc' }
|
||||
'ARM64' { return 'aarch64-pc-windows-msvc' }
|
||||
'x86' { return 'i686-pc-windows-msvc' }
|
||||
default { Die "unsupported arch: $arch" }
|
||||
}
|
||||
}
|
||||
|
||||
function Normalize-Version([string]$v) {
|
||||
if ($v.StartsWith('v')) { return $v.Substring(1) }
|
||||
return $v
|
||||
}
|
||||
|
||||
function Parse-Spec([string]$raw) {
|
||||
$script:SpecRaw = $raw
|
||||
$script:SpecChannel = $null
|
||||
$script:SpecVersion = $null
|
||||
$script:SpecFloating = $false
|
||||
|
||||
if ($raw -in @('stable', 'dev', 'night')) {
|
||||
$script:SpecChannel = $raw
|
||||
$script:SpecFloating = $true
|
||||
return
|
||||
}
|
||||
if ($raw -match '^(.*)@(.*)$') {
|
||||
$script:SpecVersion = Normalize-Version $Matches[1]
|
||||
$script:SpecChannel = $Matches[2]
|
||||
if ($script:SpecChannel -eq 'night') { Die 'cannot pin version on night' }
|
||||
if ($script:SpecChannel -notin @('stable', 'dev')) { Die "unknown channel: $($script:SpecChannel)" }
|
||||
return
|
||||
}
|
||||
$script:SpecVersion = Normalize-Version $raw
|
||||
$script:SpecChannel = 'stable'
|
||||
}
|
||||
|
||||
function Toolchain-Id([string]$extra = '') {
|
||||
if ($script:SpecChannel -eq 'night') {
|
||||
return "night-$extra"
|
||||
}
|
||||
if ($script:SpecFloating) {
|
||||
return "$($script:SpecChannel)-$extra"
|
||||
}
|
||||
return "$($script:SpecVersion)-$($script:SpecChannel)"
|
||||
}
|
||||
|
||||
function Active-Path { Join-Path $DcrupMeta 'active' }
|
||||
|
||||
function Read-Active {
|
||||
$p = Active-Path
|
||||
if (Test-Path $p) { return (Get-Content $p -Raw).Trim() }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Write-Active([string]$id) {
|
||||
Set-Content -Path (Active-Path) -Value $id -NoNewline
|
||||
}
|
||||
|
||||
function Link-Default([string]$id) {
|
||||
$src = Join-Path (Join-Path $DcrupTc $id) 'dcr.exe'
|
||||
if (-not (Test-Path $src)) { Die "toolchain not installed: $id" }
|
||||
$dst = Join-Path $DcrupBin 'dcr.exe'
|
||||
Copy-Item $src $dst -Force
|
||||
Write-Active $id
|
||||
Ok "default → $id ($dst)"
|
||||
}
|
||||
|
||||
function Fetch-Release($channel, $version) {
|
||||
if ($version) {
|
||||
return Invoke-RestMethod -Uri "https://api.github.com/repos/dexoron/dcr/releases/tags/v$version"
|
||||
}
|
||||
if ($channel -eq 'dev') {
|
||||
$all = Invoke-RestMethod -Uri $ApiAll
|
||||
$pre = $all | Where-Object { $_.prerelease } | Select-Object -First 1
|
||||
if (-not $pre) { Die 'no prerelease (dev) found' }
|
||||
return $pre
|
||||
}
|
||||
return Invoke-RestMethod -Uri $ApiLatest
|
||||
}
|
||||
|
||||
function Install-Prebuilt([switch]$Force) {
|
||||
$triple = Get-Target
|
||||
$rel = Fetch-Release $script:SpecChannel $script:SpecVersion
|
||||
$tag = $rel.tag_name
|
||||
$ver = Normalize-Version $tag
|
||||
$assetName = "dcr-$triple-$ver.exe"
|
||||
$asset = $rel.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1
|
||||
if (-not $asset) { Die "asset not found: $assetName" }
|
||||
|
||||
$id = if ($script:SpecFloating) { Toolchain-Id $ver } else { Toolchain-Id }
|
||||
$dest = Join-Path $DcrupTc $id
|
||||
$bin = Join-Path $dest 'dcr.exe'
|
||||
if ((Test-Path $bin) -and -not $Force) {
|
||||
Ok "already installed: $id"
|
||||
Link-Default $id
|
||||
return
|
||||
}
|
||||
Ensure-Dirs
|
||||
New-Item -ItemType Directory -Force -Path $dest | Out-Null
|
||||
Info "downloading $assetName …"
|
||||
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $bin
|
||||
@"
|
||||
channel=$($script:SpecChannel)
|
||||
version=$ver
|
||||
tag=$tag
|
||||
source=release
|
||||
triple=$triple
|
||||
"@ | Set-Content (Join-Path $dest 'dcrup-meta')
|
||||
Ok "installed prebuilt $id"
|
||||
Link-Default $id
|
||||
}
|
||||
|
||||
function Install-Build([switch]$Force) {
|
||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) { Die 'git not found' }
|
||||
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { Die 'cargo not found' }
|
||||
|
||||
$tmp = Join-Path $env:TEMP ("dcrup-" + [guid]::NewGuid().ToString('N'))
|
||||
New-Item -ItemType Directory -Path $tmp | Out-Null
|
||||
try {
|
||||
$src = Join-Path $tmp 'src'
|
||||
if ($script:SpecChannel -eq 'night') {
|
||||
Info "cloning dev for night …"
|
||||
git clone --depth 1 --branch dev $RepoUrl $src 2>$null
|
||||
if ($LASTEXITCODE -ne 0) { git clone --depth 1 $RepoUrl $src | Out-Null }
|
||||
$sha = (git -C $src rev-parse --short HEAD).Trim()
|
||||
$id = Toolchain-Id $sha
|
||||
$ver = 'night'
|
||||
} elseif ($script:SpecFloating) {
|
||||
$rel = Fetch-Release $script:SpecChannel $null
|
||||
$tag = $rel.tag_name
|
||||
$ver = Normalize-Version $tag
|
||||
$id = Toolchain-Id $ver
|
||||
Info "cloning $tag …"
|
||||
git clone --depth 1 --branch $tag $RepoUrl $src 2>$null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
git clone --depth 1 $RepoUrl $src | Out-Null
|
||||
git -C $src fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}"
|
||||
git -C $src checkout $tag
|
||||
}
|
||||
$sha = (git -C $src rev-parse --short HEAD).Trim()
|
||||
} else {
|
||||
$tag = "v$($script:SpecVersion)"
|
||||
$ver = $script:SpecVersion
|
||||
$id = Toolchain-Id
|
||||
Info "cloning $tag …"
|
||||
git clone --depth 1 --branch $tag $RepoUrl $src 2>$null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
git clone --depth 1 $RepoUrl $src | Out-Null
|
||||
git -C $src fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}"
|
||||
git -C $src checkout $tag
|
||||
}
|
||||
$sha = (git -C $src rev-parse --short HEAD).Trim()
|
||||
}
|
||||
|
||||
$dest = Join-Path $DcrupTc $id
|
||||
$bin = Join-Path $dest 'dcr.exe'
|
||||
if ((Test-Path $bin) -and -not $Force) {
|
||||
Ok "already installed: $id"
|
||||
Link-Default $id
|
||||
return
|
||||
}
|
||||
|
||||
$triple = Get-Target
|
||||
Info "building release (features: $Features) …"
|
||||
Push-Location $src
|
||||
cargo build --release --features $Features --target $triple
|
||||
if ($LASTEXITCODE -ne 0) { Die 'cargo build failed' }
|
||||
Pop-Location
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $dest | Out-Null
|
||||
Copy-Item (Join-Path $src "target\$triple\release\dcr.exe") $bin -Force
|
||||
@"
|
||||
channel=$($script:SpecChannel)
|
||||
version=$ver
|
||||
git=$sha
|
||||
source=build
|
||||
features=$Features
|
||||
"@ | Set-Content (Join-Path $dest 'dcrup-meta')
|
||||
Ok "installed build $id"
|
||||
Link-Default $id
|
||||
} finally {
|
||||
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-UserPath {
|
||||
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
if (-not $userPath) { $userPath = '' }
|
||||
if (-not ($userPath -split ';' | Where-Object { $_ -eq $DcrupBin })) {
|
||||
$new = if ($userPath) { "$userPath;$DcrupBin" } else { $DcrupBin }
|
||||
[Environment]::SetEnvironmentVariable('Path', $new, 'User')
|
||||
$env:Path = "$env:Path;$DcrupBin"
|
||||
Warn "added to user PATH: $DcrupBin (restart shell if needed)"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-CmdShim {
|
||||
$cmd = Join-Path $DcrupBin 'dcrup.cmd'
|
||||
$ps1 = Join-Path $DcrupBin 'dcrup.ps1'
|
||||
@"
|
||||
@echo off
|
||||
setlocal
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0dcrup.ps1" %*
|
||||
"@ | Set-Content -Path $cmd -Encoding ASCII
|
||||
if (-not (Test-Path $ps1)) {
|
||||
Copy-Item $PSCommandPath $ps1 -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Cmd-Install {
|
||||
param([string[]]$Args)
|
||||
$mode = 'release'
|
||||
$force = $false
|
||||
$spec = $null
|
||||
foreach ($a in $Args) {
|
||||
switch -Regex ($a) {
|
||||
'^--build$' { $mode = 'build'; continue }
|
||||
'^--release$' { $mode = 'release'; continue }
|
||||
'^--force$' { $force = $true; continue }
|
||||
'^-' { Die "unknown flag: $a" }
|
||||
default {
|
||||
if ($spec) { Die "unexpected: $a" }
|
||||
$spec = $a
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $spec) { Die 'usage: dcrup install <spec> [--build|--release] [--force]' }
|
||||
Parse-Spec $spec
|
||||
Ensure-Dirs
|
||||
if ($script:SpecChannel -eq 'night') { $mode = 'build' }
|
||||
Info "install spec=$spec channel=$($script:SpecChannel) mode=$mode"
|
||||
if ($mode -eq 'build') { Install-Build -Force:$force } else { Install-Prebuilt -Force:$force }
|
||||
Ensure-UserPath
|
||||
Install-CmdShim
|
||||
}
|
||||
|
||||
function Cmd-List {
|
||||
Ensure-Dirs
|
||||
$active = Read-Active
|
||||
Get-ChildItem $DcrupTc -Directory -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
$mark = if ($_.Name -eq $active) { ' *' } else { ' ' }
|
||||
Write-Host "$mark $($_.Name)"
|
||||
}
|
||||
}
|
||||
|
||||
function Cmd-Show {
|
||||
Ensure-Dirs
|
||||
Write-Host "DCRUP_HOME=$DcrupHome"
|
||||
Write-Host "DCRUP_BIN=$DcrupBin"
|
||||
Write-Host "active=$(Read-Active)"
|
||||
$exe = Join-Path $DcrupBin 'dcr.exe'
|
||||
if (Test-Path $exe) { & $exe --version }
|
||||
}
|
||||
|
||||
function Cmd-SelfInstall {
|
||||
Ensure-Dirs
|
||||
Copy-Item $PSCommandPath (Join-Path $DcrupBin 'dcrup.ps1') -Force
|
||||
Install-CmdShim
|
||||
Ensure-UserPath
|
||||
Ok "dcrup installed to $DcrupBin (use: dcrup …)"
|
||||
}
|
||||
|
||||
$cmd = if ($args.Count -ge 1) { $args[0] } else { 'help' }
|
||||
$rest = @()
|
||||
if ($args.Count -gt 1) { $rest = $args[1..($args.Count - 1)] }
|
||||
|
||||
switch ($cmd) {
|
||||
{ $_ -in @('help', '-h', '--help') } { Show-Usage }
|
||||
'install' { Cmd-Install $rest }
|
||||
'list' { Cmd-List }
|
||||
'show' { Cmd-Show }
|
||||
'which' {
|
||||
$p = Join-Path $DcrupBin 'dcr.exe'
|
||||
if (Test-Path $p) { $p } else { Die 'dcr not installed' }
|
||||
}
|
||||
'default' {
|
||||
if ($rest.Count -ne 1) { Die 'usage: dcrup default <spec>' }
|
||||
Parse-Spec $rest[0]
|
||||
if ($script:SpecFloating -or $script:SpecChannel -eq 'night') {
|
||||
$id = Get-ChildItem $DcrupTc -Directory |
|
||||
Where-Object { $_.Name -like "$($script:SpecChannel)-*" } |
|
||||
Sort-Object Name | Select-Object -Last 1
|
||||
if (-not $id) { Die "not installed: $($rest[0])" }
|
||||
Link-Default $id.Name
|
||||
} else {
|
||||
Link-Default (Toolchain-Id)
|
||||
}
|
||||
}
|
||||
'update' {
|
||||
$a = Read-Active
|
||||
if (-not $a) { Die 'no active toolchain' }
|
||||
if ($a -like 'night-*') { Cmd-Install @('night', '--build', '--force') }
|
||||
elseif ($a -like 'stable-*') { Cmd-Install @('stable', '--force') }
|
||||
elseif ($a -like 'dev-*') { Cmd-Install @('dev', '--force') }
|
||||
else { Ok "pinned $a — install a newer version explicitly" }
|
||||
}
|
||||
'uninstall' {
|
||||
if ($rest -contains '--all') {
|
||||
Remove-Item -Recurse -Force $DcrupTc -ErrorAction SilentlyContinue
|
||||
Remove-Item (Join-Path $DcrupBin 'dcr.exe') -ErrorAction SilentlyContinue
|
||||
Remove-Item (Active-Path) -ErrorAction SilentlyContinue
|
||||
Ok 'removed all'
|
||||
} else {
|
||||
Die 'use: dcrup uninstall --all (or install a replacement first)'
|
||||
}
|
||||
}
|
||||
'self-install' { Cmd-SelfInstall }
|
||||
default { Die "unknown command: $cmd" }
|
||||
}
|
||||
599
dcrup.sh
Executable file
599
dcrup.sh
Executable file
@@ -0,0 +1,599 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
DCRUP_HOME="${DCRUP_HOME:-$HOME/.dcr}"
|
||||
DCRUP_BIN="${DCRUP_BIN:-$DCRUP_HOME/bin}"
|
||||
DCRUP_TC="${DCRUP_HOME}/toolchains"
|
||||
DCRUP_META="${DCRUP_HOME}/meta"
|
||||
REPO_URL="${DCRUP_REPO:-https://github.com/dexoron/dcr}"
|
||||
API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest"
|
||||
API_ALL="https://api.github.com/repos/dexoron/dcr/releases"
|
||||
FEATURES="${DCR_FEATURES:-archive}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { printf "${CYAN}[dcrup]${NC} %s\n" "$*"; }
|
||||
ok() { printf "${GREEN}[ok]${NC} %s\n" "$*"; }
|
||||
warn() { printf "${YELLOW}[warn]${NC} %s\n" "$*"; }
|
||||
die() { printf "${RED}[error]${NC} %s\n" "$*" >&2; exit 1; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
dcrup — install and switch DCR versions
|
||||
|
||||
Usage:
|
||||
dcrup install <spec> [--build|--release] [--force] [--libc gnu|musl|auto]
|
||||
dcrup default <spec>
|
||||
dcrup update
|
||||
dcrup list
|
||||
dcrup show
|
||||
dcrup which
|
||||
dcrup uninstall [<spec>|--all]
|
||||
dcrup self-install [--to DIR]
|
||||
dcrup help
|
||||
|
||||
Spec:
|
||||
stable | dev | night
|
||||
VERSION → VERSION@stable (e.g. 0.8.2)
|
||||
VERSION@stable
|
||||
VERSION@dev
|
||||
night → always build from branch "dev" HEAD
|
||||
|
||||
Modes:
|
||||
--release download GitHub Release binary (default for stable/dev)
|
||||
--build cargo build --release --features archive
|
||||
night always --build (prebuilt not available)
|
||||
|
||||
Linux libc (prebuilt triple only):
|
||||
--libc gnu x86_64-unknown-linux-gnu (default)
|
||||
--libc musl x86_64-unknown-linux-musl
|
||||
--libc auto musl if host ldd/libc is musl, else gnu
|
||||
|
||||
Env:
|
||||
DCRUP_HOME default ~/.dcr
|
||||
DCRUP_BIN default $DCRUP_HOME/bin
|
||||
DCR_FEATURES default archive
|
||||
DCRUP_REPO git/GitHub repo URL
|
||||
DCRUP_LIBC gnu|musl|auto (default: gnu)
|
||||
EOF
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
|
||||
}
|
||||
|
||||
detect_linux_libc() {
|
||||
local pref="${DCRUP_LIBC:-gnu}"
|
||||
case "$pref" in
|
||||
gnu|glibc) echo "gnu" ;;
|
||||
musl) echo "musl" ;;
|
||||
auto)
|
||||
if command -v ldd >/dev/null 2>&1; then
|
||||
if ldd --version 2>&1 | grep -qi musl; then
|
||||
echo "musl"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if [[ -e /lib/ld-musl-x86_64.so.1 || -e /lib/ld-musl-aarch64.so.1 ]]; then
|
||||
echo "musl"
|
||||
return 0
|
||||
fi
|
||||
if command -v getconf >/dev/null 2>&1 && getconf GNU_LIBC_VERSION >/dev/null 2>&1; then
|
||||
echo "gnu"
|
||||
return 0
|
||||
fi
|
||||
echo "gnu"
|
||||
;;
|
||||
*) die "invalid DCRUP_LIBC / --libc: $pref (use gnu|musl|auto)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
detect_triple() {
|
||||
local os arch libc
|
||||
os="$(uname -s)"
|
||||
arch="$(uname -m)"
|
||||
case "$os:$arch" in
|
||||
Linux:x86_64)
|
||||
libc="$(detect_linux_libc)"
|
||||
if [[ "$libc" == "musl" ]]; then
|
||||
echo "x86_64-unknown-linux-musl"
|
||||
else
|
||||
echo "x86_64-unknown-linux-gnu"
|
||||
fi
|
||||
;;
|
||||
Linux:aarch64|Linux:arm64)
|
||||
libc="$(detect_linux_libc)"
|
||||
if [[ "$libc" == "musl" ]]; then
|
||||
echo "aarch64-unknown-linux-musl"
|
||||
else
|
||||
echo "aarch64-unknown-linux-gnu"
|
||||
fi
|
||||
;;
|
||||
Linux:i686|Linux:i386)
|
||||
libc="$(detect_linux_libc)"
|
||||
if [[ "$libc" == "musl" ]]; then
|
||||
echo "i686-unknown-linux-musl"
|
||||
else
|
||||
echo "i686-unknown-linux-gnu"
|
||||
fi
|
||||
;;
|
||||
Linux:armv7l|Linux:armv7)
|
||||
libc="$(detect_linux_libc)"
|
||||
if [[ "$libc" == "musl" ]]; then
|
||||
echo "armv7-unknown-linux-musleabihf"
|
||||
else
|
||||
echo "armv7-unknown-linux-gnueabihf"
|
||||
fi
|
||||
;;
|
||||
Linux:riscv64)
|
||||
libc="$(detect_linux_libc)"
|
||||
if [[ "$libc" == "musl" ]]; then
|
||||
echo "riscv64gc-unknown-linux-musl"
|
||||
else
|
||||
echo "riscv64gc-unknown-linux-gnu"
|
||||
fi
|
||||
;;
|
||||
Darwin:x86_64) echo "x86_64-apple-darwin" ;;
|
||||
Darwin:arm64|Darwin:aarch64) echo "aarch64-apple-darwin" ;;
|
||||
FreeBSD:x86_64|FreeBSD:amd64) echo "x86_64-unknown-freebsd" ;;
|
||||
FreeBSD:aarch64|FreeBSD:arm64) echo "aarch64-unknown-freebsd" ;;
|
||||
OpenBSD:x86_64|OpenBSD:amd64) echo "x86_64-unknown-openbsd" ;;
|
||||
NetBSD:x86_64|NetBSD:amd64) echo "x86_64-unknown-netbsd" ;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
case "$arch" in
|
||||
x86_64|amd64) echo "x86_64-pc-windows-gnu" ;;
|
||||
aarch64|arm64) echo "aarch64-pc-windows-gnullvm" ;;
|
||||
i686|i386) echo "i686-pc-windows-gnu" ;;
|
||||
*) die "unsupported Windows arch under MSYS: $arch" ;;
|
||||
esac
|
||||
;;
|
||||
*) die "unsupported platform: $os/$arch" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
normalize_version() {
|
||||
local v="$1"
|
||||
v="${v#v}"
|
||||
printf '%s' "$v"
|
||||
}
|
||||
|
||||
parse_spec() {
|
||||
local raw="$1"
|
||||
SPEC_RAW="$raw"
|
||||
SPEC_CHANNEL=""
|
||||
SPEC_VERSION=""
|
||||
SPEC_FLOATING=0
|
||||
|
||||
case "$raw" in
|
||||
"") die "missing version/channel spec" ;;
|
||||
stable|dev|night)
|
||||
SPEC_CHANNEL="$raw"
|
||||
SPEC_VERSION=""
|
||||
SPEC_FLOATING=1
|
||||
;;
|
||||
*@*)
|
||||
SPEC_VERSION="$(normalize_version "${raw%@*}")"
|
||||
SPEC_CHANNEL="${raw#*@}"
|
||||
case "$SPEC_CHANNEL" in
|
||||
stable|dev) ;;
|
||||
night) die "cannot pin a version on night (use: dcrup install night)" ;;
|
||||
*) die "unknown channel in spec: $SPEC_CHANNEL (use stable|dev)" ;;
|
||||
esac
|
||||
[[ -n "$SPEC_VERSION" ]] || die "empty version in spec: $raw"
|
||||
;;
|
||||
*)
|
||||
SPEC_VERSION="$(normalize_version "$raw")"
|
||||
SPEC_CHANNEL="stable"
|
||||
[[ -n "$SPEC_VERSION" ]] || die "invalid spec: $raw"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
toolchain_id() {
|
||||
if [[ "$SPEC_CHANNEL" == "night" ]]; then
|
||||
local sha="${1:-unknown}"
|
||||
echo "night-${sha}"
|
||||
elif [[ "$SPEC_FLOATING" -eq 1 ]]; then
|
||||
local ver="${1:-latest}"
|
||||
echo "${SPEC_CHANNEL}-${ver}"
|
||||
else
|
||||
echo "${SPEC_VERSION}-${SPEC_CHANNEL}"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_dirs() {
|
||||
mkdir -p "$DCRUP_TC" "$DCRUP_BIN" "$DCRUP_META"
|
||||
}
|
||||
|
||||
active_file() { echo "$DCRUP_META/active"; }
|
||||
|
||||
read_active() {
|
||||
if [[ -f "$(active_file)" ]]; then
|
||||
cat "$(active_file)"
|
||||
fi
|
||||
}
|
||||
|
||||
write_active() {
|
||||
printf '%s\n' "$1" >"$(active_file)"
|
||||
}
|
||||
|
||||
link_default() {
|
||||
local id="$1"
|
||||
local src="$DCRUP_TC/$id/dcr"
|
||||
[[ -x "$src" ]] || die "toolchain not installed: $id"
|
||||
ln -sfn "$src" "$DCRUP_BIN/dcr"
|
||||
write_active "$id"
|
||||
ok "default → $id ($DCRUP_BIN/dcr)"
|
||||
}
|
||||
|
||||
fetch_json() {
|
||||
need_cmd curl
|
||||
curl -fsSL "$1"
|
||||
}
|
||||
|
||||
json_tag() {
|
||||
printf '%s' "$1" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1
|
||||
}
|
||||
|
||||
json_asset_url() {
|
||||
local json="$1" asset="$2"
|
||||
printf '%s' "$json" | sed -n "s#.*\"browser_download_url\"[[:space:]]*:[[:space:]]*\"\([^\"]*/${asset}\)\"#\1#p" | head -n1
|
||||
}
|
||||
|
||||
latest_stable_json() {
|
||||
fetch_json "$API_LATEST"
|
||||
}
|
||||
|
||||
latest_dev_json() {
|
||||
need_cmd curl
|
||||
local json result
|
||||
json="$(fetch_json "$API_ALL")"
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
result="$(printf '%s' "$json" | jq -e -c '[.[] | select(.prerelease == true)][0]')" || true
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
result="$(printf '%s' "$json" | python3 -c '
|
||||
import sys, json
|
||||
rels = json.load(sys.stdin)
|
||||
pre = [r for r in rels if r.get("prerelease")]
|
||||
print(json.dumps(pre[0]) if pre else "", end="")
|
||||
')"
|
||||
else
|
||||
die "dev channel needs jq or python3 to parse GitHub API"
|
||||
fi
|
||||
[[ -n "$result" && "$result" != "null" ]] || die "no prerelease (dev) found"
|
||||
printf '%s' "$result"
|
||||
}
|
||||
|
||||
release_json_for_spec() {
|
||||
if [[ "$SPEC_FLOATING" -eq 1 ]]; then
|
||||
case "$SPEC_CHANNEL" in
|
||||
stable) latest_stable_json ;;
|
||||
dev) latest_dev_json ;;
|
||||
night) die "night has no GitHub release binary" ;;
|
||||
esac
|
||||
else
|
||||
fetch_json "https://api.github.com/repos/dexoron/dcr/releases/tags/v${SPEC_VERSION}"
|
||||
fi
|
||||
}
|
||||
|
||||
install_prebuilt() {
|
||||
local triple asset tag version url json id dest
|
||||
need_cmd curl
|
||||
triple="$(detect_triple)"
|
||||
json="$(release_json_for_spec)"
|
||||
tag="$(json_tag "$json")"
|
||||
[[ -n "$tag" ]] || die "failed to parse release tag"
|
||||
version="$(normalize_version "$tag")"
|
||||
asset="dcr-${triple}-${version}"
|
||||
case "$triple" in
|
||||
*windows*) asset="${asset}.exe" ;;
|
||||
esac
|
||||
url="$(json_asset_url "$json" "$asset")"
|
||||
[[ -n "$url" ]] || die "asset not found: $asset (tag $tag)"
|
||||
|
||||
id="$(toolchain_id "$version")"
|
||||
dest="$DCRUP_TC/$id"
|
||||
if [[ -x "$dest/dcr" && "${FORCE:-0}" != "1" ]]; then
|
||||
ok "already installed: $id"
|
||||
link_default "$id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "downloading $asset …"
|
||||
mkdir -p "$dest"
|
||||
curl -fL "$url" -o "$dest/dcr"
|
||||
chmod +x "$dest/dcr"
|
||||
printf '%s\n' \
|
||||
"channel=$SPEC_CHANNEL" \
|
||||
"version=$version" \
|
||||
"tag=$tag" \
|
||||
"source=release" \
|
||||
"triple=$triple" \
|
||||
"libc=${DCRUP_LIBC:-gnu}" \
|
||||
>"$dest/dcrup-meta"
|
||||
ok "installed prebuilt $id ($triple)"
|
||||
link_default "$id"
|
||||
}
|
||||
|
||||
install_build() {
|
||||
local tmp id dest sha version branch_or_tag
|
||||
need_cmd git
|
||||
need_cmd cargo
|
||||
|
||||
tmp="$(mktemp -d "${TMPDIR:-/tmp}/dcrup.XXXXXX")"
|
||||
cleanup_tmp() { rm -rf "$tmp"; }
|
||||
trap cleanup_tmp EXIT
|
||||
|
||||
if [[ "$SPEC_CHANNEL" == "night" ]]; then
|
||||
log "cloning $REPO_URL (branch dev) for night …"
|
||||
git clone --depth 1 --branch dev "$REPO_URL" "$tmp/src" 2>/dev/null \
|
||||
|| git clone --depth 1 "$REPO_URL" "$tmp/src"
|
||||
sha="$(git -C "$tmp/src" rev-parse --short HEAD)"
|
||||
version="night"
|
||||
id="$(toolchain_id "$sha")"
|
||||
elif [[ "$SPEC_FLOATING" -eq 1 ]]; then
|
||||
local json tag
|
||||
json="$(release_json_for_spec)"
|
||||
tag="$(json_tag "$json")"
|
||||
[[ -n "$tag" ]] || die "failed to resolve floating channel tag"
|
||||
version="$(normalize_version "$tag")"
|
||||
id="$(toolchain_id "$version")"
|
||||
log "cloning $REPO_URL @ $tag …"
|
||||
git clone --depth 1 --branch "$tag" "$REPO_URL" "$tmp/src" 2>/dev/null \
|
||||
|| {
|
||||
git clone --depth 1 "$REPO_URL" "$tmp/src"
|
||||
git -C "$tmp/src" fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}"
|
||||
git -C "$tmp/src" checkout "$tag"
|
||||
}
|
||||
sha="$(git -C "$tmp/src" rev-parse --short HEAD)"
|
||||
else
|
||||
version="$SPEC_VERSION"
|
||||
id="$(toolchain_id)"
|
||||
local tag="v${SPEC_VERSION}"
|
||||
log "cloning $REPO_URL @ $tag …"
|
||||
git clone --depth 1 --branch "$tag" "$REPO_URL" "$tmp/src" 2>/dev/null \
|
||||
|| {
|
||||
git clone --depth 1 "$REPO_URL" "$tmp/src"
|
||||
git -C "$tmp/src" fetch --depth 1 origin "refs/tags/${tag}:refs/tags/${tag}"
|
||||
git -C "$tmp/src" checkout "$tag"
|
||||
}
|
||||
sha="$(git -C "$tmp/src" rev-parse --short HEAD)"
|
||||
fi
|
||||
|
||||
dest="$DCRUP_TC/$id"
|
||||
if [[ -x "$dest/dcr" && "${FORCE:-0}" != "1" ]]; then
|
||||
ok "already installed: $id"
|
||||
link_default "$id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "building release (features: $FEATURES) …"
|
||||
(
|
||||
cd "$tmp/src"
|
||||
# shellcheck disable=SC2086
|
||||
cargo build --release --features $FEATURES
|
||||
)
|
||||
mkdir -p "$dest"
|
||||
cp "$tmp/src/target/release/dcr" "$dest/dcr"
|
||||
chmod +x "$dest/dcr"
|
||||
printf '%s\n' "channel=$SPEC_CHANNEL" "version=$version" "git=$sha" "source=build" "features=$FEATURES" >"$dest/dcrup-meta"
|
||||
ok "installed build $id"
|
||||
link_default "$id"
|
||||
trap - EXIT
|
||||
cleanup_tmp
|
||||
}
|
||||
|
||||
cmd_install() {
|
||||
local mode="release" FORCE=0
|
||||
local spec=""
|
||||
local libc_cli=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--build) mode="build"; shift ;;
|
||||
--release) mode="release"; shift ;;
|
||||
--force) FORCE=1; shift ;;
|
||||
--libc)
|
||||
[[ $# -ge 2 ]] || die "--libc needs gnu|musl|auto"
|
||||
libc_cli="$2"
|
||||
shift 2
|
||||
;;
|
||||
--libc=*)
|
||||
libc_cli="${1#--libc=}"
|
||||
shift
|
||||
;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-*) die "unknown flag: $1" ;;
|
||||
*)
|
||||
[[ -z "$spec" ]] || die "unexpected argument: $1"
|
||||
spec="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$spec" ]] || die "usage: dcrup install <spec> [--build|--release] [--force] [--libc gnu|musl|auto]"
|
||||
parse_spec "$spec"
|
||||
ensure_dirs
|
||||
|
||||
if [[ -n "$libc_cli" ]]; then
|
||||
case "$libc_cli" in
|
||||
gnu|glibc|musl|auto) DCRUP_LIBC="$libc_cli" ;;
|
||||
*) die "invalid --libc: $libc_cli (use gnu|musl|auto)" ;;
|
||||
esac
|
||||
fi
|
||||
export DCRUP_LIBC="${DCRUP_LIBC:-gnu}"
|
||||
|
||||
if [[ "$SPEC_CHANNEL" == "night" ]]; then
|
||||
mode="build"
|
||||
fi
|
||||
|
||||
export FORCE
|
||||
log "install spec=$SPEC_RAW channel=$SPEC_CHANNEL version=${SPEC_VERSION:-∅} mode=$mode libc=$DCRUP_LIBC"
|
||||
if [[ "$mode" == "build" ]]; then
|
||||
install_build
|
||||
else
|
||||
install_prebuilt
|
||||
fi
|
||||
check_path
|
||||
}
|
||||
|
||||
cmd_default() {
|
||||
[[ $# -eq 1 ]] || die "usage: dcrup default <spec>"
|
||||
parse_spec "$1"
|
||||
ensure_dirs
|
||||
local id
|
||||
if [[ "$SPEC_CHANNEL" == "night" || "$SPEC_FLOATING" -eq 1 ]]; then
|
||||
id="$(ls -1 "$DCRUP_TC" 2>/dev/null | grep -E "^${SPEC_CHANNEL}-" | sort | tail -n1 || true)"
|
||||
[[ -n "$id" ]] || die "no installed toolchain for channel $SPEC_CHANNEL (run: dcrup install $SPEC_CHANNEL)"
|
||||
else
|
||||
id="$(toolchain_id)"
|
||||
[[ -d "$DCRUP_TC/$id" ]] || die "not installed: $id (run: dcrup install $1)"
|
||||
fi
|
||||
link_default "$id"
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
ensure_dirs
|
||||
local active
|
||||
active="$(read_active)"
|
||||
[[ -n "$active" ]] || die "no active toolchain (run: dcrup install stable)"
|
||||
case "$active" in
|
||||
night-*)
|
||||
log "updating night (rebuild HEAD) …"
|
||||
FORCE=1 cmd_install night --build --force
|
||||
;;
|
||||
stable-*|dev-*)
|
||||
local ch="${active%%-*}"
|
||||
log "updating floating channel $ch …"
|
||||
FORCE=1 cmd_install "$ch" --force
|
||||
;;
|
||||
*-stable|*-dev)
|
||||
ok "pinned toolchain $active — not floating; install a newer version explicitly"
|
||||
;;
|
||||
*)
|
||||
warn "unknown active id $active; try: dcrup install stable"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
ensure_dirs
|
||||
local active
|
||||
active="$(read_active)"
|
||||
if [[ ! -d "$DCRUP_TC" ]] || [[ -z "$(ls -A "$DCRUP_TC" 2>/dev/null || true)" ]]; then
|
||||
warn "no toolchains installed"
|
||||
return 0
|
||||
fi
|
||||
local d base
|
||||
for d in "$DCRUP_TC"/*; do
|
||||
[[ -d "$d" ]] || continue
|
||||
base="$(basename "$d")"
|
||||
if [[ "$base" == "$active" ]]; then
|
||||
printf ' * %s\n' "$base"
|
||||
else
|
||||
printf ' %s\n' "$base"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
cmd_show() {
|
||||
ensure_dirs
|
||||
local active
|
||||
active="$(read_active)"
|
||||
echo "DCRUP_HOME=$DCRUP_HOME"
|
||||
echo "DCRUP_BIN=$DCRUP_BIN"
|
||||
echo "active=${active:-none}"
|
||||
if [[ -n "$active" && -f "$DCRUP_TC/$active/dcrup-meta" ]]; then
|
||||
echo "--- meta ---"
|
||||
cat "$DCRUP_TC/$active/dcrup-meta"
|
||||
fi
|
||||
if [[ -x "$DCRUP_BIN/dcr" ]]; then
|
||||
echo "--- dcr --version ---"
|
||||
"$DCRUP_BIN/dcr" --version 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_which() {
|
||||
if [[ -L "$DCRUP_BIN/dcr" || -e "$DCRUP_BIN/dcr" ]]; then
|
||||
readlink -f "$DCRUP_BIN/dcr" 2>/dev/null || readlink "$DCRUP_BIN/dcr" 2>/dev/null || echo "$DCRUP_BIN/dcr"
|
||||
else
|
||||
die "dcr not linked (run: dcrup install stable)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_uninstall() {
|
||||
ensure_dirs
|
||||
if [[ "${1:-}" == "--all" ]]; then
|
||||
rm -rf "$DCRUP_TC"
|
||||
rm -f "$DCRUP_BIN/dcr" "$(active_file)"
|
||||
ok "removed all toolchains"
|
||||
return 0
|
||||
fi
|
||||
[[ $# -eq 1 ]] || die "usage: dcrup uninstall <spec>|--all"
|
||||
parse_spec "$1"
|
||||
local id
|
||||
if [[ "$SPEC_FLOATING" -eq 1 || "$SPEC_CHANNEL" == "night" ]]; then
|
||||
id="$(ls -1 "$DCRUP_TC" 2>/dev/null | grep -E "^${SPEC_CHANNEL}-" | sort | tail -n1 || true)"
|
||||
else
|
||||
id="$(toolchain_id)"
|
||||
fi
|
||||
[[ -n "$id" && -d "$DCRUP_TC/$id" ]] || die "not installed: $1"
|
||||
rm -rf "$DCRUP_TC/$id"
|
||||
if [[ "$(read_active)" == "$id" ]]; then
|
||||
rm -f "$DCRUP_BIN/dcr" "$(active_file)"
|
||||
warn "removed active toolchain; run: dcrup install stable"
|
||||
fi
|
||||
ok "removed $id"
|
||||
}
|
||||
|
||||
check_path() {
|
||||
case ":$PATH:" in
|
||||
*":$DCRUP_BIN:"*) ;;
|
||||
*)
|
||||
warn "$DCRUP_BIN is not in PATH"
|
||||
echo "Add to ~/.bashrc / ~/.zshrc / ~/.profile:"
|
||||
echo " export PATH=\"$DCRUP_BIN:\$PATH\""
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
cmd_self_install() {
|
||||
local dest="$DCRUP_BIN"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--to) dest="$2"; shift 2 ;;
|
||||
*) die "usage: dcrup self-install [--to DIR]" ;;
|
||||
esac
|
||||
done
|
||||
ensure_dirs
|
||||
mkdir -p "$dest"
|
||||
local self
|
||||
self="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"
|
||||
cp "$self" "$dest/dcrup"
|
||||
chmod +x "$dest/dcrup"
|
||||
ok "dcrup installed to $dest/dcrup"
|
||||
check_path
|
||||
echo "Then: dcrup install stable"
|
||||
}
|
||||
|
||||
main() {
|
||||
local cmd="${1:-}"
|
||||
shift || true
|
||||
case "$cmd" in
|
||||
""|-h|--help|help) usage ;;
|
||||
install) cmd_install "$@" ;;
|
||||
default) cmd_default "$@" ;;
|
||||
update) cmd_update "$@" ;;
|
||||
list) cmd_list "$@" ;;
|
||||
show) cmd_show "$@" ;;
|
||||
which) cmd_which "$@" ;;
|
||||
uninstall) cmd_uninstall "$@" ;;
|
||||
self-install) cmd_self_install "$@" ;;
|
||||
*) die "unknown command: $cmd (try: dcrup help)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -8,8 +8,10 @@ sidebar_label: Introduction
|
||||
<summary>Quick reference</summary>
|
||||
|
||||
```bash
|
||||
# Installation
|
||||
curl -fsSL https://dcr.dexoron.su/install.sh | sh
|
||||
# Installation (dcrup)
|
||||
curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
|
||||
export PATH="$HOME/.dcr/bin:$PATH"
|
||||
dcrup install stable
|
||||
|
||||
# Create a project
|
||||
dcr new my-app
|
||||
@@ -51,8 +53,9 @@ dcr gen vscode
|
||||
|
||||
- **Incremental builds** — mtime + `.d` headers + SHA256 fingerprint
|
||||
- **Parallel compilation** — thread::scope, atomic task queue
|
||||
- **4 backends** — gcc/clang, MSVC/clang-cl, GAS (ARM), NASM (x86)
|
||||
- **7 project kinds** — bin, staticlib, sharedlib, efi, elf, none, custom
|
||||
- **7 backends** — gcc/clang (unix_cc), MSVC/clang-cl (msvc), GAS, NASM, MASM, FASM, LLVM IR
|
||||
- **8 project kinds** — bin, staticlib, sharedlib, efi, elf, none, custom, flat-bin
|
||||
- **Disk images** — optional `[archive]` packs FAT12/16/32 images after build
|
||||
- **Cross-compilation** — short names and full triples
|
||||
- **Profiles** — debug / release with field overrides
|
||||
- **Workspaces** — multi-package projects with topological sort
|
||||
|
||||
@@ -4,6 +4,145 @@ sidebar_label: Changelog
|
||||
|
||||
# Changelog
|
||||
|
||||
## 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)
|
||||
|
||||
### Added
|
||||
|
||||
- **Compile progress `[N/M]`** — live TTY status line (`compile pkg v0.1.0 [12/41]`) so long mono-package builds do not look hung.
|
||||
- **DCR status style** — fixed-width verbs: `project`, `compile`, `dep`, `ready`, `pack`, `done`, `run`.
|
||||
- **Cargo feature `archive`** — optional FAT packing via `fatfs` (`cargo build --features archive`). Release CI builds with the feature; without it `[archive]` fails with a clear message.
|
||||
- **Tests** — `flat_bin_nasm_build`, package `build.target` without CLI `--target`, stricter workspace `clean --all`.
|
||||
- **CI expansion** — unit + integration on Linux/macOS/Windows, archive feature job, rust-cache, clippy default + all-features (see `.github/workflows/ci.yml`).
|
||||
|
||||
### Changed
|
||||
|
||||
- **No forced host triple from CLI** — without `--target`, package/member `build.target` is honored (bare-metal / ISO `post_steps` paths).
|
||||
- **Relative include/lib flags** (`-I`, `-i`, `-isystem`, `-idirafter`, `-L`, `-T`) absolutized against the package root under workspace builds.
|
||||
- **`dcr clean --all`** — no spam for members without a local `target/` (shared root `target/`).
|
||||
- **`error` / `warn`** — red / yellow on stderr; CLI fully English (`Hint:`).
|
||||
- **Help / man** — list `add`, `lint`, `setup`; unknown command exits with code 1.
|
||||
- **Tool NotFound** — `linker not found: ld.lld …` / `{tool} not found …` instead of raw `os error 2`.
|
||||
|
||||
## 0.8.1 (2026-07-20)
|
||||
|
||||
### Added
|
||||
|
||||
- **`build.kind = "flat-bin"`** — raw binary (default `.bin`) for OS-dev payloads:
|
||||
- **NASM** — `-f bin` → `<stem>.bin`
|
||||
- **FASM** — direct output (`format binary` in source)
|
||||
- **GAS / MASM / LLC** — object → `objcopy -O binary` → `<stem>.bin`
|
||||
- **C/C++** — objects → freestanding link → `objcopy -O binary` → `<name>.bin`
|
||||
- Requires `llvm-objcopy` / `objcopy` / `gobjcopy` except for NASM/FASM direct emit
|
||||
- **`[archive]` section** — FAT12/16/32 disk images after build (`output`, `format`, `size`, `offset`, `label`, `bootsector`, `layout`). From 0.8.2 requires feature `archive`.
|
||||
- **Single-file `build.roots`** — a root may be a single source or header file, not only a directory.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`--force`** also re-runs `build.steps` and `build.post_steps`.
|
||||
- **`dcr run` on workspace-only root** — if `[run].cmd` is set, run that after build; otherwise delegate to a workspace member.
|
||||
|
||||
## 0.8.0 (2026-07-18)
|
||||
|
||||
### Added
|
||||
|
||||
- **Dedicated Build Engine (`src/core/build/engine.rs`)** — decoupled the build orchestration logic completely from the CLI front-end wrapper in `src/cli/build.rs` to a reusable, decoupled core build engine.
|
||||
- **Polymorphic Language Model (`Language` trait)** — introduced the abstract `Language` trait (`src/core/build/language/mod.rs`). File scanning, compiler resolution, and flags handling are now encapsulated in dedicated language modules for C, C++ (including Qt code generation), LLVM IR, and ASM (GAS, NASM, FASM, MASM).
|
||||
- **Polymorphic Compilation Dispatch (`Builder` trait)** — introduced the `Builder` trait to generalize builder invocations (`src/core/build/builder/mod.rs`). Consolidated GCC/Clang logic into `builder/cc_common.rs` and Microsoft Visual C++ logic into `builder/msvc/`.
|
||||
- **Per-Language Configuration overrides (`[build.<lang>]`)** — introduced support for configuring language-specific compiler, standard, and flags (e.g. `[build.c]`, `[build.cxx]`, `[build.asm]`) independently in `dcr.toml`.
|
||||
- **Automatic Workspace Dependency Injection** — during workspace builds, include/lib paths of dependent workspace members (including their source headers, local `include/` folders, and build target `target/include` / `target/lib` folders) are automatically resolved and injected.
|
||||
- **`BuildReporter` Event System** — decoupled build orchestration output into a structured event-driven reporter model (`src/core/build/report.rs`), making DCR suitable for library embedding and IDE integrations without stderr capturing.
|
||||
- **Build Cancellation Support** — introduced thread-safe cancellation tokens (`Arc<AtomicBool>`) allowing clients to safely abort compile runs mid-execution.
|
||||
- **TOML Formatting & Custom Keys Preservation** — migrated the config editor to `toml_edit`, preserving all unknown/user-defined TOML keys, structures, and comments during file write operations (e.g., in `dcr add`).
|
||||
- **Path Dependencies Target Include Resolution** — local path dependencies configured via tables now correctly expose headers built and packaged into their `target/include` folder to consumer packages.
|
||||
- **Modular Integration Test Suite** — refactored the monolith `tests/cli_basic.rs` file into clean, specialized integration tests: `cli_build.rs`, `cli_deps.rs`, `cli_lint.rs`, `cli_new.rs`, `cli_qt.rs`, `cli_test.rs`, `cli_workspace.rs`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Consolidated Linking and Archiving** — artifact generation (linking executables/shared libraries and archiving static libraries) consolidated into a single `src/core/build/builder/artifact.rs` module.
|
||||
- **Workspace-Aware member execution** — `dcr run` executed at the workspace root of a `workspace_only` project now triggers the member build inside the workspace context using the `--workspace` parameter, preventing standalone build issues.
|
||||
- **Elimination of Global State** — removed global variables and states from the build core, encapsulating build logic inside isolated `BuildContext` structs for thread safety.
|
||||
- **Build Cache Separation** — relocated mtime tracking, incremental caching, and header-dependency checking logic to `src/core/build/cache.rs`.
|
||||
- **Modularized Pre-build Steps** — decoupled generator scripts and pre-build commands parsing into `src/core/build/steps.rs`.
|
||||
|
||||
## 0.7.4 (2026-06-17)
|
||||
|
||||
### Added
|
||||
|
||||
- **Native Qt support** — automatic meta-object handling (MOC, UIC, RCC) added when `build.qt = true` is set in `dcr.toml`.
|
||||
- **`dcr lint` command** — C/C++ static analysis via `clang-tidy`. Supports `--fix` for automatic fixes. Respects `build.roots` and `build.src_disable` from config.
|
||||
- **Three new assembler backends**:
|
||||
- **MASM** (`compiler = "ml"` / `"ml64"`) — Microsoft Macro Assembler.
|
||||
- **FASM** (`compiler = "fasm"`) — Flat Assembler.
|
||||
- **LLVM IR** (`compiler = "llc"`) — compiles `.ll` files via `llc -filetype=obj`.
|
||||
- **Shared ASM pipeline** — linking and archiving logic consolidated into `core/builder/asm.rs`. Adding a new assembler backend now takes ~30 lines.
|
||||
- **Filesystem utilities** — `to_hex()` and `home_dir()` extracted to `utils/fs.rs`.
|
||||
- **Build utilities** — `normalize_target()`, `normalize_kind()`, `normalize_platform()`, `default_profile_flags()` extracted to `utils/build.rs`.
|
||||
- **Backend helpers** — `asm_lang_flag()`, `source_extensions()`, `elapsed_secs()` added to `core/builder/common.rs`.
|
||||
- **Full Bare-Metal / Freestanding Automation** — introduced the `build.freestanding = true` configuration option. When enabled (or when a bare-metal target is detected), DCR automatically injects `-ffreestanding` during compilation and both `-nostdlib` and `-static` during linking.
|
||||
- **Artifact Optimization (LTO & Strip)** — added `build.lto` (auto-injects `-flto` for compiler and linker) and `build.strip` (automatically strips debug symbols via linker `-s` flag) options to `dcr.toml`.
|
||||
- **Compilation Thread Control** — added the `build.codegen-units` option to strictly limit the maximum number of parallel jobs utilized by the custom `parallel_build` worker pool.
|
||||
- **Panic Behavior Management** — added `build.panic = "abort"` support. For C++ targets, it automatically strips exception handling and unwind tables via `-fno-exceptions`, `-fno-unwind-tables`, and `-fno-asynchronous-unwind-tables`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Intelligent Default Flags Generation** — automated injection of fallback optimization (`-O3`/`-O0`), debug (`-g`), and warning (`-Wall -Wextra`) flags is now suppressed for bare-metal and freestanding builds if `build.cflags` are overridden.
|
||||
- **Centralized Bare-Metal Detection** — relocated the `is_bare_metal_target` helper to common build utilities (`src/utils/build.rs`) for unified access across the building core.
|
||||
- **Architectural Refactoring**:
|
||||
- Renamed `src/config.rs` to `src/templates.rs`.
|
||||
- Started consolidation of configuration and build orchestration logic.
|
||||
|
||||
## 0.7.3 (2026-06-13)
|
||||
|
||||
### Added
|
||||
|
||||
- **`--vcs` option for `new` and `init` commands** — introduced explicit version control system selection via `--vcs <git|none>`.
|
||||
- **Git metadata integration in `--version`** — the CLI now appends the current short commit hash and a `-dirty` suffix if there are uncommitted changes in the DCR repository.
|
||||
- **Automatic `.gitignore` generation** — Git repository initialization now automatically writes a `.gitignore` file excluding the `/target` directory.
|
||||
- **Nested repository prevention** — automatic Git initialization is now skipped if `dcr new` or `dcr init` is executed inside an already existing Git repository.
|
||||
- **VCS verification tests** — added `new_vcs_options_work` and `init_vcs_options_work` integration tests to ensure reliable repository behavior.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Complete removal of `git2` dependency** — all Git actions (dependency fetching, VCS setup, status checks) are now delegated to the system `git` executable via `Command`. This simplifies compilation and drops the need for `vendored` and `openssl` features.
|
||||
- **Migrated from `reqwest` to `ureq`** — replaced the heavy `reqwest` crate in `flag_update.rs` with the lightweight synchronous `ureq 2.10` client, reducing overhead and the final binary size.
|
||||
|
||||
## 0.7.2 (2026-06-07)
|
||||
|
||||
### Added
|
||||
|
||||
- `default_target_triple()` now respects `target_env` — Linux (gnu/musl) and Windows (msvc/gnu) use the correct environment instead of hardcoded values.
|
||||
|
||||
### 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`.
|
||||
- **GitHub Stars and GPL-3.0 badges not rendering in README** — badge block converted to pure HTML.
|
||||
|
||||
### Changed
|
||||
|
||||
- Default target resolution extracted into shared `default_target_triple()` — removed duplication across build, run, clean.
|
||||
|
||||
## 0.7.1 (2026-06-02)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -13,7 +13,7 @@ dcr build # debug profile
|
||||
dcr build --release # release build
|
||||
dcr build --debug # explicit debug
|
||||
dcr build --target aarch64-unknown-linux-gnu
|
||||
dcr build --force # full rebuild
|
||||
dcr build --force # full rebuild (also re-runs build.steps / post_steps)
|
||||
dcr build --clean # clean + build
|
||||
dcr build --verbose # show compilation commands
|
||||
dcr build --workspace pkg-a # build specific workspace package
|
||||
@@ -21,6 +21,8 @@ dcr build --workspace pkg-a # build specific workspace package
|
||||
|
||||
Artifacts: `target/<triple>/<profile>/<binary-name>` (Linux with explicit target).
|
||||
|
||||
With `[archive]` in `dcr.toml`, a FAT disk image is packed after a successful build.
|
||||
|
||||
## `dcr run`
|
||||
|
||||
Builds (if sources are newer than artifacts) and runs the binary.
|
||||
@@ -28,8 +30,11 @@ Builds (if sources are newer than artifacts) and runs the binary.
|
||||
```bash
|
||||
dcr run
|
||||
dcr run --release
|
||||
dcr run --force # force rebuild then run
|
||||
```
|
||||
|
||||
On a **workspace-only** root: if `[run].cmd` is set, that command is used after build; otherwise DCR delegates to a workspace member.
|
||||
|
||||
## `dcr clean`
|
||||
|
||||
Removes `target/` directory (or `target/<target>/<profile>`).
|
||||
|
||||
@@ -12,8 +12,12 @@ Only ASCII letters, digits, underscores `_` and hyphens `-` are allowed in the p
|
||||
|
||||
```bash
|
||||
dcr new my-app
|
||||
dcr new my-app --vcs none
|
||||
```
|
||||
|
||||
Flags:
|
||||
- `--vcs <git|none>` — Initialize version control system (defaults to `git` if available, or `none`).
|
||||
|
||||
Generates:
|
||||
- `dcr.toml` with basic fields
|
||||
- `src/main.c` with a `main` template
|
||||
@@ -23,8 +27,11 @@ Generates:
|
||||
Initializes a DCR project in the current (empty) directory.
|
||||
|
||||
```bash
|
||||
mkdir app && cd app
|
||||
dcr init
|
||||
dcr init --vcs none
|
||||
```
|
||||
|
||||
Flags:
|
||||
- `--vcs <git|none>` — Initialize version control system (defaults to `git` if available, or `none`).
|
||||
|
||||
The project name is taken from the current directory name. The directory name must follow the same naming rules as `dcr new`.
|
||||
|
||||
@@ -15,9 +15,9 @@ dcr setup
|
||||
If `~/.dcr/config.toml` is not found, DCR returns an error. Create it manually:
|
||||
|
||||
```toml
|
||||
[[registry]]
|
||||
name = "default"
|
||||
[registry.default]
|
||||
url = "https://index.dcr.pm"
|
||||
priority = 1
|
||||
```
|
||||
|
||||
## `dcr --help`
|
||||
|
||||
@@ -42,9 +42,10 @@ cargo test --all-targets
|
||||
|
||||
## CI
|
||||
|
||||
CI runs:
|
||||
CI runs (see `.github/workflows/ci.yml`):
|
||||
- `cargo fmt --check`
|
||||
- `cargo clippy --all-targets -- -D warnings`
|
||||
- `cargo test --all-targets`
|
||||
- Build on Linux, macOS, Windows
|
||||
- Integration tests
|
||||
- `cargo clippy` (default + `--all-features`)
|
||||
- `cargo check --all-targets --all-features`
|
||||
- Unit + integration tests on Linux, macOS, Windows
|
||||
- Extra Linux job with `--features archive` (FAT images)
|
||||
- Tools on runners: clang, nasm, clang-format/tidy (Linux)
|
||||
|
||||
31
docs/faq.mdx
31
docs/faq.mdx
@@ -13,9 +13,9 @@ error: registry not found
|
||||
**Solution:** Make sure `~/.dcr/config.toml` exists:
|
||||
|
||||
```toml
|
||||
[[registry]]
|
||||
name = "default"
|
||||
[registry.default]
|
||||
url = "https://index.dcr.pm"
|
||||
priority = 1
|
||||
```
|
||||
|
||||
And check `DCR_INDEX_PATH`:
|
||||
@@ -39,27 +39,6 @@ which clang
|
||||
|
||||
Or specify explicitly via `[toolchain]` in `dcr.toml`.
|
||||
|
||||
## openssl-sys build failed
|
||||
|
||||
When building DCR from source:
|
||||
|
||||
```
|
||||
error: failed to run custom build command for openssl-sys
|
||||
```
|
||||
|
||||
**Solution:** Install OpenSSL development headers:
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install libssl-dev
|
||||
|
||||
# Fedora
|
||||
sudo dnf install openssl-devel
|
||||
|
||||
# macOS
|
||||
brew install openssl
|
||||
```
|
||||
|
||||
## Ctrl+C during build
|
||||
|
||||
DCR handles SIGINT: interrupts the current compilation and exits with non-zero code.
|
||||
@@ -76,6 +55,12 @@ See [Cross-Compile Recipe](/docs/recipes/cross-to-windows).
|
||||
|
||||
See [Workspace Recipe](/docs/recipes/multi-package-workspace).
|
||||
|
||||
## Bootloader / pure NASM OS image
|
||||
|
||||
Use `kind = "flat-bin"` (NASM `-f bin`) and optional `[archive]` for a FAT disk image.
|
||||
|
||||
See [OS-dev recipe](/docs/recipes/os-flat-bin-archive).
|
||||
|
||||
## Lock file
|
||||
|
||||
`dcr.lock` is created during builds (`dcr build`) when registry dependencies are present. To force an update — delete `dcr.lock` and run `dcr build`.
|
||||
|
||||
@@ -68,3 +68,11 @@ dcr test # run tests
|
||||
```bash
|
||||
dcr fmt # clang-format with .clang-format
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
```bash
|
||||
dcr lint # clang-tidy checks
|
||||
dcr lint --fix # apply fixes automatically
|
||||
```
|
||||
|
||||
|
||||
@@ -4,25 +4,64 @@ sidebar_label: Installation
|
||||
|
||||
# Installation
|
||||
|
||||
## Linux (Dexoron Packages)
|
||||
## dcrup (recommended)
|
||||
|
||||
Configure your package manager at https://pkg.dexoron.su and install:
|
||||
Install and switch DCR versions (stable / dev / night, pin `0.8.2`, optional build from source).
|
||||
|
||||
### Linux / macOS / BSD / Windows (bash)
|
||||
|
||||
**Arch Linux**
|
||||
```sh
|
||||
sudo pacman -Sy dcr
|
||||
curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
|
||||
export PATH="$HOME/.dcr/bin:$PATH"
|
||||
dcrup install stable
|
||||
```
|
||||
|
||||
**Debian/Ubuntu**
|
||||
Non-interactive install of DCR in one shot:
|
||||
|
||||
```sh
|
||||
sudo apt update && sudo apt install dcr
|
||||
curl -fsSL https://sh.dcr-tool.ru | sh -s -- install stable
|
||||
export PATH="$HOME/.dcr/bin:$PATH"
|
||||
```
|
||||
|
||||
**Fedora/RHEL**
|
||||
```sh
|
||||
sudo dnf install dcr
|
||||
### Windows (PowerShell)
|
||||
|
||||
```powershell
|
||||
irm https://ps1.dcr-tool.ru | iex
|
||||
# or download then run:
|
||||
# irm https://ps1.dcr-tool.ru -OutFile dcrup.ps1
|
||||
# powershell -File .\dcrup.ps1 self-install
|
||||
# $env:Path += ";$env:USERPROFILE\.dcr\bin"
|
||||
dcrup install stable
|
||||
```
|
||||
|
||||
Optional cmd bootstrap (if you prefer `curl` of the cmd shim):
|
||||
|
||||
```bat
|
||||
curl -fsSL -o dcrup.cmd https://cmd.dcr-tool.ru
|
||||
```
|
||||
|
||||
After `self-install`, the command is **`dcrup`** (no `.sh` / `.ps1`): shims live in `~/.dcr/bin` (Unix) or `%USERPROFILE%\.dcr\bin` (Windows `dcrup.cmd`).
|
||||
|
||||
### Common dcrup commands
|
||||
|
||||
```sh
|
||||
dcrup install stable # latest stable prebuilt
|
||||
dcrup install 0.8.2 # pin → 0.8.2@stable
|
||||
dcrup install 0.8.2@dev
|
||||
dcrup install stable --libc musl # Linux: musl asset (default: gnu)
|
||||
dcrup install stable --build # cargo build --features archive
|
||||
dcrup install night # always build from branch dev HEAD
|
||||
dcrup default 0.8.2
|
||||
dcrup update
|
||||
dcrup list
|
||||
dcrup show
|
||||
dcrup which
|
||||
```
|
||||
|
||||
Layout: `~/.dcr/toolchains/<id>/dcr` and `~/.dcr/bin/dcr` → active version.
|
||||
|
||||
---
|
||||
|
||||
## Arch Linux (AUR)
|
||||
|
||||
```sh
|
||||
@@ -42,11 +81,12 @@ brew install dcr
|
||||
sudo snap install dcrup
|
||||
```
|
||||
|
||||
> If classic Snap Store publishing is unavailable, install the `.snap` from [GitHub Releases](https://github.com/dexoron/dcr/releases/latest) with `--dangerous`.
|
||||
|
||||
## Nix (flake)
|
||||
|
||||
```sh
|
||||
nix run github:dexoron/dcr
|
||||
# or install to profile:
|
||||
nix profile install github:dexoron/dcr
|
||||
```
|
||||
|
||||
@@ -56,51 +96,40 @@ nix profile install github:dexoron/dcr
|
||||
cargo install dcr
|
||||
```
|
||||
|
||||
## Linux / macOS (script)
|
||||
Note: crates.io builds may omit optional features. For FAT disk images (`[archive]`), prefer release binaries or:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://dcr.dexoron.su/install.sh | sh
|
||||
```
|
||||
|
||||
## BSD (script)
|
||||
|
||||
```sh
|
||||
curl -fsSL https://dcr.dexoron.su/install_bsd.sh | sh
|
||||
```
|
||||
|
||||
## Windows (PowerShell)
|
||||
|
||||
```powershell
|
||||
irm https://dcr.dexoron.su/install.ps1 | iex
|
||||
cargo install dcr --features archive
|
||||
```
|
||||
|
||||
## From source
|
||||
|
||||
```sh
|
||||
git clone https://github.com/dexoron/dcr.git
|
||||
cd dcr && cargo build --release
|
||||
cd dcr
|
||||
cargo build --release --features archive
|
||||
ln -sf "$PWD/target/release/dcr" ~/.local/bin/dcr
|
||||
# or manage versions with dcrup install night / --build
|
||||
```
|
||||
|
||||
## Post-install
|
||||
|
||||
Verify the installation:
|
||||
|
||||
```bash
|
||||
dcr --version
|
||||
dcrup show # if installed via dcrup
|
||||
```
|
||||
|
||||
Man pages are available after installation:
|
||||
Man pages (package installs / release assets):
|
||||
|
||||
```bash
|
||||
man dcr
|
||||
man dcr-build
|
||||
```
|
||||
|
||||
To use the registry, create `~/.dcr/config.toml`:
|
||||
Registry (optional) — `~/.dcr/config.toml`:
|
||||
|
||||
```toml
|
||||
[[registry]]
|
||||
name = "default"
|
||||
[registry.default]
|
||||
url = "https://index.dcr.pm"
|
||||
priority = 1
|
||||
```
|
||||
|
||||
@@ -10,36 +10,23 @@ DCR itself is [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html
|
||||
|
||||
> **Note:** DCR is a build tool, not a library. The GPL applies only to DCR's own source code. Projects built with DCR are not subject to DCR's license — their licensing is determined solely by their own code and dependencies.
|
||||
|
||||
## Vendored libraries
|
||||
|
||||
DCR statically links the following C libraries through its vendored Rust dependencies:
|
||||
|
||||
| Library | License | Bundled by |
|
||||
|---|---|---|
|
||||
| [libgit2](https://libgit2.org) | MIT OR Apache-2.0 | `git2` / `libgit2-sys` |
|
||||
| [libssh2](https://libssh2.org) | MIT OR Apache-2.0 | `libgit2-sys` |
|
||||
| [zlib](https://zlib.net) | MIT OR Apache-2.0 | `libgit2-sys` |
|
||||
| [OpenSSL](https://openssl.org) | Apache-2.0 | `openssl-sys` / `openssl-src` |
|
||||
| [ring](https://github.com/briansmith/ring) | Apache-2.0 AND ISC | `rustls` |
|
||||
|
||||
## Rust dependencies
|
||||
|
||||
DCR is written in Rust. The following notable Rust crates are statically linked:
|
||||
|
||||
| Crate | License |
|
||||
|---|---|
|
||||
| [`reqwest`](https://crates.io/crates/reqwest) | MIT OR Apache-2.0 |
|
||||
| [`rustls`](https://crates.io/crates/rustls) | Apache-2.0 OR ISC OR MIT |
|
||||
| [`ureq`](https://crates.io/crates/ureq) | MIT OR Apache-2.0 |
|
||||
| [`serde`](https://crates.io/crates/serde) | MIT OR Apache-2.0 |
|
||||
| [`toml`](https://crates.io/crates/toml) | MIT OR Apache-2.0 |
|
||||
| [`toml_edit`](https://crates.io/crates/toml_edit) | MIT OR Apache-2.0 |
|
||||
| [`serde_json`](https://crates.io/crates/serde_json) | MIT OR Apache-2.0 |
|
||||
| [`sha2`](https://crates.io/crates/sha2) | MIT OR Apache-2.0 |
|
||||
| [`git2`](https://crates.io/crates/git2) | MIT OR Apache-2.0 |
|
||||
| [`openssl`](https://crates.io/crates/openssl) | Apache-2.0 |
|
||||
| [`tokio`](https://crates.io/crates/tokio) | MIT |
|
||||
| [`hyper`](https://crates.io/crates/hyper) | MIT |
|
||||
| [`glob`](https://crates.io/crates/glob) | MIT OR Apache-2.0 |
|
||||
| [`self-replace`](https://crates.io/crates/self-replace) | MIT OR Apache-2.0 |
|
||||
| [`ctrlc`](https://crates.io/crates/ctrlc) | MIT OR Apache-2.0 |
|
||||
|
||||
Full dependency tree (143 packages) is available in [`Cargo.lock`](https://github.com/dexoron/dcr/blob/main/Cargo.lock). The overwhelming majority are dual-licensed under MIT OR Apache-2.0.
|
||||
Full dependency tree is available in [`Cargo.lock`](https://github.com/dexoron/dcr/blob/main/Cargo.lock). The overwhelming majority are dual-licensed under MIT OR Apache-2.0.
|
||||
|
||||
## Additional credits
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ type = "none"
|
||||
inherit = true
|
||||
language = "c"
|
||||
standard = "c11"
|
||||
workspace_only = true
|
||||
|
||||
[workspace.lib-core]
|
||||
path = "lib-core"
|
||||
|
||||
89
docs/recipes/os-flat-bin-archive.mdx
Normal file
89
docs/recipes/os-flat-bin-archive.mdx
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
sidebar_label: OS-dev (flat-bin + archive)
|
||||
---
|
||||
|
||||
# Pure ASM / boot image (flat-bin + archive)
|
||||
|
||||
Minimal OS-dev style pipeline: assemble raw binaries with NASM, then pack a FAT floppy/image with an optional boot sector.
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
myos/
|
||||
dcr.toml
|
||||
src/
|
||||
boot.asm # boot sector (512 bytes)
|
||||
kernel.asm # payload
|
||||
```
|
||||
|
||||
For multiple flat artifacts, use a **workspace** (one member per binary) or separate packages; a single `flat-bin` package produces one binary per source stem.
|
||||
|
||||
## Boot package (`flat-bin`)
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "boot"
|
||||
version = "0.1.0"
|
||||
|
||||
[build]
|
||||
language = "asm"
|
||||
compiler = "nasm"
|
||||
kind = "flat-bin"
|
||||
extension = "bin"
|
||||
roots = ["src/boot.asm"]
|
||||
```
|
||||
|
||||
```bash
|
||||
dcr build
|
||||
# → target/<…>/debug/boot.bin (NASM -f bin, no link)
|
||||
```
|
||||
|
||||
## Disk image after build
|
||||
|
||||
```toml
|
||||
[archive]
|
||||
output = "target/{profile}/disk.img"
|
||||
format = "fat12"
|
||||
size = "1440K"
|
||||
label = "MYOS"
|
||||
bootsector = "target/{profile}/boot.bin"
|
||||
|
||||
[[archive.layout]]
|
||||
from = "target/{profile}/kernel.bin"
|
||||
to = "KERNEL.BIN"
|
||||
```
|
||||
|
||||
- `format`: `fat12`, `fat16`, or `fat32`
|
||||
- `size`: bytes or `K`/`KB`/`M`/`MB`/`G`/`GB` (default ~1.44 MiB)
|
||||
- `bootsector`: written only when `offset` is omitted or `0`
|
||||
- `from` may be a glob; `{profile}` is substituted in paths
|
||||
|
||||
## C kernel → flat binary
|
||||
|
||||
```toml
|
||||
[build]
|
||||
language = "c"
|
||||
compiler = "clang"
|
||||
kind = "flat-bin"
|
||||
freestanding = true
|
||||
filename = "kernel"
|
||||
extension = "bin"
|
||||
ldflags = ["-T", "linker.ld"]
|
||||
```
|
||||
|
||||
Pipeline: objects → temporary linked ELF → `objcopy -O binary` → `kernel.bin`. Requires `objcopy` / `llvm-objcopy` in PATH.
|
||||
|
||||
## Other assemblers
|
||||
|
||||
| Tool | Notes |
|
||||
|------|--------|
|
||||
| **FASM** | Write `format binary` in the source; DCR writes `<stem>.bin` directly |
|
||||
| **GAS** | Assemble `.s` → obj → `objcopy -O binary` |
|
||||
| **LLC** | `language = "llvm_ir"` → obj → objcopy |
|
||||
| **MASM** | COFF obj → objcopy (needs binutils/LLVM objcopy) |
|
||||
|
||||
## Notes
|
||||
|
||||
- Single-file `roots` are supported: `roots = ["src/boot.asm"]`.
|
||||
- `--force` re-runs `build.steps` / `build.post_steps` as well as recompilation.
|
||||
- Prefer `kind = "elf"` if you need a relocatable ELF kernel without stripping to raw binary.
|
||||
@@ -56,7 +56,7 @@ moc = "moc {in} -o {out}"
|
||||
### GAS / NASM / MASM / FASM / LLVM-IR
|
||||
|
||||
- GAS: `-I`, `-c -o`, `--defsym`
|
||||
- NASM: `-I`, `-o`, `-D`, `-f` (format: win64/elf64/macho64/macho32/elf32)
|
||||
- NASM: `-I`, `-o`, `-D`, `-f` (format: win64/elf64/macho64/macho32/elf32; **`bin` when `kind = "flat-bin"`**)
|
||||
- MASM: `/nologo /c /Fo<obj> <source>`
|
||||
- FASM: `<source> <output>` (output path is the object file)
|
||||
- LLVM IR: `-filetype=obj <source> -o <output>`
|
||||
@@ -87,6 +87,11 @@ Three levels of incrementality:
|
||||
| `elf` | ELF without stdlib | `target/<triple>/<profile>/<name>` |
|
||||
| `none` | Compile only, no link | — |
|
||||
| `custom` | Full filename+extension control | `target/<triple>/<profile>/<filename>.<extension>` |
|
||||
| `flat-bin` | Raw binary | ASM: `<stem>.bin` (NASM `-f bin` / FASM / GAS·MASM·LLC via objcopy); C/C++: `<name>.bin` (link + objcopy) |
|
||||
|
||||
## Disk images (`[archive]`)
|
||||
|
||||
After a successful build, if `[archive]` is present in `dcr.toml`, DCR formats a FAT volume and copies files from `layout` into the image. See [dcr.toml → archive](/docs/reference/dcr-toml#archive).
|
||||
|
||||
## Build Steps
|
||||
|
||||
|
||||
@@ -57,14 +57,8 @@ dcr build --target aarch64-none-elf
|
||||
|
||||
## Target Directory
|
||||
|
||||
On Linux with explicit target:
|
||||
By default, the compilation output directories are structured as follows:
|
||||
|
||||
```
|
||||
target/<triple>/<profile>/
|
||||
```
|
||||
|
||||
On macOS/Windows or without target:
|
||||
|
||||
```
|
||||
target/<profile>/
|
||||
```
|
||||
- **Linux and BSD**: Always output to `target/<triple>/<profile>/` (using host triple if no target is specified).
|
||||
- **macOS and Windows (without target)**: Output to `target/<profile>/`.
|
||||
- **macOS and Windows (with explicit target)**: Output to `target/<triple>/<profile>/`.
|
||||
|
||||
@@ -33,6 +33,9 @@ Main project configuration file. Located at the project root.
|
||||
|
||||
[run]
|
||||
# run settings
|
||||
|
||||
[archive]
|
||||
# optional: pack FAT disk image after build
|
||||
```
|
||||
|
||||
## [package]
|
||||
@@ -41,7 +44,7 @@ Main project configuration file. Located at the project root.
|
||||
|-------|----------|-------------|
|
||||
| `name` | yes | Project name |
|
||||
| `version` | yes | Semantic version |
|
||||
| `type` | yes | `app`, `lib`, `none` |
|
||||
| `type` | no | `app`, `lib`, `none` (defaults to `"none"`) |
|
||||
| `license` | no | SPDX license identifier |
|
||||
| `author` | no | Author |
|
||||
|
||||
@@ -62,31 +65,31 @@ author = "John Doe"
|
||||
|
||||
| Field | Default | Description |
|
||||
|-------|---------|-------------|
|
||||
| `language` | `"c"` | `"c"`, `"c++"`, `"cpp"`, `"cxx"`, `"asm"` |
|
||||
| `language` | `"c"` | `"c"`, `"c++"`, `"cpp"`, `"cxx"`, `"asm"`, `"llvm_ir"`, `"llvm-ir"`, `"ll"` (optional, defaults to `"c"`) |
|
||||
| `standard` | `"c11"` | C standard (`c11`, `c17`, `c23`) |
|
||||
| `cxx_standard` | `"c++20"` | C++ standard (`c++17`, `c++20`, `c++23`) |
|
||||
| `compiler` | `"clang"` | Preferred compiler |
|
||||
| `kind` | `"bin"` | `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom` |
|
||||
| `cxx_standard` | — | C++ standard (`c++17`, `c++20`, `c++23`) |
|
||||
| `compiler` | `"clang"` | Preferred compiler (optional, defaults to `"clang"`) |
|
||||
| `kind` | `"bin"` | `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom`, `flat-bin` |
|
||||
| `target` | host | Target triple for cross-compilation |
|
||||
| `platform` | `"native"` | `native`, `efi` |
|
||||
| `cflags` | `[]` | Additional C/C++/ASM flags |
|
||||
| `ldflags` | `[]` | Additional linker flags |
|
||||
| `filename` | `""` | Custom output file name |
|
||||
| `extension` | `""` | Custom file extension |
|
||||
| `roots` | `["src"]` | Source root directories |
|
||||
| `extension` | `""` | Custom file extension (for `flat-bin`, default is `bin`) |
|
||||
| `roots` | `["src"]` | Source roots: directories and/or individual source/header files |
|
||||
| `exclude` | `[]` | Exclude patterns |
|
||||
| `include` | `[]` | Additional include directories |
|
||||
| `src_disable` | `false` | Disable auto source discovery |
|
||||
| `inherit` | `false` | Inherit build from workspace root |
|
||||
| `clean` | `[]` | Glob patterns for custom clean paths |
|
||||
| `out_dir` | `""` | Custom output directory |
|
||||
| `workspace_only` | `false` | Workspace-only, not built standalone |
|
||||
| `workspace_only` | `false` | Workspace-only, not built standalone (no `language`/`compiler` required) |
|
||||
| `freestanding` | `false` | Compile in freestanding mode (`-ffreestanding` + `-nostdlib -static`) |
|
||||
| `opt_level` | `"0"` (debug) / `"3"` (release) | Optimization level: `0`-`3`, `"s"`, `"z"` |
|
||||
| `debug` | `true` (debug) / `false` (release) | Emit debug symbols (`-g`) |
|
||||
| `opt_level` | — | Optimization level: `0`-`3`, `"s"`, `"z"` (derived from profile if omitted) |
|
||||
| `debug` | profile-based | Emit debug symbols (`-g`): `true` in debug, `false` in release |
|
||||
| `lto` | `false` | Link-time optimization (`-flto` for both compiler and linker) |
|
||||
| `strip` | `false` | Strip symbols from output (`-s` in ldflags) |
|
||||
| `warnings` | `["all", "extra"]` (debug) / `[]` (release) | Warning flags (e.g. `"all"`, `"extra"`, `"pedantic"`) |
|
||||
| `warnings` | `[]` | Warning flags (e.g. `"all"`, `"extra"`, `"pedantic"`); engine adds `-Wall -Wextra` in debug if empty |
|
||||
| `panic` | `""` | Panic strategy: `"abort"` disables exceptions and unwind tables |
|
||||
| `codegen-units` | `""` | Max parallel compilation jobs (`"0"` = auto) |
|
||||
| `qt` | `false` | Enable automatic Qt meta-object handling (MOC, UIC, RCC) |
|
||||
@@ -96,6 +99,33 @@ Settings from raw config (not in typed struct):
|
||||
- `ldscript` — linker script path
|
||||
- `build.steps` / `build.post_steps` — codegen steps
|
||||
|
||||
## Per-language overrides: `[build.c]`, `[build.cxx]`, `[build.asm]`, `[build.llvm_ir]`
|
||||
|
||||
Each language can have its own table that overrides the flat `[build]` settings:
|
||||
|
||||
```toml
|
||||
[build]
|
||||
compiler = "clang"
|
||||
standard = "c11"
|
||||
|
||||
[build.c]
|
||||
standard = "c23"
|
||||
compiler = "gcc"
|
||||
|
||||
[build.cxx]
|
||||
standard = "c++23"
|
||||
compiler = "g++"
|
||||
|
||||
[build.asm]
|
||||
compiler = "nasm"
|
||||
flags = ["-felf64"]
|
||||
|
||||
[build.llvm_ir]
|
||||
compiler = "llc"
|
||||
```
|
||||
|
||||
The flat `[build]` acts as fallback; per-language tables take precedence for their language.
|
||||
|
||||
Example:
|
||||
|
||||
```toml
|
||||
@@ -148,3 +178,84 @@ Default `cmd` = `./target/{profile}/{name}` (macOS/Windows) or `./target/<triple
|
||||
## [workspace]
|
||||
|
||||
See [workspaces](/docs/reference/workspaces).
|
||||
|
||||
## [archive]
|
||||
|
||||
Optional post-build step: format a FAT volume and copy built artifacts into a disk image. Runs after a successful package build (and after workspace member builds that define `[archive]`).
|
||||
|
||||
Requires DCR built with the `archive` Cargo feature (`cargo build --features archive`). Release binaries include this feature.
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `output` | yes | Image path relative to project root (`{profile}` allowed) |
|
||||
| `format` | yes | `fat12`, `fat16`, or `fat32` |
|
||||
| `size` | no | Image size: bytes or `K`/`KB`/`M`/`MB`/`G`/`GB` (default `1474560` ≈ 1.44 MiB) |
|
||||
| `offset` | no | Byte offset of the FAT volume inside the image (default `0`) |
|
||||
| `label` | no | Volume label (max 11 chars, default `VOLUME`) |
|
||||
| `bootsector` | no | Path to a 512-byte boot sector written at offset 0 when `offset` is 0 (`{profile}` allowed) |
|
||||
| `layout` | no | List of `{ from, to }` entries (files or globs → path inside the volume) |
|
||||
|
||||
```toml
|
||||
[archive]
|
||||
output = "target/{profile}/disk.img"
|
||||
format = "fat12"
|
||||
size = "1440K"
|
||||
label = "MYOS"
|
||||
bootsector = "target/{profile}/boot.bin"
|
||||
|
||||
[[archive.layout]]
|
||||
from = "target/{profile}/kernel.bin"
|
||||
to = "KERNEL.BIN"
|
||||
|
||||
[[archive.layout]]
|
||||
from = "assets/*"
|
||||
to = "/"
|
||||
```
|
||||
|
||||
Typical pairing with `kind = "flat-bin"` (NASM `-f bin`) for bootloaders and pure-ASM OS images.
|
||||
|
||||
## `flat-bin` (kind)
|
||||
|
||||
Produces a raw binary (default extension `bin`) for boot sectors, kernels, and freestanding payloads.
|
||||
|
||||
### Assemblers
|
||||
|
||||
| Tool | Language / compiler | How flat-bin is produced |
|
||||
|------|---------------------|--------------------------|
|
||||
| NASM | `language = "asm"`, `compiler = "nasm"` | `-f bin` → `<stem>.bin` |
|
||||
| FASM | `compiler = "fasm"` | direct write (use `format binary` in source) |
|
||||
| GAS | `compiler = "as"` / `"gas"` | assemble → `objcopy -O binary` |
|
||||
| MASM | `compiler = "ml"` / `"ml64"` | assemble → `objcopy -O binary` |
|
||||
| LLC | `language = "llvm_ir"`, `compiler = "llc"` | `-filetype=obj` → `objcopy -O binary` |
|
||||
|
||||
```toml
|
||||
[build]
|
||||
language = "asm"
|
||||
compiler = "nasm"
|
||||
kind = "flat-bin"
|
||||
extension = "bin"
|
||||
roots = ["src/boot.asm"]
|
||||
```
|
||||
|
||||
### C / C++
|
||||
|
||||
Compile all sources, link with `-nostdlib -static` (plus your `ldflags` / `ldscript`), then convert the intermediate ELF/PE with `objcopy -O binary` to `<filename|name>.bin`.
|
||||
|
||||
```toml
|
||||
[build]
|
||||
language = "c"
|
||||
compiler = "clang"
|
||||
kind = "flat-bin"
|
||||
freestanding = true
|
||||
filename = "kernel"
|
||||
extension = "bin"
|
||||
ldscript = "linker.ld"
|
||||
ldflags = ["-T", "linker.ld"]
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Multi-file **ASM** packages emit one `<stem>.bin` per source; **C/C++** emit a single project binary.
|
||||
- `objcopy` tools tried in order: `llvm-objcopy`, `objcopy`, `gobjcopy`.
|
||||
- Incompatible with `build.qt = true`.
|
||||
- `dcr run` rejects `flat-bin` (not a host executable).
|
||||
|
||||
@@ -41,24 +41,22 @@ DCR uses a package registry for dependency lookup by name.
|
||||
|
||||
```toml
|
||||
# ~/.dcr/config.toml
|
||||
[[registry]]
|
||||
name = "default"
|
||||
[registry.default]
|
||||
url = "https://index.dcr.pm"
|
||||
priority = 1
|
||||
```
|
||||
|
||||
Registry priority: order in `config.toml`. The `DCR_INDEX_PATH` variable overrides the path to `index.json`.
|
||||
|
||||
## Git dependencies
|
||||
|
||||
DCR clones git repositories via `git2`:
|
||||
Git dependencies are parsed and recorded in `dcr.lock`. DCR supports specifying branch/tag/rev for git sources:
|
||||
|
||||
- `branch` — switch to a branch
|
||||
- `tag` — switch to a tag
|
||||
- `rev` — switch to a specific commit
|
||||
- `features` — feature flags (parsed, but does not affect build)
|
||||
|
||||
Cache path: `~/.dcr/git/<hash>/`.
|
||||
|
||||
## Path dependencies
|
||||
|
||||
Local paths. DCR automatically discovers include and lib directories from the neighbor's `dcr.toml`.
|
||||
@@ -69,6 +67,8 @@ mylib = { path = "/abs/path/to/lib" }
|
||||
mylib = { path = "../relative/path" }
|
||||
```
|
||||
|
||||
*Note: Header resolution automatically checks the packaged `target/include` directory of path dependencies, ensuring consumer packages can find headers from compiled static or dynamic libraries.*
|
||||
|
||||
## dcr.lock
|
||||
|
||||
Dependency lock file. Contains package names and sources.
|
||||
|
||||
@@ -11,17 +11,18 @@ DCR normalizes target triples. Short platform names are expanded to full triples
|
||||
### Linux
|
||||
|
||||
```
|
||||
target/<arch>-unknown-linux-gnu/<profile>/
|
||||
target/<arch>-unknown-linux-<env>/<profile>/
|
||||
```
|
||||
|
||||
Architectures: `x86_64`, `aarch64` (host-detected).
|
||||
Architectures: `x86_64`, `aarch64`, `i686`, `armv7`, `riscv64` (host-detected).
|
||||
Environments: `gnu` (default), `musl`.
|
||||
|
||||
Artifact type: ELF. Extensions: `.so` (sharedlib), `.a` (staticlib).
|
||||
|
||||
### macOS
|
||||
|
||||
```
|
||||
target/<profile>/
|
||||
target/<profile>/ (default) or target/<triple>/<profile>/ (with target)
|
||||
```
|
||||
|
||||
Architectures: `x86_64`, `aarch64` (host-detected).
|
||||
@@ -31,10 +32,11 @@ Extensions: `.dylib` (sharedlib), `.a` (staticlib).
|
||||
### Windows
|
||||
|
||||
```
|
||||
target/<profile>/
|
||||
target/<profile>/ (default) or target/<triple>/<profile>/ (with target)
|
||||
```
|
||||
|
||||
Architectures: `x86_64`, `aarch64`.
|
||||
Environments: `msvc` (default), `gnu` (MinGW).
|
||||
|
||||
Extensions: `.exe` (bin), `.lib` (staticlib), `.dll` (sharedlib), `.efi` (UEFI).
|
||||
|
||||
|
||||
@@ -43,7 +43,9 @@ dcr build # build all packages in dependency order
|
||||
dcr build --workspace app # build only app (dependencies built automatically)
|
||||
```
|
||||
|
||||
When building a workspace, member outputs (libraries) are automatically added to include/lib paths of dependent packages.
|
||||
When building a workspace, DCR automatically injects include and library paths of dependent workspace members:
|
||||
- **Include Paths**: Automatically resolves and injects header directories of dependencies, including the member's `src/` directory, local `include/` directory, and the packaged `target/include` directory.
|
||||
- **Library Paths**: Injects compiled library search paths (`target/lib` as well as target-specific build folders) to allow automatic linking with member libraries.
|
||||
|
||||
## Clean
|
||||
|
||||
@@ -65,3 +67,21 @@ standard = "c17"
|
||||
|
||||
# member inherits language and standard
|
||||
```
|
||||
|
||||
## Workspace-only root (no build of its own)
|
||||
|
||||
A workspace root can set `workspace_only = true` — it won't be built itself, and doesn't need `language` or `compiler`:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "my-workspace"
|
||||
version = "0.1.0"
|
||||
|
||||
[build]
|
||||
workspace_only = true
|
||||
kind = "bin"
|
||||
|
||||
[workspace]
|
||||
lib-core = { path = "lib-core" }
|
||||
app = { path = "app", deps = ["lib-core"] }
|
||||
```
|
||||
|
||||
@@ -8,9 +8,13 @@ dcr-run \- Build and run a DCR project
|
||||
[\fB\-\-force\fR]
|
||||
[\fB\-\-clean\fR]
|
||||
[\fB\-\-verbose\fR]
|
||||
[\fB\-\-\fR \fI<args>...\fR]
|
||||
.SH DESCRIPTION
|
||||
Builds and runs the project. Only available for projects with
|
||||
\fBkind = "bin"\fR.
|
||||
Arguments after a bare
|
||||
.B \-\-
|
||||
are forwarded to the built binary (cargo-style).
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-\-debug
|
||||
@@ -30,6 +34,14 @@ Clean before building.
|
||||
.TP
|
||||
.B \-\-verbose
|
||||
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
|
||||
.BR dcr (1),
|
||||
.BR dcr-build (1)
|
||||
|
||||
@@ -38,11 +38,14 @@ Display the dependency tree of the current project.
|
||||
.B fmt
|
||||
Format all C/C++ source files using clang-format.
|
||||
.TP
|
||||
.B lint [\fI\-\-fix\fR]
|
||||
Run clang-tidy static analysis on C/C++ sources.
|
||||
.TP
|
||||
.B gen \fI<subcommand>\fR
|
||||
Generate IDE integration files (vscode, clion, compile-commands, project-info).
|
||||
.TP
|
||||
.B setup
|
||||
Set up DCR registries for dependency resolution.
|
||||
Show configured package registries.
|
||||
.SH GLOBAL FLAGS
|
||||
.TP
|
||||
.B \-\-help
|
||||
|
||||
@@ -45,7 +45,6 @@ pub fn add(args: &[String]) -> i32 {
|
||||
println!(" Adds a dependency to the project.");
|
||||
println!();
|
||||
printc("SOURCES:", BOLD_GREEN);
|
||||
println!(" user/repo GitHub shorthand");
|
||||
println!(" github:user/repo GitHub repository");
|
||||
println!(" gitlab:user/repo GitLab repository");
|
||||
println!(" git:host.com/user/repo Generic git repository");
|
||||
@@ -140,7 +139,6 @@ fn parse_add_args(args: &[String]) -> Result<AddArgs, i32> {
|
||||
if args.is_empty() {
|
||||
error("Usage: dcr add <name> <source> [--branch <branch> | --tag <tag> | --rev <rev>]");
|
||||
error("Sources:");
|
||||
error(" user/repo -> github.com/user/repo");
|
||||
error(" github:user/repo -> github.com/user/repo");
|
||||
error(" gitlab:user/repo -> gitlab.com/user/repo");
|
||||
error(" git:host.com/user/repo -> host.com/user/repo");
|
||||
|
||||
1704
src/cli/build.rs
1704
src/cli/build.rs
File diff suppressed because it is too large
Load Diff
@@ -125,17 +125,27 @@ fn parse_clean_flags(args: &[String]) -> Result<CleanFlags, String> {
|
||||
fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
|
||||
let config = Config::open("./dcr.toml").map_err(|err| err.to_string())?;
|
||||
|
||||
let target = flags
|
||||
.target
|
||||
.clone()
|
||||
.or_else(|| {
|
||||
config
|
||||
.get("build.target")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
})
|
||||
.or_else(|| Some(default_target_triple()));
|
||||
let target = flags.target.clone().or_else(|| {
|
||||
config
|
||||
.get("build.target")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
});
|
||||
let target = match target {
|
||||
Some(t) => Some(crate::utils::build::normalize_target_os(&t).to_string()),
|
||||
None if cfg!(target_os = "linux")
|
||||
|| cfg!(any(
|
||||
target_os = "freebsd",
|
||||
target_os = "openbsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "dragonfly"
|
||||
)) =>
|
||||
{
|
||||
Some(default_target_triple())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
if flags.all
|
||||
&& let Some(workspace) = parse_workspace(
|
||||
@@ -146,7 +156,10 @@ fn clean_from_root(root: &Path, flags: &CleanFlags) -> Result<(), String> {
|
||||
)?
|
||||
{
|
||||
for member in &workspace.members {
|
||||
clean_project_at(&member.path, flags.profile.as_deref(), target.as_deref())?;
|
||||
let member_target = member.path.join("target");
|
||||
if member_target.is_dir() {
|
||||
clean_project_at(&member.path, flags.profile.as_deref(), target.as_deref())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
clean_project_at(root, flags.profile.as_deref(), target.as_deref())
|
||||
|
||||
@@ -25,16 +25,25 @@ pub struct BuildRunFlags {
|
||||
pub force: bool,
|
||||
pub clean: bool,
|
||||
pub verbose: 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> {
|
||||
let (dcr_args, bin_args_slice) = split_double_dash(args);
|
||||
let mut profile = PROFILE.to_string();
|
||||
let mut target = None;
|
||||
let mut workspace = None;
|
||||
let mut force = false;
|
||||
let mut clean = false;
|
||||
let mut verbose = false;
|
||||
let mut iter = args.iter();
|
||||
let mut iter = dcr_args.iter();
|
||||
|
||||
while let Some(arg) = iter.next() {
|
||||
if !arg.starts_with("--") {
|
||||
@@ -91,5 +100,38 @@ pub fn parse_build_run_flags(args: &[String]) -> Result<BuildRunFlags, i32> {
|
||||
force,
|
||||
clean,
|
||||
verbose,
|
||||
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"]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::builder::collect_sources;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build_config::Config;
|
||||
use crate::core::builder::BuildContext;
|
||||
use crate::core::builder::collect_sources;
|
||||
use crate::core::builder::common;
|
||||
use crate::core::workspace::parse_workspace;
|
||||
use crate::utils::build::{
|
||||
get_bool_with_profile, get_config_opt, get_config_str, get_language_with_profile_or_default,
|
||||
|
||||
@@ -29,9 +29,12 @@ pub fn help() -> i32 {
|
||||
println!(" init Initialize the current directory as a project");
|
||||
println!(" build [--profile] Build the project (default: --debug)");
|
||||
println!(" run [--profile] Build and run the project (default: --debug)");
|
||||
println!(" add <name> ... Add a dependency to dcr.toml");
|
||||
println!(" tree Display the dependency tree");
|
||||
println!(" test Run project tests (alias: tests)");
|
||||
println!(" fmt Format all C/C++ source files using clang-format");
|
||||
println!(" lint Run clang-tidy on C/C++ sources");
|
||||
println!(" setup Show configured package registries");
|
||||
println!(" clean Remove the target directory");
|
||||
println!(" gen <subcommand> Generate IDE integration files");
|
||||
printc("FLAGS:", BOLD_GREEN);
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::common;
|
||||
use crate::core::build_config::Config;
|
||||
use crate::core::builder::common;
|
||||
use crate::utils::build::{get_bool_with_profile, get_list_with_profile};
|
||||
use crate::utils::fs::find_project_root;
|
||||
use crate::utils::log::error;
|
||||
|
||||
@@ -85,7 +85,7 @@ pub fn new(args: &[String]) -> i32 {
|
||||
"Directory `{}` already exists\n",
|
||||
colored(project_name, BOLD_CYAN)
|
||||
));
|
||||
printc("Подсказка:", BOLD_CYAN);
|
||||
printc("Hint:", BOLD_CYAN);
|
||||
println!(
|
||||
" Use `{}` to initialize an existing project\n or specify a different project name",
|
||||
colored("dcr init", BOLD_CYAN)
|
||||
|
||||
153
src/cli/run.rs
153
src/cli/run.rs
@@ -19,9 +19,7 @@ use crate::cli::build::build;
|
||||
use crate::cli::flags::parse_build_run_flags;
|
||||
use crate::core::build_config::Config;
|
||||
use crate::core::runner::run_binary;
|
||||
use crate::utils::build::{
|
||||
default_target_triple, normalize_target_os, parse_version_info, substitute_vars,
|
||||
};
|
||||
use crate::utils::build::{normalize_target_os, parse_version_info, substitute_vars};
|
||||
use crate::utils::fs::find_project_root;
|
||||
use crate::utils::fs::with_dir;
|
||||
use crate::utils::log::error;
|
||||
@@ -61,12 +59,13 @@ pub fn run(args: &[String]) -> i32 {
|
||||
if args.first().is_some_and(|a| a == "--help") {
|
||||
printc("USAGE:", BOLD_GREEN);
|
||||
printc(
|
||||
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose]",
|
||||
" dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]",
|
||||
BOLD_CYAN,
|
||||
);
|
||||
println!();
|
||||
printc("DESCRIPTION:", BOLD_GREEN);
|
||||
println!(" Builds and runs the project. Only available for kind = \"bin\".");
|
||||
println!(" Arguments after `--` are passed to the built binary (cargo-style).");
|
||||
println!();
|
||||
printc("OPTIONS:", BOLD_GREEN);
|
||||
println!(" --debug Run with debug profile (default)");
|
||||
@@ -75,6 +74,7 @@ pub fn run(args: &[String]) -> i32 {
|
||||
println!(" --force Force a full rebuild");
|
||||
println!(" --clean Clean before building");
|
||||
println!(" --verbose Print detailed build output");
|
||||
println!(" -- <args>... Arguments forwarded to the binary");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -110,8 +110,21 @@ pub fn run(args: &[String]) -> i32 {
|
||||
Err(_) => return 1,
|
||||
};
|
||||
|
||||
// Handle workspace-only root: delegate to workspace member
|
||||
if config.is_workspace_only() {
|
||||
if let Some(cmd) = get_run_cmd(&config, &flags.profile, flags.target.as_deref(), "") {
|
||||
let build_status = build(&args_for_build(&flags));
|
||||
if build_status == 0 {
|
||||
let display = display_run_cmd(&cmd, &flags.bin_args);
|
||||
println!(
|
||||
" {} {}",
|
||||
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
||||
display
|
||||
);
|
||||
return run_shell_with_args(&cmd, &flags.bin_args);
|
||||
}
|
||||
return build_status;
|
||||
}
|
||||
|
||||
let ws = match crate::core::workspace::parse_workspace(
|
||||
&config,
|
||||
&flags.profile,
|
||||
@@ -177,10 +190,13 @@ fn run_project(
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
// If no target specified, use default host target for target-specific config
|
||||
let mut target = flags.target.clone();
|
||||
if target.is_none() {
|
||||
target = Some(default_target_triple());
|
||||
let bt =
|
||||
crate::cli::build::get_build_string_with_profile(&config, "target", &flags.profile);
|
||||
if !bt.is_empty() {
|
||||
target = Some(bt);
|
||||
}
|
||||
}
|
||||
|
||||
let build_kind = config
|
||||
@@ -213,12 +229,23 @@ fn run_project(
|
||||
|
||||
let kind = build_kind.trim();
|
||||
if run_cmd.is_none()
|
||||
&& (kind == "staticlib" || kind == "sharedlib" || kind == "efi" || kind == "elf")
|
||||
&& (kind == "staticlib"
|
||||
|| kind == "sharedlib"
|
||||
|| kind == "efi"
|
||||
|| kind == "elf"
|
||||
|| kind == "flat-bin")
|
||||
{
|
||||
return Err("Cannot run library build".to_string());
|
||||
}
|
||||
|
||||
let build_status = build(&args_for_build(flags));
|
||||
let build_status = if let Some(wroot) = workspace_root {
|
||||
with_dir(wroot, || {
|
||||
Ok(build(&args_for_workspace_build(flags, project_name)))
|
||||
})
|
||||
.unwrap_or(1)
|
||||
} else {
|
||||
build(&args_for_build(flags))
|
||||
};
|
||||
let bin_path = crate::platform::bin_path(
|
||||
&flags.profile,
|
||||
project_name,
|
||||
@@ -226,26 +253,36 @@ fn run_project(
|
||||
);
|
||||
if build_status == 0 {
|
||||
if let Some(cmd) = run_cmd {
|
||||
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd);
|
||||
println!("--------------------------------");
|
||||
return Ok(run_shell(&cmd));
|
||||
let display = display_run_cmd(&cmd, &flags.bin_args);
|
||||
println!(
|
||||
" {} {}",
|
||||
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
||||
display
|
||||
);
|
||||
return Ok(run_shell_with_args(&cmd, &flags.bin_args));
|
||||
}
|
||||
println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path);
|
||||
println!("--------------------------------");
|
||||
let display = display_bin_run(&bin_path, &flags.bin_args);
|
||||
println!(
|
||||
" {} {}",
|
||||
colored(&format!("{:<9}", "run"), BOLD_GREEN),
|
||||
display
|
||||
);
|
||||
return Ok(run_binary(
|
||||
project_name,
|
||||
&flags.profile,
|
||||
normalized_target_dir.as_deref(),
|
||||
&flags.bin_args,
|
||||
));
|
||||
}
|
||||
|
||||
let fallback_code = if let Some(cmd) = run_cmd {
|
||||
run_shell(&cmd)
|
||||
run_shell_with_args(&cmd, &flags.bin_args)
|
||||
} else {
|
||||
run_binary(
|
||||
project_name,
|
||||
&flags.profile,
|
||||
normalized_target_dir.as_deref(),
|
||||
&flags.bin_args,
|
||||
)
|
||||
};
|
||||
if fallback_code != 1 {
|
||||
@@ -266,6 +303,36 @@ fn args_for_build(flags: &crate::cli::flags::BuildRunFlags) -> Vec<String> {
|
||||
args.push("--workspace".to_string());
|
||||
args.push(name.clone());
|
||||
}
|
||||
if flags.force {
|
||||
args.push("--force".to_string());
|
||||
}
|
||||
if flags.clean {
|
||||
args.push("--clean".to_string());
|
||||
}
|
||||
if flags.verbose {
|
||||
args.push("--verbose".to_string());
|
||||
}
|
||||
args
|
||||
}
|
||||
|
||||
fn args_for_workspace_build(
|
||||
flags: &crate::cli::flags::BuildRunFlags,
|
||||
member_name: &str,
|
||||
) -> Vec<String> {
|
||||
let mut args = Vec::new();
|
||||
args.push(format!("--{}", flags.profile));
|
||||
if let Some(ref target) = flags.target {
|
||||
args.push("--target".to_string());
|
||||
args.push(target.clone());
|
||||
}
|
||||
args.push("--workspace".to_string());
|
||||
args.push(member_name.to_string());
|
||||
if flags.force {
|
||||
args.push("--force".to_string());
|
||||
}
|
||||
if flags.clean {
|
||||
args.push("--clean".to_string());
|
||||
}
|
||||
if flags.verbose {
|
||||
args.push("--verbose".to_string());
|
||||
}
|
||||
@@ -285,6 +352,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 {
|
||||
let info = parse_version_info(version);
|
||||
substitute_vars(cmd, &info, profile, "")
|
||||
|
||||
272
src/core/build/archive.rs
Normal file
272
src/core/build/archive.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
// DCR — Cargo-like C/C++ project manager.
|
||||
//
|
||||
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build_config::ArchiveConfig;
|
||||
use fatfs::{FileSystem, FormatVolumeOptions, format_volume};
|
||||
use glob::glob;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||
use std::path::Path;
|
||||
|
||||
struct OffsetFile<T> {
|
||||
inner: T,
|
||||
offset: u64,
|
||||
}
|
||||
|
||||
impl<T: Read> Read for OffsetFile<T> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.inner.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Write> Write for OffsetFile<T> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.inner.write(buf)
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Seek> Seek for OffsetFile<T> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
match pos {
|
||||
SeekFrom::Start(n) => {
|
||||
let actual = self.inner.seek(SeekFrom::Start(n + self.offset))?;
|
||||
if actual < self.offset {
|
||||
Ok(0)
|
||||
} else {
|
||||
Ok(actual - self.offset)
|
||||
}
|
||||
}
|
||||
SeekFrom::Current(n) => {
|
||||
let actual = self.inner.seek(SeekFrom::Current(n))?;
|
||||
if actual < self.offset {
|
||||
Ok(0)
|
||||
} else {
|
||||
Ok(actual - self.offset)
|
||||
}
|
||||
}
|
||||
SeekFrom::End(n) => {
|
||||
let actual = self.inner.seek(SeekFrom::End(n))?;
|
||||
if actual < self.offset {
|
||||
Ok(0)
|
||||
} else {
|
||||
Ok(actual - self.offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_size(s: &str) -> Result<u64, String> {
|
||||
let s = s.trim();
|
||||
if s.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
let mut num_str = String::new();
|
||||
let mut unit = String::new();
|
||||
for c in s.chars() {
|
||||
if c.is_ascii_digit() || c == '.' {
|
||||
num_str.push(c);
|
||||
} else {
|
||||
unit.push(c);
|
||||
}
|
||||
}
|
||||
let val: f64 = num_str
|
||||
.parse()
|
||||
.map_err(|_| format!("invalid size number: {num_str}"))?;
|
||||
let unit = unit.trim().to_lowercase();
|
||||
let multiplier = match unit.as_str() {
|
||||
"k" | "kb" => 1024.0,
|
||||
"m" | "mb" => 1024.0 * 1024.0,
|
||||
"g" | "gb" => 1024.0 * 1024.0 * 1024.0,
|
||||
"" => 1.0,
|
||||
_ => return Err(format!("unknown unit: {unit}")),
|
||||
};
|
||||
Ok((val * multiplier) as u64)
|
||||
}
|
||||
|
||||
pub fn pack_archive(
|
||||
project_root: &Path,
|
||||
config: &ArchiveConfig,
|
||||
profile: &str,
|
||||
) -> Result<(), String> {
|
||||
let output_substituted = config.output.replace("{profile}", profile);
|
||||
let output_path = project_root.join(&output_substituted);
|
||||
if let Some(parent) = output_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("failed to create output parent dir: {e}"))?;
|
||||
}
|
||||
|
||||
let size = if let Some(sz_str) = &config.size {
|
||||
parse_size(sz_str)?
|
||||
} else {
|
||||
1474560
|
||||
};
|
||||
|
||||
let offset = if let Some(off_str) = &config.offset {
|
||||
parse_size(off_str)?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(&output_path)
|
||||
.map_err(|e| format!("failed to open output file: {e}"))?;
|
||||
|
||||
let current_len = file.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
if current_len < size {
|
||||
file.set_len(size)
|
||||
.map_err(|e| format!("failed to set image file size: {e}"))?;
|
||||
}
|
||||
|
||||
if offset == 0
|
||||
&& let Some(boot_rel) = &config.bootsector
|
||||
{
|
||||
let boot_rel_substituted = boot_rel.replace("{profile}", profile);
|
||||
let boot_path = project_root.join(&boot_rel_substituted);
|
||||
if boot_path.is_file() {
|
||||
let mut boot_file =
|
||||
File::open(&boot_path).map_err(|e| format!("failed to open bootsector: {e}"))?;
|
||||
let mut boot_bytes = vec![0; 512];
|
||||
let _ = boot_file
|
||||
.read(&mut boot_bytes)
|
||||
.map_err(|e| format!("failed to read bootsector: {e}"))?;
|
||||
file.seek(SeekFrom::Start(0))
|
||||
.map_err(|e| format!("failed to seek to start: {e}"))?;
|
||||
file.write_all(&boot_bytes)
|
||||
.map_err(|e| format!("failed to write bootsector: {e}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
file.seek(SeekFrom::Start(0))
|
||||
.map_err(|e| format!("failed to seek to start: {e}"))?;
|
||||
|
||||
let mut wrapper = OffsetFile {
|
||||
inner: file,
|
||||
offset,
|
||||
};
|
||||
|
||||
let label = config.label.as_deref().unwrap_or("VOLUME");
|
||||
let mut label_bytes = [b' '; 11];
|
||||
let bytes_to_copy = label.len().min(11);
|
||||
label_bytes[..bytes_to_copy].copy_from_slice(&label.as_bytes()[..bytes_to_copy]);
|
||||
|
||||
let fat_type = match config.format.to_lowercase().as_str() {
|
||||
"fat12" => Some(fatfs::FatType::Fat12),
|
||||
"fat16" => Some(fatfs::FatType::Fat16),
|
||||
"fat32" => Some(fatfs::FatType::Fat32),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mut options = FormatVolumeOptions::new().volume_label(label_bytes);
|
||||
if let Some(t) = fat_type {
|
||||
options = options.fat_type(t);
|
||||
}
|
||||
|
||||
format_volume(&mut wrapper, options)
|
||||
.map_err(|e| format!("failed to format FAT volume: {e}"))?;
|
||||
|
||||
wrapper
|
||||
.seek(SeekFrom::Start(0))
|
||||
.map_err(|e| format!("failed to seek to start after format: {e}"))?;
|
||||
|
||||
let fs = FileSystem::new(wrapper, fatfs::FsOptions::new())
|
||||
.map_err(|e| format!("failed to mount FAT fs: {e}"))?;
|
||||
let root_dir = fs.root_dir();
|
||||
|
||||
for layout in &config.layout {
|
||||
let from_substituted = layout.from.replace("{profile}", profile);
|
||||
let from_pattern = project_root
|
||||
.join(&from_substituted)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let paths = if from_pattern.contains('*') || from_pattern.contains('?') {
|
||||
let mut matched = Vec::new();
|
||||
if let Ok(entries) = glob(&from_pattern) {
|
||||
for path in entries.flatten() {
|
||||
matched.push(path);
|
||||
}
|
||||
}
|
||||
matched
|
||||
} else {
|
||||
let p = project_root.join(&from_substituted);
|
||||
if p.exists() { vec![p] } else { Vec::new() }
|
||||
};
|
||||
|
||||
for path in paths {
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
if filename.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut target_filename = filename.to_string();
|
||||
let mut target_dir_str = layout.to.trim_matches('/');
|
||||
|
||||
let is_glob = from_pattern.contains('*') || from_pattern.contains('?');
|
||||
if !is_glob
|
||||
&& !layout.to.ends_with('/')
|
||||
&& let Some(last_part) = Path::new(&layout.to).file_name().and_then(|n| n.to_str())
|
||||
&& last_part.contains('.')
|
||||
{
|
||||
target_filename = last_part.to_string();
|
||||
let parent_path = Path::new(&layout.to).parent().unwrap_or(Path::new(""));
|
||||
target_dir_str = parent_path.to_str().unwrap_or("").trim_matches('/');
|
||||
}
|
||||
|
||||
let mut current_dir = root_dir.clone();
|
||||
if !target_dir_str.is_empty() {
|
||||
let parts = target_dir_str.split('/');
|
||||
for part in parts {
|
||||
if part.is_empty() {
|
||||
continue;
|
||||
}
|
||||
current_dir = match current_dir.open_dir(part) {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => current_dir
|
||||
.create_dir(part)
|
||||
.map_err(|e| format!("failed to create dir {part}: {e}"))?,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let mut out_file = current_dir
|
||||
.create_file(&target_filename)
|
||||
.map_err(|e| format!("failed to create file {target_filename}: {e}"))?;
|
||||
let mut in_file =
|
||||
File::open(&path).map_err(|e| format!("failed to open input file: {e}"))?;
|
||||
let mut buffer = Vec::new();
|
||||
in_file
|
||||
.read_to_end(&mut buffer)
|
||||
.map_err(|e| format!("failed to read input file: {e}"))?;
|
||||
out_file
|
||||
.write_all(&buffer)
|
||||
.map_err(|e| format!("failed to write to FAT file {target_filename}: {e}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
284
src/core/build/builder/artifact.rs
Normal file
284
src/core/build/builder/artifact.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
// DCR — Cargo-like C/C++ project manager.
|
||||
//
|
||||
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::platform;
|
||||
use crate::utils::build::is_bare_metal_target;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::Instant;
|
||||
|
||||
pub(crate) fn artifact_dir(ctx: &BuildContext) -> PathBuf {
|
||||
match ctx.target_dir {
|
||||
Some(dir) => Path::new(dir).to_path_buf(),
|
||||
None => Path::new("./target").join(ctx.profile),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn flat_output_path(ctx: &BuildContext) -> String {
|
||||
let name = ctx.output_filename.unwrap_or(ctx.project_name);
|
||||
let ext = ctx.output_extension.unwrap_or("bin");
|
||||
let file = if ext.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{name}.{ext}")
|
||||
};
|
||||
artifact_dir(ctx).join(file).to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn flat_source_output_path(ctx: &BuildContext, source: &str) -> String {
|
||||
let stem = Path::new(source)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("out");
|
||||
let ext = ctx.output_extension.unwrap_or("bin");
|
||||
let file = if ext.is_empty() {
|
||||
stem.to_string()
|
||||
} else {
|
||||
format!("{stem}.{ext}")
|
||||
};
|
||||
artifact_dir(ctx).join(file).to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
fn resolve_objcopy() -> Result<String, String> {
|
||||
for candidate in ["llvm-objcopy", "objcopy", "gobjcopy"] {
|
||||
if Command::new(candidate)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(candidate.to_string());
|
||||
}
|
||||
}
|
||||
Err(
|
||||
"flat-bin requires llvm-objcopy, objcopy, or gobjcopy in PATH \
|
||||
(install binutils or LLVM tools)"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn objcopy_binary(ctx: &BuildContext, input: &str, output: &str) -> Result<(), String> {
|
||||
if let Some(parent) = Path::new(output).parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|e| format!("output dir error: {e}"))?;
|
||||
}
|
||||
if !common::needs_rebuild(input, output) {
|
||||
return Ok(());
|
||||
}
|
||||
let tool = resolve_objcopy()?;
|
||||
let mut cmd = Command::new(&tool);
|
||||
cmd.arg("-O").arg("binary").arg(input).arg(output);
|
||||
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
|
||||
eprintln!("[dcr] {:?}", cmd);
|
||||
}
|
||||
common::run_command_sync_output(&mut cmd)
|
||||
}
|
||||
|
||||
pub(crate) fn link_flat_binary(
|
||||
ctx: &BuildContext,
|
||||
objects: &[String],
|
||||
default_linker: &str,
|
||||
start_time: Instant,
|
||||
) -> Result<f64, String> {
|
||||
let out_path = flat_output_path(ctx);
|
||||
if !common::needs_link(objects, &out_path) {
|
||||
return Ok(common::elapsed_secs(start_time));
|
||||
}
|
||||
|
||||
let intermediate = {
|
||||
let dir = match ctx.target_dir {
|
||||
Some(dir) => Path::new(dir).join("obj"),
|
||||
None => Path::new("./target").join(ctx.profile).join("obj"),
|
||||
};
|
||||
std::fs::create_dir_all(&dir).map_err(|e| format!("obj dir error: {e}"))?;
|
||||
dir.join(format!("{}.flat.elf", ctx.project_name))
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(ctx.linker.unwrap_or(default_linker));
|
||||
cmd.arg("-nostdlib").arg("-static");
|
||||
for obj in objects {
|
||||
cmd.arg(obj);
|
||||
}
|
||||
for dir in ctx.lib_dirs {
|
||||
cmd.arg(format!("-L{dir}"));
|
||||
}
|
||||
for lib in ctx.libs {
|
||||
cmd.arg(format!("-l{lib}"));
|
||||
}
|
||||
for flag in ctx.ldflags {
|
||||
cmd.arg(flag);
|
||||
}
|
||||
cmd.arg("-o").arg(&intermediate);
|
||||
|
||||
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
|
||||
eprintln!("[dcr] {:?}", cmd);
|
||||
}
|
||||
let program = cmd.get_program().to_string_lossy().into_owned();
|
||||
let output = cmd.output().map_err(|err| {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
format!("linker not found: {program} (check [toolchain].ld / PATH)")
|
||||
} else {
|
||||
format!("flat-bin link failed: {err}")
|
||||
}
|
||||
})?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
return Err(format!(
|
||||
"flat-bin link failed:\nstdout: {stdout}\nstderr: {stderr}"
|
||||
));
|
||||
}
|
||||
|
||||
objcopy_binary(ctx, &intermediate, &out_path)?;
|
||||
Ok(common::elapsed_secs(start_time))
|
||||
}
|
||||
|
||||
pub(crate) fn archive_static(
|
||||
ctx: &BuildContext,
|
||||
objects: &[String],
|
||||
start_time: Instant,
|
||||
) -> Result<f64, String> {
|
||||
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
|
||||
if !common::needs_link(objects, &lib_path) {
|
||||
return Ok(common::elapsed_secs(start_time));
|
||||
}
|
||||
|
||||
let archiver = ctx
|
||||
.archiver
|
||||
.unwrap_or_else(|| default_archiver(ctx.compiler));
|
||||
|
||||
let mut cmd = Command::new(archiver);
|
||||
if cfg!(target_os = "windows") && is_msvc_lib(archiver) {
|
||||
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
|
||||
} else {
|
||||
cmd.arg("rcs").arg(&lib_path);
|
||||
}
|
||||
for obj in objects {
|
||||
cmd.arg(obj);
|
||||
}
|
||||
run(ctx, cmd, start_time)
|
||||
}
|
||||
|
||||
pub(crate) fn link_binary(
|
||||
ctx: &BuildContext,
|
||||
objects: &[String],
|
||||
default_linker: &str,
|
||||
start_time: Instant,
|
||||
) -> Result<f64, String> {
|
||||
if crate::utils::build::is_flat_bin(ctx.kind) {
|
||||
return link_flat_binary(ctx, objects, default_linker, start_time);
|
||||
}
|
||||
|
||||
let mut cmd = Command::new(ctx.linker.unwrap_or(default_linker));
|
||||
if ctx.kind == "sharedlib" {
|
||||
if cfg!(target_os = "macos") {
|
||||
cmd.arg("-dynamiclib");
|
||||
} else {
|
||||
cmd.arg("-shared");
|
||||
}
|
||||
}
|
||||
if ctx.kind == "efi" {
|
||||
cmd.arg("-shared");
|
||||
cmd.arg("-nostdlib");
|
||||
cmd.arg("-Wl,-dll");
|
||||
cmd.arg("-Wl,--subsystem,10");
|
||||
} else if ctx.freestanding || is_bare_metal_target(ctx.target) {
|
||||
cmd.arg("-nostdlib");
|
||||
cmd.arg("-static");
|
||||
}
|
||||
for obj in objects {
|
||||
cmd.arg(obj);
|
||||
}
|
||||
for dir in ctx.lib_dirs {
|
||||
cmd.arg(format!("-L{dir}"));
|
||||
}
|
||||
for lib in ctx.libs {
|
||||
cmd.arg(format!("-l{lib}"));
|
||||
}
|
||||
for flag in ctx.ldflags {
|
||||
cmd.arg(flag);
|
||||
}
|
||||
|
||||
let name = ctx.output_filename.unwrap_or(ctx.project_name);
|
||||
let ext = ctx.output_extension.unwrap_or("");
|
||||
let final_name = if ext.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{}.{}", name, ext)
|
||||
};
|
||||
|
||||
let out_path = if ctx.kind == "sharedlib" {
|
||||
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
|
||||
} else if ctx.kind == "efi" {
|
||||
platform::efi_path(ctx.profile, &final_name, ctx.target_dir)
|
||||
} else if ctx.kind == "elf" {
|
||||
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
|
||||
} else {
|
||||
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
|
||||
};
|
||||
|
||||
if !common::needs_link(objects, &out_path) {
|
||||
return Ok(common::elapsed_secs(start_time));
|
||||
}
|
||||
cmd.arg("-o").arg(out_path);
|
||||
run(ctx, cmd, start_time)
|
||||
}
|
||||
|
||||
fn default_archiver(compiler: &str) -> &'static str {
|
||||
let c = compiler.to_lowercase();
|
||||
if cfg!(target_os = "windows") {
|
||||
if c.contains("cl") && !c.contains("clang") {
|
||||
"lib"
|
||||
} else if c.contains("clang") {
|
||||
"llvm-ar"
|
||||
} else {
|
||||
"ar"
|
||||
}
|
||||
} else {
|
||||
"ar"
|
||||
}
|
||||
}
|
||||
|
||||
fn is_msvc_lib(archiver: &str) -> bool {
|
||||
let a = archiver.to_lowercase();
|
||||
a == "lib" || a == "lib.exe" || a.ends_with("\\lib.exe") || a.ends_with("/lib.exe")
|
||||
}
|
||||
|
||||
fn run(ctx: &BuildContext, mut cmd: Command, start_time: Instant) -> Result<f64, String> {
|
||||
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
|
||||
eprintln!("[dcr] {:?}", cmd);
|
||||
}
|
||||
let program = cmd.get_program().to_string_lossy().into_owned();
|
||||
let output = cmd.output().map_err(|err| {
|
||||
let msg = err.to_string();
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
format!("linker not found: {program} (check [toolchain].ld / PATH)")
|
||||
} else {
|
||||
format!("Build failed: {msg}")
|
||||
}
|
||||
})?;
|
||||
if output.status.success() {
|
||||
Ok(common::elapsed_secs(start_time))
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
Err(format!("Build failed:\nstdout: {stdout}\nstderr: {stderr}"))
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::builder::BuildContext;
|
||||
use crate::core::builder::common;
|
||||
use crate::platform;
|
||||
use crate::utils::build::is_bare_metal_target;
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::builder::artifact;
|
||||
use crate::core::build::common;
|
||||
use crate::utils::build::{is_bare_metal_target, is_compile_only, is_flat_bin};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
@@ -30,13 +30,16 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
} else {
|
||||
ctx.compiler
|
||||
};
|
||||
if is_flat_bin(ctx.kind) && ctx.qt {
|
||||
return Err("flat-bin is not supported with build.qt = true".to_string());
|
||||
}
|
||||
let start_time = Instant::now();
|
||||
let obj_dir = match ctx.target_dir {
|
||||
Some(dir) => Path::new(dir).join("obj"),
|
||||
None => Path::new("./target").join(ctx.profile).join("obj"),
|
||||
};
|
||||
let qt_include_path = if ctx.qt {
|
||||
crate::core::builder::qt::process_qt(ctx, &obj_dir)?
|
||||
crate::core::build::language::cxx::qt::process_qt(ctx, &obj_dir)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -55,94 +58,15 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
)?;
|
||||
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o", qt_include_path)?;
|
||||
|
||||
if is_compile_only(ctx.kind) && !is_flat_bin(ctx.kind) {
|
||||
return Ok(common::elapsed_secs(start_time));
|
||||
}
|
||||
|
||||
if ctx.kind == "staticlib" {
|
||||
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
|
||||
if !common::needs_link(&objects, &lib_path) {
|
||||
let elapsed = common::elapsed_secs(start_time);
|
||||
return Ok(elapsed);
|
||||
}
|
||||
let mut cmd = Command::new(ctx.archiver.unwrap_or("ar"));
|
||||
cmd.arg("rcs").arg(&lib_path);
|
||||
for obj in &objects {
|
||||
cmd.arg(obj);
|
||||
}
|
||||
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
|
||||
eprintln!("[dcr] {:?}", cmd);
|
||||
}
|
||||
match cmd.status() {
|
||||
Ok(status) if status.success() => {
|
||||
let elapsed = common::elapsed_secs(start_time);
|
||||
return Ok(elapsed);
|
||||
}
|
||||
Ok(_) => return Err("Build failed".to_string()),
|
||||
Err(err) => return Err(format!("Build failed: {err}")),
|
||||
}
|
||||
return artifact::archive_static(ctx, &objects, start_time);
|
||||
}
|
||||
|
||||
let mut cmd = Command::new(ctx.linker.unwrap_or(compiler));
|
||||
if ctx.kind == "sharedlib" {
|
||||
if cfg!(target_os = "macos") {
|
||||
cmd.arg("-dynamiclib");
|
||||
} else {
|
||||
cmd.arg("-shared");
|
||||
}
|
||||
}
|
||||
if ctx.kind == "efi" {
|
||||
cmd.arg("-shared");
|
||||
cmd.arg("-nostdlib");
|
||||
cmd.arg("-Wl,-dll");
|
||||
cmd.arg("-Wl,--subsystem,10");
|
||||
} else if ctx.freestanding || is_bare_metal_target(ctx.target) {
|
||||
cmd.arg("-nostdlib");
|
||||
cmd.arg("-static");
|
||||
}
|
||||
for obj in &objects {
|
||||
cmd.arg(obj);
|
||||
}
|
||||
for dir in ctx.lib_dirs {
|
||||
cmd.arg(format!("-L{dir}"));
|
||||
}
|
||||
for lib in ctx.libs {
|
||||
cmd.arg(format!("-l{lib}"));
|
||||
}
|
||||
for flag in ctx.ldflags {
|
||||
cmd.arg(flag);
|
||||
}
|
||||
let name = ctx.output_filename.unwrap_or(ctx.project_name);
|
||||
let ext = ctx.output_extension.unwrap_or("");
|
||||
let final_name = if ext.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{}.{}", name, ext)
|
||||
};
|
||||
|
||||
let out_path = if ctx.kind == "sharedlib" {
|
||||
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
|
||||
} else if ctx.kind == "efi" {
|
||||
platform::efi_path(ctx.profile, &final_name, ctx.target_dir)
|
||||
} else if ctx.kind == "elf" {
|
||||
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
|
||||
} else {
|
||||
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
|
||||
};
|
||||
|
||||
if !common::needs_link(&objects, &out_path) {
|
||||
let elapsed = common::elapsed_secs(start_time);
|
||||
return Ok(elapsed);
|
||||
}
|
||||
cmd.arg("-o").arg(out_path);
|
||||
|
||||
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
|
||||
eprintln!("[dcr] {:?}", cmd);
|
||||
}
|
||||
match cmd.status() {
|
||||
Ok(status) if status.success() => {
|
||||
let elapsed = common::elapsed_secs(start_time);
|
||||
Ok(elapsed)
|
||||
}
|
||||
Ok(_) => Err("Build failed".to_string()),
|
||||
Err(err) => Err(format!("Build failed: {err}")),
|
||||
}
|
||||
artifact::link_binary(ctx, &objects, compiler, start_time)
|
||||
}
|
||||
|
||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
@@ -156,7 +80,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
||||
}
|
||||
|
||||
fn source_extensions(language: &str) -> Vec<&'static str> {
|
||||
crate::core::builder::common::source_extensions(language)
|
||||
crate::core::build::common::source_extensions(language)
|
||||
}
|
||||
|
||||
fn default_flags(profile: &str) -> &'static [&'static str] {
|
||||
@@ -221,7 +145,7 @@ fn build_object(
|
||||
cmd.arg("-fPIC");
|
||||
}
|
||||
|
||||
if (ctx.freestanding || is_bare_metal_target(ctx.target))
|
||||
if (ctx.freestanding || is_bare_metal_target(ctx.target) || is_flat_bin(ctx.kind))
|
||||
&& ctx.language.to_lowercase() != "asm"
|
||||
{
|
||||
cmd.arg("-ffreestanding");
|
||||
@@ -257,8 +181,8 @@ fn build_object(
|
||||
}
|
||||
}
|
||||
|
||||
let use_dcr_defaults =
|
||||
ctx.cflags.is_empty() && !(ctx.freestanding || is_bare_metal_target(ctx.target));
|
||||
let use_dcr_defaults = ctx.cflags.is_empty()
|
||||
&& !(ctx.freestanding || is_bare_metal_target(ctx.target) || is_flat_bin(ctx.kind));
|
||||
if use_dcr_defaults {
|
||||
for flag in default_flags(ctx.profile) {
|
||||
cmd.arg(flag);
|
||||
@@ -288,5 +212,5 @@ fn build_object(
|
||||
}
|
||||
|
||||
fn asm_lang_flag(source: &str) -> Option<&'static str> {
|
||||
crate::core::builder::common::asm_lang_flag(source)
|
||||
crate::core::build::common::asm_lang_flag(source)
|
||||
}
|
||||
234
src/core/build/builder/mod.rs
Normal file
234
src/core/build/builder/mod.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
// 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/>.
|
||||
|
||||
pub mod artifact;
|
||||
pub mod cc_common;
|
||||
pub mod msvc;
|
||||
|
||||
use crate::core::build::language::asm::{fasm, gas, masm, nasm};
|
||||
use crate::core::build::language::llvm_ir;
|
||||
|
||||
pub struct BuildContext<'a> {
|
||||
pub profile: &'a str,
|
||||
pub project_name: &'a str,
|
||||
pub compiler: &'a str,
|
||||
pub language: &'a str,
|
||||
pub standard: &'a str,
|
||||
pub cxx_standard: &'a str,
|
||||
pub target: Option<&'a str>,
|
||||
pub target_dir: Option<&'a str>,
|
||||
pub kind: &'a str,
|
||||
pub platform: Option<&'a str>,
|
||||
pub linker: Option<&'a str>,
|
||||
pub archiver: Option<&'a str>,
|
||||
pub moc: Option<&'a str>,
|
||||
pub uic: Option<&'a str>,
|
||||
pub rcc: Option<&'a str>,
|
||||
pub package_type: Option<&'a str>,
|
||||
pub freestanding: bool,
|
||||
pub panic_abort: bool,
|
||||
pub codegen_units: usize,
|
||||
pub source_roots: &'a [std::path::PathBuf],
|
||||
pub exclude_dirs: &'a [std::path::PathBuf],
|
||||
pub include_paths: &'a [String],
|
||||
pub include_dirs: &'a [String],
|
||||
pub lib_dirs: &'a [String],
|
||||
pub libs: &'a [String],
|
||||
pub cflags: &'a [String],
|
||||
pub ldflags: &'a [String],
|
||||
pub output_filename: Option<&'a str>,
|
||||
pub output_extension: Option<&'a str>,
|
||||
pub verbose: bool,
|
||||
pub qt: bool,
|
||||
}
|
||||
|
||||
pub trait Builder {
|
||||
#[allow(dead_code)]
|
||||
fn id(&self) -> &'static str;
|
||||
fn build(&self, ctx: &BuildContext) -> Result<f64, String>;
|
||||
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String>;
|
||||
}
|
||||
|
||||
struct Cc;
|
||||
struct Msvc;
|
||||
struct Nasm;
|
||||
struct Gas;
|
||||
struct Masm;
|
||||
struct Fasm;
|
||||
struct Llc;
|
||||
|
||||
impl Builder for Cc {
|
||||
fn id(&self) -> &'static str {
|
||||
"cc"
|
||||
}
|
||||
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
|
||||
cc_common::build(ctx)
|
||||
}
|
||||
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
cc_common::collect_sources(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Builder for Msvc {
|
||||
fn id(&self) -> &'static str {
|
||||
"msvc"
|
||||
}
|
||||
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
|
||||
msvc::build(ctx)
|
||||
}
|
||||
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
msvc::collect_sources(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Builder for Nasm {
|
||||
fn id(&self) -> &'static str {
|
||||
"nasm"
|
||||
}
|
||||
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
|
||||
nasm::build(ctx)
|
||||
}
|
||||
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
nasm::collect_sources(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Builder for Gas {
|
||||
fn id(&self) -> &'static str {
|
||||
"gas"
|
||||
}
|
||||
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
|
||||
gas::build(ctx)
|
||||
}
|
||||
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
gas::collect_sources(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Builder for Masm {
|
||||
fn id(&self) -> &'static str {
|
||||
"masm"
|
||||
}
|
||||
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
|
||||
masm::build(ctx)
|
||||
}
|
||||
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
masm::collect_sources(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Builder for Fasm {
|
||||
fn id(&self) -> &'static str {
|
||||
"fasm"
|
||||
}
|
||||
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
|
||||
fasm::build(ctx)
|
||||
}
|
||||
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
fasm::collect_sources(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Builder for Llc {
|
||||
fn id(&self) -> &'static str {
|
||||
"llvm_ir"
|
||||
}
|
||||
fn build(&self, ctx: &BuildContext) -> Result<f64, String> {
|
||||
llvm_ir::build(ctx)
|
||||
}
|
||||
fn collect_sources(&self, ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
llvm_ir::collect_sources(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn builder_for(compiler: &str) -> &'static dyn Builder {
|
||||
let c = compiler.to_lowercase();
|
||||
if c.contains("clang-cl") {
|
||||
return &Msvc;
|
||||
}
|
||||
if c == "as" || c.contains("gas") {
|
||||
return &Gas;
|
||||
}
|
||||
if c.contains("llc") {
|
||||
return &Llc;
|
||||
}
|
||||
if c.contains("fasm") {
|
||||
return &Fasm;
|
||||
}
|
||||
if c.contains("nasm") {
|
||||
return &Nasm;
|
||||
}
|
||||
if c == "ml" || c == "ml64" || c.contains("masm") {
|
||||
return &Masm;
|
||||
}
|
||||
if c == "cl" || c.contains("msvc") {
|
||||
return &Msvc;
|
||||
}
|
||||
&Cc
|
||||
}
|
||||
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
if !check_compiler_exists(ctx.compiler) {
|
||||
return Err(format!(
|
||||
"Compiler not found: {}. Make sure it is installed and available in PATH.",
|
||||
ctx.compiler
|
||||
));
|
||||
}
|
||||
builder_for(ctx.compiler).build(ctx)
|
||||
}
|
||||
|
||||
fn check_compiler_exists(compiler: &str) -> bool {
|
||||
let name = if compiler.is_empty() {
|
||||
"cc"
|
||||
} else {
|
||||
match compiler.to_lowercase().as_str() {
|
||||
"gas" | "gnu-as" => "as",
|
||||
"nasm" => "nasm",
|
||||
"fasm" | "fasm64" => "fasm",
|
||||
"masm" | "ml" | "ml64" => "ml",
|
||||
_ => compiler,
|
||||
}
|
||||
};
|
||||
std::process::Command::new(name)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
builder_for(ctx.compiler).collect_sources(ctx)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn builder_resolves_by_compiler_name() {
|
||||
assert_eq!(builder_for("gcc").id(), "cc");
|
||||
assert_eq!(builder_for("clang").id(), "cc");
|
||||
assert_eq!(builder_for("clang++").id(), "cc");
|
||||
assert_eq!(builder_for("").id(), "cc");
|
||||
assert_eq!(builder_for("nasm").id(), "nasm");
|
||||
assert_eq!(builder_for("as").id(), "gas");
|
||||
assert_eq!(builder_for("llc-18").id(), "llvm_ir");
|
||||
assert_eq!(builder_for("fasm").id(), "fasm");
|
||||
assert_eq!(builder_for("ml64").id(), "masm");
|
||||
assert_eq!(builder_for("cl").id(), "msvc");
|
||||
assert_eq!(builder_for("clang-cl").id(), "msvc");
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,11 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::builder::BuildContext;
|
||||
use crate::core::builder::common;
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::builder::artifact;
|
||||
use crate::core::build::common;
|
||||
use crate::platform;
|
||||
use crate::utils::build::{is_compile_only, is_flat_bin};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
@@ -33,6 +35,9 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
if lang.contains("asm") {
|
||||
return Err("MSVC backend does not support build.language with asm".to_string());
|
||||
}
|
||||
if is_flat_bin(ctx.kind) && ctx.qt {
|
||||
return Err("flat-bin is not supported with build.qt = true".to_string());
|
||||
}
|
||||
let start_time = Instant::now();
|
||||
let sources = collect_sources(ctx)?;
|
||||
let obj_dir = match ctx.target_dir {
|
||||
@@ -41,14 +46,27 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
};
|
||||
let objects = build_objects(compiler, &sources, &obj_dir, ctx, "obj")?;
|
||||
|
||||
if is_compile_only(ctx.kind) && !is_flat_bin(ctx.kind) {
|
||||
return Ok(common::elapsed_secs(start_time));
|
||||
}
|
||||
|
||||
if is_flat_bin(ctx.kind) {
|
||||
return link_flat_msvc(ctx, compiler, &objects, &obj_dir, start_time);
|
||||
}
|
||||
|
||||
if ctx.kind == "staticlib" {
|
||||
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
|
||||
if !common::needs_link(&objects, &lib_path) {
|
||||
let elapsed = common::elapsed_secs(start_time);
|
||||
return Ok(elapsed);
|
||||
}
|
||||
let mut cmd = Command::new(ctx.archiver.unwrap_or("lib"));
|
||||
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
|
||||
let archiver = ctx.archiver.unwrap_or("lib");
|
||||
let mut cmd = Command::new(archiver);
|
||||
if archiver == "lib" || archiver.eq_ignore_ascii_case("lib.exe") {
|
||||
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
|
||||
} else {
|
||||
cmd.arg("rcs").arg(&lib_path);
|
||||
}
|
||||
for obj in &objects {
|
||||
cmd.arg(obj);
|
||||
}
|
||||
@@ -141,6 +159,61 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn link_flat_msvc(
|
||||
ctx: &BuildContext,
|
||||
compiler: &str,
|
||||
objects: &[String],
|
||||
obj_dir: &Path,
|
||||
start_time: Instant,
|
||||
) -> Result<f64, String> {
|
||||
let out_path = artifact::flat_output_path(ctx);
|
||||
if !common::needs_link(objects, &out_path) {
|
||||
return Ok(common::elapsed_secs(start_time));
|
||||
}
|
||||
fs::create_dir_all(obj_dir).map_err(|e| format!("obj dir error: {e}"))?;
|
||||
let intermediate = obj_dir
|
||||
.join(format!("{}.flat.exe", ctx.project_name))
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let mut cmd = Command::new(compiler);
|
||||
cmd.arg("/nologo");
|
||||
for obj in objects {
|
||||
cmd.arg(obj);
|
||||
}
|
||||
for flag in ctx.cflags {
|
||||
cmd.arg(flag);
|
||||
}
|
||||
for dir in ctx.lib_dirs {
|
||||
cmd.arg(format!("/LIBPATH:{dir}"));
|
||||
}
|
||||
for lib in ctx.libs {
|
||||
if lib.to_lowercase().ends_with(".lib") {
|
||||
cmd.arg(lib);
|
||||
} else {
|
||||
cmd.arg(format!("{lib}.lib"));
|
||||
}
|
||||
}
|
||||
cmd.arg(format!("/Fe:{intermediate}"));
|
||||
if !ctx.ldflags.is_empty() {
|
||||
cmd.arg("/link");
|
||||
for flag in ctx.ldflags {
|
||||
cmd.arg(flag);
|
||||
}
|
||||
}
|
||||
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
|
||||
eprintln!("[dcr] {:?}", cmd);
|
||||
}
|
||||
let status = cmd
|
||||
.status()
|
||||
.map_err(|e| format!("flat-bin link failed: {e}"))?;
|
||||
if !status.success() {
|
||||
return Err("flat-bin link failed".to_string());
|
||||
}
|
||||
artifact::objcopy_binary(ctx, &intermediate, &out_path)?;
|
||||
Ok(common::elapsed_secs(start_time))
|
||||
}
|
||||
|
||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
let extensions = source_extensions(ctx.language);
|
||||
common::collect_sources(
|
||||
@@ -152,7 +225,7 @@ pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String>
|
||||
}
|
||||
|
||||
fn source_extensions(language: &str) -> Vec<&'static str> {
|
||||
crate::core::builder::common::source_extensions(language)
|
||||
crate::core::build::common::source_extensions(language)
|
||||
}
|
||||
|
||||
fn msvc_standard_flag(language: &str, standard: &str) -> Result<String, String> {
|
||||
264
src/core/build/cache.rs
Normal file
264
src/core/build/cache.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
// DCR — Cargo-like C/C++ project manager.
|
||||
//
|
||||
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub(crate) fn compute_build_fingerprint(
|
||||
ctx: &BuildContext,
|
||||
sources: &[String],
|
||||
headers: &[PathBuf],
|
||||
lib_files: &[PathBuf],
|
||||
) -> Result<String, String> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(ctx.profile.as_bytes());
|
||||
hasher.update(ctx.project_name.as_bytes());
|
||||
hasher.update(ctx.compiler.as_bytes());
|
||||
hasher.update(ctx.language.as_bytes());
|
||||
hasher.update(ctx.standard.as_bytes());
|
||||
hasher.update(ctx.kind.as_bytes());
|
||||
if let Some(v) = ctx.target_dir {
|
||||
hasher.update(v.as_bytes());
|
||||
}
|
||||
if let Some(v) = ctx.platform {
|
||||
hasher.update(v.as_bytes());
|
||||
}
|
||||
if let Some(v) = ctx.linker {
|
||||
hasher.update(v.as_bytes());
|
||||
}
|
||||
if let Some(v) = ctx.archiver {
|
||||
hasher.update(v.as_bytes());
|
||||
}
|
||||
for value in ctx.include_dirs {
|
||||
hasher.update(value.as_bytes());
|
||||
}
|
||||
for value in ctx.lib_dirs {
|
||||
hasher.update(value.as_bytes());
|
||||
}
|
||||
for value in ctx.libs {
|
||||
hasher.update(value.as_bytes());
|
||||
}
|
||||
for value in ctx.cflags {
|
||||
hasher.update(value.as_bytes());
|
||||
}
|
||||
for value in ctx.ldflags {
|
||||
hasher.update(value.as_bytes());
|
||||
}
|
||||
let toml =
|
||||
fs::read_to_string("dcr.toml").map_err(|err| format!("Failed to read dcr.toml: {err}"))?;
|
||||
hasher.update(toml.as_bytes());
|
||||
if let Ok(lock) = fs::read_to_string("dcr.lock") {
|
||||
hasher.update(lock.as_bytes());
|
||||
}
|
||||
for source in sources {
|
||||
let path = Path::new(source);
|
||||
update_hasher_with_file(&mut hasher, path)?;
|
||||
}
|
||||
for header in headers {
|
||||
update_hasher_with_file(&mut hasher, header)?;
|
||||
}
|
||||
for lib in lib_files {
|
||||
update_hasher_with_file(&mut hasher, lib)?;
|
||||
}
|
||||
Ok(to_hex(&hasher.finalize()))
|
||||
}
|
||||
|
||||
pub(crate) fn should_skip_build(ctx: &BuildContext, fingerprint: &str) -> bool {
|
||||
let output = build_output_path(ctx);
|
||||
if !Path::new(&output).is_file() {
|
||||
return false;
|
||||
}
|
||||
let cache_path = build_cache_path(ctx.profile, ctx.target_dir);
|
||||
let cached = fs::read_to_string(cache_path).unwrap_or_default();
|
||||
cached.trim() == fingerprint
|
||||
}
|
||||
|
||||
pub(crate) fn write_build_fingerprint(ctx: &BuildContext, fingerprint: &str) -> Result<(), String> {
|
||||
let cache_path = build_cache_path(ctx.profile, ctx.target_dir);
|
||||
if let Some(parent) = cache_path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|err| format!("Failed to create cache dir: {err}"))?;
|
||||
}
|
||||
fs::write(cache_path, format!("{fingerprint}\n"))
|
||||
.map_err(|err| format!("Failed to write cache: {err}"))
|
||||
}
|
||||
|
||||
fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf {
|
||||
match target_dir {
|
||||
Some(dir) => Path::new(dir).join(".dcr-build.hash"),
|
||||
None => Path::new("./target").join(profile).join(".dcr-build.hash"),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_output_path(ctx: &BuildContext) -> String {
|
||||
if crate::utils::build::is_flat_bin(ctx.kind) {
|
||||
return crate::core::build::builder::artifact::flat_output_path(ctx);
|
||||
}
|
||||
|
||||
let name = ctx.output_filename.unwrap_or(ctx.project_name);
|
||||
let ext = ctx.output_extension.unwrap_or("");
|
||||
|
||||
let final_name = if ext.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{}.{}", name, ext)
|
||||
};
|
||||
|
||||
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(
|
||||
ctx: &BuildContext,
|
||||
project_root: &Path,
|
||||
) -> Result<Vec<PathBuf>, String> {
|
||||
let mut out = Vec::new();
|
||||
let mut roots = Vec::new();
|
||||
if ctx.source_roots.is_empty() {
|
||||
roots.push(project_root.join("src"));
|
||||
} else {
|
||||
roots.extend(ctx.source_roots.iter().cloned());
|
||||
}
|
||||
let abs_root = project_root
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| project_root.to_path_buf());
|
||||
for dir in ctx.include_dirs {
|
||||
let p = Path::new(dir).to_path_buf();
|
||||
if let Ok(abs_p) = p.canonicalize() {
|
||||
if abs_p.starts_with(&abs_root) {
|
||||
roots.push(abs_p);
|
||||
}
|
||||
} else if p.starts_with(&abs_root) {
|
||||
roots.push(p);
|
||||
}
|
||||
}
|
||||
for root in roots {
|
||||
if !root.exists() {
|
||||
continue;
|
||||
}
|
||||
if root.is_file() {
|
||||
if is_header_file(&root) {
|
||||
out.push(root);
|
||||
}
|
||||
} else {
|
||||
collect_header_files_rec(&root, &mut out, ctx.exclude_dirs, ctx.include_paths)?;
|
||||
}
|
||||
}
|
||||
out.sort();
|
||||
out.dedup();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn collect_header_files_rec(
|
||||
dir: &Path,
|
||||
out: &mut Vec<PathBuf>,
|
||||
exclude_dirs: &[PathBuf],
|
||||
include_paths: &[String],
|
||||
) -> Result<(), String> {
|
||||
if common::is_excluded(dir, exclude_dirs, include_paths) && include_paths.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
for entry in fs::read_dir(dir).map_err(|err| format!("read_dir error: {err}"))? {
|
||||
let entry = entry.map_err(|err| format!("read_dir error: {err}"))?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if common::is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() {
|
||||
continue;
|
||||
}
|
||||
collect_header_files_rec(&path, out, exclude_dirs, include_paths)?;
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
if common::is_excluded(&path, exclude_dirs, include_paths) {
|
||||
continue;
|
||||
}
|
||||
if is_header_file(&path) {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_header_file(path: &Path) -> bool {
|
||||
let ext = path.extension().and_then(|v| v.to_str()).unwrap_or("");
|
||||
matches!(ext, "h" | "hpp" | "hh" | "hxx" | "inc")
|
||||
}
|
||||
|
||||
pub(crate) fn collect_lib_files(ctx: &BuildContext) -> Vec<PathBuf> {
|
||||
let mut out = Vec::new();
|
||||
for dir in ctx.lib_dirs {
|
||||
let dir_path = Path::new(dir);
|
||||
for lib in ctx.libs {
|
||||
for candidate in lib_candidates(lib) {
|
||||
let path = dir_path.join(candidate);
|
||||
if path.is_file() {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn lib_candidates(name: &str) -> Vec<String> {
|
||||
if cfg!(target_os = "windows") {
|
||||
return vec![format!("{name}.lib")];
|
||||
}
|
||||
if cfg!(target_os = "macos") {
|
||||
return vec![
|
||||
format!("lib{name}.a"),
|
||||
format!("lib{name}.dylib"),
|
||||
format!("lib{name}.so"),
|
||||
];
|
||||
}
|
||||
vec![
|
||||
format!("lib{name}.a"),
|
||||
format!("lib{name}.so"),
|
||||
format!("lib{name}.so.0"),
|
||||
]
|
||||
}
|
||||
|
||||
fn update_hasher_with_file(hasher: &mut Sha256, path: &Path) -> Result<(), String> {
|
||||
hasher.update(path.to_string_lossy().as_bytes());
|
||||
let meta = fs::metadata(path).map_err(|err| format!("source read error: {err}"))?;
|
||||
hasher.update(meta.len().to_le_bytes());
|
||||
if let Ok(modified) = meta.modified()
|
||||
&& let Ok(duration) = modified.duration_since(std::time::UNIX_EPOCH)
|
||||
{
|
||||
hasher.update(duration.as_nanos().to_le_bytes());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn to_hex(bytes: &[u8]) -> String {
|
||||
crate::utils::fs::to_hex(bytes)
|
||||
}
|
||||
@@ -16,17 +16,72 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::fs;
|
||||
use std::io::{self, IsTerminal, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::Instant;
|
||||
|
||||
static OUTPUT_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
static PROGRESS_LABEL: OnceLock<Mutex<Option<String>>> = OnceLock::new();
|
||||
static PROGRESS_DIRTY: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub fn get_output_lock() -> &'static Mutex<()> {
|
||||
OUTPUT_MUTEX.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
fn progress_label_lock() -> &'static Mutex<Option<String>> {
|
||||
PROGRESS_LABEL.get_or_init(|| Mutex::new(None))
|
||||
}
|
||||
|
||||
pub fn set_progress_label(label: Option<String>) {
|
||||
*progress_label_lock().lock().unwrap() = label;
|
||||
}
|
||||
|
||||
pub fn finish_progress_line() {
|
||||
let was_dirty = PROGRESS_DIRTY.swap(false, Ordering::SeqCst);
|
||||
*progress_label_lock().lock().unwrap() = None;
|
||||
if !was_dirty {
|
||||
return;
|
||||
}
|
||||
if io::stderr().is_terminal() {
|
||||
let _guard = get_output_lock().lock().unwrap();
|
||||
let _ = writeln!(io::stderr());
|
||||
let _ = io::stderr().flush();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interrupt_progress_for_output() {
|
||||
if !PROGRESS_DIRTY.swap(false, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
if io::stderr().is_terminal() {
|
||||
let _guard = get_output_lock().lock().unwrap();
|
||||
let _ = writeln!(io::stderr());
|
||||
let _ = io::stderr().flush();
|
||||
}
|
||||
}
|
||||
|
||||
fn report_compile_progress(done: usize, total: usize) {
|
||||
let label = progress_label_lock().lock().unwrap().clone();
|
||||
let Some(label) = label else {
|
||||
return;
|
||||
};
|
||||
if !io::stderr().is_terminal() {
|
||||
return;
|
||||
}
|
||||
let _guard = get_output_lock().lock().unwrap();
|
||||
let msg = format!("{label} [{done}/{total}]");
|
||||
if PROGRESS_DIRTY.load(Ordering::SeqCst) {
|
||||
eprint!("\r{msg:<80}");
|
||||
} else {
|
||||
eprint!("{msg}");
|
||||
}
|
||||
let _ = io::stderr().flush();
|
||||
PROGRESS_DIRTY.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn collect_sources(
|
||||
roots: &[PathBuf],
|
||||
extensions: &[&str],
|
||||
@@ -35,7 +90,15 @@ pub fn collect_sources(
|
||||
) -> Result<Vec<String>, String> {
|
||||
let mut sources = Vec::new();
|
||||
for root in roots {
|
||||
collect_sources_rec(root, extensions, &mut sources, exclude_dirs, include_paths)?;
|
||||
if root.is_file() {
|
||||
if let Some(ext) = root.extension().and_then(|e| e.to_str())
|
||||
&& extensions.contains(&ext.to_lowercase().as_str())
|
||||
{
|
||||
sources.push(root.to_string_lossy().to_string());
|
||||
}
|
||||
} else {
|
||||
collect_sources_rec(root, extensions, &mut sources, exclude_dirs, include_paths)?;
|
||||
}
|
||||
}
|
||||
sources.sort();
|
||||
Ok(sources)
|
||||
@@ -58,9 +121,10 @@ fn collect_sources_rec(
|
||||
if is_excluded(&full_dir, exclude_dirs, include_paths) && include_paths.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let entries = fs::read_dir(&full_dir).map_err(|err| format!("src dir error: {err}"))?;
|
||||
let entries = fs::read_dir(&full_dir)
|
||||
.map_err(|err| format!("src dir error for {:?}: {err}", full_dir))?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|err| format!("src dir error: {err}"))?;
|
||||
let entry = entry.map_err(|err| format!("src dir error for {:?}: {err}", full_dir))?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if is_excluded(&path, exclude_dirs, include_paths) && include_paths.is_empty() {
|
||||
@@ -94,7 +158,26 @@ pub fn is_excluded(path: &Path, exclude_dirs: &[PathBuf], include_paths: &[Strin
|
||||
if matches_patterns(path, include_paths, false) {
|
||||
return true;
|
||||
}
|
||||
exclude_dirs.iter().any(|dir| path.starts_with(dir))
|
||||
exclude_dirs.iter().any(|dir| path_is_under(path, dir))
|
||||
}
|
||||
|
||||
fn path_is_under(path: &Path, prefix: &Path) -> bool {
|
||||
if path.starts_with(prefix) {
|
||||
return true;
|
||||
}
|
||||
match (fs::canonicalize(path), fs::canonicalize(prefix)) {
|
||||
(Ok(p), Ok(pref)) => p.starts_with(pref),
|
||||
_ => {
|
||||
let p = path.to_string_lossy().replace('\\', "/");
|
||||
let pref = prefix.to_string_lossy().replace('\\', "/");
|
||||
if p == pref || p.starts_with(&format!("{pref}/")) {
|
||||
return true;
|
||||
}
|
||||
let p = p.strip_prefix("/private").unwrap_or(&p);
|
||||
let pref = pref.strip_prefix("/private").unwrap_or(&pref);
|
||||
p == pref || p.starts_with(&format!("{pref}/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_patterns(path: &Path, patterns: &[String], positive: bool) -> bool {
|
||||
@@ -177,12 +260,8 @@ pub fn asm_lang_flag(source: &str) -> Option<&'static str> {
|
||||
pub fn source_extensions(language: &str) -> Vec<&'static str> {
|
||||
let mut out = Vec::new();
|
||||
for part in language.split(',').map(|s| s.trim()) {
|
||||
let lang = part.to_lowercase();
|
||||
match lang.as_str() {
|
||||
"c" => out.extend(["c"]),
|
||||
"c++" | "cpp" | "cxx" => out.extend(["cpp", "cxx", "cc"]),
|
||||
"asm" => out.extend(["s", "S", "asm"]),
|
||||
_ => {}
|
||||
if let Some(lang) = crate::core::build::language::language_for_token(part) {
|
||||
out.extend(lang.extensions().iter().copied());
|
||||
}
|
||||
}
|
||||
if out.is_empty() {
|
||||
@@ -209,8 +288,13 @@ where
|
||||
};
|
||||
|
||||
let counter = std::sync::atomic::AtomicUsize::new(0);
|
||||
let completed = std::sync::atomic::AtomicUsize::new(0);
|
||||
let err_msg = std::sync::Mutex::new(None);
|
||||
|
||||
if total_tasks > 0 {
|
||||
report_compile_progress(0, total_tasks);
|
||||
}
|
||||
|
||||
std::thread::scope(|s| {
|
||||
for _ in 0..num_threads {
|
||||
s.spawn(|| {
|
||||
@@ -231,6 +315,9 @@ where
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let done = completed.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
|
||||
report_compile_progress(done, total_tasks);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -244,10 +331,21 @@ where
|
||||
}
|
||||
|
||||
pub fn run_command_sync_output(cmd: &mut Command) -> Result<(), String> {
|
||||
let output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
|
||||
let _guard = OUTPUT_MUTEX.get_or_init(|| Mutex::new(())).lock().unwrap();
|
||||
let program = cmd.get_program().to_string_lossy().into_owned();
|
||||
let output = cmd.output().map_err(|err| {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
format!("{program} not found (check PATH / [toolchain])")
|
||||
} else {
|
||||
format!("Build failed: {err}")
|
||||
}
|
||||
})?;
|
||||
let has_out = !output.stdout.is_empty() || !output.stderr.is_empty();
|
||||
if has_out {
|
||||
interrupt_progress_for_output();
|
||||
}
|
||||
let _guard = get_output_lock().lock().unwrap();
|
||||
if !output.stdout.is_empty() {
|
||||
print!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
eprint!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
}
|
||||
if !output.stderr.is_empty() {
|
||||
eprint!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
@@ -300,7 +398,7 @@ pub fn object_path(obj_dir: &Path, source: &str, obj_ext: &str) -> String {
|
||||
} else {
|
||||
out.set_extension(obj_ext);
|
||||
}
|
||||
out.to_string_lossy().to_string()
|
||||
out.to_string_lossy().replace('\\', "/")
|
||||
}
|
||||
|
||||
pub fn needs_rebuild(source: &str, object: &str) -> bool {
|
||||
@@ -331,7 +429,7 @@ pub fn needs_rebuild(source: &str, object: &str) -> bool {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true; // Missing dependency triggers rebuild
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -403,7 +501,11 @@ fn parse_d_file(content: &str) -> Vec<String> {
|
||||
}
|
||||
in_escape = false;
|
||||
} else if c == '\\' {
|
||||
in_escape = true;
|
||||
match chars.peek() {
|
||||
Some('\n' | '\r' | ' ' | '\t' | '\\') => in_escape = true,
|
||||
Some(_) => current_path.push('\\'),
|
||||
None => current_path.push('\\'),
|
||||
}
|
||||
} else if c.is_whitespace() {
|
||||
if !current_path.is_empty() {
|
||||
deps.push(current_path.clone());
|
||||
@@ -430,12 +532,18 @@ mod tests {
|
||||
static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
static CWD_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn cwd_lock() -> std::sync::MutexGuard<'static, ()> {
|
||||
CWD_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
}
|
||||
|
||||
fn temp_dir(prefix: &str) -> PathBuf {
|
||||
let n = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
let dir = std::env::temp_dir().join(format!("dcr_test_{prefix}_{n}"));
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
fs::canonicalize(&dir).unwrap_or(dir)
|
||||
}
|
||||
|
||||
fn default_roots() -> Vec<PathBuf> {
|
||||
@@ -537,7 +645,6 @@ mod tests {
|
||||
let src = dir.join("test.c");
|
||||
let obj = dir.join("test.o");
|
||||
fs::write(&src, "int main() {}").unwrap();
|
||||
// Sleep briefly to ensure mtime difference
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
fs::write(&obj, "").unwrap();
|
||||
assert!(!needs_rebuild(
|
||||
@@ -601,7 +708,7 @@ mod tests {
|
||||
fs::write(src.join("utils.c"), "").unwrap();
|
||||
fs::write(src.join("README.md"), "").unwrap(); // should be ignored
|
||||
|
||||
let _guard = CWD_LOCK.lock().unwrap();
|
||||
let _guard = cwd_lock();
|
||||
let prev = std::env::current_dir().unwrap();
|
||||
std::env::set_current_dir(&dir).unwrap();
|
||||
let include: Vec<String> = Vec::new();
|
||||
@@ -625,7 +732,7 @@ mod tests {
|
||||
fs::write(src.join("other.cc"), "").unwrap();
|
||||
fs::write(src.join("skip.c"), "").unwrap(); // should be ignored
|
||||
|
||||
let _guard = CWD_LOCK.lock().unwrap();
|
||||
let _guard = cwd_lock();
|
||||
let prev = std::env::current_dir().unwrap();
|
||||
std::env::set_current_dir(&dir).unwrap();
|
||||
let include: Vec<String> = Vec::new();
|
||||
@@ -643,7 +750,7 @@ mod tests {
|
||||
let src = dir.join("src");
|
||||
fs::create_dir_all(&src).unwrap();
|
||||
|
||||
let _guard = CWD_LOCK.lock().unwrap();
|
||||
let _guard = cwd_lock();
|
||||
let prev = std::env::current_dir().unwrap();
|
||||
std::env::set_current_dir(&dir).unwrap();
|
||||
let include: Vec<String> = Vec::new();
|
||||
@@ -665,7 +772,7 @@ mod tests {
|
||||
fs::write(vendor.join("lib.c"), "").unwrap(); // should be excluded
|
||||
|
||||
let exclude = vec![vendor.clone()];
|
||||
let _guard = CWD_LOCK.lock().unwrap();
|
||||
let _guard = cwd_lock();
|
||||
let prev = std::env::current_dir().unwrap();
|
||||
std::env::set_current_dir(&dir).unwrap();
|
||||
let include: Vec<String> = Vec::new();
|
||||
@@ -687,7 +794,7 @@ mod tests {
|
||||
fs::write(src.join("main.c"), "").unwrap();
|
||||
fs::write(sub.join("nested.c"), "").unwrap();
|
||||
|
||||
let _guard = CWD_LOCK.lock().unwrap();
|
||||
let _guard = cwd_lock();
|
||||
let prev = std::env::current_dir().unwrap();
|
||||
std::env::set_current_dir(&dir).unwrap();
|
||||
let include: Vec<String> = Vec::new();
|
||||
@@ -711,7 +818,7 @@ mod tests {
|
||||
|
||||
let exclude = vec![boot.clone()];
|
||||
let include = vec!["src/boot/arch/**".to_string()];
|
||||
let _guard = CWD_LOCK.lock().unwrap();
|
||||
let _guard = cwd_lock();
|
||||
let prev = std::env::current_dir().unwrap();
|
||||
std::env::set_current_dir(&dir).unwrap();
|
||||
let roots = default_roots();
|
||||
@@ -738,7 +845,7 @@ mod tests {
|
||||
"!src/legacy/**".to_string(),
|
||||
"src/legacy/allow/**".to_string(),
|
||||
];
|
||||
let _guard = CWD_LOCK.lock().unwrap();
|
||||
let _guard = cwd_lock();
|
||||
let prev = std::env::current_dir().unwrap();
|
||||
std::env::set_current_dir(&dir).unwrap();
|
||||
let roots = default_roots();
|
||||
@@ -761,7 +868,7 @@ mod tests {
|
||||
|
||||
let exclude: Vec<PathBuf> = Vec::new();
|
||||
let include = vec!["!src/gen/**".to_string()];
|
||||
let _guard = CWD_LOCK.lock().unwrap();
|
||||
let _guard = cwd_lock();
|
||||
let prev = std::env::current_dir().unwrap();
|
||||
std::env::set_current_dir(&dir).unwrap();
|
||||
let roots = default_roots();
|
||||
@@ -796,36 +903,44 @@ mod tests {
|
||||
#[test]
|
||||
fn needs_rebuild_header_modified() {
|
||||
let dir = temp_dir("rebuild_header");
|
||||
let src = dir.join("test.c");
|
||||
let header = dir.join("test.h");
|
||||
let obj = dir.join("test.o");
|
||||
let d_file = dir.join("test.d");
|
||||
fs::write(dir.join("test.c"), "int main() {}").unwrap();
|
||||
fs::write(dir.join("test.h"), "#define A 1").unwrap();
|
||||
fs::write(dir.join("test.o"), "").unwrap();
|
||||
|
||||
fs::write(&src, "int main() {}").unwrap();
|
||||
fs::write(&header, "#define A 1").unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
fs::write(&obj, "").unwrap();
|
||||
|
||||
let d_content = format!(
|
||||
"{}: {} {}",
|
||||
obj.to_string_lossy(),
|
||||
src.to_string_lossy(),
|
||||
header.to_string_lossy()
|
||||
);
|
||||
fs::write(&d_file, d_content).unwrap();
|
||||
let _guard = cwd_lock();
|
||||
let prev = std::env::current_dir().unwrap();
|
||||
std::env::set_current_dir(&dir).unwrap();
|
||||
|
||||
fs::write("test.d", "test.o: test.c missing.h").unwrap();
|
||||
assert!(
|
||||
!needs_rebuild(&src.to_string_lossy(), &obj.to_string_lossy()),
|
||||
"should be fresh"
|
||||
needs_rebuild("test.c", "test.o"),
|
||||
"missing dep in .d must force rebuild"
|
||||
);
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
fs::write(&header, "#define A 2").unwrap(); // modify header
|
||||
|
||||
fs::write("test.h", "#define A 1").unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
fs::write("test.o", "obj").unwrap();
|
||||
fs::write("test.d", "test.o: test.c test.h").unwrap();
|
||||
assert!(
|
||||
needs_rebuild(&src.to_string_lossy(), &obj.to_string_lossy()),
|
||||
!needs_rebuild("test.c", "test.o"),
|
||||
"object newer than source and headers should be fresh"
|
||||
);
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
fs::write("test.h", "#define A 2").unwrap();
|
||||
assert!(
|
||||
needs_rebuild("test.c", "test.o"),
|
||||
"changed header should trigger rebuild"
|
||||
);
|
||||
|
||||
std::env::set_current_dir(prev).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_d_file_keeps_windows_path_separators() {
|
||||
let content = r"target\obj\main.obj: src\main.c src\utils.h";
|
||||
let deps = parse_d_file(content);
|
||||
assert_eq!(deps, vec![r"src\main.c", r"src\utils.h"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
1229
src/core/build/engine.rs
Normal file
1229
src/core/build/engine.rs
Normal file
File diff suppressed because it is too large
Load Diff
173
src/core/build/language/asm/common.rs
Normal file
173
src/core/build/language/asm/common.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::builder::artifact;
|
||||
use crate::core::build::common;
|
||||
use crate::utils::build::{is_compile_only, is_flat_bin};
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
pub fn filter_asm_flags(flags: &[String]) -> Vec<String> {
|
||||
flags
|
||||
.iter()
|
||||
.filter(|f| {
|
||||
let f = f.as_str();
|
||||
f.starts_with("-I") || f.starts_with("-i") || f == "-g"
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn build_assembly<F>(
|
||||
ctx: &BuildContext,
|
||||
backend_name: &str,
|
||||
default_assembler: &str,
|
||||
extensions: &[&str],
|
||||
build_object: F,
|
||||
) -> Result<f64, String>
|
||||
where
|
||||
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
|
||||
{
|
||||
build_assembly_with_mode(
|
||||
ctx,
|
||||
backend_name,
|
||||
default_assembler,
|
||||
extensions,
|
||||
FlatEmit::Direct,
|
||||
build_object,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_assembly_via_objcopy<F>(
|
||||
ctx: &BuildContext,
|
||||
backend_name: &str,
|
||||
default_assembler: &str,
|
||||
extensions: &[&str],
|
||||
build_object: F,
|
||||
) -> Result<f64, String>
|
||||
where
|
||||
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
|
||||
{
|
||||
build_assembly_with_mode(
|
||||
ctx,
|
||||
backend_name,
|
||||
default_assembler,
|
||||
extensions,
|
||||
FlatEmit::ViaObjcopy,
|
||||
build_object,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum FlatEmit {
|
||||
Direct,
|
||||
ViaObjcopy,
|
||||
}
|
||||
|
||||
fn build_assembly_with_mode<F>(
|
||||
ctx: &BuildContext,
|
||||
backend_name: &str,
|
||||
default_assembler: &str,
|
||||
extensions: &[&str],
|
||||
flat_mode: FlatEmit,
|
||||
build_object: F,
|
||||
) -> Result<f64, String>
|
||||
where
|
||||
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
|
||||
{
|
||||
let lang = ctx.language.to_lowercase();
|
||||
if lang.split(',').any(|p| {
|
||||
let t = p.trim();
|
||||
t != "asm" && t != "llvm_ir" && t != "llvm-ir" && t != "ll"
|
||||
}) {
|
||||
return Err(format!(
|
||||
"{backend_name} backend requires build.language = \"asm\" (or llvm_ir for LLC)"
|
||||
));
|
||||
}
|
||||
|
||||
let assembler = if ctx.compiler.is_empty() {
|
||||
default_assembler
|
||||
} else {
|
||||
ctx.compiler
|
||||
};
|
||||
|
||||
let start_time = Instant::now();
|
||||
let sources = common::collect_sources(
|
||||
ctx.source_roots,
|
||||
extensions,
|
||||
ctx.exclude_dirs,
|
||||
ctx.include_paths,
|
||||
)?;
|
||||
|
||||
let flat = is_flat_bin(ctx.kind);
|
||||
let obj_dir = match ctx.target_dir {
|
||||
Some(dir) => Path::new(dir).join("obj"),
|
||||
None => Path::new("./target").join(ctx.profile).join("obj"),
|
||||
};
|
||||
|
||||
let objects = match (flat, flat_mode) {
|
||||
(true, FlatEmit::Direct) => {
|
||||
let outs: Vec<String> = sources
|
||||
.iter()
|
||||
.map(|s| artifact::flat_source_output_path(ctx, s))
|
||||
.collect();
|
||||
common::parallel_build(
|
||||
sources.len(),
|
||||
|i| build_object(assembler, &sources[i], &outs[i], ctx),
|
||||
ctx.codegen_units,
|
||||
)?;
|
||||
outs
|
||||
}
|
||||
(true, FlatEmit::ViaObjcopy) => {
|
||||
let intermediates: Vec<String> = sources
|
||||
.iter()
|
||||
.map(|s| common::object_path(&obj_dir, s, "o"))
|
||||
.collect();
|
||||
common::parallel_build(
|
||||
sources.len(),
|
||||
|i| build_object(assembler, &sources[i], &intermediates[i], ctx),
|
||||
ctx.codegen_units,
|
||||
)?;
|
||||
let mut outs = Vec::with_capacity(sources.len());
|
||||
for (i, src) in sources.iter().enumerate() {
|
||||
let out = artifact::flat_source_output_path(ctx, src);
|
||||
artifact::objcopy_binary(ctx, &intermediates[i], &out)?;
|
||||
outs.push(out);
|
||||
}
|
||||
outs
|
||||
}
|
||||
(false, _) => build_objects_common(assembler, &sources, &obj_dir, ctx, &build_object)?,
|
||||
};
|
||||
|
||||
if is_compile_only(ctx.kind) {
|
||||
return Ok(common::elapsed_secs(start_time));
|
||||
}
|
||||
|
||||
if ctx.kind == "staticlib" {
|
||||
return artifact::archive_static(ctx, &objects, start_time);
|
||||
}
|
||||
|
||||
artifact::link_binary(ctx, &objects, "cc", start_time)
|
||||
}
|
||||
|
||||
fn build_objects_common<F>(
|
||||
assembler: &str,
|
||||
sources: &[String],
|
||||
obj_dir: &Path,
|
||||
ctx: &BuildContext,
|
||||
build_object: &F,
|
||||
) -> Result<Vec<String>, String>
|
||||
where
|
||||
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
|
||||
{
|
||||
let objects: Vec<String> = sources
|
||||
.iter()
|
||||
.map(|s| common::object_path(obj_dir, s, "o"))
|
||||
.collect();
|
||||
|
||||
common::parallel_build(
|
||||
sources.len(),
|
||||
|i| build_object(assembler, &sources[i], &objects[i], ctx),
|
||||
ctx.codegen_units,
|
||||
)?;
|
||||
|
||||
Ok(objects)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::core::builder::BuildContext;
|
||||
use crate::core::builder::asm;
|
||||
use crate::core::builder::common;
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::language::asm::common as asm;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
@@ -35,7 +35,7 @@ fn build_object(
|
||||
let mut cmd = Command::new(assembler);
|
||||
cmd.arg(source).arg(obj_path);
|
||||
|
||||
for flag in ctx.cflags {
|
||||
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
|
||||
cmd.arg(flag);
|
||||
}
|
||||
|
||||
@@ -15,15 +15,24 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::builder::BuildContext;
|
||||
use crate::core::builder::asm;
|
||||
use crate::core::builder::common;
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::language::asm::common as asm;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
asm::build_assembly(ctx, "GAS", "as", &["s"], build_object)
|
||||
// Use full path to as
|
||||
let as_path = std::process::Command::new("which")
|
||||
.arg("as")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| "as".to_string());
|
||||
asm::build_assembly_via_objcopy(ctx, "GAS", &as_path, &["s"], build_object)
|
||||
}
|
||||
|
||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
@@ -52,9 +61,13 @@ fn build_object(
|
||||
let mut cmd = Command::new(assembler);
|
||||
cmd.arg(source).arg("-o").arg(obj_path);
|
||||
|
||||
for flag in ctx.cflags {
|
||||
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
|
||||
cmd.arg(flag);
|
||||
}
|
||||
|
||||
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
|
||||
eprintln!("[dcr] {:?}", cmd);
|
||||
}
|
||||
|
||||
common::run_command_sync_output(&mut cmd)
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::core::builder::BuildContext;
|
||||
use crate::core::builder::asm;
|
||||
use crate::core::builder::common;
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::language::asm::common as asm;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
asm::build_assembly(ctx, "MASM", "ml", &["asm"], build_object)
|
||||
asm::build_assembly_via_objcopy(ctx, "MASM", "ml", &["asm"], build_object)
|
||||
}
|
||||
|
||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
@@ -39,7 +39,7 @@ fn build_object(
|
||||
.arg(obj_path)
|
||||
.arg(source);
|
||||
|
||||
for flag in ctx.cflags {
|
||||
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
|
||||
cmd.arg(flag);
|
||||
}
|
||||
|
||||
38
src/core/build/language/asm/mod.rs
Normal file
38
src/core/build/language/asm/mod.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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/>.
|
||||
|
||||
pub mod common;
|
||||
pub mod fasm;
|
||||
pub mod gas;
|
||||
pub mod masm;
|
||||
pub mod nasm;
|
||||
|
||||
use crate::core::build::language::Language;
|
||||
|
||||
pub struct Asm;
|
||||
|
||||
impl Language for Asm {
|
||||
fn id(&self) -> &'static str {
|
||||
"asm"
|
||||
}
|
||||
fn extensions(&self) -> &'static [&'static str] {
|
||||
&["s", "S", "asm"]
|
||||
}
|
||||
fn matches_token(&self, token: &str) -> bool {
|
||||
token.eq_ignore_ascii_case("asm")
|
||||
}
|
||||
}
|
||||
@@ -15,22 +15,22 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::builder::BuildContext;
|
||||
use crate::core::builder::asm;
|
||||
use crate::core::builder::common;
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::language::asm::common as asm;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
asm::build_assembly(ctx, "NASM", "nasm", &["asm", "s"], build_object)
|
||||
asm::build_assembly(ctx, "NASM", "nasm", &["asm"], build_object)
|
||||
}
|
||||
|
||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
// NASM handles lowercase .s and .asm only (not .S which is GCC preprocessed)
|
||||
// NASM handles .asm only (not .s which is GAS syntax, not .S which is GCC preprocessed)
|
||||
common::collect_sources(
|
||||
ctx.source_roots,
|
||||
&["asm", "s"],
|
||||
&["asm"],
|
||||
ctx.exclude_dirs,
|
||||
ctx.include_paths,
|
||||
)
|
||||
@@ -50,7 +50,11 @@ fn build_object(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let format = nasm_format(ctx.platform);
|
||||
let format = if crate::utils::build::is_flat_bin(ctx.kind) {
|
||||
"bin"
|
||||
} else {
|
||||
nasm_format(ctx.platform)
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(assembler);
|
||||
cmd.arg("-f")
|
||||
@@ -59,7 +63,7 @@ fn build_object(
|
||||
.arg("-o")
|
||||
.arg(obj_path);
|
||||
|
||||
for flag in ctx.cflags {
|
||||
for flag in crate::core::build::language::asm::common::filter_asm_flags(ctx.cflags) {
|
||||
cmd.arg(flag);
|
||||
}
|
||||
|
||||
32
src/core/build/language/c/mod.rs
Normal file
32
src/core/build/language/c/mod.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
// DCR — Cargo-like C/C++ project manager.
|
||||
//
|
||||
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::language::Language;
|
||||
|
||||
pub struct C;
|
||||
|
||||
impl Language for C {
|
||||
fn id(&self) -> &'static str {
|
||||
"c"
|
||||
}
|
||||
fn extensions(&self) -> &'static [&'static str] {
|
||||
&["c"]
|
||||
}
|
||||
fn matches_token(&self, token: &str) -> bool {
|
||||
token.eq_ignore_ascii_case("c")
|
||||
}
|
||||
}
|
||||
34
src/core/build/language/cxx/mod.rs
Normal file
34
src/core/build/language/cxx/mod.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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/>.
|
||||
|
||||
pub mod qt;
|
||||
|
||||
use crate::core::build::language::Language;
|
||||
|
||||
pub struct Cxx;
|
||||
|
||||
impl Language for Cxx {
|
||||
fn id(&self) -> &'static str {
|
||||
"cxx"
|
||||
}
|
||||
fn extensions(&self) -> &'static [&'static str] {
|
||||
&["cpp", "cxx", "cc"]
|
||||
}
|
||||
fn matches_token(&self, token: &str) -> bool {
|
||||
matches!(token.to_lowercase().as_str(), "c++" | "cpp" | "cxx")
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::builder::BuildContext;
|
||||
use crate::core::builder::common;
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::utils::build::run_pkg_config;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -34,14 +34,12 @@ pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf
|
||||
ctx.include_paths,
|
||||
)?;
|
||||
|
||||
// Fetch Qt system include paths and defines for MOC
|
||||
let mut qt_moc_args = Vec::new();
|
||||
let qt_modules = vec!["Qt6Core", "Qt6Widgets", "Qt6Gui", "Qt6Svg"];
|
||||
|
||||
for module in qt_modules {
|
||||
if let Ok(cflags) = run_pkg_config(module, "--cflags") {
|
||||
for flag in cflags.split_whitespace() {
|
||||
// Pass both -I and -D flags to moc
|
||||
if flag.starts_with("-I") || flag.starts_with("-D") {
|
||||
qt_moc_args.push(flag.to_string());
|
||||
}
|
||||
@@ -96,12 +94,10 @@ pub fn process_qt(ctx: &BuildContext, build_dir: &Path) -> Result<Option<PathBuf
|
||||
|
||||
let mut moc_args = Vec::new();
|
||||
|
||||
// Add Qt system cflags (includes + defines)
|
||||
for arg in &qt_moc_args {
|
||||
moc_args.push(arg.as_str());
|
||||
}
|
||||
|
||||
// Add project include paths
|
||||
for inc in ctx.include_paths.iter().chain(ctx.include_dirs.iter()) {
|
||||
moc_args.push("-I");
|
||||
moc_args.push(inc);
|
||||
@@ -1,12 +1,27 @@
|
||||
use crate::core::builder::BuildContext;
|
||||
use crate::core::builder::asm;
|
||||
use crate::core::builder::common;
|
||||
use crate::core::build::builder::BuildContext;
|
||||
use crate::core::build::common;
|
||||
use crate::core::build::language::Language;
|
||||
use crate::core::build::language::asm::common as asm;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
pub struct LlvmIr;
|
||||
|
||||
impl Language for LlvmIr {
|
||||
fn id(&self) -> &'static str {
|
||||
"llvm_ir"
|
||||
}
|
||||
fn extensions(&self) -> &'static [&'static str] {
|
||||
&["ll"]
|
||||
}
|
||||
fn matches_token(&self, token: &str) -> bool {
|
||||
matches!(token.to_lowercase().as_str(), "llvm_ir" | "llvm-ir" | "ll")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
asm::build_assembly(ctx, "LLVM-IR", "llc", &["ll"], build_object)
|
||||
asm::build_assembly_via_objcopy(ctx, "LLVM-IR", "llc", &["ll"], build_object)
|
||||
}
|
||||
|
||||
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
57
src/core/build/language/mod.rs
Normal file
57
src/core/build/language/mod.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
// 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/>.
|
||||
|
||||
pub mod asm;
|
||||
pub mod c;
|
||||
pub mod cxx;
|
||||
pub mod llvm_ir;
|
||||
|
||||
pub trait Language {
|
||||
fn id(&self) -> &'static str;
|
||||
fn extensions(&self) -> &'static [&'static str];
|
||||
fn matches_token(&self, token: &str) -> bool;
|
||||
}
|
||||
|
||||
pub fn languages() -> [&'static dyn Language; 4] {
|
||||
[&c::C, &cxx::Cxx, &asm::Asm, &llvm_ir::LlvmIr]
|
||||
}
|
||||
|
||||
pub fn language_for_token(token: &str) -> Option<&'static dyn Language> {
|
||||
languages().into_iter().find(|l| l.matches_token(token))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tokens_resolve_to_languages() {
|
||||
assert_eq!(language_for_token("c").unwrap().id(), "c");
|
||||
assert_eq!(language_for_token("c++").unwrap().id(), "cxx");
|
||||
assert_eq!(language_for_token("cpp").unwrap().id(), "cxx");
|
||||
assert_eq!(language_for_token("cxx").unwrap().id(), "cxx");
|
||||
assert_eq!(language_for_token("asm").unwrap().id(), "asm");
|
||||
assert!(language_for_token("rust").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn languages_expose_expected_extensions() {
|
||||
assert_eq!(c::C.extensions(), &["c"]);
|
||||
assert_eq!(cxx::Cxx.extensions(), &["cpp", "cxx", "cc"]);
|
||||
assert_eq!(asm::Asm.extensions(), &["s", "S", "asm"]);
|
||||
}
|
||||
}
|
||||
31
src/core/build/mod.rs
Normal file
31
src/core/build/mod.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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/>.
|
||||
|
||||
#[cfg(feature = "archive")]
|
||||
pub mod archive;
|
||||
pub mod builder;
|
||||
pub mod cache;
|
||||
pub mod common;
|
||||
pub mod engine;
|
||||
pub mod language;
|
||||
pub mod report;
|
||||
pub mod resolve;
|
||||
pub mod steps;
|
||||
|
||||
pub use engine::run_build;
|
||||
pub use report::{BuildEvent, BuildReporter, BuildRequest};
|
||||
pub use resolve::get_build_string_with_profile;
|
||||
86
src/core/build/report.rs
Normal file
86
src/core/build/report.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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/>.
|
||||
|
||||
// The reporter decouples the build engine from any particular frontend: the CLI
|
||||
// renders events as coloured lines, a TUI/GUI would push them into a widget and
|
||||
// MCP would serialise them to JSON. The engine only ever calls `on_event`.
|
||||
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
pub struct BuildRequest {
|
||||
pub profile: String,
|
||||
pub target: Option<String>,
|
||||
pub force: bool,
|
||||
pub verbose: bool,
|
||||
pub workspace: Option<String>,
|
||||
pub cancel: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct BuildOutcome {
|
||||
pub secs: f64,
|
||||
}
|
||||
|
||||
pub struct BuildError {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl From<String> for BuildError {
|
||||
fn from(message: String) -> Self {
|
||||
BuildError { message }
|
||||
}
|
||||
}
|
||||
|
||||
pub enum BuildEvent<'a> {
|
||||
TargetStart {
|
||||
index: usize,
|
||||
total: usize,
|
||||
target: &'a str,
|
||||
},
|
||||
ProjectStart {
|
||||
name: &'a str,
|
||||
profile: &'a str,
|
||||
target: &'a str,
|
||||
},
|
||||
DepBuilding {
|
||||
name: &'a str,
|
||||
version: &'a str,
|
||||
},
|
||||
DepReady {
|
||||
name: &'a str,
|
||||
version: &'a str,
|
||||
rebuilt: bool,
|
||||
},
|
||||
Compiling {
|
||||
name: &'a str,
|
||||
version: &'a str,
|
||||
},
|
||||
Packing {
|
||||
path: &'a str,
|
||||
},
|
||||
CompilerOutput {
|
||||
stream: &'a str,
|
||||
text: &'a str,
|
||||
},
|
||||
Finished {
|
||||
secs: f64,
|
||||
},
|
||||
}
|
||||
|
||||
pub trait BuildReporter {
|
||||
fn on_event(&mut self, event: BuildEvent<'_>);
|
||||
}
|
||||
232
src/core/build/resolve.rs
Normal file
232
src/core/build/resolve.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
// DCR — Cargo-like C/C++ project manager.
|
||||
//
|
||||
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build_config::Config;
|
||||
use crate::utils::build::{get_config_str, normalize_target_os, profile_table};
|
||||
|
||||
pub(crate) fn get_config_value_raw(
|
||||
config: &Config,
|
||||
section: &str,
|
||||
field: &str,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
) -> Option<toml::Value> {
|
||||
// Order: target.profile, profile.target, target, profile, base
|
||||
let keys = [
|
||||
target.map(|t| {
|
||||
format!(
|
||||
"{}.{}.{}.{}",
|
||||
section,
|
||||
normalize_target_os(t),
|
||||
profile,
|
||||
field
|
||||
)
|
||||
}),
|
||||
target.map(|t| {
|
||||
format!(
|
||||
"{}.{}.{}.{}",
|
||||
section,
|
||||
profile,
|
||||
normalize_target_os(t),
|
||||
field
|
||||
)
|
||||
}),
|
||||
target.map(|t| format!("{}.{}.{}", section, normalize_target_os(t), field)),
|
||||
Some(format!("{}.{}.{}", section, profile, field)),
|
||||
Some(format!("{}.{}", section, field)),
|
||||
];
|
||||
for key in keys.into_iter().flatten() {
|
||||
if let Some(val) = config.get(&key) {
|
||||
return Some(val.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn get_inherit(
|
||||
config: &Config,
|
||||
section: &str,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
) -> bool {
|
||||
get_config_value_raw(config, section, "inherit", profile, target)
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub(crate) fn get_config_value(
|
||||
config: &Config,
|
||||
section: &str,
|
||||
field: &str,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
) -> Option<String> {
|
||||
get_config_value_raw(config, section, field, profile, target)
|
||||
.and_then(|v| v.as_str().map(|s| s.trim().to_string()))
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn get_string_with_profile_and_target(
|
||||
config: &Config,
|
||||
field: &str,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
) -> String {
|
||||
if get_inherit(config, "build", profile, target) {
|
||||
get_config_value(config, "build", field, profile, target)
|
||||
.unwrap_or_else(|| get_config_str(config, &format!("build.{field}")))
|
||||
} else {
|
||||
get_config_value(config, "build", field, profile, target).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_build_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
|
||||
get_string_with_profile_and_target(config, field, profile, None)
|
||||
}
|
||||
|
||||
pub(crate) fn get_lang_string(
|
||||
config: &Config,
|
||||
lang: &str,
|
||||
field: &str,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
) -> Option<String> {
|
||||
get_config_value(config, &format!("build.{lang}"), field, profile, target)
|
||||
}
|
||||
|
||||
pub(crate) fn get_lang_list(
|
||||
config: &Config,
|
||||
lang: &str,
|
||||
field: &str,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
match get_config_value_raw(config, &format!("build.{lang}"), field, profile, target) {
|
||||
Some(val) => parse_string_array(&val, field),
|
||||
None => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_string_array(value: &toml::Value, key: &str) -> Result<Vec<String>, String> {
|
||||
let arr = value
|
||||
.as_array()
|
||||
.ok_or_else(|| format!("{key} must be an array of strings"))?;
|
||||
let mut out = Vec::new();
|
||||
for item in arr {
|
||||
let s = item
|
||||
.as_str()
|
||||
.ok_or_else(|| format!("{key} must be an array of strings"))?;
|
||||
out.push(s.to_string());
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn get_list_with_profile_and_target(
|
||||
config: &Config,
|
||||
field: &str,
|
||||
profile: &str,
|
||||
target: Option<&str>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let inherit = get_inherit(config, "build", profile, target);
|
||||
let mut out = if inherit {
|
||||
get_config_list(config, &format!("build.{field}"))?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if let Some(val) = get_config_value_raw(config, "build", field, profile, target) {
|
||||
if let Some(_arr) = val.as_array() {
|
||||
let is_base = inherit
|
||||
&& config
|
||||
.get(&format!("build.{field}"))
|
||||
.map(|v| v == &val)
|
||||
.unwrap_or(false);
|
||||
if !is_base {
|
||||
let custom = parse_string_array(&val, &format!("build.{field}"))?;
|
||||
if inherit {
|
||||
out.extend(custom);
|
||||
} else {
|
||||
out = custom;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if inherit {
|
||||
if let Some(table) = profile_table(config, profile)
|
||||
&& let Some(value) = table.get(field)
|
||||
{
|
||||
let mut extra = parse_string_array(value, &format!("build.{profile}.{field}"))?;
|
||||
out.append(&mut extra);
|
||||
}
|
||||
if let Some(t) = target {
|
||||
let normalized_t = normalize_target_os(t);
|
||||
if let Some(table) = profile_table(config, normalized_t)
|
||||
&& let Some(value) = table.get(field)
|
||||
{
|
||||
let mut extra =
|
||||
parse_string_array(value, &format!("build.{normalized_t}.{field}"))?;
|
||||
out.append(&mut extra);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn get_list_with_profile(
|
||||
config: &Config,
|
||||
field: &str,
|
||||
profile: &str,
|
||||
) -> Result<Vec<String>, String> {
|
||||
get_list_with_profile_and_target(config, field, profile, None)
|
||||
}
|
||||
|
||||
pub(crate) fn get_targets(config: &Config, profile: &str) -> Result<Vec<String>, String> {
|
||||
get_list_with_profile(config, "targets", profile)
|
||||
}
|
||||
|
||||
pub(crate) fn get_config_list(config: &Config, key: &str) -> Result<Vec<String>, String> {
|
||||
let value = match config.get(key) {
|
||||
Some(v) => v,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
let arr = value
|
||||
.as_array()
|
||||
.ok_or_else(|| format!("{key} must be an array of strings"))?;
|
||||
let mut out = Vec::new();
|
||||
for item in arr {
|
||||
let s = item
|
||||
.as_str()
|
||||
.ok_or_else(|| format!("{key} must be an array of strings"))?;
|
||||
out.push(s.to_string());
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::build::normalize_target_os;
|
||||
|
||||
#[test]
|
||||
fn test_normalize_target_os() {
|
||||
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("windows"), "x86_64-pc-windows-msvc");
|
||||
assert_eq!(
|
||||
normalize_target_os("x86_64-unknown-linux-gnu"),
|
||||
"x86_64-unknown-linux-gnu"
|
||||
);
|
||||
assert_eq!(normalize_target_os("unknown"), "unknown");
|
||||
}
|
||||
}
|
||||
398
src/core/build/steps.rs
Normal file
398
src/core/build/steps.rs
Normal file
@@ -0,0 +1,398 @@
|
||||
// DCR — Cargo-like C/C++ project manager.
|
||||
//
|
||||
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::core::build::engine::ToolchainExecs;
|
||||
use crate::core::build_config::Config;
|
||||
use crate::utils::build::{VersionInfo, profile_table, substitute_vars};
|
||||
use glob::glob;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct BuildStep {
|
||||
name: String,
|
||||
input: String,
|
||||
output: String,
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
pub(crate) struct StepVars<'a> {
|
||||
pub(crate) profile: &'a str,
|
||||
pub(crate) version: &'a str,
|
||||
pub(crate) version_major: &'a str,
|
||||
pub(crate) version_minor: &'a str,
|
||||
pub(crate) version_patch: &'a str,
|
||||
pub(crate) version_suffix: &'a str,
|
||||
pub(crate) version_suffix_dash: &'a str,
|
||||
}
|
||||
|
||||
fn get_build_steps_from_value(value: &toml::Value, key: &str) -> Result<Vec<BuildStep>, String> {
|
||||
let arr = value
|
||||
.as_array()
|
||||
.ok_or_else(|| format!("{key} must be an array"))?;
|
||||
let mut out = Vec::new();
|
||||
for item in arr {
|
||||
let tbl = item
|
||||
.as_table()
|
||||
.ok_or_else(|| format!("{key} entries must be tables"))?;
|
||||
let name = tbl
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
let input = tbl
|
||||
.get("in")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
let output = tbl
|
||||
.get("out")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
let cmd = tbl
|
||||
.get("cmd")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
if name.is_empty() || input.is_empty() || output.is_empty() || cmd.is_empty() {
|
||||
return Err(format!("{key} entries must include name, in, out, cmd"));
|
||||
}
|
||||
out.push(BuildStep {
|
||||
name,
|
||||
input,
|
||||
output,
|
||||
cmd,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn get_build_steps_with_profile(
|
||||
config: &Config,
|
||||
field: &str,
|
||||
profile: &str,
|
||||
) -> Result<Vec<BuildStep>, String> {
|
||||
if let Some(table) = profile_table(config, profile)
|
||||
&& let Some(value) = table.get(field)
|
||||
{
|
||||
return get_build_steps_from_value(value, &format!("build.{profile}.{field}"));
|
||||
}
|
||||
get_build_steps(config, &format!("build.{field}"))
|
||||
}
|
||||
|
||||
fn get_build_steps(config: &Config, key: &str) -> Result<Vec<BuildStep>, String> {
|
||||
let value = match config.get(key) {
|
||||
Some(v) => v,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
get_build_steps_from_value(value, key)
|
||||
}
|
||||
|
||||
use crate::core::build::report::{BuildEvent, BuildReporter};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
pub(crate) fn run_build_steps(
|
||||
steps: &[BuildStep],
|
||||
tools: &ToolchainExecs,
|
||||
step_flags: &str,
|
||||
vars: &StepVars,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
rep: &mut dyn BuildReporter,
|
||||
) -> Result<(), String> {
|
||||
for step in steps {
|
||||
if cancel.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return Err("Build interrupted".to_string());
|
||||
}
|
||||
run_build_step(step, tools, step_flags, vars, cancel, rep)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_build_step(
|
||||
step: &BuildStep,
|
||||
tools: &ToolchainExecs,
|
||||
step_flags: &str,
|
||||
vars: &StepVars,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
rep: &mut dyn BuildReporter,
|
||||
) -> Result<(), String> {
|
||||
let input_pattern = expand_step_value(&step.input, "", vars);
|
||||
let inputs = expand_glob(&input_pattern)?;
|
||||
if inputs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let needs_stem = step.output.contains("{stem}");
|
||||
if inputs.len() > 1 && !needs_stem {
|
||||
return Err(format!(
|
||||
"build.steps '{}' output must include {{stem}} for multiple inputs",
|
||||
step.name
|
||||
));
|
||||
}
|
||||
for input in inputs {
|
||||
if !input.is_file() {
|
||||
continue;
|
||||
}
|
||||
let stem = input.file_stem().and_then(|v| v.to_str()).unwrap_or("");
|
||||
let out_path = PathBuf::from(expand_step_value(&step.output, stem, vars));
|
||||
if !should_run_step(&input, &out_path) {
|
||||
continue;
|
||||
}
|
||||
if let Some(parent) = out_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|err| format!("Failed to create step output dir: {err}"))?;
|
||||
}
|
||||
let cmd = substitute_step_cmd(&step.cmd, &input, &out_path, tools, step_flags, stem, vars);
|
||||
let status = run_shell_command(&cmd, cancel, rep)
|
||||
.map_err(|err| format!("Failed to run step '{}': {err}", step.name))?;
|
||||
if !status.success() {
|
||||
return Err(format!("Step '{}' failed", step.name));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn clean_generated_files(patterns: &[String]) -> Result<(), String> {
|
||||
for pattern in patterns {
|
||||
for path in expand_glob(pattern)? {
|
||||
if path.is_file() {
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn expand_glob(pattern: &str) -> Result<Vec<PathBuf>, String> {
|
||||
let mut out = Vec::new();
|
||||
let entries = glob(pattern).map_err(|err| format!("glob error: {err}"))?;
|
||||
for entry in entries {
|
||||
let path = entry.map_err(|err| format!("glob error: {err}"))?;
|
||||
out.push(path);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub(crate) fn verify_expectations(patterns: &[String], vars: &StepVars) -> Result<(), String> {
|
||||
for pattern in patterns {
|
||||
let expanded = expand_step_value(pattern, "", vars);
|
||||
let matches = expand_glob(&expanded)?;
|
||||
if matches.is_empty() {
|
||||
return Err(format!("Expected artifact not found: {expanded}"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_run_step(input: &Path, output: &Path) -> bool {
|
||||
let in_time = fs::metadata(input).and_then(|m| m.modified());
|
||||
let out_time = fs::metadata(output).and_then(|m| m.modified());
|
||||
match (in_time, out_time) {
|
||||
(Ok(i), Ok(o)) => i > o,
|
||||
(Ok(_), Err(_)) => true,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn substitute_step_cmd(
|
||||
template: &str,
|
||||
input: &Path,
|
||||
output: &Path,
|
||||
tools: &ToolchainExecs,
|
||||
step_flags: &str,
|
||||
stem: &str,
|
||||
vars: &StepVars,
|
||||
) -> String {
|
||||
let info = make_version_info(vars);
|
||||
let s = substitute_vars(template, &info, vars.profile, "");
|
||||
s.replace("{in}", &input.to_string_lossy())
|
||||
.replace("{out}", &output.to_string_lossy())
|
||||
.replace("{uic}", &tools.uic)
|
||||
.replace("{moc}", &tools.moc)
|
||||
.replace("{rcc}", &tools.rcc)
|
||||
.replace("{cflags}", step_flags)
|
||||
.replace("{stem}", stem)
|
||||
}
|
||||
|
||||
pub(crate) fn build_step_flags(
|
||||
cflags: &[String],
|
||||
include_dirs: &[String],
|
||||
compiler: &str,
|
||||
) -> String {
|
||||
let mut out = Vec::new();
|
||||
let msvc_style = is_msvc_compiler(compiler) || cflags.iter().any(|f| f.starts_with('/'));
|
||||
for flag in cflags {
|
||||
if flag.starts_with("-I") || flag.starts_with("-D") {
|
||||
out.push(flag.clone());
|
||||
}
|
||||
if flag.starts_with("/I") || flag.starts_with("/D") {
|
||||
out.push(flag.clone());
|
||||
}
|
||||
if msvc_style && flag.starts_with("-D") {
|
||||
out.push(format!("/D{}", flag.trim_start_matches("-D")));
|
||||
}
|
||||
}
|
||||
for dir in include_dirs {
|
||||
out.push(format!("-I{dir}"));
|
||||
if msvc_style {
|
||||
out.push(format!("/I{dir}"));
|
||||
}
|
||||
}
|
||||
out.sort();
|
||||
out.dedup();
|
||||
out.into_iter()
|
||||
.map(quote_step_arg)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
fn quote_step_arg(arg: String) -> String {
|
||||
if !arg.chars().any(|c| c.is_whitespace() || c == '"') {
|
||||
return arg;
|
||||
}
|
||||
let escaped = arg.replace('"', "\\\"");
|
||||
format!("\"{escaped}\"")
|
||||
}
|
||||
|
||||
fn is_msvc_compiler(compiler: &str) -> bool {
|
||||
let lower = compiler.to_lowercase();
|
||||
lower.contains("cl.exe")
|
||||
|| lower == "cl"
|
||||
|| lower.contains("clang-cl")
|
||||
|| lower.contains("msvc")
|
||||
}
|
||||
|
||||
fn run_shell_command(
|
||||
cmd: &str,
|
||||
cancel: &Arc<AtomicBool>,
|
||||
rep: &mut dyn BuildReporter,
|
||||
) -> Result<std::process::ExitStatus, std::io::Error> {
|
||||
let mut child = if cfg!(target_os = "windows") {
|
||||
std::process::Command::new("cmd")
|
||||
.arg("/C")
|
||||
.arg(cmd)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
} else {
|
||||
std::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
}?;
|
||||
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
let stderr = child.stderr.take().unwrap();
|
||||
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::sync::mpsc;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
let cancel_clone = cancel.clone();
|
||||
let tx_stdout = tx.clone();
|
||||
let stdout_thread = std::thread::spawn(move || {
|
||||
let reader = BufReader::new(stdout);
|
||||
for line in reader.lines() {
|
||||
if cancel_clone.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
if let Ok(line) = line {
|
||||
let _ = tx_stdout.send(("stdout", line));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let cancel_clone = cancel.clone();
|
||||
let tx_stderr = tx.clone();
|
||||
let stderr_thread = std::thread::spawn(move || {
|
||||
let reader = BufReader::new(stderr);
|
||||
for line in reader.lines() {
|
||||
if cancel_clone.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
if let Ok(line) = line {
|
||||
let _ = tx_stderr.send(("stderr", line));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let status = child.wait()?;
|
||||
let _ = stdout_thread.join();
|
||||
let _ = stderr_thread.join();
|
||||
|
||||
drop(tx);
|
||||
while let Ok((stream, text)) = rx.recv() {
|
||||
rep.on_event(BuildEvent::CompilerOutput {
|
||||
stream,
|
||||
text: &text,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
pub(crate) fn build_steps_need_run(steps: &[BuildStep], vars: &StepVars) -> Result<bool, String> {
|
||||
for step in steps {
|
||||
let input_pattern = expand_step_value(&step.input, "", vars);
|
||||
let inputs = expand_glob(&input_pattern)?;
|
||||
if inputs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let needs_stem = step.output.contains("{stem}");
|
||||
if inputs.len() > 1 && !needs_stem {
|
||||
return Err(format!(
|
||||
"build.steps '{}' output must include {{stem}} for multiple inputs",
|
||||
step.name
|
||||
));
|
||||
}
|
||||
for input in inputs {
|
||||
if !input.is_file() {
|
||||
continue;
|
||||
}
|
||||
let stem = input.file_stem().and_then(|v| v.to_str()).unwrap_or("");
|
||||
let out_path = PathBuf::from(expand_step_value(&step.output, stem, vars));
|
||||
if should_run_step(&input, &out_path) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn make_version_info(vars: &StepVars) -> VersionInfo {
|
||||
VersionInfo {
|
||||
full: vars.version.to_string(),
|
||||
major: vars.version_major.to_string(),
|
||||
minor: vars.version_minor.to_string(),
|
||||
patch: vars.version_patch.to_string(),
|
||||
suffix: vars.version_suffix.to_string(),
|
||||
suffix_dash: vars.version_suffix_dash.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_step_value(template: &str, stem: &str, vars: &StepVars) -> String {
|
||||
let info = make_version_info(vars);
|
||||
let s = substitute_vars(template, &info, vars.profile, "");
|
||||
s.replace("{stem}", stem)
|
||||
}
|
||||
@@ -21,6 +21,7 @@ use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use toml::Value;
|
||||
use toml::map::Map;
|
||||
use toml_edit::{DocumentMut, Item, Table};
|
||||
|
||||
const DEFAULT_VERSION: &str = "0.1.0";
|
||||
const DEFAULT_LANGUAGE: &str = "c";
|
||||
@@ -35,6 +36,7 @@ const VALID_KINDS: &[&str] = &[
|
||||
"elf",
|
||||
"none",
|
||||
"custom",
|
||||
"flat-bin",
|
||||
];
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -42,6 +44,7 @@ pub enum ConfigError {
|
||||
Io(std::io::Error),
|
||||
TomlDe(toml::de::Error),
|
||||
TomlSer(toml::ser::Error),
|
||||
TomlEdit(toml_edit::TomlError),
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
@@ -51,6 +54,7 @@ impl std::fmt::Display for ConfigError {
|
||||
ConfigError::Io(err) => write!(f, "I/O error: {err}"),
|
||||
ConfigError::TomlDe(err) => write!(f, "TOML parse error: {err}"),
|
||||
ConfigError::TomlSer(err) => write!(f, "TOML serialize error: {err}"),
|
||||
ConfigError::TomlEdit(err) => write!(f, "TOML parse error: {err}"),
|
||||
ConfigError::Invalid(msg) => write!(f, "Invalid config: {msg}"),
|
||||
}
|
||||
}
|
||||
@@ -80,6 +84,10 @@ pub struct Config {
|
||||
path: PathBuf,
|
||||
data: Value,
|
||||
typed: DcrConfig,
|
||||
/// Editable document mirror of `data`, used for serialization. It preserves
|
||||
/// comments, formatting, and any keys not modeled by the typed/raw views, so
|
||||
/// `save()` never drops unknown sections (e.g. `[run]`, `[[build.post_steps]]`).
|
||||
doc: DocumentMut,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -95,6 +103,28 @@ pub struct DcrConfig {
|
||||
pub toolchain: Option<ToolchainConfig>,
|
||||
#[serde(default)]
|
||||
pub workspace: BTreeMap<String, WorkspaceMemberConfig>,
|
||||
#[serde(default)]
|
||||
pub archive: Option<ArchiveConfig>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ArchiveConfig {
|
||||
pub output: String,
|
||||
pub format: String,
|
||||
pub size: Option<String>,
|
||||
pub offset: Option<String>,
|
||||
pub label: Option<String>,
|
||||
pub bootsector: Option<String>,
|
||||
#[serde(default)]
|
||||
pub layout: Vec<ArchiveLayout>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ArchiveLayout {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@@ -105,14 +135,24 @@ pub struct PackageConfig {
|
||||
pub pkg_type: Option<String>,
|
||||
}
|
||||
|
||||
fn default_language() -> LanguageConfig {
|
||||
LanguageConfig::One(DEFAULT_LANGUAGE.to_string())
|
||||
}
|
||||
|
||||
fn default_compiler() -> String {
|
||||
DEFAULT_COMPILER.to_string()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct BuildConfig {
|
||||
#[serde(default = "default_language")]
|
||||
pub language: LanguageConfig,
|
||||
#[serde(default)]
|
||||
pub standard: Option<String>,
|
||||
#[serde(default)]
|
||||
pub cxx_standard: Option<String>,
|
||||
#[serde(default = "default_compiler")]
|
||||
pub compiler: String,
|
||||
#[serde(default)]
|
||||
pub kind: Option<String>,
|
||||
@@ -182,20 +222,29 @@ pub struct WorkspaceMemberConfig {
|
||||
pub path: String,
|
||||
#[serde(default)]
|
||||
pub deps: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub main: Option<bool>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(path: &str) -> Result<Self, ConfigError> {
|
||||
let path = PathBuf::from(path);
|
||||
let data = if path.exists() {
|
||||
read_toml(&path)?
|
||||
let (data, doc) = if path.exists() {
|
||||
load_parts(&path)?
|
||||
} else {
|
||||
let default_value = default_toml()?;
|
||||
write_toml(&path, &default_value)?;
|
||||
default_value
|
||||
let content = default_toml_text();
|
||||
let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?;
|
||||
fs::write(&path, doc.to_string())?;
|
||||
let data: Value = toml::from_str(&content)?;
|
||||
(data, doc)
|
||||
};
|
||||
let typed = parse_typed_config(&data)?;
|
||||
let cfg = Self { path, data, typed };
|
||||
let cfg = Self {
|
||||
path,
|
||||
data,
|
||||
typed,
|
||||
doc,
|
||||
};
|
||||
cfg.validate()?;
|
||||
Ok(cfg)
|
||||
}
|
||||
@@ -205,9 +254,14 @@ impl Config {
|
||||
if !path.exists() {
|
||||
return Err(ConfigError::Invalid("dcr.toml not found".into()));
|
||||
}
|
||||
let data = read_toml(&path)?;
|
||||
let (data, doc) = load_parts(&path)?;
|
||||
let typed = parse_typed_config(&data)?;
|
||||
let cfg = Self { path, data, typed };
|
||||
let cfg = Self {
|
||||
path,
|
||||
data,
|
||||
typed,
|
||||
doc,
|
||||
};
|
||||
cfg.validate()?;
|
||||
Ok(cfg)
|
||||
}
|
||||
@@ -347,6 +401,13 @@ impl Config {
|
||||
.as_ref()
|
||||
.ok_or_else(|| ConfigError::Invalid("missing [build] section (required)".into()))?;
|
||||
|
||||
// For workspace_only projects, [build] section is optional
|
||||
// and language/compiler are not required
|
||||
if build.workspace_only {
|
||||
self.validate_workspace()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if pkg.name.trim().is_empty() {
|
||||
return Err(ConfigError::Invalid("package.name is empty".into()));
|
||||
}
|
||||
@@ -421,22 +482,35 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<(), ConfigError> {
|
||||
write_toml(&self.path, &self.data)
|
||||
fs::write(&self.path, self.doc.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set(&mut self, key: &str, value: Value) -> Result<(), ConfigError> {
|
||||
let parts: Vec<&str> = key.split('.').collect();
|
||||
let previous = self.data.clone();
|
||||
set_path(&mut self.data, &parts, value)?;
|
||||
let previous_data = self.data.clone();
|
||||
let previous_doc = self.doc.clone();
|
||||
|
||||
if let Err(err) = set_doc_path(&mut self.doc, &parts, &value) {
|
||||
self.doc = previous_doc;
|
||||
return Err(err);
|
||||
}
|
||||
if let Err(err) = set_path(&mut self.data, &parts, value) {
|
||||
self.data = previous_data;
|
||||
self.doc = previous_doc;
|
||||
return Err(err);
|
||||
}
|
||||
self.typed = match parse_typed_config(&self.data) {
|
||||
Ok(typed) => typed,
|
||||
Err(err) => {
|
||||
self.data = previous;
|
||||
self.data = previous_data;
|
||||
self.doc = previous_doc;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
if let Err(err) = self.validate() {
|
||||
self.data = previous;
|
||||
self.data = previous_data;
|
||||
self.doc = previous_doc;
|
||||
self.typed = parse_typed_config(&self.data)?;
|
||||
return Err(err);
|
||||
}
|
||||
@@ -658,235 +732,119 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_toml(path: &Path) -> Result<Value, ConfigError> {
|
||||
fn load_parts(path: &Path) -> Result<(Value, DocumentMut), ConfigError> {
|
||||
let content = fs::read_to_string(path)?;
|
||||
Ok(toml::from_str(&content)?)
|
||||
let data: Value = toml::from_str(&content)?;
|
||||
let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?;
|
||||
Ok((data, doc))
|
||||
}
|
||||
|
||||
fn write_toml(path: &Path, value: &Value) -> Result<(), ConfigError> {
|
||||
let content = format_toml(value)?;
|
||||
fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn default_toml() -> Result<Value, ConfigError> {
|
||||
fn default_toml_text() -> String {
|
||||
let name = std::env::current_dir()
|
||||
.ok()
|
||||
.and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string()))
|
||||
.unwrap_or_else(|| "project".to_string());
|
||||
|
||||
let mut package = Map::new();
|
||||
package.insert("name".to_string(), Value::String(name));
|
||||
package.insert(
|
||||
"version".to_string(),
|
||||
Value::String(DEFAULT_VERSION.to_string()),
|
||||
);
|
||||
package.insert("type".to_string(), Value::String("none".to_string()));
|
||||
package.insert("description".to_string(), Value::String("".to_string()));
|
||||
package.insert("author".to_string(), Value::String("".to_string()));
|
||||
package.insert(
|
||||
"license".to_string(),
|
||||
Value::String("GPL-3.0-or-later".to_string()),
|
||||
);
|
||||
|
||||
let mut build = Map::new();
|
||||
build.insert(
|
||||
"language".to_string(),
|
||||
Value::String(DEFAULT_LANGUAGE.to_string()),
|
||||
);
|
||||
build.insert(
|
||||
"standard".to_string(),
|
||||
Value::String(DEFAULT_STANDARD.to_string()),
|
||||
);
|
||||
build.insert(
|
||||
"compiler".to_string(),
|
||||
Value::String(DEFAULT_COMPILER.to_string()),
|
||||
);
|
||||
build.insert("kind".to_string(), Value::String(DEFAULT_KIND.to_string()));
|
||||
|
||||
let mut root = Map::new();
|
||||
root.insert("package".to_string(), Value::Table(package));
|
||||
root.insert("build".to_string(), Value::Table(build));
|
||||
root.insert("dependencies".to_string(), Value::Table(Map::new()));
|
||||
|
||||
Ok(Value::Table(root))
|
||||
format!(
|
||||
"[package]\n\
|
||||
name = \"{name}\"\n\
|
||||
version = \"{DEFAULT_VERSION}\"\n\
|
||||
type = \"none\"\n\
|
||||
description = \"\"\n\
|
||||
author = \"\"\n\
|
||||
license = \"GPL-3.0-or-later\"\n\
|
||||
\n\
|
||||
[build]\n\
|
||||
language = \"{DEFAULT_LANGUAGE}\"\n\
|
||||
standard = \"{DEFAULT_STANDARD}\"\n\
|
||||
compiler = \"{DEFAULT_COMPILER}\"\n\
|
||||
kind = \"{DEFAULT_KIND}\"\n\
|
||||
\n\
|
||||
[dependencies]\n"
|
||||
)
|
||||
}
|
||||
|
||||
fn format_toml(value: &Value) -> Result<String, ConfigError> {
|
||||
let root = value
|
||||
.as_table()
|
||||
.ok_or_else(|| ConfigError::Invalid("root is not a table".into()))?;
|
||||
|
||||
let package = root
|
||||
.get("package")
|
||||
.and_then(|v| v.as_table())
|
||||
.ok_or_else(|| ConfigError::Invalid("missing [package]".into()))?;
|
||||
let build = root
|
||||
.get("build")
|
||||
.and_then(|v| v.as_table())
|
||||
.ok_or_else(|| ConfigError::Invalid("missing [build]".into()))?;
|
||||
let deps = root
|
||||
.get("dependencies")
|
||||
.and_then(|v| v.as_table())
|
||||
.ok_or_else(|| ConfigError::Invalid("missing [dependencies]".into()))?;
|
||||
let toolchain = root.get("toolchain").and_then(|v| v.as_table());
|
||||
|
||||
let name = package.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let version = package
|
||||
.get("version")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let language_value = build.get("language");
|
||||
let language = match language_value {
|
||||
Some(Value::String(s)) => s.to_string(),
|
||||
Some(Value::Array(arr)) => format_string_array(&Value::Array(arr.clone())),
|
||||
_ => "".to_string(),
|
||||
};
|
||||
let standard = build.get("standard").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let compiler = build.get("compiler").and_then(|v| v.as_str()).unwrap_or("");
|
||||
|
||||
let mut out = String::new();
|
||||
out.push_str("[package]\n");
|
||||
out.push_str(&format!("name = \"{name}\"\n"));
|
||||
out.push_str(&format!("version = \"{version}\"\n"));
|
||||
|
||||
for key in [
|
||||
"type",
|
||||
"description",
|
||||
"author",
|
||||
"authors",
|
||||
"homepage",
|
||||
"license",
|
||||
"repository",
|
||||
"readme",
|
||||
"keywords",
|
||||
"categories",
|
||||
] {
|
||||
if let Some(val) = package.get(key) {
|
||||
match val {
|
||||
Value::String(s) => out.push_str(&format!("{key} = \"{s}\"\n")),
|
||||
Value::Array(_) => out.push_str(&format!("{key} = {}\n", format_string_array(val))),
|
||||
_ => {}
|
||||
}
|
||||
fn set_doc_path(doc: &mut DocumentMut, path: &[&str], value: &Value) -> Result<(), ConfigError> {
|
||||
let mut current = doc.as_table_mut();
|
||||
for &key in &path[..path.len().saturating_sub(1)] {
|
||||
if !current.contains_key(key) {
|
||||
current.insert(key, Item::Table(Table::new()));
|
||||
}
|
||||
current = current
|
||||
.get_mut(key)
|
||||
.and_then(|item| item.as_table_mut())
|
||||
.ok_or_else(|| ConfigError::Invalid(format!("'{key}' is not a table")))?;
|
||||
}
|
||||
out.push('\n');
|
||||
|
||||
out.push_str("[build]\n");
|
||||
if language.starts_with('[') {
|
||||
out.push_str(&format!("language = {language}\n"));
|
||||
if let Some(&last) = path.last() {
|
||||
current.insert(last, value_to_item(value));
|
||||
Ok(())
|
||||
} else {
|
||||
out.push_str(&format!("language = \"{language}\"\n"));
|
||||
Err(ConfigError::Invalid("empty key".into()))
|
||||
}
|
||||
if !standard.is_empty() {
|
||||
out.push_str(&format!("standard = \"{standard}\"\n"));
|
||||
}
|
||||
out.push_str(&format!("compiler = \"{compiler}\"\n"));
|
||||
let kind = build
|
||||
.get("kind")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(DEFAULT_KIND);
|
||||
out.push_str(&format!("kind = \"{kind}\"\n"));
|
||||
if let Some(target) = build.get("target").and_then(|v| v.as_str())
|
||||
&& !target.trim().is_empty()
|
||||
{
|
||||
out.push_str(&format!("target = \"{target}\"\n"));
|
||||
}
|
||||
if let Some(out_dir) = build.get("out_dir").and_then(|v| v.as_str())
|
||||
&& !out_dir.trim().is_empty()
|
||||
{
|
||||
out.push_str(&format!("out_dir = \"{out_dir}\"\n"));
|
||||
}
|
||||
if let Some(cflags) = build.get("cflags") {
|
||||
out.push_str(&format!("cflags = {}\n", format_string_array(cflags)));
|
||||
}
|
||||
if let Some(ldflags) = build.get("ldflags") {
|
||||
out.push_str(&format!("ldflags = {}\n", format_string_array(ldflags)));
|
||||
}
|
||||
out.push('\n');
|
||||
|
||||
if let Some(toolchain) = toolchain {
|
||||
let mut lines = Vec::new();
|
||||
for key in ["cc", "cxx", "as", "ar", "ld"] {
|
||||
if let Some(value) = toolchain.get(key).and_then(|v| v.as_str())
|
||||
&& !value.trim().is_empty()
|
||||
{
|
||||
lines.push(format!("{key} = \"{value}\""));
|
||||
}
|
||||
}
|
||||
if !lines.is_empty() {
|
||||
out.push_str("[toolchain]\n");
|
||||
for line in lines {
|
||||
out.push_str(&format!("{line}\n"));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
out.push_str("[dependencies]\n");
|
||||
if !deps.is_empty() {
|
||||
let mut keys: Vec<&String> = deps.keys().collect();
|
||||
keys.sort();
|
||||
for key in keys {
|
||||
if let Some(val) = deps.get(key) {
|
||||
out.push_str(&format!("{key} = {}\n", format_dep_value(val)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn format_dep_value(value: &Value) -> String {
|
||||
fn value_to_item(value: &Value) -> Item {
|
||||
Item::Value(value_to_edit_value(value))
|
||||
}
|
||||
|
||||
fn value_to_edit_value(value: &Value) -> toml_edit::Value {
|
||||
match value {
|
||||
Value::String(s) => format!("\"{s}\""),
|
||||
Value::Table(tbl) => {
|
||||
let mut parts = Vec::new();
|
||||
if let Some(v) = tbl.get("version").and_then(|v| v.as_str()) {
|
||||
parts.push(format!("version = \"{v}\""));
|
||||
}
|
||||
if let Some(v) = tbl.get("path").and_then(|v| v.as_str()) {
|
||||
parts.push(format!("path = \"{v}\""));
|
||||
}
|
||||
if let Some(v) = tbl.get("git").and_then(|v| v.as_str()) {
|
||||
parts.push(format!("git = \"{v}\""));
|
||||
}
|
||||
if let Some(v) = tbl.get("branch").and_then(|v| v.as_str()) {
|
||||
parts.push(format!("branch = \"{v}\""));
|
||||
}
|
||||
if let Some(v) = tbl.get("tag").and_then(|v| v.as_str()) {
|
||||
parts.push(format!("tag = \"{v}\""));
|
||||
}
|
||||
if let Some(v) = tbl.get("rev").and_then(|v| v.as_str()) {
|
||||
parts.push(format!("rev = \"{v}\""));
|
||||
}
|
||||
if let Some(v) = tbl.get("default-features").and_then(|v| v.as_bool()) {
|
||||
parts.push(format!(
|
||||
"default-features = {}",
|
||||
if v { "true" } else { "false" }
|
||||
));
|
||||
}
|
||||
if let Some(v) = tbl.get("features") {
|
||||
parts.push(format!("features = {}", format_string_array(v)));
|
||||
}
|
||||
if let Some(v) = tbl.get("system").and_then(|v| v.as_bool()) {
|
||||
parts.push(format!("system = {}", if v { "true" } else { "false" }));
|
||||
}
|
||||
format!("{{ {} }}", parts.join(", "))
|
||||
Value::String(s) => toml_edit::Value::from(s.clone()),
|
||||
Value::Integer(i) => toml_edit::Value::from(*i),
|
||||
Value::Float(f) => toml_edit::Value::from(*f),
|
||||
Value::Boolean(b) => toml_edit::Value::from(*b),
|
||||
Value::Datetime(dt) => {
|
||||
let s = dt.to_string();
|
||||
s.parse::<toml_edit::Datetime>()
|
||||
.map(toml_edit::Value::from)
|
||||
.unwrap_or_else(|_| toml_edit::Value::from(s))
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
let mut out = toml_edit::Array::new();
|
||||
for item in arr {
|
||||
out.push(value_to_edit_value(item));
|
||||
}
|
||||
toml_edit::Value::Array(out)
|
||||
}
|
||||
Value::Table(tbl) => {
|
||||
let mut out = toml_edit::InlineTable::new();
|
||||
for key in ordered_table_keys(tbl) {
|
||||
if let Some(v) = tbl.get(&key) {
|
||||
out.insert(&key, value_to_edit_value(v));
|
||||
}
|
||||
}
|
||||
toml_edit::Value::InlineTable(out)
|
||||
}
|
||||
_ => "\"\"".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_string_array(value: &Value) -> String {
|
||||
if let Some(arr) = value.as_array() {
|
||||
let items: Vec<String> = arr
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| format!("\"{s}\"")))
|
||||
.collect();
|
||||
return format!("[{}]", items.join(", "));
|
||||
}
|
||||
"[]".to_string()
|
||||
const DEP_KEY_ORDER: &[&str] = &[
|
||||
"version",
|
||||
"path",
|
||||
"git",
|
||||
"branch",
|
||||
"tag",
|
||||
"rev",
|
||||
"default-features",
|
||||
"features",
|
||||
"system",
|
||||
];
|
||||
|
||||
fn ordered_table_keys(tbl: &Map<String, Value>) -> Vec<String> {
|
||||
let mut keys: Vec<String> = DEP_KEY_ORDER
|
||||
.iter()
|
||||
.filter(|k| tbl.contains_key(**k))
|
||||
.map(|k| (*k).to_string())
|
||||
.collect();
|
||||
let mut rest: Vec<String> = tbl
|
||||
.keys()
|
||||
.filter(|k| !DEP_KEY_ORDER.contains(&k.as_str()))
|
||||
.cloned()
|
||||
.collect();
|
||||
rest.sort();
|
||||
keys.extend(rest);
|
||||
keys
|
||||
}
|
||||
|
||||
fn get_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> {
|
||||
@@ -1135,6 +1093,84 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_preserves_untyped_sections() {
|
||||
let dir = temp_dir("preserve_untyped");
|
||||
let path = write_toml_file(
|
||||
&dir,
|
||||
"# top comment\n\
|
||||
[package]\n\
|
||||
name = \"test\"\n\
|
||||
version = \"0.1.0\"\n\n\
|
||||
[build]\n\
|
||||
language = \"c\"\n\
|
||||
standard = \"c11\"\n\
|
||||
compiler = \"clang\"\n\
|
||||
kind = \"elf\"\n\
|
||||
filename = \"KERNEL\"\n\
|
||||
extension = \"ELF\"\n\
|
||||
inherit = false\n\
|
||||
include = [\"src/include\"]\n\n\
|
||||
[[build.post_steps]]\n\
|
||||
name = \"iso\"\n\
|
||||
cmd = \"grub-mkrescue\"\n\n\
|
||||
[run]\n\
|
||||
cmd = \"qemu-system-aarch64 -kernel KERNEL\"\n\n\
|
||||
[dependencies]\n",
|
||||
);
|
||||
|
||||
let mut config = Config::open(&path.to_string_lossy()).unwrap();
|
||||
config
|
||||
.set("dependencies.zlib", {
|
||||
let mut tbl = Map::new();
|
||||
tbl.insert("path".to_string(), Value::String("../zlib".to_string()));
|
||||
Value::Table(tbl)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Reload from disk and confirm nothing modeled-but-not-whitelisted was dropped.
|
||||
let reloaded = Config::open(&path.to_string_lossy()).unwrap();
|
||||
assert_eq!(
|
||||
reloaded.get("build.filename").and_then(|v| v.as_str()),
|
||||
Some("KERNEL"),
|
||||
"build.filename must survive save"
|
||||
);
|
||||
assert_eq!(
|
||||
reloaded.get("build.extension").and_then(|v| v.as_str()),
|
||||
Some("ELF")
|
||||
);
|
||||
assert_eq!(
|
||||
reloaded.get("build.inherit").and_then(|v| v.as_bool()),
|
||||
Some(false)
|
||||
);
|
||||
assert!(
|
||||
reloaded
|
||||
.get("build.include")
|
||||
.and_then(|v| v.as_array())
|
||||
.is_some(),
|
||||
"build.include array must survive save"
|
||||
);
|
||||
assert_eq!(
|
||||
reloaded.get("run.cmd").and_then(|v| v.as_str()),
|
||||
Some("qemu-system-aarch64 -kernel KERNEL"),
|
||||
"[run] section must survive save"
|
||||
);
|
||||
assert!(
|
||||
reloaded.get("build.post_steps").is_some(),
|
||||
"[[build.post_steps]] must survive save"
|
||||
);
|
||||
|
||||
let saved = fs::read_to_string(&path).unwrap();
|
||||
assert!(
|
||||
saved.contains("zlib = { path = \"../zlib\" }"),
|
||||
"dependency must be an inline table, got:\n{saved}"
|
||||
);
|
||||
assert!(
|
||||
saved.contains("# top comment"),
|
||||
"leading comment must be preserved, got:\n{saved}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_creates_default_config() {
|
||||
let dir = temp_dir("new_default");
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
use crate::core::builder::BuildContext;
|
||||
use crate::core::builder::common;
|
||||
use crate::platform;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::time::Instant;
|
||||
|
||||
pub fn build_assembly<F>(
|
||||
ctx: &BuildContext,
|
||||
backend_name: &str,
|
||||
default_assembler: &str,
|
||||
extensions: &[&str],
|
||||
build_object: F,
|
||||
) -> Result<f64, String>
|
||||
where
|
||||
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
|
||||
{
|
||||
let lang = ctx.language.to_lowercase();
|
||||
if lang.split(',').any(|p| p.trim() != "asm") {
|
||||
return Err(format!(
|
||||
"{backend_name} backend requires build.language = \"asm\""
|
||||
));
|
||||
}
|
||||
|
||||
let assembler = if ctx.compiler.is_empty() {
|
||||
default_assembler
|
||||
} else {
|
||||
ctx.compiler
|
||||
};
|
||||
|
||||
let start_time = Instant::now();
|
||||
let sources = common::collect_sources(
|
||||
ctx.source_roots,
|
||||
extensions,
|
||||
ctx.exclude_dirs,
|
||||
ctx.include_paths,
|
||||
)?;
|
||||
let obj_dir = match ctx.target_dir {
|
||||
Some(dir) => Path::new(dir).join("obj"),
|
||||
None => Path::new("./target").join(ctx.profile).join("obj"),
|
||||
};
|
||||
let objects = build_objects_common(assembler, &sources, &obj_dir, ctx, &build_object)?;
|
||||
|
||||
if ctx.kind == "staticlib" {
|
||||
return archive_staticlib(ctx, &objects, start_time);
|
||||
}
|
||||
|
||||
link_objects(ctx, &objects, start_time)
|
||||
}
|
||||
|
||||
fn build_objects_common<F>(
|
||||
assembler: &str,
|
||||
sources: &[String],
|
||||
obj_dir: &Path,
|
||||
ctx: &BuildContext,
|
||||
build_object: &F,
|
||||
) -> Result<Vec<String>, String>
|
||||
where
|
||||
F: Fn(&str, &str, &str, &BuildContext) -> Result<(), String> + Sync,
|
||||
{
|
||||
let objects: Vec<String> = sources
|
||||
.iter()
|
||||
.map(|s| common::object_path(obj_dir, s, "o"))
|
||||
.collect();
|
||||
|
||||
common::parallel_build(
|
||||
sources.len(),
|
||||
|i| build_object(assembler, &sources[i], &objects[i], ctx),
|
||||
ctx.codegen_units,
|
||||
)?;
|
||||
|
||||
Ok(objects)
|
||||
}
|
||||
|
||||
fn archive_staticlib(
|
||||
ctx: &BuildContext,
|
||||
objects: &[String],
|
||||
start_time: Instant,
|
||||
) -> Result<f64, String> {
|
||||
let lib_path = platform::lib_path(ctx.profile, ctx.project_name, ctx.target_dir);
|
||||
if !common::needs_link(objects, &lib_path) {
|
||||
return Ok(common::elapsed_secs(start_time));
|
||||
}
|
||||
|
||||
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
|
||||
"lib"
|
||||
} else {
|
||||
"ar"
|
||||
});
|
||||
|
||||
let mut cmd = Command::new(archiver);
|
||||
if cfg!(target_os = "windows") && archiver == "lib" {
|
||||
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
|
||||
} else {
|
||||
cmd.arg("rcs").arg(&lib_path);
|
||||
}
|
||||
for obj in objects {
|
||||
cmd.arg(obj);
|
||||
}
|
||||
|
||||
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
|
||||
eprintln!("[dcr] {:?}", cmd);
|
||||
}
|
||||
|
||||
match cmd.status() {
|
||||
Ok(status) if status.success() => Ok(common::elapsed_secs(start_time)),
|
||||
Ok(_) => Err("Build failed".to_string()),
|
||||
Err(err) => Err(format!("Build failed: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn link_objects(
|
||||
ctx: &BuildContext,
|
||||
objects: &[String],
|
||||
start_time: Instant,
|
||||
) -> Result<f64, String> {
|
||||
let mut cmd = Command::new(ctx.linker.unwrap_or("cc"));
|
||||
if ctx.kind == "sharedlib" {
|
||||
if cfg!(target_os = "macos") {
|
||||
cmd.arg("-dynamiclib");
|
||||
} else {
|
||||
cmd.arg("-shared");
|
||||
}
|
||||
}
|
||||
|
||||
for obj in objects {
|
||||
cmd.arg(obj);
|
||||
}
|
||||
for dir in ctx.lib_dirs {
|
||||
cmd.arg(format!("-L{dir}"));
|
||||
}
|
||||
for lib in ctx.libs {
|
||||
cmd.arg(format!("-l{lib}"));
|
||||
}
|
||||
for flag in ctx.ldflags {
|
||||
cmd.arg(flag);
|
||||
}
|
||||
|
||||
let name = ctx.output_filename.unwrap_or(ctx.project_name);
|
||||
let ext = ctx.output_extension.unwrap_or("");
|
||||
let final_name = if ext.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{}.{}", name, ext)
|
||||
};
|
||||
|
||||
let out_path = if ctx.kind == "sharedlib" {
|
||||
platform::shared_lib_path(ctx.profile, &final_name, ctx.target_dir)
|
||||
} else if ctx.kind == "elf" {
|
||||
platform::elf_path(ctx.profile, &final_name, ctx.target_dir)
|
||||
} else {
|
||||
platform::bin_path(ctx.profile, &final_name, ctx.target_dir)
|
||||
};
|
||||
|
||||
if !common::needs_link(objects, &out_path) {
|
||||
return Ok(common::elapsed_secs(start_time));
|
||||
}
|
||||
|
||||
cmd.arg("-o").arg(out_path);
|
||||
|
||||
if ctx.verbose || std::env::var("DCR_DEBUG").is_ok() {
|
||||
eprintln!("[dcr] {:?}", cmd);
|
||||
}
|
||||
|
||||
match cmd.status() {
|
||||
Ok(status) if status.success() => Ok(common::elapsed_secs(start_time)),
|
||||
Ok(_) => Err("Build failed".to_string()),
|
||||
Err(err) => Err(format!("Build failed: {err}")),
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// DCR — Cargo-like C/C++ project manager.
|
||||
//
|
||||
// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) <main@dexoron.su>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
pub mod asm;
|
||||
pub mod common;
|
||||
pub mod fasm;
|
||||
pub mod gas;
|
||||
pub mod llvm_ir;
|
||||
pub mod masm;
|
||||
pub mod msvc;
|
||||
pub mod nasm;
|
||||
pub mod qt;
|
||||
pub mod unix_cc;
|
||||
|
||||
pub struct BuildContext<'a> {
|
||||
pub profile: &'a str,
|
||||
pub project_name: &'a str,
|
||||
pub compiler: &'a str,
|
||||
pub language: &'a str,
|
||||
pub standard: &'a str,
|
||||
pub cxx_standard: &'a str,
|
||||
pub target: Option<&'a str>,
|
||||
pub target_dir: Option<&'a str>,
|
||||
pub kind: &'a str,
|
||||
pub platform: Option<&'a str>,
|
||||
pub linker: Option<&'a str>,
|
||||
pub archiver: Option<&'a str>,
|
||||
pub moc: Option<&'a str>,
|
||||
pub uic: Option<&'a str>,
|
||||
pub rcc: Option<&'a str>,
|
||||
pub package_type: Option<&'a str>,
|
||||
pub freestanding: bool,
|
||||
pub panic_abort: bool,
|
||||
pub codegen_units: usize,
|
||||
pub source_roots: &'a [std::path::PathBuf],
|
||||
pub exclude_dirs: &'a [std::path::PathBuf],
|
||||
pub include_paths: &'a [String],
|
||||
pub include_dirs: &'a [String],
|
||||
pub lib_dirs: &'a [String],
|
||||
pub libs: &'a [String],
|
||||
pub cflags: &'a [String],
|
||||
pub ldflags: &'a [String],
|
||||
pub output_filename: Option<&'a str>,
|
||||
pub output_extension: Option<&'a str>,
|
||||
pub verbose: bool,
|
||||
pub qt: bool,
|
||||
}
|
||||
|
||||
pub fn build(ctx: &BuildContext) -> Result<f64, String> {
|
||||
let compiler = ctx.compiler.to_lowercase();
|
||||
if !check_compiler_exists(ctx.compiler) {
|
||||
return Err(format!(
|
||||
"Compiler not found: {}. Make sure it is installed and available in PATH.",
|
||||
ctx.compiler
|
||||
));
|
||||
}
|
||||
if compiler.contains("clang-cl") {
|
||||
return msvc::build(ctx);
|
||||
}
|
||||
if compiler == "as" || compiler.contains("gas") {
|
||||
return gas::build(ctx);
|
||||
}
|
||||
if compiler.contains("llc") {
|
||||
return llvm_ir::build(ctx);
|
||||
}
|
||||
if compiler.contains("fasm") {
|
||||
return fasm::build(ctx);
|
||||
}
|
||||
if compiler.contains("nasm") {
|
||||
return nasm::build(ctx);
|
||||
}
|
||||
if compiler == "ml" || compiler == "ml64" || compiler.contains("masm") {
|
||||
return masm::build(ctx);
|
||||
}
|
||||
if compiler == "cl" || compiler.contains("msvc") {
|
||||
return msvc::build(ctx);
|
||||
}
|
||||
unix_cc::build(ctx)
|
||||
}
|
||||
|
||||
fn check_compiler_exists(compiler: &str) -> bool {
|
||||
let name = if compiler.is_empty() { "cc" } else { compiler };
|
||||
std::process::Command::new(name)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {
|
||||
let compiler = ctx.compiler.to_lowercase();
|
||||
if compiler.contains("clang-cl") || compiler == "cl" || compiler.contains("msvc") {
|
||||
return msvc::collect_sources(ctx);
|
||||
}
|
||||
if compiler == "as" || compiler.contains("gas") {
|
||||
return gas::collect_sources(ctx);
|
||||
}
|
||||
if compiler.contains("llc") {
|
||||
return llvm_ir::collect_sources(ctx);
|
||||
}
|
||||
if compiler.contains("fasm") {
|
||||
return fasm::collect_sources(ctx);
|
||||
}
|
||||
if compiler.contains("nasm") {
|
||||
return nasm::collect_sources(ctx);
|
||||
}
|
||||
if compiler == "ml" || compiler == "ml64" || compiler.contains("masm") {
|
||||
return masm::collect_sources(ctx);
|
||||
}
|
||||
unix_cc::collect_sources(ctx)
|
||||
}
|
||||
@@ -103,6 +103,10 @@ pub fn resolve_deps(
|
||||
}
|
||||
} else {
|
||||
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
|
||||
push_if_exists(
|
||||
&mut resolved.include_dirs,
|
||||
&dep_root.join("target").join("include"),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(lib_dirs) = table.get("lib").and_then(|v| v.as_array()) {
|
||||
@@ -128,6 +132,10 @@ pub fn resolve_deps(
|
||||
}
|
||||
} else {
|
||||
push_if_exists(&mut resolved.include_dirs, &dep_root.join("include"));
|
||||
push_if_exists(
|
||||
&mut resolved.include_dirs,
|
||||
&dep_root.join("target").join("include"),
|
||||
);
|
||||
push_default_lib_dirs(&mut resolved.lib_dirs, &dep_root);
|
||||
resolved.libs.push(name.clone());
|
||||
}
|
||||
@@ -234,10 +242,13 @@ mod tests {
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
std::fs::create_dir_all(root.join("target/lib")).unwrap();
|
||||
std::fs::create_dir_all(root.join("target").join("lib")).unwrap();
|
||||
let mut paths = Vec::new();
|
||||
push_default_lib_dirs(&mut paths, &root);
|
||||
assert!(paths.iter().any(|p| p.ends_with("target/lib")));
|
||||
assert!(paths.iter().any(|p| {
|
||||
let n = p.replace('\\', "/");
|
||||
n.ends_with("target/lib")
|
||||
}));
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,12 +85,7 @@ pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf,
|
||||
return Err("Registry package path is empty".to_string());
|
||||
}
|
||||
|
||||
let path = Path::new(raw_path);
|
||||
let full_path = if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
get_registry_cache_root().join(path)
|
||||
};
|
||||
let full_path = registry_path_to_pathbuf(raw_path.trim());
|
||||
|
||||
if full_path.file_name().and_then(|v| v.to_str()) == Some("dcr.toml") {
|
||||
return full_path
|
||||
@@ -102,6 +97,39 @@ pub fn package_root_from_registry_info(pkg_info: &JsonValue) -> Result<PathBuf,
|
||||
Ok(full_path)
|
||||
}
|
||||
|
||||
pub fn registry_path_to_pathbuf(raw: &str) -> PathBuf {
|
||||
let s = raw.trim();
|
||||
if let Some(rest) = s.strip_prefix("file://") {
|
||||
if rest.len() >= 3 && rest.as_bytes()[0] == b'/' && rest.as_bytes()[2] == b':' {
|
||||
return PathBuf::from(&rest[1..]);
|
||||
}
|
||||
if rest.len() >= 2 && rest.as_bytes()[1] == b':' {
|
||||
return PathBuf::from(rest);
|
||||
}
|
||||
if rest.starts_with('/') {
|
||||
return PathBuf::from(rest);
|
||||
}
|
||||
return PathBuf::from(format!("/{rest}"));
|
||||
}
|
||||
|
||||
let mut s = s.replace('\\', "/");
|
||||
if let Some(rest) = s.strip_prefix("//?/") {
|
||||
s = rest.to_string();
|
||||
} else if let Some(rest) = s.strip_prefix("//./") {
|
||||
s = rest.to_string();
|
||||
}
|
||||
if s.len() >= 3 && s.as_bytes()[0] == b'/' && s.as_bytes()[2] == b':' {
|
||||
s = s[1..].to_string();
|
||||
}
|
||||
|
||||
let path = PathBuf::from(&s);
|
||||
if path.is_absolute() || (s.len() >= 2 && s.as_bytes()[1] == b':') || s.starts_with('/') {
|
||||
path
|
||||
} else {
|
||||
get_registry_cache_root().join(path)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn registry_include_dir(dep_root: &Path) -> PathBuf {
|
||||
dep_root.join("target").join("include")
|
||||
}
|
||||
@@ -219,16 +247,44 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn registry_package_root_accepts_package_dir_or_manifest_path() {
|
||||
let pkg = serde_json::json!({ "path": "/tmp/pkg" });
|
||||
let root = std::env::temp_dir().join(format!("dcr_reg_pkg_{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let root_str = root.to_string_lossy().replace('\\', "/");
|
||||
|
||||
let pkg = serde_json::json!({ "path": root_str });
|
||||
assert_eq!(
|
||||
package_root_from_registry_info(&pkg).unwrap(),
|
||||
PathBuf::from("/tmp/pkg")
|
||||
package_root_from_registry_info(&pkg)
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/"),
|
||||
root_str
|
||||
);
|
||||
|
||||
let pkg = serde_json::json!({ "path": "/tmp/pkg/dcr.toml" });
|
||||
let manifest = format!("{root_str}/dcr.toml");
|
||||
let pkg = serde_json::json!({ "path": manifest });
|
||||
assert_eq!(
|
||||
package_root_from_registry_info(&pkg).unwrap(),
|
||||
PathBuf::from("/tmp/pkg")
|
||||
package_root_from_registry_info(&pkg)
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/"),
|
||||
root_str
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_path_strips_windows_extended_prefix() {
|
||||
let p = registry_path_to_pathbuf("//?/D:/cache/mylib");
|
||||
let s = p.to_string_lossy().replace('\\', "/");
|
||||
assert_eq!(s, "D:/cache/mylib");
|
||||
|
||||
let p2 = registry_path_to_pathbuf("/D:/cache/mylib");
|
||||
let s2 = p2.to_string_lossy().replace('\\', "/");
|
||||
assert_eq!(s2, "D:/cache/mylib");
|
||||
|
||||
let p3 = registry_path_to_pathbuf("file:///D:/cache/mylib");
|
||||
let s3 = p3.to_string_lossy().replace('\\', "/");
|
||||
assert_eq!(s3, "D:/cache/mylib");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
pub mod build;
|
||||
pub mod build_config;
|
||||
pub mod builder;
|
||||
pub mod deps;
|
||||
pub mod registry;
|
||||
pub mod runner;
|
||||
|
||||
@@ -19,10 +19,15 @@ use crate::platform;
|
||||
use std::path::Path;
|
||||
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);
|
||||
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 {
|
||||
Ok(s) => {
|
||||
return s.code().unwrap_or(0);
|
||||
|
||||
@@ -50,7 +50,6 @@ pub fn parse_workspace(
|
||||
root: &Path,
|
||||
) -> Result<Option<Workspace>, String> {
|
||||
let mut table = None;
|
||||
// Order: workspace.target.profile, workspace.profile.target, workspace.target, workspace.profile, workspace
|
||||
let combinations = if let Some(t) = target {
|
||||
let normalized_t = normalize_target_os(t);
|
||||
vec![
|
||||
|
||||
@@ -57,7 +57,7 @@ fn main() {
|
||||
"--update" => cli::flag_update::flag_update(rest),
|
||||
_ => {
|
||||
error("Unknown command or argument");
|
||||
0
|
||||
1
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -86,6 +86,14 @@ pub fn normalize_kind(kind: &str) -> &str {
|
||||
if trimmed.is_empty() { "bin" } else { trimmed }
|
||||
}
|
||||
|
||||
pub fn is_flat_bin(kind: &str) -> bool {
|
||||
kind == "flat-bin"
|
||||
}
|
||||
|
||||
pub fn is_compile_only(kind: &str) -> bool {
|
||||
matches!(kind, "none" | "custom" | "flat-bin")
|
||||
}
|
||||
|
||||
pub fn normalize_platform(platform: &str) -> Option<&str> {
|
||||
let trimmed = platform.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -97,6 +105,7 @@ pub fn normalize_platform(platform: &str) -> Option<&str> {
|
||||
|
||||
pub fn normalize_target_os(target: &str) -> &str {
|
||||
match target {
|
||||
"" => "",
|
||||
"linux" => "x86_64-unknown-linux-gnu",
|
||||
"macos" => "x86_64-apple-darwin",
|
||||
"windows" => "x86_64-pc-windows-msvc",
|
||||
@@ -280,7 +289,7 @@ pub fn get_language_with_profile(config: &Config, profile: &str) -> Result<Strin
|
||||
}
|
||||
match config.get("build.language") {
|
||||
Some(v) => parse_language_value(v, "build.language"),
|
||||
None => Err("build.language is missing".to_string()),
|
||||
None => Ok("c".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,9 +333,14 @@ pub fn resolve_compiler(
|
||||
tc_as: Option<&str>,
|
||||
) -> String {
|
||||
let lang = primary_language(language);
|
||||
env_override_compiler(&lang)
|
||||
let result = env_override_compiler(&lang)
|
||||
.or_else(|| toolchain_override_compiler(&lang, tc_cc, tc_cxx, tc_as))
|
||||
.unwrap_or_else(|| compiler.to_string())
|
||||
.unwrap_or_else(|| compiler.to_string());
|
||||
if lang == "asm" {
|
||||
map_asm_compiler(&result)
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn env_override_compiler(lang: &str) -> Option<String> {
|
||||
@@ -369,7 +383,8 @@ fn toolchain_override_compiler(
|
||||
tc_as: Option<&str>,
|
||||
) -> Option<String> {
|
||||
if lang == "asm" {
|
||||
return tc_as.map(|v| v.to_string());
|
||||
let as_compiler = tc_as.map(|v| v.to_string());
|
||||
return as_compiler.map(|v| map_asm_compiler(&v));
|
||||
}
|
||||
if (lang == "c++" || lang == "cpp" || lang == "cxx")
|
||||
&& let Some(v) = tc_cxx
|
||||
@@ -379,23 +394,31 @@ fn toolchain_override_compiler(
|
||||
tc_cc.map(|v| v.to_string())
|
||||
}
|
||||
|
||||
fn map_asm_compiler(compiler: &str) -> String {
|
||||
match compiler.to_lowercase().as_str() {
|
||||
"gas" | "gnu-as" => "as".to_string(),
|
||||
"nasm" => "nasm".to_string(),
|
||||
"fasm" | "fasm64" => "fasm".to_string(),
|
||||
"masm" | "ml" | "ml64" => "ml".to_string(),
|
||||
_ => compiler.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn primary_language(language: &str) -> String {
|
||||
let parts: Vec<String> = language
|
||||
let tokens: Vec<&str> = language
|
||||
.split(',')
|
||||
.map(|p| p.trim().to_lowercase())
|
||||
.filter(|p| !p.is_empty())
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
for p in &parts {
|
||||
if p == "c++" || p == "cpp" || p == "cxx" {
|
||||
return p.clone();
|
||||
// Priority: c++ > c > asm.
|
||||
for id in ["cxx", "c", "asm"] {
|
||||
let hit = tokens.iter().any(|t| {
|
||||
crate::core::build::language::language_for_token(t).map(|l| l.id()) == Some(id)
|
||||
});
|
||||
if hit {
|
||||
return id.to_string();
|
||||
}
|
||||
}
|
||||
if parts.iter().any(|p| p == "c") {
|
||||
return "c".to_string();
|
||||
}
|
||||
if parts.iter().any(|p| p == "asm") {
|
||||
return "asm".to_string();
|
||||
}
|
||||
language.to_lowercase()
|
||||
}
|
||||
|
||||
@@ -493,6 +516,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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("macos"), "x86_64-apple-darwin");
|
||||
assert_eq!(normalize_target_os("windows"), "x86_64-pc-windows-msvc");
|
||||
|
||||
@@ -15,27 +15,14 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::utils::text::{BOLD_CYAN, colored};
|
||||
use crate::utils::text::{BOLD_RED, BOLD_YELLOW, colored};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn error(msg: &str) {
|
||||
println!("{}: {msg}", colored("error", BOLD_CYAN),);
|
||||
eprintln!("{}: {msg}", colored("error", BOLD_RED));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn warn(msg: &str) {
|
||||
println!("{}: {msg}", colored("warn", BOLD_CYAN),);
|
||||
eprintln!("{}: {msg}", colored("warn", BOLD_YELLOW));
|
||||
}
|
||||
|
||||
// #[allow(dead_code)]
|
||||
// pub fn info(msg: &str) {
|
||||
// println!("{}: {msg}", colored("info", BOLD));
|
||||
// }
|
||||
//
|
||||
// #[allow(dead_code)]
|
||||
// pub fn ok(msg: &str) {
|
||||
// println!(
|
||||
// "{}: {msg}",
|
||||
// colored("ok", &(BRIGHT_GREEN.to_owned() + BOLD))
|
||||
// );
|
||||
// }
|
||||
|
||||
@@ -16,6 +16,48 @@ fn init_and_clean_remove_target() {
|
||||
assert!(!dir.join("target").exists(), "target should be removed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_preserves_run_and_untyped_keys() {
|
||||
// Regression: `dcr add` used to drop the [run] section and non-whitelisted
|
||||
// [build] keys (filename/extension/include) when re-serializing dcr.toml.
|
||||
let dir = unique_sandbox_dir("preserve_add");
|
||||
let out = run_dcr(&["init"], &dir);
|
||||
assert!(out.status.success(), "dcr init should succeed");
|
||||
|
||||
let toml_path = dir.join("dcr.toml");
|
||||
let toml = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
|
||||
let updated = toml.replace(
|
||||
"[build]",
|
||||
"[build]\nfilename = \"KERNEL\"\nextension = \"ELF\"\ninclude = [\"src/include\"]",
|
||||
) + "\n[run]\ncmd = \"qemu-system-aarch64 -kernel KERNEL\"\n";
|
||||
std::fs::write(&toml_path, updated).expect("failed to write dcr.toml");
|
||||
|
||||
let out = run_dcr(&["add", "zlib", "path:../zlib"], &dir);
|
||||
assert!(out.status.success(), "dcr add should succeed");
|
||||
|
||||
let saved = std::fs::read_to_string(&toml_path).expect("failed to read dcr.toml");
|
||||
assert!(
|
||||
saved.contains("filename = \"KERNEL\""),
|
||||
"build.filename lost:\n{saved}"
|
||||
);
|
||||
assert!(
|
||||
saved.contains("extension = \"ELF\""),
|
||||
"build.extension lost:\n{saved}"
|
||||
);
|
||||
assert!(
|
||||
saved.contains("include = [\"src/include\"]"),
|
||||
"build.include lost:\n{saved}"
|
||||
);
|
||||
assert!(
|
||||
saved.contains("[run]") && saved.contains("qemu-system-aarch64"),
|
||||
"[run] section lost:\n{saved}"
|
||||
);
|
||||
assert!(
|
||||
saved.contains("zlib = { path = \"../zlib\" }"),
|
||||
"dependency not added as inline table:\n{saved}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_run_clean_flags_normal_project() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
@@ -35,19 +77,56 @@ fn build_run_clean_flags_normal_project() {
|
||||
assert!(out.status.success(), "dcr build --release should succeed");
|
||||
|
||||
let out = run_dcr_env(&["run"], &dir, &envs);
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(stdout.contains("Running"), "dcr run should start");
|
||||
let combined = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
combined.contains("run") || combined.contains("Running"),
|
||||
"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);
|
||||
assert!(out.status.success(), "dcr clean --release should succeed");
|
||||
let target_dir = "target/x86_64-unknown-linux-gnu".to_string();
|
||||
let release_dir = host_profile_dir(&dir, "release");
|
||||
let debug_dir = host_profile_dir(&dir, "debug");
|
||||
assert!(
|
||||
!dir.join(&target_dir).join("release").exists(),
|
||||
"target/x86_64-unknown-linux-gnu/release should be removed"
|
||||
!release_dir.exists(),
|
||||
"release profile dir should be removed: {}",
|
||||
release_dir.display()
|
||||
);
|
||||
assert!(
|
||||
dir.join(&target_dir).join("debug").is_dir(),
|
||||
"target/x86_64-unknown-linux-gnu/debug should remain"
|
||||
debug_dir.is_dir(),
|
||||
"debug profile dir should remain: {}",
|
||||
debug_dir.display()
|
||||
);
|
||||
|
||||
let out = run_dcr_env(&["clean"], &dir, &envs);
|
||||
@@ -74,16 +153,27 @@ fn build_with_target_config() {
|
||||
|
||||
let envs = [("DCR_COMPILER", compiler)];
|
||||
let out = run_dcr_env(&["build"], &dir, &envs);
|
||||
if !out.status.success() {
|
||||
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
|
||||
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
if cfg!(windows) {
|
||||
eprintln!("skipping linux-target path assert: build failed (no linux cross toolchain)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"dcr build with target = \"linux\" should succeed"
|
||||
);
|
||||
|
||||
let artifact = default_artifact_path(&dir, &project_name);
|
||||
assert!(
|
||||
artifact.is_file(),
|
||||
"artifact should be at default path target/x86_64-unknown-linux-gnu/debug/{}",
|
||||
project_name
|
||||
let out_dir = dir
|
||||
.join("target")
|
||||
.join("x86_64-unknown-linux-gnu")
|
||||
.join("debug");
|
||||
assert_artifact_in(
|
||||
&out_dir,
|
||||
&project_name,
|
||||
"build.target=linux should place artifact under target/x86_64-unknown-linux-gnu/debug",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,11 +205,10 @@ fn build_with_out_dir() {
|
||||
"dcr build with out_dir should succeed"
|
||||
);
|
||||
|
||||
let artifact = dir.join("_BUILD").join(&project_name);
|
||||
assert!(
|
||||
artifact.is_file(),
|
||||
"artifact should be at _BUILD/{} (custom out_dir)",
|
||||
project_name
|
||||
assert_artifact_in(
|
||||
&dir.join("_BUILD"),
|
||||
&project_name,
|
||||
"artifact should be under custom out_dir _BUILD",
|
||||
);
|
||||
|
||||
let default_path = default_artifact_path(&dir, &project_name);
|
||||
@@ -128,3 +217,153 @@ fn build_with_out_dir() {
|
||||
"artifact should NOT be at default path when out_dir is set"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_bin_nasm_build() {
|
||||
let nasm_ok = std::process::Command::new("nasm")
|
||||
.arg("-v")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
if !nasm_ok {
|
||||
eprintln!("nasm not found; skipping flat-bin test");
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = unique_sandbox_dir("flat_bin");
|
||||
let out = run_dcr(&["init"], &dir);
|
||||
assert!(out.status.success(), "dcr init should succeed");
|
||||
|
||||
std::fs::write(
|
||||
dir.join("dcr.toml"),
|
||||
r#"[package]
|
||||
name = "flatdemo"
|
||||
version = "0.1.0"
|
||||
|
||||
[build]
|
||||
language = "asm"
|
||||
compiler = "nasm"
|
||||
kind = "flat-bin"
|
||||
extension = "bin"
|
||||
roots = ["src"]
|
||||
"#,
|
||||
)
|
||||
.expect("write dcr.toml");
|
||||
|
||||
std::fs::create_dir_all(dir.join("src")).expect("src");
|
||||
std::fs::write(
|
||||
dir.join("src/boot.asm"),
|
||||
"bits 16\norg 0x7c00\ncli\nhlt\ntimes 510-($-$$) db 0\ndw 0xaa55\n",
|
||||
)
|
||||
.expect("write boot.asm");
|
||||
|
||||
let out = run_dcr(&["build"], &dir);
|
||||
if !out.status.success() {
|
||||
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
|
||||
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
}
|
||||
assert!(out.status.success(), "flat-bin nasm build should succeed");
|
||||
|
||||
let candidates = [
|
||||
host_profile_dir(&dir, "debug").join("boot.bin"),
|
||||
dir.join("target/debug/boot.bin"),
|
||||
];
|
||||
assert!(
|
||||
candidates.iter().any(|p| p.is_file()),
|
||||
"expected boot.bin under host profile dir, candidates: {candidates:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_build_target_used_without_cli_flag() {
|
||||
let Some(compiler) = available_compiler() else {
|
||||
eprintln!("no compiler found; skipping package target test");
|
||||
return;
|
||||
};
|
||||
|
||||
let dir = unique_sandbox_dir("pkg_target");
|
||||
let out = run_dcr(&["init"], &dir);
|
||||
assert!(out.status.success());
|
||||
|
||||
let toml_path = dir.join("dcr.toml");
|
||||
let toml = std::fs::read_to_string(&toml_path).unwrap();
|
||||
let project_name = parse_project_name(&toml);
|
||||
let updated = toml.replace("[build]", "[build]\ntarget = \"x86_64-unknown-linux-gnu\"");
|
||||
std::fs::write(&toml_path, updated).unwrap();
|
||||
|
||||
let envs = [("DCR_COMPILER", compiler)];
|
||||
let out = run_dcr_env(&["build"], &dir, &envs);
|
||||
if !out.status.success() {
|
||||
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
|
||||
eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
if cfg!(windows) {
|
||||
eprintln!("skipping linux-target path assert: build failed (no linux cross toolchain)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
assert!(out.status.success(), "build should use package target");
|
||||
|
||||
let out_dir = dir
|
||||
.join("target")
|
||||
.join("x86_64-unknown-linux-gnu")
|
||||
.join("debug");
|
||||
assert_artifact_in(
|
||||
&out_dir,
|
||||
&project_name,
|
||||
"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}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,12 +117,24 @@ fn registry_dependency_is_built_from_cache() {
|
||||
"[registry.local]\nurl = \"file://local\"\npriority = 1\n",
|
||||
)
|
||||
.expect("failed to write registry config");
|
||||
let dep_abs = dep.canonicalize().unwrap_or(dep.clone());
|
||||
let mut dep_path = dep_abs.to_string_lossy().replace('\\', "/");
|
||||
if let Some(rest) = dep_path.strip_prefix("//?/") {
|
||||
dep_path = rest.to_string();
|
||||
}
|
||||
if dep_path.len() >= 3 && dep_path.as_bytes()[0] == b'/' && dep_path.as_bytes()[2] == b':' {
|
||||
dep_path = dep_path[1..].to_string();
|
||||
}
|
||||
std::fs::write(
|
||||
dcr_home.join("index.json"),
|
||||
format!(
|
||||
"{{\"packages\":[{{\"name\":\"mylib\",\"latest_version\":\"0.1.0\",\"path\":\"{}\"}}]}}",
|
||||
dep.display()
|
||||
),
|
||||
serde_json::json!({
|
||||
"packages": [{
|
||||
"name": "mylib",
|
||||
"latest_version": "0.1.0",
|
||||
"path": dep_path
|
||||
}]
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.expect("failed to write registry index");
|
||||
|
||||
@@ -151,11 +163,16 @@ fn registry_dependency_is_built_from_cache() {
|
||||
.expect("failed to write app source");
|
||||
|
||||
let index_path = dcr_home.join("index.json");
|
||||
let envs = [
|
||||
let home_s = home.to_string_lossy().to_string();
|
||||
let index_s = index_path.to_string_lossy().to_string();
|
||||
let mut envs = vec![
|
||||
("DCR_COMPILER", compiler),
|
||||
("HOME", home.to_str().unwrap()),
|
||||
("DCR_INDEX_PATH", index_path.to_str().unwrap()),
|
||||
("HOME", home_s.as_str()),
|
||||
("DCR_INDEX_PATH", index_s.as_str()),
|
||||
];
|
||||
if cfg!(windows) {
|
||||
envs.push(("USERPROFILE", home_s.as_str()));
|
||||
}
|
||||
let out = run_dcr_env(&["build"], &app, &envs);
|
||||
if !out.status.success() {
|
||||
eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout));
|
||||
|
||||
@@ -74,6 +74,32 @@ fn available_compiler() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn host_profile_dir(project_root: &Path, profile: &str) -> PathBuf {
|
||||
let target = project_root.join("target");
|
||||
if cfg!(target_os = "linux") {
|
||||
let arch = std::env::consts::ARCH;
|
||||
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 = "openbsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "dragonfly"
|
||||
)) {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
target.join(format!("{arch}-unknown-{os}")).join(profile)
|
||||
} else {
|
||||
target.join(profile)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
#[test]
|
||||
@@ -92,9 +118,17 @@ fn no_args_shows_help() {
|
||||
fn unknown_command_fails() {
|
||||
let dir = unique_sandbox_dir("unknown_cmd");
|
||||
let out = run_dcr(&["foobar"], &dir);
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(
|
||||
stdout.contains("error") || stdout.contains("Unknown"),
|
||||
!out.status.success(),
|
||||
"unknown command should exit non-zero"
|
||||
);
|
||||
let combined = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
combined.contains("error") || combined.contains("Unknown"),
|
||||
"unknown command should print error"
|
||||
);
|
||||
}
|
||||
@@ -104,9 +138,13 @@ fn build_without_toml_fails() {
|
||||
let dir = unique_sandbox_dir("build_no_toml");
|
||||
let out = run_dcr(&["build"], &dir);
|
||||
assert!(!out.status.success(), "build without dcr.toml should fail");
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let combined = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("dcr.toml") || stdout.contains("not found"),
|
||||
combined.contains("dcr.toml") || combined.contains("not found"),
|
||||
"should mention missing dcr.toml"
|
||||
);
|
||||
}
|
||||
@@ -132,9 +170,13 @@ fn new_existing_dir_fails() {
|
||||
std::fs::create_dir_all(dir.join("hello")).expect("failed to create dir");
|
||||
let out = run_dcr(&["new", "hello"], &dir);
|
||||
assert!(!out.status.success(), "dcr new on existing dir should fail");
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let combined = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("already exists") || stdout.contains("error"),
|
||||
combined.contains("already exists") || combined.contains("error"),
|
||||
"should report dir already exists"
|
||||
);
|
||||
}
|
||||
@@ -149,9 +191,13 @@ fn init_nonempty_dir_fails() {
|
||||
!out.status.success(),
|
||||
"dcr init in non-empty dir should fail"
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let combined = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("not empty") || stdout.contains("error"),
|
||||
combined.contains("not empty") || combined.contains("error"),
|
||||
"should report dir not empty"
|
||||
);
|
||||
}
|
||||
@@ -186,9 +232,13 @@ fn run_library_project_fails() {
|
||||
let envs = [("DCR_COMPILER", compiler)];
|
||||
let out = run_dcr_env(&["run"], &dir, &envs);
|
||||
assert!(!out.status.success(), "dcr run on staticlib should fail");
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let combined = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("library") || stdout.contains("Cannot run"),
|
||||
combined.contains("library") || combined.contains("Cannot run"),
|
||||
"should report cannot run library"
|
||||
);
|
||||
}
|
||||
@@ -199,25 +249,24 @@ fn clean_specific_profile() {
|
||||
let out = run_dcr(&["init"], &dir);
|
||||
assert!(out.status.success(), "init should succeed");
|
||||
|
||||
// Create target/x86_64-unknown-linux-gnu/debug and release dirs
|
||||
let target_base = "target/x86_64-unknown-linux-gnu";
|
||||
std::fs::create_dir_all(dir.join(target_base).join("debug")).expect("create debug");
|
||||
std::fs::write(dir.join(target_base).join("debug").join("dummy.o"), "x").expect("write");
|
||||
std::fs::create_dir_all(dir.join(target_base).join("release")).expect("create release");
|
||||
std::fs::write(dir.join(target_base).join("release").join("dummy.o"), "x").expect("write");
|
||||
let debug_dir = host_profile_dir(&dir, "debug");
|
||||
let release_dir = host_profile_dir(&dir, "release");
|
||||
std::fs::create_dir_all(&debug_dir).expect("create debug");
|
||||
std::fs::write(debug_dir.join("dummy.o"), "x").expect("write");
|
||||
std::fs::create_dir_all(&release_dir).expect("create release");
|
||||
std::fs::write(release_dir.join("dummy.o"), "x").expect("write");
|
||||
|
||||
// Clean only release
|
||||
let out = run_dcr(&["clean", "--release"], &dir);
|
||||
assert!(out.status.success(), "clean --release should succeed");
|
||||
assert!(
|
||||
!dir.join(target_base).join("release").exists(),
|
||||
"target/x86_64-unknown-linux-gnu/release should be removed"
|
||||
!release_dir.exists(),
|
||||
"release profile dir should be removed: {}",
|
||||
release_dir.display()
|
||||
);
|
||||
assert!(
|
||||
dir.join("target/x86_64-unknown-linux-gnu")
|
||||
.join("debug")
|
||||
.is_dir(),
|
||||
"target/x86_64-unknown-linux-gnu/debug should remain"
|
||||
debug_dir.is_dir(),
|
||||
"debug profile dir should remain: {}",
|
||||
debug_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,9 +275,13 @@ fn new_no_name_fails() {
|
||||
let dir = unique_sandbox_dir("new_noname");
|
||||
let out = run_dcr(&["new"], &dir);
|
||||
assert!(!out.status.success(), "dcr new without name should fail");
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let combined = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("not specified") || stdout.contains("error"),
|
||||
combined.contains("not specified") || combined.contains("error"),
|
||||
"should report name not specified"
|
||||
);
|
||||
}
|
||||
@@ -251,19 +304,20 @@ fn staticlib_build() {
|
||||
let envs = [("DCR_COMPILER", compiler)];
|
||||
let out = run_dcr_env(&["build"], &dir, &envs);
|
||||
assert!(out.status.success(), "staticlib build should succeed");
|
||||
// Check that a .a file exists
|
||||
let lib_path = dir
|
||||
.join("target")
|
||||
.join("x86_64-unknown-linux-gnu")
|
||||
.join("debug");
|
||||
let lib_path = host_profile_dir(&dir, "debug");
|
||||
let has_lib = std::fs::read_dir(&lib_path)
|
||||
.map(|entries| {
|
||||
entries
|
||||
.filter_map(|e| e.ok())
|
||||
.any(|e| e.file_name().to_string_lossy().ends_with(".a"))
|
||||
entries.filter_map(|e| e.ok()).any(|e| {
|
||||
let name = e.file_name().to_string_lossy().to_string();
|
||||
name.ends_with(".a") || name.ends_with(".lib")
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
assert!(has_lib, "staticlib should produce a .a file");
|
||||
assert!(
|
||||
has_lib,
|
||||
"staticlib should produce a .a/.lib in {}",
|
||||
lib_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -279,12 +333,11 @@ fn build_release_profile() {
|
||||
let envs = [("DCR_COMPILER", compiler)];
|
||||
let out = run_dcr_env(&["build", "--release"], &dir, &envs);
|
||||
assert!(out.status.success(), "release build should succeed");
|
||||
let release_dir = host_profile_dir(&dir, "release");
|
||||
assert!(
|
||||
dir.join("target")
|
||||
.join("x86_64-unknown-linux-gnu")
|
||||
.join("release")
|
||||
.is_dir(),
|
||||
"target/x86_64-unknown-linux-gnu/release should exist"
|
||||
release_dir.is_dir(),
|
||||
"release profile dir should exist: {}",
|
||||
release_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,14 +45,31 @@ fn workspace_build_and_clean_all() {
|
||||
out.status.success(),
|
||||
"workspace clean --all --release should succeed"
|
||||
);
|
||||
let combined = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
let spam = combined.matches("Directory target not found").count();
|
||||
assert!(
|
||||
spam <= 1,
|
||||
"clean --all should not spam missing member target/: {combined}"
|
||||
);
|
||||
|
||||
let target_dir = "target/x86_64-unknown-linux-gnu";
|
||||
let release_dir = host_profile_dir(&root, "release");
|
||||
let debug_dir = host_profile_dir(&root, "debug");
|
||||
assert!(
|
||||
!root.join(target_dir).join("release").exists(),
|
||||
"root target/x86_64-unknown-linux-gnu/release should be removed"
|
||||
!release_dir.exists(),
|
||||
"root release profile dir should be removed: {}",
|
||||
release_dir.display()
|
||||
);
|
||||
assert!(
|
||||
root.join(target_dir).join("debug").exists(),
|
||||
"root target/x86_64-unknown-linux-gnu/debug should remain"
|
||||
debug_dir.exists(),
|
||||
"root debug profile dir should remain: {}",
|
||||
debug_dir.display()
|
||||
);
|
||||
|
||||
let out = run_dcr_env(&["clean", "--all"], &root, &envs);
|
||||
assert!(out.status.success(), "workspace clean --all should succeed");
|
||||
assert!(!root.join("target").exists(), "root target/ should be gone");
|
||||
}
|
||||
|
||||
@@ -92,10 +92,63 @@ pub fn parse_project_name(toml: &str) -> String {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn default_artifact_path(project_root: &Path, project_name: &str) -> PathBuf {
|
||||
project_root
|
||||
.join("target")
|
||||
.join("x86_64-unknown-linux-gnu")
|
||||
.join("debug")
|
||||
.join(project_name)
|
||||
pub fn host_profile_dir(project_root: &Path, profile: &str) -> PathBuf {
|
||||
let target = project_root.join("target");
|
||||
if cfg!(target_os = "linux") {
|
||||
let arch = std::env::consts::ARCH;
|
||||
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 = "openbsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "dragonfly"
|
||||
)) {
|
||||
let arch = std::env::consts::ARCH;
|
||||
let os = std::env::consts::OS;
|
||||
target.join(format!("{arch}-unknown-{os}")).join(profile)
|
||||
} else {
|
||||
target.join(profile)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn bin_name(project_name: &str) -> String {
|
||||
if cfg!(windows) {
|
||||
format!("{project_name}.exe")
|
||||
} else {
|
||||
project_name.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn default_artifact_path(project_root: &Path, project_name: &str) -> PathBuf {
|
||||
host_profile_dir(project_root, "debug").join(bin_name(project_name))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn artifact_candidates(dir: &Path, project_name: &str) -> Vec<PathBuf> {
|
||||
let base = bin_name(project_name);
|
||||
let bare = project_name.to_string();
|
||||
vec![
|
||||
dir.join(&base),
|
||||
dir.join(&bare),
|
||||
dir.join(format!("{project_name}.exe")),
|
||||
]
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn assert_artifact_in(dir: &Path, project_name: &str, ctx: &str) {
|
||||
let candidates = artifact_candidates(dir, project_name);
|
||||
assert!(
|
||||
candidates.iter().any(|p| p.is_file()),
|
||||
"{ctx}: expected binary in {}, candidates: {candidates:?}",
|
||||
dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user