11 Commits

58 changed files with 3336 additions and 530 deletions

View File

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

View File

@@ -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() {

View File

@@ -1,14 +0,0 @@
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 }}"

View File

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

View File

@@ -1,5 +1,77 @@
# Changelog
## [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

279
Cargo.lock generated
View File

@@ -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,9 +154,10 @@ dependencies = [
[[package]]
name = "dcr"
version = "0.8.0"
version = "0.8.2"
dependencies = [
"ctrlc",
"fatfs",
"glob",
"self-replace",
"serde",
@@ -127,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",
@@ -166,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"
@@ -181,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"
@@ -231,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"
@@ -358,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"
@@ -400,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"
@@ -439,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"
@@ -488,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",
@@ -526,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"
@@ -541,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",
@@ -638,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"
@@ -833,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"
@@ -861,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"
@@ -1034,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",

View File

@@ -1,6 +1,6 @@
[package]
name = "dcr"
version = "0.8.0"
version = "0.8.2"
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"] }
@@ -69,6 +73,7 @@ toml = "1.0.3"
toml_edit = "0.22"
serde_json = "1"
ctrlc = "3"
fatfs = { version = "0.3", optional = true }
[profile.release]
opt-level = "z"

View File

@@ -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
View File

@@ -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
- [x] **Linter + Formatter** (`dcr fmt`, `dcr lint`)
*`dcr fmt` (clang-format) and `dcr lint` (clang-tidy) are implemented*
- [ ] **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

View File

@@ -1,3 +0,0 @@
files:
- source: /docs/**/*.mdx
translation: /docs-crowdin-export/%locale%/%original_path%/%original_file_name%.%file_extension%

13
dcrup.cmd Executable file
View 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
View 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
View 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 "$@"

View File

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

View File

@@ -4,6 +4,123 @@ sidebar_label: Changelog
# Changelog
## 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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,48 +96,37 @@ 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.default]

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

View File

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

View File

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

View File

@@ -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]
@@ -62,18 +65,18 @@ author = "John Doe"
| Field | Default | Description |
|-------|---------|-------------|
| `language` | `"c"` | `"c"`, `"c++"`, `"cpp"`, `"cxx"`, `"asm"` (optional, defaults to `"c"`) |
| `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++ standard (`c++17`, `c++20`, `c++23`) |
| `compiler` | `"clang"` | Preferred compiler (optional, defaults to `"clang"`) |
| `kind` | `"bin"` | `bin`, `staticlib`, `sharedlib`, `efi`, `elf`, `none`, `custom` |
| `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 |
@@ -96,7 +99,7 @@ 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]`
## 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:
@@ -116,6 +119,9 @@ 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.
@@ -172,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).

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,18 +17,29 @@
use crate::cli::clean::clean;
use crate::cli::flags::parse_build_run_flags;
use crate::core::build::common;
use crate::core::build::{BuildEvent, BuildReporter, BuildRequest, run_build};
use crate::core::build_config::Config;
use crate::utils::build::default_target_triple;
use crate::utils::fs::find_project_root;
use crate::utils::log::error;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
use std::io::IsTerminal;
use std::sync::{Arc, atomic::AtomicBool};
pub use crate::core::build::get_build_string_with_profile;
fn status(verb: &str, style: &str) -> String {
colored(&format!("{verb:<9}"), style)
}
struct CliReporter;
impl CliReporter {
fn line(&self, verb: &str, style: &str, rest: &str) {
eprintln!(" {} {}", status(verb, style), rest);
}
}
impl BuildReporter for CliReporter {
fn on_event(&mut self, event: BuildEvent<'_>) {
match event {
@@ -37,73 +48,57 @@ impl BuildReporter for CliReporter {
total,
target,
} => {
println!(" Building target {} of {}: {}", index, total, target);
common::finish_progress_line();
self.line("target", BOLD_CYAN, &format!("{index}/{total} {target}"));
}
BuildEvent::ProjectStart {
name,
profile,
target,
} => {
println!(
" Building project `{}`\n Profile: {}\n Target: {}",
colored(name, BOLD_GREEN),
colored(profile, BOLD_GREEN),
colored(target, BOLD_GREEN)
common::finish_progress_line();
self.line(
"project",
BOLD_CYAN,
&format!("{name} ({profile}, {target})"),
);
}
BuildEvent::DepBuilding { name, version } => {
print!(
"\r{:100}\r {} {} v{}",
"",
colored("Building", BOLD_GREEN),
name,
version
);
std::io::Write::flush(&mut std::io::stdout()).unwrap();
common::finish_progress_line();
self.line("dep", BOLD_YELLOW, &format!("{name} v{version}"));
}
BuildEvent::DepReady {
name,
version,
rebuilt,
} => {
common::finish_progress_line();
if rebuilt {
print!(
"\r{:100}\r {} {} v{}",
"",
colored("Ready", BOLD_GREEN),
name,
version
);
println!();
} else {
println!(
" {} {} v{}",
colored("Ready", BOLD_GREEN),
name,
version
);
self.line("ready", BOLD_GREEN, &format!("{name} v{version}"));
}
}
BuildEvent::Compiling { name, version } => {
println!(
" {} {} v{}",
colored("Compiling", BOLD_GREEN),
name,
version
);
common::finish_progress_line();
let label = format!(" {} {} v{}", status("compile", BOLD_GREEN), name, version);
common::set_progress_label(Some(label.clone()));
if !std::io::stderr().is_terminal() {
eprintln!("{label}");
}
}
BuildEvent::Packing { path } => {
common::finish_progress_line();
self.line("pack", BOLD_CYAN, path);
}
BuildEvent::Finished { secs } => {
println!(
" {} Build completed successfully in {} seconds",
colored("✔", BOLD_GREEN),
colored(&secs.to_string(), BOLD_GREEN)
);
common::finish_progress_line();
self.line("done", BOLD_GREEN, &format!("in {secs}s"));
}
BuildEvent::CompilerOutput { stream, text } => {
common::interrupt_progress_for_output();
if stream == "stderr" {
eprintln!("{}", text);
eprint!("{text}");
} else {
println!("{}", text);
print!("{text}");
}
}
}
@@ -166,15 +161,12 @@ pub fn build(args: &[String]) -> i32 {
if let Ok(config) = Config::open(config_path.to_str().unwrap())
&& !config.is_workspace_only()
{
let bt = get_build_string_with_profile(&config, "target", "debug");
let bt = get_build_string_with_profile(&config, "target", &flags.profile);
if !bt.is_empty() {
flags.target = Some(bt);
}
}
}
if flags.target.is_none() {
flags.target = Some(default_target_triple());
}
if flags.clean {
let mut clean_args = Vec::new();
clean_args.push(format!("--{}", flags.profile));
@@ -193,6 +185,7 @@ pub fn build(args: &[String]) -> i32 {
match run_build(&root, &req, &mut reporter) {
Ok(_) => 0,
Err(err) => {
common::finish_progress_line();
error(&err.message);
1
}

View File

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

View File

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

View File

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

View File

@@ -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;
@@ -110,8 +108,20 @@ 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 {
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
cmd
);
return run_shell(&cmd);
}
return build_status;
}
let ws = match crate::core::workspace::parse_workspace(
&config,
&flags.profile,
@@ -177,10 +187,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,7 +226,11 @@ 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());
}
@@ -233,12 +250,18 @@ fn run_project(
);
if build_status == 0 {
if let Some(cmd) = run_cmd {
println!("\n {} {}", colored("Running", BOLD_GREEN), cmd);
println!("--------------------------------");
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
cmd
);
return Ok(run_shell(&cmd));
}
println!("\n {} {}", colored("Running", BOLD_GREEN), bin_path);
println!("--------------------------------");
println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
bin_path
);
return Ok(run_binary(
project_name,
&flags.profile,

272
src/core/build/archive.rs Normal file
View 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(())
}

View File

@@ -19,9 +19,137 @@ 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],
@@ -32,14 +160,12 @@ pub(crate) fn archive_static(
return Ok(common::elapsed_secs(start_time));
}
let archiver = ctx.archiver.unwrap_or(if cfg!(target_os = "windows") {
"lib"
} else {
"ar"
});
let archiver = ctx
.archiver
.unwrap_or_else(|| default_archiver(ctx.compiler));
let mut cmd = Command::new(archiver);
if cfg!(target_os = "windows") && archiver == "lib" {
if cfg!(target_os = "windows") && is_msvc_lib(archiver) {
cmd.arg("/nologo").arg(format!("/OUT:{lib_path}"));
} else {
cmd.arg("rcs").arg(&lib_path);
@@ -56,6 +182,10 @@ pub(crate) fn link_binary(
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") {
@@ -111,11 +241,39 @@ pub(crate) fn link_binary(
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 output = cmd.output().map_err(|err| format!("Build failed: {err}"))?;
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 {

View File

@@ -18,7 +18,7 @@
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;
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,6 +30,9 @@ 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"),
@@ -55,6 +58,10 @@ 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" {
return artifact::archive_static(ctx, &objects, start_time);
}
@@ -138,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");
@@ -174,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);

View File

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

View File

@@ -107,6 +107,10 @@ fn build_cache_path(profile: &str, target_dir: Option<&str>) -> PathBuf {
}
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("");
@@ -159,7 +163,13 @@ pub(crate) fn collect_header_files(
if !root.exists() {
continue;
}
collect_header_files_rec(&root, &mut out, ctx.exclude_dirs, ctx.include_paths)?;
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();

View File

@@ -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)
@@ -95,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 {
@@ -206,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(|| {
@@ -228,6 +315,9 @@ where
}
break;
}
let done = completed.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
report_compile_progress(done, total_tasks);
}
});
}
@@ -241,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));
@@ -297,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 {
@@ -400,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());
@@ -427,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> {
@@ -597,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();
@@ -621,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();
@@ -639,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();
@@ -661,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();
@@ -683,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();
@@ -707,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();
@@ -734,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();
@@ -757,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();
@@ -792,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]

View File

@@ -90,7 +90,9 @@ fn run_compile(
_rep: &mut dyn BuildReporter,
) -> Result<f64, String> {
let start_time = Instant::now();
match build_project(ctx) {
let result = build_project(ctx);
common::finish_progress_line();
match result {
Ok(times) => {
let times = if times == 0.0 {
((start_time.elapsed().as_secs_f64() * 100.0).trunc()) / 100.0
@@ -165,6 +167,19 @@ fn build_all(
&mut *rep,
)?;
}
if let Some(archive) = &config.typed().archive {
#[cfg(feature = "archive")]
{
super::archive::pack_archive(root, archive, profile)?;
}
#[cfg(not(feature = "archive"))]
{
let _ = (archive, profile);
return Err(
"disk image packing requires building dcr with --features archive".to_string(),
);
}
}
return Ok(0.0);
}
@@ -774,10 +789,12 @@ fn build_project_at(
let resolved_cflags: Vec<String> = resolved_cflags
.iter()
.map(|f| substitute_vars(f, &version_info, profile, &project_name))
.map(|f| absolutize_flag_path(&f, project_root, &["-isystem", "-idirafter", "-I", "-i"]))
.collect();
let modified_ldflags: Vec<String> = resolved_ldflags
.iter()
.map(|f| substitute_vars(f, &version_info, profile, &project_name))
.map(|f| absolutize_flag_path(&f, project_root, &["-L", "-T"]))
.collect();
let merged_include_dirs: Vec<String> = merged_include_dirs
.iter()
@@ -843,7 +860,10 @@ fn build_project_at(
version_suffix: &version_info.suffix,
version_suffix_dash: &version_info.suffix_dash,
};
let steps_dirty = build_steps_need_run(&build_steps, &step_vars)?;
let mut steps_dirty = build_steps_need_run(&build_steps, &step_vars)?;
if force {
steps_dirty = true;
}
if steps_dirty {
clean_generated_files(&build_generated)?;
run_build_steps(
@@ -870,11 +890,11 @@ fn build_project_at(
if skip && !debug_enabled {
return Ok(());
}
rep.on_event(BuildEvent::Compiling {
name: &project_name,
version: &project_version,
});
if !skip {
rep.on_event(BuildEvent::Compiling {
name: &project_name,
version: &project_version,
});
run_compile(&ctx, cancel, rep)?;
if cancel.load(Ordering::SeqCst) {
return Err("Build interrupted".to_string());
@@ -884,7 +904,10 @@ fn build_project_at(
if ctx.package_type == Some("lib") || ctx.kind == "staticlib" || ctx.kind == "sharedlib" {
package_library(&ctx, &headers, project_root)?;
}
let post_steps_dirty = build_steps_need_run(&build_post_steps, &step_vars)?;
let mut post_steps_dirty = build_steps_need_run(&build_post_steps, &step_vars)?;
if force {
post_steps_dirty = true;
}
if post_steps_dirty {
run_build_steps(
&build_post_steps,
@@ -896,8 +919,44 @@ fn build_project_at(
)?;
}
verify_expectations(&build_expects, &step_vars)?;
if let Some(archive) = &config.typed().archive {
let out = archive.output.replace("{profile}", profile);
rep.on_event(BuildEvent::Packing { path: &out });
#[cfg(feature = "archive")]
{
super::archive::pack_archive(project_root, archive, profile)?;
}
#[cfg(not(feature = "archive"))]
{
let _ = (project_root, archive, profile, out);
return Err(
"disk image packing requires building dcr with --features archive".to_string(),
);
}
}
Ok(())
}
fn absolutize_flag_path(flag: &str, project_root: &Path, prefixes: &[&str]) -> String {
for prefix in prefixes {
if let Some(rest) = flag.strip_prefix(prefix) {
if rest.is_empty() {
return flag.to_string();
}
if rest.starts_with('/') || rest.starts_with('@') {
return flag.to_string();
}
if rest.len() >= 2 && rest.as_bytes()[1] == b':' {
return flag.to_string();
}
let abs = project_root.join(rest);
let path = abs.to_string_lossy().replace('\\', "/");
return format!("{prefix}{path}");
}
}
flag.to_string()
}
fn find_target_root(dir: &str, fallback: &Path) -> PathBuf {
let p = Path::new(dir);
for ancestor in p.ancestors() {
@@ -1082,3 +1141,35 @@ fn is_on_path(cmd: &str) -> bool {
std::env::var_os("PATH")
.is_some_and(|paths| std::env::split_paths(&paths).any(|dir| dir.join(cmd).is_file()))
}
#[cfg(test)]
mod tests {
use super::absolutize_flag_path;
use std::path::Path;
#[test]
fn absolutize_prefers_long_include_prefixes() {
let root = Path::new("/pkg");
let prefs = ["-isystem", "-idirafter", "-I", "-i"];
assert_eq!(
absolutize_flag_path("-isystemvendor/inc", root, &prefs),
"-isystem/pkg/vendor/inc"
);
assert_eq!(
absolutize_flag_path("-idiraftervendor/old", root, &prefs),
"-idirafter/pkg/vendor/old"
);
assert_eq!(
absolutize_flag_path("-I../inc", root, &prefs),
"-I/pkg/../inc"
);
assert_eq!(
absolutize_flag_path("-i../inc", root, &prefs),
"-i/pkg/../inc"
);
assert_eq!(
absolutize_flag_path("-I/usr/include", root, &prefs),
"-I/usr/include"
);
}
}

View File

@@ -1,6 +1,7 @@
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;
@@ -22,13 +23,63 @@ pub fn build_assembly<F>(
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| p.trim() != "asm") {
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\""
"{backend_name} backend requires build.language = \"asm\" (or llvm_ir for LLC)"
));
}
@@ -45,11 +96,50 @@ where
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 = build_objects_common(assembler, &sources, &obj_dir, ctx, &build_object)?;
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);

View File

@@ -32,7 +32,7 @@ pub fn build(ctx: &BuildContext) -> Result<f64, String> {
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "as".to_string());
asm::build_assembly(ctx, "GAS", &as_path, &["s"], build_object)
asm::build_assembly_via_objcopy(ctx, "GAS", &as_path, &["s"], build_object)
}
pub(crate) fn collect_sources(ctx: &BuildContext) -> Result<Vec<String>, String> {

View File

@@ -6,7 +6,7 @@ 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> {

View File

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

View File

@@ -21,7 +21,7 @@ impl Language for LlvmIr {
}
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> {

View File

@@ -15,6 +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/>.
#[cfg(feature = "archive")]
pub mod archive;
pub mod builder;
pub mod cache;
pub mod common;

View File

@@ -69,6 +69,9 @@ pub enum BuildEvent<'a> {
name: &'a str,
version: &'a str,
},
Packing {
path: &'a str,
},
CompilerOutput {
stream: &'a str,
text: &'a str,

View File

@@ -36,6 +36,7 @@ const VALID_KINDS: &[&str] = &[
"elf",
"none",
"custom",
"flat-bin",
];
#[derive(Debug)]
@@ -102,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)]

View File

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

View File

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

View File

@@ -57,7 +57,7 @@ fn main() {
"--update" => cli::flag_update::flag_update(rest),
_ => {
error("Unknown command or argument");
0
1
}
};

View File

@@ -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() {

View File

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

View File

@@ -77,19 +77,29 @@ 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"
);
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);
@@ -116,16 +126,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",
);
}
@@ -157,11 +178,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);
@@ -170,3 +190,99 @@ 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",
);
}

View File

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

View File

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

View File

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

View File

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