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