Skip to content

Commit be03178

Browse files
authored
Add --offline, --locked, --frozen (#261)
1 parent 393e1e2 commit be03178

File tree

8 files changed

+127
-38
lines changed

8 files changed

+127
-38
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
<!-- next-header -->
1010
## [Unreleased] - ReleaseDate
11+
### Added
12+
- [PR#261](https://github.com/EmbarkStudios/cargo-about/pull/261) resolved [#246](https://github.com/EmbarkStudios/cargo-about/issues/246) by adding an `--offline` (as well as `--locked` and `--frozen`) option to the `generate` command.
13+
1114
## [0.6.4] - 2024-08-12
1215
### Fixed
1316
- [PR#254](https://github.com/EmbarkStudios/cargo-about/pull/254) reverted unintended `id` -> `short_id` field rename.

docs/src/cli/generate/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,25 @@ Disables the `default` feature for a crate when determining which crates to cons
1616

1717
Scan licenses for the entire workspace, not just the active package.
1818

19+
### [`--locked`](https://doc.rust-lang.org/cargo/commands/cargo-fetch.html#option-cargo-fetch---locked)
20+
21+
Asserts that the exact same dependencies and versions are used as when the existing Cargo.lock file was originally generated. Cargo will exit with an error when either of the following scenarios arises:
22+
23+
* The lock file is missing.
24+
* Cargo attempted to change the lock file due to a different dependency resolution.
25+
26+
### [`--offline`](https://doc.rust-lang.org/cargo/commands/cargo-fetch.html#option-cargo-fetch---offline)
27+
28+
Prevents Cargo and `cargo-about` from accessing the network for any reason. Without this flag, Cargo will stop with an error if it needs to access the network and the network is not available. With this flag, Cargo will attempt to proceed without the network if possible.
29+
30+
Beware that this may result in different dependency resolution than online mode. Cargo will restrict itself to crates that are downloaded locally, even if there might be a newer version as indicated in the local copy of the index. See the cargo-fetch(1) command to download dependencies before going offline.
31+
32+
`cargo-about` will also not query clearlydefined.io for license information, meaning that user provided clarifications won't be used, and some ambiguous/complicated license files might be missed by `cargo-about`. Additionally, clarifications that use license files from the crate's source repository will not be applied, meaning that `cargo-about` will fallback to using the default license text rather than the one in the source repository, losing eg. copyright or other unique information.
33+
34+
### [`--frozen`](https://doc.rust-lang.org/cargo/commands/cargo-fetch.html#option-cargo-fetch---frozen)
35+
36+
Equivalent to specifying both `--locked` and `--offline`.
37+
1938
### `--fail`
2039

2140
Exits with a non-zero exit code if any crate's license cannot be reasonably determined

src/cargo-about/clarify.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ pub fn cmd(args: Args) -> anyhow::Result<()> {
6161
.with_context(|| format!("unable to read file '{full_path}'"))?
6262
}
6363
Subcommand::Repo { rev, repo } => {
64-
let gc = GitCache::default();
64+
let gc = GitCache::online();
6565

6666
gc.retrieve_remote(repo.as_str(), &rev, &args.path)
6767
.context("failed to retrieve remote file")?
@@ -95,7 +95,7 @@ pub fn cmd(args: Args) -> anyhow::Result<()> {
9595
let pkg: MinPkg =
9696
toml::from_str(&manifest).context("failed to deserialize Cargo.toml")?;
9797

98-
let gc = GitCache::default();
98+
let gc = GitCache::online();
9999
let vcs_info = GitCache::parse_vcs_info(&crate_path.join(".cargo_vcs_info.json"))
100100
.context("failed to read sha1")?;
101101

@@ -216,9 +216,7 @@ pub fn cmd(args: Args) -> anyhow::Result<()> {
216216

217217
let overall_expression = spdx::Expression::parse(&final_expression).map_err(|e| {
218218
anyhow::anyhow!(
219-
"failed to parse '{}' as the total expression for all of the licenses: {}",
220-
final_expression,
221-
e,
219+
"failed to parse '{final_expression}' as the total expression for all of the licenses: {e}",
222220
)
223221
})?;
224222

src/cargo-about/generate.rs

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ pub struct Args {
3232
/// Defaults to `<manifest_root>/about.toml` if not specified
3333
#[clap(short, long)]
3434
config: Option<PathBuf>,
35-
/// The confidence threshold required for license files
36-
/// to be positively identified: 0.0 - 1.0
35+
/// The confidence threshold required for license files to be positively identified: 0.0 - 1.0
3736
#[clap(long, default_value = "0.8")]
3837
threshold: f32,
39-
/// The name of the template to use when rendering. If only passing a
40-
/// single template file to `templates` this is not used.
38+
/// The name of the template to use when rendering.
39+
///
40+
/// If only passing a single template file to `templates` this is not used.
4141
#[clap(short, long)]
4242
name: Option<String>,
43-
/// A file to write the generated output to. Typically an .html file.
43+
/// A file to write the generated output to, typically an .html file.
4444
#[clap(short, long)]
4545
output_file: Option<PathBuf>,
4646
/// Space-separated list of features to activate
@@ -52,8 +52,27 @@ pub struct Args {
5252
/// Do not activate the `default` feature
5353
#[clap(long)]
5454
no_default_features: bool,
55-
/// The path of the Cargo.toml for the root crate, defaults to the
56-
/// current crate or workspace in the current working directory
55+
/// Run without accessing the network.
56+
///
57+
/// In addition to cargo not fetching crates, this will mean that only
58+
/// local files will be crawled for license information.
59+
/// 1. clearlydefined.io will not be used, so some more ambiguous/complicated
60+
/// license files might be ignored
61+
/// 2. Crates that are improperly packaged and don't contain their LICENSE
62+
/// file(s) will fallback to the default license file, missing eg.
63+
/// copyright information in the license that would be retrieved from
64+
/// the original git repo for the crate in question
65+
#[arg(long)]
66+
pub(crate) offline: bool,
67+
/// Assert that `Cargo.lock` will remain unchanged
68+
#[arg(long)]
69+
pub(crate) locked: bool,
70+
/// Equivalent to specifying both `--locked` and `--offline`
71+
#[arg(long)]
72+
pub(crate) frozen: bool,
73+
/// The path of the Cargo.toml for the root crate.
74+
///
75+
/// Defaults to the current crate or workspace in the current working directory
5776
#[clap(short, long)]
5877
manifest_path: Option<PathBuf>,
5978
/// Scan licenses for the entire workspace, not just the active package
@@ -66,8 +85,11 @@ pub struct Args {
6685
/// The format of the output, defaults to `handlebars`.
6786
#[clap(long, default_value_t)]
6887
format: OutputFormat,
69-
/// The template(s) or template directory to use. Must either be a `.hbs`
70-
/// file, or have at least one `.hbs` file in it if it is a directory.
88+
/// The template(s) or template directory to use.
89+
///
90+
/// Must either be a `.hbs` file, or have at least one `.hbs` file in it if
91+
/// it is a directory.
92+
///
7193
/// Required if `--format` is not `json`
7294
templates: Option<PathBuf>,
7395
}
@@ -157,6 +179,11 @@ pub fn cmd(args: Args, color: crate::Color) -> anyhow::Result<()> {
157179
args.all_features,
158180
args.features.clone(),
159181
args.workspace,
182+
krates::LockOptions {
183+
frozen: args.frozen,
184+
locked: args.locked,
185+
offline: args.offline,
186+
},
160187
&cfg,
161188
));
162189
});
@@ -222,15 +249,16 @@ pub fn cmd(args: Args, color: crate::Color) -> anyhow::Result<()> {
222249

223250
log::info!("gathered {} crates", krates.len());
224251

225-
let client = reqwest::blocking::ClientBuilder::new()
226-
.timeout(std::time::Duration::from_secs(
227-
cfg.clearly_defined_timeout_secs.unwrap_or(30),
228-
))
229-
.build()?;
230-
let summary = licenses::Gatherer::with_store(std::sync::Arc::new(store), client.into())
252+
let client = if !args.offline && !args.frozen {
253+
Some(reqwest::blocking::ClientBuilder::new().build()?)
254+
} else {
255+
None
256+
};
257+
258+
let summary = licenses::Gatherer::with_store(std::sync::Arc::new(store))
231259
.with_confidence_threshold(args.threshold)
232260
.with_max_depth(cfg.max_depth.map(|md| md as _))
233-
.gather(&krates, &cfg);
261+
.gather(&krates, &cfg, client);
234262

235263
let (files, resolved) =
236264
licenses::resolution::resolve(&summary, &cfg.accepted, &cfg.crates, args.fail);

src/lib.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,12 @@ pub fn get_all_crates(
100100
all_features: bool,
101101
features: Vec<String>,
102102
workspace: bool,
103+
lock_opts: krates::LockOptions,
103104
cfg: &licenses::config::Config,
104105
) -> anyhow::Result<Krates> {
105106
let mut mdc = krates::Cmd::new();
106107
mdc.manifest_path(cargo_toml);
108+
mdc.lock_opts(lock_opts);
107109

108110
// The metadata command builder is weird and only allows you to specify
109111
// one of these, but really you might need to do multiple of them
@@ -170,8 +172,7 @@ pub fn to_hex(bytes: &[u8]) -> String {
170172
pub fn validate_sha256(buffer: &str, expected: &str) -> anyhow::Result<()> {
171173
anyhow::ensure!(
172174
expected.len() == 64,
173-
"checksum '{}' length is {} instead of expected 64",
174-
expected,
175+
"checksum '{expected}' length is {} instead of expected 64",
175176
expected.len()
176177
);
177178

@@ -193,7 +194,7 @@ pub fn validate_sha256(buffer: &str, expected: &str) -> anyhow::Result<()> {
193194
b'a'..=b'f' => exp[0] - b'a' + 10,
194195
b'0'..=b'9' => exp[0] - b'0',
195196
c => {
196-
anyhow::bail!("invalid byte in checksum '{}' @ {}: {}", expected, ind, c);
197+
anyhow::bail!("invalid byte in checksum '{expected}' @ {ind}: {c}");
197198
}
198199
};
199200

@@ -204,12 +205,12 @@ pub fn validate_sha256(buffer: &str, expected: &str) -> anyhow::Result<()> {
204205
b'a'..=b'f' => exp[1] - b'a' + 10,
205206
b'0'..=b'9' => exp[1] - b'0',
206207
c => {
207-
anyhow::bail!("invalid byte in checksum '{}' @ {}: {}", expected, ind, c);
208+
anyhow::bail!("invalid byte in checksum '{expected}' @ {ind}: {c}");
208209
}
209210
};
210211

211212
if digest[ind] != cur {
212-
anyhow::bail!("checksum mismatch, expected {}", expected);
213+
anyhow::bail!("checksum mismatch, expected '{expected}'");
213214
}
214215
}
215216

src/licenses.rs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,14 @@ impl<'krate> Eq for KrateLicense<'krate> {}
122122

123123
pub struct Gatherer {
124124
store: Arc<LicenseStore>,
125-
cd_client: cd::client::Client,
126125
threshold: f32,
127126
max_depth: Option<usize>,
128127
}
129128

130129
impl Gatherer {
131-
pub fn with_store(store: Arc<LicenseStore>, client: cd::client::Client) -> Self {
130+
pub fn with_store(store: Arc<LicenseStore>) -> Self {
132131
Self {
133132
store,
134-
cd_client: client,
135133
threshold: 0.8,
136134
max_depth: None,
137135
}
@@ -151,6 +149,7 @@ impl Gatherer {
151149
self,
152150
krates: &'krate Krates,
153151
cfg: &config::Config,
152+
client: Option<reqwest::blocking::Client>,
154153
) -> Vec<KrateLicense<'krate>> {
155154
let mut licensed_krates = Vec::with_capacity(krates.len());
156155

@@ -167,7 +166,8 @@ impl Gatherer {
167166
.optimize(false)
168167
.max_passes(1);
169168

170-
let git_cache = fetch::GitCache::default();
169+
let is_offline = client.is_none();
170+
let git_cache = fetch::GitCache::maybe_offline(client);
171171

172172
// If we're ignoring crates that are private, just add them
173173
// to the list so all of the following gathers ignore them
@@ -204,7 +204,27 @@ impl Gatherer {
204204
// can get previously gathered license information + any possible
205205
// curations so that we only need to fallback to scanning local crate
206206
// sources if it's not already in clearly-defined
207-
self.gather_clearly_defined(krates, cfg, &strategy, &mut licensed_krates);
207+
if !is_offline && !cfg.no_clearly_defined {
208+
match reqwest::blocking::ClientBuilder::new()
209+
.timeout(std::time::Duration::from_secs(
210+
cfg.clearly_defined_timeout_secs.unwrap_or(30),
211+
))
212+
.build()
213+
{
214+
Ok(client) => {
215+
self.gather_clearly_defined(
216+
krates,
217+
cfg,
218+
client.into(),
219+
&strategy,
220+
&mut licensed_krates,
221+
);
222+
}
223+
Err(err) => {
224+
log::error!("failed to build clearlydefined.io HTTP client: {err:#}");
225+
}
226+
}
227+
}
208228

209229
// Finally, crawl the crate sources on disk to try and determine licenses
210230
self.gather_file_system(krates, &strategy, &mut licensed_krates);
@@ -244,7 +264,7 @@ impl Gatherer {
244264
);
245265
}
246266
Err(e) => {
247-
log::warn!("failed to validate all files specified in clarification for crate {krate}: {e}");
267+
log::warn!("failed to validate all files specified in clarification for crate {krate}: {e:#}");
248268
}
249269
}
250270
}
@@ -255,6 +275,7 @@ impl Gatherer {
255275
&self,
256276
krates: &'k Krates,
257277
cfg: &config::Config,
278+
client: cd::client::Client,
258279
strategy: &askalono::ScanStrategy<'_>,
259280
licensed_krates: &mut Vec<KrateLicense<'k>>,
260281
) {
@@ -292,7 +313,7 @@ impl Gatherer {
292313
);
293314

294315
let collected: Vec<_> = reqs.par_bridge().filter_map(|req| {
295-
match self.cd_client.execute::<cd::definitions::GetResponse>(req) {
316+
match client.execute::<cd::definitions::GetResponse>(req) {
296317
Ok(response) => {
297318
Some(response.definitions.into_iter().filter_map(|def| {
298319
if def.described.is_none() {
@@ -345,7 +366,7 @@ impl Gatherer {
345366
Some(text)
346367
}
347368
Err(err) => {
348-
log::warn!("failed to read license from '{}' for crate '{}': {}", path, krate, err);
369+
log::warn!("failed to read license from '{path}' for crate '{krate}': {err}");
349370
return None;
350371
}
351372
}

src/licenses/fetch.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,27 @@ pub struct VcsInfo {
9898
/// but not in the actual published package is due to it being in the root but
9999
/// not copied into each sub-crate in the repository, we can just not re-retrieve
100100
/// the same file multiple times
101-
#[derive(Clone, Default)]
101+
#[derive(Clone)]
102102
pub struct GitCache {
103103
cache: Arc<parking_lot::RwLock<std::collections::HashMap<u64, Arc<String>>>>,
104-
http_client: Client,
104+
http_client: Option<Client>,
105105
}
106106

107107
impl GitCache {
108+
pub fn maybe_offline(http_client: Option<Client>) -> Self {
109+
Self {
110+
http_client,
111+
cache: Default::default(),
112+
}
113+
}
114+
115+
pub fn online() -> Self {
116+
Self {
117+
http_client: Some(Client::new()),
118+
cache: Default::default(),
119+
}
120+
}
121+
108122
#[allow(clippy::unused_self)]
109123
fn retrieve_local(
110124
&self,
@@ -159,6 +173,11 @@ impl GitCache {
159173
let repo_url = url::Url::parse(repo)
160174
.with_context(|| format!("unable to parse repository url '{repo}'"))?;
161175

176+
let http_client = self
177+
.http_client
178+
.as_ref()
179+
.context("unable to fetch remote repository data in offline mode")?;
180+
162181
// Unfortunately the HTTP retrieval methods for most of the popular
163182
// providers require an API token to use, so instead we just use a
164183
// third party CDN, `raw.githack.com` for now until I can find a better
@@ -169,7 +188,7 @@ impl GitCache {
169188
let flavor = GitHostFlavor::from_repo(&repo_url)?;
170189

171190
flavor
172-
.fetch(&self.http_client, &repo_url, rev, path)
191+
.fetch(http_client, &repo_url, rev, path)
173192
.with_context(|| format!("failed to fetch contents of '{path}' from repo '{repo}'"))
174193
}
175194

src/licenses/resolution.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ pub fn resolve(
139139
if fail_on_missing {
140140
resolved.diagnostics.push(Diagnostic::new(Severity::Error).with_message(msg));
141141
} else {
142-
log::warn!("{}", msg);
142+
log::warn!("{msg}");
143143
}
144144

145145
return Some(resolved);

0 commit comments

Comments
 (0)