• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pkgxdev / pkgx / 13863219990

14 Mar 2025 06:39PM UTC coverage: 91.235%. First build
13863219990

Pull #1151

github

web-flow
Merge e39b8e6c8 into 35f6bbe1d
Pull Request #1151: --query,-Q

35 of 35 new or added lines in 4 files covered. (100.0%)

1374 of 1506 relevant lines covered (91.24%)

18538506.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.81
/crates/cli/src/main.rs
1
mod args;
2
mod execve;
3
mod help;
4
mod query;
5
mod setup;
6
#[cfg(test)]
7
mod tests;
8

9
use std::{collections::HashMap, error::Error, fmt::Write, sync::Arc, time::Duration};
10

11
use execve::execve;
12
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
13
use libpkgx::{
14
    env::{self, construct_platform_case_aware_env_key},
15
    hydrate::hydrate,
16
    install_multi, pantry_db,
17
    resolve::resolve,
18
    sync,
19
    types::PackageReq,
20
    utils,
21
};
22
use regex::Regex;
23
use rusqlite::Connection;
24
use serde_json::json;
25

26
#[tokio::main]
27
async fn main() -> Result<(), Box<dyn Error>> {
36✔
28
    let args::Args {
36✔
29
        plus,
36✔
30
        mut args,
36✔
31
        mode,
36✔
32
        flags,
36✔
33
        find_program,
36✔
34
    } = args::parse();
36✔
35

36✔
36
    if flags.version_n_continue {
36✔
37
        eprintln!("pkgx {}", env!("CARGO_PKG_VERSION"));
2✔
38
    }
34✔
39

36✔
40
    match mode {
36✔
41
        args::Mode::Help => {
36✔
42
            println!("{}", help::usage());
36✔
43
            return Ok(());
2✔
44
        }
36✔
45
        args::Mode::Version => {
36✔
46
            println!("pkgx {}", env!("CARGO_PKG_VERSION"));
36✔
47
            return Ok(());
2✔
48
        }
36✔
49
        args::Mode::Query => {
36✔
50
            let (conn, _, _, _) = setup::setup(&flags).await?;
36✔
51
            return query::query(&args, &conn);
36✔
52
        }
36✔
53
        args::Mode::X => (),
36✔
54
    }
36✔
55

36✔
56
    let (mut conn, did_sync, config, spinner) = setup::setup(&flags).await?;
36✔
57

36✔
58
    if let Some(spinner) = &spinner {
36✔
59
        spinner.set_message("resolving pkg graph…");
22✔
60
    }
22✔
61

36✔
62
    let mut pkgs = vec![];
36✔
63

36✔
64
    for pkgspec in plus.clone() {
36✔
65
        let PackageReq {
36✔
66
            project: project_or_cmd,
36✔
67
            constraint,
22✔
68
        } = PackageReq::parse(&pkgspec)?;
36✔
69
        if config
36✔
70
            .pantry_dir
22✔
71
            .join("projects")
22✔
72
            .join(project_or_cmd.clone())
22✔
73
            .is_dir()
22✔
74
        {
36✔
75
            pkgs.push(PackageReq {
8✔
76
                project: project_or_cmd,
8✔
77
                constraint,
8✔
78
            });
8✔
79
        } else {
8✔
80
            let project = which(&project_or_cmd, &conn, &pkgs).await?;
36✔
81
            pkgs.push(PackageReq {
36✔
82
                project,
14✔
83
                constraint,
14✔
84
            });
14✔
85
        }
36✔
86
    }
36✔
87

36✔
88
    if find_program {
36✔
89
        let PackageReq {
36✔
90
            constraint,
36✔
91
            project: cmd,
2✔
92
        } = PackageReq::parse(&args[0])?;
36✔
93

36✔
94
        args[0] = cmd.clone(); // invoke eg. `node` rather than eg. `node@20`
36✔
95

36✔
96
        let project = match which(&cmd, &conn, &pkgs).await {
36✔
97
            Err(WhichError::CmdNotFound(cmd)) => {
36✔
98
                if !did_sync {
×
99
                    if let Some(spinner) = &spinner {
36✔
100
                        let msg = format!("{} not found, syncing…", cmd);
×
101
                        spinner.set_message(msg);
×
102
                    }
103
                    // cmd not found ∴ sync in case it is new
36✔
104
                    sync::update(&config, &mut conn).await?;
36✔
105
                    if let Some(spinner) = &spinner {
36✔
106
                        spinner.set_message("resolving pkg graph…");
×
107
                    }
108
                    which(&cmd, &conn, &pkgs).await
36✔
109
                } else {
36✔
110
                    Err(WhichError::CmdNotFound(cmd))
36✔
111
                }
36✔
112
            }
36✔
113
            Err(err) => Err(err),
36✔
114
            Ok(project) => Ok(project),
36✔
115
        }?;
36✔
116

36✔
117
        pkgs.push(PackageReq {
36✔
118
            project,
×
119
            constraint,
×
120
        });
121
    }
36✔
122

36✔
123
    let companions = pantry_db::companions_for_projects(
36✔
124
        &pkgs
24✔
125
            .iter()
24✔
126
            .map(|project| project.project.clone())
24✔
127
            .collect::<Vec<_>>(),
24✔
128
        &conn,
24✔
129
    )?;
24✔
130

36✔
131
    pkgs.extend(companions);
36✔
132

36✔
133
    let graph = hydrate(&pkgs, |project| {
188✔
134
        pantry_db::deps_for_project(&project, &conn)
188✔
135
    })
188✔
136
    .await?;
24✔
137

36✔
138
    let resolution = resolve(graph, &config).await?;
36✔
139

36✔
140
    let spinner_clone = spinner.clone();
36✔
141
    let clear_progress_bar = move || {
24✔
142
        if let Some(spinner) = spinner_clone {
36✔
143
            spinner.finish_and_clear();
18✔
144
        }
18✔
145
    };
36✔
146

36✔
147
    let mut installations = resolution.installed;
36✔
148
    if !resolution.pending.is_empty() {
24✔
149
        let spinner = spinner.or(if !flags.silent && flags.quiet {
36✔
150
            Some(indicatif::ProgressBar::new(0))
36✔
151
        } else {
36✔
152
            None
36✔
153
        });
36✔
154
        let pb = spinner.map(|spinner| {
36✔
155
            configure_bar(&spinner);
6✔
156
            Arc::new(MultiProgressBar { pb: spinner })
6✔
157
        });
6✔
158
        let installed = install_multi::install_multi(&resolution.pending, &config, pb).await?;
36✔
159
        installations.extend(installed);
36✔
160
    }
36✔
161

36✔
162
    let env = env::map(&installations);
36✔
163

24✔
164
    if !args.is_empty() {
24✔
165
        let pkgx_lvl = std::env::var("PKGX_LVL")
36✔
166
            .unwrap_or("0".to_string())
2✔
167
            .parse()
2✔
168
            .unwrap_or(0)
2✔
169
            + 1;
2✔
170
        if pkgx_lvl >= 10 {
2✔
171
            return Err("PKGX_LVL exceeded: https://github.com/orgs/pkgxdev/discussions/11".into());
36✔
172
        }
36✔
173

36✔
174
        let cmd = if find_program {
36✔
175
            utils::find_program(&args.remove(0), &env["PATH"]).await?
36✔
176
        } else if args[0].contains('/') {
36✔
177
            // user specified a path to program which we should use
36✔
178
            args.remove(0)
36✔
179
        } else {
36✔
180
            // user wants a system tool, eg. pkgx +wget -- git clone
36✔
181
            // NOTE we still check the injected PATH since they may have added the tool anyway
36✔
182
            // it’s just this route allows the user to get a non-error for delegating through to the system
36✔
183
            let mut paths = vec![];
36✔
184
            if let Some(pkgpaths) = env.get("PATH") {
36✔
185
                paths.append(&mut pkgpaths.clone());
×
186
            }
187
            if let Ok(syspaths) = std::env::var("PATH") {
36✔
188
                #[cfg(windows)]
189
                let sep = ";";
190
                #[cfg(not(windows))]
191
                let sep = ":";
×
192
                paths.extend(
×
193
                    syspaths
×
194
                        .split(sep)
195
                        .map(|x| x.to_string())
×
196
                        .collect::<Vec<String>>(),
197
                );
198
            }
199
            utils::find_program(&args.remove(0), &paths).await?
36✔
200
        };
36✔
201
        let env = env::mix(env);
36✔
202
        let mut env = env::mix_runtime(&env, &installations, &conn)?;
36✔
203

36✔
204
        let re = Regex::new(r"^\$\{\w+:-([^}]+)\}$").unwrap();
36✔
205

206
        #[cfg(unix)]
207
        let sep = ":";
×
208
        #[cfg(windows)]
36✔
209
        let sep = ";";
36✔
210

36✔
211
        for (key, value) in env.clone() {
36✔
212
            if let Some(caps) = re.captures(&value) {
36✔
213
                env.insert(key, caps.get(1).unwrap().as_str().to_string());
×
214
            } else {
215
                let cleaned_value = value
×
216
                    .replace(&format!("{}${}", sep, key), "")
×
217
                    .replace(&format!("${}{}", key, sep), "")
×
218
                    .replace(&format!("; ${}", key), "") // one pantry instance of this
×
219
                    .replace(&format!("${}", key), "");
×
220
                env.insert(key, cleaned_value);
×
221
            }
222
        }
36✔
223

36✔
224
        // fork bomb protection
36✔
225
        env.insert(
36✔
226
            construct_platform_case_aware_env_key("PKGX_LVL".to_string()),
×
227
            pkgx_lvl.to_string(),
×
228
        );
229

230
        clear_progress_bar();
×
231

232
        if flags.shebang {
×
233
            // removes the filename of the shebang script
234
            args.remove(0);
×
235
        }
236

36✔
237
        execve(cmd, args, env)
36✔
238
    } else if !plus.is_empty() {
36✔
239
        clear_progress_bar();
36✔
240

22✔
241
        if !flags.json {
22✔
242
            let env = env
36✔
243
                .iter()
16✔
244
                .map(|(k, v)| {
117✔
245
                    (
117✔
246
                        construct_platform_case_aware_env_key(k.clone()),
117✔
247
                        v.join(":"),
117✔
248
                    )
117✔
249
                })
117✔
250
                .collect();
16✔
251
            let env = env::mix_runtime(&env, &installations, &conn)?;
36✔
252
            for (key, value) in env {
165✔
253
                println!(
149✔
254
                    "{}=\"{}\"",
149✔
255
                    key,
149✔
256
                    value.replace(&format!(":${}", key), &format!("${{{}:+:${}}}", key, key))
149✔
257
                );
149✔
258
            }
149✔
259
        } else {
36✔
260
            let mut runtime_env = HashMap::new();
36✔
261
            for pkg in installations.clone() {
60✔
262
                let pkg_runtime_env = pantry_db::runtime_env_for_project(&pkg.pkg.project, &conn)?;
60✔
263
                if !pkg_runtime_env.is_empty() {
60✔
264
                    runtime_env.insert(pkg.pkg.project, pkg_runtime_env);
18✔
265
                }
42✔
266
            }
36✔
267
            let json = json!({
36✔
268
                "pkgs": installations,
6✔
269
                "env": env,
6✔
270
                "runtime_env": runtime_env
6✔
271
            });
6✔
272
            println!("{}", json);
6✔
273
        }
36✔
274
        Ok(())
36✔
275
    } else if !flags.version_n_continue {
36✔
276
        clear_progress_bar();
36✔
277
        eprintln!("{}", help::usage());
×
278
        std::process::exit(2);
×
279
    } else {
36✔
280
        Ok(())
36✔
281
    }
36✔
282
}
36✔
283

284
#[derive(Debug)]
285
pub enum WhichError {
286
    CmdNotFound(String),
287
    MultipleProjects(String, Vec<String>),
288
    DbError(rusqlite::Error),
289
}
290

291
impl std::fmt::Display for WhichError {
292
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
293
        match self {
×
294
            WhichError::CmdNotFound(cmd) => write!(f, "cmd not found: {}", cmd),
×
295
            WhichError::MultipleProjects(cmd, projects) => {
×
296
                write!(f, "multiple projects found for {}: {:?}", cmd, projects)
×
297
            }
298
            WhichError::DbError(err) => write!(f, "db error: {}", err),
×
299
        }
300
    }
301
}
302

303
impl std::error::Error for WhichError {}
304

305
async fn which(cmd: &String, conn: &Connection, pkgs: &[PackageReq]) -> Result<String, WhichError> {
16✔
306
    let candidates = pantry_db::projects_for_symbol(cmd, conn).map_err(WhichError::DbError)?;
16✔
307
    if candidates.len() == 1 {
16✔
308
        Ok(candidates[0].clone())
14✔
309
    } else if candidates.is_empty() {
2✔
310
        Err(WhichError::CmdNotFound(cmd.clone()))
×
311
    } else {
312
        let selected_pkgs = candidates
2✔
313
            .clone()
2✔
314
            .into_iter()
2✔
315
            .filter(|candidate| {
4✔
316
                pkgs.iter().any(|pkg| {
4✔
317
                    let PackageReq { project, .. } = pkg;
×
318
                    project == candidate
×
319
                })
4✔
320
            })
4✔
321
            .collect::<Vec<String>>();
2✔
322
        if selected_pkgs.len() == 1 {
2✔
323
            Ok(selected_pkgs[0].clone())
×
324
        } else {
325
            Err(WhichError::MultipleProjects(cmd.clone(), candidates))
2✔
326
        }
327
    }
328
}
16✔
329

330
struct MultiProgressBar {
331
    pb: ProgressBar,
332
}
333

334
impl libpkgx::install_multi::ProgressBarExt for MultiProgressBar {
335
    fn inc(&self, n: u64) {
15,844✔
336
        self.pb.inc(n);
15,844✔
337
    }
15,844✔
338

339
    fn inc_length(&self, n: u64) {
24✔
340
        self.pb.inc_length(n);
24✔
341
    }
24✔
342
}
343

344
// ProgressBar is Send + Sync
345
unsafe impl Send for MultiProgressBar {}
346
unsafe impl Sync for MultiProgressBar {}
347

348
fn configure_bar(pb: &ProgressBar) {
6✔
349
    pb.set_length(1);
6✔
350
    pb.set_style(
6✔
351
        ProgressStyle::with_template(
6✔
352
            "{elapsed:.dim} ❲{wide_bar:.red}❳ {percent}% {bytes_per_sec:.dim} {bytes:.dim}",
6✔
353
        )
6✔
354
        .unwrap()
6✔
355
        .with_key("elapsed", |state: &ProgressState, w: &mut dyn Write| {
6✔
356
            let s = state.elapsed().as_secs_f64();
×
357
            let precision = precision(s);
×
358
            write!(w, "{:.precision$}s", s, precision = precision).unwrap()
×
359
        })
6✔
360
        .with_key("bytes", |state: &ProgressState, w: &mut dyn Write| {
6✔
361
            let (right, divisor) = pretty_size(state.len().unwrap());
×
362
            let left = state.pos() as f64 / divisor as f64;
×
363
            let leftprecision = precision(left);
×
364
            write!(
×
365
                w,
366
                "{:.precision$}/{}",
367
                left,
368
                right,
369
                precision = leftprecision
370
            )
371
            .unwrap()
372
        })
6✔
373
        .progress_chars("⚯ "),
6✔
374
    );
6✔
375
    pb.enable_steady_tick(Duration::from_millis(50));
6✔
376
}
6✔
377

378
fn pretty_size(n: u64) -> (String, u64) {
2,147,483,661✔
379
    let units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
2,147,483,661✔
380

381
    // number of 1024s
382
    let thousands = n.max(1).ilog(1024).clamp(0, units.len() as u32 - 1) as usize;
2,147,483,661✔
383
    // size in the appropriate unit
384
    let size = n as f64 / 1024.0f64.powi(thousands as i32);
2,147,483,661✔
385
    // the divisor to get back to bytes
386
    let divisor = 1024u64.pow(thousands as u32);
2,147,483,661✔
387
    // number of decimal places to show (0 if we're bytes. no fractional bytes. come on.)
388
    let precision = if thousands == 0 { 0 } else { precision(size) };
2,147,483,687✔
389

390
    let formatted = format!(
2,147,483,662✔
391
        "{:.precision$} {}",
392
        size,
2,147,483,660✔
393
        units[thousands],
2,147,483,661✔
394
        precision = precision
2,147,483,660✔
395
    );
396

397
    (formatted, divisor)
2,147,483,661✔
398
}
399

400
fn precision(n: f64) -> usize {
2,147,483,668✔
401
    // 1 > 1.00, 10 > 10.0, 100 > 100
402
    2 - (n.log10().clamp(0.0, 2.0) as usize)
2,147,483,668✔
403
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc