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