use std::path::{Path, PathBuf};
use cabin_core::PackageName;
use cabin_lockfile::Lockfile;
use cabin_resolver::{
LockedVersion, ResolveInput, ResolveMode, ResolveOutput, ResolvedPackage, ResolvedSource,
};
use super::{
ArtifactPipelineRequest, BTreeSet, Context, FetchArgs, LockMode, Reporter, ResolveArgs,
ResolveFormat, Result, UpdateArgs, WorkspaceSelectionArgsForUpdate, absolutise, bail,
build_selection_request, build_workspace_selection, cache_dir_for,
closure_has_versioned_deps_excluding_patches, collect_closure_versioned_deps_excluding_patches,
collect_patched_versioned_deps, compute_feature_resolution, emit_fetch_output,
lock_mode_for_flags, lockfile_from_resolution, lockfile_path_for, merge_versioned_deps,
resolve_invocation_manifest, run_artifact_pipeline, selected_resolution_packages,
};
pub(super) fn resolve(args: &ResolveArgs, reporter: Reporter) -> Result<()> {
let mode = lock_mode_for_flags(args.locked, args.frozen);
let allow_write = !(args.locked || args.frozen);
if args.frozen && args.index_url.is_some() {
bail!(
"cannot use --index-url with --frozen: there is no persistent HTTP index metadata cache, so a frozen run would have to perform network fetches it is not allowed to perform"
);
}
let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
let workspace_selection = build_workspace_selection(&args.workspace_selection);
let selection_request =
build_selection_request(&args.features, args.all_features, args.no_default_features);
run_resolution(
&ResolutionRequest {
manifest_path: &manifest_path,
index_path: args.index_path.as_deref(),
index_url: args.index_url.as_deref(),
format: args.format,
mode,
allow_write,
frozen: args.frozen,
update_package: None,
selection: workspace_selection,
selection_request,
no_patches: args.no_patches,
offline: args.offline,
},
reporter,
)
}
pub(super) fn update(args: &UpdateArgs, reporter: Reporter) -> Result<()> {
let mode = match &args.package {
Some(name) => LockMode::UpdatePackage(name.clone()),
None => LockMode::UpdateAll,
};
let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
let workspace_selection = build_update_workspace_selection(&args.workspace_selection);
run_resolution(
&ResolutionRequest {
manifest_path: &manifest_path,
index_path: args.index_path.as_deref(),
index_url: args.index_url.as_deref(),
format: args.format,
mode,
allow_write: true,
frozen: false,
update_package: args.package.as_deref(),
selection: workspace_selection,
selection_request: cabin_core::SelectionRequest::default(),
no_patches: args.no_patches,
offline: args.offline,
},
reporter,
)
}
pub(super) fn build_update_workspace_selection(
args: &WorkspaceSelectionArgsForUpdate,
) -> cabin_workspace::PackageSelection {
use cabin_workspace::SelectionMode;
let mode = if args.workspace {
SelectionMode::WholeWorkspace
} else if args.default_members {
SelectionMode::DefaultMembers
} else {
SelectionMode::CurrentPackage
};
cabin_workspace::PackageSelection {
mode,
exclude: args.exclude.clone(),
}
}
pub(super) fn fetch(args: &FetchArgs, reporter: Reporter) -> Result<()> {
let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
let offline_pre = crate::cli::config::effective_offline(args.offline)?;
let fetch_selection = build_workspace_selection(&args.workspace_selection);
let (_port_sources, initial_graph) = crate::cli::port::prepare_ports_and_load_initial_graph(
&manifest_path,
args.cache_dir.as_deref(),
offline_pre,
args.frozen,
false,
&fetch_selection,
args.no_patches,
)?;
let effective_config = crate::cli::config::load_effective_config(&initial_graph)?;
let active_patches =
crate::cli::patch::load_active_patches(&initial_graph, &effective_config, args.no_patches)?;
let patched_names = active_patches.owned_patched_names();
let workspace_selection = build_workspace_selection(&args.workspace_selection);
let resolved_selection =
cabin_workspace::resolve_package_selection(&initial_graph, &workspace_selection)?;
let initial_features = compute_feature_resolution(
&initial_graph,
&resolved_selection,
&cabin_core::SelectionRequest::default(),
)?;
let dev_for: BTreeSet<String> = BTreeSet::new();
let patched_root_deps_preview =
collect_patched_versioned_deps(&active_patches, &patched_names)?;
if patched_root_deps_preview.is_empty()
&& !closure_has_versioned_deps_excluding_patches(
&initial_graph,
&resolved_selection,
&initial_features,
&patched_names,
&dev_for,
)
{
emit_fetch_output(
&[],
args.format,
&cache_dir_for(&manifest_path, args.cache_dir.as_deref()).unwrap_or_default(),
&manifest_path,
)?;
return Ok(());
}
let resolved_index_source = crate::cli::config::resolve_index_source(
args.index_path.as_deref(),
args.index_url.as_deref(),
&effective_config,
)?;
let fetch_offline = crate::cli::config::effective_offline(args.offline)?;
crate::cli::config::enforce_offline_index_source(
fetch_offline,
resolved_index_source.as_ref(),
)?;
let resolved_cache_dir =
crate::cli::config::resolve_cache_dir(args.cache_dir.as_deref(), &effective_config);
let Some(index_source) = resolved_index_source.as_ref() else {
bail!(
"versioned dependencies require --index-path, --index-url, or a `[registry]` config setting"
);
};
let inputs = crate::cli::config::resolve_pipeline_inputs(
index_source,
&effective_config,
&manifest_path,
args.cache_dir.as_deref(),
resolved_cache_dir.as_ref(),
fetch_offline,
args.locked,
args.frozen,
args.no_patches,
false,
)?;
let fetch_request = cabin_core::SelectionRequest::default();
let pipeline = run_artifact_pipeline(&ArtifactPipelineRequest {
manifest_path: &manifest_path,
initial_graph: &initial_graph,
index_path: inputs.index_path.as_deref(),
index_url: inputs.index_url.as_deref(),
mode: inputs.mode,
allow_write: inputs.allow_write,
frozen: args.frozen,
cache_dir: &inputs.cache_dir,
reporter,
selection: workspace_selection,
selection_request: &fetch_request,
patched_names: &patched_names,
active_patches: &active_patches,
source_replacements: &effective_config.source_replacements,
no_patches: args.no_patches,
dev_for: &dev_for,
})?;
emit_fetch_output(
&pipeline.fetched,
args.format,
&inputs.cache_dir,
&manifest_path,
)?;
Ok(())
}
struct ResolutionRequest<'a> {
manifest_path: &'a Path,
index_path: Option<&'a Path>,
index_url: Option<&'a str>,
format: ResolveFormat,
mode: LockMode,
allow_write: bool,
frozen: bool,
update_package: Option<&'a str>,
selection: cabin_workspace::PackageSelection,
selection_request: cabin_core::SelectionRequest,
no_patches: bool,
offline: bool,
}
fn run_resolution(request: &ResolutionRequest<'_>, reporter: Reporter) -> Result<()> {
let manifest_path = absolutise(request.manifest_path)
.with_context(|| format!("failed to resolve {}", request.manifest_path.display()))?;
let offline = crate::cli::config::effective_offline(request.offline)?;
let (_port_sources, graph) = crate::cli::port::prepare_ports_and_load_initial_graph(
&manifest_path,
None,
offline,
request.frozen,
false,
&request.selection,
request.no_patches,
)?;
let effective_config = crate::cli::config::load_effective_config(&graph)?;
let active_patches =
crate::cli::patch::load_active_patches(&graph, &effective_config, request.no_patches)?;
let patched_names = active_patches.owned_patched_names();
let resolved_index_source = crate::cli::config::resolve_index_source(
request.index_path,
request.index_url,
&effective_config,
)?;
let resolution_offline = crate::cli::config::effective_offline(request.offline)?;
crate::cli::config::enforce_offline_index_source(
resolution_offline,
resolved_index_source.as_ref(),
)?;
let (config_index_path, config_index_url): (Option<PathBuf>, Option<String>) =
match resolved_index_source.as_ref() {
Some(source) => {
let initial = crate::cli::config::index_source_kind_to_locator(&source.kind);
let resolved = crate::cli::patch::apply_source_replacement(
initial,
&effective_config,
request.no_patches,
)?;
crate::cli::config::enforce_offline_post_replacement(
resolution_offline,
&resolved,
)?;
crate::cli::patch::locator_to_index_inputs(&resolved.resolved)
}
None => (None, None),
};
let effective_index_path = config_index_path.as_deref();
let effective_index_url = config_index_url.as_deref();
if request.frozen && effective_index_url.is_some() {
bail!(
"cannot use --index-url with --frozen: there is no persistent HTTP index metadata cache, so a frozen run would have to perform network fetches it is not allowed to perform"
);
}
let resolved_selection = selected_resolution_packages(&graph, &request.selection)?;
let features =
compute_feature_resolution(&graph, &resolved_selection, &request.selection_request)?;
let dev_for: BTreeSet<String> = BTreeSet::new();
let mut root_deps = collect_closure_versioned_deps_excluding_patches(
&graph,
&resolved_selection,
&features,
&patched_names,
&dev_for,
)?;
let patched_root_deps = collect_patched_versioned_deps(&active_patches, &patched_names)?;
merge_versioned_deps(&mut root_deps, patched_root_deps)?;
let (root_name, root_version) = match graph.root_package {
Some(idx) => (
graph.packages[idx].package.name.clone(),
graph.packages[idx].package.version.clone(),
),
None => cabin_workspace::synthetic_root_identity(&graph),
};
let lockfile_path = lockfile_path_for(&manifest_path);
if let Some(name) = request.update_package
&& !root_deps.contains_key(
&PackageName::new(name)
.map_err(|err| anyhow::anyhow!("invalid --package value {name:?}: {err}"))?,
)
{
bail!(
"package {name:?} is not a direct versioned dependency of `{}`; `cabin update --package` only refreshes direct dependencies declared under `[dependencies]`",
root_name.as_str(),
);
}
let existing_lockfile: Option<Lockfile> = if lockfile_path.is_file() {
Some(
cabin_lockfile::read_lockfile(&lockfile_path)
.with_context(|| format!("failed to read {}", lockfile_path.display()))?,
)
} else {
None
};
let active_patch_records = crate::cli::patch::lockfile_patches(&active_patches);
let active_replacement_records = crate::cli::patch::lockfile_source_replacements(
&effective_config.source_replacements,
request.no_patches,
);
if matches!(request.mode, LockMode::Locked)
&& let Some(prev) = &existing_lockfile
&& !prev.matches_patch_state(&active_patch_records, &active_replacement_records)
{
bail!(
"--locked cannot be used because active patch / source-replacement policy differs from {}; re-run without --locked to refresh the lockfile",
lockfile_path.display()
);
}
if root_deps.is_empty() {
let output = ResolveOutput {
packages: vec![ResolvedPackage {
name: root_name,
version: root_version,
source: ResolvedSource::Root,
}],
};
emit_resolve_output(&output, request.format)?;
return Ok(());
}
if existing_lockfile.is_none() && matches!(request.mode, LockMode::Locked) {
bail!(
"cannot resolve with --locked because {} does not exist",
lockfile_path.display()
);
}
let index = match (effective_index_path, effective_index_url) {
(None, None) => {
bail!(
"versioned dependencies require --index-path, --index-url, or a `[registry]` config setting"
)
}
(Some(path), None) => {
let index_path = absolutise(path)
.with_context(|| format!("failed to resolve {}", path.display()))?;
cabin_index::load_index(&index_path)
.with_context(|| format!("failed to load index at {}", index_path.display()))?
}
(None, Some(url)) => {
let client = cabin_index_http::HttpClient::new();
let http_index = cabin_index_http::HttpIndex::open(url, client)?;
let names: Vec<PackageName> = root_deps.keys().cloned().collect();
http_index.load_package_index(&names)?
}
(Some(_), Some(_)) => {
unreachable!("cli::config::resolve_index_source guarantees only one variant is set")
}
};
let resolver_mode = match &request.mode {
LockMode::PreferLocked => ResolveMode::PreferLocked,
LockMode::Locked => ResolveMode::Locked,
LockMode::UpdateAll => ResolveMode::UpdateAll,
LockMode::UpdatePackage(name) => ResolveMode::UpdatePackage(
PackageName::new(name.clone())
.map_err(|err| anyhow::anyhow!("invalid --package value {name:?}: {err}"))?,
),
};
let mut input = ResolveInput::new(root_name, root_version, root_deps);
if let Some(lock) = &existing_lockfile {
for pkg in &lock.packages {
input.locked.insert(
pkg.name.clone(),
LockedVersion {
version: pkg.version.clone(),
checksum: pkg.checksum.clone(),
},
);
}
}
input.mode = resolver_mode;
let output = cabin_resolver::resolve(&input, &index).context("dependency resolution failed")?;
let mut new_lockfile = lockfile_from_resolution(&output, &index);
new_lockfile.patches = active_patch_records;
new_lockfile.source_replacements = active_replacement_records;
if request.allow_write {
let needs_write = match &existing_lockfile {
Some(prev) => prev != &new_lockfile,
None => true,
};
if needs_write {
cabin_lockfile::write_lockfile(&lockfile_path, &new_lockfile)
.with_context(|| format!("failed to write {}", lockfile_path.display()))?;
reporter.aux_verbose(format_args!("cabin: wrote {}", lockfile_path.display()));
} else {
reporter.aux_verbose(format_args!(
"cabin: {} is up to date",
lockfile_path.display()
));
}
} else if matches!(request.mode, LockMode::Locked)
&& let Some(prev) = &existing_lockfile
&& prev != &new_lockfile
{
bail!(
"{} is stale; run `cabin resolve` or `cabin update` to refresh it",
lockfile_path.display()
);
}
emit_resolve_output(&output, request.format)?;
Ok(())
}
fn emit_resolve_output(output: &ResolveOutput, format: ResolveFormat) -> Result<()> {
match format {
ResolveFormat::Human => print_resolve_human(output),
ResolveFormat::Json => print_resolve_json(output),
}
}
fn print_resolve_human(output: &ResolveOutput) -> Result<()> {
let root = output
.packages
.iter()
.find(|p| p.source == ResolvedSource::Root)
.ok_or_else(|| anyhow::anyhow!("resolver output is missing a root package"))?;
println!(
"Resolved dependencies for {} {}:",
root.name.as_str(),
root.version
);
let mut others: Vec<&cabin_resolver::ResolvedPackage> = output
.packages
.iter()
.filter(|p| p.source != ResolvedSource::Root)
.collect();
others.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
if others.is_empty() {
println!(" (no versioned dependencies)");
} else {
for pkg in others {
println!(" {} {}", pkg.name.as_str(), pkg.version);
}
}
Ok(())
}
fn print_resolve_json(output: &ResolveOutput) -> Result<()> {
let root = output
.packages
.iter()
.find(|p| p.source == ResolvedSource::Root)
.ok_or_else(|| anyhow::anyhow!("resolver output is missing a root package"))?;
let json_root = serde_json::json!({
"name": root.name.as_str(),
"version": root.version.to_string(),
});
let json_packages: Vec<_> = output
.packages
.iter()
.filter(|p| p.source != ResolvedSource::Root)
.map(|p| {
serde_json::json!({
"name": p.name.as_str(),
"version": p.version.to_string(),
"source": p.source.as_str(),
})
})
.collect();
let value = serde_json::json!({
"root": json_root,
"packages": json_packages,
});
crate::print_pretty_json(&value, "failed to serialize resolve output as JSON")
}