refactor(package-manager): break the module cycle between install and the repeat-install fast path (#17686)

This commit is contained in:
Zoltan Kochan authored and GitHub committed 2026-11-04 14:23:39 +01:00
1 parent 81bdfdfcd0
commit 37ade57d2d
78 files changed
+1050 -961

No files matched your search

+1 -1
View File
@@ -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
@@ -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,
),
),
+73 -73
View File
@@ -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<String> {
pub(crate) fn effective_node_version(
config: &Config,
manifest: &PackageManifest,
) -> Option<String> {
config.node_version.clone().or_else(|| node_version_from_engines_runtime(manifest.value()))
}
@@ -515,81 +522,74 @@ pub struct InstallProjects<DependencyGroupList> {
pub discovered_workspace: Option<DiscoveredWorkspace>,
}
struct InstallRunOptions<'install, 'selection> {
lockfile_verification_override: Option<LockfileVerificationOverride<'install>>,
rebuild: Option<RebuildOptions>,
selection: Option<WorkspaceInstallSelection<'selection>>,
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<LockfileCheckSink>,
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<LockfileVerificationOverride<'install>>,
pub(crate) rebuild: Option<RebuildOptions>,
pub(crate) selection: Option<WorkspaceInstallSelection<'selection>>,
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<bool>,
manifests: InstallManifestOptions<'install>,
pub(crate) prompt_eligibility_override: Option<bool>,
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 `<workspace_root>/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<LockfileCheckSink>,
}
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<Vec<(PathBuf, PackageManifest)>>,
pub(crate) specifier_manifests: Option<Vec<(PathBuf, PackageManifest)>>,
/// 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<PathBuf>,
pub(crate) hooked_paths: HashSet<PathBuf>,
/// 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<pnpm_deps_restorer::RequiresBuildBySnapshot>,
}
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<pnpm_deps_restorer::RequiresBuildBySnapshot>,
}
@@ -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,
}
}
@@ -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::<Vec<_>>();
inject_deploy_dependencies_meta(manifest, names);
copy_deploy_linked_dependencies(manifest);
}
pub(crate) fn apply_deploy_manifest_hook_to_arc(
mut manifest: Arc<serde_json::Value>,
) -> Arc<serde_json::Value> {
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<Item = &'a str> {
["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<String>,
) {
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 }),
}
}
}
@@ -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 {
@@ -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::{
@@ -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<PathBuf, &'a PackageManifest>;
/// 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.
@@ -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],
@@ -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> {
@@ -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<Reporter: self::Reporter>(
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::<Reporter>(
context.tree.workspace_root,
@@ -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<Reporter: self::Reporter + 'static>(
pub(crate) async fn run_inner<Reporter: self::Reporter + 'static>(
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()
}
@@ -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 {
@@ -342,7 +342,7 @@ async fn frozen_path<Reporter: self::Reporter>(
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<Reporter: self::Reporter>(
skipped: &[UnresolvedOptionalDependency],
@@ -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),
}
}
@@ -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<StoreOperationLock, InstallE
pub(super) struct UpToDateCheck<'a> {
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<RepeatInstallVerdict, InstallError> {
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<bool, InstallError> {
pub(crate) fn check_strict_dep_builds_for_fast_path(config: &Config) -> Result<bool, InstallError> {
if !config.strict_dep_builds {
return Ok(true);
}
@@ -288,8 +285,13 @@ impl super::RunExecution<'_> {
) -> Result<RepeatInstallVerdict, InstallError> {
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,
@@ -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,
@@ -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<RepeatInstallVerdict, InstallError> {
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
@@ -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();
@@ -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,
),
@@ -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,
@@ -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,
@@ -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");
@@ -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",
);
@@ -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<Reporter: self::Reporter>(
}
}
/// Inputs for [`install_already_up_to_date`].
pub struct UpToDateFastPathCheck<'a> {
pub config: &'a Config,
pub manifest: &'a PackageManifest,
pub dependency_groups: Vec<DependencyGroup>,
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<pnpm_package_is_installable::SupportedArchitectures>,
}
/// 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<usize>,
}
/// [`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<UpToDateWorkspace, &'static str> {
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<UpToDateWorkspace, &'static str> {
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<UpToDateWorkspace, &'static str> {
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<WorkspaceState, &'static str> {
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<PathBuf, String>>,
/// 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<pnpm_workspace::WorkspaceManifest>, 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<Sys: Clock>(
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()
}
}
@@ -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)> {
@@ -19,7 +19,7 @@ pub(crate) struct StateWrite<'a> {
/// per [`build_workspace_state`].
pub(crate) filesystem_now_ms: Option<i64>,
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<std::collections::BTreeMap<String, String>>,
/// 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,
@@ -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::<Reporter>(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::<Reporter>(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<PathBuf>,
) -> Result<(), InstallError> {
Box::pin(self.run_inner::<Reporter>(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::<Reporter>(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::<Reporter>(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::<Reporter>(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<pnpm_deps_restorer::RequiresBuildBySnapshot>,
) -> Result<(), InstallError> {
Box::pin(self.run_inner::<Reporter>(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::<Reporter>(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::<Reporter>(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::<Vec<_>>();
inject_deploy_dependencies_meta(manifest, names);
copy_deploy_linked_dependencies(manifest);
}
pub(crate) fn apply_deploy_manifest_hook_to_arc(
mut manifest: Arc<serde_json::Value>,
) -> Arc<serde_json::Value> {
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<Item = &'a str> {
["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<String>,
) {
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()
}
}
@@ -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<PathBuf, &'a PackageManifest>;
/// 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
/// `<install_state_dir>/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<String, serde_json::Value>>,
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<String> {
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")
}
@@ -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,
@@ -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
@@ -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.
@@ -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<std::collections::BTreeMap<String, String>> {
config.overrides
.as_ref()
@@ -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,
}
}
@@ -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;
@@ -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;
@@ -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.
@@ -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),
)
});
+3
View File
@@ -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;
@@ -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
/// `<install_state_dir>/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<String, serde_json::Value>>,
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<String> {
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<String> {
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.
@@ -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
@@ -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)?;
@@ -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()),
)
})
}
@@ -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")
@@ -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>(
@@ -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 {
@@ -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;
@@ -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;
@@ -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};
@@ -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(),
@@ -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();
@@ -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;
@@ -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;
@@ -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;
@@ -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(),
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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<DependencyGroup>,
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<pnpm_package_is_installable::SupportedArchitectures>,
}
/// 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<usize>,
}
/// [`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<UpToDateWorkspace, &'static str> {
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<UpToDateWorkspace, &'static str> {
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<UpToDateWorkspace, &'static str> {
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<WorkspaceState, &'static str> {
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<PathBuf, String>>,
/// 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<pnpm_workspace::WorkspaceManifest>, 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()
}
}
@@ -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<Vec<pnpm_workspace::ProjectManifestPaths>>,
) -> Result<Option<Vec<pnpm_workspace::Project>>, 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<Vec<(PathBuf, &'a PackageManifest)>>) {
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,
@@ -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,
@@ -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))
}
@@ -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")?
};
@@ -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) };
+1 -1
View File
@@ -94,7 +94,7 @@ fn omits_settings_that_are_none() {
/// `packageExtensions` round-trips through `WorkspaceStateSettings`
/// as a JSON object preserving the wire shape (`<selector>:
/// { 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]