diff --git a/Cargo.lock b/Cargo.lock index 30f38588a..edb6ea954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3639,6 +3639,7 @@ dependencies = [ "rattler_lock", "thiserror", "url", + "uv-configuration", "uv-git", "uv-normalize", "uv-python", diff --git a/crates/pixi_manifest/src/pypi/pypi_options.rs b/crates/pixi_manifest/src/pypi/pypi_options.rs index 634a291d2..103a587ec 100644 --- a/crates/pixi_manifest/src/pypi/pypi_options.rs +++ b/crates/pixi_manifest/src/pypi/pypi_options.rs @@ -3,10 +3,36 @@ use indexmap::IndexSet; use rattler_lock::{FindLinksUrlOrPath, PypiIndexes}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use std::{hash::Hash, iter}; +use std::{fmt::Display, hash::Hash, iter}; use thiserror::Error; use url::Url; +// taken from: https://docs.astral.sh/uv/reference/settings/#index-strategy +/// The strategy to use when resolving against multiple index URLs. +/// By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary. +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum IndexStrategy { + #[default] + /// Only use results from the first index that returns a match for a given package name + FirstIndex, + /// Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next + UnsafeFirstMatch, + /// Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index + UnsafeBestMatch, +} + +impl Display for IndexStrategy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + IndexStrategy::FirstIndex => "first-index", + IndexStrategy::UnsafeFirstMatch => "unsafe-first-match", + IndexStrategy::UnsafeBestMatch => "unsafe-best-match", + }; + write!(f, "{}", s) + } +} + /// Specific options for a PyPI registries #[serde_as] #[derive(Debug, Clone, PartialEq, Serialize, Eq, Deserialize, Default)] @@ -21,6 +47,8 @@ pub struct PypiOptions { pub find_links: Option>, /// Disable isolated builds pub no_build_isolation: Option>, + /// The strategy to use when resolving against multiple index URLs. + pub index_strategy: Option, } /// Clones and deduplicates two iterators of values @@ -42,12 +70,14 @@ impl PypiOptions { extra_indexes: Option>, flat_indexes: Option>, no_build_isolation: Option>, + index_strategy: Option, ) -> Self { Self { index_url: index, extra_index_urls: extra_indexes, find_links: flat_indexes, no_build_isolation, + index_strategy, } } @@ -93,6 +123,20 @@ impl PypiOptions { self.index_url.clone() }; + // Allow only one index strategy + let index_strategy = if let Some(other_index_strategy) = other.index_strategy.clone() { + if let Some(own_index_strategy) = &self.index_strategy { + return Err(PypiOptionsMergeError::MultipleIndexStrategies { + first: own_index_strategy.to_string(), + second: other_index_strategy.to_string(), + }); + } else { + Some(other_index_strategy) + } + } else { + self.index_strategy.clone() + }; + // Chain together and deduplicate the extra indexes let extra_indexes = self .extra_index_urls @@ -135,6 +179,7 @@ impl PypiOptions { extra_index_urls: extra_indexes, find_links: flat_indexes, no_build_isolation, + index_strategy, }) } } @@ -165,10 +210,16 @@ pub enum PypiOptionsMergeError { "multiple primary pypi indexes are not supported, found both {first} and {second} across multiple pypi options" )] MultiplePrimaryIndexes { first: String, second: String }, + #[error( + "multiple index strategies are not supported, found both {first} and {second} across multiple pypi options" + )] + MultipleIndexStrategies { first: String, second: String }, } #[cfg(test)] mod tests { + use crate::pypi::pypi_options::IndexStrategy; + use super::PypiOptions; use rattler_lock::FindLinksUrlOrPath; use url::Url; @@ -196,7 +247,8 @@ mod tests { FindLinksUrlOrPath::Path("/path/to/flat/index".into()), FindLinksUrlOrPath::Url(Url::parse("https://flat.index").unwrap()) ]), - no_build_isolation: Some(vec!["pkg1".to_string(), "pkg2".to_string()]) + no_build_isolation: Some(vec!["pkg1".to_string(), "pkg2".to_string()]), + index_strategy: None, }, ); } @@ -212,6 +264,7 @@ mod tests { FindLinksUrlOrPath::Url(Url::parse("https://flat.index").unwrap()), ]), no_build_isolation: Some(vec!["foo".to_string(), "bar".to_string()]), + index_strategy: None, }; // Create the second set of options @@ -223,6 +276,7 @@ mod tests { FindLinksUrlOrPath::Url(Url::parse("https://flat.index2").unwrap()), ]), no_build_isolation: Some(vec!["foo".to_string()]), + index_strategy: None, }; // Merge the two options @@ -239,6 +293,7 @@ mod tests { extra_index_urls: None, find_links: None, no_build_isolation: None, + index_strategy: None, }; // Create the second set of options @@ -247,6 +302,7 @@ mod tests { extra_index_urls: None, find_links: None, no_build_isolation: None, + index_strategy: None, }; // Merge the two options @@ -254,4 +310,30 @@ mod tests { let merged_opts = opts.union(&opts2); insta::assert_snapshot!(merged_opts.err().unwrap()); } + + #[test] + fn test_error_on_multiple_index_strategies() { + // Create the first set of options + let opts = PypiOptions { + index_url: None, + extra_index_urls: None, + find_links: None, + no_build_isolation: None, + index_strategy: Some(IndexStrategy::FirstIndex), + }; + + // Create the second set of options + let opts2 = PypiOptions { + index_url: None, + extra_index_urls: None, + find_links: None, + no_build_isolation: None, + index_strategy: Some(IndexStrategy::UnsafeBestMatch), + }; + + // Merge the two options + // This should error because there are two index strategies + let merged_opts = opts.union(&opts2); + insta::assert_snapshot!(merged_opts.err().unwrap()); + } } diff --git a/crates/pixi_manifest/src/pypi/snapshots/pixi_manifest__pypi__pypi_options__tests__error_on_multiple_index_strategies.snap b/crates/pixi_manifest/src/pypi/snapshots/pixi_manifest__pypi__pypi_options__tests__error_on_multiple_index_strategies.snap new file mode 100644 index 000000000..a4ff3bfe9 --- /dev/null +++ b/crates/pixi_manifest/src/pypi/snapshots/pixi_manifest__pypi__pypi_options__tests__error_on_multiple_index_strategies.snap @@ -0,0 +1,5 @@ +--- +source: crates/pixi_manifest/src/pypi/pypi_options.rs +expression: merged_opts.err().unwrap() +--- +multiple index strategies are not supported, found both first-index and unsafe-best-match across multiple pypi options diff --git a/crates/pixi_manifest/src/pypi/snapshots/pixi_manifest__pypi__pypi_options__tests__merge_pypi_options.snap b/crates/pixi_manifest/src/pypi/snapshots/pixi_manifest__pypi__pypi_options__tests__merge_pypi_options.snap index b6afb1541..4c7bcb8a6 100644 --- a/crates/pixi_manifest/src/pypi/snapshots/pixi_manifest__pypi__pypi_options__tests__merge_pypi_options.snap +++ b/crates/pixi_manifest/src/pypi/snapshots/pixi_manifest__pypi__pypi_options__tests__merge_pypi_options.snap @@ -14,3 +14,4 @@ find-links: no-build-isolation: - foo - bar +index-strategy: ~ diff --git a/crates/pixi_manifest/src/snapshots/pixi_manifest__parsed_manifest__tests__pypi_options_default_feature.snap b/crates/pixi_manifest/src/snapshots/pixi_manifest__parsed_manifest__tests__pypi_options_default_feature.snap index 29e6b242d..48660dc4c 100644 --- a/crates/pixi_manifest/src/snapshots/pixi_manifest__parsed_manifest__tests__pypi_options_default_feature.snap +++ b/crates/pixi_manifest/src/snapshots/pixi_manifest__parsed_manifest__tests__pypi_options_default_feature.snap @@ -9,3 +9,4 @@ find-links: - path: "../foo" - url: "https://example.com/bar" no-build-isolation: ~ +index-strategy: ~ diff --git a/crates/pixi_manifest/src/snapshots/pixi_manifest__parsed_manifest__tests__pypy_options_project_and_default_feature.snap b/crates/pixi_manifest/src/snapshots/pixi_manifest__parsed_manifest__tests__pypy_options_project_and_default_feature.snap index 3004c299b..e58b050ec 100644 --- a/crates/pixi_manifest/src/snapshots/pixi_manifest__parsed_manifest__tests__pypy_options_project_and_default_feature.snap +++ b/crates/pixi_manifest/src/snapshots/pixi_manifest__parsed_manifest__tests__pypy_options_project_and_default_feature.snap @@ -7,3 +7,4 @@ extra-index-urls: - "https://pypi.org/simple2" find-links: ~ no-build-isolation: ~ +index-strategy: ~ diff --git a/crates/pixi_uv_conversions/Cargo.toml b/crates/pixi_uv_conversions/Cargo.toml index 3b9ca8555..0b1a189d3 100644 --- a/crates/pixi_uv_conversions/Cargo.toml +++ b/crates/pixi_uv_conversions/Cargo.toml @@ -20,6 +20,7 @@ pypi-types = { workspace = true } rattler_lock = { workspace = true } thiserror = { workspace = true } url = { workspace = true } +uv-configuration = { workspace = true } uv-git = { workspace = true } uv-normalize = { workspace = true } uv-python = { workspace = true } diff --git a/crates/pixi_uv_conversions/src/conversions.rs b/crates/pixi_uv_conversions/src/conversions.rs index 7906cbe97..0ce9b3bc2 100644 --- a/crates/pixi_uv_conversions/src/conversions.rs +++ b/crates/pixi_uv_conversions/src/conversions.rs @@ -4,7 +4,10 @@ use distribution_types::{FlatIndexLocation, IndexLocations, IndexUrl}; use pep508_rs::{ InvalidNameError, PackageName, UnnamedRequirementUrl, VerbatimUrl, VerbatimUrlError, }; -use pixi_manifest::pypi::{pypi_options::PypiOptions, GitRev}; +use pixi_manifest::pypi::{ + pypi_options::{IndexStrategy, PypiOptions}, + GitRev, +}; use rattler_lock::FindLinksUrlOrPath; use uv_git::GitReference; use uv_python::PythonEnvironment; @@ -153,3 +156,18 @@ pub fn names_to_build_isolation<'a>( ) -> uv_types::BuildIsolation<'a> { packages_to_build_isolation(names, env) } + +/// Convert pixi `IndexStrategy` to `uv_types::IndexStrategy` +pub fn to_index_strategy( + index_strategy: Option<&IndexStrategy>, +) -> uv_configuration::IndexStrategy { + if let Some(index_strategy) = index_strategy { + match index_strategy { + IndexStrategy::FirstIndex => uv_configuration::IndexStrategy::FirstIndex, + IndexStrategy::UnsafeFirstMatch => uv_configuration::IndexStrategy::UnsafeFirstMatch, + IndexStrategy::UnsafeBestMatch => uv_configuration::IndexStrategy::UnsafeBestMatch, + } + } else { + uv_configuration::IndexStrategy::default() + } +} diff --git a/docs/reference/project_configuration.md b/docs/reference/project_configuration.md index 4d4c2f77e..21149e1b6 100644 --- a/docs/reference/project_configuration.md +++ b/docs/reference/project_configuration.md @@ -293,21 +293,29 @@ The `pypi-options` table is used to define options that are specific to PyPI reg These options can be specified either at the root level, which will add it to the default options feature, or on feature level, which will create a union of these options when the features are included in the environment. The options that can be defined are: - - `index-url`: replaces the main index url. - - `extra-index-urls`: adds an extra index url. - - `find-links`: similar to `--find-links` option in `pip`. - - `no-build-isolation`: disables build isolation, can only be set per package. + +- `index-url`: replaces the main index url. +- `extra-index-urls`: adds an extra index url. +- `find-links`: similar to `--find-links` option in `pip`. +- `no-build-isolation`: disables build isolation, can only be set per package. +- `index-strategy`: allows for specifying the index strategy to use. + +These options are explained in the sections below. Most of these options are taken directly or with slight modifications from the [uv settings](https://docs.astral.sh/uv/reference/settings/). If any are missing that you need feel free to create an issue [requesting](https://github.com/prefix-dev/pixi/issues) them. + ### Alternative registries -Currently the main reason to use this table is to define alternative registries. -We support: +!!! info "Strict Index Priority" + Unlike pip, because we make use of uv, we have a strict index priority. This means that the first index is used where a package can be found. + The order is determined by the order in the toml file. Where the `extra-index-urls` are preferred over the `index-url`. Read more about this on the [uv docs](https://docs.astral.sh/uv/pip/compatibility/#packages-that-exist-on-multiple-indexes) + +Often you might want to use an alternative or extra index for your project. This can be done by adding the `pypi-options` table to your `pixi.toml` file, the following options are available: -- `index-url`: replaces the main index url. - Only one `index-url` can be defined per environment. -- `extra-index-urls`: adds an extra index url. +- `index-url`: replaces the main index url. If this is not set the default index used is `https://pypi.org/simple`. + **Only one** `index-url` can be defined per environment. +- `extra-index-urls`: adds an extra index url. The urls are used in the order they are defined. And are preferred over the `index-url`. These are merged across features into an environment. - `find-links`: which can either be a path `{path = './links'}` or a url `{url = 'https://example.com/links'}`. - This is similar to the `--find-links` option in `pip`. + This is similar to the `--find-links` option in `pip`. These are merged across features into an environment. An example: @@ -318,12 +326,11 @@ extra-index-urls = ["https://example.com/simple"] find-links = [{path = './links'}] ``` -There are some examples in the pixi repository that make use of this feature. -To read about existing authentication methods, please check the [PyPI Authentication](../advanced/authentication.md#pypi-authentication) section. +There are some [examples](https://github.com/prefix-dev/pixi/tree/main/examples/pypi-custom-registry) in the pixi repository, that make use of this feature. + +!!! tip "Authentication Methods" + To read about existing authentication methods for private registries, please check the [PyPI Authentication](../advanced/authentication.md#pypi-authentication) section. -!!! info "Strict Index Priority" - Unlike pip, because we make use of uv, we have a strict index priority. This means that the first index is used where a package can be found. - The order is determined by the order in the toml file. Where the `extra-index-urls` are preferred over the `index-url`. Read more about this on the [UV Readme](https://github.com/astral-sh/uv/blob/main/PIP_COMPATIBILITY.md#packages-that-exist-on-multiple-indexes) ### No Build Isolation Even though build isolation is a good default. @@ -332,6 +339,9 @@ This is convenient if you want to use `torch` or something similar for your buil ```toml +[dependencies] +pytorch = "2.4.0" + [pypi-options] no-build-isolation = ["detectron2"] @@ -339,6 +349,27 @@ no-build-isolation = ["detectron2"] detectron2 = { git = "https://github.com/facebookresearch/detectron2.git", rev = "5b72c27ae39f99db75d43f18fd1312e1ea934e60"} ``` +!!! tip "Conda dependencies define the build environment" + To use `no-build-isolation` effectively, use conda dependencies to define the build environment. These are installed before the PyPI dependencies are resolved, this way these dependencies are available during the build process. In the example above adding `torch` as a PyPI dependency would be ineffective, as it would not yet be installed during the PyPI resolution phase. + +### Index Strategy + +The strategy to use when resolving against multiple index URLs. Description modified from the [uv](https://docs.astral.sh/uv/reference/settings/#index-strategy) documentation: + +By default, `uv` and thus `pixi`, will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-match). This prevents *dependency confusion* attacks, whereby an attack can upload a malicious package under the same name to a secondary index. + +!!! warning "One index strategy per environment" + Only one `index-strategy` can be defined per environment or solve-group, otherwise, an error will be shown. + +#### Possible values: + +- **"first-index"**: Only use results from the first index that returns a match for a given package name +- **"unsafe-first-match"**: Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next. Meaning if the package `a` is available on index `x` and `y`, it will prefer the version from `x` unless you've requested a package version that is **only** available on `y`. +- **"unsafe-best-match"**: Search for every package name across all indexes, preferring the *best* version found. If a package version is in multiple indexes, only look at the entry for the first index. So given index, `x` and `y` that both contain package `a`, it will take the *best* version from either `x` or `y`, but should **that version** be available on both indexes it will prefer `x`. + +!!! info "PyPI only" + The `index-strategy` only changes PyPI package resolution and not conda package resolution. + ## The `dependencies` table(s) This section defines what dependencies you would like to use for your project. diff --git a/pixi.lock b/pixi.lock index b40bb2a2c..aaf8a3125 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1003,6 +1003,125 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_20.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h8ffe710_2.tar.bz2 + pypi-gen: + channels: + - url: https://fast.prefix.dev/conda-forge/ + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.8.30-hbcca054_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/editables-0.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.4.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-hf3520f5_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.3-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.1.0-h77fa898_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.1.0-h69a702a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.1.0-h77fa898_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.46.1-hadc24fc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-h4ab18f5_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-he02047a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyproject_hooks-1.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.5-h2ad013b_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-build-1.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/trove-classifiers-2024.7.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.20.1-pyhd8ed1ab_0.conda + osx-64: + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-hfdf4475_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.8.30-h8857fd0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/editables-0.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.4.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.3-hac325c4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.46.1-h4b8f8c9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-h87427d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-hf036a51_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.3.2-hd23fc13_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyproject_hooks-1.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.5-h37a9e06_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-build-1.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/trove-classifiers-2024.7.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.20.1-pyhd8ed1ab_0.conda + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.8.30-hf0a4a13_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/editables-0.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.4.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.3-hf9b8971_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.46.1-hc14010f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-hfb2fe0b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h7bae524_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.3.2-h8359307_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyproject_hooks-1.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.5-h30c5eda_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-build-1.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/trove-classifiers-2024.7.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.20.1-pyhd8ed1ab_0.conda + win-64: + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2024.8.30-h56e8100_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/editables-0.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.4.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.3-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.2-h8ffe710_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.46.1-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.3.2-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyproject_hooks-1.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.5-h889d299_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-build-1.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/trove-classifiers-2024.7.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h8a93ad2_20.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-hcc2c482_20.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_20.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.20.1-pyhd8ed1ab_0.conda schema: channels: - url: https://fast.prefix.dev/conda-forge/ @@ -2061,6 +2180,21 @@ packages: license_family: MIT size: 14691 timestamp: 1530916777462 +- kind: conda + name: editables + version: '0.5' + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/editables-0.5-pyhd8ed1ab_0.conda + sha256: de160a7494e7bc72360eea6a29cbddf194d0a79f45ff417a4de20e6858cf79a9 + md5: 9873878e2a069bc358b69e9a29c1ecd5 + depends: + - python >=3.7 + license: MIT + license_family: MIT + size: 10988 + timestamp: 1705857085102 - kind: conda name: exceptiongroup version: 1.2.2 @@ -2718,6 +2852,28 @@ packages: license_family: MIT size: 46754 timestamp: 1634280590080 +- kind: conda + name: hatchling + version: 1.25.0 + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.25.0-pyhd8ed1ab_0.conda + sha256: fb8a16a913f909d8f7d2ae95c18a1aeb7be3eebfb1b7a4246500c06d54498f89 + md5: 7571d6e5561b04aef679a11904dfcebf + depends: + - editables >=0.3 + - importlib-metadata + - packaging >=21.3 + - pathspec >=0.10.1 + - pluggy >=1.0.0 + - python >=3.7 + - tomli >=1.2.2 + - trove-classifiers + license: MIT + license_family: MIT + size: 64580 + timestamp: 1719090878694 - kind: conda name: hpack version: 4.0.0 @@ -3476,6 +3632,73 @@ packages: license_family: MIT size: 63655 timestamp: 1710362424980 +- kind: conda + name: libexpat + version: 2.6.3 + build: h5888daf_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.3-h5888daf_0.conda + sha256: 4bb47bb2cd09898737a5211e2992d63c555d63715a07ba56eae0aff31fb89c22 + md5: 59f4c43bb1b5ef1c71946ff2cbf59524 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - expat 2.6.3.* + license: MIT + license_family: MIT + size: 73616 + timestamp: 1725568742634 +- kind: conda + name: libexpat + version: 2.6.3 + build: hac325c4_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.3-hac325c4_0.conda + sha256: dd22dffad6731c352f4c14603868c9cce4d3b50ff5ff1e50f416a82dcb491947 + md5: c1db99b0a94a2f23bd6ce39e2d314e07 + depends: + - __osx >=10.13 + constrains: + - expat 2.6.3.* + license: MIT + license_family: MIT + size: 70517 + timestamp: 1725568864316 +- kind: conda + name: libexpat + version: 2.6.3 + build: he0c23c2_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.3-he0c23c2_0.conda + sha256: 9543965d155b8da96fc67dd81705fe5c2571c7c00becc8de5534c850393d4e3c + md5: 21415fbf4d0de6767a621160b43e5dea + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - expat 2.6.3.* + license: MIT + license_family: MIT + size: 138992 + timestamp: 1725569106114 +- kind: conda + name: libexpat + version: 2.6.3 + build: hf9b8971_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.3-hf9b8971_0.conda + sha256: 5cbe5a199fba14ade55457a468ce663aac0b54832c39aa54470b3889b4c75c4a + md5: 5f22f07c2ab2dea8c66fe9585a062c96 + depends: + - __osx >=11.0 + constrains: + - expat 2.6.3.* + license: MIT + license_family: MIT + size: 63895 + timestamp: 1725568783033 - kind: conda name: libffi version: 3.4.2 @@ -4187,6 +4410,64 @@ packages: license: Unlicense size: 830198 timestamp: 1718050644825 +- kind: conda + name: libsqlite + version: 3.46.1 + build: h2466b09_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.46.1-h2466b09_0.conda + sha256: ef83f90961630bc54a95e48062b05cf9c9173a822ea01784288029613a45eea4 + md5: 8a7c1ad01f58623bfbae8d601db7cf3b + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Unlicense + size: 876666 + timestamp: 1725354171439 +- kind: conda + name: libsqlite + version: 3.46.1 + build: h4b8f8c9_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.46.1-h4b8f8c9_0.conda + sha256: 1d075cb823f0cad7e196871b7c57961d669cbbb6cd0e798bf50cbf520dda65fb + md5: 84de0078b58f899fc164303b0603ff0e + depends: + - __osx >=10.13 + - libzlib >=1.3.1,<2.0a0 + license: Unlicense + size: 908317 + timestamp: 1725353652135 +- kind: conda + name: libsqlite + version: 3.46.1 + build: hadc24fc_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.46.1-hadc24fc_0.conda + sha256: 9851c049abafed3ee329d6c7c2033407e2fc269d33a75c071110ab52300002b0 + md5: 36f79405ab16bf271edb55b213836dac + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + license: Unlicense + size: 865214 + timestamp: 1725353659783 +- kind: conda + name: libsqlite + version: 3.46.1 + build: hc14010f_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.46.1-hc14010f_0.conda + sha256: 3725f962f490c5d44dae326d5f5b2e3c97f71a6322d914ccc85b5ddc2e50d120 + md5: 58050ec1724e58668d0126a1615553fa + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: Unlicense + size: 829500 + timestamp: 1725353720793 - kind: conda name: libssh2 version: 1.11.0 @@ -5311,6 +5592,69 @@ packages: license_family: Apache size: 2549881 timestamp: 1724403015051 +- kind: conda + name: openssl + version: 3.3.2 + build: h2466b09_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/openssl-3.3.2-h2466b09_0.conda + sha256: a45c42f3577294e22ac39ddb6ef5a64fd5322e8a6725afefbf4f2b4109340bf9 + md5: 1dc86753693df5e3326bb8a85b74c589 + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Apache-2.0 + license_family: Apache + size: 8396053 + timestamp: 1725412961673 +- kind: conda + name: openssl + version: 3.3.2 + build: h8359307_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.3.2-h8359307_0.conda + sha256: 940fa01c4dc6152158fe8943e05e55a1544cab639df0994e3b35937839e4f4d1 + md5: 1773ebccdc13ec603356e8ff1db9e958 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + size: 2882450 + timestamp: 1725410638874 +- kind: conda + name: openssl + version: 3.3.2 + build: hb9d3cd8_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.2-hb9d3cd8_0.conda + sha256: cee91036686419f6dd6086902acf7142b4916e1c4ba042e9ca23e151da012b6d + md5: 4d638782050ab6faa27275bed57e9b4e + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=13 + license: Apache-2.0 + license_family: Apache + size: 2891789 + timestamp: 1725410790053 +- kind: conda + name: openssl + version: 3.3.2 + build: hd23fc13_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.3.2-hd23fc13_0.conda + sha256: 2b75d4b56e45992adf172b158143742daeb316c35274b36f385ccb6644e93268 + md5: 2ff47134c8e292868a4609519b1ea3b6 + depends: + - __osx >=10.13 + - ca-certificates + license: Apache-2.0 + license_family: Apache + size: 2544654 + timestamp: 1725410973572 - kind: conda name: packaging version: '24.1' @@ -6067,6 +6411,22 @@ packages: license_family: MIT size: 90129 timestamp: 1724616224956 +- kind: conda + name: pyproject_hooks + version: 1.1.0 + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/pyproject_hooks-1.1.0-pyhd8ed1ab_0.conda + sha256: 7431bd1c1273facccae3875218dff1faae5a717a0605326fc0370ea8f780ba40 + md5: 03736d8ced74deece64e54be348ddd3e + depends: + - python >=3.7 + - tomli >=1.1.0 + license: MIT + license_family: MIT + size: 15301 + timestamp: 1714415314463 - kind: conda name: pyrsistent version: 0.20.0 @@ -6298,6 +6658,136 @@ packages: license: Python-2.0 size: 31991381 timestamp: 1713208036041 +- kind: conda + name: python + version: 3.12.5 + build: h2ad013b_0_cpython + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.5-h2ad013b_0_cpython.conda + sha256: e2aad83838988725d4ffba4e9717b9328054fd18a668cff3377e0c50f109e8bd + md5: 9c56c4df45f6571b13111d8df2448692 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.6.2,<3.0a0 + - libffi >=3.4,<4.0a0 + - libgcc-ng >=12 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.46.0,<4.0a0 + - libuuid >=2.38.1,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.3.1,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + size: 31663253 + timestamp: 1723143721353 +- kind: conda + name: python + version: 3.12.5 + build: h30c5eda_0_cpython + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.5-h30c5eda_0_cpython.conda + sha256: 1319e918fb54c9491832a9731cad00235a76f61c6f9b23fc0f70cdfb74c950ea + md5: 5e315581e2948dfe3bcac306540e9803 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.6.2,<3.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.46.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.3.1,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + size: 12926356 + timestamp: 1723142203193 +- kind: conda + name: python + version: 3.12.5 + build: h37a9e06_0_cpython + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.5-h37a9e06_0_cpython.conda + sha256: c0f39e625b2fd65f70a9cc086fe4b25cc72228453dbbcd92cd5d140d080e38c5 + md5: 517cb4e16466f8d96ba2a72897d14c48 + depends: + - __osx >=10.13 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.6.2,<3.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.46.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.3.1,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + size: 12173272 + timestamp: 1723142761765 +- kind: conda + name: python + version: 3.12.5 + build: h889d299_0_cpython + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/python-3.12.5-h889d299_0_cpython.conda + sha256: 4cef304eb8877fd3094c14b57097ccc1b817b4afbf2223dd45d2b61e44064740 + md5: db056d8b140ab2edd56a2f9bdb203dcd + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.6.2,<3.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.46.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.3.1,<4.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + size: 15897752 + timestamp: 1723141830317 +- kind: conda + name: python-build + version: 1.2.2 + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/python-build-1.2.2-pyhd8ed1ab_0.conda + sha256: dcf00631f394ee8aaf62beb93129f4c4c324d81bd06c496af8a8ddb1fa52777c + md5: 7309d5de1e4e866df29bcd8ea5550035 + depends: + - colorama + - importlib-metadata >=4.6 + - packaging >=19.0 + - pyproject_hooks + - python >=3.8 + - tomli >=1.1.0 + constrains: + - build <0 + license: MIT + size: 25019 + timestamp: 1725676759343 - kind: conda name: python-dateutil version: 2.9.0 @@ -7248,6 +7738,21 @@ packages: license_family: MIT size: 37279 timestamp: 1723631592742 +- kind: conda + name: trove-classifiers + version: 2024.7.2 + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/trove-classifiers-2024.7.2-pyhd8ed1ab_0.conda + sha256: ab5575f5908fcb578ecde73701e1ceb8dde708f93111b3f692c163f11bc119fc + md5: 2b9f52c7ecb8d017e50f91852aead307 + depends: + - python >=3.7 + license: Apache-2.0 + license_family: Apache + size: 18302 + timestamp: 1719995164492 - kind: conda name: typing-extensions version: 4.12.2 diff --git a/pixi.toml b/pixi.toml index eeefc6640..76d68c5be 100644 --- a/pixi.toml +++ b/pixi.toml @@ -102,6 +102,13 @@ jsonschema = "*" pydantic = ">=2.6.3,<2.7" pyyaml = ">=6.0.1,<6.1" +[feature.pypi-gen.dependencies] +hatchling = ">=1.25.0,<2" +python-build = ">=1.2.2,<2" + +[feature.pypi-gen.tasks] +pypi-gen-indexes = "python tests/pypi-indexes/generate-indexes.py" + [environments] default = { features = [ "build", @@ -115,6 +122,7 @@ docs = { features = [ lint = { features = [ "lint", ], no-default-feature = true, solve-group = "default" } +pypi-gen = { features = ["pypi-gen"] } schema = { features = [ "schema", "pytest", diff --git a/schema/model.py b/schema/model.py index 121e85c43..e330c3c2a 100644 --- a/schema/model.py +++ b/schema/model.py @@ -477,12 +477,12 @@ class FindLinksURL(StrictBaseModel): class PyPIOptions(StrictBaseModel): - """Options related to PyPI indexes""" + """Options that determine the behavior of PyPI package resolution and installation""" index_url: NonEmptyStr | None = Field( None, alias="index-url", - description="Alternative PyPI registry that should be used as the main index", + description="PyPI registry that should be used as the primary index", examples=["https://pypi.org/simple"], ) extra_index_urls: list[NonEmptyStr] | None = Field( @@ -500,9 +500,17 @@ class PyPIOptions(StrictBaseModel): no_build_isolation: list[PyPIPackageName] = Field( None, alias="no-build-isolation", - description="Packages that should not be isolated during the build process", + description="Packages that should NOT be isolated during the build process", examples=[["numpy"]], ) + index_strategy: ( + Literal["first-index"] | Literal["unsafe-first-match"] | Literal["unsafe-best-match"] | None + ) = Field( + None, + alias="index-strategy", + description="The strategy to use when resolving packages from multiple indexes", + examples=["first-index", "unsafe-first-match", "unsafe-best-match"], + ) ####################### diff --git a/schema/schema.json b/schema/schema.json index c52684136..39244295f 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -913,7 +913,7 @@ }, "PyPIOptions": { "title": "PyPIOptions", - "description": "Options related to PyPI indexes", + "description": "Options that determine the behavior of PyPI package resolution and installation", "type": "object", "additionalProperties": false, "properties": { @@ -951,9 +951,29 @@ ] ] }, + "index-strategy": { + "title": "Index-Strategy", + "description": "The strategy to use when resolving packages from multiple indexes", + "anyOf": [ + { + "const": "first-index" + }, + { + "const": "unsafe-first-match" + }, + { + "const": "unsafe-best-match" + } + ], + "examples": [ + "first-index", + "unsafe-first-match", + "unsafe-best-match" + ] + }, "index-url": { "title": "Index-Url", - "description": "Alternative PyPI registry that should be used as the main index", + "description": "PyPI registry that should be used as the primary index", "type": "string", "minLength": 1, "examples": [ @@ -962,7 +982,7 @@ }, "no-build-isolation": { "title": "No-Build-Isolation", - "description": "Packages that should not be isolated during the build process", + "description": "Packages that should NOT be isolated during the build process", "type": "array", "items": { "type": "string", diff --git a/src/lock_file/resolve/pypi.rs b/src/lock_file/resolve/pypi.rs index 1adfb9383..9d4b8ff25 100644 --- a/src/lock_file/resolve/pypi.rs +++ b/src/lock_file/resolve/pypi.rs @@ -23,7 +23,7 @@ use pep508_rs::{VerbatimUrl, VersionOrUrl}; use pixi_manifest::{pypi::pypi_options::PypiOptions, PyPiRequirement, SystemRequirements}; use pixi_uv_conversions::{ as_uv_req, isolated_names_to_packages, names_to_build_isolation, - pypi_options_to_index_locations, + pypi_options_to_index_locations, to_index_strategy, }; use pypi_modifiers::{ pypi_marker_env::determine_marker_environment, @@ -260,14 +260,17 @@ pub async fn resolve_pypi( pypi_options_to_index_locations(pypi_options, project_root).into_diagnostic()?; // TODO: create a cached registry client per index_url set? + let index_strategy = to_index_strategy(pypi_options.index_strategy.as_ref()); let registry_client = Arc::new( RegistryClientBuilder::new(context.cache.clone()) .client(context.client.clone()) .index_urls(index_locations.index_urls()) + .index_strategy(index_strategy) .keyring(context.keyring_provider) .connectivity(Connectivity::Online) .build(), ); + // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(®istry_client, &context.cache); @@ -300,7 +303,10 @@ pub async fn resolve_pypi( let build_isolation = names_to_build_isolation(non_isolated_packages.as_deref(), &env); tracing::debug!("using build-isolation: {:?}", build_isolation); - let options = Options::default(); + let options = Options { + index_strategy, + ..Options::default() + }; let git_resolver = GitResolver::default(); let build_dispatch = BuildDispatch::new( ®istry_client, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 53516707c..9d7497af2 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -10,27 +10,22 @@ use std::{ str::FromStr, }; -use self::builders::{HasDependencyConfig, RemoveBuilder}; -use crate::common::builders::{ - AddBuilder, InitBuilder, InstallBuilder, ProjectChannelAddBuilder, - ProjectEnvironmentAddBuilder, TaskAddBuilder, TaskAliasBuilder, UpdateBuilder, -}; use indicatif::ProgressDrawTarget; use miette::{Context, Diagnostic, IntoDiagnostic}; -use pixi::cli::cli_config::{PrefixUpdateConfig, ProjectConfig}; -use pixi::task::{ - get_task_env, ExecutableTask, RunOutput, SearchEnvironments, TaskExecutionError, TaskGraph, - TaskGraphError, -}; use pixi::{ cli::{ - add, init, + add, + cli_config::{PrefixUpdateConfig, ProjectConfig}, + init, install::Args, project, remove, run, task::{self, AddArgs, AliasArgs}, update, LockFileUsageArgs, }, - task::TaskName, + task::{ + get_task_env, ExecutableTask, RunOutput, SearchEnvironments, TaskExecutionError, TaskGraph, + TaskGraphError, TaskName, + }, Project, UpdateLockFileOptions, }; use pixi_consts::consts; @@ -41,6 +36,12 @@ use rattler_lock::{LockFile, Package}; use tempfile::TempDir; use thiserror::Error; +use self::builders::{HasDependencyConfig, RemoveBuilder}; +use crate::common::builders::{ + AddBuilder, InitBuilder, InstallBuilder, ProjectChannelAddBuilder, + ProjectEnvironmentAddBuilder, TaskAddBuilder, TaskAliasBuilder, UpdateBuilder, +}; + /// To control the pixi process pub struct PixiControl { /// The path to the project working file @@ -93,6 +94,13 @@ pub trait LockFileExt { platform: Platform, requirement: pep508_rs::Requirement, ) -> bool; + + fn get_pypi_package_version( + &self, + environment: &str, + platform: Platform, + package: &str, + ) -> Option; } impl LockFileExt for LockFile { @@ -157,6 +165,20 @@ impl LockFileExt for LockFile { .any(move |p| p.satisfies(&requirement)); package_found } + + fn get_pypi_package_version( + &self, + environment: &str, + platform: Platform, + package: &str, + ) -> Option { + self.environment(environment) + .and_then(|env| { + env.packages(platform) + .and_then(|mut packages| packages.find(|p| p.name() == package)) + }) + .map(|p| p.version().to_string()) + } } impl PixiControl { diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 000000000..34799d8db --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,8 @@ +from pathlib import Path + +import pytest + + +@pytest.fixture +def pixi() -> Path: + return Path(__file__).parent.joinpath("../../.pixi/target/release/pixi") diff --git a/tests/integration/test_main_cli.py b/tests/integration/test_main_cli.py index be850e3e7..240f5ac8e 100644 --- a/tests/integration/test_main_cli.py +++ b/tests/integration/test_main_cli.py @@ -1,7 +1,6 @@ from enum import IntEnum from pathlib import Path import subprocess -import pytest PIXI_VERSION = "0.29.0" @@ -12,11 +11,6 @@ class ExitCode(IntEnum): INCORRECT_USAGE = 2 -@pytest.fixture -def pixi() -> Path: - return Path(__file__).parent.joinpath("../../.pixi/target/release/pixi") - - def verify_cli_command( command: list[Path | str], expected_exit_code: ExitCode, diff --git a/tests/pypi-indexes/generate-indexes.py b/tests/pypi-indexes/generate-indexes.py new file mode 100644 index 000000000..50468c417 --- /dev/null +++ b/tests/pypi-indexes/generate-indexes.py @@ -0,0 +1,62 @@ +"""This is a little script to generate a custom pypi simple index from a directory of source packages.""" + +from pathlib import Path +import shutil +from build import ProjectBuilder + +indexes_path = Path(__file__).parent + +index_html_template = """\ + + + + + %LINKS% + + +""" + +for index_path in indexes_path.iterdir(): + if not index_path.is_dir(): + continue + + print(index_path) + + flat_index = index_path / "flat" + shutil.rmtree(flat_index) + flat_index.mkdir(exist_ok=True) + + wheels: list[Path] = [] + for package in (index_path / "src").iterdir(): + wheels.append(Path(ProjectBuilder(package).build("sdist", flat_index))) + wheels.append(Path(ProjectBuilder(package).build("wheel", flat_index))) + + index = index_path / "index" + shutil.rmtree(index) + index.mkdir(exist_ok=True) + + projects = {} + for wheel in wheels: + project = wheel.name.split("-")[0] + wheel_list = projects[project] = projects.get(project, []) + wheel_list.append(wheel) + + for project, wheels in projects.items(): + index_dir = index / project + index_dir.mkdir(exist_ok=True) + + for wheel in wheels: + (index_dir / wheel.name).hardlink_to(wheel) + + (index_dir / "index.html").write_text( + index_html_template.replace( + "%LINKS%", "\n".join(f'{wheel.name}' for wheel in wheels) + ) + ) + + (index / "index.html").write_text( + index_html_template.replace( + "%LINKS%", + "\n".join(f'{project}' for project in projects.keys()), + ) + ) diff --git a/tests/pypi-indexes/multiple-indexes-a/flat/foo-1.0.0-py2.py3-none-any.whl b/tests/pypi-indexes/multiple-indexes-a/flat/foo-1.0.0-py2.py3-none-any.whl new file mode 100644 index 000000000..c0ad6c04e Binary files /dev/null and b/tests/pypi-indexes/multiple-indexes-a/flat/foo-1.0.0-py2.py3-none-any.whl differ diff --git a/tests/pypi-indexes/multiple-indexes-a/flat/foo-1.0.0.tar.gz b/tests/pypi-indexes/multiple-indexes-a/flat/foo-1.0.0.tar.gz new file mode 100644 index 000000000..f1e201ccd Binary files /dev/null and b/tests/pypi-indexes/multiple-indexes-a/flat/foo-1.0.0.tar.gz differ diff --git a/tests/pypi-indexes/multiple-indexes-a/index/foo/foo-1.0.0-py2.py3-none-any.whl b/tests/pypi-indexes/multiple-indexes-a/index/foo/foo-1.0.0-py2.py3-none-any.whl new file mode 100644 index 000000000..c0ad6c04e Binary files /dev/null and b/tests/pypi-indexes/multiple-indexes-a/index/foo/foo-1.0.0-py2.py3-none-any.whl differ diff --git a/tests/pypi-indexes/multiple-indexes-a/index/foo/foo-1.0.0.tar.gz b/tests/pypi-indexes/multiple-indexes-a/index/foo/foo-1.0.0.tar.gz new file mode 100644 index 000000000..f1e201ccd Binary files /dev/null and b/tests/pypi-indexes/multiple-indexes-a/index/foo/foo-1.0.0.tar.gz differ diff --git a/tests/pypi-indexes/multiple-indexes-a/index/foo/index.html b/tests/pypi-indexes/multiple-indexes-a/index/foo/index.html new file mode 100644 index 000000000..af7bb9987 --- /dev/null +++ b/tests/pypi-indexes/multiple-indexes-a/index/foo/index.html @@ -0,0 +1,8 @@ + + + + + foo-1.0.0.tar.gz +foo-1.0.0-py2.py3-none-any.whl + + diff --git a/tests/pypi-indexes/multiple-indexes-a/index/index.html b/tests/pypi-indexes/multiple-indexes-a/index/index.html new file mode 100644 index 000000000..8d3fab220 --- /dev/null +++ b/tests/pypi-indexes/multiple-indexes-a/index/index.html @@ -0,0 +1,7 @@ + + + + + foo + + diff --git a/tests/pypi-indexes/multiple-indexes-a/src/foo-1.0.0/pyproject.toml b/tests/pypi-indexes/multiple-indexes-a/src/foo-1.0.0/pyproject.toml new file mode 100644 index 000000000..0e8510ea8 --- /dev/null +++ b/tests/pypi-indexes/multiple-indexes-a/src/foo-1.0.0/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[project] +name = "foo" +version = "1.0.0" diff --git a/tests/pypi-indexes/multiple-indexes-a/src/foo-1.0.0/src/foo/__init__.py b/tests/pypi-indexes/multiple-indexes-a/src/foo-1.0.0/src/foo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/pypi-indexes/multiple-indexes-b/flat/foo-2.0.0-py2.py3-none-any.whl b/tests/pypi-indexes/multiple-indexes-b/flat/foo-2.0.0-py2.py3-none-any.whl new file mode 100644 index 000000000..03dda3dac Binary files /dev/null and b/tests/pypi-indexes/multiple-indexes-b/flat/foo-2.0.0-py2.py3-none-any.whl differ diff --git a/tests/pypi-indexes/multiple-indexes-b/flat/foo-2.0.0.tar.gz b/tests/pypi-indexes/multiple-indexes-b/flat/foo-2.0.0.tar.gz new file mode 100644 index 000000000..ed2aa2710 Binary files /dev/null and b/tests/pypi-indexes/multiple-indexes-b/flat/foo-2.0.0.tar.gz differ diff --git a/tests/pypi-indexes/multiple-indexes-b/index/foo/foo-2.0.0-py2.py3-none-any.whl b/tests/pypi-indexes/multiple-indexes-b/index/foo/foo-2.0.0-py2.py3-none-any.whl new file mode 100644 index 000000000..03dda3dac Binary files /dev/null and b/tests/pypi-indexes/multiple-indexes-b/index/foo/foo-2.0.0-py2.py3-none-any.whl differ diff --git a/tests/pypi-indexes/multiple-indexes-b/index/foo/foo-2.0.0.tar.gz b/tests/pypi-indexes/multiple-indexes-b/index/foo/foo-2.0.0.tar.gz new file mode 100644 index 000000000..ed2aa2710 Binary files /dev/null and b/tests/pypi-indexes/multiple-indexes-b/index/foo/foo-2.0.0.tar.gz differ diff --git a/tests/pypi-indexes/multiple-indexes-b/index/foo/index.html b/tests/pypi-indexes/multiple-indexes-b/index/foo/index.html new file mode 100644 index 000000000..91bbcf596 --- /dev/null +++ b/tests/pypi-indexes/multiple-indexes-b/index/foo/index.html @@ -0,0 +1,8 @@ + + + + + foo-2.0.0.tar.gz +foo-2.0.0-py2.py3-none-any.whl + + diff --git a/tests/pypi-indexes/multiple-indexes-b/index/index.html b/tests/pypi-indexes/multiple-indexes-b/index/index.html new file mode 100644 index 000000000..8d3fab220 --- /dev/null +++ b/tests/pypi-indexes/multiple-indexes-b/index/index.html @@ -0,0 +1,7 @@ + + + + + foo + + diff --git a/tests/pypi-indexes/multiple-indexes-b/src/foo-2.0.0/pyproject.toml b/tests/pypi-indexes/multiple-indexes-b/src/foo-2.0.0/pyproject.toml new file mode 100644 index 000000000..a5c8b5919 --- /dev/null +++ b/tests/pypi-indexes/multiple-indexes-b/src/foo-2.0.0/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[project] +name = "foo" +version = "2.0.0" diff --git a/tests/pypi-indexes/multiple-indexes-b/src/foo-2.0.0/src/foo/__init__.py b/tests/pypi-indexes/multiple-indexes-b/src/foo-2.0.0/src/foo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/pypi-indexes/multiple-indexes-c/flat/foo-3.0.0-py2.py3-none-any.whl b/tests/pypi-indexes/multiple-indexes-c/flat/foo-3.0.0-py2.py3-none-any.whl new file mode 100644 index 000000000..f103e66ba Binary files /dev/null and b/tests/pypi-indexes/multiple-indexes-c/flat/foo-3.0.0-py2.py3-none-any.whl differ diff --git a/tests/pypi-indexes/multiple-indexes-c/flat/foo-3.0.0.tar.gz b/tests/pypi-indexes/multiple-indexes-c/flat/foo-3.0.0.tar.gz new file mode 100644 index 000000000..6167484cc Binary files /dev/null and b/tests/pypi-indexes/multiple-indexes-c/flat/foo-3.0.0.tar.gz differ diff --git a/tests/pypi-indexes/multiple-indexes-c/index/foo/foo-3.0.0-py2.py3-none-any.whl b/tests/pypi-indexes/multiple-indexes-c/index/foo/foo-3.0.0-py2.py3-none-any.whl new file mode 100644 index 000000000..f103e66ba Binary files /dev/null and b/tests/pypi-indexes/multiple-indexes-c/index/foo/foo-3.0.0-py2.py3-none-any.whl differ diff --git a/tests/pypi-indexes/multiple-indexes-c/index/foo/foo-3.0.0.tar.gz b/tests/pypi-indexes/multiple-indexes-c/index/foo/foo-3.0.0.tar.gz new file mode 100644 index 000000000..6167484cc Binary files /dev/null and b/tests/pypi-indexes/multiple-indexes-c/index/foo/foo-3.0.0.tar.gz differ diff --git a/tests/pypi-indexes/multiple-indexes-c/index/foo/index.html b/tests/pypi-indexes/multiple-indexes-c/index/foo/index.html new file mode 100644 index 000000000..72f56b076 --- /dev/null +++ b/tests/pypi-indexes/multiple-indexes-c/index/foo/index.html @@ -0,0 +1,8 @@ + + + + + foo-3.0.0.tar.gz +foo-3.0.0-py2.py3-none-any.whl + + diff --git a/tests/pypi-indexes/multiple-indexes-c/index/index.html b/tests/pypi-indexes/multiple-indexes-c/index/index.html new file mode 100644 index 000000000..8d3fab220 --- /dev/null +++ b/tests/pypi-indexes/multiple-indexes-c/index/index.html @@ -0,0 +1,7 @@ + + + + + foo + + diff --git a/tests/pypi-indexes/multiple-indexes-c/src/foo-3.0.0/pyproject.toml b/tests/pypi-indexes/multiple-indexes-c/src/foo-3.0.0/pyproject.toml new file mode 100644 index 000000000..5b1e677d5 --- /dev/null +++ b/tests/pypi-indexes/multiple-indexes-c/src/foo-3.0.0/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling"] + +[project] +name = "foo" +version = "3.0.0" diff --git a/tests/pypi-indexes/multiple-indexes-c/src/foo-3.0.0/src/foo/__init__.py b/tests/pypi-indexes/multiple-indexes-c/src/foo-3.0.0/src/foo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/pypi-tests.rs b/tests/pypi-tests.rs new file mode 100644 index 000000000..2c77de8fe --- /dev/null +++ b/tests/pypi-tests.rs @@ -0,0 +1,85 @@ +mod common; + +use std::path::Path; + +use crate::common::{LockFileExt, PixiControl}; +use rattler_conda_types::Platform; +use url::Url; + +#[tokio::test] +async fn test_index_strategy() { + let pypi_indexes = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/pypi-indexes"); + let pypi_indexes_url = Url::from_directory_path(pypi_indexes).unwrap(); + + let pixi = PixiControl::from_manifest(&format!( + r#" + [project] + name = "pypi-extra-index-url" + platforms = ["{platform}"] + channels = ["conda-forge"] + + [dependencies] + python = "~=3.12.0" + + [pypi-dependencies] + foo = "*" + + [pypi-options] + extra-index-urls = [ + "{pypi_indexes}multiple-indexes-a/index", + "{pypi_indexes}multiple-indexes-b/index", + "{pypi_indexes}multiple-indexes-c/index", + ] + + [feature.first-index.pypi-options] + index-strategy = "first-index" + + [feature.unsafe-first-match-unconstrained.pypi-options] + index-strategy = "unsafe-first-match" + + [feature.unsafe-first-match-constrained.pypi-options] + index-strategy = "unsafe-first-match" + + [feature.unsafe-first-match-constrained.pypi-dependencies] + foo = "==3.0.0" + + [feature.unsafe-best-match.pypi-options] + index-strategy = "unsafe-best-match" + + [environments] + default = ["first-index"] + unsafe-first-match-unconstrained = ["unsafe-first-match-unconstrained"] + unsafe-first-match-constrained = ["unsafe-first-match-constrained"] + unsafe-best-match = ["unsafe-best-match"] + "#, + platform = Platform::current(), + pypi_indexes = pypi_indexes_url, + )); + + let lock_file = pixi.unwrap().update_lock_file().await.unwrap(); + + assert_eq!( + lock_file.get_pypi_package_version("default", Platform::current(), "foo"), + Some("1.0.0".into()) + ); + assert_eq!( + lock_file.get_pypi_package_version( + "unsafe-first-match-unconstrained", + Platform::current(), + "foo" + ), + Some("1.0.0".into()) + ); + assert_eq!( + lock_file.get_pypi_package_version( + "unsafe-first-match-constrained", + Platform::current(), + "foo" + ), + Some("3.0.0".into()) + ); + assert_eq!( + lock_file.get_pypi_package_version("unsafe-best-match", Platform::current(), "foo"), + Some("3.0.0".into()) + ); +}