git.delta.rocks / jrsonnet / refs/commits / de9f0cb0f75a

difftreelog

source

crates/jrsonnet-pkg/src/install/github.rs6.0 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 camino::Utf8PathBuf;11use reqwest::{blocking::Response, header};12use tracing::{debug, info, warn};1314use super::{15	Error, LocalExtraction, ResolveResult, Result, VendorSource,16	accessor::{AccessorEntry, ZipFileAccessor},17};18use crate::{19	install::{PKG_USER_AGENT, cache_dir},20	jsonnet_bundler::{Dependency, GitSource, JsonnetFile, Source, SubDir},21};2223fn is_sha(s: &str) -> bool {24	s.len() == 40 && s.bytes().all(|b| b.is_ascii_hexdigit())25}2627fn commit_cache_path(source: &GitSource, sha: &str) -> Result<PathBuf> {28	Ok(cache_dir("github")?29		.join(source.plain_repo_name())30		.join(format!("{sha}.zip")))31}3233fn resolve_sha(source: &GitSource, version: &str) -> Result<String> {34	let url = format!(35		"https://api.github.com/repos/{}/commits/{}",36		source.plain_repo_name(),37		version38	);39	let response = reqwest::blocking::Client::new()40		.get(&url)41		.header(header::ACCEPT, "application/vnd.github.sha")42		.header(header::USER_AGENT, PKG_USER_AGENT)43		.send()44		.and_then(Response::error_for_status)?;45	let sha = response.text()?;46	Ok(sha.trim().to_owned())47}4849fn fetch_zip(source: &GitSource, sha: &str) -> Result<ZipFileAccessor> {50	let cached = commit_cache_path(source, sha)?;51	if cached.exists() {52		debug!("using cached archive {}", cached.display());53		return Ok(ZipFileAccessor::new_prefixed(54			File::open(&cached).map_err(|e| Error::Io(cached.clone(), e))?,55		)?);56	}5758	let url = format!(59		"https://github.com/{}/archive/{}.zip",60		source.plain_repo_name(),61		sha62	);63	info!("downloading {url}");6465	let bytes = reqwest::blocking::Client::new()66		.get(&url)67		.header(header::USER_AGENT, PKG_USER_AGENT)68		.send()69		.and_then(Response::error_for_status)?70		.bytes()?;7172	if let Some(parent) = cached.parent() {73		fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;74	}75	let mut downloaded = File::create_new(&cached).map_err(|e| Error::Io(cached.clone(), e))?;76	downloaded77		.write_all(&bytes)78		.map_err(|e| Error::Io(cached.clone(), e))?;7980	Ok(ZipFileAccessor::new_prefixed(downloaded)?)81}8283fn open_cached_zip(zip_path: &Path) -> Result<ZipFileAccessor> {84	Ok(ZipFileAccessor::new_prefixed(85		File::open(zip_path).map_err(|e| Error::Io(zip_path.to_owned(), e))?,86	)?)87}8889#[cfg(unix)]90fn make_symlink(target: &str, link: &Path) -> std::io::Result<()> {91	std::os::unix::fs::symlink(target, link)92}9394#[cfg(windows)]95fn make_symlink(target: &str, link: &Path) -> std::io::Result<()> {96	std::os::windows::fs::symlink_file(target, link)97}9899#[cfg(not(any(unix, windows)))]100fn make_symlink(_target: &str, _link: &Path) -> std::io::Result<()> {101	Err(std::io::Error::new(102		std::io::ErrorKind::Unsupported,103		"symlinks are not supported on this platform",104	))105}106107fn extract_subdir(archive: &ZipFileAccessor, subdir: &SubDir, dest: &Path) -> Result<()> {108	archive.iter(subdir, &mut |name, entry| {109		let target = dest.join(&name);110		match entry {111			AccessorEntry::Dir => {112				fs::create_dir_all(&target).map_err(|e| Error::Io(target, e))?;113			}114			AccessorEntry::File(data) => {115				if let Some(parent) = target.parent() {116					fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;117				}118				fs::write(&target, &data).map_err(|e| Error::Io(target, e))?;119			}120			AccessorEntry::Symlink(link_target) => {121				let symlink_parent = name122					.as_path()123					.parent()124					.map(|p| SubDir::try_from(Utf8PathBuf::from(p)))125					.transpose()126					.expect("parent of a SubDir is a SubDir")127					.unwrap_or_else(SubDir::empty);128				if link_target.resolve_under(&symlink_parent).is_err() {129					warn!("symlink {name} -> {link_target} escapes extraction; skipping");130					return Ok(());131				}132				if let Some(parent) = target.parent() {133					fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;134				}135				make_symlink(&link_target.to_string(), &target)136					.map_err(|e| Error::Io(target, e))?;137			}138		}139		Ok(())140	})141}142143fn collect_archive_deps(144	archive: &ZipFileAccessor,145	dir: &SubDir,146	git_deps: &mut Vec<Dependency>,147	local_extractions: &mut Vec<LocalExtraction>,148	visited: &mut HashSet<SubDir>,149) -> Result<()> {150	if !visited.insert(dir.clone()) {151		return Ok(());152	}153154	let manifest_path = dir155		.join("jsonnetfile.json")156		.expect("appending a literal filename keeps it within parent");157158	let Some(data) = archive.read(&manifest_path)? else {159		return Ok(());160	};161	let Ok(manifest) = serde_json::from_slice::<JsonnetFile>(&data) else {162		return Ok(());163	};164165	for dep in manifest.dependencies {166		match &dep.source {167			Source::Git(_) => git_deps.push(dep),168			Source::Local(local) => {169				let Ok(child_dir) = local.resolve_under(dir) else {170					tracing::info!("local source {local} escapes its package; skipping");171					continue;172				};173				let name = child_dir174					.file_name()175					.map_or_else(|| local.to_string(), str::to_owned);176				local_extractions.push(LocalExtraction {177					tree_path: child_dir.clone(),178					name,179				});180				collect_archive_deps(archive, &child_dir, git_deps, local_extractions, visited)?;181			}182		}183	}184	Ok(())185}186187pub(super) fn resolve(source: &GitSource, version: Option<&str>) -> Result<ResolveResult> {188	let version_str = version.unwrap_or("HEAD");189	let sha = if is_sha(version_str) {190		version_str.to_owned()191	} else {192		let resolved = resolve_sha(source, version_str)?;193		info!("resolved {version_str} to {resolved}");194		resolved195	};196197	let archive = fetch_zip(source, &sha)?;198199	let mut transitive_git_deps = Vec::new();200	let mut local_extractions = Vec::new();201	let mut visited = HashSet::new();202	collect_archive_deps(203		&archive,204		&source.subdir,205		&mut transitive_git_deps,206		&mut local_extractions,207		&mut visited,208	)?;209210	let zip_path = commit_cache_path(source, &sha)?;211212	Ok(ResolveResult {213		version: sha.clone(),214		transitive_git_deps,215		local_extractions,216		source: VendorSource::GithubZip {217			zip_path,218			commit_sha: sha,219			subdir: source.subdir.clone(),220		},221	})222}223224pub(super) fn extract(zip_path: &Path, subdir: &SubDir, dest: &Path) -> Result<()> {225	let archive = open_cached_zip(zip_path)?;226	extract_subdir(&archive, subdir, dest)227}