diff --git a/crate-layers.toml b/crate-layers.toml index 2f7d035a66..f1f0d03ae7 100644 --- a/crate-layers.toml +++ b/crate-layers.toml @@ -193,7 +193,7 @@ pnpm-git-fetcher = 5 pnpm-http = 2 pnpm-lockfile-verification = 2 pnpm-network-web-auth = 2 -pnpm-package-manager = 12 +pnpm-package-manager = 6 pnpm-platform-variants = 2 pnpm-pnpr-client = 2 pnpm-process = 2 diff --git a/pnpm/crates/package-manager/src/fast_update_settings.rs b/pnpm/crates/package-manager/src/fast_update_settings.rs index 435d2a874e..b0191f3015 100644 --- a/pnpm/crates/package-manager/src/fast_update_settings.rs +++ b/pnpm/crates/package-manager/src/fast_update_settings.rs @@ -40,7 +40,7 @@ pub(crate) fn resolution_settings_from_config(config: &Config) -> ResolutionSett dedupe_injected_deps: Some(config.dedupe_injected_deps), dedupe_peer_dependents: Some(config.dedupe_peer_dependents), link_workspace_packages: Some( - crate::optimistic_repeat_install::settings::link_workspace_packages_to_json( + crate::install_state::settings::link_workspace_packages_to_json( config.link_workspace_packages, ), ), diff --git a/pnpm/crates/package-manager/src/install.rs b/pnpm/crates/package-manager/src/install.rs index 4bb672e7eb..4dfb58832a 100644 --- a/pnpm/crates/package-manager/src/install.rs +++ b/pnpm/crates/package-manager/src/install.rs @@ -1,8 +1,11 @@ pub(crate) mod state_options; -pub use entry_points::apply_deploy_manifest_hook; -pub(crate) use entry_points::apply_deploy_manifest_hook_to_arc; +pub use deploy_manifest::apply_deploy_manifest_hook; +pub(crate) use deploy_manifest::apply_deploy_manifest_hook_to_arc; pub use errors::{InstallError, defer_ignored_builds}; +pub(crate) use lifecycle::{ + checked_projects_opts, load_checked_workspace_projects, load_workspace_projects, +}; pub(crate) use lockfile_freshness::{ CheckLockfileSettingsDriftOptions, DirectoryManifests, FreshnessCheckError, FreshnessScope, ImporterSatisfactionCheck, OptionalDependencyExclusions, ProjectManifestsByDir, @@ -15,25 +18,26 @@ pub use lockfile_freshness::{ }; pub(crate) use modules_state::{ frozen_tree_intact, hoisted_linker_workspace_links_intact, hoisted_workspace_packages_present, - modules_layout_consistent_with, moved_tree_is_reusable, tree_may_move, + modules_layout_consistent_with, moved_tree_is_reusable, project_requires_lifecycle_scripts, + tree_may_move, }; -pub(crate) use run::hook_one_manifest; pub use run::{InstallExecution, InstallLockfilePolicy, ResolutionInputs}; -pub(crate) use workspace_state::{ - CarriedSettings, StateProjects, StateWrite, configured_or_discovered_workspace_dir, - lockfile_root_dir, recorded_workspace_state, registry_keys_current, replace_project_manifest, - update_workspace_state_or_warn, workspace_packages_for_freshness, +pub(crate) use run::{ + check_strict_dep_builds_for_fast_path, gvs_build_markers_complete, hook_one_manifest, + register_workspace_in_store, }; -pub use workspace_state::{ - GateWorkspace, UpToDateFastPathCheck, UpToDateWorkspace, build_workspace_packages_map, - check_deps_status_before_run_at, deps_install_root, install_already_up_to_date, - install_already_up_to_date_from_records, install_already_up_to_date_per_project, +pub use workspace_state::build_workspace_packages_map; +pub(crate) use workspace_state::{ + CarriedSettings, StateProjects, StateWrite, build_project_manifests_list, + configured_or_discovered_workspace_dir, lockfile_root_dir, lockfile_root_for, + recorded_workspace_state, registry_keys_current, replace_project_manifest, + update_workspace_state_or_warn, workspace_packages_for_freshness, }; pub(crate) use verification_gate::IsReplaced; pub use verification_gate::LockfileVerificationGate; -mod entry_points; +mod deploy_manifest; mod errors; @@ -45,9 +49,9 @@ use crate::{ HoistedDependencies, InstallFrozenLockfile, InstallWithFreshLockfile, InstallWithFreshLockfileError, LockfileVerificationOverride, OptimisticRepeatInstallCheck, RebuildOptions, ResolvedPackages, UpdateSeedPolicy, build_resolution_verifiers, - emit_initial_package_manifest, link_project_bins, - optimistic_repeat_install::Decision as OptimisticRepeatInstallDecision, - prune_merged_branch_lockfile::prune_merged_branch_lockfile, report_merged_lockfile_conflicts, + emit_initial_package_manifest, install_state::Decision as OptimisticRepeatInstallDecision, + link_project_bins, prune_merged_branch_lockfile::prune_merged_branch_lockfile, + report_merged_lockfile_conflicts, }; use pnpm_catalogs_config::get_catalogs_from_workspace_manifest; use pnpm_catalogs_types::Catalogs; @@ -58,8 +62,8 @@ use pnpm_executor::{ RunPostinstallHooks, run_project_lifecycle_stages, }; use pnpm_lockfile::{ - LazyLockfile, Lockfile, LockfileEntries, MaybeLazyLockfile, PnpmfileChecksumCheck, - StalenessReason, VersionPart, satisfies_package_manifest, + Lockfile, LockfileEntries, MaybeLazyLockfile, PnpmfileChecksumCheck, StalenessReason, + VersionPart, satisfies_package_manifest, }; use pnpm_lockfile_verification::{ VerifyLockfileResolutionsOptions, record_lockfile_verified, verify_lockfile_resolutions, @@ -95,7 +99,7 @@ mod prepare_modules_state; mod time_machine; /// The dependency groups the install includes, as `.modules.yaml` records /// them and the dependency-graph walker observes them. -pub(super) fn included_dependencies(dependency_groups: &[DependencyGroup]) -> IncludedDependencies { +pub(crate) fn included_dependencies(dependency_groups: &[DependencyGroup]) -> IncludedDependencies { IncludedDependencies { dependencies: dependency_groups.contains(&DependencyGroup::Prod), dev_dependencies: dependency_groups.contains(&DependencyGroup::Dev), @@ -120,9 +124,9 @@ mod workspace_state; use apply_materialization::{ApplyMaterializationInputs, apply_materialization_result}; use lifecycle::{ - dev_preinstall_already_ran, load_workspace_projects, project_lifecycle_graph, - project_script_stages, root_preinstall_already_ran, run_pre_uninstall_scripts, - run_projects_lifecycle_scripts, run_root_hook, + dev_preinstall_already_ran, project_lifecycle_graph, project_script_stages, + root_preinstall_already_ran, run_pre_uninstall_scripts, run_projects_lifecycle_scripts, + run_root_hook, }; use lockfile_freshness::{ FastUpdateLockfileOptions, LockfileFreshnessInputs, check_lockfile_freshness, @@ -134,8 +138,8 @@ use modules_state::{ gvs_build_marker_present, gvs_build_markers_may_require_recovery, has_newly_allowed_ignored_builds, keep_recorded_layout, manifest_string_field, merge_filtered_modules_metadata, merge_pending_builds, modules_consistent_with, - project_requires_implicit_gyp_build, project_requires_lifecycle_scripts, - recorded_allow_builds_differ, refuse_drifted_modules_dir, unapproved_recorded_ignored_builds, + project_requires_implicit_gyp_build, recorded_allow_builds_differ, refuse_drifted_modules_dir, + unapproved_recorded_ignored_builds, }; use prepare_modules_state::{ PrepareModulesStateInputs, PreparedModulesState, prepare_modules_state, @@ -143,9 +147,9 @@ use prepare_modules_state::{ }; use time_machine::TimeMachineExclusions; use workspace_state::{ - ProjectScriptsInputs, build_project_manifests_list, build_root_importer_project_manifests_list, - build_selected_project_manifests_list, lockfile_root_for, mutated_project_dirs, - projects_running_own_scripts, selected_manifest_freshness_inputs, + ProjectScriptsInputs, build_root_importer_project_manifests_list, + build_selected_project_manifests_list, mutated_project_dirs, projects_running_own_scripts, + selected_manifest_freshness_inputs, }; #[cfg(test)] @@ -211,7 +215,10 @@ fn pnpmfile_checksum_check<'a>( /// manifest. The early host detection and the install paths must /// derive it identically or the pre-spawned host would disagree with /// the one the install would have detected. -fn effective_node_version(config: &Config, manifest: &PackageManifest) -> Option { +pub(crate) fn effective_node_version( + config: &Config, + manifest: &PackageManifest, +) -> Option { config.node_version.clone().or_else(|| node_version_from_engines_runtime(manifest.value())) } @@ -515,81 +522,74 @@ pub struct InstallProjects { pub discovered_workspace: Option, } -struct InstallRunOptions<'install, 'selection> { - lockfile_verification_override: Option>, - rebuild: Option, - selection: Option>, - root_manifest_as_workspace_root: bool, - save: InstallSaveOptions, - /// pnpm's `lockfileCheck`: the run hands the wanted lockfile to the sink - /// instead of writing it, and must leave nothing else on disk changed - /// either. Only `pacquet dedupe --check` sets it. - lockfile_check: Option, +pub(crate) struct InstallRunOptions<'install, 'selection> { + /// The repeat-install fast path the run consults before it sets up + /// anything. + pub(crate) repeat_install: crate::install_state::RepeatInstallCheck, + pub(crate) lockfile_verification_override: Option>, + pub(crate) rebuild: Option, + pub(crate) selection: Option>, + pub(crate) root_manifest_as_workspace_root: bool, + pub(crate) save: InstallSaveOptions, /// Forces the interactive-prompt eligibility that is otherwise derived /// from the process environment, so tests can exercise both branches. - prompt_eligibility_override: Option, - manifests: InstallManifestOptions<'install>, + pub(crate) prompt_eligibility_override: Option, + pub(crate) manifests: InstallManifestOptions<'install>, } /// The workspace files the run may write besides the installed modules. -#[derive(Clone, Copy)] -struct InstallSaveOptions { +pub(crate) struct InstallSaveOptions { /// pnpm's `saveLockfile`: whether the resolved graph may be written /// to `/pnpm-lock.yaml`. `false` for an install /// whose resolution belongs to a project other than the one that /// owns that lockfile, so the run must leave it untouched. - lockfile: bool, + pub(crate) lockfile: bool, /// Whether the run may record itself in /// `node_modules/.pnpm-workspace-state-v1.json`. `false` for an install /// whose importers are not the workspace's projects, so that state /// keeps describing the workspace's last install. - workspace_state: bool, + pub(crate) workspace_state: bool, + /// pnpm's `lockfileCheck`: the run hands the wanted lockfile to the sink + /// instead of writing it, and must leave nothing else on disk changed + /// either. Only `pacquet dedupe --check` sets it. + pub(crate) lockfile_check: Option, +} + +impl Default for InstallSaveOptions { + fn default() -> Self { + InstallSaveOptions { lockfile: true, workspace_state: true, lockfile_check: None } + } +} + +impl InstallSaveOptions { + /// Whether the run writes the wanted lockfile to disk. + fn writes_lockfile(&self) -> bool { + self.lockfile && self.lockfile_check.is_none() + } } #[derive(Default)] -struct InstallManifestOptions<'install> { - deploy_hook: bool, +pub(crate) struct InstallManifestOptions<'install> { + pub(crate) deploy_hook: bool, /// pnpm's `pruneDirectDependencies`: scan each importer's `node_modules` /// and remove extraneous direct entries not recorded in the wanted /// lockfile or public hoist set. Only `pacquet prune` sets it. - prune_direct_dependencies: bool, + pub(crate) prune_direct_dependencies: bool, /// Project manifests used only as the source for lockfile importer /// specifiers. `pacquet update --no-save` resolves against an in-memory /// manifest rewrite but must serialize importer specifiers from the /// manifest the user kept on disk. Supplied already /// `readPackage`-transformed. - specifier_manifests: Option>, + pub(crate) specifier_manifests: Option>, /// Manifest paths `pacquet update --no-save` already ran `readPackage` /// over before preparing its in-memory resolution rewrite. The hook must /// observe each project manifest exactly once, so the install layer skips /// these and still hooks every project manifest outside the set — the /// workspace projects the non-selected update path never loads. Dependency /// manifests always flow through the resolver's hook path. - hooked_paths: HashSet, + pub(crate) hooked_paths: HashSet, /// See [`crate::ManifestSpecBumps`]. Only `pacquet update` sets it. - spec_bumps: Option<&'install crate::ManifestSpecBumps>, + pub(crate) spec_bumps: Option<&'install crate::ManifestSpecBumps>, /// Pre-seeded `requiresBuild` verdicts from a `pnpr` resolve stream. - known_requires_build: Option, -} - -impl Default for InstallRunOptions<'_, '_> { - fn default() -> Self { - InstallRunOptions { - lockfile_verification_override: None, - rebuild: None, - selection: None, - root_manifest_as_workspace_root: false, - save: InstallSaveOptions { lockfile: true, workspace_state: true }, - lockfile_check: None, - prompt_eligibility_override: None, - manifests: crate::install::InstallManifestOptions { - deploy_hook: false, - prune_direct_dependencies: false, - specifier_manifests: None, - hooked_paths: HashSet::new(), - spec_bumps: None, - known_requires_build: None, - }, - } - } + pub(crate) known_requires_build: Option, } diff --git a/pnpm/crates/package-manager/src/install/apply_materialization.rs b/pnpm/crates/package-manager/src/install/apply_materialization.rs index 360716e0c8..f191c784df 100644 --- a/pnpm/crates/package-manager/src/install/apply_materialization.rs +++ b/pnpm/crates/package-manager/src/install/apply_materialization.rs @@ -18,9 +18,7 @@ use super::{ BTreeMap, CarriedSettings, HoistedDependencies, InstallError, Lockfile, Materialized, Reporter, StateWrite, recorded_workspace_state, update_workspace_state_or_warn, }; -use crate::optimistic_repeat_install::{ - ResolvedImporters, filesystem_now_ms_in_modules, validation::Vouched, -}; +use crate::install_state::{ResolvedImporters, filesystem_now_ms_in_modules, validation::Vouched}; use std::{collections::HashSet, path::PathBuf}; pub(super) struct ApplyMaterializationInputs<'a, 'selection> { @@ -350,14 +348,12 @@ fn applied_carried_settings<'a>( CarriedSettings { established_auto_dedupe: (config.auto_dedupe && resolved.is_some()).then_some(true), registry_keys_verified: crate::install::registry_keys_current(config, lockfile), - registry_routes: Some( - crate::optimistic_repeat_install::recorded_registry_routes_after_install( - config, - inputs.resolution.locked_registry_routes.as_ref(), - lockfile, - resolution, - ), - ), + registry_routes: Some(crate::install_state::recorded_registry_routes_after_install( + config, + inputs.resolution.locked_registry_routes.as_ref(), + lockfile, + resolution, + )), kept_settings: None, } } diff --git a/pnpm/crates/package-manager/src/install/deploy_manifest.rs b/pnpm/crates/package-manager/src/install/deploy_manifest.rs new file mode 100644 index 0000000000..9d80d23688 --- /dev/null +++ b/pnpm/crates/package-manager/src/install/deploy_manifest.rs @@ -0,0 +1,75 @@ +use std::sync::Arc; + +pub fn apply_deploy_manifest_hook(manifest: &mut serde_json::Value) { + let names = + deploy_dependency_names(manifest, "workspace:").map(str::to_owned).collect::>(); + inject_deploy_dependencies_meta(manifest, names); + copy_deploy_linked_dependencies(manifest); +} +pub(crate) fn apply_deploy_manifest_hook_to_arc( + mut manifest: Arc, +) -> Arc { + if deploy_dependency_names(&manifest, "workspace:").next().is_none() + && deploy_dependency_names(&manifest, "link:").next().is_none() + { + return manifest; + } + apply_deploy_manifest_hook(Arc::make_mut(&mut manifest)); + manifest +} +fn deploy_dependency_names<'a>( + manifest: &'a serde_json::Value, + protocol: &'static str, +) -> impl Iterator { + ["optionalDependencies", "dependencies", "devDependencies"] + .into_iter() + .filter_map(move |field| manifest.get(field)?.as_object()) + .flat_map(|dependencies| dependencies.iter()) + .filter_map(move |(name, specifier)| { + specifier + .as_str() + .is_some_and(|specifier| specifier.starts_with(protocol)) + .then_some(name.as_str()) + }) +} + +fn copy_deploy_linked_dependencies(manifest: &mut serde_json::Value) { + for field in ["optionalDependencies", "dependencies", "devDependencies"] { + let Some(dependencies) = manifest.get_mut(field).and_then(serde_json::Value::as_object_mut) + else { + continue; + }; + for specifier in dependencies.values_mut() { + let Some(directory) = specifier + .as_str() + .and_then(|value| value.strip_prefix("link:")) + else { + continue; + }; + *specifier = serde_json::Value::String(format!("file:{directory}")); + } + } +} + +pub(super) fn inject_deploy_dependencies_meta( + manifest: &mut serde_json::Value, + names: Vec, +) { + if names.is_empty() { + return; + } + let Some(object) = manifest.as_object_mut() else { return }; + let dependencies_meta = object + .entry("dependenciesMeta") + .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new())); + let Some(meta_object) = dependencies_meta.as_object_mut() else { return }; + for name in names { + let dependency_meta = meta_object.entry(name).or_insert(serde_json::Value::Null); + match dependency_meta { + serde_json::Value::Object(object) => { + object.insert("injected".to_owned(), serde_json::Value::Bool(true)); + } + value => *value = serde_json::json!({ "injected": true }), + } + } +} diff --git a/pnpm/crates/package-manager/src/install/lifecycle.rs b/pnpm/crates/package-manager/src/install/lifecycle.rs index 616ab50cb4..9e330bb86d 100644 --- a/pnpm/crates/package-manager/src/install/lifecycle.rs +++ b/pnpm/crates/package-manager/src/install/lifecycle.rs @@ -22,7 +22,7 @@ use std::sync::Mutex; /// — the install isn't a workspace install, so the caller should use /// the top-level `Install.manifest` as its only importer and pass /// `None` for the `workspace:`-spec lookup. -pub(super) fn load_workspace_projects( +pub(crate) fn load_workspace_projects( workspace_root: &std::path::Path, workspace_manifest: Option<&pnpm_workspace::WorkspaceManifest>, ignored_directories: &[std::path::PathBuf], @@ -50,7 +50,7 @@ pub(super) fn workspace_projects_opts( /// [`load_workspace_projects`] for the repeat-install checks, which read the /// projects the install itself would read: the `--workspace-packages` /// patterns when given, else the ones `workspace_manifest` declares. -pub(super) fn load_checked_workspace_projects( +pub(crate) fn load_checked_workspace_projects( config: &Config, workspace_root: &std::path::Path, workspace_manifest: Option<&pnpm_workspace::WorkspaceManifest>, @@ -64,7 +64,7 @@ pub(super) fn load_checked_workspace_projects( } /// The discovery options of [`load_checked_workspace_projects`]. -pub(super) fn checked_projects_opts( +pub(crate) fn checked_projects_opts( config: &Config, workspace_manifest: &pnpm_workspace::WorkspaceManifest, ) -> pnpm_workspace::FindWorkspaceProjectsOpts { diff --git a/pnpm/crates/package-manager/src/install/lockfile_freshness/directory_deps.rs b/pnpm/crates/package-manager/src/install/lockfile_freshness/directory_deps.rs index 8b602c3aec..37be9a3586 100644 --- a/pnpm/crates/package-manager/src/install/lockfile_freshness/directory_deps.rs +++ b/pnpm/crates/package-manager/src/install/lockfile_freshness/directory_deps.rs @@ -1,5 +1,5 @@ +pub(crate) use crate::install_state::ProjectManifestsByDir; pub(crate) use importer_deps::check_directory_dependencies_freshness; -pub use injected_manifest::ProjectManifestsByDir; pub(super) use injected_manifest::{InjectedDirectory, is_deploy_file_url_key, unhooked_manifest}; pub(crate) use injected_manifest::{dependency_manifests_by_dir, project_manifests_by_dir}; pub(crate) use nested::{ diff --git a/pnpm/crates/package-manager/src/install/lockfile_freshness/directory_deps/injected_manifest.rs b/pnpm/crates/package-manager/src/install/lockfile_freshness/directory_deps/injected_manifest.rs index e4a1a1ffbd..99fd404ce1 100644 --- a/pnpm/crates/package-manager/src/install/lockfile_freshness/directory_deps/injected_manifest.rs +++ b/pnpm/crates/package-manager/src/install/lockfile_freshness/directory_deps/injected_manifest.rs @@ -1,13 +1,10 @@ +use crate::install_state::ProjectManifestsByDir; use pnpm_package_manifest::PackageManifest; use std::{ borrow::Cow, - collections::HashMap, path::{Path, PathBuf}, }; -/// Project manifests keyed by their lexically normalized project directory. -pub type ProjectManifestsByDir<'a> = HashMap; - /// Each project's manifest as another importer's injected dependency reads /// it: the project's entry in `dependency_manifests` when it has one, else /// its importer manifest. diff --git a/pnpm/crates/package-manager/src/install/modules_state.rs b/pnpm/crates/package-manager/src/install/modules_state.rs index a4603d7f4d..ee5af88a98 100644 --- a/pnpm/crates/package-manager/src/install/modules_state.rs +++ b/pnpm/crates/package-manager/src/install/modules_state.rs @@ -494,7 +494,7 @@ where /// Whether the project defines any of `stages`, counting the executor's /// implicit `node-gyp rebuild` fallback for `install` (which is skipped /// when the project opts out with `gypfile: false`). -pub(super) fn project_requires_lifecycle_scripts( +pub(crate) fn project_requires_lifecycle_scripts( project_dir: &Path, manifest: &PackageManifest, stages: &[&str], diff --git a/pnpm/crates/package-manager/src/install/prepare_modules_state.rs b/pnpm/crates/package-manager/src/install/prepare_modules_state.rs index d8b31be7c0..640e30af02 100644 --- a/pnpm/crates/package-manager/src/install/prepare_modules_state.rs +++ b/pnpm/crates/package-manager/src/install/prepare_modules_state.rs @@ -14,9 +14,7 @@ use super::{ Config, Host, InstallError, Lockfile, Modules, NodeLinker, PackageManifest, PathBuf, Reporter, WorkspaceState, modules_layout_consistent_with, tree_may_move, }; -use crate::{ - install::state_options::RecordedWorkspace, optimistic_repeat_install::recorded_elsewhere, -}; +use crate::{install::state_options::RecordedWorkspace, install_state::recorded_elsewhere}; use pnpm_workspace_state::load_workspace_state_in_modules; pub(super) struct PrepareModulesStateInputs<'a, 'install> { diff --git a/pnpm/crates/package-manager/src/install/prepare_modules_state/up_to_date.rs b/pnpm/crates/package-manager/src/install/prepare_modules_state/up_to_date.rs index ae9a789260..3c30a9474e 100644 --- a/pnpm/crates/package-manager/src/install/prepare_modules_state/up_to_date.rs +++ b/pnpm/crates/package-manager/src/install/prepare_modules_state/up_to_date.rs @@ -6,7 +6,7 @@ use super::super::{ moved_tree_is_reusable, recorded_allow_builds_differ, unapproved_recorded_ignored_builds, update_workspace_state_or_warn, verify_lockfile_eagerly, }; -use crate::optimistic_repeat_install::{ +use crate::install_state::{ filesystem_now_ms_in_modules, materialized_shape_matches, validation::{project_bins_digest, project_bins_match, workspace_bins_digest}, }; @@ -76,11 +76,11 @@ pub(super) fn frozen_tree_up_to_date<'a>( // `extendNodePath` change the bin shims, without touching the lockfile or // `.modules.yaml`, so the unchanged-layout premise doesn't hold. if !modules_consistent_with(modules, config, context.tree.node_linker, context.tree.included) - || !crate::optimistic_repeat_install::recorded_supported_architectures_match( + || !crate::install_state::recorded_supported_architectures_match( context.recorded.state, context.repeat.supported_architectures, ) - || !crate::optimistic_repeat_install::recorded_extend_node_path_matches( + || !crate::install_state::recorded_extend_node_path_matches( context.recorded.state, config.extend_node_path, ) @@ -297,17 +297,15 @@ pub(super) fn refresh_up_to_date_workspace( context.tree.config, Some(context.wanted_lockfile), ), - registry_routes: Some( - crate::optimistic_repeat_install::recorded_registry_routes_after_install( - context.tree.config, - context.carried_state.recorded_registry_routes.as_ref(), - Some(context.wanted_lockfile), - crate::optimistic_repeat_install::ResolvedImporters::None, - ), - ), + registry_routes: Some(crate::install_state::recorded_registry_routes_after_install( + context.tree.config, + context.carried_state.recorded_registry_routes.as_ref(), + Some(context.wanted_lockfile), + crate::install_state::ResolvedImporters::None, + )), kept_settings: None, }, - vouched: &crate::optimistic_repeat_install::validation::Vouched::All, + vouched: &crate::install_state::validation::Vouched::All, }); update_workspace_state_or_warn::( context.tree.workspace_root, diff --git a/pnpm/crates/package-manager/src/install/run.rs b/pnpm/crates/package-manager/src/install/run.rs index 26bc1eac2f..91bdb5f107 100644 --- a/pnpm/crates/package-manager/src/install/run.rs +++ b/pnpm/crates/package-manager/src/install/run.rs @@ -1,4 +1,4 @@ -pub(super) use fast_path::{ +pub(crate) use fast_path::{ check_strict_dep_builds_for_fast_path, gvs_build_markers_complete, register_workspace_in_store, }; pub(crate) use manifests::hook_one_manifest; @@ -70,7 +70,7 @@ where /// Both live out here because every success path of /// [`Self::run_inner_impl`] — including the short-circuits that do /// nothing but rewrite the lockfile — has to run them. - pub(super) async fn run_inner( + pub(crate) async fn run_inner( self, options: InstallRunOptions<'a, '_>, ) -> Result<(), InstallError> { @@ -83,8 +83,7 @@ where // resolutions no file is left holding. let merge_will_be_saved = self.context.config.merge_git_branch_lockfiles && self.context.config.lockfile - && options.save.lockfile - && options.lockfile_check.is_none() + && options.save.writes_lockfile() && !self.execution.dry_run; let branch_lockfiles_to_clean = merge_will_be_saved .then(|| { @@ -144,8 +143,7 @@ where self.lockfile_policy.excludes == PolicyExcludes::Persist && matches!(self.execution.mutation, ProjectMutation::InstallWorkspace) && self.context.config.lockfile - && options.save.lockfile - && options.lockfile_check.is_none() + && options.save.writes_lockfile() && !self.execution.dry_run && (self.context.config.minimum_release_age_exclude_prune || self.context.config.trust_policy_exclude_prune) @@ -256,8 +254,7 @@ pub(super) struct InstallView<'a> { impl InstallView<'_> { fn should_prune_catalogs(&self, options: &InstallRunOptions<'_, '_>) -> bool { self.context.config.catalog_prune - && options.save.lockfile - && options.lockfile_check.is_none() + && options.save.writes_lockfile() && !self.execution.dry_run && self.execution.mutation.is_full_install() } diff --git a/pnpm/crates/package-manager/src/install/run/auto_dedupe.rs b/pnpm/crates/package-manager/src/install/run/auto_dedupe.rs index c93a9be616..d367f02a92 100644 --- a/pnpm/crates/package-manager/src/install/run/auto_dedupe.rs +++ b/pnpm/crates/package-manager/src/install/run/auto_dedupe.rs @@ -6,7 +6,7 @@ use super::{ InstallScope, RunExecution, settled::Settled, }; -use crate::{ProjectMutation, optimistic_repeat_install::current_settings_with_catalogs}; +use crate::{ProjectMutation, install_state::current_settings_with_catalogs}; impl RunExecution<'_> { pub(super) fn wanted_lockfile_path(&self) -> PathBuf { diff --git a/pnpm/crates/package-manager/src/install/run/dispatch.rs b/pnpm/crates/package-manager/src/install/run/dispatch.rs index ca2894b88f..8761b8afcb 100644 --- a/pnpm/crates/package-manager/src/install/run/dispatch.rs +++ b/pnpm/crates/package-manager/src/install/run/dispatch.rs @@ -342,7 +342,7 @@ async fn frozen_path( fn no_project_has_dependencies(manifests: &[(String, &PackageManifest)]) -> bool { manifests .iter() - .all(|(_, manifest)| !crate::optimistic_repeat_install::manifest_has_runtime_deps(manifest)) + .all(|(_, manifest)| !crate::install_state::manifest_has_runtime_deps(manifest)) } fn report_unresolved_optional_dependencies( skipped: &[UnresolvedOptionalDependency], diff --git a/pnpm/crates/package-manager/src/install/run/execution.rs b/pnpm/crates/package-manager/src/install/run/execution.rs index e2eaa60b16..2e2b0d5e5f 100644 --- a/pnpm/crates/package-manager/src/install/run/execution.rs +++ b/pnpm/crates/package-manager/src/install/run/execution.rs @@ -286,7 +286,7 @@ impl<'a> RunExecution<'a> { deploy_manifest_hook: self.options.manifests.deploy_hook, manifest_spec_bumps: self.options.manifests.spec_bumps, known_requires_build: self.options.manifests.known_requires_build.take(), - lockfile_check_sink: self.options.lockfile_check.take(), + lockfile_check_sink: self.options.save.lockfile_check.take(), inputs: std::mem::take(&mut self.owned.resolution), } } diff --git a/pnpm/crates/package-manager/src/install/run/fast_path.rs b/pnpm/crates/package-manager/src/install/run/fast_path.rs index bab6630aa0..8dfe7e609b 100644 --- a/pnpm/crates/package-manager/src/install/run/fast_path.rs +++ b/pnpm/crates/package-manager/src/install/run/fast_path.rs @@ -4,6 +4,7 @@ use super::super::{ gvs_build_marker_present, gvs_build_markers_may_require_recovery, unapproved_recorded_ignored_builds, }; +use crate::install_state::RepeatInstallCheck; use pnpm_config::Config; use pnpm_store_dir::StoreOperationLock; use std::path::Path; @@ -25,7 +26,7 @@ pub(super) fn lock_store(config: &Config) -> Result { pub(super) workspace: OptimisticRepeatInstallCheck<'a>, /// The projects the install covers. - pub(super) scope: crate::optimistic_repeat_install::Scope<'a>, + pub(super) scope: crate::install_state::Scope<'a>, pub(super) mutation: crate::ProjectMutation, pub(super) update_seed_policy: &'a UpdateSeedPolicy, pub(super) frozen_lockfile: bool, @@ -59,6 +60,7 @@ pub(super) struct UpToDateCheck<'a> { /// filtered install would materialize. pub(super) fn repeat_install_verdict( check: &UpToDateCheck<'_>, + repeat_install: RepeatInstallCheck, ) -> Result { if !check.resolve_only { register_workspace_in_store(check.workspace.config, check.workspace.workspace_root)?; @@ -71,10 +73,7 @@ pub(super) fn repeat_install_verdict( return Ok(RepeatInstallVerdict::Changed); } if let OptimisticRepeatInstallDecision::Skipped { reason } = - crate::optimistic_repeat_install::check_optimistic_repeat_install_in( - &check.workspace, - check.scope, - ) + repeat_install(&check.workspace, check.scope) { tracing::debug!( target: "pacquet::install", @@ -140,7 +139,7 @@ pub(super) fn build_state_allows_short_circuit( /// `Err` names what stops the fast path: a build marker the full install has /// to recover, or a wanted lockfile it cannot read to look for one. -pub(in crate::install) fn gvs_build_markers_complete( +pub(crate) fn gvs_build_markers_complete( config: &Config, lockfile: pnpm_lockfile::MaybeLazyLockfile<'_>, lockfile_root: &Path, @@ -163,9 +162,7 @@ pub(in crate::install) fn gvs_build_markers_complete( } /// Whether `strictDepBuilds` allows the fast path to stand. -pub(in crate::install) fn check_strict_dep_builds_for_fast_path( - config: &Config, -) -> Result { +pub(crate) fn check_strict_dep_builds_for_fast_path(config: &Config) -> Result { if !config.strict_dep_builds { return Ok(true); } @@ -288,8 +285,13 @@ impl super::RunExecution<'_> { ) -> Result { let embedder_hooks = self.owned.projects.pnpmfile_hook_override.clone(); self.check_custom_fetcher_reuse().await?; - let verdict = - scope.repeat_install_verdict(self.install, &self.owned, &self.mode, &self.workspace)?; + let verdict = scope.repeat_install_verdict( + self.install, + &self.owned, + &self.mode, + &self.workspace, + self.options.repeat_install, + )?; let changed = match verdict { RepeatInstallVerdict::Changed => true, RepeatInstallVerdict::Unchanged | RepeatInstallVerdict::UnchangedFrozen => false, diff --git a/pnpm/crates/package-manager/src/install/run/frozen_local_tarballs.rs b/pnpm/crates/package-manager/src/install/run/frozen_local_tarballs.rs index 3d850a0f55..01bb250a7e 100644 --- a/pnpm/crates/package-manager/src/install/run/frozen_local_tarballs.rs +++ b/pnpm/crates/package-manager/src/install/run/frozen_local_tarballs.rs @@ -102,8 +102,8 @@ async fn verify_frozen_tarballs(settled: Settled<'_, '_>) -> Result<(), InstallE let importer_ids = installed_importer_ids(settled, lockfile); let (skipped, groups) = compute_frozen_skip_set(&settled, lockfile, &importer_ids).await?; - let targets = crate::optimistic_repeat_install::frozen_local_tarballs_to_verify( - &crate::optimistic_repeat_install::FrozenLocalTarballCheck { + let targets = crate::install_state::frozen_local_tarballs_to_verify( + &crate::install_state::FrozenLocalTarballCheck { workspace_root: &settled.projects.workspace.dirs.workspace_root, importer_ids: &importer_ids, groups: &groups, diff --git a/pnpm/crates/package-manager/src/install/run/workspace.rs b/pnpm/crates/package-manager/src/install/run/workspace.rs index 9307d68305..f0cecc5bf2 100644 --- a/pnpm/crates/package-manager/src/install/run/workspace.rs +++ b/pnpm/crates/package-manager/src/install/run/workspace.rs @@ -233,7 +233,7 @@ impl<'w> InstallScope<'w> { workspace_projects_are_overridden, config: install.context.config, }); - let bin_projects = crate::optimistic_repeat_install::validation::dedicated_bin_projects( + let bin_projects = crate::install_state::validation::dedicated_bin_projects( install.context.config, &project_manifests, workspace_projects, @@ -264,49 +264,62 @@ impl<'w> InstallScope<'w> { Some(dirs) } + /// The manifests the repeat-install check reads besides the importers'. + fn repeat_install_manifests<'s>( + &'s self, + owned: &'s InstallOwned, + ) -> crate::RepeatInstallManifests<'s> { + crate::RepeatInstallManifests { + dependency_manifests: self.dependency_manifests.as_ref(), + workspace_projects: self.workspace_projects.as_deref(), + hooks: owned.projects.pnpmfile_hook_override + .as_ref() + .map_or(crate::ManifestHooks::Pnpmfiles, crate::ManifestHooks::InMemory), + } + } + pub(super) fn repeat_install_verdict( &self, install: InstallView<'_>, owned: &InstallOwned, mode: &RunMode, workspace: &InstallWorkspace<'_>, + repeat_install: crate::install_state::RepeatInstallCheck, ) -> Result { let selected_dirs = self.requested_project_dirs(&workspace.dirs.workspace_root); - repeat_install_verdict(&UpToDateCheck { - scope: selected_dirs - .as_deref() - .map_or(crate::optimistic_repeat_install::Scope::Workspace, |selected| { - crate::optimistic_repeat_install::Scope::Selected(selected) - }), - workspace: super::super::OptimisticRepeatInstallCheck { - config: install.context.config, - workspace_root: &workspace.dirs.workspace_root, - project_manifests: self.declared_manifests(), - is_workspace_install: workspace.workspace_manifest.is_some(), - lockfile: install.context.lockfile, - catalogs: &workspace.catalogs, - layout: crate::RepeatInstallLayout { - node_linker: install.execution.node_linker, - included: mode.included, - supported_architectures: owned.projects - .supported_architectures - .as_ref(), - }, - manifests: crate::RepeatInstallManifests { - dependency_manifests: self.dependency_manifests.as_ref(), - workspace_projects: self.workspace_projects.as_deref(), - hooks: owned.projects.pnpmfile_hook_override - .as_ref() - .map_or(crate::ManifestHooks::Pnpmfiles, crate::ManifestHooks::InMemory), + repeat_install_verdict( + &UpToDateCheck { + scope: selected_dirs + .as_deref() + .map_or(crate::install_state::Scope::Workspace, |selected| { + crate::install_state::Scope::Selected(selected) + }), + workspace: super::super::OptimisticRepeatInstallCheck { + config: install.context.config, + workspace_root: &workspace.dirs.workspace_root, + project_manifests: self.declared_manifests(), + is_workspace_install: workspace.workspace_manifest.is_some(), + lockfile: install.context.lockfile, + catalogs: &workspace.catalogs, + layout: crate::RepeatInstallLayout { + node_linker: install.execution.node_linker, + included: mode.included, + supported_architectures: owned.projects + .supported_architectures + .as_ref(), + }, + manifests: self.repeat_install_manifests(owned), }, + mutation: install.execution.mutation, + update_seed_policy: &owned.resolution.update_seed_policy, + frozen_lockfile: install.lockfile_policy.frozen, + resolve_only: mode.resolve_only, + disable_optimistic_repeat_install: install.lockfile_policy + .disable_optimistic_repeat, + effective_node_version: mode.effective_node_version.as_deref(), }, - mutation: install.execution.mutation, - update_seed_policy: &owned.resolution.update_seed_policy, - frozen_lockfile: install.lockfile_policy.frozen, - resolve_only: mode.resolve_only, - disable_optimistic_repeat_install: install.lockfile_policy.disable_optimistic_repeat, - effective_node_version: mode.effective_node_version.as_deref(), - }) + repeat_install, + ) } } /// Whether anything could declare a catalog this install has to resolve diff --git a/pnpm/crates/package-manager/src/install/tests/build_workspace_state_tests.rs b/pnpm/crates/package-manager/src/install/tests/build_workspace_state_tests.rs index 39ac22cbd9..566bd7b82e 100644 --- a/pnpm/crates/package-manager/src/install/tests/build_workspace_state_tests.rs +++ b/pnpm/crates/package-manager/src/install/tests/build_workspace_state_tests.rs @@ -222,14 +222,12 @@ fn corp_lockfile() -> pnpm_lockfile::Lockfile { /// leaves a plain key under a route that moved since. #[test] fn recorded_registry_routes_follow_the_install_that_resolved_the_plain_keys() { - use crate::optimistic_repeat_install::{ - ResolvedImporters, recorded_registry_routes_after_install, - }; + use crate::install_state::{ResolvedImporters, recorded_registry_routes_after_install}; let old = "https://old.example.test/"; let new = "https://new.example.test/"; let mut config = Config::new(); config.registries_by_scope.insert("@corp".to_string(), new.to_string()); - let current = crate::optimistic_repeat_install::settings::recorded_registry_routes(&config); + let current = crate::install_state::settings::recorded_registry_routes(&config); let mut previous = current.clone(); previous.insert("@corp".to_string(), old.to_string()); let lockfile = corp_lockfile(); diff --git a/pnpm/crates/package-manager/src/install/tests/frozen_dispatch.rs b/pnpm/crates/package-manager/src/install/tests/frozen_dispatch.rs index 200e9298e2..eafea4026c 100644 --- a/pnpm/crates/package-manager/src/install/tests/frozen_dispatch.rs +++ b/pnpm/crates/package-manager/src/install/tests/frozen_dispatch.rs @@ -959,14 +959,14 @@ async fn frozen_install_short_circuits_when_modules_and_lockfile_are_consistent( let written = load_workspace_state(&dirs.project_root) .expect("read workspace state") .expect("workspace state must be written"); - let manifest_mtime = crate::optimistic_repeat_install::file_mtime(manifest.path()) - .expect("manifest should have an mtime"); + let manifest_mtime = + crate::install_state::file_mtime(manifest.path()).expect("manifest should have an mtime"); assert_eq!( manifest_mtime.ns, 1_700_000_000_000_500_000, "the short-circuit must leave the manifest untouched", ); assert!( - !crate::optimistic_repeat_install::modified_at_or_after( + !crate::install_state::modified_at_or_after( manifest_mtime, written.last_validated_timestamp, ), diff --git a/pnpm/crates/package-manager/src/install/tests/frozen_policy.rs b/pnpm/crates/package-manager/src/install/tests/frozen_policy.rs index 9c49ba694a..0c3edede9b 100644 --- a/pnpm/crates/package-manager/src/install/tests/frozen_policy.rs +++ b/pnpm/crates/package-manager/src/install/tests/frozen_policy.rs @@ -99,7 +99,7 @@ async fn frozen_lockfile_disables_optimistic_short_circuit() { has_modules_dir: false, }, ); - let settings = crate::optimistic_repeat_install::settings::current_settings( + let settings = crate::install_state::settings::current_settings( config, pnpm_config::NodeLinker::Isolated, included, diff --git a/pnpm/crates/package-manager/src/install/tests/lockfile_recovery.rs b/pnpm/crates/package-manager/src/install/tests/lockfile_recovery.rs index 3906e4abb2..59663161d9 100644 --- a/pnpm/crates/package-manager/src/install/tests/lockfile_recovery.rs +++ b/pnpm/crates/package-manager/src/install/tests/lockfile_recovery.rs @@ -1,12 +1,10 @@ use super::{ - super::{ - Install, InstallError, ProjectMutation, UpToDateFastPathCheck, install_already_up_to_date, - }, + super::{Install, InstallError, ProjectMutation}, InstallDirs, PARTIAL_INSTALL_LOCKFILE, assert_package_absent, assert_package_present, fresh_lockfile_only_with_overrides, install_then_go_offline, install_with_pnpmfile, touch_manifest, }; -use crate::PolicyExcludes; +use crate::{PolicyExcludes, UpToDateFastPathCheck, install_already_up_to_date}; use pnpm_config::Config; use pnpm_lockfile::{Lockfile, MaybeLazyLockfile}; use pnpm_modules_yaml::{Host, LayoutVersion, Modules, NodeLinker, write_modules_manifest}; @@ -198,7 +196,7 @@ fn sync_fast_path_reads_the_workspace_root_wanted_lockfile_from_a_member() { filtered_install: false, config_dependencies: None, validation: None, - settings: crate::optimistic_repeat_install::settings::current_settings( + settings: crate::install_state::settings::current_settings( config, pnpm_config::NodeLinker::Isolated, included, @@ -330,7 +328,7 @@ pub(super) async fn optimistic_repeat_install_does_not_short_circuit_when_lockfi has_modules_dir: false, }, ); - let settings = crate::optimistic_repeat_install::settings::current_settings( + let settings = crate::install_state::settings::current_settings( config, pnpm_config::NodeLinker::Isolated, included, diff --git a/pnpm/crates/package-manager/src/install/tests/repeat_install.rs b/pnpm/crates/package-manager/src/install/tests/repeat_install.rs index 829ede2ee0..4bf8303e7f 100644 --- a/pnpm/crates/package-manager/src/install/tests/repeat_install.rs +++ b/pnpm/crates/package-manager/src/install/tests/repeat_install.rs @@ -1,11 +1,9 @@ use super::{ - super::{ - Install, InstallError, ProjectMutation, UpToDateFastPathCheck, install_already_up_to_date, - }, + super::{Install, InstallError, ProjectMutation}, InstallDirs, assert_purge_diagnostic, fresh_lockfile_only_with_compatibility_db, install_then_go_offline, recorded_verified_file_integrity_report, touch_manifest, }; -use crate::PolicyExcludes; +use crate::{PolicyExcludes, UpToDateFastPathCheck, install_already_up_to_date}; use pnpm_config::Config; use pnpm_lockfile::{Lockfile, MaybeLazyLockfile}; use pnpm_modules_yaml::{Host, LayoutVersion, Modules, NodeLinker, write_modules_manifest}; @@ -128,12 +126,10 @@ async fn optimistic_repeat_install_skips_entire_pipeline_when_state_is_fresh() { carried: crate::install::CarriedSettings { established_auto_dedupe: None, registry_keys_verified: true, - registry_routes: Some( - crate::optimistic_repeat_install::settings::recorded_registry_routes(config), - ), + registry_routes: Some(crate::install_state::settings::recorded_registry_routes(config)), kept_settings: None, }, - vouched: &crate::optimistic_repeat_install::validation::Vouched::All, + vouched: &crate::install_state::validation::Vouched::All, }); workspace_state::update_workspace_state(&dirs.project_root, &seed_state) .expect("seed workspace state"); @@ -276,12 +272,10 @@ fn sync_fast_path_matches_optimistic_short_circuit() { carried: crate::install::CarriedSettings { established_auto_dedupe: None, registry_keys_verified: true, - registry_routes: Some( - crate::optimistic_repeat_install::settings::recorded_registry_routes(config), - ), + registry_routes: Some(crate::install_state::settings::recorded_registry_routes(config)), kept_settings: None, }, - vouched: &crate::optimistic_repeat_install::validation::Vouched::All, + vouched: &crate::install_state::validation::Vouched::All, }); workspace_state::update_workspace_state(&project_root, &seed_state) .expect("seed workspace state"); @@ -430,12 +424,10 @@ async fn partial_install_disables_optimistic_short_circuit() { carried: crate::install::CarriedSettings { established_auto_dedupe: None, registry_keys_verified: true, - registry_routes: Some( - crate::optimistic_repeat_install::settings::recorded_registry_routes(config), - ), + registry_routes: Some(crate::install_state::settings::recorded_registry_routes(config)), kept_settings: None, }, - vouched: &crate::optimistic_repeat_install::validation::Vouched::All, + vouched: &crate::install_state::validation::Vouched::All, }); workspace_state::update_workspace_state(&dirs.project_root, &seed_state) .expect("seed workspace state"); diff --git a/pnpm/crates/package-manager/src/install/tests/workspace.rs b/pnpm/crates/package-manager/src/install/tests/workspace.rs index 447a8b7e8d..fcf4030152 100644 --- a/pnpm/crates/package-manager/src/install/tests/workspace.rs +++ b/pnpm/crates/package-manager/src/install/tests/workspace.rs @@ -308,13 +308,10 @@ async fn install_writes_workspace_state() { "lastValidatedTimestamp should be populated, got {}", state.last_validated_timestamp, ); - let manifest_mtime = crate::optimistic_repeat_install::file_mtime(manifest.path()) - .expect("manifest should have an mtime"); + let manifest_mtime = + crate::install_state::file_mtime(manifest.path()).expect("manifest should have an mtime"); assert!( - !crate::optimistic_repeat_install::modified_at_or_after( - manifest_mtime, - state.last_validated_timestamp, - ), + !crate::install_state::modified_at_or_after(manifest_mtime, state.last_validated_timestamp,), "fresh install state should cover the validated manifest mtime", ); diff --git a/pnpm/crates/package-manager/src/install/workspace_state.rs b/pnpm/crates/package-manager/src/install/workspace_state.rs index a7d3a0e51a..5fe4111239 100644 --- a/pnpm/crates/package-manager/src/install/workspace_state.rs +++ b/pnpm/crates/package-manager/src/install/workspace_state.rs @@ -1,38 +1,22 @@ -pub use discovery::{GateWorkspace, check_deps_status_before_run_at, deps_install_root}; -pub use per_project::install_already_up_to_date_per_project; -pub(crate) use projects::replace_project_manifest; pub(super) use projects::{ - ProjectScriptsInputs, build_project_manifests_list, build_root_importer_project_manifests_list, + ProjectScriptsInputs, build_root_importer_project_manifests_list, build_selected_project_manifests_list, mutated_project_dirs, projects_running_own_scripts, selected_manifest_freshness_inputs, }; +pub(crate) use projects::{build_project_manifests_list, replace_project_manifest}; pub(crate) use write::{CarriedSettings, StateProjects, StateWrite, recorded_workspace_state}; mod projects; -mod discovery; - -mod from_files; - -mod per_project; - -mod stand_ins; - -mod nothing_to_install; - mod write; use super::{ - BTreeMap, Catalogs, Clock, Config, DependencyGroup, IncludedDependencies, LazyLockfile, - MaybeLazyLockfile, NodeLinker, OptimisticRepeatInstallCheck, OptimisticRepeatInstallDecision, - PackageManifest, Path, PathBuf, ProjectEntry, WorkspaceState, manifest_string_field, -}; -use crate::optimistic_repeat_install::{ - Scope, check_optimistic_repeat_install_with_state, config_blocks_fast_path, - last_validated_timestamp, + BTreeMap, Catalogs, Clock, Config, IncludedDependencies, NodeLinker, PackageManifest, Path, + PathBuf, ProjectEntry, WorkspaceState, manifest_string_field, }; +use crate::install_state::last_validated_timestamp; use pnpm_reporter::Reporter; -use pnpm_workspace_state::{load_workspace_state, update_workspace_state_in_modules}; +use pnpm_workspace_state::update_workspace_state_in_modules; /// Persist `.pnpm-workspace-state-v1.json`, warning instead of failing /// the command when the write is lost. @@ -59,219 +43,6 @@ pub(crate) fn update_workspace_state_or_warn( } } -/// Inputs for [`install_already_up_to_date`]. -pub struct UpToDateFastPathCheck<'a> { - pub config: &'a Config, - pub manifest: &'a PackageManifest, - pub dependency_groups: Vec, - pub node_linker: NodeLinker, - /// The CLI-merged effective `supportedArchitectures` (yaml plus - /// `--cpu` / `--os` / `--libc`) — the fast path must not report - /// "Already up to date" when a flag changed the target platforms. - pub supported_architectures: Option, -} - -/// What an up-to-date install reports about the workspace it covered. -#[derive(Debug, PartialEq, Eq)] -pub struct UpToDateWorkspace { - /// The workspace root — the reporter `prefix` for the "Already up to - /// date" emission. - pub root: PathBuf, - /// How many projects the workspace has, or `None` when there is no - /// `pnpm-workspace.yaml`. Feeds the `pnpm:scope` report, which the - /// full install path derives from the same walk. - pub project_count: Option, -} - -/// [`crate::check_optimistic_repeat_install`] inputs, callable from a -/// synchronous context so the CLI can finish an up-to-date install -/// before paying for the async runtime, the HTTP client, and the -/// state setup. Returns what the short-circuit covered when the install -/// can take it, after registering the workspace in the store as -/// [`super::Install::run`] does. -/// -/// `Err` carries the reason the shortcut was not taken, for the -/// `pacquet::install` trace. Failures deliberately collapse into it: the -/// caller falls through to the full install path, which reproduces the -/// failure with its established error shape. -/// -/// The phases that read the project tree run inside `pacquet::install` -/// spans, so a trace of a slow up-to-date install shows which of them took -/// the time. -pub fn install_already_up_to_date( - check: &UpToDateFastPathCheck<'_>, -) -> Result { - already_up_to_date(check, Decide::FromRecordsOrFiles) -} - -/// [`install_already_up_to_date`] decided from the manifest records alone. A -/// caller whose install reads the project manifests anyway when the -/// shortcut is not taken uses this, so a declined attempt reads none of them. -pub fn install_already_up_to_date_from_records( - check: &UpToDateFastPathCheck<'_>, -) -> Result { - already_up_to_date(check, Decide::FromRecords) -} - -/// What an up-to-date verdict may be decided from. -#[derive(Clone, Copy)] -enum Decide { - FromRecords, - FromRecordsOrFiles, -} - -fn already_up_to_date( - check: &UpToDateFastPathCheck<'_>, - decide: Decide, -) -> Result { - let manifest_dir = check.manifest - .path() - .parent() - .ok_or("the manifest has no directory")?; - let workspace_dir_opt = configured_or_discovered_workspace_dir(check.config, manifest_dir) - .map_err(|_| "the workspace root cannot be located")?; - let workspace_root = workspace_dir_opt.as_deref().unwrap_or(manifest_dir); - // The lockfile the install wrote sits at its `lockfileDir`, which - // both `sharedWorkspaceLockfile: false` and an explicit pin move away - // from the discovered workspace root. The workspace *state* keeps its - // own root, which only a pin moves — `state_root` below. - let lockfile_root = lockfile_root_for(check.config, workspace_dir_opt.as_deref(), manifest_dir); - let state_root = check.config.lockfile_dir.as_deref().unwrap_or(workspace_root); - let state = load_fast_path_state(check.config, &lockfile_root, state_root)?; - let (workspace_manifest, catalogs) = - fast_path_workspace_context(check.config, workspace_dir_opt.as_deref()) - .ok_or("the workspace manifest or its catalogs cannot be read")?; - let lockfile = lazy_wanted_lockfile(check.config, &lockfile_root); - let context = FastPathContext { - state_root, - lockfile_root: &lockfile_root, - workspace_root, - workspace_manifest: workspace_manifest.as_ref(), - lockfile: &lockfile, - catalogs: &catalogs, - }; - let project_count = match stand_ins::check_from_records(check, &context, &state) { - stand_ins::RecordsCheck::UpToDate(project_count) => Some(project_count), - stand_ins::RecordsCheck::Undecided(listed) => match decide { - Decide::FromRecordsOrFiles => { - from_files::check_from_files(check, &context, &state, listed)? - } - Decide::FromRecords => return Err("the recorded manifests cannot decide"), - }, - }; - register_fast_path_workspace(check.config, &lockfile_root)?; - Ok(UpToDateWorkspace { root: state_root.to_path_buf(), project_count }) -} - -fn load_fast_path_state( - config: &Config, - lockfile_root: &Path, - state_root: &Path, -) -> Result { - if let Some(reason) = config_blocks_fast_path(config) { - return Err(reason); - } - if !matches!(super::run::check_strict_dep_builds_for_fast_path(config), Ok(true)) { - return Err("strictDepBuilds has to re-check the recorded ignored builds"); - } - if crate::agent_skills::agent_skill_targets_changed(config, lockfile_root) { - return Err("an agent skill directory changed"); - } - load_workspace_state(state_root) - .ok() - .flatten() - .ok_or("no workspace state on disk") -} - -/// What [`install_already_up_to_date`] has established about the workspace -/// before it reads any project. -struct FastPathContext<'a> { - state_root: &'a Path, - lockfile_root: &'a Path, - workspace_root: &'a Path, - /// `None` when no `pnpm-workspace.yaml` drives the install. - workspace_manifest: Option<&'a pnpm_workspace::WorkspaceManifest>, - lockfile: &'a LazyLockfile, - catalogs: &'a Catalogs, -} - -/// The projects one repeat-install check covers. -#[derive(Clone, Copy)] -struct FastPathProjects<'a> { - state: &'a WorkspaceState, - manifests: &'a [(PathBuf, &'a PackageManifest)], - /// See [`crate::DeclaredManifests::with_stand_ins`]. - recorded_digests: Option<&'a std::collections::HashMap>, - /// See [`crate::RepeatInstallManifests::workspace_projects`]. - bin_projects: Option<&'a [(PathBuf, &'a PackageManifest)]>, -} - -/// [`check_optimistic_repeat_install_with_state`] inside the -/// `check_repeat_install` span, with a skip surfaced as the fast path's reason. -fn check_fast_path_repeat_install( - check: &UpToDateFastPathCheck<'_>, - context: &FastPathContext<'_>, - projects: FastPathProjects<'_>, -) -> Result<(), &'static str> { - let _span = tracing::debug_span!(target: "pacquet::install", "check_repeat_install").entered(); - let project_manifests = match projects.recorded_digests { - Some(digests) => crate::DeclaredManifests::with_stand_ins(projects.manifests, digests), - None => crate::DeclaredManifests::new(projects.manifests), - }; - let decision = check_optimistic_repeat_install_with_state( - &OptimisticRepeatInstallCheck { - workspace_root: context.state_root, - config: check.config, - project_manifests, - is_workspace_install: context.workspace_manifest.is_some(), - lockfile: MaybeLazyLockfile::Lazy(context.lockfile), - catalogs: context.catalogs, - layout: crate::RepeatInstallLayout { - node_linker: check.node_linker, - included: super::included_dependencies(&check.dependency_groups), - supported_architectures: check.supported_architectures.as_ref(), - }, - manifests: crate::RepeatInstallManifests { - workspace_projects: projects.bin_projects, - ..crate::RepeatInstallManifests::ON_DISK - }, - }, - projects.state, - Scope::Workspace, - ); - match decision { - OptimisticRepeatInstallDecision::UpToDate => super::run::gvs_build_markers_complete( - check.config, - MaybeLazyLockfile::Lazy(context.lockfile), - context.lockfile_root, - super::effective_node_version(check.config, check.manifest).as_deref(), - ), - OptimisticRepeatInstallDecision::Skipped { reason } => Err(reason), - } -} - -/// [`super::run::register_workspace_in_store`] inside the -/// `register_workspace_in_store` span. -fn register_fast_path_workspace(config: &Config, lockfile_root: &Path) -> Result<(), &'static str> { - let _span = - tracing::debug_span!(target: "pacquet::install", "register_workspace_in_store").entered(); - super::run::register_workspace_in_store(config, lockfile_root) - .map_err(|_| "the workspace cannot be registered in the store") -} - -fn fast_path_workspace_context( - config: &Config, - workspace_dir: Option<&Path>, -) -> Option<(Option, super::Catalogs)> { - let workspace_manifest = workspace_dir - .map(pnpm_workspace::read_workspace_manifest) - .transpose() - .ok()? - .flatten(); - let catalogs = discovery::configured_catalogs(config, workspace_manifest.as_ref())?; - Some((workspace_manifest, catalogs)) -} - /// The workspace root the run belongs to: the configured one, else the /// nearest ancestor carrying a `pnpm-workspace.yaml`. /// @@ -320,7 +91,7 @@ pub(crate) fn lockfile_root_dir( /// [`lockfile_root_dir`] for a caller that has already resolved the /// workspace dir, so the ancestor walk is not repeated. -pub(super) fn lockfile_root_for( +pub(crate) fn lockfile_root_for( config: &Config, workspace_dir: Option<&Path>, manifest_dir: &Path, @@ -448,7 +219,7 @@ pub(super) fn build_projects_map( /// Assemble the [`WorkspaceState`] payload for /// [`pnpm_workspace_state::update_workspace_state`], for -/// [`crate::optimistic_repeat_install::validation::record_validation`] to +/// [`crate::install_state::validation::record_validation`] to /// complete once the caller has settled its settings. /// /// Records the projects pacquet just materialized plus the resolved @@ -480,17 +251,17 @@ pub(crate) fn build_workspace_state( filesystem_now_ms, ), projects: build_projects_map(project_manifests), - pnpmfiles: crate::optimistic_repeat_install::current_pnpmfiles(workspace_root, config), + pnpmfiles: crate::install_state::current_pnpmfiles(workspace_root, config), filtered_install: false, config_dependencies: config.config_dependencies.clone(), validation: None, // Settings construction is shared with - // `optimistic_repeat_install::current_settings` so the + // `install_state::current_settings` so the // freshness check sees the same byte shape this writer // produces. Keeping the construction in one place guarantees // adding a field on one side doesn't silently flip the other // into "drift" on the next install. - settings: crate::optimistic_repeat_install::current_settings_with_catalogs( + settings: crate::install_state::current_settings_with_catalogs( config, node_linker, included, @@ -546,17 +317,7 @@ fn record_hoisted_modules_dirs( for (root_dir, _) in project_manifests { if let Some(entry) = projects.get_mut(&*root_dir.to_string_lossy()) { entry.has_modules_dir = - crate::optimistic_repeat_install::hoisted_project_modules_dir(root_dir).is_dir(); + crate::install_state::hoisted_project_modules_dir(root_dir).is_dir(); } } } - -/// The wanted lockfile, read on first use — or a stand-in that never reads -/// one when the install is configured without a lockfile. -fn lazy_wanted_lockfile(config: &Config, lockfile_root: &Path) -> LazyLockfile { - if config.lockfile { - LazyLockfile::deferred(lockfile_root.to_path_buf(), config.wanted_lockfile_selection()) - } else { - LazyLockfile::disabled() - } -} diff --git a/pnpm/crates/package-manager/src/install/workspace_state/projects.rs b/pnpm/crates/package-manager/src/install/workspace_state/projects.rs index 523ed8c0f7..66846d5ff8 100644 --- a/pnpm/crates/package-manager/src/install/workspace_state/projects.rs +++ b/pnpm/crates/package-manager/src/install/workspace_state/projects.rs @@ -1,6 +1,6 @@ use super::super::{DependencyGroup, HashSet, PackageManifest, Path, PathBuf, ProjectMutation}; -pub(in super::super) fn build_project_manifests_list<'a>( +pub(crate) fn build_project_manifests_list<'a>( root_manifest: &'a PackageManifest, workspace_projects: Option<&'a [pnpm_workspace::Project]>, ) -> Vec<(std::path::PathBuf, &'a PackageManifest)> { diff --git a/pnpm/crates/package-manager/src/install/workspace_state/write.rs b/pnpm/crates/package-manager/src/install/workspace_state/write.rs index fefd437bf9..0925aee1dd 100644 --- a/pnpm/crates/package-manager/src/install/workspace_state/write.rs +++ b/pnpm/crates/package-manager/src/install/workspace_state/write.rs @@ -19,7 +19,7 @@ pub(crate) struct StateWrite<'a> { /// per [`build_workspace_state`]. pub(crate) filesystem_now_ms: Option, pub(crate) carried: CarriedSettings<'a>, - pub(crate) vouched: &'a crate::optimistic_repeat_install::validation::Vouched<'a>, + pub(crate) vouched: &'a crate::install_state::validation::Vouched<'a>, } /// The projects a write records. @@ -28,7 +28,7 @@ pub(crate) struct StateProjects<'a> { pub(crate) manifests: crate::DeclaredManifests<'a>, /// See [`crate::RepeatInstallManifests::workspace_projects`]. pub(crate) workspace_projects: Option<&'a [(PathBuf, &'a PackageManifest)]>, - /// See [`RecordValidation::lockfile`](crate::optimistic_repeat_install::validation::RecordValidation::lockfile). + /// See [`RecordValidation::lockfile`](crate::install_state::validation::RecordValidation::lockfile). pub(crate) lockfile: Option<&'a pnpm_lockfile::Lockfile>, } @@ -43,7 +43,7 @@ pub(crate) struct CarriedSettings<'a> { /// [`WorkspaceStateSettings::registry_prefixes`](pnpm_workspace_state::WorkspaceStateSettings::registry_prefixes)). pub(crate) registry_keys_verified: bool, /// The `registryRoutes` to record, per - /// [`recorded_registry_routes_after_install`](crate::optimistic_repeat_install::recorded_registry_routes_after_install) + /// [`recorded_registry_routes_after_install`](crate::install_state::recorded_registry_routes_after_install) /// for an install and the recorded ones for a passing check. pub(crate) registry_routes: Option>, /// The recorded dependency groups, runtime setting and store location to @@ -68,9 +68,9 @@ pub(crate) fn recorded_workspace_state(write: &StateWrite<'_>) -> WorkspaceState recorded_auto_dedupe(write.config, write.carried.established_auto_dedupe); if write.carried.registry_keys_verified { state.settings.registry_prefixes = - crate::optimistic_repeat_install::recorded_registry_prefixes(write.config); + crate::install_state::recorded_registry_prefixes(write.config); state.settings.default_registry = - crate::optimistic_repeat_install::recorded_default_registry(write.config); + crate::install_state::recorded_default_registry(write.config); } state.settings.registry_routes.clone_from(&write.carried.registry_routes); if let Some(kept) = write.carried.kept_settings { @@ -81,9 +81,9 @@ pub(crate) fn recorded_workspace_state(write: &StateWrite<'_>) -> WorkspaceState state.settings.store_dir.clone_from(&kept.store_dir); state.settings.virtual_store_dir.clone_from(&kept.virtual_store_dir); } - crate::optimistic_repeat_install::validation::record_validation( + crate::install_state::validation::record_validation( &mut state, - &crate::optimistic_repeat_install::validation::RecordValidation { + &crate::install_state::validation::RecordValidation { workspace_root: write.workspace_root, config: write.config, project_manifests: write.projects.manifests, diff --git a/pnpm/crates/package-manager/src/install/entry_points.rs b/pnpm/crates/package-manager/src/install_entry.rs similarity index 75% rename from pnpm/crates/package-manager/src/install/entry_points.rs rename to pnpm/crates/package-manager/src/install_entry.rs index 58e70d22d2..294d31d699 100644 --- a/pnpm/crates/package-manager/src/install/entry_points.rs +++ b/pnpm/crates/package-manager/src/install_entry.rs @@ -1,8 +1,15 @@ -use super::{ - Install, InstallRunOptions, InstallSaveOptions, ProjectMutation, WorkspaceInstallSelection, - errors::InstallError, +//! The public ways to run an [`Install`]. They sit above both `install` and +//! `optimistic_repeat_install`, so they are what hands an install the +//! repeat-install fast path. + +use crate::{ + LockfileCheckSink, LockfileVerificationOverride, RebuildOptions, ResolvedPackages, + install::{ + Install, InstallError, InstallManifestOptions, InstallRunOptions, InstallSaveOptions, + ProjectMutation, WorkspaceInstallSelection, + }, + optimistic_repeat_install::check_optimistic_repeat_install_in, }; -use crate::{LockfileCheckSink, LockfileVerificationOverride, RebuildOptions, ResolvedPackages}; use pnpm_config::Config; use pnpm_lockfile::MaybeLazyLockfile; use pnpm_network::ThrottledClient; @@ -75,7 +82,7 @@ where ) -> Result<(), InstallError> { Box::pin(self.run_inner::(InstallRunOptions { selection, - manifests: crate::install::InstallManifestOptions { + manifests: InstallManifestOptions { prune_direct_dependencies: true, ..Default::default() }, @@ -102,8 +109,11 @@ where self.lockfile_policy.disable_optimistic_repeat = true; let sink = LockfileCheckSink::default(); Box::pin(self.run_inner::(InstallRunOptions { - lockfile_check: Some(Arc::clone(&sink)), selection, + save: InstallSaveOptions { + lockfile_check: Some(Arc::clone(&sink)), + ..InstallSaveOptions::default() + }, ..Default::default() })) .await?; @@ -122,7 +132,7 @@ where read_package_hooked_manifest_paths: HashSet, ) -> Result<(), InstallError> { Box::pin(self.run_inner::(InstallRunOptions { - manifests: crate::install::InstallManifestOptions { + manifests: InstallManifestOptions { specifier_manifests: Some(lockfile_specifier_project_manifests), hooked_paths: read_package_hooked_manifest_paths, ..Default::default() @@ -176,7 +186,7 @@ where ) -> Result<(), InstallError> { Box::pin(self.run_inner::(InstallRunOptions { selection: Some(selection), - manifests: crate::install::InstallManifestOptions { + manifests: InstallManifestOptions { specifier_manifests: Some(lockfile_specifier_project_manifests), hooked_paths: read_package_hooked_manifest_paths, ..Default::default() @@ -194,10 +204,7 @@ where bumps: &'a crate::ManifestSpecBumps, ) -> Result<(), InstallError> { Box::pin(self.run_inner::(InstallRunOptions { - manifests: crate::install::InstallManifestOptions { - spec_bumps: Some(bumps), - ..Default::default() - }, + manifests: InstallManifestOptions { spec_bumps: Some(bumps), ..Default::default() }, ..Default::default() })) .await @@ -212,10 +219,7 @@ where ) -> Result<(), InstallError> { Box::pin(self.run_inner::(InstallRunOptions { selection: Some(selection), - manifests: crate::install::InstallManifestOptions { - spec_bumps: Some(bumps), - ..Default::default() - }, + manifests: InstallManifestOptions { spec_bumps: Some(bumps), ..Default::default() }, ..Default::default() })) .await @@ -239,10 +243,7 @@ where known_requires_build: Option, ) -> Result<(), InstallError> { Box::pin(self.run_inner::(InstallRunOptions { - manifests: crate::install::InstallManifestOptions { - known_requires_build, - ..Default::default() - }, + manifests: InstallManifestOptions { known_requires_build, ..Default::default() }, ..Default::default() })) .await @@ -255,10 +256,7 @@ where ) -> Result<(), InstallError> { Box::pin(self.run_inner::(InstallRunOptions { selection: Some(selection), - manifests: crate::install::InstallManifestOptions { - known_requires_build, - ..Default::default() - }, + manifests: InstallManifestOptions { known_requires_build, ..Default::default() }, ..Default::default() })) .await @@ -279,11 +277,12 @@ where self.execution.mutation = ProjectMutation::Deploy; Box::pin(self.run_inner::(InstallRunOptions { root_manifest_as_workspace_root: true, - save: crate::install::InstallSaveOptions { lockfile: false, workspace_state: false }, - manifests: crate::install::InstallManifestOptions { - deploy_hook: true, - ..Default::default() + save: InstallSaveOptions { + lockfile: false, + workspace_state: false, + lockfile_check: None, }, + manifests: InstallManifestOptions { deploy_hook: true, ..Default::default() }, ..Default::default() })) .await @@ -326,76 +325,24 @@ where .await } } -pub fn apply_deploy_manifest_hook(manifest: &mut serde_json::Value) { - let names = - deploy_dependency_names(manifest, "workspace:").map(str::to_owned).collect::>(); - inject_deploy_dependencies_meta(manifest, names); - copy_deploy_linked_dependencies(manifest); -} -pub(crate) fn apply_deploy_manifest_hook_to_arc( - mut manifest: Arc, -) -> Arc { - if deploy_dependency_names(&manifest, "workspace:").next().is_none() - && deploy_dependency_names(&manifest, "link:").next().is_none() - { - return manifest; - } - apply_deploy_manifest_hook(Arc::make_mut(&mut manifest)); - manifest -} -fn deploy_dependency_names<'a>( - manifest: &'a serde_json::Value, - protocol: &'static str, -) -> impl Iterator { - ["optionalDependencies", "dependencies", "devDependencies"] - .into_iter() - .filter_map(move |field| manifest.get(field)?.as_object()) - .flat_map(|dependencies| dependencies.iter()) - .filter_map(move |(name, specifier)| { - specifier - .as_str() - .is_some_and(|specifier| specifier.starts_with(protocol)) - .then_some(name.as_str()) - }) -} - -fn copy_deploy_linked_dependencies(manifest: &mut serde_json::Value) { - for field in ["optionalDependencies", "dependencies", "devDependencies"] { - let Some(dependencies) = manifest.get_mut(field).and_then(serde_json::Value::as_object_mut) - else { - continue; - }; - for specifier in dependencies.values_mut() { - let Some(directory) = specifier - .as_str() - .and_then(|value| value.strip_prefix("link:")) - else { - continue; - }; - *specifier = serde_json::Value::String(format!("file:{directory}")); - } - } -} - -pub(super) fn inject_deploy_dependencies_meta( - manifest: &mut serde_json::Value, - names: Vec, -) { - if names.is_empty() { - return; - } - let Some(object) = manifest.as_object_mut() else { return }; - let dependencies_meta = object - .entry("dependenciesMeta") - .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new())); - let Some(meta_object) = dependencies_meta.as_object_mut() else { return }; - for name in names { - let dependency_meta = meta_object.entry(name).or_insert(serde_json::Value::Null); - match dependency_meta { - serde_json::Value::Object(object) => { - object.insert("injected".to_owned(), serde_json::Value::Bool(true)); - } - value => *value = serde_json::json!({ "injected": true }), +impl Default for InstallRunOptions<'_, '_> { + fn default() -> Self { + InstallRunOptions { + repeat_install: check_optimistic_repeat_install_in, + lockfile_verification_override: None, + rebuild: None, + selection: None, + root_manifest_as_workspace_root: false, + save: InstallSaveOptions::default(), + prompt_eligibility_override: None, + manifests: InstallManifestOptions { + deploy_hook: false, + prune_direct_dependencies: false, + specifier_manifests: None, + hooked_paths: HashSet::new(), + spec_bumps: None, + known_requires_build: None, + }, } } } @@ -409,7 +356,7 @@ fn rebuild_run_options<'install, 'selection>( ) -> InstallRunOptions<'install, 'selection> { InstallRunOptions { rebuild: Some(rebuild), - save: InstallSaveOptions { lockfile: true, workspace_state: false }, + save: InstallSaveOptions { workspace_state: false, ..InstallSaveOptions::default() }, ..Default::default() } } diff --git a/pnpm/crates/package-manager/src/install_state.rs b/pnpm/crates/package-manager/src/install_state.rs new file mode 100644 index 0000000000..58dd6feb31 --- /dev/null +++ b/pnpm/crates/package-manager/src/install_state.rs @@ -0,0 +1,252 @@ +//! What a completed install records in the workspace state, and the inputs +//! a later command compares with those records. The install writes the +//! records and the repeat-install fast path reads them. Both depend on this +//! module, so the install never depends on the fast path. + +pub(crate) mod local_file_deps; +pub(crate) mod settings; +pub(crate) mod timestamps; +pub(crate) mod validation; +pub(crate) use local_file_deps::{ + FrozenLocalTarballCheck, frozen_local_tarballs_to_verify, has_local_file_dep_requiring_install, + has_local_file_override, has_local_file_package_extension, +}; +pub use manifest_source::{DeclaredManifests, ManifestHooks, RepeatInstallManifests}; +pub(crate) use settings::{ + ResolvedImporters, catalogs_cache_matches, current_settings_with_catalogs, first_setting_drift, + recorded_default_registry, recorded_extend_node_path_matches, recorded_registry_prefixes, + recorded_registry_routes_after_install, recorded_supported_architectures_match, + records_registry_keys_of, +}; +pub(crate) use timestamps::{ + file_mtime, file_mtime_from_metadata, filesystem_now_ms_in_modules, + filesystem_now_ns_in_modules, last_validated_timestamp, lockfile_modified_since, + modified_at_or_after, +}; + +mod manifest_source; + +use pnpm_catalogs_types::Catalogs; +use pnpm_config::{Config, NodeLinker}; +use pnpm_lockfile::{Lockfile, MaybeLazyLockfile}; +use pnpm_modules_yaml::IncludedDependencies; +use pnpm_package_is_installable::SupportedArchitectures; +use pnpm_package_manifest::PackageManifest; +use pnpm_workspace_state::WorkspaceState; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +/// The repeat-install check an install consults before it sets up anything, +/// for the projects `Scope` covers. The fast path supplies it, so that the +/// install does not depend on the fast path. +pub(crate) type RepeatInstallCheck = fn(&OptimisticRepeatInstallCheck<'_>, Scope<'_>) -> Decision; + +/// Project manifests keyed by their lexically normalized project directory. +pub(crate) type ProjectManifestsByDir<'a> = HashMap; + +/// Outcome of [`crate::check_optimistic_repeat_install`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Decision { + /// The install is fully up to date — emit "Already up to date" + /// and exit before any of the install setup runs. + UpToDate, + /// Fall through to the full install path. `reason` is a short + /// diagnostic string surfaced via `tracing::debug!` for + /// diagnosability without contaminating the reporter stream. + Skipped { reason: &'static str }, +} + +/// Inputs to [`crate::check_optimistic_repeat_install`]. +pub struct OptimisticRepeatInstallCheck<'a> { + /// The root the install recorded its lockfile and workspace state + /// against, which importer ids and relative local `pnpm.overrides` + /// targets are named from too. The directory containing + /// `pnpm-workspace.yaml` (or the project root when no workspace + /// manifest exists — same fallback as + /// [`Install::run`](crate::Install::run)), unless the configuration + /// pins the lockfile somewhere else. + pub workspace_root: &'a Path, + pub config: &'a Config, + /// Every importer's `(root_dir, manifest)` pair. For a + /// single-project install it's just the root manifest; for a + /// workspace install it's every project the resolver would + /// otherwise walk. The caller passes this in (rather than this + /// function rediscovering it) so the same walk seeds the regular + /// install path on the fall-through. + pub project_manifests: DeclaredManifests<'a>, + /// `true` when a `pnpm-workspace.yaml` drives the install — that + /// selects the workspace branch, which keys the manifest and + /// lockfile comparisons off `lastValidatedTimestamp`. `false` (no + /// workspace manifest) selects the single-project branch, which + /// additionally requires `pnpm-lock.yaml` to exist on disk — + /// `RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND` is raised otherwise, which + /// resolves to not-up-to-date — and keys its comparisons off the + /// lockfile mtimes instead. + pub is_workspace_install: bool, + /// The wanted lockfile (`None` once loaded when `pnpm-lock.yaml` + /// is absent or empty). Consulted only by the modified-manifests + /// content re-check; the pure-mtime fast path never parses it — + /// which is why it arrives lazily, so the common repeat-install + /// run skips the YAML parse entirely. A separately bounded byte + /// scan only runs when lockfile metadata changed. When absent and + /// `/lock.yaml` exists, the current lockfile + /// stands in as the wanted one — it records exactly what the + /// previous install materialized — and `pnpm-lock.yaml` is + /// regenerated from it before the check reports up-to-date. + pub lockfile: MaybeLazyLockfile<'a>, + /// Catalogs from the workspace manifest or an `updateConfig` + /// pnpmfile hook, for resolving `catalog:` values inside + /// `pnpm.overrides` before the lockfile settings comparison. + pub catalogs: &'a Catalogs, + pub layout: RepeatInstallLayout<'a>, + pub manifests: RepeatInstallManifests<'a>, +} + +impl<'a> OptimisticRepeatInstallCheck<'a> { + /// The projects whose commands the importers link, per + /// [`RepeatInstallManifests::workspace_projects`]. + pub(crate) fn bin_projects(&self) -> &'a [(PathBuf, &'a PackageManifest)] { + self.manifests.workspace_projects.unwrap_or(self.project_manifests.as_slice()) + } +} + +#[derive(Clone, Copy)] +pub struct RepeatInstallLayout<'a> { + pub node_linker: NodeLinker, + pub included: IncludedDependencies, + /// The CLI-merged effective `supportedArchitectures` this run would + /// install with (yaml plus `--cpu` / `--os` / `--libc`), compared + /// against the recorded value like `included`. + pub supported_architectures: Option<&'a SupportedArchitectures>, +} + +/// The projects a gate holds to being installed. Whatever the gate reports +/// must be something the install it leads to can fix: a filtered install +/// leaves every project outside its selection as it is. +#[derive(Clone, Copy)] +pub(crate) enum Scope<'a> { + /// Every project, for an unfiltered install. + Workspace, + /// The selected projects and the workspace projects they depend on, for a + /// filtered install or a filtered command. + Selected(&'a [&'a Path]), + /// An unfiltered command run from the selected projects. Its install + /// covers the whole workspace, so a project whose recorded install went + /// stale is held anywhere. A project never installed is held only where + /// the selection depends on it, so an install filtered to other projects + /// does not make every command install again. + Command(&'a [&'a Path]), +} + +pub(crate) fn manifest_has_runtime_deps(manifest: &PackageManifest) -> bool { + let value = manifest.value(); + [value.get("dependencies"), value.get("devDependencies"), value.get("optionalDependencies")] + .into_iter() + .flatten() + .any(|deps| { + deps.as_object() + .is_some_and(|map| !map.is_empty()) + }) +} + +/// Whether the manifest declares at least one peer that is not marked +/// optional. The caller decides whether `autoInstallPeers` fetches them. +pub(crate) fn manifest_has_required_peers(manifest: &PackageManifest) -> bool { + let Some(peers) = manifest + .value() + .get("peerDependencies") + .and_then(serde_json::Value::as_object) + else { + return false; + }; + let meta = manifest + .value() + .get("peerDependenciesMeta") + .and_then(serde_json::Value::as_object); + peers + .keys() + .any(|name| !peer_dependency_is_optional(meta, name)) +} + +fn peer_dependency_is_optional( + meta: Option<&serde_json::Map>, + name: &str, +) -> bool { + meta.and_then(|meta| meta.get(name)) + .and_then(serde_json::Value::as_object) + .and_then(|entry| entry.get("optional")) + .and_then(serde_json::Value::as_bool) + == Some(true) +} + +/// The pnpmfile list recorded in the workspace state and compared by +/// the freshness check: every pnpmfile the install loads. An install +/// that ignores the pnpmfile records none, so the next install that +/// honors it again sees the list change and re-validates. +pub(crate) fn current_pnpmfiles(workspace_root: &Path, config: &Config) -> Vec { + if config.ignore_pnpmfile { + return Vec::new(); + } + crate::find_install_pnpmfiles(config, workspace_root) + .into_iter() + .map(|path| path.to_string_lossy().into_owned()) + .collect() +} + +/// Whether no current project dir is a key of `state`. +pub(crate) fn recorded_elsewhere( + state: &WorkspaceState, + project_manifests: &[(PathBuf, &PackageManifest)], +) -> bool { + !project_manifests + .iter() + .any(|(root_dir, _)| state.projects.contains_key(root_dir.to_string_lossy().as_ref())) +} + +/// Whether `current` already records what materializing `wanted` would +/// produce. +/// +/// The current lockfile keeps only what the importers reach +/// ([`crate::filter_lockfile_for_current`]) and none of the top-level keys +/// pnpm does not define, so a wanted lockfile carrying a snapshot no +/// importer reaches any more, or an embedder's extension block, can never +/// equal it. Comparing the same shape both sides is what lets such a tree +/// settle instead of re-materializing on every run. +/// +/// The equal case is the common one and answers without building the +/// filtered shape at all. Under `config.skip_runtimes` the direct runtimes are +/// left out of the shape, so a tree that still holds one does not match. +pub(crate) fn materialized_shape_matches( + wanted: &Lockfile, + current: &Lockfile, + included: IncludedDependencies, + config: &Config, +) -> bool { + let mut skipped = crate::SkippedSnapshots::new(); + if config.skip_runtimes + && let Some(packages) = wanted.packages.as_ref() + { + crate::add_direct_runtime_skips(&mut skipped, &wanted.importers, packages); + } + if skipped.is_empty() && wanted == current { + return true; + } + // A transient skip (a failed optional fetch) prunes the current lockfile + // further, and its set is not known here. Such a tree falls + // through to materialization, which retries the fetch anyway. + current + == &crate::filter_lockfile_for_current( + wanted, + &crate::GroupSelection::classify(wanted, included, config.peer_edge_options()), + &skipped, + ) +} + +/// Where the hoisted linker nests the dependencies of the workspace project at +/// `root_dir` that it cannot hoist to the root. The walker always uses +/// `node_modules` here, whatever `modulesDir` says. +pub(crate) fn hoisted_project_modules_dir(root_dir: &Path) -> PathBuf { + root_dir.join("node_modules") +} diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/local_file_deps.rs b/pnpm/crates/package-manager/src/install_state/local_file_deps.rs similarity index 97% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/local_file_deps.rs rename to pnpm/crates/package-manager/src/install_state/local_file_deps.rs index 03f53ee7df..2a20ac5ce9 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/local_file_deps.rs +++ b/pnpm/crates/package-manager/src/install_state/local_file_deps.rs @@ -6,17 +6,23 @@ pub(crate) use specs::{ mod injected; -use super::{ - CatalogAnchor, CatalogResolutionResult, Catalogs, DependencyGroup, Lockfile, - OptimisticRepeatInstallCheck, Path, PathBuf, WantedDependency, resolve_from_catalog, -}; +use super::OptimisticRepeatInstallCheck; use injected::{has_unvouched_injected_dep, settings_add_workspace_path}; +use pnpm_catalogs_resolver::{ + CatalogAnchor, CatalogResolutionResult, WantedDependency, resolve_from_catalog, +}; +use pnpm_catalogs_types::Catalogs; use pnpm_config_parse_overrides::VersionOverride; -use pnpm_lockfile::{LockfileResolution, PkgName, is_local_tarball_path}; +use pnpm_lockfile::{Lockfile, LockfileResolution, PkgName, is_local_tarball_path}; +use pnpm_package_manifest::DependencyGroup; use pnpm_resolving_local_resolver::local_tarball_path; use pnpm_workspace::importer_id_from_root_dir; use ssri::Integrity; -use std::{borrow::Cow, collections::HashSet}; +use std::{ + borrow::Cow, + collections::HashSet, + path::{Path, PathBuf}, +}; struct LocalTarballDependency { project_dir: PathBuf, diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/local_file_deps/injected.rs b/pnpm/crates/package-manager/src/install_state/local_file_deps/injected.rs similarity index 100% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/local_file_deps/injected.rs rename to pnpm/crates/package-manager/src/install_state/local_file_deps/injected.rs diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/local_file_deps/specs.rs b/pnpm/crates/package-manager/src/install_state/local_file_deps/specs.rs similarity index 96% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/local_file_deps/specs.rs rename to pnpm/crates/package-manager/src/install_state/local_file_deps/specs.rs index 0f615c8968..a62da4a900 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/local_file_deps/specs.rs +++ b/pnpm/crates/package-manager/src/install_state/local_file_deps/specs.rs @@ -1,9 +1,12 @@ -use crate::optimistic_repeat_install::{ - CatalogAnchor, CatalogResolutionResult, Catalogs, Config, DependencyGroup, - IncludedDependencies, WantedDependency, resolve_from_catalog, +use pnpm_catalogs_resolver::{ + CatalogAnchor, CatalogResolutionResult, WantedDependency, resolve_from_catalog, }; +use pnpm_catalogs_types::Catalogs; +use pnpm_config::Config; use pnpm_config_parse_overrides::VersionOverride; use pnpm_lockfile::is_local_tarball_path; +use pnpm_modules_yaml::IncludedDependencies; +use pnpm_package_manifest::DependencyGroup; /// Whether a `catalog:` spec dereferences (through the workspace /// catalogs) to a local file specifier. A misconfigured catalog entry diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/local_file_deps/specs/tests.rs b/pnpm/crates/package-manager/src/install_state/local_file_deps/specs/tests.rs similarity index 100% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/local_file_deps/specs/tests.rs rename to pnpm/crates/package-manager/src/install_state/local_file_deps/specs/tests.rs diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/manifest_source.rs b/pnpm/crates/package-manager/src/install_state/manifest_source.rs similarity index 97% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/manifest_source.rs rename to pnpm/crates/package-manager/src/install_state/manifest_source.rs index 29c81c87e6..769c167ec6 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/manifest_source.rs +++ b/pnpm/crates/package-manager/src/install_state/manifest_source.rs @@ -1,6 +1,6 @@ //! How the repeat-install check reads the project manifests it is handed. -use crate::install::ProjectManifestsByDir; +use super::ProjectManifestsByDir; use pnpm_hooks::PnpmfileHooks; use pnpm_package_manifest::PackageManifest; use std::{ @@ -27,7 +27,7 @@ impl<'a> DeclaredManifests<'a> { } /// Manifests some of which the last validation's records stand in for, - /// per [`crate::optimistic_repeat_install::validation::manifest_stand_ins`]. + /// per [`crate::install_state::validation::manifest_stand_ins`]. /// `recorded_digests` holds the digest of each of those, by project /// directory. The check then never writes: what it would write is /// derived from the files. diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/settings.rs b/pnpm/crates/package-manager/src/install_state/settings.rs similarity index 91% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/settings.rs rename to pnpm/crates/package-manager/src/install_state/settings.rs index 1b440d7def..c9a191b92c 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/settings.rs +++ b/pnpm/crates/package-manager/src/install_state/settings.rs @@ -1,6 +1,10 @@ //! Comparing the settings a previous install recorded against the current ones. pub(crate) use install_policy::recorded_extend_node_path_matches; +pub(crate) use recorded_values::{ + catalogs_to_json, empty_json_object, filter_null_object_values, + link_workspace_packages_to_json, map_node_linker, +}; pub(crate) use registry_prefixes::{ ResolvedImporters, recorded_default_registry, recorded_registry_prefixes, recorded_registry_routes, recorded_registry_routes_after_install, records_registry_keys_of, @@ -10,6 +14,7 @@ pub(crate) use registry_prefixes::{ mod agent_skills; mod equivalence; mod install_policy; +mod recorded_values; mod registry_prefixes; mod store_location; use agent_skills::agent_skills_settings; @@ -18,12 +23,14 @@ use equivalence::{ patched_dependencies_match, }; use install_policy::install_policy_settings; +use recorded_values::map_trust_policy; -use super::{ - Catalogs, Config, IncludedDependencies, LinkWorkspacePackages, NodeLinker, Path, - SupportedArchitectures, TrustPolicy, WorkspaceState, WorkspaceStateNodeLinker, - WorkspaceStateSettings, WorkspaceStateTrustPolicy, -}; +use pnpm_catalogs_types::Catalogs; +use pnpm_config::{Config, NodeLinker}; +use pnpm_modules_yaml::IncludedDependencies; +use pnpm_package_is_installable::SupportedArchitectures; +use pnpm_workspace_state::{WorkspaceState, WorkspaceStateSettings}; +use std::path::Path; /// Whether the `supportedArchitectures` recorded by the last install /// matches `live` (today's CLI-merged value). Read from the workspace @@ -426,44 +433,6 @@ pub(crate) fn catalogs_cache_matches( recorded == current } -pub(crate) fn catalogs_to_json(catalogs: &Catalogs) -> serde_json::Value { - serde_json::to_value(catalogs).expect("Catalogs serialize to a JSON object") -} - -pub(crate) fn empty_json_object() -> serde_json::Value { - serde_json::Value::Object(serde_json::Map::new()) -} - -pub(crate) fn filter_null_object_values(value: serde_json::Value) -> serde_json::Value { - let serde_json::Value::Object(mut map) = value else { return value }; - map.retain(|_, value| !value.is_null()); - serde_json::Value::Object(map) -} - -pub(crate) fn link_workspace_packages_to_json(value: LinkWorkspacePackages) -> serde_json::Value { - match value { - LinkWorkspacePackages::Off => serde_json::Value::Bool(false), - LinkWorkspacePackages::DirectOnly => serde_json::Value::Bool(true), - LinkWorkspacePackages::Deep => serde_json::Value::String("deep".to_string()), - } -} - -pub(crate) fn map_node_linker(linker: NodeLinker) -> WorkspaceStateNodeLinker { - match linker { - NodeLinker::Isolated => WorkspaceStateNodeLinker::Isolated, - NodeLinker::Hoisted => WorkspaceStateNodeLinker::Hoisted, - NodeLinker::Pnp => WorkspaceStateNodeLinker::Pnp, - NodeLinker::Loaded => WorkspaceStateNodeLinker::Loaded, - } -} - -fn map_trust_policy(policy: TrustPolicy) -> WorkspaceStateTrustPolicy { - match policy { - TrustPolicy::Off => WorkspaceStateTrustPolicy::Off, - TrustPolicy::NoDowngrade => WorkspaceStateTrustPolicy::NoDowngrade, - } -} - fn recorded_overrides(config: &Config) -> Option> { config.overrides .as_ref() diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/settings/agent_skills.rs b/pnpm/crates/package-manager/src/install_state/settings/agent_skills.rs similarity index 100% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/settings/agent_skills.rs rename to pnpm/crates/package-manager/src/install_state/settings/agent_skills.rs diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/settings/equivalence.rs b/pnpm/crates/package-manager/src/install_state/settings/equivalence.rs similarity index 100% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/settings/equivalence.rs rename to pnpm/crates/package-manager/src/install_state/settings/equivalence.rs diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/settings/install_policy.rs b/pnpm/crates/package-manager/src/install_state/settings/install_policy.rs similarity index 100% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/settings/install_policy.rs rename to pnpm/crates/package-manager/src/install_state/settings/install_policy.rs diff --git a/pnpm/crates/package-manager/src/install_state/settings/recorded_values.rs b/pnpm/crates/package-manager/src/install_state/settings/recorded_values.rs new file mode 100644 index 0000000000..92add559d8 --- /dev/null +++ b/pnpm/crates/package-manager/src/install_state/settings/recorded_values.rs @@ -0,0 +1,45 @@ +//! Config values in the form the workspace state records them. + +use pnpm_catalogs_types::Catalogs; +use pnpm_config::{LinkWorkspacePackages, NodeLinker, TrustPolicy}; +use pnpm_workspace_state::{ + NodeLinker as WorkspaceStateNodeLinker, TrustPolicy as WorkspaceStateTrustPolicy, +}; + +pub(crate) fn catalogs_to_json(catalogs: &Catalogs) -> serde_json::Value { + serde_json::to_value(catalogs).expect("Catalogs serialize to a JSON object") +} + +pub(crate) fn empty_json_object() -> serde_json::Value { + serde_json::Value::Object(serde_json::Map::new()) +} + +pub(crate) fn filter_null_object_values(value: serde_json::Value) -> serde_json::Value { + let serde_json::Value::Object(mut map) = value else { return value }; + map.retain(|_, value| !value.is_null()); + serde_json::Value::Object(map) +} + +pub(crate) fn link_workspace_packages_to_json(value: LinkWorkspacePackages) -> serde_json::Value { + match value { + LinkWorkspacePackages::Off => serde_json::Value::Bool(false), + LinkWorkspacePackages::DirectOnly => serde_json::Value::Bool(true), + LinkWorkspacePackages::Deep => serde_json::Value::String("deep".to_string()), + } +} + +pub(crate) fn map_node_linker(linker: NodeLinker) -> WorkspaceStateNodeLinker { + match linker { + NodeLinker::Isolated => WorkspaceStateNodeLinker::Isolated, + NodeLinker::Hoisted => WorkspaceStateNodeLinker::Hoisted, + NodeLinker::Pnp => WorkspaceStateNodeLinker::Pnp, + NodeLinker::Loaded => WorkspaceStateNodeLinker::Loaded, + } +} + +pub(super) fn map_trust_policy(policy: TrustPolicy) -> WorkspaceStateTrustPolicy { + match policy { + TrustPolicy::Off => WorkspaceStateTrustPolicy::Off, + TrustPolicy::NoDowngrade => WorkspaceStateTrustPolicy::NoDowngrade, + } +} diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/settings/registry_prefixes.rs b/pnpm/crates/package-manager/src/install_state/settings/registry_prefixes.rs similarity index 100% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/settings/registry_prefixes.rs rename to pnpm/crates/package-manager/src/install_state/settings/registry_prefixes.rs diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/settings/store_location.rs b/pnpm/crates/package-manager/src/install_state/settings/store_location.rs similarity index 100% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/settings/store_location.rs rename to pnpm/crates/package-manager/src/install_state/settings/store_location.rs diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/timestamps.rs b/pnpm/crates/package-manager/src/install_state/timestamps.rs similarity index 98% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/timestamps.rs rename to pnpm/crates/package-manager/src/install_state/timestamps.rs index b81f42f704..1cbc66f8a8 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/timestamps.rs +++ b/pnpm/crates/package-manager/src/install_state/timestamps.rs @@ -1,7 +1,14 @@ //! Modification times, and the baseline a recorded validation is compared against. -use super::{Config, Lockfile, PackageManifest, Path, PathBuf, SystemTime, fs}; +use pnpm_config::Config; +use pnpm_lockfile::Lockfile; use pnpm_modules_yaml::Clock; +use pnpm_package_manifest::PackageManifest; +use std::{ + fs, + path::{Path, PathBuf}, + time::SystemTime, +}; pub(crate) const NANOS_PER_MILLI: i64 = 1_000_000; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/validation.rs b/pnpm/crates/package-manager/src/install_state/validation.rs similarity index 98% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/validation.rs rename to pnpm/crates/package-manager/src/install_state/validation.rs index 051cbea9cc..a68e6b4a9f 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/validation.rs +++ b/pnpm/crates/package-manager/src/install_state/validation.rs @@ -9,21 +9,24 @@ //! others survive it unchanged. pub(crate) use bins::{ - dedicated_bin_projects, held_workspace_bins_match, links_workspace_commands, - project_bins_digest, project_bins_match, workspace_bins_digest, + dedicated_bin_projects, links_workspace_commands, project_bins_digest, project_bins_match, + workspace_bins_digest, }; pub(crate) use file_stat::{FileStat, trusts_every_file}; pub(crate) use local_dependencies::changed as local_dependency_changed; pub(crate) use manifest_files::{ManifestStandIns, manifest_stand_ins, records_need_refresh}; -use super::{Config, Lockfile, PackageManifest, Path, PathBuf}; use file_stat::{signature_trusted, stat_trusted}; +use pnpm_config::Config; +use pnpm_lockfile::Lockfile; +use pnpm_package_manifest::PackageManifest; use pnpm_workspace::importer_id_from_root_dir; use pnpm_workspace_state::{FileSignature, Validation, WorkspaceState}; use sha2::{Digest as _, Sha256}; use std::{ collections::{BTreeMap, HashSet}, fs, io, + path::{Path, PathBuf}, }; mod bins; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/validation/bins.rs b/pnpm/crates/package-manager/src/install_state/validation/bins.rs similarity index 77% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/validation/bins.rs rename to pnpm/crates/package-manager/src/install_state/validation/bins.rs index 1a04f00f30..a3a8f488b1 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/validation/bins.rs +++ b/pnpm/crates/package-manager/src/install_state/validation/bins.rs @@ -1,8 +1,6 @@ use super::{ - Config, PackageManifest, Path, PathBuf, Validation, WorkspaceState, importer_id_from_root_dir, - value_digest, + Config, PackageManifest, Path, PathBuf, Validation, importer_id_from_root_dir, value_digest, }; -use crate::optimistic_repeat_install::{ManifestStat, OptimisticRepeatInstallCheck}; pub(crate) fn workspace_bins_digest( workspace_root: &Path, @@ -49,7 +47,7 @@ pub(crate) fn project_bins_digest<'a>( /// Whether the project declares a dependency or a peer, through which a /// workspace project's commands reach its `node_modules/.bin`. pub(crate) fn links_workspace_commands(manifest: &PackageManifest) -> bool { - crate::optimistic_repeat_install::manifest_has_runtime_deps(manifest) + crate::install_state::manifest_has_runtime_deps(manifest) || manifest .value() .get("peerDependencies") @@ -70,26 +68,6 @@ pub(crate) fn project_bins_match( validation.workspace_bins.get(importer_id).map(String::as_str) == bins } -pub(crate) fn held_workspace_bins_match( - check: &OptimisticRepeatInstallCheck<'_>, - state: &WorkspaceState, - held: &[&ManifestStat<'_>], -) -> bool { - if held.is_empty() || state.validation.is_none() { - return true; - } - let bins = workspace_bins_digest(check.workspace_root, check.bin_projects()); - held.iter() - .all(|stat| { - let id = importer_id_from_root_dir(check.workspace_root, stat.root_dir); - project_bins_match( - state.validation.as_ref(), - &id, - project_bins_digest(&id, stat.manifest, bins.as_deref()), - ) - }) -} - /// [`crate::RepeatInstallManifests::workspace_projects`] for an install with /// dedicated lockfiles whose projects link workspace commands. `None` with a /// shared lockfile, whose projects already are the whole workspace. diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/validation/file_stat.rs b/pnpm/crates/package-manager/src/install_state/validation/file_stat.rs similarity index 100% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/validation/file_stat.rs rename to pnpm/crates/package-manager/src/install_state/validation/file_stat.rs diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/validation/local_dependencies.rs b/pnpm/crates/package-manager/src/install_state/validation/local_dependencies.rs similarity index 100% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/validation/local_dependencies.rs rename to pnpm/crates/package-manager/src/install_state/validation/local_dependencies.rs diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/validation/local_dependencies/tests.rs b/pnpm/crates/package-manager/src/install_state/validation/local_dependencies/tests.rs similarity index 100% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/validation/local_dependencies/tests.rs rename to pnpm/crates/package-manager/src/install_state/validation/local_dependencies/tests.rs diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/validation/manifest_files.rs b/pnpm/crates/package-manager/src/install_state/validation/manifest_files.rs similarity index 99% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/validation/manifest_files.rs rename to pnpm/crates/package-manager/src/install_state/validation/manifest_files.rs index 1e6cbc233e..a3ac09667b 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/validation/manifest_files.rs +++ b/pnpm/crates/package-manager/src/install_state/validation/manifest_files.rs @@ -76,7 +76,7 @@ fn recordable( return false; } let now = *filesystem_now_ns.get_or_insert_with(|| { - crate::optimistic_repeat_install::filesystem_now_ns_in_modules( + crate::install_state::filesystem_now_ns_in_modules( &check.config.workspace_state_modules_dir(check.workspace_root), ) }); diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/validation/manifest_files/stand_ins.rs b/pnpm/crates/package-manager/src/install_state/validation/manifest_files/stand_ins.rs similarity index 100% rename from pnpm/crates/package-manager/src/optimistic_repeat_install/validation/manifest_files/stand_ins.rs rename to pnpm/crates/package-manager/src/install_state/validation/manifest_files/stand_ins.rs diff --git a/pnpm/crates/package-manager/src/lib.rs b/pnpm/crates/package-manager/src/lib.rs index 07b9395843..b7e67a9bcf 100644 --- a/pnpm/crates/package-manager/src/lib.rs +++ b/pnpm/crates/package-manager/src/lib.rs @@ -17,6 +17,7 @@ pub(crate) use hoisted_selection::{ selected_materialization_closure, selected_materialization_ids, }; pub use install::*; +pub use install_state::*; pub use install_with_fresh_lockfile::*; pub use link_manifest_link_deps::*; pub use linked_peer_dependencies::linked_peer_dependencies_warning; @@ -70,6 +71,8 @@ mod fast_update_patched_dependencies; mod fast_update_settings; mod hoisted_selection; mod install; +mod install_entry; +mod install_state; mod install_with_fresh_lockfile; mod link_manifest_link_deps; mod linked_peer_dependencies; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install.rs index 4105f7a4db..e372c3b1f4 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install.rs @@ -14,43 +14,35 @@ pub(crate) mod conflict_markers; pub(crate) mod deps_status; pub(crate) mod freshness; -pub(crate) mod local_file_deps; pub(crate) mod manifest_agreement; -pub(crate) mod settings; -pub(crate) mod timestamps; -pub(crate) mod validation; pub(crate) use conflict_markers::{ LockfileConflictCheckFailure, first_lockfile_requiring_conflict_safe_install, }; -pub(crate) use current_lockfile::materialized_shape_matches; pub use deps_status::{RunDepsStatus, check_deps_status_before_run}; -pub(crate) use freshness::Scope; -pub(crate) use local_file_deps::{ - FrozenLocalTarballCheck, frozen_local_tarballs_to_verify, has_local_file_dep_requiring_install, - has_local_file_override, has_local_file_package_extension, -}; pub(crate) use manifest_agreement::{ManifestStat, held_projects_match_lockfile}; -pub use manifest_source::{DeclaredManifests, ManifestHooks, RepeatInstallManifests}; -pub(crate) use modules_dirs::{hoisted_project_modules_dir, project_id}; -pub(crate) use relocation::recorded_elsewhere; -pub(crate) use settings::{ - ResolvedImporters, catalogs_cache_matches, current_settings_with_catalogs, first_setting_drift, - recorded_default_registry, recorded_extend_node_path_matches, recorded_registry_prefixes, - recorded_registry_routes_after_install, recorded_supported_architectures_match, - records_registry_keys_of, +pub(crate) use modules_dirs::project_id; +pub use up_to_date::{ + GateWorkspace, UpToDateFastPathCheck, UpToDateWorkspace, check_deps_status_before_run_at, + deps_install_root, install_already_up_to_date, install_already_up_to_date_from_records, + install_already_up_to_date_per_project, }; -pub(crate) use timestamps::{ - file_mtime, file_mtime_from_metadata, filesystem_now_ms_in_modules, - filesystem_now_ns_in_modules, last_validated_timestamp, lockfile_modified_since, - modified_at_or_after, + +use crate::install_state::{ + Decision, OptimisticRepeatInstallCheck, Scope, catalogs_cache_matches, current_pnpmfiles, + file_mtime, file_mtime_from_metadata, filesystem_now_ms_in_modules, first_setting_drift, + has_local_file_dep_requiring_install, has_local_file_override, + has_local_file_package_extension, hoisted_project_modules_dir, local_file_deps, + lockfile_modified_since, manifest_has_required_peers, manifest_has_runtime_deps, + materialized_shape_matches, modified_at_or_after, recorded_elsewhere, records_registry_keys_of, + validation, }; mod current_lockfile; -mod manifest_source; mod modules_dirs; mod project_structure; mod relocation; mod settle; +mod up_to_date; use freshness::{ Gate, GatePolicy, MissingWantedLockfile, NewerPatch, RecordedGroups, Stale, check_freshness, }; @@ -66,98 +58,17 @@ use std::{ fs, io::{ErrorKind, Read}, path::{Path, PathBuf}, - time::SystemTime, }; use pnpm_catalogs_resolver::{ CatalogAnchor, CatalogResolutionResult, WantedDependency, resolve_from_catalog, }; use pnpm_catalogs_types::Catalogs; -use pnpm_config::{Config, LinkWorkspacePackages, NodeLinker, TrustPolicy}; -use pnpm_lockfile::{ImporterDepVersion, Lockfile, MaybeLazyLockfile, ProjectSnapshot}; -use pnpm_modules_yaml::{Host, IncludedDependencies}; -use pnpm_package_is_installable::SupportedArchitectures; +use pnpm_config::{Config, LinkWorkspacePackages, NodeLinker}; +use pnpm_lockfile::{ImporterDepVersion, Lockfile, ProjectSnapshot}; +use pnpm_modules_yaml::Host; use pnpm_package_manifest::{DependencyGroup, PackageManifest}; -use pnpm_workspace_state::{ - NodeLinker as WorkspaceStateNodeLinker, TrustPolicy as WorkspaceStateTrustPolicy, - WorkspaceState, WorkspaceStateSettings, load_workspace_state, -}; - -/// Outcome of [`check_optimistic_repeat_install`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Decision { - /// The install is fully up to date — emit "Already up to date" - /// and exit before any of the install setup runs. - UpToDate, - /// Fall through to the full install path. `reason` is a short - /// diagnostic string surfaced via `tracing::debug!` for - /// diagnosability without contaminating the reporter stream. - Skipped { reason: &'static str }, -} - -/// Inputs to [`check_optimistic_repeat_install`]. -pub struct OptimisticRepeatInstallCheck<'a> { - /// The root the install recorded its lockfile and workspace state - /// against, which importer ids and relative local `pnpm.overrides` - /// targets are named from too. The directory containing - /// `pnpm-workspace.yaml` (or the project root when no workspace - /// manifest exists — same fallback as - /// [`Install::run`](crate::Install::run)), unless the configuration - /// pins the lockfile somewhere else. - pub workspace_root: &'a Path, - pub config: &'a Config, - /// Every importer's `(root_dir, manifest)` pair. For a - /// single-project install it's just the root manifest; for a - /// workspace install it's every project the resolver would - /// otherwise walk. The caller passes this in (rather than this - /// function rediscovering it) so the same walk seeds the regular - /// install path on the fall-through. - pub project_manifests: DeclaredManifests<'a>, - /// `true` when a `pnpm-workspace.yaml` drives the install — that - /// selects the workspace branch, which keys the manifest and - /// lockfile comparisons off `lastValidatedTimestamp`. `false` (no - /// workspace manifest) selects the single-project branch, which - /// additionally requires `pnpm-lock.yaml` to exist on disk — - /// `RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND` is raised otherwise, which - /// resolves to not-up-to-date — and keys its comparisons off the - /// lockfile mtimes instead. - pub is_workspace_install: bool, - /// The wanted lockfile (`None` once loaded when `pnpm-lock.yaml` - /// is absent or empty). Consulted only by the modified-manifests - /// content re-check; the pure-mtime fast path never parses it — - /// which is why it arrives lazily, so the common repeat-install - /// run skips the YAML parse entirely. A separately bounded byte - /// scan only runs when lockfile metadata changed. When absent and - /// `/lock.yaml` exists, the current lockfile - /// stands in as the wanted one — it records exactly what the - /// previous install materialized — and `pnpm-lock.yaml` is - /// regenerated from it before the check reports up-to-date. - pub lockfile: MaybeLazyLockfile<'a>, - /// Catalogs from the workspace manifest or an `updateConfig` - /// pnpmfile hook, for resolving `catalog:` values inside - /// `pnpm.overrides` before the lockfile settings comparison. - pub catalogs: &'a Catalogs, - pub layout: RepeatInstallLayout<'a>, - pub manifests: RepeatInstallManifests<'a>, -} - -impl<'a> OptimisticRepeatInstallCheck<'a> { - /// The projects whose commands the importers link, per - /// [`RepeatInstallManifests::workspace_projects`]. - pub(crate) fn bin_projects(&self) -> &'a [(PathBuf, &'a PackageManifest)] { - self.manifests.workspace_projects.unwrap_or(self.project_manifests.as_slice()) - } -} - -#[derive(Clone, Copy)] -pub struct RepeatInstallLayout<'a> { - pub node_linker: NodeLinker, - pub included: IncludedDependencies, - /// The CLI-merged effective `supportedArchitectures` this run would - /// install with (yaml plus `--cpu` / `--os` / `--libc`), compared - /// against the recorded value like `included`. - pub supported_architectures: Option<&'a SupportedArchitectures>, -} +use pnpm_workspace_state::{WorkspaceState, load_workspace_state}; /// Run the workspace-state freshness fast path. Returns /// [`Decision::UpToDate`] when the install can short-circuit. @@ -319,47 +230,6 @@ fn recorded_local_dependency_changed( .is_some_and(|validation| validation::local_dependency_changed(validation, &workspace_dirs)) } -pub(crate) fn manifest_has_runtime_deps(manifest: &PackageManifest) -> bool { - let value = manifest.value(); - [value.get("dependencies"), value.get("devDependencies"), value.get("optionalDependencies")] - .into_iter() - .flatten() - .any(|deps| { - deps.as_object() - .is_some_and(|map| !map.is_empty()) - }) -} - -/// Whether the manifest declares at least one peer that is not marked -/// optional. The caller decides whether `autoInstallPeers` fetches them. -pub(crate) fn manifest_has_required_peers(manifest: &PackageManifest) -> bool { - let Some(peers) = manifest - .value() - .get("peerDependencies") - .and_then(serde_json::Value::as_object) - else { - return false; - }; - let meta = manifest - .value() - .get("peerDependenciesMeta") - .and_then(serde_json::Value::as_object); - peers - .keys() - .any(|name| !peer_dependency_is_optional(meta, name)) -} - -fn peer_dependency_is_optional( - meta: Option<&serde_json::Map>, - name: &str, -) -> bool { - meta.and_then(|meta| meta.get(name)) - .and_then(serde_json::Value::as_object) - .and_then(|entry| entry.get("optional")) - .and_then(serde_json::Value::as_bool) - == Some(true) -} - fn manifest_string_field(manifest: &PackageManifest, key: &str) -> Option { manifest .value() @@ -420,20 +290,6 @@ fn held_selection<'a>( } } -/// The pnpmfile list recorded in the workspace state and compared by -/// the freshness check: every pnpmfile the install loads. An install -/// that ignores the pnpmfile records none, so the next install that -/// honors it again sees the list change and re-validates. -pub(crate) fn current_pnpmfiles(workspace_root: &Path, config: &Config) -> Vec { - if config.ignore_pnpmfile { - return Vec::new(); - } - crate::find_install_pnpmfiles(config, workspace_root) - .into_iter() - .map(|path| path.to_string_lossy().into_owned()) - .collect() -} - /// How the pnpmfiles changed since the last validation, in pnpm's issue /// wording: the recorded list must match the current one, and every recorded /// pnpmfile must still exist with the content it was validated with. diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/current_lockfile.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/current_lockfile.rs index b6f9e3d4ec..4814dd0068 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/current_lockfile.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/current_lockfile.rs @@ -1,5 +1,4 @@ use super::{DependencyGroup, Lockfile, OptimisticRepeatInstallCheck, Path, WorkspaceState}; -use pnpm_config::Config; use pnpm_modules_yaml::IncludedDependencies; use std::{cell::OnceCell, collections::HashSet}; @@ -69,45 +68,6 @@ pub(crate) fn assert_loaded_current_lockfile_records( } } -/// Whether `current` already records what materializing `wanted` would -/// produce. -/// -/// The current lockfile keeps only what the importers reach -/// ([`crate::filter_lockfile_for_current`]) and none of the top-level keys -/// pnpm does not define, so a wanted lockfile carrying a snapshot no -/// importer reaches any more, or an embedder's extension block, can never -/// equal it. Comparing the same shape both sides is what lets such a tree -/// settle instead of re-materializing on every run. -/// -/// The equal case is the common one and answers without building the -/// filtered shape at all. Under `config.skip_runtimes` the direct runtimes are -/// left out of the shape, so a tree that still holds one does not match. -pub(crate) fn materialized_shape_matches( - wanted: &Lockfile, - current: &Lockfile, - included: IncludedDependencies, - config: &Config, -) -> bool { - let mut skipped = crate::SkippedSnapshots::new(); - if config.skip_runtimes - && let Some(packages) = wanted.packages.as_ref() - { - crate::add_direct_runtime_skips(&mut skipped, &wanted.importers, packages); - } - if skipped.is_empty() && wanted == current { - return true; - } - // A transient skip (a failed optional fetch) prunes the current lockfile - // further, and its set is not known here. Such a tree falls - // through to materialization, which retries the fetch anyway. - current - == &crate::filter_lockfile_for_current( - wanted, - &crate::GroupSelection::classify(wanted, included, config.peer_edge_options()), - &skipped, - ) -} - /// Whether `current` records exactly what `wanted` materializes for /// `importer_ids` in the dependency groups `state` recorded. Every group /// `current` records counts, so a tree that still holds a group the install diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/freshness.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/freshness.rs index 13823bf432..9a969b7f20 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/freshness.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/freshness.rs @@ -9,7 +9,7 @@ use missing_lockfile::settle_missing_wanted_lockfile; use registry_keys::state_unverified_by_changed_lockfile; use super::{ - LockfileConflictCheckFailure, OptimisticRepeatInstallCheck, Path, PathBuf, WorkspaceState, + LockfileConflictCheckFailure, OptimisticRepeatInstallCheck, PathBuf, Scope, WorkspaceState, catalogs_cache_matches, current_lockfile::CurrentLockfile, current_lockfile_file_has_content, @@ -81,24 +81,6 @@ pub(super) enum RecordedGroups { Kept, } -/// The projects a gate holds to being installed. Whatever the gate reports -/// must be something the install it leads to can fix: a filtered install -/// leaves every project outside its selection as it is. -#[derive(Clone, Copy)] -pub(crate) enum Scope<'a> { - /// Every project, for an unfiltered install. - Workspace, - /// The selected projects and the workspace projects they depend on, for a - /// filtered install or a filtered command. - Selected(&'a [&'a Path]), - /// An unfiltered command run from the selected projects. Its install - /// covers the whole workspace, so a project whose recorded install went - /// stale is held anywhere. A project never installed is held only where - /// the selection depends on it, so an install filtered to other projects - /// does not make every command install again. - Command(&'a [&'a Path]), -} - /// Why [`check_freshness`] cannot report the tree current. Each gate words it /// for its own audience. pub(crate) enum Stale { @@ -157,7 +139,7 @@ pub(super) fn check_freshness( check_static_freshness(check, state, policy, scope)?; let drift = InputDrift::compute(check, state); let held = drift.held(check, scope); - if !super::validation::held_workspace_bins_match(check, state, &held) { + if !drift::held_workspace_bins_match(check, state, &held) { return Err(Stale::Content("workspace package bins changed since the last install")); } stand_in_writes::refuse(check, moved || !held.is_empty() || drift.patches_modified)?; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/freshness/drift.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/freshness/drift.rs index d5e81671f0..745bd8540a 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/freshness/drift.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/freshness/drift.rs @@ -5,7 +5,9 @@ use super::{ super::{ ManifestStat, OptimisticRepeatInstallCheck, Path, PathBuf, WorkspaceState, current_lockfile::CurrentLockfile, - validation::{FileDigest, FileDigests}, + validation::{ + FileDigest, FileDigests, project_bins_digest, project_bins_match, workspace_bins_digest, + }, }, Scope, }; @@ -208,7 +210,7 @@ fn project_stats<'a>( if record.lockfile != lockfile_digest { ProjectStatus::LockfileChanged } else if record.manifest != digest - || !super::super::validation::project_bins_match( + || !project_bins_match( recorded, &importer_id, super::super::validation::project_bins_digest( @@ -375,3 +377,23 @@ fn reached_importers( #[cfg(test)] mod tests; + +pub(super) fn held_workspace_bins_match( + check: &OptimisticRepeatInstallCheck<'_>, + state: &WorkspaceState, + held: &[&ManifestStat<'_>], +) -> bool { + if held.is_empty() || state.validation.is_none() { + return true; + } + let bins = workspace_bins_digest(check.workspace_root, check.bin_projects()); + held.iter() + .all(|stat| { + let id = importer_id_from_root_dir(check.workspace_root, stat.root_dir); + project_bins_match( + state.validation.as_ref(), + &id, + project_bins_digest(&id, stat.manifest, bins.as_deref()), + ) + }) +} diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/modules_dirs.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/modules_dirs.rs index bda13db667..73c7579023 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/modules_dirs.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/modules_dirs.rs @@ -3,8 +3,8 @@ pub(super) mod direct_links; pub(super) use direct_links::direct_dependency_link_dangling; use super::{ - OptimisticRepeatInstallCheck, manifest_has_required_peers, manifest_has_runtime_deps, - manifest_string_field, + OptimisticRepeatInstallCheck, hoisted_project_modules_dir, manifest_has_required_peers, + manifest_has_runtime_deps, manifest_string_field, }; use pnpm_config::{Config, NodeLinker}; use pnpm_fs::lexical_normalize; @@ -109,13 +109,6 @@ fn first_project_missing_recorded_hoisted_modules_dir( }) } -/// Where the hoisted linker nests the dependencies of the workspace project at -/// `root_dir` that it cannot hoist to the root. The walker always uses -/// `node_modules` here, whatever `modulesDir` says. -pub(crate) fn hoisted_project_modules_dir(root_dir: &Path) -> PathBuf { - root_dir.join("node_modules") -} - /// A project's `name`, falling back to its root directory. pub(crate) fn project_id(root_dir: &Path, manifest: &PackageManifest) -> String { manifest_string_field(manifest, "name") diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/relocation.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/relocation.rs index bc1a1554fb..f61e2f1ca9 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/relocation.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/relocation.rs @@ -9,7 +9,7 @@ use super::{ current_pnpmfiles, freshness::{InputDrift, validation_now}, manifest_agreement::check_projects_content, - project_structure_matches, + project_structure_matches, recorded_elsewhere, }; use std::{collections::BTreeSet, path::Component}; @@ -45,16 +45,6 @@ pub(super) fn relocated_state( project_structure_matches(&relocated, project_manifests, config, catalogs).then_some(relocated) } -/// Whether no current project dir is a key of `state`. -pub(crate) fn recorded_elsewhere( - state: &WorkspaceState, - project_manifests: &[(PathBuf, &PackageManifest)], -) -> bool { - !project_manifests - .iter() - .any(|(root_dir, _)| state.projects.contains_key(root_dir.to_string_lossy().as_ref())) -} - /// The unique old root reproducing every current workspace-relative project /// path. The workspace root itself need not have a manifest or recorded entry. fn recorded_root<'a>( diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests.rs index 7457e43a71..fd36135452 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests.rs @@ -35,8 +35,8 @@ mod settings_classification; use super::{ Decision, OptimisticRepeatInstallCheck, check_optimistic_repeat_install, deps_status::{RunDepsStatus, check_deps_status_before_run}, - settings::current_settings, }; +use crate::install_state::settings::current_settings; use pnpm_catalogs_types::Catalogs; use pnpm_config::Config; use pnpm_lockfile::{Lockfile, MaybeLazyLockfile}; @@ -427,9 +427,7 @@ fn record_test_workspace_state( carried: crate::install::CarriedSettings { established_auto_dedupe: config.auto_dedupe.then_some(true), registry_keys_verified: true, - registry_routes: Some( - crate::optimistic_repeat_install::settings::recorded_registry_routes(config), - ), + registry_routes: Some(crate::install_state::settings::recorded_registry_routes(config)), kept_settings: None, }, vouched: &super::validation::Vouched::All, @@ -584,8 +582,10 @@ fn date_lockfile_after(workspace_root: &std::path::Path, recorded: i64) { /// that records a file records a stat the next check trusts. fn wait_for_the_next_filesystem_tick(workspace_root: &std::path::Path) { let now = || { - super::timestamps::filesystem_now_ns_in_modules(&workspace_root.join("node_modules")) - .unwrap() + crate::install_state::timestamps::filesystem_now_ns_in_modules(&workspace_root.join( + "node_modules", + )) + .unwrap() }; let written = now(); while now() <= written { diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/builds.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/builds.rs index cfdbb3d331..2ec162a5f2 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/builds.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/builds.rs @@ -1,10 +1,9 @@ use super::{ - super::{ - Decision, deps_status::RunDepsStatus, first_setting_drift, settings::current_settings, - }, + super::{Decision, deps_status::RunDepsStatus, first_setting_drift}, MTIME_STEP_MS, backdate_validated_files, check, isolated_included, recorded_timestamp, single_project_deps_status, workspace_deps_status, write_empty_lockfile, write_state, }; +use crate::install_state::settings::current_settings; use pnpm_config::Config; use pnpm_lockfile::Lockfile; use pnpm_package_manifest::PackageManifest; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/catalogs.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/catalogs.rs index bb04983a73..f6c3551630 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/catalogs.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/catalogs.rs @@ -1,11 +1,9 @@ use super::{ - super::{ - Decision, OptimisticRepeatInstallCheck, check_optimistic_repeat_install, - settings::current_settings_with_catalogs, - }, + super::{Decision, OptimisticRepeatInstallCheck, check_optimistic_repeat_install}, backdate_validated_files, check_with_catalogs, check_workspace, isolated_included, setup_fresh_install, setup_fresh_install_with_config, write_empty_lockfile, write_state, }; +use crate::install_state::settings::current_settings_with_catalogs; use indexmap::IndexMap; use pnpm_catalogs_types::Catalogs; use pnpm_config::Config; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/hooks.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/hooks.rs index 9156f83462..e451cebeb4 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/hooks.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/hooks.rs @@ -1,8 +1,9 @@ use super::{ - super::{Decision, current_pnpmfiles, settings::current_settings}, + super::{Decision, current_pnpmfiles}, backdate_validated_files, check, isolated_included, setup_fresh_install, write_state_with_pnpmfiles, }; +use crate::install_state::settings::current_settings; use pnpm_workspace_state::ProjectEntry; use std::{collections::BTreeMap, fs}; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/injected.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/injected.rs index 4bdc9482e0..f9fad24891 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/injected.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/injected.rs @@ -65,7 +65,7 @@ fn injected_sibling_decision( fs::create_dir_all(sibling_dir.join("node_modules")).unwrap(); let config = config.leak(); - let settings = super::super::settings::current_settings( + let settings = crate::install_state::settings::current_settings( config, pnpm_config::NodeLinker::Isolated, isolated_included(), diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/installation.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/installation.rs index 5e69ec8aa3..40f27cf54e 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/installation.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/installation.rs @@ -2,14 +2,16 @@ use super::{ super::{ Decision, OptimisticRepeatInstallCheck, check_optimistic_repeat_install, deps_status::install_args_from_state, - settings::current_settings, - timestamps::{FileMtime, modified_at_or_after}, }, assert_deps_status_converges_after_collision, backdate_validated_files, check, check_with_lockfile, content_check_decision, isolated_included, setup_content_check_project, setup_fresh_install, setup_fresh_install_with_config, validate_existing_files, write_empty_lockfile, write_registry_lockfile, write_state, }; +use crate::install_state::{ + settings::current_settings, + timestamps::{FileMtime, modified_at_or_after}, +}; use indexmap::IndexMap; use pnpm_config::Config; use pnpm_lockfile::MaybeLazyLockfile; @@ -528,16 +530,16 @@ fn returns_skipped_when_a_scope_moves_to_another_registry() { }, ); let project_manifests = [(dir.path().to_path_buf(), &manifest)]; - let recorded = super::super::recorded_registry_prefixes(recorded_config); + let recorded = crate::install_state::recorded_registry_prefixes(recorded_config); assert_eq!(recorded, Some(BTreeMap::from([("work".to_string(), work.to_string())]))); let mut routed = recorded_config.clone(); routed.registries_by_scope.insert("@corp".to_string(), work.to_string()); let routed = routed.leak(); - assert_eq!(super::super::recorded_registry_prefixes(routed), recorded); + assert_eq!(crate::install_state::recorded_registry_prefixes(routed), recorded); assert_eq!( - super::super::settings::recorded_registry_routes(routed).get("@corp"), + crate::install_state::settings::recorded_registry_routes(routed).get("@corp"), Some(&work.to_string()), ); assert!(matches!( @@ -916,7 +918,7 @@ fn install_args_reproduce_the_recorded_dependency_groups() { #[test] fn install_already_up_to_date_bails_before_walking_projects_when_workspace_state_is_missing() { - use crate::install::{UpToDateFastPathCheck, install_already_up_to_date}; + use crate::{UpToDateFastPathCheck, install_already_up_to_date}; use pnpm_package_manifest::DependencyGroup; let dir = tempdir().unwrap(); diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/integrity.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/integrity.rs index c56a25b0db..93bb1db48b 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/integrity.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/integrity.rs @@ -1,7 +1,8 @@ use super::{ - super::{Decision, settings::current_settings}, - backdate_validated_files, check, isolated_included, write_empty_lockfile, write_state, + super::Decision, backdate_validated_files, check, isolated_included, write_empty_lockfile, + write_state, }; +use crate::install_state::settings::current_settings; use pnpm_config::Config; use pnpm_package_manifest::PackageManifest; use pnpm_workspace_state::ProjectEntry; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/lockfile.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/lockfile.rs index d0d7c04717..f959cce5e9 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/lockfile.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/lockfile.rs @@ -3,8 +3,6 @@ use super::{ Decision, OptimisticRepeatInstallCheck, check_optimistic_repeat_install, deps_status::{RunDepsStatus, check_deps_status_before_run}, freshness::InputDrift, - settings::current_settings, - timestamps::{FileMtime, lockfile_modified_since, modified_at_or_after}, }, FOO_LOCKFILE, FOO_LOCKFILE_WITHOUT_PACKAGES, FOO_MANIFEST, MTIME_STEP_MS, backdate_validated_files, check, check_with_lockfile, check_workspace, content_check_decision, @@ -12,6 +10,10 @@ use super::{ setup_content_check_project, setup_fresh_install, setup_fresh_install_with_config, single_project_deps_status, validate_existing_files, write_bare_tarball_lockfile, write_state, }; +use crate::install_state::{ + settings::current_settings, + timestamps::{FileMtime, lockfile_modified_since, modified_at_or_after}, +}; use pnpm_config::Config; use pnpm_lockfile::{LazyLockfile, Lockfile, MaybeLazyLockfile}; use pnpm_package_manifest::PackageManifest; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/modules_dir.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/modules_dir.rs index 0b8024570f..31588ac8bf 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/modules_dir.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/modules_dir.rs @@ -1,10 +1,8 @@ use super::{ - super::{ - Decision, OptimisticRepeatInstallCheck, check_optimistic_repeat_install, - settings::current_settings, - }, + super::{Decision, OptimisticRepeatInstallCheck, check_optimistic_repeat_install}, backdate_validated_files, isolated_included, setup_fresh_install_with_config, write_state, }; +use crate::install_state::settings::current_settings; use indexmap::IndexMap; use pnpm_config::{Config, ProjectConfig}; use pnpm_lockfile::MaybeLazyLockfile; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/overrides.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/overrides.rs index c1de640ddd..b2134ddd4d 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/overrides.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/overrides.rs @@ -1,8 +1,8 @@ use super::{ - super::{Decision, settings::current_settings_with_catalogs}, - backdate_validated_files, check, check_with_catalogs, isolated_included, + super::Decision, backdate_validated_files, check, check_with_catalogs, isolated_included, setup_fresh_install_with_config, write_state, }; +use crate::install_state::settings::current_settings_with_catalogs; use indexmap::IndexMap; use pnpm_catalogs_types::Catalogs; use pnpm_lockfile::Lockfile; @@ -260,7 +260,7 @@ fn returns_up_to_date_when_a_linked_workspace_dependency_is_overridden_and_manif let sibling_manifest = pnpm_package_manifest::PackageManifest::from_path(sibling_dir.join("package.json")) .unwrap(); - let settings = super::super::settings::current_settings( + let settings = crate::install_state::settings::current_settings( config, pnpm_config::NodeLinker::Isolated, isolated_included(), diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/resolution.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/resolution.rs index a4d5b8560e..3eefd3277b 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/resolution.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/resolution.rs @@ -2,11 +2,11 @@ use super::{ super::{ Decision, manifest_agreement::{LinkedPackagesContext, linked_packages_are_up_to_date}, - settings::current_settings, }, backdate_validated_files, check, isolated_included, linked_sibling_decision, setup_fresh_install_with_config, write_state, }; +use crate::install_state::settings::current_settings; use indexmap::IndexMap; use pnpm_config::Config; use pnpm_lockfile::Lockfile; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/settings_classification.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/settings_classification.rs index 218d58c302..5c7fe41d35 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/settings_classification.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/settings_classification.rs @@ -7,7 +7,7 @@ //! The other buckets carry the reason, which the test cannot check. use super::{super::config_blocks_fast_path, current_settings, isolated_included}; -use crate::optimistic_repeat_install::first_setting_drift; +use crate::install_state::first_setting_drift; use indexmap::IndexMap; use pnpm_config::{Config, LinkWorkspacePackages, NodeLinker, TrustPolicy}; use pnpm_workspace_state::WorkspaceState; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/store.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/store.rs index 07c223bd8f..80b6779a61 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/store.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/store.rs @@ -1,7 +1,8 @@ use super::{ - super::{Decision, settings::current_settings}, - backdate_validated_files, check, isolated_included, setup_fresh_install, write_state, + super::Decision, backdate_validated_files, check, isolated_included, setup_fresh_install, + write_state, }; +use crate::install_state::settings::current_settings; use pnpm_config::Config; use pnpm_package_manifest::PackageManifest; use pnpm_workspace_state::ProjectEntry; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/workspace.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/workspace.rs index f326bafdc9..d307b8fc33 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/workspace.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests/workspace.rs @@ -1,16 +1,14 @@ #[cfg(unix)] use super::FOO_LOCKFILE; use super::{ - super::{ - Decision, OptimisticRepeatInstallCheck, check_optimistic_repeat_install, - settings::current_settings, - }, + super::{Decision, OptimisticRepeatInstallCheck, check_optimistic_repeat_install}, FOO_MANIFEST, RunDepsStatus, assert_content_check_converges_after_collision, backdate_validated_files, check, check_with_lockfile, collide_mtimes_with_recorded_state, content_check_decision, isolated_included, linked_sibling_decision_for_spec, setup_content_check_project, setup_fresh_install, setup_fresh_install_with_config, validate_existing_files, workspace_deps_status, write_local_tarball_lockfile, write_state, }; +use crate::install_state::settings::current_settings; use pnpm_config::Config; use pnpm_lockfile::{Lockfile, MaybeLazyLockfile}; use pnpm_modules_yaml::IncludedDependencies; diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date.rs new file mode 100644 index 0000000000..f62aac2070 --- /dev/null +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date.rs @@ -0,0 +1,259 @@ +//! The repeat-install check a command runs on its own, before it sets up an +//! install: the `pnpm install` shortcut the CLI takes before starting the +//! async runtime, and the verify-deps gate before `pnpm run` and `pnpm exec`. + +pub use discovery::{GateWorkspace, check_deps_status_before_run_at, deps_install_root}; +pub use per_project::install_already_up_to_date_per_project; + +mod discovery; + +mod from_files; + +mod per_project; + +mod stand_ins; + +mod nothing_to_install; + +use super::{check_optimistic_repeat_install_with_state, config_blocks_fast_path}; +use crate::{ + install::{ + build_project_manifests_list, check_strict_dep_builds_for_fast_path, + configured_or_discovered_workspace_dir, effective_node_version, gvs_build_markers_complete, + included_dependencies, load_workspace_projects, lockfile_root_for, + project_requires_lifecycle_scripts, register_workspace_in_store, + }, + install_state::{Decision, OptimisticRepeatInstallCheck, Scope}, +}; +use pnpm_catalogs_config::get_catalogs_from_workspace_manifest; +use pnpm_catalogs_types::Catalogs; +use pnpm_config::{Config, NodeLinker}; +use pnpm_executor::PROJECT_LIFECYCLE_STAGES; +use pnpm_lockfile::{LazyLockfile, MaybeLazyLockfile}; +use pnpm_modules_yaml::IncludedDependencies; +use pnpm_package_manifest::{DependencyGroup, PackageManifest}; +use pnpm_workspace_state::{WorkspaceState, load_workspace_state}; +use std::path::{Path, PathBuf}; + +/// Inputs for [`install_already_up_to_date`]. +pub struct UpToDateFastPathCheck<'a> { + pub config: &'a Config, + pub manifest: &'a PackageManifest, + pub dependency_groups: Vec, + pub node_linker: NodeLinker, + /// The CLI-merged effective `supportedArchitectures` (yaml plus + /// `--cpu` / `--os` / `--libc`) — the fast path must not report + /// "Already up to date" when a flag changed the target platforms. + pub supported_architectures: Option, +} + +/// What an up-to-date install reports about the workspace it covered. +#[derive(Debug, PartialEq, Eq)] +pub struct UpToDateWorkspace { + /// The workspace root — the reporter `prefix` for the "Already up to + /// date" emission. + pub root: PathBuf, + /// How many projects the workspace has, or `None` when there is no + /// `pnpm-workspace.yaml`. Feeds the `pnpm:scope` report, which the + /// full install path derives from the same walk. + pub project_count: Option, +} + +/// [`crate::check_optimistic_repeat_install`] inputs, callable from a +/// synchronous context so the CLI can finish an up-to-date install +/// before paying for the async runtime, the HTTP client, and the +/// state setup. Returns what the short-circuit covered when the install +/// can take it, after registering the workspace in the store as +/// [`crate::Install::run`] does. +/// +/// `Err` carries the reason the shortcut was not taken, for the +/// `pacquet::install` trace. Failures deliberately collapse into it: the +/// caller falls through to the full install path, which reproduces the +/// failure with its established error shape. +/// +/// The phases that read the project tree run inside `pacquet::install` +/// spans, so a trace of a slow up-to-date install shows which of them took +/// the time. +pub fn install_already_up_to_date( + check: &UpToDateFastPathCheck<'_>, +) -> Result { + already_up_to_date(check, Decide::FromRecordsOrFiles) +} + +/// [`install_already_up_to_date`] decided from the manifest records alone. A +/// caller whose install reads the project manifests anyway when the +/// shortcut is not taken uses this, so a declined attempt reads none of them. +pub fn install_already_up_to_date_from_records( + check: &UpToDateFastPathCheck<'_>, +) -> Result { + already_up_to_date(check, Decide::FromRecords) +} + +/// What an up-to-date verdict may be decided from. +#[derive(Clone, Copy)] +enum Decide { + FromRecords, + FromRecordsOrFiles, +} + +fn already_up_to_date( + check: &UpToDateFastPathCheck<'_>, + decide: Decide, +) -> Result { + let manifest_dir = check.manifest + .path() + .parent() + .ok_or("the manifest has no directory")?; + let workspace_dir_opt = configured_or_discovered_workspace_dir(check.config, manifest_dir) + .map_err(|_| "the workspace root cannot be located")?; + let workspace_root = workspace_dir_opt.as_deref().unwrap_or(manifest_dir); + // The lockfile the install wrote sits at its `lockfileDir`, which + // both `sharedWorkspaceLockfile: false` and an explicit pin move away + // from the discovered workspace root. The workspace *state* keeps its + // own root, which only a pin moves — `state_root` below. + let lockfile_root = lockfile_root_for(check.config, workspace_dir_opt.as_deref(), manifest_dir); + let state_root = check.config.lockfile_dir.as_deref().unwrap_or(workspace_root); + let state = load_fast_path_state(check.config, &lockfile_root, state_root)?; + let (workspace_manifest, catalogs) = + fast_path_workspace_context(check.config, workspace_dir_opt.as_deref()) + .ok_or("the workspace manifest or its catalogs cannot be read")?; + let lockfile = lazy_wanted_lockfile(check.config, &lockfile_root); + let context = FastPathContext { + state_root, + lockfile_root: &lockfile_root, + workspace_root, + workspace_manifest: workspace_manifest.as_ref(), + lockfile: &lockfile, + catalogs: &catalogs, + }; + let project_count = match stand_ins::check_from_records(check, &context, &state) { + stand_ins::RecordsCheck::UpToDate(project_count) => Some(project_count), + stand_ins::RecordsCheck::Undecided(listed) => match decide { + Decide::FromRecordsOrFiles => { + from_files::check_from_files(check, &context, &state, listed)? + } + Decide::FromRecords => return Err("the recorded manifests cannot decide"), + }, + }; + register_fast_path_workspace(check.config, &lockfile_root)?; + Ok(UpToDateWorkspace { root: state_root.to_path_buf(), project_count }) +} + +fn load_fast_path_state( + config: &Config, + lockfile_root: &Path, + state_root: &Path, +) -> Result { + if let Some(reason) = config_blocks_fast_path(config) { + return Err(reason); + } + if !matches!(check_strict_dep_builds_for_fast_path(config), Ok(true)) { + return Err("strictDepBuilds has to re-check the recorded ignored builds"); + } + if crate::agent_skills::agent_skill_targets_changed(config, lockfile_root) { + return Err("an agent skill directory changed"); + } + load_workspace_state(state_root) + .ok() + .flatten() + .ok_or("no workspace state on disk") +} + +/// What [`install_already_up_to_date`] has established about the workspace +/// before it reads any project. +struct FastPathContext<'a> { + state_root: &'a Path, + lockfile_root: &'a Path, + workspace_root: &'a Path, + /// `None` when no `pnpm-workspace.yaml` drives the install. + workspace_manifest: Option<&'a pnpm_workspace::WorkspaceManifest>, + lockfile: &'a LazyLockfile, + catalogs: &'a Catalogs, +} + +/// The projects one repeat-install check covers. +#[derive(Clone, Copy)] +struct FastPathProjects<'a> { + state: &'a WorkspaceState, + manifests: &'a [(PathBuf, &'a PackageManifest)], + /// See [`crate::DeclaredManifests::with_stand_ins`]. + recorded_digests: Option<&'a std::collections::HashMap>, + /// See [`crate::RepeatInstallManifests::workspace_projects`]. + bin_projects: Option<&'a [(PathBuf, &'a PackageManifest)]>, +} + +/// [`check_optimistic_repeat_install_with_state`] inside the +/// `check_repeat_install` span, with a skip surfaced as the fast path's reason. +fn check_fast_path_repeat_install( + check: &UpToDateFastPathCheck<'_>, + context: &FastPathContext<'_>, + projects: FastPathProjects<'_>, +) -> Result<(), &'static str> { + let _span = tracing::debug_span!(target: "pacquet::install", "check_repeat_install").entered(); + let project_manifests = match projects.recorded_digests { + Some(digests) => crate::DeclaredManifests::with_stand_ins(projects.manifests, digests), + None => crate::DeclaredManifests::new(projects.manifests), + }; + let decision = check_optimistic_repeat_install_with_state( + &OptimisticRepeatInstallCheck { + workspace_root: context.state_root, + config: check.config, + project_manifests, + is_workspace_install: context.workspace_manifest.is_some(), + lockfile: MaybeLazyLockfile::Lazy(context.lockfile), + catalogs: context.catalogs, + layout: crate::RepeatInstallLayout { + node_linker: check.node_linker, + included: included_dependencies(&check.dependency_groups), + supported_architectures: check.supported_architectures.as_ref(), + }, + manifests: crate::RepeatInstallManifests { + workspace_projects: projects.bin_projects, + ..crate::RepeatInstallManifests::ON_DISK + }, + }, + projects.state, + Scope::Workspace, + ); + match decision { + Decision::UpToDate => gvs_build_markers_complete( + check.config, + MaybeLazyLockfile::Lazy(context.lockfile), + context.lockfile_root, + effective_node_version(check.config, check.manifest).as_deref(), + ), + Decision::Skipped { reason } => Err(reason), + } +} + +/// [`register_workspace_in_store`] inside the +/// `register_workspace_in_store` span. +fn register_fast_path_workspace(config: &Config, lockfile_root: &Path) -> Result<(), &'static str> { + let _span = + tracing::debug_span!(target: "pacquet::install", "register_workspace_in_store").entered(); + register_workspace_in_store(config, lockfile_root) + .map_err(|_| "the workspace cannot be registered in the store") +} + +fn fast_path_workspace_context( + config: &Config, + workspace_dir: Option<&Path>, +) -> Option<(Option, Catalogs)> { + let workspace_manifest = workspace_dir + .map(pnpm_workspace::read_workspace_manifest) + .transpose() + .ok()? + .flatten(); + let catalogs = discovery::configured_catalogs(config, workspace_manifest.as_ref())?; + Some((workspace_manifest, catalogs)) +} + +/// The wanted lockfile, read on first use — or a stand-in that never reads +/// one when the install is configured without a lockfile. +fn lazy_wanted_lockfile(config: &Config, lockfile_root: &Path) -> LazyLockfile { + if config.lockfile { + LazyLockfile::deferred(lockfile_root.to_path_buf(), config.wanted_lockfile_selection()) + } else { + LazyLockfile::disabled() + } +} diff --git a/pnpm/crates/package-manager/src/install/workspace_state/discovery.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/discovery.rs similarity index 94% rename from pnpm/crates/package-manager/src/install/workspace_state/discovery.rs rename to pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/discovery.rs index 608157000d..195842ed62 100644 --- a/pnpm/crates/package-manager/src/install/workspace_state/discovery.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/discovery.rs @@ -1,16 +1,13 @@ use super::{ - super::{ - Catalogs, Config, IncludedDependencies, MaybeLazyLockfile, OptimisticRepeatInstallCheck, - PackageManifest, Path, PathBuf, get_catalogs_from_workspace_manifest, - }, - build_project_manifests_list, configured_or_discovered_workspace_dir, lazy_wanted_lockfile, - lockfile_root_for, - nothing_to_install::projects_have_nothing_to_install, + Catalogs, Config, IncludedDependencies, MaybeLazyLockfile, OptimisticRepeatInstallCheck, + PackageManifest, Path, PathBuf, build_project_manifests_list, + configured_or_discovered_workspace_dir, get_catalogs_from_workspace_manifest, + lazy_wanted_lockfile, lockfile_root_for, nothing_to_install::projects_have_nothing_to_install, stand_ins, }; use std::borrow::Cow; -/// Discovery twin of [`install_already_up_to_date`](crate::install::workspace_state::install_already_up_to_date) for the +/// Discovery twin of [`super::install_already_up_to_date`] for the /// verify-deps-before-run gate: assemble the same /// [`OptimisticRepeatInstallCheck`] inputs from a bare directory and run /// [`crate::check_deps_status_before_run`]. @@ -122,13 +119,13 @@ impl GateWorkspace { let discovered = self.discovered .get_or_init(|| { let manifest = pnpm_workspace::read_workspace_manifest(&self.root).ok()??; - let opts = super::super::lifecycle::checked_projects_opts(config, &manifest); + let opts = crate::install::checked_projects_opts(config, &manifest); let projects = pnpm_workspace::find_workspace_projects(&self.root, &opts).ok()?; let key = opts.discovery_key(&self.root); Some(DiscoveredProjects { manifest, key, projects }) }) .as_ref()?; - let key = super::super::lifecycle::checked_projects_opts(config, &discovered.manifest) + let key = crate::install::checked_projects_opts(config, &discovered.manifest) .discovery_key(&self.root); (discovered.key == key).then_some(discovered.projects.as_slice()) } @@ -324,7 +321,7 @@ fn gate_workspace_projects<'a>( workspace, .. } = inputs; - let links = crate::optimistic_repeat_install::validation::links_workspace_commands(manifest); + let links = crate::install_state::validation::links_workspace_commands(manifest); let discovered = workspace .filter(|workspace| workspace.root == workspace_root) .filter(|_| !config.shares_one_lockfile() && links) @@ -388,13 +385,13 @@ fn load_gated_workspace( listed: Option>, ) -> Result>, pnpm_workspace::FindWorkspaceProjectsError> { let needed = config.shares_one_lockfile() - || crate::optimistic_repeat_install::validation::links_workspace_commands(manifest); + || crate::install_state::validation::links_workspace_commands(manifest); if !needed { return Ok(None); } match listed { Some(listed) => pnpm_workspace::read_workspace_projects(listed).map(Some), - None => super::super::lifecycle::load_checked_workspace_projects( + None => crate::install::load_checked_workspace_projects( config, workspace_root, workspace_manifest, @@ -415,7 +412,7 @@ fn gated_projects<'a>( ) -> (Vec<(PathBuf, &'a PackageManifest)>, Option>) { let shared = workspace_projects.filter(|_| config.shares_one_lockfile()); let project_manifests = build_project_manifests_list(manifest, shared); - let bin_projects = crate::optimistic_repeat_install::validation::dedicated_bin_projects( + let bin_projects = crate::install_state::validation::dedicated_bin_projects( config, &project_manifests, workspace_projects, diff --git a/pnpm/crates/package-manager/src/install/workspace_state/discovery/tests.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/discovery/tests.rs similarity index 100% rename from pnpm/crates/package-manager/src/install/workspace_state/discovery/tests.rs rename to pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/discovery/tests.rs diff --git a/pnpm/crates/package-manager/src/install/workspace_state/from_files.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/from_files.rs similarity index 92% rename from pnpm/crates/package-manager/src/install/workspace_state/from_files.rs rename to pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/from_files.rs index 985382990a..30bc6fc37c 100644 --- a/pnpm/crates/package-manager/src/install/workspace_state/from_files.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/from_files.rs @@ -30,7 +30,7 @@ pub(super) fn check_from_files( Ok(workspace_projects.as_ref().map(Vec::len)) } -/// [`super::super::lifecycle::load_checked_workspace_projects`] inside the +/// [`crate::install::load_checked_workspace_projects`] inside the /// `discover_workspace_projects` /// span, skipping the pnpm-managed directories, or the read of the projects /// `listed` names. @@ -43,7 +43,7 @@ fn discover_fast_path_projects( tracing::debug_span!(target: "pacquet::install", "discover_workspace_projects").entered(); let discovered = match listed { Some(listed) => pnpm_workspace::read_workspace_projects(listed).map(Some), - None => super::super::lifecycle::load_checked_workspace_projects( + None => crate::install::load_checked_workspace_projects( config, context.workspace_root, context.workspace_manifest, diff --git a/pnpm/crates/package-manager/src/install/workspace_state/nothing_to_install.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/nothing_to_install.rs similarity index 82% rename from pnpm/crates/package-manager/src/install/workspace_state/nothing_to_install.rs rename to pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/nothing_to_install.rs index f0f7c3fd5b..7531287120 100644 --- a/pnpm/crates/package-manager/src/install/workspace_state/nothing_to_install.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/nothing_to_install.rs @@ -1,10 +1,6 @@ use super::{ - super::{ - Config, PROJECT_LIFECYCLE_STAGES, PackageManifest, Path, load_workspace_projects, - project_requires_lifecycle_scripts, - }, - build_project_manifests_list, - discovery::GateInputs, + Config, PROJECT_LIFECYCLE_STAGES, PackageManifest, Path, build_project_manifests_list, + discovery::GateInputs, load_workspace_projects, project_requires_lifecycle_scripts, }; use pnpm_executor::DEV_PREINSTALL_STAGE; @@ -30,7 +26,7 @@ pub(super) fn projects_have_nothing_to_install(inputs: &GateInputs<'_>) -> bool if manifest.path().parent() != Some(lockfile_root) || project_has_install_work(config, manifest) || runs_dev_preinstall(config, manifest, workspace_root) - || !crate::optimistic_repeat_install::current_pnpmfiles(lockfile_root, config).is_empty() + || !crate::install_state::current_pnpmfiles(lockfile_root, config).is_empty() { return false; } @@ -52,9 +48,9 @@ fn project_has_install_work(config: &Config, manifest: &PackageManifest) -> bool .path() .parent() .expect("manifest path always has a parent dir"); - crate::optimistic_repeat_install::manifest_has_runtime_deps(manifest) + crate::install_state::manifest_has_runtime_deps(manifest) || (config.auto_install_peers - && crate::optimistic_repeat_install::manifest_has_required_peers(manifest)) + && crate::install_state::manifest_has_required_peers(manifest)) || (!config.ignore_scripts && project_requires_lifecycle_scripts(project_dir, manifest, &PROJECT_LIFECYCLE_STAGES)) } diff --git a/pnpm/crates/package-manager/src/install/workspace_state/per_project.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/per_project.rs similarity index 97% rename from pnpm/crates/package-manager/src/install/workspace_state/per_project.rs rename to pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/per_project.rs index 1d1f879772..0135e11914 100644 --- a/pnpm/crates/package-manager/src/install/workspace_state/per_project.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/per_project.rs @@ -8,7 +8,7 @@ use super::{ configured_or_discovered_workspace_dir, fast_path_workspace_context, lazy_wanted_lockfile, load_fast_path_state, register_fast_path_workspace, }; -use crate::optimistic_repeat_install::validation::{dedicated_bin_projects, manifest_stand_ins}; +use crate::install_state::validation::{dedicated_bin_projects, manifest_stand_ins}; use pnpm_workspace::{Project, ProjectManifestPaths}; use rayon::prelude::*; use std::collections::HashMap; @@ -111,7 +111,7 @@ fn recorded_projects( let listed = { let _span = tracing::debug_span!(target: "pacquet::install", "list_workspace_projects").entered(); - let opts = super::super::lifecycle::checked_projects_opts(check.config, workspace.manifest); + let opts = crate::install::checked_projects_opts(check.config, workspace.manifest); pnpm_workspace::find_workspace_project_manifests(workspace.root, &opts) .map_err(|_| "the workspace projects cannot be listed")? }; diff --git a/pnpm/crates/package-manager/src/install/workspace_state/stand_ins.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/stand_ins.rs similarity index 96% rename from pnpm/crates/package-manager/src/install/workspace_state/stand_ins.rs rename to pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/stand_ins.rs index 0447d1f015..d38955a393 100644 --- a/pnpm/crates/package-manager/src/install/workspace_state/stand_ins.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/up_to_date/stand_ins.rs @@ -7,7 +7,7 @@ use super::{ UpToDateFastPathCheck, WorkspaceState, build_project_manifests_list, check_fast_path_repeat_install, }; -use crate::optimistic_repeat_install::validation::{ManifestStandIns, manifest_stand_ins}; +use crate::install_state::validation::{ManifestStandIns, manifest_stand_ins}; use pnpm_workspace::ProjectManifestPaths; use std::collections::HashMap; @@ -105,8 +105,7 @@ pub(super) fn list_stand_ins(workspace: &ListedWorkspace<'_>) -> Listing { let listed = { let _span = tracing::debug_span!(target: "pacquet::install", "list_workspace_projects").entered(); - let opts = - super::super::lifecycle::checked_projects_opts(workspace.config, workspace_manifest); + let opts = crate::install::checked_projects_opts(workspace.config, workspace_manifest); pnpm_workspace::find_workspace_project_manifests(workspace.workspace_root, &opts) }; let Ok(listed) = listed else { return Listing::Files(None) }; diff --git a/pnpm/crates/workspace-state/src/tests.rs b/pnpm/crates/workspace-state/src/tests.rs index 85e2ca6621..b31301e5fe 100644 --- a/pnpm/crates/workspace-state/src/tests.rs +++ b/pnpm/crates/workspace-state/src/tests.rs @@ -94,7 +94,7 @@ fn omits_settings_that_are_none() { /// `packageExtensions` round-trips through `WorkspaceStateSettings` /// as a JSON object preserving the wire shape (`: /// { dependencies: { … } }`). The drift gate in -/// `optimistic_repeat_install::settings_match` reads this field, so +/// `first_setting_drift` in `pnpm-package-manager` reads this field, so /// the on-disk shape has to stay stable for the gate to be byte- /// comparable with pnpm's writer. #[test]