1use std::process::ExitCode;
2
3use crate::cli::Cli;
4use crate::config::model::ExecutionMode;
5use crate::config::{LoadOptions, load_config};
6use crate::error::SboxError;
7use crate::exec::{execute_host, execute_sandbox, run_pre_run_commands, validate_execution_safety};
8use crate::resolve::{ResolutionTarget, resolve_execution_plan};
9
10pub fn execute(cli: &Cli) -> Result<ExitCode, SboxError> {
16 let loaded = load_config(&LoadOptions {
17 workspace: cli.workspace.clone(),
18 config: cli.config.clone(),
19 })?;
20
21 let pm = loaded
22 .config
23 .package_manager
24 .as_ref()
25 .ok_or_else(|| SboxError::ConfigValidation {
26 message: "sbox bootstrap requires `package_manager:` in sbox.yaml\n\
27 for manually-configured profiles, generate the lockfile directly:\n\
28 sbox run -- <pm> lock (or equivalent)"
29 .to_string(),
30 })?;
31
32 let bootstrap_cmd = bootstrap_command_for(&pm.name)?;
33
34 let plan = resolve_execution_plan(cli, &loaded, ResolutionTarget::Run, &bootstrap_cmd)?;
38 validate_execution_safety(&plan, false)?;
39 run_pre_run_commands(&plan)?;
40
41 eprintln!(
42 "sbox bootstrap: generating {} lockfile inside sandbox...",
43 pm.name
44 );
45
46 let exit = match plan.mode {
47 ExecutionMode::Host => execute_host(&plan)?,
48 ExecutionMode::Sandbox => execute_sandbox(&plan)?,
49 };
50
51 if exit == ExitCode::SUCCESS {
52 eprintln!("\nlockfile generated.");
53 eprintln!("next: sbox run -- {}", rebuild_hint(&pm.name));
54 }
55
56 Ok(exit)
57}
58
59fn bootstrap_command_for(pm_name: &str) -> Result<Vec<String>, SboxError> {
61 let cmd: &[&str] = match pm_name {
62 "npm" => &["npm", "install", "--ignore-scripts"],
63 "yarn" => &["yarn", "install", "--ignore-scripts"],
64 "pnpm" => &["pnpm", "install", "--ignore-scripts"],
65 "bun" => &["bun", "install", "--no-scripts"],
66 "uv" => &["uv", "lock"],
67 "pip" => {
68 return Err(SboxError::ConfigValidation {
70 message: "pip does not support lockfile-only bootstrap.\n\
71 To generate a pinned requirements.txt safely:\n\
72 pip-compile requirements.in (install pip-tools first)\n\
73 uv pip compile requirements.in -o requirements.txt\n\
74 Or switch to the `uv` preset which supports `sbox bootstrap` natively."
75 .to_string(),
76 });
77 }
78 "poetry" => &["poetry", "lock"],
79 "cargo" => &["cargo", "fetch"],
80 "go" => &["go", "mod", "download"],
81 _ => {
82 return Err(SboxError::ConfigValidation {
83 message: format!(
84 "no bootstrap command known for package manager `{pm_name}`; \
85 generate the lockfile manually and run `sbox run` directly"
86 ),
87 });
88 }
89 };
90 Ok(cmd.iter().map(|s| s.to_string()).collect())
91}
92
93fn rebuild_hint(pm_name: &str) -> &'static str {
95 match pm_name {
96 "npm" => "npm rebuild # runs install scripts, network off",
97 "yarn" => "yarn install # runs install scripts, network off",
98 "pnpm" => "pnpm rebuild # runs install scripts, network off",
99 "bun" => "bun install # runs install scripts, network off",
100 "uv" => "uv sync # install from lockfile, network off",
101 "pip" => "pip install -r requirements.txt # install from requirements",
102 "poetry" => "poetry install # install from lockfile, network off",
103 "cargo" => "cargo build # compile from fetched sources",
104 "go" => "go build ./... # compile from downloaded modules",
105 _ => "<install-command>",
106 }
107}