diff --git a/cep-XXX0.md b/cep-XXX0.md new file mode 100644 index 00000000..7cc0c985 --- /dev/null +++ b/cep-XXX0.md @@ -0,0 +1,258 @@ +# CEP XXXX - Wheel support in conda (overview) + + + + + + + + + + +
Title Wheel support in conda (overview)
Status Draft
Author(s) + Dan Yeaw <dyeaw@anaconda.com> +
Created Apr 24, 2026
Updated Apr 24, 2026
Discussion https://github.com/conda/ceps/pull/145
Implementation TBD
See also [CEP XXX1 – Repodata wheel support](cep-XXX1.md), [CEP XXX2 – Wheel conda client support](cep-XXX2.md)
+ +> This document is **informative context** for the normative split between [CEP XXX1 – Repodata wheel support][cep-xxx1] (channel index / `whl` records) and [CEP XXX2 – Wheel conda client support][cep-xxx2] (solver, download, install). It collects motivation, history, and rejected alternatives. Unless stated otherwise, it does not add new SHALL requirements beyond pointing readers to those CEPs. + +## Abstract + +Pure Python wheels can be indexed in conda channel repodata so users solve environments in one pass with conda packages. This overview explains **why** that direction was chosen for conda, traces **historical** discussion and tooling, and documents **rejected ideas** and ecosystem alternatives. The repodata format and publisher rules live in [CEP XXX1][cep-xxx1]; conda client behavior lives in [CEP XXX2][cep-xxx2]. + +## Companion CEPs + +| CEP | Role | +|---------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [CEP XXX1 – Repodata wheel support][cep-xxx1] | Normative specification of the `whl` mapping under `v{revision}`, record fields, `METADATA` conversion, validation, and channel-operator guidance. | +| [CEP XXX2 – Wheel conda client support][cep-xxx2] | Normative specification of loading `whl` into the solver index with other repodata records, conditional dependencies and extras, download, and prefix integration as `noarch: python` conda packages. | + +Together, the three documents describe **native** wheel support: wheels are first-class index entries, not a parallel pip-only install path. Scope is limited to **pure Python** wheels (`py*-none-any` class) in [CEP XXX1][cep-xxx1] to avoid platform-specific binary compatibility questions in this round of work. + +## Motivation + +While conda remains a language-agnostic package manager, installing packages for use with Python represents the majority of the current conda usage. Users frequently encounter packages only available as wheels on PyPI, forcing them to use hybrid workflows that mix conda and PyPI installations. This approach creates several problems: + +- Mixing of conda and pip or uv can result in overwritten files and broken environments +- Users must understand two package managers, their interaction points, and which of their dependencies are available from which ecosystem +- Where there is support for mixing environments, it requires multiple solves which reduces overall package installation performance + +Adding native support for pure Python wheels—**indexed in repodata** and **consumed by conda clients** per [CEP XXX1][cep-xxx1] and [CEP XXX2][cep-xxx2], has the following advantages: + +- Reduce the cognitive burden of managing two package managers +- Provide users with transparent access to a broader slice of the Python ecosystem through channels that choose to publish wheels +- Maintain environment consistency and reproducibility under conda's package model +- Resolve dependencies across conda and wheel records in a single solve where channels publish both +- Fill gaps in conda package availability for simpler pure-Python packages +- Reduce the maintenance burden for straightforward pure-Python packages that do not require recipe-level metadata fixes + +[CEP XXX1][cep-xxx1] does not eliminate the need for conda recipes. Many packages still require feedstocks due to metadata differences, dependency corrections, compiled dependencies, or ecosystem-specific needs. Indexed wheels are an additional tool for simpler pure-Python cases. + +## History + +PyPI integration has been debated for years in tension with conda's emphasis on reproducible, binary-first environments. + +### Early Vision (2012-2014) + +Early `conda` assumed environments could mix conda-installed packages with software brought in by other tools, and tried to fold those installs into environments managed by conda. The first concrete step was PyPI: within weeks of conda's first release, it shipped a `pip` subcommand. +This feature took a snapshot of untracked files, ran that environment's `pip install`, created a diff, packed the new files into one `.tar.bz2`, and then reinstalled them with conda. However, folding arbitrary pip installs into a single conda record was a poor fit for dependency identity, upgrades, and reuse. The subcommand was removed in 1.8 for that model and to reduce confusion between `conda pip` and ordinary `pip` in an activated environment. + +Issue [#327](https://github.com/conda/conda/issues/327) added `--use-pypi`. conda 4.6.0 introduced experimental `prefix_data_interoperability` to reconcile pip-installed metadata with conda's when enabled. However, it remained off by default because of the performance cost. + +Representative issues from the same period: [#307](https://github.com/conda/conda/issues/307) (2013, pip, npm, gems, rpm, brew), [#292](https://github.com/conda/conda/issues/292) (2013, "ultimate package manager"), [#224](https://github.com/conda/conda/issues/224) (2012, native conda vs pip commands). + +### The PyPI Integration Debate (2013-2016) + +Two competing philosophies emerged on how or if we should provide better integration with PyPI. + +#### Pro-integration + +Issue [#262](https://github.com/conda/conda/issues/262) (2013): conda should install directly from PyPI to reduce duplication and improve package availability. + +conda-forge issue [#28](https://github.com/conda-forge/conda-forge.github.io/issues/28) (Feb 2016): Chris Barker raises the pure-Python package gap from the conda-forge community's perspective, proposing either a real-time PyPI bridge (on-the-fly conda package generation from PyPI) or an automated conda skeleton farm to mirror popular PyPI packages with version tracking. + +Arguments for this approach: + +- Users frequently need packages not available on conda channels +- Building conda packages for every PyPI package is unsustainable + +#### Status quo + +Arguments for maintaining an independent packaging ecosystem prevailed at the time for the following reasons: + +- Conda's value proposition is reproducible, binary-focused environments with precise dependency resolution +- PyPI's source distributions and pip's resolver could compromise conda's guarantees +- The conda-forge community successfully scaled recipe creation + +### Wheel Support Proposal (2017) + +Issue [#5202](https://github.com/conda/conda/issues/5202) proposed direct wheel installation support, recognizing: + +- Wheels provide binary distributions similar to conda packages +- Growing wheel availability on PyPI reduced build complexity +- Could bridge the gap between conda's reliability and PyPI's breadth + +### Broader Context (2018) + +The Python Discourse thread on [packaging scope boundaries](https://discuss.python.org/t/drawing-a-line-to-the-scope-of-python-packaging/883) reflects the larger ecosystem's struggle with: + +- Multiple competing package management tools (pip, conda, poetry, pipenv) +- Unclear responsibilities and interoperability expectations +- Need for clearer standards and communication between tools + +### conda-pypi development starts (2022) + +Jaime Rodríguez-Guerra starts development on the conda-pypi plugin aimed to improve conda and PyPI interoperability. + +### Rip development starts (2023) + +Prefix.dev starts a barebones pip implemented in Rust to resolve and install PyPI dependencies with Pixi. + +### Whl2conda development starts (Aug 2023) + +Christopher Barber develops [whl2conda](https://github.com/zuzukin/whl2conda), a command-line tool that generates conda packages from pure Python wheels (and related packaging workflows) without going through a full `conda-build` recipe for every package. + +### Pixi Integrates with uv (Jan 2024) + +Pixi changes course to use uv directly instead of rip, which unlocks features like editable installations, and git and path dependencies. + +### conda-pupa creates on-the-fly conversion plugin (July 2024) + +Daniel Holth creates a conda plugin that supports on-the-fly conversion of PyPI wheels to conda packages in a local channel. + +### conda-whl-channel creates proof-of-concept wheel channel from repodata (Nov 2024) + +Jonathan Helmus and Anil Kulkarni begin development on [conda-whl-channel](https://github.com/Anaconda/conda-whl-channel), a proof-of-concept that adds wheels to the `packages` section of repodata and patches conda to recognize them. The implementation supports conditional dependencies through additional meta packages. In February 2025, the conda-specific functionality is extracted into a separate plugin called [conda-whl-support](https://github.com/Anaconda/conda-whl-support). + +### conda-pypi merges in conda-pupa functionality (Oct 2025) + +conda-pupa is merged into conda-pypi which adds a `conda pypi install ` command and support for editable installations. + +### conda-pypi integrates parts of conda-whl-support (Nov 2025) + +conda-pypi incorporates the wheel detection logic from conda-whl-support, providing core functionality beyond the solver and index changes required to support the `whl` section proposed in [CEP XXX1][cep-xxx1], together with channel relation metadata when channels adopt [Channel relations in repodata][cep-channel-relations]. + +### Native wheel unpack in conda-pypi; Rattler reads wheels in repodata (Dec 2025) + +conda-pypi adds native support for unpacking `.whl` artifacts into the target environment (rather than delegating that step to pip). Rattler adds support for reading wheel records from repodata, for example, the `whl` mapping under a registered `v{revision}`, so clients built on Rattler can load the same index surface described in [CEP XXX1][cep-xxx1]. + +## Rejected ideas + +### Only install Python and pip inside conda environments + +In this scenario, users only install Python and pip inside a clean conda environment. Here, we simply use conda as an environment manager and let pip manage the project dependencies. + +This is what that typically looks like: + +```bash +conda create -n pip-environment python pip +conda activate pip-environment +pip install +``` + +Despite its safety for environment management, relying solely on `conda` for this purpose prevents leveraging the package distribution capabilities of the conda ecosystem. + +### Editable installs with `conda` for dependencies only + +Conda provides all the dependencies of a given package. Then that package is installed on top in editable mode, without addressing dependencies to make sure conda files aren't accidentally overwritten: + +```bash +git clone https://github.com/owner/package.git +conda create -n editable-install package --deps-only +conda activate editable-install +pip install -e . --no-deps +``` + +This pattern was rejected because it still splits responsibility between `conda` and `pip`: the editable project is not a first-class conda package, so `conda list`, upgrades, removals, and reproducible environment exports do not fully describe what is on disk. +Relying on `--no-deps` avoids one class of conflicts but leaves no single solver pass for the project under development and its dependencies, and it does not remove the risk of accidental `pip install` (with dependencies) overwriting files conda manages. + +### Add more conda packages + +Create and maintain new conda packages for each PyPI dependency needed. Tools like [Grayskull][grayskull] exist to make this easier to convert. However, this is a significant workload for the community, with over half of all conda-forge packages being pure Python. Even with more dedicated resources, creating recipes for over 500 thousand pure Python packages is not achievable. + +### Add interoperability to tools through pip dependency scanning + +The original version of the conda-pypi plugin called `pip` with the `--dry-run` option to analyze the solution to install a package. With a list of all the dependencies needed, the plugin installed everything available in conda channels first and what was left over would be installed with `pip install --no-deps`. Disadvantages of this approach include: + +- Users still have to know which packages they want from PyPI and then have to run `conda pip install` to install them. +- There is no guarantee that the conda and pip packages installed have ABI compatibility. +- Calling pip and conda multiple times is slow. + +### Add interoperability to tools through on-the-fly conversion + +This is the approach that the [conda-pupa][conda-pupa] plugin used and was then implemented in [conda-pypi][conda-pypi]. When `conda pypi install ` is called, it fetches its set of required dependencies iteratively from PyPI just like `pip`. Similar to the dependency scanning option above, it then attempts to install as many dependencies as it can from `conda`. +However, this is where these two approaches start to differ. While conda-pypi simply used `pip` to install the remaining Python dependencies, conda-pupa converts wheel packages to conda and stores them in a local channel, essentially caching these converted wheels on disk. This means that a repodata.json is also generated allowing us to perform a solve entirely in conda. + +Unfortunately, there are also disadvantages with this approach. Like the solution above, users still have to know which packages they want from PyPI and then have to run `conda pypi install` to install them. Additionally, the following problems also arise: + +- Package conversion can take some time, especially for larger packages. +- Users must rely on a local cache for installing wheels, and this cache cannot easily be shared across computers. +- The current version must solve multiple times which is slow, although this could be optimized. + +### Add interoperability to tools through uv integration + +Pixi has integrated uv for installing packages from PyPI. The user adds the dependency through `pixi add --pypi `. Then, when Pixi is solving the environment, it solves the conda packages using Rattler, and then calls uv to solve the PyPI dependencies. Disadvantages of this approach include: + +- Like the solutions above, users still have to know which packages they want from PyPI and then have to run `pixi add --pypi` on them. +- Although Pixi and uv are both very fast, it is still slower than performing a single solve of the environment. + +### Direct PyPI communication without repodata + +Another alternative would be for conda clients to query PyPI's API directly during solving, fetching wheel metadata on-demand rather than including it in repodata. This idea was rejected due to: + +- While resolvo (used by Rattler) supports dynamic metadata fetching during solving (as showcased in [rip](https://github.com/prefix-dev/rip)), libsolv requires complete package metadata upfront. This inconsistency across solvers would complicate implementation and limit compatibility. + +- While on-demand fetching works well for pip and uv, using repodata provides consistency with conda's existing infrastructure, enables better caching strategies, and allows channels to curate and validate packages before they're available to users. + +### Magic local channel approach + +The "magic local channel" approach () was considered but rejected. This approach involves: + +- A local FastAPI app intercepts the CLI and detects PyPI specs +- Fetches metadata on the fly and converts it to repodata (following some of the ideas discussed above) +- Downloads the wheels and converts them to .conda via whl2conda +- Caches and installs the .conda artifacts + +While this approach provides on-demand conversion and caching, it requires a separate service to be running and adds complexity to the user workflow. The chosen approach of native repodata wheel support provides a more seamless experience where wheels are pre-indexed in channels and work with standard conda workflows without requiring additional services. + +### Automatic wheel to conda package conversion and hosting + +Another alternative would be establishing a build farm to automatically convert wheels to conda packages and host them on a channel, with conversion triggered by popularity, community requests, or on-demand. + +#### Advantages of automatic conversion + +- All packages are conda packages, simplifying client implementation +- Leverages mature conda infrastructure and tooling without client changes +- Enables metadata enrichment, quality control, and validation before publishing + +#### Disadvantages and why native wheel support was chosen + +Despite these advantages, this approach was rejected because: + +- **Infrastructure burden:** Requires significant storage and bandwidth to host and serve converted packages that duplicate PyPI's CDN infrastructure +- **Resource inefficiency:** Wheels are already an excellent format for pure Python packages. Conversion adds no technical value and wastes resources + +Native wheel support provides the same user experience (transparent access to pure-Python packages through channels that index and host wheels like other packages) while avoiding the infrastructure burden and resource inefficiency of conversion. Channel operators who prefer converted packages can continue building conda packages from PyPI sources. + +## References + +- [CEP XXX1 – Repodata wheel support][cep-xxx1] +- [CEP XXX2 – Wheel conda client support][cep-xxx2] +- [Adopting uv in pixi][uv-in-pixi] +- [rip][rip] +- [conda-pypi project][conda-pypi] +- [Channel relations in repodata (PR 155)][cep-channel-relations] +- [conda-pupa][conda-pupa] + +## Copyright + +All CEPs are explicitly [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). + + +[cep-xxx1]: cep-XXX1.md +[cep-xxx2]: cep-XXX2.md +[conda-pypi]: https://github.com/conda-incubator/conda-pypi +[cep-channel-relations]: https://github.com/conda/ceps/pull/155 +[conda-pupa]: https://github.com/dholth/conda-pupa +[uv-in-pixi]: https://prefix.dev/blog/uv_in_pixi +[rip]: https://github.com/prefix-dev/rip +[grayskull]: https://conda.github.io/grayskull/ diff --git a/cep-XXX1.md b/cep-XXX1.md new file mode 100644 index 00000000..70a7cca0 --- /dev/null +++ b/cep-XXX1.md @@ -0,0 +1,377 @@ +# CEP XXXX - Repodata Wheel Support + + + + + + + + + + + +
Title Repodata Wheel Support
Status Draft
Author(s) + Dan Yeaw <dyeaw@anaconda.com>
+ Travis Hathaway <travis.j.hathaway@gmail.com> +
Created Dec 23, 2025
Updated Apr 24, 2026
Discussion https://github.com/conda/ceps/pull/145
Implementation TBD
Requires https://github.com/conda/ceps/pull/146 https://github.com/conda/ceps/pull/155 [CEP 43](https://conda.org/learn/ceps/cep-0043) [CEP 44](https://conda.org/learn/ceps/cep-0044) [CEP XXX2 (Wheel conda client support)](cep-XXX2.md)
See also [CEP XXX0 – Wheel support in conda (overview)](cep-XXX0.md)
+ +> The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", +"RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as +described in [RFC2119][RFC2119] when, and only when, they appear in all capitals, as shown here. + +## Abstract + +This CEP specifies how channel operators publish pure Python wheel packages in conda's package index (repodata): the `whl` member under `v{revision}`, record fields, publisher-side dependency conversion from wheel `METADATA`, and validation rules. +It does not define conda client solver, download, or install-time behavior; those are specified in [CEP XXX2 – Wheel conda client support][cep-xxx2]. +Motivation, ecosystem history, and rejected alternatives are in [CEP XXX0 – Wheel support in conda (overview)][cep-xxx0]. +We limit the scope to pure Python wheels to avoid platform-specific binary compatibility issues. Even so, publishing wheels in repodata greatly increases the packages channels can expose without building conda recipes for every case. + +## Relationship to companion CEPs + +- **[CEP XXX0 – Wheel support in conda (overview)][cep-xxx0]** — Why native wheel support, historical context, and discussion of rejected ideas (informative). +- **[CEP XXX2 – Wheel conda client support][cep-xxx2]** — Normative client behavior: loading `whl` into the solve with other repodata records, download, and installation. + +## Specification + +### Expose wheels via `whl` inside a repodata revision (`v{revision}`) + +According to the current schema for [repodata.json][repodata-schema], the repodata object is traditionally organized around these top-level keys (the surface unchanged clients rely on): + +- repodata_version +- info +- packages +- packages.conda +- removed + +Wheel support MUST follow [A backwards-compatible repodata update strategy](https://github.com/conda/ceps/pull/146): publishers SHALL register the revision in `info.repodata_revisions` and SHALL place the wheel index under the matching top-level `v{revision}` dictionary (where `revision` is the integer listed for that entry). This CEP specifies a `whl` member inside that `v{revision}` object. The value MUST be a mapping whose entries are [repodata record][repodata-record-schema] objects. + +### `whl` dictionary structure + +The `whl` dictionary maps conda-like filenames to repodata records. The key MUST follow the format specified in [Key naming requirements](#key-naming-requirements). The value MUST be a repodata record object that conforms to the [repodata record schema][repodata-record-schema] with the following field specifications: + +- **`name`**: Taken from the wheel's METADATA `Name` field, normalized per [CEP 26][cep-26] and any name mappings aligned with the channel declared in `info.channel_relations.base` when present (see [Naming standard and channel mapping](#naming-standard-and-channel-mapping)). +- **`version`**: Taken from the wheel's METADATA `Version` field, normalized per PEP 440. +- **`build`**: Format `py{PY_MAJOR_VERSION}_{abi_tag}_{platform_tag}_{build_number}` (e.g., `py3_none_any_0`), conforming to CEP 26 build string conventions and the repodata record schema pattern `^([a-z0-9_.]+_)?[0-9]+$`. The build number MUST be at the end of the build string. The `{abi_tag}` and `{platform_tag}` are extracted from the wheel filename. +- **`build_number`**: As in regular conda packages. MUST be 0 initially. MAY be incremented for rebuilds. +- **`depends`**: Array including: + - `python` dependency from `Requires-Python` (if present), converted to conda format + - All `Requires-Dist` entries from METADATA, converted from PEP 440 to conda format per [Dependency conversion](#dependency-conversion) + - Package names normalized to conda-style names per [CEP 26][cep-26] +- **`extra_depends`**: MAY be present. When present, MUST be an object mapping extra names to lists of dependency strings for optional groups, per [CEP 44][cep-44]. When absent or empty, the record declares no optional groups beyond `depends`. +- **`subdir`**: MUST be `"noarch"`. +- **`noarch`**: MUST be `"python"`. +- **`fn`**: MUST be the wheel filename (`.whl`), as in standard conda repodata records (for example `requests-2.32.5-py3-none-any.whl`). +- **`sha256`**, **`size`**: Standard repodata fields for the wheel file. + +### Key naming requirements + +The key for each entry in `whl` MUST follow the standard conda distribution string format per [CEP 26][cep-26]: `{name}-{version}-{build string}`, where: + +- `{name}` is derived from the wheel's METADATA file (the `Name` field), normalized according to conda naming conventions per [CEP 26][cep-26] and any name mappings inherited from the base channel when `info.channel_relations.base` is declared (see [Naming standard and channel mapping](#naming-standard-and-channel-mapping)) +- `{version}` is the package version from METADATA +- `{build}` is the build string (e.g., `py3_none_any_0`, `py3_none_any_1`) from the `build` field, which includes the Python version, ABI tag, platform tag, and build number (with build number at the end) + +Examples: + +- `httpx-0.28.1-py3_none_any_0` +- `typing_extensions-4.15.0-py3_none_any_0` (METADATA name `typing-extensions` mapped to conda name `typing_extensions`) +- `requests-2.32.5-py3_none_any_1` (rebuild with build_number 1) + +### Naming standard and channel mapping + +The key name for wheel records SHALL follow the standard conda distribution string format `{name}-{version}-{build}` per [CEP 26][cep-26]. The build string includes the Python version, ABI tag, platform tag, and build number (e.g., `py3_none_any_0`), where the build number is at the end per the repodata record schema pattern. +The ABI and platform tags are extracted from the wheel filename. The python_tag from the wheel filename is omitted as it is redundant with the Python version in the build string. This ensures compatibility with standard conda parsing while preserving wheel tag information within the build string. + +When there are naming differences between PyPI wheels and conda packages, channel operators MUST determine the appropriate conda-style name by applying conda naming conventions per [CEP 26 - Identifying Packages and Channels in the conda Ecosystem][cep-26]. + +To help users understand which naming conventions are being used, a wheel channel MAY declare a **base** channel using `info.channel_relations.base` as specified in [Channel relations in repodata][cep-channel-relations]. +The base channel is the one clients load with higher priority. It is also the natural source of conda-style naming conventions and PyPI-to-conda name mappings for wheels published alongside that stack (for example, `conda-forge` as the base for a specialized wheel index on the same channel host). +Channel references MUST use relative paths as required by that CEP (for example `"base": "../conda-forge"`). Absolute URLs in `info` are not valid channel references for relations. + +Channel operators SHOULD document any naming conventions and mappings specific to their channel, including how they relate to their declared `channel_relations`. + +### Wheel download locations + +Wheel artifacts SHALL be fetched like other conda packages: combine the channel location (or the repodata `info.base_url` when present, per [CEP 15][cep-15] / [CEP 16][cep-16]) with `subdir` and `fn`. Channel operators MUST make the `.whl` file available at that URL (typically `{channel}/noarch/{fn}`). + +The `whl` mapping (inside the `v{revision}` payload) SHALL follow the backwards-compatible update strategy ([PR 146](https://github.com/conda/ceps/pull/146)). + +### Wheel-Specific Record Values + +When populating repodata records for pure Python wheels: + +- `build`: MUST be py`PY_MAJOR_VERSION`_`abi_tag`_`platform_tag`_`build_number` (e.g. `py3_none_any_0`), where `{abi_tag}` and `{platform_tag}` are extracted from the wheel filename, and the build number MUST be at the end of the build string per the repodata record schema pattern +- `build_number`: MUST be 0 for the initial addition of a wheel version. MAY be incremented for subsequent rebuilds of the same wheel version (e.g., to correct dependencies or metadata) +- `subdir`: MUST be "noarch" +- `noarch`: MUST be "python" +- `fn`: MUST be the wheel filename (`.whl`) + +### Pure Python wheel validation + +Before adding a wheel to `whl`, channel operators MUST verify: + +- The wheel's platform tag is `any` (i.e., wheel filenames ending in `-none-any.whl`, such as `requests-2.32.5-py3-none-any.whl`) +- The wheel's ABI tag is `none` +- The wheel contains no compiled extensions (`.so`, `.pyd`, `.dylib` files) +- The wheel's `METADATA` file is present and valid + +Wheels that fail any of these checks MUST NOT be added to `whl`. + +### Dependency conversion + +Channel operators MUST convert Python dependency declarations—`Requires-Dist`, `Requires-Python`, and extras—into conda `depends`, **`extra_depends`** when applicable, and related fields, following the rules below. + +Those declarations MAY be read from the wheel's embedded **`METADATA`** and/or from the [**PyPI JSON API**][pypi-json-api] release payload for the same **project** and **version** as the wheel being indexed. Using the API can simplify batch indexing and mirrors. Regardless of source, the same conversion rules and interoperable minimum in [PEP 508 marker translation guidance](#pep-508-marker-translation-guidance) apply. + +- **Package names:** Names per [CEP 26][cep-26] and match existing conda-forge package names where they exist +- **Version specifiers:** Map PEP 440 [version specifiers][version-specifiers] to conda format: + - The `==` operator is converted to an exact pin (removing the `==`) + - All other PEP 440 operators (`>=`, `<=`, `<`, `>`, `~=`, `!=`) are used as-is + - Multiple version specifiers are combined with commas (e.g., `>=1.0,<2.0`) + +- **Python version requirements:** Convert Requires-Python to explicit python dependency +- **Environment markers:** Map Python-version markers on `Requires-Dist` to conditional `MatchSpec` `when=` as specified in [CEP 43][cep-43]. Other markers are out of scope for the default conversion rules in this CEP (see [Limitations](#limitations)). + +Example conversion: + +```text +# Wheel METADATA +Requires-Python: >=3.8 +Requires-Dist: requests (>=2.20.0,<3.0.0) +Requires-Dist: click (>=7.0) +Requires-Dist: numpy (>=1.20.0,!=1.24.0) +Requires-Dist: importlib-metadata (>=1.0) ; python_version < '3.10' +``` + +Resulting conda record: + +```json +{ + "depends": [ + "python >=3.8", + "requests >=2.20.0,<3.0", + "click >=7.0", + "numpy >=1.20.0,!=1.24.0", + "importlib-metadata>=1.0[when=\"python<3.10\"]" + ] +} +``` + +### PEP 508 marker translation guidance + +This subsection layers interoperable expectations for the repodata format, non-normative reference material for richer mappings, and a general translation policy. + +#### Interoperable minimum + +For published `whl` records, the following conversions are the **normative floor** aligned with [Dependency conversion](#dependency-conversion) and [CEP 43][cep-43] and [CEP 44][cep-44]: + +- **`Requires-Python`:** MUST appear as an explicit `python` dependency in `depends`. +- **Python-version conditions on `Requires-Dist`:** MUST be represented with conditional `MatchSpec` `when=` on the relevant dependency strings per [CEP 43][cep-43]. +- **`extra` (optional groups):** MUST be represented with **`extra_depends`** and optional-group selection per [CEP 44][cep-44], not only as opaque marker text on `depends`. + +Channels and tools MAY apply additional, stricter mappings where they can express them in repodata. + +#### Non-normative extended mappings + +Per-variable translation of other PEP 508 markers (for example `sys_platform`, `platform_system`) to `when=` fragments, virtual packages, or omissions is **not** fully specified in this CEP. For a non-normative, per-variable description—including how **conda-pypi** maps markers to `when=` and **`extra_depends`**—see [PEP 508 marker conversion][conda-pypi-marker-conversion] in the conda-pypi documentation. + +#### Lossy translation + +When a channel cannot encode a PEP 508 dimension in repodata (for example no virtual package for an architecture string, or unsupported boolean branches), the operator MAY **omit** the condition, **simplify** the requirement, or **widen** the dependency (for example listing a package in `depends` without `when=` so it applies in more environments than PEP 508 would). +Channel operators SHOULD document their policy for such cases (including any conservative “include unconditionally” behavior). + +### Handling conditional dependencies and extras + +Wheel `depends` entries that encode PEP 508 environment markers MUST use conditional `MatchSpec` syntax with `when=` as specified in [CEP 43][cep-43]. + +PyPI **extras** (optional dependency groups) MUST be represented on the wheel repodata record using an **`extra_depends`** object (a mapping from extra name to lists of dependency strings) as specified in [CEP 44][cep-44]. A default install uses only `depends` and MUST NOT union in `extra_depends` entries unless the user selects optional groups (for example with `extras=` in `MatchSpec` as described in [CEP 44][cep-44]). + +### Solver behavior + +Normative rules for how conda clients treat `whl` records during dependency resolution (same candidate pool as other repodata records; no CEP-mandated default between conda builds and wheels) are defined in [CEP XXX2 – Wheel conda client support][cep-xxx2]. +This CEP only requires that published `whl` records are valid [repodata records][repodata-record-schema] so that clients can consume them alongside `packages` and `packages.conda`. + +### Limitations + +This CEP has the following known limitations: + +1. **Pure Python only:** This CEP explicitly does not address wheels with binary extensions, which require platform-specific compatibility guarantees beyond the current scope. Conda’s strength is binary compatibility, so using conda packages may be the optimal solution. +2. **Environment markers (publisher defaults):** Default conversion rules in this CEP focus on Python-version markers on `Requires-Dist`, mapped to `when=` per [CEP 43][cep-43]. Other markers (for example `sys_platform`) are out of scope for those default publisher rules and are not required to be converted into repodata here; see [PEP 508 marker translation guidance](#pep-508-marker-translation-guidance). +How conda clients evaluate `when=` and optional groups at solve time (including environment context) is specified in [CEP XXX2][cep-xxx2] together with [CEP 43][cep-43] and [CEP 44][cep-44]. +3. **Conditionals and extras:** Normative syntax and record fields for `when=` on `depends` and for optional groups in **`extra_depends`** are specified in [CEP 43][cep-43] and [CEP 44][cep-44], on which this repodata CEP and [CEP XXX2][cep-xxx2] rely for PyPI-aligned conditionals and extras in published records and at client solve time, respectively. +4. **Repodata size:** Supporting a significant portion of pure Python packages from PyPI (potentially hundreds of thousands of packages with multiple versions each) will substantially increase repodata size. Channels adopting wheel support at scale SHOULD implement sharded repodata ([CEP 16][cep-16]) to maintain acceptable performance. + +## Rationale + +### Why wheel names may differ from conda names + +Several factors can cause wheel names to differ from conda-style names: + +1. **Wheel filename normalization:** Wheel filenames normalize hyphens to underscores per PEP 427, but the METADATA `Name` field contains the canonical name (which may have hyphens). + - Example: `lazy-loader` (METADATA name) vs `lazy_loader` (wheel filename) + +2. **Python-specific clarification:** PyPI packages are implicitly Python libraries + - Example: `authzed-py` (conda-forge) vs `authzed` (PyPI) + +3. **Variant differences:** Conda may offer multiple variants with different dependencies + - Example: `seaborn-base` (conda-forge) vs `seaborn` (PyPI) + +4. **Cross-channel naming:** Different conda channels may use different names + - Example: `pyperformance` (conda-forge) vs `performance` (main) + +## Implementation Notes + +### For conda clients + +Client behavior (loading `whl` into the solve with other repodata records, download, and installation into the environment prefix) is specified in [CEP XXX2 – Wheel conda client support][cep-xxx2]. Channel operators need not implement that CEP; they only publish repodata that conforms to this document. + +### For channel operators + +Channel operators adding wheel support SHOULD: + +- Implement validation to ensure only pure Python wheels are included +- When using the [PyPI JSON API][pypi-json-api] for dependency metadata, consider comparing against the wheel's `METADATA` and fall back (or merge) when the API response is incomplete relative to the artifact +- Ensure that dependencies are solvable, including that compiled dependencies exist on the conda channel declared as `info.channel_relations.base` when wheels depend on conda packages from that stack +- Maintain a mapping of PyPI to conda-style names for their channel, or declare `channel_relations.base` so clients load a base channel that defines those mappings (see [Naming standard and channel mapping](#naming-standard-and-channel-mapping) and [Channel relations in repodata][cep-channel-relations]) +- Consider automation to keep the repodata up to date with newer releases on PyPI +- Document any naming conventions specific to their channel +- Host wheel artifacts on the channel (under `noarch/` by default) so clients can fetch them with standard conda URL construction +- Establish patching workflows to correct metadata issues and resolve dependency conflicts +- Document patching policies and maintain transparency about modified packages + +### Implementation approaches + +Channels MAY implement wheel support through various approaches: + +- **Manual curation**: Channels can manually add specific pure-Python wheels via repodata patching, providing full control over which packages are included and allowing for quality control and metadata validation before exposure to users. +- **Semi-automated**: Channels can implement automated processes to discover and add wheels, with manual review and patching workflows for quality assurance. +- **Full automation**: Channels can automatically mirror and index wheels from PyPI, though this requires robust validation, patching infrastructure, and consideration of repodata size impacts. + +A phased approach starting with manual curation and moving toward increased automation as processes mature is RECOMMENDED. This balances the lower barrier to entry (no recipes needed for many packages) with quality control. Complex packages with metadata conflicts or special requirements should still use traditional conda feedstocks. + +## Examples + +The JSON fragments below use revision `3` as an example (`v3`). The integer MUST match an entry in +`info.repodata_revisions` per [the backwards-compatible repodata update strategy](https://github.com/conda/ceps/pull/146). +A complete channel index also includes the traditional top-level keys (`repodata_version`, `packages`, +`packages.conda`, `removed`, and so on). A full generated example is checked in with [conda-pypi][conda-pypi-example-repodata]. + +### Channel-hosted wheel (default location) + +A channel-hosted wheel uses standard conda URL construction. The `.whl` file is served under the channel’s `noarch` directory (or under `info.base_url` when set), the same way as other `noarch` artifacts: + +```json +{ + "info": { + "subdir": "noarch", + "channel_relations": { + "base": "../core" + } + }, + "v3": { + "whl": { + "requests-2.32.5-py3_none_any_0": { + "name": "requests", + "version": "2.32.5", + "build": "py3_none_any_0", + "build_number": 0, + "depends": [ + "charset-normalizer <4,>=2", + "idna <4,>=2.5", + "urllib3 <3,>=1.21.1", + "certifi >=2017.4.17", + "python >=3.9" + ], + "fn": "requests-2.32.5-py3-none-any.whl", + "sha256": "78820a3e5d9d3b25ce8e1c99c1c89cd19caa904a92973a3e50f8426009e8a4b3", + "size": 6899, + "subdir": "noarch", + "noarch": "python" + } + } + } +} +``` + +With this configuration, the wheel file is downloaded from (assuming the channel is at `https://repo.example.com/channel`): + +- `https://repo.example.com/channel/noarch/requests-2.32.5-py3-none-any.whl` + +### Name mapping + +Here is an example of name mapping and normalization of the record name and dependencies. + +```json +{ + "v3": { + "whl": { + "annotated_types-0.7.0-py3_none_any_0": { + "name": "annotated-types", + "version": "0.7.0", + "build": "py3_none_any_0", + "build_number": 0, + "depends": [ + "typing_extensions>=4.0.0[when=\"python<3.9\"]", + "python >=3.8" + ], + "fn": "annotated_types-0.7.0-py3-none-any.whl", + "sha256": "1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", + "size": 13643, + "subdir": "noarch", + "noarch": "python" + } + } + } +} +``` + +This example demonstrates two types of name normalization: + +1. Record key format: The package is indexed using the standard conda distribution string format `{conda_name}-{version}-{build}` per [CEP 26][cep-26]: `annotated_types-0.7.0-py3_none_any_0`. The name portion (`annotated_types`) comes from the METADATA `Name` field (`annotated-types`), normalized to conda conventions (mapped to `annotated_types` to match conda-forge naming). +The build string (`py3_none_any_0`) includes the Python version, ABI tag (`none`), platform tag (`any`), and build number (`0` at the end), all extracted from the wheel filename (`annotated_types-0.7.0-py3-none-any.whl`), which normalizes the package name to underscores per PEP 427. +2. Dependency name mapping: This package depends on `typing_extensions`, which is listed in the `depends` field. On PyPI, this package is named `typing-extensions` (with a hyphen), but it has been mapped to the name `typing_extensions` (with an underscore) to match the existing conda-forge package name. Such mappings may be aligned with the channel declared in `info.channel_relations.base` (see [Naming standard and channel mapping](#naming-standard-and-channel-mapping)). + +This example also demonstrates conditional dependencies. The original `METADATA` file from the wheel has the following dependency information: + +```text +Requires-Python: >=3.8 +Requires-Dist: typing-extensions>=4.0.0; python_version < '3.9' +``` + +The package record expresses the conditional with `when=` so `typing_extensions` applies only when the Python version is less than 3.9, matching the original wheel METADATA and [CEP 43][cep-43]. The Python version constraint of >=3.8 is directly mapped. + +## References + +- [CEP XXX0 – Wheel support in conda (overview)][cep-xxx0] +- [CEP XXX2 – Wheel conda client support][cep-xxx2] +- [conda-pypi project][conda-pypi] +- [PEP 508 marker conversion (conda-pypi developer docs)][conda-pypi-marker-conversion] +- [Example `repodata.json` (conda-pypi test channel)][conda-pypi-example-repodata] +- [Channel relations in repodata (PR 155)][cep-channel-relations] +- [CEP 43 – Conditional dependencies][cep-43] +- [CEP 44 – Optional dependency groups][cep-44] +- [conda-pupa][conda-pupa] +- [PyPI JSON API documentation][pypi-json-api] + +## Copyright + +All CEPs are explicitly [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). + + +[RFC2119]: https://datatracker.ietf.org/doc/html/rfc2119 +[repodata-schema]: https://schemas.conda.org/repodata-1.schema.json +[repodata-record-schema]: https://schemas.conda.org/repodata-record-1.schema.json +[cep-15]: https://conda.org/learn/ceps/cep-0015 +[cep-16]: https://conda.org/learn/ceps/cep-0016 +[cep-26]: https://conda.org/learn/ceps/cep-0026 +[version-specifiers]: https://packaging.python.org/en/latest/specifications/version-specifiers/#id5 +[conda-pypi]: https://github.com/conda-incubator/conda-pypi +[conda-pypi-marker-conversion]: https://conda.github.io/conda-pypi/developer/marker-conversion/#pep-508-variables +[conda-pypi-example-repodata]: https://github.com/conda-incubator/conda-pypi/blob/main/tests/conda_local_channel/noarch/repodata.json +[cep-channel-relations]: https://github.com/conda/ceps/pull/155 +[cep-43]: https://conda.org/learn/ceps/cep-0043 +[cep-44]: https://conda.org/learn/ceps/cep-0044 +[conda-pupa]: https://github.com/dholth/conda-pupa +[pypi-json-api]: https://docs.pypi.org/api/json/ +[cep-xxx0]: cep-XXX0.md +[cep-xxx2]: cep-XXX2.md diff --git a/cep-XXX2.md b/cep-XXX2.md new file mode 100644 index 00000000..da1160a1 --- /dev/null +++ b/cep-XXX2.md @@ -0,0 +1,155 @@ +# CEP XXXX - Wheel conda client support + + + + + + + + + + + +
Title Wheel conda client support
Status Draft
Author(s) + Dan Yeaw <dyeaw@anaconda.com> +
Created Apr 24, 2026
Updated Apr 24, 2026
Discussion https://github.com/conda/ceps/pull/145
Implementation TBD
Requires [CEP XXX1 – Repodata wheel support](cep-XXX1.md) https://github.com/conda/ceps/pull/146 https://github.com/conda/ceps/pull/155 [CEP 43](https://conda.org/learn/ceps/cep-0043) [CEP 44](https://conda.org/learn/ceps/cep-0044)
See also [CEP XXX0 – Wheel support in conda (overview)](cep-XXX0.md)
+ +> The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", +"RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as +described in [RFC2119][RFC2119] when, and only when, they appear in all capitals, as shown here. + +## Abstract + +This CEP specifies how conda clients consume wheel entries published in repodata: loading `v{revision}.whl` into the same index as other repodata records, evaluating conditional dependencies and extras per [CEP 43][cep-43] and [CEP 44][cep-44], downloading wheel artifacts, and integrating them into the environment **as equivalent `noarch: python` conda packages** (layout, package database, and lifecycle). +The shape of the `whl` index and publisher-side `METADATA` conversion are defined in [CEP XXX1 – Repodata wheel support][cep-xxx1]. +Background on **why** channels and clients pursue native wheel support, plus historical context and rejected ecosystem alternatives, is in [CEP XXX0 – Wheel support in conda (overview)][cep-xxx0]. + +## Motivation + +[CEP XXX1][cep-xxx1] makes wheel metadata available in channel repodata so environments can be solved in one pass together with conda packages. +That is not sufficient by itself: users need a single, well-defined **client** story for (1) which packages the solver may choose, (2) how optional groups and `when=` interact with the live environment, (3) where artifacts are downloaded from, and (4) how wheel contents map onto the prefix (site-packages, scripts, and conda metadata) without corrupting the environment or duplicating work already covered by conda package conventions. + +## Specification + +### Relationship to [CEP XXX1][cep-xxx1] + +This CEP assumes repodata that conforms to [CEP XXX1 – Repodata wheel support][cep-xxx1], including the `whl` mapping under a registered `v{revision}` and valid [repodata record][repodata-record-schema] fields for each entry. + +### Repodata consumption + +Conda clients that implement wheel support MUST load records from the `whl` mapping in each supported [repodata revision](https://github.com/conda/ceps/pull/146) into the same index used for `packages` and `packages.conda` for that revision. Clients SHOULD apply the same subdir and channel filtering hooks used for other repodata records unless a separate policy is explicitly documented. + +Wheel records for pure Python packages use `subdir: "noarch"` per [CEP XXX1][cep-xxx1] and no additional platform filter is required for those wheels beyond the normal noarch and virtual-package logic for the target prefix. + +### Solver: dependency resolution + +Wheel records are normal [repodata records][repodata-record-schema] and solvers treat them like other index entries for dependency resolution, `depends`, and (where applicable) subdir / virtual-package filtering—the same model as for `packages` and `packages.conda`. + +### Conditional dependencies and extras + +For wheel records, [CEP 43][cep-43] defines `when=` on `depends` and [CEP 44][cep-44] defines the **`extra_depends`** object for optional groups. Clients that implement this CEP MUST: + +- Evaluate `when=` for wheel `depends` entries according to the same rules as for other new-syntax records, using an environment model that provides at least Python version and, where applicable, virtual packages (see [Marker handling](#marker-handling)). +- Honor **`extra_depends`** when the user selects optional groups (for example with `extras=` in `MatchSpec`) per [CEP 44][cep-44], and MUST NOT pull in `extra_depends` groups on a default install. +- Load wheel records that use `when=` or `extra_depends` in the same `v{revision}` payload and revision-handling as other records introduced under the [backwards-compatible repodata update strategy](https://github.com/conda/ceps/pull/146). + +The strings on a wheel’s `depends` and `extra_depends` are produced by channel operators from wheel `METADATA` per [CEP XXX1][cep-xxx1]; the client’s job is to interpret them, not to re-parse the wheel for dependency resolution (though reading `METADATA` for validation or error messages is allowed). + +### Marker handling + +**Publisher output (in repodata):** [CEP XXX1][cep-xxx1] requires converting certain PEP 508 markers on `Requires-Dist` into `when=` and optional-group fields. Other markers are out of scope for the default *publisher* conversion rules; repodata may omit or simplify them. + +**Client (solve time):** The solver MUST use the MatchSpec features from [CEP 43][cep-43] and [CEP 44][cep-44] carried on the repodata record: `when=` subexpressions, `python` constraints, and virtual packages, so that the effective dependency graph matches the published strings for the current environment. Clients MAY map additional environment dimensions to virtual packages where documented (non-normative per-variable notes appear in the [conda-pypi marker conversion][conda-pypi-marker-conversion] documentation). + +**Out-of-scope dimensions:** Some PEP 508 variables (for example, `implementation_name`, or markers tied to a specific wheel build matrix beyond the pure-Python subset) do not have a defined mapping in the normative [CEP XXX1][cep-xxx1] record; behavior for such wheels remains undefined unless a future CEP or channel policy encodes them in repodata. Clients MUST NOT claim full PEP 508 equivalence beyond what the record expresses. + +### Download + +After the solver selects a wheel record, the client MUST download the `.whl` file using the same rules as for other `noarch` artifacts with a `fn`: + +- Construct the download URL from the channel location (or repodata `info.base_url` when present) together with `subdir` and `fn`, as for other conda packages. See [Wheel download locations][cep-xxx1-download] in [CEP XXX1][cep-xxx1]. +- Verify integrity using `sha256` and `size` from the repodata record when those fields are present, consistent with other conda downloads. + +### Installation layout and prefix placement + +Wheels selected from repodata MUST be integrated into the target environment **as the equivalent of a `noarch: python` conda package** ([CEP 20][cep-20], [CEP 34][cep-34]): the same style of layout under the prefix, registration in conda's package database, and `conda list` / `conda remove` / upgrade behavior that matches other packages from the index—not a standalone "pip install this wheel" interaction. + +Importable code MUST end up under the environment's [site-packages][cep-20] (or the site-packages / path layout for `noarch: python` described in that package’s `info/index.json` and related files per [CEP 34][cep-34]). Headers, data, and other install-tree paths from the wheel MUST be placed under prefix locations that are tracked and removed the same way as in a built conda package. +The repodata record points at a wheel file as the artifact to obtain; the **normative** requirement is conda-equivalent integration as in the preceding paragraph, not a wheel-only layout profile considered in isolation from conda's package model. + +- **Metadata:** The install MUST create a valid `.dist-info` so `importlib.metadata` can load the distribution, consistent with other Python packages in the environment. +- **RECORD:** The client SHOULD verify the wheel's `RECORD` where present to detect tampering or corruption and it MUST reject path entries that escape the intended prefix or package staging area. +- **Console and GUI entry points:** Application entry points SHOULD be materialized like other conda `noarch: python` packages: through `info/link.json` and conda's link machinery per [CEP 20][cep-20] and [CEP 34][cep-34], so shebangs and `bin` scripts remain under the conda client's control. A client implementation MAY avoid emitting duplicate script files from the wheel when `link.json` (or an equivalent) defines the public commands. + +## Rationale + +### One solve, predictable layout + +Native wheel support in the client avoids separate PyPI or pip phases for the packages indexed in [CEP XXX1][cep-xxx1], matches user expectations for where importable code lives, and reuses conda's environment model and fetch stack. + +### Native unpack + +Conda clients install from the `.whl` artifact referenced in repodata and lay out the environment as a normal conda-managed `noarch: python` install (see [Installation layout and prefix placement](#installation-layout-and-prefix-placement)). + +## Implementation Notes + +### For conda client implementers + +- **Index:** Parse `whl` inside each supported `v{revision}` object alongside `packages` / `packages.conda` into the same solver-facing index. +- **Conditional dependencies and extras ([CEP 43][cep-43], [CEP 44][cep-44]):** Evaluate `when=` on `depends` and honor **`extra_depends`** when the user requests optional groups, consistent with the published record strings (wheel rows use the same syntax as other CEP 43 / CEP 44 records). +- **Revision gating:** Treat new-syntax wheel records with the same `v{revision}` handling as the rest of the PR 146 strategy (do not expect older repodata only clients to read `whl`). +- **Download:** Reuse the channel and fetch stack used for other artifacts (`fn`, channel/`base_url`, and checksum verification). +- **Install:** Integrate wheels using the same prefix layout and metadata model as `noarch: python` conda packages ([CEP 20][cep-20], [CEP 34][cep-34]), including `info/link.json` for entry points when using conda's link pipeline. The [conda-pypi][conda-pypi] project demonstrates populating a conda package shape from a wheel (for example, suppressing duplicate `bin` script generation when `link.json` is used) as a non-normative example. + +## Examples + +### Install outcome (illustrative) + +After installing `requests-2.32.5-py3-none-any.whl` from a wheel repodata record, the environment’s site-packages should contain the `requests` import package and a `requests-2.32.5.dist-info` (or equivalent) directory such that `import requests` and `importlib.metadata.version("requests")` succeed for that prefix’s Python. + +## Backwards compatibility + +- **Repodata shape:** The `whl` key appears only under a registered `v{revision}` (see [PR 146](https://github.com/conda/ceps/pull/146)). Conda clients that do not support that revision or the `whl` index MUST ignore unknown `v{revision}` members or fall back to earlier revisions per channel policy. +- **Record syntax:** Records that use `when=` and `extra_depends` are part of the new-syntax repodata world defined by [CEP 43][cep-43] and [CEP 44][cep-44]; wheel rows are not a separate syntax fork. + +## Rejected ideas + +Ecosystem-level alternatives (relying on pip only, on-the-fly PyPI without repodata, build farms that convert every wheel) are discussed under **Rejected ideas** in [CEP XXX0 – Wheel support in conda (overview)][cep-xxx0]. This CEP is the client-side complement to the chosen approach: indexed wheels plus native client install. + +## References + +- [CEP XXX0 – Wheel support in conda (overview)][cep-xxx0] +- [CEP XXX1 – Repodata wheel support][cep-xxx1] +- [CEP 20 – `noarch` (python) packages and site-packages][cep-20] +- [CEP 34 – Package metadata and `info/`](./cep-0034.md) (`index.json`, `link.json`, etc.) +- [CEP 26 – Identifying packages and channels][cep-26] +- [CEP 43 – Conditional dependencies][cep-43] +- [CEP 44 – Optional dependency groups][cep-44] +- [PEP 508 marker conversion (conda-pypi developer docs)][conda-pypi-marker-conversion] +- [Example `repodata.json` (conda-pypi test channel)][conda-pypi-example-repodata] +- [conda-pypi project][conda-pypi] +- [Adopting uv in pixi][uv-in-pixi] +- [rip][rip] +- [conda-pupa][conda-pupa] + +## Copyright + +All CEPs are explicitly [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). + + +[RFC2119]: https://datatracker.ietf.org/doc/html/rfc2119 +[repodata-record-schema]: https://schemas.conda.org/repodata-record-1.schema.json +[cep-26]: https://conda.org/learn/ceps/cep-0026 +[cep-20]: ./cep-0020.md +[cep-34]: ./cep-0034.md +[cep-xxx0]: cep-XXX0.md +[cep-xxx1]: cep-XXX1.md +[cep-xxx1-download]: cep-XXX1.md#wheel-download-locations +[conda-pypi]: https://github.com/conda-incubator/conda-pypi +[conda-pypi-marker-conversion]: https://conda.github.io/conda-pypi/developer/marker-conversion/#pep-508-variables +[conda-pypi-example-repodata]: https://github.com/conda-incubator/conda-pypi/blob/main/tests/conda_local_channel/noarch/repodata.json +[cep-43]: https://conda.org/learn/ceps/cep-0043 +[cep-44]: https://conda.org/learn/ceps/cep-0044 +[conda-pupa]: https://github.com/dholth/conda-pupa +[uv-in-pixi]: https://prefix.dev/blog/uv_in_pixi +[rip]: https://github.com/prefix-dev/rip