diff --git a/CHANGELOG.md b/CHANGELOG.md index fa67b99..5bdd571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ # Changelog +## [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`, прямой вывод `.bin` + - **FASM** — прямой вывод (ожидается `format binary` в исходнике) + - **GAS / MASM / LLVM IR (`llc`)** — объект → `objcopy -O binary` → `.bin` + - **C/C++ (gcc/clang/MSVC)** — compile → link (nostdlib/static) → `objcopy -O binary` → `.bin` + - Требуется `llvm-objcopy` / `objcopy` / `gobjcopy` в PATH (кроме NASM/FASM direct) +- **Секция `[archive]`** — после успешной сборки DCR собирает образ диска (FAT12/16/32 через `fatfs`): размер, offset, volume label, опциональный bootsector (512 байт в начало при `offset = 0`) и `layout` (копирование файлов/glob в FS). Плейсхолдер `{profile}` в путях. +- **Одиночные файлы в `build.roots`** — корень может быть файлом (`.c`, `.asm`, header), а не только каталогом; то же для сбора заголовков в cache. + +**Изменено:** + +- **`dcr build --force` / `dcr run --force`** — force также перезапускает `build.steps` и `build.post_steps`. +- **`dcr run` на workspace-only root** — если задан `[run].cmd`, сначала build + run по конфигу корня; иначе делегирование члену workspace (как в 0.8.0). + +**Зависимости:** + +- `fatfs = "0.3"` для упаковки FAT-образов. + +### EN + +**Added:** + +- **`build.kind = "flat-bin"`** — raw binary artifact (default extension `.bin`): + - **NASM** — `-f bin`, direct `.bin` + - **FASM** — direct output (`format binary` in source) + - **GAS / MASM / LLVM IR (`llc`)** — object → `objcopy -O binary` → `.bin` + - **C/C++ (gcc/clang/MSVC)** — compile → link (`-nostdlib -static`) → `objcopy -O binary` → `.bin` + - Needs `llvm-objcopy` / `objcopy` / `gobjcopy` in PATH (except NASM/FASM direct) +- **`[archive]` section** — after a successful build, pack a disk image (FAT12/16/32 via `fatfs`): size, offset, volume label, optional bootsector (512 bytes at start when `offset = 0`), and `layout` (file/glob copy into the FS). `{profile}` substitution in paths. +- **Single-file `build.roots`** — a root may be a file (`.c`, `.asm`, header), not only a directory; header collection in the cache follows the same rule. + +**Changed:** + +- **`dcr build --force` / `dcr run --force`** — force also re-runs `build.steps` and `build.post_steps`. +- **`dcr run` on a workspace-only root** — if `[run].cmd` is set, build + run that command first; otherwise fall back to workspace member delegation (0.8.0 behavior). + +**Dependencies:** + +- `fatfs = "0.3"` for FAT image packing. + ## [0.8.0] - 2026-07-18 "Модульный Движок Сборки, Новая Архитектура Ядра и Улучшение Workspaces / Modular Build Engine, New Core Architecture, & Workspace Improvements" ### RU diff --git a/Cargo.lock b/Cargo.lock index 4f5a03f..d654054 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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.1" 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", diff --git a/Cargo.toml b/Cargo.toml index ae168cf..d46ffab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dcr" -version = "0.8.0" +version = "0.8.1" edition = "2024" build = "build.rs" description = "DCR is a utility for managing C/C++ projects in a Cargo-like style." @@ -69,6 +69,7 @@ toml = "1.0.3" toml_edit = "0.22" serde_json = "1" ctrlc = "3" +fatfs = "0.3" [profile.release] opt-level = "z" diff --git a/docs/README.mdx b/docs/README.mdx index 08bd33f..da4008e 100644 --- a/docs/README.mdx +++ b/docs/README.mdx @@ -51,8 +51,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 diff --git a/docs/changelog.mdx b/docs/changelog.mdx index 66b0d74..d78a810 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -4,6 +4,108 @@ sidebar_label: Changelog # Changelog +## 0.8.1 (2026-07-20) + +### Added + +- **`build.kind = "flat-bin"`** — raw binary (default `.bin`) for OS-dev payloads: + - **NASM** — `-f bin` → `.bin` + - **FASM** — direct output (`format binary` in source) + - **GAS / MASM / LLC** — object → `objcopy -O binary` → `.bin` + - **C/C++** — objects → freestanding link → `objcopy -O binary` → `.bin` + - Requires `llvm-objcopy` / `objcopy` / `gobjcopy` except for NASM/FASM direct emit +- **`[archive]` section** — after a successful build, pack a FAT disk image (`fatfs`): `output`, `format` (`fat12`/`fat16`/`fat32`), optional `size` (default 1.44 MiB), `offset`, `label`, `bootsector`, and `layout` (`from` → `to`, supports globs). `{profile}` works in paths. +- **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. + +### Dependencies + +- `fatfs = "0.3"` for FAT image packing. + +## 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.]`)** — 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`) 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 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 diff --git a/docs/commands/build-commands.mdx b/docs/commands/build-commands.mdx index 5c5e4e4..d18e0f8 100644 --- a/docs/commands/build-commands.mdx +++ b/docs/commands/build-commands.mdx @@ -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///` (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//`). diff --git a/docs/contributing.mdx b/docs/contributing.mdx index b363ed6..d91c7fd 100644 --- a/docs/contributing.mdx +++ b/docs/contributing.mdx @@ -45,6 +45,6 @@ cargo test --all-targets CI runs: - `cargo fmt --check` - `cargo clippy --all-targets -- -D warnings` -- `cargo test --all-targets` +- `cargo check --all-targets --all-features` - Build on Linux, macOS, Windows -- Integration tests +- Integration tests (compilation check) diff --git a/docs/faq.mdx b/docs/faq.mdx index 085dbe2..4680f99 100644 --- a/docs/faq.mdx +++ b/docs/faq.mdx @@ -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`. diff --git a/docs/getting-started/first-steps.mdx b/docs/getting-started/first-steps.mdx index 9dc5624..5b54176 100644 --- a/docs/getting-started/first-steps.mdx +++ b/docs/getting-started/first-steps.mdx @@ -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 +``` + diff --git a/docs/recipes/os-flat-bin-archive.mdx b/docs/recipes/os-flat-bin-archive.mdx new file mode 100644 index 0000000..99c000c --- /dev/null +++ b/docs/recipes/os-flat-bin-archive.mdx @@ -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 `.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. diff --git a/docs/reference/build-system.mdx b/docs/reference/build-system.mdx index e1b3ee9..bd42542 100644 --- a/docs/reference/build-system.mdx +++ b/docs/reference/build-system.mdx @@ -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 ` - FASM: ` ` (output path is the object file) - LLVM IR: `-filetype=obj -o ` @@ -87,6 +87,11 @@ Three levels of incrementality: | `elf` | ELF without stdlib | `target///` | | `none` | Compile only, no link | — | | `custom` | Full filename+extension control | `target///.` | +| `flat-bin` | Raw binary | ASM: `.bin` (NASM `-f bin` / FASM / GAS·MASM·LLC via objcopy); C/C++: `.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 diff --git a/docs/reference/cross-compilation.mdx b/docs/reference/cross-compilation.mdx index ac3a321..4f4d54d 100644 --- a/docs/reference/cross-compilation.mdx +++ b/docs/reference/cross-compilation.mdx @@ -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/// -``` - -On macOS/Windows or without target: - -``` -target// -``` +- **Linux and BSD**: Always output to `target///` (using host triple if no target is specified). +- **macOS and Windows (without target)**: Output to `target//`. +- **macOS and Windows (with explicit target)**: Output to `target///`. diff --git a/docs/reference/dcr-toml.mdx b/docs/reference/dcr-toml.mdx index 63b4c68..b806c58 100644 --- a/docs/reference/dcr-toml.mdx +++ b/docs/reference/dcr-toml.mdx @@ -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,82 @@ Default `cmd` = `./target/{profile}/{name}` (macOS/Windows) or `./target/.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 `.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 `.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). diff --git a/docs/reference/dependencies.mdx b/docs/reference/dependencies.mdx index 1cb2be4..aeb8eca 100644 --- a/docs/reference/dependencies.mdx +++ b/docs/reference/dependencies.mdx @@ -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. diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx index cc63c00..a97eab5 100644 --- a/docs/reference/platform-support.mdx +++ b/docs/reference/platform-support.mdx @@ -11,17 +11,18 @@ DCR normalizes target triples. Short platform names are expanded to full triples ### Linux ``` -target/-unknown-linux-gnu// +target/-unknown-linux-// ``` -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// +target// (default) or target/// (with target) ``` Architectures: `x86_64`, `aarch64` (host-detected). @@ -31,10 +32,11 @@ Extensions: `.dylib` (sharedlib), `.a` (staticlib). ### Windows ``` -target// +target// (default) or target/// (with target) ``` Architectures: `x86_64`, `aarch64`. +Environments: `msvc` (default), `gnu` (MinGW). Extensions: `.exe` (bin), `.lib` (staticlib), `.dll` (sharedlib), `.efi` (UEFI). diff --git a/docs/reference/workspaces.mdx b/docs/reference/workspaces.mdx index b9e4331..670d370 100644 --- a/docs/reference/workspaces.mdx +++ b/docs/reference/workspaces.mdx @@ -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 diff --git a/src/cli/run.rs b/src/cli/run.rs index 5658681..49d216b 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -110,8 +110,17 @@ 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!("\n {} {}", colored("Running", BOLD_GREEN), cmd); + println!("--------------------------------"); + return run_shell(&cmd); + } + return build_status; + } + let ws = match crate::core::workspace::parse_workspace( &config, &flags.profile, @@ -213,7 +222,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()); } diff --git a/src/core/build/archive.rs b/src/core/build/archive.rs new file mode 100644 index 0000000..9e047ea --- /dev/null +++ b/src/core/build/archive.rs @@ -0,0 +1,272 @@ +// DCR — Cargo-like C/C++ project manager. +// +// Copyright (C) 2026 Dexoron (Bezotechestvo Vladimir) +// +// 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 . + +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 { + inner: T, + offset: u64, +} + +impl Read for OffsetFile { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.inner.read(buf) + } +} + +impl Write for OffsetFile { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.inner.write(buf) + } + fn flush(&mut self) -> io::Result<()> { + self.inner.flush() + } +} + +impl Seek for OffsetFile { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + 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 { + 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(()) +} diff --git a/src/core/build/builder/artifact.rs b/src/core/build/builder/artifact.rs index f78bdf4..73d8c5e 100644 --- a/src/core/build/builder/artifact.rs +++ b/src/core/build/builder/artifact.rs @@ -19,9 +19,130 @@ 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 { + 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 { + 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 output = cmd.output().map_err(|err| format!("Build 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], @@ -56,6 +177,10 @@ pub(crate) fn link_binary( default_linker: &str, start_time: Instant, ) -> Result { + 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") { diff --git a/src/core/build/builder/cc_common.rs b/src/core/build/builder/cc_common.rs index 6db4e9e..c23c0ae 100644 --- a/src/core/build/builder/cc_common.rs +++ b/src/core/build/builder/cc_common.rs @@ -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 { } 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,7 +58,7 @@ pub fn build(ctx: &BuildContext) -> Result { )?; let objects = build_objects(compiler, &sources, &obj_dir, ctx, "o", qt_include_path)?; - if ctx.kind == "none" || ctx.kind == "custom" { + if is_compile_only(ctx.kind) && !is_flat_bin(ctx.kind) { return Ok(common::elapsed_secs(start_time)); } @@ -142,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"); @@ -178,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); diff --git a/src/core/build/builder/msvc/mod.rs b/src/core/build/builder/msvc/mod.rs index e174c7e..dfcf69b 100644 --- a/src/core/build/builder/msvc/mod.rs +++ b/src/core/build/builder/msvc/mod.rs @@ -16,8 +16,10 @@ // along with this program. If not, see . 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 { 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,10 +46,14 @@ pub fn build(ctx: &BuildContext) -> Result { }; let objects = build_objects(compiler, &sources, &obj_dir, ctx, "obj")?; - if ctx.kind == "none" || ctx.kind == "custom" { + 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) { @@ -145,6 +154,61 @@ pub fn build(ctx: &BuildContext) -> Result { } } +fn link_flat_msvc( + ctx: &BuildContext, + compiler: &str, + objects: &[String], + obj_dir: &Path, + start_time: Instant, +) -> Result { + 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, String> { let extensions = source_extensions(ctx.language); common::collect_sources( diff --git a/src/core/build/cache.rs b/src/core/build/cache.rs index b93d543..03edfe4 100644 --- a/src/core/build/cache.rs +++ b/src/core/build/cache.rs @@ -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(); diff --git a/src/core/build/common.rs b/src/core/build/common.rs index 14a4b51..995cad0 100644 --- a/src/core/build/common.rs +++ b/src/core/build/common.rs @@ -35,7 +35,15 @@ pub fn collect_sources( ) -> Result, 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) diff --git a/src/core/build/engine.rs b/src/core/build/engine.rs index 29ac7a6..bb306b9 100644 --- a/src/core/build/engine.rs +++ b/src/core/build/engine.rs @@ -165,6 +165,9 @@ fn build_all( &mut *rep, )?; } + if let Some(archive) = &config.typed().archive { + super::archive::pack_archive(root, archive, profile)?; + } return Ok(0.0); } @@ -843,7 +846,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( @@ -884,7 +890,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,6 +905,9 @@ fn build_project_at( )?; } verify_expectations(&build_expects, &step_vars)?; + if let Some(archive) = &config.typed().archive { + super::archive::pack_archive(project_root, archive, profile)?; + } Ok(()) } fn find_target_root(dir: &str, fallback: &Path) -> PathBuf { diff --git a/src/core/build/language/asm/common.rs b/src/core/build/language/asm/common.rs index 75dccef..9b1d20b 100644 --- a/src/core/build/language/asm/common.rs +++ b/src/core/build/language/asm/common.rs @@ -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( extensions: &[&str], build_object: F, ) -> Result +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( + ctx: &BuildContext, + backend_name: &str, + default_assembler: &str, + extensions: &[&str], + build_object: F, +) -> Result +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( + ctx: &BuildContext, + backend_name: &str, + default_assembler: &str, + extensions: &[&str], + flat_mode: FlatEmit, + build_object: F, +) -> Result 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,13 +96,48 @@ 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)?; - if ctx.kind == "none" || ctx.kind == "custom" { + let objects = match (flat, flat_mode) { + (true, FlatEmit::Direct) => { + let outs: Vec = 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 = 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)); } diff --git a/src/core/build/language/asm/gas/mod.rs b/src/core/build/language/asm/gas/mod.rs index 85865e2..5adf735 100644 --- a/src/core/build/language/asm/gas/mod.rs +++ b/src/core/build/language/asm/gas/mod.rs @@ -32,7 +32,7 @@ pub fn build(ctx: &BuildContext) -> Result { .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, String> { diff --git a/src/core/build/language/asm/masm/mod.rs b/src/core/build/language/asm/masm/mod.rs index 14e7a42..558093f 100644 --- a/src/core/build/language/asm/masm/mod.rs +++ b/src/core/build/language/asm/masm/mod.rs @@ -6,7 +6,7 @@ use std::path::Path; use std::process::Command; pub fn build(ctx: &BuildContext) -> Result { - 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, String> { diff --git a/src/core/build/language/asm/nasm/mod.rs b/src/core/build/language/asm/nasm/mod.rs index 24fa3f9..7a273f6 100644 --- a/src/core/build/language/asm/nasm/mod.rs +++ b/src/core/build/language/asm/nasm/mod.rs @@ -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") diff --git a/src/core/build/language/llvm_ir/mod.rs b/src/core/build/language/llvm_ir/mod.rs index 69eb30f..425a82b 100644 --- a/src/core/build/language/llvm_ir/mod.rs +++ b/src/core/build/language/llvm_ir/mod.rs @@ -21,7 +21,7 @@ impl Language for LlvmIr { } pub fn build(ctx: &BuildContext) -> Result { - 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, String> { diff --git a/src/core/build/mod.rs b/src/core/build/mod.rs index 8928a31..27ea698 100644 --- a/src/core/build/mod.rs +++ b/src/core/build/mod.rs @@ -15,6 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +pub mod archive; pub mod builder; pub mod cache; pub mod common; diff --git a/src/core/build_config.rs b/src/core/build_config.rs index 9fb9c81..f77149d 100644 --- a/src/core/build_config.rs +++ b/src/core/build_config.rs @@ -36,6 +36,7 @@ const VALID_KINDS: &[&str] = &[ "elf", "none", "custom", + "flat-bin", ]; #[derive(Debug)] @@ -102,6 +103,27 @@ pub struct DcrConfig { pub toolchain: Option, #[serde(default)] pub workspace: BTreeMap, + #[serde(default)] + pub archive: Option, +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Deserialize)] +pub struct ArchiveConfig { + pub output: String, + pub format: String, + pub size: Option, + pub offset: Option, + pub label: Option, + pub bootsector: Option, + #[serde(default)] + pub layout: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ArchiveLayout { + pub from: String, + pub to: String, } #[derive(Debug, Clone, Deserialize)] diff --git a/src/utils/build.rs b/src/utils/build.rs index 9d82c69..0f70a6b 100644 --- a/src/utils/build.rs +++ b/src/utils/build.rs @@ -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() {