Add static properties support

This commit is contained in:
konstin 2025-09-17 19:54:39 +02:00 committed by Charlie Marsh
parent 03d5fab103
commit 75179d7ef2
11 changed files with 288 additions and 13 deletions

3
Cargo.lock generated
View File

@ -6802,9 +6802,12 @@ dependencies = [
"rustc-hash",
"serde",
"serde_json",
"thiserror 2.0.16",
"tracing",
"uv-distribution-filename",
"uv-normalize",
"uv-once-map",
"uv-pep440",
"uv-pep508",
"uv-pypi-types",
]

View File

@ -1,12 +1,14 @@
use std::future::Future;
use std::io;
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use futures::{FutureExt, StreamExt, TryStreamExt};
use rustc_hash::FxHashMap;
use std::ffi::OsString;
use std::future::Future;
use std::path::Path;
use std::path::PathBuf;
use std::pin::Pin;
use std::str::FromStr;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::{env, io};
use tempfile::TempDir;
use tokio::io::{AsyncRead, AsyncSeekExt, ReadBuf};
use tokio::sync::Semaphore;
@ -27,13 +29,15 @@ use uv_distribution_types::{
};
use uv_extract::hash::Hasher;
use uv_fs::write_atomic;
use uv_pep508::{MarkerEnvironment, MarkerVariantsUniversal, VariantNamespace};
use uv_pep440::VersionSpecifiers;
use uv_pep508::{MarkerEnvironment, MarkerVariantsUniversal, VariantNamespace, VersionOrUrl};
use uv_platform_tags::Tags;
use uv_pypi_types::{HashDigest, HashDigests, PyProjectToml};
use uv_redacted::DisplaySafeUrl;
use uv_types::{BuildContext, BuildStack, VariantsTrait};
use uv_variants::VariantProviderOutput;
use uv_variants::resolved_variants::ResolvedVariants;
use uv_variants::variant_lock::{VariantLock, VariantLockProvider, VariantLockResolved};
use uv_variants::variants_json::{Provider, VariantsJsonContent};
use crate::archive::Archive;
@ -594,7 +598,33 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
marker_env: &MarkerEnvironment,
debug_filename: &VariantsJson,
) -> Result<ResolvedVariants, Error> {
// TODO: parse_boolish_environment_variable
let locked_and_inferred =
env::var_os("UV_VARIANT_LOCK_INCOMPLETE").is_some_and(|var| var == "1");
// TODO(konsti): Integrate this properly, and add this to the CLI.
let variant_lock = if let Some(variant_lock_path) = env::var_os("UV_VARIANT_LOCK") {
let variant_lock: VariantLock = toml::from_slice(
&fs_err::read(&variant_lock_path).map_err(Error::VariantLockRead)?,
)
.map_err(|err| Error::VariantLockParse(PathBuf::from(&variant_lock_path), err))?;
// TODO(konsti): If parsing fails, check the version
if !VersionSpecifiers::from_str(">=0.1,<0.2")
.unwrap()
.contains(&variant_lock.metadata.version)
{
return Err(Error::VariantLockVersion(
PathBuf::from(variant_lock_path),
variant_lock.metadata.version,
));
}
Some((variant_lock_path, variant_lock))
} else {
None
};
// Query the install time provider.
// TODO(konsti): Don't use threads if we're fully static.
let mut provider_outputs: FxHashMap<VariantNamespace, Arc<VariantProviderOutput>> =
futures::stream::iter(variants_json.providers.iter().filter(|(_, provider)| {
provider.plugin_use.unwrap_or_default().run_on_install()
@ -602,10 +632,11 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
.enable_if
.evaluate(marker_env, MarkerVariantsUniversal, &[])
}))
.map(|(name, provider)| self.query_variant_provider(name, provider))
.map(|(name, provider)| {
self.resolve_provider(locked_and_inferred, variant_lock.as_ref(), name, provider)
})
// TODO(konsti): Buffer size
.buffered(8)
.map_ok(|config| (config.namespace.clone(), config))
.try_collect()
.await?;
@ -638,6 +669,42 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
})
}
async fn resolve_provider(
&self,
locked_and_inferred: bool,
variant_lock: Option<&(OsString, VariantLock)>,
name: &VariantNamespace,
provider: &Provider,
) -> Result<(VariantNamespace, Arc<VariantProviderOutput>), Error> {
if let Some((variant_lock_path, variant_lock)) = &variant_lock {
if let Some(static_provider) = variant_lock
.provider
.iter()
.find(|static_provider| satisfies_provider_requires(provider, static_provider))
{
Ok((
static_provider.namespace.clone(),
Arc::new(VariantProviderOutput {
namespace: static_provider.namespace.clone(),
features: static_provider.properties.clone().into_iter().collect(),
}),
))
} else if locked_and_inferred {
let config = self.query_variant_provider(name, provider).await?;
Ok((config.namespace.clone(), config))
} else {
Err(Error::VariantLockMissing {
variant_lock: PathBuf::from(variant_lock_path),
requires: provider.requires.clone(),
plugin_api: provider.plugin_api.clone().unwrap_or_default().clone(),
})
}
} else {
let config = self.query_variant_provider(name, provider).await?;
Ok((config.namespace.clone(), config))
}
}
async fn query_variant_provider(
&self,
name: &VariantNamespace,
@ -1465,6 +1532,43 @@ fn add_tar_zst_extension(mut url: DisplaySafeUrl) -> DisplaySafeUrl {
url
}
fn satisfies_provider_requires(
requested_provider: &Provider,
static_provider: &VariantLockProvider,
) -> bool {
// TODO(konsti): Correct plugin_api inference.
if static_provider.plugin_api.clone().or(static_provider
.resolved
.first()
.map(|resolved| resolved.name().to_string()))
!= requested_provider.plugin_api.clone().or(static_provider
.resolved
.first()
.map(|resolved| resolved.name().to_string()))
{
return false;
}
requested_provider.requires.iter().all(|requested| {
static_provider.resolved.iter().any(|resolved| {
// We ignore extras for simplicity.
&requested.name == resolved.name()
&& match (&requested.version_or_url, resolved) {
(None, _) => true,
(
Some(VersionOrUrl::VersionSpecifier(requested)),
VariantLockResolved::Version(_name, resolved),
) => requested.contains(resolved),
(
Some(VersionOrUrl::Url(resolved_url)),
VariantLockResolved::Url(_name, url),
) => resolved_url == &**url,
// We don't support URL<->version matching
_ => false,
}
})
})
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -1,5 +1,6 @@
use std::path::PathBuf;
use itertools::Itertools;
use owo_colors::OwoColorize;
use tokio::task::JoinError;
use zip::result::ZipError;
@ -11,8 +12,8 @@ use uv_distribution_types::{InstalledDist, InstalledDistError, IsBuildBackendErr
use uv_fs::Simplified;
use uv_normalize::PackageName;
use uv_pep440::{Version, VersionSpecifiers};
use uv_pep508::VariantNamespace;
use uv_pypi_types::{HashAlgorithm, HashDigest};
use uv_pep508::{Requirement, VariantNamespace};
use uv_pypi_types::{HashAlgorithm, HashDigest, VerbatimParsedUrl};
use uv_redacted::DisplaySafeUrl;
use uv_types::AnyErrorBuild;
@ -84,6 +85,23 @@ pub enum Error {
declared: VariantNamespace,
actual: VariantNamespace,
},
#[error("Failed to read variant lock")]
VariantLockRead(#[source] std::io::Error),
#[error("Variant lock has an unsupported format: {}", _0.user_display())]
VariantLockParse(PathBuf, #[source] toml::de::Error),
#[error("Variant lock has an unsupported version {}, only version 0.1 is supported: {}", _1, _0.user_display())]
VariantLockVersion(PathBuf, Version),
#[error(
"Variant lock is missing a matching provider and `UV_VARIANT_LOCK_INCOMPLETE` is not set\n variant lock: {}\n requires: `{}`\n plugin-api: {}",
variant_lock.user_display(),
requires.iter().join("`, `"),
plugin_api
)]
VariantLockMissing {
variant_lock: PathBuf,
requires: Vec<Requirement<VerbatimParsedUrl>>,
plugin_api: String,
},
#[error("Failed to parse metadata from built wheel")]
Metadata(#[from] uv_pypi_types::MetadataError),
#[error("Failed to read metadata: `{}`", _0.user_display())]

View File

@ -11,7 +11,9 @@ license.workspace = true
[dependencies]
uv-distribution-filename = { workspace = true }
uv-normalize = { workspace = true }
uv-once-map = { workspace = true }
uv-pep440 = { workspace = true, features = ["serde"] }
uv-pep508 = { workspace = true, features = ["serde"] }
uv-pypi-types = { workspace = true }
@ -19,6 +21,7 @@ indexmap = { workspace = true }
indoc = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]

View File

@ -4,6 +4,7 @@ use uv_pep508::{VariantFeature, VariantNamespace, VariantValue};
pub mod cache;
pub mod resolved_variants;
pub mod variant_lock;
pub mod variants_json;
/// Wire format between with the Python shim for provider plugins.

View File

@ -0,0 +1,94 @@
use std::str::FromStr;
use indexmap::IndexMap;
use serde::{Deserialize, Deserializer};
use thiserror::Error;
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_pep508::{UnnamedRequirementUrl, VariantFeature, VariantNamespace, VariantValue};
use uv_pypi_types::VerbatimParsedUrl;
#[derive(Debug, Error)]
pub enum VariantLockError {
#[error("Invalid resolved requirement format: {0}")]
InvalidResolvedFormat(String),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct VariantLock {
pub metadata: VariantLockMetadata,
pub provider: Vec<VariantLockProvider>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct VariantLockMetadata {
pub created_by: String,
pub version: Version,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct VariantLockProvider {
pub resolved: Vec<VariantLockResolved>,
pub plugin_api: Option<String>,
pub namespace: VariantNamespace,
pub properties: IndexMap<VariantFeature, Vec<VariantValue>>,
}
/// A resolved requirement in the form `<name>==<version>` or `<name> @ <url>`
#[derive(Debug, Clone)]
pub enum VariantLockResolved {
Version(PackageName, Version),
Url(PackageName, Box<VerbatimParsedUrl>),
}
impl VariantLockResolved {
pub fn name(&self) -> &PackageName {
match self {
Self::Version(name, _) => name,
Self::Url(name, _) => name,
}
}
}
impl FromStr for VariantLockResolved {
type Err = VariantLockError;
fn from_str(resolved: &str) -> Result<Self, Self::Err> {
if let Some((name, version)) = resolved.split_once("==") {
Ok(Self::Version(
PackageName::from_str(name.trim())
.map_err(|_| VariantLockError::InvalidResolvedFormat(resolved.to_string()))?,
Version::from_str(version.trim())
.map_err(|_| VariantLockError::InvalidResolvedFormat(resolved.to_string()))?,
))
} else if let Some((name, url)) = resolved.split_once(" @ ") {
Ok(Self::Url(
PackageName::from_str(name.trim())
.map_err(|_| VariantLockError::InvalidResolvedFormat(resolved.to_string()))?,
Box::new(
VerbatimParsedUrl::parse_unnamed_url(url.trim()).map_err(|_| {
VariantLockError::InvalidResolvedFormat(resolved.to_string())
})?,
),
))
} else {
Err(VariantLockError::InvalidResolvedFormat(
resolved.to_string(),
))
}
}
}
impl<'de> Deserialize<'de> for VariantLockResolved {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
}
}

View File

@ -24,7 +24,7 @@
"enable-if": "platform_machine == 'x86_64' or platform_machine == 'AMD64'",
"plugin-api": "cpu_level_provider",
"requires": [
"cpu_level_provider"
"cpu_level_provider >= 0.1, <0.2"
]
}
},

View File

@ -0,0 +1,12 @@
[metadata]
created-by = "tool-that-created-the-file" # Freeform string
version = "0.1" # PEP 440 version
# We don't have static information for the requested provider
[[provider]]
resolved = ["foo==0.0.1"]
plugin-api = "foo"
namespace = "foo"
[provider.properties]

View File

@ -0,0 +1,11 @@
[metadata]
created-by = "tool-that-created-the-file" # Freeform string
version = "0.1" # PEP 440 version
[[provider]]
resolved = ["cpu_level_provider==0.1"]
plugin-api = "cpu_level_provider"
namespace = "cpu_level"
[provider.properties]
x86_64_level = []

20
files/variant-lock.toml Normal file
View File

@ -0,0 +1,20 @@
[metadata]
created-by = "tool-that-created-the-file" # Freeform string
version = "0.1" # PEP 440 version
[[provider]]
resolved = ["cpu_level_provider==0.1"]
plugin-api = "cpu_level_provider"
namespace = "cpu_level"
[provider.properties]
x86_64_level = ["v1", "v2"]
[[provider]]
resolved = ["nvidia-variant-provider==0.0.1"]
plugin-api = "nvidia_variant_provider.plugin:NvidiaVariantPlugin"
namespace = "nvidia"
[provider.properties]
cuda_version_lower_bound = ["12.8"]
sm_arch = ["100_real", "120_real", "70_real", "75_real", "80_real", "86_real", "90_real"]

View File

@ -18,6 +18,15 @@ uv venv -c -q && UV_CPU_LEVEL_OVERRIDE=2 ${uv} pip install built-by-uv --no-inde
echo "# Matching cpu2 variant wheel, to be preferred over the non-variant wheel and the sdist"
uv venv -c -q && UV_CPU_LEVEL_OVERRIDE=2 RUST_LOG=uv_distribution_types=debug ${uv} pip install built-by-uv --no-index --no-cache --no-progress --find-links ./files --find-links ./files_wheel --find-links ./files_sdist
echo "Valid variants lock"
uv venv -c -q && UV_VARIANT_LOCK=files/variant-lock.toml RUST_LOG=uv_distribution_types=debug ${uv} pip install built-by-uv --no-index --no-cache --no-progress --find-links ./files
echo "No matching variants wheel with variants lock"
uv venv -c -q && ( ( UV_VARIANT_LOCK=files/variant-lock-none.toml RUST_LOG=uv_distribution_types=debug ${uv} pip install built-by-uv --no-index --no-cache --no-progress --find-links ./files && exit 1 ) || exit 0 )
echo "No matching variant provider in variants lock"
uv venv -c -q && ( ( UV_VARIANT_LOCK=files/variant-lock-incomplete.toml RUST_LOG=uv_distribution_types=debug ${uv} pip install built-by-uv --no-index --no-cache --no-progress --find-links ./files && exit 1 ) || exit 0 )
echo "No matching variants in variants lock, but UV_VARIANT_LOCK_INCOMPLETE set"
uv venv -c -q && UV_VARIANT_LOCK_INCOMPLETE=1 UV_VARIANT_LOCK=files/variant-lock-incomplete.toml RUST_LOG=uv_distribution_types=debug ${uv} pip install built-by-uv --no-index --no-cache --no-progress --find-links ./files
echo "# sync without a compatible variant wheel (fresh)"
( cd scripts/packages/cpu_user && rm -f uv.lock && ( ( uv venv -c -q && UV_CPU_LEVEL_OVERRIDE=0 ${uv} sync && exit 1 ) || exit 0 ) )
echo "# sync without a compatible variant wheel (existing lockfile)"