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, ResolvedSource};
use cabin_workspace::{PackageGraph, RegistryPackageSource, collect_patched_versioned_deps};
use crate::completions::CompgenArgs;
use crate::manpages::MangenArgs;
pub(crate) mod add;
pub(crate) mod build_prep;
pub(crate) mod config;
pub(crate) mod env_flags;
pub(crate) mod explain;
pub(crate) mod fetch_output;
pub(crate) mod fmt;
pub(crate) mod metadata;
pub(crate) mod ninja;
pub(crate) mod patch;
pub(crate) mod port;
pub(crate) mod remove;
pub(crate) mod run;
pub(crate) mod source_tooling;
pub(crate) mod standard_compat;
pub(crate) mod system_deps;
pub(crate) mod term_color;
pub(crate) mod term_verbosity;
pub(crate) mod test;
pub(crate) mod tidy;
pub(crate) mod tree;
pub(crate) mod vendor;
pub(crate) mod version;
mod build;
mod clean;
mod init;
mod manifest_edit;
mod package;
mod resolve;
use self::build::{BuildMode, build};
use self::clean::clean;
use self::init::{init, new};
use self::package::{package, publish};
use self::resolve::{fetch, resolve, update};
use crate::cli::fetch_output::emit_fetch_output;
use crate::cli::term_color::CliColorChoice;
use crate::cli::term_verbosity::Reporter;
pub(crate) const VERSIONED_DEPS_REQUIRE_INDEX: &str =
"versioned dependencies require --index-path, --index-url, or a `[registry]` config setting";
pub(crate) const FROZEN_INDEX_URL_ERR: &str = "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";
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 = 'Z',
value_name = "FEATURE",
global = true,
action = clap::ArgAction::Append,
value_parser = parse_experimental_feature,
display_order = 5,
)]
pub(crate) unstable: Vec<cabin_core::ExperimentalFeature>,
#[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>,
}
fn parse_experimental_feature(
raw: &str,
) -> Result<cabin_core::ExperimentalFeature, cabin_core::UnknownExperimentalFeature> {
raw.parse()
}
#[derive(Debug, Subcommand)]
pub(crate) enum Command {
Init(InitArgs),
New(NewArgs),
Add(crate::cli::add::AddArgs),
Remove(crate::cli::remove::RemoveArgs),
#[command(hide = true)]
Metadata(ManifestArgs),
#[command(visible_alias = "b")]
Build(BuildArgs),
Check(BuildArgs),
Clean(CleanArgs),
#[command(visible_alias = "r")]
Run(crate::cli::run::RunArgs),
#[command(visible_alias = "t")]
Test(crate::cli::test::TestArgs),
#[command(hide = true)]
Resolve(ResolveArgs),
Update(UpdateArgs),
#[command(hide = true)]
Fetch(FetchArgs),
#[command(hide = true)]
Vendor(crate::cli::vendor::VendorArgs),
#[command(hide = true)]
Tree(crate::cli::tree::TreeArgs),
#[command(hide = true)]
Explain(crate::cli::explain::ExplainArgs),
#[command(hide = true)]
Package(PackageArgs),
Publish(PublishArgs),
Fmt(crate::cli::fmt::FmtArgs),
Tidy(crate::cli::tidy::TidyArgs),
Port(crate::port_subcommand::PortArgs),
#[command(hide = true)]
Compgen(CompgenArgs),
#[command(hide = true)]
Mangen(MangenArgs),
Version(crate::cli::version::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::cli::version::version(crate::cli::version::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);
};
debug_assert!(
cli.unstable.is_empty(),
"no experimental features are registered"
);
match command {
Command::Init(args) => init(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::New(args) => new(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::Add(args) => crate::cli::add::add(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::Remove(args) => {
crate::cli::remove::remove(&args, reporter).map(|()| ExitCode::SUCCESS)
}
Command::Metadata(args) => {
crate::cli::metadata::metadata(&args, reporter).map(|()| ExitCode::SUCCESS)
}
Command::Build(args) => {
build(&args, reporter, BuildMode::Build, color).map(|()| ExitCode::SUCCESS)
}
Command::Check(args) => {
build(&args, reporter, BuildMode::Check, color).map(|()| ExitCode::SUCCESS)
}
Command::Clean(args) => clean(&args, reporter).map(|()| ExitCode::SUCCESS),
Command::Run(args) => crate::cli::run::run(&args, reporter, color),
Command::Test(args) => {
crate::cli::test::test(&args, reporter, color).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::cli::vendor::vendor(&args, reporter).map(|()| ExitCode::SUCCESS)
}
Command::Tree(args) => crate::cli::tree::tree(&args).map(|()| ExitCode::SUCCESS),
Command::Explain(args) => {
crate::cli::explain::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::cli::fmt::fmt(&args, reporter),
Command::Tidy(args) => crate::cli::tidy::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::cli::version::version(args, reporter.verbosity()).map(|()| ExitCode::SUCCESS)
}
}
}
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 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())?;
return Ok(cabin_core::ProfileSelection::from_name(pname));
}
if release {
return Ok(cabin_core::ProfileSelection::release_alias());
}
if let Some((selection, _source)) = crate::cli::config::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> {
profile_selection_from_flags(name, false, config)
}
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::cli::config::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_request: Option<&cabin_core::CompilerWrapperRequest>,
effective_config: &cabin_config::EffectiveConfig,
) -> Result<Option<cabin_core::ResolvedCompilerWrapper>> {
let mut wrapper_inputs =
cabin_toolchain::WrapperInputs::from_process(cli_override, manifest_request);
if let Some(layer) = crate::cli::config::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,
) -> Option<cabin_core::CompilerWrapperRequest> {
graph.root_settings.compiler_wrapper.clone()
}
pub(crate) fn resolve_per_package_language_standards(
graph: &PackageGraph,
) -> HashMap<usize, cabin_core::ResolvedLanguageStandards> {
graph
.packages
.iter()
.enumerate()
.map(|(idx, pkg)| {
(
idx,
cabin_core::resolve_language_standards(&pkg.package.language),
)
})
.collect()
}
pub(crate) fn resolve_per_package_build_flags(
graph: &PackageGraph,
profile: &cabin_core::ResolvedProfile,
host_platform: &cabin_core::TargetPlatform,
feature_resolution: &cabin_feature::FeatureResolution,
detection: Option<&cabin_core::ToolchainDetectionReport>,
) -> (
HashMap<usize, cabin_core::ResolvedProfileFlags>,
HashMap<usize, Vec<cabin_core::StandardFlagConflict>>,
) {
let (cc_identity, cxx_identity) = match detection {
Some(report) => (
report.cc.as_ref().map(|tool| &tool.identity),
Some(&report.cxx.identity),
),
None => (None, None),
};
let mut out = HashMap::with_capacity(graph.packages.len());
let mut conflicts: HashMap<usize, Vec<cabin_core::StandardFlagConflict>> = HashMap::new();
for (idx, pkg) in graph.packages.iter().enumerate() {
let package_trusted = matches!(pkg.kind, cabin_workspace::PackageKind::Local);
let package_features = feature_resolution.for_package(idx);
let ctx = cabin_core::ConditionContext::with_features(
host_platform,
&package_features.enabled_features,
)
.with_compilers(cc_identity, cxx_identity);
let resolved = cabin_core::resolve_build_flags(
&pkg.package.build,
Some(profile),
&graph.root_settings.profiles,
&ctx,
package_trusted,
);
let pkg_conflicts = cabin_core::find_standard_flag_conflicts(
pkg.package.name.as_str(),
&pkg.package.language,
&pkg.package.targets,
&resolved,
);
if !pkg_conflicts.is_empty() {
conflicts.insert(idx, pkg_conflicts);
}
out.insert(idx, resolved);
}
(out, conflicts)
}
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::cli::system_deps::augment_build_flags_with_system_deps(
graph,
host_platform,
dev_for,
build_flags,
reporter,
)?;
let (build_flags, _env_build_flags) = crate::cli::env_flags::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,
language: cabin_core::LanguageStandardsSummary::from_package(&pkg.package),
})
.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,
dev_for: &BTreeSet<String>,
) -> 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,
dev_for,
)
.map_err(anyhow::Error::from)
}
pub(crate) fn enabled_features_by_package(
resolution: &cabin_feature::FeatureResolution,
) -> std::collections::HashMap<usize, BTreeSet<String>> {
resolution
.per_package
.iter()
.map(|(idx, resolved)| (*idx, resolved.enabled_features.clone()))
.collect()
}
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)
}
enum SinglePackageSelection {
Standalone { manifest_path: PathBuf },
WorkspaceMember {
manifest_path: PathBuf,
package: Box<cabin_core::Package>,
workspace_dep_requirements: cabin_core::WorkspaceDepRequirements,
},
}
impl SinglePackageSelection {
fn into_parts(
self,
) -> (
PathBuf,
Option<cabin_core::Package>,
cabin_core::WorkspaceDepRequirements,
) {
match self {
Self::Standalone { manifest_path } => (
manifest_path,
None,
cabin_core::WorkspaceDepRequirements::default(),
),
Self::WorkspaceMember {
manifest_path,
package,
workspace_dep_requirements,
} => (manifest_path, Some(*package), workspace_dep_requirements),
}
}
}
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::Standalone {
manifest_path: invocation.to_path_buf(),
});
}
let mut workspace_dep_requirements = cabin_core::WorkspaceDepRequirements::default();
if let Some(workspace) = &parsed.workspace {
for (kind, table) in [
(cabin_core::DependencyKind::Normal, &workspace.dependencies),
(cabin_core::DependencyKind::Dev, &workspace.dev_dependencies),
] {
for (name, requirement) in table {
workspace_dep_requirements.insert(kind, name.clone(), requirement.clone());
}
}
}
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::WorkspaceMember {
manifest_path: graph.packages[idx].manifest_path.clone(),
package: Box::new(graph.packages[idx].package.clone()),
workspace_dep_requirements,
})
}
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(override_dir: Option<&Path>) -> Result<PathBuf> {
use etcetera::{BaseStrategy, choose_base_strategy};
let user_cache_home = choose_base_strategy()
.ok()
.map(|dirs| dirs.cache_dir().join("cabin"));
cache_dir_for_with_env(
override_dir,
&|key| std::env::var_os(key),
user_cache_home.as_deref(),
)
}
fn cache_dir_for_with_env(
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_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()));
}
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_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) incompatible_standards: cabin_core::IncompatibleStandards,
}
pub(crate) struct ArtifactPipeline {
pub(crate) fetched: Vec<FetchedPackage>,
pub(crate) lockfile_pinned: BTreeSet<(String, String)>,
}
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,
request.dev_for,
)?;
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(),
lockfile_pinned: BTreeSet::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 = request.mode.resolve_mode()?;
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;
input.consumer_standards = graph.consumer_standards(
&resolved_selection.closure(graph),
&resolved_selection.packages,
&enabled_features_by_package(&features),
request.dev_for,
);
input.incompatible_standards = request.incompatible_standards;
let active_patch_records = crate::cli::patch::lockfile_patches(request.active_patches);
let active_replacement_records = crate::cli::patch::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_packages(&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,
lockfile_pinned: match &existing_lockfile {
Some(lock) if matches!(request.mode, LockMode::PreferLocked | LockMode::Locked) => {
output
.packages
.iter()
.filter(|p| lock.find(&p.name).is_some_and(|l| l.version == p.version))
.map(|p| (p.name.as_str().to_owned(), p.version.to_string()))
.collect()
}
_ => BTreeSet::new(),
},
})
}
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) => Ok((load_local_index(path)?, IndexAccess::Local)),
(None, Some(url)) => {
if frozen {
bail!(FROZEN_INDEX_URL_ERR);
}
let (index, client) = load_http_index(url, root_deps)?;
Ok((index, IndexAccess::Http(client)))
}
}
}
fn load_local_index(path: &Path) -> Result<PackageIndex> {
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()))
}
fn load_http_index(
url: &str,
root_deps: &BTreeMap<PackageName, semver::VersionReq>,
) -> Result<(PackageIndex, cabin_index_http::HttpClient)> {
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, 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),
}
impl LockMode {
pub(crate) fn resolve_mode(&self) -> Result<ResolveMode> {
Ok(match self {
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}"))?,
),
})
}
}
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(),
}
}
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(),
is_port: false,
}
}
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 profile = cabin_core::resolve_profile(
&cabin_core::ProfileSelection::default_dev(),
&graph.root_settings.profiles,
)
.unwrap();
let (resolved, _conflicts) = resolve_per_package_build_flags(
&graph,
&profile,
&host,
&cabin_feature::FeatureResolution::default(),
None,
);
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 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(Some(Path::new("/tmp/from-flag")), &env, Some(&xdg)).unwrap();
assert_eq!(out, absolutise(Path::new("/tmp/from-flag")).unwrap());
}
#[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(None, &env, Some(&xdg)).unwrap();
assert_eq!(out, absolutise(Path::new("/tmp/from-env")).unwrap());
}
#[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(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(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(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(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(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(None, &env, None).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("no cache directory"),
"expected diagnostic mentioning 'no cache directory', got: {msg}"
);
}
}