cargo_search2/
lib.rs

1// Copyright (c) The cargo-search2 Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! A version of `cargo search`, suitable for CI caching in GitHub Actions and elsewhere.
5//!
6//! Provides output in JSON and GitHub Actions formats.
7//!
8//! For more, see the [crates.io page](https://crates.io/cargo-search2).
9
10use blake2b_simd::Params;
11use color_eyre::{
12    eyre::{bail, eyre, WrapErr},
13    Result,
14};
15use crates_index::BareIndex;
16use semver::{Version, VersionReq};
17use serde::Serialize;
18use std::path::PathBuf;
19use structopt::{clap::arg_enum, StructOpt};
20
21static DEFAULT_INDEX_URL: &str = "https://github.com/rust-lang/crates.io-index";
22
23/// Search packages on crates.io by version.
24///
25/// This is a version of `cargo search` that uses exact names rather than searching on prefixes. It
26/// supports matching by semver ranges, hashing version numbers, and a number of output formats
27/// including JSON and GitHub Actions.
28///
29/// `cargo search2` is optimized for cache keys and lookups on GitHub Actions and other CI
30/// platforms.
31#[derive(Debug, StructOpt)]
32#[doc(hidden)]
33pub struct Args {
34    /// The index to use (default: same as Cargo's).
35    #[structopt(long)]
36    index_path: Option<PathBuf>,
37
38    /// Version specification to match
39    #[structopt(long = "req", default_value)]
40    version_req: VersionReq,
41
42    /// Bump this up to invalidate cache keys
43    #[structopt(long, short = "c", default_value)]
44    cache_version: u64,
45
46    /// Output format
47    #[structopt(long, possible_values = &MessageFormat::variants(), case_insensitive = true, default_value = "plain")]
48    message_format: MessageFormat,
49
50    /// The name of the package to look for.
51    crate_name: String,
52}
53
54const BLAKE2B_PREFIX: &str = "blake2b24:";
55const BLAKE2B_HASH_LEN: usize = 24;
56const MAX_VERSIONS_IN_ERR: usize = 8;
57
58arg_enum! {
59    #[derive(Copy, Clone, Debug)]
60    #[allow(non_camel_case_types)]
61    enum MessageFormat {
62        toml,
63        plain,
64        json,
65        github,
66    }
67}
68
69impl Args {
70    /// Execute this and return
71    pub fn exec(self) -> Result<()> {
72        let index = match &self.index_path {
73            Some(path) => BareIndex::with_path(path.clone(), DEFAULT_INDEX_URL),
74            None => BareIndex::new_cargo_default(),
75        };
76
77        let mut repo = index.open_or_clone().wrap_err_with(|| {
78            format!(
79                "opening or cloning index at {} failed",
80                index.path().display()
81            )
82        })?;
83
84        repo.retrieve()
85            .wrap_err_with(|| format!("updating index at {} failed", index.path().display()))?;
86
87        let crate_ = repo
88            .crate_(&self.crate_name)
89            .ok_or_else(|| eyre!("crate {} not found", &self.crate_name))?;
90        let mut matching_versions: Vec<Version> = crate_
91            .versions()
92            .iter()
93            .filter_map(|version| {
94                let version = match version.version().parse::<Version>() {
95                    Ok(version) => version,
96                    Err(_) => {
97                        // Weird but ok, skip over this
98                        return None;
99                    }
100                };
101
102                if self.version_req.matches(&version) {
103                    Some(version)
104                } else {
105                    None
106                }
107            })
108            .collect();
109
110        matching_versions.sort_unstable();
111        let output = match matching_versions.last() {
112            Some(found_version) => {
113                let mut params = Params::new();
114                let mut state = params.hash_length(BLAKE2B_HASH_LEN).to_state();
115
116                state.update(crate_.name().as_bytes());
117                state.update(b"\0");
118                state.update(found_version.to_string().as_bytes());
119                state.update(b"\0");
120                state.update(&self.cache_version.to_be_bytes());
121                let blake2b_hash = state.finalize();
122                let blake2b_hex = blake2b_hash.to_hex();
123
124                // Currently support blake2b only.
125
126                let hash_str = BLAKE2B_PREFIX.to_owned() + blake2b_hex.as_str();
127                Output {
128                    crate_name: crate_.name().to_owned(),
129                    version: found_version.clone(),
130                    hash: hash_str,
131                }
132            }
133            None => {
134                let versions = crate_.versions();
135                let latest_versions: Vec<_> = crate_
136                    .versions()
137                    .iter()
138                    .rev()
139                    .take(8)
140                    .map(|v| v.version())
141                    .collect();
142                let versions_found_str = latest_versions.join(", ");
143
144                let and_more = if versions.len() > MAX_VERSIONS_IN_ERR {
145                    format!(" and {} more", versions.len() - MAX_VERSIONS_IN_ERR)
146                } else {
147                    String::new()
148                };
149                bail!(
150                    "for crate {}, no matching versions for req {} (versions found: {}{})",
151                    crate_.name(),
152                    self.version_req,
153                    versions_found_str,
154                    and_more,
155                );
156            }
157        };
158
159        match self.message_format {
160            MessageFormat::plain => {
161                println!(
162                    "{} {} (hash: {})",
163                    output.crate_name, output.version, output.hash
164                );
165            }
166            MessageFormat::toml => {
167                println!(r#"{} = "{}""#, output.crate_name, output.version);
168            }
169            MessageFormat::json => {
170                let json = serde_json::to_string(&output).wrap_err_with(|| {
171                    format!("couldn't serialize serde output for {:?}", output)
172                })?;
173                println!("{}", json);
174            }
175            MessageFormat::github => {
176                println!("::set-output name=crate-name::{}", output.crate_name);
177                println!("::set-output name=version::{}", output.version);
178                println!("::set-output name=hash::{}", output.hash);
179            }
180        }
181
182        Ok(())
183    }
184}
185
186#[derive(Debug, Serialize)]
187#[serde(rename_all = "kebab-case")]
188struct Output {
189    crate_name: String,
190    version: Version,
191    hash: String,
192}