Files
dcr/src/core/workspace.rs
Dexoron 933fae749d Fixed:
- 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`
2026-05-18 16:13:33 +08:00

151 lines
4.3 KiB
Rust

use crate::core::config::Config;
use crate::utils::build::normalize_target_os;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use toml::Value;
#[derive(Debug, Clone)]
pub struct WorkspaceMember {
pub name: String,
pub path: PathBuf,
pub deps: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct Workspace {
pub members: Vec<WorkspaceMember>,
}
pub fn parse_workspace(
config: &Config,
profile: &str,
target: Option<&str>,
root: &Path,
) -> Result<Option<Workspace>, String> {
let mut table = None;
// Order: workspace.target.profile, workspace.profile.target, workspace.target, workspace.profile, workspace
let combinations = if let Some(t) = target {
let normalized_t = normalize_target_os(t);
vec![
format!("workspace.{}.{}", normalized_t, profile),
format!("workspace.{}.{}", profile, normalized_t),
format!("workspace.{}", normalized_t),
format!("workspace.{}", profile),
"workspace".to_string(),
]
} else {
vec![format!("workspace.{}", profile), "workspace".to_string()]
};
for key in combinations {
if let Some(val) = config.get(&key).and_then(|v| v.as_table()) {
table = Some(val);
break;
}
}
let table = match table {
Some(t) => t,
None => return Ok(None),
};
let mut members = Vec::new();
for (name, value) in table {
let tbl = value
.as_table()
.ok_or_else(|| format!("workspace.{name} must be a table with path and deps"))?;
let path_raw = tbl
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| format!("workspace.{name}.path is required"))?;
let path = resolve_path(root, path_raw)?;
if !path.join("dcr.toml").is_file() {
return Err(format!(
"workspace.{name}.path does not contain dcr.toml: {}",
path.display()
));
}
let deps = parse_deps(tbl.get("deps"))?;
members.push(WorkspaceMember {
name: name.to_string(),
path,
deps,
});
}
let mut ws = Workspace { members };
ws.members = topo_sort(&ws.members)?;
Ok(Some(ws))
}
fn resolve_path(root: &Path, raw: &str) -> Result<PathBuf, String> {
let p = Path::new(raw);
let full = if p.is_absolute() {
p.to_path_buf()
} else {
root.join(p)
};
Ok(full)
}
fn parse_deps(value: Option<&Value>) -> Result<Vec<String>, String> {
let Some(value) = value else {
return Ok(Vec::new());
};
let arr = value
.as_array()
.ok_or_else(|| "workspace deps must be an array of strings".to_string())?;
let mut out = Vec::new();
for item in arr {
let s = item
.as_str()
.ok_or_else(|| "workspace deps must be an array of strings".to_string())?;
out.push(s.to_string());
}
Ok(out)
}
fn topo_sort(members: &[WorkspaceMember]) -> Result<Vec<WorkspaceMember>, String> {
let mut map = HashMap::new();
for m in members {
map.insert(m.name.clone(), m.clone());
}
let mut state: HashMap<String, u8> = HashMap::new();
let mut order: Vec<WorkspaceMember> = Vec::new();
for name in map.keys() {
if *state.get(name).unwrap_or(&0) == 0 {
visit(name, &map, &mut state, &mut order)?;
}
}
Ok(order)
}
fn visit(
name: &str,
map: &HashMap<String, WorkspaceMember>,
state: &mut HashMap<String, u8>,
order: &mut Vec<WorkspaceMember>,
) -> Result<(), String> {
match state.get(name).copied().unwrap_or(0) {
1 => return Err(format!("workspace dependency cycle at {name}")),
2 => return Ok(()),
_ => {}
}
state.insert(name.to_string(), 1);
let member = map
.get(name)
.ok_or_else(|| format!("unknown workspace member {name}"))?;
let mut seen = HashSet::new();
for dep in &member.deps {
if !seen.insert(dep) {
continue;
}
if !map.contains_key(dep) {
return Err(format!("workspace dependency '{dep}' not found"));
}
visit(dep, map, state, order)?;
}
state.insert(name.to_string(), 2);
order.push(member.clone());
Ok(())
}