feat: add pkg-config system dependencies and fix dcr new in non-ascii paths

This commit is contained in:
2026-09-08 20:40:49 +08:00
parent 1e3eb658dc
commit 5cb51878a5
22 changed files with 359 additions and 47 deletions

View File

@@ -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<String>,
/// Version string resolved from the package registry when no explicit source is given.
pub version_from_registry: Option<String>,
/// Package names resolved through pkg-config.
pub pkg_config: Option<Vec<String>>,
}
/// 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> 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<AddArgs, i32> {
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<AddArgs, i32> {
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<AddArgs, i32> {
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::<Vec<_>>();
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<AddArgs, i32> {
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<AddArgs, i32> {
tag,
rev,
version_from_registry: None,
pkg_config,
})
}

View File

@@ -42,11 +42,13 @@ struct GenDeps {
include_dirs: Vec<String>,
lib_dirs: Vec<String>,
libs: Vec<String>,
cflags: Vec<String>,
ldflags: Vec<String>,
}
/// 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<PathBuf> = Vec::new();

View File

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

View File

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

View File

@@ -134,8 +134,12 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
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());

View File

@@ -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::<Vec<_>>()
.join(", ");
format!(" (pkg-config: {})", names)
} else {
"".to_string()
}
} else {
"".to_string()
}