diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fd6fd8..a66c53a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## [v0.9.1] - 2026-09-13 "Системные pkg-config-зависимости / System pkg-config Dependencies" + +### RU + +**Добавлено:** + +- **Зависимости через `pkg-config`** — `fmt = { pkg-config = "fmt" }` или массив имён пакетов (`pkg-config = ["openssl", "libcrypto"]`) подключает системные библиотеки через `.pc`-метаданные. +- **Поддержка префиксов в `dcr add`** — команды `dcr add fmt pkg-config:fmt` и `dcr add openssl pkg:openssl,libcrypto` добавляют зависимости от системных пакетов в `dcr.toml`. +- **Полный набор флагов** — DCR собирает и пробрасывает compiler/linker flags (`-I`, `-L`, `-l`, `-pthread` и другие), включая транзитивные зависимости и интеграцию с `dcr gen` / `dcr test`. +- **Фиксация в `dcr.lock`** — записывается источник `pkg-config+...` и обнаруженная версия пакета. +- **Отображение в `dcr tree`** — `pkg-config` зависимости наглядно помечаются в дереве зависимостей. + +**Исправлено:** + +- **Ошибка создания проекта в папках со спецсимволами** — `dcr new` больше не завершается ошибкой `Failed to create dcr.toml`, если родительская директория содержит пробелы или не-ASCII символы. + +### EN + +**Added:** + +- **`pkg-config` dependencies** — `fmt = { pkg-config = "fmt" }` or an array of package names (`pkg-config = ["openssl", "libcrypto"]`) connects system libraries through their `.pc` metadata. +- **Prefix support in `dcr add`** — `dcr add fmt pkg-config:fmt` and `dcr add openssl pkg:openssl,libcrypto` add system package dependencies to `dcr.toml`. +- **Complete flag propagation** — compiler/linker flags (`-I`, `-L`, `-l`, `-pthread`, and others) are preserved and passed to builds, `dcr test`, and IDE integrations via `dcr gen`. +- **Lockfile entries** — `dcr.lock` records the `pkg-config+...` source and detected package version. +- **`dcr tree` representation** — `pkg-config` dependencies are clearly labeled in the dependency tree. + +**Fixed:** + +- **Project creation failure in special character directories** — `dcr new` no longer fails with `Failed to create dcr.toml` when invoked inside parent folders with spaces or non-ASCII characters. + ## [v0.9.0] - 2026-08-31 "Полные Path/Git-зависимости и готовые библиотеки / Complete Path/Git Dependencies & Prebuilt Libraries" ### RU diff --git a/Cargo.lock b/Cargo.lock index 19e43e3..d54e563 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,7 +154,7 @@ dependencies = [ [[package]] name = "dcr" -version = "0.9.0" +version = "0.9.1" dependencies = [ "ctrlc", "fatfs", diff --git a/Cargo.toml b/Cargo.toml index 44a2809..ebf2e9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dcr" -version = "0.9.0" +version = "0.9.1" edition = "2024" build = "build.rs" description = "DCR is a utility for managing C/C++ projects in a Cargo-like style." @@ -97,4 +97,4 @@ debug = "line-tables-only" [profile.dev.package."*"] opt-level = 1 -debug = false \ No newline at end of file +debug = false diff --git a/docs/changelog.mdx b/docs/changelog.mdx index 48aa30b..85da40c 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -4,6 +4,13 @@ sidebar_label: Changelog # Changelog +## 0.9.1 (2026-09-13) + +- **`pkg-config` dependencies** — declare `fmt = { pkg-config = "fmt" }` or multiple package names in `[dependencies]`; DCR applies `.pc` compiler/linker flags and records the resolved package in `dcr.lock`. +- **`dcr add` support for `pkg-config`** — easily add system package dependencies with `dcr add fmt pkg-config:fmt` or `dcr add openssl pkg:openssl,libcrypto`. +- **`dcr tree` & IDE integration** — `pkg-config` packages are marked in `dcr tree` and resolved into compiler flags in `dcr gen` (`compile_commands.json`). +- **Fixed `dcr new` in non-ASCII / spaced paths** — initializing projects from folders with non-ASCII characters or spaces no longer fails validation. + ## 0.9.0 (2026-08-31) - **Download mirrors in `dcrup`** — `--mirror auto|cf|gh|sf|ya` and `DCRUP_MIRROR`. diff --git a/docs/commands/dependency-commands.mdx b/docs/commands/dependency-commands.mdx index 64150fb..8ee6bb1 100644 --- a/docs/commands/dependency-commands.mdx +++ b/docs/commands/dependency-commands.mdx @@ -15,6 +15,8 @@ dcr add mylib path:./lib # explicit path prefix dcr add mylib git:https://github.com/user/repo # explicit git prefix dcr add mylib github:user/repo # GitHub shorthand dcr add mylib gitlab:user/repo # GitLab shorthand +dcr add fmt pkg-config:fmt # system package via pkg-config +dcr add openssl pkg:openssl,libcrypto # multiple pkg-config packages ``` Source prefixes: @@ -22,6 +24,7 @@ Source prefixes: - `git:` — generic git URL - `github:` — expands to `https://github.com//` - `gitlab:` — expands to `https://gitlab.com//` +- `pkg-config:` / `pkg:` — one or more packages resolved by `pkg-config` - `http://` / `https://` / `git@` — full URL Flags: @@ -31,6 +34,11 @@ Flags: If source is omitted — DCR searches connected registries. A bare relative or absolute path is accepted as a path dependency. +For `pkg-config` dependencies, the referenced packages must already be installed +and provide `.pc` files. DCR invokes `pkg-config` during the build (using +`PATH` and `PKG_CONFIG_PATH`) and forwards the returned compiler and linker +flags; it does not download or build these packages. + ## `dcr tree` Displays the project dependency tree. diff --git a/docs/reference/dcr-toml.mdx b/docs/reference/dcr-toml.mdx index ac0a45d..2c07f63 100644 --- a/docs/reference/dcr-toml.mdx +++ b/docs/reference/dcr-toml.mdx @@ -111,7 +111,11 @@ Never fails the command — warnings only. Pre-release suffixes (`1.0.0-dev`) ar | `qt` | `false` | Enable automatic Qt meta-object handling (MOC, UIC, RCC) | Settings from raw config (not in typed struct): -- `pkg_config` — list of pkg-config packages +- `pkg_config` — list of packages queried for build-wide compiler/linker flags + +Dependencies can also declare a system package directly, for example +`fmt = { pkg-config = "fmt" }`. This records the package in `dcr.lock` and applies +the flags returned by `pkg-config --cflags` and `pkg-config --libs`. - `ldscript` — linker script path - `build.steps` / `build.post_steps` — codegen steps diff --git a/docs/reference/dependencies.mdx b/docs/reference/dependencies.mdx index f2a696d..cdc04f0 100644 --- a/docs/reference/dependencies.mdx +++ b/docs/reference/dependencies.mdx @@ -6,7 +6,7 @@ sidebar_label: Dependencies ## Formats -The `[dependencies]` section supports three formats: +The `[dependencies]` section supports registry, path, Git, and `pkg-config` dependencies: ### String (registry) @@ -44,6 +44,23 @@ names (`libs`): dcr-math = { path = "../deps/dcr-math", include = ["include"], lib = ["lib"], libs = ["dcr-math"] } ``` +### Table (`pkg-config`) + +Use an installed `.pc` package when the library is provided by the operating system +or another system package manager. DCR invokes `pkg-config --cflags` and +`pkg-config --libs`, preserving include, library, linker, and other flags: + +```toml +[dependencies] +fmt = { pkg-config = "fmt" } +openssl = { pkg-config = ["openssl", "libcrypto"] } +``` + +The `pkg-config` executable must be available in `PATH`. The named `.pc` files must +be available through pkg-config's standard search paths or via `PKG_CONFIG_PATH`. +These dependencies are not downloaded or built by DCR; they are recorded in +`dcr.lock` with a `pkg-config+...` source. + ## Registry DCR uses a package registry for dependency lookup by name. diff --git a/man/man1/dcr-add.1 b/man/man1/dcr-add.1 index e3899a2..a7e07b8 100644 --- a/man/man1/dcr-add.1 +++ b/man/man1/dcr-add.1 @@ -25,6 +25,9 @@ Generic git repository. .B path:./path/to/lib Local path dependency. .TP +.B pkg-config:name +System pkg-config dependency (also accepts \fBpkg:\fR or comma-separated packages). +.TP .B Version from registry. .SH OPTIONS diff --git a/man/man1/dcr.1 b/man/man1/dcr.1 index a5bba7b..4cd34c8 100644 --- a/man/man1/dcr.1 +++ b/man/man1/dcr.1 @@ -1,4 +1,4 @@ -.TH DCR 1 "2026-06-02" "dcr 0.7.0" "DCR Manual" +.TH DCR 1 "2026-08-31" "dcr 0.9.1" "DCR Manual" .SH NAME dcr \- Cargo\-like build tool and package manager for C/C++ projects .SH SYNOPSIS @@ -120,6 +120,9 @@ Generic git repository. .B path:./path/to/lib Local path dependency. .TP +.B pkg-config:name +System pkg-config dependency (also accepts \fBpkg:name1,name2\fR). +.TP .B \fI\fR Version from registry (e.g. \fB1.2.3\fR). .SH GEN SUBCOMMANDS diff --git a/src/cli/add.rs b/src/cli/add.rs index 72e2b0a..44cfb40 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -25,7 +25,7 @@ use toml::map::Map; /// Parsed arguments for the `dcr add` command. /// -/// Exactly one of `path`, `git`, or `version_from_registry` is expected to be set, +/// Exactly one of `path`, `git`, `pkg_config`, or `version_from_registry` is expected to be set, /// depending on how the dependency source was specified. pub struct AddArgs { /// Dependency name as it will appear under `[dependencies]` in `dcr.toml`. @@ -42,6 +42,8 @@ pub struct AddArgs { pub rev: Option, /// Version string resolved from the package registry when no explicit source is given. pub version_from_registry: Option, + /// Package names resolved through pkg-config. + pub pkg_config: Option>, } /// Adds a dependency to the current project's `dcr.toml`. @@ -71,6 +73,7 @@ pub fn add(args: &[String]) -> i32 { println!(" gitlab:user/repo GitLab repository"); println!(" git:host.com/user/repo Generic git repository"); println!(" path:./path/to/lib Local path dependency"); + println!(" pkg-config:fmt System pkg-config dependency"); println!(" Version from registry"); println!(); printc("OPTIONS:", BOLD_GREEN); @@ -133,6 +136,17 @@ pub fn add(args: &[String]) -> i32 { } Value::Table(table) } + } else if let Some(packages) = add_args.pkg_config { + let mut table = Map::new(); + if packages.len() == 1 { + table.insert("pkg-config".to_string(), Value::String(packages[0].clone())); + } else { + table.insert( + "pkg-config".to_string(), + Value::Array(packages.into_iter().map(Value::String).collect()), + ); + } + Value::Table(table) } else if let Some(path) = add_args.path { let mut table = Map::new(); table.insert("path".to_string(), Value::String(path)); @@ -140,7 +154,7 @@ pub fn add(args: &[String]) -> i32 { } else if let Some(version) = add_args.version_from_registry { Value::String(version) } else { - error("Dependency source (path or git) must be provided"); + error("Dependency source (path, git, or pkg-config) must be provided"); return 1; }; @@ -171,6 +185,7 @@ fn parse_add_args(args: &[String]) -> Result { error(" gitlab:user/repo -> gitlab.com/user/repo"); error(" git:host.com/user/repo -> host.com/user/repo"); error(" path:./path/to/lib -> local path"); + error(" pkg-config:fmt -> system pkg-config package"); return Err(1); } @@ -221,6 +236,7 @@ fn parse_add_args(args: &[String]) -> Result { tag: None, rev: None, version_from_registry: Some(version.to_string()), + pkg_config: None, }); } Err(e) => { @@ -262,9 +278,25 @@ fn parse_add_args(args: &[String]) -> Result { let mut path = None; let mut git = None; + let mut pkg_config = None; // Map source prefixes / bare URLs to path or git fields - if let Some(p) = source_spec.strip_prefix("path:") { + if let Some(p) = source_spec + .strip_prefix("pkg-config:") + .or_else(|| source_spec.strip_prefix("pkg:")) + { + let packages = p + .split(',') + .map(str::trim) + .filter(|name| !name.is_empty()) + .map(str::to_string) + .collect::>(); + if packages.is_empty() { + error("pkg-config source must name at least one package"); + return Err(1); + } + pkg_config = Some(packages); + } else if let Some(p) = source_spec.strip_prefix("path:") { path = Some(p.to_string()); } else if let Some(g) = source_spec.strip_prefix("github:") { git = Some(format!("https://github.com/{}", g)); @@ -295,7 +327,7 @@ fn parse_add_args(args: &[String]) -> Result { path = Some(source_spec); } else { error( - "Source must be a path, a prefixed source (path:, git:, github:, gitlab:), or a full URL", + "Source must be a path, pkg-config:, a prefixed source (path:, git:, github:, gitlab:), or a full URL", ); return Err(1); } @@ -308,5 +340,6 @@ fn parse_add_args(args: &[String]) -> Result { tag, rev, version_from_registry: None, + pkg_config, }) } diff --git a/src/cli/gen.rs b/src/cli/gen.rs index d75d0ca..6e73b3a 100644 --- a/src/cli/gen.rs +++ b/src/cli/gen.rs @@ -42,11 +42,13 @@ struct GenDeps { include_dirs: Vec, lib_dirs: Vec, libs: Vec, + cflags: Vec, + ldflags: Vec, } -/// Collects path-dependency include/lib/libs from `dcr.toml` for code generation. +/// Collects dependency metadata from `dcr.toml` for code generation. /// -/// Skips `system` deps and non-path entries. Falls back to common include/lib +/// Resolves path layouts and pkg-config flags while skipping system deps. Falls back to common include/lib /// directory names only when those directories exist. fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> GenDeps { let deps_val = match config.get("dependencies") { @@ -56,6 +58,8 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> include_dirs: vec![], lib_dirs: vec![], libs: vec![], + cflags: vec![], + ldflags: vec![], }; } }; @@ -66,6 +70,8 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> include_dirs: vec![], lib_dirs: vec![], libs: vec![], + cflags: vec![], + ldflags: vec![], }; } }; @@ -73,12 +79,37 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> let mut include_dirs = Vec::new(); let mut lib_dirs = Vec::new(); let mut libs = Vec::new(); + let mut cflags = Vec::new(); + let mut ldflags = Vec::new(); for (name, value) in deps_table { let tbl = match value.as_table() { Some(t) => t, None => continue, }; + let pkg_names = tbl + .get("pkg-config") + .or_else(|| tbl.get("pkg_config")) + .map(|raw| { + raw.as_str() + .map(|s| vec![s.to_string()]) + .or_else(|| { + raw.as_array().map(|a| { + a.iter() + .filter_map(|v| v.as_str().map(str::to_string)) + .collect() + }) + }) + .unwrap_or_default() + }) + .unwrap_or_default(); + if !pkg_names.is_empty() { + let (flags, link_flags) = + crate::utils::build::resolve_pkg_config_flags_lossy(&pkg_names, &[], &[]); + cflags.extend(flags); + ldflags.extend(link_flags); + continue; + } // skip system deps if tbl.get("system").and_then(|v| v.as_bool()).unwrap_or(false) { continue; @@ -169,6 +200,8 @@ fn resolve_deps_for_gen(config: &Config, profile: &str, project_root: &Path) -> include_dirs, lib_dirs, libs, + cflags, + ldflags, } } @@ -350,8 +383,10 @@ fn collect_project_info_inner( }); let resolved = resolve_deps_for_gen(&config, profile, &root); - let (resolved_cflags, resolved_ldflags) = + let (mut resolved_cflags, mut resolved_ldflags) = resolve_pkg_config_flags_lossy(&pkg_configs, &base_cflags, &base_ldflags); + resolved_cflags.extend(resolved.cflags.clone()); + resolved_ldflags.extend(resolved.ldflags.clone()); // Build exclude/include pattern lists (same logic as cli::build) let mut combined_excludes: Vec = Vec::new(); diff --git a/src/cli/init.rs b/src/cli/init.rs index 0c0537c..4d80411 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -23,7 +23,6 @@ use crate::utils::log::{error, warn}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use std::fs; use std::io::Write; -use toml::Value; /// Initializes the current working directory as a new DCR project. /// @@ -91,18 +90,8 @@ pub fn init(args: &[String]) -> i32 { println!("Initializing the project in {cwd}"); // Create and initialize default manifest (dcr.toml). - let mut config = match Config::new("./dcr.toml") { - Ok(cfg) => cfg, - Err(_) => { - error("Failed to create dcr.toml"); - return 1; - } - }; - if config - .edit("package.name", Value::String(project_name.clone())) - .is_err() - { - error("Failed to write dcr.toml"); + if Config::create("./dcr.toml", Some(&project_name)).is_err() { + error("Failed to create dcr.toml"); return 1; } println!( diff --git a/src/cli/new.rs b/src/cli/new.rs index 3592a39..f0671d4 100644 --- a/src/cli/new.rs +++ b/src/cli/new.rs @@ -23,7 +23,6 @@ use crate::utils::log::{error, warn}; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc}; use std::fs; use std::io::Write; -use toml::Value; /// Executes the `dcr new` command to scaffold a new C/C++ project directory structure. /// @@ -110,18 +109,8 @@ pub fn new(args: &[String]) -> i32 { // Initialize and write project configuration manifest (dcr.toml). let toml_path = format!("./{project_name}/dcr.toml"); - let mut config = match Config::new(&toml_path) { - Ok(cfg) => cfg, - Err(_) => { - error("Failed to create dcr.toml"); - return 1; - } - }; - if config - .edit("package.name", Value::String(project_name.to_string())) - .is_err() - { - error("Failed to write dcr.toml"); + if Config::create(&toml_path, Some(project_name)).is_err() { + error("Failed to create dcr.toml"); return 1; } println!( diff --git a/src/cli/test.rs b/src/cli/test.rs index f536d1f..0d614c9 100644 --- a/src/cli/test.rs +++ b/src/cli/test.rs @@ -134,8 +134,12 @@ fn run_testsuite(profile: &str) -> Result { cflags.extend(get_list_with_profile(&config, "cflags", profile)); let ldflags = get_list_with_profile(&config, "ldflags", profile); let pkg_configs = get_list_with_profile(&config, "pkg_config", profile); - let (cflags, ldflags) = resolve_pkg_config_flags(&pkg_configs, &cflags, &ldflags)?; - let includes = test_include_dirs(&config, profile); + let (mut cflags, mut ldflags) = resolve_pkg_config_flags(&pkg_configs, &cflags, &ldflags)?; + let deps = crate::core::deps::resolve_deps(&config, profile, None, Path::new("."))?; + cflags.extend(deps.cflags); + ldflags.extend(deps.ldflags); + let mut includes = test_include_dirs(&config, profile); + includes.extend(deps.include_dirs); let test_sources = collect_test_sources()?; if test_sources.is_empty() { return Err("tests/test.c not found; run 'dcr test --init' first".to_string()); diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 4c24517..5717c9b 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -115,6 +115,19 @@ fn print_deps( format!(" v{}", v) } else if let Some(git) = t.get("git").and_then(|v| v.as_str()) { format!(" ({})", git) + } else if let Some(pkg) = t.get("pkg-config").or_else(|| t.get("pkg_config")) { + if let Some(s) = pkg.as_str() { + format!(" (pkg-config: {})", s) + } else if let Some(arr) = pkg.as_array() { + let names = arr + .iter() + .filter_map(|v| v.as_str()) + .collect::>() + .join(", "); + format!(" (pkg-config: {})", names) + } else { + "".to_string() + } } else { "".to_string() } diff --git a/src/core/build/engine.rs b/src/core/build/engine.rs index 8facd20..4833ab8 100644 --- a/src/core/build/engine.rs +++ b/src/core/build/engine.rs @@ -769,8 +769,10 @@ fn build_project_at_inner( )?; } - let (resolved_cflags, resolved_ldflags) = + let (mut resolved_cflags, mut resolved_ldflags) = resolve_pkg_config_flags(&pkg_configs, &build_cflags, &build_ldflags)?; + resolved_cflags.extend(resolved.cflags.clone()); + resolved_ldflags.extend(resolved.ldflags.clone()); let mut combined_excludes = Vec::new(); for dir in exclude_dirs { combined_excludes.push(dir.clone()); @@ -1011,6 +1013,8 @@ fn extend_transitive_link_deps( consumer_deps.include_dirs.extend(deps.include_dirs); consumer_deps.lib_dirs.extend(deps.lib_dirs); consumer_deps.libs.extend(deps.libs); + consumer_deps.cflags.extend(deps.cflags); + consumer_deps.ldflags.extend(deps.ldflags); for child_root in deps.package_roots { extend_transitive_link_deps( Path::new(&child_root), diff --git a/src/core/build_config.rs b/src/core/build_config.rs index 8144a24..e8ccd6b 100644 --- a/src/core/build_config.rs +++ b/src/core/build_config.rs @@ -244,13 +244,19 @@ pub struct WorkspaceMemberConfig { } impl Config { + #[allow(dead_code)] /// Creates a new Config instance from a TOML file path. If the file does not exist, it creates a default one. pub fn new(path: &str) -> Result { + Self::create(path, None) + } + + /// Creates a new Config instance with an explicit package name for the default template when creating a new file. + pub fn create(path: &str, package_name: Option<&str>) -> Result { let path = PathBuf::from(path); let (data, doc) = if path.exists() { load_parts(&path)? } else { - let content = default_toml_text(); + let content = default_toml_text(package_name)?; let doc: DocumentMut = content.parse().map_err(ConfigError::TomlEdit)?; fs::write(&path, doc.to_string())?; let data: Value = toml::from_str(&content)?; @@ -830,14 +836,20 @@ fn load_parts(path: &Path) -> Result<(Value, DocumentMut), ConfigError> { Ok((data, doc)) } -fn default_toml_text() -> String { - let name = std::env::current_dir() - .ok() - .and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string())) +fn default_toml_text(package_name: Option<&str>) -> Result { + let name = package_name + .map(|s| s.to_string()) + .or_else(|| { + std::env::current_dir() + .ok() + .and_then(|p| p.file_name().map(|v| v.to_string_lossy().to_string())) + }) .unwrap_or_else(|| "project".to_string()); + validate_non_empty_string(&name, "package.name")?; + validate_package_name(&name)?; let dcr_version = env!("CARGO_PKG_VERSION"); - format!( + Ok(format!( "[package]\n\ name = \"{name}\"\n\ version = \"{DEFAULT_VERSION}\"\n\ @@ -854,7 +866,7 @@ fn default_toml_text() -> String { kind = \"{DEFAULT_KIND}\"\n\ \n\ [dependencies]\n" - ) + )) } fn dcr_version_warn_once(path_key: &str) -> bool { @@ -1285,6 +1297,22 @@ mod tests { ); } + #[test] + fn create_rejects_invalid_name_without_writing_manifest() { + let dir = temp_dir("invalid_create_name"); + let path = dir.join("dcr.toml"); + for name in ["", "bad name", "../bad", "bad\"name", " bad", "bad."] { + let result = Config::create(&path.to_string_lossy(), Some(name)); + assert!(matches!(result, Err(ConfigError::Invalid(_))), "{name:?}"); + assert!(!path.exists(), "invalid name must not create a manifest"); + } + let config = Config::create(&path.to_string_lossy(), Some("valid-name")).unwrap(); + assert_eq!( + config.get("package.name").and_then(Value::as_str), + Some("valid-name") + ); + } + #[test] fn new_creates_default_config() { let dir = temp_dir("new_default"); diff --git a/src/core/deps/common.rs b/src/core/deps/common.rs index 76fa078..18a98c2 100644 --- a/src/core/deps/common.rs +++ b/src/core/deps/common.rs @@ -4,5 +4,7 @@ pub struct ResolvedDeps { pub include_dirs: Vec, pub lib_dirs: Vec, pub libs: Vec, + pub cflags: Vec, + pub ldflags: Vec, pub package_roots: Vec, } diff --git a/src/core/deps/mod.rs b/src/core/deps/mod.rs index 71f1ba7..78e8269 100644 --- a/src/core/deps/mod.rs +++ b/src/core/deps/mod.rs @@ -23,6 +23,7 @@ pub mod register; use crate::core::build_config::Config; use crate::core::deps::common::ResolvedDeps; use crate::core::deps::lock::{DepLock, write_lock}; +use crate::utils::build::{resolve_pkg_config_flags, run_pkg_config}; use std::path::Path; /// Returns the version string from a dependency's dcr.toml file, or an empty string if missing. @@ -73,7 +74,22 @@ pub fn resolve_deps( if let Some(deps) = deps_table { // Process each dependency declaration in the TOML table for (name, value) in deps { - if register::is_registry_dep(value) { + if let Some(packages) = pkg_config_dep(value, name)? { + let (cflags, ldflags) = resolve_pkg_config_flags(&packages, &[], &[])?; + resolved.cflags.extend(cflags); + resolved.ldflags.extend(ldflags); + let version = packages + .first() + .and_then(|pkg| run_pkg_config(pkg, "--modversion").ok()) + .map(|v| v.trim().to_string()) + .unwrap_or_default(); + lock_packages.push(DepLock { + name: name.clone(), + version, + checksum: String::new(), + source: format!("pkg-config+{}", packages.join(",")), + }); + } else if register::is_registry_dep(value) { let pkg_info = register::resolve_package_from_registry(name)?; let dep_root = register::package_root_from_registry_info(&pkg_info)?; @@ -274,6 +290,48 @@ fn git_dep(value: &toml::Value) -> Option> { None } +/// Extracts one or more pkg-config package names from a dependency table. +/// Both `pkg-config` and the legacy-style `pkg_config` spelling are accepted. +fn pkg_config_dep( + value: &toml::Value, + dependency_name: &str, +) -> Result>, String> { + let Some(table) = value.as_table() else { + return Ok(None); + }; + let raw = table.get("pkg-config").or_else(|| table.get("pkg_config")); + let Some(raw) = raw else { + return Ok(None); + }; + let packages = if let Some(pkg) = raw.as_str() { + vec![pkg.to_string()] + } else { + raw.as_array() + .ok_or_else(|| { + format!( + "Dependency `{dependency_name}` field `pkg-config` must be a string or array of strings" + ) + })? + .iter() + .map(|value| { + value.as_str().map(str::to_string).ok_or_else(|| { + format!( + "Dependency `{dependency_name}` field `pkg-config` must contain only strings" + ) + }) + }) + .collect::, _>>()? + .into_iter() + .collect() + }; + if packages.is_empty() || packages.iter().any(|pkg| pkg.trim().is_empty()) { + return Err(format!( + "Dependency `{dependency_name}` field `pkg-config` must not be empty" + )); + } + Ok(Some(packages)) +} + /// Extracts the path for a path-based dependency, supporting both table format and legacy string format. fn path_dep_path(value: &toml::Value) -> Option<&str> { if let Some(table) = value.as_table() { @@ -436,6 +494,20 @@ mod tests { assert_eq!(path_dep_path(&Value::String("1.2.3".to_string())), None); } + #[test] + fn pkg_config_dep_accepts_string_and_array() { + let single: Value = toml::from_str("pkg-config = \"fmt\"").unwrap(); + assert_eq!( + pkg_config_dep(&single, "fmt"), + Ok(Some(vec!["fmt".to_string()])) + ); + let many: Value = toml::from_str("pkg_config = [\"fmt\", \"zlib\"]").unwrap(); + assert_eq!( + pkg_config_dep(&many, "base"), + Ok(Some(vec!["fmt".to_string(), "zlib".to_string()])) + ); + } + /// Verifies that push_default_lib_dirs includes the target/lib directory when it exists. #[test] fn default_lib_dirs_include_packaged_library_output() { diff --git a/tests/cli_deps.rs b/tests/cli_deps.rs index 297e5ba..9a5a44a 100644 --- a/tests/cli_deps.rs +++ b/tests/cli_deps.rs @@ -52,6 +52,24 @@ fn dcr_add_dependencies() { "git short dep not found in toml" ); + // Test pkg-config prefix. + let out = run_dcr(&["add", "fmt", "pkg-config:fmt"], &dir); + assert!(out.status.success(), "dcr add pkg-config should succeed"); + let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); + assert!( + toml.contains("fmt = { pkg-config = \"fmt\" }"), + "pkg-config dep not found in toml" + ); + + // Test multiple pkg-config packages via pkg: prefix. + let out = run_dcr(&["add", "openssl", "pkg:openssl,libcrypto"], &dir); + assert!(out.status.success(), "dcr add multiple pkg should succeed"); + let toml = std::fs::read_to_string(dir.join("dcr.toml")).unwrap(); + assert!( + toml.contains("openssl = { pkg-config = [\"openssl\", \"libcrypto\"] }"), + "multi pkg-config dep not found in toml" + ); + // Test flags (branch) let out = run_dcr( &["add", "branch_lib", "github:user/repo", "--branch", "dev"], @@ -146,6 +164,56 @@ fn path_prebuilt_dependency_without_manifest_links() { ); } +#[test] +fn pkg_config_dependency_resolves_system_library() { + let Some(compiler) = available_compiler() else { + eprintln!("no compiler found; skipping pkg-config dependency test"); + return; + }; + if !std::process::Command::new("pkg-config") + .args(["--exists", "zlib"]) + .status() + .map(|status| status.success()) + .unwrap_or(false) + { + eprintln!("pkg-config zlib unavailable; skipping pkg-config dependency test"); + return; + } + + let root = unique_sandbox_dir("pkg_config_dep"); + let app = root.join("app"); + std::fs::create_dir_all(app.join("src")).unwrap(); + std::fs::write( + app.join("dcr.toml"), + "[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[build]\nlanguage = \"c\"\ncompiler = \"clang\"\nkind = \"bin\"\n\n[dependencies]\nzlib = { pkg-config = \"zlib\" }\n", + ) + .unwrap(); + std::fs::write( + app.join("src/main.c"), + "#include \nint main(void) { return zlibVersion() == 0 ? 1 : 0; }\n", + ) + .unwrap(); + + let out = run_dcr_env(&["build"], &app, &[("DCR_COMPILER", compiler)]); + assert!( + out.status.success(), + "pkg-config dependency build failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + let lock = std::fs::read_to_string(app.join("dcr.lock")).unwrap(); + assert!(lock.contains("source = \"pkg-config+zlib\"")); + + // Check dcr tree displays pkg-config info + let tree_out = run_dcr(&["tree"], &app); + assert!(tree_out.status.success(), "dcr tree should succeed"); + let tree_str = String::from_utf8_lossy(&tree_out.stdout); + assert!( + tree_str.contains("zlib (pkg-config: zlib)"), + "dcr tree should format pkg-config dependency, got: {}", + tree_str + ); +} + #[test] fn path_dcr_dependency_is_built_before_consumer() { let Some(compiler) = available_compiler() else { diff --git a/tests/cli_new.rs b/tests/cli_new.rs index ddf07ea..b6f683c 100644 --- a/tests/cli_new.rs +++ b/tests/cli_new.rs @@ -79,3 +79,24 @@ fn init_vcs_options_work() { assert!(gitignore.contains(".dcr/")); } } + +#[test] +fn new_in_non_ascii_parent_directory() { + let parent_dir = unique_sandbox_dir("Проекты с пробелами"); + let out = run_dcr(&["new", "my_app"], &parent_dir); + assert!( + out.status.success(), + "dcr new should succeed inside non-ascii parent dir: {}", + String::from_utf8_lossy(&out.stderr) + ); + + let project_dir = parent_dir.join("my_app"); + assert!(project_dir.is_dir(), "project dir should exist"); + assert!(project_dir.join("dcr.toml").is_file(), "dcr.toml missing"); + + let toml_content = std::fs::read_to_string(project_dir.join("dcr.toml")).unwrap(); + assert!( + toml_content.contains("name = \"my_app\""), + "package name should be my_app in dcr.toml" + ); +}