use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use clap::{Args, Parser, Subcommand};
use cabin_artifact::{ArtifactCache, FetchEntry, FetchOptions, FetchPlan, FetchedPackage};
use cabin_build::{PlanRequest, plan};
use cabin_core::PackageName;
use cabin_index::PackageIndex;
use cabin_lockfile::{LockedPackage, LockedSource, Lockfile};
use cabin_package::scaffold;
use cabin_resolver::{
LockedVersion, ResolveInput, ResolveMode, ResolveOutput, ResolvedPackage, ResolvedSource,
};
use cabin_workspace::{PackageGraph, RegistryPackageSource, collect_patched_versioned_deps};
use crate::completions::CompgenArgs;
use crate::fetch_output_glue::emit_fetch_output;
use crate::manpages::MangenArgs;
use crate::metadata_glue::{MetadataInputs, MetadataView};
use crate::term_color_glue::CliColorChoice;
use crate::term_verbosity_glue::Reporter;
fn cli_styles() -> clap::builder::Styles {
use clap::builder::styling::{AnsiColor, Color, Style};
let header_usage = Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::BrightGreen)));
let literal = Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::BrightCyan)));
let placeholder = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan)));
let error = Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::BrightRed)));
let invalid = Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::Yellow)));
let valid = Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::BrightGreen)));
clap::builder::Styles::styled()
.usage(header_usage)
.header(header_usage)
.literal(literal)
.placeholder(placeholder)
.error(error)
.invalid(invalid)
.valid(valid)
}
const HELP_TEMPLATE: &str = concat!(
"{about-with-newline}\n",
"{usage-heading} {usage}\n",
"\n",
"\x1b[1m\x1b[92mOptions:\x1b[0m\n",
"{options}",
"{after-help}",
);
#[derive(Debug, Parser)]
#[command(
name = "cabin",
about = "A package manager and build system for C/C++",
disable_version_flag = true,
styles = cli_styles(),
help_template = HELP_TEMPLATE,
// Compact, cargo-style option rows: keep the description
// inline with the flag name rather than dropping it to
// its own line for every entry.
next_line_help = false,
)]
pub struct Cli {
#[arg(
short = 'v',
long = "verbose",
global = true,
action = clap::ArgAction::Count,
conflicts_with = "quiet",
display_order = 1,
)]
pub(crate) verbose: u8,
#[arg(
short = 'q',
long = "quiet",
global = true,
conflicts_with = "verbose",
display_order = 2
)]
pub(crate) quiet: bool,
#[arg(
long,
value_name = "WHEN",
value_enum,
global = true,
hide_possible_values = true,
display_order = 3
)]
pub(crate) color: Option<CliColorChoice>,
#[arg(long, display_order = 4)]
pub(crate) list: bool,
#[arg(
short = 'V',
long = "version",
global = true,
action = clap::ArgAction::SetTrue,
display_order = 6,
)]
pub(crate) version: bool,
#[command(subcommand)]
pub(crate) command: Option<Command>,
}
#[derive(Debug, Subcommand)]
pub(crate) enum Command {
Init(InitArgs),
New(NewArgs),
#[command(hide = true)]
Metadata(ManifestArgs),
#[command(visible_alias = "b")]
Build(BuildArgs),
Clean(CleanArgs),
#[command(visible_alias = "r")]
Run(crate::run_glue::RunArgs),
#[command(visible_alias = "t")]
Test(crate::test_glue::TestArgs),
#[command(hide = true)]
Resolve(ResolveArgs),
Update(UpdateArgs),
#[command(hide = true)]
Fetch(FetchArgs),
#[command(hide = true)]
Vendor(crate::vendor_glue::VendorArgs),
#[command(hide = true)]
Tree(crate::tree_glue::TreeArgs),
#[command(hide = true)]
Explain(crate::explain_glue::ExplainArgs),
#[command(hide = true)]
Package(PackageArgs),
Publish(PublishArgs),
Fmt(crate::fmt_glue::FmtArgs),
Tidy(crate::tidy_glue::TidyArgs),
Port(crate::port_subcommand::PortArgs),
#[command(hide = true)]
Compgen(CompgenArgs),
#[command(hide = true)]
Mangen(MangenArgs),
Version(crate::version_glue::VersionArgs),
}
#[derive(Debug, Args)]
pub(crate) struct InitArgs {
#[arg(long)]
pub name: Option<String>,
#[arg(short = 'b', long, group = "init_scaffold_kind")]
pub bin: bool,
#[arg(short = 'l', long, group = "init_scaffold_kind")]
pub lib: bool,
}
#[derive(Debug, Args)]
pub(crate) struct NewArgs {
#[arg(value_name = "PATH")]
pub path: PathBuf,
#[arg(long)]
pub name: Option<String>,
#[arg(short = 'b', long, group = "new_scaffold_kind")]
pub bin: bool,
#[arg(short = 'l', long, group = "new_scaffold_kind")]
pub lib: bool,
}
#[derive(Debug, Args)]
pub(crate) struct CleanArgs {
#[arg(long, value_name = "PATH")]
pub manifest_path: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
pub build_dir: Option<PathBuf>,
#[arg(long, conflicts_with = "profile")]
pub release: bool,
#[arg(long, value_name = "NAME")]
pub profile: Option<String>,
#[arg(long)]
pub dry_run: bool,
#[command(flatten)]
pub workspace_selection: WorkspaceSelectionArgs,
}
#[derive(Debug, Args)]
pub(crate) struct ManifestArgs {
#[arg(long, value_name = "PATH")]
pub manifest_path: Option<PathBuf>,
#[command(flatten)]
pub selection: ConfigSelectionArgs,
#[command(flatten)]
pub workspace_selection: WorkspaceSelectionArgs,
#[arg(long, value_name = "FORMAT", default_value = "json")]
pub format: ResolveFormat,
#[arg(long, value_name = "NAME")]
pub profile: Option<String>,
#[command(flatten)]
pub toolchain: ToolchainSelectionArgs,
#[arg(long)]
pub no_patches: bool,
#[arg(long)]
pub offline: bool,
}
#[derive(Debug, Args)]
pub(crate) struct BuildArgs {
#[arg(long, value_name = "PATH")]
pub manifest_path: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
pub build_dir: Option<PathBuf>,
#[arg(short = 'r', long, conflicts_with = "profile")]
pub release: bool,
#[arg(long, value_name = "NAME")]
pub profile: Option<String>,
#[arg(long, value_name = "PATH")]
pub index_path: Option<PathBuf>,
#[arg(long, value_name = "URL")]
pub index_url: Option<String>,
#[arg(long, value_name = "PATH")]
pub cache_dir: Option<PathBuf>,
#[arg(long, conflicts_with = "frozen")]
pub locked: bool,
#[arg(long)]
pub frozen: bool,
#[arg(long)]
pub offline: bool,
#[arg(long, value_name = "FEATURES")]
pub features: Vec<String>,
#[arg(long)]
pub all_features: bool,
#[arg(long)]
pub no_default_features: bool,
#[command(flatten)]
pub workspace_selection: WorkspaceSelectionArgs,
#[command(flatten)]
pub toolchain: ToolchainSelectionArgs,
#[arg(long)]
pub no_patches: bool,
#[arg(short = 'j', long = "jobs", value_name = "N")]
pub jobs: Option<cabin_core::BuildJobs>,
}
#[derive(Debug, Args, Default)]
pub(crate) struct ToolchainSelectionArgs {
#[arg(long, value_name = "PATH-OR-NAME")]
pub cc: Option<String>,
#[arg(long, value_name = "PATH-OR-NAME")]
pub cxx: Option<String>,
#[arg(long, value_name = "PATH-OR-NAME")]
pub ar: Option<String>,
#[arg(long, value_name = "WRAPPER", conflicts_with = "no_compiler_wrapper")]
pub compiler_wrapper: Option<String>,
#[arg(long)]
pub no_compiler_wrapper: bool,
}
#[derive(Debug, Args, Default)]
pub(crate) struct ConfigSelectionArgs {
#[arg(long, value_name = "FEATURES")]
pub features: Vec<String>,
#[arg(long)]
pub all_features: bool,
#[arg(long)]
pub no_default_features: bool,
}
#[derive(Debug, Args, Default)]
pub(crate) struct WorkspaceSelectionArgsForUpdate {
#[arg(long, conflicts_with = "default_members")]
pub workspace: bool,
#[arg(long, conflicts_with = "workspace")]
pub default_members: bool,
#[arg(long, value_name = "PACKAGE")]
pub exclude: Vec<String>,
}
#[derive(Debug, Args, Default)]
pub(crate) struct WorkspaceSelectionArgs {
#[arg(
long,
conflicts_with_all = &["package", "default_members"],
)]
pub workspace: bool,
#[arg(long = "package", short = 'p', value_name = "PACKAGE")]
pub package: Vec<String>,
#[arg(long, conflicts_with_all = &["workspace", "package"])]
pub default_members: bool,
#[arg(long, value_name = "PACKAGE")]
pub exclude: Vec<String>,
}
#[derive(Debug, Args)]
pub(crate) struct FetchArgs {
#[arg(long, value_name = "PATH")]
pub manifest_path: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
pub index_path: Option<PathBuf>,
#[arg(long, value_name = "URL")]
pub index_url: Option<String>,
#[arg(long, value_name = "PATH")]
pub cache_dir: Option<PathBuf>,
#[arg(long, conflicts_with = "frozen")]
pub locked: bool,
#[arg(long)]
pub frozen: bool,
#[arg(long)]
pub offline: bool,
#[arg(long, value_name = "FORMAT", default_value = "human")]
pub format: ResolveFormat,
#[command(flatten)]
pub workspace_selection: WorkspaceSelectionArgs,
#[arg(long)]
pub no_patches: bool,
}
#[derive(Debug, Args)]
pub(crate) struct PackageArgs {
#[arg(long, value_name = "PATH")]
pub manifest_path: Option<PathBuf>,
#[arg(long, default_value = "dist")]
pub output_dir: PathBuf,
#[arg(long, value_name = "FORMAT", default_value = "human")]
pub format: ResolveFormat,
#[command(flatten)]
pub workspace_selection: WorkspaceSelectionArgs,
}
#[derive(Debug, Args)]
pub(crate) struct PublishArgs {
#[arg(long, value_name = "PATH")]
pub manifest_path: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
pub output_dir: Option<PathBuf>,
#[arg(long)]
pub dry_run: bool,
#[arg(long, value_name = "PATH")]
pub registry_dir: Option<PathBuf>,
#[arg(long, value_name = "FORMAT", default_value = "human")]
pub format: ResolveFormat,
#[command(flatten)]
pub workspace_selection: WorkspaceSelectionArgs,
}
#[derive(Debug, Args)]
pub(crate) struct ResolveArgs {
#[arg(long, value_name = "PATH")]
pub manifest_path: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
pub index_path: Option<PathBuf>,
#[arg(long, value_name = "URL")]
pub index_url: Option<String>,
#[arg(long, value_name = "FORMAT", default_value = "human")]
pub format: ResolveFormat,
#[arg(long, conflicts_with = "frozen")]
pub locked: bool,
#[arg(long)]
pub frozen: bool,
#[arg(long)]
pub offline: bool,
#[command(flatten)]
pub workspace_selection: WorkspaceSelectionArgs,
#[arg(long, value_name = "FEATURES")]
pub features: Vec<String>,
#[arg(long)]
pub all_features: bool,
#[arg(long)]
pub no_default_features: bool,
#[arg(long)]
pub no_patches: bool,
}
#[derive(Debug, Args)]
pub(crate) struct UpdateArgs {
#[arg(long, value_name = "PATH")]
pub manifest_path: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
pub index_path: Option<PathBuf>,
#[arg(long, value_name = "URL")]
pub index_url: Option<String>,
#[arg(long, value_name = "NAME")]
pub package: Option<String>,
#[arg(long, value_name = "FORMAT", default_value = "human")]
pub format: ResolveFormat,
#[arg(long)]
pub offline: bool,
#[command(flatten)]
pub workspace_selection: WorkspaceSelectionArgsForUpdate,
#[arg(long)]
pub no_patches: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub(crate) enum ResolveFormat {
Human,
Json,
}
const MANIFEST_FILENAME: &str = scaffold::MANIFEST_FILENAME;
pub(crate) fn run(
cli: Cli,
reporter: Reporter,
color: cabin_core::ColorChoice,
) -> Result<std::process::ExitCode> {
use std::process::ExitCode;
if cli.version {
crate::version_glue::version(crate::version_glue::VersionArgs {}, reporter.verbosity())?;
return Ok(ExitCode::SUCCESS);
}
if cli.list {
let mut stdout =
termcolor::StandardStream::stdout(cabin_diagnostics::termcolor_choice(color));
crate::command_list::print_list(&mut stdout)?;
return Ok(ExitCode::SUCCESS);
}
let Some(command) = cli.command else {
let mut cmd = <Cli as clap::CommandFactory>::command();
cmd.print_help().context("failed to print top-level help")?;
return Ok(ExitCode::SUCCESS);
};
match command {
Command::Init(args) => init(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::New(args) => new(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::Metadata(args) => metadata(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::Build(args) => build(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::Clean(args) => clean(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::Run(args) => crate::run_glue::run(&args, reporter),
Command::Test(args) => crate::test_glue::test(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::Resolve(args) => resolve(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::Update(args) => update(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::Fetch(args) => fetch(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::Vendor(args) => {
crate::vendor_glue::vendor(&args, reporter).map(|()| ExitCode::SUCCESS)
}
Command::Tree(args) => crate::tree_glue::tree(&args).map(|()| ExitCode::SUCCESS),
Command::Explain(args) => {
crate::explain_glue::explain(&args, reporter).map(|()| ExitCode::SUCCESS)
}
Command::Package(args) => package(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::Publish(args) => publish(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::Fmt(args) => crate::fmt_glue::fmt(&args, reporter),
Command::Tidy(args) => crate::tidy_glue::tidy(&args, reporter),
Command::Port(args) => {
crate::port_subcommand::port(&args, reporter).map(|()| ExitCode::SUCCESS)
}
Command::Compgen(args) => crate::completions::run(&args).map(|()| ExitCode::SUCCESS),
Command::Mangen(args) => crate::manpages::run(&args).map(|()| ExitCode::SUCCESS),
Command::Version(args) => {
crate::version_glue::version(args, reporter.verbosity()).map(|()| ExitCode::SUCCESS)
}
}
}
fn scaffold_kind_from_flags(_bin: bool, lib: bool) -> scaffold::ScaffoldKind {
if lib {
scaffold::ScaffoldKind::Library
} else {
scaffold::ScaffoldKind::Binary
}
}
fn report_scaffold(reporter: Reporter, verb: &str, report: &scaffold::ScaffoldReport, dest: &Path) {
reporter.status(
verb,
format_args!(
"{kind} `{name}` package",
kind = report.kind.label(),
name = report.name.as_str(),
),
);
for created in &report.files_created {
let relative = created.strip_prefix(dest).unwrap_or(created);
reporter.verbose(format_args!(
"cabin: wrote {}",
relative.display().to_string().replace('\\', "/")
));
}
}
fn init(args: &InitArgs, reporter: Reporter) -> Result<()> {
let cwd = std::env::current_dir().context("failed to determine current directory")?;
let kind = scaffold_kind_from_flags(args.bin, args.lib);
let request = scaffold::ScaffoldRequest::new(&cwd)
.with_name(args.name.as_deref())
.with_kind(kind)
.with_gitignore(true);
let report = scaffold::scaffold(request)?;
report_scaffold(reporter, "Created", &report, &cwd);
Ok(())
}
fn new(args: &NewArgs, reporter: Reporter) -> Result<()> {
let target = args.path.clone();
if target.as_os_str().is_empty() {
bail!("destination path must not be empty");
}
if target.exists() {
bail!(
"destination {} already exists; use `cabin init` to initialize an existing directory",
target.display()
);
}
if let Some(parent) = target.parent()
&& !parent.as_os_str().is_empty()
&& !parent.is_dir()
{
bail!(
"parent directory {} does not exist; create it first or pass a path under an existing directory",
parent.display()
);
}
std::fs::create_dir(&target)
.with_context(|| format!("failed to create directory {}", target.display()))?;
let kind = scaffold_kind_from_flags(args.bin, args.lib);
let request = scaffold::ScaffoldRequest::new(&target)
.with_name(args.name.as_deref())
.with_kind(kind)
.with_gitignore(true);
match scaffold::scaffold(request) {
Ok(report) => {
report_scaffold(reporter, "Created", &report, &target);
Ok(())
}
Err(err) => {
let _ = std::fs::remove_dir_all(&target);
Err(err.into())
}
}
}
fn metadata(args: &ManifestArgs, reporter: Reporter) -> Result<()> {
let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
let metadata_selection = cabin_workspace::PackageSelection {
mode: cabin_workspace::SelectionMode::WholeWorkspace,
exclude: Vec::new(),
};
let port_prep = crate::port_glue::prepare_ports_and_load_initial_graph(
&manifest_path,
None,
true,
false,
false,
&metadata_selection,
args.no_patches,
);
let (prepared_ports, initial_graph) = match port_prep {
Ok(result) => result,
Err(err) if crate::port_glue::is_metadata_recoverable(&err) => (
Vec::new(),
cabin_workspace::load_workspace_skip_ports(&manifest_path)?,
),
Err(err) => return Err(err),
};
let port_sources: Vec<cabin_workspace::PortPackageSource> = prepared_ports
.iter()
.map(crate::port_glue::workspace_source)
.collect();
let effective_config = crate::config_glue::load_effective_config(&initial_graph)?;
let resolved_index_for_offline_check =
crate::config_glue::resolve_index_source(None, None, &effective_config)?;
let metadata_offline = crate::config_glue::effective_offline(args.offline)?;
crate::config_glue::enforce_offline_index_source(
metadata_offline,
resolved_index_for_offline_check.as_ref(),
)?;
let active_patches =
crate::patch_glue::load_active_patches(&initial_graph, &effective_config, args.no_patches)?;
let patched_sources = active_patches.workspace_sources();
let graph = crate::patch_glue::reload_for_patches(
&manifest_path,
initial_graph,
&patched_sources,
&port_sources,
)?;
let lockfile_path = lockfile_path_for(&manifest_path);
let lockfile = read_optional_lockfile(&lockfile_path)?;
let request = build_selection_request(
&args.selection.features,
args.selection.all_features,
args.selection.no_default_features,
);
let workspace_selection = build_workspace_selection(&args.workspace_selection);
let resolved_selection =
cabin_workspace::resolve_package_selection(&graph, &workspace_selection)?;
let _feature_resolution = compute_feature_resolution(&graph, &resolved_selection, &request)?;
let manifest_profiles = workspace_profile_definitions(&graph);
let profile_selection =
profile_selection_for_metadata(args.profile.as_deref(), &effective_config)?;
let profile = cabin_core::resolve_profile(&profile_selection, &manifest_profiles)
.map_err(|err| anyhow::anyhow!(err.to_string()))?;
let host_platform = cabin_core::TargetPlatform::current();
let toolchain_selection = toolchain_selection_from_args(&args.toolchain)?;
let toolchain = resolve_toolchain_layered(
&graph,
&toolchain_selection,
&effective_config,
&host_platform,
)?;
let detection_report =
match cabin_toolchain::detect_toolchain(&toolchain, &cabin_toolchain::ProcessRunner) {
Ok(report) => Some(report),
Err(err) => {
reporter.warning(format_args!("toolchain detection failed: {err}"));
None
}
};
let manifest_compiler_wrapper = workspace_compiler_wrapper_settings(&graph);
let cli_compiler_wrapper = compiler_wrapper_override_from_args(&args.toolchain)?;
let mut wrapper_inputs = cabin_toolchain::WrapperInputs::from_process(
cli_compiler_wrapper,
&manifest_compiler_wrapper,
&host_platform,
);
if let Some(layer) = crate::config_glue::wrapper_layer(&effective_config) {
wrapper_inputs = wrapper_inputs.with_config(layer);
}
let compiler_wrapper = match cabin_toolchain::resolve_compiler_wrapper(
&wrapper_inputs,
Some(&cabin_toolchain::ProcessRunner),
) {
Ok(w) => w,
Err(err) => {
reporter.warning(format_args!("compiler-wrapper resolution failed: {err}"));
None
}
};
let toolchain_summary =
cabin_core::ToolchainSummary::from_resolved_parts(&toolchain, compiler_wrapper.as_ref());
let profile_build = profile.build.as_ref();
let build_flags = resolve_per_package_build_flags(&graph, profile_build, &host_platform);
let dev_for: BTreeSet<String> = BTreeSet::new();
let build_flags = augment_build_flags(&graph, &host_platform, &dev_for, build_flags, reporter)?;
let configurations = resolve_build_configurations(
&graph,
&request,
&resolved_selection.packages,
&profile,
&toolchain_summary,
&build_flags,
)?;
let view = MetadataView::from_graph_and_lock(&MetadataInputs {
graph: &graph,
lockfile: lockfile.as_ref(),
lockfile_path: &lockfile_path,
configurations: &configurations,
selection: &resolved_selection,
profile: &profile,
manifest_profiles: &manifest_profiles,
toolchain: &toolchain,
build_flags: &build_flags,
detection: detection_report.as_ref(),
compiler_wrapper: compiler_wrapper.as_ref(),
config: &effective_config,
active_patches: &active_patches,
no_patches: args.no_patches,
ports: &prepared_ports,
});
match args.format {
ResolveFormat::Json => {
crate::print_pretty_json(&view, "failed to serialize metadata as JSON")?;
}
ResolveFormat::Human => {
for pkg in &view.packages {
println!(
"{} {} ({})",
pkg.name,
pkg.version,
if pkg.is_root {
"root"
} else if pkg.is_primary {
"primary"
} else {
"dep"
}
);
}
}
}
Ok(())
}
fn build(args: &BuildArgs, reporter: Reporter) -> Result<()> {
let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
let offline = crate::config_glue::effective_offline(args.offline)?;
let build_selection = build_workspace_selection(&args.workspace_selection);
let (prepared_ports, initial_graph) = crate::port_glue::prepare_ports_and_load_initial_graph(
&manifest_path,
args.cache_dir.as_deref(),
offline,
args.frozen,
false,
&build_selection,
args.no_patches,
)?;
let port_sources: Vec<cabin_workspace::PortPackageSource> = prepared_ports
.iter()
.map(crate::port_glue::workspace_source)
.collect();
let effective_config = crate::config_glue::load_effective_config(&initial_graph)?;
let active_patches =
crate::patch_glue::load_active_patches(&initial_graph, &effective_config, args.no_patches)?;
let patched_names = active_patches.owned_patched_names();
let resolved_index_source = crate::config_glue::resolve_index_source(
args.index_path.as_deref(),
args.index_url.as_deref(),
&effective_config,
)?;
let build_offline = crate::config_glue::effective_offline(args.offline)?;
crate::config_glue::enforce_offline_index_source(
build_offline,
resolved_index_source.as_ref(),
)?;
let resolved_cache_dir =
crate::config_glue::resolve_cache_dir(args.cache_dir.as_deref(), &effective_config);
let workspace_selection_for_pipeline = build_workspace_selection(&args.workspace_selection);
let initial_resolved_selection = cabin_workspace::resolve_package_selection(
&initial_graph,
&workspace_selection_for_pipeline,
)?;
let initial_request =
build_selection_request(&args.features, args.all_features, args.no_default_features);
let initial_features = compute_feature_resolution(
&initial_graph,
&initial_resolved_selection,
&initial_request,
)?;
let dev_for: BTreeSet<String> = BTreeSet::new();
let patched_root_deps_preview =
collect_patched_versioned_deps(&active_patches, &patched_names)?;
let has_versioned = !patched_root_deps_preview.is_empty()
|| closure_has_versioned_deps_excluding_patches(
&initial_graph,
&initial_resolved_selection,
&initial_features,
&patched_names,
&dev_for,
);
let registry: Vec<RegistryPackageSource> = if has_versioned {
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::config_glue::resolve_pipeline_inputs(
index_source,
&effective_config,
&manifest_path,
args.cache_dir.as_deref(),
resolved_cache_dir.as_ref(),
build_offline,
args.locked,
args.frozen,
args.no_patches,
false,
)?;
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_for_pipeline,
selection_request: &initial_request,
patched_names: &patched_names,
active_patches: &active_patches,
source_replacements: &effective_config.source_replacements,
no_patches: args.no_patches,
dev_for: &dev_for,
})?;
pipeline.registry_sources()
} else {
Vec::new()
};
let mut strict_packages: BTreeSet<String> =
initial_resolved_selection.closure_package_names(&initial_graph);
strict_packages.extend(patched_names.iter().cloned());
strict_packages.extend(registry.iter().map(|r| r.name.as_str().to_owned()));
let patched_sources = active_patches.workspace_sources();
let graph = cabin_workspace::load_workspace_with_options(
&manifest_path,
&cabin_workspace::WorkspaceLoadOptions {
registry: ®istry,
patches: &patched_sources,
ports: &port_sources,
registry_policy: cabin_workspace::RegistryPolicy::StrictFor(&strict_packages),
include_dev_for: &BTreeSet::new(),
port_policy: cabin_workspace::PortPolicy::TolerateExcept(&strict_packages),
},
)?;
let (build_dir_input, _build_dir_source) = crate::config_glue::resolve_build_dir_with_env(
args.build_dir.as_deref(),
&effective_config,
);
let build_dir = absolutise(&build_dir_input)
.with_context(|| format!("failed to resolve build dir {}", build_dir_input.display()))?;
let host_platform = cabin_core::TargetPlatform::current();
let toolchain_selection = toolchain_selection_from_args(&args.toolchain)?;
let toolchain = resolve_toolchain_layered(
&graph,
&toolchain_selection,
&effective_config,
&host_platform,
)?;
let detection_report =
cabin_toolchain::detect_toolchain(&toolchain, &cabin_toolchain::ProcessRunner)
.map_err(|err| anyhow::anyhow!(err.to_string()))?;
cabin_build::validate_toolchain_for_backend(&toolchain, &detection_report)?;
let ninja = cabin_toolchain::locate_ninja()?;
let manifest_compiler_wrapper = workspace_compiler_wrapper_settings(&graph);
let cli_compiler_wrapper = compiler_wrapper_override_from_args(&args.toolchain)?;
let profile_selection = profile_selection_for_build(args, &effective_config)?;
let manifest_profiles = workspace_profile_definitions(&graph);
let profile = cabin_core::resolve_profile(&profile_selection, &manifest_profiles)
.map_err(|err| anyhow::anyhow!(err.to_string()))?;
let dev_for: BTreeSet<String> = BTreeSet::new();
let prep =
crate::build_prep_glue::resolve_build_prep(crate::build_prep_glue::BuildConfigInputs {
graph: &graph,
host_platform: &host_platform,
toolchain: &toolchain,
cli_compiler_wrapper,
manifest_compiler_wrapper: &manifest_compiler_wrapper,
effective_config: &effective_config,
profile: &profile,
dev_for: &dev_for,
reporter,
})?;
let workspace_selection = build_workspace_selection(&args.workspace_selection);
let resolved_selection =
cabin_workspace::resolve_package_selection(&graph, &workspace_selection)?;
let selection_request =
build_selection_request(&args.features, args.all_features, args.no_default_features);
let configurations = resolve_build_configurations(
&graph,
&selection_request,
&resolved_selection.packages,
&profile,
&prep.toolchain_summary,
&prep.build_flags,
)?;
let feature_resolution =
compute_feature_resolution(&graph, &resolved_selection, &selection_request)?;
let root_configuration = graph
.root_package
.and_then(|i| configurations.get(&i))
.cloned();
let plan_graph = plan(&PlanRequest {
graph: &graph,
toolchain: &toolchain,
build_flags: &prep.build_flags,
build_dir: build_dir.clone(),
profile: profile.clone(),
selected: None,
configuration: root_configuration.as_ref(),
selected_packages: Some(&resolved_selection.packages),
compiler_wrapper: prep.compiler_wrapper.as_ref(),
})?;
let profile_build_root = build_dir.join(profile.name.as_str());
std::fs::create_dir_all(&profile_build_root).with_context(|| {
format!(
"failed to create build directory {}",
profile_build_root.display()
)
})?;
let ninja_file = profile_build_root.join("build.ninja");
cabin_ninja::write_build_ninja(&ninja_file, &plan_graph)?;
let ccmd_file = profile_build_root.join("compile_commands.json");
cabin_ninja::write_compile_commands(&ccmd_file, &plan_graph)?;
reporter.verbose(format_args!("cabin: profile = {}", profile.name.as_str()));
reporter.verbose(format_args!("cabin: build dir = {}", build_dir.display()));
reporter.verbose(format_args!(
"cabin: c++ compiler = {}",
toolchain.cxx.path.display()
));
if let Some(cc) = &toolchain.cc {
reporter.very_verbose(format_args!("cabin: c compiler = {}", cc.path.display()));
}
reporter.very_verbose(format_args!(
"cabin: archiver = {}",
toolchain.ar.path.display()
));
reporter.verbose(format_args!("cabin: wrote {}", ninja_file.display()));
reporter.verbose(format_args!("cabin: wrote {}", ccmd_file.display()));
let jobs = crate::config_glue::resolve_build_jobs(args.jobs, &effective_config)?;
reporter.verbose(format_args!(
"cabin: invoking {} {}-C {}",
ninja.display(),
crate::ninja_glue::ninja_jobs_echo(jobs),
profile_build_root.display()
));
let mut ninja_cmd = std::process::Command::new(&ninja);
if let Some(jobs) = jobs {
ninja_cmd.arg(crate::ninja_glue::ninja_jobs_arg(jobs));
}
let build_started = std::time::Instant::now();
let run = crate::ninja_glue::run_ninja(
ninja_cmd.arg("-C").arg(&profile_build_root),
reporter,
&graph,
)
.with_context(|| format!("failed to invoke ninja at {}", ninja.display()))?;
if !run.status.success() {
crate::ninja_glue::emit_link_diagnostic_if_applicable(
&run,
&graph,
&feature_resolution,
&dev_for,
reporter,
);
bail!("ninja exited with {}", run.status);
}
let elapsed = build_started.elapsed();
reporter.status(
"Finished",
format_args!(
"`{}` profile [{}] target(s) in {:.2}s",
profile.name.as_str(),
profile_descriptor(&profile),
elapsed.as_secs_f64(),
),
);
Ok(())
}
pub(crate) fn profile_descriptor(profile: &cabin_core::ResolvedProfile) -> String {
let opt = if matches!(profile.opt_level, cabin_core::OptLevel::O0) {
"unoptimized"
} else {
"optimized"
};
if profile.debug {
format!("{opt} + debuginfo")
} else {
opt.to_owned()
}
}
fn clean(args: &CleanArgs, reporter: Reporter) -> Result<()> {
use cabin_build::clean::{CleanRequest, CleanScope, execute_clean, plan_clean};
let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
let graph = cabin_workspace::load_workspace_skip_ports(&manifest_path)?;
let effective_config = crate::config_glue::load_effective_config(&graph)?;
let (build_dir_input, _build_dir_source) = crate::config_glue::resolve_build_dir_with_env(
args.build_dir.as_deref(),
&effective_config,
);
let build_dir = absolutise(&build_dir_input)
.with_context(|| format!("failed to resolve build dir {}", build_dir_input.display()))?;
let workspace_root = graph.root_dir.clone();
let package_roots: Vec<PathBuf> = graph
.packages
.iter()
.map(|pkg| pkg.manifest_dir.clone())
.collect();
let protected_source_paths = clean_protected_source_paths(&graph);
let workspace_selection = build_workspace_selection(&args.workspace_selection);
let resolved_selection =
cabin_workspace::resolve_package_selection(&graph, &workspace_selection)?;
let selected_explicitly = !args.workspace_selection.package.is_empty()
|| !args.workspace_selection.exclude.is_empty();
let profile_selection =
profile_selection_from_flags(args.profile.as_deref(), args.release, &effective_config)?;
let manifest_profiles = workspace_profile_definitions(&graph);
let resolved_profile = cabin_core::resolve_profile(&profile_selection, &manifest_profiles)
.map_err(|err| anyhow::anyhow!(err.to_string()))?;
let profile_was_chosen = args.profile.is_some() || args.release;
let scope = if selected_explicitly {
let packages: Vec<cabin_core::PackageName> = resolved_selection
.packages
.iter()
.map(|&idx| graph.packages[idx].package.name.clone())
.collect();
let profiles = if profile_was_chosen {
vec![resolved_profile.name]
} else {
known_profile_names(&manifest_profiles)
};
CleanScope::Packages { profiles, packages }
} else if profile_was_chosen {
CleanScope::Profile(resolved_profile.name)
} else {
CleanScope::Whole
};
let plan = plan_clean(&CleanRequest {
build_dir: &build_dir,
workspace_root: &workspace_root,
package_roots: &package_roots,
protected_source_paths: &protected_source_paths,
scope,
})
.map_err(|err| anyhow::anyhow!(err.to_string()))?;
if plan.removals.is_empty() {
if args.dry_run {
reporter.status(
"Removed",
format_args!("nothing under {} (dry-run)", build_dir.display()),
);
} else {
reporter.status(
"Removed",
format_args!(
"nothing under {} (build directory does not exist)",
build_dir.display()
),
);
}
return Ok(());
}
if args.dry_run {
reporter.status(
"Removed",
format_args!(
"{} path{} under {} (dry-run; re-run without --dry-run to apply)",
plan.removals.len(),
crate::plural(plan.removals.len()),
build_dir.display(),
),
);
print_plan_paths(&plan, reporter);
return Ok(());
}
let report = execute_clean(&plan).map_err(|err| anyhow::anyhow!(err.to_string()))?;
reporter.status(
"Removed",
format_args!(
"{} path{} under {}",
report.removed.len(),
crate::plural(report.removed.len()),
build_dir.display()
),
);
Ok(())
}
fn clean_protected_source_paths(graph: &cabin_workspace::PackageGraph) -> Vec<PathBuf> {
let mut paths = Vec::new();
for pkg in &graph.packages {
for target in &pkg.package.targets {
paths.extend(
target
.sources
.iter()
.map(|source| pkg.manifest_dir.join(source)),
);
paths.extend(
target
.include_dirs
.iter()
.map(|include_dir| pkg.manifest_dir.join(include_dir)),
);
}
}
paths.sort();
paths.dedup();
paths
}
fn print_plan_paths(plan: &cabin_build::clean::CleanPlan, reporter: Reporter) {
for path in &plan.removals {
reporter.note(format_args!(" {}", path.display()));
}
}
fn known_profile_names(
manifest_profiles: &BTreeMap<cabin_core::ProfileName, cabin_core::ProfileDefinition>,
) -> Vec<cabin_core::ProfileName> {
let mut out: BTreeSet<cabin_core::ProfileName> = BTreeSet::new();
for builtin in cabin_core::BuiltinProfile::all() {
out.insert(cabin_core::ProfileName::builtin(builtin));
}
for name in manifest_profiles.keys() {
out.insert(name.clone());
}
out.into_iter().collect()
}
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,
)
}
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,
)
}
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(),
}
}
fn fetch(args: &FetchArgs, reporter: Reporter) -> Result<()> {
let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
let offline_pre = crate::config_glue::effective_offline(args.offline)?;
let fetch_selection = build_workspace_selection(&args.workspace_selection);
let (_port_sources, initial_graph) = crate::port_glue::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::config_glue::load_effective_config(&initial_graph)?;
let active_patches =
crate::patch_glue::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::config_glue::resolve_index_source(
args.index_path.as_deref(),
args.index_url.as_deref(),
&effective_config,
)?;
let fetch_offline = crate::config_glue::effective_offline(args.offline)?;
crate::config_glue::enforce_offline_index_source(
fetch_offline,
resolved_index_source.as_ref(),
)?;
let resolved_cache_dir =
crate::config_glue::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::config_glue::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(())
}
fn package(args: &PackageArgs, _reporter: Reporter) -> Result<()> {
let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
let target =
select_single_package_manifest(&manifest_path, &args.workspace_selection, "package")?;
let output_dir = absolutise(&args.output_dir)
.with_context(|| format!("failed to resolve {}", args.output_dir.display()))?;
let artifact = cabin_package::package_with_project(
cabin_package::PackageRequest {
manifest_path: &target.manifest_path,
output_dir: &output_dir,
},
target.resolved_project,
)?;
emit_package_output(&artifact, args.format)?;
Ok(())
}
fn publish(args: &PublishArgs, _reporter: Reporter) -> Result<()> {
if args.output_dir.is_some() && args.registry_dir.is_some() {
bail!("--output-dir is not compatible with --registry-dir; pick one");
}
let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
let target =
select_single_package_manifest(&manifest_path, &args.workspace_selection, "publish")?;
match (args.registry_dir.as_deref(), args.dry_run) {
(Some(registry_dir), true) => {
let registry_dir = absolutise(registry_dir)
.with_context(|| format!("failed to resolve {}", registry_dir.display()))?;
let report = cabin_publish::dry_run_against_file_registry(
cabin_publish::RegistryPublishWorkflow {
manifest_path: &target.manifest_path,
registry_dir: ®istry_dir,
resolved_project: target.resolved_project.clone(),
},
)?;
emit_registry_publish_output(&report, args.format)?;
}
(Some(registry_dir), false) => {
let registry_dir = absolutise(registry_dir)
.with_context(|| format!("failed to resolve {}", registry_dir.display()))?;
let report =
cabin_publish::publish_to_file_registry(cabin_publish::RegistryPublishWorkflow {
manifest_path: &target.manifest_path,
registry_dir: ®istry_dir,
resolved_project: target.resolved_project.clone(),
})?;
emit_registry_publish_output(&report, args.format)?;
}
(None, true) => {
let output_dir = args
.output_dir
.clone()
.unwrap_or_else(|| PathBuf::from("dist"));
let output_dir = absolutise(&output_dir)
.with_context(|| format!("failed to resolve {}", output_dir.display()))?;
let report = cabin_publish::dry_run(cabin_publish::DryRunRequest {
manifest_path: &target.manifest_path,
output_dir: &output_dir,
resolved_project: target.resolved_project.clone(),
})?;
emit_dry_run_output(&report, args.format)?;
}
(None, false) => {
return Err(cabin_publish::PublishError::DryRunRequired.into());
}
}
Ok(())
}
fn emit_package_output(
artifact: &cabin_package::PackagedArtifact,
format: ResolveFormat,
) -> Result<()> {
match format {
ResolveFormat::Human => {
print_package_human(artifact);
Ok(())
}
ResolveFormat::Json => print_package_json(artifact),
}
}
fn print_package_human(artifact: &cabin_package::PackagedArtifact) {
println!("Packaged {} {}", artifact.name.as_str(), artifact.version);
println!(" archive: {}", artifact.archive_path.display());
println!(" metadata: {}", artifact.metadata_path.display());
println!(" checksum: {}", artifact.checksum);
}
fn print_package_json(artifact: &cabin_package::PackagedArtifact) -> Result<()> {
let value = serde_json::json!({
"name": artifact.name.as_str(),
"version": artifact.version.to_string(),
"archive_path": artifact.archive_path,
"metadata_path": artifact.metadata_path,
"checksum": artifact.checksum,
});
crate::print_pretty_json(&value, "failed to serialize package output as JSON")
}
fn emit_dry_run_output(report: &cabin_publish::DryRunReport, format: ResolveFormat) -> Result<()> {
match format {
ResolveFormat::Human => {
print_dry_run_human(report);
Ok(())
}
ResolveFormat::Json => print_dry_run_json(report),
}
}
fn print_dry_run_human(report: &cabin_publish::DryRunReport) {
println!(
"Publish dry-run for {} {}",
report.name.as_str(),
report.version
);
println!();
println!("Generated:");
println!(" archive: {}", report.archive_path.display());
println!(" metadata: {}", report.metadata_path.display());
println!(" checksum: {}", report.checksum);
println!();
println!("This was a dry run. No registry was modified.");
}
fn print_dry_run_json(report: &cabin_publish::DryRunReport) -> Result<()> {
let value = serde_json::json!({
"dry_run": true,
"name": report.name.as_str(),
"version": report.version.to_string(),
"archive_path": report.archive_path,
"metadata_path": report.metadata_path,
"checksum": report.checksum,
"registry_modified": report.registry_modified,
});
crate::print_pretty_json(&value, "failed to serialize publish dry-run output as JSON")
}
fn emit_registry_publish_output(
report: &cabin_publish::RegistryPublishReport,
format: ResolveFormat,
) -> Result<()> {
match format {
ResolveFormat::Human => {
print_registry_publish_human(report);
Ok(())
}
ResolveFormat::Json => print_registry_publish_json(report),
}
}
fn print_registry_publish_human(report: &cabin_publish::RegistryPublishReport) {
if report.dry_run {
println!(
"Publish dry-run for {} {} against file registry",
report.name.as_str(),
report.version
);
} else {
println!(
"Published {} {} to file registry",
report.name.as_str(),
report.version
);
}
println!(" registry: {}", report.registry_dir.display());
println!(" package index: {}", report.package_index_path.display());
println!(" artifact: {}", report.artifact_path.display());
println!(" checksum: {}", report.checksum);
if report.dry_run {
println!();
if report.registry_initialized {
println!("Registry would be initialized at this path.");
}
println!("This was a dry run. No registry was modified.");
} else if report.registry_initialized {
println!();
println!("Registry was initialized at this path.");
}
}
fn print_registry_publish_json(report: &cabin_publish::RegistryPublishReport) -> Result<()> {
let value = serde_json::json!({
"published": !report.dry_run,
"dry_run": report.dry_run,
"name": report.name.as_str(),
"version": report.version.to_string(),
"registry_dir": report.registry_dir,
"package_index_path": report.package_index_path,
"artifact_path": report.artifact_path,
"checksum": report.checksum,
"source_path": report.source_path,
"registry_modified": report.registry_modified,
"registry_initialized": report.registry_initialized,
});
crate::print_pretty_json(&value, "failed to serialize publish output as JSON")
}
fn profile_selection_for_build(
args: &BuildArgs,
config: &cabin_config::EffectiveConfig,
) -> Result<cabin_core::ProfileSelection> {
profile_selection_from_flags(args.profile.as_deref(), args.release, config)
}
pub(crate) fn profile_selection_from_flags(
profile: Option<&str>,
release: bool,
config: &cabin_config::EffectiveConfig,
) -> Result<cabin_core::ProfileSelection> {
if let Some(name) = profile {
let pname = cabin_core::ProfileName::new(name.to_owned())
.map_err(|err| anyhow::anyhow!(err.to_string()))?;
return Ok(cabin_core::ProfileSelection::from_name(pname));
}
if release {
return Ok(cabin_core::ProfileSelection::release_alias());
}
if let Some((selection, _source)) = crate::config_glue::config_profile_selection(config)? {
return Ok(selection);
}
Ok(cabin_core::ProfileSelection::default_dev())
}
pub(crate) fn profile_selection_for_metadata(
name: Option<&str>,
config: &cabin_config::EffectiveConfig,
) -> Result<cabin_core::ProfileSelection> {
if let Some(n) = name {
let pname = cabin_core::ProfileName::new(n.to_owned())
.map_err(|err| anyhow::anyhow!(err.to_string()))?;
return Ok(cabin_core::ProfileSelection::from_name(pname));
}
if let Some((selection, _source)) = crate::config_glue::config_profile_selection(config)? {
return Ok(selection);
}
Ok(cabin_core::ProfileSelection::default_dev())
}
pub(crate) fn workspace_profile_definitions(
graph: &PackageGraph,
) -> BTreeMap<cabin_core::ProfileName, cabin_core::ProfileDefinition> {
graph.root_settings.profiles.clone()
}
pub(crate) fn workspace_toolchain_settings(graph: &PackageGraph) -> cabin_core::ToolchainSettings {
graph.root_settings.toolchain.clone()
}
pub(crate) fn toolchain_selection_from_args(
args: &ToolchainSelectionArgs,
) -> Result<cabin_core::ToolchainSelection> {
let mut sel = cabin_core::ToolchainSelection::default();
if let Some(raw) = &args.cc {
sel = sel.with_cli(cabin_core::ToolKind::CCompiler, parse_cli_tool(raw)?);
}
if let Some(raw) = &args.cxx {
sel = sel.with_cli(cabin_core::ToolKind::CxxCompiler, parse_cli_tool(raw)?);
}
if let Some(raw) = &args.ar {
sel = sel.with_cli(cabin_core::ToolKind::Archiver, parse_cli_tool(raw)?);
}
Ok(sel)
}
fn parse_cli_tool(raw: &str) -> Result<cabin_core::ToolSpec> {
let trimmed = raw.trim();
if trimmed.is_empty() {
bail!("tool argument must be a non-empty path or command name");
}
Ok(cabin_core::ToolSpec::parse(trimmed.to_owned()))
}
pub(crate) fn resolve_toolchain_layered(
graph: &PackageGraph,
selection: &cabin_core::ToolchainSelection,
effective_config: &cabin_config::EffectiveConfig,
host_platform: &cabin_core::TargetPlatform,
) -> Result<cabin_core::ResolvedToolchain> {
let manifest_toolchain_settings = workspace_toolchain_settings(graph);
let config_toolchain_layer = crate::config_glue::toolchain_layer(effective_config);
let mut toolchain_inputs = cabin_toolchain::ResolveInputs::from_process(
selection,
&manifest_toolchain_settings,
host_platform,
);
if let Some(layer) = config_toolchain_layer.as_ref() {
toolchain_inputs = toolchain_inputs.with_config(layer);
}
Ok(cabin_toolchain::resolve_toolchain(&toolchain_inputs)?)
}
pub(crate) fn compiler_wrapper_override_from_args(
args: &ToolchainSelectionArgs,
) -> Result<Option<cabin_core::CompilerWrapperRequest>> {
if args.no_compiler_wrapper {
return Ok(Some(cabin_core::CompilerWrapperRequest::Disabled));
}
let Some(raw) = args.compiler_wrapper.as_deref() else {
return Ok(None);
};
let parsed = cabin_core::CompilerWrapperRequest::parse(raw)
.with_context(|| format!("invalid --compiler-wrapper value `{raw}`"))?;
Ok(Some(parsed))
}
pub(crate) fn resolve_compiler_wrapper_layered(
cli_override: Option<cabin_core::CompilerWrapperRequest>,
manifest_settings: &cabin_core::CompilerWrapperManifestSettings,
effective_config: &cabin_config::EffectiveConfig,
host_platform: &cabin_core::TargetPlatform,
) -> Result<Option<cabin_core::ResolvedCompilerWrapper>> {
let mut wrapper_inputs = cabin_toolchain::WrapperInputs::from_process(
cli_override,
manifest_settings,
host_platform,
);
if let Some(layer) = crate::config_glue::wrapper_layer(effective_config) {
wrapper_inputs = wrapper_inputs.with_config(layer);
}
cabin_toolchain::resolve_compiler_wrapper(
&wrapper_inputs,
Some(&cabin_toolchain::ProcessRunner),
)
.map_err(|err| anyhow::anyhow!(err.to_string()))
}
pub(crate) fn workspace_compiler_wrapper_settings(
graph: &PackageGraph,
) -> cabin_core::CompilerWrapperManifestSettings {
graph.root_settings.compiler_wrapper.clone()
}
pub(crate) fn resolve_per_package_build_flags(
graph: &PackageGraph,
profile_build: Option<&cabin_core::ProfileFlags>,
host_platform: &cabin_core::TargetPlatform,
) -> HashMap<usize, cabin_core::ResolvedProfileFlags> {
let mut out = HashMap::with_capacity(graph.packages.len());
for (idx, pkg) in graph.packages.iter().enumerate() {
let package_trusted = matches!(pkg.kind, cabin_workspace::PackageKind::Local);
let resolved = cabin_core::resolve_build_flags(
&pkg.package.build,
profile_build,
host_platform,
package_trusted,
);
out.insert(idx, resolved);
}
out
}
pub(crate) fn augment_build_flags(
graph: &PackageGraph,
host_platform: &cabin_core::TargetPlatform,
dev_for: &BTreeSet<String>,
build_flags: HashMap<usize, cabin_core::ResolvedProfileFlags>,
reporter: Reporter,
) -> Result<HashMap<usize, cabin_core::ResolvedProfileFlags>> {
let (build_flags, _system_dep_reports) =
crate::system_deps_glue::augment_build_flags_with_system_deps(
graph,
host_platform,
dev_for,
build_flags,
reporter,
)?;
let (build_flags, _env_build_flags) = crate::env_flags_glue::augment_build_flags_with_env(
graph,
build_flags,
|k| std::env::var_os(k),
reporter,
)?;
Ok(build_flags)
}
pub(crate) fn build_selection_request(
feature_args: &[String],
all_features: bool,
no_default_features: bool,
) -> cabin_core::SelectionRequest {
let mut features: BTreeSet<String> = BTreeSet::new();
for raw in feature_args {
for token in raw.split(',') {
let trimmed = token.trim();
if trimmed.is_empty() {
continue;
}
features.insert(trimmed.to_owned());
}
}
cabin_core::SelectionRequest {
features,
all_features,
no_default_features,
}
}
pub(crate) fn resolve_build_configurations(
graph: &PackageGraph,
request: &cabin_core::SelectionRequest,
selected: &[usize],
profile: &cabin_core::ResolvedProfile,
toolchain: &cabin_core::ToolchainSummary,
build_flags: &HashMap<usize, cabin_core::ResolvedProfileFlags>,
) -> Result<HashMap<usize, cabin_core::BuildConfiguration>> {
use HashMap;
let selected_set: HashSet<usize> = selected.iter().copied().collect();
let mut out: HashMap<usize, cabin_core::BuildConfiguration> = HashMap::new();
for (idx, pkg) in graph.packages.iter().enumerate() {
let pkg_request = if selected_set.contains(&idx) {
request.clone()
} else {
cabin_core::SelectionRequest::default()
};
let pkg_flags = build_flags.get(&idx).cloned().unwrap_or_default();
let cfg = cabin_core::BuildConfiguration::resolve(cabin_core::BuildConfigurationInput {
package: pkg.package.name.as_str(),
features: &pkg.package.features,
request: &pkg_request,
profile: profile.clone(),
toolchain: toolchain.clone(),
build_flags: pkg_flags,
})
.with_context(|| {
format!(
"invalid configuration selection for package `{}`",
pkg.package.name.as_str()
)
})?;
out.insert(idx, cfg);
}
Ok(out)
}
pub(crate) fn resolve_invocation_manifest(args_path: Option<&Path>) -> Result<PathBuf> {
let cwd = std::env::current_dir().context("failed to determine current directory")?;
match args_path {
Some(path) => {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
Ok(cwd.join(path))
}
}
None => {
if let Some(found) = cabin_workspace::discover_workspace_root(&cwd)? {
Ok(found.manifest_path)
} else {
Ok(cwd.join(MANIFEST_FILENAME))
}
}
}
}
pub(crate) fn build_workspace_selection(
args: &WorkspaceSelectionArgs,
) -> cabin_workspace::PackageSelection {
use cabin_workspace::SelectionMode;
let mode = if args.workspace {
SelectionMode::WholeWorkspace
} else if !args.package.is_empty() {
SelectionMode::ExplicitPackages(args.package.clone())
} else if args.default_members {
SelectionMode::DefaultMembers
} else {
SelectionMode::CurrentPackage
};
cabin_workspace::PackageSelection {
mode,
exclude: args.exclude.clone(),
}
}
fn closure_and_optional_filter<'a>(
graph: &PackageGraph,
selection: &cabin_workspace::ResolvedSelection,
features: &'a cabin_feature::FeatureResolution,
) -> (BTreeSet<usize>, impl Fn(usize, &str) -> bool + 'a) {
(selection.closure(graph), move |idx, name| {
features.is_optional_dep_enabled(idx, name)
})
}
pub(crate) fn collect_closure_versioned_deps_excluding_patches(
graph: &PackageGraph,
selection: &cabin_workspace::ResolvedSelection,
features: &cabin_feature::FeatureResolution,
patched_names: &BTreeSet<String>,
dev_for: &BTreeSet<String>,
) -> Result<BTreeMap<PackageName, semver::VersionReq>> {
let (closure, is_optional_dep_enabled) =
closure_and_optional_filter(graph, selection, features);
cabin_workspace::collect_closure_versioned_deps_excluding_with_dev(
graph,
&closure,
is_optional_dep_enabled,
patched_names,
dev_for,
)
.map_err(Into::into)
}
fn merge_versioned_deps(
into: &mut BTreeMap<PackageName, semver::VersionReq>,
extra: BTreeMap<PackageName, semver::VersionReq>,
) -> Result<()> {
for (name, req) in extra {
match into.entry(name.clone()) {
std::collections::btree_map::Entry::Vacant(slot) => {
slot.insert(req);
}
std::collections::btree_map::Entry::Occupied(mut slot) => {
let parsed = cabin_workspace::combine_version_reqs(&[
slot.get().to_string(),
req.to_string(),
])
.map_err(|(joined, err)| {
anyhow::anyhow!(
"conflicting dependency requirements for {}: {}: {}",
name.as_str(),
joined,
err
)
})?;
slot.insert(parsed);
}
}
}
Ok(())
}
pub(crate) fn closure_has_versioned_deps_excluding_patches(
graph: &PackageGraph,
selection: &cabin_workspace::ResolvedSelection,
features: &cabin_feature::FeatureResolution,
patched_names: &BTreeSet<String>,
dev_for: &BTreeSet<String>,
) -> bool {
let (closure, is_optional_dep_enabled) =
closure_and_optional_filter(graph, selection, features);
cabin_workspace::closure_has_versioned_deps_excluding_with_dev(
graph,
&closure,
is_optional_dep_enabled,
patched_names,
dev_for,
)
}
pub(crate) fn compute_feature_resolution(
graph: &PackageGraph,
selection: &cabin_workspace::ResolvedSelection,
request: &cabin_core::SelectionRequest,
) -> Result<cabin_feature::FeatureResolution> {
let root_request: cabin_feature::RootFeatureRequest = request.into();
let platform = cabin_core::TargetPlatform::current();
cabin_feature::resolve_features(graph, &selection.packages, &root_request, &platform)
.map_err(|e| anyhow::anyhow!(e.to_string()))
}
fn selected_resolution_packages(
graph: &PackageGraph,
selection: &cabin_workspace::PackageSelection,
) -> Result<cabin_workspace::ResolvedSelection> {
cabin_workspace::resolve_package_selection(graph, selection).map_err(std::convert::Into::into)
}
struct SinglePackageSelection {
manifest_path: PathBuf,
resolved_project: Option<cabin_core::Package>,
}
fn select_single_package_manifest(
invocation: &Path,
selection: &WorkspaceSelectionArgs,
command: &'static str,
) -> Result<SinglePackageSelection> {
let parsed = cabin_manifest::load_manifest(invocation)
.with_context(|| format!("failed to load manifest at {}", invocation.display()))?;
if parsed.workspace.is_none() {
if selection.workspace
|| selection.default_members
|| !selection.package.is_empty()
|| !selection.exclude.is_empty()
{
bail!(
"workspace package-selection flags are not valid for `cabin {command}` against a non-workspace manifest"
);
}
return Ok(SinglePackageSelection {
manifest_path: invocation.to_path_buf(),
resolved_project: None,
});
}
if selection.package.len() != 1 || selection.workspace || selection.default_members {
bail!(
"`cabin {command}` requires a single `--package <name>` selection inside a workspace; use `--package <name>` to pick the package to {command}"
);
}
if !selection.exclude.is_empty() {
bail!(
"`--exclude` is not valid for `cabin {command}`; pass exactly one `--package <name>`"
);
}
let graph = cabin_workspace::load_workspace_skip_ports(invocation)?;
let name = &selection.package[0];
let idx = graph
.index_of(name)
.ok_or_else(|| anyhow::anyhow!("package `{name}` is not a member of this workspace"))?;
if !graph.primary_packages.contains(&idx) {
bail!("package `{name}` is not a member of this workspace");
}
Ok(SinglePackageSelection {
manifest_path: graph.packages[idx].manifest_path.clone(),
resolved_project: Some(graph.packages[idx].package.clone()),
})
}
pub(crate) fn lock_mode_for_flags(locked: bool, frozen: bool) -> LockMode {
if locked || frozen {
LockMode::Locked
} else {
LockMode::PreferLocked
}
}
pub(crate) fn cache_dir_for(manifest_path: &Path, override_dir: Option<&Path>) -> Result<PathBuf> {
let xdg_cache_home = xdg::BaseDirectories::with_prefix("cabin").get_cache_home();
cache_dir_for_with_env(
manifest_path,
override_dir,
&|key| std::env::var_os(key),
xdg_cache_home.as_deref(),
)
}
fn cache_dir_for_with_env(
manifest_path: &Path,
override_dir: Option<&Path>,
env: &dyn Fn(&str) -> Option<std::ffi::OsString>,
xdg_cache_home: Option<&Path>,
) -> Result<PathBuf> {
if let Some(p) = override_dir {
return absolutise(p)
.with_context(|| format!("failed to resolve cache dir {}", p.display()));
}
if let Some(val) = env("CABIN_CACHE_DIR").filter(|v| !v.is_empty()) {
let p = PathBuf::from(val);
return absolutise(&p)
.with_context(|| format!("failed to resolve cache dir {}", p.display()));
}
let _ = manifest_path;
user_cache_default(env, xdg_cache_home).ok_or_else(|| {
anyhow::anyhow!(
"no cache directory: set --cache-dir, CABIN_CACHE_DIR, CABIN_CACHE_HOME, XDG_CACHE_HOME, or HOME"
)
})
}
fn user_cache_default(
env: &dyn Fn(&str) -> Option<std::ffi::OsString>,
xdg_cache_home: Option<&Path>,
) -> Option<PathBuf> {
if let Some(d) = env("CABIN_CACHE_HOME").filter(|v| !v.is_empty()) {
return Some(PathBuf::from(d));
}
xdg_cache_home.map(Path::to_path_buf)
}
pub(crate) struct ArtifactPipelineRequest<'a> {
pub(crate) manifest_path: &'a Path,
pub(crate) initial_graph: &'a PackageGraph,
pub(crate) index_path: Option<&'a Path>,
pub(crate) index_url: Option<&'a str>,
pub(crate) mode: LockMode,
pub(crate) allow_write: bool,
pub(crate) frozen: bool,
pub(crate) cache_dir: &'a Path,
pub(crate) reporter: Reporter,
pub(crate) selection: cabin_workspace::PackageSelection,
pub(crate) selection_request: &'a cabin_core::SelectionRequest,
pub(crate) patched_names: &'a BTreeSet<String>,
pub(crate) active_patches: &'a cabin_workspace::ActivePatchSet,
pub(crate) source_replacements: &'a cabin_core::SourceReplacementSettings,
pub(crate) no_patches: bool,
pub(crate) dev_for: &'a BTreeSet<String>,
}
pub(crate) struct ArtifactPipeline {
pub(crate) fetched: Vec<FetchedPackage>,
}
impl ArtifactPipeline {
pub(crate) fn registry_sources(&self) -> Vec<RegistryPackageSource> {
self.fetched
.iter()
.map(|p| RegistryPackageSource {
name: p.name.clone(),
version: p.version.clone(),
manifest_path: p.source_dir.join("cabin.toml"),
})
.collect()
}
}
enum IndexAccess {
Local,
Http(cabin_index_http::HttpClient),
}
pub(crate) fn run_artifact_pipeline(
request: &ArtifactPipelineRequest<'_>,
) -> Result<ArtifactPipeline> {
let manifest_path = request.manifest_path;
let graph = request.initial_graph;
let resolved_selection = selected_resolution_packages(graph, &request.selection)?;
let features =
compute_feature_resolution(graph, &resolved_selection, request.selection_request)?;
let mut root_deps = collect_closure_versioned_deps_excluding_patches(
graph,
&resolved_selection,
&features,
request.patched_names,
request.dev_for,
)?;
let patched_root_deps =
collect_patched_versioned_deps(request.active_patches, request.patched_names)?;
merge_versioned_deps(&mut root_deps, patched_root_deps)?;
if root_deps.is_empty() {
return Ok(ArtifactPipeline {
fetched: Vec::new(),
});
}
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);
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 {
if matches!(request.mode, LockMode::Locked) {
bail!(
"cannot resolve with --locked because {} does not exist",
lockfile_path.display()
);
}
None
};
let (index, access) = load_index_for_pipeline(
request.index_path,
request.index_url,
request.frozen,
&root_deps,
)?;
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 active_patch_records = crate::patch_glue::lockfile_patches(request.active_patches);
let active_replacement_records = crate::patch_glue::lockfile_source_replacements(
request.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()
);
}
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()))?;
request
.reporter
.aux_verbose(format_args!("cabin: wrote {}", lockfile_path.display()));
} else {
request.reporter.aux_verbose(format_args!(
"cabin: {} is up to date",
lockfile_path.display()
));
}
}
let plan = build_fetch_plan(&output, &index, &access)?;
let cache = ArtifactCache::new(request.cache_dir);
let result = cabin_artifact::fetch(
&plan,
&cache,
FetchOptions {
frozen: request.frozen,
},
)?;
Ok(ArtifactPipeline {
fetched: result.packages,
})
}
fn load_index_for_pipeline(
index_path: Option<&Path>,
index_url: Option<&str>,
frozen: bool,
root_deps: &BTreeMap<PackageName, semver::VersionReq>,
) -> Result<(PackageIndex, IndexAccess)> {
match (index_path, index_url) {
(Some(_), Some(_)) => bail!("use either --index-path or --index-url, not both"),
(None, None) => {
bail!("versioned dependencies require --index-path or --index-url")
}
(Some(path), None) => {
let index_path = absolutise(path)
.with_context(|| format!("failed to resolve {}", path.display()))?;
let index = cabin_index::load_index(&index_path)
.with_context(|| format!("failed to load index at {}", index_path.display()))?;
Ok((index, IndexAccess::Local))
}
(None, Some(url)) => {
if frozen {
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 client = cabin_index_http::HttpClient::new();
let http_index = cabin_index_http::HttpIndex::open(url, client.clone())?;
let names: Vec<PackageName> = root_deps.keys().cloned().collect();
let index = http_index.load_package_index(&names)?;
Ok((index, IndexAccess::Http(client)))
}
}
}
fn build_fetch_plan(
output: &ResolveOutput,
index: &PackageIndex,
access: &IndexAccess,
) -> Result<FetchPlan> {
let mut entries = Vec::new();
for resolved in &output.packages {
if resolved.source != ResolvedSource::Index {
continue;
}
let entry = index.package(&resolved.name).ok_or_else(|| {
anyhow::anyhow!(
"resolver chose `{} {}`, but it is not in the index",
resolved.name.as_str(),
resolved.version
)
})?;
let meta = entry.versions.get(&resolved.version).ok_or_else(|| {
anyhow::anyhow!(
"resolver chose `{} {}`, but the index has no entry for this version",
resolved.name.as_str(),
resolved.version
)
})?;
let source = meta.source.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"package `{} {}` has no source artifact in the index",
resolved.name.as_str(),
resolved.version
)
})?;
let checksum = meta.checksum.clone().ok_or_else(|| {
anyhow::anyhow!(
"missing checksum for `{} {}`; cabin fetch requires a sha256:<hex> entry in the index",
resolved.name.as_str(),
resolved.version
)
})?;
let fetch_source = match (&source.location, access) {
(cabin_index::SourceLocation::LocalPath(p), _) => {
cabin_artifact::FetchSource::LocalArchive(p.clone())
}
(cabin_index::SourceLocation::HttpUrl(url), IndexAccess::Http(client)) => {
let label = format!("{} {}", resolved.name.as_str(), resolved.version);
let bytes = client.download(url, &label).with_context(|| {
format!(
"failed to download source archive for `{} {}`",
resolved.name.as_str(),
resolved.version
)
})?;
cabin_artifact::FetchSource::InMemoryArchive(bytes)
}
(cabin_index::SourceLocation::HttpUrl(_), IndexAccess::Local) => {
bail!(
"package `{} {}` has an HTTP source URL but the run is using a local index",
resolved.name.as_str(),
resolved.version
);
}
};
entries.push(FetchEntry {
name: resolved.name.clone(),
version: resolved.version.clone(),
checksum,
source: fetch_source,
});
}
Ok(FetchPlan { entries })
}
#[derive(Debug, Clone)]
pub(crate) enum LockMode {
PreferLocked,
Locked,
UpdateAll,
UpdatePackage(String),
}
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::config_glue::effective_offline(request.offline)?;
let (_port_sources, graph) = crate::port_glue::prepare_ports_and_load_initial_graph(
&manifest_path,
None,
offline,
request.frozen,
false,
&request.selection,
request.no_patches,
)?;
let effective_config = crate::config_glue::load_effective_config(&graph)?;
let active_patches =
crate::patch_glue::load_active_patches(&graph, &effective_config, request.no_patches)?;
let patched_names = active_patches.owned_patched_names();
let resolved_index_source = crate::config_glue::resolve_index_source(
request.index_path,
request.index_url,
&effective_config,
)?;
let resolution_offline = crate::config_glue::effective_offline(request.offline)?;
crate::config_glue::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::config_glue::index_source_kind_to_locator(&source.kind);
let resolved = crate::patch_glue::apply_source_replacement(
initial,
&effective_config,
request.no_patches,
)?;
crate::config_glue::enforce_offline_post_replacement(
resolution_offline,
&resolved,
)?;
crate::patch_glue::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::patch_glue::lockfile_patches(&active_patches);
let active_replacement_records = crate::patch_glue::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!("config_glue::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(())
}
pub(crate) fn lockfile_path_for(manifest_path: &Path) -> PathBuf {
manifest_path
.parent()
.map_or_else(|| PathBuf::from("."), std::path::Path::to_path_buf)
.join("cabin.lock")
}
pub(crate) fn read_optional_lockfile(lockfile_path: &Path) -> Result<Option<Lockfile>> {
if lockfile_path.is_file() {
Ok(Some(
cabin_lockfile::read_lockfile(lockfile_path)
.with_context(|| format!("failed to read {}", lockfile_path.display()))?,
))
} else {
Ok(None)
}
}
fn lockfile_from_resolution(output: &ResolveOutput, index: &cabin_index::PackageIndex) -> Lockfile {
let resolved_names: BTreeSet<&str> = output
.packages
.iter()
.filter(|p| p.source == ResolvedSource::Index)
.map(|p| p.name.as_str())
.collect();
let mut packages: Vec<LockedPackage> = Vec::new();
for pkg in &output.packages {
if pkg.source != ResolvedSource::Index {
continue;
}
let entry = index
.package(&pkg.name)
.expect("index has every resolved package");
let meta = entry
.versions
.get(&pkg.version)
.expect("index has the resolved version");
let mut deps: Vec<PackageName> = meta
.dependencies
.keys()
.filter(|n| resolved_names.contains(n.as_str()))
.cloned()
.collect();
deps.sort();
packages.push(LockedPackage {
name: pkg.name.clone(),
version: pkg.version.clone(),
source: LockedSource::Index,
checksum: meta.checksum.clone(),
dependencies: deps,
});
}
packages.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
Lockfile {
version: cabin_lockfile::LOCKFILE_VERSION,
packages,
patches: Vec::new(),
source_replacements: Vec::new(),
}
}
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")
}
pub(crate) fn absolutise(path: &Path) -> std::io::Result<PathBuf> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
Ok(std::env::current_dir()?.join(path))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rendered_binary_template_round_trips_through_parser() {
let manifest = scaffold::render_manifest("hello", scaffold::ScaffoldKind::Binary);
let parsed = cabin_manifest::parse_manifest_str(&manifest).unwrap();
let package = parsed.package.expect("template should parse as a package");
assert_eq!(package.name.as_str(), "hello");
assert_eq!(package.targets.len(), 1);
assert_eq!(package.targets[0].name.as_str(), "hello");
}
#[test]
fn rendered_library_template_round_trips_through_parser() {
let manifest = scaffold::render_manifest("hello", scaffold::ScaffoldKind::Library);
let parsed = cabin_manifest::parse_manifest_str(&manifest).unwrap();
let package = parsed.package.expect("template should parse as a package");
assert_eq!(package.name.as_str(), "hello");
assert_eq!(package.targets.len(), 1);
assert_eq!(package.targets[0].name.as_str(), "hello");
}
#[test]
fn registry_dependency_build_flags_are_dropped_but_local_kept() {
use cabin_core::{Package, Target};
use cabin_workspace::{PackageKind, WorkspacePackage};
use std::path::PathBuf;
fn dep_with_command_flags(name: &str, kind: PackageKind) -> WorkspacePackage {
let mut package = Package::new(
PackageName::new(name).unwrap(),
semver::Version::parse("0.1.0").unwrap(),
Vec::<Target>::new(),
Vec::new(),
)
.unwrap();
package.build.general.cflags = vec!["-fplugin=evil.so".into()];
package.build.general.cxxflags = vec!["-B.".into()];
package.build.general.ldflags = vec!["-fuse-ld=/tmp/evil".into()];
WorkspacePackage {
package,
manifest_dir: PathBuf::from("/tmp"),
manifest_path: PathBuf::from("/tmp/cabin.toml"),
kind,
deps: Vec::new(),
}
}
let graph = PackageGraph {
root_manifest_path: PathBuf::from("/tmp/cabin.toml"),
root_dir: PathBuf::from("/tmp"),
is_workspace_root: false,
root_package: Some(0),
root_settings: Default::default(),
primary_packages: vec![0],
default_members: vec![0],
excluded_members: Vec::new(),
packages: vec![
dep_with_command_flags("local_dep", PackageKind::Local),
dep_with_command_flags("registry_dep", PackageKind::Registry),
],
};
let host = cabin_core::TargetPlatform::current();
let resolved = resolve_per_package_build_flags(&graph, None, &host);
let local = resolved.get(&0).expect("local package flags");
assert_eq!(local.cflags, vec!["-fplugin=evil.so".to_owned()]);
assert_eq!(local.cxxflags, vec!["-B.".to_owned()]);
assert_eq!(local.ldflags, vec!["-fuse-ld=/tmp/evil".to_owned()]);
let registry = resolved.get(&1).expect("registry package flags");
assert!(registry.cflags.is_empty());
assert!(registry.cxxflags.is_empty());
assert!(registry.ldflags.is_empty());
}
type EnvFn = Box<dyn Fn(&str) -> Option<std::ffi::OsString>>;
fn env_with(items: &[(&'static str, &str)]) -> EnvFn {
let map: std::collections::HashMap<&'static str, std::ffi::OsString> = items
.iter()
.map(|(k, v)| (*k, std::ffi::OsString::from(*v)))
.collect();
Box::new(move |k| map.get(k).cloned())
}
fn fake_manifest() -> &'static Path {
Path::new("/abs/ws/cabin.toml")
}
fn home_xdg_cache_home(home: &str) -> PathBuf {
PathBuf::from(home).join(".cache").join("cabin")
}
#[test]
fn cache_dir_flag_wins_over_everything() {
let env = env_with(&[
("CABIN_CACHE_DIR", "/tmp/from-env"),
("CABIN_CACHE_HOME", "/tmp/cabin-home"),
]);
let xdg = PathBuf::from("/tmp/xdg/cabin");
let out = cache_dir_for_with_env(
fake_manifest(),
Some(Path::new("/tmp/from-flag")),
&env,
Some(&xdg),
)
.unwrap();
assert_eq!(out, PathBuf::from("/tmp/from-flag"));
}
#[test]
fn cabin_cache_dir_env_wins_over_xdg() {
let env = env_with(&[
("CABIN_CACHE_DIR", "/tmp/from-env"),
("CABIN_CACHE_HOME", "/tmp/cabin-home"),
]);
let xdg = PathBuf::from("/tmp/xdg/cabin");
let out = cache_dir_for_with_env(fake_manifest(), None, &env, Some(&xdg)).unwrap();
assert_eq!(out, PathBuf::from("/tmp/from-env"));
}
#[test]
fn cabin_cache_home_used_when_cabin_cache_dir_unset() {
let env = env_with(&[("CABIN_CACHE_HOME", "/tmp/cabin-home")]);
let xdg = PathBuf::from("/tmp/xdg/cabin");
let out = cache_dir_for_with_env(fake_manifest(), None, &env, Some(&xdg)).unwrap();
assert_eq!(out, PathBuf::from("/tmp/cabin-home"));
}
#[test]
fn xdg_cache_home_appends_cabin_segment() {
let env = env_with(&[]);
let xdg = PathBuf::from("/tmp/xdg/cabin");
let out = cache_dir_for_with_env(fake_manifest(), None, &env, Some(&xdg)).unwrap();
assert_eq!(out, PathBuf::from("/tmp/xdg/cabin"));
}
#[test]
fn home_cache_fallback_used_when_xdg_unset() {
let env = env_with(&[]);
let xdg = home_xdg_cache_home("/tmp/home");
let out = cache_dir_for_with_env(fake_manifest(), None, &env, Some(&xdg)).unwrap();
assert_eq!(out, PathBuf::from("/tmp/home/.cache/cabin"));
}
#[test]
fn empty_cabin_cache_dir_value_falls_through() {
let env = env_with(&[("CABIN_CACHE_DIR", "")]);
let xdg = home_xdg_cache_home("/tmp/home");
let out = cache_dir_for_with_env(fake_manifest(), None, &env, Some(&xdg)).unwrap();
assert_eq!(out, PathBuf::from("/tmp/home/.cache/cabin"));
}
#[test]
fn empty_cabin_cache_home_value_falls_through_to_xdg() {
let env = env_with(&[("CABIN_CACHE_HOME", "")]);
let xdg = PathBuf::from("/tmp/xdg/cabin");
let out = cache_dir_for_with_env(fake_manifest(), None, &env, Some(&xdg)).unwrap();
assert_eq!(out, PathBuf::from("/tmp/xdg/cabin"));
}
#[test]
fn all_envs_unset_returns_error() {
let env = env_with(&[]);
let err = cache_dir_for_with_env(fake_manifest(), None, &env, None).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("no cache directory"),
"expected diagnostic mentioning 'no cache directory', got: {msg}"
);
}
}