Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1c41ca1
Drop PPC64 (big endian) builds (#17626)
konstin Jan 26, 2026
d2c7f93
Stabilize `uv add --bounds` and the `add-bounds` configuration option…
konstin Jan 27, 2026
f290123
chore: remove bookworm, alpine 3.21, and py38 published images (#17755)
zanieb Jan 30, 2026
b5f08b6
Install PyPy and GraalPy executables as `pypy` and `graalpy` instead …
zanieb Jan 30, 2026
7501c51
Error if multiple indexes include `default = true` (#17011)
charliermarsh Jan 30, 2026
1d809ad
Fix `lock_check_multiple_default_indexes_explicit_assignment_dependen…
zanieb Jan 30, 2026
f8af7ef
Skip generating `activate.csh` for relocatable virtualenvs (#17759)
zanieb Jan 30, 2026
1696514
Stabilize `extra-build-dependencies` (#17767)
zanieb Jan 30, 2026
cb0c342
Stabilize `workspace list` and `workspace dir` (#17768)
zanieb Jan 30, 2026
d92d00a
Install Pyodide executables as `pyodide` instead of `python` (#17760)
zanieb Jan 30, 2026
0b7ef33
Error when an `explicit` index is unnamed (#17777)
zanieb Jan 31, 2026
79eba59
Require `--clear` to remove virtual environments (#17757)
zanieb Feb 1, 2026
955eaf0
Respect global Python version pins in `uv tool run` and `uv tool inst…
zanieb Feb 3, 2026
62baa7a
Bump the `uv format` ruff version to 0.15.0 (#17838)
zanieb Feb 3, 2026
02eb1cd
Bail when trying to attach ambiguous credentials instead of picking a…
zsol Feb 4, 2026
958b323
Avoid invalidating the lockfile versions after an exclude newer chang…
zanieb Feb 5, 2026
42badd1
Update uv test features to use `test-` as a prefix (#17860)
zanieb Feb 5, 2026
83846a9
Stabilize Python upgrades (#17766)
zanieb Feb 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 2 additions & 19 deletions .github/workflows/build-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,19 +170,16 @@ jobs:
# Mapping of base image followed by a comma followed by one or more base tags (comma separated)
# Note, org.opencontainers.image.version label will use the first base tag (use the most specific tag first)
image-mapping:
- alpine:3.22,alpine3.22,alpine
- alpine:3.21,alpine3.21
- alpine:3.23,alpine3.23,alpine
- alpine:3.22,alpine3.22
- debian:trixie-slim,trixie-slim,debian-slim
- buildpack-deps:trixie,trixie,debian
- debian:bookworm-slim,bookworm-slim
- buildpack-deps:bookworm,bookworm
- python:3.14-alpine3.23,python3.14-alpine3.23,python3.14-alpine
- python:3.13-alpine3.23,python3.13-alpine3.23,python3.13-alpine
- python:3.12-alpine3.23,python3.12-alpine3.23,python3.12-alpine
- python:3.11-alpine3.23,python3.11-alpine3.23,python3.11-alpine
- python:3.10-alpine3.23,python3.10-alpine3.23,python3.10-alpine
- python:3.9-alpine3.22,python3.9-alpine3.22,python3.9-alpine
- python:3.8-alpine3.20,python3.8-alpine3.20,python3.8-alpine
- python:3.14-trixie,python3.14-trixie
- python:3.13-trixie,python3.13-trixie
- python:3.12-trixie,python3.12-trixie
Expand All @@ -195,20 +192,6 @@ jobs:
- python:3.11-slim-trixie,python3.11-trixie-slim
- python:3.10-slim-trixie,python3.10-trixie-slim
- python:3.9-slim-trixie,python3.9-trixie-slim
- python:3.14-bookworm,python3.14-bookworm
- python:3.13-bookworm,python3.13-bookworm
- python:3.12-bookworm,python3.12-bookworm
- python:3.11-bookworm,python3.11-bookworm
- python:3.10-bookworm,python3.10-bookworm
- python:3.9-bookworm,python3.9-bookworm
- python:3.8-bookworm,python3.8-bookworm
- python:3.14-slim-bookworm,python3.14-bookworm-slim
- python:3.13-slim-bookworm,python3.13-bookworm-slim
- python:3.12-slim-bookworm,python3.12-bookworm-slim
- python:3.11-slim-bookworm,python3.11-bookworm-slim
- python:3.10-slim-bookworm,python3.10-bookworm-slim
- python:3.9-slim-bookworm,python3.9-bookworm-slim
- python:3.8-slim-bookworm,python3.8-bookworm-slim
steps:
# Login to DockerHub (when not pushing, it's to avoid rate-limiting)
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
Expand Down
7 changes: 0 additions & 7 deletions .github/workflows/build-release-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,6 @@ jobs:
args: --release --locked --out dist --features self-update --compatibility pypi
rust-toolchain: ${{ matrix.platform.toolchain || null }}
- uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1
if: matrix.platform.arch != 'ppc64'
name: "Test wheel"
with:
arch: ${{ matrix.platform.arch }}
Expand Down Expand Up @@ -609,7 +608,6 @@ jobs:
docker-options: ${{ matrix.platform.maturin_docker_options }}
args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi
- uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1
if: matrix.platform.arch != 'ppc64'
name: "Test wheel uv-build"
with:
arch: ${{ matrix.platform.arch }}
Expand Down Expand Up @@ -643,10 +641,6 @@ jobs:
arch: ppc64le
# see https://github.com/astral-sh/uv/issues/6528
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
- target: powerpc64-unknown-linux-gnu
arch: ppc64
# see https://github.com/astral-sh/uv/issues/6528
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -677,7 +671,6 @@ jobs:
fi
# TODO(charlie): Re-enable testing for PPC wheels.
# - uses: uraimo/run-on-arch-action@d94c13912ea685de38fccc1109385b83fd79427d # v3.0.1
# if: matrix.platform.arch != 'ppc64'
# name: "Test wheel"
# with:
# arch: ${{ matrix.platform.arch }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
run: |
cargo nextest run \
--cargo-profile fast-build \
--features python-patch,native-auth,secret-service \
--features test-python-patch,native-auth,secret-service \
--workspace \
--profile ci-linux

Expand Down Expand Up @@ -118,7 +118,7 @@ jobs:
cargo nextest run \
--cargo-profile fast-build \
--no-default-features \
--features python,python-managed,pypi,git,git-lfs,performance,crates-io,native-auth,apple-native \
--features test-python,test-python-managed,test-pypi,test-git,test-git-lfs,performance,test-crates-io,native-auth,apple-native \
--workspace \
--profile ci-macos

Expand Down Expand Up @@ -180,7 +180,7 @@ jobs:
cargo nextest run \
--cargo-profile fast-build \
--no-default-features \
--features python,pypi,python-managed,native-auth,windows-native \
--features test-python,test-pypi,test-python-managed,native-auth,windows-native \
--workspace \
--profile ci-windows \
--partition hash:${{ matrix.partition }}/3
5 changes: 1 addition & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 12 additions & 8 deletions crates/uv-auth/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -803,14 +803,18 @@ impl AuthMiddleware {
// Text credential store support.
} else if let Some(credentials) = self.text_store.get().await.and_then(|text_store| {
debug!("Checking text store for credentials for {url}");
text_store
.get_credentials(
url,
credentials
.as_ref()
.and_then(|credentials| credentials.username()),
)
.cloned()
match text_store.get_credentials(
url,
credentials
.as_ref()
.and_then(|credentials| credentials.username()),
) {
Ok(credentials) => credentials.cloned(),
Err(err) => {
debug!("Failed to get credentials from text store: {err}");
None
}
}
}) {
debug!("Found credentials in plaintext store for {url}");
Some(credentials)
Expand Down
81 changes: 59 additions & 22 deletions crates/uv-auth/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ pub enum BearerAuthError {
UnexpectedPassword,
}

#[derive(Debug, Error, PartialEq)]
pub enum LookupError {
#[error("Multiple credentials found for URL '{0}', specify which username to use")]
AmbiguousUsername(DisplaySafeUrl),
}

/// A single credential entry in a TOML credentials file.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "TomlCredentialWire", into = "TomlCredentialWire")]
Expand Down Expand Up @@ -334,7 +340,7 @@ impl TextCredentialStore {
&self,
url: &DisplaySafeUrl,
username: Option<&str>,
) -> Option<&Credentials> {
) -> Result<Option<&Credentials>, LookupError> {
let request_realm = Realm::from(url);

// Perform an exact lookup first
Expand All @@ -345,7 +351,7 @@ impl TextCredentialStore {
url_service.clone(),
Username::from(username.map(str::to_string)),
)) {
return Some(credential);
return Ok(Some(credential));
}
}

Expand Down Expand Up @@ -376,15 +382,17 @@ impl TextCredentialStore {
let specificity = service.url().path().len();
if best.is_none_or(|(best_specificity, _, _)| specificity > best_specificity) {
best = Some((specificity, service, credential));
} else if best.is_some_and(|(best_specificity, _, _)| specificity == best_specificity) {
return Err(LookupError::AmbiguousUsername(url.clone()));
}
}

// Return the most specific match
if let Some((_, _, credential)) = best {
return Some(credential);
return Ok(Some(credential));
}

None
Ok(None)
}

/// Store credentials for a given service.
Expand Down Expand Up @@ -455,10 +463,10 @@ mod tests {
let service = Service::from_str("https://example.com").unwrap();
store.insert(service.clone(), credentials.clone());
let url = DisplaySafeUrl::parse("https://example.com/").unwrap();
assert!(store.get_credentials(&url, None).is_some());
assert!(store.get_credentials(&url, None).unwrap().is_some());

let url = DisplaySafeUrl::parse("https://example.com/path").unwrap();
let retrieved = store.get_credentials(&url, None).unwrap();
let retrieved = store.get_credentials(&url, None).unwrap().unwrap();
assert_eq!(retrieved.username(), Some("user"));
assert_eq!(retrieved.password(), Some("pass"));

Expand All @@ -468,7 +476,7 @@ mod tests {
.is_some()
);
let url = DisplaySafeUrl::parse("https://example.com/").unwrap();
assert!(store.get_credentials(&url, None).is_none());
assert!(store.get_credentials(&url, None).unwrap().is_none());
}

#[tokio::test]
Expand All @@ -494,12 +502,12 @@ password = "pass2"
let store = TextCredentialStore::from_file(temp_file.path()).unwrap();

let url = DisplaySafeUrl::parse("https://example.com/").unwrap();
assert!(store.get_credentials(&url, None).is_some());
assert!(store.get_credentials(&url, None).unwrap().is_some());
let url = DisplaySafeUrl::parse("https://test.org/").unwrap();
assert!(store.get_credentials(&url, None).is_some());
assert!(store.get_credentials(&url, None).unwrap().is_some());

let url = DisplaySafeUrl::parse("https://example.com").unwrap();
let cred = store.get_credentials(&url, None).unwrap();
let cred = store.get_credentials(&url, None).unwrap().unwrap();
assert_eq!(cred.username(), Some("testuser"));
assert_eq!(cred.password(), Some("testpass"));

Expand Down Expand Up @@ -535,7 +543,7 @@ password = "pass2"

for url_str in matching_urls {
let url = DisplaySafeUrl::parse(url_str).unwrap();
let cred = store.get_credentials(&url, None);
let cred = store.get_credentials(&url, None).unwrap();
assert!(cred.is_some(), "Failed to match URL with prefix: {url_str}");
}

Expand All @@ -548,7 +556,7 @@ password = "pass2"

for url_str in non_matching_urls {
let url = DisplaySafeUrl::parse(url_str).unwrap();
let cred = store.get_credentials(&url, None);
let cred = store.get_credentials(&url, None).unwrap();
assert!(cred.is_none(), "Should not match non-prefix URL: {url_str}");
}
}
Expand All @@ -572,7 +580,7 @@ password = "pass2"

for url_str in matching_urls {
let url = DisplaySafeUrl::parse(url_str).unwrap();
let cred = store.get_credentials(&url, None);
let cred = store.get_credentials(&url, None).unwrap();
assert!(
cred.is_some(),
"Failed to match URL in same realm: {url_str}"
Expand All @@ -588,7 +596,7 @@ password = "pass2"

for url_str in non_matching_urls {
let url = DisplaySafeUrl::parse(url_str).unwrap();
let cred = store.get_credentials(&url, None);
let cred = store.get_credentials(&url, None).unwrap();
assert!(
cred.is_none(),
"Should not match URL in different realm: {url_str}"
Expand All @@ -612,12 +620,12 @@ password = "pass2"

// Should match the most specific prefix
let url = DisplaySafeUrl::parse("https://example.com/api/v1/users").unwrap();
let cred = store.get_credentials(&url, None).unwrap();
let cred = store.get_credentials(&url, None).unwrap().unwrap();
assert_eq!(cred.username(), Some("specific"));

// Should match the general prefix for non-specific paths
let url = DisplaySafeUrl::parse("https://example.com/api/v2").unwrap();
let cred = store.get_credentials(&url, None).unwrap();
let cred = store.get_credentials(&url, None).unwrap().unwrap();
assert_eq!(cred.username(), Some("general"));
}

Expand All @@ -630,17 +638,17 @@ password = "pass2"
store.insert(service.clone(), user1_creds.clone());

// Should return credentials when username matches
let result = store.get_credentials(&url, Some("user1"));
let result = store.get_credentials(&url, Some("user1")).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().username(), Some("user1"));
assert_eq!(result.unwrap().password(), Some("pass1"));

// Should not return credentials when username doesn't match
let result = store.get_credentials(&url, Some("user2"));
let result = store.get_credentials(&url, Some("user2")).unwrap();
assert!(result.is_none());

// Should return credentials when no username is specified
let result = store.get_credentials(&url, None);
let result = store.get_credentials(&url, None).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().username(), Some("user1"));
}
Expand Down Expand Up @@ -668,21 +676,50 @@ password = "pass2"
let url = DisplaySafeUrl::parse("https://example.com/api/v1/users").unwrap();

// Should match specific credentials when username matches
let result = store.get_credentials(&url, Some("specific_user"));
let result = store.get_credentials(&url, Some("specific_user")).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().username(), Some("specific_user"));

// Should match the general credentials when requesting general_user (falls back to less specific prefix)
let result = store.get_credentials(&url, Some("general_user"));
let result = store.get_credentials(&url, Some("general_user")).unwrap();
assert!(
result.is_some(),
"Should match general_user from less specific prefix"
);
assert_eq!(result.unwrap().username(), Some("general_user"));

// Should match most specific when no username specified
let result = store.get_credentials(&url, None);
let result = store.get_credentials(&url, None).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().username(), Some("specific_user"));
}

#[test]
fn test_ambiguous_username_error() {
let mut store = TextCredentialStore::default();

// Add two credentials for the same service with different usernames
let service = Service::from_str("https://example.com/api").unwrap();
let user1_creds = Credentials::basic(Some("user1".to_string()), Some("pass1".to_string()));
let user2_creds = Credentials::basic(Some("user2".to_string()), Some("pass2".to_string()));

store.insert(service.clone(), user1_creds);
store.insert(service.clone(), user2_creds);

let url = DisplaySafeUrl::parse("https://example.com/api/v1").unwrap();

// When no username is specified, should return an error because there are multiple matches with same specificity
let result = store.get_credentials(&url, None);
assert!(result.is_err());
assert_eq!(result, Err(LookupError::AmbiguousUsername(url.clone())));

// When a specific username is provided, should return the correct credentials
let result = store.get_credentials(&url, Some("user1")).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().username(), Some("user1"));

let result = store.get_credentials(&url, Some("user2")).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().username(), Some("user2"));
}
}
2 changes: 1 addition & 1 deletion crates/uv-bin-install/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ impl Binary {
pub fn default_version(&self) -> Version {
match self {
// TODO(zanieb): Figure out a nice way to automate updating this
Self::Ruff => Version::new([0, 12, 5]),
Self::Ruff => Version::new([0, 15, 0]),
}
}

Expand Down
Loading
Loading