1 #![warn(clippy::pedantic)]
3 use crate::cacache::{Cache, Key};
4 use anyhow::{anyhow, bail};
6 use serde_json::{Map, Value};
10 path::{Path, PathBuf},
13 use tempfile::tempdir;
21 fn cache_map_path() -> Option<PathBuf> {
22 env::var_os("CACHE_MAP_PATH").map(PathBuf::from)
25 /// `fixup_lockfile` rewrites `integrity` hashes to match cache and removes the `integrity` field from Git dependencies.
27 /// Sometimes npm has multiple instances of a given `resolved` URL that have different types of `integrity` hashes (e.g. SHA-1
28 /// and SHA-512) in the lockfile. Given we only cache one version of these, the `integrity` field must be normalized to the hash
29 /// we cache as (which is the strongest available one).
31 /// Git dependencies from specific providers can be retrieved from those providers' automatic tarball features.
32 /// When these dependencies are specified with a commit identifier, npm generates a tarball, and inserts the integrity hash of that
33 /// tarball into the lockfile.
35 /// Thus, we remove this hash, to replace it with our own determinstic copies of dependencies from hosted Git providers.
37 /// If no fixups were performed, `None` is returned and the lockfile structure should be left as-is. If fixups were performed, the
38 /// `dependencies` key in v2 lockfiles designed for backwards compatibility with v1 parsers is removed because of inconsistent data.
40 mut lock: Map<String, Value>,
41 cache: &Option<HashMap<String, String>>,
42 ) -> anyhow::Result<Option<Map<String, Value>>> {
43 let mut fixed = false;
46 .get("lockfileVersion")
47 .ok_or_else(|| anyhow!("couldn't get lockfile version"))?
49 .ok_or_else(|| anyhow!("lockfile version isn't an int"))?
52 lock.get_mut("dependencies")
62 .ok_or_else(|| anyhow!("couldn't get packages"))?
64 .ok_or_else(|| anyhow!("packages isn't a map"))?
67 if let Some(Value::String(resolved)) = package.get("resolved") {
68 if let Some(Value::String(integrity)) = package.get("integrity") {
69 if resolved.starts_with("git+ssh://") {
74 .ok_or_else(|| anyhow!("package isn't a map"))?
76 } else if let Some(cache_hashes) = cache {
77 let cache_hash = cache_hashes
79 .expect("dependency should have a hash");
81 if integrity != cache_hash {
86 .ok_or_else(|| anyhow!("package isn't a map"))?
88 .unwrap() = Value::String(cache_hash.clone());
96 lock.remove("dependencies");
99 v => bail!("unsupported lockfile version {v}"),
109 // Recursive helper to fixup v1 lockfile deps
111 dependencies: &mut Map<String, Value>,
112 cache: &Option<HashMap<String, String>>,
115 for dep in dependencies.values_mut() {
116 if let Some(Value::String(resolved)) = dep
118 .expect("v1 dep must be object")
121 if let Some(Value::String(integrity)) = dep
123 .expect("v1 dep must be object")
126 if resolved.starts_with("git+ssh://") {
130 .expect("v1 dep must be object")
131 .remove("integrity");
132 } else if let Some(cache_hashes) = cache {
133 let cache_hash = cache_hashes
135 .expect("dependency should have a hash");
137 if integrity != cache_hash {
141 .expect("v1 dep must be object")
142 .get_mut("integrity")
143 .unwrap() = Value::String(cache_hash.clone());
149 if let Some(Value::Object(more_deps)) = dep.as_object_mut().unwrap().get_mut("dependencies")
151 fixup_v1_deps(more_deps, cache, fixed);
156 fn map_cache() -> anyhow::Result<HashMap<Url, String>> {
157 let mut hashes = HashMap::new();
159 let content_path = Path::new(&env::var_os("npmDeps").unwrap()).join("_cacache/index-v5");
161 for entry in WalkDir::new(content_path) {
164 if entry.file_type().is_file() {
165 let content = fs::read_to_string(entry.path())?;
166 let key: Key = serde_json::from_str(content.split_ascii_whitespace().nth(1).unwrap())?;
168 hashes.insert(key.metadata.url, key.integrity);
175 fn main() -> anyhow::Result<()> {
178 let args = env::args().collect::<Vec<_>>();
181 println!("usage: {} <path/to/package-lock.json>", args[0]);
183 println!("Prefetches npm dependencies for usage by fetchNpmDeps.");
188 if let Ok(jobs) = env::var("NIX_BUILD_CORES") {
189 if !jobs.is_empty() {
190 rayon::ThreadPoolBuilder::new()
193 .expect("NIX_BUILD_CORES must be a whole number"),
200 if args[1] == "--fixup-lockfile" {
201 let lock = serde_json::from_str(&fs::read_to_string(&args[2])?)?;
203 let cache = cache_map_path()
204 .map(|map_path| Ok::<_, anyhow::Error>(serde_json::from_slice(&fs::read(map_path)?)?))
207 if let Some(fixed) = fixup_lockfile(lock, &cache)? {
208 println!("Fixing lockfile");
210 fs::write(&args[2], serde_json::to_string(&fixed)?)?;
214 } else if args[1] == "--map-cache" {
215 let map = map_cache()?;
218 cache_map_path().expect("CACHE_MAP_PATH environment variable must be set"),
219 serde_json::to_string(&map)?,
225 let lock_content = fs::read_to_string(&args[1])?;
229 let (out, print_hash) = if let Some(path) = args.get(2) {
230 (Path::new(path), false)
232 out_tempdir = tempdir()?;
234 (out_tempdir.path(), true)
237 let packages = parse::lockfile(
239 env::var("FORCE_GIT_DEPS").is_ok(),
240 env::var("FORCE_EMPTY_CACHE").is_ok(),
243 let cache = Cache::new(out.join("_cacache"));
246 packages.into_par_iter().try_for_each(|package| {
247 let tarball = package
249 .map_err(|e| anyhow!("couldn't fetch {} at {}: {e:?}", package.name, package.url))?;
250 let integrity = package.integrity().map(ToString::to_string);
254 format!("make-fetch-happen:request-cache:{}", package.url),
259 .map_err(|e| anyhow!("couldn't insert cache entry for {}: {e:?}", package.name))?;
261 Ok::<_, anyhow::Error>(())
264 fs::write(out.join("package-lock.json"), lock_content)?;
267 println!("{}", util::make_sri_hash(out)?);
275 use std::collections::HashMap;
277 use super::fixup_lockfile;
278 use serde_json::json;
281 fn lockfile_fixup() -> anyhow::Result<()> {
283 "lockfileVersion": 2,
290 "resolved": "https://github.com/NixOS/nixpkgs",
291 "integrity": "sha1-aaa"
294 "resolved": "git+ssh://git@github.com/NixOS/nixpkgs.git",
295 "integrity": "sha512-aaa"
299 "integrity": "sha1-foo"
303 "integrity": "sha512-foo"
308 let expected = json!({
309 "lockfileVersion": 2,
316 "resolved": "https://github.com/NixOS/nixpkgs",
320 "resolved": "git+ssh://git@github.com/NixOS/nixpkgs.git",
324 "integrity": "sha512-foo"
328 "integrity": "sha512-foo"
333 let mut hashes = HashMap::new();
336 String::from("https://github.com/NixOS/nixpkgs"),
341 String::from("git+ssh://git@github.com/NixOS/nixpkgs.git"),
345 hashes.insert(String::from("foo"), String::from("sha512-foo"));
348 fixup_lockfile(input.as_object().unwrap().clone(), &Some(hashes))?,
349 Some(expected.as_object().unwrap().clone())
356 fn lockfile_v1_fixup() -> anyhow::Result<()> {
358 "lockfileVersion": 1,
362 "resolved": "https://github.com/NixOS/nixpkgs",
363 "integrity": "sha512-aaa"
367 "integrity": "sha512-foo"
370 "resolved": "git+ssh://git@github.com/NixOS/nixpkgs.git",
371 "integrity": "sha512-bbb",
375 "integrity": "sha1-foo"
382 let expected = json!({
383 "lockfileVersion": 1,
387 "resolved": "https://github.com/NixOS/nixpkgs",
392 "integrity": "sha512-foo"
395 "resolved": "git+ssh://git@github.com/NixOS/nixpkgs.git",
399 "integrity": "sha512-foo"
406 let mut hashes = HashMap::new();
409 String::from("https://github.com/NixOS/nixpkgs"),
414 String::from("git+ssh://git@github.com/NixOS/nixpkgs.git"),
418 hashes.insert(String::from("foo"), String::from("sha512-foo"));
421 fixup_lockfile(input.as_object().unwrap().clone(), &Some(hashes))?,
422 Some(expected.as_object().unwrap().clone())