aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-registry/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-22 11:07:42 +0900
committernsfisis <nsfisis@gmail.com>2026-02-22 11:15:29 +0900
commit9f0d210021c54f63c9984446862b6ec68834bc63 (patch)
treed1522b8047c60bc7ee7a9d832178dd24e1b07636 /crates/mozart-registry/src
parent2c243a3cb814939bbe40fda1608781825ab0d77d (diff)
downloadphp-mozart-9f0d210021c54f63c9984446862b6ec68834bc63.tar.gz
php-mozart-9f0d210021c54f63c9984446862b6ec68834bc63.tar.zst
php-mozart-9f0d210021c54f63c9984446862b6ec68834bc63.zip
refactor(async): migrate from blocking HTTP to async/await with tokio
Replace reqwest::blocking with async reqwest across the entire codebase. All command execute functions, registry API calls (packagist, downloader, resolver, lockfile), and the main entry point now use async/await with the tokio runtime. The pubgrub resolver runs on spawn_blocking since its DependencyProvider trait is synchronous, using Handle::block_on for async I/O within that context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-registry/src')
-rw-r--r--crates/mozart-registry/src/downloader.rs23
-rw-r--r--crates/mozart-registry/src/lockfile.rs23
-rw-r--r--crates/mozart-registry/src/packagist.rs27
-rw-r--r--crates/mozart-registry/src/resolver.rs189
4 files changed, 149 insertions, 113 deletions
diff --git a/crates/mozart-registry/src/downloader.rs b/crates/mozart-registry/src/downloader.rs
index cfed951..9a5ed24 100644
--- a/crates/mozart-registry/src/downloader.rs
+++ b/crates/mozart-registry/src/downloader.rs
@@ -79,7 +79,7 @@ impl DownloadProgress {
/// the `Content-Length` response header.
/// If `files_cache` is provided, the downloaded bytes are cached by URL; cache hits skip
/// the network request entirely.
-pub fn download_dist(
+pub async fn download_dist(
url: &str,
expected_shasum: Option<&str>,
progress: Option<&mut DownloadProgress>,
@@ -108,7 +108,7 @@ pub fn download_dist(
}
}
- let response = reqwest::blocking::get(url)?;
+ let response = reqwest::get(url).await?;
if !response.status().is_success() {
anyhow::bail!(
@@ -123,20 +123,15 @@ pub fn download_dist(
if let Some(content_length) = response.content_length() {
pb.set_total(content_length);
}
- let mut reader = response;
let mut buf = Vec::new();
- let mut chunk = [0u8; 8192];
- loop {
- let n = reader.read(&mut chunk)?;
- if n == 0 {
- break;
- }
- buf.extend_from_slice(&chunk[..n]);
- pb.inc(n as u64);
+ let mut stream = response;
+ while let Some(chunk) = stream.chunk().await? {
+ buf.extend_from_slice(&chunk);
+ pb.inc(chunk.len() as u64);
}
buf
} else {
- response.bytes()?.to_vec()
+ response.bytes().await?.to_vec()
};
// Verify SHA-1 checksum if provided
@@ -325,7 +320,7 @@ pub fn extract_tar_gz(data: &[u8], target_dir: &Path) -> anyhow::Result<()> {
/// - `package_name`: e.g. `"monolog/monolog"`
/// - `progress`: optional mutable progress tracker to update during download
/// - `files_cache`: optional files cache; if provided, the archive bytes are cached by URL
-pub fn install_package(
+pub async fn install_package(
dist_url: &str,
dist_type: &str,
dist_shasum: Option<&str>,
@@ -342,7 +337,7 @@ pub fn install_package(
}
fs::create_dir_all(&target)?;
- let bytes = download_dist(dist_url, dist_shasum, progress, files_cache)?;
+ let bytes = download_dist(dist_url, dist_shasum, progress, files_cache).await?;
match dist_type {
"zip" => extract_zip(&bytes, &target)?,
diff --git a/crates/mozart-registry/src/lockfile.rs b/crates/mozart-registry/src/lockfile.rs
index 16337c4..9064109 100644
--- a/crates/mozart-registry/src/lockfile.rs
+++ b/crates/mozart-registry/src/lockfile.rs
@@ -392,11 +392,12 @@ fn extract_platform_requirements(requirements: &BTreeMap<String, String>) -> ser
/// 2. Separates packages into production vs dev-only
/// 3. Computes the content-hash
/// 4. Assembles the complete `LockFile` struct
-pub fn generate_lock_file(request: &LockFileGenerationRequest) -> anyhow::Result<LockFile> {
+pub async fn generate_lock_file(request: &LockFileGenerationRequest) -> anyhow::Result<LockFile> {
// 1. Fetch full metadata for all resolved packages
let mut package_metadata: HashMap<String, PackagistVersion> = HashMap::new();
for pkg in &request.resolved_packages {
- let versions = packagist::fetch_package_versions(&pkg.name, request.repo_cache.as_ref())?;
+ let versions =
+ packagist::fetch_package_versions(&pkg.name, request.repo_cache.as_ref()).await?;
// Find the exact version matching pkg.version_normalized
let matching = versions
.into_iter()
@@ -913,8 +914,8 @@ mod tests {
assert_eq!(platform, serde_json::json!({}));
}
- #[test]
- fn test_generate_lock_file_minimal() {
+ #[tokio::test]
+ async fn test_generate_lock_file_minimal() {
let composer_json_content =
r#"{"name": "test/project", "require": {"php": ">=8.1"}}"#.to_string();
let composer_json: RawPackageData = serde_json::from_str(&composer_json_content).unwrap();
@@ -927,7 +928,7 @@ mod tests {
repo_cache: None,
};
- let lock = generate_lock_file(&request).unwrap();
+ let lock = generate_lock_file(&request).await.unwrap();
assert_eq!(lock.packages.len(), 0);
assert_eq!(lock.packages_dev.as_ref().unwrap().len(), 0);
@@ -1009,9 +1010,9 @@ mod tests {
assert_eq!(packages[1].name, "vendor/zebra");
}
- #[test]
+ #[tokio::test]
#[ignore]
- fn test_generate_lock_file_monolog() {
+ async fn test_generate_lock_file_monolog() {
use crate::resolver::PlatformConfig;
use crate::resolver::{ResolveRequest, resolve};
use mozart_core::package::Stability;
@@ -1031,7 +1032,9 @@ mod tests {
repo_cache: None,
};
- let resolved = resolve(&resolve_request).expect("Resolution should succeed");
+ let resolved = resolve(&resolve_request)
+ .await
+ .expect("Resolution should succeed");
assert!(!resolved.is_empty());
let composer_json_content =
@@ -1046,7 +1049,9 @@ mod tests {
repo_cache: None,
};
- let lock = generate_lock_file(&gen_request).expect("Lock file generation should succeed");
+ let lock = generate_lock_file(&gen_request)
+ .await
+ .expect("Lock file generation should succeed");
// Verify monolog is in packages
assert!(
diff --git a/crates/mozart-registry/src/packagist.rs b/crates/mozart-registry/src/packagist.rs
index ba80e7e..95d02ad 100644
--- a/crates/mozart-registry/src/packagist.rs
+++ b/crates/mozart-registry/src/packagist.rs
@@ -128,7 +128,7 @@ pub fn parse_p2_response(json: &str, package_name: &str) -> anyhow::Result<Vec<P
/// If `repo_cache` is provided, the JSON response is cached on disk under the
/// key `"provider-{vendor}~{package}.json"`. Subsequent calls for the same
/// package are served from cache without a network request.
-pub fn fetch_package_versions(
+pub async fn fetch_package_versions(
package_name: &str,
repo_cache: Option<&Cache>,
) -> anyhow::Result<Vec<PackagistVersion>> {
@@ -144,7 +144,7 @@ pub fn fetch_package_versions(
// Cache miss — fetch from Packagist
let url = format!("https://repo.packagist.org/p2/{package_name}.json");
- let response = reqwest::blocking::get(&url)?;
+ let response = reqwest::get(&url).await?;
if !response.status().is_success() {
anyhow::bail!(
@@ -153,7 +153,7 @@ pub fn fetch_package_versions(
);
}
- let body = response.text()?;
+ let body = response.text().await?;
// Write to cache
if let Some(cache) = repo_cache {
@@ -209,11 +209,11 @@ fn url_encode(s: &str) -> String {
///
/// Fetches up to `SEARCH_MAX_PAGES` pages of results and returns the full list.
/// An optional `package_type` filter can narrow results (e.g. `"library"`).
-pub fn search_packages(
+pub async fn search_packages(
query: &str,
package_type: Option<&str>,
) -> anyhow::Result<(Vec<SearchResult>, u64)> {
- let client = reqwest::blocking::Client::builder()
+ let client = reqwest::Client::builder()
.user_agent("mozart/0.1.0")
.build()?;
@@ -224,11 +224,11 @@ pub fn search_packages(
loop {
let response: SearchResponse = if let Some(ref url) = next_url {
- let resp = client.get(url).send()?;
+ let resp = client.get(url).send().await?;
if !resp.status().is_success() {
anyhow::bail!("Packagist search request failed (HTTP {})", resp.status());
}
- resp.json()?
+ resp.json().await?
} else {
let encoded_query = url_encode(query);
let mut url = format!("https://packagist.org/search.json?q={encoded_query}");
@@ -237,11 +237,11 @@ pub fn search_packages(
url.push_str(&url_encode(t));
}
- let resp = client.get(&url).send()?;
+ let resp = client.get(&url).send().await?;
if !resp.status().is_success() {
anyhow::bail!("Packagist search request failed (HTTP {})", resp.status());
}
- resp.json()?
+ resp.json().await?
};
if page == 1 {
@@ -321,10 +321,10 @@ pub struct SecurityAdvisoriesResponse {
///
/// If the package list is very large (500+), requests are batched in chunks of
/// 500 names per request and the results are merged.
-pub fn fetch_security_advisories(
+pub async fn fetch_security_advisories(
package_names: &[&str],
) -> anyhow::Result<BTreeMap<String, Vec<SecurityAdvisory>>> {
- let client = reqwest::blocking::Client::builder()
+ let client = reqwest::Client::builder()
.user_agent("mozart/0.1.0")
.build()?;
@@ -343,7 +343,8 @@ pub fn fetch_security_advisories(
.post("https://packagist.org/api/security-advisories/")
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body)
- .send()?;
+ .send()
+ .await?;
if !response.status().is_success() {
anyhow::bail!(
@@ -352,7 +353,7 @@ pub fn fetch_security_advisories(
);
}
- let parsed: SecurityAdvisoriesResponse = response.json()?;
+ let parsed: SecurityAdvisoriesResponse = response.json().await?;
for (pkg_name, advisories) in parsed.advisories {
if !advisories.is_empty() {
diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs
index eb4f6e5..84e6a96 100644
--- a/crates/mozart-registry/src/resolver.rs
+++ b/crates/mozart-registry/src/resolver.rs
@@ -586,6 +586,9 @@ struct VersionDependencies {
/// pubgrub `DependencyProvider` that fetches package metadata from Packagist.
pub struct MozartProvider {
+ /// Tokio runtime handle for calling async functions from sync trait methods.
+ handle: tokio::runtime::Handle,
+
/// Cache of fetched package metadata. Populated lazily from Packagist.
package_cache: RefCell<HashMap<String, PackageVersions>>,
@@ -632,13 +635,16 @@ impl MozartProvider {
}
// Fetch from Packagist (with optional on-disk repo cache)
- let packagist_versions = packagist::fetch_package_versions(
- package_name,
- self.repo_cache.as_ref(),
- )
- .map_err(|e| {
- ResolverError::PackagistError(format!("Failed to fetch {}: {}", package_name, e))
- })?;
+ // Uses block_on because pubgrub's DependencyProvider trait is synchronous.
+ let packagist_versions = self
+ .handle
+ .block_on(packagist::fetch_package_versions(
+ package_name,
+ self.repo_cache.as_ref(),
+ ))
+ .map_err(|e| {
+ ResolverError::PackagistError(format!("Failed to fetch {}: {}", package_name, e))
+ })?;
// Convert and filter
let mut versions = BTreeMap::new();
@@ -975,8 +981,8 @@ pub struct ResolvedPackage {
///
/// Returns a list of resolved packages (excluding root and platform packages),
/// or a human-readable error.
-pub fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, ResolveError> {
- // 1. Build root dependencies
+pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, ResolveError> {
+ // 1. Build root dependencies (parsing is CPU-only, no async needed)
let mut root_deps: Vec<(PackageName, ComposerVS)> = Vec::new();
let root_conflicts: Vec<(PackageName, ComposerVS)> = Vec::new();
@@ -1012,79 +1018,94 @@ pub fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, Resolve
}
}
- // 2. Build the provider
- let provider = MozartProvider {
- package_cache: RefCell::new(HashMap::new()),
- repo_cache: request.repo_cache.clone(),
- platform_packages: request.platform.to_versions(),
- minimum_stability: request.minimum_stability,
- stability_flags: request.stability_flags.clone(),
- prefer_stable: request.prefer_stable,
- prefer_lowest: request.prefer_lowest,
- root_dependencies: root_deps,
- root_conflicts,
- ignore_platform_reqs: request.ignore_platform_reqs,
- ignore_platform_req_list: request.ignore_platform_req_list.clone(),
- };
+ // Capture the current tokio Handle so the provider can call async functions
+ // from within pubgrub's synchronous DependencyProvider trait methods.
+ let handle = tokio::runtime::Handle::current();
- // 3. Run pubgrub
- let root = PackageName::root();
- let root_version = ComposerVersion::stable(0, 0, 0, 0);
+ // Clone data needed by spawn_blocking (which requires 'static)
+ let repo_cache = request.repo_cache.clone();
+ let platform_packages = request.platform.to_versions();
+ let minimum_stability = request.minimum_stability;
+ let stability_flags = request.stability_flags.clone();
+ let prefer_stable = request.prefer_stable;
+ let prefer_lowest = request.prefer_lowest;
+ let ignore_platform_reqs = request.ignore_platform_reqs;
+ let ignore_platform_req_list = request.ignore_platform_req_list.clone();
- match pubgrub::resolve(&provider, root, root_version) {
- Ok(solution) => {
- // 4. Convert solution to ResolvedPackage list
- let mut result = Vec::new();
- for (pkg, version) in solution {
- // Skip root and platform packages
- if pkg.is_root() || pkg.is_platform() {
- continue;
- }
+ // 2. Run pubgrub on a blocking thread (it is CPU-bound + uses block_on for I/O)
+ tokio::task::spawn_blocking(move || {
+ let provider = MozartProvider {
+ handle,
+ package_cache: RefCell::new(HashMap::new()),
+ repo_cache,
+ platform_packages,
+ minimum_stability,
+ stability_flags,
+ prefer_stable,
+ prefer_lowest,
+ root_dependencies: root_deps,
+ root_conflicts,
+ ignore_platform_reqs,
+ ignore_platform_req_list,
+ };
- // Look up the original version string from the cache
- let cache = provider.package_cache.borrow();
- let (version_str, version_normalized) = if let Some(pvs) = cache.get(&pkg.0) {
- if let Some(vd) = pvs.versions.get(&version) {
- (vd.version_string.clone(), vd.version_normalized.clone())
+ let root = PackageName::root();
+ let root_version = ComposerVersion::stable(0, 0, 0, 0);
+
+ match pubgrub::resolve(&provider, root, root_version) {
+ Ok(solution) => {
+ let mut result = Vec::new();
+ for (pkg, version) in solution {
+ if pkg.is_root() || pkg.is_platform() {
+ continue;
+ }
+
+ let cache = provider.package_cache.borrow();
+ let (version_str, version_normalized) = if let Some(pvs) = cache.get(&pkg.0) {
+ if let Some(vd) = pvs.versions.get(&version) {
+ (vd.version_string.clone(), vd.version_normalized.clone())
+ } else {
+ (version.to_string(), version.to_string())
+ }
} else {
(version.to_string(), version.to_string())
- }
- } else {
- (version.to_string(), version.to_string())
- };
+ };
- result.push(ResolvedPackage {
- name: pkg.0.clone(),
- version: version_str,
- version_normalized,
- is_dev: version.stability < STABILITY_ALPHA_BASE,
- });
+ result.push(ResolvedPackage {
+ name: pkg.0.clone(),
+ version: version_str,
+ version_normalized,
+ is_dev: version.stability < STABILITY_ALPHA_BASE,
+ });
+ }
+ Ok(result)
+ }
+ Err(PubGrubError::NoSolution(mut derivation_tree)) => {
+ derivation_tree.collapse_no_versions();
+ let report = DefaultStringReporter::report(&derivation_tree);
+ Err(ResolveError::NoSolution(report))
+ }
+ Err(PubGrubError::ErrorRetrievingDependencies {
+ package,
+ version,
+ source,
+ }) => Err(ResolveError::DependencyFetchError(format!(
+ "Error retrieving dependencies for {} {}: {}",
+ package, version, source
+ ))),
+ Err(PubGrubError::ErrorChoosingVersion { package, source }) => {
+ Err(ResolveError::DependencyFetchError(format!(
+ "Error choosing version for {}: {}",
+ package, source
+ )))
+ }
+ Err(PubGrubError::ErrorInShouldCancel(e)) => {
+ Err(ResolveError::Internal(format!("Resolver cancelled: {}", e)))
}
- Ok(result)
- }
- Err(PubGrubError::NoSolution(mut derivation_tree)) => {
- derivation_tree.collapse_no_versions();
- let report = DefaultStringReporter::report(&derivation_tree);
- Err(ResolveError::NoSolution(report))
- }
- Err(PubGrubError::ErrorRetrievingDependencies {
- package,
- version,
- source,
- }) => Err(ResolveError::DependencyFetchError(format!(
- "Error retrieving dependencies for {} {}: {}",
- package, version, source
- ))),
- Err(PubGrubError::ErrorChoosingVersion { package, source }) => {
- Err(ResolveError::DependencyFetchError(format!(
- "Error choosing version for {}: {}",
- package, source
- )))
- }
- Err(PubGrubError::ErrorInShouldCancel(e)) => {
- Err(ResolveError::Internal(format!("Resolver cancelled: {}", e)))
}
- }
+ })
+ .await
+ .unwrap()
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -1096,6 +1117,13 @@ mod tests {
use super::*;
use pubgrub::{OfflineDependencyProvider, Ranges};
+ fn test_handle() -> tokio::runtime::Handle {
+ static RT: std::sync::OnceLock<tokio::runtime::Runtime> = std::sync::OnceLock::new();
+ RT.get_or_init(|| tokio::runtime::Runtime::new().unwrap())
+ .handle()
+ .clone()
+ }
+
// ──────────── ComposerVersion parsing ────────────
#[test]
@@ -1532,6 +1560,7 @@ mod tests {
#[test]
fn test_stability_filter() {
let provider = MozartProvider {
+ handle: test_handle(),
package_cache: RefCell::new(HashMap::new()),
platform_packages: HashMap::new(),
minimum_stability: Stability::Stable,
@@ -1585,6 +1614,7 @@ mod tests {
#[test]
fn test_stability_filter_beta() {
let provider = MozartProvider {
+ handle: test_handle(),
package_cache: RefCell::new(HashMap::new()),
platform_packages: HashMap::new(),
minimum_stability: Stability::Beta,
@@ -1630,6 +1660,7 @@ mod tests {
#[test]
fn test_stability_filter_dev() {
let provider = MozartProvider {
+ handle: test_handle(),
package_cache: RefCell::new(HashMap::new()),
platform_packages: HashMap::new(),
minimum_stability: Stability::Dev,
@@ -1656,6 +1687,7 @@ mod tests {
#[test]
fn test_skip_platform_dep() {
let provider = MozartProvider {
+ handle: test_handle(),
package_cache: RefCell::new(HashMap::new()),
platform_packages: HashMap::new(),
minimum_stability: Stability::Stable,
@@ -1677,6 +1709,7 @@ mod tests {
#[test]
fn test_skip_specific_platform_dep() {
let provider = MozartProvider {
+ handle: test_handle(),
package_cache: RefCell::new(HashMap::new()),
platform_packages: HashMap::new(),
minimum_stability: Stability::Stable,
@@ -1699,6 +1732,7 @@ mod tests {
#[test]
fn test_root_package_choose_version() {
let provider = MozartProvider {
+ handle: test_handle(),
package_cache: RefCell::new(HashMap::new()),
platform_packages: HashMap::new(),
minimum_stability: Stability::Stable,
@@ -1726,6 +1760,7 @@ mod tests {
platform.insert("php".to_string(), php_v);
let provider = MozartProvider {
+ handle: test_handle(),
package_cache: RefCell::new(HashMap::new()),
platform_packages: platform,
minimum_stability: Stability::Stable,
@@ -1884,9 +1919,9 @@ mod tests {
// ──────────── End-to-end tests (require network, marked #[ignore]) ────────────
- #[test]
+ #[tokio::test]
#[ignore]
- fn test_resolve_monolog_e2e() {
+ async fn test_resolve_monolog_e2e() {
let request = ResolveRequest {
require: vec![("monolog/monolog".to_string(), "^3.0".to_string())],
require_dev: vec![],
@@ -1901,7 +1936,7 @@ mod tests {
repo_cache: None,
};
- let result = resolve(&request);
+ let result = resolve(&request).await;
match result {
Ok(packages) => {
println!("Resolved {} packages:", packages.len());