aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/mozart/src/autoload.rs16
-rw-r--r--crates/mozart/src/commands/dump_autoload.rs1
-rw-r--r--crates/mozart/src/commands/install.rs191
-rw-r--r--crates/mozart/src/commands/remove.rs13
-rw-r--r--crates/mozart/src/commands/require.rs29
-rw-r--r--crates/mozart/src/commands/update.rs29
-rw-r--r--crates/mozart/src/downloader.rs102
7 files changed, 340 insertions, 41 deletions
diff --git a/crates/mozart/src/autoload.rs b/crates/mozart/src/autoload.rs
index 38c00d5..d4421a6 100644
--- a/crates/mozart/src/autoload.rs
+++ b/crates/mozart/src/autoload.rs
@@ -20,6 +20,8 @@ pub struct AutoloadConfig {
/// Unique suffix for the autoloader class names (typically the lock file content-hash).
/// Used to generate `ComposerAutoloaderInit{suffix}` and `ComposerStaticInit{suffix}`.
pub suffix: String,
+ /// When true, emit `$loader->setClassMapAuthoritative(true)` in the generated autoloader.
+ pub classmap_authoritative: bool,
}
/// Collected autoload mappings from all packages.
@@ -427,7 +429,7 @@ fn generate_autoload_static(static_data: &AutoloadData, suffix: &str) -> String
}
/// Generate `vendor/composer/autoload_real.php`.
-fn generate_autoload_real(suffix: &str, has_files: bool) -> String {
+fn generate_autoload_real(suffix: &str, has_files: bool, classmap_authoritative: bool) -> String {
let mut out = String::new();
out.push_str("<?php\n\n");
out.push_str("// autoload_real.php @generated by Composer\n\n");
@@ -463,6 +465,10 @@ fn generate_autoload_real(suffix: &str, has_files: bool) -> String {
));
out.push_str(" $loader->register(true);\n");
+ if classmap_authoritative {
+ out.push_str(" $loader->setClassMapAuthoritative(true);\n");
+ }
+
if has_files {
out.push('\n');
out.push_str(&format!(
@@ -690,7 +696,7 @@ pub fn generate(config: &AutoloadConfig) -> anyhow::Result<()> {
)?;
std::fs::write(
composer_dir.join("autoload_real.php"),
- generate_autoload_real(&config.suffix, has_files),
+ generate_autoload_real(&config.suffix, has_files, config.classmap_authoritative),
)?;
std::fs::write(
config.vendor_dir.join("autoload.php"),
@@ -1022,7 +1028,7 @@ mod tests {
#[test]
fn test_generate_autoload_real_with_files() {
- let output = generate_autoload_real("abc123", true);
+ let output = generate_autoload_real("abc123", true, false);
assert!(output.contains("class ComposerAutoloaderInitabc123"));
assert!(output.contains("ComposerStaticInitabc123::$files"));
assert!(output.contains("$requireFile"));
@@ -1031,7 +1037,7 @@ mod tests {
#[test]
fn test_generate_autoload_real_without_files() {
- let output = generate_autoload_real("abc123", false);
+ let output = generate_autoload_real("abc123", false, false);
assert!(output.contains("class ComposerAutoloaderInitabc123"));
assert!(!output.contains("$filesToLoad"));
assert!(!output.contains("__composer_autoload_files"));
@@ -1104,6 +1110,7 @@ mod tests {
vendor_dir: vendor_dir.clone(),
dev_mode: false,
suffix: "abc123def456".to_string(),
+ classmap_authoritative: false,
};
generate(&config).unwrap();
@@ -1199,6 +1206,7 @@ mod tests {
vendor_dir: vendor_dir.clone(),
dev_mode: false,
suffix: "test".to_string(),
+ classmap_authoritative: false,
};
generate(&config).unwrap();
diff --git a/crates/mozart/src/commands/dump_autoload.rs b/crates/mozart/src/commands/dump_autoload.rs
index ff3dfc2..2c17c55 100644
--- a/crates/mozart/src/commands/dump_autoload.rs
+++ b/crates/mozart/src/commands/dump_autoload.rs
@@ -70,6 +70,7 @@ pub fn execute(args: &DumpAutoloadArgs, cli: &super::Cli) -> anyhow::Result<()>
vendor_dir,
dev_mode,
suffix,
+ classmap_authoritative: args.classmap_authoritative,
})?;
eprintln!("Generated autoload files");
diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs
index ff53d1e..6c023a1 100644
--- a/crates/mozart/src/commands/install.rs
+++ b/crates/mozart/src/commands/install.rs
@@ -92,6 +92,41 @@ pub struct InstallArgs {
pub ignore_platform_reqs: bool,
}
+/// Configuration for `install_from_lock`, replacing positional boolean parameters.
+pub struct InstallConfig {
+ /// Install dev dependencies as well as prod dependencies.
+ pub dev_mode: bool,
+ /// Print what would happen without making changes.
+ pub dry_run: bool,
+ /// Skip generating autoload files.
+ pub no_autoloader: bool,
+ /// Suppress download progress bars.
+ pub no_progress: bool,
+ /// Ignore all platform requirements (php, ext-*, lib-*).
+ pub ignore_platform_reqs: bool,
+ /// Ignore specific platform requirements by name.
+ pub ignore_platform_req: Vec<String>,
+ /// Optimize autoloader by generating a classmap.
+ pub optimize_autoloader: bool,
+ /// Use classmap-only autoloading (implies optimize_autoloader).
+ pub classmap_authoritative: bool,
+}
+
+impl Default for InstallConfig {
+ fn default() -> Self {
+ Self {
+ dev_mode: true,
+ dry_run: false,
+ no_autoloader: false,
+ no_progress: false,
+ ignore_platform_reqs: false,
+ ignore_platform_req: vec![],
+ optimize_autoloader: false,
+ classmap_authoritative: false,
+ }
+ }
+}
+
/// The action to take for a package during install.
#[derive(Debug, PartialEq, Eq)]
pub enum Action {
@@ -201,27 +236,82 @@ pub fn cleanup_empty_vendor_dirs(vendor_dir: &Path) -> anyhow::Result<()> {
Ok(())
}
+/// Check whether a package name refers to a platform package.
+///
+/// Platform packages are: names starting with "php", "ext-", or "lib-".
+fn is_platform_package(name: &str) -> bool {
+ let lower = name.to_lowercase();
+ lower == "php"
+ || lower.starts_with("php-")
+ || lower.starts_with("ext-")
+ || lower.starts_with("lib-")
+}
+
+/// Warn about platform requirements found in locked packages.
+///
+/// Iterates all locked packages' `require` fields, filters for platform entries,
+/// and emits a warning for any that are not in the ignore list (unless
+/// `ignore_platform_reqs` is set).
+fn warn_platform_requirements(
+ packages: &[&lockfile::LockedPackage],
+ ignore_platform_reqs: bool,
+ ignore_platform_req: &[String],
+) {
+ if ignore_platform_reqs {
+ return;
+ }
+
+ let ignored_set: HashSet<String> = ignore_platform_req
+ .iter()
+ .map(|s| s.to_lowercase())
+ .collect();
+
+ for pkg in packages {
+ for (req_name, req_constraint) in &pkg.require {
+ if is_platform_package(req_name) {
+ let lower = req_name.to_lowercase();
+ if !ignored_set.contains(&lower) {
+ eprintln!(
+ "{}",
+ console::warning(&format!(
+ "Platform requirement {req_name} {req_constraint} (required by {}) \
+ has not been verified. Platform detection is not yet fully implemented.",
+ pkg.name
+ ))
+ );
+ }
+ }
+ }
+ }
+}
+
+/// Create a download progress tracker for a package.
+fn make_progress(show: bool, pkg_name: &str, version: &str) -> downloader::DownloadProgress {
+ downloader::DownloadProgress::new(show, format!("{pkg_name} ({version})"))
+}
+
/// Install packages from a lock file into vendor/.
///
/// Used by both the `install` and `update` commands.
///
/// This function:
/// 1. Determines which packages to install (prod + optionally dev)
-/// 2. Reads currently installed packages
-/// 3. Computes install/update/skip/removal operations
-/// 4. Prints a summary
-/// 5. Executes downloads and removals (unless dry_run)
-/// 6. Writes vendor/composer/installed.json
-/// 7. Cleans up empty vendor directories
-/// 8. Generates the autoloader (unless no_autoloader)
+/// 2. Warns about platform requirements (unless ignored)
+/// 3. Reads currently installed packages
+/// 4. Computes install/update/skip/removal operations
+/// 5. Prints a summary
+/// 6. Executes downloads with optional progress bars (unless dry_run)
+/// 7. Writes vendor/composer/installed.json
+/// 8. Cleans up empty vendor directories
+/// 9. Generates the autoloader (unless no_autoloader)
pub fn install_from_lock(
lock: &lockfile::LockFile,
working_dir: &Path,
vendor_dir: &Path,
- dev_mode: bool,
- dry_run: bool,
- no_autoloader: bool,
+ config: &InstallConfig,
) -> anyhow::Result<()> {
+ let dev_mode = config.dev_mode;
+
// Step 1: Determine which packages to install
let mut packages_to_install: Vec<&lockfile::LockedPackage> = lock.packages.iter().collect();
@@ -237,13 +327,20 @@ pub fn install_from_lock(
}
eprintln!("Verifying lock file contents can be installed on current platform.");
- // Step 2: Read currently installed packages
+ // Step 2: Warn about platform requirements
+ warn_platform_requirements(
+ &packages_to_install,
+ config.ignore_platform_reqs,
+ &config.ignore_platform_req,
+ );
+
+ // Step 3: Read currently installed packages
let installed = installed::InstalledPackages::read(vendor_dir)?;
- // Step 3: Compute install operations
+ // Step 4: Compute install operations
let (ops, removals) = compute_operations(&packages_to_install, &installed);
- // Step 4: Print operation summary
+ // Step 5: Print operation summary
let installs: Vec<_> = ops
.iter()
.filter(|(_, a)| matches!(a, Action::Install))
@@ -270,8 +367,8 @@ pub fn install_from_lock(
);
}
- // Step 5: Execute operations (unless dry_run)
- if dry_run {
+ // Step 6: Execute operations (unless dry_run)
+ if config.dry_run {
for (pkg, action) in &ops {
match action {
Action::Skip => {}
@@ -305,13 +402,18 @@ pub fn install_from_lock(
)
})?;
+ let mut progress = make_progress(!config.no_progress, &pkg.name, &pkg.version);
+
downloader::install_package(
&dist.url,
&dist.dist_type,
dist.shasum.as_deref(),
vendor_dir,
&pkg.name,
+ Some(&mut progress),
)?;
+
+ progress.finish();
}
// Handle removals
@@ -323,12 +425,12 @@ pub fn install_from_lock(
}
}
- // Step 6: Clean up empty vendor namespace directories
+ // Step 7: Clean up empty vendor namespace directories
if !removals.is_empty() {
cleanup_empty_vendor_dirs(vendor_dir)?;
}
- // Step 7: Write updated vendor/composer/installed.json
+ // Step 8: Write updated vendor/composer/installed.json
let mut new_installed = installed::InstalledPackages::new();
new_installed.dev = dev_mode;
@@ -343,10 +445,27 @@ pub fn install_from_lock(
new_installed.write(vendor_dir)?;
- // Step 8: Generate autoloader (unless no_autoloader)
- if !no_autoloader {
+ // Step 9: Generate autoloader (unless no_autoloader)
+ if !config.no_autoloader {
eprintln!("Generating autoload files");
+ if config.classmap_authoritative {
+ eprintln!(
+ "{}",
+ console::info(
+ "Classmap-authoritative mode: autoloader will only look up classes in the classmap."
+ )
+ );
+ } else if config.optimize_autoloader {
+ eprintln!(
+ "{}",
+ console::info(
+ "Optimize autoloader: classmap scanning is not yet fully supported. \
+ PSR-4/PSR-0 autoloading will still be used."
+ )
+ );
+ }
+
let suffix = lock.content_hash.clone();
crate::autoload::generate(&crate::autoload::AutoloadConfig {
@@ -354,6 +473,7 @@ pub fn install_from_lock(
vendor_dir: vendor_dir.to_path_buf(),
dev_mode,
suffix,
+ classmap_authoritative: config.classmap_authoritative,
})?;
eprintln!("Generated autoload files");
@@ -432,18 +552,41 @@ pub fn execute(args: &InstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
}
}
- // Step 5: Determine dev mode and vendor directory
+ // Step 5: Warn about prefer-source (not yet supported)
+ let prefer_source = args.prefer_source
+ || args
+ .prefer_install
+ .as_deref()
+ .map(|s| s.eq_ignore_ascii_case("source"))
+ .unwrap_or(false);
+ if prefer_source {
+ eprintln!(
+ "{}",
+ console::warning(
+ "Warning: Source installs are not yet supported. Falling back to dist."
+ )
+ );
+ }
+
+ // Step 6: Determine dev mode and vendor directory
let dev_mode = !args.no_dev;
let vendor_dir = working_dir.join("vendor");
- // Step 6: Delegate to shared install_from_lock()
+ // Step 7: Delegate to shared install_from_lock()
install_from_lock(
&lock,
&working_dir,
&vendor_dir,
- dev_mode,
- args.dry_run,
- args.no_autoloader,
+ &InstallConfig {
+ dev_mode,
+ dry_run: args.dry_run,
+ no_autoloader: args.no_autoloader,
+ no_progress: args.no_progress,
+ ignore_platform_reqs: args.ignore_platform_reqs,
+ ignore_platform_req: args.ignore_platform_req.clone(),
+ optimize_autoloader: args.optimize_autoloader,
+ classmap_authoritative: args.classmap_authoritative,
+ },
)
}
diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs
index 350ea39..b227df8 100644
--- a/crates/mozart/src/commands/remove.rs
+++ b/crates/mozart/src/commands/remove.rs
@@ -417,9 +417,16 @@ pub fn execute(args: &RemoveArgs, cli: &super::Cli) -> anyhow::Result<()> {
&new_lock,
&working_dir,
&vendor_dir,
- dev_mode,
- false, // dry_run already handled above
- false, // no_autoloader: always generate autoloader
+ &super::install::InstallConfig {
+ dev_mode,
+ dry_run: false, // dry_run already handled above
+ no_autoloader: false, // always generate autoloader
+ no_progress: args.no_progress,
+ ignore_platform_reqs: args.ignore_platform_reqs,
+ ignore_platform_req: args.ignore_platform_req.clone(),
+ optimize_autoloader: args.optimize_autoloader,
+ classmap_authoritative: args.classmap_authoritative,
+ },
)?;
}
diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs
index ee40d54..b9ec258 100644
--- a/crates/mozart/src/commands/require.rs
+++ b/crates/mozart/src/commands/require.rs
@@ -477,13 +477,36 @@ pub fn execute(args: &RequireArgs, cli: &super::Cli) -> anyhow::Result<()> {
// Install packages (unless --no-install or --dry-run)
if !args.no_install && !args.dry_run {
+ // Warn about prefer-source (not yet supported)
+ let prefer_source = args.prefer_source
+ || args
+ .prefer_install
+ .as_deref()
+ .map(|s| s.eq_ignore_ascii_case("source"))
+ .unwrap_or(false);
+ if prefer_source {
+ eprintln!(
+ "{}",
+ crate::console::warning(
+ "Warning: Source installs are not yet supported. Falling back to dist."
+ )
+ );
+ }
+
super::install::install_from_lock(
&new_lock,
&working_dir,
&vendor_dir,
- dev_mode,
- false, // dry_run already handled above
- false, // no_autoloader: always generate autoloader
+ &super::install::InstallConfig {
+ dev_mode,
+ dry_run: false, // dry_run already handled above
+ no_autoloader: false, // always generate autoloader
+ no_progress: args.no_progress,
+ ignore_platform_reqs: args.ignore_platform_reqs,
+ ignore_platform_req: args.ignore_platform_req.clone(),
+ optimize_autoloader: args.optimize_autoloader,
+ classmap_authoritative: args.classmap_authoritative,
+ },
)?;
}
diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs
index ba21432..d58d0a9 100644
--- a/crates/mozart/src/commands/update.rs
+++ b/crates/mozart/src/commands/update.rs
@@ -573,13 +573,36 @@ pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
// Step 12: Install packages (unless --no-install or --dry-run)
if !args.no_install && !args.dry_run {
+ // Warn about prefer-source (not yet supported)
+ let prefer_source = args.prefer_source
+ || args
+ .prefer_install
+ .as_deref()
+ .map(|s| s.eq_ignore_ascii_case("source"))
+ .unwrap_or(false);
+ if prefer_source {
+ eprintln!(
+ "{}",
+ crate::console::warning(
+ "Warning: Source installs are not yet supported. Falling back to dist."
+ )
+ );
+ }
+
super::install::install_from_lock(
&new_lock,
&working_dir,
&vendor_dir,
- dev_mode,
- false, // dry_run already checked above
- args.no_autoloader,
+ &super::install::InstallConfig {
+ dev_mode,
+ dry_run: false, // dry_run already checked above
+ no_autoloader: args.no_autoloader,
+ no_progress: args.no_progress,
+ ignore_platform_reqs: args.ignore_platform_reqs,
+ ignore_platform_req: args.ignore_platform_req.clone(),
+ optimize_autoloader: args.optimize_autoloader,
+ classmap_authoritative: args.classmap_authoritative,
+ },
)?;
}
diff --git a/crates/mozart/src/downloader.rs b/crates/mozart/src/downloader.rs
index 403f4d6..e7ded03 100644
--- a/crates/mozart/src/downloader.rs
+++ b/crates/mozart/src/downloader.rs
@@ -1,13 +1,86 @@
use sha1::{Digest, Sha1};
use std::collections::HashSet;
use std::fs;
-use std::io::{Cursor, Read};
+use std::io::{Cursor, Read, Write};
use std::path::Path;
+/// A simple download progress tracker that writes to stderr.
+///
+/// When `show` is false, all methods are no-ops. This lets callers toggle
+/// progress display without branching on every call.
+pub struct DownloadProgress {
+ show: bool,
+ total: u64,
+ downloaded: u64,
+ label: String,
+}
+
+impl DownloadProgress {
+ /// Create a new progress tracker.
+ ///
+ /// - `show`: whether to actually display anything.
+ /// - `label`: a human-readable label (e.g. "psr/log (3.0.2)").
+ pub fn new(show: bool, label: impl Into<String>) -> Self {
+ Self {
+ show,
+ total: 0,
+ downloaded: 0,
+ label: label.into(),
+ }
+ }
+
+ /// Set the total expected bytes from a `Content-Length` header.
+ pub fn set_total(&mut self, total: u64) {
+ self.total = total;
+ }
+
+ /// Advance the downloaded byte count and redraw the line.
+ pub fn inc(&mut self, n: u64) {
+ if !self.show {
+ return;
+ }
+ self.downloaded += n;
+ let stderr = std::io::stderr();
+ let mut out = stderr.lock();
+ if let Some(pct) = (self.downloaded * 100).checked_div(self.total) {
+ let _ = write!(
+ out,
+ "\r Downloading {} ({}/{} bytes, {}%)",
+ self.label, self.downloaded, self.total, pct
+ );
+ } else {
+ let _ = write!(
+ out,
+ "\r Downloading {} ({} bytes)",
+ self.label, self.downloaded
+ );
+ }
+ let _ = out.flush();
+ }
+
+ /// Clear the progress line from the terminal.
+ pub fn finish(&self) {
+ if !self.show {
+ return;
+ }
+ let stderr = std::io::stderr();
+ let mut out = stderr.lock();
+ // Clear the line with spaces then return to start
+ let _ = write!(out, "\r{}\r", " ".repeat(80));
+ let _ = out.flush();
+ }
+}
+
/// Download a dist archive from a URL.
/// Returns the raw bytes of the downloaded archive.
/// If `expected_shasum` is provided and non-empty, verifies SHA-1 of the downloaded bytes.
-pub fn download_dist(url: &str, expected_shasum: Option<&str>) -> anyhow::Result<Vec<u8>> {
+/// If `progress` is provided, increments it as bytes are received and sets the total from
+/// the `Content-Length` response header.
+pub fn download_dist(
+ url: &str,
+ expected_shasum: Option<&str>,
+ progress: Option<&mut DownloadProgress>,
+) -> anyhow::Result<Vec<u8>> {
let response = reqwest::blocking::get(url)?;
if !response.status().is_success() {
@@ -18,7 +91,26 @@ pub fn download_dist(url: &str, expected_shasum: Option<&str>) -> anyhow::Result
);
}
- let bytes = response.bytes()?.to_vec();
+ // Stream the response body, updating progress as bytes arrive
+ let bytes = if let Some(pb) = progress {
+ 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);
+ }
+ buf
+ } else {
+ response.bytes()?.to_vec()
+ };
// Verify SHA-1 checksum if provided
if let Some(shasum) = expected_shasum
@@ -199,12 +291,14 @@ pub fn extract_tar_gz(data: &[u8], target_dir: &Path) -> anyhow::Result<()> {
/// - `dist_shasum`: optional SHA-1 checksum
/// - `vendor_dir`: path to `vendor/` directory
/// - `package_name`: e.g. `"monolog/monolog"`
+/// - `progress`: optional mutable progress tracker to update during download
pub fn install_package(
dist_url: &str,
dist_type: &str,
dist_shasum: Option<&str>,
vendor_dir: &Path,
package_name: &str,
+ progress: Option<&mut DownloadProgress>,
) -> anyhow::Result<()> {
let target = vendor_dir.join(package_name);
@@ -214,7 +308,7 @@ pub fn install_package(
}
fs::create_dir_all(&target)?;
- let bytes = download_dist(dist_url, dist_shasum)?;
+ let bytes = download_dist(dist_url, dist_shasum, progress)?;
match dist_type {
"zip" => extract_zip(&bytes, &target)?,