├── .codespell-whitelist.txt
├── .gitattributes
├── .github
├── dependabot.yml
└── workflows
│ ├── docs.yaml
│ └── rust-compile.yml
├── .gitignore
├── .gitmodules
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── cliff.toml
├── crates
├── rattler_installs_packages
│ ├── Cargo.toml
│ ├── benches
│ │ └── html.rs
│ ├── src
│ │ ├── artifacts
│ │ │ ├── mod.rs
│ │ │ ├── sdist.rs
│ │ │ ├── snapshots
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__build_rich_as_folder_as_source_dependency.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__build_rich_git_reference_source_code.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__build_rich_git_reference_with_tag_source_code.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__build_rich_http_reference_source_code.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__build_rich_no_metadata.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__build_rich_sdist_as_source_dependency.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__build_rich_with_metadata.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__build_wheel_and_pass_env_variables.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__build_wheel_and_with_clean_env_and_pass_env_variables.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__build_wheel_with_backend_path.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__check_direct_url_json_by_tag_for_remote_git.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__check_direct_url_json_for_local_wheel.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__check_direct_url_json_for_remote_sdist.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__check_direct_url_json_with_commit_for_remote_git.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__get_only_metadata_for_local_sdist_rich_without_calling_available_artifacts.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__get_only_metadata_for_local_stree_rich_without_calling_available_artifacts.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__get_only_metadata_for_local_whl_rich_without_calling_available_artifacts.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__get_whl_for_local_sdist_rich.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__get_whl_for_local_stree_rich.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__get_whl_for_local_whl.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__read_tar_gz_archive_for_a_file-2.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__read_tar_gz_archive_for_a_file.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__read_zip_metadata.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__sdist_metadata.snap
│ │ │ │ ├── rattler_installs_packages__artifacts__sdist__tests__sdist_without_name.snap
│ │ │ │ └── rattler_installs_packages__artifacts__sdist__tests__zip_timestamps_1980.snap
│ │ │ ├── stree.rs
│ │ │ └── wheel.rs
│ │ ├── index
│ │ │ ├── direct_url
│ │ │ │ ├── file.rs
│ │ │ │ ├── git.rs
│ │ │ │ ├── http.rs
│ │ │ │ └── mod.rs
│ │ │ ├── file_store.rs
│ │ │ ├── git_interop.rs
│ │ │ ├── html.rs
│ │ │ ├── http.rs
│ │ │ ├── lazy_metadata.rs
│ │ │ ├── mod.rs
│ │ │ ├── package_database.rs
│ │ │ └── package_sources.rs
│ │ ├── install
│ │ │ ├── install_paths.rs
│ │ │ ├── mod.rs
│ │ │ └── snapshots
│ │ │ │ ├── rattler_installs_packages__install__test__byte_code_compilation.snap
│ │ │ │ ├── rattler_installs_packages__install__test__entry_points.snap
│ │ │ │ ├── rattler_installs_packages__install__test__miniblack-23.1.0-py3-none-any.whl.snap
│ │ │ │ ├── rattler_installs_packages__install__test__purelib_and_platlib-1.0.0-cp38-cp38-linux_x86_64.whl.snap
│ │ │ │ ├── rattler_installs_packages__install__test__selenium-2.53.2-py2.py3-none-any.whl.snap
│ │ │ │ └── rattler_installs_packages__install__test__selenium-4.1.0-py3-none-any.whl.snap
│ │ ├── lib.rs
│ │ ├── python_env
│ │ │ ├── byte_code_compiler.rs
│ │ │ ├── compile_pyc.py
│ │ │ ├── distribution_finder.rs
│ │ │ ├── env_markers
│ │ │ │ ├── from_env.rs
│ │ │ │ ├── mod.rs
│ │ │ │ └── pep508.py
│ │ │ ├── mod.rs
│ │ │ ├── snapshots
│ │ │ │ └── rattler_installs_packages__python_env__distribution_finder__test__find_distributions.snap
│ │ │ ├── system_python.rs
│ │ │ ├── tags
│ │ │ │ ├── from_env.rs
│ │ │ │ ├── mod.rs
│ │ │ │ └── platform_tags.py
│ │ │ ├── uninstall.rs
│ │ │ └── venv.rs
│ │ ├── resolve
│ │ │ ├── dependency_provider.rs
│ │ │ ├── mod.rs
│ │ │ ├── pypi_version_types.rs
│ │ │ ├── solve.rs
│ │ │ ├── solve_options.rs
│ │ │ └── solve_types.rs
│ │ ├── types
│ │ │ ├── artifact.rs
│ │ │ ├── artifact_name.rs
│ │ │ ├── core_metadata.rs
│ │ │ ├── direct_url_json.rs
│ │ │ ├── entry_points.rs
│ │ │ ├── extra.rs
│ │ │ ├── mod.rs
│ │ │ ├── package_name.rs
│ │ │ ├── project_info.rs
│ │ │ ├── record.rs
│ │ │ └── rfc822ish.rs
│ │ ├── utils
│ │ │ ├── mod.rs
│ │ │ ├── read_and_seek.rs
│ │ │ ├── seek_slice.rs
│ │ │ ├── streaming_or_local.rs
│ │ │ └── test.rs
│ │ ├── wheel_builder
│ │ │ ├── build_environment.rs
│ │ │ ├── error.rs
│ │ │ ├── mod.rs
│ │ │ ├── wheel_builder_frontend.py
│ │ │ └── wheel_cache.rs
│ │ └── win
│ │ │ ├── launcher.rs
│ │ │ ├── mod.rs
│ │ │ └── windows-launcher
│ │ │ ├── README.md
│ │ │ ├── t32.exe
│ │ │ ├── t64-arm.exe
│ │ │ ├── t64.exe
│ │ │ ├── w32.exe
│ │ │ ├── w64-arm.exe
│ │ │ └── w64.exe
│ ├── tests
│ │ └── resolver.rs
│ └── vendor
│ │ └── packaging
│ │ ├── LICENSE
│ │ ├── LICENSE.APACHE
│ │ ├── LICENSE.BSD
│ │ ├── __init__.py
│ │ ├── _elffile.py
│ │ ├── _manylinux.py
│ │ ├── _musllinux.py
│ │ ├── _parser.py
│ │ ├── _structures.py
│ │ ├── _tokenizer.py
│ │ ├── markers.py
│ │ ├── metadata.py
│ │ ├── py.typed
│ │ ├── requirements.py
│ │ ├── specifiers.py
│ │ ├── tags.py
│ │ ├── utils.py
│ │ └── version.py
├── rip_bin
│ ├── Cargo.toml
│ └── src
│ │ ├── cli
│ │ ├── mod.rs
│ │ ├── resolve.rs
│ │ └── wheels.rs
│ │ ├── lib.rs
│ │ └── main.rs
└── test-utils
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── end_to_end_tests
└── test_endtoend.py
├── pixi.lock
├── pixi.toml
├── rust-toolchain
└── test-data
├── find_distributions
├── Lib
│ └── site-packages
│ │ ├── Flask-1.1.4.dist-info
│ │ ├── INSTALLER
│ │ ├── LICENSE.rst
│ │ ├── METADATA
│ │ ├── RECORD
│ │ ├── WHEEL
│ │ ├── entry_points.txt
│ │ └── top_level.txt
│ │ ├── Jinja2-2.11.3.dist-info
│ │ ├── INSTALLER
│ │ ├── LICENSE.rst
│ │ ├── METADATA
│ │ ├── RECORD
│ │ ├── WHEEL
│ │ ├── entry_points.txt
│ │ └── top_level.txt
│ │ ├── MarkupSafe-1.1.1.dist-info
│ │ ├── INSTALLER
│ │ ├── LICENSE.rst
│ │ ├── METADATA
│ │ ├── RECORD
│ │ ├── WHEEL
│ │ └── top_level.txt
│ │ ├── Werkzeug-1.0.1.dist-info
│ │ ├── INSTALLER
│ │ ├── LICENSE.rst
│ │ ├── METADATA
│ │ ├── RECORD
│ │ ├── WHEEL
│ │ └── top_level.txt
│ │ ├── click-7.1.2.dist-info
│ │ ├── INSTALLER
│ │ ├── LICENSE.rst
│ │ ├── METADATA
│ │ ├── RECORD
│ │ ├── WHEEL
│ │ └── top_level.txt
│ │ ├── itsdangerous-1.1.0.dist-info
│ │ ├── INSTALLER
│ │ ├── LICENSE.rst
│ │ ├── METADATA
│ │ ├── RECORD
│ │ ├── WHEEL
│ │ └── top_level.txt
│ │ ├── pip-9.0.1.dist-info
│ │ ├── DESCRIPTION.rst
│ │ ├── INSTALLER
│ │ ├── METADATA
│ │ ├── RECORD
│ │ ├── WHEEL
│ │ ├── entry_points.txt
│ │ ├── metadata.json
│ │ └── top_level.txt
│ │ ├── setuptools-28.8.0.dist-info
│ │ ├── DESCRIPTION.rst
│ │ ├── INSTALLER
│ │ ├── METADATA
│ │ ├── RECORD
│ │ ├── WHEEL
│ │ ├── dependency_links.txt
│ │ ├── entry_points.txt
│ │ ├── metadata.json
│ │ ├── top_level.txt
│ │ └── zip-safe
│ │ ├── zipp-3.17.0.dist-info
│ │ └── .gitkeep
│ │ └── zipp
│ │ └── .gitkeep
└── pyvenv.cfg
├── scripts
└── test_wordle.py
├── sdists
├── env_package-0.1.tar.gz
├── fake-flask-3.0.0.tar.gz
├── filterpy-1.4.5.zip
├── rich-13.6.0.tar.gz
├── rich_without_metadata_in_path.tar.gz
├── setuptools-69.0.2.tar.gz
├── tampered-rich-13.6.0.tar.gz
└── zip_read_package-1.0.0.zip
├── stree
└── dev_folder_with_rich
│ ├── LICENSE
│ ├── README.md
│ ├── pyproject.toml
│ └── rich
│ └── __init__.py
└── wheels
├── miniblack-23.1.0-py3-none-any.whl
├── purelib_and_platlib-1.0.0-cp38-cp38-linux_x86_64.whl
└── wordle_python-2.3.32-py3-none-any.whl
/.codespell-whitelist.txt:
--------------------------------------------------------------------------------
1 | crate
2 | ser
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # GitHub syntax highlighting
2 | pixi.lock linguist-language=YAML
3 |
4 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "cargo"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | time: "04:00" # UTC
8 | labels:
9 | - "dependencies"
10 | commit-message:
11 | prefix: "bump"
12 | open-pull-requests-limit: 10
13 | - package-ecosystem: "github-actions"
14 | directory: "/"
15 | schedule:
16 | interval: "daily"
17 | labels:
18 | - "dependencies"
19 | commit-message:
20 | prefix: "chore(ci)"
21 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy Docs
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | env:
9 | CARGO_TERM_COLOR: always
10 | RUSTFLAGS: "-D warnings"
11 | RUSTDOCFLAGS: --html-in-header header.html
12 |
13 |
14 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
15 | permissions:
16 | contents: read
17 | pages: write
18 | id-token: write
19 |
20 | # Allow one concurrent deployment
21 | concurrency:
22 | group: "pages"
23 | cancel-in-progress: true
24 |
25 | jobs:
26 | build-and-deploy:
27 | if: github.repository == 'prefix-dev/rattler_installs_packages'
28 | runs-on: ubuntu-latest
29 | steps:
30 | - name: Checkout repository
31 | uses: actions/checkout@v4
32 |
33 | - uses: actions-rs/toolchain@v1
34 | with:
35 | profile: minimal
36 |
37 | - name: Setup Pages
38 | uses: actions/configure-pages@v5
39 |
40 | # This does the following:
41 | # - Replaces the docs icon with one that clearly denotes it's not the released package on crates.io
42 | # - Adds a meta tag that forces Google not to index any page on the site.
43 | - name: Pre-docs-build
44 | run: |
45 | echo "" > header.html
46 |
47 | - name: Build Documentation
48 | run: cargo doc --workspace --no-deps --all-features --lib
49 |
50 | # This adds the following:
51 | # - A top level redirect to the rattler_installs_packages crate documentation
52 | # - A robots.txt file to forbid any crawling of the site (to defer to the docs.rs site on search engines).
53 | # - A .nojekyll file to disable Jekyll GitHub Pages builds.
54 | - name: Finalize documentation
55 | run: |
56 | echo "" > target/doc/index.html
57 | echo "User-Agent: *\nDisallow: /" > target/doc/robots.txt
58 | touch target/doc/.nojekyll
59 |
60 | # https://github.com/actions/upload-pages-artifact#file-permissions
61 | - run: chmod -c -R +rX target/doc/
62 |
63 | - name: Upload artifact
64 | uses: actions/upload-pages-artifact@v3
65 | with:
66 | path: 'target/doc'
67 |
68 | - name: Deploy to GitHub Pages
69 | id: deployment
70 | uses: actions/deploy-pages@v4
71 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | .idea/
3 | *.sqlite3
4 | **/__pycache__/**
5 | .DS_STORE
6 | # pixi environments
7 | .pixi
8 | # other venvs
9 | .venv*/
10 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "test-data/packse"]
2 | path = test-data/packse
3 | url = https://github.com/zanieb/packse
4 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.4.0
4 | hooks:
5 | - id: check-yaml
6 | - id: end-of-file-fixer
7 | - id: trailing-whitespace
8 | # Copied from Mozilla https://github.com/mozilla/grcov/blob/master/.pre-commit-config.yaml
9 | - repo: https://github.com/DevinR528/cargo-sort
10 | rev: v1.0.9
11 | hooks:
12 | - id: cargo-sort
13 | - repo: local
14 | hooks:
15 | - id: fmt
16 | name: fmt
17 | language: system
18 | types: [file, rust]
19 | entry: cargo fmt
20 | pass_filenames: false
21 |
22 | - id: clippy
23 | name: clippy
24 | language: system
25 | types: [file, rust]
26 | entry: cargo clippy --all -- -D warnings # Use -D warnings option to ensure the job fails when encountering warnings
27 | pass_filenames: false
28 |
29 | - id: test
30 | name: test
31 | language: system
32 | stages: [push]
33 | types: [file, rust]
34 | entry: cargo test
35 | pass_filenames: false
36 | - repo: https://github.com/codespell-project/codespell
37 | rev: v2.2.5
38 | hooks:
39 | - id: codespell
40 | args: [--ignore-words=.codespell-whitelist.txt]
41 | exclude: '(Cargo.lock|CHANGELOG.md)'
42 | exclude: 'crates/rattler_installs_packages/vendor/.*|test-data/'
43 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing 😍
2 |
3 | We would love to have you contribute!
4 | For a good list of things you could help us with, take a look at our [*good first issues*](https://github.com/prefix-dev/rip/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
5 | If you want to go deeper though, any unassigned [open issue](https://github.com/prefix-dev/rip/issues) is up for grabs.
6 | When starting on an issue please let us know, and we'll assign you to the issue.
7 | When contributing to this repository, please first discuss the change you wish to make via issue, email, chat or any other method with the owners of this repository before making a change.
8 |
9 | For questions, requests or a casual chat, we are very active on our discord server.
10 | You can [join our discord server via this link][chat-url].
11 |
12 | [chat-url]: https://discord.gg/kKV8ZxyzY4
13 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2"
3 | members = ["crates/*"]
4 |
5 | [profile.dev.package.insta]
6 | opt-level = 3
7 |
8 | [workspace.package]
9 | version = "0.10.0"
10 | categories = ["development-tools"]
11 | homepage = "https://github.com/prefix-dev/rip"
12 | repository = "https://github.com/prefix-dev/rip"
13 | license = "BSD-3-Clause"
14 | edition = "2021"
15 | readme = "README.md"
16 | rust-version = "1.70"
17 |
18 | [workspace.metadata.release]
19 | allow-branch = ["main"]
20 | consolidate-commits = true
21 | tag-prefix = ""
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2023, prefix.dev GmbH
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | # RIP: Fast, barebones **pip** implementation in Rust
10 |
11 | ![License][license-badge]
12 | [![Build Status][build-badge]][build]
13 | [![Project Chat][chat-badge]][chat-url]
14 | [![docs main][docs-main-badge]][docs-main]
15 |
16 | # Introduction
17 |
18 | `rip` is a library that allows the resolving and installing of Python [PyPI](https://pypi.org/) packages from Rust into a virtual environment.
19 | It's based on our experience with building [rattler] and aims to provide the same
20 | experience but for PyPI instead of Conda.
21 |
22 | ## What should I use this for?
23 |
24 | Like [rattler], `rip` should be fast and easy to use. This library is not a package manager itself but provides the low-level plumbing to be used in one.
25 | To see an example of this take a look at our package manager: [pixi](https://github.com/prefix-dev/pixi) 📦
26 |
27 | `rip` is based on the quite excellent work of [posy](https://github.com/njsmith/posy) and we have tried to credit
28 | the authors where possible.
29 |
30 | # Showcase
31 |
32 | `rip` has a very incomplete pip-like binary that can be used to test package installs.
33 | Let's resolve and install the `flask` python package. Running `cargo run install flask /tmp/flask` we get something like this:
34 |
35 | 
36 |
37 | This showcases the downloading and caching of metadata from PyPI. As well as the package resolution using our incremental SAT solver: [Resolvo](https://github.com/mamba-org/resolvo), more on this below.
38 | Finally, after resolution it installs the package into a venv.
39 | We cache everything locally so that we can reuse the PyPI metadata.
40 |
41 | ## Features
42 |
43 | This is a list of current features of `rip`, the biggest are listed below:
44 |
45 | - [x] Async downloading and aggressive caching of PyPI metadata.
46 | - [x] Resolving of PyPI packages using [Resolvo](https://github.com/mamba-org/resolvo).
47 | - [x] Installation of wheel files.
48 | - [x] Support sdist files (must currently adhere to the `PEP 517` and `PEP 518` standards).
49 | - [x] Caching of locally built wheels.
50 |
51 | More intricacies of the PyPI ecosystem need to be implemented, see our [GitHub issues](https://github.com/prefix-dev/rip/issues) for more details.
52 |
53 | # Details
54 |
55 | ## Resolving
56 |
57 | We have integrated the stand-alone packaging SAT solver [Resolvo](https://github.com/mamba-org/resolvo), to resolve PyPI packages.
58 | This solver is incremental and adds packaging metadata during resolution of the SAT problem.
59 | This feature can be enabled with the `resolvo` feature flag.
60 |
61 | ## Installation
62 |
63 | We have very simple installation support for the resolved packages.
64 | This should be used for testing purposes exclusively
65 | e.g. `cargo run -- install flask /tmp/flask_env` to create a venv and install the flask and it's into it.
66 | After which you can run:
67 |
68 | 1. `/tmp/flask_env/bin/python` to start python in the venv.
69 | 2. `import flask #`, this should import the flask package from the venv.
70 | There is no detection of existing packages in the venv yet, although this should be relatively straightforward.
71 |
72 | # Contributing 😍
73 |
74 | We would love to have you contribute!
75 | See the [CONTRIBUTING.md](./CONTRIBUTING.md) for more info.
76 |
77 | For questions, requests or a casual chat, we are very active on our [Discord server][chat-url].
78 |
79 | [//]: # "[![crates.io][crates-badge]][crates]"
80 | [license-badge]: https://img.shields.io/badge/license-BSD--3--Clause-blue?style=flat-square
81 | [build-badge]: https://img.shields.io/github/actions/workflow/status/prefix-dev/rip/rust-compile.yml?style=flat-square&branch=main
82 | [build]: https://github.com/prefix-dev/rip/actions
83 | [chat-badge]: https://img.shields.io/discord/1082332781146800168.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2&style=flat-square
84 | [chat-url]: https://discord.gg/kKV8ZxyzY4
85 | [docs-main-badge]: https://img.shields.io/badge/docs-main-yellow.svg?style=flat-square
86 | [docs-main]: https://prefix-dev.github.io/rip
87 | [crates]: https://crates.io/crates/rattler_installs_packages
88 | [crates-badge]: https://img.shields.io/crates/v/rattler_installs_packages.svg
89 | [rattler]: https://github.com/mamba-org/rattler
90 |
--------------------------------------------------------------------------------
/cliff.toml:
--------------------------------------------------------------------------------
1 | # git-cliff ~ default configuration file
2 | # https://git-cliff.org/docs/configuration
3 | #
4 | # Lines starting with "#" are comments.
5 | # Configuration options are organized into tables and keys.
6 | # See documentation for more information on available options.
7 |
8 | [remote.github]
9 | owner = "prefix-dev"
10 | repo = "pixi"
11 |
12 | [changelog]
13 | # changelog header
14 | header = """
15 | # Changelog\n
16 | All notable changes to this project will be documented in this file.
17 |
18 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
19 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n
20 | """
21 | # template for the changelog body
22 | # https://keats.github.io/tera/docs/#introduction
23 | body = """
24 | {% if version %}\
25 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
26 | {% else %}\
27 | ## [Unreleased]
28 | {% endif %}\
29 |
30 | ### ✨ Highlights
31 |
32 | ### 📃 Details
33 | {% for group, commits in commits | group_by(attribute="group") %}
34 | #### {{ group | upper_first }}
35 | {%- for commit in commits %}
36 | - {{ commit.message | upper_first | trim }}\
37 | {% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}
38 | {% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}]\
39 | (https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/pull/{{ commit.github.pr_number }}){%- endif -%}
40 | {% endfor %}
41 | {% endfor %}
42 |
43 | """
44 | # # template for the changelog footer
45 | # footer = """
46 | # {% for release in releases -%}
47 | # {% if release.version -%}
48 | # {% if release.previous.version -%}
49 | # [{{ release.version | trim_start_matches(pat="v") }}]: \
50 | # https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
51 | # /compare/{{ release.previous.version }}..{{ release.version }}
52 | # {% endif -%}
53 | # {% else -%}
54 | # [unreleased]: https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}\
55 | # /compare/{{ release.previous.version }}..HEAD
56 | # {% endif -%}
57 | # {% endfor %}
58 | #
59 | # """
60 | # remove the leading and trailing whitespace from the template
61 | trim = true
62 | # changelog footer
63 | # postprocessors
64 | postprocessors = [
65 | # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
66 | ]
67 | [git]
68 | # parse the commits based on https://www.conventionalcommits.org
69 | conventional_commits = true
70 | # filter out the commits that are not conventional
71 | filter_unconventional = true
72 | # process each line of a commit as an individual commit
73 | split_commits = false
74 | # regex for preprocessing the commit messages
75 | commit_preprocessors = [
76 | # remove issue numbers from commits
77 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
78 | ]
79 | # regex for parsing and grouping commits
80 | commit_parsers = [
81 | { message = "^.*: add", group = "Added" },
82 | { message = "^.*: support", group = "Added" },
83 | { message = "^.*: remove", group = "Removed" },
84 | { message = "^.*: delete", group = "Removed" },
85 | { message = "^test", group = "Fixed" },
86 | { message = "^fix", group = "Fixed" },
87 | { message = "^.*: fix", group = "Fixed" },
88 | { message = "^chore|ci", skip = true },
89 | { message = "^.*", group = "Changed" },
90 | ]
91 | # protect breaking changes from being skipped due to matching a skipping commit_parser
92 | protect_breaking_commits = false
93 | # filter out the commits that are not matched by commit parsers
94 | filter_commits = false
95 | # regex for matching git tags
96 | tag_pattern = "v[0-9].*"
97 | # regex for skipping tags
98 | skip_tags = ""
99 | # regex for ignoring tags
100 | ignore_tags = ""
101 | # sort the tags topologically
102 | topo_order = false
103 | # sort the commits inside sections by oldest/newest order
104 | sort_commits = "oldest"
105 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "rattler_installs_packages"
3 | version.workspace = true
4 | edition.workspace = true
5 | authors = ["Bas Zalmstra ", "Tim de Jager "]
6 | description = "Datastructures and algorithms to interact with Python packaging ecosystem"
7 | categories.workspace = true
8 | homepage.workspace = true
9 | repository.workspace = true
10 | license.workspace = true
11 | readme.workspace = true
12 | rust-version.workspace = true
13 | include = ["src/", "vendor/", "benches/"]
14 |
15 | [features]
16 | default = ["native-tls"]
17 | native-tls = ['reqwest/native-tls']
18 | rustls-tls = ['reqwest/rustls-tls']
19 |
20 | [dependencies]
21 | async-trait = "0.1.80"
22 | bytes = "1.6.0"
23 | ciborium = "0.2.2"
24 | csv = "1.3.0"
25 | data-encoding = "2.5.0"
26 | dunce = "1.0.4"
27 | elsa = "1.10.0"
28 | fs4 = "0.8.2"
29 | futures = "0.3.30"
30 | html-escape = "0.2.13"
31 | # reqwest needs an update to 1.0.0
32 | http = "1.1.0"
33 | http-cache-semantics = { version = "2.1.0", default-features = false, features = ["serde", "reqwest"] }
34 | include_dir = "0.7.3"
35 | indexmap = { version = "2.2.6", features = ["serde"] }
36 | itertools = "0.12.1"
37 | miette = "7.2.0"
38 | mime = "0.3.17"
39 | once_cell = "1.19.0"
40 | parking_lot = "0.12.1"
41 | peg = "0.8.2"
42 | pep440_rs = { version = "0.4.0", features = ["serde"] }
43 | pep508_rs = { version = "0.3.0", features = ["serde"] }
44 | pin-project-lite = "0.2.14"
45 | rattler_digest = { version = "0.19.3", features = ["serde"] }
46 | regex = "1.10.4"
47 | reqwest = { version = "0.12.3", default-features = false, features = ["json", "stream"] }
48 | reqwest-middleware = "0.4.0"
49 | serde = "1.0.198"
50 | serde_json = "1.0.116"
51 | serde_with = "3.7.0"
52 | smallvec = { version = "1.13.2", features = ["const_generics", "const_new"] }
53 | tempfile = "3.10.1"
54 | thiserror = "1.0.58"
55 | tl = "0.7.8"
56 | tokio = { version = "1.37.0", features = ["process", "rt-multi-thread"] }
57 | tokio-util = { version = "0.7.10", features = ["compat"] }
58 | tracing = { version = "0.1.40", default-features = false, features = ["attributes"] }
59 | url = { version = "2.5.0", features = ["serde"] }
60 | zip = "0.6.6"
61 | resolvo = { version = "0.4.0", default-features = false, features = ["tokio"] }
62 | pathdiff = "0.2.1"
63 | async_zip = { version = "0.0.16", features = ["tokio", "deflate"] }
64 | tar = "0.4.40"
65 | flate2 = "1.0.28"
66 | pyproject-toml = "0.9.0"
67 | async-once-cell = "0.5.3"
68 | configparser = "3.0.4"
69 | cacache = { version = "13.0.0", default-features = false, features = ["tokio-runtime", "mmap"] }
70 | async-recursion = "1.1.0"
71 | fs-err = "2.11.0"
72 | fs_extra = "1.3.0"
73 | async_http_range_reader = "0.9.1"
74 | which = "6.0.1"
75 |
76 | [dev-dependencies]
77 | anyhow = "1.0.82"
78 | axum = "0.7.5"
79 | criterion = "0.5"
80 | insta = { version = "1.38.0", features = ["ron", "redactions"] }
81 | miette = { version = "7.2.0", features = ["fancy"] }
82 | once_cell = "1.19.0"
83 | rstest = "0.19.0"
84 | test-utils = { path = "../test-utils" }
85 | tokio = { version = "1.37.0", features = ["rt", "macros", "rt-multi-thread"] }
86 | tokio-test = "0.4.4"
87 | tower-http = { version = "0.5.2", features = ["add-extension"] }
88 | tracing-test = "0.2.4"
89 |
90 | [[bench]]
91 | name = "html"
92 | harness = false
93 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/benches/html.rs:
--------------------------------------------------------------------------------
1 | use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
2 | use rattler_installs_packages::index::html::{parse_package_names_html, parse_project_info_html};
3 | use std::str::FromStr;
4 | use url::Url;
5 |
6 | fn parse_project_info(c: &mut Criterion) {
7 | let html = r#"
8 |
9 |
10 |
11 |
12 |
13 | link1
14 | link2
15 | link3
16 |
17 |
18 | "#;
19 | let url = Url::from_str("https://example.com/simple/link").unwrap();
20 | c.bench_with_input(
21 | BenchmarkId::new("parse_project_info", "html"),
22 | &(html, url),
23 | |b, (html, url)| b.iter(|| parse_project_info_html(url, html)),
24 | );
25 | }
26 |
27 | fn parse_package_names(c: &mut Criterion) {
28 | let html = r#"
29 |
30 |
31 |
32 | Simple index
33 |
34 |
35 | 0
36 | 0-._.-._.-._.-._.-._.-._.-0
37 | 000
38 | 0.0.1
39 | 00101s
40 | 00print_lol
41 | 00SMALINUX
42 | 0101
43 | 01changer
44 | 01d61084-d29e-11e9-96d1-7c5cf84ffe8e
45 | 01-distributions
46 | 021
47 | 024travis-test024
48 | 02exercicio
49 | 0411-test
50 | 0.618
51 | 0706xiaoye
52 | 0805nexter
53 | 090807040506030201testpip
54 | 0-core-client
55 | 0FELA
56 | 0html
57 | 0imap
58 | 0lever-so
59 | 0lever-utils
60 | 0-orchestrator
61 | 0proto
62 | 0rest
63 | 0rss
64 | 0wdg9nbmpm
65 | 0wneg
66 | 0x01-autocert-dns-aliyun
67 | 0x01-cubic-sdk
68 | 0x01-letsencrypt
69 | 0x0-python
70 | 0x10c-asm
71 | 0x20bf
72 | 0x2nac0nda
73 | 0x-contract-addresses
74 | 0x-contract-artifacts
75 | 0x-contract-wrappers
76 |
77 |
78 | "#;
79 |
80 | c.bench_function("parse_package_names", |b| {
81 | b.iter(|| parse_package_names_html(black_box(html)))
82 | });
83 | }
84 |
85 | criterion_group!(benches, parse_project_info, parse_package_names);
86 | criterion_main!(benches);
87 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/artifacts/mod.rs:
--------------------------------------------------------------------------------
1 | //! Module containing artifacts that can be resolved and installed.
2 | mod sdist;
3 |
4 | mod stree;
5 | /// Module for working with PyPA wheels. Contains the [`Wheel`] type, and related functionality.
6 | pub mod wheel;
7 |
8 | pub use sdist::SDist;
9 | pub use stree::STree;
10 | pub use wheel::Wheel;
11 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__build_rich_as_folder_as_source_dependency.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/artifacts/sdist.rs
3 | expression: "artifact_info[0].filename.as_stree().unwrap().distribution"
4 | ---
5 | PackageName {
6 | source: "rich",
7 | normalized: "rich",
8 | }
9 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__build_rich_git_reference_source_code.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/artifacts/sdist.rs
3 | expression: "artifact_info[0].filename"
4 | ---
5 | STree(
6 | STreeFilename {
7 | distribution: PackageName {
8 | source: "rich",
9 | normalized: "rich",
10 | },
11 | version: Version {
12 | epoch: 0,
13 | release: [
14 | 13,
15 | 7,
16 | 0,
17 | ],
18 | pre: None,
19 | post: None,
20 | dev: None,
21 | local: None,
22 | },
23 | url: Url {
24 | scheme: "git+https",
25 | cannot_be_a_base: false,
26 | username: "",
27 | password: None,
28 | host: Some(
29 | Domain(
30 | "github.com",
31 | ),
32 | ),
33 | port: None,
34 | path: "/Textualize/rich.git@v13.7.0",
35 | query: None,
36 | fragment: None,
37 | },
38 | },
39 | )
40 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__build_rich_sdist_as_source_dependency.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/artifacts/sdist.rs
3 | expression: "artifact_info[0].filename"
4 | ---
5 | SDist(
6 | SDistFilename {
7 | distribution: PackageName {
8 | source: "rich",
9 | normalized: "rich",
10 | },
11 | version: Version {
12 | epoch: 0,
13 | release: [
14 | 13,
15 | 6,
16 | 0,
17 | ],
18 | pre: None,
19 | post: None,
20 | dev: None,
21 | local: None,
22 | },
23 | format: TarGz,
24 | },
25 | )
26 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__build_wheel_and_pass_env_variables.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/artifacts/sdist.rs
3 | expression: metadata
4 | ---
5 | WheelCoreMetadata {
6 | name: PackageName {
7 | source: "env_package",
8 | normalized: "env-package",
9 | },
10 | version: Version {
11 | epoch: 0,
12 | release: [
13 | 0,
14 | 1,
15 | ],
16 | pre: None,
17 | post: None,
18 | dev: None,
19 | local: None,
20 | },
21 | metadata_version: MetadataVersion(
22 | Version {
23 | epoch: 0,
24 | release: [
25 | 2,
26 | 1,
27 | ],
28 | pre: None,
29 | post: None,
30 | dev: None,
31 | local: None,
32 | },
33 | ),
34 | requires_dist: [],
35 | requires_python: None,
36 | extras: {},
37 | }
38 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__build_wheel_and_with_clean_env_and_pass_env_variables.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/artifacts/sdist.rs
3 | expression: metadata
4 | ---
5 | WheelCoreMetadata {
6 | name: PackageName {
7 | source: "env_package",
8 | normalized: "env-package",
9 | },
10 | version: Version {
11 | epoch: 0,
12 | release: [
13 | 0,
14 | 1,
15 | ],
16 | pre: None,
17 | post: None,
18 | dev: None,
19 | local: None,
20 | },
21 | metadata_version: MetadataVersion(
22 | Version {
23 | epoch: 0,
24 | release: [
25 | 2,
26 | 1,
27 | ],
28 | pre: None,
29 | post: None,
30 | dev: None,
31 | local: None,
32 | },
33 | ),
34 | requires_dist: [],
35 | requires_python: None,
36 | extras: {},
37 | }
38 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__check_direct_url_json_by_tag_for_remote_git.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/artifacts/sdist.rs
3 | expression: direct_url_json
4 | ---
5 | Some(
6 | DirectUrlJson {
7 | url: Url {
8 | scheme: "https",
9 | cannot_be_a_base: false,
10 | username: "",
11 | password: None,
12 | host: Some(
13 | Domain(
14 | "github.com",
15 | ),
16 | ),
17 | port: None,
18 | path: "/mahmoud/boltons.git",
19 | query: None,
20 | fragment: None,
21 | },
22 | source: Vcs {
23 | vcs: Git,
24 | requested_revision: Some(
25 | "21.0.0",
26 | ),
27 | commit_id: "737daafc388a2380da5c5c517fc1d08f9db36990",
28 | },
29 | },
30 | )
31 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__check_direct_url_json_for_local_wheel.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/artifacts/sdist.rs
3 | expression: json
4 | ---
5 | DirectUrlJson {
6 | url: Url {
7 | scheme: "file",
8 | cannot_be_a_base: false,
9 | username: "",
10 | password: None,
11 | host: None,
12 | port: None,
13 | path: "/",
14 | query: None,
15 | fragment: None,
16 | },
17 | source: Dir {
18 | editable: None,
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__check_direct_url_json_for_remote_sdist.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/artifacts/sdist.rs
3 | expression: direct_url_json
4 | ---
5 | Some(
6 | DirectUrlJson {
7 | url: Url {
8 | scheme: "https",
9 | cannot_be_a_base: false,
10 | username: "",
11 | password: None,
12 | host: Some(
13 | Domain(
14 | "files.pythonhosted.org",
15 | ),
16 | ),
17 | port: None,
18 | path: "/packages/ea/65/163134cb3c06d42557c0d1a7bc0b53d28fb674c16489f990d9e1bbccfa7b/boltons-20.2.1.tar.gz",
19 | query: None,
20 | fragment: None,
21 | },
22 | source: Archive {
23 | hashes: Some(
24 | DirectUrlHashes {
25 | sha256: "dd362291a460cc1e0c2e91cc6a60da3036ced77099b623112e8f833e6734bdc5",
26 | },
27 | ),
28 | },
29 | },
30 | )
31 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__check_direct_url_json_with_commit_for_remote_git.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/artifacts/sdist.rs
3 | expression: direct_url_json
4 | ---
5 | Some(
6 | DirectUrlJson {
7 | url: Url {
8 | scheme: "https",
9 | cannot_be_a_base: false,
10 | username: "",
11 | password: None,
12 | host: Some(
13 | Domain(
14 | "github.com",
15 | ),
16 | ),
17 | port: None,
18 | path: "/mahmoud/boltons.git",
19 | query: None,
20 | fragment: None,
21 | },
22 | source: Vcs {
23 | vcs: Git,
24 | requested_revision: Some(
25 | "47c8046492d4db49f163bb977d20d5942e4ddb25",
26 | ),
27 | commit_id: "47c8046492d4db49f163bb977d20d5942e4ddb25",
28 | },
29 | },
30 | )
31 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__read_zip_metadata.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/artifacts/sdist.rs
3 | expression: result.1
4 | ---
5 | WheelCoreMetadata {
6 | name: PackageName {
7 | source: "filterpy",
8 | normalized: "filterpy",
9 | },
10 | version: Version {
11 | epoch: 0,
12 | release: [
13 | 1,
14 | 4,
15 | 5,
16 | ],
17 | pre: None,
18 | post: None,
19 | dev: None,
20 | local: None,
21 | },
22 | metadata_version: MetadataVersion(
23 | Version {
24 | epoch: 0,
25 | release: [
26 | 2,
27 | 1,
28 | ],
29 | pre: None,
30 | post: None,
31 | dev: None,
32 | local: None,
33 | },
34 | ),
35 | requires_dist: [
36 | Requirement {
37 | name: "numpy",
38 | extras: None,
39 | version_or_url: None,
40 | marker: None,
41 | },
42 | Requirement {
43 | name: "scipy",
44 | extras: None,
45 | version_or_url: None,
46 | marker: None,
47 | },
48 | Requirement {
49 | name: "matplotlib",
50 | extras: None,
51 | version_or_url: None,
52 | marker: None,
53 | },
54 | ],
55 | requires_python: None,
56 | extras: {},
57 | }
58 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/artifacts/snapshots/rattler_installs_packages__artifacts__sdist__tests__sdist_without_name.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/artifacts/sdist.rs
3 | expression: "artifact_info[0].filename"
4 | ---
5 | SDist(
6 | SDistFilename {
7 | distribution: PackageName {
8 | source: "rich",
9 | normalized: "rich",
10 | },
11 | version: Version {
12 | epoch: 0,
13 | release: [
14 | 13,
15 | 6,
16 | 0,
17 | ],
18 | pre: None,
19 | post: None,
20 | dev: None,
21 | local: None,
22 | },
23 | format: TarGz,
24 | },
25 | )
26 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/artifacts/stree.rs:
--------------------------------------------------------------------------------
1 | use crate::resolve::PypiVersion;
2 | use crate::types::ArtifactFromSource;
3 | use crate::types::ReadPyProjectError;
4 | use crate::types::{HasArtifactName, STreeFilename, SourceArtifactName};
5 | use fs_err as fs;
6 | use std::collections::hash_map::DefaultHasher;
7 | use std::hash::{Hash, Hasher};
8 | use std::path::{Path, PathBuf};
9 |
10 | /// Represents a source tree which can be a simple directory on filesystem
11 | /// or something cloned from git
12 | pub struct STree {
13 | /// Name of the source tree
14 | pub name: STreeFilename,
15 |
16 | /// Source tree location
17 | pub location: parking_lot::Mutex,
18 | }
19 |
20 | impl STree {
21 | /// Get a lock on the inner data
22 | pub fn lock_data(&self) -> parking_lot::MutexGuard {
23 | self.location.lock()
24 | }
25 |
26 | /// Copy source tree directory in specific location
27 | fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> {
28 | fs::create_dir_all(&dst)?;
29 | for entry in fs::read_dir(src.as_ref())? {
30 | let entry = entry?;
31 | let ty = entry.file_type()?;
32 | if ty.is_dir() {
33 | Self::copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
34 | } else {
35 | fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
36 | }
37 | }
38 | Ok(())
39 | }
40 | }
41 |
42 | impl HasArtifactName for STree {
43 | type Name = STreeFilename;
44 |
45 | fn name(&self) -> &Self::Name {
46 | &self.name
47 | }
48 | }
49 |
50 | impl ArtifactFromSource for STree {
51 | fn try_get_bytes(&self) -> Result, std::io::Error> {
52 | let vec = vec![];
53 | let inner = self.lock_data();
54 | let mut dir_entry = fs::read_dir(inner.as_path())?;
55 |
56 | let next_entry = dir_entry.next();
57 | if let Some(Ok(root_folder)) = next_entry {
58 | let modified = root_folder.metadata()?.modified()?;
59 | let mut hasher = DefaultHasher::new();
60 | modified.hash(&mut hasher);
61 | let hash = hasher.finish().to_be_bytes().as_slice().to_owned();
62 | return Ok(hash);
63 | }
64 |
65 | Ok(vec)
66 | }
67 |
68 | fn distribution_name(&self) -> String {
69 | self.name.distribution.as_source_str().to_owned()
70 | }
71 |
72 | fn version(&self) -> PypiVersion {
73 | PypiVersion::Url(self.name.url.clone())
74 | }
75 |
76 | fn artifact_name(&self) -> SourceArtifactName {
77 | SourceArtifactName::STree(self.name.clone())
78 | }
79 |
80 | fn read_pyproject_toml(&self) -> Result {
81 | let location = self.lock_data().join("pyproject.toml");
82 |
83 | if let Ok(bytes) = fs::read(location) {
84 | let source = String::from_utf8(bytes).map_err(|e| {
85 | ReadPyProjectError::PyProjectTomlParseError(format!(
86 | "could not parse pyproject.toml (bad encoding): {}",
87 | e
88 | ))
89 | })?;
90 | let project = pyproject_toml::PyProjectToml::new(&source).map_err(|e| {
91 | ReadPyProjectError::PyProjectTomlParseError(format!(
92 | "could not parse pyproject.toml (bad toml): {}",
93 | e
94 | ))
95 | })?;
96 | Ok(project)
97 | } else {
98 | Err(ReadPyProjectError::NoPyProjectTomlFound)
99 | }
100 | }
101 | /// move all files to a specific directory
102 | fn extract_to(&self, work_dir: &Path) -> std::io::Result<()> {
103 | let src = self.lock_data();
104 | Self::copy_dir_all(src.as_path(), work_dir)
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/index/direct_url/git.rs:
--------------------------------------------------------------------------------
1 | use crate::index::git_interop::{git_clone, GitSource, ParsedUrl};
2 | use crate::index::package_database::DirectUrlArtifactResponse;
3 | use crate::resolve::PypiVersion;
4 | use crate::types::{
5 | ArtifactHashes, ArtifactInfo, ArtifactName, ArtifactType, DirectUrlJson, DirectUrlSource,
6 | DirectUrlVcs, DistInfoMetadata, HasArtifactName, NormalizedPackageName, Yanked,
7 | };
8 | use crate::wheel_builder::WheelBuilder;
9 | use indexmap::IndexMap;
10 | use miette::IntoDiagnostic;
11 | use rattler_digest::{compute_bytes_digest, Sha256};
12 | use std::str::FromStr;
13 | use std::sync::Arc;
14 | use url::Url;
15 |
16 | /// Get artifact by git reference
17 | pub(crate) async fn get_artifacts_and_metadata>(
18 | p: P,
19 | url: Url,
20 | wheel_builder: &Arc,
21 | ) -> miette::Result {
22 | let normalized_package_name = p.into();
23 |
24 | let parsed_url = ParsedUrl::new(&url)?;
25 |
26 | let git_source = GitSource {
27 | url: parsed_url.git_url,
28 | rev: parsed_url.revision,
29 | };
30 |
31 | let (mut location, git_rev) = git_clone(&git_source).into_diagnostic()?;
32 |
33 | if let Some(subdirectory) = parsed_url.subdirectory {
34 | location.push(&subdirectory);
35 | if !location.exists() {
36 | return Err(miette::miette!(
37 | "Requested subdirectory fragment {:?} can't be located at following url {:?}",
38 | subdirectory,
39 | url
40 | ));
41 | }
42 | };
43 |
44 | let (wheel_metadata, artifact) = super::file::get_stree_from_file_path(
45 | &normalized_package_name,
46 | url.clone(),
47 | Some(location),
48 | wheel_builder,
49 | )
50 | .await?;
51 |
52 | let requires_python = wheel_metadata.1.requires_python.clone();
53 |
54 | let dist_info_metadata = DistInfoMetadata {
55 | available: false,
56 | hashes: ArtifactHashes::default(),
57 | };
58 |
59 | let yanked = Yanked {
60 | yanked: false,
61 | reason: None,
62 | };
63 |
64 | let direct_url_json = DirectUrlJson {
65 | url: Url::from_str(parsed_url.url.as_str()).expect("URL should be parseable"),
66 | source: DirectUrlSource::Vcs {
67 | vcs: DirectUrlVcs::Git,
68 | requested_revision: git_source.rev,
69 | commit_id: git_rev.get_commit(),
70 | },
71 | };
72 |
73 | let project_hash = ArtifactHashes {
74 | sha256: Some(compute_bytes_digest::(url.as_str().as_bytes())),
75 | };
76 |
77 | let artifact_info = Arc::new(ArtifactInfo {
78 | filename: ArtifactName::STree(artifact.name().clone()),
79 | url: url.clone(),
80 | is_direct_url: true,
81 | hashes: Some(project_hash),
82 | requires_python,
83 | dist_info_metadata,
84 | yanked,
85 | });
86 |
87 | let mut result = IndexMap::default();
88 | result.insert(PypiVersion::Url(url.clone()), vec![artifact_info.clone()]);
89 |
90 | Ok(DirectUrlArtifactResponse {
91 | artifact_info,
92 | metadata: (wheel_metadata.0, wheel_metadata.1),
93 | artifact_versions: result,
94 | artifact: ArtifactType::STree(artifact),
95 | direct_url_json,
96 | })
97 | }
98 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/index/direct_url/mod.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use crate::index::http::Http;
4 | use crate::index::package_database::DirectUrlArtifactResponse;
5 | use crate::types::NormalizedPackageName;
6 | use crate::wheel_builder::WheelBuilder;
7 | use url::Url;
8 |
9 | pub(crate) mod file;
10 | pub(crate) mod git;
11 | pub(crate) mod http;
12 |
13 | /// Get artifact directly from file, vcs, or url
14 | pub(crate) async fn fetch_artifact_and_metadata_by_direct_url>(
15 | http: &Http,
16 | p: P,
17 | url: Url,
18 | wheel_builder: &Arc,
19 | ) -> miette::Result {
20 | let p = p.into();
21 |
22 | let response = if url.scheme() == "file" {
23 | // This can result in a Wheel, Sdist or STree
24 | super::direct_url::file::get_artifacts_and_metadata(p.clone(), url, wheel_builder).await
25 | } else if url.scheme() == "https" {
26 | // This can be a Wheel or SDist artifact
27 | super::direct_url::http::get_artifacts_and_metadata(http, p.clone(), url, wheel_builder)
28 | .await
29 | } else if url.scheme() == "git+https" || url.scheme() == "git+file" {
30 | // This can be a STree artifact
31 | super::direct_url::git::get_artifacts_and_metadata(p.clone(), url, wheel_builder).await
32 | } else {
33 | Err(miette::miette!(
34 | "Usage of insecure protocol or unsupported scheme {:?}",
35 | url.scheme()
36 | ))
37 | }?;
38 |
39 | Ok(response)
40 | }
41 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/index/lazy_metadata.rs:
--------------------------------------------------------------------------------
1 | use crate::artifacts::wheel::{find_dist_info_metadata, WheelVitalsError};
2 | use crate::types::{WheelCoreMetadata, WheelFilename};
3 | use async_http_range_reader::AsyncHttpRangeReader;
4 | use async_zip::base::read::seek::ZipFileReader;
5 | use tokio_util::compat::TokioAsyncReadCompatExt;
6 |
7 | /// Reads the metadata from a wheel by only reading parts of the wheel zip.
8 | ///
9 | /// This function uses [`AsyncHttpRangeReader`] which allows reading parts of a file by performing
10 | /// http range requests. First the end of the file is read to index the central directory of the
11 | /// zip. This provides an index into the file which allows accessing the exact bytes that contain
12 | /// the METADATA file.
13 | pub(crate) async fn lazy_read_wheel_metadata(
14 | name: &WheelFilename,
15 | stream: &mut AsyncHttpRangeReader,
16 | ) -> Result<(Vec, WheelCoreMetadata), WheelVitalsError> {
17 | // Make sure we have the back part of the stream.
18 | // Best guess for the central directory size inside the zip
19 | const CENTRAL_DIRECTORY_SIZE: u64 = 16384;
20 | // Because the zip index is at the back
21 | stream
22 | .prefetch(stream.len().saturating_sub(CENTRAL_DIRECTORY_SIZE)..stream.len())
23 | .await;
24 |
25 | // Construct a zip reader to uses the stream.
26 | let mut reader = ZipFileReader::new(stream.compat())
27 | .await
28 | .map_err(|err| WheelVitalsError::from_async_zip("/".into(), err))?;
29 |
30 | // Collect all top-level filenames
31 | let file_names = reader
32 | .file()
33 | .entries()
34 | .iter()
35 | .enumerate()
36 | .filter_map(|(idx, entry)| Some(((idx, entry), entry.filename().as_str().ok()?)));
37 |
38 | // Determine the name of the dist-info directory
39 | let ((metadata_idx, metadata_entry), dist_info_prefix) =
40 | find_dist_info_metadata(name, file_names)?;
41 | let metadata_path = format!("{dist_info_prefix}.dist-info/METADATA");
42 |
43 | // Get the size of the entry plus the header + size of the filename. We should also actually
44 | // include bytes for the extra fields but we don't have that information.
45 | let offset = metadata_entry.header_offset();
46 | let size = metadata_entry.compressed_size()
47 | + 30 // Header size in bytes
48 | + metadata_entry.filename().as_bytes().len() as u64;
49 |
50 | // The zip archive uses as BufReader which reads in chunks of 8192. To ensure we prefetch
51 | // enough data we round the size up to the nearest multiple of the buffer size.
52 | let buffer_size = 8192;
53 | let size = ((size + buffer_size - 1) / buffer_size) * buffer_size;
54 |
55 | // Fetch the bytes from the zip archive that contain the requested file.
56 | reader
57 | .inner_mut()
58 | .get_mut()
59 | .prefetch(offset..offset + size)
60 | .await;
61 |
62 | // Read the contents of the metadata.json file
63 | let mut contents = Vec::new();
64 | reader
65 | .reader_with_entry(metadata_idx)
66 | .await
67 | .map_err(|e| WheelVitalsError::from_async_zip(metadata_path.clone(), e))?
68 | .read_to_end_checked(&mut contents)
69 | .await
70 | .map_err(|e| WheelVitalsError::from_async_zip(metadata_path, e))?;
71 |
72 | // Parse the wheel data
73 | let metadata = WheelCoreMetadata::try_from(contents.as_slice())?;
74 |
75 | let stream = reader.into_inner().into_inner();
76 | let ranges = stream.requested_ranges().await;
77 | let total_bytes_fetched: u64 = ranges.iter().map(|r| r.end - r.start).sum();
78 | tracing::debug!(
79 | "fetched {} ranges, total of {} bytes, total file length {} ({}%)",
80 | ranges.len(),
81 | total_bytes_fetched,
82 | stream.len(),
83 | (total_bytes_fetched as f64 / stream.len() as f64 * 100000.0).round() / 100.0
84 | );
85 |
86 | Ok((contents, metadata))
87 | }
88 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/index/mod.rs:
--------------------------------------------------------------------------------
1 | //! This module contains functions for working with PyPA packaging repositories.
2 |
3 | mod file_store;
4 |
5 | mod direct_url;
6 | mod git_interop;
7 | pub mod html;
8 | mod http;
9 | mod lazy_metadata;
10 | mod package_database;
11 | mod package_sources;
12 |
13 | pub use package_database::{ArtifactRequest, CheckAvailablePackages, PackageDb};
14 | pub use package_sources::{PackageSources, PackageSourcesBuilder};
15 |
16 | pub use self::http::CacheMode;
17 | pub use html::parse_hash;
18 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/install/install_paths.rs:
--------------------------------------------------------------------------------
1 | use crate::python_env::PythonInterpreterVersion;
2 | use std::borrow::Cow;
3 | use std::path::{Path, PathBuf};
4 |
5 | /// A struct of installation categories to where they should be stored relative to the
6 | /// installation destination.
7 | #[derive(Debug, Clone)]
8 | pub struct InstallPaths {
9 | purelib: PathBuf,
10 | platlib: PathBuf,
11 | scripts: PathBuf,
12 | data: PathBuf,
13 | headers: PathBuf,
14 | windows: bool,
15 | }
16 |
17 | impl InstallPaths {
18 | /// Populates mappings of installation targets for a virtualenv layout. The mapping depends on
19 | /// the python version and whether or not the installation targets windows. Specifically on
20 | /// windows some of the paths are different. :shrug:
21 | pub fn for_venv>(version: V, windows: bool) -> Self {
22 | let version = version.into();
23 |
24 | let site_packages = if windows {
25 | Path::new("Lib").join("site-packages")
26 | } else {
27 | Path::new("lib").join(format!(
28 | "python{}.{}/site-packages",
29 | version.major, version.minor
30 | ))
31 | };
32 | let scripts = if windows {
33 | PathBuf::from("Scripts")
34 | } else {
35 | PathBuf::from("bin")
36 | };
37 |
38 | // Data should just be the root of the venv
39 | let data = PathBuf::from("");
40 |
41 | // purelib and platlib locations are not relevant when using venvs
42 | // https://stackoverflow.com/a/27882460/3549270
43 | Self {
44 | purelib: site_packages.clone(),
45 | platlib: site_packages,
46 | scripts,
47 | data,
48 | windows,
49 | headers: PathBuf::from("include"),
50 | }
51 | }
52 |
53 | /// Determines whether this is a windows InstallPath
54 | pub fn is_windows(&self) -> bool {
55 | self.windows
56 | }
57 |
58 | /// Returns the site-packages location. This is done by searching for the purelib location.
59 | pub fn site_packages(&self) -> &Path {
60 | &self.purelib
61 | }
62 |
63 | /// Reference to pure python library location.
64 | pub fn purelib(&self) -> &Path {
65 | &self.purelib
66 | }
67 |
68 | /// Reference to platform specific library location.
69 | pub fn platlib(&self) -> &Path {
70 | &self.platlib
71 | }
72 |
73 | /// Returns the binaries location.
74 | pub fn scripts(&self) -> &Path {
75 | &self.scripts
76 | }
77 |
78 | /// Returns the location of the data directory
79 | pub fn data(&self) -> &Path {
80 | &self.data
81 | }
82 |
83 | /// Returns the location of the include directory
84 | pub fn include(&self) -> PathBuf {
85 | if self.windows {
86 | PathBuf::from("Include")
87 | } else {
88 | PathBuf::from("include")
89 | }
90 | }
91 |
92 | /// Returns the location of the headers directory. The location of headers is specific to a
93 | /// distribution name.
94 | pub fn headers(&self, distribution_name: &str) -> PathBuf {
95 | self.headers.join(distribution_name)
96 | }
97 |
98 | /// Matches the different categories to their install paths.
99 | pub fn match_category(&self, category: &str, distribution_name: &str) -> Option> {
100 | match category {
101 | "purelib" => Some(self.purelib().into()),
102 | "platlib" => Some(self.platlib().into()),
103 | "scripts" => Some(self.scripts().into()),
104 | "data" => Some(self.data().into()),
105 | "headers" => Some(self.headers(distribution_name).into()),
106 | &_ => None,
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/install/snapshots/rattler_installs_packages__install__test__entry_points.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/install/mod.rs
3 | expression: stdout
4 | ---
5 | borg : '=='
6 | dead : 'xx'
7 | default : 'oo'
8 | greedy : '$$'
9 | paranoid : '@@'
10 | tired : '--'
11 | wired : 'OO'
12 | young : '..'
13 |
14 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/install/snapshots/rattler_installs_packages__install__test__miniblack-23.1.0-py3-none-any.whl.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/install/mod.rs
3 | expression: record_content
4 | ---
5 | ../../../bin/black,sha256=G2L8O1CciVzKcy6rDIj66_vauFAGNMXYpKPgqaow3XE,212
6 | ../../../bin/blackd,sha256=7FFQGF_2mwatv_1_GpVmEh9QJhn0IIi9PtJUHvnRySs,213
7 | black/__init__.py,sha256=VWazgq80l38r1Cn5Ea7fH5bMuDdFuQJtYgBKGWJganI,46682
8 | blackd/__init__.py,sha256=cTJotjnER9YB0rDWo3hOWSg4v0mNAx5DFmCJdyB1s5k,8068
9 | miniblack-23.1.0.dist-info/INSTALLER,sha256=FpzDrzfcP6oI1mbsvYAjw7hT2I4P4wr99k5NKqmKJt0,10
10 | miniblack-23.1.0.dist-info/METADATA,sha256=galtIfXUJ5UEq_3ERFi03BdCo6KHkz2r6jc0EYPsEPY,58963
11 | miniblack-23.1.0.dist-info/RECORD,,
12 | miniblack-23.1.0.dist-info/WHEEL,sha256=rE4t8wm7r0N5wTLLeSbOvJha-KwM1ALPuB-FmvNahDo,144
13 | miniblack-23.1.0.dist-info/entry_points.txt,sha256=qBIyywHwGRkJj7kieq86kqf77rz3qGC4Joj36lHnxwc,78
14 | miniblack-23.1.0.dist-info/licenses/AUTHORS.md,sha256=cpHlH2nYyIXWEnA0LccoGenN-g5HZ7341klZ7MwbdIU,8043
15 | miniblack-23.1.0.dist-info/licenses/LICENSE,sha256=nAQo8MO0d5hQz1vZbhGqqK_HLUqG1KNiI9erouWNbgA,1080
16 |
17 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/install/snapshots/rattler_installs_packages__install__test__purelib_and_platlib-1.0.0-cp38-cp38-linux_x86_64.whl.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/install/mod.rs
3 | expression: record_content
4 | ---
5 | pure.py,sha256=VRmPEZgJrqUEQPsd8wxEp7cMWzySWs7MojKDGZhjUCQ,28
6 | purelib_and_platlib-1.0.0.dist-info/INSTALLER,sha256=FpzDrzfcP6oI1mbsvYAjw7hT2I4P4wr99k5NKqmKJt0,10
7 | purelib_and_platlib-1.0.0.dist-info/METADATA,sha256=3YoTq5hsIcUfHSB9eMPScL0ntE8JfFNnwUAL9qUjy5g,62
8 | purelib_and_platlib-1.0.0.dist-info/RECORD,,
9 | purelib_and_platlib-1.0.0.dist-info/WHEEL,sha256=3L-5uHiSOZxNkLCtSZK7mYrls2MmCfq0WHor5xNVzco,86
10 |
11 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! RIP is a library that allows the resolving and installing of Python PyPI packages from Rust into a virtual environment.
2 | //! It's based on our experience with building Rattler and aims to provide the same experience but for PyPI instead of Conda.
3 | //! It should be fast and easy to use.
4 | //! Like Rattler, this library is not a package manager itself but provides the low-level plumbing to be used in one.
5 |
6 | #![deny(missing_docs)]
7 |
8 | /// Contains the types that are used throughout the library.
9 | pub mod types;
10 |
11 | pub mod python_env;
12 |
13 | pub mod index;
14 |
15 | pub mod install;
16 | mod utils;
17 |
18 | pub mod resolve;
19 |
20 | pub mod wheel_builder;
21 |
22 | mod win;
23 |
24 | pub mod artifacts;
25 | pub use utils::normalize_index_url;
26 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/python_env/compile_pyc.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import json
3 | import importlib
4 | import compileall
5 | from multiprocessing import Pool
6 |
7 |
8 | def compile_one(path):
9 | success = compileall.compile_file(path, quiet=2, force=True)
10 | output_path = importlib.util.cache_from_source(path) if success else None
11 | return path, output_path
12 |
13 |
14 | def compilation_finished(compilation_result):
15 | path, output_path = compilation_result
16 | print(json.dumps({"path": path, "output_path": output_path}))
17 |
18 |
19 | if __name__ == "__main__":
20 | with sys.stdin:
21 | with Pool() as pool:
22 | while True:
23 | path = sys.stdin.readline().strip()
24 | if not path:
25 | break
26 | pool.apply_async(compile_one, (path,), callback=compilation_finished)
27 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/python_env/env_markers/from_env.rs:
--------------------------------------------------------------------------------
1 | use super::Pep508EnvMakers;
2 | use crate::python_env::{system_python_executable, FindPythonError};
3 | use std::io;
4 | use std::io::ErrorKind;
5 | use std::path::Path;
6 | use std::process::ExitStatus;
7 | use thiserror::Error;
8 |
9 | #[derive(Debug, Error)]
10 | pub enum FromPythonError {
11 | #[error(transparent)]
12 | CouldNotFindPythonExecutable(#[from] FindPythonError),
13 |
14 | #[error(transparent)]
15 | FailedToExecute(#[from] io::Error),
16 |
17 | #[error(transparent)]
18 | FailedToParse(#[from] serde_json::Error),
19 |
20 | #[error("execution failed with exit code {0}")]
21 | FailedToRun(ExitStatus),
22 | }
23 |
24 | impl Pep508EnvMakers {
25 | /// Try to determine the environment markers by executing python.
26 | pub async fn from_env() -> Result {
27 | let python = system_python_executable()?;
28 | tracing::info!("using python executable at {}", python.display());
29 | Self::from_python(python.as_path()).await
30 | }
31 |
32 | /// Try to determine the environment markers from an existing python executable. The executable
33 | /// is used to run a simple python program to extract the information.
34 | pub async fn from_python(python: &Path) -> Result {
35 | let pep508_bytes = include_str!("pep508.py");
36 |
37 | // Execute the python executable
38 | let output = match tokio::process::Command::new(python)
39 | .arg("-c")
40 | .arg(pep508_bytes)
41 | .output()
42 | .await
43 | {
44 | Err(e) if e.kind() == ErrorKind::NotFound => {
45 | return Err(FromPythonError::CouldNotFindPythonExecutable(
46 | FindPythonError::NotFound,
47 | ))
48 | }
49 | Err(e) => return Err(FromPythonError::FailedToExecute(e)),
50 | Ok(output) => output,
51 | };
52 |
53 | // Ensure that we have a valid success code
54 | if !output.status.success() {
55 | return Err(FromPythonError::FailedToRun(output.status));
56 | }
57 |
58 | // Convert the JSON
59 | let stdout = String::from_utf8_lossy(&output.stdout);
60 | Ok(serde_json::from_str(stdout.trim())?)
61 | }
62 | }
63 |
64 | #[cfg(test)]
65 | mod test {
66 | use super::*;
67 |
68 | #[tokio::test]
69 | pub async fn test_from_env() {
70 | match Pep508EnvMakers::from_env().await {
71 | Err(FromPythonError::CouldNotFindPythonExecutable(_)) => {
72 | // This is fine, the test machine does not include a python binary.
73 | }
74 | Err(e) => panic!("{e}"),
75 | Ok(env) => {
76 | println!(
77 | "Found the following environment markers on the current system:\n\n{env:#?}"
78 | )
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/python_env/env_markers/mod.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 | use std::ops::Deref;
3 |
4 | mod from_env;
5 |
6 | /// Describes the environment markers that can be used in dependency specifications to enable or
7 | /// disable certain dependencies based on runtime environment.
8 | ///
9 | /// Exactly the markers defined in this struct must be present during version resolution. Unknown
10 | /// variables should raise an error.
11 | ///
12 | /// Note that the "extra" variable is not defined in this struct because it depends on the wheel
13 | /// that is being inspected.
14 | ///
15 | /// The behavior and the names of the markers are described in PEP 508.
16 | #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
17 | #[allow(missing_docs)]
18 | #[serde(transparent)]
19 | pub struct Pep508EnvMakers(pub pep508_rs::MarkerEnvironment);
20 |
21 | impl From for Pep508EnvMakers {
22 | fn from(value: pep508_rs::MarkerEnvironment) -> Self {
23 | Self(value)
24 | }
25 | }
26 |
27 | impl Deref for Pep508EnvMakers {
28 | type Target = pep508_rs::MarkerEnvironment;
29 |
30 | fn deref(&self) -> &Self::Target {
31 | &self.0
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/python_env/env_markers/pep508.py:
--------------------------------------------------------------------------------
1 | # A program that outputs PEP 508 environment markers in a JSON format. Most of the
2 | # implementation has been taken from the example in the PEP.
3 | #
4 | # See: https://peps.python.org/pep-0508/
5 |
6 | import os
7 | import sys
8 | import platform
9 | import json
10 |
11 |
12 | def format_full_version(info):
13 | version = '{0.major}.{0.minor}.{0.micro}'.format(info)
14 | kind = info.releaselevel
15 | if kind != 'final':
16 | version += kind[0] + str(info.serial)
17 | return version
18 |
19 |
20 | if hasattr(sys, 'implementation'):
21 | implementation_version = format_full_version(sys.implementation.version)
22 | implementation_name = sys.implementation.name
23 | else:
24 | implementation_version = '0'
25 | implementation_name = ''
26 | bindings = {
27 | 'implementation_name': implementation_name,
28 | 'implementation_version': implementation_version,
29 | 'os_name': os.name,
30 | 'platform_machine': platform.machine(),
31 | 'platform_python_implementation': platform.python_implementation(),
32 | 'platform_release': platform.release(),
33 | 'platform_system': platform.system(),
34 | 'platform_version': platform.version(),
35 | 'python_full_version': platform.python_version(),
36 | 'python_version': '.'.join(platform.python_version_tuple()[:2]),
37 | 'sys_platform': sys.platform,
38 | }
39 |
40 | json.dump(bindings, sys.stdout)
41 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/python_env/mod.rs:
--------------------------------------------------------------------------------
1 | //! Module for working with python environments.
2 | //! Contains functionality for querying and manipulating python environments.
3 |
4 | mod tags;
5 |
6 | mod distribution_finder;
7 |
8 | mod env_markers;
9 |
10 | mod system_python;
11 |
12 | mod uninstall;
13 | mod venv;
14 |
15 | mod byte_code_compiler;
16 |
17 | pub use tags::{WheelTag, WheelTags};
18 |
19 | pub use byte_code_compiler::{ByteCodeCompiler, CompilationError, SpawnCompilerError};
20 | pub use distribution_finder::{
21 | find_distributions_in_directory, find_distributions_in_venv, Distribution,
22 | FindDistributionError,
23 | };
24 | pub use env_markers::Pep508EnvMakers;
25 | pub(crate) use system_python::{system_python_executable, FindPythonError};
26 | pub use system_python::{ParsePythonInterpreterVersionError, PythonInterpreterVersion};
27 | pub use uninstall::{uninstall_distribution, UninstallDistributionError};
28 | pub use venv::{PythonLocation, VEnv, VEnvError};
29 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/python_env/snapshots/rattler_installs_packages__python_env__distribution_finder__test__find_distributions.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: crates/rattler_installs_packages/src/python_env/distribution_finder.rs
3 | expression: distributions
4 | ---
5 | [
6 | Distribution(
7 | name: "click",
8 | version: "7.1.2",
9 | installer: Some("pip"),
10 | dist_info: "Lib/site-packages/click-7.1.2.dist-info",
11 | tags: Some([
12 | "py2-none-any",
13 | "py3-none-any",
14 | ]),
15 | ),
16 | Distribution(
17 | name: "flask",
18 | version: "1.1.4",
19 | installer: Some("pip"),
20 | dist_info: "Lib/site-packages/Flask-1.1.4.dist-info",
21 | tags: Some([
22 | "py2-none-any",
23 | "py3-none-any",
24 | ]),
25 | ),
26 | Distribution(
27 | name: "itsdangerous",
28 | version: "1.1.0",
29 | installer: Some("pip"),
30 | dist_info: "Lib/site-packages/itsdangerous-1.1.0.dist-info",
31 | tags: Some([
32 | "py2-none-any",
33 | "py3-none-any",
34 | ]),
35 | ),
36 | Distribution(
37 | name: "jinja2",
38 | version: "2.11.3",
39 | installer: Some("pip"),
40 | dist_info: "Lib/site-packages/Jinja2-2.11.3.dist-info",
41 | tags: Some([
42 | "py2-none-any",
43 | "py3-none-any",
44 | ]),
45 | ),
46 | Distribution(
47 | name: "markupsafe",
48 | version: "1.1.1",
49 | installer: Some("pip"),
50 | dist_info: "Lib/site-packages/MarkupSafe-1.1.1.dist-info",
51 | tags: Some([
52 | "cp35-cp35m-win_amd64",
53 | ]),
54 | ),
55 | Distribution(
56 | name: "pip",
57 | version: "9.0.1",
58 | installer: Some("pip"),
59 | dist_info: "Lib/site-packages/pip-9.0.1.dist-info",
60 | tags: Some([
61 | "py2-none-any",
62 | "py3-none-any",
63 | ]),
64 | ),
65 | Distribution(
66 | name: "setuptools",
67 | version: "28.8.0",
68 | installer: Some("pip"),
69 | dist_info: "Lib/site-packages/setuptools-28.8.0.dist-info",
70 | tags: Some([
71 | "py2-none-any",
72 | "py3-none-any",
73 | ]),
74 | ),
75 | Distribution(
76 | name: "werkzeug",
77 | version: "1.0.1",
78 | installer: Some("pip"),
79 | dist_info: "Lib/site-packages/Werkzeug-1.0.1.dist-info",
80 | tags: Some([
81 | "py2-none-any",
82 | "py3-none-any",
83 | ]),
84 | ),
85 | ]
86 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/python_env/tags/from_env.rs:
--------------------------------------------------------------------------------
1 | use crate::python_env::{system_python_executable, FindPythonError, WheelTag, WheelTags};
2 | use crate::utils::VENDORED_PACKAGING_DIR;
3 | use serde::Deserialize;
4 | use std::io;
5 | use std::io::ErrorKind;
6 | use std::path::Path;
7 | use std::process::ExitStatus;
8 | use thiserror::Error;
9 |
10 | #[derive(Debug, Error)]
11 | pub enum FromPythonError {
12 | #[error(transparent)]
13 | CouldNotFindPythonExecutable(#[from] FindPythonError),
14 |
15 | #[error("{0}")]
16 | PythonError(String),
17 |
18 | #[error(transparent)]
19 | FailedToExecute(#[from] io::Error),
20 |
21 | #[error(transparent)]
22 | FailedToParse(#[from] serde_json::Error),
23 |
24 | #[error("execution failed with exit code {0}")]
25 | FailedToRun(ExitStatus),
26 | }
27 |
28 | impl WheelTags {
29 | /// Try to determine the platform tags by executing the python command and extracting `sys_tags`
30 | /// using the vendored `packaging` module.
31 | pub async fn from_env() -> Result {
32 | Self::from_python(system_python_executable()?.as_path()).await
33 | }
34 |
35 | /// Try to determine the platform tags by executing the python command and extracting `sys_tags`
36 | /// using the vendored `packaging` module.
37 | pub async fn from_python(python: &Path) -> Result {
38 | // Create a temporary directory to place our vendored packages in
39 | let vendored_dir = tempfile::tempdir()?;
40 | let packaging_target_dir = vendored_dir.path().join("packaging");
41 | tokio::fs::create_dir_all(&packaging_target_dir).await?;
42 | VENDORED_PACKAGING_DIR.extract(&packaging_target_dir)?;
43 |
44 | // Execute the python executable
45 | let output = match tokio::process::Command::new(python)
46 | .arg("-c")
47 | .arg(include_str!("platform_tags.py"))
48 | .env("PYTHONPATH", vendored_dir.path())
49 | .output()
50 | .await
51 | {
52 | Err(e) if e.kind() == ErrorKind::NotFound => {
53 | return Err(FromPythonError::CouldNotFindPythonExecutable(
54 | FindPythonError::NotFound,
55 | ))
56 | }
57 | Err(e) => return Err(FromPythonError::FailedToExecute(e)),
58 | Ok(output) => output,
59 | };
60 |
61 | // Ensure that we have a valid success code
62 | if !output.status.success() {
63 | return Err(FromPythonError::FailedToRun(output.status));
64 | }
65 |
66 | #[derive(Deserialize)]
67 | #[serde(untagged)]
68 | enum Result {
69 | Tags(Vec<(String, String, String)>),
70 | Error(String),
71 | }
72 |
73 | // Convert the JSON
74 | let stdout = String::from_utf8_lossy(&output.stdout);
75 | match serde_json::from_str(stdout.trim())? {
76 | Result::Tags(tags) => Ok(Self {
77 | tags: tags
78 | .into_iter()
79 | .map(|(interpreter, abi, platform)| WheelTag {
80 | interpreter,
81 | abi,
82 | platform,
83 | })
84 | .collect(),
85 | }),
86 | Result::Error(err) => Err(FromPythonError::PythonError(err)),
87 | }
88 | }
89 | }
90 |
91 | #[cfg(test)]
92 | mod test {
93 | use super::*;
94 | use itertools::Itertools;
95 |
96 | #[tokio::test]
97 | pub async fn test_from_env() {
98 | match WheelTags::from_env().await {
99 | Err(FromPythonError::CouldNotFindPythonExecutable(_)) => {
100 | // This is fine, the test machine does not include a python binary.
101 | }
102 | Err(FromPythonError::PythonError(e)) => {
103 | println!("{e}")
104 | }
105 | Err(e) => panic!("{e:?}"),
106 | Ok(tags) => {
107 | println!(
108 | "Found the following platform tags on the current system:\n{}",
109 | tags.tags.iter().format(", ")
110 | )
111 | }
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/python_env/tags/mod.rs:
--------------------------------------------------------------------------------
1 | //! Wheels encode the Python interpreter, ABI, and platform that they support in their filenames
2 | //! using platform compatibility tags. This module provides support for discovering what tags the
3 | //! running Python interpreter supports and determining if a wheel is compatible with a set of tags.
4 |
5 | mod from_env;
6 |
7 | use indexmap::IndexSet;
8 | use itertools::Itertools;
9 | use serde_with::{DeserializeFromStr, SerializeDisplay};
10 | use std::fmt::{Debug, Display, Formatter};
11 | use std::str::FromStr;
12 |
13 | /// A representation of a tag triple for a wheel.
14 | #[derive(Debug, Clone, Hash, Eq, PartialEq, SerializeDisplay, DeserializeFromStr)]
15 | pub struct WheelTag {
16 | /// The interpreter name, e.g. "py"
17 | pub interpreter: String,
18 |
19 | /// The ABI that a wheel supports, e.g. "cp37m"
20 | pub abi: String,
21 |
22 | /// The OS/platform the wheel supports, e.g. "win_am64".
23 | pub platform: String,
24 | }
25 |
26 | impl WheelTag {
27 | /// Parses a compound string into a `WheelTag`. A compound string is a string that contains
28 | /// multiple tags in a single string.
29 | ///
30 | /// ```rust
31 | /// # use rattler_installs_packages::python_env::WheelTag;
32 | /// let tags = WheelTag::from_compound_string(
33 | /// "cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64").unwrap();
34 | ///
35 | /// assert_eq!(tags.len(), 2);
36 | /// assert_eq!(tags[0].interpreter, "cp310");
37 | /// assert_eq!(tags[0].abi, "cp310");
38 | /// assert_eq!(tags[0].platform, "manylinux_2_17_x86_64");
39 | /// assert_eq!(tags[1].interpreter, "cp310");
40 | /// assert_eq!(tags[1].abi, "cp310");
41 | /// assert_eq!(tags[1].platform, "manylinux2014_x86_64");
42 | ///
43 | /// ```
44 | pub fn from_compound_string(s: &str) -> Result, String> {
45 | let Some((interpreter, abi, platform)) =
46 | s.split('-').map(ToOwned::to_owned).collect_tuple()
47 | else {
48 | return Err(String::from("not enough '-' separators"));
49 | };
50 |
51 | Ok(interpreter
52 | .split('.')
53 | .cartesian_product(abi.split('.'))
54 | .cartesian_product(platform.split('.'))
55 | .map(|((interpreter, abi), platform)| Self {
56 | interpreter: interpreter.to_string(),
57 | abi: abi.to_string(),
58 | platform: platform.to_string(),
59 | })
60 | .collect())
61 | }
62 | }
63 |
64 | impl FromStr for WheelTag {
65 | type Err = String;
66 |
67 | fn from_str(s: &str) -> Result {
68 | let Some((interpreter, abi, platform)) =
69 | s.split('-').map(ToOwned::to_owned).collect_tuple()
70 | else {
71 | return Err(String::from("not enough '-' separators"));
72 | };
73 | Ok(Self {
74 | interpreter,
75 | abi,
76 | platform,
77 | })
78 | }
79 | }
80 |
81 | impl Display for WheelTag {
82 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
83 | write!(f, "{}-{}-{}", &self.interpreter, &self.abi, &self.platform)
84 | }
85 | }
86 |
87 | /// Contains an ordered set of platform tags with which compatibility of wheels can be determined.
88 | #[derive(Debug, Clone)]
89 | pub struct WheelTags {
90 | tags: IndexSet,
91 | }
92 |
93 | impl WheelTags {
94 | /// Returns an iterator over the supported tags.
95 | pub fn tags(&self) -> impl Iterator- + '_ {
96 | self.tags.iter()
97 | }
98 |
99 | /// Determines the compatibility of the specified tag with the tags in this instance. Returns
100 | /// `None` if the specified tag is not compatible with any of the tags in this instance. Returns
101 | /// `Some(i)` where `i` indicates the compatibility level. The higher the number the more
102 | /// specific the tag is to the platform. The wheel artifact with the highest number should be
103 | /// preferred over others.
104 | pub fn compatibility(&self, tag: &WheelTag) -> Option {
105 | self.tags.get_index_of(tag).map(|score| -(score as i32))
106 | }
107 |
108 | /// Returns if the specified tag is compatible with this set.
109 | pub fn is_compatible(&self, tag: &WheelTag) -> bool {
110 | self.tags.contains(tag)
111 | }
112 | }
113 |
114 | impl FromIterator for WheelTags {
115 | fn from_iter>(iter: T) -> Self {
116 | Self {
117 | tags: FromIterator::from_iter(iter),
118 | }
119 | }
120 | }
121 |
122 | #[cfg(test)]
123 | mod test {
124 | use super::*;
125 |
126 | #[test]
127 | fn test_from_str() {
128 | let tag = WheelTag::from_str("py2-none-any").unwrap();
129 | assert_eq!(tag.interpreter, "py2");
130 | assert_eq!(tag.abi, "none");
131 | assert_eq!(tag.platform, "any");
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/python_env/tags/platform_tags.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sys
3 | import platform
4 |
5 | if sys.version_info < (3, 6):
6 | print(
7 | '"could not determine compatible interpreter tags, the python version is too old. '
8 | 'Requires at least 3.6, but currently running %s"' % platform.python_version()
9 | )
10 | exit(0)
11 |
12 | # The implementation has the packaging module vendored
13 | from packaging.tags import sys_tags
14 |
15 | json.dump([(tag.interpreter, tag.abi, tag.platform) for tag in sys_tags()], sys.stdout)
16 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/resolve/mod.rs:
--------------------------------------------------------------------------------
1 | //! This module contains the [`resolve`] function which is used
2 | //! to make the PyPI ecosystem compatible with the [`resolvo`] crate.
3 | //!
4 | //! To use this enable the `resolve` feature.
5 | //! Note that this module can also serve an example to integrate an alternate packaging system
6 | //! with [`resolvo`].
7 | //!
8 | //! See the `rip_bin` crate for an example of how to use the [`resolve`] function in the: [RIP Repo](https://github.com/prefix-dev/rip)
9 | //!
10 |
11 | mod dependency_provider;
12 | mod pypi_version_types;
13 | mod solve;
14 | pub mod solve_options;
15 | mod solve_types;
16 |
17 | pub use pypi_version_types::PypiVersion;
18 | pub use pypi_version_types::PypiVersionSet;
19 | pub use solve::{resolve, PinnedPackage};
20 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/resolve/solve_types.rs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/types/artifact.rs:
--------------------------------------------------------------------------------
1 | use super::artifact_name::InnerAsArtifactName;
2 | use crate::resolve::PypiVersion;
3 | use crate::types::SourceArtifactName;
4 | use crate::utils::ReadAndSeek;
5 | use std::path::Path;
6 |
7 | /// Trait to implement if it is a type that has an [`super::artifact_name::ArtifactName`]
8 | /// this is then used by the [`crate::index::PackageDb`] to make a difference
9 | /// between the different types of artifacts.
10 | pub trait HasArtifactName {
11 | /// The name of the artifact which describes the artifact.
12 | ///
13 | /// Artifacts are describes by a string. [`super::artifact_name::ArtifactName`] describes the
14 | /// general format.
15 | type Name: Clone + InnerAsArtifactName;
16 |
17 | /// Returns the name of this instance
18 | fn name(&self) -> &Self::Name;
19 | }
20 |
21 | /// Trait that represents an artifact type in the PyPI ecosystem.
22 | /// That is a single file like a wheel, sdist.
23 | pub trait ArtifactFromBytes: HasArtifactName + Sized {
24 | /// Construct a new artifact from the given bytes
25 | fn from_bytes(name: Self::Name, bytes: Box) -> miette::Result;
26 | }
27 |
28 | /// Error while reading pyproject.toml
29 | #[derive(thiserror::Error, Debug)]
30 | #[allow(missing_docs)]
31 | pub enum ReadPyProjectError {
32 | #[error("IO error while reading pyproject.toml: {0}")]
33 | Io(#[from] std::io::Error),
34 |
35 | #[error("No pyproject.toml found in archive")]
36 | NoPyProjectTomlFound,
37 |
38 | #[error("Could not parse pyproject.toml")]
39 | PyProjectTomlParseError(String),
40 | }
41 |
42 | /// SDist or STree act as a SourceArtifact
43 | /// so we can use it in methods where we expect sdist
44 | /// to extract metadata
45 | pub trait ArtifactFromSource: HasArtifactName + Sync {
46 | /// get bytes of an artifact
47 | /// that will we be used for hashing
48 | fn try_get_bytes(&self) -> Result, std::io::Error>;
49 |
50 | /// Distribution Name
51 | fn distribution_name(&self) -> String;
52 |
53 | /// Version ( URL or Version )
54 | fn version(&self) -> PypiVersion;
55 |
56 | /// Source artifact name
57 | fn artifact_name(&self) -> SourceArtifactName;
58 |
59 | /// Read the build system info from the pyproject.toml
60 | fn read_pyproject_toml(&self) -> Result;
61 |
62 | /// extract to a specific location
63 | /// for sdist we unpack it
64 | /// for stree we move it
65 | /// as example this method is used by install_build_files
66 | fn extract_to(&self, work_dir: &Path) -> std::io::Result<()>;
67 | }
68 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/types/direct_url_json.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 | use url::Url;
3 |
4 | /// Specifies the PyPa `direct_url.json` format.
5 | /// See:
6 | ///
7 | #[derive(Debug, Serialize, Deserialize)]
8 | #[serde_with::skip_serializing_none]
9 | pub struct DirectUrlJson {
10 | /// Url to the source.
11 | pub url: Url,
12 | /// Information about the source.
13 | #[serde(flatten)]
14 | pub source: DirectUrlSource,
15 | }
16 |
17 | /// Specifies the source of a direct url.
18 | ///
19 | /// currently we do not support the deprecated `hash` field
20 | #[derive(Debug, Serialize, Deserialize)]
21 | pub enum DirectUrlSource {
22 | #[serde(rename = "archive_info")]
23 | /// Information about the archive file.
24 | Archive {
25 | /// Hashes of the archive file.
26 | hashes: Option,
27 | },
28 | /// Information about a source from a VCS directly
29 | #[serde(rename = "vcs_info")]
30 | Vcs {
31 | /// The VCS used
32 | vcs: DirectUrlVcs,
33 | /// Revision of the source
34 | requested_revision: Option,
35 | /// Actual commit
36 | commit_id: String,
37 | },
38 | /// Information about a local directory source
39 | #[serde(rename = "dir_info")]
40 | Dir {
41 | /// Is this a editable source
42 | /// See:
43 | editable: Option,
44 | },
45 | }
46 |
47 | /// Hashes for internal archive files.
48 | /// multiple hashes can be included but per recommendation only sha256 should be used.
49 | #[derive(Debug, Serialize, Deserialize)]
50 | pub struct DirectUrlHashes {
51 | /// Sha256 hash of the archive file.
52 | pub sha256: String,
53 | }
54 |
55 | /// Name of the VCS in a DirectUrlSource
56 | #[derive(Debug, Serialize, Deserialize)]
57 | #[allow(missing_docs)]
58 | pub enum DirectUrlVcs {
59 | #[serde(rename = "git")]
60 | Git,
61 | #[serde(rename = "svn")]
62 | Svn,
63 | #[serde(rename = "bzr")]
64 | Bazaar,
65 | #[serde(rename = "hg")]
66 | Mercurial,
67 | }
68 |
69 | #[cfg(test)]
70 | mod tests {
71 | use crate::types::direct_url_json::DirectUrlJson;
72 |
73 | /// Tests if json outputs aligns with the examples at:
74 | /// https://packaging.python.org/en/latest/specifications/direct-url-data-structure/
75 | /// try to parse the example cases from there
76 | #[test]
77 | pub fn test_examples_pypa() {
78 | // Source archive:
79 | let example = r#"
80 | {
81 | "url": "https://github.com/pypa/pip/archive/1.3.1.zip",
82 | "archive_info": {
83 | "hashes": {
84 | "sha256": "2dc6b5a470a1bde68946f263f1af1515a2574a150a30d6ce02c6ff742fcc0db8"
85 | }
86 | }
87 | }
88 | "#;
89 | serde_json::from_str::(example).unwrap();
90 |
91 | // Git URL with tag and commit-hash:
92 | let example = r#"
93 | {
94 | "url": "https://github.com/pypa/pip.git",
95 | "vcs_info": {
96 | "vcs": "git",
97 | "requested_revision": "1.3.1",
98 | "commit_id": "7921be1537eac1e97bc40179a57f0349c2aee67d"
99 | }
100 | }
101 | "#;
102 | serde_json::from_str::(example).unwrap();
103 |
104 | // Local directory:
105 | let example = r#"
106 | {
107 | "url": "file:///home/user/project",
108 | "dir_info": {}
109 | }
110 | "#;
111 | serde_json::from_str::(example).unwrap();
112 |
113 | // Local directory in editable mode:
114 | let example = r#"
115 | {
116 | "url": "file:///home/user/project",
117 | "dir_info": {
118 | "editable": true
119 | }
120 | }
121 | "#;
122 | serde_json::from_str::(example).unwrap();
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/types/extra.rs:
--------------------------------------------------------------------------------
1 | // Implementation comes from https://github.com/njsmith/posy/blob/main/src/vocab/extra.rs
2 | // Licensed under MIT or Apache-2.0
3 |
4 | // 'Extra' string format is not well specified. It looks like what poetry does is simply normalize
5 | // the name and be done with it.
6 | //
7 | // PEP 508's grammar for requirement specifiers says that extras have to
8 | // be "identifiers", which means: first char [A-Za-z0-9], remaining chars also
9 | // allowed to include '-_.'. But in practice we've seen Extras like "ssl:sys_platform=='win32'"
10 | // which do not follow that rule at all.
11 |
12 | // ORIGINAL comment from Posy.
13 |
14 | // 'Extra' string format is not well specified. It looks like what pip does is
15 | // run things through pkg_resources.safe_extra, which does:
16 | //
17 | // re.sub('[^A-Za-z0-9.-]+', '_', extra).lower()
18 | //
19 | // So A-Z becomes a-z, a-z 0-9 . - are preserved, and any contiguous run of
20 | // other characters becomes a single _.
21 | //
22 | // OTOH, PEP 508's grammar for requirement specifiers says that extras have to
23 | // be "identifiers", which means: first char [A-Za-z0-9], remaining chars also
24 | // allowed to include -_.
25 | //
26 | // I guess for now I'll just pretend that they act the same as package names,
27 | // and see how long I can get away with it.
28 | //
29 | // There's probably a better way to factor this and reduce code duplication...
30 |
31 | use miette::Diagnostic;
32 | use serde::{Serialize, Serializer};
33 | use serde_with::DeserializeFromStr;
34 | use std::borrow::Borrow;
35 | use std::cmp::Ordering;
36 | use std::hash::{Hash, Hasher};
37 | use std::str::FromStr;
38 | use thiserror::Error;
39 |
40 | #[derive(Debug, Clone, Eq, DeserializeFromStr)]
41 | /// Structure that holds both the source string and the normalized version of an extra.
42 | pub struct Extra {
43 | /// The original string this instance was created from
44 | source: Box,
45 |
46 | /// The normalized version of `source`.
47 | normalized: Box,
48 | }
49 |
50 | impl Extra {
51 | /// Returns the source representation of the name. This is the string from which this
52 | /// instance was created.
53 | pub fn as_source_str(&self) -> &str {
54 | self.source.as_ref()
55 | }
56 |
57 | /// Returns the normalized version of the name. The normalized string is guaranteed to
58 | /// be a valid python package name.
59 | pub fn as_str(&self) -> &str {
60 | self.normalized.as_ref()
61 | }
62 | }
63 |
64 | #[derive(Debug, Clone, Error, Diagnostic)]
65 | pub enum ParseExtraError {}
66 |
67 | impl FromStr for Extra {
68 | type Err = ParseExtraError;
69 |
70 | fn from_str(s: &str) -> Result {
71 | // https://www.python.org/dev/peps/pep-0503/#normalized-names
72 | let mut normalized = s.replace(['-', '_', '.'], "-");
73 | normalized.make_ascii_lowercase();
74 |
75 | Ok(Self {
76 | source: s.to_owned().into_boxed_str(),
77 | normalized: normalized.into_boxed_str(),
78 | })
79 | }
80 | }
81 |
82 | impl Hash for Extra {
83 | fn hash(&self, state: &mut H) {
84 | self.normalized.hash(state)
85 | }
86 | }
87 |
88 | impl PartialEq for Extra {
89 | fn eq(&self, other: &Self) -> bool {
90 | self.normalized.eq(&other.normalized)
91 | }
92 | }
93 |
94 | impl PartialOrd for Extra {
95 | fn partial_cmp(&self, other: &Self) -> Option {
96 | Some(self.cmp(other))
97 | }
98 | }
99 |
100 | impl Ord for Extra {
101 | fn cmp(&self, other: &Self) -> Ordering {
102 | self.normalized.cmp(&other.normalized)
103 | }
104 | }
105 |
106 | impl Serialize for Extra {
107 | fn serialize
(&self, serializer: S) -> Result
108 | where
109 | S: Serializer,
110 | {
111 | self.source.as_ref().serialize(serializer)
112 | }
113 | }
114 |
115 | impl Borrow for Extra {
116 | fn borrow(&self) -> &str {
117 | self.normalized.as_ref()
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/types/mod.rs:
--------------------------------------------------------------------------------
1 | //! This module contains all the types for working with PyPA packaging repositories.
2 | //! We have tried to follow the PEP's and PyPA packaging guide as closely as possible.
3 | mod artifact;
4 |
5 | mod artifact_name;
6 |
7 | mod package_name;
8 |
9 | mod core_metadata;
10 |
11 | mod record;
12 |
13 | mod extra;
14 |
15 | mod entry_points;
16 |
17 | mod project_info;
18 |
19 | mod direct_url_json;
20 | mod rfc822ish;
21 |
22 | pub use artifact::{ArtifactFromBytes, ArtifactFromSource, HasArtifactName, ReadPyProjectError};
23 |
24 | pub use artifact_name::{
25 | ArtifactName, ArtifactType, BuildTag, InnerAsArtifactName, ParseArtifactNameError,
26 | SDistFilename, SDistFormat, STreeFilename, SourceArtifactName, WheelFilename,
27 | };
28 |
29 | pub use direct_url_json::{DirectUrlHashes, DirectUrlJson, DirectUrlSource, DirectUrlVcs};
30 |
31 | pub use core_metadata::{MetadataVersion, PackageInfo, WheelCoreMetaDataError, WheelCoreMetadata};
32 |
33 | pub use record::{Record, RecordEntry};
34 |
35 | pub use package_name::{NormalizedPackageName, PackageName, ParsePackageNameError};
36 |
37 | pub use extra::Extra;
38 |
39 | pub use entry_points::{EntryPoint, ParseEntryPointError};
40 |
41 | pub use project_info::{ArtifactHashes, ArtifactInfo, DistInfoMetadata, Meta, ProjectInfo, Yanked};
42 |
43 | pub(crate) use rfc822ish::RFC822ish;
44 |
45 | pub use pep440_rs::*;
46 | pub use pep508_rs::*;
47 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/types/record.rs:
--------------------------------------------------------------------------------
1 | //! Defines the [`Record`] struct which holds the information stored in a `RECORD` file which is
2 | //! found in a wheel archive or installation.
3 |
4 | use itertools::Itertools;
5 | use serde::{Deserialize, Serialize};
6 | use std::io::Read;
7 | use std::path::Path;
8 |
9 | /// Represents the RECORD file found in a wheels .dist-info folder.
10 | ///
11 | /// See for more information about the format.
12 | #[derive(Debug, Clone)]
13 | pub struct Record {
14 | entries: Vec,
15 | }
16 |
17 | /// A single entry in a `RECORD` file
18 | #[derive(Debug, Deserialize, Serialize, PartialOrd, PartialEq, Ord, Eq, Clone)]
19 | pub struct RecordEntry {
20 | /// The path relative to the root of the environment or archive
21 | pub path: String,
22 |
23 | /// The hash of the file. Usually this is a Sha256 hash.
24 | pub hash: Option,
25 |
26 | /// The size of the file in bytes.
27 | pub size: Option,
28 | }
29 |
30 | impl Record {
31 | /// Reads the contents of a `RECORD` file from disk.
32 | pub fn from_path(path: &Path) -> csv::Result {
33 | Self::from_reader(fs_err::File::open(path)?)
34 | }
35 |
36 | /// Reads the contents of a `RECORD` file from a reader.
37 | pub fn from_reader(reader: impl Read) -> csv::Result {
38 | Ok(Self {
39 | entries: csv::ReaderBuilder::new()
40 | .has_headers(false)
41 | .escape(Some(b'"'))
42 | .from_reader(reader)
43 | .deserialize()
44 | .collect::, csv::Error>>()?,
45 | })
46 | }
47 |
48 | /// Write to a `RECORD` file on disk
49 | pub fn write_to_path(&self, path: &Path) -> csv::Result<()> {
50 | let mut record_writer = csv::WriterBuilder::new()
51 | .has_headers(false)
52 | .escape(b'"')
53 | .from_path(path)?;
54 | for entry in self.entries.iter().sorted() {
55 | record_writer.serialize(entry)?;
56 | }
57 | Ok(())
58 | }
59 |
60 | /// Returns an iterator over the entries in this instance.
61 | pub fn iter(&self) -> std::slice::Iter {
62 | self.entries.iter()
63 | }
64 | }
65 |
66 | impl IntoIterator for Record {
67 | type Item = RecordEntry;
68 | type IntoIter = std::vec::IntoIter;
69 |
70 | fn into_iter(self) -> Self::IntoIter {
71 | self.entries.into_iter()
72 | }
73 | }
74 |
75 | impl FromIterator for Record {
76 | fn from_iter>(iter: T) -> Self {
77 | Self {
78 | entries: FromIterator::from_iter(iter),
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/crates/rattler_installs_packages/src/types/rfc822ish.rs:
--------------------------------------------------------------------------------
1 | // Implementation comes from https://github.com/njsmith/posy/blob/main/src/vocab/rfc822ish.rs
2 | // Licensed under MIT or Apache-2.0
3 | use std::collections::HashMap;
4 | use std::str::FromStr;
5 |
6 | pub type Fields = HashMap>;
7 |
8 | #[cfg_attr(test, derive(Debug, serde::Deserialize, PartialEq, Eq))]
9 | pub struct RFC822ish {
10 | pub fields: Fields,
11 | pub body: Option,
12 | }
13 |
14 | impl RFC822ish {
15 | pub fn take_all(&mut self, key: &str) -> Vec {
16 | match self.fields.remove(&key.to_ascii_lowercase()) {
17 | Some(vec) => vec,
18 | None => Vec::new(),
19 | }
20 | }
21 |
22 | pub fn maybe_take(&mut self, key: &str) -> miette::Result