difftreelog
feat(jrb) use native-tls on apple/windows-mingw
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
Cargo.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"
Cargo.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]
crates/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"] }
crates/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<()> {
crates/jrsonnet-pkg/src/install/mod.rsdiffbeforeafterboth1#![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>;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>;