@@ -1103,22 +1103,26 @@ fn collect_and_fetch_assets(
11031103 let unique_assets: HashSet < _ > = all_assets. into_iter ( ) . collect ( ) ;
11041104
11051105 // Dedupe repos by (repo_url, ref) - multiple subpaths share the same repo
1106- let mut repos_to_fetch: HashSet < ( String , String ) > = HashSet :: new ( ) ;
1106+ // Track which asset_keys belong to each repo for offline validation
1107+ let mut repos_to_fetch: HashMap < ( String , String ) , Vec < String > > = HashMap :: new ( ) ;
11071108 for ( asset_key, ref_str) in & unique_assets {
11081109 let ( repo_url, _) = git:: split_asset_repo_and_subpath ( asset_key) ;
1109- repos_to_fetch. insert ( ( repo_url. to_string ( ) , ref_str. clone ( ) ) ) ;
1110+ repos_to_fetch
1111+ . entry ( ( repo_url. to_string ( ) , ref_str. clone ( ) ) )
1112+ . or_default ( )
1113+ . push ( asset_key. clone ( ) ) ;
11101114 }
11111115
11121116 // Print repos we're fetching
1113- for ( repo_url, ref_str) in & repos_to_fetch {
1117+ for ( repo_url, ref_str) in repos_to_fetch. keys ( ) {
11141118 log:: debug!( " {}@{}" , repo_url, ref_str) ;
11151119 }
11161120
11171121 // Fetch repos in parallel
11181122 let errors: Vec < _ > = repos_to_fetch
11191123 . par_iter ( )
1120- . filter_map ( |( repo_url, ref_str) | {
1121- fetch_asset_repo ( workspace_info, repo_url, ref_str, offline)
1124+ . filter_map ( |( ( repo_url, ref_str) , asset_keys ) | {
1125+ fetch_asset_repo ( workspace_info, repo_url, ref_str, asset_keys , offline)
11221126 . err ( )
11231127 . map ( |e| format ! ( "{}@{}: {}" , repo_url, ref_str, e) )
11241128 } )
@@ -1308,113 +1312,104 @@ fn read_manifest_from_path(pcb_toml_path: &Path) -> Result<PackageManifest> {
13081312/// 4. Network fetch (only if !offline)
13091313fn fetch_asset_repo (
13101314 workspace_info : & WorkspaceInfo ,
1311- asset_key : & str ,
1315+ repo_url : & str ,
13121316 ref_str : & str ,
1317+ asset_keys : & [ String ] ,
13131318 offline : bool ,
13141319) -> Result < PathBuf > {
1315- let ( repo_url, subpath) = git:: split_asset_repo_and_subpath ( asset_key) ;
1320+ // Check if asset has a local path patch (branch/rev patches fall through to normal fetch)
1321+ let get_path_patch = |key : & str | {
1322+ workspace_info
1323+ . config
1324+ . as_ref ( )
1325+ . and_then ( |c| c. patch . get ( key) )
1326+ . and_then ( |p| p. path . as_ref ( ) )
1327+ } ;
13161328
1317- // 1. Check if this asset is patched with a local path (use full asset_key for lookup)
1318- // (branch/rev patches for assets fall through to normal fetch)
1319- if let Some ( patch) = workspace_info
1320- . config
1321- . as_ref ( )
1322- . and_then ( |c| c. patch . get ( asset_key) )
1323- {
1324- if let Some ( path) = & patch. path {
1325- let patched_path = workspace_info. root . join ( path) ;
1329+ // For offline mode, verify all non-patched asset_keys are vendored and in lockfile
1330+ if offline {
1331+ let vendor_base = workspace_info
1332+ . root
1333+ . join ( "vendor" )
1334+ . join ( repo_url)
1335+ . join ( ref_str) ;
13261336
1327- log:: debug!( "Asset {} using patched source: {}" , asset_key, path) ;
1337+ for asset_key in asset_keys {
1338+ if get_path_patch ( asset_key) . is_some ( ) {
1339+ continue ;
1340+ }
13281341
1329- if !patched_path. exists ( ) {
1342+ let ( _, subpath) = git:: split_asset_repo_and_subpath ( asset_key) ;
1343+ let vendor_dir = if subpath. is_empty ( ) {
1344+ vendor_base. clone ( )
1345+ } else {
1346+ vendor_base. join ( subpath)
1347+ } ;
1348+ let in_lockfile = workspace_info
1349+ . lockfile
1350+ . as_ref ( )
1351+ . map ( |lf| lf. get ( asset_key, ref_str) . is_some ( ) )
1352+ . unwrap_or ( false ) ;
1353+
1354+ if !vendor_dir. exists ( ) || !in_lockfile {
13301355 anyhow:: bail!(
1331- "Asset '{}' is patched to a non-existent path \n \
1332- Patch path: {} ",
1356+ "Asset {} @ {} not vendored (offline mode) \n \
1357+ Run `pcb vendor` to vendor dependencies for offline builds ",
13331358 asset_key,
1334- patched_path . display ( )
1359+ ref_str
13351360 ) ;
13361361 }
1337-
1338- return Ok ( patched_path) ;
1362+ log:: debug!( "Asset {}@{} vendored" , asset_key, ref_str) ;
13391363 }
1340- }
1341-
1342- // Open cache index
1343- let index = CacheIndex :: open ( ) ?;
1344-
1345- // 2. Check vendor directory: vendor/{repo}/{ref}/{subpath}/ (only if also in lockfile)
1346- let vendor_base = workspace_info
1347- . root
1348- . join ( "vendor" )
1349- . join ( repo_url)
1350- . join ( ref_str) ;
1351- let vendor_dir = if subpath. is_empty ( ) {
1352- vendor_base. clone ( )
1353- } else {
1354- vendor_base. join ( subpath)
1355- } ;
1356- let in_lockfile = workspace_info
1357- . lockfile
1358- . as_ref ( )
1359- . map ( |lf| lf. get ( asset_key, ref_str) . is_some ( ) )
1360- . unwrap_or ( false ) ;
13611364
1362- if vendor_dir. exists ( ) && in_lockfile {
1363- log:: debug!( "Asset {}@{} vendored" , asset_key, ref_str) ;
1364- return Ok ( vendor_dir) ;
1365+ return Ok ( vendor_base) ;
13651366 }
13661367
1367- // 3. If offline, fail here - vendor is the only allowed source for offline builds
1368- if offline {
1369- anyhow:: bail!(
1370- "Asset {} @ {} not vendored (offline mode)\n \
1371- Run `pcb vendor` to vendor dependencies for offline builds",
1372- asset_key,
1373- ref_str
1374- ) ;
1375- }
1376-
1377- // 4. Check cache: full repo at cache/{repo}/{ref}/, target is subpath within it
1378- let cache = cache_base ( ) ;
1379- let repo_cache_dir = cache. join ( repo_url) . join ( ref_str) ;
1380- let target_path = if subpath. is_empty ( ) {
1381- repo_cache_dir. clone ( )
1382- } else {
1383- repo_cache_dir. join ( subpath)
1384- } ;
1385-
1386- // Check if subpath already indexed and exists in cache
1387- if index. get_asset ( repo_url, subpath, ref_str) . is_some ( ) && target_path. exists ( ) {
1388- log:: debug!( "Asset {}@{} cached" , asset_key, ref_str) ;
1389- return Ok ( target_path) ;
1390- }
1368+ // Online mode: fetch repo and index subpaths
1369+ let index = CacheIndex :: open ( ) ?;
1370+ let repo_cache_dir = cache_base ( ) . join ( repo_url) . join ( ref_str) ;
13911371
1392- // 5. Ensure base repo is fetched (archive download or sparse checkout)
1393- // Check for .git (git source) or any content like pcb.toml (archive source)
1372+ // Ensure base repo is fetched (check for .git or any content)
13941373 let repo_exists = repo_cache_dir. join ( ".git" ) . exists ( )
13951374 || ( repo_cache_dir. exists ( )
13961375 && std:: fs:: read_dir ( & repo_cache_dir) . is_ok_and ( |mut d| d. next ( ) . is_some ( ) ) ) ;
13971376 if !repo_exists {
1398- log:: debug!( "Asset {}@{} fetching" , asset_key , ref_str) ;
1377+ log:: debug!( "Asset {}@{} fetching" , repo_url , ref_str) ;
13991378 ensure_sparse_checkout ( & repo_cache_dir, repo_url, ref_str, false ) ?;
14001379 }
14011380
1402- // Verify subpath exists in the cloned repo
1403- if !subpath. is_empty ( ) && !target_path. exists ( ) {
1404- anyhow:: bail!(
1405- "Subpath '{}' not found in {}@{}" ,
1406- subpath,
1407- repo_url,
1408- ref_str
1409- ) ;
1410- }
1381+ // Verify and index each subpath
1382+ for asset_key in asset_keys {
1383+ if get_path_patch ( asset_key) . is_some ( ) {
1384+ continue ;
1385+ }
1386+
1387+ let ( _, subpath) = git:: split_asset_repo_and_subpath ( asset_key) ;
1388+ let target_path = if subpath. is_empty ( ) {
1389+ repo_cache_dir. clone ( )
1390+ } else {
1391+ repo_cache_dir. join ( subpath)
1392+ } ;
1393+
1394+ if !subpath. is_empty ( ) && !target_path. exists ( ) {
1395+ anyhow:: bail!(
1396+ "Subpath '{}' not found in {}@{}" ,
1397+ subpath,
1398+ repo_url,
1399+ ref_str
1400+ ) ;
1401+ }
14111402
1412- // Compute and store content hash on subpath only
1413- let content_hash = compute_content_hash_from_dir ( & target_path) ?;
1414- index. set_asset ( repo_url, subpath, ref_str, & content_hash) ?;
1415- log:: debug!( "Asset {}@{} hashed: {}" , asset_key, ref_str, content_hash) ;
1403+ if index. get_asset ( repo_url, subpath, ref_str) . is_none ( ) {
1404+ let content_hash = compute_content_hash_from_dir ( & target_path) ?;
1405+ index. set_asset ( repo_url, subpath, ref_str, & content_hash) ?;
1406+ log:: debug!( "Asset {}@{} hashed: {}" , asset_key, ref_str, content_hash) ;
1407+ } else {
1408+ log:: debug!( "Asset {}@{} cached" , asset_key, ref_str) ;
1409+ }
1410+ }
14161411
1417- Ok ( target_path )
1412+ Ok ( repo_cache_dir )
14181413}
14191414
14201415/// Build the final dependency closure using selected versions
0 commit comments