diff options
Diffstat (limited to 'crates/mozart-registry/src')
| -rw-r--r-- | crates/mozart-registry/src/inline_package.rs | 171 | ||||
| -rw-r--r-- | crates/mozart-registry/src/lib.rs | 1 | ||||
| -rw-r--r-- | crates/mozart-registry/src/lockfile.rs | 20 | ||||
| -rw-r--r-- | crates/mozart-registry/src/resolver.rs | 26 | ||||
| -rw-r--r-- | crates/mozart-registry/src/vcs_bridge.rs | 10 |
5 files changed, 222 insertions, 6 deletions
diff --git a/crates/mozart-registry/src/inline_package.rs b/crates/mozart-registry/src/inline_package.rs new file mode 100644 index 0000000..e10cd2b --- /dev/null +++ b/crates/mozart-registry/src/inline_package.rs @@ -0,0 +1,171 @@ +//! Support for inline `type: package` repositories. +//! +//! `composer.json` may embed full package metadata under +//! `repositories[].package`, mirroring `Composer\Repository\PackageRepository`. +//! These packages need no network fetch — they go straight into the resolver +//! pool and into the generated lockfile entry verbatim. + +use crate::packagist::PackagistVersion; +use mozart_core::package::RawRepository; + +/// One package extracted from a `type: package` repository. +pub struct InlinePackage { + pub name: String, + pub version: PackagistVersion, +} + +/// Collect every package definition from `type: package` repositories. +/// +/// Each repository's `package` field may be a single object or an array of +/// objects. Entries that fail to parse (missing `name`/`version`, etc.) are +/// silently skipped so the rest of the repositories list still applies — +/// matching Composer's lenient PackageRepository constructor. +pub fn collect_inline_packages(repositories: &[RawRepository]) -> Vec<InlinePackage> { + let mut packages = Vec::new(); + for repo in repositories { + if repo.repo_type != "package" { + continue; + } + let Some(value) = &repo.package else { + continue; + }; + + match value { + serde_json::Value::Array(arr) => { + for entry in arr { + if let Some(pkg) = parse_inline_package(entry) { + packages.push(pkg); + } + } + } + serde_json::Value::Object(_) => { + if let Some(pkg) = parse_inline_package(value) { + packages.push(pkg); + } + } + _ => {} + } + } + packages +} + +fn parse_inline_package(value: &serde_json::Value) -> Option<InlinePackage> { + let obj = value.as_object()?; + let name = obj.get("name")?.as_str()?.to_string(); + let version_str = obj.get("version")?.as_str()?.to_string(); + + // PackagistVersion requires `version_normalized`. If the inline definition + // omits it (the common case), compute it the same way Packagist does: + // run the version through Mozart's normalizer. + let mut value_for_parse = value.clone(); + if let serde_json::Value::Object(ref mut map) = value_for_parse + && !map.contains_key("version_normalized") + { + let normalized = mozart_semver::Version::parse(&version_str) + .map(|v| v.to_string()) + .unwrap_or_else(|_| version_str.clone()); + map.insert( + "version_normalized".to_string(), + serde_json::Value::String(normalized), + ); + } + + let version: PackagistVersion = serde_json::from_value(value_for_parse).ok()?; + Some(InlinePackage { name, version }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn pkg_repo(value: serde_json::Value) -> RawRepository { + RawRepository { + repo_type: "package".to_string(), + url: None, + package: Some(value), + } + } + + #[test] + fn collects_single_inline_package_object() { + let repos = vec![pkg_repo(serde_json::json!({ + "name": "a/a", + "version": "1.0.0" + }))]; + let pkgs = collect_inline_packages(&repos); + assert_eq!(pkgs.len(), 1); + assert_eq!(pkgs[0].name, "a/a"); + assert_eq!(pkgs[0].version.version, "1.0.0"); + assert_eq!(pkgs[0].version.version_normalized, "1.0.0.0"); + } + + #[test] + fn collects_inline_package_array() { + let repos = vec![pkg_repo(serde_json::json!([ + {"name": "a/a", "version": "1.0.0"}, + {"name": "b/b", "version": "2.0.0"} + ]))]; + let pkgs = collect_inline_packages(&repos); + assert_eq!(pkgs.len(), 2); + assert_eq!(pkgs[0].name, "a/a"); + assert_eq!(pkgs[1].name, "b/b"); + } + + #[test] + fn ignores_non_package_repos() { + let repos = vec![RawRepository { + repo_type: "vcs".to_string(), + url: Some("https://example.com/foo.git".to_string()), + package: None, + }]; + assert!(collect_inline_packages(&repos).is_empty()); + } + + #[test] + fn skips_entries_missing_name_or_version() { + let repos = vec![pkg_repo(serde_json::json!([ + {"name": "a/a", "version": "1.0.0"}, + {"name": "missing/version"}, + {"version": "2.0.0"}, + {"name": "b/b", "version": "2.0.0"} + ]))]; + let pkgs = collect_inline_packages(&repos); + assert_eq!(pkgs.len(), 2); + assert_eq!(pkgs[0].name, "a/a"); + assert_eq!(pkgs[1].name, "b/b"); + } + + #[test] + fn preserves_explicit_version_normalized() { + let repos = vec![pkg_repo(serde_json::json!({ + "name": "a/a", + "version": "1.0", + "version_normalized": "1.0.0.0-explicit" + }))]; + let pkgs = collect_inline_packages(&repos); + assert_eq!(pkgs[0].version.version_normalized, "1.0.0.0-explicit"); + } + + #[test] + fn parses_full_metadata_fields() { + let repos = vec![pkg_repo(serde_json::json!({ + "name": "a/a", + "version": "1.0.0", + "type": "library", + "require": {"b/b": "^2.0"}, + "replace": {"old/x": "1.0"}, + "provide": {"some/iface": "1.0"}, + "conflict": {"bad/pkg": "*"}, + "dist": {"type": "zip", "url": "https://e.com/a.zip"} + }))]; + let pkgs = collect_inline_packages(&repos); + assert_eq!(pkgs.len(), 1); + let v = &pkgs[0].version; + assert_eq!(v.package_type.as_deref(), Some("library")); + assert_eq!(v.require.get("b/b").map(String::as_str), Some("^2.0")); + assert_eq!(v.replace.get("old/x").map(String::as_str), Some("1.0")); + assert_eq!(v.provide.get("some/iface").map(String::as_str), Some("1.0")); + assert_eq!(v.conflict.get("bad/pkg").map(String::as_str), Some("*")); + assert!(v.dist.is_some()); + } +} diff --git a/crates/mozart-registry/src/lib.rs b/crates/mozart-registry/src/lib.rs index 4c26c1e..a4afacd 100644 --- a/crates/mozart-registry/src/lib.rs +++ b/crates/mozart-registry/src/lib.rs @@ -1,5 +1,6 @@ pub mod cache; pub mod downloader; +pub mod inline_package; pub mod installed; pub mod lockfile; pub mod packagist; diff --git a/crates/mozart-registry/src/lockfile.rs b/crates/mozart-registry/src/lockfile.rs index 331f58e..a99c921 100644 --- a/crates/mozart-registry/src/lockfile.rs +++ b/crates/mozart-registry/src/lockfile.rs @@ -354,6 +354,19 @@ pub struct LockFileGenerationRequest { pub repo_cache: Cache, } +impl LockFileGenerationRequest { + /// Look up an inline `type: package` definition for `name` (if any). + /// Returns the matching `PackagistVersion` so callers can short-circuit + /// the Packagist fetch for resolved packages that came from a `type: + /// package` repository. + fn inline_lookup(&self, name: &str, version_normalized: &str) -> Option<PackagistVersion> { + crate::inline_package::collect_inline_packages(&self.composer_json.repositories) + .into_iter() + .find(|ipkg| ipkg.name == name && ipkg.version.version_normalized == version_normalized) + .map(|ipkg| ipkg.version) + } +} + /// Convert a `PackagistSource` to a `LockedSource`. fn packagist_source_to_locked(ps: &PackagistSource) -> LockedSource { LockedSource { @@ -504,6 +517,13 @@ pub async fn generate_lock_file(request: &LockFileGenerationRequest) -> anyhow:: // 1. Fetch full metadata for all resolved packages let mut package_metadata: HashMap<String, PackagistVersion> = HashMap::new(); for pkg in &request.resolved_packages { + // Inline `type: package` repositories carry full metadata in + // composer.json — use it directly instead of hitting Packagist. + if let Some(inline) = request.inline_lookup(&pkg.name, &pkg.version_normalized) { + package_metadata.insert(pkg.name.clone(), inline); + continue; + } + let versions = packagist::fetch_package_versions(&pkg.name, &request.repo_cache).await?; // Find the exact version matching pkg.version_normalized let matching = versions diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs index e3cc1f6..7aab6b2 100644 --- a/crates/mozart-registry/src/resolver.rs +++ b/crates/mozart-registry/src/resolver.rs @@ -463,14 +463,32 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R } } + // Collect inline `type: package` repositories. These don't require any + // network fetch; they go straight into the pool and are also tracked by + // name so the Packagist seed/transitive loops below skip them. + let inline_packages = crate::inline_package::collect_inline_packages(&request.repositories); + let mut inline_package_names: HashSet<String> = HashSet::new(); + for ipkg in &inline_packages { + inline_package_names.insert(ipkg.name.clone()); + let inputs = packagist_to_pool_inputs( + &ipkg.name, + &ipkg.version, + request.minimum_stability, + &request.stability_flags, + ); + for input in inputs { + builder.add_package(input); + } + } + // Seed the builder with packages for root requirements for name in root_requires.keys() { if PackageName(name.clone()).is_platform() { continue; // platform packages already added } - // Skip packages already provided by VCS repositories - if vcs_package_names.contains(name) { + // Skip packages already provided by VCS or inline-package repositories + if vcs_package_names.contains(name) || inline_package_names.contains(name) { continue; } @@ -500,8 +518,8 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R continue; } - // Skip packages already provided by VCS repositories - if vcs_package_names.contains(&name) { + // Skip packages already provided by VCS or inline-package repositories + if vcs_package_names.contains(&name) || inline_package_names.contains(&name) { continue; } diff --git a/crates/mozart-registry/src/vcs_bridge.rs b/crates/mozart-registry/src/vcs_bridge.rs index 78aaaa5..be61845 100644 --- a/crates/mozart-registry/src/vcs_bridge.rs +++ b/crates/mozart-registry/src/vcs_bridge.rs @@ -32,14 +32,20 @@ pub async fn scan_vcs_repositories(repositories: &[RawRepository]) -> Vec<VcsPac other => Some(other), }; - let vcs_repo = VcsRepository::new(repo.url.clone(), forced_type, config.clone()); + // VCS repositories require `url`; skip silently if missing (Composer + // would reject this earlier in RepositoryFactory). + let Some(url) = repo.url.clone() else { + continue; + }; + + let vcs_repo = VcsRepository::new(url.clone(), forced_type, config.clone()); match vcs_repo.scan().await { Ok(versions) => { all_versions.extend(versions); } Err(e) => { - eprintln!("Warning: Failed to scan VCS repository {}: {}", repo.url, e,); + eprintln!("Warning: Failed to scan VCS repository {url}: {e}"); } } } |
