diff --git a/crates/pi-natives/build.rs b/crates/pi-natives/build.rs index 13ee63bf0..3ed8d4aed 100644 --- a/crates/pi-natives/build.rs +++ b/crates/pi-natives/build.rs @@ -31,6 +31,11 @@ struct GrammarSpec { node_types_rel: &'static str, } +struct LockedPackage { + version: String, + source: Option, +} + #[derive(Deserialize)] struct RawTypeRef { #[serde(rename = "type")] @@ -366,15 +371,17 @@ fn generate_chunk_schema() { .expect("pi-natives should live under the workspace root"); let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR should be set")); let output_path = out_dir.join("chunk_schema.json"); - let locked_versions = locked_package_versions(&workspace_root.join("Cargo.lock")); + let locked_packages = locked_packages(&workspace_root.join("Cargo.lock")); let registry_roots = cargo_registry_roots(); + let git_roots = cargo_git_checkout_roots(); let mut languages = BTreeMap::new(); for grammar in GRAMMARS { - let Some(version) = locked_versions.get(grammar.package) else { + let Some(locked) = locked_packages.get(grammar.package) else { continue; }; - let Some(package_dir) = find_registry_package_dir(®istry_roots, grammar.package, version) + let Some(package_dir) = + find_locked_package_dir(®istry_roots, &git_roots, grammar.package, locked) else { continue; }; @@ -632,6 +639,36 @@ fn cargo_registry_roots() -> Vec { roots } +fn cargo_git_checkout_roots() -> Vec { + let mut roots = Vec::new(); + if let Some(cargo_home) = env::var_os("CARGO_HOME") { + roots.push(PathBuf::from(cargo_home).join("git").join("checkouts")); + } + if let Some(home) = env::var_os("HOME") { + roots.push( + PathBuf::from(home) + .join(".cargo") + .join("git") + .join("checkouts"), + ); + } + roots +} + +fn find_locked_package_dir( + registry_roots: &[PathBuf], + git_roots: &[PathBuf], + package: &str, + locked: &LockedPackage, +) -> Option { + match locked.source.as_deref() { + Some(source) if source.starts_with("git+") => { + find_git_package_dir(git_roots, package, &locked.version, git_revision(source)) + }, + _ => find_registry_package_dir(registry_roots, package, &locked.version), + } +} + fn find_registry_package_dir( registry_roots: &[PathBuf], package: &str, @@ -651,32 +688,145 @@ fn find_registry_package_dir( None } -fn locked_package_versions(lock_path: &Path) -> HashMap { +fn find_git_package_dir( + git_roots: &[PathBuf], + package: &str, + version: &str, + revision: Option<&str>, +) -> Option { + for git_root in git_roots { + let Ok(checkout_dirs) = fs::read_dir(git_root) else { + continue; + }; + for checkout_dir in checkout_dirs.flatten() { + let Ok(revision_dirs) = fs::read_dir(checkout_dir.path()) else { + continue; + }; + for revision_dir in revision_dirs.flatten() { + let revision_path = revision_dir.path(); + let Some(revision_name) = revision_path.file_name().and_then(|name| name.to_str()) + else { + continue; + }; + if !revision_matches(revision_name, revision) { + continue; + } + if let Some(package_dir) = find_manifest_package_dir(&revision_path, package, version) { + return Some(package_dir); + } + } + } + } + None +} + +fn revision_matches(revision_name: &str, revision: Option<&str>) -> bool { + revision.is_none_or(|revision| { + revision.starts_with(revision_name) || revision_name.starts_with(revision) + }) +} + +fn find_manifest_package_dir(root: &Path, package: &str, version: &str) -> Option { + if manifest_matches_package(&root.join("Cargo.toml"), package, version) { + return Some(root.to_path_buf()); + } + + let Ok(entries) = fs::read_dir(root) else { + return None; + }; + for entry in entries.flatten() { + let candidate = entry.path(); + if candidate.is_dir() + && manifest_matches_package(&candidate.join("Cargo.toml"), package, version) + { + return Some(candidate); + } + } + None +} + +fn manifest_matches_package(manifest_path: &Path, package: &str, version: &str) -> bool { + let Ok(source) = fs::read_to_string(manifest_path) else { + return false; + }; + let mut in_package = false; + let mut name_matches = false; + let mut version_matches = false; + + for line in source.lines() { + let trimmed = line.trim(); + if trimmed.starts_with('[') { + in_package = trimmed == "[package]"; + continue; + } + if !in_package { + continue; + } + if let Some(value) = toml_string_value(trimmed, "name") { + name_matches = value == package; + continue; + } + if let Some(value) = toml_string_value(trimmed, "version") { + version_matches = value == version; + } + } + + name_matches && version_matches +} + +fn git_revision(source: &str) -> Option<&str> { + source.rsplit_once('#').and_then(|(_, revision)| { + if revision.is_empty() { + None + } else { + Some(revision) + } + }) +} + +fn locked_packages(lock_path: &Path) -> HashMap { let source = fs::read_to_string(lock_path).expect("Cargo.lock should be readable"); - let mut versions = HashMap::new(); + let mut packages = HashMap::new(); let mut current_name = None; let mut current_version = None; + let mut current_source = None; for line in source.lines() { let trimmed = line.trim(); if trimmed == "[[package]]" { if let (Some(name), Some(version)) = (current_name.take(), current_version.take()) { - versions.insert(name, version); + packages.insert(name, LockedPackage { version, source: current_source.take() }); } + current_source = None; continue; } - if let Some(value) = trimmed.strip_prefix("name = \"") { - current_name = value.strip_suffix('"').map(ToOwned::to_owned); + if let Some(value) = toml_string_value(trimmed, "name") { + current_name = Some(value.to_string()); continue; } - if let Some(value) = trimmed.strip_prefix("version = \"") { - current_version = value.strip_suffix('"').map(ToOwned::to_owned); + if let Some(value) = toml_string_value(trimmed, "version") { + current_version = Some(value.to_string()); + continue; + } + if let Some(value) = toml_string_value(trimmed, "source") { + current_source = Some(value.to_string()); } } if let (Some(name), Some(version)) = (current_name, current_version) { - versions.insert(name, version); + packages.insert(name, LockedPackage { version, source: current_source }); } - versions + packages +} + +fn toml_string_value<'a>(line: &'a str, key: &str) -> Option<&'a str> { + let value = line + .strip_prefix(key)? + .trim_start() + .strip_prefix('=')? + .trim_start() + .strip_prefix('"')?; + let end = value.find('"')?; + Some(&value[..end]) } diff --git a/crates/pi-natives/src/chunk/schema.rs b/crates/pi-natives/src/chunk/schema.rs index ae0126ecd..65f609ef6 100644 --- a/crates/pi-natives/src/chunk/schema.rs +++ b/crates/pi-natives/src/chunk/schema.rs @@ -123,7 +123,7 @@ mod tests { #[test] fn generated_schema_covers_expected_languages() { - for language in ["python", "nix", "toml", "typescript", "rust", "yaml"] { + for language in ["python", "nix", "toml", "typescript", "rust", "yaml", "handlebars"] { assert!(has_schema(language), "{language} should have generated schema data"); } }