Use files instead of junctions on Windows (#11269)

Instead of using junctions, we can just write files that contain (as the
file contents) the target path. This requires a little more finesse in
that, as readers, we need to know where to expect these. But it also
means we get to avoid junctions, which have led to a variety of
confusing behaviors. Further, `replace_symlink` should now be on atomic
on Windows.

Closes #11263.
This commit is contained in:
Charlie Marsh 2025-02-07 19:13:19 -05:00 committed by Zanie Blue
parent 59c65c3e77
commit 4d5041dc00
14 changed files with 351 additions and 59 deletions

View file

@ -1,12 +1,7 @@
use uv_cache::{ArchiveId, Cache};
use uv_cache::{ArchiveId, Cache, ARCHIVE_VERSION};
use uv_distribution_types::Hashed;
use uv_pypi_types::HashDigest;
/// The version of the archive bucket.
///
/// Must be kept in-sync with the version in [`uv_cache::CacheBucket::to_str`].
const ARCHIVE_VERSION: u8 = 0;
/// An archive (unzipped wheel) that exists in the local cache.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Archive {

View file

@ -371,7 +371,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
// If the wheel was unzipped previously, respect it. Source distributions are
// cached under a unique revision ID, so unzipped directories are never stale.
match built_wheel.target.canonicalize() {
match self.build_context.cache().resolve_link(&built_wheel.target) {
Ok(archive) => {
return Ok(LocalWheel {
dist: Dist::Source(dist.clone()),

View file

@ -1,6 +1,3 @@
use crate::index::cached_wheel::CachedWheel;
use crate::source::{HttpRevisionPointer, LocalRevisionPointer, HTTP_REVISION, LOCAL_REVISION};
use crate::Error;
use uv_cache::{Cache, CacheBucket, CacheShard, WheelCache};
use uv_cache_info::CacheInfo;
use uv_cache_key::cache_digest;
@ -8,10 +5,13 @@ use uv_configuration::ConfigSettings;
use uv_distribution_types::{
DirectUrlSourceDist, DirectorySourceDist, GitSourceDist, Hashed, PathSourceDist,
};
use uv_fs::symlinks;
use uv_platform_tags::Tags;
use uv_types::HashStrategy;
use crate::index::cached_wheel::CachedWheel;
use crate::source::{HttpRevisionPointer, LocalRevisionPointer, HTTP_REVISION, LOCAL_REVISION};
use crate::Error;
/// A local index of built distributions for a specific source distribution.
#[derive(Debug)]
pub struct BuiltWheelIndex<'a> {
@ -203,8 +203,16 @@ impl<'a> BuiltWheelIndex<'a> {
let mut candidate: Option<CachedWheel> = None;
// Unzipped wheels are stored as symlinks into the archive directory.
for subdir in symlinks(shard) {
match CachedWheel::from_built_source(&subdir) {
for wheel_dir in uv_fs::entries(shard) {
// Ignore any `.lock` files.
if wheel_dir
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("lock"))
{
continue;
}
match CachedWheel::from_built_source(&wheel_dir, self.cache) {
None => {}
Some(dist_info) => {
// Pick the wheel with the highest priority

View file

@ -26,7 +26,7 @@ pub struct CachedWheel {
impl CachedWheel {
/// Try to parse a distribution from a cached directory name (like `typing-extensions-4.8.0-py3-none-any`).
pub fn from_built_source(path: impl AsRef<Path>) -> Option<Self> {
pub fn from_built_source(path: impl AsRef<Path>, cache: &Cache) -> Option<Self> {
let path = path.as_ref();
// Determine the wheel filename.
@ -34,7 +34,7 @@ impl CachedWheel {
let filename = WheelFilename::from_stem(filename).ok()?;
// Convert to a cached wheel.
let archive = path.canonicalize().ok()?;
let archive = cache.resolve_link(path).ok()?;
let entry = CacheEntry::from_path(archive);
let hashes = Vec::new();
let cache_info = CacheInfo::default();

View file

@ -6,7 +6,7 @@ use uv_cache::{Cache, CacheBucket, WheelCache};
use uv_cache_key::cache_digest;
use uv_configuration::ConfigSettings;
use uv_distribution_types::{CachedRegistryDist, Hashed, Index, IndexLocations, IndexUrl};
use uv_fs::{directories, files, symlinks};
use uv_fs::{directories, files};
use uv_normalize::PackageName;
use uv_platform_tags::Tags;
use uv_types::HashStrategy;
@ -205,8 +205,16 @@ impl<'a> RegistryWheelIndex<'a> {
cache_shard.shard(cache_digest(build_configuration))
};
for wheel_dir in symlinks(cache_shard) {
if let Some(wheel) = CachedWheel::from_built_source(wheel_dir) {
for wheel_dir in uv_fs::entries(cache_shard) {
// Ignore any `.lock` files.
if wheel_dir
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("lock"))
{
continue;
}
if let Some(wheel) = CachedWheel::from_built_source(wheel_dir, cache) {
if wheel.filename.compatibility(tags).is_compatible() {
// Enforce hash-checking based on the source distribution.
if revision.satisfies(