Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
at python-updates 427 lines 13 kB view raw
1#![warn(clippy::pedantic)] 2 3use crate::cacache::{Cache, Key}; 4use anyhow::{anyhow, bail}; 5use rayon::prelude::*; 6use serde_json::{Map, Value}; 7use std::{ 8 collections::HashMap, 9 env, fs, 10 path::{Path, PathBuf}, 11 process, 12}; 13use tempfile::tempdir; 14use url::Url; 15use walkdir::WalkDir; 16 17mod cacache; 18mod parse; 19mod util; 20 21fn cache_map_path() -> Option<PathBuf> { 22 env::var_os("CACHE_MAP_PATH").map(PathBuf::from) 23} 24 25/// `fixup_lockfile` rewrites `integrity` hashes to match cache and removes the `integrity` field from Git dependencies. 26/// 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). 30/// 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. 34/// 35/// Thus, we remove this hash, to replace it with our own determinstic copies of dependencies from hosted Git providers. 36/// 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. 39fn fixup_lockfile( 40 mut lock: Map<String, Value>, 41 cache: &Option<HashMap<String, String>>, 42) -> anyhow::Result<Option<Map<String, Value>>> { 43 let mut fixed = false; 44 45 match lock 46 .get("lockfileVersion") 47 .ok_or_else(|| anyhow!("couldn't get lockfile version"))? 48 .as_i64() 49 .ok_or_else(|| anyhow!("lockfile version isn't an int"))? 50 { 51 1 => fixup_v1_deps( 52 lock.get_mut("dependencies") 53 .unwrap() 54 .as_object_mut() 55 .unwrap(), 56 cache, 57 &mut fixed, 58 ), 59 2 | 3 => { 60 for package in lock 61 .get_mut("packages") 62 .ok_or_else(|| anyhow!("couldn't get packages"))? 63 .as_object_mut() 64 .ok_or_else(|| anyhow!("packages isn't a map"))? 65 .values_mut() 66 { 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+") { 70 fixed = true; 71 72 package 73 .as_object_mut() 74 .ok_or_else(|| anyhow!("package isn't a map"))? 75 .remove("integrity"); 76 } else if let Some(cache_hashes) = cache { 77 let cache_hash = cache_hashes 78 .get(resolved) 79 .expect("dependency should have a hash"); 80 81 if integrity != cache_hash { 82 fixed = true; 83 84 *package 85 .as_object_mut() 86 .ok_or_else(|| anyhow!("package isn't a map"))? 87 .get_mut("integrity") 88 .unwrap() = Value::String(cache_hash.clone()); 89 } 90 } 91 } 92 } 93 } 94 95 if fixed { 96 lock.remove("dependencies"); 97 } 98 } 99 v => bail!("unsupported lockfile version {v}"), 100 } 101 102 if fixed { 103 Ok(Some(lock)) 104 } else { 105 Ok(None) 106 } 107} 108 109// Recursive helper to fixup v1 lockfile deps 110fn fixup_v1_deps( 111 dependencies: &mut Map<String, Value>, 112 cache: &Option<HashMap<String, String>>, 113 fixed: &mut bool, 114) { 115 for dep in dependencies.values_mut() { 116 if let Some(Value::String(resolved)) = dep 117 .as_object() 118 .expect("v1 dep must be object") 119 .get("resolved") 120 { 121 if let Some(Value::String(integrity)) = dep 122 .as_object() 123 .expect("v1 dep must be object") 124 .get("integrity") 125 { 126 if resolved.starts_with("git+ssh://") { 127 *fixed = true; 128 129 dep.as_object_mut() 130 .expect("v1 dep must be object") 131 .remove("integrity"); 132 } else if let Some(cache_hashes) = cache { 133 let cache_hash = cache_hashes 134 .get(resolved) 135 .expect("dependency should have a hash"); 136 137 if integrity != cache_hash { 138 *fixed = true; 139 140 *dep.as_object_mut() 141 .expect("v1 dep must be object") 142 .get_mut("integrity") 143 .unwrap() = Value::String(cache_hash.clone()); 144 } 145 } 146 } 147 } 148 149 if let Some(Value::Object(more_deps)) = dep.as_object_mut().unwrap().get_mut("dependencies") 150 { 151 fixup_v1_deps(more_deps, cache, fixed); 152 } 153 } 154} 155 156fn map_cache() -> anyhow::Result<HashMap<Url, String>> { 157 let mut hashes = HashMap::new(); 158 159 let content_path = Path::new(&env::var_os("npmDeps").unwrap()).join("_cacache/index-v5"); 160 161 for entry in WalkDir::new(content_path) { 162 let entry = entry?; 163 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())?; 167 168 hashes.insert(key.metadata.url, key.integrity); 169 } 170 } 171 172 Ok(hashes) 173} 174 175fn main() -> anyhow::Result<()> { 176 env_logger::init(); 177 178 let args = env::args().collect::<Vec<_>>(); 179 180 if args.len() < 2 { 181 println!("usage: {} <path/to/package-lock.json>", args[0]); 182 println!(); 183 println!("Prefetches npm dependencies for usage by fetchNpmDeps."); 184 185 process::exit(1); 186 } 187 188 if let Ok(jobs) = env::var("NIX_BUILD_CORES") { 189 if !jobs.is_empty() { 190 rayon::ThreadPoolBuilder::new() 191 .num_threads( 192 jobs.parse() 193 .expect("NIX_BUILD_CORES must be a whole number"), 194 ) 195 .build_global() 196 .unwrap(); 197 } 198 } 199 200 if args[1] == "--fixup-lockfile" { 201 let lock = serde_json::from_str(&fs::read_to_string(&args[2])?)?; 202 203 let cache = cache_map_path() 204 .map(|map_path| Ok::<_, anyhow::Error>(serde_json::from_slice(&fs::read(map_path)?)?)) 205 .transpose()?; 206 207 if let Some(fixed) = fixup_lockfile(lock, &cache)? { 208 println!("Fixing lockfile"); 209 210 fs::write(&args[2], serde_json::to_string(&fixed)?)?; 211 } 212 213 return Ok(()); 214 } else if args[1] == "--map-cache" { 215 let map = map_cache()?; 216 217 fs::write( 218 cache_map_path().expect("CACHE_MAP_PATH environment variable must be set"), 219 serde_json::to_string(&map)?, 220 )?; 221 222 return Ok(()); 223 } 224 225 let lock_content = fs::read_to_string(&args[1])?; 226 227 let out_tempdir; 228 229 let (out, print_hash) = if let Some(path) = args.get(2) { 230 (Path::new(path), false) 231 } else { 232 out_tempdir = tempdir()?; 233 234 (out_tempdir.path(), true) 235 }; 236 237 let packages = parse::lockfile( 238 &lock_content, 239 env::var("FORCE_GIT_DEPS").is_ok(), 240 env::var("FORCE_EMPTY_CACHE").is_ok(), 241 )?; 242 243 let cache = Cache::new(out.join("_cacache")); 244 cache.init()?; 245 246 packages.into_par_iter().try_for_each(|package| { 247 let tarball = package 248 .tarball() 249 .map_err(|e| anyhow!("couldn't fetch {} at {}: {e:?}", package.name, package.url))?; 250 let integrity = package.integrity().map(ToString::to_string); 251 252 cache 253 .put( 254 format!("make-fetch-happen:request-cache:{}", package.url), 255 package.url, 256 &tarball, 257 integrity, 258 ) 259 .map_err(|e| anyhow!("couldn't insert cache entry for {}: {e:?}", package.name))?; 260 261 Ok::<_, anyhow::Error>(()) 262 })?; 263 264 fs::write(out.join("package-lock.json"), lock_content)?; 265 266 if print_hash { 267 println!("{}", util::make_sri_hash(out)?); 268 } 269 270 Ok(()) 271} 272 273#[cfg(test)] 274mod tests { 275 use std::collections::HashMap; 276 277 use super::fixup_lockfile; 278 use serde_json::json; 279 280 #[test] 281 fn lockfile_fixup() -> anyhow::Result<()> { 282 let input = json!({ 283 "lockfileVersion": 2, 284 "name": "foo", 285 "packages": { 286 "": { 287 288 }, 289 "foo": { 290 "resolved": "https://github.com/NixOS/nixpkgs", 291 "integrity": "sha1-aaa" 292 }, 293 "bar": { 294 "resolved": "git+ssh://git@github.com/NixOS/nixpkgs.git", 295 "integrity": "sha512-aaa" 296 }, 297 "foo-bad": { 298 "resolved": "foo", 299 "integrity": "sha1-foo" 300 }, 301 "foo-good": { 302 "resolved": "foo", 303 "integrity": "sha512-foo" 304 }, 305 } 306 }); 307 308 let expected = json!({ 309 "lockfileVersion": 2, 310 "name": "foo", 311 "packages": { 312 "": { 313 314 }, 315 "foo": { 316 "resolved": "https://github.com/NixOS/nixpkgs", 317 "integrity": "" 318 }, 319 "bar": { 320 "resolved": "git+ssh://git@github.com/NixOS/nixpkgs.git", 321 }, 322 "foo-bad": { 323 "resolved": "foo", 324 "integrity": "sha512-foo" 325 }, 326 "foo-good": { 327 "resolved": "foo", 328 "integrity": "sha512-foo" 329 }, 330 } 331 }); 332 333 let mut hashes = HashMap::new(); 334 335 hashes.insert( 336 String::from("https://github.com/NixOS/nixpkgs"), 337 String::new(), 338 ); 339 340 hashes.insert( 341 String::from("git+ssh://git@github.com/NixOS/nixpkgs.git"), 342 String::new(), 343 ); 344 345 hashes.insert(String::from("foo"), String::from("sha512-foo")); 346 347 assert_eq!( 348 fixup_lockfile(input.as_object().unwrap().clone(), &Some(hashes))?, 349 Some(expected.as_object().unwrap().clone()) 350 ); 351 352 Ok(()) 353 } 354 355 #[test] 356 fn lockfile_v1_fixup() -> anyhow::Result<()> { 357 let input = json!({ 358 "lockfileVersion": 1, 359 "name": "foo", 360 "dependencies": { 361 "foo": { 362 "resolved": "https://github.com/NixOS/nixpkgs", 363 "integrity": "sha512-aaa" 364 }, 365 "foo-good": { 366 "resolved": "foo", 367 "integrity": "sha512-foo" 368 }, 369 "bar": { 370 "resolved": "git+ssh://git@github.com/NixOS/nixpkgs.git", 371 "integrity": "sha512-bbb", 372 "dependencies": { 373 "foo-bad": { 374 "resolved": "foo", 375 "integrity": "sha1-foo" 376 }, 377 }, 378 }, 379 } 380 }); 381 382 let expected = json!({ 383 "lockfileVersion": 1, 384 "name": "foo", 385 "dependencies": { 386 "foo": { 387 "resolved": "https://github.com/NixOS/nixpkgs", 388 "integrity": "" 389 }, 390 "foo-good": { 391 "resolved": "foo", 392 "integrity": "sha512-foo" 393 }, 394 "bar": { 395 "resolved": "git+ssh://git@github.com/NixOS/nixpkgs.git", 396 "dependencies": { 397 "foo-bad": { 398 "resolved": "foo", 399 "integrity": "sha512-foo" 400 }, 401 }, 402 }, 403 } 404 }); 405 406 let mut hashes = HashMap::new(); 407 408 hashes.insert( 409 String::from("https://github.com/NixOS/nixpkgs"), 410 String::new(), 411 ); 412 413 hashes.insert( 414 String::from("git+ssh://git@github.com/NixOS/nixpkgs.git"), 415 String::new(), 416 ); 417 418 hashes.insert(String::from("foo"), String::from("sha512-foo")); 419 420 assert_eq!( 421 fixup_lockfile(input.as_object().unwrap().clone(), &Some(hashes))?, 422 Some(expected.as_object().unwrap().clone()) 423 ); 424 425 Ok(()) 426 } 427}