git.delta.rocks / jrsonnet / refs/commits / 80f6128c09ce

difftreelog

feat(jrb) use native-tls on apple/windows-mingw

pkwxmtqpYaroslav Bolyukin2026-05-07parent: #de9f0cb.patch.diff
in: master
Otherwise it is has some quirks. Also allows downstreams to have
different features for rustls if not building with jrb.

5 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -831,6 +831,21 @@
 checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
 
 [[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
 name = "form_urlencoded"
 version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1356,7 +1371,7 @@
 checksum = "39eb0623e15e4cb83c02ce6a959e48fadd1ae3b715b36b5acc01816e01388c82"
 dependencies = [
  "bstr",
- "hashbrown 0.16.1",
+ "hashbrown 0.15.5",
 ]
 
 [[package]]
@@ -2125,6 +2140,22 @@
 ]
 
 [[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
 name = "hyper-util"
 version = "0.1.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2934,6 +2965,23 @@
 ]
 
 [[package]]
+name = "native-tls"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
 name = "nix"
 version = "0.31.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3017,12 +3065,50 @@
 checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
 
 [[package]]
+name = "openssl"
+version = "0.10.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
 name = "openssl-probe"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
 
 [[package]]
+name = "openssl-sys"
+version = "0.9.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
 name = "option-ext"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3131,6 +3217,12 @@
 checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
 
 [[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
 name = "plain"
 version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3470,10 +3562,12 @@
  "http-body-util",
  "hyper",
  "hyper-rustls",
+ "hyper-tls",
  "hyper-util",
  "js-sys",
  "log",
  "mime",
+ "native-tls",
  "percent-encoding",
  "pin-project-lite",
  "quinn",
@@ -3482,6 +3576,7 @@
  "rustls-platform-verifier",
  "sync_wrapper",
  "tokio",
+ "tokio-native-tls",
  "tokio-rustls",
  "tower",
  "tower-http",
@@ -4096,6 +4191,16 @@
 ]
 
 [[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
 name = "tokio-rustls"
 version = "0.26.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4333,6 +4438,12 @@
 checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
 
 [[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
 name = "version_check"
 version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -125,16 +125,12 @@
 # Bundler
 tracing = "0.1.44"
 tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
-reqwest = { version = "0.13", features = [
+reqwest = { version = "0.13", default-features = false, features = [
   "blocking",
-  "rustls",
-], default-features = false }
+] }
 zip = { version = "8", default-features = false, features = ["deflate"] }
 directories = "6.0.0"
-gix = { version = "0.83.0", features = [
-  "blocking-network-client",
-  "blocking-http-transport-reqwest-rust-tls",
-] }
+gix = { version = "0.83.0", features = ["blocking-network-client"] }
 camino = { version = "1.2.2", features = ["serde1"] }
 
 [workspace.lints.rust]
modifiedcrates/jrsonnet-pkg/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-pkg/Cargo.toml
+++ b/crates/jrsonnet-pkg/Cargo.toml
@@ -20,11 +20,21 @@
 peg.workspace = true
 
 # Gix for git repos, reqwest + zip for github
-gix.workspace = true
-reqwest.workspace = true
 zip.workspace = true
 url.workspace = true
 camino.workspace = true
 
 # Global cache dir
 directories.workspace = true
+
+[target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_vendor = "apple")))'.dependencies]
+gix = { workspace = true, features = [
+  "blocking-http-transport-reqwest-rust-tls",
+] }
+reqwest = { workspace = true, features = ["rustls"] }
+
+[target.'cfg(any(all(target_os = "windows", target_env = "gnu"), target_vendor = "apple"))'.dependencies]
+gix = { workspace = true, features = [
+  "blocking-http-transport-reqwest-native-tls",
+] }
+reqwest = { workspace = true, features = ["native-tls"] }
modifiedcrates/jrsonnet-pkg/src/install/github.rsdiffbeforeafterboth
--- a/crates/jrsonnet-pkg/src/install/github.rs
+++ b/crates/jrsonnet-pkg/src/install/github.rs
@@ -14,6 +14,7 @@
 use super::{
 	Error, LocalExtraction, ResolveResult, Result, VendorSource,
 	accessor::{AccessorEntry, ZipFileAccessor},
+	make_symlink,
 };
 use crate::{
 	install::{PKG_USER_AGENT, cache_dir},
@@ -84,24 +85,6 @@
 	Ok(ZipFileAccessor::new_prefixed(
 		File::open(zip_path).map_err(|e| Error::Io(zip_path.to_owned(), e))?,
 	)?)
-}
-
-#[cfg(unix)]
-fn make_symlink(target: &str, link: &Path) -> std::io::Result<()> {
-	std::os::unix::fs::symlink(target, link)
-}
-
-#[cfg(windows)]
-fn make_symlink(target: &str, link: &Path) -> std::io::Result<()> {
-	std::os::windows::fs::symlink_file(target, link)
-}
-
-#[cfg(not(any(unix, windows)))]
-fn make_symlink(_target: &str, _link: &Path) -> std::io::Result<()> {
-	Err(std::io::Error::new(
-		std::io::ErrorKind::Unsupported,
-		"symlinks are not supported on this platform",
-	))
 }
 
 fn extract_subdir(archive: &ZipFileAccessor, subdir: &SubDir, dest: &Path) -> Result<()> {
modifiedcrates/jrsonnet-pkg/src/install/mod.rsdiffbeforeafterboth
before · crates/jrsonnet-pkg/src/install/mod.rs
1#![allow(clippy::result_large_err)]23pub mod accessor;4mod git;5mod github;67use std::{8	collections::{BTreeMap, HashSet},9	fs,10	path::{Path, PathBuf},11	result,12};1314use camino::Utf8PathBuf;15use tracing::info;1617use crate::jsonnet_bundler::{Dependency, GitScheme, GitSource, JsonnetFile, Source, SubDir};1819pub const PKG_USER_AGENT: &str = "jrsonnet-pkg (https://delta.rocks/jrsonnet)";2021pub fn cache_dir(subdir: &str) -> Result<std::path::PathBuf> {22	Ok(directories::ProjectDirs::from("rocks", "delta", "jrsonnet")23		.ok_or(Error::XdgUnavailable)?24		.cache_dir()25		.join(subdir))26}2728pub(crate) struct LocalExtraction {29	/// Path inside the parent repo's tree where this local source lives.30	pub tree_path: SubDir,31	pub name: String,32}3334pub(crate) struct ResolveResult {35	pub version: String,36	pub transitive_git_deps: Vec<Dependency>,37	pub local_extractions: Vec<LocalExtraction>,38	pub source: VendorSource,39}4041const VERSION_FILE: &str = ".version";4243/// How to populate a vendor path.44pub enum VendorSource {45	GitTree {46		repo_path: PathBuf,47		commit_sha: String,48		subdir: SubDir,49	},50	GithubZip {51		zip_path: PathBuf,52		commit_sha: String,53		subdir: SubDir,54	},55	Symlink(Utf8PathBuf),56}5758impl VendorSource {59	fn with_subdir(&self, new_subdir: SubDir) -> Self {60		match self {61			VendorSource::GitTree {62				repo_path,63				commit_sha,64				..65			} => VendorSource::GitTree {66				repo_path: repo_path.clone(),67				commit_sha: commit_sha.clone(),68				subdir: new_subdir,69			},70			VendorSource::GithubZip {71				zip_path,72				commit_sha,73				..74			} => VendorSource::GithubZip {75				zip_path: zip_path.clone(),76				commit_sha: commit_sha.clone(),77				subdir: new_subdir,78			},79			VendorSource::Symlink(target) => VendorSource::Symlink(target.clone()),80		}81	}82}8384pub struct InstallPlan {85	pub lock: JsonnetFile,86	/// vendor-relative path -> how to obtain it.87	pub entries: BTreeMap<Utf8PathBuf, VendorSource>,88}8990pub fn install(91	manifest: &JsonnetFile,92	lock: Option<&JsonnetFile>,93	vendor_dir: &Path,94	dry_run: bool,95) -> Result<JsonnetFile, Error> {96	let plan = resolve(manifest, lock)?;97	execute(&plan, vendor_dir, dry_run)?;98	Ok(plan.lock)99}100101pub fn resolve(manifest: &JsonnetFile, lock: Option<&JsonnetFile>) -> Result<InstallPlan, Error> {102	let mut plan = InstallPlan {103		lock: JsonnetFile {104			version: manifest.version,105			dependencies: Vec::new(),106			legacy_imports: manifest.legacy_imports,107		},108		entries: BTreeMap::new(),109	};110	let mut installed = HashSet::new();111112	resolve_deps(113		&manifest.dependencies,114		lock,115		manifest.legacy_imports,116		&mut plan,117		&mut installed,118	)?;119120	Ok(plan)121}122123fn is_up_to_date(dest: &Path, version: &str) -> bool {124	fs::read_to_string(dest.join(VERSION_FILE)).is_ok_and(|v| v.trim() == version)125}126127fn write_version(dest: &Path, version: &str) -> Result<(), Error> {128	fs::write(dest.join(VERSION_FILE), format!("{version}\n"))129		.map_err(|e| Error::Io(dest.join(VERSION_FILE), e))130}131132pub fn execute(plan: &InstallPlan, vendor_dir: &Path, dry_run: bool) -> Result<(), Error> {133	if !dry_run {134		for (path, source) in &plan.entries {135			let dest = vendor_dir.join(path);136			match source {137				VendorSource::GitTree {138					repo_path,139					commit_sha,140					subdir,141				} => {142					if is_up_to_date(&dest, commit_sha) {143						continue;144					}145					info!("extract {path}");146					if dest.exists() {147						fs::remove_dir_all(&dest).map_err(|e| Error::Io(dest.clone(), e))?;148					}149					fs::create_dir_all(&dest).map_err(|e| Error::Io(dest.clone(), e))?;150					git::extract(repo_path, commit_sha, subdir, &dest)?;151					write_version(&dest, commit_sha)?;152				}153				VendorSource::GithubZip {154					zip_path,155					commit_sha,156					subdir,157				} => {158					if is_up_to_date(&dest, commit_sha) {159						continue;160					}161					info!("extract {path}");162					if dest.exists() {163						fs::remove_dir_all(&dest).map_err(|e| Error::Io(dest.clone(), e))?;164					}165					fs::create_dir_all(&dest).map_err(|e| Error::Io(dest.clone(), e))?;166					github::extract(zip_path, subdir, &dest)?;167					write_version(&dest, commit_sha)?;168				}169				VendorSource::Symlink(_) => {}170			}171		}172		for (path, source) in &plan.entries {173			if let VendorSource::Symlink(target) = source {174				let dest = vendor_dir.join(path);175				if dest176					.symlink_metadata()177					.is_ok_and(|m| m.file_type().is_symlink())178				{179					if fs::read_link(&dest).is_ok_and(|t| t == target.as_std_path()) {180						continue;181					}182					fs::remove_file(&dest).map_err(|e| Error::Io(dest.clone(), e))?;183				}184				info!("symlink {path} -> {target}");185				std::os::unix::fs::symlink(target.as_std_path(), &dest)186					.map_err(|e| Error::Io(dest.clone(), e))?;187			}188		}189	}190	prune(plan, vendor_dir, dry_run)?;191	Ok(())192}193194fn prune(plan: &InstallPlan, vendor_dir: &Path, dry_run: bool) -> Result<(), Error> {195	if !vendor_dir.is_dir() {196		return Ok(());197	}198	prune_recursive(plan, vendor_dir, vendor_dir, dry_run)199}200201fn prune_recursive(202	plan: &InstallPlan,203	vendor_dir: &Path,204	dir: &Path,205	dry_run: bool,206) -> Result<(), Error> {207	let entries = fs::read_dir(dir).map_err(|e| Error::Io(dir.to_owned(), e))?;208	for entry in entries {209		let entry = entry.map_err(|e| Error::Io(dir.to_owned(), e))?;210		let path = entry.path();211		let rel = path212			.strip_prefix(vendor_dir)213			.expect("path is under vendor_dir");214		let Ok(rel) = Utf8PathBuf::try_from(rel.to_owned()) else {215			info!("prune (non-utf8) {}", rel.display());216			continue;217		};218219		if plan.entries.contains_key(&rel) {220			continue;221		}222223		let ft = entry.file_type().map_err(|e| Error::Io(path.clone(), e))?;224		if ft.is_symlink() {225			info!("prune {rel}");226			if !dry_run {227				fs::remove_file(&path).map_err(|e| Error::Io(path, e))?;228			}229		} else if ft.is_dir() {230			let prefix: Utf8PathBuf = format!("{rel}/").into();231			let has_descendants = plan232				.entries233				.range(prefix.clone()..)234				.next()235				.is_some_and(|(k, _)| k.starts_with(&prefix));236			if has_descendants {237				prune_recursive(plan, vendor_dir, &path, dry_run)?;238			} else {239				info!("prune {rel}");240				if !dry_run {241					fs::remove_dir_all(&path).map_err(|e| Error::Io(path, e))?;242				}243			}244		} else {245			info!("prune {rel}");246			if !dry_run {247				fs::remove_file(&path).map_err(|e| Error::Io(path, e))?;248			}249		}250	}251252	if !dry_run253		&& dir != vendor_dir254		&& let Ok(mut entries) = fs::read_dir(dir)255		&& entries.next().is_none()256	{257		let _ = fs::remove_dir(dir);258	}259260	Ok(())261}262263fn resolve_one(git_source: &GitSource, version: Option<&str>) -> Result<ResolveResult, Error> {264	if git_source.host == "github.com" && git_source.scheme == GitScheme::Https {265		match github::resolve(git_source, version) {266			Ok(result) => return Ok(result),267			Err(e) => {268				info!("github archive failed ({e}), falling back to git");269			}270		}271	}272	git::resolve(git_source, version)273}274275fn locked_version<'a>(dep: &Dependency, lock: Option<&'a JsonnetFile>) -> Option<&'a str> {276	let lock = lock?;277	let key = dep.canonical_name();278	lock.dependencies279		.iter()280		.find(|d| d.canonical_name() == key)281		.and_then(|d| d.version.as_deref())282}283284fn resolve_deps(285	deps: &[Dependency],286	lock: Option<&JsonnetFile>,287	legacy_imports: bool,288	plan: &mut InstallPlan,289	installed: &mut HashSet<Utf8PathBuf>,290) -> Result<(), Error> {291	for dep in deps {292		let Source::Git(git_source) = &dep.source else {293			continue;294		};295296		let canonical = dep.canonical_name();297		if !installed.insert(canonical.clone()) {298			continue;299		}300301		let version = locked_version(dep, lock).or(dep.version.as_deref());302303		info!(304			"resolving {canonical} (version: {})",305			version.unwrap_or("<TBD>")306		);307308		let result = resolve_one(git_source, version)?;309310		plan.lock.dependencies.push(Dependency {311			source: dep.source.clone(),312			version: Some(result.version),313			sum: dep.sum.clone(),314			name: dep.name.clone(),315			single: dep.single,316		});317318		let mut repo_base = Utf8PathBuf::from(git_source.host.as_str());319		repo_base.push(git_source.plain_repo_name());320321		// Legacy symlink for the dep. Skipped if `legacyImports: false`, unless322		// the user explicitly set `dep.name` (which is always honored).323		if legacy_imports || dep.name.is_some() {324			let legacy = Utf8PathBuf::from(dep.legacy_link_name());325			if legacy != canonical {326				plan.entries327					.insert(legacy, VendorSource::Symlink(canonical.clone()));328			}329		}330331		for extraction in &result.local_extractions {332			let extraction_canonical = repo_base.join(&extraction.tree_path);333			plan.entries.insert(334				extraction_canonical.clone(),335				result.source.with_subdir(extraction.tree_path.clone()),336			);337			if legacy_imports {338				let extraction_name = Utf8PathBuf::from(&extraction.name);339				if extraction_name != extraction_canonical {340					plan.entries341						.insert(extraction_name, VendorSource::Symlink(extraction_canonical));342				}343			}344		}345346		// Main entry (after local extractions used with_subdir)347		plan.entries.insert(canonical, result.source);348349		resolve_deps(350			&result.transitive_git_deps,351			lock,352			legacy_imports,353			plan,354			installed,355		)?;356	}357358	Ok(())359}360361#[derive(Debug, thiserror::Error)]362pub enum Error {363	#[error("io error for {0}: {1}")]364	Io(PathBuf, std::io::Error),365	#[error("failed to discover xdg directories")]366	XdgUnavailable,367	#[error("git clone failed: {0}")]368	GitClone(#[from] gix::clone::Error),369	#[error(transparent)]370	GitRemote(#[from] gix::remote::init::Error),371	#[error(transparent)]372	GitConnect(#[from] gix::remote::connect::Error),373	#[error(transparent)]374	GitFetchPrepare(#[from] gix::remote::fetch::prepare::Error),375	#[error(transparent)]376	GitRemoteFetch(#[from] gix::remote::fetch::Error),377	#[error(transparent)]378	GitCloneFetch(#[from] gix::clone::fetch::Error),379	#[error(transparent)]380	GitFindObject(#[from] gix::object::find::existing::Error),381	#[error(transparent)]382	GitTraverse(#[from] gix::traverse::tree::breadthfirst::Error),383	#[error(transparent)]384	GitHead(#[from] gix::reference::head_id::Error),385	#[error(transparent)]386	GitCommit(#[from] gix::object::commit::Error),387	#[error(transparent)]388	GitRevparse(#[from] gix::revision::spec::parse::single::Error),389	#[error(transparent)]390	GitRefspec(#[from] gix::refspec::parse::Error),391	#[error(transparent)]392	GitPeel(#[from] gix::reference::peel::Error),393	#[error(transparent)]394	GitPeelToKind(#[from] gix::object::peel::to_kind::Error),395	#[error(transparent)]396	GitOpen(#[from] gix::open::Error),397	#[error("http error: {0}")]398	Http(#[from] reqwest::Error),399	#[error("zip error: {0}")]400	Zip(Box<zip::result::ZipError>),401	#[error(transparent)]402	Accessor(#[from] accessor::Error),403	#[error("unknown subdir: {0}")]404	SubdirNotFound(String),405	#[error("invalid path in tree: {0}")]406	InvalidPath(String),407}408pub(crate) type Result<T, E = Error> = result::Result<T, E>;
after · crates/jrsonnet-pkg/src/install/mod.rs
1#![allow(clippy::result_large_err)]23pub mod accessor;4mod git;5mod github;67use std::{8	collections::{BTreeMap, HashSet},9	fs,10	path::{Path, PathBuf},11	result,12};1314use camino::Utf8PathBuf;15use tracing::info;1617use crate::jsonnet_bundler::{Dependency, GitScheme, GitSource, JsonnetFile, Source, SubDir};1819pub const PKG_USER_AGENT: &str = "jrsonnet-pkg (https://delta.rocks/jrsonnet)";2021pub fn cache_dir(subdir: &str) -> Result<std::path::PathBuf> {22	Ok(directories::ProjectDirs::from("rocks", "delta", "jrsonnet")23		.ok_or(Error::XdgUnavailable)?24		.cache_dir()25		.join(subdir))26}2728pub(crate) struct LocalExtraction {29	/// Path inside the parent repo's tree where this local source lives.30	pub tree_path: SubDir,31	pub name: String,32}3334pub(crate) struct ResolveResult {35	pub version: String,36	pub transitive_git_deps: Vec<Dependency>,37	pub local_extractions: Vec<LocalExtraction>,38	pub source: VendorSource,39}4041const VERSION_FILE: &str = ".version";4243/// How to populate a vendor path.44pub enum VendorSource {45	GitTree {46		repo_path: PathBuf,47		commit_sha: String,48		subdir: SubDir,49	},50	GithubZip {51		zip_path: PathBuf,52		commit_sha: String,53		subdir: SubDir,54	},55	Symlink(Utf8PathBuf),56}5758impl VendorSource {59	fn with_subdir(&self, new_subdir: SubDir) -> Self {60		match self {61			VendorSource::GitTree {62				repo_path,63				commit_sha,64				..65			} => VendorSource::GitTree {66				repo_path: repo_path.clone(),67				commit_sha: commit_sha.clone(),68				subdir: new_subdir,69			},70			VendorSource::GithubZip {71				zip_path,72				commit_sha,73				..74			} => VendorSource::GithubZip {75				zip_path: zip_path.clone(),76				commit_sha: commit_sha.clone(),77				subdir: new_subdir,78			},79			VendorSource::Symlink(target) => VendorSource::Symlink(target.clone()),80		}81	}82}8384pub struct InstallPlan {85	pub lock: JsonnetFile,86	/// vendor-relative path -> how to obtain it.87	pub entries: BTreeMap<Utf8PathBuf, VendorSource>,88}8990pub fn install(91	manifest: &JsonnetFile,92	lock: Option<&JsonnetFile>,93	vendor_dir: &Path,94	dry_run: bool,95) -> Result<JsonnetFile, Error> {96	let plan = resolve(manifest, lock)?;97	execute(&plan, vendor_dir, dry_run)?;98	Ok(plan.lock)99}100101pub fn resolve(manifest: &JsonnetFile, lock: Option<&JsonnetFile>) -> Result<InstallPlan, Error> {102	let mut plan = InstallPlan {103		lock: JsonnetFile {104			version: manifest.version,105			dependencies: Vec::new(),106			legacy_imports: manifest.legacy_imports,107		},108		entries: BTreeMap::new(),109	};110	let mut installed = HashSet::new();111112	resolve_deps(113		&manifest.dependencies,114		lock,115		manifest.legacy_imports,116		&mut plan,117		&mut installed,118	)?;119120	Ok(plan)121}122123#[cfg(unix)]124fn make_symlink(target: &str, link: &Path) -> std::io::Result<()> {125	std::os::unix::fs::symlink(target, link)126}127128#[cfg(windows)]129fn make_symlink(target: &str, link: &Path) -> std::io::Result<()> {130	std::os::windows::fs::symlink_dir(target, link)131}132133#[cfg(not(any(unix, windows)))]134fn make_symlink(_target: &str, _link: &Path) -> std::io::Result<()> {135	Err(std::io::Error::new(136		std::io::ErrorKind::Unsupported,137		"symlinks are not supported on this platform",138	))139}140141fn is_up_to_date(dest: &Path, version: &str) -> bool {142	fs::read_to_string(dest.join(VERSION_FILE)).is_ok_and(|v| v.trim() == version)143}144145fn write_version(dest: &Path, version: &str) -> Result<(), Error> {146	fs::write(dest.join(VERSION_FILE), format!("{version}\n"))147		.map_err(|e| Error::Io(dest.join(VERSION_FILE), e))148}149150pub fn execute(plan: &InstallPlan, vendor_dir: &Path, dry_run: bool) -> Result<(), Error> {151	if !dry_run {152		for (path, source) in &plan.entries {153			let dest = vendor_dir.join(path);154			match source {155				VendorSource::GitTree {156					repo_path,157					commit_sha,158					subdir,159				} => {160					if is_up_to_date(&dest, commit_sha) {161						continue;162					}163					info!("extract {path}");164					if dest.exists() {165						fs::remove_dir_all(&dest).map_err(|e| Error::Io(dest.clone(), e))?;166					}167					fs::create_dir_all(&dest).map_err(|e| Error::Io(dest.clone(), e))?;168					git::extract(repo_path, commit_sha, subdir, &dest)?;169					write_version(&dest, commit_sha)?;170				}171				VendorSource::GithubZip {172					zip_path,173					commit_sha,174					subdir,175				} => {176					if is_up_to_date(&dest, commit_sha) {177						continue;178					}179					info!("extract {path}");180					if dest.exists() {181						fs::remove_dir_all(&dest).map_err(|e| Error::Io(dest.clone(), e))?;182					}183					fs::create_dir_all(&dest).map_err(|e| Error::Io(dest.clone(), e))?;184					github::extract(zip_path, subdir, &dest)?;185					write_version(&dest, commit_sha)?;186				}187				VendorSource::Symlink(_) => {}188			}189		}190		for (path, source) in &plan.entries {191			if let VendorSource::Symlink(target) = source {192				let dest = vendor_dir.join(path);193				if dest194					.symlink_metadata()195					.is_ok_and(|m| m.file_type().is_symlink())196				{197					if fs::read_link(&dest).is_ok_and(|t| t == target.as_std_path()) {198						continue;199					}200					fs::remove_file(&dest).map_err(|e| Error::Io(dest.clone(), e))?;201				}202				info!("symlink {path} -> {target}");203				make_symlink(target.as_str(), &dest).map_err(|e| Error::Io(dest.clone(), e))?;204			}205		}206	}207	prune(plan, vendor_dir, dry_run)?;208	Ok(())209}210211fn prune(plan: &InstallPlan, vendor_dir: &Path, dry_run: bool) -> Result<(), Error> {212	if !vendor_dir.is_dir() {213		return Ok(());214	}215	prune_recursive(plan, vendor_dir, vendor_dir, dry_run)216}217218fn prune_recursive(219	plan: &InstallPlan,220	vendor_dir: &Path,221	dir: &Path,222	dry_run: bool,223) -> Result<(), Error> {224	let entries = fs::read_dir(dir).map_err(|e| Error::Io(dir.to_owned(), e))?;225	for entry in entries {226		let entry = entry.map_err(|e| Error::Io(dir.to_owned(), e))?;227		let path = entry.path();228		let rel = path229			.strip_prefix(vendor_dir)230			.expect("path is under vendor_dir");231		let Ok(rel) = Utf8PathBuf::try_from(rel.to_owned()) else {232			info!("prune (non-utf8) {}", rel.display());233			continue;234		};235236		if plan.entries.contains_key(&rel) {237			continue;238		}239240		let ft = entry.file_type().map_err(|e| Error::Io(path.clone(), e))?;241		if ft.is_symlink() {242			info!("prune {rel}");243			if !dry_run {244				fs::remove_file(&path).map_err(|e| Error::Io(path, e))?;245			}246		} else if ft.is_dir() {247			let prefix: Utf8PathBuf = format!("{rel}/").into();248			let has_descendants = plan249				.entries250				.range(prefix.clone()..)251				.next()252				.is_some_and(|(k, _)| k.starts_with(&prefix));253			if has_descendants {254				prune_recursive(plan, vendor_dir, &path, dry_run)?;255			} else {256				info!("prune {rel}");257				if !dry_run {258					fs::remove_dir_all(&path).map_err(|e| Error::Io(path, e))?;259				}260			}261		} else {262			info!("prune {rel}");263			if !dry_run {264				fs::remove_file(&path).map_err(|e| Error::Io(path, e))?;265			}266		}267	}268269	if !dry_run270		&& dir != vendor_dir271		&& let Ok(mut entries) = fs::read_dir(dir)272		&& entries.next().is_none()273	{274		let _ = fs::remove_dir(dir);275	}276277	Ok(())278}279280fn resolve_one(git_source: &GitSource, version: Option<&str>) -> Result<ResolveResult, Error> {281	if git_source.host == "github.com" && git_source.scheme == GitScheme::Https {282		match github::resolve(git_source, version) {283			Ok(result) => return Ok(result),284			Err(e) => {285				info!("github archive failed ({e}), falling back to git");286			}287		}288	}289	git::resolve(git_source, version)290}291292fn locked_version<'a>(dep: &Dependency, lock: Option<&'a JsonnetFile>) -> Option<&'a str> {293	let lock = lock?;294	let key = dep.canonical_name();295	lock.dependencies296		.iter()297		.find(|d| d.canonical_name() == key)298		.and_then(|d| d.version.as_deref())299}300301fn resolve_deps(302	deps: &[Dependency],303	lock: Option<&JsonnetFile>,304	legacy_imports: bool,305	plan: &mut InstallPlan,306	installed: &mut HashSet<Utf8PathBuf>,307) -> Result<(), Error> {308	for dep in deps {309		let Source::Git(git_source) = &dep.source else {310			continue;311		};312313		let canonical = dep.canonical_name();314		if !installed.insert(canonical.clone()) {315			continue;316		}317318		let version = locked_version(dep, lock).or(dep.version.as_deref());319320		info!(321			"resolving {canonical} (version: {})",322			version.unwrap_or("<TBD>")323		);324325		let result = resolve_one(git_source, version)?;326327		plan.lock.dependencies.push(Dependency {328			source: dep.source.clone(),329			version: Some(result.version),330			sum: dep.sum.clone(),331			name: dep.name.clone(),332			single: dep.single,333		});334335		let mut repo_base = Utf8PathBuf::from(git_source.host.as_str());336		repo_base.push(git_source.plain_repo_name());337338		// Legacy symlink for the dep. Skipped if `legacyImports: false`, unless339		// the user explicitly set `dep.name` (which is always honored).340		if legacy_imports || dep.name.is_some() {341			let legacy = Utf8PathBuf::from(dep.legacy_link_name());342			if legacy != canonical {343				plan.entries344					.insert(legacy, VendorSource::Symlink(canonical.clone()));345			}346		}347348		for extraction in &result.local_extractions {349			let extraction_canonical = repo_base.join(&extraction.tree_path);350			plan.entries.insert(351				extraction_canonical.clone(),352				result.source.with_subdir(extraction.tree_path.clone()),353			);354			if legacy_imports {355				let extraction_name = Utf8PathBuf::from(&extraction.name);356				if extraction_name != extraction_canonical {357					plan.entries358						.insert(extraction_name, VendorSource::Symlink(extraction_canonical));359				}360			}361		}362363		// Main entry (after local extractions used with_subdir)364		plan.entries.insert(canonical, result.source);365366		resolve_deps(367			&result.transitive_git_deps,368			lock,369			legacy_imports,370			plan,371			installed,372		)?;373	}374375	Ok(())376}377378#[derive(Debug, thiserror::Error)]379pub enum Error {380	#[error("io error for {0}: {1}")]381	Io(PathBuf, std::io::Error),382	#[error("failed to discover xdg directories")]383	XdgUnavailable,384	#[error("git clone failed: {0}")]385	GitClone(#[from] gix::clone::Error),386	#[error(transparent)]387	GitRemote(#[from] gix::remote::init::Error),388	#[error(transparent)]389	GitConnect(#[from] gix::remote::connect::Error),390	#[error(transparent)]391	GitFetchPrepare(#[from] gix::remote::fetch::prepare::Error),392	#[error(transparent)]393	GitRemoteFetch(#[from] gix::remote::fetch::Error),394	#[error(transparent)]395	GitCloneFetch(#[from] gix::clone::fetch::Error),396	#[error(transparent)]397	GitFindObject(#[from] gix::object::find::existing::Error),398	#[error(transparent)]399	GitTraverse(#[from] gix::traverse::tree::breadthfirst::Error),400	#[error(transparent)]401	GitHead(#[from] gix::reference::head_id::Error),402	#[error(transparent)]403	GitCommit(#[from] gix::object::commit::Error),404	#[error(transparent)]405	GitRevparse(#[from] gix::revision::spec::parse::single::Error),406	#[error(transparent)]407	GitRefspec(#[from] gix::refspec::parse::Error),408	#[error(transparent)]409	GitPeel(#[from] gix::reference::peel::Error),410	#[error(transparent)]411	GitPeelToKind(#[from] gix::object::peel::to_kind::Error),412	#[error(transparent)]413	GitOpen(#[from] gix::open::Error),414	#[error("http error: {0}")]415	Http(#[from] reqwest::Error),416	#[error("zip error: {0}")]417	Zip(Box<zip::result::ZipError>),418	#[error(transparent)]419	Accessor(#[from] accessor::Error),420	#[error("unknown subdir: {0}")]421	SubdirNotFound(String),422	#[error("invalid path in tree: {0}")]423	InvalidPath(String),424}425pub(crate) type Result<T, E = Error> = result::Result<T, E>;