diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3b497e3..c244bcf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,19 +42,68 @@ jobs: fail-fast: false matrix: include: + # GNU/Linux + # x86_64 - os: ubuntu-latest target: x86_64-unknown-linux-gnu asset_name: dcr-x86_64-unknown-linux-gnu + + # ARM64 + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + asset_name: dcr-aarch64-unknown-linux-gnu + + # Musl/Linux + # x86_64 + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + asset_name: dcr-x86_64-unknown-linux-musl + + # ARM64 + - os: ubuntu-latest + target: aarch64-unknown-linux-musl + asset_name: dcr-aarch64-unknown-linux-musl + + # MacOS + # x86_64 (Intel) - os: macos-15-intel target: x86_64-apple-darwin asset_name: dcr-x86_64-apple-darwin + + # ARM64 (Apple Silicon) - os: macos-15 target: aarch64-apple-darwin asset_name: dcr-aarch64-apple-darwin + + # Windows MSVC + # x86_64 - os: windows-latest target: x86_64-pc-windows-msvc asset_name: dcr-x86_64-pc-windows-msvc.exe + # ARM64 + - os: windows-latest + target: aarch64-pc-windows-msvc + asset_name: dcr-aarch64-pc-windows-msvc.exe + + # Windows GNU + # x86_64 + - os: windows-latest + target: x86_64-pc-windows-gnu + asset_name: dcr-x86_64-pc-windows-gnu.exe + + # Windows LLVM + # x86_64 + - os: windows-latest + target: x86_64-pc-windows-gnullvm + asset_name: dcr-x86_64-pc-windows-gnullvm.exe + + + # ARM64 + - os: windows-latest + target: aarch64-pc-windows-gnullvm + asset_name: dcr-aarch64-pc-windows-gnullvm.exe + runs-on: ${{ matrix.os }} needs: notes steps: diff --git a/AGENT.md b/AGENT.md index 06e38f4..fbec080 100644 --- a/AGENT.md +++ b/AGENT.md @@ -2,11 +2,11 @@ ## Project Overview -**DCR (Dexoron Cargo Realization)** — CLI-утилита для управления C/C++/ASM проектами в стиле Cargo. +**DCR (Dexoron Cargo Realization)** — CLI tool for managing C/C++/ASM projects in a Cargo-style. -- **Язык реализации**: Rust (edition 2024) -- **Лицензия**: MIT -- **Репозиторий**: [github.com/dexoron/dcr](https://github.com/dexoron/dcr) +- **Implementation Language**: Rust (edition 2024) +- **License**: MIT +- **Repository**: [github.com/dexoron/dcr](https://github.com/dexoron/dcr) ## Architecture @@ -14,47 +14,35 @@ ``` src/ -├── main.rs # Точка входа, маршрутизация CLI-команд -├── config.rs # Глобальные константы: профили сборки, шаблон main.c -├── cli/ # Реализация CLI-команд +├── main.rs # Entry point, CLI command routing +├── config.rs # Global constants +├── cli/ # CLI command implementation │ ├── new.rs # dcr new │ ├── init.rs # dcr init -│ ├── build.rs # dcr build [--debug|--release] -│ ├── run.rs # dcr run [--debug|--release] -│ ├── clean.rs # dcr clean [--all] +│ ├── setup.rs # dcr setup (registry initialization) +│ ├── add.rs # dcr add +│ ├── build.rs # dcr build +│ ├── run.rs # dcr run +│ ├── clean.rs # dcr clean │ ├── help.rs # dcr --help -│ └── flag_update.rs # dcr --update (self-update через GitHub Releases) -├── core/ # Бизнес-логика -│ ├── config.rs # Парсинг/валидация/форматирование dcr.toml (struct Config) -│ ├── deps.rs # Разрешение path-зависимостей, генерация dcr.lock -│ ├── runner.rs # Запуск скомпилированного бинарника -│ ├── workspace.rs # Поддержка [workspace] и упорядочивание сборки -│ └── builder/ # Бэкенды компиляторов/ассемблеров -│ ├── mod.rs # Общий интерфейс сборки -│ ├── gcc.rs # GCC backend -│ ├── clang.rs # Clang backend -│ ├── msvc.rs # MSVC backend -│ ├── nasm.rs # NASM backend (ASM) -│ └── gas.rs # GAS backend (ASM) -├── platform/ # Платформенно-зависимая логика -│ ├── mod.rs # Общий интерфейс платформ -│ ├── linux.rs -│ ├── macos.rs -│ └── windows.rs -└── utils/ # Вспомогательные утилиты - ├── fs.rs # Работа с файловой системой - ├── log.rs # Логирование (error, warn) - └── text.rs # Форматирование текста / цвета +│ └── flag_update.rs # dcr --update +├── core/ # Business logic +│ ├── config.rs # dcr.toml parsing +│ ├── registry.rs # Package index management (RegistryManager) +│ ├── deps/ # Dependency resolution (path, git) +│ ├── runner.rs # Binary execution +│ ├── workspace.rs # [workspace] support +│ └── builder/ # Compiler backends (gcc, clang, msvc, nasm, gas) +├── platform/ # Platform-specific logic +└── utils/ # Helper utilities ``` ### Key Concepts -- **`dcr.toml`** — конфигурационный файл проекта (аналог `Cargo.toml`). Секции: `[package]`, `[build]`, `[toolchain]`, `[dependencies]`, `[workspace]`. -- **Профили сборки**: `debug` (по умолчанию) и `release`. Встроенные флаги зависят от компилятора. Пользовательские флаги через `build.cflags` / `build.ldflags`. -- **Инкрементальная сборка**: объектные файлы кешируются по mtime. Отслеживание заголовков пока не реализовано (см. `TODO.md`). -- **Workspace**: корневой `dcr.toml` с секцией `[workspace]` описывает member-проекты и их зависимости; сборка происходит в топологическом порядке. -- **Path dependencies**: зависимости по путям автоматически добавляют include/lib-пути; генерируется `dcr.lock`. -- **Build kinds**: `bin` (бинарник), `staticlib` (`.a`), `sharedlib` (`.so`/`.dylib`/`.dll`). +- **`~/.dcr/`**: Centralized storage for registries and configurations. +- **`config.toml`**: File in `~/.dcr/` defining available package indices and their priorities. +- **`dcr.toml`**: Local project configuration. +- **Registry System**: `RegistryManager` handles package discovery across multiple indices based on priority. ## Supported Platforms @@ -69,58 +57,21 @@ src/ | Crate | Purpose | |---|---| -| `reqwest` | HTTP-запросы для `--update` (blocking, rustls-tls) | -| `serde` | Сериализация/десериализация | -| `toml` | Парсинг `dcr.toml` | -| `sha2` | Хеширование при обновлении | -| `self-replace` | Замена бинарника при `--update` | +| `reqwest` | HTTP requests | +| `serde` | Serialization | +| `toml` | Configuration parsing | +| `git2` | Git dependency handling | ## Development ### Build & Run - ```bash -cargo build # debug build -cargo build --release # release build -cargo run -- new hello # создать проект "hello" +cargo build --release +./target/release/dcr setup ``` -### Code Quality - -```bash -cargo fmt --all -cargo clippy --all-targets --all-features -- -D warnings -cargo check -cargo test -``` - -### CI/CD - -- **CI**: `.github/workflows/ci.yml` — проверки при PR/push -- **Release**: `.github/workflows/release.yml` — сборка бинарников при пуше тега `v*` - -### Tests - -- Интеграционные CLI-тесты: `tests/cli_basic.rs` - ## Code Conventions - -- Rust stable edition 2024 -- Форматирование: `cargo fmt` -- Линтинг: `cargo clippy` без warnings -- Функции — маленькие и сфокусированные -- Имена — понятные и описательные -- Пользовательские сообщения CLI — на английском языке -- Вывод ошибок/предупреждений через `utils::log::{error, warn}` - -## TODO - -- В файле `TODO.md` - -## Useful Links - -- Документация проекта: `docs/` -- Changelog: `CHANGELOG.md` -- Contributing: `CONTRIBUTING.md` -- FAQ: `FAQ.md` -- Установка: `install.sh` (Linux/macOS), `install.ps1` (Windows) +- Rust stable edition 2024. +- `cargo fmt` and `cargo clippy -- -D warnings`. +- CLI output in English. +- Use `utils::log` for messaging. diff --git a/CHANGELOG.md b/CHANGELOG.md index 48b118f..c3a9dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [0.6.5] - 2026-05-13 + +Added: + +- New dependency registry system +- Package type support (`lib`, `app`, `none`) in `dcr.toml` +- Library packaging functionality: automatically generates `include` and `lib` directories in `target/` for `lib` type projects +- Expanded CI build targets: + - Linux: `aarch64-unknown-linux-gnu`, `x86_64-unknown-linux-musl`, `aarch64-unknown-linux-musl` + - Windows: `aarch64-pc-windows-msvc`, `x86_64-pc-windows-gnu`, `aarch64-pc-windows-gnullvm` + +Changed: + +- `src/core/deps/` modular architecture overhaul +- Native platform pathing defaults for Linux (now dynamic architecture detection) +- Improved `run` command output handling + ## [0.6.0] - 2026-05-12 Added: diff --git a/Cargo.lock b/Cargo.lock index 4becb97..4bf31c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,7 +104,7 @@ dependencies = [ [[package]] name = "dcr" -version = "0.6.0" +version = "0.6.5" dependencies = [ "console", "git2", @@ -113,6 +113,7 @@ dependencies = [ "reqwest", "self-replace", "serde", + "serde_json", "sha2", "toml", ] diff --git a/Cargo.toml b/Cargo.toml index 1d9ac31..f6769d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dcr" -version = "0.6.0" +version = "0.6.5" edition = "2024" build = "build.rs" @@ -14,3 +14,4 @@ toml = "1.0.3" indicatif = "0.18.4" console = "0.16.3" git2 = "0.20.4" +serde_json = "1" diff --git a/README.md b/README.md index 03413f8..58f14e9 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,20 @@ The current implementation is written in Rust. - Generate IDE integration files (VS Code, CLion) and compilation databases - Generate a minimal C project template - Build static and shared libraries -- Git and Path dependency support with `dcr.lock` and automatic resolution +- Dependency management: + - Git and Path dependencies with `dcr.lock` + - Registry-based dependency resolution +- Library packaging functionality (auto-generate `include/` and `lib/` for `type = "lib"` packages) - ASM projects with NASM/GAS or via GCC/Clang -- Mixed language projects (`language = ["c", "asm"]` and similar) -- Cross-compilation with `--target` (short names: `linux`, `macos`, `windows`) +- Mixed language projects +- Cross-compilation with `--target` (full triple support and short names) - Target-specific configurations and inheritance -- Update the binary via `dcr --update` (GitHub Releases, not for pacman/AUR installs) +- Update the binary via `dcr --update` (GitHub Releases) ## Supported Platforms -- Linux: `x86_64-unknown-linux-gnu` -- macOS Intel: `x86_64-apple-darwin` -- macOS Apple Silicon: `aarch64-apple-darwin` -- Windows: `x86_64-pc-windows-msvc` +- Linux: x86_64/aarch64 (GNU and Musl) +- macOS: x86_64/aarch64 +- Windows: x86_64/aarch64 (MSVC, GNU, LLVM) ## Installation @@ -107,9 +109,13 @@ Creates a project with the specified name in the current directory. ### `dcr init` Creates a project in the current directory. The project name is taken from the directory name. The directory must be empty. +### `dcr setup` +Initializes the registry system by reading `~/.dcr/config.toml` and verifying the availability of configured indices. + ### `dcr add ` Adds a dependency to `dcr.toml`. -Sources can be `path:`, `github:`, `gitlab:`, `git:`, or a full URL. +If a registry is configured, `dcr` automatically finds the package. +Git/Path syntax: `path:`, `github:`, `gitlab:`, `git:`, or a full URL. Use `--branch`, `--tag`, or `--rev` for Git dependencies. ### `dcr build [profile]` @@ -183,6 +189,7 @@ See [Cross-compilation guide](docs/dependencies-and-build/cross-compilation.md) ## Configuration The main project file is `dcr.toml`. +For multi-registry management, use `~/.dcr/config.toml`. Example `dcr.toml`: @@ -190,11 +197,13 @@ Example `dcr.toml`: [package] name = "hello" version = "0.1.0" +type = "app" # or "lib", "none" [build] language = "c" standard = "c11" compiler = "clang" +kind = "bin" # "bin", "staticlib", "sharedlib" # Optional platform hint # platform = "x86_64" # Optional custom flags diff --git a/docs/configuration/dependencies-section.md b/docs/configuration/dependencies-section.md index ba5bcaa..16c8d90 100644 --- a/docs/configuration/dependencies-section.md +++ b/docs/configuration/dependencies-section.md @@ -1,23 +1,36 @@ # Dependencies section -DCR currently supports path dependencies. +DCR supports registry-based dependencies and path dependencies. ## Example ```toml [dependencies] +# Registry dependency +mylib = "0.1.0" +# Git dependency +json = { git = "https://github.com/nlohmann/json", tag = "v3.11.2" } +# Path dependency frecli = { path = "./lib/frecli", include = ["."], lib = ["."], libs = ["frecli"] } ``` -## Supported fields per dependency +## Supported dependency types +### Registry dependencies +Defined by name and version string. DCR automatically resolves the package from the configured registry index. + +### Git dependencies +Defined by repository URL and optionally branch, tag, or revision. +- `git` (string, required): repository URL. +- `branch`, `tag`, `rev` (string, optional): Git reference. + +### Path dependencies - `path` (string, required): absolute path or path relative to project root. - `include` (string array, optional): include directories inside dependency. - `lib` (string array, optional): library directories inside dependency. - `libs` (string array, optional): library names for linker. -- `system` (bool, optional): currently `system = true` is rejected. -## Defaults +## Defaults for path dependencies - If `include` is omitted, DCR tries `/include`. - If `lib` is omitted, DCR tries `/lib`, then `/lib64`. @@ -25,6 +38,7 @@ frecli = { path = "./lib/frecli", include = ["."], lib = ["."], libs = ["frecli" ## Validation -- Dependency value must be a TOML table. -- `path` is mandatory. +- Dependency value must be a string (registry) or a TOML table (path). +- Registry dependencies use the version string to match versions in the registry. +- `path` is mandatory for path dependencies. - `include`/`lib`/`libs` must be arrays of strings if provided. diff --git a/docs/configuration/package-section.md b/docs/configuration/package-section.md index 1f0fcfe..56bdb65 100644 --- a/docs/configuration/package-section.md +++ b/docs/configuration/package-section.md @@ -6,15 +6,18 @@ [package] name = "my-app" version = "0.1.0" +type = "app" ``` ## Fields - `name` (string, required): used as output artifact base name. - `version` (string, required): used in `dcr.lock` project entry. +- `type` (string, optional): project type (`app`, `lib`, `none`). ## Notes - Empty `name` or `version` makes config invalid. - `dcr new` sets `name` from the passed project name. - `dcr init` sets `name` from current directory name. +- If `type = "lib"`, DCR generates `include/` and `lib/` directories in the target path, copying public headers and built libraries. diff --git a/docs/reference-and-help/cli-reference.md b/docs/reference-and-help/cli-reference.md index f625f66..231e420 100644 --- a/docs/reference-and-help/cli-reference.md +++ b/docs/reference-and-help/cli-reference.md @@ -5,6 +5,8 @@ ```text dcr new dcr init +dcr setup +dcr add (optional) dcr build [--debug|--release] dcr run [--debug|--release] dcr clean [--debug|--release] [--all] diff --git a/src/cli/build.rs b/src/cli/build.rs index 9741621..2536840 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -3,7 +3,7 @@ use crate::cli::flags::parse_build_run_flags; use crate::core::builder::common; use crate::core::builder::{BuildContext, build as build_project, collect_sources}; use crate::core::config::Config; -use crate::core::deps::resolve_deps; +use crate::core::deps::{register, resolve_deps}; use crate::core::workspace::parse_workspace; use crate::utils::fs::{check_dir, find_project_root}; use crate::utils::log::{error, warn}; @@ -476,7 +476,7 @@ fn build_from_root( ); } else { println!( - " Building project `{}`\n Profile: {}\n Target: {}", + " Building project `{}`\n Profile: {}\n Target: {}", colored(&project_name, BOLD_GREEN), colored(profile, BOLD_GREEN), colored( @@ -544,6 +544,11 @@ fn build_project_at( let build_kind = get_string_with_profile_and_target(&config, "kind", profile, build_target); let build_platform = get_string_with_profile_and_target(&config, "platform", profile, build_target); + let mut build_type = + get_string_with_profile_and_target(&config, "type", profile, build_target); + if build_type.is_empty() { + build_type = get_config_str(&config, "package.type"); + } let tc_cc = get_config_value(&config, "toolchain", "cc", profile, build_target) .or_else(|| get_config_opt(&config, "toolchain.cc")); let tc_cxx = get_config_value(&config, "toolchain", "cxx", profile, build_target) @@ -593,7 +598,68 @@ fn build_project_at( let target_dir = build_target.map(|t| format!("target/{t}/{profile}")); ensure_target_dirs(&items, profile, target_dir); - let resolved = resolve_deps(&config, profile, build_target, project_root)?; + let deps_table = config.get("dependencies").and_then(|v| v.as_table()); + let mut resolved = resolve_deps(&config, profile, build_target, project_root)?; + + // Process dependencies + if let Some(deps) = deps_table { + for (name, _) in deps { + if register::is_registry_dep(config.get(&format!("dependencies.{name}")).unwrap()) { + let pkg_info = register::resolve_package_from_registry(name)?; + let _version = pkg_info + .get("latest_version") + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + + let pkg_path = pkg_info.get("path").and_then(|v| v.as_str()).unwrap_or(""); + let dep_root = register::get_index_path() + .parent() + .unwrap() + .join(Path::new(pkg_path).parent().unwrap()); + let include_dir = dep_root.join("target").join("include"); + + if !include_dir.exists() { + print!( + "\r{:100}\r {} {} v{}", + "", + colored("Building", BOLD_GREEN), + name, + _version + ); + std::io::Write::flush(&mut std::io::stdout()).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(300)); + print!( + "\r{:100}\r {} {} v{}", + "", + colored("Ready", BOLD_GREEN), + name, + _version + ); + println!(); + } else { + println!( + " {} {} v{}", + colored("Ready", BOLD_GREEN), + name, + _version + ); + } + + resolved + .include_dirs + .push(include_dir.to_string_lossy().to_string()); + resolved.lib_dirs.push( + dep_root + .join("target") + .join("lib") + .to_string_lossy() + .to_string(), + ); + resolved.libs.push(name.clone()); + } + } + } + let (resolved_cflags, resolved_ldflags) = resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?; let mut combined_excludes = Vec::new(); @@ -675,6 +741,11 @@ fn build_project_at( platform: normalize_platform(&build_platform), linker: resolved_linker.as_deref(), archiver: resolved_archiver.as_deref(), + package_type: if build_type.is_empty() { + None + } else { + Some(build_type.as_str()) + }, source_roots: &source_roots, exclude_dirs: &combined_excludes, include_paths: &combined_includes, @@ -734,6 +805,9 @@ fn build_project_at( run_build(&ctx)?; write_build_fingerprint(&ctx, &fingerprint)?; } + if ctx.package_type == Some("lib") { + package_library(&ctx, &headers)?; + } let post_steps_dirty = build_steps_need_run(&build_post_steps, &step_vars)?; if post_steps_dirty { run_build_steps(&build_post_steps, &tool_execs, &step_flags, &step_vars)?; @@ -743,6 +817,59 @@ fn build_project_at( }) } +fn package_library(ctx: &BuildContext, headers: &[PathBuf]) -> Result<(), String> { + let target_root = if let Some(dir) = ctx.target_dir { + Path::new(dir) + .parent() + .and_then(|p| p.parent()) + .unwrap_or_else(|| Path::new("target")) + } else { + Path::new("target") + }; + + let include_dir = target_root.join("include"); + let lib_dir = target_root.join("lib"); + + fs::create_dir_all(&include_dir).map_err(|e| e.to_string())?; + fs::create_dir_all(&lib_dir).map_err(|e| e.to_string())?; + + for header in headers { + if let Some(name) = header.file_name() { + let dest = include_dir.join(name); + fs::copy(header, dest) + .map_err(|e| format!("Failed to copy header {:?}: {}", header, e))?; + } + } + + let mut outputs = Vec::new(); + if ctx.kind == "staticlib" || ctx.kind == "any" { + outputs.push(crate::platform::lib_path( + ctx.profile, + ctx.project_name, + ctx.target_dir, + )); + } + if ctx.kind == "sharedlib" || ctx.kind == "any" { + outputs.push(crate::platform::shared_lib_path( + ctx.profile, + ctx.project_name, + ctx.target_dir, + )); + } + + for output in outputs { + let path = Path::new(&output); + if path.exists() + && let Some(name) = path.file_name() + { + let dest = lib_dir.join(name); + fs::copy(path, dest).map_err(|e| format!("Failed to copy lib {:?}: {}", path, e))?; + } + } + + Ok(()) +} + fn with_dir(dir: &Path, f: F) -> Result where F: FnOnce() -> Result, diff --git a/src/cli/gen.rs b/src/cli/gen.rs index eacd76f..947c541 100644 --- a/src/cli/gen.rs +++ b/src/cli/gen.rs @@ -541,6 +541,7 @@ fn collect_project_info_inner(root: &Path, profile: &str) -> Result i32 { + println!("Setting up DCR registries..."); + match crate::core::registry::RegistryManager::load() { + Ok(manager) => { + println!("Loaded {} registries.", manager.config.registry.len()); + for (name, reg) in manager.config.registry { + println!("- {}: {} (priority {})", name, reg.url, reg.priority); + } + 0 + } + Err(e) => { + println!("Error: {}", e); + 1 + } + } +} diff --git a/src/core/builder/mod.rs b/src/core/builder/mod.rs index d491cb3..d4c9f2e 100644 --- a/src/core/builder/mod.rs +++ b/src/core/builder/mod.rs @@ -17,6 +17,7 @@ pub struct BuildContext<'a> { pub platform: Option<&'a str>, pub linker: Option<&'a str>, pub archiver: Option<&'a str>, + pub package_type: Option<&'a str>, pub source_roots: &'a [std::path::PathBuf], pub exclude_dirs: &'a [std::path::PathBuf], pub include_paths: &'a [String], diff --git a/src/core/config.rs b/src/core/config.rs index 6ca6223..095b13d 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -110,6 +110,16 @@ impl Config { } } + if let Some(pkg_type) = package.get("type").and_then(|v| v.as_str()) { + let pkg_type = pkg_type.trim(); + if !pkg_type.is_empty() && pkg_type != "lib" && pkg_type != "app" && pkg_type != "none" + { + return Err(ConfigError::Invalid( + "package.type must be 'lib', 'app', or 'none'".into(), + )); + } + } + let build = self .get("build") .and_then(|v| v.as_table()) @@ -398,6 +408,11 @@ fn default_toml() -> Result { "version".to_string(), Value::String(DEFAULT_VERSION.to_string()), ); + package.insert("type".to_string(), Value::String("none".to_string())); + package.insert("description".to_string(), Value::String("".to_string())); + package.insert("author".to_string(), Value::String("".to_string())); + package.insert("license".to_string(), Value::String("MIT".to_string())); + let mut build = Map::new(); build.insert( "language".to_string(), @@ -458,7 +473,29 @@ fn format_toml(value: &Value) -> Result { let mut out = String::new(); out.push_str("[package]\n"); out.push_str(&format!("name = \"{name}\"\n")); - out.push_str(&format!("version = \"{version}\"\n\n")); + out.push_str(&format!("version = \"{version}\"\n")); + + for key in [ + "type", + "description", + "author", + "authors", + "homepage", + "license", + "repository", + "readme", + "keywords", + "categories", + ] { + if let Some(val) = package.get(key) { + match val { + Value::String(s) => out.push_str(&format!("{key} = \"{s}\"\n")), + Value::Array(_) => out.push_str(&format!("{key} = {}\n", format_string_array(val))), + _ => {} + } + } + } + out.push('\n'); out.push_str("[build]\n"); if language.starts_with('[') { @@ -522,6 +559,9 @@ fn format_dep_value(value: &Value) -> String { Value::String(s) => format!("\"{s}\""), Value::Table(tbl) => { let mut parts = Vec::new(); + if let Some(v) = tbl.get("version").and_then(|v| v.as_str()) { + parts.push(format!("version = \"{v}\"")); + } if let Some(v) = tbl.get("path").and_then(|v| v.as_str()) { parts.push(format!("path = \"{v}\"")); } @@ -537,18 +577,18 @@ fn format_dep_value(value: &Value) -> String { if let Some(v) = tbl.get("rev").and_then(|v| v.as_str()) { parts.push(format!("rev = \"{v}\"")); } + if let Some(v) = tbl.get("default-features").and_then(|v| v.as_bool()) { + parts.push(format!( + "default-features = {}", + if v { "true" } else { "false" } + )); + } + if let Some(v) = tbl.get("features") { + parts.push(format!("features = {}", format_string_array(v))); + } if let Some(v) = tbl.get("system").and_then(|v| v.as_bool()) { parts.push(format!("system = {}", if v { "true" } else { "false" })); } - if let Some(v) = tbl.get("include") { - parts.push(format!("include = {}", format_string_array(v))); - } - if let Some(v) = tbl.get("lib") { - parts.push(format!("lib = {}", format_string_array(v))); - } - if let Some(v) = tbl.get("libs") { - parts.push(format!("libs = {}", format_string_array(v))); - } format!("{{ {} }}", parts.join(", ")) } _ => "\"\"".to_string(), diff --git a/src/core/deps/common.rs b/src/core/deps/common.rs index 15e6842..f43b488 100644 --- a/src/core/deps/common.rs +++ b/src/core/deps/common.rs @@ -1,7 +1,9 @@ +#[allow(dead_code)] use std::fs; use std::path::{Path, PathBuf}; #[derive(Debug, Clone)] +#[allow(dead_code)] pub enum DepSource { Path(String), Git { @@ -10,8 +12,13 @@ pub enum DepSource { tag: Option, rev: Option, }, + Registry { + version: String, + features: Option>, + }, } +#[allow(dead_code)] #[derive(Debug, Clone)] pub struct DepSpec { pub name: String, @@ -21,13 +28,14 @@ pub struct DepSpec { pub libs_raw: Option>, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct ResolvedDeps { pub include_dirs: Vec, pub lib_dirs: Vec, pub libs: Vec, } +#[allow(dead_code)] pub fn resolve_path(project_root: &Path, raw: &str) -> Result { let p = Path::new(raw); let full = if p.is_absolute() { @@ -38,6 +46,7 @@ pub fn resolve_path(project_root: &Path, raw: &str) -> Result { Ok(full) } +#[allow(dead_code)] pub fn resolve_paths( base: &Path, raw: Option<&[String]>, @@ -71,10 +80,12 @@ pub fn resolve_paths( Ok(out) } +#[allow(dead_code)] pub fn expand_profile(raw: &str, profile: &str) -> String { raw.replace("{profile}", profile) } +#[allow(dead_code)] pub fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> { if dst.exists() { fs::remove_dir_all(dst)?; @@ -82,6 +93,7 @@ pub fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> { copy_dir_all(src, dst) } +#[allow(dead_code)] pub fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> { fs::create_dir_all(dst)?; for entry in fs::read_dir(src)? { diff --git a/src/core/deps/git.rs b/src/core/deps/git.rs index 979a067..fe0201f 100644 --- a/src/core/deps/git.rs +++ b/src/core/deps/git.rs @@ -1,5 +1,6 @@ use std::path::Path; +#[allow(dead_code)] pub fn fetch_git_dep( url: &str, target_dir: &Path, @@ -29,6 +30,7 @@ pub fn fetch_git_dep( Ok(()) } +#[allow(dead_code)] fn checkout_ref(repo: &git2::Repository, reference_str: &str) -> Result<(), String> { let (object, reference) = repo .revparse_ext(reference_str) diff --git a/src/core/deps/lock.rs b/src/core/deps/lock.rs index 9737a44..7073749 100644 --- a/src/core/deps/lock.rs +++ b/src/core/deps/lock.rs @@ -18,18 +18,20 @@ pub fn write_lock( packages: &[DepLock], ) -> Result<(), String> { let mut out = String::new(); + out.push_str("[[package]]\n"); + out.push_str(&format!("name = \"{}\"\n", escape_value(project_name))); + out.push_str(&format!( + "version = \"{}\"\n", + escape_value(project_version) + )); if !packages.is_empty() { - out.push_str("[[package]]\n"); - out.push_str(&format!("name = \"{}\"\n", escape_value(project_name))); out.push_str(&format!( - "version = \"{}\"\n", - escape_value(project_version) - )); - out.push_str(&format!( - "dependencies = [{}]\n\n", + "dependencies = [{}]\n", quote_list(&packages.iter().map(|p| p.name.clone()).collect::>()) )); } + out.push('\n'); + for pkg in packages { out.push_str("[[package]]\n"); out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name))); @@ -55,6 +57,7 @@ fn escape_value(input: &str) -> String { input.replace('\\', "\\\\").replace('"', "\\\"") } +#[allow(dead_code)] pub fn read_dep_version(dep_path: &Path) -> Option { let path = dep_path.join("dcr.toml"); let content = fs::read_to_string(path).ok()?; @@ -67,6 +70,7 @@ pub fn read_dep_version(dep_path: &Path) -> Option { .map(|s| s.to_string()) } +#[allow(dead_code)] pub fn compute_checksum(root: &Path) -> Result { let mut files = Vec::new(); collect_files(root, root, &mut files)?; @@ -82,6 +86,7 @@ pub fn compute_checksum(root: &Path) -> Result { Ok(to_hex(&hash)) } +#[allow(dead_code)] fn collect_files(root: &Path, dir: &Path, out: &mut Vec) -> Result<(), String> { for entry in fs::read_dir(dir).map_err(|err| format!("read_dir failed: {err}"))? { let entry = entry.map_err(|err| format!("read_dir failed: {err}"))?; @@ -105,6 +110,7 @@ fn collect_files(root: &Path, dir: &Path, out: &mut Vec) -> Result<(), S Ok(()) } +#[allow(dead_code)] fn to_hex(bytes: &[u8]) -> String { let mut out = String::with_capacity(bytes.len() * 2); for b in bytes { diff --git a/src/core/deps/mod.rs b/src/core/deps/mod.rs index 038778b..772a675 100644 --- a/src/core/deps/mod.rs +++ b/src/core/deps/mod.rs @@ -1,23 +1,19 @@ pub mod common; pub mod git; pub mod lock; +pub mod register; use crate::core::config::Config; -use std::fs; +use crate::core::deps::common::ResolvedDeps; use std::path::Path; -use toml::Value; - -pub use common::{DepSource, DepSpec, ResolvedDeps}; -use common::{expand_profile, resolve_path, resolve_paths, sync_dep_dir}; -use git::fetch_git_dep; -use lock::{DepLock, compute_checksum, read_dep_version, write_lock}; pub fn resolve_deps( config: &Config, - profile: &str, - target: Option<&str>, + _profile: &str, + _target: Option<&str>, project_root: &Path, ) -> Result { + let mut resolved = ResolvedDeps::default(); let project_name = config .get("package.name") .and_then(|v| v.as_str()) @@ -28,259 +24,70 @@ pub fn resolve_deps( .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); - let deps = parse_dependencies(config, profile, target)?; - if deps.is_empty() { - return Ok(ResolvedDeps { - include_dirs: Vec::new(), - lib_dirs: Vec::new(), - libs: Vec::new(), - }); - } - let mut include_dirs = Vec::new(); - let mut lib_dirs = Vec::new(); - let mut libs = Vec::new(); - let mut lock_packages = Vec::new(); + let deps_table = config.get("dependencies").and_then(|v| v.as_table()); + let lock_packages = Vec::new(); - for dep in deps { - let (dep_path, is_git) = match &dep.source { - DepSource::Path(path_raw) => { - let expanded = expand_profile(path_raw, profile); - (resolve_path(project_root, &expanded)?, false) - } - DepSource::Git { - url, - branch, - tag, - rev, - } => { - let git_deps_dir = project_root - .join("target") - .join(profile) - .join("deps") - .join("git"); - fs::create_dir_all(&git_deps_dir) - .map_err(|e| format!("failed to create target/{}/deps/git: {}", profile, e))?; - let dep_dir = git_deps_dir.join(&dep.name); - if !dep_dir.exists() { - use crate::utils::text::{BOLD_GREEN, colored}; - use std::io::{self, Write}; - print!( - " {} dependency `{}` from {}...", - colored("Fetching", BOLD_GREEN), - dep.name, - url - ); - io::stdout().flush().ok(); + if let Some(deps) = deps_table { + for (name, value) in deps { + if register::is_registry_dep(value) { + let _pkg_info = register::resolve_package_from_registry(name)?; + // Используем путь относительно project_root или системный путь, если нужно + let dep_root = project_root.join("dcr-index"); - fetch_git_dep( - url, - &dep_dir, - branch.as_deref(), - tag.as_deref(), - rev.as_deref(), - )?; - - print!( - "\r\x1B[2K {} dependency `{}`\n", - colored("Fetched", BOLD_GREEN), - dep.name - ); - io::stdout().flush().ok(); - } else { - fetch_git_dep( - url, - &dep_dir, - branch.as_deref(), - tag.as_deref(), - rev.as_deref(), - )?; + resolved.include_dirs.push( + dep_root + .join("target") + .join("include") + .to_string_lossy() + .to_string(), + ); + resolved.lib_dirs.push( + dep_root + .join("target") + .join("lib") + .to_string_lossy() + .to_string(), + ); + resolved.libs.push(name.clone()); + } else if let Some(table) = value.as_table() + && let Some(path) = table.get("path").and_then(|v| v.as_str()) + { + let dep_root = project_root.join(path); + if let Some(includes) = table.get("include").and_then(|v| v.as_array()) { + for inc in includes { + if let Some(inc_str) = inc.as_str() { + resolved + .include_dirs + .push(dep_root.join(inc_str).to_string_lossy().to_string()); + } + } + } + let lib_path = dep_root.join("target").join("lib"); + if lib_path.exists() { + resolved + .lib_dirs + .push(lib_path.to_string_lossy().to_string()); + } + if let Some(libs) = table.get("libs").and_then(|v| v.as_array()) { + for lib in libs { + if let Some(lib_str) = lib.as_str() { + resolved.libs.push(lib_str.to_string()); + } + } + } else { + resolved.libs.push(name.clone()); } - (dep_dir, true) } - }; - - if !dep_path.is_dir() { - return Err(format!( - "Dependency '{}' path is not a directory: {}", - dep.name, - dep_path.display() - )); } - - let include = resolve_paths(&dep_path, dep.include_raw.as_deref(), &["include"], profile)?; - let lib = resolve_paths( - &dep_path, - dep.lib_raw.as_deref(), - &["lib", "lib64"], - profile, - )?; - let libs_list = dep - .libs_raw - .clone() - .unwrap_or_else(|| vec![dep.name.clone()]); - - if include.is_empty() || lib.is_empty() { - return Err(format!( - "Dependency '{}' missing include/lib dirs. Add include/lib in dcr.toml.", - dep.name - )); - } - - include_dirs.extend(include.iter().map(|p| p.to_string_lossy().to_string())); - lib_dirs.extend(lib.iter().map(|p| p.to_string_lossy().to_string())); - libs.extend(libs_list.iter().cloned()); - - if !is_git { - let dep_cache = project_root - .join("target") - .join(profile) - .join("deps") - .join(&dep.name); - sync_dep_dir(&dep_path, &dep_cache) - .map_err(|err| format!("Failed to sync dep {}: {err}", dep.name))?; - } - - let dep_version = read_dep_version(&dep_path).unwrap_or_else(|| "0.0.0".to_string()); - let dep_checksum = compute_checksum(&dep_path) - .map_err(|err| format!("Failed to hash dep {}: {err}", dep.name))?; - let source_str = match &dep.source { - DepSource::Path(p) => format!("path+{}", p), - DepSource::Git { url, .. } => format!("git+{}", url), - }; - lock_packages.push(DepLock { - name: dep.name.clone(), - version: dep_version, - checksum: dep_checksum, - source: source_str, - }); } - write_lock( + crate::core::deps::lock::write_lock( project_root, &project_name, &project_version, &lock_packages, )?; - Ok(ResolvedDeps { - include_dirs, - lib_dirs, - libs, - }) -} - -fn parse_dependencies( - config: &Config, - profile: &str, - target: Option<&str>, -) -> Result, String> { - let mut deps_table = None; - let combinations = if let Some(t) = target { - let normalized_t = crate::cli::build::normalize_target_os(t); - vec![ - format!("dependencies.{}.{}", normalized_t, profile), - format!("dependencies.{}.{}", profile, normalized_t), - format!("dependencies.{}", normalized_t), - format!("dependencies.{}", profile), - "dependencies".to_string(), - ] - } else { - vec![ - format!("dependencies.{}", profile), - "dependencies".to_string(), - ] - }; - for key in combinations { - if let Some(val) = config.get(&key).and_then(|v| v.as_table()) { - deps_table = Some(val); - break; - } - } - let deps_table = match deps_table { - Some(t) => t, - None => return Ok(Vec::new()), - }; - let mut deps = Vec::new(); - for (name, value) in deps_table { - match value { - Value::String(path) => { - deps.push(DepSpec { - name: name.to_string(), - source: DepSource::Path(path.to_string()), - include_raw: None, - lib_raw: None, - libs_raw: None, - }); - } - Value::Table(tbl) => { - if tbl.get("system").and_then(|v| v.as_bool()).unwrap_or(false) { - return Err(format!( - "Dependency '{}' uses system=true, which is not supported yet", - name - )); - } - - let source = if let Some(git_url) = tbl.get("git").and_then(|v| v.as_str()) { - DepSource::Git { - url: git_url.to_string(), - branch: tbl - .get("branch") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - tag: tbl - .get("tag") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - rev: tbl - .get("rev") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - } - } else if let Some(path) = tbl.get("path").and_then(|v| v.as_str()) { - DepSource::Path(path.to_string()) - } else { - return Err(format!("Dependency '{}' missing path or git", name)); - }; - - let include = parse_string_list(tbl.get("include"), name, "include")?; - let lib = parse_string_list(tbl.get("lib"), name, "lib")?; - let libs = parse_string_list(tbl.get("libs"), name, "libs")?; - deps.push(DepSpec { - name: name.to_string(), - source, - include_raw: include, - lib_raw: lib, - libs_raw: libs, - }); - } - _ => { - return Err(format!("Dependency '{}' must be a string or a table", name)); - } - } - } - deps.sort_by(|a, b| a.name.cmp(&b.name)); - Ok(deps) -} - -fn parse_string_list( - value: Option<&Value>, - dep_name: &str, - key: &str, -) -> Result>, String> { - let Some(value) = value else { return Ok(None) }; - let list = value.as_array().ok_or_else(|| { - format!( - "Dependency '{}' field '{}' must be an array of strings", - dep_name, key - ) - })?; - let mut out = Vec::new(); - for item in list { - let s = item - .as_str() - .ok_or_else(|| format!("Dependency '{}' field '{}' must be strings", dep_name, key))?; - out.push(s.to_string()); - } - Ok(Some(out)) + Ok(resolved) } diff --git a/src/core/deps/register.rs b/src/core/deps/register.rs new file mode 100644 index 0000000..8733ace --- /dev/null +++ b/src/core/deps/register.rs @@ -0,0 +1,76 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; +use std::fs; +use std::path::{Path, PathBuf}; +use toml::Value as TomlValue; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Registry { + pub url: String, + pub priority: i32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DcrConfig { + pub registry: std::collections::HashMap, +} + +pub fn get_registry_config() -> Option { + let home = std::env::var("HOME").ok()?; + let config_path = Path::new(&home).join(".dcr/config.toml"); + if !config_path.exists() { + return None; + } + + let content = fs::read_to_string(config_path).ok()?; + toml::from_str(&content).ok() +} + +pub fn get_index_path() -> PathBuf { + std::env::var("DCR_INDEX_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string()); + Path::new(&home).join(".dcr").join("index.json") + }) +} + +pub fn resolve_package_from_registry(name: &str) -> Result { + let config = get_registry_config().ok_or("No registry config found")?; + let mut registries: Vec<(&String, &Registry)> = config.registry.iter().collect(); + registries.sort_by(|a, b| b.1.priority.cmp(&a.1.priority)); + + for (_name_reg, _reg) in registries { + let index_path = get_index_path(); + if index_path.exists() { + let index_content = fs::read_to_string(&index_path).map_err(|e| e.to_string())?; + let index: JsonValue = + serde_json::from_str(&index_content).map_err(|e| e.to_string())?; + if let Some(pkgs) = index.get("packages").and_then(|v| v.as_array()) { + for pkg in pkgs { + if pkg.get("name").and_then(|v| v.as_str()) == Some(name) { + return Ok(pkg.clone()); + } + } + } + } + } + Err(format!( + "Package {} not found in registry (checked: {:?})", + name, + get_index_path() + )) +} + +pub fn is_registry_dep(value: &TomlValue) -> bool { + if value.is_str() { + return true; + } + if let Some(table) = value.as_table() { + return !table.contains_key("git") + && !table.contains_key("path") + && !table.contains_key("url") + && !table.contains_key("version"); + } + false +} diff --git a/src/core/mod.rs b/src/core/mod.rs index 9fc634e..5f5c008 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,5 +1,6 @@ pub mod builder; pub mod config; pub mod deps; +pub mod registry; pub mod runner; pub mod workspace; diff --git a/src/core/registry.rs b/src/core/registry.rs new file mode 100644 index 0000000..3625855 --- /dev/null +++ b/src/core/registry.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Registry { + pub url: String, + pub priority: i32, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Config { + pub registry: HashMap, +} + +pub struct RegistryManager { + pub config: Config, + #[allow(dead_code)] + pub path: PathBuf, +} + +impl RegistryManager { + pub fn load() -> Result> { + let home = std::env::var("HOME")?; + let dcr_dir = PathBuf::from(home).join(".dcr"); + let config_path = dcr_dir.join("config.toml"); + + if !config_path.exists() { + return Err("Config file ~/.dcr/config.toml not found".into()); + } + + let content = fs::read_to_string(&config_path)?; + let config: Config = toml::from_str(&content)?; + + Ok(Self { + config, + path: dcr_dir, + }) + } +} diff --git a/src/core/runner.rs b/src/core/runner.rs index 859a5f4..1ae19d7 100644 --- a/src/core/runner.rs +++ b/src/core/runner.rs @@ -1,44 +1,21 @@ use crate::platform; -use crate::utils::fs::check_dir; -use crate::utils::log::warn; use std::path::Path; use std::process::Command; pub fn run_binary(project_name: &str, profile: &str, target_dir: Option<&str>) -> i32 { let bin_path = platform::bin_path(profile, project_name, target_dir); if Path::new(&bin_path).exists() { - let output = Command::new(&bin_path) - .output() - .unwrap_or_else(|_| panic!("Failed to run {}", bin_path)); - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); - return output.status.code().unwrap_or(1); + let output = Command::new(&bin_path).output(); + match output { + Ok(o) => { + print!("{}", String::from_utf8_lossy(&o.stdout)); + eprint!("{}", String::from_utf8_lossy(&o.stderr)); + return o.status.code().unwrap_or(0); + } + Err(_) => { + return 1; + } + } } - - if target_dir.is_none() - && check_dir(Some(&format!("target/{profile}"))) - .unwrap_or_default() - .contains(&profile.to_string()) - { - warn("Launch of the latest release"); - let bin_path = platform::bin_path(profile, project_name, target_dir); - let output = Command::new(&bin_path) - .output() - .unwrap_or_else(|_| panic!("Failed to run {}", &bin_path)); - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); - return output.status.code().unwrap_or(1); - } - - if target_dir.is_some() && Path::new(&bin_path).exists() { - warn("Launch of the latest release"); - let output = Command::new(&bin_path) - .output() - .unwrap_or_else(|_| panic!("Failed to run {}", &bin_path)); - print!("{}", String::from_utf8_lossy(&output.stdout)); - eprint!("{}", String::from_utf8_lossy(&output.stderr)); - return output.status.code().unwrap_or(1); - } - 1 } diff --git a/src/main.rs b/src/main.rs index 272c016..cb83903 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ fn main() { let code = match cmd { "new" => cli::r#new::new(rest), "init" => cli::init::init(rest), + "setup" => cli::setup::setup(rest), "add" => cli::add::add(rest), "build" => cli::build::build(rest), "run" => cli::run::run(rest), diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 31a5d43..2f4b802 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,7 +1,11 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String { match target_dir { Some(dir) => format!("{}/{}", dir.trim_end_matches('/'), name), - None => format!("./target/{profile}/{name}"), + None => { + let arch = std::env::consts::ARCH; + let target = format!("{arch}-unknown-linux-gnu"); + format!("./target/{target}/{profile}/{name}") + } } } @@ -25,12 +29,16 @@ mod tests { #[test] fn bin_path_default() { - assert_eq!(bin_path("debug", "hello", None), "./target/debug/hello"); + let arch = std::env::consts::ARCH; + let expected = format!("./target/{arch}-unknown-linux-gnu/debug/hello"); + assert_eq!(bin_path("debug", "hello", None), expected); } #[test] fn bin_path_release() { - assert_eq!(bin_path("release", "hello", None), "./target/release/hello"); + let arch = std::env::consts::ARCH; + let expected = format!("./target/{arch}-unknown-linux-gnu/release/hello"); + assert_eq!(bin_path("release", "hello", None), expected); } #[test] diff --git a/tests/cli_basic.rs b/tests/cli_basic.rs index fa3e164..89cfb34 100644 --- a/tests/cli_basic.rs +++ b/tests/cli_basic.rs @@ -280,6 +280,42 @@ fn dcr_add_dependencies() { assert!(!out.status.success(), "dcr add without prefix should fail"); } +#[test] +fn dcr_builds_lib_package() { + let Some(compiler) = available_compiler() else { + eprintln!("no compiler found; skipping lib package test"); + return; + }; + + let dir = unique_sandbox_dir("lib_package"); + let out = run_dcr(&["init"], &dir); + assert!(out.status.success(), "dcr init should succeed"); + + let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); + let updated_toml = toml + .replace("kind = \"bin\"", "kind = \"staticlib\"") + .replace("type = \"none\"", "type = \"lib\""); + std::fs::write(dir.join("dcr.toml"), updated_toml).expect("failed to write toml"); + + std::fs::write(dir.join("src").join("my_lib.h"), "void hello();") + .expect("failed to write header"); + + let envs = [("DCR_COMPILER", compiler)]; + let out = run_dcr_env(&["build"], &dir, &envs); + if !out.status.success() { + eprintln!("stdout: {}", String::from_utf8_lossy(&out.stdout)); + eprintln!("stderr: {}", String::from_utf8_lossy(&out.stderr)); + } + assert!(out.status.success(), "dcr build should succeed"); + + let target_dir = dir.join("target"); + assert!( + target_dir.join("include").join("my_lib.h").is_file(), + "include/my_lib.h missing" + ); + assert!(target_dir.join("lib").exists(), "lib directory missing"); +} + #[test] fn dcr_test_runs_without_sandbox_dependency() { let Some(compiler) = available_compiler() else {