1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result, bail};
5use clap::{Args, Parser, Subcommand};
6
7use cabin_artifact::{ArtifactCache, FetchEntry, FetchOptions, FetchPlan, FetchedPackage};
8use cabin_build::{PlanRequest, plan};
9use cabin_core::PackageName;
10use cabin_index::PackageIndex;
11use cabin_lockfile::{LockedPackage, LockedSource, Lockfile};
12use cabin_package::scaffold;
13use cabin_resolver::{
14 LockedVersion, ResolveInput, ResolveMode, ResolveOutput, ResolvedPackage, ResolvedSource,
15};
16use cabin_workspace::{PackageGraph, RegistryPackageSource, collect_patched_versioned_deps};
17
18use crate::completions::CompgenArgs;
19use crate::fetch_output_glue::emit_fetch_output;
20use crate::manpages::MangenArgs;
21use crate::metadata_glue::{MetadataInputs, MetadataView};
22use crate::term_color_glue::CliColorChoice;
23use crate::term_verbosity_glue::Reporter;
24
25fn cli_styles() -> clap::builder::Styles {
35 use clap::builder::styling::{AnsiColor, Color, Style};
36
37 let header_usage = Style::new()
38 .bold()
39 .fg_color(Some(Color::Ansi(AnsiColor::BrightGreen)));
40 let literal = Style::new()
41 .bold()
42 .fg_color(Some(Color::Ansi(AnsiColor::BrightCyan)));
43 let placeholder = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan)));
44 let error = Style::new()
45 .bold()
46 .fg_color(Some(Color::Ansi(AnsiColor::BrightRed)));
47 let invalid = Style::new()
48 .bold()
49 .fg_color(Some(Color::Ansi(AnsiColor::Yellow)));
50 let valid = Style::new()
51 .bold()
52 .fg_color(Some(Color::Ansi(AnsiColor::BrightGreen)));
53
54 clap::builder::Styles::styled()
55 .usage(header_usage)
56 .header(header_usage)
57 .literal(literal)
58 .placeholder(placeholder)
59 .error(error)
60 .invalid(invalid)
61 .valid(valid)
62}
63
64const HELP_TEMPLATE: &str = concat!(
80 "{about-with-newline}\n",
81 "{usage-heading} {usage}\n",
82 "\n",
83 "\x1b[1m\x1b[92mOptions:\x1b[0m\n",
85 "{options}",
86 "{after-help}",
87);
88
89#[derive(Debug, Parser)]
91#[command(
92 name = "cabin",
93 about = "A package manager and build system for C/C++",
94 disable_version_flag = true,
95 styles = cli_styles(),
96 help_template = HELP_TEMPLATE,
97 next_line_help = false,
101)]
102pub struct Cli {
103 #[arg(
108 short = 'v',
109 long = "verbose",
110 global = true,
111 action = clap::ArgAction::Count,
112 conflicts_with = "quiet",
113 display_order = 1,
114 )]
115 pub(crate) verbose: u8,
116
117 #[arg(
119 short = 'q',
120 long = "quiet",
121 global = true,
122 conflicts_with = "verbose",
123 display_order = 2
124 )]
125 pub(crate) quiet: bool,
126
127 #[arg(
138 long,
139 value_name = "WHEN",
140 value_enum,
141 global = true,
142 hide_possible_values = true,
143 display_order = 3
144 )]
145 pub(crate) color: Option<CliColorChoice>,
146
147 #[arg(long, display_order = 4)]
156 pub(crate) list: bool,
157
158 #[arg(
166 short = 'V',
167 long = "version",
168 global = true,
169 action = clap::ArgAction::SetTrue,
170 display_order = 6,
171 )]
172 pub(crate) version: bool,
173
174 #[command(subcommand)]
179 pub(crate) command: Option<Command>,
180}
181
182#[derive(Debug, Subcommand)]
211pub(crate) enum Command {
212 Init(InitArgs),
214 New(NewArgs),
219 #[command(hide = true)]
227 Metadata(ManifestArgs),
228 #[command(visible_alias = "b")]
239 Build(BuildArgs),
240 Clean(CleanArgs),
246 #[command(visible_alias = "r")]
252 Run(crate::run_glue::RunArgs),
253 #[command(visible_alias = "t")]
259 Test(crate::test_glue::TestArgs),
260 #[command(hide = true)]
266 Resolve(ResolveArgs),
267 Update(UpdateArgs),
269 #[command(hide = true)]
276 Fetch(FetchArgs),
277 #[command(hide = true)]
285 Vendor(crate::vendor_glue::VendorArgs),
286 #[command(hide = true)]
294 Tree(crate::tree_glue::TreeArgs),
295 #[command(hide = true)]
302 Explain(crate::explain_glue::ExplainArgs),
303 #[command(hide = true)]
309 Package(PackageArgs),
310 Publish(PublishArgs),
318 Fmt(crate::fmt_glue::FmtArgs),
323 Tidy(crate::tidy_glue::TidyArgs),
328 Port(crate::port_subcommand::PortArgs),
330 #[command(hide = true)]
332 Compgen(CompgenArgs),
333 #[command(hide = true)]
335 Mangen(MangenArgs),
336 Version(crate::version_glue::VersionArgs),
345}
346
347#[derive(Debug, Args)]
348pub(crate) struct InitArgs {
349 #[arg(long)]
351 pub name: Option<String>,
352
353 #[arg(short = 'b', long, group = "init_scaffold_kind")]
357 pub bin: bool,
358
359 #[arg(short = 'l', long, group = "init_scaffold_kind")]
363 pub lib: bool,
364}
365
366#[derive(Debug, Args)]
367pub(crate) struct NewArgs {
368 #[arg(value_name = "PATH")]
370 pub path: PathBuf,
371
372 #[arg(long)]
374 pub name: Option<String>,
375
376 #[arg(short = 'b', long, group = "new_scaffold_kind")]
380 pub bin: bool,
381
382 #[arg(short = 'l', long, group = "new_scaffold_kind")]
386 pub lib: bool,
387}
388
389#[derive(Debug, Args)]
390pub(crate) struct CleanArgs {
391 #[arg(long, value_name = "PATH")]
394 pub manifest_path: Option<PathBuf>,
395
396 #[arg(long, value_name = "PATH")]
401 pub build_dir: Option<PathBuf>,
402
403 #[arg(long, conflicts_with = "profile")]
406 pub release: bool,
407
408 #[arg(long, value_name = "NAME")]
411 pub profile: Option<String>,
412
413 #[arg(long)]
417 pub dry_run: bool,
418
419 #[command(flatten)]
421 pub workspace_selection: WorkspaceSelectionArgs,
422}
423
424#[derive(Debug, Args)]
425pub(crate) struct ManifestArgs {
426 #[arg(long, value_name = "PATH")]
429 pub manifest_path: Option<PathBuf>,
430
431 #[command(flatten)]
436 pub selection: ConfigSelectionArgs,
437
438 #[command(flatten)]
442 pub workspace_selection: WorkspaceSelectionArgs,
443
444 #[arg(long, value_name = "FORMAT", default_value = "json")]
449 pub format: ResolveFormat,
450
451 #[arg(long, value_name = "NAME")]
456 pub profile: Option<String>,
457
458 #[command(flatten)]
462 pub toolchain: ToolchainSelectionArgs,
463
464 #[arg(long)]
470 pub no_patches: bool,
471
472 #[arg(long)]
477 pub offline: bool,
478}
479
480#[derive(Debug, Args)]
481pub(crate) struct BuildArgs {
482 #[arg(long, value_name = "PATH")]
484 pub manifest_path: Option<PathBuf>,
485
486 #[arg(long, value_name = "PATH")]
490 pub build_dir: Option<PathBuf>,
491
492 #[arg(short = 'r', long, conflicts_with = "profile")]
498 pub release: bool,
499
500 #[arg(long, value_name = "NAME")]
504 pub profile: Option<String>,
505
506 #[arg(long, value_name = "PATH")]
511 pub index_path: Option<PathBuf>,
512
513 #[arg(long, value_name = "URL")]
518 pub index_url: Option<String>,
519
520 #[arg(long, value_name = "PATH")]
522 pub cache_dir: Option<PathBuf>,
523
524 #[arg(long, conflicts_with = "frozen")]
527 pub locked: bool,
528
529 #[arg(long)]
533 pub frozen: bool,
534
535 #[arg(long)]
542 pub offline: bool,
543
544 #[arg(long, value_name = "FEATURES")]
548 pub features: Vec<String>,
549
550 #[arg(long)]
554 pub all_features: bool,
555
556 #[arg(long)]
559 pub no_default_features: bool,
560
561 #[command(flatten)]
563 pub workspace_selection: WorkspaceSelectionArgs,
564
565 #[command(flatten)]
569 pub toolchain: ToolchainSelectionArgs,
570
571 #[arg(long)]
574 pub no_patches: bool,
575
576 #[arg(short = 'j', long = "jobs", value_name = "N")]
582 pub jobs: Option<cabin_core::BuildJobs>,
583}
584
585#[derive(Debug, Args, Default)]
590pub(crate) struct ToolchainSelectionArgs {
591 #[arg(long, value_name = "PATH-OR-NAME")]
595 pub cc: Option<String>,
596
597 #[arg(long, value_name = "PATH-OR-NAME")]
601 pub cxx: Option<String>,
602
603 #[arg(long, value_name = "PATH-OR-NAME")]
607 pub ar: Option<String>,
608
609 #[arg(long, value_name = "WRAPPER", conflicts_with = "no_compiler_wrapper")]
617 pub compiler_wrapper: Option<String>,
618
619 #[arg(long)]
625 pub no_compiler_wrapper: bool,
626}
627
628#[derive(Debug, Args, Default)]
630pub(crate) struct ConfigSelectionArgs {
631 #[arg(long, value_name = "FEATURES")]
633 pub features: Vec<String>,
634
635 #[arg(long)]
637 pub all_features: bool,
638
639 #[arg(long)]
641 pub no_default_features: bool,
642}
643
644#[derive(Debug, Args, Default)]
654pub(crate) struct WorkspaceSelectionArgsForUpdate {
655 #[arg(long, conflicts_with = "default_members")]
657 pub workspace: bool,
658
659 #[arg(long, conflicts_with = "workspace")]
662 pub default_members: bool,
663
664 #[arg(long, value_name = "PACKAGE")]
667 pub exclude: Vec<String>,
668}
669
670#[derive(Debug, Args, Default)]
678pub(crate) struct WorkspaceSelectionArgs {
679 #[arg(
682 long,
683 conflicts_with_all = &["package", "default_members"],
684 )]
685 pub workspace: bool,
686
687 #[arg(long = "package", short = 'p', value_name = "PACKAGE")]
691 pub package: Vec<String>,
692
693 #[arg(long, conflicts_with_all = &["workspace", "package"])]
696 pub default_members: bool,
697
698 #[arg(long, value_name = "PACKAGE")]
702 pub exclude: Vec<String>,
703}
704
705#[derive(Debug, Args)]
706pub(crate) struct FetchArgs {
707 #[arg(long, value_name = "PATH")]
709 pub manifest_path: Option<PathBuf>,
710
711 #[arg(long, value_name = "PATH")]
716 pub index_path: Option<PathBuf>,
717
718 #[arg(long, value_name = "URL")]
721 pub index_url: Option<String>,
722
723 #[arg(long, value_name = "PATH")]
725 pub cache_dir: Option<PathBuf>,
726
727 #[arg(long, conflicts_with = "frozen")]
730 pub locked: bool,
731
732 #[arg(long)]
736 pub frozen: bool,
737
738 #[arg(long)]
742 pub offline: bool,
743
744 #[arg(long, value_name = "FORMAT", default_value = "human")]
747 pub format: ResolveFormat,
748
749 #[command(flatten)]
751 pub workspace_selection: WorkspaceSelectionArgs,
752
753 #[arg(long)]
756 pub no_patches: bool,
757}
758
759#[derive(Debug, Args)]
760pub(crate) struct PackageArgs {
761 #[arg(long, value_name = "PATH")]
765 pub manifest_path: Option<PathBuf>,
766
767 #[arg(long, default_value = "dist")]
769 pub output_dir: PathBuf,
770
771 #[arg(long, value_name = "FORMAT", default_value = "human")]
774 pub format: ResolveFormat,
775
776 #[command(flatten)]
780 pub workspace_selection: WorkspaceSelectionArgs,
781}
782
783#[derive(Debug, Args)]
784pub(crate) struct PublishArgs {
785 #[arg(long, value_name = "PATH")]
788 pub manifest_path: Option<PathBuf>,
789
790 #[arg(long, value_name = "PATH")]
794 pub output_dir: Option<PathBuf>,
795
796 #[arg(long)]
801 pub dry_run: bool,
802
803 #[arg(long, value_name = "PATH")]
807 pub registry_dir: Option<PathBuf>,
808
809 #[arg(long, value_name = "FORMAT", default_value = "human")]
811 pub format: ResolveFormat,
812
813 #[command(flatten)]
817 pub workspace_selection: WorkspaceSelectionArgs,
818}
819
820#[derive(Debug, Args)]
821pub(crate) struct ResolveArgs {
822 #[arg(long, value_name = "PATH")]
824 pub manifest_path: Option<PathBuf>,
825
826 #[arg(long, value_name = "PATH")]
831 pub index_path: Option<PathBuf>,
832
833 #[arg(long, value_name = "URL")]
836 pub index_url: Option<String>,
837
838 #[arg(long, value_name = "FORMAT", default_value = "human")]
841 pub format: ResolveFormat,
842
843 #[arg(long, conflicts_with = "frozen")]
847 pub locked: bool,
848
849 #[arg(long)]
852 pub frozen: bool,
853
854 #[arg(long)]
858 pub offline: bool,
859
860 #[command(flatten)]
865 pub workspace_selection: WorkspaceSelectionArgs,
866
867 #[arg(long, value_name = "FEATURES")]
870 pub features: Vec<String>,
871
872 #[arg(long)]
875 pub all_features: bool,
876
877 #[arg(long)]
879 pub no_default_features: bool,
880
881 #[arg(long)]
884 pub no_patches: bool,
885}
886
887#[derive(Debug, Args)]
888pub(crate) struct UpdateArgs {
889 #[arg(long, value_name = "PATH")]
891 pub manifest_path: Option<PathBuf>,
892
893 #[arg(long, value_name = "PATH")]
898 pub index_path: Option<PathBuf>,
899
900 #[arg(long, value_name = "URL")]
903 pub index_url: Option<String>,
904
905 #[arg(long, value_name = "NAME")]
917 pub package: Option<String>,
918
919 #[arg(long, value_name = "FORMAT", default_value = "human")]
921 pub format: ResolveFormat,
922
923 #[arg(long)]
927 pub offline: bool,
928
929 #[command(flatten)]
933 pub workspace_selection: WorkspaceSelectionArgsForUpdate,
934
935 #[arg(long)]
938 pub no_patches: bool,
939}
940
941#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
942pub(crate) enum ResolveFormat {
943 Human,
944 Json,
945}
946
947const MANIFEST_FILENAME: &str = scaffold::MANIFEST_FILENAME;
949
950pub(crate) fn run(
965 cli: Cli,
966 reporter: Reporter,
967 color: cabin_core::ColorChoice,
968) -> Result<std::process::ExitCode> {
969 use std::process::ExitCode;
970 if cli.version {
977 crate::version_glue::version(crate::version_glue::VersionArgs {}, reporter.verbosity())?;
978 return Ok(ExitCode::SUCCESS);
979 }
980 if cli.list {
988 let mut stdout =
989 termcolor::StandardStream::stdout(cabin_diagnostics::termcolor_choice(color));
990 crate::command_list::print_list(&mut stdout)?;
991 return Ok(ExitCode::SUCCESS);
992 }
993 let Some(command) = cli.command else {
994 let mut cmd = <Cli as clap::CommandFactory>::command();
999 cmd.print_help().context("failed to print top-level help")?;
1000 return Ok(ExitCode::SUCCESS);
1003 };
1004 match command {
1005 Command::Init(args) => init(&args, reporter).map(|()| ExitCode::SUCCESS),
1006 Command::New(args) => new(&args, reporter).map(|()| ExitCode::SUCCESS),
1007 Command::Metadata(args) => metadata(&args, reporter).map(|()| ExitCode::SUCCESS),
1008 Command::Build(args) => build(&args, reporter).map(|()| ExitCode::SUCCESS),
1009 Command::Clean(args) => clean(&args, reporter).map(|()| ExitCode::SUCCESS),
1010 Command::Run(args) => crate::run_glue::run(&args, reporter),
1011 Command::Test(args) => crate::test_glue::test(&args, reporter).map(|()| ExitCode::SUCCESS),
1012 Command::Resolve(args) => resolve(&args, reporter).map(|()| ExitCode::SUCCESS),
1013 Command::Update(args) => update(&args, reporter).map(|()| ExitCode::SUCCESS),
1014 Command::Fetch(args) => fetch(&args, reporter).map(|()| ExitCode::SUCCESS),
1015 Command::Vendor(args) => {
1016 crate::vendor_glue::vendor(&args, reporter).map(|()| ExitCode::SUCCESS)
1017 }
1018 Command::Tree(args) => crate::tree_glue::tree(&args).map(|()| ExitCode::SUCCESS),
1019 Command::Explain(args) => {
1020 crate::explain_glue::explain(&args, reporter).map(|()| ExitCode::SUCCESS)
1021 }
1022 Command::Package(args) => package(&args, reporter).map(|()| ExitCode::SUCCESS),
1023 Command::Publish(args) => publish(&args, reporter).map(|()| ExitCode::SUCCESS),
1024 Command::Fmt(args) => crate::fmt_glue::fmt(&args, reporter),
1025 Command::Tidy(args) => crate::tidy_glue::tidy(&args, reporter),
1026 Command::Port(args) => {
1027 crate::port_subcommand::port(&args, reporter).map(|()| ExitCode::SUCCESS)
1028 }
1029 Command::Compgen(args) => crate::completions::run(&args).map(|()| ExitCode::SUCCESS),
1030 Command::Mangen(args) => crate::manpages::run(&args).map(|()| ExitCode::SUCCESS),
1031 Command::Version(args) => {
1032 crate::version_glue::version(args, reporter.verbosity()).map(|()| ExitCode::SUCCESS)
1033 }
1034 }
1035}
1036
1037fn scaffold_kind_from_flags(_bin: bool, lib: bool) -> scaffold::ScaffoldKind {
1038 if lib {
1043 scaffold::ScaffoldKind::Library
1044 } else {
1045 scaffold::ScaffoldKind::Binary
1046 }
1047}
1048
1049fn report_scaffold(reporter: Reporter, verb: &str, report: &scaffold::ScaffoldReport, dest: &Path) {
1050 reporter.status(
1060 verb,
1061 format_args!(
1062 "{kind} `{name}` package",
1063 kind = report.kind.label(),
1064 name = report.name.as_str(),
1065 ),
1066 );
1067 for created in &report.files_created {
1068 let relative = created.strip_prefix(dest).unwrap_or(created);
1069 reporter.verbose(format_args!(
1070 "cabin: wrote {}",
1071 relative.display().to_string().replace('\\', "/")
1072 ));
1073 }
1074}
1075
1076fn init(args: &InitArgs, reporter: Reporter) -> Result<()> {
1077 let cwd = std::env::current_dir().context("failed to determine current directory")?;
1078 let kind = scaffold_kind_from_flags(args.bin, args.lib);
1079 let request = scaffold::ScaffoldRequest::new(&cwd)
1080 .with_name(args.name.as_deref())
1081 .with_kind(kind)
1082 .with_gitignore(true);
1083 let report = scaffold::scaffold(request)?;
1084 report_scaffold(reporter, "Created", &report, &cwd);
1087 Ok(())
1088}
1089
1090fn new(args: &NewArgs, reporter: Reporter) -> Result<()> {
1091 let target = args.path.clone();
1092 if target.as_os_str().is_empty() {
1093 bail!("destination path must not be empty");
1094 }
1095 if target.exists() {
1096 bail!(
1097 "destination {} already exists; use `cabin init` to initialize an existing directory",
1098 target.display()
1099 );
1100 }
1101 if let Some(parent) = target.parent()
1102 && !parent.as_os_str().is_empty()
1103 && !parent.is_dir()
1104 {
1105 bail!(
1106 "parent directory {} does not exist; create it first or pass a path under an existing directory",
1107 parent.display()
1108 );
1109 }
1110
1111 std::fs::create_dir(&target)
1112 .with_context(|| format!("failed to create directory {}", target.display()))?;
1113
1114 let kind = scaffold_kind_from_flags(args.bin, args.lib);
1115 let request = scaffold::ScaffoldRequest::new(&target)
1116 .with_name(args.name.as_deref())
1117 .with_kind(kind)
1118 .with_gitignore(true);
1119 match scaffold::scaffold(request) {
1120 Ok(report) => {
1121 report_scaffold(reporter, "Created", &report, &target);
1122 Ok(())
1123 }
1124 Err(err) => {
1125 let _ = std::fs::remove_dir_all(&target);
1129 Err(err.into())
1130 }
1131 }
1132}
1133
1134fn metadata(args: &ManifestArgs, reporter: Reporter) -> Result<()> {
1135 let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
1136 let metadata_selection = cabin_workspace::PackageSelection {
1140 mode: cabin_workspace::SelectionMode::WholeWorkspace,
1141 exclude: Vec::new(),
1142 };
1143 let port_prep = crate::port_glue::prepare_ports_and_load_initial_graph(
1151 &manifest_path,
1152 None,
1153 true,
1154 false,
1155 false,
1156 &metadata_selection,
1157 args.no_patches,
1158 );
1159 let (prepared_ports, initial_graph) = match port_prep {
1160 Ok(result) => result,
1161 Err(err) if crate::port_glue::is_metadata_recoverable(&err) => (
1162 Vec::new(),
1163 cabin_workspace::load_workspace_skip_ports(&manifest_path)?,
1164 ),
1165 Err(err) => return Err(err),
1166 };
1167 let port_sources: Vec<cabin_workspace::PortPackageSource> = prepared_ports
1168 .iter()
1169 .map(crate::port_glue::workspace_source)
1170 .collect();
1171 let effective_config = crate::config_glue::load_effective_config(&initial_graph)?;
1172 let resolved_index_for_offline_check =
1177 crate::config_glue::resolve_index_source(None, None, &effective_config)?;
1178 let metadata_offline = crate::config_glue::effective_offline(args.offline)?;
1179 crate::config_glue::enforce_offline_index_source(
1180 metadata_offline,
1181 resolved_index_for_offline_check.as_ref(),
1182 )?;
1183 let active_patches =
1186 crate::patch_glue::load_active_patches(&initial_graph, &effective_config, args.no_patches)?;
1187 let patched_sources = active_patches.workspace_sources();
1188 let graph = crate::patch_glue::reload_for_patches(
1189 &manifest_path,
1190 initial_graph,
1191 &patched_sources,
1192 &port_sources,
1193 )?;
1194 let lockfile_path = lockfile_path_for(&manifest_path);
1195 let lockfile = read_optional_lockfile(&lockfile_path)?;
1196 let request = build_selection_request(
1197 &args.selection.features,
1198 args.selection.all_features,
1199 args.selection.no_default_features,
1200 );
1201 let workspace_selection = build_workspace_selection(&args.workspace_selection);
1202 let resolved_selection =
1203 cabin_workspace::resolve_package_selection(&graph, &workspace_selection)?;
1204 let _feature_resolution = compute_feature_resolution(&graph, &resolved_selection, &request)?;
1208 let manifest_profiles = workspace_profile_definitions(&graph);
1209 let profile_selection =
1210 profile_selection_for_metadata(args.profile.as_deref(), &effective_config)?;
1211 let profile = cabin_core::resolve_profile(&profile_selection, &manifest_profiles)
1212 .map_err(|err| anyhow::anyhow!(err.to_string()))?;
1213 let host_platform = cabin_core::TargetPlatform::current();
1214 let toolchain_selection = toolchain_selection_from_args(&args.toolchain)?;
1215 let toolchain = resolve_toolchain_layered(
1216 &graph,
1217 &toolchain_selection,
1218 &effective_config,
1219 &host_platform,
1220 )?;
1221 let detection_report =
1227 match cabin_toolchain::detect_toolchain(&toolchain, &cabin_toolchain::ProcessRunner) {
1228 Ok(report) => Some(report),
1229 Err(err) => {
1230 reporter.warning(format_args!("toolchain detection failed: {err}"));
1231 None
1232 }
1233 };
1234 let manifest_compiler_wrapper = workspace_compiler_wrapper_settings(&graph);
1239 let cli_compiler_wrapper = compiler_wrapper_override_from_args(&args.toolchain)?;
1240 let mut wrapper_inputs = cabin_toolchain::WrapperInputs::from_process(
1241 cli_compiler_wrapper,
1242 &manifest_compiler_wrapper,
1243 &host_platform,
1244 );
1245 if let Some(layer) = crate::config_glue::wrapper_layer(&effective_config) {
1246 wrapper_inputs = wrapper_inputs.with_config(layer);
1247 }
1248 let compiler_wrapper = match cabin_toolchain::resolve_compiler_wrapper(
1249 &wrapper_inputs,
1250 Some(&cabin_toolchain::ProcessRunner),
1251 ) {
1252 Ok(w) => w,
1253 Err(err) => {
1254 reporter.warning(format_args!("compiler-wrapper resolution failed: {err}"));
1255 None
1256 }
1257 };
1258 let toolchain_summary =
1259 cabin_core::ToolchainSummary::from_resolved_parts(&toolchain, compiler_wrapper.as_ref());
1260 let profile_build = profile.build.as_ref();
1261 let build_flags = resolve_per_package_build_flags(&graph, profile_build, &host_platform);
1262 let dev_for: BTreeSet<String> = BTreeSet::new();
1266 let build_flags = augment_build_flags(&graph, &host_platform, &dev_for, build_flags, reporter)?;
1267 let configurations = resolve_build_configurations(
1268 &graph,
1269 &request,
1270 &resolved_selection.packages,
1271 &profile,
1272 &toolchain_summary,
1273 &build_flags,
1274 )?;
1275 let view = MetadataView::from_graph_and_lock(&MetadataInputs {
1276 graph: &graph,
1277 lockfile: lockfile.as_ref(),
1278 lockfile_path: &lockfile_path,
1279 configurations: &configurations,
1280 selection: &resolved_selection,
1281 profile: &profile,
1282 manifest_profiles: &manifest_profiles,
1283 toolchain: &toolchain,
1284 build_flags: &build_flags,
1285 detection: detection_report.as_ref(),
1286 compiler_wrapper: compiler_wrapper.as_ref(),
1287 config: &effective_config,
1288 active_patches: &active_patches,
1289 no_patches: args.no_patches,
1290 ports: &prepared_ports,
1291 });
1292 match args.format {
1293 ResolveFormat::Json => {
1294 crate::print_pretty_json(&view, "failed to serialize metadata as JSON")?;
1295 }
1296 ResolveFormat::Human => {
1297 for pkg in &view.packages {
1301 println!(
1302 "{} {} ({})",
1303 pkg.name,
1304 pkg.version,
1305 if pkg.is_root {
1306 "root"
1307 } else if pkg.is_primary {
1308 "primary"
1309 } else {
1310 "dep"
1311 }
1312 );
1313 }
1314 }
1315 }
1316 Ok(())
1317}
1318
1319fn build(args: &BuildArgs, reporter: Reporter) -> Result<()> {
1320 let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
1321
1322 let offline = crate::config_glue::effective_offline(args.offline)?;
1327 let build_selection = build_workspace_selection(&args.workspace_selection);
1328 let (prepared_ports, initial_graph) = crate::port_glue::prepare_ports_and_load_initial_graph(
1329 &manifest_path,
1330 args.cache_dir.as_deref(),
1331 offline,
1332 args.frozen,
1333 false,
1334 &build_selection,
1335 args.no_patches,
1336 )?;
1337 let port_sources: Vec<cabin_workspace::PortPackageSource> = prepared_ports
1338 .iter()
1339 .map(crate::port_glue::workspace_source)
1340 .collect();
1341 let effective_config = crate::config_glue::load_effective_config(&initial_graph)?;
1342 let active_patches =
1346 crate::patch_glue::load_active_patches(&initial_graph, &effective_config, args.no_patches)?;
1347 let patched_names = active_patches.owned_patched_names();
1348 let resolved_index_source = crate::config_glue::resolve_index_source(
1349 args.index_path.as_deref(),
1350 args.index_url.as_deref(),
1351 &effective_config,
1352 )?;
1353 let build_offline = crate::config_glue::effective_offline(args.offline)?;
1354 crate::config_glue::enforce_offline_index_source(
1355 build_offline,
1356 resolved_index_source.as_ref(),
1357 )?;
1358 let resolved_cache_dir =
1359 crate::config_glue::resolve_cache_dir(args.cache_dir.as_deref(), &effective_config);
1360
1361 let workspace_selection_for_pipeline = build_workspace_selection(&args.workspace_selection);
1366 let initial_resolved_selection = cabin_workspace::resolve_package_selection(
1367 &initial_graph,
1368 &workspace_selection_for_pipeline,
1369 )?;
1370 let initial_request =
1371 build_selection_request(&args.features, args.all_features, args.no_default_features);
1372 let initial_features = compute_feature_resolution(
1373 &initial_graph,
1374 &initial_resolved_selection,
1375 &initial_request,
1376 )?;
1377 let dev_for: BTreeSet<String> = BTreeSet::new();
1378 let patched_root_deps_preview =
1379 collect_patched_versioned_deps(&active_patches, &patched_names)?;
1380 let has_versioned = !patched_root_deps_preview.is_empty()
1381 || closure_has_versioned_deps_excluding_patches(
1382 &initial_graph,
1383 &initial_resolved_selection,
1384 &initial_features,
1385 &patched_names,
1386 &dev_for,
1387 );
1388
1389 let registry: Vec<RegistryPackageSource> = if has_versioned {
1390 let Some(index_source) = resolved_index_source.as_ref() else {
1391 bail!(
1392 "versioned dependencies require --index-path, --index-url, or a `[registry]` config setting"
1393 );
1394 };
1395 let inputs = crate::config_glue::resolve_pipeline_inputs(
1396 index_source,
1397 &effective_config,
1398 &manifest_path,
1399 args.cache_dir.as_deref(),
1400 resolved_cache_dir.as_ref(),
1401 build_offline,
1402 args.locked,
1403 args.frozen,
1404 args.no_patches,
1405 false,
1406 )?;
1407 let pipeline = run_artifact_pipeline(&ArtifactPipelineRequest {
1408 manifest_path: &manifest_path,
1409 initial_graph: &initial_graph,
1410 index_path: inputs.index_path.as_deref(),
1411 index_url: inputs.index_url.as_deref(),
1412 mode: inputs.mode,
1413 allow_write: inputs.allow_write,
1414 frozen: args.frozen,
1415 cache_dir: &inputs.cache_dir,
1416 reporter,
1417 selection: workspace_selection_for_pipeline,
1418 selection_request: &initial_request,
1419 patched_names: &patched_names,
1420 active_patches: &active_patches,
1421 source_replacements: &effective_config.source_replacements,
1422 no_patches: args.no_patches,
1423 dev_for: &dev_for,
1424 })?;
1425 pipeline.registry_sources()
1426 } else {
1427 Vec::new()
1428 };
1429
1430 let mut strict_packages: BTreeSet<String> =
1450 initial_resolved_selection.closure_package_names(&initial_graph);
1451 strict_packages.extend(patched_names.iter().cloned());
1452 strict_packages.extend(registry.iter().map(|r| r.name.as_str().to_owned()));
1453 let patched_sources = active_patches.workspace_sources();
1454 let graph = cabin_workspace::load_workspace_with_options(
1455 &manifest_path,
1456 &cabin_workspace::WorkspaceLoadOptions {
1457 registry: ®istry,
1458 patches: &patched_sources,
1459 ports: &port_sources,
1460 registry_policy: cabin_workspace::RegistryPolicy::StrictFor(&strict_packages),
1461 include_dev_for: &BTreeSet::new(),
1462 port_policy: cabin_workspace::PortPolicy::TolerateExcept(&strict_packages),
1463 },
1464 )?;
1465
1466 let (build_dir_input, _build_dir_source) = crate::config_glue::resolve_build_dir_with_env(
1470 args.build_dir.as_deref(),
1471 &effective_config,
1472 );
1473 let build_dir = absolutise(&build_dir_input)
1474 .with_context(|| format!("failed to resolve build dir {}", build_dir_input.display()))?;
1475
1476 let host_platform = cabin_core::TargetPlatform::current();
1477 let toolchain_selection = toolchain_selection_from_args(&args.toolchain)?;
1478 let toolchain = resolve_toolchain_layered(
1479 &graph,
1480 &toolchain_selection,
1481 &effective_config,
1482 &host_platform,
1483 )?;
1484 let detection_report =
1491 cabin_toolchain::detect_toolchain(&toolchain, &cabin_toolchain::ProcessRunner)
1492 .map_err(|err| anyhow::anyhow!(err.to_string()))?;
1493 cabin_build::validate_toolchain_for_backend(&toolchain, &detection_report)?;
1494 let ninja = cabin_toolchain::locate_ninja()?;
1495
1496 let manifest_compiler_wrapper = workspace_compiler_wrapper_settings(&graph);
1497 let cli_compiler_wrapper = compiler_wrapper_override_from_args(&args.toolchain)?;
1498
1499 let profile_selection = profile_selection_for_build(args, &effective_config)?;
1506 let manifest_profiles = workspace_profile_definitions(&graph);
1507 let profile = cabin_core::resolve_profile(&profile_selection, &manifest_profiles)
1508 .map_err(|err| anyhow::anyhow!(err.to_string()))?;
1509
1510 let dev_for: BTreeSet<String> = BTreeSet::new();
1519 let prep =
1523 crate::build_prep_glue::resolve_build_prep(crate::build_prep_glue::BuildConfigInputs {
1524 graph: &graph,
1525 host_platform: &host_platform,
1526 toolchain: &toolchain,
1527 cli_compiler_wrapper,
1528 manifest_compiler_wrapper: &manifest_compiler_wrapper,
1529 effective_config: &effective_config,
1530 profile: &profile,
1531 dev_for: &dev_for,
1532 reporter,
1533 })?;
1534
1535 let workspace_selection = build_workspace_selection(&args.workspace_selection);
1540 let resolved_selection =
1541 cabin_workspace::resolve_package_selection(&graph, &workspace_selection)?;
1542
1543 let selection_request =
1546 build_selection_request(&args.features, args.all_features, args.no_default_features);
1547 let configurations = resolve_build_configurations(
1548 &graph,
1549 &selection_request,
1550 &resolved_selection.packages,
1551 &profile,
1552 &prep.toolchain_summary,
1553 &prep.build_flags,
1554 )?;
1555 let feature_resolution =
1556 compute_feature_resolution(&graph, &resolved_selection, &selection_request)?;
1557
1558 let root_configuration = graph
1559 .root_package
1560 .and_then(|i| configurations.get(&i))
1561 .cloned();
1562 let plan_graph = plan(&PlanRequest {
1563 graph: &graph,
1564 toolchain: &toolchain,
1565 build_flags: &prep.build_flags,
1566 build_dir: build_dir.clone(),
1567 profile: profile.clone(),
1568 selected: None,
1569 configuration: root_configuration.as_ref(),
1570 selected_packages: Some(&resolved_selection.packages),
1571 compiler_wrapper: prep.compiler_wrapper.as_ref(),
1572 })?;
1573
1574 let profile_build_root = build_dir.join(profile.name.as_str());
1579 std::fs::create_dir_all(&profile_build_root).with_context(|| {
1580 format!(
1581 "failed to create build directory {}",
1582 profile_build_root.display()
1583 )
1584 })?;
1585
1586 let ninja_file = profile_build_root.join("build.ninja");
1587 cabin_ninja::write_build_ninja(&ninja_file, &plan_graph)?;
1588
1589 let ccmd_file = profile_build_root.join("compile_commands.json");
1590 cabin_ninja::write_compile_commands(&ccmd_file, &plan_graph)?;
1591
1592 reporter.verbose(format_args!("cabin: profile = {}", profile.name.as_str()));
1593 reporter.verbose(format_args!("cabin: build dir = {}", build_dir.display()));
1594 reporter.verbose(format_args!(
1595 "cabin: c++ compiler = {}",
1596 toolchain.cxx.path.display()
1597 ));
1598 if let Some(cc) = &toolchain.cc {
1599 reporter.very_verbose(format_args!("cabin: c compiler = {}", cc.path.display()));
1600 }
1601 reporter.very_verbose(format_args!(
1602 "cabin: archiver = {}",
1603 toolchain.ar.path.display()
1604 ));
1605 reporter.verbose(format_args!("cabin: wrote {}", ninja_file.display()));
1609 reporter.verbose(format_args!("cabin: wrote {}", ccmd_file.display()));
1610 let jobs = crate::config_glue::resolve_build_jobs(args.jobs, &effective_config)?;
1611 reporter.verbose(format_args!(
1612 "cabin: invoking {} {}-C {}",
1613 ninja.display(),
1614 crate::ninja_glue::ninja_jobs_echo(jobs),
1615 profile_build_root.display()
1616 ));
1617
1618 let mut ninja_cmd = std::process::Command::new(&ninja);
1619 if let Some(jobs) = jobs {
1620 ninja_cmd.arg(crate::ninja_glue::ninja_jobs_arg(jobs));
1621 }
1622 let build_started = std::time::Instant::now();
1623 let run = crate::ninja_glue::run_ninja(
1624 ninja_cmd.arg("-C").arg(&profile_build_root),
1625 reporter,
1626 &graph,
1627 )
1628 .with_context(|| format!("failed to invoke ninja at {}", ninja.display()))?;
1629
1630 if !run.status.success() {
1631 crate::ninja_glue::emit_link_diagnostic_if_applicable(
1632 &run,
1633 &graph,
1634 &feature_resolution,
1635 &dev_for,
1636 reporter,
1637 );
1638 bail!("ninja exited with {}", run.status);
1639 }
1640
1641 let elapsed = build_started.elapsed();
1645 reporter.status(
1646 "Finished",
1647 format_args!(
1648 "`{}` profile [{}] target(s) in {:.2}s",
1649 profile.name.as_str(),
1650 profile_descriptor(&profile),
1651 elapsed.as_secs_f64(),
1652 ),
1653 );
1654
1655 Ok(())
1656}
1657
1658pub(crate) fn profile_descriptor(profile: &cabin_core::ResolvedProfile) -> String {
1667 let opt = if matches!(profile.opt_level, cabin_core::OptLevel::O0) {
1668 "unoptimized"
1669 } else {
1670 "optimized"
1671 };
1672 if profile.debug {
1673 format!("{opt} + debuginfo")
1674 } else {
1675 opt.to_owned()
1676 }
1677}
1678
1679fn clean(args: &CleanArgs, reporter: Reporter) -> Result<()> {
1680 use cabin_build::clean::{CleanRequest, CleanScope, execute_clean, plan_clean};
1681
1682 let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
1686 let graph = cabin_workspace::load_workspace_skip_ports(&manifest_path)?;
1690 let effective_config = crate::config_glue::load_effective_config(&graph)?;
1691
1692 let (build_dir_input, _build_dir_source) = crate::config_glue::resolve_build_dir_with_env(
1693 args.build_dir.as_deref(),
1694 &effective_config,
1695 );
1696 let build_dir = absolutise(&build_dir_input)
1697 .with_context(|| format!("failed to resolve build dir {}", build_dir_input.display()))?;
1698
1699 let workspace_root = graph.root_dir.clone();
1700 let package_roots: Vec<PathBuf> = graph
1701 .packages
1702 .iter()
1703 .map(|pkg| pkg.manifest_dir.clone())
1704 .collect();
1705 let protected_source_paths = clean_protected_source_paths(&graph);
1706
1707 let workspace_selection = build_workspace_selection(&args.workspace_selection);
1708 let resolved_selection =
1709 cabin_workspace::resolve_package_selection(&graph, &workspace_selection)?;
1710 let selected_explicitly = !args.workspace_selection.package.is_empty()
1711 || !args.workspace_selection.exclude.is_empty();
1712
1713 let profile_selection =
1714 profile_selection_from_flags(args.profile.as_deref(), args.release, &effective_config)?;
1715 let manifest_profiles = workspace_profile_definitions(&graph);
1716 let resolved_profile = cabin_core::resolve_profile(&profile_selection, &manifest_profiles)
1717 .map_err(|err| anyhow::anyhow!(err.to_string()))?;
1718 let profile_was_chosen = args.profile.is_some() || args.release;
1719
1720 let scope = if selected_explicitly {
1721 let packages: Vec<cabin_core::PackageName> = resolved_selection
1722 .packages
1723 .iter()
1724 .map(|&idx| graph.packages[idx].package.name.clone())
1725 .collect();
1726 let profiles = if profile_was_chosen {
1727 vec![resolved_profile.name]
1728 } else {
1729 known_profile_names(&manifest_profiles)
1730 };
1731 CleanScope::Packages { profiles, packages }
1732 } else if profile_was_chosen {
1733 CleanScope::Profile(resolved_profile.name)
1734 } else {
1735 CleanScope::Whole
1736 };
1737
1738 let plan = plan_clean(&CleanRequest {
1739 build_dir: &build_dir,
1740 workspace_root: &workspace_root,
1741 package_roots: &package_roots,
1742 protected_source_paths: &protected_source_paths,
1743 scope,
1744 })
1745 .map_err(|err| anyhow::anyhow!(err.to_string()))?;
1746
1747 if plan.removals.is_empty() {
1748 if args.dry_run {
1749 reporter.status(
1750 "Removed",
1751 format_args!("nothing under {} (dry-run)", build_dir.display()),
1752 );
1753 } else {
1754 reporter.status(
1755 "Removed",
1756 format_args!(
1757 "nothing under {} (build directory does not exist)",
1758 build_dir.display()
1759 ),
1760 );
1761 }
1762 return Ok(());
1763 }
1764
1765 if args.dry_run {
1766 reporter.status(
1767 "Removed",
1768 format_args!(
1769 "{} path{} under {} (dry-run; re-run without --dry-run to apply)",
1770 plan.removals.len(),
1771 crate::plural(plan.removals.len()),
1772 build_dir.display(),
1773 ),
1774 );
1775 print_plan_paths(&plan, reporter);
1776 return Ok(());
1777 }
1778
1779 let report = execute_clean(&plan).map_err(|err| anyhow::anyhow!(err.to_string()))?;
1780 reporter.status(
1781 "Removed",
1782 format_args!(
1783 "{} path{} under {}",
1784 report.removed.len(),
1785 crate::plural(report.removed.len()),
1786 build_dir.display()
1787 ),
1788 );
1789 Ok(())
1790}
1791
1792fn clean_protected_source_paths(graph: &cabin_workspace::PackageGraph) -> Vec<PathBuf> {
1793 let mut paths = Vec::new();
1794 for pkg in &graph.packages {
1795 for target in &pkg.package.targets {
1796 paths.extend(
1797 target
1798 .sources
1799 .iter()
1800 .map(|source| pkg.manifest_dir.join(source)),
1801 );
1802 paths.extend(
1803 target
1804 .include_dirs
1805 .iter()
1806 .map(|include_dir| pkg.manifest_dir.join(include_dir)),
1807 );
1808 }
1809 }
1810 paths.sort();
1811 paths.dedup();
1812 paths
1813}
1814
1815fn print_plan_paths(plan: &cabin_build::clean::CleanPlan, reporter: Reporter) {
1816 for path in &plan.removals {
1822 reporter.note(format_args!(" {}", path.display()));
1823 }
1824}
1825
1826fn known_profile_names(
1832 manifest_profiles: &BTreeMap<cabin_core::ProfileName, cabin_core::ProfileDefinition>,
1833) -> Vec<cabin_core::ProfileName> {
1834 let mut out: BTreeSet<cabin_core::ProfileName> = BTreeSet::new();
1835 for builtin in cabin_core::BuiltinProfile::all() {
1836 out.insert(cabin_core::ProfileName::builtin(builtin));
1837 }
1838 for name in manifest_profiles.keys() {
1839 out.insert(name.clone());
1840 }
1841 out.into_iter().collect()
1842}
1843
1844fn resolve(args: &ResolveArgs, reporter: Reporter) -> Result<()> {
1845 let mode = lock_mode_for_flags(args.locked, args.frozen);
1846 let allow_write = !(args.locked || args.frozen);
1850 if args.frozen && args.index_url.is_some() {
1851 bail!(
1852 "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"
1853 );
1854 }
1855 let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
1856 let workspace_selection = build_workspace_selection(&args.workspace_selection);
1857 let selection_request =
1858 build_selection_request(&args.features, args.all_features, args.no_default_features);
1859 run_resolution(
1860 &ResolutionRequest {
1861 manifest_path: &manifest_path,
1862 index_path: args.index_path.as_deref(),
1863 index_url: args.index_url.as_deref(),
1864 format: args.format,
1865 mode,
1866 allow_write,
1867 frozen: args.frozen,
1868 update_package: None,
1869 selection: workspace_selection,
1870 selection_request,
1871 no_patches: args.no_patches,
1872 offline: args.offline,
1873 },
1874 reporter,
1875 )
1876}
1877
1878fn update(args: &UpdateArgs, reporter: Reporter) -> Result<()> {
1879 let mode = match &args.package {
1880 Some(name) => LockMode::UpdatePackage(name.clone()),
1881 None => LockMode::UpdateAll,
1882 };
1883 let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
1884 let workspace_selection = build_update_workspace_selection(&args.workspace_selection);
1888 run_resolution(
1889 &ResolutionRequest {
1890 manifest_path: &manifest_path,
1891 index_path: args.index_path.as_deref(),
1892 index_url: args.index_url.as_deref(),
1893 format: args.format,
1894 mode,
1895 allow_write: true,
1896 frozen: false,
1897 update_package: args.package.as_deref(),
1898 selection: workspace_selection,
1899 selection_request: cabin_core::SelectionRequest::default(),
1900 no_patches: args.no_patches,
1901 offline: args.offline,
1902 },
1903 reporter,
1904 )
1905}
1906
1907fn build_update_workspace_selection(
1912 args: &WorkspaceSelectionArgsForUpdate,
1913) -> cabin_workspace::PackageSelection {
1914 use cabin_workspace::SelectionMode;
1915 let mode = if args.workspace {
1916 SelectionMode::WholeWorkspace
1917 } else if args.default_members {
1918 SelectionMode::DefaultMembers
1919 } else {
1920 SelectionMode::CurrentPackage
1921 };
1922 cabin_workspace::PackageSelection {
1923 mode,
1924 exclude: args.exclude.clone(),
1925 }
1926}
1927
1928fn fetch(args: &FetchArgs, reporter: Reporter) -> Result<()> {
1929 let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
1930 let offline_pre = crate::config_glue::effective_offline(args.offline)?;
1931 let fetch_selection = build_workspace_selection(&args.workspace_selection);
1932 let (_port_sources, initial_graph) = crate::port_glue::prepare_ports_and_load_initial_graph(
1933 &manifest_path,
1934 args.cache_dir.as_deref(),
1935 offline_pre,
1936 args.frozen,
1937 false,
1938 &fetch_selection,
1939 args.no_patches,
1940 )?;
1941 let effective_config = crate::config_glue::load_effective_config(&initial_graph)?;
1942 let active_patches =
1943 crate::patch_glue::load_active_patches(&initial_graph, &effective_config, args.no_patches)?;
1944 let patched_names = active_patches.owned_patched_names();
1945 let workspace_selection = build_workspace_selection(&args.workspace_selection);
1949 let resolved_selection =
1950 cabin_workspace::resolve_package_selection(&initial_graph, &workspace_selection)?;
1951 let initial_features = compute_feature_resolution(
1959 &initial_graph,
1960 &resolved_selection,
1961 &cabin_core::SelectionRequest::default(),
1962 )?;
1963
1964 let dev_for: BTreeSet<String> = BTreeSet::new();
1971 let patched_root_deps_preview =
1972 collect_patched_versioned_deps(&active_patches, &patched_names)?;
1973 if patched_root_deps_preview.is_empty()
1974 && !closure_has_versioned_deps_excluding_patches(
1975 &initial_graph,
1976 &resolved_selection,
1977 &initial_features,
1978 &patched_names,
1979 &dev_for,
1980 )
1981 {
1982 emit_fetch_output(
1983 &[],
1984 args.format,
1985 &cache_dir_for(&manifest_path, args.cache_dir.as_deref()).unwrap_or_default(),
1986 &manifest_path,
1987 )?;
1988 return Ok(());
1989 }
1990
1991 let resolved_index_source = crate::config_glue::resolve_index_source(
1992 args.index_path.as_deref(),
1993 args.index_url.as_deref(),
1994 &effective_config,
1995 )?;
1996 let fetch_offline = crate::config_glue::effective_offline(args.offline)?;
1997 crate::config_glue::enforce_offline_index_source(
1998 fetch_offline,
1999 resolved_index_source.as_ref(),
2000 )?;
2001 let resolved_cache_dir =
2002 crate::config_glue::resolve_cache_dir(args.cache_dir.as_deref(), &effective_config);
2003 let Some(index_source) = resolved_index_source.as_ref() else {
2004 bail!(
2005 "versioned dependencies require --index-path, --index-url, or a `[registry]` config setting"
2006 );
2007 };
2008 let inputs = crate::config_glue::resolve_pipeline_inputs(
2009 index_source,
2010 &effective_config,
2011 &manifest_path,
2012 args.cache_dir.as_deref(),
2013 resolved_cache_dir.as_ref(),
2014 fetch_offline,
2015 args.locked,
2016 args.frozen,
2017 args.no_patches,
2018 false,
2019 )?;
2020
2021 let fetch_request = cabin_core::SelectionRequest::default();
2022 let pipeline = run_artifact_pipeline(&ArtifactPipelineRequest {
2023 manifest_path: &manifest_path,
2024 initial_graph: &initial_graph,
2025 index_path: inputs.index_path.as_deref(),
2026 index_url: inputs.index_url.as_deref(),
2027 mode: inputs.mode,
2028 allow_write: inputs.allow_write,
2029 frozen: args.frozen,
2030 cache_dir: &inputs.cache_dir,
2031 reporter,
2032 selection: workspace_selection,
2033 selection_request: &fetch_request,
2034 patched_names: &patched_names,
2035 active_patches: &active_patches,
2036 source_replacements: &effective_config.source_replacements,
2037 no_patches: args.no_patches,
2038 dev_for: &dev_for,
2039 })?;
2040
2041 emit_fetch_output(
2042 &pipeline.fetched,
2043 args.format,
2044 &inputs.cache_dir,
2045 &manifest_path,
2046 )?;
2047 Ok(())
2048}
2049
2050fn package(args: &PackageArgs, _reporter: Reporter) -> Result<()> {
2051 let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
2052 let target =
2053 select_single_package_manifest(&manifest_path, &args.workspace_selection, "package")?;
2054 let output_dir = absolutise(&args.output_dir)
2055 .with_context(|| format!("failed to resolve {}", args.output_dir.display()))?;
2056 let artifact = cabin_package::package_with_project(
2057 cabin_package::PackageRequest {
2058 manifest_path: &target.manifest_path,
2059 output_dir: &output_dir,
2060 },
2061 target.resolved_project,
2062 )?;
2063 emit_package_output(&artifact, args.format)?;
2064 Ok(())
2065}
2066
2067fn publish(args: &PublishArgs, _reporter: Reporter) -> Result<()> {
2068 if args.output_dir.is_some() && args.registry_dir.is_some() {
2072 bail!("--output-dir is not compatible with --registry-dir; pick one");
2073 }
2074
2075 let manifest_path = resolve_invocation_manifest(args.manifest_path.as_deref())?;
2076 let target =
2077 select_single_package_manifest(&manifest_path, &args.workspace_selection, "publish")?;
2078
2079 match (args.registry_dir.as_deref(), args.dry_run) {
2080 (Some(registry_dir), true) => {
2081 let registry_dir = absolutise(registry_dir)
2082 .with_context(|| format!("failed to resolve {}", registry_dir.display()))?;
2083 let report = cabin_publish::dry_run_against_file_registry(
2084 cabin_publish::RegistryPublishWorkflow {
2085 manifest_path: &target.manifest_path,
2086 registry_dir: ®istry_dir,
2087 resolved_project: target.resolved_project.clone(),
2088 },
2089 )?;
2090 emit_registry_publish_output(&report, args.format)?;
2091 }
2092 (Some(registry_dir), false) => {
2093 let registry_dir = absolutise(registry_dir)
2094 .with_context(|| format!("failed to resolve {}", registry_dir.display()))?;
2095 let report =
2096 cabin_publish::publish_to_file_registry(cabin_publish::RegistryPublishWorkflow {
2097 manifest_path: &target.manifest_path,
2098 registry_dir: ®istry_dir,
2099 resolved_project: target.resolved_project.clone(),
2100 })?;
2101 emit_registry_publish_output(&report, args.format)?;
2102 }
2103 (None, true) => {
2104 let output_dir = args
2105 .output_dir
2106 .clone()
2107 .unwrap_or_else(|| PathBuf::from("dist"));
2108 let output_dir = absolutise(&output_dir)
2109 .with_context(|| format!("failed to resolve {}", output_dir.display()))?;
2110 let report = cabin_publish::dry_run(cabin_publish::DryRunRequest {
2111 manifest_path: &target.manifest_path,
2112 output_dir: &output_dir,
2113 resolved_project: target.resolved_project.clone(),
2114 })?;
2115 emit_dry_run_output(&report, args.format)?;
2116 }
2117 (None, false) => {
2118 return Err(cabin_publish::PublishError::DryRunRequired.into());
2119 }
2120 }
2121 Ok(())
2122}
2123
2124fn emit_package_output(
2125 artifact: &cabin_package::PackagedArtifact,
2126 format: ResolveFormat,
2127) -> Result<()> {
2128 match format {
2129 ResolveFormat::Human => {
2130 print_package_human(artifact);
2131 Ok(())
2132 }
2133 ResolveFormat::Json => print_package_json(artifact),
2134 }
2135}
2136
2137fn print_package_human(artifact: &cabin_package::PackagedArtifact) {
2138 println!("Packaged {} {}", artifact.name.as_str(), artifact.version);
2139 println!(" archive: {}", artifact.archive_path.display());
2140 println!(" metadata: {}", artifact.metadata_path.display());
2141 println!(" checksum: {}", artifact.checksum);
2142}
2143
2144fn print_package_json(artifact: &cabin_package::PackagedArtifact) -> Result<()> {
2145 let value = serde_json::json!({
2146 "name": artifact.name.as_str(),
2147 "version": artifact.version.to_string(),
2148 "archive_path": artifact.archive_path,
2149 "metadata_path": artifact.metadata_path,
2150 "checksum": artifact.checksum,
2151 });
2152 crate::print_pretty_json(&value, "failed to serialize package output as JSON")
2153}
2154
2155fn emit_dry_run_output(report: &cabin_publish::DryRunReport, format: ResolveFormat) -> Result<()> {
2156 match format {
2157 ResolveFormat::Human => {
2158 print_dry_run_human(report);
2159 Ok(())
2160 }
2161 ResolveFormat::Json => print_dry_run_json(report),
2162 }
2163}
2164
2165fn print_dry_run_human(report: &cabin_publish::DryRunReport) {
2166 println!(
2167 "Publish dry-run for {} {}",
2168 report.name.as_str(),
2169 report.version
2170 );
2171 println!();
2172 println!("Generated:");
2173 println!(" archive: {}", report.archive_path.display());
2174 println!(" metadata: {}", report.metadata_path.display());
2175 println!(" checksum: {}", report.checksum);
2176 println!();
2177 println!("This was a dry run. No registry was modified.");
2178}
2179
2180fn print_dry_run_json(report: &cabin_publish::DryRunReport) -> Result<()> {
2181 let value = serde_json::json!({
2182 "dry_run": true,
2183 "name": report.name.as_str(),
2184 "version": report.version.to_string(),
2185 "archive_path": report.archive_path,
2186 "metadata_path": report.metadata_path,
2187 "checksum": report.checksum,
2188 "registry_modified": report.registry_modified,
2189 });
2190 crate::print_pretty_json(&value, "failed to serialize publish dry-run output as JSON")
2191}
2192
2193fn emit_registry_publish_output(
2194 report: &cabin_publish::RegistryPublishReport,
2195 format: ResolveFormat,
2196) -> Result<()> {
2197 match format {
2198 ResolveFormat::Human => {
2199 print_registry_publish_human(report);
2200 Ok(())
2201 }
2202 ResolveFormat::Json => print_registry_publish_json(report),
2203 }
2204}
2205
2206fn print_registry_publish_human(report: &cabin_publish::RegistryPublishReport) {
2207 if report.dry_run {
2208 println!(
2209 "Publish dry-run for {} {} against file registry",
2210 report.name.as_str(),
2211 report.version
2212 );
2213 } else {
2214 println!(
2215 "Published {} {} to file registry",
2216 report.name.as_str(),
2217 report.version
2218 );
2219 }
2220 println!(" registry: {}", report.registry_dir.display());
2221 println!(" package index: {}", report.package_index_path.display());
2222 println!(" artifact: {}", report.artifact_path.display());
2223 println!(" checksum: {}", report.checksum);
2224 if report.dry_run {
2225 println!();
2226 if report.registry_initialized {
2227 println!("Registry would be initialized at this path.");
2228 }
2229 println!("This was a dry run. No registry was modified.");
2230 } else if report.registry_initialized {
2231 println!();
2232 println!("Registry was initialized at this path.");
2233 }
2234}
2235
2236fn print_registry_publish_json(report: &cabin_publish::RegistryPublishReport) -> Result<()> {
2237 let value = serde_json::json!({
2238 "published": !report.dry_run,
2239 "dry_run": report.dry_run,
2240 "name": report.name.as_str(),
2241 "version": report.version.to_string(),
2242 "registry_dir": report.registry_dir,
2243 "package_index_path": report.package_index_path,
2244 "artifact_path": report.artifact_path,
2245 "checksum": report.checksum,
2246 "source_path": report.source_path,
2247 "registry_modified": report.registry_modified,
2248 "registry_initialized": report.registry_initialized,
2249 });
2250 crate::print_pretty_json(&value, "failed to serialize publish output as JSON")
2251}
2252
2253fn profile_selection_for_build(
2261 args: &BuildArgs,
2262 config: &cabin_config::EffectiveConfig,
2263) -> Result<cabin_core::ProfileSelection> {
2264 profile_selection_from_flags(args.profile.as_deref(), args.release, config)
2265}
2266
2267pub(crate) fn profile_selection_from_flags(
2272 profile: Option<&str>,
2273 release: bool,
2274 config: &cabin_config::EffectiveConfig,
2275) -> Result<cabin_core::ProfileSelection> {
2276 if let Some(name) = profile {
2277 let pname = cabin_core::ProfileName::new(name.to_owned())
2278 .map_err(|err| anyhow::anyhow!(err.to_string()))?;
2279 return Ok(cabin_core::ProfileSelection::from_name(pname));
2280 }
2281 if release {
2282 return Ok(cabin_core::ProfileSelection::release_alias());
2283 }
2284 if let Some((selection, _source)) = crate::config_glue::config_profile_selection(config)? {
2285 return Ok(selection);
2286 }
2287 Ok(cabin_core::ProfileSelection::default_dev())
2288}
2289
2290pub(crate) fn profile_selection_for_metadata(
2295 name: Option<&str>,
2296 config: &cabin_config::EffectiveConfig,
2297) -> Result<cabin_core::ProfileSelection> {
2298 if let Some(n) = name {
2299 let pname = cabin_core::ProfileName::new(n.to_owned())
2300 .map_err(|err| anyhow::anyhow!(err.to_string()))?;
2301 return Ok(cabin_core::ProfileSelection::from_name(pname));
2302 }
2303 if let Some((selection, _source)) = crate::config_glue::config_profile_selection(config)? {
2304 return Ok(selection);
2305 }
2306 Ok(cabin_core::ProfileSelection::default_dev())
2307}
2308
2309pub(crate) fn workspace_profile_definitions(
2314 graph: &PackageGraph,
2315) -> BTreeMap<cabin_core::ProfileName, cabin_core::ProfileDefinition> {
2316 graph.root_settings.profiles.clone()
2317}
2318
2319pub(crate) fn workspace_toolchain_settings(graph: &PackageGraph) -> cabin_core::ToolchainSettings {
2324 graph.root_settings.toolchain.clone()
2325}
2326
2327pub(crate) fn toolchain_selection_from_args(
2330 args: &ToolchainSelectionArgs,
2331) -> Result<cabin_core::ToolchainSelection> {
2332 let mut sel = cabin_core::ToolchainSelection::default();
2333 if let Some(raw) = &args.cc {
2334 sel = sel.with_cli(cabin_core::ToolKind::CCompiler, parse_cli_tool(raw)?);
2335 }
2336 if let Some(raw) = &args.cxx {
2337 sel = sel.with_cli(cabin_core::ToolKind::CxxCompiler, parse_cli_tool(raw)?);
2338 }
2339 if let Some(raw) = &args.ar {
2340 sel = sel.with_cli(cabin_core::ToolKind::Archiver, parse_cli_tool(raw)?);
2341 }
2342 Ok(sel)
2343}
2344
2345fn parse_cli_tool(raw: &str) -> Result<cabin_core::ToolSpec> {
2346 let trimmed = raw.trim();
2347 if trimmed.is_empty() {
2348 bail!("tool argument must be a non-empty path or command name");
2349 }
2350 Ok(cabin_core::ToolSpec::parse(trimmed.to_owned()))
2351}
2352
2353pub(crate) fn resolve_toolchain_layered(
2358 graph: &PackageGraph,
2359 selection: &cabin_core::ToolchainSelection,
2360 effective_config: &cabin_config::EffectiveConfig,
2361 host_platform: &cabin_core::TargetPlatform,
2362) -> Result<cabin_core::ResolvedToolchain> {
2363 let manifest_toolchain_settings = workspace_toolchain_settings(graph);
2364 let config_toolchain_layer = crate::config_glue::toolchain_layer(effective_config);
2365 let mut toolchain_inputs = cabin_toolchain::ResolveInputs::from_process(
2366 selection,
2367 &manifest_toolchain_settings,
2368 host_platform,
2369 );
2370 if let Some(layer) = config_toolchain_layer.as_ref() {
2371 toolchain_inputs = toolchain_inputs.with_config(layer);
2372 }
2373 Ok(cabin_toolchain::resolve_toolchain(&toolchain_inputs)?)
2374}
2375
2376pub(crate) fn compiler_wrapper_override_from_args(
2382 args: &ToolchainSelectionArgs,
2383) -> Result<Option<cabin_core::CompilerWrapperRequest>> {
2384 if args.no_compiler_wrapper {
2385 return Ok(Some(cabin_core::CompilerWrapperRequest::Disabled));
2386 }
2387 let Some(raw) = args.compiler_wrapper.as_deref() else {
2388 return Ok(None);
2389 };
2390 let parsed = cabin_core::CompilerWrapperRequest::parse(raw)
2391 .with_context(|| format!("invalid --compiler-wrapper value `{raw}`"))?;
2392 Ok(Some(parsed))
2393}
2394
2395pub(crate) fn resolve_compiler_wrapper_layered(
2403 cli_override: Option<cabin_core::CompilerWrapperRequest>,
2404 manifest_settings: &cabin_core::CompilerWrapperManifestSettings,
2405 effective_config: &cabin_config::EffectiveConfig,
2406 host_platform: &cabin_core::TargetPlatform,
2407) -> Result<Option<cabin_core::ResolvedCompilerWrapper>> {
2408 let mut wrapper_inputs = cabin_toolchain::WrapperInputs::from_process(
2409 cli_override,
2410 manifest_settings,
2411 host_platform,
2412 );
2413 if let Some(layer) = crate::config_glue::wrapper_layer(effective_config) {
2414 wrapper_inputs = wrapper_inputs.with_config(layer);
2415 }
2416 cabin_toolchain::resolve_compiler_wrapper(
2417 &wrapper_inputs,
2418 Some(&cabin_toolchain::ProcessRunner),
2419 )
2420 .map_err(|err| anyhow::anyhow!(err.to_string()))
2421}
2422
2423pub(crate) fn workspace_compiler_wrapper_settings(
2428 graph: &PackageGraph,
2429) -> cabin_core::CompilerWrapperManifestSettings {
2430 graph.root_settings.compiler_wrapper.clone()
2431}
2432
2433pub(crate) fn resolve_per_package_build_flags(
2438 graph: &PackageGraph,
2439 profile_build: Option<&cabin_core::ProfileFlags>,
2440 host_platform: &cabin_core::TargetPlatform,
2441) -> HashMap<usize, cabin_core::ResolvedProfileFlags> {
2442 let mut out = HashMap::with_capacity(graph.packages.len());
2443 for (idx, pkg) in graph.packages.iter().enumerate() {
2444 let package_trusted = matches!(pkg.kind, cabin_workspace::PackageKind::Local);
2451 let resolved = cabin_core::resolve_build_flags(
2452 &pkg.package.build,
2453 profile_build,
2454 host_platform,
2455 package_trusted,
2456 );
2457 out.insert(idx, resolved);
2458 }
2459 out
2460}
2461
2462pub(crate) fn augment_build_flags(
2471 graph: &PackageGraph,
2472 host_platform: &cabin_core::TargetPlatform,
2473 dev_for: &BTreeSet<String>,
2474 build_flags: HashMap<usize, cabin_core::ResolvedProfileFlags>,
2475 reporter: Reporter,
2476) -> Result<HashMap<usize, cabin_core::ResolvedProfileFlags>> {
2477 let (build_flags, _system_dep_reports) =
2478 crate::system_deps_glue::augment_build_flags_with_system_deps(
2479 graph,
2480 host_platform,
2481 dev_for,
2482 build_flags,
2483 reporter,
2484 )?;
2485 let (build_flags, _env_build_flags) = crate::env_flags_glue::augment_build_flags_with_env(
2486 graph,
2487 build_flags,
2488 |k| std::env::var_os(k),
2489 reporter,
2490 )?;
2491 Ok(build_flags)
2492}
2493
2494pub(crate) fn build_selection_request(
2498 feature_args: &[String],
2499 all_features: bool,
2500 no_default_features: bool,
2501) -> cabin_core::SelectionRequest {
2502 let mut features: BTreeSet<String> = BTreeSet::new();
2503 for raw in feature_args {
2504 for token in raw.split(',') {
2505 let trimmed = token.trim();
2506 if trimmed.is_empty() {
2507 continue;
2508 }
2509 features.insert(trimmed.to_owned());
2510 }
2511 }
2512 cabin_core::SelectionRequest {
2513 features,
2514 all_features,
2515 no_default_features,
2516 }
2517}
2518
2519pub(crate) fn resolve_build_configurations(
2525 graph: &PackageGraph,
2526 request: &cabin_core::SelectionRequest,
2527 selected: &[usize],
2528 profile: &cabin_core::ResolvedProfile,
2529 toolchain: &cabin_core::ToolchainSummary,
2530 build_flags: &HashMap<usize, cabin_core::ResolvedProfileFlags>,
2531) -> Result<HashMap<usize, cabin_core::BuildConfiguration>> {
2532 use HashMap;
2533 let selected_set: HashSet<usize> = selected.iter().copied().collect();
2534 let mut out: HashMap<usize, cabin_core::BuildConfiguration> = HashMap::new();
2535 for (idx, pkg) in graph.packages.iter().enumerate() {
2536 let pkg_request = if selected_set.contains(&idx) {
2542 request.clone()
2543 } else {
2544 cabin_core::SelectionRequest::default()
2545 };
2546 let pkg_flags = build_flags.get(&idx).cloned().unwrap_or_default();
2547 let cfg = cabin_core::BuildConfiguration::resolve(cabin_core::BuildConfigurationInput {
2548 package: pkg.package.name.as_str(),
2549 features: &pkg.package.features,
2550 request: &pkg_request,
2551 profile: profile.clone(),
2552 toolchain: toolchain.clone(),
2553 build_flags: pkg_flags,
2554 })
2555 .with_context(|| {
2556 format!(
2557 "invalid configuration selection for package `{}`",
2558 pkg.package.name.as_str()
2559 )
2560 })?;
2561 out.insert(idx, cfg);
2562 }
2563 Ok(out)
2564}
2565
2566pub(crate) fn resolve_invocation_manifest(args_path: Option<&Path>) -> Result<PathBuf> {
2574 let cwd = std::env::current_dir().context("failed to determine current directory")?;
2575 match args_path {
2576 Some(path) => {
2577 if path.is_absolute() {
2578 Ok(path.to_path_buf())
2579 } else {
2580 Ok(cwd.join(path))
2581 }
2582 }
2583 None => {
2584 if let Some(found) = cabin_workspace::discover_workspace_root(&cwd)? {
2585 Ok(found.manifest_path)
2586 } else {
2587 Ok(cwd.join(MANIFEST_FILENAME))
2588 }
2589 }
2590 }
2591}
2592
2593pub(crate) fn build_workspace_selection(
2597 args: &WorkspaceSelectionArgs,
2598) -> cabin_workspace::PackageSelection {
2599 use cabin_workspace::SelectionMode;
2600 let mode = if args.workspace {
2601 SelectionMode::WholeWorkspace
2602 } else if !args.package.is_empty() {
2603 SelectionMode::ExplicitPackages(args.package.clone())
2604 } else if args.default_members {
2605 SelectionMode::DefaultMembers
2606 } else {
2607 SelectionMode::CurrentPackage
2608 };
2609 cabin_workspace::PackageSelection {
2610 mode,
2611 exclude: args.exclude.clone(),
2612 }
2613}
2614
2615fn closure_and_optional_filter<'a>(
2621 graph: &PackageGraph,
2622 selection: &cabin_workspace::ResolvedSelection,
2623 features: &'a cabin_feature::FeatureResolution,
2624) -> (BTreeSet<usize>, impl Fn(usize, &str) -> bool + 'a) {
2625 (selection.closure(graph), move |idx, name| {
2626 features.is_optional_dep_enabled(idx, name)
2627 })
2628}
2629
2630pub(crate) fn collect_closure_versioned_deps_excluding_patches(
2634 graph: &PackageGraph,
2635 selection: &cabin_workspace::ResolvedSelection,
2636 features: &cabin_feature::FeatureResolution,
2637 patched_names: &BTreeSet<String>,
2638 dev_for: &BTreeSet<String>,
2639) -> Result<BTreeMap<PackageName, semver::VersionReq>> {
2640 let (closure, is_optional_dep_enabled) =
2641 closure_and_optional_filter(graph, selection, features);
2642 cabin_workspace::collect_closure_versioned_deps_excluding_with_dev(
2643 graph,
2644 &closure,
2645 is_optional_dep_enabled,
2646 patched_names,
2647 dev_for,
2648 )
2649 .map_err(Into::into)
2650}
2651
2652fn merge_versioned_deps(
2657 into: &mut BTreeMap<PackageName, semver::VersionReq>,
2658 extra: BTreeMap<PackageName, semver::VersionReq>,
2659) -> Result<()> {
2660 for (name, req) in extra {
2661 match into.entry(name.clone()) {
2662 std::collections::btree_map::Entry::Vacant(slot) => {
2663 slot.insert(req);
2664 }
2665 std::collections::btree_map::Entry::Occupied(mut slot) => {
2666 let parsed = cabin_workspace::combine_version_reqs(&[
2667 slot.get().to_string(),
2668 req.to_string(),
2669 ])
2670 .map_err(|(joined, err)| {
2671 anyhow::anyhow!(
2672 "conflicting dependency requirements for {}: {}: {}",
2673 name.as_str(),
2674 joined,
2675 err
2676 )
2677 })?;
2678 slot.insert(parsed);
2679 }
2680 }
2681 }
2682 Ok(())
2683}
2684
2685pub(crate) fn closure_has_versioned_deps_excluding_patches(
2690 graph: &PackageGraph,
2691 selection: &cabin_workspace::ResolvedSelection,
2692 features: &cabin_feature::FeatureResolution,
2693 patched_names: &BTreeSet<String>,
2694 dev_for: &BTreeSet<String>,
2695) -> bool {
2696 let (closure, is_optional_dep_enabled) =
2697 closure_and_optional_filter(graph, selection, features);
2698 cabin_workspace::closure_has_versioned_deps_excluding_with_dev(
2699 graph,
2700 &closure,
2701 is_optional_dep_enabled,
2702 patched_names,
2703 dev_for,
2704 )
2705}
2706
2707pub(crate) fn compute_feature_resolution(
2715 graph: &PackageGraph,
2716 selection: &cabin_workspace::ResolvedSelection,
2717 request: &cabin_core::SelectionRequest,
2718) -> Result<cabin_feature::FeatureResolution> {
2719 let root_request: cabin_feature::RootFeatureRequest = request.into();
2720 let platform = cabin_core::TargetPlatform::current();
2721 cabin_feature::resolve_features(graph, &selection.packages, &root_request, &platform)
2722 .map_err(|e| anyhow::anyhow!(e.to_string()))
2723}
2724
2725fn selected_resolution_packages(
2731 graph: &PackageGraph,
2732 selection: &cabin_workspace::PackageSelection,
2733) -> Result<cabin_workspace::ResolvedSelection> {
2734 cabin_workspace::resolve_package_selection(graph, selection).map_err(std::convert::Into::into)
2735}
2736
2737struct SinglePackageSelection {
2749 manifest_path: PathBuf,
2750 resolved_project: Option<cabin_core::Package>,
2756}
2757
2758fn select_single_package_manifest(
2759 invocation: &Path,
2760 selection: &WorkspaceSelectionArgs,
2761 command: &'static str,
2762) -> Result<SinglePackageSelection> {
2763 let parsed = cabin_manifest::load_manifest(invocation)
2764 .with_context(|| format!("failed to load manifest at {}", invocation.display()))?;
2765 if parsed.workspace.is_none() {
2766 if selection.workspace
2770 || selection.default_members
2771 || !selection.package.is_empty()
2772 || !selection.exclude.is_empty()
2773 {
2774 bail!(
2775 "workspace package-selection flags are not valid for `cabin {command}` against a non-workspace manifest"
2776 );
2777 }
2778 return Ok(SinglePackageSelection {
2779 manifest_path: invocation.to_path_buf(),
2780 resolved_project: None,
2781 });
2782 }
2783 if selection.package.len() != 1 || selection.workspace || selection.default_members {
2784 bail!(
2785 "`cabin {command}` requires a single `--package <name>` selection inside a workspace; use `--package <name>` to pick the package to {command}"
2786 );
2787 }
2788 if !selection.exclude.is_empty() {
2789 bail!(
2790 "`--exclude` is not valid for `cabin {command}`; pass exactly one `--package <name>`"
2791 );
2792 }
2793 let graph = cabin_workspace::load_workspace_skip_ports(invocation)?;
2798 let name = &selection.package[0];
2799 let idx = graph
2800 .index_of(name)
2801 .ok_or_else(|| anyhow::anyhow!("package `{name}` is not a member of this workspace"))?;
2802 if !graph.primary_packages.contains(&idx) {
2803 bail!("package `{name}` is not a member of this workspace");
2804 }
2805 Ok(SinglePackageSelection {
2806 manifest_path: graph.packages[idx].manifest_path.clone(),
2807 resolved_project: Some(graph.packages[idx].package.clone()),
2808 })
2809}
2810
2811pub(crate) fn lock_mode_for_flags(locked: bool, frozen: bool) -> LockMode {
2812 if locked || frozen {
2813 LockMode::Locked
2814 } else {
2815 LockMode::PreferLocked
2816 }
2817}
2818
2819pub(crate) fn cache_dir_for(manifest_path: &Path, override_dir: Option<&Path>) -> Result<PathBuf> {
2833 let xdg_cache_home = xdg::BaseDirectories::with_prefix("cabin").get_cache_home();
2834 cache_dir_for_with_env(
2835 manifest_path,
2836 override_dir,
2837 &|key| std::env::var_os(key),
2838 xdg_cache_home.as_deref(),
2839 )
2840}
2841
2842fn cache_dir_for_with_env(
2847 manifest_path: &Path,
2848 override_dir: Option<&Path>,
2849 env: &dyn Fn(&str) -> Option<std::ffi::OsString>,
2850 xdg_cache_home: Option<&Path>,
2851) -> Result<PathBuf> {
2852 if let Some(p) = override_dir {
2853 return absolutise(p)
2854 .with_context(|| format!("failed to resolve cache dir {}", p.display()));
2855 }
2856 if let Some(val) = env("CABIN_CACHE_DIR").filter(|v| !v.is_empty()) {
2857 let p = PathBuf::from(val);
2858 return absolutise(&p)
2859 .with_context(|| format!("failed to resolve cache dir {}", p.display()));
2860 }
2861 let _ = manifest_path;
2867 user_cache_default(env, xdg_cache_home).ok_or_else(|| {
2868 anyhow::anyhow!(
2869 "no cache directory: set --cache-dir, CABIN_CACHE_DIR, CABIN_CACHE_HOME, XDG_CACHE_HOME, or HOME"
2870 )
2871 })
2872}
2873
2874fn user_cache_default(
2881 env: &dyn Fn(&str) -> Option<std::ffi::OsString>,
2882 xdg_cache_home: Option<&Path>,
2883) -> Option<PathBuf> {
2884 if let Some(d) = env("CABIN_CACHE_HOME").filter(|v| !v.is_empty()) {
2885 return Some(PathBuf::from(d));
2886 }
2887 xdg_cache_home.map(Path::to_path_buf)
2888}
2889
2890pub(crate) struct ArtifactPipelineRequest<'a> {
2891 pub(crate) manifest_path: &'a Path,
2892 pub(crate) initial_graph: &'a PackageGraph,
2893 pub(crate) index_path: Option<&'a Path>,
2894 pub(crate) index_url: Option<&'a str>,
2895 pub(crate) mode: LockMode,
2896 pub(crate) allow_write: bool,
2897 pub(crate) frozen: bool,
2898 pub(crate) cache_dir: &'a Path,
2899 pub(crate) reporter: Reporter,
2900 pub(crate) selection: cabin_workspace::PackageSelection,
2904 pub(crate) selection_request: &'a cabin_core::SelectionRequest,
2907 pub(crate) patched_names: &'a BTreeSet<String>,
2911 pub(crate) active_patches: &'a cabin_workspace::ActivePatchSet,
2914 pub(crate) source_replacements: &'a cabin_core::SourceReplacementSettings,
2917 pub(crate) no_patches: bool,
2921 pub(crate) dev_for: &'a BTreeSet<String>,
2927}
2928
2929pub(crate) struct ArtifactPipeline {
2930 pub(crate) fetched: Vec<FetchedPackage>,
2931}
2932
2933impl ArtifactPipeline {
2934 pub(crate) fn registry_sources(&self) -> Vec<RegistryPackageSource> {
2940 self.fetched
2941 .iter()
2942 .map(|p| RegistryPackageSource {
2943 name: p.name.clone(),
2944 version: p.version.clone(),
2945 manifest_path: p.source_dir.join("cabin.toml"),
2946 })
2947 .collect()
2948 }
2949}
2950
2951enum IndexAccess {
2955 Local,
2956 Http(cabin_index_http::HttpClient),
2957}
2958
2959pub(crate) fn run_artifact_pipeline(
2962 request: &ArtifactPipelineRequest<'_>,
2963) -> Result<ArtifactPipeline> {
2964 let manifest_path = request.manifest_path;
2965 let graph = request.initial_graph;
2966 let resolved_selection = selected_resolution_packages(graph, &request.selection)?;
2967 let features =
2968 compute_feature_resolution(graph, &resolved_selection, request.selection_request)?;
2969 let mut root_deps = collect_closure_versioned_deps_excluding_patches(
2970 graph,
2971 &resolved_selection,
2972 &features,
2973 request.patched_names,
2974 request.dev_for,
2975 )?;
2976 let patched_root_deps =
2982 collect_patched_versioned_deps(request.active_patches, request.patched_names)?;
2983 merge_versioned_deps(&mut root_deps, patched_root_deps)?;
2984 if root_deps.is_empty() {
2989 return Ok(ArtifactPipeline {
2990 fetched: Vec::new(),
2991 });
2992 }
2993 let (root_name, root_version) = match graph.root_package {
2996 Some(idx) => (
2997 graph.packages[idx].package.name.clone(),
2998 graph.packages[idx].package.version.clone(),
2999 ),
3000 None => cabin_workspace::synthetic_root_identity(graph),
3001 };
3002
3003 let lockfile_path = lockfile_path_for(manifest_path);
3004
3005 let existing_lockfile: Option<Lockfile> = if lockfile_path.is_file() {
3006 Some(
3007 cabin_lockfile::read_lockfile(&lockfile_path)
3008 .with_context(|| format!("failed to read {}", lockfile_path.display()))?,
3009 )
3010 } else {
3011 if matches!(request.mode, LockMode::Locked) {
3012 bail!(
3013 "cannot resolve with --locked because {} does not exist",
3014 lockfile_path.display()
3015 );
3016 }
3017 None
3018 };
3019
3020 let (index, access) = load_index_for_pipeline(
3021 request.index_path,
3022 request.index_url,
3023 request.frozen,
3024 &root_deps,
3025 )?;
3026
3027 let resolver_mode = match &request.mode {
3028 LockMode::PreferLocked => ResolveMode::PreferLocked,
3029 LockMode::Locked => ResolveMode::Locked,
3030 LockMode::UpdateAll => ResolveMode::UpdateAll,
3031 LockMode::UpdatePackage(name) => ResolveMode::UpdatePackage(
3032 PackageName::new(name.clone())
3033 .map_err(|err| anyhow::anyhow!("invalid --package value {name:?}: {err}"))?,
3034 ),
3035 };
3036
3037 let mut input = ResolveInput::new(root_name, root_version, root_deps);
3038 if let Some(lock) = &existing_lockfile {
3039 for pkg in &lock.packages {
3040 input.locked.insert(
3041 pkg.name.clone(),
3042 LockedVersion {
3043 version: pkg.version.clone(),
3044 checksum: pkg.checksum.clone(),
3045 },
3046 );
3047 }
3048 }
3049 input.mode = resolver_mode;
3050
3051 let active_patch_records = crate::patch_glue::lockfile_patches(request.active_patches);
3055 let active_replacement_records = crate::patch_glue::lockfile_source_replacements(
3056 request.source_replacements,
3057 request.no_patches,
3058 );
3059 if matches!(request.mode, LockMode::Locked)
3060 && let Some(prev) = &existing_lockfile
3061 && !prev.matches_patch_state(&active_patch_records, &active_replacement_records)
3062 {
3063 bail!(
3064 "--locked cannot be used because active patch / source-replacement policy differs from {}; re-run without --locked to refresh the lockfile",
3065 lockfile_path.display()
3066 );
3067 }
3068
3069 let output = cabin_resolver::resolve(&input, &index).context("dependency resolution failed")?;
3070
3071 let mut new_lockfile = lockfile_from_resolution(&output, &index);
3072 new_lockfile.patches = active_patch_records;
3073 new_lockfile.source_replacements = active_replacement_records;
3074
3075 if request.allow_write {
3076 let needs_write = match &existing_lockfile {
3077 Some(prev) => prev != &new_lockfile,
3078 None => true,
3079 };
3080 if needs_write {
3081 cabin_lockfile::write_lockfile(&lockfile_path, &new_lockfile)
3082 .with_context(|| format!("failed to write {}", lockfile_path.display()))?;
3083 request
3084 .reporter
3085 .aux_verbose(format_args!("cabin: wrote {}", lockfile_path.display()));
3086 } else {
3087 request.reporter.aux_verbose(format_args!(
3088 "cabin: {} is up to date",
3089 lockfile_path.display()
3090 ));
3091 }
3092 }
3093
3094 let plan = build_fetch_plan(&output, &index, &access)?;
3095 let cache = ArtifactCache::new(request.cache_dir);
3096 let result = cabin_artifact::fetch(
3097 &plan,
3098 &cache,
3099 FetchOptions {
3100 frozen: request.frozen,
3101 },
3102 )?;
3103 Ok(ArtifactPipeline {
3104 fetched: result.packages,
3105 })
3106}
3107
3108fn load_index_for_pipeline(
3113 index_path: Option<&Path>,
3114 index_url: Option<&str>,
3115 frozen: bool,
3116 root_deps: &BTreeMap<PackageName, semver::VersionReq>,
3117) -> Result<(PackageIndex, IndexAccess)> {
3118 match (index_path, index_url) {
3119 (Some(_), Some(_)) => bail!("use either --index-path or --index-url, not both"),
3120 (None, None) => {
3121 bail!("versioned dependencies require --index-path or --index-url")
3122 }
3123 (Some(path), None) => {
3124 let index_path = absolutise(path)
3125 .with_context(|| format!("failed to resolve {}", path.display()))?;
3126 let index = cabin_index::load_index(&index_path)
3127 .with_context(|| format!("failed to load index at {}", index_path.display()))?;
3128 Ok((index, IndexAccess::Local))
3129 }
3130 (None, Some(url)) => {
3131 if frozen {
3132 bail!(
3133 "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"
3134 );
3135 }
3136 let client = cabin_index_http::HttpClient::new();
3137 let http_index = cabin_index_http::HttpIndex::open(url, client.clone())?;
3138 let names: Vec<PackageName> = root_deps.keys().cloned().collect();
3139 let index = http_index.load_package_index(&names)?;
3140 Ok((index, IndexAccess::Http(client)))
3141 }
3142 }
3143}
3144
3145fn build_fetch_plan(
3154 output: &ResolveOutput,
3155 index: &PackageIndex,
3156 access: &IndexAccess,
3157) -> Result<FetchPlan> {
3158 let mut entries = Vec::new();
3159 for resolved in &output.packages {
3160 if resolved.source != ResolvedSource::Index {
3161 continue;
3162 }
3163 let entry = index.package(&resolved.name).ok_or_else(|| {
3164 anyhow::anyhow!(
3165 "resolver chose `{} {}`, but it is not in the index",
3166 resolved.name.as_str(),
3167 resolved.version
3168 )
3169 })?;
3170 let meta = entry.versions.get(&resolved.version).ok_or_else(|| {
3171 anyhow::anyhow!(
3172 "resolver chose `{} {}`, but the index has no entry for this version",
3173 resolved.name.as_str(),
3174 resolved.version
3175 )
3176 })?;
3177 let source = meta.source.as_ref().ok_or_else(|| {
3178 anyhow::anyhow!(
3179 "package `{} {}` has no source artifact in the index",
3180 resolved.name.as_str(),
3181 resolved.version
3182 )
3183 })?;
3184 let checksum = meta.checksum.clone().ok_or_else(|| {
3185 anyhow::anyhow!(
3186 "missing checksum for `{} {}`; cabin fetch requires a sha256:<hex> entry in the index",
3187 resolved.name.as_str(),
3188 resolved.version
3189 )
3190 })?;
3191 let fetch_source = match (&source.location, access) {
3192 (cabin_index::SourceLocation::LocalPath(p), _) => {
3193 cabin_artifact::FetchSource::LocalArchive(p.clone())
3194 }
3195 (cabin_index::SourceLocation::HttpUrl(url), IndexAccess::Http(client)) => {
3196 let label = format!("{} {}", resolved.name.as_str(), resolved.version);
3197 let bytes = client.download(url, &label).with_context(|| {
3198 format!(
3199 "failed to download source archive for `{} {}`",
3200 resolved.name.as_str(),
3201 resolved.version
3202 )
3203 })?;
3204 cabin_artifact::FetchSource::InMemoryArchive(bytes)
3205 }
3206 (cabin_index::SourceLocation::HttpUrl(_), IndexAccess::Local) => {
3207 bail!(
3208 "package `{} {}` has an HTTP source URL but the run is using a local index",
3209 resolved.name.as_str(),
3210 resolved.version
3211 );
3212 }
3213 };
3214 entries.push(FetchEntry {
3215 name: resolved.name.clone(),
3216 version: resolved.version.clone(),
3217 checksum,
3218 source: fetch_source,
3219 });
3220 }
3221 Ok(FetchPlan { entries })
3222}
3223
3224#[derive(Debug, Clone)]
3226pub(crate) enum LockMode {
3227 PreferLocked,
3228 Locked,
3229 UpdateAll,
3230 UpdatePackage(String),
3231}
3232
3233struct ResolutionRequest<'a> {
3234 manifest_path: &'a Path,
3235 index_path: Option<&'a Path>,
3236 index_url: Option<&'a str>,
3237 format: ResolveFormat,
3238 mode: LockMode,
3239 allow_write: bool,
3240 frozen: bool,
3245 update_package: Option<&'a str>,
3249 selection: cabin_workspace::PackageSelection,
3252 selection_request: cabin_core::SelectionRequest,
3255 no_patches: bool,
3257 offline: bool,
3259}
3260
3261fn run_resolution(request: &ResolutionRequest<'_>, reporter: Reporter) -> Result<()> {
3262 let manifest_path = absolutise(request.manifest_path)
3263 .with_context(|| format!("failed to resolve {}", request.manifest_path.display()))?;
3264 let offline = crate::config_glue::effective_offline(request.offline)?;
3265 let (_port_sources, graph) = crate::port_glue::prepare_ports_and_load_initial_graph(
3266 &manifest_path,
3267 None,
3268 offline,
3269 request.frozen,
3270 false,
3271 &request.selection,
3272 request.no_patches,
3273 )?;
3274 let effective_config = crate::config_glue::load_effective_config(&graph)?;
3279 let active_patches =
3280 crate::patch_glue::load_active_patches(&graph, &effective_config, request.no_patches)?;
3281 let patched_names = active_patches.owned_patched_names();
3282 let resolved_index_source = crate::config_glue::resolve_index_source(
3283 request.index_path,
3284 request.index_url,
3285 &effective_config,
3286 )?;
3287 let resolution_offline = crate::config_glue::effective_offline(request.offline)?;
3288 crate::config_glue::enforce_offline_index_source(
3289 resolution_offline,
3290 resolved_index_source.as_ref(),
3291 )?;
3292 let (config_index_path, config_index_url): (Option<PathBuf>, Option<String>) =
3293 match resolved_index_source.as_ref() {
3294 Some(source) => {
3295 let initial = crate::config_glue::index_source_kind_to_locator(&source.kind);
3296 let resolved = crate::patch_glue::apply_source_replacement(
3297 initial,
3298 &effective_config,
3299 request.no_patches,
3300 )?;
3301 crate::config_glue::enforce_offline_post_replacement(
3302 resolution_offline,
3303 &resolved,
3304 )?;
3305 crate::patch_glue::locator_to_index_inputs(&resolved.resolved)
3306 }
3307 None => (None, None),
3308 };
3309 let effective_index_path = config_index_path.as_deref();
3310 let effective_index_url = config_index_url.as_deref();
3311 if request.frozen && effective_index_url.is_some() {
3312 bail!(
3313 "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"
3314 );
3315 }
3316
3317 let resolved_selection = selected_resolution_packages(&graph, &request.selection)?;
3321 let features =
3322 compute_feature_resolution(&graph, &resolved_selection, &request.selection_request)?;
3323 let dev_for: BTreeSet<String> = BTreeSet::new();
3324 let mut root_deps = collect_closure_versioned_deps_excluding_patches(
3325 &graph,
3326 &resolved_selection,
3327 &features,
3328 &patched_names,
3329 &dev_for,
3330 )?;
3331 let patched_root_deps = collect_patched_versioned_deps(&active_patches, &patched_names)?;
3336 merge_versioned_deps(&mut root_deps, patched_root_deps)?;
3337 let (root_name, root_version) = match graph.root_package {
3338 Some(idx) => (
3339 graph.packages[idx].package.name.clone(),
3340 graph.packages[idx].package.version.clone(),
3341 ),
3342 None => cabin_workspace::synthetic_root_identity(&graph),
3343 };
3344
3345 let lockfile_path = lockfile_path_for(&manifest_path);
3346
3347 if let Some(name) = request.update_package
3353 && !root_deps.contains_key(
3354 &PackageName::new(name)
3355 .map_err(|err| anyhow::anyhow!("invalid --package value {name:?}: {err}"))?,
3356 )
3357 {
3358 bail!(
3367 "package {name:?} is not a direct versioned dependency of `{}`; `cabin update --package` only refreshes direct dependencies declared under `[dependencies]`",
3368 root_name.as_str(),
3369 );
3370 }
3371
3372 let existing_lockfile: Option<Lockfile> = if lockfile_path.is_file() {
3377 Some(
3378 cabin_lockfile::read_lockfile(&lockfile_path)
3379 .with_context(|| format!("failed to read {}", lockfile_path.display()))?,
3380 )
3381 } else {
3382 None
3383 };
3384
3385 let active_patch_records = crate::patch_glue::lockfile_patches(&active_patches);
3393 let active_replacement_records = crate::patch_glue::lockfile_source_replacements(
3394 &effective_config.source_replacements,
3395 request.no_patches,
3396 );
3397 if matches!(request.mode, LockMode::Locked)
3398 && let Some(prev) = &existing_lockfile
3399 && !prev.matches_patch_state(&active_patch_records, &active_replacement_records)
3400 {
3401 bail!(
3402 "--locked cannot be used because active patch / source-replacement policy differs from {}; re-run without --locked to refresh the lockfile",
3403 lockfile_path.display()
3404 );
3405 }
3406
3407 if root_deps.is_empty() {
3408 let output = ResolveOutput {
3413 packages: vec![ResolvedPackage {
3414 name: root_name,
3415 version: root_version,
3416 source: ResolvedSource::Root,
3417 }],
3418 };
3419 emit_resolve_output(&output, request.format)?;
3420 return Ok(());
3421 }
3422
3423 if existing_lockfile.is_none() && matches!(request.mode, LockMode::Locked) {
3427 bail!(
3428 "cannot resolve with --locked because {} does not exist",
3429 lockfile_path.display()
3430 );
3431 }
3432
3433 let index = match (effective_index_path, effective_index_url) {
3434 (None, None) => {
3435 bail!(
3436 "versioned dependencies require --index-path, --index-url, or a `[registry]` config setting"
3437 )
3438 }
3439 (Some(path), None) => {
3440 let index_path = absolutise(path)
3441 .with_context(|| format!("failed to resolve {}", path.display()))?;
3442 cabin_index::load_index(&index_path)
3443 .with_context(|| format!("failed to load index at {}", index_path.display()))?
3444 }
3445 (None, Some(url)) => {
3446 let client = cabin_index_http::HttpClient::new();
3447 let http_index = cabin_index_http::HttpIndex::open(url, client)?;
3448 let names: Vec<PackageName> = root_deps.keys().cloned().collect();
3449 http_index.load_package_index(&names)?
3450 }
3451 (Some(_), Some(_)) => {
3452 unreachable!("config_glue::resolve_index_source guarantees only one variant is set")
3453 }
3454 };
3455
3456 let resolver_mode = match &request.mode {
3457 LockMode::PreferLocked => ResolveMode::PreferLocked,
3458 LockMode::Locked => ResolveMode::Locked,
3459 LockMode::UpdateAll => ResolveMode::UpdateAll,
3460 LockMode::UpdatePackage(name) => ResolveMode::UpdatePackage(
3461 PackageName::new(name.clone())
3462 .map_err(|err| anyhow::anyhow!("invalid --package value {name:?}: {err}"))?,
3463 ),
3464 };
3465
3466 let mut input = ResolveInput::new(root_name, root_version, root_deps);
3467 if let Some(lock) = &existing_lockfile {
3468 for pkg in &lock.packages {
3469 input.locked.insert(
3470 pkg.name.clone(),
3471 LockedVersion {
3472 version: pkg.version.clone(),
3473 checksum: pkg.checksum.clone(),
3474 },
3475 );
3476 }
3477 }
3478 input.mode = resolver_mode;
3479
3480 let output = cabin_resolver::resolve(&input, &index).context("dependency resolution failed")?;
3481
3482 let mut new_lockfile = lockfile_from_resolution(&output, &index);
3483 new_lockfile.patches = active_patch_records;
3484 new_lockfile.source_replacements = active_replacement_records;
3485
3486 if request.allow_write {
3487 let needs_write = match &existing_lockfile {
3488 Some(prev) => prev != &new_lockfile,
3489 None => true,
3490 };
3491 if needs_write {
3492 cabin_lockfile::write_lockfile(&lockfile_path, &new_lockfile)
3493 .with_context(|| format!("failed to write {}", lockfile_path.display()))?;
3494 reporter.aux_verbose(format_args!("cabin: wrote {}", lockfile_path.display()));
3495 } else {
3496 reporter.aux_verbose(format_args!(
3497 "cabin: {} is up to date",
3498 lockfile_path.display()
3499 ));
3500 }
3501 } else if matches!(request.mode, LockMode::Locked)
3502 && let Some(prev) = &existing_lockfile
3503 && prev != &new_lockfile
3504 {
3505 bail!(
3510 "{} is stale; run `cabin resolve` or `cabin update` to refresh it",
3511 lockfile_path.display()
3512 );
3513 }
3514
3515 emit_resolve_output(&output, request.format)?;
3516 Ok(())
3517}
3518
3519pub(crate) fn lockfile_path_for(manifest_path: &Path) -> PathBuf {
3520 manifest_path
3521 .parent()
3522 .map_or_else(|| PathBuf::from("."), std::path::Path::to_path_buf)
3523 .join("cabin.lock")
3524}
3525
3526pub(crate) fn read_optional_lockfile(lockfile_path: &Path) -> Result<Option<Lockfile>> {
3533 if lockfile_path.is_file() {
3534 Ok(Some(
3535 cabin_lockfile::read_lockfile(lockfile_path)
3536 .with_context(|| format!("failed to read {}", lockfile_path.display()))?,
3537 ))
3538 } else {
3539 Ok(None)
3540 }
3541}
3542
3543fn lockfile_from_resolution(output: &ResolveOutput, index: &cabin_index::PackageIndex) -> Lockfile {
3544 let resolved_names: BTreeSet<&str> = output
3549 .packages
3550 .iter()
3551 .filter(|p| p.source == ResolvedSource::Index)
3552 .map(|p| p.name.as_str())
3553 .collect();
3554 let mut packages: Vec<LockedPackage> = Vec::new();
3555 for pkg in &output.packages {
3556 if pkg.source != ResolvedSource::Index {
3557 continue;
3558 }
3559 let entry = index
3560 .package(&pkg.name)
3561 .expect("index has every resolved package");
3562 let meta = entry
3563 .versions
3564 .get(&pkg.version)
3565 .expect("index has the resolved version");
3566 let mut deps: Vec<PackageName> = meta
3568 .dependencies
3569 .keys()
3570 .filter(|n| resolved_names.contains(n.as_str()))
3571 .cloned()
3572 .collect();
3573 deps.sort();
3574 packages.push(LockedPackage {
3575 name: pkg.name.clone(),
3576 version: pkg.version.clone(),
3577 source: LockedSource::Index,
3578 checksum: meta.checksum.clone(),
3579 dependencies: deps,
3580 });
3581 }
3582 packages.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
3583 Lockfile {
3584 version: cabin_lockfile::LOCKFILE_VERSION,
3585 packages,
3586 patches: Vec::new(),
3587 source_replacements: Vec::new(),
3588 }
3589}
3590
3591fn emit_resolve_output(output: &ResolveOutput, format: ResolveFormat) -> Result<()> {
3592 match format {
3593 ResolveFormat::Human => print_resolve_human(output),
3594 ResolveFormat::Json => print_resolve_json(output),
3595 }
3596}
3597
3598fn print_resolve_human(output: &ResolveOutput) -> Result<()> {
3599 let root = output
3600 .packages
3601 .iter()
3602 .find(|p| p.source == ResolvedSource::Root)
3603 .ok_or_else(|| anyhow::anyhow!("resolver output is missing a root package"))?;
3604 println!(
3605 "Resolved dependencies for {} {}:",
3606 root.name.as_str(),
3607 root.version
3608 );
3609 let mut others: Vec<&cabin_resolver::ResolvedPackage> = output
3610 .packages
3611 .iter()
3612 .filter(|p| p.source != ResolvedSource::Root)
3613 .collect();
3614 others.sort_by(|a, b| a.name.as_str().cmp(b.name.as_str()));
3615 if others.is_empty() {
3616 println!(" (no versioned dependencies)");
3617 } else {
3618 for pkg in others {
3619 println!(" {} {}", pkg.name.as_str(), pkg.version);
3620 }
3621 }
3622 Ok(())
3623}
3624
3625fn print_resolve_json(output: &ResolveOutput) -> Result<()> {
3626 let root = output
3627 .packages
3628 .iter()
3629 .find(|p| p.source == ResolvedSource::Root)
3630 .ok_or_else(|| anyhow::anyhow!("resolver output is missing a root package"))?;
3631 let json_root = serde_json::json!({
3632 "name": root.name.as_str(),
3633 "version": root.version.to_string(),
3634 });
3635 let json_packages: Vec<_> = output
3636 .packages
3637 .iter()
3638 .filter(|p| p.source != ResolvedSource::Root)
3639 .map(|p| {
3640 serde_json::json!({
3641 "name": p.name.as_str(),
3642 "version": p.version.to_string(),
3643 "source": p.source.as_str(),
3644 })
3645 })
3646 .collect();
3647 let value = serde_json::json!({
3648 "root": json_root,
3649 "packages": json_packages,
3650 });
3651 crate::print_pretty_json(&value, "failed to serialize resolve output as JSON")
3652}
3653
3654pub(crate) fn absolutise(path: &Path) -> std::io::Result<PathBuf> {
3656 if path.is_absolute() {
3657 Ok(path.to_path_buf())
3658 } else {
3659 Ok(std::env::current_dir()?.join(path))
3660 }
3661}
3662
3663#[cfg(test)]
3664mod tests {
3665 use super::*;
3666
3667 #[test]
3668 fn rendered_binary_template_round_trips_through_parser() {
3669 let manifest = scaffold::render_manifest("hello", scaffold::ScaffoldKind::Binary);
3670 let parsed = cabin_manifest::parse_manifest_str(&manifest).unwrap();
3671 let package = parsed.package.expect("template should parse as a package");
3672 assert_eq!(package.name.as_str(), "hello");
3673 assert_eq!(package.targets.len(), 1);
3674 assert_eq!(package.targets[0].name.as_str(), "hello");
3675 }
3676
3677 #[test]
3678 fn rendered_library_template_round_trips_through_parser() {
3679 let manifest = scaffold::render_manifest("hello", scaffold::ScaffoldKind::Library);
3680 let parsed = cabin_manifest::parse_manifest_str(&manifest).unwrap();
3681 let package = parsed.package.expect("template should parse as a package");
3682 assert_eq!(package.name.as_str(), "hello");
3683 assert_eq!(package.targets.len(), 1);
3684 assert_eq!(package.targets[0].name.as_str(), "hello");
3685 }
3686
3687 #[test]
3688 fn registry_dependency_build_flags_are_dropped_but_local_kept() {
3689 use cabin_core::{Package, Target};
3690 use cabin_workspace::{PackageKind, WorkspacePackage};
3691 use std::path::PathBuf;
3692
3693 fn dep_with_command_flags(name: &str, kind: PackageKind) -> WorkspacePackage {
3694 let mut package = Package::new(
3695 PackageName::new(name).unwrap(),
3696 semver::Version::parse("0.1.0").unwrap(),
3697 Vec::<Target>::new(),
3698 Vec::new(),
3699 )
3700 .unwrap();
3701 package.build.general.cflags = vec!["-fplugin=evil.so".into()];
3702 package.build.general.cxxflags = vec!["-B.".into()];
3703 package.build.general.ldflags = vec!["-fuse-ld=/tmp/evil".into()];
3704 WorkspacePackage {
3705 package,
3706 manifest_dir: PathBuf::from("/tmp"),
3707 manifest_path: PathBuf::from("/tmp/cabin.toml"),
3708 kind,
3709 deps: Vec::new(),
3710 }
3711 }
3712
3713 let graph = PackageGraph {
3714 root_manifest_path: PathBuf::from("/tmp/cabin.toml"),
3715 root_dir: PathBuf::from("/tmp"),
3716 is_workspace_root: false,
3717 root_package: Some(0),
3718 root_settings: Default::default(),
3719 primary_packages: vec![0],
3720 default_members: vec![0],
3721 excluded_members: Vec::new(),
3722 packages: vec![
3723 dep_with_command_flags("local_dep", PackageKind::Local),
3724 dep_with_command_flags("registry_dep", PackageKind::Registry),
3725 ],
3726 };
3727
3728 let host = cabin_core::TargetPlatform::current();
3729 let resolved = resolve_per_package_build_flags(&graph, None, &host);
3730
3731 let local = resolved.get(&0).expect("local package flags");
3734 assert_eq!(local.cflags, vec!["-fplugin=evil.so".to_owned()]);
3735 assert_eq!(local.cxxflags, vec!["-B.".to_owned()]);
3736 assert_eq!(local.ldflags, vec!["-fuse-ld=/tmp/evil".to_owned()]);
3737
3738 let registry = resolved.get(&1).expect("registry package flags");
3741 assert!(registry.cflags.is_empty());
3742 assert!(registry.cxxflags.is_empty());
3743 assert!(registry.ldflags.is_empty());
3744 }
3745
3746 type EnvFn = Box<dyn Fn(&str) -> Option<std::ffi::OsString>>;
3751
3752 fn env_with(items: &[(&'static str, &str)]) -> EnvFn {
3757 let map: std::collections::HashMap<&'static str, std::ffi::OsString> = items
3758 .iter()
3759 .map(|(k, v)| (*k, std::ffi::OsString::from(*v)))
3760 .collect();
3761 Box::new(move |k| map.get(k).cloned())
3762 }
3763
3764 fn fake_manifest() -> &'static Path {
3765 Path::new("/abs/ws/cabin.toml")
3768 }
3769
3770 fn home_xdg_cache_home(home: &str) -> PathBuf {
3775 PathBuf::from(home).join(".cache").join("cabin")
3776 }
3777
3778 #[test]
3779 fn cache_dir_flag_wins_over_everything() {
3780 let env = env_with(&[
3781 ("CABIN_CACHE_DIR", "/tmp/from-env"),
3782 ("CABIN_CACHE_HOME", "/tmp/cabin-home"),
3783 ]);
3784 let xdg = PathBuf::from("/tmp/xdg/cabin");
3785 let out = cache_dir_for_with_env(
3786 fake_manifest(),
3787 Some(Path::new("/tmp/from-flag")),
3788 &env,
3789 Some(&xdg),
3790 )
3791 .unwrap();
3792 assert_eq!(out, PathBuf::from("/tmp/from-flag"));
3793 }
3794
3795 #[test]
3796 fn cabin_cache_dir_env_wins_over_xdg() {
3797 let env = env_with(&[
3798 ("CABIN_CACHE_DIR", "/tmp/from-env"),
3799 ("CABIN_CACHE_HOME", "/tmp/cabin-home"),
3800 ]);
3801 let xdg = PathBuf::from("/tmp/xdg/cabin");
3802 let out = cache_dir_for_with_env(fake_manifest(), None, &env, Some(&xdg)).unwrap();
3803 assert_eq!(out, PathBuf::from("/tmp/from-env"));
3804 }
3805
3806 #[test]
3807 fn cabin_cache_home_used_when_cabin_cache_dir_unset() {
3808 let env = env_with(&[("CABIN_CACHE_HOME", "/tmp/cabin-home")]);
3813 let xdg = PathBuf::from("/tmp/xdg/cabin");
3814 let out = cache_dir_for_with_env(fake_manifest(), None, &env, Some(&xdg)).unwrap();
3815 assert_eq!(out, PathBuf::from("/tmp/cabin-home"));
3816 }
3817
3818 #[test]
3819 fn xdg_cache_home_appends_cabin_segment() {
3820 let env = env_with(&[]);
3825 let xdg = PathBuf::from("/tmp/xdg/cabin");
3826 let out = cache_dir_for_with_env(fake_manifest(), None, &env, Some(&xdg)).unwrap();
3827 assert_eq!(out, PathBuf::from("/tmp/xdg/cabin"));
3828 }
3829
3830 #[test]
3831 fn home_cache_fallback_used_when_xdg_unset() {
3832 let env = env_with(&[]);
3835 let xdg = home_xdg_cache_home("/tmp/home");
3836 let out = cache_dir_for_with_env(fake_manifest(), None, &env, Some(&xdg)).unwrap();
3837 assert_eq!(out, PathBuf::from("/tmp/home/.cache/cabin"));
3838 }
3839
3840 #[test]
3841 fn empty_cabin_cache_dir_value_falls_through() {
3842 let env = env_with(&[("CABIN_CACHE_DIR", "")]);
3846 let xdg = home_xdg_cache_home("/tmp/home");
3847 let out = cache_dir_for_with_env(fake_manifest(), None, &env, Some(&xdg)).unwrap();
3848 assert_eq!(out, PathBuf::from("/tmp/home/.cache/cabin"));
3849 }
3850
3851 #[test]
3852 fn empty_cabin_cache_home_value_falls_through_to_xdg() {
3853 let env = env_with(&[("CABIN_CACHE_HOME", "")]);
3857 let xdg = PathBuf::from("/tmp/xdg/cabin");
3858 let out = cache_dir_for_with_env(fake_manifest(), None, &env, Some(&xdg)).unwrap();
3859 assert_eq!(out, PathBuf::from("/tmp/xdg/cabin"));
3860 }
3861
3862 #[test]
3863 fn all_envs_unset_returns_error() {
3864 let env = env_with(&[]);
3867 let err = cache_dir_for_with_env(fake_manifest(), None, &env, None).unwrap_err();
3868 let msg = err.to_string();
3869 assert!(
3870 msg.contains("no cache directory"),
3871 "expected diagnostic mentioning 'no cache directory', got: {msg}"
3872 );
3873 }
3874}