Skip to content

Commit c604e97

Browse files
authored
Track which asset_keys belong to each repo for offline validation (#342)
1 parent 6790d58 commit c604e97

File tree

1 file changed

+83
-88
lines changed

1 file changed

+83
-88
lines changed

crates/pcb-zen/src/resolve_v2.rs

Lines changed: 83 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
13091313
fn 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

Comments
 (0)