diff --git a/examples/solve-groups/.gitattributes b/examples/solve-groups/.gitattributes new file mode 100644 index 000000000..d5799bd69 --- /dev/null +++ b/examples/solve-groups/.gitattributes @@ -0,0 +1,2 @@ +# GitHub syntax highlighting +pixi.lock linguist-language=YAML diff --git a/examples/solve-groups/.gitignore b/examples/solve-groups/.gitignore new file mode 100644 index 000000000..c9314b7c2 --- /dev/null +++ b/examples/solve-groups/.gitignore @@ -0,0 +1,2 @@ +# pixi environments +.pixi diff --git a/examples/solve-groups/pixi.lock b/examples/solve-groups/pixi.lock new file mode 100644 index 000000000..e569c4f51 --- /dev/null +++ b/examples/solve-groups/pixi.lock @@ -0,0 +1,1749 @@ +version: 4 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.5.0-hcb278e6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.44.2-h2797004_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-h59595ed_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/py_rattler-0.2.1-py312h241aef2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.1-hab00c5b_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-4_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/py-rattler-0.2.1-hb6292c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + osx-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/py-rattler-0.2.1-hb6292c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.2.2-h8857fd0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.5.0-hf0c8a7f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.44.2-h92b6c6a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.4-h93d8f39_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.2.1-hd75f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/py_rattler-0.2.1-py312h68f415e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.1-h9f0c242_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-4_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2 + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/py-rattler-0.2.1-hb6292c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h93a5062_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.2.2-hf0a4a13_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.5.0-hb7217d7_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.44.2-h091b4b1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.2.13-h53f4e23_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.4-h463b476_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.2.1-h0d3ecfb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/py_rattler-0.2.1-py312h99f8e83_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.1-hdf0ec26_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-4_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/py-rattler-0.2.1-hb6292c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-hcfcfb64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2024.2.2-h56e8100_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.5.0-h63175ca_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.2-h8ffe710_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.44.2-hcfcfb64_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.2.13-hcfcfb64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.2.1-hcfcfb64_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/py_rattler-0.2.1-py312hfccd98a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.1-h2628c8c_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.12-4_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hcf57466_18.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.38.33130-h82b7239_18.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.38.33130-hcb4865c_18.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 + max-py310: + channels: + - url: https://conda.anaconda.org/conda-forge/ + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.44.2-h2797004_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-h59595ed_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.16.2-py310hcb5633a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.0-h543edf9_3_cpython.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.10-4_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.44.2-h2c6b66d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.9.0-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.9.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + osx-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.9.0-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.9.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.2.2-h8857fd0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.44.2-h92b6c6a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.4-h93d8f39_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.2.1-hd75f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/pydantic-core-2.16.2-py310h54baaa9_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.10.0-h38b4d05_3_cpython.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.10-4_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/sqlite-3.44.2-h7461747_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2 + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.9.0-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.9.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h93a5062_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.2.2-hf0a4a13_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.44.2-h091b4b1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.2.13-h53f4e23_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.4-h463b476_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.2.1-h0d3ecfb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.16.2-py310hd442715_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.10.0-h43b31ca_3_cpython.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.10-4_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sqlite-3.44.2-hf2abe2d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.6.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.9.0-hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.9.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-hcfcfb64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2024.2.2-h56e8100_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.2-h8ffe710_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.44.2-hcfcfb64_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.2.13-hcfcfb64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.2.1-hcfcfb64_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.16.2-py310h87d50f1_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.10.0-hcf16a7b_3_cpython.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.10-4_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sqlite-3.44.2-hcfcfb64_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hcf57466_18.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.38.33130-h82b7239_18.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.38.33130-hcb4865c_18.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 + min-py38: + channels: + - url: https://conda.anaconda.org/conda-forge/ + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.44.2-h2797004_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-h59595ed_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/py_rattler-0.2.1-py310h75e40e8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.0-h543edf9_3_cpython.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.10-4_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.44.2-h2c6b66d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/py-rattler-0.2.1-hb6292c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + osx-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/py-rattler-0.2.1-hb6292c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.2.2-h8857fd0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.44.2-h92b6c6a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.4-h93d8f39_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.2.1-hd75f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/py_rattler-0.2.1-py310haee03e7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/python-3.10.0-h38b4d05_3_cpython.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.10-4_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/sqlite-3.44.2-h7461747_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2 + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/py-rattler-0.2.1-hb6292c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h93a5062_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.2.2-hf0a4a13_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.44.2-h091b4b1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.2.13-h53f4e23_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.4-h463b476_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.2.1-h0d3ecfb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/py_rattler-0.2.1-py310h0e6f4b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.10.0-h43b31ca_3_cpython.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.10-4_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sqlite-3.44.2-hf2abe2d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/py-rattler-0.2.1-hb6292c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-hcfcfb64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2024.2.2-h56e8100_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.2-h8ffe710_5.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.44.2-hcfcfb64_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.2.13-hcfcfb64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.2.1-hcfcfb64_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/py_rattler-0.2.1-py310h87d50f1_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.10.0-hcf16a7b_3_cpython.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.10-4_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sqlite-3.44.2-hcfcfb64_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hcf57466_18.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.38.33130-h82b7239_18.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.38.33130-hcb4865c_18.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 +packages: +- kind: conda + name: _libgcc_mutex + version: '0.1' + build: conda_forge + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + size: 2562 + timestamp: 1578324546067 +- kind: conda + name: _openmp_mutex + version: '4.5' + build: 2_gnu + build_number: 16 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + size: 23621 + timestamp: 1650670423406 +- kind: conda + name: annotated-types + version: 0.6.0 + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.6.0-pyhd8ed1ab_0.conda + sha256: 3a2c98154d95cfd54daba6b7d507d31f5ba07ac2ad955c44eb041b66563193cd + md5: 997c29372bdbe2afee073dff71f35923 + depends: + - python >=3.7 + - typing-extensions >=4.0.0 + license: MIT + license_family: MIT + size: 17026 + timestamp: 1696634393637 +- kind: conda + name: bzip2 + version: 1.0.8 + build: h10d778d_5 + build_number: 5 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h10d778d_5.conda + sha256: 61fb2b488928a54d9472113e1280b468a309561caa54f33825a3593da390b242 + md5: 6097a6ca9ada32699b5fc4312dd6ef18 + license: bzip2-1.0.6 + license_family: BSD + size: 127885 + timestamp: 1699280178474 +- kind: conda + name: bzip2 + version: 1.0.8 + build: h93a5062_5 + build_number: 5 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h93a5062_5.conda + sha256: bfa84296a638bea78a8bb29abc493ee95f2a0218775642474a840411b950fe5f + md5: 1bbc659ca658bfd49a481b5ef7a0f40f + license: bzip2-1.0.6 + license_family: BSD + size: 122325 + timestamp: 1699280294368 +- kind: conda + name: bzip2 + version: 1.0.8 + build: hcfcfb64_5 + build_number: 5 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-hcfcfb64_5.conda + sha256: ae5f47a5c86fd6db822931255dcf017eb12f60c77f07dc782ccb477f7808aab2 + md5: 26eb8ca6ea332b675e11704cce84a3be + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: bzip2-1.0.6 + license_family: BSD + size: 124580 + timestamp: 1699280668742 +- kind: conda + name: bzip2 + version: 1.0.8 + build: hd590300_5 + build_number: 5 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hd590300_5.conda + sha256: 242c0c324507ee172c0e0dd2045814e746bb303d1eb78870d182ceb0abc726a8 + md5: 69b8b6202a07720f448be700e300ccf4 + depends: + - libgcc-ng >=12 + license: bzip2-1.0.6 + license_family: BSD + size: 254228 + timestamp: 1699279927352 +- kind: conda + name: ca-certificates + version: 2024.2.2 + build: h56e8100_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2024.2.2-h56e8100_0.conda + sha256: 4d587088ecccd393fec3420b64f1af4ee1a0e6897a45cfd5ef38055322cea5d0 + md5: 63da060240ab8087b60d1357051ea7d6 + license: ISC + size: 155886 + timestamp: 1706843918052 +- kind: conda + name: ca-certificates + version: 2024.2.2 + build: h8857fd0_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/ca-certificates-2024.2.2-h8857fd0_0.conda + sha256: 54a794aedbb4796afeabdf54287b06b1d27f7b13b3814520925f4c2c80f58ca9 + md5: f2eacee8c33c43692f1ccfd33d0f50b1 + license: ISC + size: 155665 + timestamp: 1706843838227 +- kind: conda + name: ca-certificates + version: 2024.2.2 + build: hbcca054_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.2.2-hbcca054_0.conda + sha256: 91d81bfecdbb142c15066df70cc952590ae8991670198f92c66b62019b251aeb + md5: 2f4327a1cbe7f022401b236e915a5fef + license: ISC + size: 155432 + timestamp: 1706843687645 +- kind: conda + name: ca-certificates + version: 2024.2.2 + build: hf0a4a13_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2024.2.2-hf0a4a13_0.conda + sha256: 49bc3439816ac72d0c0e0f144b8cc870fdcc4adec2e861407ec818d8116b2204 + md5: fb416a1795f18dcc5a038bc2dc54edf9 + license: ISC + size: 155725 + timestamp: 1706844034242 +- kind: conda + name: ld_impl_linux-64 + version: '2.40' + build: h41732ed_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda + sha256: f6cc89d887555912d6c61b295d398cff9ec982a3417d38025c45d5dd9b9e79cd + md5: 7aca3059a1729aa76c597603f10b0dd3 + constrains: + - binutils_impl_linux-64 2.40 + license: GPL-3.0-only + license_family: GPL + size: 704696 + timestamp: 1674833944779 +- kind: conda + name: libexpat + version: 2.5.0 + build: h63175ca_1 + build_number: 1 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.5.0-h63175ca_1.conda + sha256: 794b2a9be72f176a2767c299574d330ffb76b2ed75d7fd20bee3bbadce5886cf + md5: 636cc3cbbd2e28bcfd2f73b2044aac2c + constrains: + - expat 2.5.0.* + license: MIT + license_family: MIT + size: 138689 + timestamp: 1680190844101 +- kind: conda + name: libexpat + version: 2.5.0 + build: hb7217d7_1 + build_number: 1 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.5.0-hb7217d7_1.conda + sha256: 7d143a9c991579ad4207f84c632650a571c66329090daa32b3c87cf7311c3381 + md5: 5a097ad3d17e42c148c9566280481317 + constrains: + - expat 2.5.0.* + license: MIT + license_family: MIT + size: 63442 + timestamp: 1680190916539 +- kind: conda + name: libexpat + version: 2.5.0 + build: hcb278e6_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.5.0-hcb278e6_1.conda + sha256: 74c98a563777ae2ad71f1f74d458a8ab043cee4a513467c159ccf159d0e461f3 + md5: 6305a3dd2752c76335295da4e581f2fd + depends: + - libgcc-ng >=12 + constrains: + - expat 2.5.0.* + license: MIT + license_family: MIT + size: 77980 + timestamp: 1680190528313 +- kind: conda + name: libexpat + version: 2.5.0 + build: hf0c8a7f_1 + build_number: 1 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/libexpat-2.5.0-hf0c8a7f_1.conda + sha256: 80024bd9f44d096c4cc07fb2bac76b5f1f7553390112dab3ad6acb16a05f0b96 + md5: 6c81cb022780ee33435cca0127dd43c9 + constrains: + - expat 2.5.0.* + license: MIT + license_family: MIT + size: 69602 + timestamp: 1680191040160 +- kind: conda + name: libffi + version: 3.4.2 + build: h0d85af4_5 + build_number: 5 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2 + sha256: 7a2d27a936ceee6942ea4d397f9c7d136f12549d86f7617e8b6bad51e01a941f + md5: ccb34fb14960ad8b125962d3d79b31a9 + license: MIT + license_family: MIT + size: 51348 + timestamp: 1636488394370 +- kind: conda + name: libffi + version: 3.4.2 + build: h3422bc3_5 + build_number: 5 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 + sha256: 41b3d13efb775e340e4dba549ab5c029611ea6918703096b2eaa9c015c0750ca + md5: 086914b672be056eb70fd4285b6783b6 + license: MIT + license_family: MIT + size: 39020 + timestamp: 1636488587153 +- kind: conda + name: libffi + version: 3.4.2 + build: h7f98852_5 + build_number: 5 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 + sha256: ab6e9856c21709b7b517e940ae7028ae0737546122f83c2aa5d692860c3b149e + md5: d645c6d2ac96843a2bfaccd2d62b3ac3 + depends: + - libgcc-ng >=9.4.0 + license: MIT + license_family: MIT + size: 58292 + timestamp: 1636488182923 +- kind: conda + name: libffi + version: 3.4.2 + build: h8ffe710_5 + build_number: 5 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.2-h8ffe710_5.tar.bz2 + sha256: 1951ab740f80660e9bc07d2ed3aefb874d78c107264fd810f24a1a6211d4b1a5 + md5: 2c96d1b6915b408893f9472569dee135 + depends: + - vc >=14.1,<15.0a0 + - vs2015_runtime >=14.16.27012 + license: MIT + license_family: MIT + size: 42063 + timestamp: 1636489106777 +- kind: conda + name: libgcc-ng + version: 13.2.0 + build: h807b86a_5 + build_number: 5 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_5.conda + sha256: d32f78bfaac282cfe5205f46d558704ad737b8dbf71f9227788a5ca80facaba4 + md5: d4ff227c46917d3b4565302a2bbb276b + depends: + - _libgcc_mutex 0.1 conda_forge + - _openmp_mutex >=4.5 + constrains: + - libgomp 13.2.0 h807b86a_5 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 770506 + timestamp: 1706819192021 +- kind: conda + name: libgomp + version: 13.2.0 + build: h807b86a_5 + build_number: 5 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_5.conda + sha256: 0d3d4b1b0134283ea02d58e8eb5accf3655464cf7159abf098cc694002f8d34e + md5: d211c42b9ce49aee3734fdc828731689 + depends: + - _libgcc_mutex 0.1 conda_forge + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 419751 + timestamp: 1706819107383 +- kind: conda + name: libnsl + version: 2.0.1 + build: hd590300_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda + sha256: 26d77a3bb4dceeedc2a41bd688564fe71bf2d149fdcf117049970bc02ff1add6 + md5: 30fd6e37fe21f86f4bd26d6ee73eeec7 + depends: + - libgcc-ng >=12 + license: LGPL-2.1-only + license_family: GPL + size: 33408 + timestamp: 1697359010159 +- kind: conda + name: libsqlite + version: 3.44.2 + build: h091b4b1_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.44.2-h091b4b1_0.conda + sha256: f0dc2fe69eddb4bab72ff6bb0da51d689294f466ee1b01e80ced1e7878a21aa5 + md5: d7e1af696cfadec251a0abdd7b79ed77 + depends: + - libzlib >=1.2.13,<1.3.0a0 + license: Unlicense + size: 815254 + timestamp: 1700863572318 +- kind: conda + name: libsqlite + version: 3.44.2 + build: h2797004_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.44.2-h2797004_0.conda + sha256: ee2c4d724a3ed60d5b458864d66122fb84c6ce1df62f735f90d8db17b66cd88a + md5: 3b6a9f225c3dbe0d24f4fedd4625c5bf + depends: + - libgcc-ng >=12 + - libzlib >=1.2.13,<1.3.0a0 + license: Unlicense + size: 845830 + timestamp: 1700863204572 +- kind: conda + name: libsqlite + version: 3.44.2 + build: h92b6c6a_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.44.2-h92b6c6a_0.conda + sha256: 8a317d2aa6352feba951ca09d5bf34f565f9dd10bb14ff842b8650baa321d781 + md5: d4419f90019e6a2b152cd4d32f73a82f + depends: + - libzlib >=1.2.13,<1.3.0a0 + license: Unlicense + size: 891089 + timestamp: 1700863475542 +- kind: conda + name: libsqlite + version: 3.44.2 + build: hcfcfb64_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.44.2-hcfcfb64_0.conda + sha256: 25bfcf79ec863c2c0f0b3599981e2eac57efc5302faf2bb84f68c3f0faa55d1c + md5: 4a5f5ab56cbf3ccd08d71a1168061213 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Unlicense + size: 853171 + timestamp: 1700863704859 +- kind: conda + name: libuuid + version: 2.38.1 + build: h0b41bf4_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 + md5: 40b61aab5c7ba9ff276c41cfffe6b80b + depends: + - libgcc-ng >=12 + license: BSD-3-Clause + license_family: BSD + size: 33601 + timestamp: 1680112270483 +- kind: conda + name: libxcrypt + version: 4.4.36 + build: hd590300_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c + md5: 5aa797f8787fe7a17d1b0821485b5adc + depends: + - libgcc-ng >=12 + license: LGPL-2.1-or-later + size: 100393 + timestamp: 1702724383534 +- kind: conda + name: libzlib + version: 1.2.13 + build: h53f4e23_5 + build_number: 5 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.2.13-h53f4e23_5.conda + sha256: ab1c8aefa2d54322a63aaeeefe9cf877411851738616c4068e0dccc66b9c758a + md5: 1a47f5236db2e06a320ffa0392f81bd8 + constrains: + - zlib 1.2.13 *_5 + license: Zlib + license_family: Other + size: 48102 + timestamp: 1686575426584 +- kind: conda + name: libzlib + version: 1.2.13 + build: h8a1eda9_5 + build_number: 5 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.2.13-h8a1eda9_5.conda + sha256: fc58ad7f47ffea10df1f2165369978fba0a1cc32594aad778f5eec725f334867 + md5: 4a3ad23f6e16f99c04e166767193d700 + constrains: + - zlib 1.2.13 *_5 + license: Zlib + license_family: Other + size: 59404 + timestamp: 1686575566695 +- kind: conda + name: libzlib + version: 1.2.13 + build: hcfcfb64_5 + build_number: 5 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.2.13-hcfcfb64_5.conda + sha256: c161822ee8130b71e08b6d282b9919c1de2c5274b29921a867bca0f7d30cad26 + md5: 5fdb9c6a113b6b6cb5e517fd972d5f41 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - zlib 1.2.13 *_5 + license: Zlib + license_family: Other + size: 55800 + timestamp: 1686575452215 +- kind: conda + name: libzlib + version: 1.2.13 + build: hd590300_5 + build_number: 5 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda + sha256: 370c7c5893b737596fd6ca0d9190c9715d89d888b8c88537ae1ef168c25e82e4 + md5: f36c115f1ee199da648e0597ec2047ad + depends: + - libgcc-ng >=12 + constrains: + - zlib 1.2.13 *_5 + license: Zlib + license_family: Other + size: 61588 + timestamp: 1686575217516 +- kind: conda + name: ncurses + version: '6.4' + build: h463b476_2 + build_number: 2 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.4-h463b476_2.conda + sha256: f6890634f815e8408d08f36503353f8dfd7b055e4c3b9ea2ee52180255cf4b0a + md5: 52b6f254a7b9663e854f44b6570ed982 + depends: + - __osx >=10.9 + license: X11 AND BSD-3-Clause + size: 794741 + timestamp: 1698751574074 +- kind: conda + name: ncurses + version: '6.4' + build: h59595ed_2 + build_number: 2 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-h59595ed_2.conda + sha256: 91cc03f14caf96243cead96c76fe91ab5925a695d892e83285461fb927dece5e + md5: 7dbaa197d7ba6032caf7ae7f32c1efa0 + depends: + - libgcc-ng >=12 + license: X11 AND BSD-3-Clause + size: 884434 + timestamp: 1698751260967 +- kind: conda + name: ncurses + version: '6.4' + build: h93d8f39_2 + build_number: 2 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.4-h93d8f39_2.conda + sha256: ea0fca66bbb52a1ef0687d466518fe120b5f279684effd6fd336a7b0dddc423a + md5: e58f366bd4d767e9ab97ab8b272e7670 + depends: + - __osx >=10.9 + license: X11 AND BSD-3-Clause + size: 822031 + timestamp: 1698751567986 +- kind: conda + name: openssl + version: 3.2.1 + build: h0d3ecfb_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.2.1-h0d3ecfb_0.conda + sha256: 13663fcd4abc8681b31ccbad39800fee2127cb6159b51a989ed48a816af36cf5 + md5: 421cc6e8715447b73c2c57dcf78cb9d2 + depends: + - ca-certificates + constrains: + - pyopenssl >=22.1 + license: Apache-2.0 + license_family: Apache + size: 2862719 + timestamp: 1706635779319 +- kind: conda + name: openssl + version: 3.2.1 + build: hcfcfb64_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/openssl-3.2.1-hcfcfb64_0.conda + sha256: 1df1c43136f863d5e9ba20b703001caf9a4d0ea56bdc3eeb948c977e3d4f91d3 + md5: 158df8eead8092cf0e27167c8761a8dd + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - pyopenssl >=22.1 + license: Apache-2.0 + license_family: Apache + size: 8229619 + timestamp: 1706638014697 +- kind: conda + name: openssl + version: 3.2.1 + build: hd590300_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.2.1-hd590300_0.conda + sha256: c02c12bdb898daacf7eb3d09859f93ea8f285fd1a6132ff6ff0493ab52c7fe57 + md5: 51a753e64a3027bd7e23a189b1f6e91e + depends: + - ca-certificates + - libgcc-ng >=12 + constrains: + - pyopenssl >=22.1 + license: Apache-2.0 + license_family: Apache + size: 2863069 + timestamp: 1706635653339 +- kind: conda + name: openssl + version: 3.2.1 + build: hd75f5a5_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.2.1-hd75f5a5_0.conda + sha256: 20c1b1a34a1831c24d37ed1500ca07300171184af0c66598f3c5ca901634d713 + md5: 3033be9a59fd744172b03971b9ccd081 + depends: + - ca-certificates + constrains: + - pyopenssl >=22.1 + license: Apache-2.0 + license_family: Apache + size: 2509168 + timestamp: 1706636810736 +- kind: conda + name: py-rattler + version: 0.2.1 + build: hb6292c7_0 + subdir: noarch + noarch: generic + url: https://conda.anaconda.org/conda-forge/noarch/py-rattler-0.2.1-hb6292c7_0.conda + sha256: 792a479eb5345deea1390c022d26e4d2a4db476912d6e43d2a516b4c752a65e3 + md5: 96a1b4099e55e60502718b750e2bf3e0 + depends: + - py_rattler >=0.2.1,<0.2.2.0a0 + license: BSD-3-Clause + license_family: BSD + size: 7534 + timestamp: 1703159922880 +- kind: conda + name: py_rattler + version: 0.2.1 + build: py310h0e6f4b3_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/py_rattler-0.2.1-py310h0e6f4b3_0.conda + sha256: e525bf42a9d86707bc81da9662e76094c1dce66a77456d8773d97d58fa2cd2fd + md5: f239c4e2c469bbe82000994e57b02591 + depends: + - openssl >=3.2.0,<4.0a0 + - python >=3.10,<3.11.0a0 + - python >=3.10,<3.11.0a0 *_cpython + - python_abi 3.10.* *_cp310 + license: BSD-3-Clause + license_family: BSD + size: 3254367 + timestamp: 1703159910904 +- kind: conda + name: py_rattler + version: 0.2.1 + build: py310h75e40e8_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/py_rattler-0.2.1-py310h75e40e8_0.conda + sha256: 96f748009e3b455b214f3c7380fdcd2f17c74b799ca4e333fce305e874363aae + md5: 91659bca40d94c5168ff31511a29baf3 + depends: + - libgcc-ng >=12 + - openssl >=3.2.0,<4.0a0 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 + license: BSD-3-Clause + license_family: BSD + size: 5281728 + timestamp: 1703152203281 +- kind: conda + name: py_rattler + version: 0.2.1 + build: py310h87d50f1_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/py_rattler-0.2.1-py310h87d50f1_0.conda + sha256: c4bc8b7027113fed9b5a5cd357230b1aba1d8d15b59d65df073bb02921856452 + md5: 76bf997628bd8004bf880656f244e338 + depends: + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + size: 2971283 + timestamp: 1703157561184 +- kind: conda + name: py_rattler + version: 0.2.1 + build: py310haee03e7_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/py_rattler-0.2.1-py310haee03e7_0.conda + sha256: 03dee326e6a8e08a29ed4e63bf66974fbe90a67edeae6bf83aa3f80b90ae75ca + md5: ee3dd93e1f69bd4e1599e407d0cffc5e + depends: + - openssl >=3.2.0,<4.0a0 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 + license: BSD-3-Clause + license_family: BSD + size: 3350287 + timestamp: 1703157701725 +- kind: conda + name: py_rattler + version: 0.2.1 + build: py312h241aef2_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/py_rattler-0.2.1-py312h241aef2_0.conda + sha256: 3c763a5930575e29f7f101e7de8f67273756290578eca85fe98acaa2c3b78138 + md5: 4be1176011778730f228522bb8bff7e5 + depends: + - libgcc-ng >=12 + - openssl >=3.2.0,<4.0a0 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + size: 5283553 + timestamp: 1703150841080 +- kind: conda + name: py_rattler + version: 0.2.1 + build: py312h68f415e_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/py_rattler-0.2.1-py312h68f415e_0.conda + sha256: bf0c7cd42a776ca7adc9e624fabd4eccf33a87d9707ea81aa893225a33cae84e + md5: 87752087d8f1343b331e83f484647b36 + depends: + - openssl >=3.2.0,<4.0a0 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + size: 3356027 + timestamp: 1703157940533 +- kind: conda + name: py_rattler + version: 0.2.1 + build: py312h99f8e83_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/py_rattler-0.2.1-py312h99f8e83_0.conda + sha256: 5966c47d7de5d58251abade7c0613b8cbb23b2e1bfb71ebdf33ade3d9af991d8 + md5: fdf230d5916e16badb16aa734e10fc2b + depends: + - openssl >=3.2.0,<4.0a0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + size: 3265253 + timestamp: 1703159652638 +- kind: conda + name: py_rattler + version: 0.2.1 + build: py312hfccd98a_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/py_rattler-0.2.1-py312hfccd98a_0.conda + sha256: f15911a565477b0457a4390a52ef2af32f4713b3f050458a4cd40721a62d84f4 + md5: d7fdcbd24a32e9465c2c53889055903e + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + size: 2968695 + timestamp: 1703157410086 +- kind: conda + name: pydantic + version: 2.6.1 + build: pyhd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.6.1-pyhd8ed1ab_0.conda + sha256: 27083637287bb08a93e28616b5030f5eab31deb83d16fc132901ada987a62cfa + md5: 3b1698c91820d852d802fc21471f52d8 + depends: + - annotated-types >=0.4.0 + - pydantic-core 2.16.2 + - python >=3.7 + - typing-extensions >=4.6.1 + license: MIT + license_family: MIT + size: 271768 + timestamp: 1707149490082 +- kind: conda + name: pydantic-core + version: 2.16.2 + build: py310h54baaa9_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/pydantic-core-2.16.2-py310h54baaa9_0.conda + sha256: 89ae81ac9bdb3e3940328bfe4689553bb4796939eb2970788dee583da43c608a + md5: 6e195e5d56fadfa9fd189df2c5853466 + depends: + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 + - typing-extensions >=4.6.0,!=4.7.0 + constrains: + - __osx >=10.12 + license: MIT + license_family: MIT + size: 1586660 + timestamp: 1706907396112 +- kind: conda + name: pydantic-core + version: 2.16.2 + build: py310h87d50f1_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.16.2-py310h87d50f1_0.conda + sha256: 63c448393381cf246a5c17cd8d8760997410a451cc55e029c3221f37da29d2cc + md5: 5fcb5b018181aad91e630402a1a0cefa + depends: + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 + - typing-extensions >=4.6.0,!=4.7.0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + size: 1616615 + timestamp: 1706907875200 +- kind: conda + name: pydantic-core + version: 2.16.2 + build: py310hcb5633a_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.16.2-py310hcb5633a_0.conda + sha256: 1e491ddbfd53a67f0734fb3f4ccd8782c24935549c281cf5a9bcf9c5ce8b9ec4 + md5: 1a6153b4a9a54c7250c163bcc0c1ac66 + depends: + - libgcc-ng >=12 + - python >=3.10,<3.11.0a0 + - python_abi 3.10.* *_cp310 + - typing-extensions >=4.6.0,!=4.7.0 + license: MIT + license_family: MIT + size: 1657498 + timestamp: 1706907069048 +- kind: conda + name: pydantic-core + version: 2.16.2 + build: py310hd442715_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.16.2-py310hd442715_0.conda + sha256: a0dae9f763a5f5b9735b4ad8fdc20e3db0aa2a0f04dbb5d45964b92ef2919cae + md5: 441ce6cc8d003038dff595d2726ace69 + depends: + - python >=3.10,<3.11.0a0 + - python >=3.10,<3.11.0a0 *_cpython + - python_abi 3.10.* *_cp310 + - typing-extensions >=4.6.0,!=4.7.0 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + size: 1474830 + timestamp: 1706907541328 +- kind: conda + name: python + version: 3.10.0 + build: h38b4d05_3_cpython + build_number: 3 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/python-3.10.0-h38b4d05_3_cpython.tar.bz2 + sha256: a4323c8f8855047b33bf151eec5552e632845a6ed7c7347eef401bb884bd7098 + md5: 8b04dda703f05e42299bf50c6fb497f5 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libffi >=3.4.2,<3.5.0a0 + - libzlib >=1.2.11,<1.3.0a0 + - ncurses >=6.2,<7.0.0a0 + - openssl >=3.0.0,<4.0a0 + - readline >=8.1,<9.0a0 + - sqlite >=3.36.0,<4.0a0 + - tk >=8.6.11,<8.7.0a0 + - tzdata + - xz >=5.2.5,<6.0.0a0 + constrains: + - python_abi 3.10.* *_cp310 + license: Python-2.0 + size: 13518160 + timestamp: 1637377444619 +- kind: conda + name: python + version: 3.10.0 + build: h43b31ca_3_cpython + build_number: 3 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.10.0-h43b31ca_3_cpython.tar.bz2 + sha256: 2c116191cf46984ae171973beb86fe8914f3833db23865b052db3bdf1276c92c + md5: 79f489d167f29b2f1d6d628b34dad49c + depends: + - bzip2 >=1.0.8,<2.0a0 + - libffi >=3.4.2,<3.5.0a0 + - libzlib >=1.2.11,<1.3.0a0 + - ncurses >=6.2,<7.0.0a0 + - openssl >=3.0.0,<4.0a0 + - readline >=8.1,<9.0a0 + - sqlite >=3.36.0,<4.0a0 + - tk >=8.6.11,<8.7.0a0 + - tzdata + - xz >=5.2.5,<6.0.0a0 + constrains: + - python_abi 3.10.* *_cp310 + license: Python-2.0 + size: 12899235 + timestamp: 1637375579641 +- kind: conda + name: python + version: 3.10.0 + build: h543edf9_3_cpython + build_number: 3 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.0-h543edf9_3_cpython.tar.bz2 + sha256: 0661f43d7bc446c22bfcf1730ad5c7a2ac695c696ba4609bac896cefff879431 + md5: 67cdff58413ce9f034fb971188060313 + depends: + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libffi >=3.4.2,<3.5.0a0 + - libgcc-ng >=9.4.0 + - libnsl >=2.0.0,<2.1.0a0 + - libuuid >=2.32.1,<3.0a0 + - libzlib >=1.2.11,<1.3.0a0 + - ncurses >=6.2,<7.0.0a0 + - openssl >=3.0.0,<4.0a0 + - readline >=8.1,<9.0a0 + - sqlite >=3.36.0,<4.0a0 + - tk >=8.6.11,<8.7.0a0 + - tzdata + - xz >=5.2.5,<6.0.0a0 + constrains: + - python_abi 3.10.* *_cp310 + license: Python-2.0 + size: 31281695 + timestamp: 1637376125446 +- kind: conda + name: python + version: 3.10.0 + build: hcf16a7b_3_cpython + build_number: 3 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/python-3.10.0-hcf16a7b_3_cpython.tar.bz2 + sha256: 444a45caf1b6334dbe6c49dc21015cddbda5d35b58749b805f74d535ad23eb67 + md5: 14b44eee89a77696eb6f68b75fea6cd4 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libffi >=3.4.2,<3.5.0a0 + - libzlib >=1.2.11,<1.3.0a0 + - openssl >=3.0.0,<4.0a0 + - sqlite >=3.36.0,<4.0a0 + - tk >=8.6.11,<8.7.0a0 + - tzdata + - vc >=14.1,<15.0a0 + - vs2015_runtime >=14.16.27012 + - xz >=5.2.5,<6.0.0a0 + constrains: + - python_abi 3.10.* *_cp310 + license: Python-2.0 + size: 15986716 + timestamp: 1637374998463 +- kind: conda + name: python + version: 3.12.1 + build: h2628c8c_1_cpython + build_number: 1 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/python-3.12.1-h2628c8c_1_cpython.conda + sha256: e3c4d9a961dac0e15b501144b333ede8515bff83aec51f4815c575311053352f + md5: 19f49a5c0aea545792ad88bb40dc1ec9 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.5.0,<3.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.44.2,<4.0a0 + - libzlib >=1.2.13,<1.3.0a0 + - openssl >=3.2.0,<4.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + size: 16082514 + timestamp: 1703318525844 +- kind: conda + name: python + version: 3.12.1 + build: h9f0c242_1_cpython + build_number: 1 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/python-3.12.1-h9f0c242_1_cpython.conda + sha256: d55c0875fc6f4228024d1fd0c4fcde8a61fdd4d852a98f3b05f121011a2fd753 + md5: 41d5549764b9f37199e6255e5e9daee6 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.5.0,<3.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.44.2,<4.0a0 + - libzlib >=1.2.13,<1.3.0a0 + - ncurses >=6.4,<7.0a0 + - openssl >=3.2.0,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + size: 14537279 + timestamp: 1703319526863 +- kind: conda + name: python + version: 3.12.1 + build: hab00c5b_1_cpython + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.1-hab00c5b_1_cpython.conda + sha256: d44521b3ffd7edcad75bd55276ae3fb4cb07e63b2aa3545fef62bfda774b8a2b + md5: 0bab699354cbd66959550eb9b9866620 + depends: + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.5.0,<3.0a0 + - libffi >=3.4,<4.0a0 + - libgcc-ng >=12 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.44.2,<4.0a0 + - libuuid >=2.38.1,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.2.13,<1.3.0a0 + - ncurses >=6.4,<7.0a0 + - openssl >=3.2.0,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + size: 32286118 + timestamp: 1703320043028 +- kind: conda + name: python + version: 3.12.1 + build: hdf0ec26_1_cpython + build_number: 1 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.1-hdf0ec26_1_cpython.conda + sha256: c9bdc2478a983da8c1b965727889fe58e2ea594c6f280a73c822beab1926d40d + md5: 7b5d48e25f131864762bfef1914d2014 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.5.0,<3.0a0 + - libffi >=3.4,<4.0a0 + - libsqlite >=3.44.2,<4.0a0 + - libzlib >=1.2.13,<1.3.0a0 + - ncurses >=6.4,<7.0a0 + - openssl >=3.2.0,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - xz >=5.2.6,<6.0a0 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + size: 13089201 + timestamp: 1703318862740 +- kind: conda + name: python_abi + version: '3.10' + build: 4_cp310 + build_number: 4 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.10-4_cp310.conda + sha256: 456bec815bfc2b364763084d08b412fdc4c17eb9ccc66a36cb775fa7ac3cbaec + md5: 26322ec5d7712c3ded99dd656142b8ce + constrains: + - python 3.10.* *_cpython + license: BSD-3-Clause + license_family: BSD + size: 6398 + timestamp: 1695147363189 +- kind: conda + name: python_abi + version: '3.10' + build: 4_cp310 + build_number: 4 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.10-4_cp310.conda + sha256: abc26b3b5a62f9c8112a2303d24b0c590d5f7fc9470521f5a520472d59c2223e + md5: b15c816c5a86abcc4d1458dd63aa4c65 + constrains: + - python 3.10.* *_cpython + license: BSD-3-Clause + license_family: BSD + size: 6484 + timestamp: 1695147705581 +- kind: conda + name: python_abi + version: '3.10' + build: 4_cp310 + build_number: 4 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.10-4_cp310.conda + sha256: f69bac2f28082a275ef67313968b2c366d8236c3a6869b9cdf5cdb97a5821812 + md5: 1a3d9c6bb5f0b1b22d9e9296c127e8c7 + constrains: + - python 3.10.* *_cpython + license: BSD-3-Clause + license_family: BSD + size: 6490 + timestamp: 1695147522999 +- kind: conda + name: python_abi + version: '3.10' + build: 4_cp310 + build_number: 4 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.10-4_cp310.conda + sha256: 19066c462fd0e32c64503c688f77cb603beb4019b812caf855d03f2a5447960b + md5: b41195997c14fb7473d26637ea4c3946 + constrains: + - python 3.10.* *_cpython + license: BSD-3-Clause + license_family: BSD + size: 6773 + timestamp: 1695147715814 +- kind: conda + name: python_abi + version: '3.12' + build: 4_cp312 + build_number: 4 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-4_cp312.conda + sha256: 182a329de10a4165f6e8a3804caf751f918f6ea6176dd4e5abcdae1ed3095bf6 + md5: dccc2d142812964fcc6abdc97b672dff + constrains: + - python 3.12.* *_cpython + license: BSD-3-Clause + license_family: BSD + size: 6385 + timestamp: 1695147396604 +- kind: conda + name: python_abi + version: '3.12' + build: 4_cp312 + build_number: 4 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/python_abi-3.12-4_cp312.conda + sha256: 82c154d95c1637604671a02a89e72f1382e89a4269265a03506496bd928f6f14 + md5: 87201ac4314b911b74197e588cca3639 + constrains: + - python 3.12.* *_cpython + license: BSD-3-Clause + license_family: BSD + size: 6496 + timestamp: 1695147498447 +- kind: conda + name: python_abi + version: '3.12' + build: 4_cp312 + build_number: 4 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-4_cp312.conda + sha256: db25428e4f24f8693ffa39f3ff6dfbb8fd53bc298764b775b57edab1c697560f + md5: bbb3a02c78b2d8219d7213f76d644a2a + constrains: + - python 3.12.* *_cpython + license: BSD-3-Clause + license_family: BSD + size: 6508 + timestamp: 1695147497048 +- kind: conda + name: python_abi + version: '3.12' + build: 4_cp312 + build_number: 4 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/python_abi-3.12-4_cp312.conda + sha256: 488f8519d04b48f59bd6fde21ebe2d7a527718ff28aac86a8b53aa63658bdef6 + md5: 17f4ccf6be9ded08bd0a376f489ac1a6 + constrains: + - python 3.12.* *_cpython + license: BSD-3-Clause + license_family: BSD + size: 6785 + timestamp: 1695147430513 +- kind: conda + name: readline + version: '8.2' + build: h8228510_1 + build_number: 1 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda + sha256: 5435cf39d039387fbdc977b0a762357ea909a7694d9528ab40f005e9208744d7 + md5: 47d31b792659ce70f470b5c82fdfb7a4 + depends: + - libgcc-ng >=12 + - ncurses >=6.3,<7.0a0 + license: GPL-3.0-only + license_family: GPL + size: 281456 + timestamp: 1679532220005 +- kind: conda + name: readline + version: '8.2' + build: h92ec313_1 + build_number: 1 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda + sha256: a1dfa679ac3f6007362386576a704ad2d0d7a02e98f5d0b115f207a2da63e884 + md5: 8cbb776a2f641b943d413b3e19df71f4 + depends: + - ncurses >=6.3,<7.0a0 + license: GPL-3.0-only + license_family: GPL + size: 250351 + timestamp: 1679532511311 +- kind: conda + name: readline + version: '8.2' + build: h9e318b2_1 + build_number: 1 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda + sha256: 41e7d30a097d9b060037f0c6a2b1d4c4ae7e942c06c943d23f9d481548478568 + md5: f17f77f2acf4d344734bda76829ce14e + depends: + - ncurses >=6.3,<7.0a0 + license: GPL-3.0-only + license_family: GPL + size: 255870 + timestamp: 1679532707590 +- kind: conda + name: sqlite + version: 3.44.2 + build: h2c6b66d_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.44.2-h2c6b66d_0.conda + sha256: bae479520fe770fe11996b4c240923ed097f851fbd2401d55540e551c9dbbef7 + md5: 4f2892c672829693fd978d065db4e8be + depends: + - libgcc-ng >=12 + - libsqlite 3.44.2 h2797004_0 + - libzlib >=1.2.13,<1.3.0a0 + - ncurses >=6.4,<7.0a0 + - readline >=8.2,<9.0a0 + license: Unlicense + size: 836378 + timestamp: 1700863215372 +- kind: conda + name: sqlite + version: 3.44.2 + build: h7461747_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/sqlite-3.44.2-h7461747_0.conda + sha256: 120f42ee2b7cee46711693609f8a7e7918befbd370c960332c0ef13ca651c0d8 + md5: ac6662948d2ccf800474dfdf59fb94bc + depends: + - libsqlite 3.44.2 h92b6c6a_0 + - libzlib >=1.2.13,<1.3.0a0 + - ncurses >=6.4,<7.0a0 + - readline >=8.2,<9.0a0 + license: Unlicense + size: 890038 + timestamp: 1700863497227 +- kind: conda + name: sqlite + version: 3.44.2 + build: hcfcfb64_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/sqlite-3.44.2-hcfcfb64_0.conda + sha256: 77496bb1b15fe40bae1ca9a9841b906b66f212a534e7c4ef7878c82511c2d0e4 + md5: 27ac1a237f0c9964afba717848811ba8 + depends: + - libsqlite 3.44.2 hcfcfb64_0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Unlicense + size: 856472 + timestamp: 1700863720976 +- kind: conda + name: sqlite + version: 3.44.2 + build: hf2abe2d_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/sqlite-3.44.2-hf2abe2d_0.conda + sha256: b034405d93e7153f777d52c18fe26120356c568e4ca85626712d633d939a8923 + md5: c98aa8eb8f02260610c5bb981027ba5d + depends: + - libsqlite 3.44.2 h091b4b1_0 + - libzlib >=1.2.13,<1.3.0a0 + - ncurses >=6.4,<7.0a0 + - readline >=8.2,<9.0a0 + license: Unlicense + size: 803166 + timestamp: 1700863604745 +- kind: conda + name: tk + version: 8.6.13 + build: h1abcd95_1 + build_number: 1 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda + sha256: 30412b2e9de4ff82d8c2a7e5d06a15f4f4fef1809a72138b6ccb53a33b26faf5 + md5: bf830ba5afc507c6232d4ef0fb1a882d + depends: + - libzlib >=1.2.13,<1.3.0a0 + license: TCL + license_family: BSD + size: 3270220 + timestamp: 1699202389792 +- kind: conda + name: tk + version: 8.6.13 + build: h5083fa2_1 + build_number: 1 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda + sha256: 72457ad031b4c048e5891f3f6cb27a53cb479db68a52d965f796910e71a403a8 + md5: b50a57ba89c32b62428b71a875291c9b + depends: + - libzlib >=1.2.13,<1.3.0a0 + license: TCL + license_family: BSD + size: 3145523 + timestamp: 1699202432999 +- kind: conda + name: tk + version: 8.6.13 + build: h5226925_1 + build_number: 1 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda + sha256: 2c4e914f521ccb2718946645108c9bd3fc3216ba69aea20c2c3cedbd8db32bb1 + md5: fc048363eb8f03cd1737600a5d08aafe + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: TCL + license_family: BSD + size: 3503410 + timestamp: 1699202577803 +- kind: conda + name: tk + version: 8.6.13 + build: noxft_h4845f30_101 + build_number: 101 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e + md5: d453b98d9c83e71da0741bb0ff4d76bc + depends: + - libgcc-ng >=12 + - libzlib >=1.2.13,<1.3.0a0 + license: TCL + license_family: BSD + size: 3318875 + timestamp: 1699202167581 +- kind: conda + name: typing-extensions + version: 4.9.0 + build: hd8ed1ab_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.9.0-hd8ed1ab_0.conda + sha256: d795c1eb1db4ea147f01ece74e5a504d7c2e8d5ee8c11ec987884967dd938f9c + md5: c16524c1b7227dc80b36b4fa6f77cc86 + depends: + - typing_extensions 4.9.0 pyha770c72_0 + license: PSF-2.0 + license_family: PSF + size: 10191 + timestamp: 1702176301116 +- kind: conda + name: typing_extensions + version: 4.9.0 + build: pyha770c72_0 + subdir: noarch + noarch: python + url: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.9.0-pyha770c72_0.conda + sha256: f3c5be8673bfd905c4665efcb27fa50192f24f84fa8eff2f19cba5d09753d905 + md5: a92a6440c3fe7052d63244f3aba2a4a7 + depends: + - python >=3.8 + license: PSF-2.0 + license_family: PSF + size: 36058 + timestamp: 1702176292645 +- kind: conda + name: tzdata + version: 2024a + build: h0c530f3_0 + subdir: noarch + noarch: generic + url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + sha256: 7b2b69c54ec62a243eb6fba2391b5e443421608c3ae5dbff938ad33ca8db5122 + md5: 161081fc7cec0bfda0d86d7cb595f8d8 + license: LicenseRef-Public-Domain + size: 119815 + timestamp: 1706886945727 +- kind: conda + name: ucrt + version: 10.0.22621.0 + build: h57928b3_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_0.tar.bz2 + sha256: f29cdaf8712008f6b419b8b1a403923b00ab2504bfe0fb2ba8eb60e72d4f14c6 + md5: 72608f6cd3e5898229c3ea16deb1ac43 + constrains: + - vs2015_runtime >=14.29.30037 + license: LicenseRef-Proprietary + license_family: PROPRIETARY + size: 1283972 + timestamp: 1666630199266 +- kind: conda + name: vc + version: '14.3' + build: hcf57466_18 + build_number: 18 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-hcf57466_18.conda + sha256: 447a8d8292a7b2107dcc18afb67f046824711a652725fc0f522c368e7a7b8318 + md5: 20e1e652a4c740fa719002a8449994a2 + depends: + - vc14_runtime >=14.38.33130 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD + size: 16977 + timestamp: 1702511255313 +- kind: conda + name: vc14_runtime + version: 14.38.33130 + build: h82b7239_18 + build_number: 18 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.38.33130-h82b7239_18.conda + sha256: bf94c9af4b2e9cba88207001197e695934eadc96a5c5e4cd7597e950aae3d8ff + md5: 8be79fdd2725ddf7bbf8a27a4c1f79ba + depends: + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.38.33130.* *_18 + license: LicenseRef-ProprietaryMicrosoft + license_family: Proprietary + size: 749868 + timestamp: 1702511239004 +- kind: conda + name: vs2015_runtime + version: 14.38.33130 + build: hcb4865c_18 + build_number: 18 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.38.33130-hcb4865c_18.conda + sha256: a2fec221f361d6263c117f4ea6d772b21c90a2f8edc6f3eb0eadec6bfe8843db + md5: 10d42885e3ed84e575b454db30f1aa93 + depends: + - vc14_runtime >=14.38.33130 + license: BSD-3-Clause + license_family: BSD + size: 16988 + timestamp: 1702511261442 +- kind: conda + name: xz + version: 5.2.6 + build: h166bdaf_0 + subdir: linux-64 + url: https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2 + sha256: 03a6d28ded42af8a347345f82f3eebdd6807a08526d47899a42d62d319609162 + md5: 2161070d867d1b1204ea749c8eec4ef0 + depends: + - libgcc-ng >=12 + license: LGPL-2.1 and GPL-2.0 + size: 418368 + timestamp: 1660346797927 +- kind: conda + name: xz + version: 5.2.6 + build: h57fd34a_0 + subdir: osx-arm64 + url: https://conda.anaconda.org/conda-forge/osx-arm64/xz-5.2.6-h57fd34a_0.tar.bz2 + sha256: 59d78af0c3e071021cfe82dc40134c19dab8cdf804324b62940f5c8cd71803ec + md5: 39c6b54e94014701dd157f4f576ed211 + license: LGPL-2.1 and GPL-2.0 + size: 235693 + timestamp: 1660346961024 +- kind: conda + name: xz + version: 5.2.6 + build: h775f41a_0 + subdir: osx-64 + url: https://conda.anaconda.org/conda-forge/osx-64/xz-5.2.6-h775f41a_0.tar.bz2 + sha256: eb09823f34cc2dd663c0ec4ab13f246f45dcd52e5b8c47b9864361de5204a1c8 + md5: a72f9d4ea13d55d745ff1ed594747f10 + license: LGPL-2.1 and GPL-2.0 + size: 238119 + timestamp: 1660346964847 +- kind: conda + name: xz + version: 5.2.6 + build: h8d14728_0 + subdir: win-64 + url: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 + sha256: 54d9778f75a02723784dc63aff4126ff6e6749ba21d11a6d03c1f4775f269fe0 + md5: 515d77642eaa3639413c6b1bc3f94219 + depends: + - vc >=14.1,<15 + - vs2015_runtime >=14.16.27033 + license: LGPL-2.1 and GPL-2.0 + size: 217804 + timestamp: 1660346976440 diff --git a/examples/solve-groups/pixi.toml b/examples/solve-groups/pixi.toml new file mode 100644 index 000000000..b98ea8a27 --- /dev/null +++ b/examples/solve-groups/pixi.toml @@ -0,0 +1,30 @@ +[project] +name = "solve-groups" +version = "0.1.0" +description = "Add a short description here" +authors = ["Bas Zalmstra "] +channels = ["conda-forge"] +platforms = ["win-64", "linux-64", "osx-64", "osx-arm64"] + +[feature.max_py310.dependencies] +python = "<=3.10" +pydantic = "*" + +[feature.min_py38.dependencies] +python = ">=3.8" +py-rattler = "*" + +[environments] +# The solve-group mixes the dependencies of all features in the group and solves them together. +# Both environments should have at most python 3.10, even though `min-py38` environment only +# specifies a lower bound. +# +# The environments do not contain any dependencies from the other environments. This means that +# the `min-py38` environment does not contain `pydantic` and the `max-py310` environment does not +# contain `py-rattler`. +min-py38 = { features = ["min_py38"], solve-group = "group1" } +max-py310 = { features = ["max_py310"], solve-group = "group1" } + +# The default environment does not specify a solve-group which means the python version does not +# have an upperbound. +default = ["min_py38"] diff --git a/src/cli/info.rs b/src/cli/info.rs index e76e3f347..4f643eaa3 100644 --- a/src/cli/info.rs +++ b/src/cli/info.rs @@ -303,7 +303,9 @@ pub async fn execute(args: Args) -> miette::Result<()> { .features(true) .map(|feature| feature.name.clone()) .collect(), - solve_group: env.manifest().solve_group.clone(), + solve_group: env + .solve_group() + .map(|solve_group| solve_group.name().to_string()), environment_size: None, dependencies: env .dependencies(None, Some(Platform::current())) diff --git a/src/environment.rs b/src/environment.rs index 5a6efdd5d..68fa54541 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -1,6 +1,8 @@ use miette::{Context, IntoDiagnostic}; use crate::lock_file::{resolve_pypi, LockedCondaPackages, LockedPypiPackages}; +use crate::project::virtual_packages::get_minimal_virtual_packages; +use crate::project::{Dependencies, SolveGroup}; use crate::{ config, consts, install, install_pypi, lock_file, lock_file::{ @@ -16,7 +18,7 @@ use crate::{ }, repodata::fetch_sparse_repodata_targets, utils::BarrierCell, - Project, + Project, SpecType, }; use futures::future::Either; use futures::stream::FuturesUnordered; @@ -27,9 +29,9 @@ use itertools::Itertools; use rattler::install::{PythonInfo, Transaction}; use rattler::package_cache::PackageCache; use rattler_conda_types::{ - Channel, MatchSpec, PackageName, Platform, PrefixRecord, RepoDataRecord, + Channel, GenericVirtualPackage, MatchSpec, PackageName, Platform, PrefixRecord, RepoDataRecord, }; -use rattler_lock::{LockFile, PypiPackageData, PypiPackageEnvironmentData}; +use rattler_lock::{LockFile, Package, PypiPackageData, PypiPackageEnvironmentData}; use rattler_repodata_gateway::sparse::SparseRepoData; use reqwest_middleware::ClientWithMiddleware; use rip::{index::PackageDb, resolve::SDistResolution}; @@ -552,6 +554,145 @@ impl<'p> OutdatedEnvironments<'p> { } } + // Determine which solve-groups are out of date. + let mut conda_solve_groups_out_of_date = HashMap::new(); + let mut pypi_solve_groups_out_of_date = HashMap::new(); + for (environment, platforms) in &outdated_conda { + let Some(solve_group) = environment.solve_group() else { + continue; + }; + conda_solve_groups_out_of_date + .entry(solve_group) + .or_insert_with(HashSet::new) + .extend(platforms.iter().copied()); + } + for (environment, platforms) in &outdated_pypi { + let Some(solve_group) = environment.solve_group() else { + continue; + }; + pypi_solve_groups_out_of_date + .entry(solve_group) + .or_insert_with(HashSet::new) + .extend(platforms.iter().copied()); + } + + // Check solve-groups, all environments in the same solve group must share the same + // dependencies. + for solve_group in project.solve_groups() { + for platform in solve_group + .environments() + .flat_map(|env| env.platforms()) + .unique() + { + // Keep track of if any of the package types are out of date + let mut conda_package_mismatch = false; + let mut pypi_package_mismatch = false; + + // Keep track of the packages by name to check for mismatches between environments. + let mut conda_packages_by_name = HashMap::new(); + let mut pypi_packages_by_name = HashMap::new(); + + // Iterate over all environments to compare the packages. + for env in solve_group.environments() { + if outdated_conda + .get(&env) + .and_then(|p| p.get(&platform)) + .is_some() + { + // If the environment is already out-of-date there is no need to check it, + // because the solve-group is already out-of-date. + break; + } + + let Some(locked_env) = lock_file.environment(env.name().as_str()) else { + // If the environment is missing, we already marked it as out of date. + continue; + }; + + for package in locked_env.packages(platform).into_iter().flatten() { + match package { + Package::Conda(pkg) => { + match conda_packages_by_name.get(&pkg.package_record().name) { + None => { + conda_packages_by_name.insert( + pkg.package_record().name.clone(), + pkg.url().clone(), + ); + } + Some(url) if pkg.url() != url => { + conda_package_mismatch = true; + } + _ => {} + } + } + Package::Pypi(pkg) => { + match pypi_packages_by_name.get(&pkg.data().package.name) { + None => { + pypi_packages_by_name.insert( + pkg.data().package.name.clone(), + pkg.url().clone(), + ); + } + Some(url) if pkg.url() != url => { + pypi_package_mismatch = true; + } + _ => {} + } + } + } + + // If there is a conda package mismatch there is also a pypi mismatch and we + // can break early. + if conda_package_mismatch { + pypi_package_mismatch = true; + break; + } + } + + // If there is a conda package mismatch there is also a pypi mismatch and we can + // break early. + if conda_package_mismatch { + pypi_package_mismatch = true; + break; + } + } + + // If there is a mismatch there is a mismatch for the entire group + if conda_package_mismatch { + conda_solve_groups_out_of_date + .entry(solve_group.clone()) + .or_default() + .insert(platform); + } + + if pypi_package_mismatch { + pypi_solve_groups_out_of_date + .entry(solve_group.clone()) + .or_default() + .insert(platform); + } + } + } + + // Mark the rest of the environments out of date for all solve groups + for (solve_group, platforms) in conda_solve_groups_out_of_date { + for env in solve_group.environments() { + outdated_conda + .entry(env.clone()) + .or_default() + .extend(platforms.iter().copied()); + } + } + + for (solve_group, platforms) in pypi_solve_groups_out_of_date { + for env in solve_group.environments() { + outdated_pypi + .entry(env.clone()) + .or_default() + .extend(platforms.iter().copied()); + } + } + // For all targets where conda is out of date, the pypi packages are also out of date. for (environment, platforms) in outdated_conda.iter() { outdated_pypi @@ -574,6 +715,9 @@ impl<'p> OutdatedEnvironments<'p> { type PerEnvironment<'p, T> = HashMap, T>; type PerEnvironmentAndPlatform<'p, T> = HashMap, HashMap>; +type PerGroupAndPlatform<'p, T> = HashMap, HashMap>; + +type LockedCondaPackagesByName = HashMap; #[derive(Default)] struct UpdateContext<'p> { @@ -604,6 +748,9 @@ struct UpdateContext<'p> { /// [`BarrierCell`] that will eventually contain the solved records computed by another task. /// This allows tasks to wait for the records to be solved before proceeding. solved_pypi_records: PerEnvironmentAndPlatform<'p, Arc>>>, + + grouped_solved_repodata_records: + PerGroupAndPlatform<'p, Arc>>>, } impl<'p> UpdateContext<'p> { @@ -841,6 +988,7 @@ async fn ensure_up_to_date_lock_file( solved_repodata_records: HashMap::new(), instantiated_conda_prefixes: HashMap::new(), solved_pypi_records: HashMap::new(), + grouped_solved_repodata_records: HashMap::new(), }; // This will keep track of all outstanding tasks that we need to wait for. All tasks are added @@ -858,33 +1006,91 @@ async fn ensure_up_to_date_lock_file( ordered_platforms.move_index(current_platform_index, 0); } + // Determine the source of the solve information + let source = GroupedEnvironment::from(environment.clone()); + for platform in ordered_platforms { - // Extract the records from the existing lock file - let existing_records = context - .locked_repodata_records - .get_mut(&environment) - .and_then(|records| records.remove(&platform)) - .map(|records| Arc::try_unwrap(records).unwrap_or_else(|arc| (*arc).clone())) - .unwrap_or_default(); - - // Spawn a task to solve the environment. - let conda_solve_task = spawn_solve_conda_environment_task( - environment.clone(), - existing_records, - context.repo_data.clone(), - platform, - ) - .boxed_local(); + // Is there an existing pending task to solve the group? + let group_solve_records = if let Some(cell) = context + .grouped_solved_repodata_records + .get(&source) + .and_then(|platforms| platforms.get(&platform)) + { + // Yes, we can reuse the existing cell. + cell.clone() + } else { + // No, we need to spawn a task to update for the entire solve group. + // + // Determine the existing records for the group and platform. If there are multiple + // environments that contain the same packages (because they were previously not in + // the same solve group), we only take the latest version of the package. + let mut existing_records = HashMap::new(); + for env in source.environments() { + for record in context + .locked_repodata_records + .get(&env) + .and_then(|env| env.get(&platform)) + .into_iter() + .flat_map(|records| records.iter()) + { + match existing_records.get(&record.package_record.name) { + None => { + existing_records + .insert(record.package_record.name.clone(), record.clone()); + } + Some(existing) + if existing.package_record.version + < record.package_record.version => + { + existing_records + .insert(record.package_record.name.clone(), record.clone()); + } + _ => {} + } + } + } + let existing_records = existing_records.into_values().collect_vec(); + + // Spawn a task to solve the group. + let group_solve_task = spawn_solve_conda_environment_task( + source.clone(), + existing_records, + context.repo_data.clone(), + platform, + ) + .boxed_local(); + + // Store the task so we can poll it later. + pending_futures.push(group_solve_task); - pending_futures.push(conda_solve_task); + // Create an entry that can be used by other tasks to wait for the result. + let cell = Arc::new(BarrierCell::new()); + let previous_cell = context + .grouped_solved_repodata_records + .entry(source.clone()) + .or_default() + .insert(platform, cell.clone()); + assert!( + previous_cell.is_none(), + "a cell has already been added to update conda records" + ); + + cell + }; + + // Spawn a task to extract the records from the group solve task. + let records_future = + spawn_extract_conda_environment_task(environment.clone(), platform, async move { + group_solve_records.wait().await.clone() + }) + .boxed_local(); + + pending_futures.push(records_future); let previous_cell = context .solved_repodata_records .entry(environment.clone()) .or_default() - .insert( - platform, - Arc::new(BarrierCell::>>::new()), - ); + .insert(platform, Arc::default()); assert!( previous_cell.is_none(), "a cell has already been added to update conda records" @@ -1013,6 +1219,42 @@ async fn ensure_up_to_date_lock_file( while let Some(result) = pending_futures.next().await { top_level_progress.inc(1); match result? { + TaskResult::CondaGroupSolved(group_name, platform, records) => { + let group = match &group_name { + GroupedEnvironmentName::Group(name) => GroupedEnvironment::Group( + project.solve_group(name).expect("solve group should exist"), + ), + GroupedEnvironmentName::Environment(name) => GroupedEnvironment::Environment( + project.environment(name).expect("environment should exist"), + ), + }; + + context + .grouped_solved_repodata_records + .get_mut(&group) + .expect("the entry for this environment should exist") + .get_mut(&platform) + .expect("the entry for this platform should exist") + .set(Arc::new(records)) + .expect("records should not be solved twice"); + + match group_name { + GroupedEnvironmentName::Group(group_name) => { + tracing::info!( + "solved conda package for solve group '{}' '{}'", + group_name, + platform + ); + } + GroupedEnvironmentName::Environment(env_name) => { + tracing::info!( + "solved conda package for environment '{}' '{}'", + env_name, + platform + ); + } + } + } TaskResult::CondaSolved(environment, platform, records) => { let environment = project .environment(&environment) @@ -1028,7 +1270,7 @@ async fn ensure_up_to_date_lock_file( .expect("records should not be solved twice"); tracing::info!( - "solved conda packages for '{}' '{}'", + "extracted conda packages for '{}' '{}'", environment.name().fancy_display(), platform ); @@ -1127,7 +1369,8 @@ async fn ensure_up_to_date_lock_file( /// Represents data that is sent back from a task. This is used to communicate the result of a task /// back to the main task which will forward the information to other tasks waiting for results. enum TaskResult { - CondaSolved(EnvironmentName, Platform, Vec), + CondaGroupSolved(GroupedEnvironmentName, Platform, LockedCondaPackagesByName), + CondaSolved(EnvironmentName, Platform, LockedCondaPackages), CondaPrefixUpdated(EnvironmentName, Prefix, Box), PypiSolved( EnvironmentName, @@ -1138,28 +1381,28 @@ enum TaskResult { /// A task that solves the conda dependencies for a given environment. async fn spawn_solve_conda_environment_task( - environment: Environment<'_>, + group: GroupedEnvironment<'_>, existing_repodata_records: Vec, sparse_repo_data: Arc>, platform: Platform, ) -> miette::Result { // Get the dependencies for this platform - let dependencies = environment.dependencies(None, Some(platform)); + let dependencies = group.dependencies(None, Some(platform)); // Get the virtual packages for this platform - let virtual_packages = environment.virtual_packages(platform); + let virtual_packages = group.virtual_packages(platform); // Get the environment name - let environment_name = environment.name().clone(); + let group_name = group.name(); // The list of channels and platforms we need for this task - let channels = environment.channels().into_iter().cloned().collect_vec(); + let channels = group.channels().into_iter().cloned().collect_vec(); // Capture local variables let sparse_repo_data = sparse_repo_data.clone(); // Whether there are pypi dependencies, and we should fetch purls. - let has_pypi_dependencies = environment.has_pypi_dependencies(); + let has_pypi_dependencies = group.has_pypi_dependencies(); tokio::spawn(async move { let pb = @@ -1201,10 +1444,20 @@ async fn spawn_solve_conda_environment_task( lock_file::pypi::amend_pypi_purls(&mut records).await?; } + // Turn the records into a map by name + let records_by_name = records + .into_iter() + .map(|record| (record.package_record.name.clone(), record)) + .collect(); + // Finish the progress bar pb.finish(); - Ok(TaskResult::CondaSolved(environment_name, platform, records)) + Ok(TaskResult::CondaGroupSolved( + group_name, + platform, + records_by_name, + )) }) .await .unwrap_or_else(|e| match e.try_into_panic() { @@ -1213,6 +1466,51 @@ async fn spawn_solve_conda_environment_task( }) } +/// Distill the repodata that is applicable for the given `environment` from the repodata of an entire solve group. +async fn spawn_extract_conda_environment_task( + environment: Environment<'_>, + platform: Platform, + solve_group_records: impl Future>, +) -> miette::Result { + let group = GroupedEnvironment::from(environment.clone()); + + // Await the records from the group + let group_records = solve_group_records.await; + + // If the group is just the environment on its own we can immediately return the records. + if matches!(group, GroupedEnvironment::Environment(_)) { + return Ok(TaskResult::CondaSolved( + environment.name().clone(), + platform, + group_records + .iter() + .map(|(_, record)| record) + .cloned() + .collect(), + )); + } + + let virtual_package_names = group + .virtual_packages(platform) + .into_iter() + .map(|vp| vp.name) + .collect::>(); + + let environment_dependencies = environment.dependencies(None, Some(platform)); + + let environment_records = extract_referenced_conda_records( + &group_records, + &virtual_package_names, + &environment_dependencies, + ); + + Ok(TaskResult::CondaSolved( + environment.name().clone(), + platform, + environment_records, + )) +} + /// A task that solves the pypi dependencies for a given environment. async fn spawn_solve_pypi_task( environment: Environment<'_>, @@ -1412,3 +1710,130 @@ impl SolveProgressBar { self.pb.finish_and_clear(); } } + +/// Either a solve group or an individual environment without a solve group. +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub enum GroupedEnvironment<'p> { + Group(SolveGroup<'p>), + Environment(Environment<'p>), +} + +pub enum GroupedEnvironmentName { + Group(String), + Environment(EnvironmentName), +} + +impl<'p> From> for GroupedEnvironment<'p> { + fn from(source: SolveGroup<'p>) -> Self { + GroupedEnvironment::Group(source) + } +} + +impl<'p> From> for GroupedEnvironment<'p> { + fn from(source: Environment<'p>) -> Self { + source.solve_group().map_or_else( + || GroupedEnvironment::Environment(source), + GroupedEnvironment::Group, + ) + } +} + +impl<'p> GroupedEnvironment<'p> { + pub fn name(&self) -> GroupedEnvironmentName { + match self { + GroupedEnvironment::Group(group) => { + GroupedEnvironmentName::Group(group.name().to_string()) + } + GroupedEnvironment::Environment(env) => { + GroupedEnvironmentName::Environment(env.name().clone()) + } + } + } + + pub fn environments(&self) -> impl Iterator> + '_ { + match self { + GroupedEnvironment::Group(group) => itertools::Either::Left(group.environments()), + GroupedEnvironment::Environment(env) => { + itertools::Either::Right(vec![env.clone()].into_iter()) + } + } + } + + pub fn dependencies(&self, kind: Option, platform: Option) -> Dependencies { + match self { + GroupedEnvironment::Group(group) => group.dependencies(kind, platform), + GroupedEnvironment::Environment(env) => env.dependencies(kind, platform), + } + } + + pub fn system_requirements(&self) -> SystemRequirements { + match self { + GroupedEnvironment::Group(group) => group.system_requirements(), + GroupedEnvironment::Environment(env) => env.system_requirements(), + } + } + + pub fn virtual_packages(&self, platform: Platform) -> Vec { + get_minimal_virtual_packages(platform, &self.system_requirements()) + .into_iter() + .map(GenericVirtualPackage::from) + .collect() + } + + pub fn channels(&self) -> IndexSet<&'p Channel> { + match self { + GroupedEnvironment::Group(group) => group.channels(), + GroupedEnvironment::Environment(env) => env.channels(), + } + } + + pub fn has_pypi_dependencies(&self) -> bool { + match self { + GroupedEnvironment::Group(group) => group.has_pypi_dependencies(), + GroupedEnvironment::Environment(env) => env.has_pypi_dependencies(), + } + } +} + +/// Given a list of dependencies, and list of conda repodata records by package name recursively extract all the +/// repodata records that are needed to satisfy the requirements. +/// +/// This function only looks at the names of the packages and does not actually match the requirements of the +/// dependencies. This function assumes that the repodata records from `records` form a consistent environment. If +/// this turns out not to be the case this function might panic. +fn extract_referenced_conda_records( + records: &LockedCondaPackagesByName, + virtual_packages: &HashSet, + dependencies: &Dependencies, +) -> LockedCondaPackages { + let mut queue = dependencies + .iter_specs() + .map(|(name, _)| name.clone()) + .collect_vec(); + let mut queued_names = queue.iter().cloned().collect::>(); + let mut result = Vec::new(); + while let Some(package) = queue.pop() { + // Find the record in the superset of records + let found_package = if virtual_packages.contains(&package) { + continue; + } else if let Some(record) = records.get(&package) { + record + } else { + unreachable!("missing package '{}' from superset", package.as_source()); + }; + + // Find all the dependencies of the package and add them to the queue + for dependency in found_package.package_record.depends.iter() { + let dependency_name = PackageName::new_unchecked( + dependency.split_once(' ').unwrap_or((&dependency, "")).0, + ); + if queued_names.insert(dependency_name.clone()) { + queue.push(dependency_name); + } + } + + result.push(found_package.clone()); + } + + result +} diff --git a/src/project/environment.rs b/src/project/environment.rs index 158f1bce4..fb949eada 100644 --- a/src/project/environment.rs +++ b/src/project/environment.rs @@ -2,7 +2,7 @@ use super::{ dependencies::Dependencies, errors::{UnknownTask, UnsupportedPlatformError}, manifest::{self, EnvironmentName, Feature, FeatureName, SystemRequirements}, - PyPiRequirement, SpecType, + PyPiRequirement, SolveGroup, SpecType, }; use crate::task::TaskName; use crate::{task::Task, Project}; @@ -46,6 +46,15 @@ impl Debug for Environment<'_> { } } +impl<'p> PartialEq for Environment<'p> { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self.project, other.project) + && std::ptr::eq(self.environment, other.environment) + } +} + +impl<'p> Eq for Environment<'p> {} + impl<'p> Environment<'p> { /// Returns true if this environment is the default environment. pub fn is_default(&self) -> bool { @@ -62,6 +71,18 @@ impl<'p> Environment<'p> { &self.environment.name } + /// Returns the solve group to which this environment belongs, or `None` if no solve group was + /// specified. + pub fn solve_group(&self) -> Option> { + self.environment + .solve_group + .map(|solve_group_idx| SolveGroup { + project: self.project, + solve_group: &self.project.manifest.parsed.solve_groups.solve_groups + [solve_group_idx], + }) + } + /// Returns the manifest definition of this environment. See the documentation of /// [`Environment`] for an overview of the difference between [`manifest::Environment`] and /// [`Environment`]. @@ -81,7 +102,7 @@ impl<'p> Environment<'p> { pub fn features( &self, include_default: bool, - ) -> impl Iterator + DoubleEndedIterator + '_ { + ) -> impl Iterator + DoubleEndedIterator + 'p { let environment_features = self.environment.features.iter().map(|feature_name| { self.project .manifest @@ -205,7 +226,31 @@ impl<'p> Environment<'p> { /// The system requirements of the environment are the union of the system requirements of all /// the features that make up the environment. If multiple features specify a requirement for /// the same system package, the highest is chosen. + /// + /// If an environment defines a solve group the system requirements of all environments in the + /// solve group are also combined. This means that if two environments in the same solve group + /// specify conflicting system requirements that the highest system requirements are chosen. + /// + /// This is done to ensure that the requirements of all environments in the same solve group are + /// compatible with each other. + /// + /// If you want to get the system requirements for this environment without taking the solve + /// group into account, use the [`Self::local_system_requirements`] method. pub fn system_requirements(&self) -> SystemRequirements { + if let Some(solve_group) = self.solve_group() { + solve_group.system_requirements() + } else { + self.local_system_requirements() + } + } + + /// Returns the system requirements for this environment without taking the solve-group into + /// account. + /// + /// The system requirements of the environment are the union of the system requirements of all + /// the features that make up the environment. If multiple features specify a requirement for + /// the same system package, the highest is chosen. + pub fn local_system_requirements(&self) -> SystemRequirements { self.features(true) .map(|feature| &feature.system_requirements) .fold(SystemRequirements::default(), |acc, req| { @@ -300,14 +345,6 @@ impl<'p> Hash for Environment<'p> { } } -impl<'p> PartialEq for Environment<'p> { - fn eq(&self, other: &Self) -> bool { - self.environment.name == other.environment.name - } -} - -impl<'p> Eq for Environment<'p> {} - #[cfg(test)] mod tests { use super::*; diff --git a/src/project/manifest/environment.rs b/src/project/manifest/environment.rs index 6200b237f..95e62c003 100644 --- a/src/project/manifest/environment.rs +++ b/src/project/manifest/environment.rs @@ -54,6 +54,12 @@ impl fmt::Display for EnvironmentName { } } +impl PartialEq for EnvironmentName { + fn eq(&self, other: &str) -> bool { + self.as_str() == other + } +} + #[derive(Debug, Clone, Error, Diagnostic, PartialEq)] #[error("Failed to parse environment name '{attempted_parse}', please use only lowercase letters, numbers and dashes")] pub struct ParseEnvironmentNameError { @@ -111,7 +117,7 @@ pub struct Environment { /// An optional solver-group. Multiple environments can share the same solve-group. All the /// dependencies of the environment that share the same solve-group will be solved together. - pub solve_group: Option, + pub solve_group: Option, } /// Helper struct to deserialize the environment from TOML. @@ -119,6 +125,7 @@ pub struct Environment { #[derive(Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] pub(super) struct TomlEnvironment { + #[serde(default)] pub features: PixiSpanned>, pub solve_group: Option, } @@ -127,27 +134,7 @@ pub(super) enum TomlEnvironmentMapOrSeq { Map(TomlEnvironment), Seq(Vec), } -impl TomlEnvironmentMapOrSeq { - pub fn into_environment(self, name: EnvironmentName) -> Environment { - match self { - TomlEnvironmentMapOrSeq::Map(TomlEnvironment { - features, - solve_group, - }) => Environment { - name, - features: features.value, - features_source_loc: features.span, - solve_group, - }, - TomlEnvironmentMapOrSeq::Seq(features) => Environment { - name, - features, - features_source_loc: None, - solve_group: None, - }, - } - } -} + impl<'de> Deserialize<'de> for TomlEnvironmentMapOrSeq { fn deserialize(deserializer: D) -> Result where diff --git a/src/project/manifest/mod.rs b/src/project/manifest/mod.rs index 175a16730..b9bfb74d1 100644 --- a/src/project/manifest/mod.rs +++ b/src/project/manifest/mod.rs @@ -24,7 +24,7 @@ pub use metadata::ProjectMetadata; use miette::{miette, Diagnostic, IntoDiagnostic, LabeledSpan, NamedSource}; pub use python::PyPiRequirement; use rattler_conda_types::{MatchSpec, NamelessMatchSpec, PackageName, Platform, Version}; -use serde_with::{serde_as, DisplayFromStr, Map, PickFirst}; +use serde_with::{serde_as, DisplayFromStr, PickFirst}; use std::hash::Hash; use std::{ collections::HashMap, @@ -632,7 +632,15 @@ impl Manifest { where Q: Hash + Equivalent, { - self.parsed.environments.get(name) + self.parsed.environments.find(name) + } + + /// Returns the solve group with the given name or `None` if it does not exist. + pub fn solve_group(&self, name: &Q) -> Option<&SolveGroup> + where + Q: Hash + Equivalent, + { + self.parsed.solve_groups.find(name) } } @@ -693,6 +701,61 @@ fn get_or_insert_toml_table<'a>( Ok(current_table) } +/// The environments in the project. +#[derive(Debug, Clone, Default)] +pub struct Environments { + /// A list of all environments, in the order they are defined in the manifest. + pub(super) environments: Vec, + + /// A map of all environments, indexed by their name. + pub(super) by_name: IndexMap, +} + +impl Environments { + /// Returns the environment with the given name or `None` if it does not exist. + pub fn find(&self, name: &Q) -> Option<&Environment> + where + Q: Hash + Equivalent, + { + let index = self.by_name.get(name)?; + Some(&self.environments[*index]) + } + + /// Returns an iterator over all the environments in the project. + pub fn iter(&self) -> impl Iterator + '_ { + self.environments.iter() + } +} + +/// A solve group is a group of environments that are solved together. +#[derive(Debug, Clone)] +pub struct SolveGroup { + pub name: String, + pub environments: Vec, +} + +#[derive(Debug, Clone, Default)] +pub struct SolveGroups { + pub(super) solve_groups: Vec, + pub(super) by_name: IndexMap, +} + +impl SolveGroups { + /// Returns the solve group with the given name or `None` if it does not exist. + pub fn find(&self, name: &Q) -> Option<&SolveGroup> + where + Q: Hash + Equivalent, + { + let index = self.by_name.get(name)?; + Some(&self.solve_groups[*index]) + } + + /// Returns an iterator over all the solve groups in the project. + pub fn iter(&self) -> impl Iterator + '_ { + self.solve_groups.iter() + } +} + /// Describes the contents of a project manifest. #[derive(Debug, Clone)] pub struct ProjectManifest { @@ -703,7 +766,10 @@ pub struct ProjectManifest { pub features: IndexMap, /// All the environments defined in the project. - pub environments: IndexMap, + pub environments: Environments, + + /// The solve groups that are part of the project. + pub solve_groups: SolveGroups, } impl ProjectManifest { @@ -735,12 +801,20 @@ impl ProjectManifest { /// feature. The default environment can be overwritten by a environment named `default`. pub fn default_environment(&self) -> &Environment { let envs = &self.environments; - envs.get(&EnvironmentName::Named(String::from( + envs.find(&EnvironmentName::Named(String::from( consts::DEFAULT_ENVIRONMENT_NAME, ))) - .or_else(|| envs.get(&EnvironmentName::Default)) + .or_else(|| envs.find(&EnvironmentName::Default)) .expect("default environment should always exist") } + + /// Returns the environment with the given name or `None` if it does not exist. + pub fn environment(&self, name: &Q) -> Option<&Environment> + where + Q: Hash + Equivalent, + { + self.environments.find(name) + } } impl<'de> Deserialize<'de> for ProjectManifest { @@ -795,8 +869,7 @@ impl<'de> Deserialize<'de> for ProjectManifest { /// The environments the project can create. #[serde(default)] - #[serde_as(as = "Map<_, _>")] - environments: Vec<(EnvironmentName, TomlEnvironmentMapOrSeq)>, + environments: IndexMap, } let toml_manifest = TomlProjectManifest::deserialize(deserializer)?; @@ -831,14 +904,6 @@ impl<'de> Deserialize<'de> for ProjectManifest { targets: Targets::from_default_and_user_defined(default_target, toml_manifest.target), }; - // Construct a default environment - let default_environment = Environment { - name: EnvironmentName::Default, - features: Vec::new(), - features_source_loc: None, - solve_group: None, - }; - // Construct the features including the default feature let features: IndexMap = IndexMap::from_iter([(FeatureName::Default, default_feature)]); @@ -853,22 +918,72 @@ impl<'de> Deserialize<'de> for ProjectManifest { let features = features.into_iter().chain(named_features).collect(); // Construct the environments including the default environment - let environments: IndexMap = - IndexMap::from_iter([(EnvironmentName::Default, default_environment)]); - let named_environments = toml_manifest + let mut environments = Environments::default(); + let mut solve_groups = SolveGroups::default(); + + // Add the default environment first if it was not redefined. + if !toml_manifest .environments - .into_iter() - .map(|(name, t_env)| { - let env = t_env.into_environment(name.clone()); - (name, env) - }) - .collect::>(); - let environments = environments.into_iter().chain(named_environments).collect(); + .contains_key(&EnvironmentName::Default) + { + environments.environments.push(Environment { + name: EnvironmentName::Default, + features: Vec::new(), + features_source_loc: None, + solve_group: None, + }); + environments.by_name.insert(EnvironmentName::Default, 0); + } + + // Add all named environments + for (name, env) in toml_manifest.environments { + let environment_idx = environments.environments.len(); + environments.by_name.insert(name.clone(), environment_idx); + + // Decompose the TOML + let (features, features_source_loc, solve_group) = match env { + TomlEnvironmentMapOrSeq::Map(env) => { + (env.features.value, env.features.span, env.solve_group) + } + TomlEnvironmentMapOrSeq::Seq(features) => (features, None, None), + }; + + // Add to the solve group if defined + let solve_group = if let Some(solve_group) = solve_group { + Some(match solve_groups.by_name.get(&solve_group) { + Some(idx) => { + solve_groups.solve_groups[*idx] + .environments + .push(environment_idx); + *idx + } + None => { + let idx = solve_groups.solve_groups.len(); + solve_groups.solve_groups.push(SolveGroup { + name: solve_group.clone(), + environments: vec![environment_idx], + }); + solve_groups.by_name.insert(solve_group, idx); + idx + } + }) + } else { + None + }; + + environments.environments.push(Environment { + name, + features, + features_source_loc, + solve_group, + }); + } Ok(Self { project: toml_manifest.project, features, environments, + solve_groups, }) } } @@ -1773,6 +1888,7 @@ platforms = ["linux-64", "win-64"] [environments] default = ["py39"] + standard = { solve-group = "test" } cuda = ["cuda", "py310"] test1 = {features = ["test", "py310"], solve-group = "test"} test2 = {features = ["py39"], solve-group = "test"} @@ -1792,13 +1908,28 @@ platforms = ["linux-64", "win-64"] .environment(&EnvironmentName::Named("test1".to_string())) .unwrap(); assert_eq!(test1_env.features, vec!["test", "py310"]); - assert_eq!(test1_env.solve_group, Some(String::from("test"))); + assert_eq!( + test1_env + .solve_group + .map(|idx| manifest.parsed.solve_groups.solve_groups[idx].name.as_str()), + Some("test") + ); let test2_env = manifest .environment(&EnvironmentName::Named("test2".to_string())) .unwrap(); assert_eq!(test2_env.features, vec!["py39"]); - assert_eq!(test2_env.solve_group, Some(String::from("test"))); + assert_eq!( + test2_env + .solve_group + .map(|idx| manifest.parsed.solve_groups.solve_groups[idx].name.as_str()), + Some("test") + ); + + assert_eq!( + test1_env.solve_group, test2_env.solve_group, + "both environments should share the same solve group" + ); } #[test] diff --git a/src/project/manifest/validation.rs b/src/project/manifest/validation.rs index ae818cf5f..4c6dcfb38 100644 --- a/src/project/manifest/validation.rs +++ b/src/project/manifest/validation.rs @@ -38,7 +38,7 @@ impl ProjectManifest { // Check if all features are used in environments, warn if not. let mut features_used = HashSet::new(); - for env in self.environments.values() { + for env in self.environments.iter() { for feature in env.features.iter() { features_used.insert(feature); } @@ -81,7 +81,7 @@ impl ProjectManifest { check_file_existence(&self.project.readme)?; // Validate the environments defined in the project - for (_name, env) in self.environments.iter() { + for env in self.environments.environments.iter() { if let Err(report) = self.validate_environment(env) { return Err(report.with_source_code(source)); } diff --git a/src/project/mod.rs b/src/project/mod.rs index 5094b355d..b498b23d4 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -2,6 +2,7 @@ mod dependencies; mod environment; pub mod errors; pub mod manifest; +mod solve_group; pub mod virtual_packages; use indexmap::{Equivalent, IndexMap, IndexSet}; @@ -33,6 +34,7 @@ use url::Url; use crate::task::TaskName; pub use dependencies::Dependencies; pub use environment::Environment; +pub use solve_group::SolveGroup; /// The dependency types we support #[derive(Debug, Copy, Clone)] @@ -256,13 +258,38 @@ impl Project { .parsed .environments .iter() - .map(|(_name, env)| Environment { + .map(|env| Environment { project: self, environment: env, }) .collect() } + /// Returns all the solve groups in the project. + pub fn solve_groups(&self) -> Vec { + self.manifest + .parsed + .solve_groups + .iter() + .map(|group| SolveGroup { + project: self, + solve_group: group, + }) + .collect() + } + + /// Returns the solve group with the given name or `None` if no such group exists. + pub fn solve_group(&self, name: &str) -> Option { + self.manifest + .parsed + .solve_groups + .find(name) + .map(|group| SolveGroup { + project: self, + solve_group: group, + }) + } + /// Returns the channels used by this project. /// /// TODO: Remove this function and use the channels from the default environment instead. diff --git a/src/project/solve_group.rs b/src/project/solve_group.rs new file mode 100644 index 000000000..9967f2e05 --- /dev/null +++ b/src/project/solve_group.rs @@ -0,0 +1,253 @@ +use super::{manifest, Dependencies, Environment, Project}; +use crate::project::manifest::{PyPiRequirement, SystemRequirements}; +use crate::{FeatureName, SpecType}; +use indexmap::{IndexMap, IndexSet}; +use itertools::{Either, Itertools}; +use rattler_conda_types::{Channel, Platform}; +use std::borrow::Cow; +use std::hash::Hash; + +/// A grouping of environments that are solved together. +#[derive(Debug, Clone)] +pub struct SolveGroup<'p> { + /// The project that the group is part of. + pub(super) project: &'p Project, + + /// A reference to the solve group in the manifest + pub(super) solve_group: &'p manifest::SolveGroup, +} + +impl PartialEq for SolveGroup<'_> { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self.solve_group, other.solve_group) + && std::ptr::eq(self.project, other.project) + } +} + +impl Eq for SolveGroup<'_> {} + +impl Hash for SolveGroup<'_> { + fn hash(&self, state: &mut H) { + std::ptr::hash(self.solve_group, state); + std::ptr::hash(self.project, state); + } +} + +impl<'p> SolveGroup<'p> { + /// Returns the project to which the group belongs. + pub fn project(&self) -> &'p Project { + self.project + } + + /// The name of the group + pub fn name(&self) -> &str { + &self.solve_group.name + } + + /// Returns an iterator over all the environments that are part of the group. + pub fn environments(&self) -> impl Iterator> + DoubleEndedIterator + 'p { + self.solve_group + .environments + .iter() + .map(|env_idx| Environment { + project: self.project, + environment: &self.project.manifest.parsed.environments.environments[*env_idx], + }) + } + + /// Returns all features that are part of the solve group. + /// + /// If `include_default` is `true` the default feature is also included. + /// + /// All features of all environments are combined and deduplicated. + pub fn features( + &self, + include_default: bool, + ) -> impl Iterator + DoubleEndedIterator + 'p { + self.environments() + .flat_map(move |env| env.features(include_default)) + .unique_by(|feat| &feat.name) + } + + /// Returns the system requirements for this solve group. + /// + /// The system requirements of the solve group are the union of the system requirements of all + /// the environments that share the same solve group. If multiple environments specify a + /// requirement for the same system package, the highest is chosen. + pub fn system_requirements(&self) -> SystemRequirements { + self.features(true) + .map(|feature| &feature.system_requirements) + .fold(SystemRequirements::default(), |acc, req| { + acc.union(req) + .expect("system requirements should have been validated upfront") + }) + } + + /// Returns all the dependencies of the solve group. + /// + /// The dependencies of all features of all environments are combined. This means that if two + /// features define a requirement for the same package that both requirements are returned. The + /// different requirements per package are sorted in the same order as the features they came + /// from. + pub fn dependencies(&self, kind: Option, platform: Option) -> Dependencies { + self.features(true) + .filter_map(|feat| feat.dependencies(kind, platform)) + .map(|deps| Dependencies::from(deps.into_owned())) + .reduce(|acc, deps| acc.union(&deps)) + .unwrap_or_default() + } + + /// Returns all the pypi dependencies of the solve group. + /// + /// The dependencies of all features of all environments in the solve group are combined. This + /// means that if two features define a requirement for the same package that both requirements + /// are returned. The different requirements per package are sorted in the same order as the + /// features they came from. + pub fn pypi_dependencies( + &self, + platform: Option, + ) -> IndexMap> { + self.features(true) + .filter_map(|f| f.pypi_dependencies(platform)) + .fold(IndexMap::default(), |mut acc, deps| { + // Either clone the values from the Cow or move the values from the owned map. + let deps_iter = match deps { + Cow::Borrowed(borrowed) => Either::Left( + borrowed + .into_iter() + .map(|(name, spec)| (name.clone(), spec.clone())), + ), + Cow::Owned(owned) => Either::Right(owned.into_iter()), + }; + + // Add the requirements to the accumulator. + for (name, spec) in deps_iter { + acc.entry(name).or_default().push(spec); + } + + acc + }) + } + + /// Returns the channels associated with this solve group. + /// + /// Users can specify custom channels on a per-feature basis. This method collects and + /// deduplicates all the channels from all the features in the order they are defined in the + /// manifest. + /// + /// If a feature does not specify any channel the default channels from the project metadata are + /// used instead. However, these are not considered during deduplication. This means the default + /// channels are always added to the end of the list. + pub fn channels(&self) -> IndexSet<&'p Channel> { + self.features(true) + .filter_map(|feature| match feature.name { + // Use the user-specified channels of each feature if the feature defines them. Only + // for the default feature do we use the default channels from the project metadata + // if the feature itself does not specify any channels. This guarantees that the + // channels from the default feature are always added to the end of the list. + FeatureName::Named(_) => feature.channels.as_deref(), + FeatureName::Default => feature + .channels + .as_deref() + .or(Some(&self.project.manifest.parsed.project.channels)), + }) + .flatten() + // The prioritized channels contain a priority, sort on this priority. + // Higher priority comes first. [-10, 1, 0 ,2] -> [2, 1, 0, -10] + .sorted_by(|a, b| { + let a = a.priority.unwrap_or(0); + let b = b.priority.unwrap_or(0); + b.cmp(&a) + }) + .map(|prioritized_channel| &prioritized_channel.channel) + .collect() + } + + /// Returns true if any of the environments contain a feature with any reference to a pypi dependency. + pub fn has_pypi_dependencies(&self) -> bool { + self.features(true).any(|f| f.has_pypi_dependencies()) + } +} + +#[cfg(test)] +mod tests { + use crate::Project; + use itertools::Itertools; + use rattler_conda_types::PackageName; + use std::collections::HashSet; + use std::path::Path; + + #[test] + fn test_solve_group() { + let project = Project::from_str( + Path::new(""), + r#" + [project] + name = "foobar" + channels = ["conda-forge"] + platforms = ["linux-64", "osx-64"] + + [dependencies] + a = "*" + + [feature.foo.dependencies] + b = "*" + + [feature.bar.dependencies] + c = "*" + + [feature.bar.system-requirements] + cuda = "12.0" + + [environments] + foo = { features=["foo"], solve-group="group1" } + bar = { features=["bar"], solve-group="group1" } + "#, + ) + .unwrap(); + + let environments = project.environments(); + assert_eq!(environments.len(), 3); + + let default_environment = project.default_environment(); + let foo_environment = project.environment("foo").unwrap(); + let bar_environment = project.environment("bar").unwrap(); + + let solve_groups = project.solve_groups(); + assert_eq!(solve_groups.len(), 1); + + let solve_group = solve_groups[0].clone(); + let solve_group_envs = solve_group.environments().collect_vec(); + assert_eq!(solve_group_envs.len(), 2); + assert_eq!(solve_group_envs[0].name(), "foo"); + assert_eq!(solve_group_envs[1].name(), "bar"); + + // Make sure that the environments properly reference the group + assert_eq!(foo_environment.solve_group(), Some(solve_group.clone())); + assert_eq!(bar_environment.solve_group(), Some(solve_group.clone())); + assert_eq!(default_environment.solve_group(), None); + + // Make sure that all the environments share the same system requirements, because they are + // in the same solve-group. + let foo_system_requirements = foo_environment.system_requirements(); + let bar_system_requirements = bar_environment.system_requirements(); + let default_system_requirements = default_environment.system_requirements(); + assert_eq!(foo_system_requirements.cuda, "12.0".parse().ok()); + assert_eq!(bar_system_requirements.cuda, "12.0".parse().ok()); + assert_eq!(default_system_requirements.cuda, None); + + // Check that the solve group contains all the dependencies of its environments + let package_names: HashSet<_> = solve_group + .dependencies(None, None) + .names() + .cloned() + .collect(); + assert_eq!( + package_names, + ["a", "b", "c"] + .into_iter() + .map(PackageName::new_unchecked) + .collect() + ); + } +} diff --git a/src/utils/spanned.rs b/src/utils/spanned.rs index 57b283fb3..0b69c8b59 100644 --- a/src/utils/spanned.rs +++ b/src/utils/spanned.rs @@ -30,6 +30,15 @@ pub struct PixiSpanned { pub value: T, } +impl Default for PixiSpanned { + fn default() -> Self { + Self { + span: None, + value: T::default(), + } + } +} + impl From for PixiSpanned { fn from(value: T) -> Self { Self { span: None, value } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 62638f5b6..68faefd10 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -19,7 +19,7 @@ use pixi::{ }; use rattler_conda_types::{MatchSpec, Platform}; -use miette::{Diagnostic, IntoDiagnostic}; +use miette::{Context, Diagnostic, IntoDiagnostic}; use pixi::cli::run::get_task_env; use pixi::cli::LockFileUsageArgs; use pixi::task::TaskName; @@ -153,6 +153,15 @@ impl PixiControl { Ok(PixiControl { tmpdir: tempdir }) } + /// Creates a new PixiControl instance from an existing manifest + pub fn from_manifest(manifest: &str) -> miette::Result { + let pixi = Self::new()?; + std::fs::write(&pixi.manifest_path(), manifest) + .into_diagnostic() + .context("failed to write pixi.toml")?; + Ok(pixi) + } + /// Loads the project manifest and returns it. pub fn project(&self) -> miette::Result { Project::load_or_else_discover(Some(&self.manifest_path())) @@ -297,12 +306,23 @@ impl PixiControl { } } - /// Get the associated lock file + /// Load the current lock-file. + /// + /// If you want to lock-file to be up-to-date with the project call [`Self::up_to_date_lock_file`]. pub async fn lock_file(&self) -> miette::Result { let project = Project::load_or_else_discover(Some(&self.manifest_path()))?; pixi::load_lock_file(&project).await } + /// Load the current lock-file and makes sure that its up to date with the project. + pub async fn up_to_date_lock_file(&self) -> miette::Result { + let project = self.project()?; + Ok(project + .up_to_date_lock_file(UpdateLockFileOptions::default()) + .await? + .lock_file) + } + pub fn tasks(&self) -> TasksControl { TasksControl { pixi: self } } diff --git a/tests/solve_group_tests.rs b/tests/solve_group_tests.rs new file mode 100644 index 000000000..913838f51 --- /dev/null +++ b/tests/solve_group_tests.rs @@ -0,0 +1,85 @@ +use crate::common::{ + package_database::{Package, PackageDatabase}, + LockFileExt, PixiControl, +}; +use rattler_conda_types::Platform; +use tempfile::TempDir; +use url::Url; + +mod common; + +#[tokio::test] +async fn add_functionality() { + let mut package_database = PackageDatabase::default(); + + // Add a package `foo` with 3 different versions + package_database.add_package(Package::build("foo", "1").finish()); + package_database.add_package(Package::build("foo", "2").finish()); + package_database.add_package(Package::build("foo", "3").finish()); + + // Add a package `bar` with 1 version that restricts `foo` to version 2 or lower. + package_database.add_package( + Package::build("bar", "1") + .with_dependency("foo <3") + .finish(), + ); + + // Write the repodata to disk + let channel_dir = TempDir::new().unwrap(); + package_database + .write_repodata(channel_dir.path()) + .await + .unwrap(); + + let channel = Url::from_file_path(channel_dir.path()).unwrap(); + let platform = Platform::current(); + let pixi = PixiControl::from_manifest(&format!( + r#" + [project] + name = "test-solve-group" + channels = ["{channel}"] + platforms = ["{platform}"] + + [dependencies] + foo = "*" + + [feature.test.dependencies] + bar = "*" + + [environments] + prod = {{ solve-group = "prod" }} + test = {{ features=["test"], solve-group = "prod" }} + "# + )) + .unwrap(); + + // Get an up-to-date lockfile + let lock_file = pixi.up_to_date_lock_file().await.unwrap(); + + assert!( + lock_file.contains_match_spec("default", platform, "foo ==3"), + "default should have the highest version of foo" + ); + assert!( + !lock_file.contains_match_spec("default", platform, "bar"), + "default should not contain bar" + ); + + assert!( + lock_file.contains_match_spec("prod", platform, "foo ==2"), + "prod should have foo==2 because it shares the solve group with test" + ); + assert!( + !lock_file.contains_match_spec("prod", platform, "bar"), + "prod should not contain bar" + ); + + assert!( + lock_file.contains_match_spec("test", platform, "foo ==2"), + "test should have foo==2 because bar depends on foo <3" + ); + assert!( + lock_file.contains_match_spec("test", platform, "bar"), + "test should contain bar" + ); +}