diff --git a/CHANGELOG.md b/CHANGELOG.md index 3098ddb..c68e55a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.2.4] - 2026-02-22 +Added: +- Custom build flags: `build.cflags` and `build.ldflags` +- Recursive source discovery inside `src/` +- Path dependencies with auto include/lib resolution and `dcr.lock` generation (experimental) +- Add docs/index.md and docs/dcr-toml.md + ## [0.2.3] - 2026-02-21 Added: - Modular builders for `gcc`, `clang`, `msvc` diff --git a/Cargo.lock b/Cargo.lock index 4cd691f..c2c6c96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,15 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -60,16 +69,46 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "dcr" -version = "0.2.3" +version = "0.2.4" dependencies = [ "reqwest", "self-replace", "serde", + "sha2", "toml", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -173,6 +212,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -900,6 +949,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1162,6 +1222,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1198,6 +1264,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 2aa3c33..0071582 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dcr" -version = "0.2.3" +version = "0.2.4" edition = "2024" build = "build.rs" @@ -8,4 +8,5 @@ build = "build.rs" reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } serde = { version = "1", features = ["derive"] } self-replace = "1" +sha2 = "0.10" toml = "1.0.3" diff --git a/README.md b/README.md index 6f502f6..e2c1d6d 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,10 @@ Removes the `target` directory in the project root. ## Build Profiles Two profiles are supported: -- `--debug` (default) - flags: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG`(Clang) -- `--release` - flags: `-O3 -DNDEBUG -march=native`(Clang) +- `--debug` (default) - built-in flags per compiler +- `--release` - built-in flags per compiler + +Custom flags can be added in `dcr.toml` via `build.cflags` and `build.ldflags`. ## Configuration The main project file is `dcr.toml`. @@ -131,10 +133,15 @@ version = "0.1.0" language = "c" standard = "c11" compiler = "clang" +# Optional custom flags +cflags = ["-Wall", "-Wextra"] +ldflags = ["-lm"] [dependencies] ``` +Path dependencies are supported. DCR will resolve them on build and generate `dcr.lock`. + ## Requirements - Rust toolchain (`rustc`, `cargo`) - for building DCR from source - C compiler (`clang`, `gcc`, or 'cl'(msvc)) diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..3abced1 --- /dev/null +++ b/TODO.md @@ -0,0 +1,4 @@ +# TODO + +- [ ] Add incremental builds +- [ ] Add external package managers (Conan, vcpkg) diff --git a/docs/dcr-toml.md b/docs/dcr-toml.md new file mode 100644 index 0000000..1a81d8b --- /dev/null +++ b/docs/dcr-toml.md @@ -0,0 +1,53 @@ +# dcr.toml + +`dcr.toml` describes your project, build options, and dependencies. + +## Example + +```toml +[package] +name = "neoarc-frontent" +version = "0.1.0" + +[build] +language = "c" +standard = "c11" +compiler = "gcc" +# Custom compile/link flags (optional) +cflags = ["-D_POSIX_C_SOURCE=200809L", "-Wall", "-Wextra", "-O2", "-Ilib/frecli"] +ldflags = ["-Llib/frecli", "-lfrecli", "-lm"] + +[dependencies] +frecli = { path = "./lib/frecli", include = ["."], lib = ["."], libs = ["frecli"] } +``` + +## Sections + +### [package] +- `name` (string, required) +- `version` (string, required) + +### [build] +- `language` (string, required): `c`, `c++`, `cpp`, or `cxx` +- `standard` (string, required): e.g. `c99`, `c11`, `c++17` +- `compiler` (string, required): `gcc`, `clang`, `cl`(msvc), or `clang-cl` +- `cflags` (string[], optional): additional compile flags +- `ldflags` (string[], optional): additional link flags + +### [dependencies] +Only `path` dependencies are supported right now. + +```toml +[dependencies] +fmt = { path = "./lib/fmt", include = ["include"], lib = ["lib"], libs = ["fmt"] } +``` + +Rules: +- `path` can be absolute or relative to the project root. +- If `include`/`lib` are missing, DCR looks for `include`, `lib`, or `lib64` under the dependency path. +- If not found, build fails with a message suggesting to add `include`/`lib`. +- `libs` defaults to the dependency name. + +## Source Discovery + +DCR compiles all `.c`/`.cpp` files under `src/` recursively. The file containing `main()` can be located anywhere in that tree. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..435cb3d --- /dev/null +++ b/docs/index.md @@ -0,0 +1,41 @@ +# DCR Documentation + +DCR is a lightweight build tool for C/C++ projects with a Cargo-like workflow. + +## Quick Start + +```sh +dcr new hello +cd hello +dcr run +``` + +## Configuration + +Project configuration lives in `dcr.toml`. +See `docs/dcr-toml.md` for the full schema and examples. + +## Build Flags + +You can add custom compile and link flags in `[build]`: + +```toml +[build] +compiler = "gcc" +standard = "c99" +cflags = ["-D_POSIX_C_SOURCE=200809L", "-Wall", "-Wextra", "-O2", "-Ilib/frecli"] +ldflags = ["-Llib/frecli", "-lfrecli", "-lm"] +``` + +## Dependencies + +Path dependencies can be declared in `[dependencies]` and are resolved on build. + +```toml +[dependencies] +frecli = { path = "./lib/frecli", include = ["."], lib = ["."], libs = ["frecli"] } +``` + +## Source Discovery + +DCR compiles all `.c`/`.cpp` files under `src/` recursively. The file containing `main()` can be in any subdirectory. diff --git a/docs/CONTRIBUTING_ru.md b/docs/ru/CONTRIBUTING_ru.md similarity index 100% rename from docs/CONTRIBUTING_ru.md rename to docs/ru/CONTRIBUTING_ru.md diff --git a/docs/FAQ_ru.md b/docs/ru/FAQ_ru.md similarity index 100% rename from docs/FAQ_ru.md rename to docs/ru/FAQ_ru.md diff --git a/docs/README_ru.md b/docs/ru/README_ru.md similarity index 89% rename from docs/README_ru.md rename to docs/ru/README_ru.md index 26ce904..825a508 100644 --- a/docs/README_ru.md +++ b/docs/ru/README_ru.md @@ -114,8 +114,10 @@ hello/ ## Профили сборки Поддерживаются два профиля: -- `--debug` (по умолчанию) — флаги: `-O0 -g -Wall -Wextra -fno-omit-frame-pointer -DDEBUG`(Clang) -- `--release` — флаги: `-O3 -DNDEBUG -march=native`(Clang) +- `--debug` (по умолчанию) — встроенные флаги для каждого компилятора +- `--release` — встроенные флаги для каждого компилятора + +Кастомные флаги можно добавить через `build.cflags` и `build.ldflags` в `dcr.toml`. ## Конфигурация Основной файл проекта — `dcr.toml`. @@ -131,10 +133,15 @@ version = "0.1.0" language = "c" standard = "c11" compiler = "clang" +# Опциональные флаги +cflags = ["-Wall", "-Wextra"] +ldflags = ["-lm"] [dependencies] ``` +Поддерживаются path-зависимости. DCR резолвит их при сборке и создаёт `dcr.lock`. + ## Требования - Rust toolchain (`rustc`, `cargo`) - Для ручной сборки DCR - C-компилятор (`clang`, `gcc` или 'cl'(msvc)) diff --git a/src/cli/build.rs b/src/cli/build.rs index cf7ddd3..5b55297 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -1,6 +1,7 @@ use crate::config::{PROFILE, flags}; use crate::core::builder::{BuildContext, build as build_project}; use crate::core::config::Config; +use crate::core::deps::resolve_deps; use crate::utils::fs::check_dir; use crate::utils::log::{error, warn}; use crate::utils::text::{BOLD_GREEN, colored}; @@ -29,6 +30,20 @@ pub fn build(args: &[String]) -> i32 { let project_compiler = get_config_str(&config, "build.compiler"); let build_language = get_config_str(&config, "build.language"); let build_standard = get_config_str(&config, "build.standard"); + let build_cflags = match get_config_list(&config, "build.cflags") { + Ok(v) => v, + Err(msg) => { + error(&msg); + return 1; + } + }; + let build_ldflags = match get_config_list(&config, "build.ldflags") { + Ok(v) => v, + Err(msg) => { + error(&msg); + return 1; + } + }; println!( " Building project `{}`\n Profile: {}\n Compiler: {}\n", @@ -39,12 +54,31 @@ pub fn build(args: &[String]) -> i32 { ensure_target_dirs(&items, &active_profile); + let project_root = match std::env::current_dir() { + Ok(p) => p, + Err(_) => { + error("Failed to determine project root"); + return 1; + } + }; + let resolved = match resolve_deps(&config, &active_profile, &project_root) { + Ok(r) => r, + Err(msg) => { + error(&msg); + return 1; + } + }; let ctx = BuildContext { profile: &active_profile, project_name: &project_name, compiler: &project_compiler, language: &build_language, standard: &build_standard, + include_dirs: &resolved.include_dirs, + lib_dirs: &resolved.lib_dirs, + libs: &resolved.libs, + cflags: &build_cflags, + ldflags: &build_ldflags, }; match run_build(&ctx) { Ok(times) => { @@ -86,6 +120,24 @@ fn get_config_str(config: &Config, key: &str) -> String { .to_string() } +fn get_config_list(config: &Config, key: &str) -> Result, String> { + let value = match config.get(key) { + Some(v) => v, + None => return Ok(Vec::new()), + }; + let arr = value + .as_array() + .ok_or_else(|| format!("{key} must be an array of strings"))?; + let mut out = Vec::new(); + for item in arr { + let s = item + .as_str() + .ok_or_else(|| format!("{key} must be an array of strings"))?; + out.push(s.to_string()); + } + Ok(out) +} + fn ensure_target_dirs(items: &[String], profile: &str) { if !items.contains(&"target".to_string()) { let _ = fs::create_dir("./target"); diff --git a/src/core/builder/clang.rs b/src/core/builder/clang.rs index 3888b35..4c690a6 100644 --- a/src/core/builder/clang.rs +++ b/src/core/builder/clang.rs @@ -22,6 +22,21 @@ pub fn build(ctx: &BuildContext) -> Result { for flag in default_flags(ctx.profile) { cmd.arg(flag); } + for flag in ctx.cflags { + cmd.arg(flag); + } + for dir in ctx.include_dirs { + cmd.arg(format!("-I{dir}")); + } + 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(platform::bin_path(ctx.profile, ctx.project_name)); @@ -38,10 +53,23 @@ pub fn build(ctx: &BuildContext) -> Result { fn collect_sources(language: &str) -> Result, String> { let lang = language.to_lowercase(); let mut sources = Vec::new(); - let entries = fs::read_dir("./src").map_err(|err| format!("src dir error: {err}"))?; + collect_sources_rec("./src", &lang, &mut sources)?; + sources.sort(); + if sources.is_empty() { + return Err("No source files found in ./src".to_string()); + } + Ok(sources) +} + +fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec) -> Result<(), String> { + let entries = fs::read_dir(dir).map_err(|err| format!("src dir error: {err}"))?; for entry in entries { let entry = entry.map_err(|err| format!("src dir error: {err}"))?; let path = entry.path(); + if path.is_dir() { + collect_sources_rec(&path.to_string_lossy(), lang, out)?; + continue; + } if !path.is_file() { continue; } @@ -55,14 +83,10 @@ fn collect_sources(language: &str) -> Result, String> { || ((lang == "c++" || lang == "cpp" || lang == "cxx") && (ext == "cpp" || ext == "cxx" || ext == "cc")); if allowed { - sources.push(file); + out.push(file); } } - sources.sort(); - if sources.is_empty() { - return Err("No source files found in ./src".to_string()); - } - Ok(sources) + Ok(()) } fn default_flags(profile: &str) -> &'static [&'static str] { diff --git a/src/core/builder/gcc.rs b/src/core/builder/gcc.rs index 3888b35..4c690a6 100644 --- a/src/core/builder/gcc.rs +++ b/src/core/builder/gcc.rs @@ -22,6 +22,21 @@ pub fn build(ctx: &BuildContext) -> Result { for flag in default_flags(ctx.profile) { cmd.arg(flag); } + for flag in ctx.cflags { + cmd.arg(flag); + } + for dir in ctx.include_dirs { + cmd.arg(format!("-I{dir}")); + } + 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(platform::bin_path(ctx.profile, ctx.project_name)); @@ -38,10 +53,23 @@ pub fn build(ctx: &BuildContext) -> Result { fn collect_sources(language: &str) -> Result, String> { let lang = language.to_lowercase(); let mut sources = Vec::new(); - let entries = fs::read_dir("./src").map_err(|err| format!("src dir error: {err}"))?; + collect_sources_rec("./src", &lang, &mut sources)?; + sources.sort(); + if sources.is_empty() { + return Err("No source files found in ./src".to_string()); + } + Ok(sources) +} + +fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec) -> Result<(), String> { + let entries = fs::read_dir(dir).map_err(|err| format!("src dir error: {err}"))?; for entry in entries { let entry = entry.map_err(|err| format!("src dir error: {err}"))?; let path = entry.path(); + if path.is_dir() { + collect_sources_rec(&path.to_string_lossy(), lang, out)?; + continue; + } if !path.is_file() { continue; } @@ -55,14 +83,10 @@ fn collect_sources(language: &str) -> Result, String> { || ((lang == "c++" || lang == "cpp" || lang == "cxx") && (ext == "cpp" || ext == "cxx" || ext == "cc")); if allowed { - sources.push(file); + out.push(file); } } - sources.sort(); - if sources.is_empty() { - return Err("No source files found in ./src".to_string()); - } - Ok(sources) + Ok(()) } fn default_flags(profile: &str) -> &'static [&'static str] { diff --git a/src/core/builder/mod.rs b/src/core/builder/mod.rs index 5d275c0..660b12c 100644 --- a/src/core/builder/mod.rs +++ b/src/core/builder/mod.rs @@ -8,6 +8,11 @@ pub struct BuildContext<'a> { pub compiler: &'a str, pub language: &'a str, pub standard: &'a str, + pub include_dirs: &'a [String], + pub lib_dirs: &'a [String], + pub libs: &'a [String], + pub cflags: &'a [String], + pub ldflags: &'a [String], } pub fn build(ctx: &BuildContext) -> Result { diff --git a/src/core/builder/msvc.rs b/src/core/builder/msvc.rs index 9162b8e..3c46269 100644 --- a/src/core/builder/msvc.rs +++ b/src/core/builder/msvc.rs @@ -35,6 +35,28 @@ pub fn build(ctx: &BuildContext) -> Result { for flag in default_flags(ctx.profile) { cmd.arg(flag); } + for flag in ctx.cflags { + cmd.arg(flag); + } + for dir in ctx.include_dirs { + cmd.arg(format!("/I{dir}")); + } + 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")); + } + } + if !ctx.ldflags.is_empty() { + cmd.arg("/link"); + for flag in ctx.ldflags { + cmd.arg(flag); + } + } for source in &sources { cmd.arg(source); @@ -57,10 +79,23 @@ pub fn build(ctx: &BuildContext) -> Result { fn collect_sources(language: &str) -> Result, String> { let lang = language.to_lowercase(); let mut sources = Vec::new(); - let entries = fs::read_dir("./src").map_err(|err| format!("src dir error: {err}"))?; + collect_sources_rec("./src", &lang, &mut sources)?; + sources.sort(); + if sources.is_empty() { + return Err("No source files found in ./src".to_string()); + } + Ok(sources) +} + +fn collect_sources_rec(dir: &str, lang: &str, out: &mut Vec) -> Result<(), String> { + let entries = fs::read_dir(dir).map_err(|err| format!("src dir error: {err}"))?; for entry in entries { let entry = entry.map_err(|err| format!("src dir error: {err}"))?; let path = entry.path(); + if path.is_dir() { + collect_sources_rec(&path.to_string_lossy(), lang, out)?; + continue; + } if !path.is_file() { continue; } @@ -74,14 +109,10 @@ fn collect_sources(language: &str) -> Result, String> { || ((lang == "c++" || lang == "cpp" || lang == "cxx") && (ext == "cpp" || ext == "cxx" || ext == "cc")); if allowed { - sources.push(file); + out.push(file); } } - sources.sort(); - if sources.is_empty() { - return Err("No source files found in ./src".to_string()); - } - Ok(sources) + Ok(()) } fn msvc_standard_flag(language: &str, standard: &str) -> Result { diff --git a/src/core/config.rs b/src/core/config.rs index cf732f5..dda2c1c 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -217,19 +217,66 @@ fn format_toml(value: &Value) -> Result { out.push_str(&format!("language = \"{language}\"\n")); out.push_str(&format!("standard = \"{standard}\"\n")); out.push_str(&format!("compiler = \"{compiler}\"\n\n")); + if let Some(cflags) = build.get("cflags") { + out.push_str(&format!("cflags = {}\n", format_string_array(cflags))); + } + if let Some(ldflags) = build.get("ldflags") { + out.push_str(&format!("ldflags = {}\n", format_string_array(ldflags))); + } + if build.contains_key("cflags") || build.contains_key("ldflags") { + out.push('\n'); + } out.push_str("[dependencies]\n"); if !deps.is_empty() { let mut keys: Vec<&String> = deps.keys().collect(); keys.sort(); for key in keys { - let val = deps.get(key).and_then(|v| v.as_str()).unwrap_or(""); - out.push_str(&format!("{key} = \"{val}\"\n")); + if let Some(val) = deps.get(key) { + out.push_str(&format!("{key} = {}\n", format_dep_value(val))); + } } } Ok(out) } +fn format_dep_value(value: &Value) -> String { + match value { + Value::String(s) => format!("\"{s}\""), + Value::Table(tbl) => { + let mut parts = Vec::new(); + if let Some(v) = tbl.get("path").and_then(|v| v.as_str()) { + parts.push(format!("path = \"{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(), + } +} + +fn format_string_array(value: &Value) -> String { + if let Some(arr) = value.as_array() { + let items: Vec = arr + .iter() + .filter_map(|v| v.as_str().map(|s| format!("\"{s}\""))) + .collect(); + return format!("[{}]", items.join(", ")); + } + "[]".to_string() +} + fn get_path<'a>(value: &'a Value, path: &[&str]) -> Option<&'a Value> { let mut current = value; for key in path { diff --git a/src/core/deps.rs b/src/core/deps.rs new file mode 100644 index 0000000..41b8e5a --- /dev/null +++ b/src/core/deps.rs @@ -0,0 +1,353 @@ +use crate::core::config::Config; +use sha2::{Digest, Sha256}; +use std::fs; +use std::path::{Path, PathBuf}; +use toml::Value; + +#[derive(Debug, Clone)] +pub struct DepSpec { + pub name: String, + pub path_raw: String, + pub include_raw: Option>, + pub lib_raw: Option>, + pub libs_raw: Option>, +} + +#[derive(Debug, Clone)] +pub struct ResolvedDeps { + pub include_dirs: Vec, + pub lib_dirs: Vec, + pub libs: Vec, +} + +pub fn resolve_deps( + config: &Config, + profile: &str, + project_root: &Path, +) -> Result { + let project_name = config + .get("package.name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let project_version = config + .get("package.version") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let deps = parse_dependencies(config)?; + 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(); + + for dep in deps { + let dep_path = resolve_path(project_root, &dep.path_raw)?; + 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"])?; + let lib = resolve_paths(&dep_path, dep.lib_raw.as_deref(), &["lib", "lib64"])?; + 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()); + + 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))?; + lock_packages.push(DepLock { + name: dep.name.clone(), + version: dep_version, + checksum: dep_checksum, + source: format!("path+{}", dep.path_raw), + }); + } + + write_lock( + project_root, + &project_name, + &project_version, + &lock_packages, + )?; + + Ok(ResolvedDeps { + include_dirs, + lib_dirs, + libs, + }) +} + +#[derive(Debug, Clone)] +struct DepLock { + name: String, + version: String, + checksum: String, + source: String, +} + +fn parse_dependencies(config: &Config) -> Result, String> { + let deps_val = match config.get("dependencies") { + Some(v) => v, + None => return Ok(Vec::new()), + }; + let deps_table = deps_val + .as_table() + .ok_or_else(|| "dependencies is not a table".to_string())?; + let mut deps = Vec::new(); + for (name, value) in deps_table { + match value { + 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 path = tbl + .get("path") + .and_then(|v| v.as_str()) + .ok_or_else(|| format!("Dependency '{}' missing path", name))? + .to_string(); + 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(), + path_raw: path, + include_raw: include, + lib_raw: lib, + libs_raw: libs, + }); + } + _ => { + return Err(format!( + "Dependency '{}' must be a table with path/include/lib", + 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)) +} + +fn resolve_path(project_root: &Path, raw: &str) -> Result { + let p = Path::new(raw); + let full = if p.is_absolute() { + p.to_path_buf() + } else { + project_root.join(p) + }; + Ok(full) +} + +fn resolve_paths( + base: &Path, + raw: Option<&[String]>, + defaults: &[&str], +) -> Result, String> { + let mut out = Vec::new(); + if let Some(raw) = raw { + for r in raw { + let p = Path::new(r); + let full = if p.is_absolute() { + p.to_path_buf() + } else { + base.join(p) + }; + if !full.exists() { + return Err(format!("Path does not exist: {}", full.display())); + } + out.push(full); + } + return Ok(out); + } + + for d in defaults { + let candidate = base.join(d); + if candidate.exists() { + out.push(candidate); + } + } + Ok(out) +} + +fn sync_dep_dir(src: &Path, dst: &Path) -> std::io::Result<()> { + if dst.exists() { + fs::remove_dir_all(dst)?; + } + copy_dir_all(src, dst) +} + +fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + let from = entry.path(); + let to = dst.join(entry.file_name()); + if ty.is_dir() { + copy_dir_all(&from, &to)?; + } else { + fs::copy(&from, &to)?; + } + } + Ok(()) +} + +fn write_lock( + project_root: &Path, + project_name: &str, + project_version: &str, + packages: &[DepLock], +) -> Result<(), String> { + let mut out = String::new(); + 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", + quote_list(&packages.iter().map(|p| p.name.clone()).collect::>()) + )); + } + for pkg in packages { + out.push_str("[[package]]\n"); + out.push_str(&format!("name = \"{}\"\n", escape_value(&pkg.name))); + out.push_str(&format!("version = \"{}\"\n", escape_value(&pkg.version))); + out.push_str(&format!("source = \"{}\"\n", escape_value(&pkg.source))); + out.push_str(&format!("checksum = \"{}\"\n", escape_value(&pkg.checksum))); + out.push('\n'); + } + fs::write(project_root.join("dcr.lock"), out) + .map_err(|err| format!("Failed to write dcr.lock: {err}"))?; + Ok(()) +} + +fn quote_list(items: &[String]) -> String { + items + .iter() + .map(|s| format!("\"{}\"", escape_value(s))) + .collect::>() + .join(", ") +} + +fn escape_value(input: &str) -> String { + input.replace('\\', "\\\\").replace('"', "\\\"") +} + +fn read_dep_version(dep_path: &Path) -> Option { + let path = dep_path.join("dcr.toml"); + let content = fs::read_to_string(path).ok()?; + let value: Value = content.parse().ok()?; + value + .get("package") + .and_then(|v| v.as_table()) + .and_then(|t| t.get("version")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) +} + +fn compute_checksum(root: &Path) -> Result { + let mut files = Vec::new(); + collect_files(root, root, &mut files)?; + files.sort(); + let mut hasher = Sha256::new(); + for rel in files { + hasher.update(rel.as_bytes()); + let data = + fs::read(root.join(&rel)).map_err(|err| format!("failed to read {}: {err}", rel))?; + hasher.update(&data); + } + let hash = hasher.finalize(); + Ok(to_hex(&hash)) +} + +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}"))?; + let path = entry.path(); + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if name_str == "target" { + continue; + } + if path.is_dir() { + collect_files(root, &path, out)?; + } else if path.is_file() { + let rel = path + .strip_prefix(root) + .unwrap_or(&path) + .to_string_lossy() + .to_string(); + out.push(rel); + } + } + Ok(()) +} + +fn to_hex(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len() * 2); + for b in bytes { + out.push_str(&format!("{:02x}", b)); + } + out +} diff --git a/src/core/mod.rs b/src/core/mod.rs index 5dc0a79..8c5794b 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,2 +1,3 @@ pub mod builder; pub mod config; +pub mod deps;