@@ -8,9 +8,11 @@ use std::collections::{BTreeMap, HashMap};
88use std:: path:: { Path , PathBuf } ;
99
1010use 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 ;
1212use 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 } ;
1416use 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)
159237fn 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
371449fn 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