use crate::progress::InstallProgress;
use aube_lockfile::{DriftStatus, LockfileGraph, LockfileKind};
use miette::{Context, IntoDiagnostic, miette};
use std::collections::HashMap;
use std::path::Path;
use super::frozen::FrozenMode;
use super::lockfile_dir::{
parse_lockfile_dir_remapped_with_kind_and_options, write_lockfile_dir_remapped,
};
use super::settings::{
ResolverConfigInputs, configure_resolver, maybe_cleanup_unused_catalogs,
stamp_pnpm_config_checksums,
};
use super::workspace::write_per_project_lockfiles;
pub(super) type ParsedLockfile = Option<(LockfileGraph, LockfileKind)>;
pub(super) fn pre_parse_lockfile(
lockfile_enabled: bool,
mode: FrozenMode,
lockfile_dir: &Path,
lockfile_importer_key: &str,
manifest: &aube_manifest::PackageJson,
parse_options: aube_lockfile::ParseOptions,
) -> miette::Result<ParsedLockfile> {
if !lockfile_enabled || !matches!(mode, FrozenMode::Fix | FrozenMode::Prefer) {
return Ok(None);
}
match parse_lockfile_dir_remapped_with_kind_and_options(
lockfile_dir,
lockfile_importer_key,
manifest,
parse_options,
) {
Ok(parsed) => Ok(Some(parsed)),
Err(aube_lockfile::Error::NotFound(_)) => Ok(None),
Err(e) if active_lockfile_has_conflict_markers(lockfile_dir) => {
warn_lockfile_conflict_markers(lockfile_dir, &e);
Ok(None)
}
Err(e) => Err(miette::Report::new(e)).wrap_err("failed to parse lockfile"),
}
}
pub(super) struct LockfileOnlyInput<'a> {
pub cwd: &'a Path,
pub mode: FrozenMode,
pub lockfile_dir: &'a Path,
pub lockfile_importer_key: &'a str,
pub manifest: &'a aube_manifest::PackageJson,
pub parse_options: aube_lockfile::ParseOptions,
pub manifests: &'a [(String, aube_manifest::PackageJson)],
pub per_project_write_selection: Option<&'a std::collections::BTreeSet<String>>,
pub ws_config: &'a aube_manifest::workspace::WorkspaceConfig,
pub workspace_catalogs: &'a crate::commands::CatalogMap,
pub settings_ctx: &'a aube_settings::ResolveCtx<'a>,
pub dependency_policy: &'a aube_resolver::DependencyPolicy,
pub lockfile_pre_parse: Option<&'a (LockfileGraph, LockfileKind)>,
pub lockfile_conflict_marker_warning_emitted: bool,
pub existing_for_resolver: Option<&'a LockfileGraph>,
pub write_kind: LockfileKind,
pub lockfile_enabled: bool,
pub lockfile_include_tarball_url: bool,
pub shared_workspace_lockfile: bool,
pub has_workspace: bool,
pub is_workspace_project: bool,
pub ignore_pnpmfile: bool,
pub network_mode: aube_registry::NetworkMode,
pub global_pnpmfile: Option<&'a Path>,
pub pnpmfile: Option<&'a Path>,
pub minimum_release_age_override: Option<u64>,
pub ws_package_versions: &'a HashMap<String, String>,
pub ignore_scripts: bool,
pub write_lockfile: bool,
pub prog_ref: Option<&'a InstallProgress>,
}
pub(super) async fn run_lockfile_only(input: LockfileOnlyInput<'_>) -> miette::Result<()> {
let LockfileOnlyInput {
cwd,
mode,
lockfile_dir,
lockfile_importer_key,
manifest,
parse_options,
manifests,
per_project_write_selection,
ws_config,
workspace_catalogs,
settings_ctx,
dependency_policy,
lockfile_pre_parse,
lockfile_conflict_marker_warning_emitted,
existing_for_resolver,
write_kind,
lockfile_enabled,
lockfile_include_tarball_url,
shared_workspace_lockfile,
has_workspace,
is_workspace_project,
ignore_pnpmfile,
network_mode,
global_pnpmfile,
pnpmfile,
minimum_release_age_override,
ws_package_versions,
ignore_scripts,
write_lockfile,
prog_ref,
} = input;
let force_resolve = matches!(mode, FrozenMode::No);
let parsed_owned;
let parsed: Result<(&LockfileGraph, LockfileKind), &aube_lockfile::Error> =
if let Some((g, k)) = lockfile_pre_parse {
Ok((g, *k))
} else {
parsed_owned = parse_lockfile_dir_remapped_with_kind_and_options(
lockfile_dir,
lockfile_importer_key,
manifest,
parse_options,
);
match &parsed_owned {
Ok((g, k)) => Ok((g, *k)),
Err(e) => Err(e),
}
};
if let Err(e) = parsed
&& !matches!(e, aube_lockfile::Error::NotFound(_))
{
if active_lockfile_has_conflict_markers(lockfile_dir) {
if !lockfile_conflict_marker_warning_emitted {
warn_lockfile_conflict_markers(lockfile_dir, e);
}
} else {
match parse_lockfile_dir_remapped_with_kind_and_options(
lockfile_dir,
lockfile_importer_key,
manifest,
parse_options,
) {
Ok(_) => {
return Err(miette!("failed to parse lockfile: {e}"));
}
Err(owned) => {
return Err(miette::Report::new(owned)).wrap_err("failed to parse lockfile");
}
}
}
}
let fresh = !force_resolve
&& match parsed {
Ok((g, k)) => matches!(check_patch_drift(cwd, g, k)?, DriftStatus::Fresh),
Err(_) => true,
}
&& matches!(
parsed,
Ok((g, k))
if matches!(
g.check_drift_workspace_for_kind(
manifests,
&ws_config.overrides,
&ws_config.ignored_optional_dependencies,
workspace_catalogs,
is_workspace_project,
k,
),
DriftStatus::Fresh,
)
&& matches!(g.check_catalogs_drift(workspace_catalogs), DriftStatus::Fresh)
);
if fresh {
tracing::debug!("--lockfile-only: lockfile already up to date");
if let Some(p) = prog_ref {
p.finish(true, crate::progress::TtyFinishBehavior::Preserve);
}
if !write_lockfile {
super::control::output(
super::InstallOutputLevel::Info,
None,
"Dry run: lockfile is up to date; fetch/link steps were not run and node_modules were not modified",
);
return Ok(());
}
super::control::output(
super::InstallOutputLevel::Info,
None,
"Lockfile is up to date, resolution step is skipped",
);
return Ok(());
}
if let Some(p) = prog_ref {
p.set_phase("resolving");
}
super::control::check_cancelled()?;
let client =
std::sync::Arc::new(crate::commands::make_client(cwd).with_network_mode(network_mode));
let pnpmfile_paths = if ignore_pnpmfile {
Vec::new()
} else {
crate::pnpmfile::ordered_paths(
crate::pnpmfile::detect_global(cwd, global_pnpmfile).as_deref(),
crate::pnpmfile::detect(cwd, pnpmfile, ws_config.pnpmfile_path.as_deref()).as_deref(),
)
};
crate::commands::run_pnpmfile_pre_resolution(&pnpmfile_paths, cwd, existing_for_resolver)
.await?;
super::control::check_cancelled()?;
let (read_package_host, read_package_forwarders) =
match crate::pnpmfile::ReadPackageHostChain::spawn(&pnpmfile_paths, cwd)
.await
.wrap_err("failed to start pnpmfile readPackage host")?
{
Some((h, f)) => (Some(h), f),
None => (None, Vec::new()),
};
let read_package_hook: Option<Box<dyn aube_resolver::ReadPackageHook>> =
read_package_host.map(|h| Box::new(h) as Box<dyn aube_resolver::ReadPackageHook>);
let mut resolver = configure_resolver(
aube_resolver::Resolver::new(client.clone()),
cwd,
manifest,
ResolverConfigInputs {
settings_ctx,
workspace_config: ws_config,
workspace_catalogs,
minimum_release_age_override,
target_lockfile_kind: lockfile_enabled.then_some(write_kind),
dependency_policy: dependency_policy.clone(),
cache_full_packuments: true,
ignore_scripts,
},
read_package_hook,
);
let mut graph = if has_workspace {
resolver
.resolve_workspace(manifests, existing_for_resolver, ws_package_versions)
.await
} else {
resolver.resolve(manifest, existing_for_resolver).await
}
.map_err(miette::Report::new)
.wrap_err("failed to resolve dependencies")?;
drop(resolver);
crate::pnpmfile::ReadPackageHostChain::drain_forwarders(read_package_forwarders).await;
crate::pnpmfile::run_after_all_resolved_chain(&pnpmfile_paths, cwd, &mut graph).await?;
if lockfile_include_tarball_url {
let lo_client = client.as_ref();
graph.settings.lockfile_include_tarball_url = true;
for pkg in graph.packages.values_mut() {
if pkg.local_source.is_some() {
continue;
}
if pkg.tarball_url.is_none() {
pkg.tarball_url = Some(lo_client.tarball_url(pkg.registry_name(), &pkg.version));
}
}
}
let lo_write_kind = write_kind;
if matches!(lo_write_kind, LockfileKind::Pnpm) {
graph.patched_dependencies = crate::patches::read_patched_dependencies(cwd)?;
}
crate::runtime::refresh_lockfile_pin(
&mut graph,
manifest,
crate::runtime::RuntimeSettings::from_ctx(settings_ctx),
lo_write_kind,
)
.await?;
let lo_local_pnpmfile = if ignore_pnpmfile {
None
} else {
crate::pnpmfile::detect(cwd, pnpmfile, ws_config.pnpmfile_path.as_deref())
};
stamp_pnpm_config_checksums(
&mut graph,
lo_write_kind,
manifest,
settings_ctx,
lo_local_pnpmfile.as_deref(),
)
.await;
crate::commands::prepare_resolved_graph_for_lockfile_write(&mut graph);
if !write_lockfile {
if let Some(p) = prog_ref {
p.finish(true, crate::progress::TtyFinishBehavior::Preserve);
}
super::control::output(
super::InstallOutputLevel::Info,
None,
format!(
"Dry run: resolved {} package(s); lockfile and node_modules were not modified",
graph.packages.len()
),
);
return Ok(());
}
if shared_workspace_lockfile || !has_workspace {
let lo_written = write_lockfile_dir_remapped(
lockfile_dir,
lockfile_importer_key,
&graph,
manifest,
lo_write_kind,
)
.into_diagnostic()
.wrap_err("failed to write lockfile")?;
tracing::debug!(
"--lockfile-only: wrote {}",
lo_written
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| lo_written.display().to_string())
);
} else {
write_per_project_lockfiles(
cwd,
&graph,
manifests,
lo_write_kind,
per_project_write_selection,
)?;
}
maybe_cleanup_unused_catalogs(cwd, settings_ctx, workspace_catalogs, &graph.catalogs)?;
if let Some(p) = prog_ref {
p.finish(true, crate::progress::TtyFinishBehavior::Preserve);
}
super::control::output(
super::InstallOutputLevel::Info,
None,
format!(
"Lockfile written ({} packages); skipped node_modules linking",
graph.packages.len()
),
);
Ok(())
}
pub(super) struct SelectLockfileInput<'a> {
pub lockfile_enabled: bool,
pub mode: FrozenMode,
pub cwd: &'a Path,
pub lockfile_dir: &'a Path,
pub lockfile_importer_key: &'a str,
pub manifest: &'a aube_manifest::PackageJson,
pub parse_options: aube_lockfile::ParseOptions,
pub manifests: &'a [(String, aube_manifest::PackageJson)],
pub ws_config: &'a aube_manifest::workspace::WorkspaceConfig,
pub workspace_catalogs: &'a crate::commands::CatalogMap,
pub is_workspace_project: bool,
pub lockfile_pre_parse: Option<&'a (LockfileGraph, LockfileKind)>,
}
pub(super) fn select_lockfile_result(
input: SelectLockfileInput<'_>,
) -> miette::Result<Result<(LockfileGraph, LockfileKind), aube_lockfile::Error>> {
let SelectLockfileInput {
lockfile_enabled,
mode,
cwd,
lockfile_dir,
lockfile_importer_key,
manifest,
parse_options,
manifests,
ws_config,
workspace_catalogs,
is_workspace_project,
lockfile_pre_parse,
} = input;
if !lockfile_enabled {
tracing::debug!("lockfile=false: skipping lockfile parse, re-resolving");
return Ok(Err(aube_lockfile::Error::NotFound(cwd.to_path_buf())));
}
match mode {
FrozenMode::No => Ok(Err(aube_lockfile::Error::NotFound(cwd.to_path_buf()))),
FrozenMode::Fix => Ok(Err(aube_lockfile::Error::NotFound(cwd.to_path_buf()))),
FrozenMode::Frozen => {
let parsed = parse_lockfile_dir_remapped_with_kind_and_options(
lockfile_dir,
lockfile_importer_key,
manifest,
parse_options,
);
if let Ok((ref graph, kind)) = parsed {
if let DriftStatus::Stale { reason } =
graph.check_catalogs_drift(workspace_catalogs)
{
return Err(miette!(
"lockfile is out of date with pnpm-workspace.yaml: {reason}\n\
help: run without --frozen-lockfile to update the lockfile"
));
}
if let DriftStatus::Stale { reason } = check_patch_drift(cwd, graph, kind)? {
return Err(miette!(
code = aube_codes::errors::ERR_AUBE_LOCKFILE_CONFIG_MISMATCH,
"lockfile is out of date with patchedDependencies: {reason}\n\
help: run without --frozen-lockfile to update the lockfile"
));
}
if let DriftStatus::Stale { reason } = graph.check_drift_workspace_for_kind(
manifests,
&ws_config.overrides,
&ws_config.ignored_optional_dependencies,
workspace_catalogs,
is_workspace_project,
kind,
) {
return Err(miette!(
"lockfile is out of date with package.json: {reason}\n\
help: run without --frozen-lockfile to update the lockfile, \
or run `{} --no-frozen-lockfile` to regenerate it",
aube_util::cmd("install")
));
}
}
Ok(parsed)
}
FrozenMode::Prefer => {
match lockfile_pre_parse {
Some((graph, kind)) => {
if let DriftStatus::Stale { reason } =
graph.check_catalogs_drift(workspace_catalogs)
{
tracing::debug!(
"Lockfile out of date with workspace catalogs ({reason}), re-resolving..."
);
Ok(Err(aube_lockfile::Error::NotFound(cwd.to_path_buf())))
} else if let DriftStatus::Stale { reason } =
check_patch_drift(cwd, graph, *kind)?
{
tracing::debug!(
"Lockfile out of date with patchedDependencies ({reason}), re-resolving..."
);
Ok(Err(aube_lockfile::Error::NotFound(cwd.to_path_buf())))
} else {
match graph.check_drift_workspace_for_kind(
manifests,
&ws_config.overrides,
&ws_config.ignored_optional_dependencies,
workspace_catalogs,
is_workspace_project,
*kind,
) {
DriftStatus::Fresh => Ok(Ok((graph.clone(), *kind))),
DriftStatus::Stale { reason } => {
tracing::debug!("Lockfile out of date ({reason}), re-resolving...");
Ok(Err(aube_lockfile::Error::NotFound(cwd.to_path_buf())))
}
}
}
}
None => Ok(Err(aube_lockfile::Error::NotFound(cwd.to_path_buf()))),
}
}
}
}
pub(crate) fn check_patch_drift(
cwd: &Path,
graph: &LockfileGraph,
kind: LockfileKind,
) -> miette::Result<DriftStatus> {
if !matches!(kind, LockfileKind::Pnpm) {
return Ok(DriftStatus::Fresh);
}
Ok(
match crate::patches::pnpm_patch_hash_drift(cwd, &graph.patched_dependencies)? {
Some(reason) => DriftStatus::Stale { reason },
None => DriftStatus::Fresh,
},
)
}
fn active_lockfile_has_conflict_markers(lockfile_dir: &Path) -> bool {
aube_lockfile::active_lockfile_has_conflict_markers(lockfile_dir)
}
fn warn_lockfile_conflict_markers(lockfile_dir: &Path, err: &aube_lockfile::Error) {
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_LOCKFILE_CONFLICT_MARKERS,
"lockfile in {} contains Git conflict markers; regenerating from package.json ({err})",
lockfile_dir.display()
);
}
pub(super) fn apply_lockfile_graph_platform_rules(
mut graph: LockfileGraph,
kind: LockfileKind,
manifest: &aube_manifest::PackageJson,
ws_config: &aube_manifest::workspace::WorkspaceConfig,
settings_ctx: &aube_settings::ResolveCtx<'_>,
) -> miette::Result<LockfileGraph> {
let (sup_os, sup_cpu, sup_libc) =
aube_manifest::effective_supported_architectures(manifest, ws_config);
let supported_architectures = aube_resolver::SupportedArchitectures {
os: sup_os,
cpu: sup_cpu,
libc: sup_libc,
..Default::default()
};
let ignored_optional_deps =
aube_manifest::effective_ignored_optional_dependencies(manifest, ws_config);
let needs_peer_pass = matches!(
kind,
LockfileKind::Npm | LockfileKind::NpmShrinkwrap | LockfileKind::Bun
);
let mut hoist_elapsed: Option<std::time::Duration> = None;
if needs_peer_pass {
let hoist_start = std::time::Instant::now();
graph = aube_resolver::hoist_auto_installed_peers(graph);
hoist_elapsed = Some(hoist_start.elapsed());
}
aube_resolver::platform::filter_graph(
&mut graph,
&supported_architectures,
&ignored_optional_deps,
);
if let Some(hoist_elapsed) = hoist_elapsed {
let peer_options = aube_resolver::PeerContextOptions {
dedupe_peer_dependents: super::settings::resolve_dedupe_peer_dependents(settings_ctx),
dedupe_peers: super::settings::resolve_dedupe_peers(settings_ctx),
resolve_from_workspace_root: super::settings::resolve_peers_from_workspace_root(
settings_ctx,
),
peers_suffix_max_length: super::settings::resolve_peers_suffix_max_length(settings_ctx),
};
let pkgs_before = graph.packages.len();
let apply_start = std::time::Instant::now();
graph = aube_resolver::apply_peer_contexts(graph, &peer_options)
.map_err(|e| miette!("peer-context pass failed: {e}"))?;
tracing::debug!(
"peer-context pass (lockfile={:?}) {} → {} packages in {:.1?}",
kind,
pkgs_before,
graph.packages.len(),
hoist_elapsed + apply_start.elapsed()
);
}
Ok(graph)
}
pub(super) fn lockfile_source_label(kind: LockfileKind) -> &'static str {
match kind {
LockfileKind::Aube => "Lockfile",
LockfileKind::Pnpm => "pnpm-lock.yaml",
LockfileKind::Yarn | LockfileKind::YarnBerry => "yarn.lock",
LockfileKind::Npm => "package-lock.json",
LockfileKind::NpmShrinkwrap => "npm-shrinkwrap.json",
LockfileKind::Bun => "bun.lock",
}
}