1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
// Copyright (C) 2023 Andreas Hartmann <hartan@7x.de>
// GNU General Public License v3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)
// SPDX-License-Identifier: GPL-3.0-or-later

//! Command-line wrapper for [`cnf_lib`].
// FIXME(hartan): Go back to a full binary crate once
// https://github.com/rust-lang/docs.rs/issues/238 is resolved.
pub mod alias;
mod cli;
pub mod config;
mod directories;
pub mod env;
mod trace;
pub mod ui;

use anyhow::{Context, Result};
use cnf_lib::{
    prelude::*,
    provider::{apt, cargo, cwd, dnf, flatpak, pacman, path},
};
use logerr::LoggableError;
use std::{str::FromStr, sync::Arc};
use tracing::{debug, info};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;

pub use cli::Args;
pub use env::Env;

/// Take a provider base type, wrap it into a [`cnf_lib::Provider`] enum and wrap that into an
/// [`Arc`].
macro_rules! arc_provider {
    ($provider:ty) => {
        Arc::new(<$provider>::new().into())
    };
}

/// Application entrypoint.
#[doc(hidden)]
pub async fn main(args: cli::Args) -> Result<()> {
    // Load application config
    config::load();

    // Handle shell hooks
    if let Some(hook) = args.hooks {
        cli::install_hook(hook);
        return Ok(());
    }

    // holds guards which ensure that all captured trace data is flushed on program exit
    let mut guards: Vec<Box<dyn trace::Guard>> = vec![];
    // the registry holds all the layers we want to add for tracing purposes
    let registry = tracing_subscriber::registry();
    // logfile
    let log_layer = trace::logfile().context("failed to enable application logging")?;
    let registry = registry.with(log_layer.layer);
    guards.push(log_layer.guard);
    // flame graphs
    #[cfg(feature = "debug-flame")]
    let registry = {
        let flame_file = trace::flame_file()?;
        let (writer, guard) = tracing_appender::non_blocking(flame_file);
        guards.push(Box::new(guard));
        registry.with(
            tracing_flame::FlameLayer::new(writer)
                .with_empty_samples(true)
                .with_threads_collapsed(true)
                .with_file_and_line(true),
        )
    };
    // register all tracing components
    registry.init();

    info!("Launching application");
    let cur_env = Arc::new(cnf_lib::environment::current());
    info!("Running in environment '{}'", cur_env);

    // Check and limit recursion depth
    if let Some(recursion_depth) = Env::RecursionDepth.get::<usize>() {
        debug!("cnf execution at recursion depth {}", recursion_depth);
        let max_recursion = config::get().max_recursion_depth;
        if recursion_depth >= max_recursion {
            anyhow::bail!(
                "current recursion depth {} exceeds configured maximum recursion depth {}",
                recursion_depth,
                max_recursion
            );
        }

        Env::RecursionDepth.set(recursion_depth + 1);
    } else {
        Env::RecursionDepth.set(0)
    }

    // The base command being searched for
    let command = args.command[0].to_string();

    // Execute command aliases
    let alias = args.alias_target_env.and_then(|target_env| {
        let mut cmd = CommandLine::new(&args.command);
        cmd.needs_privileges(args.alias_privileged);
        cmd.is_interactive(args.alias_interactive);
        let alias = alias::Alias {
            source_env: "".to_string(),
            target_env,
            command: command.clone(),
            alias: cmd,
        };

        if args
            .alias_source_env
            .is_some_and(|env| env != cur_env.to_json())
        {
            // source environment doesn't match, skip it
            None
        } else {
            Some(alias)
        }
    });

    if let Some(alias) = alias {
        info!(command, "executing command alias");
        let target_env = Environment::from_str(&alias.target_env)
            .with_context(|| format!("failed to resolve alias '{:?}'", alias))?;
        // the environment may not be started yet, so we enforce this here
        target_env.start()?;

        let mut cmd = alias.alias.clone();
        if Env::AliasPrivileged.is_set() {
            debug!("privileged alias execution requested by env variable");
            cmd.needs_privileges(true);
        }

        let status = target_env
            .execute(cmd)
            .await
            .with_context(|| format!("failed to prepare alias execution in env '{}'", target_env))?
            .status()
            .await
            .map_err(anyhow::Error::new)?;
        match status.code() {
            Some(code) if code != 0 => std::process::exit(code),
            None => std::process::exit(255),
            _ => return Ok(()),
        }
    }

    // There's no point in spinning up a full-blown TUI when run non-interactively.
    if !std::io::IsTerminal::is_terminal(&std::io::stdout()) {
        anyhow::bail!("command not found: {}", command);
    }

    // No alias, fire up the regular application flow
    let (tx, rx) = tokio::sync::mpsc::unbounded_channel();

    // Collect environments
    let mut envs = vec![
        cur_env,
        Arc::new(cnf_lib::Environment::Host(cnf_lib::env::host::Host::new())),
    ];

    // Add toolboxes from config
    let mut toolbx_names = config::get()
        .toolbx_names
        .clone()
        .into_iter()
        .map(|n| if n.is_empty() { None } else { Some(n) })
        .collect::<Vec<_>>();
    if toolbx_names.is_empty() {
        toolbx_names.push(None);
    }
    for toolbx_name in toolbx_names {
        if let Ok(toolbx) = cnf_lib::env::toolbx::Toolbx::new(toolbx_name)
            .context("cannot search in 'toolbx' environment")
            .to_log()
        {
            if toolbx.exists().await {
                envs.push(Arc::new(toolbx.into()));
            }
        }
    }

    // Add distroboxes from config
    let mut distrobox_names = config::get()
        .distrobox_names
        .clone()
        .into_iter()
        .map(|n| if n.is_empty() { None } else { Some(n) })
        .collect::<Vec<_>>();
    if distrobox_names.is_empty() {
        distrobox_names.push(None)
    }
    for distrobox_name in distrobox_names {
        if let Ok(distrobox) = cnf_lib::env::distrobox::Distrobox::new(distrobox_name)
            .context("cannot search in 'distrobox' environment")
            .to_log()
        {
            if distrobox.exists().await {
                envs.push(Arc::new(distrobox.into()));
            }
        }
    }

    // Deduplicate environments
    envs.sort();
    envs.dedup();

    // Check for invalid envs in config
    for origin in &config::get().query_origins {
        let _ = origin.check_env_exists(&envs).to_log();
    }

    // Collect providers
    // TODO(hartan): Some providers need really only be run once. Maybe have the TUI filter for
    // identical results and remove them? Or offer this as config option?
    let mut providers: Vec<Arc<Provider>> = vec![
        arc_provider!(path::Path),
        arc_provider!(dnf::Dnf),
        arc_provider!(cargo::Cargo),
        arc_provider!(pacman::Pacman),
        arc_provider!(apt::Apt),
        arc_provider!(flatpak::Flatpak),
    ];
    if let Ok(val) = cwd::Cwd::new()
        .context("cannot search in provider 'cwd'")
        .to_log()
    {
        providers.push(Arc::new(val.into()));
    }
    // Custom providers
    config::get()
        .custom_providers
        .iter()
        .for_each(|provider| providers.push(Arc::new(Provider::from(provider.clone()))));

    // Check for invalid providers in config
    for origin in &config::get().query_origins {
        let _ = origin.check_providers_exist(&providers).to_log();
    }

    for env in envs {
        // Configuration entries for this environment
        let env_config = config::get()
            .query_origins
            .iter()
            .find(|origin| origin.environment == env.to_string());

        let empty: Vec<String> = vec![];
        let disabled_providers = env_config
            .map(|config| &config.disabled_providers)
            .unwrap_or(&empty);
        let enabled_providers = env_config
            .map(|config| &config.enabled_providers)
            .unwrap_or(&empty);

        // By default (no config), enable all environments
        if env_config.map(|config| config.enabled).unwrap_or(true) {
            for prov in &providers {
                let prov_name = prov.to_string();
                if disabled_providers.is_empty() && enabled_providers.is_empty() {
                    // By default (no config), enable all providers
                } else if disabled_providers.contains(&prov_name) {
                    debug!(
                        "provider '{}' for env '{}' set to inactive in config",
                        prov.to_string(),
                        env.to_string()
                    );
                    continue;
                } else if disabled_providers.is_empty() && !enabled_providers.contains(&prov_name) {
                    debug!(
                        "provider '{}' for env '{}' not set to active in config",
                        prov.to_string(),
                        env.to_string()
                    );
                    continue;
                }

                let cloned_env = env.clone();
                let env_name = env.to_string();
                let cloned_sender = tx.clone();
                let cloned_provider = prov.clone();
                let provider_name = prov.to_string();
                let cloned_cmd = command.clone();
                tokio::task::spawn(async move {
                    let result = search_in(cloned_provider, &cloned_cmd, cloned_env).await;
                    let _ = cloned_sender
                        .send(result)
                        .with_context(|| {
                            format!(
                                "failed to report results from '{}' in '{}'",
                                &provider_name, &env_name
                            )
                        })
                        .to_log();
                });
            }
        } else {
            debug!("env '{}' is ignored according to config", env.to_string());
        }
    }

    // Manually force the drop so the channel is closed when the last async task exits.
    drop(tx);

    ui::tui(rx, &args.command).await
}