Skip to content

Commit 19c08ab

Browse files
authored
Simplify dependency semantics with conservative auto-deps and branch commit pinning (#558)
* resolve: simplify fetch concurrency paths * auto_deps: require materialization before adding deps * auto_deps: make phase explicitly online-only * auto_deps: pin branch deps and add branch regression tests * changelog: note auto-deps online-only phase * update: refresh branch+rev pins and cover with integration test * auto_deps: drop lockfile URL mapping and simplify resolution flow * cache_index: unify remote lookup API with explicit policy * dedup resolve/update logic and streamline auto-deps * test(pcb): add resilient auto-dep branch refresh coverage * chore: drop branch-rev pinning spec draft * refactor: simplify and dedupe dependency resolution paths * fix(resolve): ignore stale lockfile pseudo-version when rev changed * test(cache_index): avoid network on remote package miss assertion
1 parent 2ea1e6d commit 19c08ab

File tree

10 files changed

+1157
-274
lines changed

10 files changed

+1157
-274
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ and this project adheres to Semantic Versioning (https://semver.org/spec/v2.0.0.
1111
### Fixed
1212

1313
- Reduced `layout.sync` false positives in publish/check flows by normalizing `.kicad_pro` newline writes and ignoring trailing whitespace-only drift when comparing synced layout files.
14+
- Simplified dependency fetch/index concurrency paths and reuse a shared cache index during resolve/fetch phases to reduce open-file pressure on macOS.
15+
- Auto-deps is now conservative and online-only: it adds remote deps only after successful materialization, skips imports already covered by existing `dependencies`/`assets`, and no longer infers missing deps from `pcb.sum`.
16+
- Branch-based dependencies now require commit pinning for reproducibility: online resolve/update pins `branch` deps to `rev`, while `--locked`/`--offline` reject branch-only declarations.
1417

1518
## [0.3.43] - 2026-02-18
1619

crates/pcb-zen/src/auto_deps.rs

Lines changed: 177 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ use std::collections::{BTreeMap, HashMap};
88
use std::path::{Path, PathBuf};
99

1010
use crate::ast_utils::{skip_vendor, visit_string_literals};
11-
use crate::cache_index::{cache_base, find_lockfile_entry, CacheIndex};
11+
use crate::cache_index::CacheIndex;
1212
use crate::git;
13-
use pcb_zen_core::config::{AssetDependencySpec, DependencySpec, Lockfile, PcbToml, KICAD_ASSETS};
13+
use crate::resolve::{fetch_asset_repo, fetch_package};
14+
use crate::workspace::WorkspaceInfo;
15+
use pcb_zen_core::config::{AssetDependencySpec, DependencySpec, PcbToml, KICAD_ASSETS};
1416
use pcb_zen_core::DefaultFileProvider;
1517

1618
#[derive(Debug, Default)]
@@ -20,7 +22,6 @@ pub struct AutoDepsSummary {
2022
pub packages_updated: usize,
2123
pub unknown_aliases: Vec<(PathBuf, Vec<String>)>,
2224
pub unknown_urls: Vec<(PathBuf, Vec<String>)>,
23-
pub discovered_remote: usize,
2425
pub stdlib_removed: usize,
2526
}
2627

@@ -30,115 +31,82 @@ struct CollectedImports {
3031
urls: HashSet<String>,
3132
}
3233

34+
#[derive(Debug, Clone)]
35+
struct ResolvedDep {
36+
module_path: String,
37+
version: String,
38+
is_asset: bool,
39+
}
40+
41+
impl ResolvedDep {
42+
fn package(module_path: String, version: String) -> Self {
43+
Self {
44+
module_path,
45+
version,
46+
is_asset: false,
47+
}
48+
}
49+
50+
fn asset(module_path: String, version: String) -> Self {
51+
Self {
52+
module_path,
53+
version,
54+
is_asset: true,
55+
}
56+
}
57+
}
58+
3359
/// Scan workspace for .zen files and auto-add missing dependencies to pcb.toml files
3460
///
3561
/// Resolution order for URL imports:
3662
/// 1. Workspace members (local packages)
37-
/// 2. Lockfile entries (pcb.sum) - fast path, no git operations
38-
/// 3. Remote package discovery (git tags) - slow path, cached per repo (skipped when offline)
39-
pub fn auto_add_zen_deps(
40-
workspace_root: &Path,
41-
packages: &BTreeMap<String, crate::workspace::MemberPackage>,
42-
lockfile: Option<&Lockfile>,
43-
offline: bool,
44-
) -> Result<AutoDepsSummary> {
63+
/// 2. Remote package discovery (git tags) - slow path, cached per repo
64+
pub fn auto_add_zen_deps(workspace_info: &WorkspaceInfo) -> Result<AutoDepsSummary> {
65+
let workspace_root = &workspace_info.root;
66+
let packages = &workspace_info.packages;
4567
let package_imports = collect_imports_by_package(workspace_root, packages)?;
4668
let mut summary = AutoDepsSummary::default();
69+
let file_provider = DefaultFileProvider::new();
4770

48-
let index = if !offline {
49-
CacheIndex::open().ok()
50-
} else {
51-
None
52-
};
71+
let index = CacheIndex::open()?;
5372

5473
for (pcb_toml_path, imports) in package_imports {
55-
let mut deps_to_add: Vec<(String, String, bool)> = Vec::new();
56-
let mut unknown_aliases: Vec<String> = Vec::new();
74+
let existing_config = PcbToml::from_file(&file_provider, &pcb_toml_path)?;
75+
let mut deps_to_add: Vec<ResolvedDep> = Vec::new();
76+
let unknown_aliases: Vec<String> = imports
77+
.aliases
78+
.iter()
79+
.filter(|alias| alias.as_str() != "stdlib")
80+
.cloned()
81+
.collect();
5782
let mut unknown_urls: Vec<String> = Vec::new();
5883

59-
// Process @alias imports
60-
// Note: @stdlib is handled implicitly by the toolchain, no need to add to [dependencies]
61-
for alias in &imports.aliases {
62-
if alias != "stdlib" {
63-
unknown_aliases.push(alias.clone());
64-
}
65-
}
66-
6784
// Process URL imports
68-
let cache = cache_base();
6985
for url in &imports.urls {
70-
// Check if this is a known KiCad asset with subpath
71-
if let Some(version) = get_kicad_asset_version(url) {
72-
// Opportunistically verify path exists if we have the repo cached
73-
let (repo_url, subpath) = git::split_asset_repo_and_subpath(url);
74-
let repo_cache_dir = cache.join(repo_url).join(&version);
75-
if repo_cache_dir.exists() && !subpath.is_empty() {
76-
let target_path = repo_cache_dir.join(subpath);
77-
if !target_path.exists() {
78-
// Path doesn't exist in cached repo, skip auto-dep
79-
unknown_urls.push(url.clone());
80-
continue;
81-
}
82-
}
83-
deps_to_add.push((url.clone(), version, true));
84-
continue;
85-
}
86-
87-
// Try workspace members first
88-
if let Some((package_url, version)) = find_matching_workspace_member(url, packages) {
89-
deps_to_add.push((package_url, version, false));
86+
if is_url_covered_by_manifest(url, &existing_config) {
9087
continue;
9188
}
9289

93-
// Try lockfile
94-
if let Some(lf) = lockfile {
95-
if let Some((module_path, version)) = find_lockfile_entry(url, lf) {
96-
deps_to_add.push((module_path, version, false));
97-
continue;
98-
}
99-
}
100-
101-
// Try sqlite cache
102-
if let Some(ref idx) = index {
103-
if let Some((module_path, version)) = idx.find_remote_package(url) {
104-
deps_to_add.push((module_path, version, false));
105-
continue;
106-
}
107-
}
90+
let candidate = resolve_dep_candidate(url, packages, &index);
10891

109-
// Fetch and populate cache (only if online)
110-
if offline {
92+
let Some(candidate) = candidate else {
11193
unknown_urls.push(url.clone());
11294
continue;
113-
}
95+
};
11496

115-
if let Some(ref idx) = index {
116-
match idx.find_or_discover_remote_package(url) {
117-
Ok(Some((module_path, version))) => {
118-
deps_to_add.push((module_path, version, false));
119-
summary.discovered_remote += 1;
120-
}
121-
Ok(None) => unknown_urls.push(url.clone()),
122-
Err(e) => {
123-
eprintln!(" Warning: Failed to discover package for {}: {}", url, e);
124-
unknown_urls.push(url.clone());
125-
}
126-
}
97+
if can_materialize_dep(workspace_info, &index, &candidate) {
98+
deps_to_add.push(candidate);
12799
} else {
128100
unknown_urls.push(url.clone());
129101
}
130102
}
131103

132-
if !unknown_aliases.is_empty() {
133-
summary
134-
.unknown_aliases
135-
.push((pcb_toml_path.clone(), unknown_aliases));
136-
}
137-
if !unknown_urls.is_empty() {
138-
summary
139-
.unknown_urls
140-
.push((pcb_toml_path.clone(), unknown_urls));
141-
}
104+
push_unknown(
105+
&mut summary.unknown_aliases,
106+
&pcb_toml_path,
107+
unknown_aliases,
108+
);
109+
push_unknown(&mut summary.unknown_urls, &pcb_toml_path, unknown_urls);
142110

143111
let (added, corrected) =
144112
add_and_correct_dependencies(&pcb_toml_path, &deps_to_add, packages)?;
@@ -155,6 +123,116 @@ pub fn auto_add_zen_deps(
155123
Ok(summary)
156124
}
157125

126+
fn is_url_covered_by_manifest(url: &str, config: &PcbToml) -> bool {
127+
config
128+
.dependencies
129+
.keys()
130+
.any(|dep| dep_covers_url(dep, url))
131+
|| config.assets.keys().any(|asset| dep_covers_url(asset, url))
132+
}
133+
134+
fn dep_covers_url(dep: &str, url: &str) -> bool {
135+
if dep == url {
136+
return true;
137+
}
138+
139+
url.strip_prefix(dep)
140+
.is_some_and(|rest| rest.starts_with('/'))
141+
}
142+
143+
fn push_unknown(summary: &mut Vec<(PathBuf, Vec<String>)>, path: &Path, items: Vec<String>) {
144+
if items.is_empty() {
145+
return;
146+
}
147+
summary.push((path.to_path_buf(), items));
148+
}
149+
150+
fn can_materialize_dep(
151+
workspace_info: &WorkspaceInfo,
152+
index: &CacheIndex,
153+
dep: &ResolvedDep,
154+
) -> bool {
155+
if dep.is_asset {
156+
let (repo_url, subpath) = git::split_asset_repo_and_subpath(&dep.module_path);
157+
let asset_key = dep.module_path.clone();
158+
let result = fetch_asset_repo(workspace_info, repo_url, &dep.version, &[asset_key], false)
159+
.and_then(|base| {
160+
let target = if subpath.is_empty() {
161+
base
162+
} else {
163+
base.join(subpath)
164+
};
165+
anyhow::ensure!(
166+
target.exists(),
167+
"Asset subpath '{}' not found in {}@{}",
168+
subpath,
169+
repo_url,
170+
dep.version
171+
);
172+
Ok(())
173+
});
174+
175+
if let Err(e) = result {
176+
log::debug!(
177+
"Skipping auto-dep asset {}@{} (materialization failed): {}",
178+
dep.module_path,
179+
dep.version,
180+
e
181+
);
182+
return false;
183+
}
184+
return true;
185+
}
186+
187+
let Some(parsed_version) = crate::tags::parse_relaxed_version(&dep.version) else {
188+
log::debug!(
189+
"Skipping auto-dep package {}@{} (invalid version)",
190+
dep.module_path,
191+
dep.version
192+
);
193+
return false;
194+
};
195+
196+
if let Err(e) = fetch_package(
197+
workspace_info,
198+
&dep.module_path,
199+
&parsed_version,
200+
index,
201+
false,
202+
) {
203+
log::debug!(
204+
"Skipping auto-dep package {}@{} (materialization failed): {}",
205+
dep.module_path,
206+
dep.version,
207+
e
208+
);
209+
return false;
210+
}
211+
212+
true
213+
}
214+
215+
fn resolve_dep_candidate(
216+
url: &str,
217+
packages: &BTreeMap<String, crate::workspace::MemberPackage>,
218+
index: &CacheIndex,
219+
) -> Option<ResolvedDep> {
220+
get_kicad_asset_version(url)
221+
.map(|version| ResolvedDep::asset(url.to_string(), version))
222+
.or_else(|| {
223+
find_matching_workspace_member(url, packages)
224+
.map(|(module_path, version)| ResolvedDep::package(module_path, version))
225+
})
226+
.or_else(|| match index.find_remote_package(url) {
227+
Ok(Some(dep)) => Some(ResolvedDep::package(dep.module_path, dep.version)),
228+
Ok(None) => None,
229+
Err(e) => {
230+
eprintln!(" Warning: Failed to discover package for {}: {}", url, e);
231+
None
232+
}
233+
})
234+
}
235+
158236
/// Get the version for a known KiCad asset URL (returns None if not a KiCad asset with subpath)
159237
fn get_kicad_asset_version(url: &str) -> Option<String> {
160238
for (_, base_url, version) in KICAD_ASSETS {
@@ -370,34 +448,28 @@ fn extract_from_str(s: &str, aliases: &mut HashSet<String>, urls: &mut HashSet<S
370448
/// Add dependencies to a pcb.toml file and correct workspace member versions
371449
fn add_and_correct_dependencies(
372450
pcb_toml_path: &Path,
373-
deps: &[(String, String, bool)],
451+
deps: &[ResolvedDep],
374452
packages: &BTreeMap<String, crate::workspace::MemberPackage>,
375453
) -> Result<(usize, usize)> {
376454
let mut config = PcbToml::from_file(&DefaultFileProvider::new(), pcb_toml_path)?;
377455
let mut added = 0;
378456
let mut corrected = 0;
379457

380-
for (url, version, is_asset) in deps {
381-
if config.dependencies.contains_key(url) || config.assets.contains_key(url) {
458+
for dep in deps {
459+
if is_url_covered_by_manifest(&dep.module_path, &config) {
382460
continue;
383461
}
384462

385-
// For assets, check if already satisfied by a whole-repo entry
386-
if *is_asset {
387-
let (repo_url, subpath) = git::split_asset_repo_and_subpath(url);
388-
if !subpath.is_empty() && config.assets.contains_key(repo_url) {
389-
continue;
390-
}
391-
}
392-
393-
if *is_asset {
394-
config
395-
.assets
396-
.insert(url.clone(), AssetDependencySpec::Ref(version.clone()));
463+
if dep.is_asset {
464+
config.assets.insert(
465+
dep.module_path.clone(),
466+
AssetDependencySpec::Ref(dep.version.clone()),
467+
);
397468
} else {
398-
config
399-
.dependencies
400-
.insert(url.clone(), DependencySpec::Version(version.clone()));
469+
config.dependencies.insert(
470+
dep.module_path.clone(),
471+
DependencySpec::Version(dep.version.clone()),
472+
);
401473
}
402474
added += 1;
403475
}

0 commit comments

Comments
 (0)