diff --git a/repos/c2sm/packages/ghex/package.py b/repos/c2sm/packages/ghex/package.py new file mode 100644 index 0000000000..b90e862e19 --- /dev/null +++ b/repos/c2sm/packages/ghex/package.py @@ -0,0 +1,99 @@ +from spack.package import * + + +class Ghex(CMakePackage, CudaPackage, ROCmPackage): + """ + GHEX is a generic halo-exchange library. + + This Spack package was originally copied from: + https://github.com/ghex-org/spack-repos/blob/main/packages/ghex/package.py + + License: ghex-org + """ + + homepage = "https://github.com/ghex-org/GHEX" + url = "https://github.com/ghex-org/GHEX/archive/refs/tags/v0.3.0.tar.gz" + git = "https://github.com/ghex-org/GHEX.git" + maintainers = ["boeschf"] + + version("0.4.1", tag="v0.4.1", submodules=True) + version("0.4.0", tag="v0.4.0", submodules=True) + version("0.3.0", tag="v0.3.0", submodules=True) + version("master", branch="master", submodules=True) + + depends_on("cxx", type="build") + + generator("ninja") + + backends = ("mpi", "ucx", "libfabric") + variant( + "backend", + default="mpi", + description="Transport backend", + values=backends, + multi=False, + ) + variant("xpmem", default=False, description="Use xpmem shared memory") + variant("python", default=True, description="Build Python bindings") + + depends_on("cmake@3.21:", type="build") + depends_on("mpi") + depends_on("boost") + depends_on("xpmem", when="+xpmem", type=("build", "run")) + + depends_on("oomph") + for backend in backends: + depends_on(f"oomph backend={backend}", when=f"backend={backend}") + depends_on("oomph+cuda", when="+cuda") + depends_on("oomph+rocm", when="+rocm") + depends_on("oomph@0.3:", when="@0.3:") + + conflicts("+cuda+rocm") + + with when("+python"): + extends("python") + depends_on("python@3.7:", type="build") + depends_on("py-pip", type="build") + depends_on("py-pybind11", type="build") + depends_on("py-mpi4py", type=("build", "run")) + depends_on("py-numpy", type=("build", "run")) + + depends_on("py-pytest", when="+python", type=("test")) + + def cmake_args(self): + spec = self.spec + + args = [ + self.define("GHEX_USE_BUNDLED_LIBS", True), + self.define("GHEX_USE_BUNDLED_GRIDTOOLS", True), + self.define("GHEX_USE_BUNDLED_GTEST", self.run_tests), + self.define("GHEX_USE_BUNDLED_OOMPH", False), + self.define("GHEX_TRANSPORT_BACKEND", + spec.variants["backend"].value.upper()), + self.define_from_variant("GHEX_USE_XPMEM", "xpmem"), + self.define_from_variant("GHEX_BUILD_PYTHON_BINDINGS", "python"), + self.define("GHEX_WITH_TESTING", self.run_tests), + ] + + if spec.satisfies("+python"): + args.append(self.define("GHEX_PYTHON_LIB_PATH", python_platlib)) + + if self.run_tests and spec.satisfies("^openmpi"): + args.append(self.define("MPIEXEC_PREFLAGS", "--oversubscribe")) + + if "+cuda" in spec and spec.variants["cuda_arch"].value != "none": + arch_str = ";".join(spec.variants["cuda_arch"].value) + args.append(self.define("CMAKE_CUDA_ARCHITECTURES", arch_str)) + args.append(self.define("GHEX_USE_GPU", True)) + args.append(self.define("GHEX_GPU_TYPE", "NVIDIA")) + + if "+rocm" in spec and spec.variants["amdgpu_target"].value != "none": + arch_str = ";".join(spec.variants["amdgpu_target"].value) + args.append(self.define("CMAKE_HIP_ARCHITECTURES", arch_str)) + args.append(self.define("GHEX_USE_GPU", True)) + args.append(self.define("GHEX_GPU_TYPE", "AMD")) + + if spec.satisfies("~cuda~rocm"): + args.append(self.define("GHEX_USE_GPU", False)) + + return args diff --git a/repos/c2sm/packages/hwmalloc/cmake_install_path.patch b/repos/c2sm/packages/hwmalloc/cmake_install_path.patch new file mode 100644 index 0000000000..fa6fde13d8 --- /dev/null +++ b/repos/c2sm/packages/hwmalloc/cmake_install_path.patch @@ -0,0 +1,27 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index d5420e0..35dbe56 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -105,11 +105,11 @@ install(FILES ${PROJECT_BINARY_DIR}/include/hwmalloc/config.hpp + install(EXPORT HWMALLOC-targets + FILE HWMALLOC-targets.cmake + NAMESPACE HWMALLOC:: +- DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake) ++ DESTINATION ${CMAKE_INSTALL_LIBDIR}/hwmalloc/cmake) + + configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/HWMALLOCConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/HWMALLOCConfig.cmake +- INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake) ++ INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/hwmalloc/cmake) + + write_basic_package_version_file(HWMALLOCConfigVersion.cmake + VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion) +@@ -120,7 +120,7 @@ install( + ${CMAKE_CURRENT_BINARY_DIR}/HWMALLOCConfigVersion.cmake + ${CMAKE_CURRENT_LIST_DIR}/cmake/FindNUMA.cmake + DESTINATION +- ${CMAKE_INSTALL_LIBDIR}/cmake) ++ ${CMAKE_INSTALL_LIBDIR}/hwmalloc/cmake) + + export(EXPORT HWMALLOC-targets + FILE "${CMAKE_CURRENT_BINARY_DIR}/HWMALLOC-targets.cmake") diff --git a/repos/c2sm/packages/hwmalloc/package.py b/repos/c2sm/packages/hwmalloc/package.py new file mode 100644 index 0000000000..b24ed67b4c --- /dev/null +++ b/repos/c2sm/packages/hwmalloc/package.py @@ -0,0 +1,67 @@ +from spack.package import * + + +class Hwmalloc(CMakePackage, CudaPackage, ROCmPackage): + """ + HWMALLOC is a allocator which supports memory registration for e.g. remote memory access + + This Spack package was originally copied from: + https://github.com/ghex-org/spack-repos/blob/main/packages/hwmalloc/package.py + + License: ghex-org + """ + + homepage = "https://github.com/ghex-org/hwmalloc" + url = "https://github.com/ghex-org/hwmalloc/archive/refs/tags/v0.3.0.tar.gz" + git = "https://github.com/ghex-org/hwmalloc.git" + maintainers = ["boeschf"] + + version("0.3.0", + sha256= + "d4d4ac6087a806600d79fb62c02719ca3d58a412968fe1ef4a2fd58d9e7ee950") + version("0.2.0", + sha256= + "734758a390a3258b86307e4aef50a7ca2e5d0e2e579f18aeefcd05397e114419") + version("0.1.0", + sha256= + "06e9bfcef0ecce4d19531ccbe03592b502d1281c7a092bc0ff51ca187899b21c") + version("master", branch="master") + + depends_on("cxx", type="build") + + generator("ninja") + + depends_on("numactl", type=("build", "run")) + depends_on("boost", type=("build")) + depends_on("cmake@3.19:", type="build") + + variant( + "numa-throws", + default=False, + description="True if numa_tools may throw during initialization", + ) + variant("numa-local", + default=True, + description="Use numa_tools for local node allocations") + variant("logging", default=False, description="print logging info to cerr") + + patch("cmake_install_path.patch", when="@:0.3.0", level=1) + + def cmake_args(self): + args = [ + self.define_from_variant("HWMALLOC_NUMA_THROWS", "numa-throws"), + self.define_from_variant("HWMALLOC_NUMA_FOR_LOCAL", "numa-local"), + self.define_from_variant("HWMALLOC_ENABLE_LOGGING", "logging"), + self.define("HWMALLOC_WITH_TESTING", self.run_tests), + ] + + if "+cuda" in self.spec: + args.append(self.define("HWMALLOC_ENABLE_DEVICE", True)) + args.append(self.define("HWMALLOC_DEVICE_RUNTIME", "cuda")) + elif "+rocm" in self.spec: + args.append(self.define("HWMALLOC_ENABLE_DEVICE", True)) + args.append(self.define("HWMALLOC_DEVICE_RUNTIME", "hip")) + else: + args.append(self.define("HWMALLOC_ENABLE_DEVICE", False)) + + return args diff --git a/repos/c2sm/packages/icon-exclaim/package.py b/repos/c2sm/packages/icon-exclaim/package.py new file mode 100755 index 0000000000..f67bc43b69 --- /dev/null +++ b/repos/c2sm/packages/icon-exclaim/package.py @@ -0,0 +1,111 @@ +from spack.pkg.c2sm.icon import Icon +import shutil +import os +import re +from collections import defaultdict +import spack.error as error + + +def validate_variant_dsl(pkg, name, value): + set_mutual_excl = set(['substitute', 'verify', 'serialize']) + set_input_var = set(value) + if len(set_mutual_excl.intersection(set_input_var)) > 1: + raise error.SpecError( + 'Cannot have more than one of (substitute, verify, serialize) in the same build' + ) + + +class IconExclaim(Icon): + git = 'git@github.com:C2SM/icon-exclaim.git' + + maintainers('jonasjucker', 'huppd') + + version('develop', branch='icon-dsl', submodules=True) + + # EXCLAIM-GT4Py specific features: + dsl_values = ('substitute', 'verify') + variant('dsl', + default='none', + validator=validate_variant_dsl, + values=('none', ) + dsl_values, + description='Build with GT4Py dynamical core', + multi=True) + + for x in dsl_values: + depends_on('icon4py', type="build", when=f"dsl={x}") + + def configure_args(self): + raw_args = super().configure_args() + + # Split into categories + args_flags = [] + icon_ldflags = [] + ldflags = [] + libs = [] + + for a in raw_args: + if a.startswith("LIBS="): + libs.append(a.split("=", 1)[1].strip()) + elif a.startswith("ICON_LDFLAGS="): + icon_ldflags.append(a.split("=", 1)[1].strip()) + elif a.startswith("LDFLAGS="): + ldflags.append(a.split("=", 1)[1].strip()) + else: + args_flags.append(a) + + # Handle DSL variants + dsl = self.spec.variants['dsl'].value + if dsl != ('none', ): + if 'substitute' in dsl: + args_flags.append('--enable-py2f=substitute') + elif 'verify' in dsl: + args_flags.append('--enable-py2f=verify') + else: + raise ValueError( + f"Unknown DSL variant '{dsl}'. " + f"Valid options are: {', '.join(('none',) + dsl_values)}") + + # Add icon4py paths and libs + icon4py_prefix = self.spec["icon4py"].prefix + bindings_dir = os.path.join(icon4py_prefix, "src") + + ldflags.append(f"-L{bindings_dir} -Wl,-rpath,{bindings_dir}") + libs.append("-licon4py_bindings") + + # Remove duplicates + icon_ldflags = list(dict.fromkeys(icon_ldflags)) + ldflags = list(dict.fromkeys(ldflags)) + libs = list(dict.fromkeys(libs)) + + # Reconstruct final configure args + final_args = args_flags + if icon_ldflags: + final_args.append("ICON_LDFLAGS=" + " ".join(icon_ldflags)) + if ldflags: + final_args.append("LDFLAGS=" + " ".join(ldflags)) + if libs: + final_args.append("LIBS=" + " ".join(libs)) + + return final_args + + def build(self, spec, prefix): + # Check the variant + dsl = self.spec.variants['dsl'].value + if dsl != ('none', ): + file = "icon4py_bindings.f90" + + bindings_dir = os.path.join(self.spec["icon4py"].prefix, "src") + src_file = os.path.join(bindings_dir, file) + + build_py2f_dir = os.path.join(self.stage.source_path, "src", + "build_py2f") + os.makedirs(build_py2f_dir, exist_ok=True) + dest_file = os.path.join(build_py2f_dir, file) + + shutil.copy2(src_file, dest_file) + print( + f"Copied {src_file} to build directory {dest_file} because +dsl is enabled" + ) + + # Proceed with the normal build + super().build(spec, prefix) diff --git a/repos/c2sm/packages/icon/package.py b/repos/c2sm/packages/icon/package.py index 8c9f41d6ef..b0b46e37a8 100755 --- a/repos/c2sm/packages/icon/package.py +++ b/repos/c2sm/packages/icon/package.py @@ -212,10 +212,6 @@ def configure_args(self): 'You use variant extra-config-args. Injecting non-variant configure arguments may potentially disrupt the build process!' ) - # Finalize the LIBS variable (we always put the real collected - # libraries to the front): - flags['LIBS'].insert(0, libs.link_flags) - # Help the libtool scripts of the bundled libraries find the correct # paths to the external libraries. Specify the library search (-L) flags # in the reversed order diff --git a/repos/c2sm/packages/icon4py/package.py b/repos/c2sm/packages/icon4py/package.py new file mode 100644 index 0000000000..dc4b40a5af --- /dev/null +++ b/repos/c2sm/packages/icon4py/package.py @@ -0,0 +1,150 @@ +import json +import os +import pathlib + +from llnl.util import tty +from spack import * + + +class Icon4py(Package): + """ICON4Py Python interface package.""" + + homepage = "https://github.com/C2SM/icon4py" + git = "https://github.com/C2SM/icon4py.git" + + # --- Versions --- + version("main", branch="main") + version( + "0.0.14", + sha256="8aadb6fe7af55fc41d09daa4e74739bd7ab01b4e", + extension="zip", + ) + + def url_for_version(self, version): + return f"https://github.com/C2SM/icon4py/archive/refs/tags/v{version}.zip" + + # --- Variants --- + variant("cuda", default=True, description="Enable CUDA support") + variant( + "cuda_arch", + default="none", + description="CUDA architecture (e.g. 80 for A100, 90 for H100)", + values=lambda x: True, # accept any user-specified string + ) + + # --- Dependencies --- + extends("python") + depends_on("python@3.11:") + + depends_on("git") + depends_on("uv@0.7:", type="build") + depends_on("bzip2", type="build") + depends_on("py-numpy") + depends_on("py-cffi") + depends_on("py-pybind11") + depends_on("py-nanobind") + depends_on("py-mpi4py") + + with when("+cuda"): + depends_on("py-cupy +cuda") + depends_on("ghex +python +cuda") + + # --- Environment setup --- + def setup_build_environment(self, env): + """Propagate CUDA architecture to dependencies.""" + cuda_arch = self.spec.variants["cuda_arch"].value + if "+cuda" in self.spec: + if cuda_arch == "none": + tty.warn( + "Building with +cuda but no cuda_arch set. " + "Consider specifying e.g. cuda_arch=80 or cuda_arch=90.") + else: + env.set("SPACK_CUDA_ARCH", cuda_arch) + tty.msg(f"Building for CUDA architecture: {cuda_arch}") + + # --- Build/install logic --- + def install(self, spec, prefix): + uv = prepare_uv() + python_spec = spec["python"] + venv_path = prefix.share.venv + + tty.msg( + f"Creating venv using Spack Python at: {python_spec.command.path}") + uv( + "venv", + "--seed", + "--relocatable", + "--system-site-packages", + str(venv_path), + "--python", + python_spec.command.path, + ) + + tty.msg("Grabbing Spack-installed packages (distributions)") + pip = Executable(venv_path.bin.pip) + spack_installed = get_installed_pkg(pip) + + tty.msg("Installing missing packages via uv sync") + uv( + "sync", + "--active", + "--extra", + "all", + "--extra", + "cuda12", + "--inexact", + "--no-editable", + "--python", + str(venv_path.bin.python), + *no_install_options([*spack_installed, "cupy-cuda12x", "ghex"]), + extra_env={ + "VIRTUAL_ENV": str(venv_path), + "CC": "gcc", + "CXX": "g++", + }, + ) + + tty.msg("Linking Spack-installed packages into venv") + pathlib.Path( + f"{venv_path.lib.python}{python_spec.version.up_to(2)}/site-packages/spack_installed.pth" + ).write_text(pythonpath_to_pth()) + + tty.msg("Running py2fgen code generator") + py2fgen = Executable(venv_path.bin.py2fgen) + py2fgen( + "icon4py.tools.py2fgen.wrappers.all_bindings", + "diffusion_init,diffusion_run,grid_init,solve_nh_init,solve_nh_run", + "icon4py_bindings", + "-o", + prefix.src, + extra_env={ + "VIRTUAL_ENV": str(venv_path), + "CC": "gcc", + "CXX": "g++", + }, + ) + + +def prepare_uv(): + uv = which("uv") + uv.add_default_env("UV_NO_CACHE", "true") + uv.add_default_env("UV_NO_MANAGED_PYTHON", "true") + uv.add_default_env("UV_PYTHON_DOWNLOADS", "never") + return uv + + +def get_installed_pkg(pip): + return [ + item["name"] + for item in json.loads(pip("list", "--format", "json", output=str)) + ] + + +def no_install_options(installed): + for name in installed: + yield "--no-install-package" + yield name + + +def pythonpath_to_pth(): + return "\n".join(os.environ.get("PYTHONPATH", "").split(":")) diff --git a/repos/c2sm/packages/oomph/package.py b/repos/c2sm/packages/oomph/package.py new file mode 100644 index 0000000000..fc7d18080e --- /dev/null +++ b/repos/c2sm/packages/oomph/package.py @@ -0,0 +1,125 @@ +from spack.package import * + + +class Oomph(CMakePackage, CudaPackage, ROCmPackage): + """ + Oomph is a non-blocking callback-based point-to-point communication library. + + This Spack package was originally copied from: + https://github.com/ghex-org/spack-repos/tree/main/packages/oomph + + Modifications from the original version: + - removed version below 0.4 + + License: ghex-org + """ + + homepage = "https://github.com/ghex-org/oomph" + url = "https://github.com/ghex-org/oomph/archive/refs/tags/v0.4.0.tar.gz" + git = "https://github.com/ghex-org/oomph.git" + maintainers = ["boeschf"] + + version("0.4.0", + sha256= + "e342c872dfe4832be047f172dc55c12951950c79da2630b071c61607ef913144") + version("main", branch="main") + + depends_on("cxx", type="build") + depends_on("fortran", type="build", when="+fortran-bindings") + + generator("ninja") + + backends = ("mpi", "ucx", "libfabric") + variant("backend", + default="mpi", + description="Transport backend", + values=backends, + multi=False) + + variant("fortran-bindings", + default=False, + description="Build Fortran bindings") + with when("+fortran-bindings"): + variant( + "fortran-fp", + default="float", + description="Floating point type", + values=("float", "double"), + multi=False, + ) + variant("fortran-openmp", + default=True, + description="Compile with OpenMP") + + variant( + "enable-barrier", + default=True, + description="Enable thread barrier (disable for task based runtime)", + ) + + depends_on("hwmalloc+cuda", when="+cuda") + depends_on("hwmalloc+rocm", when="+rocm") + depends_on("hwmalloc", when="~cuda~rocm") + + with when("backend=ucx"): + depends_on("ucx+thread_multiple") + depends_on("ucx+cuda", when="+cuda") + depends_on("ucx+rocm", when="+rocm") + variant("use-pmix", + default="False", + description="Use PMIx to establish out-of-band setup") + variant("use-spin-lock", + default="False", + description="Use pthread spin locks") + depends_on("pmix", when="+use-pmix") + + libfabric_providers = ("cxi", "efa", "gni", "psm2", "tcp", "verbs") + with when("backend=libfabric"): + variant( + "libfabric-provider", + default="tcp", + description="fabric", + values=libfabric_providers, + multi=False, + ) + for provider in libfabric_providers: + depends_on(f"libfabric fabrics={provider}", + when=f"libfabric-provider={provider}") + + depends_on("mpi") + depends_on("boost+thread") + depends_on("googletest", type=("build", "test")) + + def cmake_args(self): + args = [ + self.define_from_variant("OOMPH_BUILD_FORTRAN", + "fortran-bindings"), + self.define_from_variant("OOMPH_FORTRAN_OPENMP", "fortran-openmp"), + self.define_from_variant("OOMPH_UCX_USE_PMI", "use-pmix"), + self.define_from_variant("OOMPH_UCX_USE_SPIN_LOCK", + "use-spin-lock"), + self.define_from_variant("OOMPH_ENABLE_BARRIER", "enable-barrier"), + self.define("OOMPH_WITH_TESTING", self.run_tests), + self.define("OOMPH_GIT_SUBMODULE", False), + self.define("OOMPH_USE_BUNDLED_LIBS", False), + ] + + if self.run_tests and self.spec.satisfies("^openmpi"): + args.append(self.define("MPIEXEC_PREFLAGS", "--oversubscribe")) + + if self.spec.variants["fortran-bindings"].value == True: + args.append( + self.define("OOMPH_FORTRAN_FP", + self.spec.variants["fortran-fp"].value)) + + for backend in self.backends: + args.append( + self.define(f"OOMPH_WITH_{backend.upper()}", + self.spec.variants["backend"].value == backend)) + + if self.spec.satisfies("backend=libfabric"): + args.append( + self.define("OOMPH_LIBFABRIC_PROVIDER", + self.spec.variants["libfabric-provider"].value)) + + return args diff --git a/repos/c2sm/packages/uv/package.py b/repos/c2sm/packages/uv/package.py new file mode 100644 index 0000000000..f0700fe744 --- /dev/null +++ b/repos/c2sm/packages/uv/package.py @@ -0,0 +1,81 @@ +from spack.package import * + + +def translate_platform(platform_name: str) -> str: + if platform_name is None: + return "unknown-linux-gnu" + if platform_name == "darwin": + return "apple-darwin" + elif platform_name == "linux": + return "unknown-linux-gnu" + return "unknown-linux-gnu" + + +def translate_arch(arch_name: str) -> str: + if arch_name is None: + return "aarch64" + if arch_name in ["m1", "m2", "neoverse_v2"]: + return "aarch64" + if arch_name in ["zen3"]: + return "x86_64" + return "aarch64" + + +class Uv(Package): + """Install UV from binary releases""" + + url = "https://github.com/astral-sh/uv/releases/download/0.0.0/uv-ARCH-PLATFORM.tar.gz" + + version("0.7.12", sha256="dummy") + version("0.7.20", sha256="dummy") + version("0.9.3", sha256="dummy") + version("0.9.4", sha256="dummy") + + # Platform-specific checksums + checksums = { + ("0.7.12", "apple-darwin", "aarch64"): + "189108cd026c25d40fb086eaaf320aac52c3f7aab63e185bac51305a1576fc7e", + ("0.7.12", "unknown-linux-gnu", "aarch64"): + "23233d2e950ed8187858350da5c6803b14cbbeaef780382093bb2f2bc4ba1200", + ("0.7.12", "unknown-linux-gnu", "x86_64"): + "735891fb553d0be129f3aa39dc8e9c4c49aaa76ec17f7dfb6a732e79a714873a", + ("0.7.20", "unknown-linux-gnu", "aarch64"): + "675165f879d6833aa313ecb25ac44781e131933a984727e180b3218d2cd6c1e9", + ("0.7.20", "unknown-linux-gnu", "x86_64"): + "10f204426ff188925d22a53c1d0310d190a8d4d24513712e1b8e2ca9873f0666", + ("0.9.3", "unknown-linux-gnu", "aarch64"): + "2094a3ead5a026a2f6894c4d3f71026129c8878939a57f17f0c8246a737bed1d", + ("0.9.3", "unknown-linux-gnu", "x86_64"): + "4d6f84490da4b21bb6075ffc1c6b22e0cf37bc98d7cca8aff9fbb759093cdc23", + ("0.9.4", "unknown-linux-gnu", "aarch64"): + "c507e8cc4df18ed16533364013d93c2ace2c7f81a2a0d892a0dc833915b02e8b", + ("0.9.4", "unknown-linux-gnu", "x86_64"): + "e02f7fc102d6a1ebfa3b260b788e9adf35802be28c8d85640e83246e61519c1e", + } + + def url_for_version(self, version): + arch = translate_arch(getattr(self.spec, "target", "aarch64")) + platform = translate_platform( + getattr(self.spec, "platform", "unknown-linux-gnu")) + return f"https://github.com/astral-sh/uv/releases/download/{version}/uv-{arch}-{platform}.tar.gz" + + def do_stage(self, mirror_only=False): + version = str(self.spec.version) + arch = translate_arch(getattr(self.spec, "target", "aarch64")) + platform = translate_platform( + getattr(self.spec, "platform", "unknown-linux-gnu")) + key = (version, platform, arch) + + if key not in self.checksums: + raise InstallError( + f"Unsupported platform/arch for version {version}: {platform}-{arch}." + ) + + # Override fetcher digest with the correct checksum + self.fetcher.digest = self.checksums[key] + super().do_stage(mirror_only) + + def install(self, spec, prefix): + mkdir(prefix.bin) + install("uv", prefix.bin.uv) + install("uvx", prefix.bin.uvx)