Skip to main content

treeboot_core/
run.rs

1use std::path::PathBuf;
2use std::process::Command;
3
4use crate::config::RuntimeOptionOverrides;
5use crate::context;
6use crate::{
7    ActionPlan, Config, EnvironmentInput, Error, ExecuteOptions, Executor, InitScriptDiscovery,
8    OutputEvent, Reporter, Result, Worktree, WorktreeOptions,
9};
10
11/// Options for running worktree bootstrap.
12#[derive(Debug, Clone, Default, PartialEq, Eq)]
13pub struct RunOptions {
14    /// Directory from which the run starts. Defaults to the process cwd.
15    pub cwd: Option<PathBuf>,
16    /// Overrides the root checkout used as the file-operation source.
17    pub root: Option<PathBuf>,
18    /// Explicit environment input used for compatibility discovery and options.
19    pub environment: EnvironmentInput,
20    /// Uses one specific config file and skips init script discovery.
21    pub config: Option<PathBuf>,
22    /// Skips init script discovery and uses declarative config discovery.
23    pub no_init_script: bool,
24    /// Fails on missing config and stricter file-operation conflicts.
25    pub strict: bool,
26    /// Replaces existing file-operation targets where supported.
27    pub force: bool,
28    /// Prints planned work without changing files or running commands.
29    pub dry_run: bool,
30    /// Prints detailed file-operation actions instead of compact summaries.
31    pub verbose: bool,
32    /// Runs file operations only.
33    pub skip_commands: bool,
34}
35
36/// Completed action for a `treeboot run` invocation.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum RunAction {
39    /// No config or executable init script was detected.
40    MissingConfig,
41    /// The run started from the root checkout and had no work to do.
42    RootWorktreeSkipped,
43    /// An init script would run in dry-run mode.
44    WouldRunInitScript {
45        /// Script path.
46        path: PathBuf,
47    },
48    /// An init script was executed.
49    RanInitScript {
50        /// Script path.
51        path: PathBuf,
52    },
53    /// A declarative config was detected.
54    ConfigDetected {
55        /// Config file path.
56        path: PathBuf,
57    },
58    /// Declarative config file operations were applied.
59    ConfigApplied {
60        /// Config file path.
61        path: PathBuf,
62    },
63}
64
65/// Result summary for a `treeboot run` invocation.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct RunReport {
68    /// Runtime context used by the run.
69    pub context: Worktree,
70    /// Action taken by the run flow.
71    pub action: RunAction,
72}
73
74/// Runs worktree bootstrap according to the provided options.
75///
76/// Resolves the worktree context, discovers executable init scripts unless
77/// disabled, discovers declarative config files, reports the selected action,
78/// and executes an init script when one should run.
79///
80/// # Errors
81///
82/// Returns an error if context discovery fails, output reporting fails, an init
83/// script cannot be started or exits unsuccessfully, a configured file cannot
84/// be read, or strict mode treats a missing config as a failure.
85pub fn run(options: RunOptions, reporter: &mut dyn Reporter) -> Result<RunReport> {
86    let env_options = RuntimeOptionOverrides::from_environment(&options.environment)?;
87    let pre_config_strict = env_options.pre_config_strict(options.strict);
88    let context = context::resolve(&WorktreeOptions {
89        cwd: options.cwd.clone(),
90        root: options.root.clone(),
91        environment: options.environment.clone(),
92    })?;
93
94    if context.root_path == context.worktree_path {
95        report(reporter, OutputEvent::RootWorktreeDetected)?;
96
97        if pre_config_strict {
98            return Err(Error::RootWorktreeStrict);
99        }
100
101        return Ok(RunReport {
102            context,
103            action: RunAction::RootWorktreeSkipped,
104        });
105    }
106
107    if options.config.is_none() && !options.no_init_script {
108        let scripts = InitScriptDiscovery::discover(&context);
109
110        for ignored in scripts.ignored {
111            report(
112                reporter,
113                OutputEvent::IgnoredInitScript { path: ignored.path },
114            )?;
115        }
116
117        if let Some(path) = scripts.executable {
118            return run_init_script(path, context, &options, reporter);
119        }
120    }
121
122    match Config::discover_path(&context, options.config.as_deref())? {
123        Some(path) => {
124            report(reporter, OutputEvent::ConfigDetected { path: path.clone() })?;
125            let config = Config::load(&path, &context)?;
126            let plan_options = env_options.resolve(&config.options, options.strict);
127            let strict = plan_options.strict;
128            let plan = ActionPlan::from_manifest(&path, &config, &context, plan_options.into())?;
129            Executor::new(ExecuteOptions {
130                strict,
131                force: options.force,
132                dry_run: options.dry_run,
133                verbose: options.verbose,
134                skip_commands: options.skip_commands,
135            })
136            .execute(&plan, reporter)?;
137
138            Ok(RunReport {
139                context,
140                action: RunAction::ConfigApplied { path },
141            })
142        }
143        None => {
144            report(reporter, OutputEvent::NoConfigDetected)?;
145
146            if pre_config_strict {
147                Err(Error::NoConfigDetectedStrict)
148            } else {
149                Ok(RunReport {
150                    context,
151                    action: RunAction::MissingConfig,
152                })
153            }
154        }
155    }
156}
157
158fn run_init_script(
159    path: PathBuf,
160    context: Worktree,
161    options: &RunOptions,
162    reporter: &mut dyn Reporter,
163) -> Result<RunReport> {
164    if options.dry_run {
165        report(
166            reporter,
167            OutputEvent::WouldRunInitScript {
168                path: path.clone(),
169                root_path: context.root_path.clone(),
170            },
171        )?;
172
173        return Ok(RunReport {
174            context,
175            action: RunAction::WouldRunInitScript { path },
176        });
177    }
178
179    report(reporter, OutputEvent::RunInitScript { path: path.clone() })?;
180
181    let status = Command::new(&path)
182        .arg(&context.root_path)
183        .current_dir(&context.worktree_path)
184        .envs(&context.environment)
185        .status()
186        .map_err(|source| Error::ScriptIo {
187            path: path.clone(),
188            source,
189        })?;
190
191    if !status.success() {
192        return Err(Error::ScriptFailed { path, status });
193    }
194
195    Ok(RunReport {
196        context,
197        action: RunAction::RanInitScript { path },
198    })
199}
200
201fn report(reporter: &mut dyn Reporter, event: OutputEvent) -> Result<()> {
202    reporter
203        .report(event)
204        .map_err(|source| Error::Output { source })
205}