git.delta.rocks / jrsonnet / refs/commits / 6710888e6b7f

difftreelog

source

crates/jrsonnet-pkg/src/install/github.rs4.9 KiBsourcehistory
1#![allow(clippy::result_large_err)]23use std::{4	collections::HashSet,5	fs::{self, File},6	io::Write as _,7	path::{Path, PathBuf},8};910use reqwest::{blocking::Response, header};11use tracing::{debug, info};1213use super::{14	Error, LocalExtraction, ResolveResult, Result, VendorSource,15	accessor::{AccessorEntry, ZipFileAccessor},16};17use crate::{18	install::{PKG_USER_AGENT, cache_dir},19	jsonnet_bundler::{Dependency, GitSource, JsonnetFile, Source, SubDir},20};2122fn is_sha(s: &str) -> bool {23	s.len() == 40 && s.bytes().all(|b| b.is_ascii_hexdigit())24}2526fn commit_cache_path(source: &GitSource, sha: &str) -> Result<PathBuf> {27	Ok(cache_dir("github")?28		.join(source.plain_repo_name())29		.join(format!("{sha}.zip")))30}3132fn resolve_sha(source: &GitSource, version: &str) -> Result<String> {33	let url = format!(34		"https://api.github.com/repos/{}/commits/{}",35		source.plain_repo_name(),36		version37	);38	let response = reqwest::blocking::Client::new()39		.get(&url)40		.header(header::ACCEPT, "application/vnd.github.sha")41		.header(header::USER_AGENT, PKG_USER_AGENT)42		.send()43		.and_then(Response::error_for_status)?;44	let sha = response.text()?;45	Ok(sha.trim().to_owned())46}4748fn fetch_zip(source: &GitSource, sha: &str) -> Result<ZipFileAccessor> {49	let cached = commit_cache_path(source, sha)?;50	if cached.exists() {51		debug!("using cached archive {}", cached.display());52		return Ok(ZipFileAccessor::new_prefixed(53			File::open(&cached).map_err(|e| Error::Io(cached.clone(), e))?,54		)?);55	}5657	let url = format!(58		"https://github.com/{}/archive/{}.zip",59		source.plain_repo_name(),60		sha61	);62	info!("downloading {url}");6364	let bytes = reqwest::blocking::Client::new()65		.get(&url)66		.header(header::USER_AGENT, PKG_USER_AGENT)67		.send()68		.and_then(Response::error_for_status)?69		.bytes()?;7071	if let Some(parent) = cached.parent() {72		fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;73	}74	let mut downloaded = File::create_new(&cached).map_err(|e| Error::Io(cached.clone(), e))?;75	downloaded76		.write_all(&bytes)77		.map_err(|e| Error::Io(cached.clone(), e))?;7879	Ok(ZipFileAccessor::new_prefixed(downloaded)?)80}8182fn open_cached_zip(zip_path: &Path) -> Result<ZipFileAccessor> {83	Ok(ZipFileAccessor::new_prefixed(84		File::open(zip_path).map_err(|e| Error::Io(zip_path.to_owned(), e))?,85	)?)86}8788fn extract_subdir(archive: &ZipFileAccessor, subdir: &SubDir, dest: &Path) -> Result<()> {89	archive.iter(subdir, &mut |name, entry| {90		let target = dest.join(name);91		match entry {92			AccessorEntry::Dir => {93				fs::create_dir_all(&target).map_err(|e| Error::Io(target, e))?;94			}95			AccessorEntry::File(data) => {96				if let Some(parent) = target.parent() {97					fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;98				}99				fs::write(&target, &data).map_err(|e| Error::Io(target, e))?;100			}101		}102		Ok(())103	})104}105106fn collect_archive_deps(107	archive: &ZipFileAccessor,108	dir: &SubDir,109	git_deps: &mut Vec<Dependency>,110	local_extractions: &mut Vec<LocalExtraction>,111	visited: &mut HashSet<SubDir>,112) -> Result<()> {113	if !visited.insert(dir.clone()) {114		return Ok(());115	}116117	let manifest_path = dir118		.join("jsonnetfile.json")119		.expect("appending a literal filename keeps it within parent");120121	let Some(data) = archive.read(&manifest_path)? else {122		return Ok(());123	};124	let Ok(manifest) = serde_json::from_slice::<JsonnetFile>(&data) else {125		return Ok(());126	};127128	for dep in manifest.dependencies {129		match &dep.source {130			Source::Git(_) => git_deps.push(dep),131			Source::Local(local) => {132				let Ok(child_dir) = local.resolve_under(dir) else {133					tracing::info!("local source {local} escapes its package; skipping");134					continue;135				};136				let name = child_dir137					.file_name()138					.map_or_else(|| local.to_string(), str::to_owned);139				local_extractions.push(LocalExtraction {140					tree_path: child_dir.clone(),141					name,142				});143				collect_archive_deps(archive, &child_dir, git_deps, local_extractions, visited)?;144			}145		}146	}147	Ok(())148}149150pub(super) fn resolve(source: &GitSource, version: Option<&str>) -> Result<ResolveResult> {151	let version_str = version.unwrap_or("HEAD");152	let sha = if is_sha(version_str) {153		version_str.to_owned()154	} else {155		let resolved = resolve_sha(source, version_str)?;156		info!("resolved {version_str} to {resolved}");157		resolved158	};159160	let archive = fetch_zip(source, &sha)?;161162	let mut transitive_git_deps = Vec::new();163	let mut local_extractions = Vec::new();164	let mut visited = HashSet::new();165	collect_archive_deps(166		&archive,167		&source.subdir,168		&mut transitive_git_deps,169		&mut local_extractions,170		&mut visited,171	)?;172173	let zip_path = commit_cache_path(source, &sha)?;174175	Ok(ResolveResult {176		version: sha.clone(),177		transitive_git_deps,178		local_extractions,179		source: VendorSource::GithubZip {180			zip_path,181			commit_sha: sha,182			subdir: source.subdir.clone(),183		},184	})185}186187pub(super) fn extract(zip_path: &Path, subdir: &SubDir, dest: &Path) -> Result<()> {188	let archive = open_cached_zip(zip_path)?;189	extract_subdir(&archive, subdir, dest)190}