mirror of
https://github.com/dexoron/dcr.git
synced 2026-10-21 09:38:29 +00:00
- Path dependencies now work correctly: `dcr add` stores them as `{ path =
"./..." }` tables, `is_registry_dep` properly distinguishes registry strings
from path/git strings, and `deps/mod.rs` resolves both table-form and
legacy string-form path dependencies
- Registry dependency paths no longer hardcoded to `project_root/dcr-index`;
now use `package_root_from_registry_info()` which resolves relative to the
registry cache root (`~/.dcr/`)
- Registry dependencies are now actually built: `build_project_at()` is
called when `include_dir` or `lib_dir` is missing
Added:
- `SIGINT`/`Ctrl+C` handler via `ctrlc` crate — `dcr build` now checks
`BUILD_INTERRUPTED` flag at key points and aborts cleanly
- `utils/build.rs` — extracted shared utilities: `parse_version_info`,
`normalize_target_os`, `resolve_compiler`, `primary_language`,
`resolve_pkg_config_flags` and config helpers. Eliminates code duplication
between `cli/build.rs`, `cli/run.rs`, `cli/clean.rs`, `cli/gen.rs`
- `utils/fs.rs::with_dir` — extracted common directory-scoped execution
- `run_command_sync_output` in `builder/common.rs` with global `OUTPUT_MUTEX`
— synchronized compiler output across all backends (unix_cc, gas, nasm, msvc)
- New helper functions in `deps/register.rs`: `get_registry_cache_root`,
`package_root_from_registry_info`, `registry_include_dir`, `registry_lib_dir`,
`path_from_string_dep`
- Unit tests for `register.rs` (`is_registry_dep`, `package_root_from_registry_info`),
`deps/mod.rs` (`path_dep_path`, `push_default_lib_dirs`),
`utils/build.rs` (`normalize_target_os`, `parse_version_info`)
Removed:
- Removed unused dependencies `indicatif` and `console` from `Cargo.toml`
377 lines
11 KiB
Rust
377 lines
11 KiB
Rust
use crate::core::config::Config;
|
|
use crate::utils::log::warn;
|
|
|
|
pub struct VersionInfo {
|
|
pub full: String,
|
|
pub major: String,
|
|
pub minor: String,
|
|
pub patch: String,
|
|
pub suffix: String,
|
|
pub suffix_dash: String,
|
|
}
|
|
|
|
pub fn parse_version_info(version: &str) -> VersionInfo {
|
|
let mut full = version.trim().to_string();
|
|
if full.is_empty() {
|
|
full = "0.0.0".to_string();
|
|
}
|
|
let full_clone = full.clone();
|
|
let (base, suffix) = match full_clone.split_once('-') {
|
|
Some((head, tail)) => (head, tail),
|
|
None => (full_clone.as_str(), ""),
|
|
};
|
|
let mut parts = base.split('.');
|
|
let major = parts.next().unwrap_or("0").to_string();
|
|
let minor = parts.next().unwrap_or("0").to_string();
|
|
let patch = parts.next().unwrap_or("0").to_string();
|
|
let suffix_dash = if suffix.is_empty() {
|
|
"".to_string()
|
|
} else {
|
|
format!("-{suffix}")
|
|
};
|
|
VersionInfo {
|
|
full,
|
|
major,
|
|
minor,
|
|
patch,
|
|
suffix: suffix.to_string(),
|
|
suffix_dash,
|
|
}
|
|
}
|
|
|
|
pub fn normalize_target_os(target: &str) -> &str {
|
|
match target {
|
|
"linux" => "x86_64-unknown-linux-gnu",
|
|
"macos" => "x86_64-apple-darwin",
|
|
"windows" => "x86_64-pc-windows-msvc",
|
|
_ if target.contains('-') => target,
|
|
_ => {
|
|
warn(&format!(
|
|
"Unknown target '{}', using as-is. Supported short names: linux, macos, windows",
|
|
target
|
|
));
|
|
target
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn get_config_str(config: &Config, key: &str) -> String {
|
|
config
|
|
.get(key)
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.to_string()
|
|
}
|
|
|
|
pub fn profile_table<'a>(config: &'a Config, profile: &str) -> Option<&'a toml::value::Table> {
|
|
config
|
|
.get("build")
|
|
.and_then(|v| v.as_table())
|
|
.and_then(|b| b.get(profile))
|
|
.and_then(|v| v.as_table())
|
|
}
|
|
|
|
pub fn get_config_opt(config: &Config, key: &str) -> Option<String> {
|
|
let value = config.get(key)?.as_str()?;
|
|
let trimmed = value.trim();
|
|
if trimmed.is_empty() {
|
|
None
|
|
} else {
|
|
Some(trimmed.to_string())
|
|
}
|
|
}
|
|
|
|
pub fn get_string_with_profile(config: &Config, field: &str, profile: &str) -> String {
|
|
let base = get_config_str(config, &format!("build.{field}"));
|
|
let Some(table) = profile_table(config, profile) else {
|
|
return base;
|
|
};
|
|
let value = table.get(field).and_then(|v| v.as_str()).unwrap_or("");
|
|
let trimmed = value.trim();
|
|
if trimmed.is_empty() {
|
|
base
|
|
} else {
|
|
trimmed.to_string()
|
|
}
|
|
}
|
|
|
|
pub fn get_config_list(config: &Config, key: &str) -> Vec<String> {
|
|
config
|
|
.get(key)
|
|
.and_then(|v| v.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| v.as_str())
|
|
.map(|s| s.to_string())
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn get_list_with_profile(config: &Config, field: &str, profile: &str) -> Vec<String> {
|
|
let mut out = get_config_list(config, &format!("build.{field}"));
|
|
if let Some(table) = profile_table(config, profile)
|
|
&& let Some(extra) = table.get(field).and_then(|v| v.as_array())
|
|
{
|
|
out.extend(
|
|
extra
|
|
.iter()
|
|
.filter_map(|v| v.as_str())
|
|
.map(|s| s.to_string()),
|
|
);
|
|
}
|
|
out
|
|
}
|
|
|
|
pub fn get_bool_with_profile(config: &Config, field: &str, profile: &str, default: bool) -> bool {
|
|
let base = config
|
|
.get(&format!("build.{field}"))
|
|
.and_then(|v| v.as_bool());
|
|
let profile_val =
|
|
profile_table(config, profile).and_then(|t| t.get(field).and_then(|v| v.as_bool()));
|
|
profile_val.or(base).unwrap_or(default)
|
|
}
|
|
|
|
pub fn get_language_with_profile(config: &Config, profile: &str) -> Result<String, String> {
|
|
if let Some(table) = profile_table(config, profile)
|
|
&& let Some(value) = table.get("language")
|
|
{
|
|
return parse_language_value(value, "build.language");
|
|
}
|
|
match config.get("build.language") {
|
|
Some(v) => parse_language_value(v, "build.language"),
|
|
None => Err("build.language is missing".to_string()),
|
|
}
|
|
}
|
|
|
|
pub fn get_language_with_profile_or_default(config: &Config, profile: &str) -> String {
|
|
get_language_with_profile(config, profile).unwrap_or_else(|_| "c".to_string())
|
|
}
|
|
|
|
pub fn parse_language_value(value: &toml::Value, key: &str) -> Result<String, String> {
|
|
if let Some(s) = value.as_str() {
|
|
let trimmed = s.trim();
|
|
if trimmed.is_empty() {
|
|
return Err(format!("{key} is empty"));
|
|
}
|
|
return Ok(trimmed.to_string());
|
|
}
|
|
let arr = value
|
|
.as_array()
|
|
.ok_or_else(|| format!("{key} must be string or array of strings"))?;
|
|
let mut parts = Vec::new();
|
|
for item in arr {
|
|
let s = item
|
|
.as_str()
|
|
.ok_or_else(|| format!("{key} must be string or array of strings"))?;
|
|
let trimmed = s.trim();
|
|
if trimmed.is_empty() {
|
|
return Err(format!("{key} contains empty value"));
|
|
}
|
|
parts.push(trimmed.to_string());
|
|
}
|
|
if parts.is_empty() {
|
|
return Err(format!("{key} is empty"));
|
|
}
|
|
Ok(parts.join("+"))
|
|
}
|
|
|
|
pub fn resolve_compiler(
|
|
language: &str,
|
|
compiler: &str,
|
|
tc_cc: Option<&str>,
|
|
tc_cxx: Option<&str>,
|
|
tc_as: Option<&str>,
|
|
) -> String {
|
|
let lang = primary_language(language);
|
|
env_override_compiler(&lang)
|
|
.or_else(|| toolchain_override_compiler(&lang, tc_cc, tc_cxx, tc_as))
|
|
.unwrap_or_else(|| compiler.to_string())
|
|
}
|
|
|
|
fn env_override_compiler(lang: &str) -> Option<String> {
|
|
if let Ok(value) = std::env::var("DCR_COMPILER") {
|
|
let trimmed = value.trim();
|
|
if !trimmed.is_empty() {
|
|
return Some(trimmed.to_string());
|
|
}
|
|
}
|
|
if lang == "asm" {
|
|
if let Ok(value) = std::env::var("DCR_AS") {
|
|
let trimmed = value.trim();
|
|
if !trimmed.is_empty() {
|
|
return Some(trimmed.to_string());
|
|
}
|
|
}
|
|
return None;
|
|
}
|
|
if (lang == "c++" || lang == "cpp" || lang == "cxx")
|
|
&& let Ok(value) = std::env::var("DCR_CXX")
|
|
{
|
|
let trimmed = value.trim();
|
|
if !trimmed.is_empty() {
|
|
return Some(trimmed.to_string());
|
|
}
|
|
}
|
|
if let Ok(value) = std::env::var("DCR_CC") {
|
|
let trimmed = value.trim();
|
|
if !trimmed.is_empty() {
|
|
return Some(trimmed.to_string());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn toolchain_override_compiler(
|
|
lang: &str,
|
|
tc_cc: Option<&str>,
|
|
tc_cxx: Option<&str>,
|
|
tc_as: Option<&str>,
|
|
) -> Option<String> {
|
|
if lang == "asm" {
|
|
return tc_as.map(|v| v.to_string());
|
|
}
|
|
if (lang == "c++" || lang == "cpp" || lang == "cxx")
|
|
&& let Some(v) = tc_cxx
|
|
{
|
|
return Some(v.to_string());
|
|
}
|
|
tc_cc.map(|v| v.to_string())
|
|
}
|
|
|
|
pub fn primary_language(language: &str) -> String {
|
|
let parts: Vec<String> = language
|
|
.split('+')
|
|
.map(|p| p.trim().to_lowercase())
|
|
.filter(|p| !p.is_empty())
|
|
.collect();
|
|
for p in &parts {
|
|
if p == "c++" || p == "cpp" || p == "cxx" {
|
|
return p.clone();
|
|
}
|
|
}
|
|
if parts.iter().any(|p| p == "c") {
|
|
return "c".to_string();
|
|
}
|
|
if parts.iter().any(|p| p == "asm") {
|
|
return "asm".to_string();
|
|
}
|
|
language.to_lowercase()
|
|
}
|
|
|
|
pub fn resolve_tool(env_key: &str, fallback: Option<&str>) -> Option<String> {
|
|
if let Ok(value) = std::env::var(env_key) {
|
|
let trimmed = value.trim();
|
|
if !trimmed.is_empty() {
|
|
return Some(trimmed.to_string());
|
|
}
|
|
}
|
|
fallback.map(|v| v.to_string())
|
|
}
|
|
|
|
pub fn resolve_pkg_config_flags(
|
|
pkgs: &[String],
|
|
base_cflags: &[String],
|
|
base_ldflags: &[String],
|
|
) -> Result<(Vec<String>, Vec<String>), String> {
|
|
let mut cflags = base_cflags.to_vec();
|
|
let mut ldflags = base_ldflags.to_vec();
|
|
for pkg in pkgs {
|
|
let c_out = run_pkg_config(pkg, "--cflags")?;
|
|
let l_out = run_pkg_config(pkg, "--libs")?;
|
|
cflags.extend(split_flags(&c_out));
|
|
ldflags.extend(split_flags(&l_out));
|
|
}
|
|
Ok((cflags, ldflags))
|
|
}
|
|
|
|
pub fn resolve_pkg_config_flags_lossy(
|
|
pkgs: &[String],
|
|
base_cflags: &[String],
|
|
base_ldflags: &[String],
|
|
) -> (Vec<String>, Vec<String>) {
|
|
match resolve_pkg_config_flags(pkgs, base_cflags, base_ldflags) {
|
|
Ok(flags) => flags,
|
|
Err(err) => {
|
|
eprintln!("Warning: {err}");
|
|
(base_cflags.to_vec(), base_ldflags.to_vec())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn run_pkg_config(pkg: &str, arg: &str) -> Result<String, String> {
|
|
let output = std::process::Command::new("pkg-config")
|
|
.arg(arg)
|
|
.arg(pkg)
|
|
.output()
|
|
.map_err(|err| format!("Failed to run pkg-config: {err}"))?;
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
return Err(format!("pkg-config failed for {pkg}: {stderr}"));
|
|
}
|
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
|
}
|
|
|
|
fn split_flags(value: &str) -> Vec<String> {
|
|
let mut out = Vec::new();
|
|
let mut current = String::new();
|
|
let mut chars = value.chars().peekable();
|
|
let mut quote: Option<char> = None;
|
|
while let Some(ch) = chars.next() {
|
|
if let Some(q) = quote {
|
|
if ch == q {
|
|
quote = None;
|
|
} else {
|
|
current.push(ch);
|
|
}
|
|
continue;
|
|
}
|
|
match ch {
|
|
'\'' | '"' => quote = Some(ch),
|
|
'\\' => {
|
|
if let Some(next) = chars.next() {
|
|
current.push(next);
|
|
}
|
|
}
|
|
c if c.is_whitespace() => {
|
|
if !current.is_empty() {
|
|
out.push(std::mem::take(&mut current));
|
|
}
|
|
}
|
|
c => current.push(c),
|
|
}
|
|
}
|
|
if !current.is_empty() {
|
|
out.push(current);
|
|
}
|
|
out
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn normalize_target_short_names() {
|
|
assert_eq!(normalize_target_os("linux"), "x86_64-unknown-linux-gnu");
|
|
assert_eq!(normalize_target_os("macos"), "x86_64-apple-darwin");
|
|
assert_eq!(normalize_target_os("windows"), "x86_64-pc-windows-msvc");
|
|
assert_eq!(
|
|
normalize_target_os("x86_64-unknown-linux-gnu"),
|
|
"x86_64-unknown-linux-gnu"
|
|
);
|
|
assert_eq!(normalize_target_os("unknown"), "unknown");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_version_parts() {
|
|
let info = parse_version_info("1.2.3-beta");
|
|
assert_eq!(info.full, "1.2.3-beta");
|
|
assert_eq!(info.major, "1");
|
|
assert_eq!(info.minor, "2");
|
|
assert_eq!(info.patch, "3");
|
|
assert_eq!(info.suffix, "beta");
|
|
assert_eq!(info.suffix_dash, "-beta");
|
|
}
|
|
}
|