Merge branch 'main' into sysinfo

This commit is contained in:
LIghtJUNction 2025-11-19 00:58:01 +08:00 committed by GitHub
commit 05edb1a988
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1488 additions and 214 deletions

View File

@ -49,3 +49,4 @@ jobs:
title: "Sync latest Python releases"
body: "Automated update for Python releases."
base: "main"
draft: true

View File

@ -3,6 +3,33 @@
<!-- prettier-ignore-start -->
## 0.9.10
Released on 2025-11-17.
### Enhancements
- Add support for `SSL_CERT_DIR` ([#16473](https://github.com/astral-sh/uv/pull/16473))
- Enforce UTF8-encoded license files during `uv build` ([#16699](https://github.com/astral-sh/uv/pull/16699))
- Error when a `project.license-files` glob matches nothing ([#16697](https://github.com/astral-sh/uv/pull/16697))
- `pip install --target` (and `sync`) install Python if necessary ([#16694](https://github.com/astral-sh/uv/pull/16694))
- Account for `python_downloads_json_url` in pre-release Python version warnings ([#16737](https://github.com/astral-sh/uv/pull/16737))
- Support HTTP/HTTPS URLs in `uv python --python-downloads-json-url` ([#16542](https://github.com/astral-sh/uv/pull/16542))
### Preview features
- Add support for `--upgrade` in `uv python install` ([#16676](https://github.com/astral-sh/uv/pull/16676))
- Fix handling of `python install --default` for pre-release Python versions ([#16706](https://github.com/astral-sh/uv/pull/16706))
- Add `uv workspace list` to list workspace members ([#16691](https://github.com/astral-sh/uv/pull/16691))
### Bug fixes
- Don't check file URLs for ambiguously parsed credentials ([#16759](https://github.com/astral-sh/uv/pull/16759))
### Documentation
- Add a "storage" reference document ([#15954](https://github.com/astral-sh/uv/pull/15954))
## 0.9.9
Released on 2025-11-12.
@ -38,7 +65,6 @@ Released on 2025-11-12.
- Fix `CMD` path in FastAPI Dockerfile ([#16701](https://github.com/astral-sh/uv/pull/16701))
## 0.9.8
Released on 2025-11-07.
@ -257,25 +283,25 @@ There are no breaking changes to [`uv_build`](https://docs.astral.sh/uv/concepts
### Breaking changes
- **Python 3.14 is now the default stable version**
The default Python version has changed from 3.13 to 3.14. This applies to Python version installation when no Python version is requested, e.g., `uv python install`. By default, uv will use the system Python version if present, so this may not cause changes to general use of uv. For example, if Python 3.13 is installed already, then `uv venv` will use that version. If no Python versions are installed on a machine and automatic downloads are enabled, uv will now use 3.14 instead of 3.13, e.g., for `uv venv` or `uvx python`. This change will not affect users who are using a `.python-version` file to pin to a specific Python version.
- **Allow use of free-threaded variants in Python 3.14+ without explicit opt-in** ([#16142](https://github.com/astral-sh/uv/pull/16142))
Previously, free-threaded variants of Python were considered experimental and required explicit opt-in (i.e., with `3.14t`) for usage. Now uv will allow use of free-threaded Python 3.14+ interpreters without explicit selection. The GIL-enabled build of Python will still be preferred, e.g., when performing an installation with `uv python install 3.14`. However, e.g., if a free-threaded interpreter comes before a GIL-enabled build on the `PATH`, it will be used. This change does not apply to free-threaded Python 3.13 interpreters, which will continue to require opt-in.
- **Use Python 3.14 stable Docker images** ([#16150](https://github.com/astral-sh/uv/pull/16150))
Previously, the Python 3.14 images had an `-rc` suffix, e.g., `python:3.14-rc-alpine` or
`python:3.14-rc-trixie`. Now, the `-rc` suffix has been removed to match the stable
[upstream images](https://hub.docker.com/_/python). The `-rc` images tags will no longer be
updated. This change should not break existing workflows.
- **Upgrade Alpine Docker image to Alpine 3.22**
Previously, the `uv:alpine` Docker image was based on Alpine 3.21. Now, this image is based on Alpine 3.22. The previous image can be recovered with `uv:alpine3.21` and will continue to be updated until a future release.
- **Upgrade Debian Docker images to Debian 13 "Trixie"**
Previously, the `uv:debian` and `uv:debian-slim` Docker images were based on Debian 12 "Bookworm". Now, these images are based on Debian 13 "Trixie". The previous images can be recovered with `uv:bookworm` and `uv:bookworm-slim` and will continue to be updated until a future release.
- **Fix incorrect output path when a trailing `/` is used in `uv build`** ([#15133](https://github.com/astral-sh/uv/pull/15133))
When using `uv build` in a workspace, the artifacts are intended to be written to a `dist` directory in the workspace root. A bug caused workspace root determination to fail when the input path included a trailing `/` causing the `dist` directory to be placed in the child directory. This bug has been fixed in this release. For example, `uv build child/` is used, the output path will now be in `<workspace root>/dist/` rather than `<workspace root>/child/dist/`.
### Python

23
Cargo.lock generated
View File

@ -1214,7 +1214,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.0",
"windows-sys 0.59.0",
]
[[package]]
@ -2126,9 +2126,9 @@ dependencies = [
[[package]]
name = "indicatif"
version = "0.18.2"
version = "0.18.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65"
checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88"
dependencies = [
"console 0.16.1",
"portable-atomic",
@ -2285,7 +2285,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-sys 0.61.0",
"windows-sys 0.52.0",
]
[[package]]
@ -2623,7 +2623,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08"
dependencies = [
"windows-sys 0.61.0",
"windows-sys 0.60.2",
]
[[package]]
@ -3872,9 +3872,9 @@ dependencies = [
[[package]]
name = "rustls-native-certs"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923"
dependencies = [
"openssl-probe",
"rustls-pki-types",
@ -5301,7 +5301,7 @@ dependencies = [
[[package]]
name = "uv"
version = "0.9.9"
version = "0.9.10"
dependencies = [
"anstream",
"anyhow",
@ -5513,7 +5513,7 @@ dependencies = [
[[package]]
name = "uv-build"
version = "0.9.9"
version = "0.9.10"
dependencies = [
"anstream",
"anyhow",
@ -5710,16 +5710,19 @@ dependencies = [
"itertools 0.14.0",
"jiff",
"percent-encoding",
"rcgen",
"reqwest",
"rkyv",
"rmp-serde",
"rustc-hash",
"rustls",
"serde",
"serde_json",
"sysinfo",
"tempfile",
"thiserror 2.0.17",
"tokio",
"tokio-rustls",
"tokio-util",
"tracing",
"url",
@ -6807,7 +6810,7 @@ dependencies = [
[[package]]
name = "uv-version"
version = "0.9.9"
version = "0.9.10"
[[package]]
name = "uv-virtualenv"

View File

@ -211,15 +211,17 @@ byteorder = { version = "1.5.0" }
filetime = { version = "0.2.25" }
http-body-util = { version = "0.1.2" }
hyper = { version = "1.4.1", features = ["server", "http1"] }
hyper-util = { version = "0.1.8", features = ["tokio"] }
hyper-util = { version = "0.1.8", features = ["tokio", "server", "http1"] }
ignore = { version = "0.4.23" }
insta = { version = "1.40.0", features = ["json", "filters", "redactions"] }
predicates = { version = "3.1.2" }
rcgen = { version = "0.14.5", features = ["crypto", "pem", "ring"], default-features = false }
rustls = { version = "0.23.29", default-features = false }
similar = { version = "2.6.0" }
temp-env = { version = "0.3.6" }
test-case = { version = "3.3.1" }
test-log = { version = "0.2.16", features = ["trace"], default-features = false }
tokio-rustls = { version = "0.26.2", default-features = false }
whoami = { version = "1.6.0" }
[workspace.metadata.cargo-shear]

View File

@ -1,6 +1,6 @@
[package]
name = "uv-build"
version = "0.9.9"
version = "0.9.10"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }

View File

@ -1,6 +1,6 @@
[project]
name = "uv-build"
version = "0.9.9"
version = "0.9.10"
description = "The uv build backend"
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
requires-python = ">=3.8"

View File

@ -7167,6 +7167,11 @@ pub enum WorkspaceCommand {
///
/// If used outside of a workspace, i.e., if a `pyproject.toml` cannot be found, uv will exit with an error.
Dir(WorkspaceDirArgs),
/// List the members of a workspace.
///
/// Displays newline separated names of workspace members.
#[command(hide = true)]
List(WorkspaceListArgs),
}
#[derive(Args, Debug)]
@ -7179,6 +7184,9 @@ pub struct WorkspaceDirArgs {
pub package: Option<PackageName>,
}
#[derive(Args, Debug)]
pub struct WorkspaceListArgs;
/// See [PEP 517](https://peps.python.org/pep-0517/) and
/// [PEP 660](https://peps.python.org/pep-0660/) for specifications of the parameters.
#[derive(Subcommand)]

View File

@ -72,6 +72,9 @@ http-body-util = { workspace = true }
hyper = { workspace = true }
hyper-util = { workspace = true }
insta = { workspace = true }
rcgen = { workspace = true }
rustls = { workspace = true }
tokio = { workspace = true }
tokio-rustls = { workspace = true }
wiremock = { workspace = true }
tempfile = { workspace = true }

View File

@ -363,7 +363,9 @@ impl<'a> BaseClientBuilder<'a> {
let _ = write!(user_agent_string, " {output}");
}
// Check for the presence of an `SSL_CERT_FILE`.
// Checks for the presence of `SSL_CERT_FILE`.
// Certificate loading support is delegated to `rustls-native-certs`.
// See https://github.com/rustls/rustls-native-certs/blob/813790a297ad4399efe70a8e5264ca1b420acbec/src/lib.rs#L118-L125
let ssl_cert_file_exists = env::var_os(EnvVars::SSL_CERT_FILE).is_some_and(|path| {
let path_exists = Path::new(&path).exists();
if !path_exists {
@ -375,11 +377,61 @@ impl<'a> BaseClientBuilder<'a> {
path_exists
});
// Checks for the presence of `SSL_CERT_DIR`.
// Certificate loading support is delegated to `rustls-native-certs`.
// See https://github.com/rustls/rustls-native-certs/blob/813790a297ad4399efe70a8e5264ca1b420acbec/src/lib.rs#L118-L125
let ssl_cert_dir_exists = env::var_os(EnvVars::SSL_CERT_DIR)
.filter(|v| !v.is_empty())
.is_some_and(|dirs| {
// Parse `SSL_CERT_DIR`, with support for multiple entries using
// a platform-specific delimiter (`:` on Unix, `;` on Windows)
let (existing, missing): (Vec<_>, Vec<_>) =
env::split_paths(&dirs).partition(|p| p.exists());
if existing.is_empty() {
let end_note = if missing.len() == 1 {
"The directory does not exist."
} else {
"The entries do not exist."
};
warn_user_once!(
"Ignoring invalid `SSL_CERT_DIR`. {end_note}: {}.",
missing
.iter()
.map(Simplified::simplified_display)
.join(", ")
.cyan()
);
return false;
}
// Warn on any missing entries
if !missing.is_empty() {
let end_note = if missing.len() == 1 {
"The following directory does not exist:"
} else {
"The following entries do not exist:"
};
warn_user_once!(
"Invalid entries in `SSL_CERT_DIR`. {end_note}: {}.",
missing
.iter()
.map(Simplified::simplified_display)
.join(", ")
.cyan()
);
}
// Proceed while ignoring missing entries
true
});
// Create a secure client that validates certificates.
let raw_client = self.create_client(
&user_agent_string,
timeout,
ssl_cert_file_exists,
ssl_cert_dir_exists,
Security::Secure,
self.redirect_policy,
);
@ -389,6 +441,7 @@ impl<'a> BaseClientBuilder<'a> {
&user_agent_string,
timeout,
ssl_cert_file_exists,
ssl_cert_dir_exists,
Security::Insecure,
self.redirect_policy,
);
@ -401,6 +454,7 @@ impl<'a> BaseClientBuilder<'a> {
user_agent: &str,
timeout: Duration,
ssl_cert_file_exists: bool,
ssl_cert_dir_exists: bool,
security: Security,
redirect_policy: RedirectPolicy,
) -> Client {
@ -419,7 +473,7 @@ impl<'a> BaseClientBuilder<'a> {
Security::Insecure => client_builder.danger_accept_invalid_certs(true),
};
let client_builder = if self.native_tls || ssl_cert_file_exists {
let client_builder = if self.native_tls || ssl_cert_file_exists || ssl_cert_dir_exists {
client_builder.tls_built_in_native_certs(true)
} else {
client_builder.tls_built_in_webpki_certs(true)

View File

@ -0,0 +1,382 @@
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Context, Result};
use futures::future;
use http_body_util::combinators::BoxBody;
use http_body_util::{BodyExt, Full};
use hyper::body::{Bytes, Incoming};
use hyper::header::USER_AGENT;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::{TokioExecutor, TokioIo};
use hyper_util::server::conn::auto::Builder;
use rcgen::{
BasicConstraints, Certificate, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa,
Issuer, KeyPair, KeyUsagePurpose, SanType, date_time_ymd,
};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::server::WebPkiClientVerifier;
use rustls::{RootCertStore, ServerConfig};
use tokio::net::TcpListener;
use tokio::task::JoinHandle;
use tokio_rustls::TlsAcceptor;
use uv_fs::Simplified;
/// An issued certificate, together with the subject keypair.
#[derive(Debug)]
pub(crate) struct SelfSigned {
/// An issued certificate.
pub public: Certificate,
/// The certificate's subject signing key.
pub private: KeyPair,
}
/// Defines the base location for temporary generated certs.
///
/// See [`TestContext::test_bucket_dir`] for implementation rationale.
pub(crate) fn test_cert_dir() -> PathBuf {
std::env::temp_dir()
.simple_canonicalize()
.expect("failed to canonicalize temp dir")
.join("uv")
.join("tests")
.join("certs")
}
/// Generates a self-signed server certificate for `uv-test-server`, `localhost` and `127.0.0.1`.
/// This certificate is standalone and not issued by a self-signed Root CA.
///
/// Use sparingly as generation of certs is a slow operation.
pub(crate) fn generate_self_signed_certs() -> Result<SelfSigned> {
let mut params = CertificateParams::default();
params.is_ca = IsCa::NoCa;
params.not_before = date_time_ymd(1975, 1, 1);
params.not_after = date_time_ymd(4096, 1, 1);
params.key_usages.push(KeyUsagePurpose::DigitalSignature);
params.key_usages.push(KeyUsagePurpose::KeyEncipherment);
params
.extended_key_usages
.push(ExtendedKeyUsagePurpose::ServerAuth);
params
.distinguished_name
.push(DnType::OrganizationName, "Astral Software Inc.");
params
.distinguished_name
.push(DnType::CommonName, "uv-test-server");
params
.subject_alt_names
.push(SanType::DnsName("uv-test-server".try_into()?));
params
.subject_alt_names
.push(SanType::DnsName("localhost".try_into()?));
params
.subject_alt_names
.push(SanType::IpAddress("127.0.0.1".parse()?));
let private = KeyPair::generate()?;
let public = params.self_signed(&private)?;
Ok(SelfSigned { public, private })
}
/// Generates a self-signed root CA, server certificate, and client certificate.
/// There are no intermediate certs generated as part of this function.
/// The server certificate is for `uv-test-server`, `localhost` and `127.0.0.1` issued by this CA.
/// The client certificate is for `uv-test-client` issued by this CA.
///
/// Use sparingly as generation of these certs is a very slow operation.
pub(crate) fn generate_self_signed_certs_with_ca() -> Result<(SelfSigned, SelfSigned, SelfSigned)> {
// Generate the CA
let mut ca_params = CertificateParams::default();
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); // root cert
ca_params.not_before = date_time_ymd(1975, 1, 1);
ca_params.not_after = date_time_ymd(4096, 1, 1);
ca_params.key_usages.push(KeyUsagePurpose::DigitalSignature);
ca_params.key_usages.push(KeyUsagePurpose::KeyCertSign);
ca_params.key_usages.push(KeyUsagePurpose::CrlSign);
ca_params
.distinguished_name
.push(DnType::OrganizationName, "Astral Software Inc.");
ca_params
.distinguished_name
.push(DnType::CommonName, "uv-test-ca");
ca_params
.subject_alt_names
.push(SanType::DnsName("uv-test-ca".try_into()?));
let ca_private_key = KeyPair::generate()?;
let ca_public_cert = ca_params.self_signed(&ca_private_key)?;
let ca_cert_issuer = Issuer::new(ca_params, &ca_private_key);
// Generate server cert issued by this CA
let mut server_params = CertificateParams::default();
server_params.is_ca = IsCa::NoCa;
server_params.not_before = date_time_ymd(1975, 1, 1);
server_params.not_after = date_time_ymd(4096, 1, 1);
server_params.use_authority_key_identifier_extension = true;
server_params
.key_usages
.push(KeyUsagePurpose::DigitalSignature);
server_params
.key_usages
.push(KeyUsagePurpose::KeyEncipherment);
server_params
.extended_key_usages
.push(ExtendedKeyUsagePurpose::ServerAuth);
server_params
.distinguished_name
.push(DnType::OrganizationName, "Astral Software Inc.");
server_params
.distinguished_name
.push(DnType::CommonName, "uv-test-server");
server_params
.subject_alt_names
.push(SanType::DnsName("uv-test-server".try_into()?));
server_params
.subject_alt_names
.push(SanType::DnsName("localhost".try_into()?));
server_params
.subject_alt_names
.push(SanType::IpAddress("127.0.0.1".parse()?));
let server_private_key = KeyPair::generate()?;
let server_public_cert = server_params.signed_by(&server_private_key, &ca_cert_issuer)?;
// Generate client cert issued by this CA
let mut client_params = CertificateParams::default();
client_params.is_ca = IsCa::NoCa;
client_params.not_before = date_time_ymd(1975, 1, 1);
client_params.not_after = date_time_ymd(4096, 1, 1);
client_params.use_authority_key_identifier_extension = true;
client_params
.key_usages
.push(KeyUsagePurpose::DigitalSignature);
client_params
.extended_key_usages
.push(ExtendedKeyUsagePurpose::ClientAuth);
client_params
.distinguished_name
.push(DnType::OrganizationName, "Astral Software Inc.");
client_params
.distinguished_name
.push(DnType::CommonName, "uv-test-client");
client_params
.subject_alt_names
.push(SanType::DnsName("uv-test-client".try_into()?));
let client_private_key = KeyPair::generate()?;
let client_public_cert = client_params.signed_by(&client_private_key, &ca_cert_issuer)?;
let ca_self_signed = SelfSigned {
public: ca_public_cert,
private: ca_private_key,
};
let server_self_signed = SelfSigned {
public: server_public_cert,
private: server_private_key,
};
let client_self_signed = SelfSigned {
public: client_public_cert,
private: client_private_key,
};
Ok((ca_self_signed, server_self_signed, client_self_signed))
}
// Plain is fine for now; Arc/Box could be used later if we need to support move.
type ServerSvcFn =
fn(
Request<Incoming>,
) -> future::Ready<Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error>>;
#[derive(Default)]
pub(crate) struct TestServerBuilder<'a> {
// Custom server response function
svc_fn: Option<ServerSvcFn>,
// CA certificate
ca_cert: Option<&'a SelfSigned>,
// Server certificate
server_cert: Option<&'a SelfSigned>,
// Enable mTLS Verification
mutual_tls: bool,
}
impl<'a> TestServerBuilder<'a> {
pub(crate) fn new() -> Self {
Self {
svc_fn: None,
server_cert: None,
ca_cert: None,
mutual_tls: false,
}
}
#[expect(unused)]
/// Provide a custom server response function.
pub(crate) fn with_svc_fn(mut self, svc_fn: ServerSvcFn) -> Self {
self.svc_fn = Some(svc_fn);
self
}
/// Provide the server certificate. This will enable TLS (HTTPS).
pub(crate) fn with_server_cert(mut self, server_cert: &'a SelfSigned) -> Self {
self.server_cert = Some(server_cert);
self
}
/// CA certificate used to build the `RootCertStore` for client verification.
/// Requires `with_server_cert`.
pub(crate) fn with_ca_cert(mut self, ca_cert: &'a SelfSigned) -> Self {
self.ca_cert = Some(ca_cert);
self
}
/// Enforce mutual TLS (client cert auth).
/// Requires `with_server_cert` and `with_ca_cert`.
pub(crate) fn with_mutual_tls(mut self, mutual: bool) -> Self {
self.mutual_tls = mutual;
self
}
/// Starts the HTTP(S) server with optional mTLS enforcement.
pub(crate) async fn start(self) -> Result<(JoinHandle<Result<()>>, SocketAddr)> {
// Validate builder input combinations
if self.ca_cert.is_some() && self.server_cert.is_none() {
anyhow::bail!("server certificate is required when CA certificate is provided");
}
if self.mutual_tls && (self.ca_cert.is_none() || self.server_cert.is_none()) {
anyhow::bail!("ca certificate is required for mTLS");
}
// Set up the TCP listener on a random available port
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
// Setup TLS Config (if any)
let tls_acceptor = if let Some(server_cert) = self.server_cert {
// Prepare Server Cert and KeyPair
let server_key = PrivateKeyDer::try_from(server_cert.private.serialize_der()).unwrap();
let server_cert = vec![CertificateDer::from(server_cert.public.der().to_vec())];
// Setup CA Verifier
let client_verifier = if let Some(ca_cert) = self.ca_cert {
let mut root_store = RootCertStore::empty();
root_store
.add(CertificateDer::from(ca_cert.public.der().to_vec()))
.expect("failed to add CA cert");
if self.mutual_tls {
// Setup mTLS CA config
WebPkiClientVerifier::builder(root_store.into())
.build()
.expect("failed to setup client verifier")
} else {
// Only load the CA roots
WebPkiClientVerifier::builder(root_store.into())
.allow_unauthenticated()
.build()
.expect("failed to setup client verifier")
}
} else {
WebPkiClientVerifier::no_client_auth()
};
let mut tls_config = ServerConfig::builder()
.with_client_cert_verifier(client_verifier)
.with_single_cert(server_cert, server_key)?;
tls_config.alpn_protocols = vec![b"http/1.1".to_vec(), b"http/1.0".to_vec()];
Some(TlsAcceptor::from(Arc::new(tls_config)))
} else {
None
};
// Setup Response Handler
let svc_fn = if let Some(custom_svc_fn) = self.svc_fn {
custom_svc_fn
} else {
|req: Request<Incoming>| {
// Get User Agent Header and send it back in the response
let user_agent = req
.headers()
.get(USER_AGENT)
.and_then(|v| v.to_str().ok())
.map(ToString::to_string)
.unwrap_or_default(); // Empty Default
let response_content = Full::new(Bytes::from(user_agent))
.map_err(|_| unreachable!())
.boxed();
// If we ever want a true echo server, we can use instead
// let response_content = req.into_body().boxed();
// although uv-client doesn't expose post currently.
future::ok::<_, hyper::Error>(Response::new(response_content))
}
};
// Spawn the server loop in a background task
let server_task = tokio::spawn(async move {
let svc = service_fn(move |req: Request<Incoming>| svc_fn(req));
let (tcp_stream, _remote_addr) = listener
.accept()
.await
.context("Failed to accept TCP connection")?;
// Start Server (not wrapped in loop {} since we want a single response server)
// If we want server to accept multiple connections, we can wrap it in loop {}
// but we'll need to ensure to handle termination signals in the tests otherwise
// it may never stop.
if let Some(tls_acceptor) = tls_acceptor {
let tls_stream = tls_acceptor
.accept(tcp_stream)
.await
.context("Failed to accept TLS connection")?;
let socket = TokioIo::new(tls_stream);
tokio::task::spawn(async move {
Builder::new(TokioExecutor::new())
.serve_connection(socket, svc)
.await
.expect("HTTPS Server Started");
});
} else {
let socket = TokioIo::new(tcp_stream);
tokio::task::spawn(async move {
Builder::new(TokioExecutor::new())
.serve_connection(socket, svc)
.await
.expect("HTTP Server Started");
});
}
Ok(())
});
Ok((server_task, addr))
}
}
/// Single Request HTTP server that echoes the User Agent Header.
pub(crate) async fn start_http_user_agent_server() -> Result<(JoinHandle<Result<()>>, SocketAddr)> {
TestServerBuilder::new().start().await
}
/// Single Request HTTPS server that echoes the User Agent Header.
pub(crate) async fn start_https_user_agent_server(
server_cert: &SelfSigned,
) -> Result<(JoinHandle<Result<()>>, SocketAddr)> {
TestServerBuilder::new()
.with_server_cert(server_cert)
.start()
.await
}
/// Single Request HTTPS mTLS server that echoes the User Agent Header.
pub(crate) async fn start_https_mtls_user_agent_server(
ca_cert: &SelfSigned,
server_cert: &SelfSigned,
) -> Result<(JoinHandle<Result<()>>, SocketAddr)> {
TestServerBuilder::new()
.with_ca_cert(ca_cert)
.with_server_cert(server_cert)
.with_mutual_tls(true)
.start()
.await
}

View File

@ -1,2 +1,4 @@
mod http_util;
mod remote_metadata;
mod ssl_certs;
mod user_agent_version;

View File

@ -21,11 +21,11 @@ async fn remote_metadata_with_and_without_cache() -> Result<()> {
let filename = WheelFilename::from_str(url.rsplit_once('/').unwrap().1)?;
let dist = BuiltDist::DirectUrl(DirectUrlBuiltDist {
filename,
location: Box::new(DisplaySafeUrl::parse(url).unwrap()),
url: VerbatimUrl::from_str(url).unwrap(),
location: Box::new(DisplaySafeUrl::parse(url)?),
url: VerbatimUrl::from_str(url)?,
});
let capabilities = IndexCapabilities::default();
let metadata = client.wheel_metadata(&dist, &capabilities).await.unwrap();
let metadata = client.wheel_metadata(&dist, &capabilities).await?;
assert_eq!(metadata.version.to_string(), "4.66.1");
}

View File

@ -0,0 +1,333 @@
use std::str::FromStr;
use anyhow::Result;
use rustls::AlertDescription;
use url::Url;
use uv_cache::Cache;
use uv_client::BaseClientBuilder;
use uv_client::RegistryClientBuilder;
use uv_redacted::DisplaySafeUrl;
use uv_static::EnvVars;
use crate::http_util::{
generate_self_signed_certs, generate_self_signed_certs_with_ca,
start_https_mtls_user_agent_server, start_https_user_agent_server, test_cert_dir,
};
// SAFETY: This test is meant to run with single thread configuration
#[tokio::test]
#[allow(unsafe_code)]
async fn ssl_env_vars() -> Result<()> {
// Ensure our environment is not polluted with anything that may affect `rustls-native-certs`
unsafe {
std::env::remove_var(EnvVars::UV_NATIVE_TLS);
std::env::remove_var(EnvVars::SSL_CERT_FILE);
std::env::remove_var(EnvVars::SSL_CERT_DIR);
std::env::remove_var(EnvVars::SSL_CLIENT_CERT);
}
// Create temporary cert dirs
let cert_dir = test_cert_dir();
fs_err::create_dir_all(&cert_dir).expect("Failed to create test cert bucket");
let cert_dir =
tempfile::TempDir::new_in(cert_dir).expect("Failed to create test cert directory");
let does_not_exist_cert_dir = cert_dir.path().join("does_not_exist");
// Generate self-signed standalone cert
let standalone_server_cert = generate_self_signed_certs()?;
let standalone_public_pem_path = cert_dir.path().join("standalone_public.pem");
let standalone_private_pem_path = cert_dir.path().join("standalone_private.pem");
// Generate self-signed CA, server, and client certs
let (ca_cert, server_cert, client_cert) = generate_self_signed_certs_with_ca()?;
let ca_public_pem_path = cert_dir.path().join("ca_public.pem");
let ca_private_pem_path = cert_dir.path().join("ca_private.pem");
let server_public_pem_path = cert_dir.path().join("server_public.pem");
let server_private_pem_path = cert_dir.path().join("server_private.pem");
let client_combined_pem_path = cert_dir.path().join("client_combined.pem");
// Persist the certs in PKCS8 format as the env vars expect a path on disk
fs_err::write(
standalone_public_pem_path.as_path(),
standalone_server_cert.public.pem(),
)?;
fs_err::write(
standalone_private_pem_path.as_path(),
standalone_server_cert.private.serialize_pem(),
)?;
fs_err::write(ca_public_pem_path.as_path(), ca_cert.public.pem())?;
fs_err::write(
ca_private_pem_path.as_path(),
ca_cert.private.serialize_pem(),
)?;
fs_err::write(server_public_pem_path.as_path(), server_cert.public.pem())?;
fs_err::write(
server_private_pem_path.as_path(),
server_cert.private.serialize_pem(),
)?;
fs_err::write(
client_combined_pem_path.as_path(),
// SSL_CLIENT_CERT expects a "combined" cert with the public and private key.
format!(
"{}\n{}",
client_cert.public.pem(),
client_cert.private.serialize_pem()
),
)?;
// ** Set SSL_CERT_FILE to non-existent location
// ** Then verify our request fails to establish a connection
unsafe {
std::env::set_var(EnvVars::SSL_CERT_FILE, does_not_exist_cert_dir.as_os_str());
}
let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?;
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
let cache = Cache::temp()?.init()?;
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
let res = client
.cached_client()
.uncached()
.for_host(&url)
.get(Url::from(url))
.send()
.await;
unsafe {
std::env::remove_var(EnvVars::SSL_CERT_FILE);
}
// Validate the client error
let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.err() else {
panic!("expected middleware error");
};
let reqwest_error = middleware_error
.chain()
.find_map(|err| {
err.downcast_ref::<reqwest_middleware::Error>().map(|err| {
if let reqwest_middleware::Error::Reqwest(inner) = err {
inner
} else {
panic!("expected reqwest error")
}
})
})
.expect("expected reqwest error");
assert!(reqwest_error.is_connect());
// Validate the server error
let server_res = server_task.await?;
let expected_err = if let Err(anyhow_err) = server_res
&& let Some(io_err) = anyhow_err.downcast_ref::<std::io::Error>()
&& let Some(wrapped_err) = io_err.get_ref()
&& let Some(tls_err) = wrapped_err.downcast_ref::<rustls::Error>()
&& matches!(
tls_err,
rustls::Error::AlertReceived(AlertDescription::UnknownCA)
) {
true
} else {
false
};
assert!(expected_err);
// ** Set SSL_CERT_FILE to our public certificate
// ** Then verify our request successfully establishes a connection
unsafe {
std::env::set_var(
EnvVars::SSL_CERT_FILE,
standalone_public_pem_path.as_os_str(),
);
}
let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?;
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
let cache = Cache::temp()?.init()?;
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
let res = client
.cached_client()
.uncached()
.for_host(&url)
.get(Url::from(url))
.send()
.await;
assert!(res.is_ok());
let _ = server_task.await?; // wait for server shutdown
unsafe {
std::env::remove_var(EnvVars::SSL_CERT_FILE);
}
// ** Set SSL_CERT_DIR to our cert dir as well as some other dir that does not exist
// ** Then verify our request still successfully establishes a connection
unsafe {
std::env::set_var(
EnvVars::SSL_CERT_DIR,
std::env::join_paths(vec![
cert_dir.path().as_os_str(),
does_not_exist_cert_dir.as_os_str(),
])?,
);
}
let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?;
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
let cache = Cache::temp()?.init()?;
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
let res = client
.cached_client()
.uncached()
.for_host(&url)
.get(Url::from(url))
.send()
.await;
assert!(res.is_ok());
let _ = server_task.await?; // wait for server shutdown
unsafe {
std::env::remove_var(EnvVars::SSL_CERT_DIR);
}
// ** Set SSL_CERT_DIR to only the dir that does not exist
// ** Then verify our request fails to establish a connection
unsafe {
std::env::set_var(EnvVars::SSL_CERT_DIR, does_not_exist_cert_dir.as_os_str());
}
let (server_task, addr) = start_https_user_agent_server(&standalone_server_cert).await?;
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
let cache = Cache::temp()?.init()?;
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
let res = client
.cached_client()
.uncached()
.for_host(&url)
.get(Url::from(url))
.send()
.await;
unsafe {
std::env::remove_var(EnvVars::SSL_CERT_DIR);
}
// Validate the client error
let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.err() else {
panic!("expected middleware error");
};
let reqwest_error = middleware_error
.chain()
.find_map(|err| {
err.downcast_ref::<reqwest_middleware::Error>().map(|err| {
if let reqwest_middleware::Error::Reqwest(inner) = err {
inner
} else {
panic!("expected reqwest error")
}
})
})
.expect("expected reqwest error");
assert!(reqwest_error.is_connect());
// Validate the server error
let server_res = server_task.await?;
let expected_err = if let Err(anyhow_err) = server_res
&& let Some(io_err) = anyhow_err.downcast_ref::<std::io::Error>()
&& let Some(wrapped_err) = io_err.get_ref()
&& let Some(tls_err) = wrapped_err.downcast_ref::<rustls::Error>()
&& matches!(
tls_err,
rustls::Error::AlertReceived(AlertDescription::UnknownCA)
) {
true
} else {
false
};
assert!(expected_err);
// *** mTLS Tests
// ** Set SSL_CERT_FILE to our CA and SSL_CLIENT_CERT to our client cert
// ** Then verify our request still successfully establishes a connection
// We need to set SSL_CERT_FILE or SSL_CERT_DIR to our CA as we need to tell
// our HTTP client that we trust certificates issued by our self-signed CA.
// This inherently also tests that our server cert is also validated as part
// of the certificate path validation algorithm.
unsafe {
std::env::set_var(EnvVars::SSL_CERT_FILE, ca_public_pem_path.as_os_str());
std::env::set_var(
EnvVars::SSL_CLIENT_CERT,
client_combined_pem_path.as_os_str(),
);
}
let (server_task, addr) = start_https_mtls_user_agent_server(&ca_cert, &server_cert).await?;
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
let cache = Cache::temp()?.init()?;
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
let res = client
.cached_client()
.uncached()
.for_host(&url)
.get(Url::from(url))
.send()
.await;
assert!(res.is_ok());
let _ = server_task.await?; // wait for server shutdown
unsafe {
std::env::remove_var(EnvVars::SSL_CERT_FILE);
std::env::remove_var(EnvVars::SSL_CLIENT_CERT);
}
// ** Set SSL_CERT_FILE to our CA and unset SSL_CLIENT_CERT
// ** Then verify our request fails to establish a connection
unsafe {
std::env::set_var(EnvVars::SSL_CERT_FILE, ca_public_pem_path.as_os_str());
}
let (server_task, addr) = start_https_mtls_user_agent_server(&ca_cert, &server_cert).await?;
let url = DisplaySafeUrl::from_str(&format!("https://{addr}"))?;
let cache = Cache::temp()?.init()?;
let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build();
let res = client
.cached_client()
.uncached()
.for_host(&url)
.get(Url::from(url))
.send()
.await;
unsafe {
std::env::remove_var(EnvVars::SSL_CERT_FILE);
}
// Validate the client error
let Some(reqwest_middleware::Error::Middleware(middleware_error)) = res.err() else {
panic!("expected middleware error");
};
let reqwest_error = middleware_error
.chain()
.find_map(|err| {
err.downcast_ref::<reqwest_middleware::Error>().map(|err| {
if let reqwest_middleware::Error::Reqwest(inner) = err {
inner
} else {
panic!("expected reqwest error")
}
})
})
.expect("expected reqwest error");
assert!(reqwest_error.is_connect());
// Validate the server error
let server_res = server_task.await?;
let expected_err = if let Err(anyhow_err) = server_res
&& let Some(io_err) = anyhow_err.downcast_ref::<std::io::Error>()
&& let Some(wrapped_err) = io_err.get_ref()
&& let Some(tls_err) = wrapped_err.downcast_ref::<rustls::Error>()
&& matches!(tls_err, rustls::Error::NoCertificatesPresented)
{
true
} else {
false
};
assert!(expected_err);
// Fin.
Ok(())
}

View File

@ -1,17 +1,12 @@
use std::str::FromStr;
use anyhow::Result;
use futures::future;
use http_body_util::Full;
use hyper::body::Bytes;
use hyper::header::USER_AGENT;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use insta::{assert_json_snapshot, assert_snapshot, with_settings};
use std::io::Read;
use std::str::FromStr;
use tokio::net::TcpListener;
use url::Url;
use uv_cache::Cache;
use uv_client::RegistryClientBuilder;
use uv_client::{BaseClientBuilder, LineHaul};
@ -42,37 +37,12 @@ fn get_version_codename() -> Result<Option<String>, std::io::Error> {
Ok(None)
}
use crate::http_util::start_http_user_agent_server;
#[tokio::test]
async fn test_user_agent_has_version() -> Result<()> {
// Set up the TCP listener on a random available port
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
// Spawn the server loop in a background task
let server_task = tokio::spawn(async move {
let svc = service_fn(move |req: Request<hyper::body::Incoming>| {
// Get User Agent Header and send it back in the response
let user_agent = req
.headers()
.get(USER_AGENT)
.and_then(|v| v.to_str().ok())
.map(ToString::to_string)
.unwrap_or_default(); // Empty Default
future::ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from(user_agent))))
});
// Start Server (not wrapped in loop {} since we want a single response server)
// If you want server to accept multiple connections, wrap it in loop {}
let (socket, _) = listener.accept().await.unwrap();
let socket = TokioIo::new(socket);
tokio::task::spawn(async move {
http1::Builder::new()
.serve_connection(socket, svc)
.with_upgrades()
.await
.expect("Server Started");
});
});
// Initialize dummy http server
let (server_task, addr) = start_http_user_agent_server().await?;
// Initialize uv-client
let cache = Cache::temp()?.init()?;
@ -118,41 +88,15 @@ async fn test_user_agent_has_version() -> Result<()> {
});
// Wait for the server task to complete, to be a good citizen.
server_task.await?;
let _ = server_task.await?;
Ok(())
}
#[tokio::test]
async fn test_user_agent_has_linehaul() -> Result<()> {
// Set up the TCP listener on a random available port
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
// Spawn the server loop in a background task
let server_task = tokio::spawn(async move {
let svc = service_fn(move |req: Request<hyper::body::Incoming>| {
// Get User Agent Header and send it back in the response
let user_agent = req
.headers()
.get(USER_AGENT)
.and_then(|v| v.to_str().ok())
.map(ToString::to_string)
.unwrap_or_default(); // Empty Default
future::ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from(user_agent))))
});
// Start Server (not wrapped in loop {} since we want a single response server)
// If you want server to accept multiple connections, wrap it in loop {}
let (socket, _) = listener.accept().await.unwrap();
let socket = TokioIo::new(socket);
tokio::task::spawn(async move {
http1::Builder::new()
.serve_connection(socket, svc)
.with_upgrades()
.await
.expect("Server Started");
});
});
// Initialize dummy http server
let (server_task, addr) = start_http_user_agent_server().await?;
// Add some representative markers for an Ubuntu CI runner
let markers = MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
@ -167,8 +111,7 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
python_full_version: "3.12.2",
python_version: "3.12",
sys_platform: "linux",
})
.unwrap();
})?;
// Initialize uv-client
let cache = Cache::temp()?.init()?;
@ -213,7 +156,7 @@ async fn test_user_agent_has_linehaul() -> Result<()> {
let body = res.text().await?;
// Wait for the server task to complete, to be a good citizen.
server_task.await?;
let _ = server_task.await?;
// Unpack User-Agent with linehaul
let (uv_version, uv_linehaul) = body

View File

@ -24,6 +24,7 @@ bitflags::bitflags! {
const INIT_PROJECT_FLAG = 1 << 12;
const WORKSPACE_METADATA = 1 << 13;
const WORKSPACE_DIR = 1 << 14;
const WORKSPACE_LIST = 1 << 15;
}
}
@ -48,6 +49,7 @@ impl PreviewFeatures {
Self::INIT_PROJECT_FLAG => "init-project-flag",
Self::WORKSPACE_METADATA => "workspace-metadata",
Self::WORKSPACE_DIR => "workspace-dir",
Self::WORKSPACE_LIST => "workspace-list",
_ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
}
}
@ -100,6 +102,7 @@ impl FromStr for PreviewFeatures {
"init-project-flag" => Self::INIT_PROJECT_FLAG,
"workspace-metadata" => Self::WORKSPACE_METADATA,
"workspace-dir" => Self::WORKSPACE_DIR,
"workspace-list" => Self::WORKSPACE_LIST,
_ => {
warn_user_once!("Unknown preview feature: `{part}`");
continue;

View File

@ -63,45 +63,69 @@ impl DisplaySafeUrl {
pub fn parse(input: &str) -> Result<Self, DisplaySafeUrlError> {
let url = Url::parse(input)?;
// Reject some ambiguous cases, e.g., `https://user/name:password@domain/a/b/c`
//
// In this case the user *probably* meant to have a username of "user/name", but both RFC
// 3986 and WHATWG URL expect the userinfo (RFC 3986) or authority (WHATWG) to not contain a
// non-percent-encoded slash or other special character.
//
// This ends up being moderately annoying to detect, since the above gets parsed into a
// "valid" WHATWG URL where the host is `used` and the pathname is
// `/name:password@domain/a/b/c` rather than causing a parse error.
//
// To detect it, we use a heuristic: if the password component is missing but the path or
// fragment contain a `:` followed by a `@`, then we assume the URL is ambiguous.
if url.password().is_none()
&& (url
.path()
.find(':')
.is_some_and(|pos| url.path()[pos..].contains('@'))
|| url
.fragment()
.map(|fragment| {
fragment
.find(':')
.is_some_and(|pos| fragment[pos..].contains('@'))
})
.unwrap_or(false))
// If the above is true, we should always expect to find these in the given URL
&& let Some(col_pos) = input.find(':')
&& let Some(at_pos) = input.rfind('@')
{
// Our ambiguous URL probably has credentials in it, so we don't want to blast it out in
// the error message. We somewhat aggressively replace everything between the scheme's
// ':' and the lastmost `@` with `***`.
let redacted_path = format!("{}***{}", &input[0..=col_pos], &input[at_pos..]);
return Err(DisplaySafeUrlError::AmbiguousAuthority(redacted_path));
}
Self::reject_ambiguous_credentials(input, &url)?;
Ok(Self(url))
}
/// Reject some ambiguous cases, e.g., `https://user/name:password@domain/a/b/c`
///
/// In this case the user *probably* meant to have a username of "user/name", but both RFC
/// 3986 and WHATWG URL expect the userinfo (RFC 3986) or authority (WHATWG) to not contain a
/// non-percent-encoded slash or other special character.
///
/// This ends up being moderately annoying to detect, since the above gets parsed into a
/// "valid" WHATWG URL where the host is `used` and the pathname is
/// `/name:password@domain/a/b/c` rather than causing a parse error.
///
/// To detect it, we use a heuristic: if the password component is missing but the path or
/// fragment contain a `:` followed by a `@`, then we assume the URL is ambiguous.
fn reject_ambiguous_credentials(input: &str, url: &Url) -> Result<(), DisplaySafeUrlError> {
// `git://`, `http://`, and `https://` URLs may carry credentials, while `file://` URLs
// on Windows may contain both sigils, but it's always safe, e.g.
// `file://C:/Users/ferris/project@home/workspace`.
if url.scheme() == "file" {
return Ok(());
}
if url.password().is_some() {
return Ok(());
}
// Check for the suspicious pattern.
if !url
.path()
.find(':')
.is_some_and(|pos| url.path()[pos..].contains('@'))
&& !url
.fragment()
.map(|fragment| {
fragment
.find(':')
.is_some_and(|pos| fragment[pos..].contains('@'))
})
.unwrap_or(false)
{
return Ok(());
}
// If the previous check passed, we should always expect to find these in the given URL.
let (Some(col_pos), Some(at_pos)) = (input.find(':'), input.rfind('@')) else {
if cfg!(debug_assertions) {
unreachable!(
"`:` or `@` sign missing in URL that was confirmed to contain them: {input}"
);
}
return Ok(());
};
// Our ambiguous URL probably has credentials in it, so we don't want to blast it out in
// the error message. We somewhat aggressively replace everything between the scheme's
// ':' and the lastmost `@` with `***`.
let redacted_path = format!("{}***{}", &input[0..=col_pos], &input[at_pos..]);
Err(DisplaySafeUrlError::AmbiguousAuthority(redacted_path))
}
/// Create a new [`DisplaySafeUrl`] from a [`Url`].
///
/// Unlike [`Self::parse`], this doesn't perform any ambiguity checks.
@ -453,4 +477,15 @@ mod tests {
}
}
}
#[test]
fn parse_url_not_ambiguous() {
#[allow(clippy::single_element_loop)]
for url in &[
// https://github.com/astral-sh/uv/issues/16756
"file:///C:/jenkins/ython_Environment_Manager_PR-251@2/venv%201/workspace",
] {
DisplaySafeUrl::parse(url).unwrap();
}
}
}

View File

@ -586,9 +586,19 @@ impl EnvVars {
pub const XDG_BIN_HOME: &'static str = "XDG_BIN_HOME";
/// Custom certificate bundle file path for SSL connections.
///
/// Takes precedence over `UV_NATIVE_TLS` when set.
#[attr_added_in("0.1.14")]
pub const SSL_CERT_FILE: &'static str = "SSL_CERT_FILE";
/// Custom path for certificate bundles for SSL connections.
/// Multiple entries are supported separated using a platform-specific
/// delimiter (`:` on Unix, `;` on Windows).
///
/// Takes precedence over `UV_NATIVE_TLS` when set.
#[attr_added_in("0.9.10")]
pub const SSL_CERT_DIR: &'static str = "SSL_CERT_DIR";
/// If set, uv will use this file for mTLS authentication.
/// This should be a single file containing both the certificate and the private key in PEM format.
#[attr_added_in("0.2.11")]

View File

@ -1,6 +1,6 @@
[package]
name = "uv-version"
version = "0.9.9"
version = "0.9.10"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }

View File

@ -1,6 +1,6 @@
[package]
name = "uv"
version = "0.9.9"
version = "0.9.10"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }

View File

@ -69,6 +69,7 @@ use uv_python::PythonEnvironment;
use uv_scripts::Pep723Script;
pub(crate) use venv::venv;
pub(crate) use workspace::dir::dir;
pub(crate) use workspace::list::list;
pub(crate) use workspace::metadata::metadata;
use crate::printer::Printer;

View File

@ -0,0 +1,36 @@
use std::fmt::Write;
use std::path::Path;
use anyhow::Result;
use owo_colors::OwoColorize;
use uv_preview::{Preview, PreviewFeatures};
use uv_warnings::warn_user;
use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache};
use crate::commands::ExitStatus;
use crate::printer::Printer;
/// List workspace members
pub(crate) async fn list(
project_dir: &Path,
preview: Preview,
printer: Printer,
) -> Result<ExitStatus> {
if !preview.is_enabled(PreviewFeatures::WORKSPACE_LIST) {
warn_user!(
"The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeatures::WORKSPACE_LIST
);
}
let workspace_cache = WorkspaceCache::default();
let workspace =
Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache).await?;
for name in workspace.packages().keys() {
writeln!(printer.stdout(), "{}", name.cyan())?;
}
Ok(ExitStatus::Success)
}

View File

@ -1,2 +1,3 @@
pub(crate) mod dir;
pub(crate) mod list;
pub(crate) mod metadata;

View File

@ -1746,6 +1746,9 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
WorkspaceCommand::Dir(args) => {
commands::dir(args.package, &project_dir, globals.preview, printer).await
}
WorkspaceCommand::List(_args) => {
commands::list(&project_dir, globals.preview, printer).await
}
},
Commands::BuildBackend { command } => spawn_blocking(move || match command {
BuildBackendCommand::BuildSdist { sdist_directory } => {

View File

@ -1093,6 +1093,14 @@ impl TestContext {
command
}
/// Create a `uv workspace list` command with options shared across scenarios.
pub fn workspace_list(&self) -> Command {
let mut command = Self::new_command();
command.arg("workspace").arg("list");
self.add_shared_options(&mut command, false);
command
}
/// Create a `uv export` command with options shared across scenarios.
pub fn export(&self) -> Command {
let mut command = Self::new_command();

View File

@ -142,4 +142,5 @@ mod workflow;
mod extract;
mod workspace;
mod workspace_dir;
mod workspace_list;
mod workspace_metadata;

View File

@ -7831,7 +7831,7 @@ fn preview_features() {
show_settings: true,
preview: Preview {
flags: PreviewFeatures(
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR,
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST,
),
},
python_preference: Managed,
@ -8059,7 +8059,7 @@ fn preview_features() {
show_settings: true,
preview: Preview {
flags: PreviewFeatures(
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR,
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST,
),
},
python_preference: Managed,

View File

@ -0,0 +1,220 @@
use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::fixture::PathChild;
use crate::common::{TestContext, copy_dir_ignore, uv_snapshot};
/// Test basic list output for a simple workspace with one member.
#[test]
fn workspace_list_simple() {
let context = TestContext::new("3.12");
// Initialize a workspace with one member
context.init().arg("foo").assert().success();
let workspace = context.temp_dir.child("foo");
uv_snapshot!(context.filters(), context.workspace_list().current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
foo
----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);
}
/// Test list output for a root workspace (workspace with a root package).
#[test]
fn workspace_list_root_workspace() -> Result<()> {
let context = TestContext::new("3.12");
let workspace = context.temp_dir.child("workspace");
copy_dir_ignore(
context
.workspace_root
.join("scripts/workspaces/albatross-root-workspace"),
&workspace,
)?;
uv_snapshot!(context.filters(), context.workspace_list().current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
albatross
bird-feeder
seeds
----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);
Ok(())
}
/// Test list output for a virtual workspace (no root package).
#[test]
fn workspace_list_virtual_workspace() -> Result<()> {
let context = TestContext::new("3.12");
let workspace = context.temp_dir.child("workspace");
copy_dir_ignore(
context
.workspace_root
.join("scripts/workspaces/albatross-virtual-workspace"),
&workspace,
)?;
uv_snapshot!(context.filters(), context.workspace_list().current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
albatross
bird-feeder
seeds
----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);
Ok(())
}
/// Test list output when run from a workspace member directory.
#[test]
fn workspace_list_from_member() -> Result<()> {
let context = TestContext::new("3.12");
let workspace = context.temp_dir.child("workspace");
copy_dir_ignore(
context
.workspace_root
.join("scripts/workspaces/albatross-root-workspace"),
&workspace,
)?;
let member_dir = workspace.join("packages").join("bird-feeder");
uv_snapshot!(context.filters(), context.workspace_list().current_dir(&member_dir), @r"
success: true
exit_code: 0
----- stdout -----
albatross
bird-feeder
seeds
----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);
Ok(())
}
/// Test list output for a workspace with multiple packages.
#[test]
fn workspace_list_multiple_members() {
let context = TestContext::new("3.12");
// Initialize workspace root
context.init().arg("pkg-a").assert().success();
let workspace_root = context.temp_dir.child("pkg-a");
// Add more members
context
.init()
.arg("pkg-b")
.current_dir(&workspace_root)
.assert()
.success();
context
.init()
.arg("pkg-c")
.current_dir(&workspace_root)
.assert()
.success();
uv_snapshot!(context.filters(), context.workspace_list().current_dir(&workspace_root), @r"
success: true
exit_code: 0
----- stdout -----
pkg-a
pkg-b
pkg-c
----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);
}
/// Test list output for a single project (not a workspace).
#[test]
fn workspace_list_single_project() {
let context = TestContext::new("3.12");
context.init().arg("my-project").assert().success();
let project = context.temp_dir.child("my-project");
uv_snapshot!(context.filters(), context.workspace_list().current_dir(&project), @r"
success: true
exit_code: 0
----- stdout -----
my-project
----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);
}
/// Test list output with excluded packages.
#[test]
fn workspace_list_with_excluded() -> Result<()> {
let context = TestContext::new("3.12");
let workspace = context.temp_dir.child("workspace");
copy_dir_ignore(
context
.workspace_root
.join("scripts/workspaces/albatross-project-in-excluded"),
&workspace,
)?;
uv_snapshot!(context.filters(), context.workspace_list().current_dir(&workspace), @r"
success: true
exit_code: 0
----- stdout -----
albatross
----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
"
);
Ok(())
}
/// Test list error output when not in a project.
#[test]
fn workspace_list_no_project() {
let context = TestContext::new("3.12");
uv_snapshot!(context.filters(), context.workspace_list(), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: The `uv workspace list` command is experimental and may change without warning. Pass `--preview-features workspace-list` to disable this warning.
error: No `pyproject.toml` found in current directory or any parent directory
"
);
}

View File

@ -31,7 +31,7 @@ To use uv as a build backend in an existing project, add `uv_build` to the
```toml title="pyproject.toml"
[build-system]
requires = ["uv_build>=0.9.9,<0.10.0"]
requires = ["uv_build>=0.9.10,<0.10.0"]
build-backend = "uv_build"
```

View File

@ -42,13 +42,14 @@ default = true
`pyproject.toml` files are present in a directory, configuration will be read from `uv.toml`, and
`[tool.uv]` section in the accompanying `pyproject.toml` will be ignored.
uv will also discover user-level configuration at `~/.config/uv/uv.toml` (or
`$XDG_CONFIG_HOME/uv/uv.toml`) on macOS and Linux, or `%APPDATA%\uv\uv.toml` on Windows; and
system-level configuration at `/etc/uv/uv.toml` (or `$XDG_CONFIG_DIRS/uv/uv.toml`) on macOS and
Linux, or `%SYSTEMDRIVE%\ProgramData\uv\uv.toml` on Windows.
uv will also discover `uv.toml` configuration files in the user- and system-level
[configuration directories](../reference/storage.md#configuration-directories), e.g., user-level
configuration in `~/.config/uv/uv.toml`, and system-level configuration at `/etc/uv/uv.toml` on
macOS and Linux.
User-and system-level configuration must use the `uv.toml` format, rather than the `pyproject.toml`
format, as a `pyproject.toml` is intended to define a Python _project_.
!!! important
User- and system-level configuration files cannot use the `pyproject.toml` format.
If project-, user-, and system-level configuration files are found, the settings will be merged,
with project-level configuration taking precedence over the user-level configuration, and user-level

View File

@ -74,6 +74,7 @@ The following preview features are available:
[system-native location](../concepts/authentication/http.md#the-uv-credentials-store).
- `workspace-metadata`: Allows using `uv workspace metadata`.
- `workspace-dir`: Allows using `uv workspace dir`.
- `workspace-list`: Allows using `uv workspace list`.
## Disabling preview features

View File

@ -113,7 +113,7 @@ dependencies = []
example-pkg = "example_pkg:main"
[build-system]
requires = ["uv_build>=0.9.9,<0.10.0"]
requires = ["uv_build>=0.9.10,<0.10.0"]
build-backend = "uv_build"
```
@ -136,7 +136,7 @@ dependencies = []
example-pkg = "example_pkg:main"
[build-system]
requires = ["uv_build>=0.9.9,<0.10.0"]
requires = ["uv_build>=0.9.10,<0.10.0"]
build-backend = "uv_build"
```
@ -197,7 +197,7 @@ requires-python = ">=3.11"
dependencies = []
[build-system]
requires = ["uv_build>=0.9.9,<0.10.0"]
requires = ["uv_build>=0.9.10,<0.10.0"]
build-backend = "uv_build"
```

View File

@ -75,7 +75,7 @@ bird-feeder = { workspace = true }
members = ["packages/*"]
[build-system]
requires = ["uv_build>=0.9.9,<0.10.0"]
requires = ["uv_build>=0.9.10,<0.10.0"]
build-backend = "uv_build"
```
@ -106,7 +106,7 @@ tqdm = { git = "https://github.com/tqdm/tqdm" }
members = ["packages/*"]
[build-system]
requires = ["uv_build>=0.9.9,<0.10.0"]
requires = ["uv_build>=0.9.10,<0.10.0"]
build-backend = "uv_build"
```
@ -188,7 +188,7 @@ dependencies = ["bird-feeder", "tqdm>=4,<5"]
bird-feeder = { path = "packages/bird-feeder" }
[build-system]
requires = ["uv_build>=0.9.9,<0.10.0"]
requires = ["uv_build>=0.9.10,<0.10.0"]
build-backend = "uv_build"
```

View File

@ -121,10 +121,15 @@ present, uv will install all the Python versions listed in the file.
The available Python versions are frozen for each uv release. To install new Python versions,
you may need upgrade uv.
See the [storage documentation](../reference/storage.md#python-versions) for details about where
installed Python versions are stored.
### Installing Python executables
uv installs Python executables into your `PATH` by default, e.g., `uv python install 3.12` will
install a Python executable into `~/.local/bin`, e.g., as `python3.12`.
uv installs Python executables into your `PATH` by default, e.g., on Unix `uv python install 3.12`
will install a Python executable into `~/.local/bin`, e.g., as `python3.12`. See the
[storage documentation](../reference/storage.md#python-executables) for more details about the
target directory.
!!! tip

View File

@ -35,9 +35,14 @@ treated as disposable, i.e., if you run `uv cache clean` the environment will be
environment is only cached to reduce the overhead of repeated invocations. If the environment is
removed, a new one will be created automatically.
When installing a tool with `uv tool install`, a virtual environment is created in the uv tools
directory. The environment will not be removed unless the tool is uninstalled. If the environment is
manually deleted, the tool will fail to run.
When installing a tool with `uv tool install`, a virtual environment is created in the
[uv tools directory](../reference/storage.md#tools). The environment will not be removed unless the
tool is uninstalled. If the environment is manually deleted, the tool will fail to run.
!!! important
Tool environments are _not_ intended to be mutated directly. It is strongly recommended never to
mutate a tool environment manually, e.g., with a `pip` operation.
## Tool versions
@ -109,26 +114,6 @@ $ uv tool install ruff@latest
$ uv tool install ruff@0.6.0
```
## Tools directory
By default, the uv tools directory is named `tools` and is in the uv application state directory,
e.g., `~/.local/share/uv/tools`. The location may be customized with the `UV_TOOL_DIR` environment
variable.
To display the path to the tool installation directory:
```console
$ uv tool dir
```
Tool environments are placed in a directory with the same name as the tool package, e.g.,
`.../tools/<name>`.
!!! important
Tool environments are _not_ intended to be mutated directly. It is strongly recommended never to
mutate a tool environment manually, e.g., with a `pip` operation.
## Upgrading tools
Tool environments may be upgraded via `uv tool upgrade`, or re-created entirely via subsequent
@ -259,35 +244,23 @@ tool may be unusable.
## Tool executables
Tool executables include all console entry points, script entry points, and binary scripts provided
by a Python package. Tool executables are symlinked into the `bin` directory on Unix and copied on
Windows.
by a Python package. Tool executables are symlinked into the
[executable directory](../reference/storage.md#tool-executables) on Unix and copied on Windows.
### The `bin` directory
!!! note
Executables are installed into the user `bin` directory following the XDG standard, e.g.,
`~/.local/bin`. Unlike other directory schemes in uv, the XDG standard is used on _all platforms_
notably including Windows and macOS — there is no clear alternative location to place executables on
these platforms. The installation directory is determined from the first available environment
variable:
Executables provided by dependencies of tool packages are not installed.
- `$UV_TOOL_BIN_DIR`
- `$XDG_BIN_HOME`
- `$XDG_DATA_HOME/../bin`
- `$HOME/.local/bin`
Executables provided by dependencies of tool packages are not installed.
### The `PATH`
The `bin` directory must be in the `PATH` variable for tool executables to be available from the
shell. If it is not in the `PATH`, a warning will be displayed. The `uv tool update-shell` command
can be used to add the `bin` directory to the `PATH` in common shell configuration files.
The [executable directory](../reference/storage.md#executable-directory) must be in the `PATH`
variable for tool executables to be available from the shell. If it is not in the `PATH`, a warning
will be displayed. The `uv tool update-shell` command can be used to add the executable directory to
the `PATH` in common shell configuration files.
### Overwriting executables
Installation of tools will not overwrite executables in the `bin` directory that were not previously
installed by uv. For example, if `pipx` has been used to install a tool, `uv tool install` will
fail. The `--force` flag can be used to override this behavior.
Installation of tools will not overwrite executables in the executable directory that were not
previously installed by uv. For example, if `pipx` has been used to install a tool,
`uv tool install` will fail. The `--force` flag can be used to override this behavior.
## Relationship to `uv run`

View File

@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv:
Request a specific version by including it in the URL:
```console
$ curl -LsSf https://astral.sh/uv/0.9.9/install.sh | sh
$ curl -LsSf https://astral.sh/uv/0.9.10/install.sh | sh
```
=== "Windows"
@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv:
Request a specific version by including it in the URL:
```pwsh-session
PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.9.9/install.ps1 | iex"
PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.9.10/install.ps1 | iex"
```
!!! tip
@ -260,7 +260,8 @@ If you need to remove uv from your system, follow these steps:
!!! tip
Before removing the binaries, you may want to remove any data that uv has stored.
Before removing the binaries, you may want to remove any data that uv has stored. See the
[storage reference](../reference/storage.md) for details on where uv stores data.
2. Remove the uv, uvx, and uvw binaries:

View File

@ -92,7 +92,7 @@ the second stage, we'll copy this directory over to the final image, omitting th
other unnecessary files.
```dockerfile title="Dockerfile"
FROM ghcr.io/astral-sh/uv:0.9.9 AS uv
FROM ghcr.io/astral-sh/uv:0.9.10 AS uv
# First, bundle the dependencies into the task root.
FROM public.ecr.aws/lambda/python:3.13 AS builder
@ -334,7 +334,7 @@ And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hell
Finally, we'll update the Dockerfile to include the local library in the deployment package:
```dockerfile title="Dockerfile"
FROM ghcr.io/astral-sh/uv:0.9.9 AS uv
FROM ghcr.io/astral-sh/uv:0.9.10 AS uv
# First, bundle the dependencies into the task root.
FROM public.ecr.aws/lambda/python:3.13 AS builder

View File

@ -31,7 +31,7 @@ $ docker run --rm -it ghcr.io/astral-sh/uv:debian uv --help
The following distroless images are available:
- `ghcr.io/astral-sh/uv:latest`
- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.9.9`
- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.9.10`
- `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.8` (the latest patch
version)
@ -95,7 +95,7 @@ And the following derived images are available:
As with the distroless image, each derived image is published with uv version tags as
`ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and
`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.9.9-alpine`.
`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.9.10-alpine`.
In addition, starting with `0.8` each derived image also sets `UV_TOOL_BIN_DIR` to `/usr/local/bin`
to allow `uv tool install` to work as expected with the default user.
@ -136,7 +136,7 @@ Note this requires `curl` to be available.
In either case, it is best practice to pin to a specific uv version, e.g., with:
```dockerfile
COPY --from=ghcr.io/astral-sh/uv:0.9.9 /uv /uvx /bin/
COPY --from=ghcr.io/astral-sh/uv:0.9.10 /uv /uvx /bin/
```
!!! tip
@ -154,7 +154,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.9.9 /uv /uvx /bin/
Or, with the installer:
```dockerfile
ADD https://astral.sh/uv/0.9.9/install.sh /uv-installer.sh
ADD https://astral.sh/uv/0.9.10/install.sh /uv-installer.sh
```
### Installing a project
@ -590,5 +590,5 @@ Verified OK
!!! tip
These examples use `latest`, but best practice is to verify the attestation for a specific
version tag, e.g., `ghcr.io/astral-sh/uv:0.9.9`, or (even better) the specific image digest,
version tag, e.g., `ghcr.io/astral-sh/uv:0.9.10`, or (even better) the specific image digest,
such as `ghcr.io/astral-sh/uv:0.5.27@sha256:5adf09a5a526f380237408032a9308000d14d5947eafa687ad6c6a2476787b4f`.

View File

@ -113,7 +113,7 @@ WORKDIR /app
RUN uv sync --frozen --no-cache
# Run the application.
CMD ["/app/.venv/bin/fastapi", "run", "main.py", "--port", "80", "--host", "0.0.0.0"]
CMD ["/app/.venv/bin/fastapi", "run", "app/main.py", "--port", "80", "--host", "0.0.0.0"]
```
Build the Docker image with:

View File

@ -47,7 +47,7 @@ jobs:
uses: astral-sh/setup-uv@v6
with:
# Install a specific version of uv.
version: "0.9.9"
version: "0.9.10"
```
## Setting up Python

View File

@ -19,7 +19,7 @@ To make sure your `uv.lock` file is up to date even if your `pyproject.toml` fil
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.9.9
rev: 0.9.10
hooks:
- id: uv-lock
```
@ -30,7 +30,7 @@ To keep a `requirements.txt` file in sync with your `uv.lock` file:
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.9.9
rev: 0.9.10
hooks:
- id: uv-export
```
@ -41,7 +41,7 @@ To compile requirements files:
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.9.9
rev: 0.9.10
hooks:
# Compile requirements
- id: pip-compile
@ -54,7 +54,7 @@ To compile alternative requirements files, modify `args` and `files`:
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.9.9
rev: 0.9.10
hooks:
# Compile requirements
- id: pip-compile
@ -68,7 +68,7 @@ To run the hook over multiple files at the same time, add additional entries:
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.9.9
rev: 0.9.10
hooks:
# Compile requirements
- id: pip-compile

View File

@ -1004,11 +1004,22 @@ the fact that Windows' real main thread is only 1MB. That thread has size
The standard `SHELL` posix env var.
### `SSL_CERT_DIR`
<small class="added-in">added in `0.9.10`</small>
Custom path for certificate bundles for SSL connections.
Multiple entries are supported separated using a platform-specific
delimiter (`:` on Unix, `;` on Windows).
Takes precedence over `UV_NATIVE_TLS` when set.
### `SSL_CERT_FILE`
<small class="added-in">added in `0.1.14`</small>
Custom certificate bundle file path for SSL connections.
Takes precedence over `UV_NATIVE_TLS` when set.
### `SSL_CLIENT_CERT`
<small class="added-in">added in `0.2.11`</small>

View File

@ -5,6 +5,7 @@ The reference section provides information about specific parts of uv:
- [Commands](./cli.md): A reference for uv's command line interface.
- [Settings](./settings.md): A reference for uv's configuration schema.
- [Resolver](./internals/resolver.md): Details about the internals of uv's resolver.
- [Storage](./storage.md): Information about where uv stores data on your system.
- [Policies](./policies/index.md): uv's versioning policy, platform support policy, and license.
Looking for a broader overview? Check out the [concepts](../concepts/index.md) documentation.

View File

@ -2,9 +2,7 @@
## Changing the installation path
By default, uv is installed to `~/.local/bin`. If `XDG_BIN_HOME` is set, it will be used instead.
Similarly, if `XDG_DATA_HOME` is set, the target directory will be inferred as
`XDG_DATA_HOME/../bin`.
By default, uv is installed in the user [executable directory](./storage.md#executable-directory).
To change the installation path, use `UV_INSTALL_DIR`:
@ -20,6 +18,12 @@ To change the installation path, use `UV_INSTALL_DIR`:
PS> powershell -ExecutionPolicy ByPass -c {$env:UV_INSTALL_DIR = "C:\Custom\Path";irm https://astral.sh/uv/install.ps1 | iex}
```
!!! note
Changing the installation path only affects where the uv binary is installed. uv will still store
its data (cache, Python installations, tools, etc.) in the default locations. See the
[storage reference](./storage.md) for details on these locations and how to customize them.
## Disabling shell modifications
The installer may also update your shell profiles to ensure the uv binary is on your `PATH`. To

200
docs/reference/storage.md Normal file
View File

@ -0,0 +1,200 @@
# Storage
## Storage directories
uv uses the following high-level directories for storage.
For each location, uv checks for the existence of environment variables in the given order and uses
the first path found.
The paths of storage directories are platform-specific. uv follows the
[XDG](https://specifications.freedesktop.org/basedir-spec/latest/) conventions on Linux and macOS
and the [Known Folder](https://learn.microsoft.com/en-us/windows/win32/shell/known-folders) scheme
on Windows.
### Temporary directory
The temporary directory is used for ephemeral data.
=== "Unix"
1. `$TMPDIR`
1. `/tmp`
=== "Windows"
1. `%TMP%`
1. `%TEMP%`
1. `%USERPROFILE%`
### Cache directory
The cache directory is used for data that is disposable, but is useful to be long-lived.
=== "Unix"
1. `$XDG_CACHE_HOME/uv`
1. `$HOME/.cache/uv`
=== "Windows"
1. `%LOCALAPPDATA%\uv\cache`
1. `uv\cache` within [`FOLDERID_LocalAppData`](https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#FOLDERID_LocalAppData)
### Persistent data directory
The persistent data directory is used for non-disposable data.
=== "Unix"
1. `$XDG_DATA_HOME/uv`
1. `$HOME/.local/share/uv`
1. `$CWD/.uv`
=== "Windows"
1. `%APPDATA%\uv\data`
1. `.\.uv`
### Configuration directories
The configuration directories are used to store changes to uv's settings.
User-level configuration
=== "Unix"
1. `$XDG_CONFIG_HOME/uv`
1. `$HOME/.config/uv`
=== "Windows"
1. `%APPDATA%\uv`
1. `uv` within [`FOLDERID_RoamingAppData`](https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#FOLDERID_RoamingAppData)
System-level configuration
=== "Unix"
1. `$XDG_CONFIG_DIRS/uv`
1. `/etc/uv`
=== "Windows"
1. `%PROGRAMDATA%\uv`
1. `uv` within [`FOLDERID_AppDataProgramData`](https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#FOLDERID_AppDataProgramData)
### Executable directory
The executable directory is used to store files that can be run by the user, i.e., a directory that
should be on the `PATH`.
=== "Unix"
1. `$XDG_BIN_HOME`
1. `$XDG_DATA_HOME/../bin`
1. `$HOME/.local/bin`
=== "Windows"
1. `%XDG_BIN_HOME%`
1. `%XDG_DATA_HOME%\..\bin`
1. `%USERPROFILE%\.local\bin`
## Types of data
### Dependency cache
uv uses a local cache to avoid re-downloading and re-building dependencies.
By default, the cache is stored in the [cache directory](#cache-directory) but it can be overridden
via command line arguments, environment variables, or settings as detailed in
[the cache documentation](../concepts/cache.md#cache-directory). When the cache is disabled, the
cache will be stored in a [temporary directory](#temporary-directory).
Use `uv cache dir` to show the current cache directory path.
!!! important
For optimal performance, the cache directory needs to be on the same filesystem as virtual
environments.
### Python versions
uv can install managed [Python versions](../concepts/python-versions.md), e.g., with
`uv python install`.
By default, Python versions managed by uv are stored in a `python/` subdirectory of the
[persistent data directory](#persistent-data-directory), e.g., `~/.local/share/uv/python`.
Use `uv python dir` to show the Python installation directory.
Use the `UV_PYTHON_INSTALL_DIR` environment variable to override the installation directory.
!!! note
Changing where Python is installed will not be automatically reflected in existing virtual environments; they will keep referring to the old location, and will need to be updated manually (e.g. by re-creating them).
### Python executables
uv installs executables for [Python versions](#python-versions), e.g., `python3.13`.
By default, Python executables are stored in the [executable directory](#executable-directory).
Use `uv python dir --bin` to show the Python executable directory.
Use the `UV_PYTHON_BIN_DIR` environment variable to override the Python executable directory.
### Tools
uv can install Python packages as [command-line tools](../concepts/tools.md) using
`uv tool install`.
By default, tools are installed in a `tools/` subdirectory of the
[persistent data directory](#persistent-data-directory), e.g., `~/.local/share/uv/tools`.
Use `uv tool dir` to show the tool installation directory.
Use the `UV_TOOL_DIR` environment variable to configure the installation directory.
### Tool executables
uv installs executables for installed [tools](#tools), e.g., `ruff`.
By default, tool executables are stored in the [executable directory](#executable-directory).
Use `uv tool dir --bin` to show the tool executable directory.
Use the `UV_TOOL_BIN_DIR` environment variable to configure the tool executable directory.
### The uv executable
When using uv's [standalone installer](./installer.md) to install uv, the `uv` and `uvx` executables
are installed into the [executable directory](#executable-directory).
Use the `UV_INSTALL_DIR` environment variable to configure uv's installation directory.
### Configuration files
uv's behavior can be configured through TOML files.
Configuration files are discovered in the [configuration directories](#configuration-directories).
For more details, see the [configuration files documentation](../concepts/configuration-files.md).
### Project virtual environments
When working on [projects](../concepts/projects/index.md), uv creates a dedicated virtual
environment for each project.
By default, project virtual environments are created in `.venv` in the project or workspace root,
i.e., next to the `pyproject.toml`.
Use the `UV_PROJECT_ENVIRONMENT` environment variable to override this location. For more details,
see the
[projects environment documentation](../concepts/projects/config.md#project-environment-path).
### Script virtual environments
When running [scripts with inline metadata](../guides/scripts.md), uv creates a dedicated virtual
environment for each script in the [cache directory](#cache-directory).

View File

@ -144,6 +144,7 @@ plugins:
Reference:
- reference/cli.md
- reference/settings.md
- reference/storage.md
- reference/environment.md
- reference/installer.md
extra_css:
@ -236,6 +237,7 @@ nav:
- Commands: reference/cli.md
- Settings: reference/settings.md
- Environment variables: reference/environment.md
- Storage: reference/storage.md
- Installer options: reference/installer.md
- Troubleshooting:
- reference/troubleshooting/index.md

View File

@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "uv"
version = "0.9.9"
version = "0.9.10"
description = "An extremely fast Python package and project manager, written in Rust."
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
requires-python = ">=3.8"