diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ab7c601be..18bdd3a57 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -446,3 +446,9 @@ jobs: needs: export-filter if: needs.export-filter.outputs.exports == 'true' uses: ./.github/workflows/test_exports.yml + + test_common_wheels: + name: "Test installation of common wheels" + needs: + - build + uses: ./.github/workflows/test_common_wheels.yml diff --git a/.github/workflows/test_common_wheels.yml b/.github/workflows/test_common_wheels.yml new file mode 100644 index 000000000..997bc0da3 --- /dev/null +++ b/.github/workflows/test_common_wheels.yml @@ -0,0 +1,77 @@ +name: "Test common wheel files for installation with pixi" + +on: + workflow_call: + +jobs: + test_common_wheels: + name: ${{ matrix.arch.name }} - Test Installation of Common Wheels + runs-on: ${{ matrix.arch.os }} + env: + TARGET_RELEASE: "${{ github.workspace }}/.pixi/target/release" + LOGS_DIR: "${{ github.workspace }}/tests/wheel_tests/.logs" + SUMMARY_FILE: "${{ github.workspace }}/tests/wheel_tests/.summary.md" + PYTHONIOENCODING: utf-8 + strategy: + fail-fast: false + matrix: + arch: + # Linux + - { + target: x86_64-unknown-linux-musl, + os: 8core_ubuntu_latest_runner, + name: "Linux", + } + # MacOS + - { target: x86_64-apple-darwin, os: macos-13, name: "MacOS-x86" } + - { target: aarch64-apple-darwin, os: macos-14, name: "MacOS-Arm" } # macOS-14 is the ARM chipset + # Windows + - { + target: x86_64-pc-windows-msvc, + os: windows-latest, + extension: .exe, + name: "Windows", + } + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Download binary from build + uses: actions/download-artifact@v4 + with: + name: pixi-${{ matrix.arch.target }}${{ matrix.arch.extension }} + path: pixi_bin + - name: Debug + run: | + pwd + - name: Create Directory and Move Executable to TARGET_RELEASE + if: matrix.arch.name != 'Windows' + run: | + mkdir -p ${{ env.TARGET_RELEASE }} + mv pixi_bin/pixi-${{ matrix.arch.target }} ${{ env.TARGET_RELEASE }}/pixi + chmod a+x ${{ env.TARGET_RELEASE }}/pixi + - name: Create Directory and Move Executable to TARGET_RELEASE + if: matrix.arch.name == 'Windows' && matrix.arch.target == 'x86_64-pc-windows-msvc' + run: | + New-Item -ItemType Directory -Force -Path "${{ env.TARGET_RELEASE }}" + Move-Item -Path "pixi_bin/pixi-${{ matrix.arch.target }}${{ matrix.arch.extension }}" -Destination "${{ env.TARGET_RELEASE }}/pixi.exe" + shell: pwsh + - name: Test common wheels + run: ${{ env.TARGET_RELEASE }}/pixi${{ matrix.arch.extension }} run test-common-wheels-ci + - name: Write .summary.md to Github Summary + if: ${{ matrix.arch.name != 'Windows' && always() }} + shell: bash + run: | + cat ${{ env.SUMMARY_FILE }} >> $GITHUB_STEP_SUMMARY + - name: Write .summary.md to GitHub Summary (Windows) + if: ${{ matrix.arch.name == 'Windows' && always() }} + shell: pwsh + run: | + $resolvedPath = Resolve-Path $env:SUMMARY_FILE + Get-Content $resolvedPath | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY + - name: Upload Logs + uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: wheel-tests-logs-${{ matrix.arch.name }} + include-hidden-files: true + path: ${{ env.LOGS_DIR }} diff --git a/crates/pypi_modifiers/src/pypi_tags.rs b/crates/pypi_modifiers/src/pypi_tags.rs index 5df553b72..de11097ee 100644 --- a/crates/pypi_modifiers/src/pypi_tags.rs +++ b/crates/pypi_modifiers/src/pypi_tags.rs @@ -16,129 +16,161 @@ pub fn package_name_is_python(record: &rattler_conda_types::PackageName) -> bool record.as_normalized() == "python" } +/// Get the python version and implementation name for the specified platform. pub fn get_pypi_tags( platform: Platform, system_requirements: &SystemRequirements, python_record: &PackageRecord, ) -> miette::Result { - let platform = if platform.is_linux() { - let arch = match platform.arch() { - None => unreachable!("every platform we support has an arch"), - Some(Arch::X86) => platform_tags::Arch::X86, - Some(Arch::X86_64) => platform_tags::Arch::X86_64, - Some(Arch::Aarch64 | Arch::Arm64) => platform_tags::Arch::Aarch64, - Some(Arch::ArmV7l) => platform_tags::Arch::Armv7L, - Some(Arch::Ppc64le) => platform_tags::Arch::Powerpc64Le, - Some(Arch::Ppc64) => platform_tags::Arch::Powerpc64, - Some(Arch::S390X) => platform_tags::Arch::S390X, - Some(unsupported_arch) => { - miette::bail!("unsupported arch for pypi packages '{unsupported_arch}'") - } - }; + let platform = get_platform_tags(platform, system_requirements)?; + let python_version = get_python_version(python_record)?; + let implementation_name = get_implementation_name(python_record)?; + create_tags(platform, python_version, implementation_name) +} - // Find the glibc version - match system_requirements - .libc - .as_ref() - .map(LibCSystemRequirement::family_and_version) - { - None => { - let (major, minor) = default_glibc_version() - .as_major_minor() - .expect("expected default glibc version to be a major.minor version"); - platform_tags::Platform::new( - Os::Manylinux { - major: major as _, - minor: minor as _, - }, - arch, - ) - } - Some(("glibc", version)) => { - let Some((major, minor)) = version.as_major_minor() else { - miette::bail!( - "expected glibc version to be a major.minor version, but got '{version}'" - ) - }; - platform_tags::Platform::new( - Os::Manylinux { - major: major as _, - minor: minor as _, - }, - arch, - ) - } - Some((family, _)) => { - miette::bail!("unsupported libc family for pypi packages '{family}'"); - } - } +/// Create a uv platform tag for the specified platform +fn get_platform_tags( + platform: Platform, + system_requirements: &SystemRequirements, +) -> miette::Result { + if platform.is_linux() { + get_linux_platform_tags(platform, system_requirements) } else if platform.is_windows() { - let arch = match platform.arch() { - None => unreachable!("every platform we support has an arch"), - Some(Arch::X86) => platform_tags::Arch::X86, - Some(Arch::X86_64) => platform_tags::Arch::X86_64, - Some(Arch::Aarch64 | Arch::Arm64) => platform_tags::Arch::Aarch64, - Some(unsupported_arch) => { - miette::bail!("unsupported arch for pypi packages '{unsupported_arch}'") - } - }; - - platform_tags::Platform::new(Os::Windows, arch) + get_windows_platform_tags(platform) } else if platform.is_osx() { - let osx_version = system_requirements - .macos - .clone() - .unwrap_or_else(|| default_mac_os_version(platform)); - let Some((major, minor)) = osx_version.as_major_minor() else { - miette::bail!( - "expected macos version to be a major.minor version, but got '{osx_version}'" - ) - }; - - let arch = match platform.arch() { - None => unreachable!("every platform we support has an arch"), - Some(Arch::X86) => platform_tags::Arch::X86, - Some(Arch::X86_64) => platform_tags::Arch::X86_64, - Some(Arch::Aarch64 | Arch::Arm64) => platform_tags::Arch::Aarch64, - Some(unsupported_arch) => { - miette::bail!("unsupported arch for pypi packages '{unsupported_arch}'") - } - }; - - platform_tags::Platform::new( - Os::Macos { - major: major as _, - minor: minor as _, - }, - arch, - ) + get_macos_platform_tags(platform, system_requirements) } else { miette::bail!("unsupported platform for pypi packages {platform}") + } +} + +/// Get linux specific platform tags +fn get_linux_platform_tags( + platform: Platform, + system_requirements: &SystemRequirements, +) -> miette::Result { + let arch = get_arch_tags(platform)?; + + // Find the glibc version + match system_requirements + .libc + .as_ref() + .map(LibCSystemRequirement::family_and_version) + { + None => { + let (major, minor) = default_glibc_version() + .as_major_minor() + .expect("expected default glibc version to be a major.minor version"); + Ok(platform_tags::Platform::new( + Os::Manylinux { + major: major as _, + minor: minor as _, + }, + arch, + )) + } + Some(("glibc", version)) => { + let Some((major, minor)) = version.as_major_minor() else { + miette::bail!( + "expected glibc version to be a major.minor version, but got '{version}'" + ) + }; + Ok(platform_tags::Platform::new( + Os::Manylinux { + major: major as _, + minor: minor as _, + }, + arch, + )) + } + Some((family, _)) => { + miette::bail!("unsupported libc family for pypi packages '{family}'"); + } + } +} + +/// Get windows specific platform tags +fn get_windows_platform_tags(platform: Platform) -> miette::Result { + let arch = get_arch_tags(platform)?; + Ok(platform_tags::Platform::new(Os::Windows, arch)) +} + +/// Get macos specific platform tags +fn get_macos_platform_tags( + platform: Platform, + system_requirements: &SystemRequirements, +) -> miette::Result { + let osx_version = system_requirements + .macos + .clone() + .unwrap_or_else(|| default_mac_os_version(platform)); + let Some((major, minor)) = osx_version.as_major_minor() else { + miette::bail!("expected macos version to be a major.minor version, but got '{osx_version}'") }; - // Build the wheel tags based on the interpreter, the target platform, and the python version. + let arch = get_arch_tags(platform)?; + + Ok(platform_tags::Platform::new( + Os::Macos { + major: major as _, + minor: minor as _, + }, + arch, + )) +} + +/// Get the arch tag for the specified platform +fn get_arch_tags(platform: Platform) -> miette::Result { + match platform.arch() { + None => unreachable!("every platform we support has an arch"), + Some(Arch::X86) => Ok(platform_tags::Arch::X86), + Some(Arch::X86_64) => Ok(platform_tags::Arch::X86_64), + Some(Arch::Aarch64 | Arch::Arm64) => Ok(platform_tags::Arch::Aarch64), + Some(Arch::ArmV7l) => Ok(platform_tags::Arch::Armv7L), + Some(Arch::Ppc64le) => Ok(platform_tags::Arch::Powerpc64Le), + Some(Arch::Ppc64) => Ok(platform_tags::Arch::Powerpc64), + Some(Arch::S390X) => Ok(platform_tags::Arch::S390X), + Some(unsupported_arch) => { + miette::bail!("unsupported arch for pypi packages '{unsupported_arch}'") + } + } +} + +fn get_python_version(python_record: &PackageRecord) -> miette::Result<(u8, u8)> { let Some(python_version) = python_record.version.as_major_minor() else { miette::bail!( "expected python version to be a major.minor version, but got '{}'", &python_record.version ); }; - let implementation_name = match python_record.name.as_normalized() { - "python" => "cpython", - "pypy" => "pypy", + Ok((python_version.0 as u8, python_version.1 as u8)) +} + +fn get_implementation_name(python_record: &PackageRecord) -> miette::Result<&'static str> { + match python_record.name.as_normalized() { + "python" => Ok("cpython"), + "pypy" => Ok("pypy"), _ => { miette::bail!( "unsupported python implementation '{}'", python_record.name.as_source() ); } - }; + } +} + +fn create_tags( + platform: platform_tags::Platform, + python_version: (u8, u8), + implementation_name: &str, +) -> miette::Result { + // Build the wheel tags based on the interpreter, the target platform, and the python version. let tags = Tags::from_env( &platform, - (python_version.0 as u8, python_version.1 as u8), + python_version, implementation_name, // TODO: This might not be entirely correct.. - (python_version.0 as u8, python_version.1 as u8), + python_version, true, // Should revisit this when this lands: https://github.com/conda-forge/python-feedstock/pull/679 false, diff --git a/pixi.lock b/pixi.lock index 2e2f9b202..dbdaae103 100644 --- a/pixi.lock +++ b/pixi.lock @@ -33,6 +33,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/docopt-0.6.2-py_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/fortran-compiler-1.7.0-heb67821_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc-12.4.0-h236703b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.4.0-hb2e57f8_1.conda @@ -83,6 +84,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.7-he7c6b58_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-h4ab18f5_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/make-4.4.1-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/mimalloc-2.1.7-hac33072_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/mold-2.33.0-h3b4bb38_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.11.2-py312h66e93f0_0.conda @@ -98,10 +101,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.6.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.16.3-py312h4b3b743_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pykwalify-1.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyrsistent-0.20.0-py312h98912ed_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-rerunfailures-14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.3-hab00c5b_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda @@ -109,6 +114,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h41a817b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml-0.18.6-py312h98912ed_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml.clib-0.2.8-py312h98912ed_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rust-1.80.1-h0a17960_0.conda @@ -122,6 +128,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tbump-6.9.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-w-1.0.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.2-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda @@ -151,6 +158,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/docopt-0.6.2-py_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/git-2.46.0-pl5321h9b6aa9b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 @@ -172,6 +180,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.46.0-h1b8f9f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.0-hd019ec5_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-h87427d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.11.2-py312hb553811_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-hf036a51_1.conda @@ -185,10 +195,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.6.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/pydantic-core-2.16.3-py312h1b0e595_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pykwalify-1.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/pyrsistent-0.20.0-py312h41838bb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-rerunfailures-14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.3-h1411813_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda @@ -196,6 +208,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.2-py312hbd25219_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ruamel.yaml-0.18.6-py312h41838bb_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ruamel.yaml.clib-0.2.8-py312h41838bb_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/rust-1.80.1-h6c54e5d_0.conda @@ -207,6 +220,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tbump-6.9.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-w-1.0.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.2-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda @@ -236,6 +250,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/docopt-0.6.2-py_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/git-2.46.0-pl5321h41514c7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 @@ -258,6 +273,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.46.0-hfb93653_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.0-h7a5bd25_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-hfb2fe0b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.11.2-py312h024a12e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h7bae524_1.conda @@ -271,10 +288,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.6.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.16.3-py312h5280bc4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pykwalify-1.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyrsistent-0.20.0-py312he37b823_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-rerunfailures-14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.3-h4a7b5fc_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda @@ -282,6 +301,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py312h7e5086c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml-0.18.6-py312he37b823_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml.clib-0.2.8-py312he37b823_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rust-1.80.1-h4ff7c5d_0.conda @@ -293,6 +313,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tbump-6.9.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-w-1.0.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.2-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda @@ -321,6 +342,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/docopt-0.6.2-py_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/git-2.46.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 @@ -336,6 +358,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.46.0-h2466b09_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.11.2-py312h4389bb4_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.3.1-h2466b09_3.conda @@ -347,16 +371,19 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.6.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.16.3-py312hfccd98a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pykwalify-1.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyrsistent-0.20.0-py312he70551f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh0701188_6.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-rerunfailures-14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.3-h2628c8c_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.12-5_cp312.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py312h4389bb4_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml-0.18.6-py312he70551f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml.clib-0.2.8-py312he70551f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/rust-1.80.1-hf8d6059_0.conda @@ -368,6 +395,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tbump-6.9.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-w-1.0.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.2-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda @@ -854,7 +882,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.0-py312h06ac9bb_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.3.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.8-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.15.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-hf3520f5_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda @@ -899,7 +927,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/cffi-1.17.0-py312hf857d28_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.3.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.8-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.15.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libcxx-18.1.8-hd876a4e_6.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.6.2-h73e2aa4_0.conda @@ -936,7 +964,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.0-py312h0fad829_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.3.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.8-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.15.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-18.1.8-h3ed4263_6.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.2-hebf3989_0.conda @@ -973,7 +1001,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.0-py312h4389bb4_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.3.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.8-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.15.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.6.2-h63175ca_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.2-h8ffe710_5.tar.bz2 @@ -1144,6 +1172,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.4.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-3.2.0-pyhd8ed1ab_3.tar.bz2 @@ -1158,6 +1187,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-h4ab18f5_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.11.2-py312h66e93f0_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-he02047a_1.conda @@ -1167,17 +1198,21 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-6.0.0-py312h66e93f0_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.6.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.16.3-py312h4b3b743_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyrsistent-0.20.0-py312h98912ed_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-rerunfailures-14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.3-hab00c5b_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-5_cp312.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h41a817b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-72.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-w-1.0.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda @@ -1192,6 +1227,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.4.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-3.2.0-pyhd8ed1ab_3.tar.bz2 @@ -1199,6 +1235,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.46.0-h1b8f9f3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-h87427d6_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/mypy-1.11.2-py312hb553811_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-hf036a51_1.conda @@ -1208,17 +1246,21 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/psutil-6.0.0-py312hb553811_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.6.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/pydantic-core-2.16.3-py312h1b0e595_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/pyrsistent-0.20.0-py312h41838bb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-rerunfailures-14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.3-h1411813_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-5_cp312.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/pyyaml-6.0.2-py312hbd25219_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-72.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-w-1.0.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda @@ -1233,6 +1275,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.4.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-3.2.0-pyhd8ed1ab_3.tar.bz2 @@ -1240,6 +1283,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.46.0-hfb93653_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-hfb2fe0b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.11.2-py312h024a12e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h7bae524_1.conda @@ -1249,17 +1294,21 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-6.0.0-py312h024a12e_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.6.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.16.3-py312h5280bc4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyrsistent-0.20.0-py312he37b823_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-rerunfailures-14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.3-h4a7b5fc_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-5_cp312.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py312h7e5086c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-72.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-w-1.0.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda @@ -1274,6 +1323,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.4.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-3.2.0-pyhd8ed1ab_3.tar.bz2 @@ -1281,6 +1331,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.2-h8ffe710_5.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.46.0-h2466b09_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.11.2-py312h4389bb4_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.3.1-h2466b09_3.conda @@ -1289,16 +1341,20 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-6.0.0-py312h4389bb4_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.6.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.16.3-py312hfccd98a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyrsistent-0.20.0-py312he70551f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-rerunfailures-14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.6.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.3-h2628c8c_0_cpython.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.12-5_cp312.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-72.2.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-w-1.0.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h8827d51_1.conda @@ -2299,18 +2355,18 @@ packages: timestamp: 1710362455984 - kind: conda name: filelock - version: 3.15.4 + version: 3.16.0 build: pyhd8ed1ab_0 subdir: noarch noarch: python - url: https://conda.anaconda.org/conda-forge/noarch/filelock-3.15.4-pyhd8ed1ab_0.conda - sha256: f78d9c0be189a77cb0c67d02f33005f71b89037a85531996583fb79ff3fe1a0a - md5: 0e7e4388e9d5283e22b35a9443bdbcc9 + url: https://conda.anaconda.org/conda-forge/noarch/filelock-3.16.0-pyhd8ed1ab_0.conda + sha256: f55c9af3d92a363fa9e4f164038db85a028befb65d56df0b2cb34911eba8a37a + md5: ec288789b07ae3be555046e099798a56 depends: - python >=3.7 license: Unlicense - size: 17592 - timestamp: 1719088395353 + size: 17402 + timestamp: 1725740654220 - kind: conda name: font-ttf-dejavu-sans-mono version: '2.37' @@ -5060,6 +5116,22 @@ packages: license_family: BSD size: 78331 timestamp: 1710435316163 +- kind: conda + name: markdown-it-py + version: 3.0.0 + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda + sha256: c041b0eaf7a6af3344d5dd452815cdc148d6284fec25a4fa3f4263b3a021e962 + md5: 93a8e71256479c62074356ef6ebf501b + depends: + - mdurl >=0.1,<1 + - python >=3.8 + license: MIT + license_family: MIT + size: 64356 + timestamp: 1686175179621 - kind: conda name: markupsafe version: 2.1.5 @@ -5140,6 +5212,21 @@ packages: license_family: BSD size: 25414 timestamp: 1724959688117 +- kind: conda + name: mdurl + version: 0.1.2 + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda + sha256: 64073dfb6bb429d52fff30891877b48c7ec0f89625b1bf844905b66a81cce6e1 + md5: 776a8dd9e824f77abac30e6ef43a8f7a + depends: + - python >=3.6 + license: MIT + license_family: MIT + size: 14680 + timestamp: 1704317789138 - kind: conda name: mdx_truly_sane_lists version: '1.3' @@ -6586,6 +6673,25 @@ packages: license_family: MIT size: 257671 timestamp: 1721923749407 +- kind: conda + name: pytest-rerunfailures + version: '14.0' + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/pytest-rerunfailures-14.0-pyhd8ed1ab_0.conda + sha256: 08fb77f313c5739a3526a07c865671f6e36d1458d073c1b23b4f0b66501138bd + md5: 0696324a8c882d182485f20368d21583 + depends: + - importlib-metadata >=1 + - packaging >=17.1 + - pytest >=7.2 + - python >=3.8 + - setuptools + license: MPL-2.0 + license_family: OTHER + size: 17835 + timestamp: 1710357496750 - kind: conda name: pytest-xdist version: 3.6.1 @@ -7161,6 +7267,24 @@ packages: license_family: APACHE size: 58810 timestamp: 1717057174842 +- kind: conda + name: rich + version: 13.7.1 + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda + sha256: 2b26d58aa59e46f933c3126367348651b0dab6e0bf88014e857415bb184a4667 + md5: ba445bf767ae6f0d959ff2b40c20912b + depends: + - markdown-it-py >=2.2.0 + - pygments >=2.13.0,<3.0.0 + - python >=3.7.0 + - typing_extensions >=4.0.0,<5.0.0 + license: MIT + license_family: MIT + size: 184347 + timestamp: 1709150578093 - kind: conda name: ruamel.yaml version: 0.18.6 @@ -7778,6 +7902,21 @@ packages: license_family: MIT size: 15940 timestamp: 1644342331069 +- kind: conda + name: tomli-w + version: 1.0.0 + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/tomli-w-1.0.0-pyhd8ed1ab_0.tar.bz2 + sha256: efb5f78a224c4bb14aab04690c9912256ea12c3a8b8413e60167573ce1282b02 + md5: 73506d1ab4202481841c68c169b7ef6c + depends: + - python >=3.7 + license: MIT + license_family: MIT + size: 10052 + timestamp: 1638551820635 - kind: conda name: tomlkit version: 0.13.2 diff --git a/pixi.toml b/pixi.toml index 7e2a7fdb2..fc3c45ba0 100644 --- a/pixi.toml +++ b/pixi.toml @@ -12,7 +12,7 @@ channels = ["https://fast.prefix.dev/conda-forge"] platforms = ["linux-64", "win-64", "osx-64", "osx-arm64"] [dependencies] -python = ">=3.12.3,<4" +python = "3.12.*" [tasks] build = "cargo build --release" @@ -31,9 +31,18 @@ mypy = ">=1.11,<1.12" # For detecting cpu cores with pytest-xdist psutil = ">=6.0.0,<7" # For running tests in parallel, use this instead of regular pytest +filelock = ">=3.16.0,<4" +pytest = "*" +pytest-rerunfailures = ">=14.0,<15" pytest-xdist = ">=3.6.1,<4" +rich = ">=13.7.1,<14" +tomli-w = ">=1.0,<2" [feature.pytest.tasks] +test-common-wheels-ci = { cmd = "pytest -n logical tests/wheel_tests/" } +test-common-wheels-dev = { cmd = "pytest -n logical tests/wheel_tests/", depends-on = [ + "build", +] } test-integration-ci = "pytest -n logical tests/integration" test-integration-dev = { cmd = "pytest -n logical tests/integration", depends-on = [ "build", diff --git a/src/cli/tree.rs b/src/cli/tree.rs index 6c0abd231..141252215 100644 --- a/src/cli/tree.rs +++ b/src/cli/tree.rs @@ -1,17 +1,20 @@ use std::collections::HashMap; +use std::io::{StdoutLock, Write}; use ahash::{HashSet, HashSetExt}; use clap::Parser; use console::Color; use fancy_display::FancyDisplay; use itertools::Itertools; +use miette::{IntoDiagnostic, WrapErr}; use pixi_manifest::FeaturesExt; use rattler_conda_types::Platform; +use regex::Regex; use crate::{ cli::cli_config::{PrefixUpdateConfig, ProjectConfig}, lock_file::UpdateLockFileOptions, - Project, + project::{Environment, Project}, }; /// Show a tree of project dependencies @@ -56,7 +59,6 @@ struct Symbols { down: &'static str, tee: &'static str, ell: &'static str, - // right: &'static str, empty: &'static str, } @@ -64,20 +66,26 @@ static UTF8_SYMBOLS: Symbols = Symbols { down: "│ ", tee: "├──", ell: "└──", - // right: "───", empty: " ", }; pub async fn execute(args: Args) -> miette::Result<()> { - let project = Project::load_or_else_discover(args.project_config.manifest_path.as_deref())?; - let environment = project.environment_from_name_or_env_var(args.environment)?; + let project = Project::load_or_else_discover(args.project_config.manifest_path.as_deref()) + .wrap_err("Failed to load project")?; + + let environment = project + .environment_from_name_or_env_var(args.environment) + .wrap_err("Environment not found")?; + let lock_file = project .update_lock_file(UpdateLockFileOptions { lock_file_usage: args.prefix_update_config.lock_file_usage(), no_install: args.prefix_update_config.no_install, ..UpdateLockFileOptions::default() }) - .await?; + .await + .wrap_err("Failed to update lock file")?; + let platform = args.platform.unwrap_or_else(|| environment.best_platform()); let locked_deps = lock_file .lock_file @@ -93,10 +101,19 @@ pub async fn execute(args: Args) -> miette::Result<()> { eprintln!("Environment: {}", environment.name().fancy_display()); } + let stdout = std::io::stdout(); + let mut handle = stdout.lock(); if args.invert { - print_inverted_dependency_tree(&invert_dep_map(&dep_map), &direct_deps, &args.regex)?; + print_inverted_dependency_tree( + &mut handle, + &invert_dep_map(&dep_map), + &direct_deps, + &args.regex, + ) + .wrap_err("Couldn't print the inverted dependency tree")?; } else { - print_dependency_tree(&dep_map, &direct_deps, &args.regex)?; + print_dependency_tree(&mut handle, &dep_map, &direct_deps, &args.regex) + .wrap_err("Couldn't print the dependency tree")?; } Project::warn_on_discovered_from_env(args.project_config.manifest_path.as_deref()); Ok(()) @@ -104,39 +121,45 @@ pub async fn execute(args: Args) -> miette::Result<()> { /// Filter and print an inverted dependency tree fn print_inverted_dependency_tree( + handle: &mut StdoutLock, inverted_dep_map: &HashMap, direct_deps: &HashSet, regex: &Option, -) -> Result<(), miette::Error> { +) -> miette::Result<()> { let regex = regex .as_ref() - .ok_or("") - .map_err(|_| miette::miette!("The -i flag requires a package name."))?; - let regex = regex::Regex::new(regex).map_err(|_| miette::miette!("Invalid regex pattern"))?; + .ok_or_else(|| miette::miette!("The -i flag requires a package name."))?; + + let regex = Regex::new(regex) + .into_diagnostic() + .wrap_err("Invalid regular expression")?; - let mut root_pkg_names = inverted_dep_map.keys().collect_vec(); - root_pkg_names.retain(|p| regex.is_match(p)); + let root_pkg_names: Vec<_> = inverted_dep_map + .keys() + .filter(|p| regex.is_match(p)) + .collect(); if root_pkg_names.is_empty() { - Err(miette::miette!( - "Nothing depends on the given regular expression", - ))?; + return Err(miette::miette!( + "Nothing depends on the given regular expression" + )); } let mut visited_pkgs = HashSet::new(); for pkg_name in root_pkg_names { if let Some(pkg) = inverted_dep_map.get(pkg_name) { let visited = !visited_pkgs.insert(pkg_name.clone()); - print_package("\n", pkg, direct_deps.contains(&pkg.name), visited); + print_package(handle, "\n", pkg, direct_deps.contains(&pkg.name), visited)?; if !visited { print_inverted_leaf( + handle, pkg, String::from(""), inverted_dep_map, direct_deps, &mut visited_pkgs, - ); + )?; } } } @@ -146,12 +169,13 @@ fn print_inverted_dependency_tree( /// Recursively print inverted dependency tree leaf nodes fn print_inverted_leaf( + handle: &mut StdoutLock, pkg: &Package, prefix: String, inverted_dep_map: &HashMap, direct_deps: &HashSet, visited_pkgs: &mut HashSet, -) { +) -> miette::Result<()> { let needed_count = pkg.needed_by.len(); for (index, needed_name) in pkg.needed_by.iter().enumerate() { let last = index == needed_count - 1; @@ -164,84 +188,103 @@ fn print_inverted_leaf( if let Some(needed_pkg) = inverted_dep_map.get(needed_name) { let visited = !visited_pkgs.insert(needed_pkg.name.clone()); print_package( + handle, &format!("{prefix}{symbol} "), needed_pkg, direct_deps.contains(&needed_pkg.name), visited, - ); + )?; if !visited { - let new_prefix = if index == needed_count - 1 { + let new_prefix = if last { format!("{}{} ", prefix, UTF8_SYMBOLS.empty) } else { format!("{}{} ", prefix, UTF8_SYMBOLS.down) }; print_inverted_leaf( + handle, needed_pkg, new_prefix, inverted_dep_map, direct_deps, visited_pkgs, - ) + )?; } } } + Ok(()) } /// Print a transitive dependency tree fn print_transitive_dependency_tree( + handle: &mut StdoutLock, dep_map: &HashMap, direct_deps: &HashSet, filtered_keys: Vec, -) -> Result<(), miette::Error> { - let mut visited_pkgs = Vec::new(); +) -> miette::Result<()> { + let mut visited_pkgs = HashSet::new(); for pkg_name in filtered_keys.iter() { - visited_pkgs.push(pkg_name.clone()); + if !visited_pkgs.insert(pkg_name.clone()) { + continue; + } if let Some(pkg) = dep_map.get(pkg_name) { - print_package("\n", pkg, direct_deps.contains(&pkg.name), false); + print_package(handle, "\n", pkg, direct_deps.contains(&pkg.name), false)?; - print_dependency_leaf(pkg, "".to_string(), dep_map, &mut visited_pkgs, direct_deps) + print_dependency_leaf( + handle, + pkg, + "".to_string(), + dep_map, + &mut visited_pkgs, + direct_deps, + )?; } } Ok(()) } -/// Filter and print a top down dependency tree +/// Filter and print a top-down dependency tree fn print_dependency_tree( + handle: &mut StdoutLock, dep_map: &HashMap, direct_deps: &HashSet, regex: &Option, -) -> Result<(), miette::Error> { +) -> miette::Result<()> { let mut filtered_deps = direct_deps.clone(); if let Some(regex) = regex { - let regex = regex::Regex::new(regex).map_err(|_| miette::miette!("Invalid regex"))?; + let regex = Regex::new(regex) + .into_diagnostic() + .wrap_err("Invalid regular expression")?; + filtered_deps.retain(|p| regex.is_match(p)); if filtered_deps.is_empty() { - let mut filtered_keys = dep_map.keys().map(|p| p.to_owned()).collect_vec(); + let mut filtered_keys = dep_map.keys().cloned().collect_vec(); filtered_keys.retain(|p| regex.is_match(p)); if filtered_keys.is_empty() { - Err(miette::miette!( + return Err(miette::miette!( "No dependencies matched the given regular expression" - ))?; + )); } - tracing::info!("No top level dependencies matched the regular expression, showing matching transitive dependencies"); + tracing::info!("No top-level dependencies matched the regular expression, showing matching transitive dependencies"); - return print_transitive_dependency_tree(dep_map, direct_deps, filtered_keys); + return print_transitive_dependency_tree(handle, dep_map, direct_deps, filtered_keys); } } - let mut visited_pkgs = Vec::new(); + let mut visited_pkgs = HashSet::new(); let direct_dep_count = filtered_deps.len(); for (index, pkg_name) in filtered_deps.iter().enumerate() { - visited_pkgs.push(pkg_name.to_owned()); + if !visited_pkgs.insert(pkg_name.clone()) { + continue; + } let last = index == direct_dep_count - 1; let symbol = if last { @@ -251,11 +294,12 @@ fn print_dependency_tree( }; if let Some(pkg) = dep_map.get(pkg_name) { print_package( + handle, &format!("{symbol} "), pkg, direct_deps.contains(&pkg.name), false, - ); + )?; let prefix = if last { UTF8_SYMBOLS.empty @@ -263,25 +307,27 @@ fn print_dependency_tree( UTF8_SYMBOLS.down }; print_dependency_leaf( + handle, pkg, format!("{} ", prefix), dep_map, &mut visited_pkgs, direct_deps, - ) + )?; } } Ok(()) } -/// Recursively print top down dependency tree nodes +/// Recursively print top-down dependency tree nodes fn print_dependency_leaf( + handle: &mut StdoutLock, pkg: &Package, prefix: String, dep_map: &HashMap, - visited_pkgs: &mut Vec, + visited_pkgs: &mut HashSet, direct_deps: &HashSet, -) { +) -> miette::Result<()> { let dep_count = pkg.dependencies.len(); for (index, dep_name) in pkg.dependencies.iter().enumerate() { let last = index == dep_count - 1; @@ -292,15 +338,15 @@ fn print_dependency_leaf( }; if let Some(dep) = dep_map.get(dep_name) { - let visited = visited_pkgs.contains(&dep.name); - visited_pkgs.push(dep.name.to_owned()); + let visited = !visited_pkgs.insert(dep.name.clone()); print_package( + handle, &format!("{prefix}{symbol} "), dep, direct_deps.contains(&dep.name), visited, - ); + )?; if visited { continue; @@ -311,12 +357,12 @@ fn print_dependency_leaf( } else { format!("{}{} ", prefix, UTF8_SYMBOLS.down) }; - print_dependency_leaf(dep, new_prefix, dep_map, visited_pkgs, direct_deps); + print_dependency_leaf(handle, dep, new_prefix, dep_map, visited_pkgs, direct_deps)?; } else { - let visited = visited_pkgs.contains(dep_name); - visited_pkgs.push(dep_name.to_owned()); + let visited = !visited_pkgs.insert(dep_name.clone()); print_package( + handle, &format!("{prefix}{symbol} "), &Package { name: dep_name.to_owned(), @@ -327,17 +373,22 @@ fn print_dependency_leaf( }, false, visited, - ) + )?; } } + Ok(()) } -/// Print package and style by attributes, like if are a direct dependency (name -/// is green and bold), or by the source of the package (yellow version string -/// for Conda, blue for PyPI). Packages that have already been visited and will -/// not be recursed into again are marked with a star (*). -fn print_package(prefix: &str, package: &Package, direct: bool, visited: bool) { - println!( +/// Print package and style by attributes +fn print_package( + handle: &mut StdoutLock, + prefix: &str, + package: &Package, + direct: bool, + visited: bool, +) -> miette::Result<()> { + writeln!( + handle, "{}{} {} {}", prefix, if direct { @@ -350,12 +401,22 @@ fn print_package(prefix: &str, package: &Package, direct: bool, visited: bool) { PackageSource::Pypi => console::style(&package.version).fg(Color::Blue), }, if visited { "(*)" } else { "" } - ); + ) + .map_err(|e| { + if e.kind() == std::io::ErrorKind::BrokenPipe { + // Exit gracefully + std::process::exit(0); + } else { + e + } + }) + .into_diagnostic() + .wrap_err("Failed to write package information") } /// Extract the direct Conda and PyPI dependencies from the environment fn direct_dependencies( - environment: &crate::project::Environment<'_>, + environment: &Environment<'_>, platform: &Platform, dep_map: &HashMap, ) -> HashSet { @@ -403,71 +464,95 @@ struct Package { source: PackageSource, } -/// Builds a hashmap of dependencies, with names, versions, and what they depend -/// on -fn generate_dependency_map(locked_deps: &Vec) -> HashMap { - let mut package_dependencies_map = HashMap::new(); - - for package in locked_deps { - let version = package.version().into_owned(); +/// Simplified package information extracted from the lock file +struct PackageInfo { + name: String, + dependencies: Vec, + source: PackageSource, +} - if let Some(conda_package) = package.as_conda() { - let name = conda_package - .package_record() - .name - .as_normalized() - .to_string(); - // Parse the dependencies of the package - let dependencies: Vec = conda_package - .package_record() - .depends - .iter() - .map(|d| { - d.split_once(' ') - .map_or_else(|| d.to_string(), |(dep_name, _)| dep_name.to_string()) - }) - .collect(); +/// Helper function to extract package information +fn extract_package_info(package: &rattler_lock::Package) -> Option { + if let Some(conda_package) = package.as_conda() { + // Extract name + let name = conda_package + .package_record() + .name + .as_normalized() + .to_string(); + + // Extract dependencies + let dependencies: Vec = conda_package + .package_record() + .depends + .iter() + .map(|d| { + d.split_once(' ') + .map_or_else(|| d.to_string(), |(dep_name, _)| dep_name.to_string()) + }) + .collect(); - package_dependencies_map.insert( - name.clone(), - Package { - name: name.clone(), - version, - dependencies: dependencies.into_iter().unique().collect(), - needed_by: Vec::new(), - source: PackageSource::Conda, - }, - ); - } else if let Some(pypi_package) = package.as_pypi() { - let name = pypi_package - .data() - .package - .name - .as_dist_info_name() - .into_owned(); - - let mut dependencies = Vec::new(); - for p in pypi_package.data().package.requires_dist.iter() { - // If this is not true, it means that the marker does not hold for every environment - if !&p.marker.is_true() { + Some(PackageInfo { + name, + dependencies, + source: PackageSource::Conda, + }) + } else if let Some(pypi_package) = package.as_pypi() { + // Extract name + let name = pypi_package + .data() + .package + .name + .as_dist_info_name() + .into_owned(); + + // Extract dependencies + let dependencies = pypi_package + .data() + .package + .requires_dist + .iter() + .filter_map(|p| { + if p.marker.is_true() { + Some(p.name.as_dist_info_name().into_owned()) + } else { tracing::info!( - "Extra and environment markers currently cannot be parsed on {} which is specified by {}, skipping. {:?}", + "Skipping {} specified by {} due to marker {:?}", p.name, name, p.marker ); - } else { - dependencies.push(p.name.as_dist_info_name().into_owned()) + None } - } + }) + .collect(); + + Some(PackageInfo { + name, + dependencies, + source: PackageSource::Pypi, + }) + } else { + None + } +} + +/// Generate a map of dependencies from a list of locked packages +fn generate_dependency_map(locked_deps: &Vec) -> HashMap { + let mut package_dependencies_map = HashMap::new(); + + for package in locked_deps { + let version = package.version().into_owned(); + + if let Some(package_info) = extract_package_info(package) { package_dependencies_map.insert( - name.clone(), + package_info.name.clone(), Package { - name: name.clone(), - version, - dependencies: dependencies.into_iter().unique().collect(), + name: package_info.name, + version: version.clone(), + dependencies: package_info.dependencies.into_iter().unique().collect(), needed_by: Vec::new(), - source: PackageSource::Pypi, + source: package_info.source, }, ); } @@ -475,8 +560,7 @@ fn generate_dependency_map(locked_deps: &Vec) -> HashMap< package_dependencies_map } -/// Given a map of dependencies, invert it so that it has what a package is -/// needed by, rather than what it depends on +/// Given a map of dependencies, invert it fn invert_dep_map(dep_map: &HashMap) -> HashMap { let mut inverted_deps = dep_map.clone(); diff --git a/tests/test_export.sh b/tests/test_export.sh index fa10db1f9..618f5ca9e 100644 --- a/tests/test_export.sh +++ b/tests/test_export.sh @@ -35,7 +35,7 @@ rm test-env-source-deps.yml cd ../.. echo "Export an environment.yml with custom pip registry" -cd examples/pypi-custom-registry/ +cd examples/pypi-custom-registry pixi project export conda-environment | tee test-env-custom-registry.yml echo "Creating the custom pip registry test environment with micromamba" micromamba create -y -f test-env-custom-registry.yml -n export-test-custom-registry @@ -45,7 +45,7 @@ rm test-env-custom-registry.yml cd ../.. echo "Export an environment.yml with pip find links" -cd examples/pypi-find-links/ +cd examples/pypi-find-links pixi project export conda-environment | tee test-env-find-links.yml echo "Creating the pip find links test environment with micromamba" micromamba create -y -f test-env-find-links.yml -n export-test-find-links @@ -55,7 +55,7 @@ rm test-env-find-links.yml cd ../.. echo "Export an environment.yml from a pyproject.toml that has caused panics" -cd examples/docker/ +cd examples/docker pixi project export conda-environment | tee test-env-pyproject-panic.yml echo "Creating the pyproject.toml panic test environment with micromamba" micromamba create -y -f test-env-pyproject-panic.yml -n export-test-pyproject-panic diff --git a/tests/wheel_tests/.gitignore b/tests/wheel_tests/.gitignore new file mode 100644 index 000000000..1e641ca21 --- /dev/null +++ b/tests/wheel_tests/.gitignore @@ -0,0 +1,4 @@ +.wheel_test_results.toml +.logs/** +.summary.md +.wheel_test_results.lock diff --git a/tests/wheel_tests/conftest.py b/tests/wheel_tests/conftest.py new file mode 100644 index 000000000..21456f0f0 --- /dev/null +++ b/tests/wheel_tests/conftest.py @@ -0,0 +1,26 @@ +from generate_summaries import terminal_summary, markdown_summary +from helpers import setup_stdout_stderr_logging + + +def pytest_configure(config): + setup_stdout_stderr_logging() + + +def pytest_addoption(parser): + # Used to override the default path to the pixi executable + parser.addoption("--pixi-exec", action="store", help="Path to the pixi executable") + + +def pytest_terminal_summary(terminalreporter, exitstatus, config): + """ + At the end of the test session, generate a summary report. + """ + terminal_summary() + + +def pytest_sessionfinish(session, exitstatus): + """ + At the end of the test session, generate a `.summary.md` report. That contains the + same information as the terminal summary. + """ + markdown_summary() diff --git a/tests/wheel_tests/generate_summaries.py b/tests/wheel_tests/generate_summaries.py new file mode 100644 index 000000000..be5ef8f65 --- /dev/null +++ b/tests/wheel_tests/generate_summaries.py @@ -0,0 +1,116 @@ +from record_results import RESULTS_FILE +import tomllib +from rich.console import Console +from rich.table import Table +from rich.text import Text +from rich.panel import Panel +from pathlib import Path + +from read_wheels import read_wheel_file + + +def terminal_summary(): + # Read aggregated results from the shared file + results_file = RESULTS_FILE + if not results_file.exists(): + print("Error: No test results found.") + return + + with results_file.open("rb") as f: + results = tomllib.load(f)["results"] + + packages = read_wheel_file() + + console = Console() + table = Table(title="Test Results", show_header=True, header_style="bold magenta") + table.add_column("Test Name", style="dim") + table.add_column("Outcome", justify="right") + table.add_column("Duration (s)", justify="right") + table.add_column("Error Details") + + # Populate the table with collected results + names = [] + for result in sorted(results, key=lambda r: r["name"]): + outcome_color = "green" if result["outcome"] == "passed" else "red" + error_details = result["longrepr"] if result["outcome"] == "failed" else "" + table.add_row( + Text(result["name"]), + Text(result["outcome"], style=outcome_color), + f"{result['duration']:.2f}", + error_details, + ) + # Record name + names.append(result["name"]) + + for package in packages: + if package.to_add_cmd() not in names: + table.add_row( + Text(package.to_add_cmd()), + Text("N/A", style="dim"), + Text("N/A", style="dim"), + Text("N/A", style="dim"), + ) + + # Display the table in the terminal + console.print(table) + + # Add a summary box with instructions + summary_text = ( + "[bold]Summary:[/bold]\n\n" + f"- Total tests run: {len(results)}\n" + f"- Passed: {sum(1 for r in results if r['outcome'] == 'passed')}\n" + f"- Failed: {sum(1 for r in results if r['outcome'] == 'failed')}\n\n" + "To filter tests by a specific wheel, use the command:\n" + "[bold green]pytest -k ''[/]\n\n" + "Replace [bold][/] with the desired wheel's name to run only tests for that wheel.\n" + r'E.g use [magenta] pixi r test-common-wheels-dev -k "jax\[cuda12]"[/] to run tests for the [bold]jax\[cuda12][/] wheel.' + "\n\n" + "[bold yellow]Note:[/]\n" + "Any [italic]failed[/] tests will have recorded their output to the [bold].log/[/] directory, which" + " resides next to to `wheels.toml` file.\n" + ) + + # Create a Rich panel (box) for the summary text + summary_panel = Panel( + summary_text, title="Test Debrief", title_align="left", border_style="bright_blue" + ) + + # Display the summary box in the terminal + console.print(summary_panel) + + +def markdown_summary(): + if not RESULTS_FILE.exists(): + return + + summary_file = Path(__file__).parent / ".summary.md" + with summary_file.open("w") as f: + # Read the RESULTS_FILE and generate a markdown summary + f.write("# Test Summary\n\n") + f.write(""" +This document contains a summary of the test results for the wheels in the `wheels.toml` file. +You can use the following command, in the pixi repository, to filter tests by a specific wheel: +```bash +pixi r test-common-wheels -k "" +# E.g +pixi r test-common-wheels-dev -k "jax[cuda12]" +``` + +""") + f.write("## Test Results\n\n") + f.write("\n") + f.write("| Test Name | Outcome | Duration (s) | Error Details |\n") + f.write("| :--- | ---: | ---: | --- |\n") + + results_file = RESULTS_FILE + with results_file.open("rb") as r: + results = tomllib.load(r)["results"] + for result in results: + outcome = ( + 'Passed' + if result["outcome"] == "passed" + else 'Failed' + ) + error_details = result["longrepr"] if result["outcome"] == "failed" else "" + f.write(f"|{result["name"]}|{outcome}|{result['duration']:.2f}|{error_details}|\n") + f.write("\n") diff --git a/tests/wheel_tests/helpers.py b/tests/wheel_tests/helpers.py new file mode 100644 index 000000000..2610e4a66 --- /dev/null +++ b/tests/wheel_tests/helpers.py @@ -0,0 +1,62 @@ +import os +import subprocess +import pathlib +from typing import Any +import tomllib +import tomli_w + +StrPath = str | os.PathLike[str] +LOG_DIR = pathlib.Path(__file__).parent / ".logs" + + +def run(args: list[StrPath], cwd: StrPath | None = None) -> None: + """ + Run a subprocess and check the return code + """ + proc: subprocess.CompletedProcess[bytes] = subprocess.run( + args, cwd=cwd, capture_output=True, check=False + ) + proc.check_returncode() + + +def add_system_requirements(manifest_path: pathlib.Path, system_requirements: dict[str, Any]): + """ + Add system requirements to the manifest file + add something like this: + [system-requirements] + libc = { family = "glibc", version = "2.17" } + to the manifest file. + """ + with manifest_path.open("rb") as f: + manifest = tomllib.load(f) + manifest["system-requirements"] = system_requirements + with manifest_path.open("wb") as f: + tomli_w.dump(manifest, f) + + +def setup_stdout_stderr_logging(): + """ + Set up the logging directory + """ + if not LOG_DIR.exists(): + LOG_DIR.mkdir() + for file in LOG_DIR.iterdir(): + file.unlink() + + +def log_called_process_error(name: str, err: subprocess.CalledProcessError, std_err_only=False): + """ + Log the output of a subprocess that failed + has the option to log only the stderr + """ + if not LOG_DIR.exists(): + raise RuntimeError("Call setup_stdout_stderr_logging before logging") + + std_out_log = LOG_DIR / f"{name}.stdout" + std_err_log = LOG_DIR / f"{name}.stderr" + if err.returncode != 0: + if not std_err_only: + with std_out_log.open("w", encoding="utf-8") as f: + f.write(err.stdout.decode("uft-8")) + with std_err_log.open("w", encoding="utf-8") as f: + f.write(err.stderr.decode("utf-8")) diff --git a/tests/wheel_tests/read_wheels.py b/tests/wheel_tests/read_wheels.py new file mode 100644 index 000000000..6f809b019 --- /dev/null +++ b/tests/wheel_tests/read_wheels.py @@ -0,0 +1,120 @@ +from dataclasses import dataclass +from pathlib import Path +import tomllib +from typing import Literal, Iterable, Self, Any + + +@dataclass +class PackageSpec: + """ + A class to represent the package specification in the `wheels.toml` file. + A single Package Name can have multiple specifications. + """ + + version: Literal["*"] | str = "*" + extras: str | None = None + target: str | list[str] | None = None + system_requirements: dict[str, Any] | None = None + + def target_iter(self) -> Iterable[str]: + """ + Returns an iterable of the target platforms + """ + if isinstance(self.target, str): + return [self.target] + elif isinstance(self.target, list): + return self.target + return [] + + @classmethod + def __from_toml(cls, spec: dict[str, str] | str) -> Self: + if isinstance(spec, str): + return cls(version=spec, extras=None, target=None, system_requirements=None) + if isinstance(spec, dict): + return cls( + spec.get("version", "*"), + spec.get("extras"), + spec.get("target"), + spec.get("system-requirements"), + ) + + @classmethod + def from_toml(cls, spec: dict[str, str] | list[dict[str, str]] | str) -> Self | list[Self]: + if isinstance(spec, list): + return [cls.__from_toml(s) for s in spec] + else: + return cls.__from_toml(spec) + + +@dataclass +class Package: + """ + Specifies a package which is a name and a specification + on how to install it. + """ + + # Name of the package + name: str + # Specification of the package + spec: PackageSpec + + def to_add_cmd(self) -> str: + """ + Converts the package to a command that can be consumed with the + `pixi add` command. + """ + cmd = f"{self.name}" + if self.spec.extras: + cmd = f"{cmd}[{self.spec.extras}]" + if self.spec.version and self.spec.version != "*": + cmd = f"{cmd}=={self.spec.version}" + return cmd + + +@dataclass +class WheelTest: + """ + A class to represent the `wheels.toml` file + """ + + # Mapping of wheel names to installation specifications + name: dict[str, list[PackageSpec] | PackageSpec] + + def to_packages(self) -> Iterable[Package]: + """ + Converts to a list of installable packages + """ + for name, specs in self.name.items(): + if isinstance(specs, PackageSpec): + yield Package(name, specs) + else: + yield from [Package(name, spec) for spec in specs] + + @classmethod + def from_toml(cls, file: Path) -> Self: + """ + Read the wheels from the toml file and return the instance + """ + with file.open("rb") as f: + toml = tomllib.load(f) + if not isinstance(toml, dict): + raise ValueError("Expected a dictionary") + wheels = toml + return cls({name: PackageSpec.from_toml(spec) for name, spec in wheels.items()}) + + @classmethod + def from_str(cls, s: str) -> Self: + """ + Read the wheels from the toml string and return the instance + """ + toml = tomllib.loads(s) + return cls({name: PackageSpec.from_toml(spec) for name, spec in toml.items()}) + + +def read_wheel_file() -> Iterable[Package]: + """ + Read the wheel file `wheels.toml` and return the package + instances. + """ + wheel_path = Path(__file__).parent / Path("wheels.toml") + return WheelTest.from_toml(wheel_path).to_packages() diff --git a/tests/wheel_tests/record_results.py b/tests/wheel_tests/record_results.py new file mode 100644 index 000000000..b73e495af --- /dev/null +++ b/tests/wheel_tests/record_results.py @@ -0,0 +1,43 @@ +from pathlib import Path +import tomllib +import tomli_w +from filelock import FileLock + +# Path to the results file, containing test outcomes +RESULTS_FILE = Path(__file__).parent / ".wheel_test_results.toml" +# Lock file to ensure process-safe write access to the results file +LOCK_FILE = RESULTS_FILE.with_suffix(".lock") + + +def record_result(test_id: str, name: str, outcome: str, duration: float, details: str): + """ + Collects test status after each test run, compatible with pytest-xdist. + """ + result = {"name": name, "outcome": outcome, "duration": duration, "longrepr": details} + + # Use file lock for process-safe write access to the results file + lock = FileLock(str(LOCK_FILE)) + + with lock: + test = {"id": test_id, "results": []} + + # Get the existing results + if RESULTS_FILE.exists(): + with RESULTS_FILE.open("rb") as f: + data = tomllib.load(f) + # If this doesn't hold, don't use the recorded data + if "id" in data and data["id"] == test_id: + test = data + + # Append the new result + # if we are in the same session + if test["id"] == test_id: + test["results"].append(result) + # The data is from a different session + # so we overwrite the data + else: + test["results"] = [result] + + # Write the results back to the file + with RESULTS_FILE.open("wb") as f: + tomli_w.dump(test, f) diff --git a/tests/wheel_tests/test_common_wheels.py b/tests/wheel_tests/test_common_wheels.py new file mode 100644 index 000000000..74bb23310 --- /dev/null +++ b/tests/wheel_tests/test_common_wheels.py @@ -0,0 +1,95 @@ +import pytest +import os +import pathlib +import subprocess +from read_wheels import Package, read_wheel_file +import time +from record_results import record_result +from helpers import add_system_requirements, log_called_process_error, run +import sys + + +@pytest.mark.flaky(reruns=5, reruns_delay=1, condition=sys.platform.startswith("win32")) +def test_wheel(pixi: str, package: Package, testrun_uid: str, tmp_path: pathlib.Path): + """ + Create a temporary directory and install the wheel in it. + The `testrun_uid` is a unique identifier for the test run + this is created by pytest-xdist + """ + start = time.perf_counter() + try: + # Path to the manifest file + manifest_path = tmp_path / "pixi.toml" + run([pixi, "init"], cwd=tmp_path) + + # Check if we need to add system-requirements + # There is no CLI for it currently so we need to manually edit the file + if package.spec.system_requirements: + add_system_requirements(manifest_path, package.spec.system_requirements) + + # Add python to the project + run([pixi, "add", "--no-progress", "--manifest-path", manifest_path, "python==3.12.*"]) + + # Add the wheel to the project + run_args = [ + pixi, + "-vvv", + "add", + "--no-progress", + "--manifest-path", + manifest_path, + "--pypi", + package.to_add_cmd(), + ] + + # Add for another platform, if specified + for platform in package.spec.target_iter(): + run_args.extend(["--platform", package.spec.target]) + + run(run_args) + # Record the success of the test + record_result(testrun_uid, package.to_add_cmd(), "passed", time.perf_counter() - start, "") + except subprocess.CalledProcessError as e: + # Record the failure details + record_result( + testrun_uid, package.to_add_cmd(), "failed", time.perf_counter() - start, str(e) + ) + # Log the error + log_called_process_error(package.to_add_cmd(), e, std_err_only=True) + # Re-raise the exception to fail the test + raise e + + +def pytest_generate_tests(metafunc): + """ + This generates the test for the wheels by reading the wheels from the toml specification + creates a test for each entry in the toml file + """ + if "package" in metafunc.fixturenames: + packages = read_wheel_file() + metafunc.parametrize("package", [pytest.param(w, id=f"{w.to_add_cmd()}") for w in packages]) + + +@pytest.fixture(scope="session") +def pixi(pytestconfig): + # The command line argument overrides the default path + if pytestconfig.getoption("pixi_exec"): + return pytestconfig.getoption("pixi_exec") + + # Check pixi environment variable + project_root = os.environ.get("PIXI_PROJECT_ROOT") + if not project_root: + pytest.exit("PROJECT_ROOT environment variable is not set, run from pixi task") + + # Check if the target directory exists + # This assertion is for the type checker + assert project_root + project_root = pathlib.Path(project_root) + target_dir = project_root.joinpath(".pixi/target/release") + if not target_dir.exists(): + pytest.exit("pixi executable not found, run `pixi r build` first") + + if sys.platform.startswith("win"): + return target_dir.joinpath("pixi.exe") + else: + return target_dir.joinpath("pixi") diff --git a/tests/wheel_tests/test_read_wheels.py b/tests/wheel_tests/test_read_wheels.py new file mode 100644 index 000000000..0e7e9970f --- /dev/null +++ b/tests/wheel_tests/test_read_wheels.py @@ -0,0 +1,21 @@ +from read_wheels import Package, PackageSpec, WheelTest + + +def test_spec_to_add_cmd(): + assert Package("foo", PackageSpec()).to_add_cmd() == "foo" + assert Package("foo", PackageSpec("1.0")).to_add_cmd() == "foo==1.0" + assert Package("foo", PackageSpec("1.0", "bar")).to_add_cmd() == "foo[bar]==1.0" + + +def test_wheel_test_from_str(): + toml = """ + foo = "*" + bar = { version = "1.0", extras = "baz", target = "linux-64" } + laz = ["*", { version = "1.0", extras = "baz" }] + """ + wt = WheelTest.from_str(toml) + assert len(list(wt.to_packages())) == 4 + assert Package("foo", PackageSpec()) in wt.to_packages() + assert Package("bar", PackageSpec("1.0", "baz", "linux-64")) in wt.to_packages() + assert Package("laz", PackageSpec()) in wt.to_packages() + assert Package("laz", PackageSpec("1.0", "baz")) in wt.to_packages() diff --git a/tests/wheel_tests/wheels.toml b/tests/wheel_tests/wheels.toml new file mode 100644 index 000000000..711b22b80 --- /dev/null +++ b/tests/wheel_tests/wheels.toml @@ -0,0 +1,67 @@ +# This file defines the wheels that are used to test common wheels and adding them using pixi +# Add the top-level wheels, for where you want a simple `name` = `version` mapping +# Add more complex mappings using the [[name]] table, where you can specify extras, version, and more +# Or a single [name] for a more complex version +confz = "*" +dvc = "*" +dvclive = "*" +icecream = "*" +matplotlib = "*" +mkdocs = "*" +mkdocs-material = "*" +numpy = "*" +pymeshfix = "*" +pyright = "*" +pytest = "*" +pytest-benchmark = "*" +pyvista = "*" +ruff = "*" +scipy = "*" +taichi = "*" +trimesh = "*" +typeguard = "*" + +## Add more complex dependencies after this line: + +## Failing for windows (add for linux until the issue is resolved) +[meshpy] +target = "linux-64" +version = "*" + +[meshtaichi-patcher] +target = "linux-64" +version = "*" + +[meshtools] +target = "linux-64" +version = "*" + +[rerun-sdk] +target = "linux-64" +version = "*" +# Use these system requirements +# because the wheel requires a higher version of glibc than the conda counterpart +[rerun-sdk.system-requirements] +libc = { family = "glibc", version = "2.31" } + +[mkdocstrings] +extras = "python" +version = "*" + +[meshio] +extras = "all" +version = "*" +[meshio.system-requirements] +macos = "14.0" + +[[jax]] +version = "*" +[[jax]] +extras = "cuda12" +version = "*" + +[nvidia-cuda-nvrtc-cu11] +target = "linux-64" +version = "*" +[nvidia-cuda-nvrtc-cu11.system-requirements] +cuda = "11.0"