diff --git a/.github/workflows/build-labmaze.yml b/.github/workflows/build-labmaze.yml new file mode 100644 index 000000000..80513007c --- /dev/null +++ b/.github/workflows/build-labmaze.yml @@ -0,0 +1,274 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +# +# labmaze has no upstream CI: it ships no .github/workflows/ at all. This +# workflow instead drives its setup.py's own BazelExtension build directly, +# https://github.com/google-deepmind/labmaze/blob/6d7e8f058428025cd4253f1ef6a1ed6618d9b0ea/setup.py +--- +name: Build labmaze wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'labmaze version to build' + required: true + default: '1.0.6' + pull_request: + paths: + - '.github/workflows/build-labmaze.yml' + - 'patches/labmaze/**' + +run-name: build-labmaze - ${{ inputs.version || '1.0.6' }} + +concurrency: + group: ${{ github.workflow }}-${{ inputs.version || '1.0.6' }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +env: + LABMAZE_VERSION: ${{ inputs.version || '1.0.6' }} + # Upstream never tagged the 1.0.6 release (google-deepmind/labmaze#5); this is + # master's tip, proven byte-identical to the PyPI 1.0.6 sdist. Move together. + LABMAZE_REF: 6d7e8f058428025cd4253f1ef6a1ed6618d9b0ea + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + # Fetched from the bazel releases, which publish no riscv64 binary; 7.5.0 is + # the version this repo bootstraps from source. + BAZEL_VERSION: '7.5.0' + # versions bazel 7.5.0's MODULE.bazel pins; both need a riscv64 fix below. + RULES_PYTHON_VERSION: '0.33.2' + RULES_JAVA_VERSION: '7.6.5' + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + + bazel: + needs: [setup] + name: Bootstrap bazel (riscv64) + runs-on: ubuntu-24.04-riscv + timeout-minutes: 720 + + steps: + - name: Restore bazel binary + id: cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: bazel-bin + key: bazel-${{ env.BAZEL_VERSION }}-manylinux_riscv64 + + - name: Bootstrap bazel ${{ env.BAZEL_VERSION }} + if: steps.cache.outputs.cache-hit != 'true' + run: | + mkdir -p bazel-bin + docker run --rm -i --network=host \ + -v "${GITHUB_WORKSPACE}:/work" \ + -w /work \ + -e BAZEL_VERSION="${BAZEL_VERSION}" \ + -e RULES_PYTHON_VERSION="${RULES_PYTHON_VERSION}" \ + -e RULES_JAVA_VERSION="${RULES_JAVA_VERSION}" \ + "${MANYLINUX_RISCV64_IMAGE}" \ + bash <<'SCRIPT' + set -eux + + dnf install -y --disablerepo=extras --setopt=install_weak_deps=False java-21-openjdk-devel zip unzip + JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")" + export JAVA_HOME + + # rules_python 0.33.2's PLATFORMS has no riscv64 entry, aborting the bootstrap + # (bazelbuild/bazel#23018). Any linux entry is a safe stand-in: the toolchain it + # names is never selected on a riscv64 host. Fixed in bazel 8.2.0; the 7.x + # backport is open. + mkdir -p /tmp/rules_python + curl -fsSLo /tmp/rules_python.tar.gz "https://github.com/bazel-contrib/rules_python/releases/download/${RULES_PYTHON_VERSION}/rules_python-${RULES_PYTHON_VERSION}.tar.gz" + tar -xzf /tmp/rules_python.tar.gz -C /tmp/rules_python --strip-components=1 + sed -i 's|fail("No platform declared for host OS {} on arch {}".format(os_name, arch))|return "x86_64-unknown-linux-gnu"|' \ + /tmp/rules_python/python/private/toolchains_repo.bzl + + # rules_java 7.x maps riscv64 to a stray-colon include path, so a JNI library + # can't find jni_md.h. Fixed in rules_java 8.x, never backported. + mkdir -p /tmp/rules_java + curl -fsSLo /tmp/rules_java.tar.gz "https://github.com/bazelbuild/rules_java/releases/download/${RULES_JAVA_VERSION}/rules_java-${RULES_JAVA_VERSION}.tar.gz" + tar -xzf /tmp/rules_java.tar.gz -C /tmp/rules_java + sed -i 's|\[":include/linux"\]|["include/linux"]|g' /tmp/rules_java/toolchains/BUILD + + mkdir -p /tmp/bazel-src + cd /tmp/bazel-src + curl -fsSLo dist.zip "https://github.com/bazelbuild/bazel/releases/download/${BAZEL_VERSION}/bazel-${BAZEL_VERSION}-dist.zip" + unzip -q dist.zip + + EXTRA_BAZEL_ARGS="--tool_java_runtime_version=local_jdk \ + --override_module=rules_python=/tmp/rules_python \ + --override_module=rules_java=/tmp/rules_java" \ + bash ./compile.sh + install -m 0755 output/bazel /work/bazel-bin/bazel + SCRIPT + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: bazel-${{ env.BAZEL_VERSION }}-riscv64 + path: bazel-bin/bazel + if-no-files-found: error + + build_wheels: + name: Build labmaze ${{ inputs.version || '1.0.6' }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 720 + needs: [setup, bazel] + + strategy: + fail-fast: false + matrix: + # Upstream has published no wheel past cp312 (last release: Nov 2022); + # this port follows the registry's current default matrix instead. + python: ["cp312", "cp313", "cp314", "cp314t"] + + steps: + - name: Checkout labmaze ${{ inputs.version || '1.0.6' }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: google-deepmind/labmaze + ref: ${{ env.LABMAZE_REF }} + path: labmaze + fetch-depth: 1 + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + fetch-depth: 1 + persist-credentials: false + + - name: Apply patches + working-directory: labmaze + run: git apply -v ../python-wheels/patches/labmaze/${{ env.LABMAZE_VERSION }}/*.patch + + - name: Download bazel + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: bazel-${{ env.BAZEL_VERSION }}-riscv64 + path: bazel-bin + + - name: Build wheel + env: + PYTHON_TAG: ${{ matrix.python }} + run: | + mkdir -p wheelhouse + set -o pipefail + docker run --rm -i --network=host \ + -v "${GITHUB_WORKSPACE}:/work" \ + -w /work/labmaze \ + -e PYTHON_TAG \ + "${MANYLINUX_RISCV64_IMAGE}" \ + bash <<'SCRIPT' 2>&1 | tee build.log + set -eux + + dnf install -y --disablerepo=extras --setopt=install_weak_deps=False java-21-openjdk-devel zip unzip + JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")" + export JAVA_HOME + install -m 0755 /work/bazel-bin/bazel /usr/local/bin/bazel + + case "${PYTHON_TAG}" in + *t) python_dir="/opt/python/${PYTHON_TAG%t}-${PYTHON_TAG}/bin" ;; + *) python_dir="/opt/python/${PYTHON_TAG}-${PYTHON_TAG}/bin" ;; + esac + export PATH="${python_dir}:${PATH}" + python_exe="${python_dir}/python" + "${python_exe}" -m pip install -q -U pip setuptools wheel auditwheel + + # First pass just to populate bazel's external-repo cache with abseil/pybind11. + "${python_exe}" -m pip wheel . --no-deps --no-build-isolation -w /tmp/wheelbuild + + # The extension statically links these; upstream ships none of their licences, + # so collect them from the sources bazel fetched and let setuptools' own + # LICENSE* auto-detection carry them into the wheel on the second pass. + OUTPUT_BASE="$(bazel info output_base)" + for repo in com_google_absl:abseil-cpp pybind11:pybind11; do + name="${repo#*:}" + dir="$(find "${OUTPUT_BASE}/external" -mindepth 1 -maxdepth 1 -type d \ + \( -name "${repo%:*}" -o -name "${repo%:*}~" \) | head -1)" + file="$(find "${dir}" -maxdepth 1 -type f -iname 'LICENSE*' | head -1)" + cp "${file}" "LICENSE.${name}" + done + + "${python_exe}" -m pip wheel . --no-deps --no-build-isolation -w /work/labmaze/wheelhouse + auditwheel repair --plat manylinux_2_39_riscv64 -w /work/labmaze/wheelhouse_repaired /work/labmaze/wheelhouse/*.whl + SCRIPT + + - name: Upload build log + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: labmaze-${{ env.LABMAZE_VERSION }}-${{ matrix.python }}-build-log + path: labmaze/build.log + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: labmaze-${{ env.LABMAZE_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: labmaze/wheelhouse_repaired/*.whl + if-no-files-found: error + + - name: Test wheel + env: + PYTHON_TAG: ${{ matrix.python }} + run: | + docker run --rm -i --network=host \ + -v "${GITHUB_WORKSPACE}:/work" \ + -e PYTHON_TAG \ + -e PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ \ + "${MANYLINUX_RISCV64_IMAGE}" \ + bash <<'SCRIPT' + set -eux + + case "${PYTHON_TAG}" in + *t) python_dir="/opt/python/${PYTHON_TAG%t}-${PYTHON_TAG}/bin" ;; + *) python_dir="/opt/python/${PYTHON_TAG}-${PYTHON_TAG}/bin" ;; + esac + export PATH="${python_dir}:${PATH}" + python_exe="${python_dir}/python" + + "${python_exe}" -m pip install -q /work/labmaze/wheelhouse_repaired/*.whl pytest + + # /work holds the checkout, whose labmaze/ would shadow the installed wheel. + cd /tmp + "${python_exe}" -c " + from labmaze.cc.python import _defaults, _random_maze + assert _defaults.__file__.endswith('.so'), _defaults.__file__ + assert _random_maze.__file__.endswith('.so'), _random_maze.__file__ + + import labmaze + maze = labmaze.RandomMaze(height=11, width=11, random_seed=1) + assert maze.entity_layer.shape == (11, 11)" + + # The two golden-regeneration tests fail on any toolchain newer than the one + # upstream last released against (see conda-forge/labmaze-feedstock's meta.yaml), + # not on riscv64 specifically. + "${python_exe}" -m pytest --pyargs labmaze -v \ + -k "not testGoldenTwoRoom9x11Maze and not testGoldenMazeRegeneration" + + "${python_exe}" -c " + import importlib.metadata + + licenses = { + str(f).rsplit('/', 1)[1] + for f in importlib.metadata.files('labmaze') + if '.dist-info/licenses/' in str(f) + } + # setuptools' default license-files glob also matches AUTHORS* -- labmaze + # ships a top-level AUTHORS file, so it rides along automatically. + expected = {'LICENSE', 'AUTHORS', 'LICENSE.abseil-cpp', 'LICENSE.pybind11'} + assert licenses == expected, licenses" + SCRIPT + + publish: + name: Publish labmaze ${{ inputs.version || '1.0.6' }} + needs: [setup, build_wheels] + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + with: + artifact-pattern: labmaze-${{ inputs.version || '1.0.6' }}-*-manylinux_riscv64 diff --git a/patches/labmaze/1.0.6/0001-WORKSPACE-bump-vendored-pybind11-to-v3.1.0-for-Pytho.patch b/patches/labmaze/1.0.6/0001-WORKSPACE-bump-vendored-pybind11-to-v3.1.0-for-Pytho.patch new file mode 100644 index 000000000..0f69aeedd --- /dev/null +++ b/patches/labmaze/1.0.6/0001-WORKSPACE-bump-vendored-pybind11-to-v3.1.0-for-Pytho.patch @@ -0,0 +1,55 @@ +From 82d7b5a0fc0b4ff2f3b0f17534239645c35bedd3 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 11 Sep 2026 11:19:44 +0200 +Subject: [PATCH 1/2] WORKSPACE: bump vendored pybind11 to v3.1.0 for Python + 3.12+ support + +pybind11 2.10.1 (the version pinned when labmaze was last released, Nov +2022) predates Python 3.12 support (added in pybind11 2.12.0) and free +threading support (3.13t+); building any of the interpreters this port +targets needs a newer pybind11. + +pybind11 3.x also splits its headers across new include/pybind11/conduit/, +include/pybind11/eigen/ and include/pybind11/stl/ subdirectories. +detail/common.h now unconditionally includes +conduit/wrap_include_python_h.h, so bazel/pybind11.BUILD's hdrs glob needs +a matching entry or the sandboxed compile can't see the file. + +Upstream-Status: To upstream [google-deepmind/labmaze has had no commits since 2022-11-03 and issue #5 is unanswered since 2023; this port must not file issues/PRs against third-party repos] +--- + WORKSPACE | 6 +++--- + bazel/pybind11.BUILD | 1 + + 2 files changed, 4 insertions(+), 3 deletions(-) + +diff --git a/WORKSPACE b/WORKSPACE +index b997d5d..ad1c629 100644 +--- a/WORKSPACE ++++ b/WORKSPACE +@@ -41,9 +41,9 @@ http_archive( + http_archive( + name = "pybind11", + build_file = "@//bazel:pybind11.BUILD", +- sha256 = "fcf94065efcfd0a7a828bacf118fa11c43f6390d0c805e3e6342ac119f2e9976", +- strip_prefix = "pybind11-2.10.1", +- url = "https://github.com/pybind/pybind11/archive/v2.10.1.zip", ++ sha256 = "affea1ada7b39fe1d835559fcb78800c7927d514bd480ed01b71b88205a5e536", ++ strip_prefix = "pybind11-3.1.0", ++ url = "https://github.com/pybind/pybind11/archive/v3.1.0.zip", + ) + + new_local_repository( +diff --git a/bazel/pybind11.BUILD b/bazel/pybind11.BUILD +index 99b25d0..964c43e 100644 +--- a/bazel/pybind11.BUILD ++++ b/bazel/pybind11.BUILD +@@ -22,6 +22,7 @@ cc_library( + include = [ + "include/pybind11/*.h", + "include/pybind11/detail/*.h", ++ "include/pybind11/conduit/*.h", + ], + exclude = [ + "include/pybind11/common.h", +-- +2.50.1 (Apple Git-155) + diff --git a/patches/labmaze/1.0.6/0002-random_maze_test-use-assertRaisesRegex-removed-in-Py.patch b/patches/labmaze/1.0.6/0002-random_maze_test-use-assertRaisesRegex-removed-in-Py.patch new file mode 100644 index 000000000..f291f9fc1 --- /dev/null +++ b/patches/labmaze/1.0.6/0002-random_maze_test-use-assertRaisesRegex-removed-in-Py.patch @@ -0,0 +1,86 @@ +From 15fcab03019261ef7b72fd5ce325f314d10324af Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 11 Sep 2026 11:19:44 +0200 +Subject: [PATCH 2/2] random_maze_test: use assertRaisesRegex, removed in + Python 3.12 + +unittest dropped the assertRaisesRegexp alias (deprecated since Python +3.2) in 3.12, so testInvalidArguments errors out on every interpreter +this port tests against with AttributeError instead of exercising the +argument-validation paths it covers. + +Upstream-Status: To upstream [google-deepmind/labmaze has had no commits since 2022-11-03 and issue #5 is unanswered since 2023; this port must not file issues/PRs against third-party repos] +--- + labmaze/random_maze_test.py | 36 ++++++++++++++++++------------------ + 1 file changed, 18 insertions(+), 18 deletions(-) + +diff --git a/labmaze/random_maze_test.py b/labmaze/random_maze_test.py +index 51f8516..77680f8 100644 +--- a/labmaze/random_maze_test.py ++++ b/labmaze/random_maze_test.py +@@ -255,44 +255,44 @@ class RandomMazeTest(absltest.TestCase): + self.assertIn(str(maze.entity_layer), expected_mazes_2) + + def testInvalidArguments(self): +- with self.assertRaisesRegexp(ValueError, 'height.*integer'): ++ with self.assertRaisesRegex(ValueError, 'height.*integer'): + labmaze.RandomMaze(height=2.5) +- with self.assertRaisesRegexp(ValueError, 'height.*positive'): ++ with self.assertRaisesRegex(ValueError, 'height.*positive'): + labmaze.RandomMaze(height=-3) +- with self.assertRaisesRegexp(ValueError, 'height.*odd'): ++ with self.assertRaisesRegex(ValueError, 'height.*odd'): + labmaze.RandomMaze(height=4) +- with self.assertRaisesRegexp(ValueError, 'width.*integer'): ++ with self.assertRaisesRegex(ValueError, 'width.*integer'): + labmaze.RandomMaze(width=1.25) +- with self.assertRaisesRegexp(ValueError, 'width.*positive'): ++ with self.assertRaisesRegex(ValueError, 'width.*positive'): + labmaze.RandomMaze(width=-5) +- with self.assertRaisesRegexp(ValueError, 'width.*odd'): ++ with self.assertRaisesRegex(ValueError, 'width.*odd'): + labmaze.RandomMaze(width=2) +- with self.assertRaisesRegexp(ValueError, 'room_min_size.*integer'): ++ with self.assertRaisesRegex(ValueError, 'room_min_size.*integer'): + labmaze.RandomMaze(room_min_size=3.3) +- with self.assertRaisesRegexp(ValueError, 'room_min_size.*positive'): ++ with self.assertRaisesRegex(ValueError, 'room_min_size.*positive'): + labmaze.RandomMaze(room_min_size=-1) +- with self.assertRaisesRegexp(ValueError, 'room_max_size.*integer'): ++ with self.assertRaisesRegex(ValueError, 'room_max_size.*integer'): + labmaze.RandomMaze(room_max_size=4.4) +- with self.assertRaisesRegexp(ValueError, 'room_max_size.*positive'): ++ with self.assertRaisesRegex(ValueError, 'room_max_size.*positive'): + labmaze.RandomMaze(room_max_size=-2) +- with self.assertRaisesRegexp( ++ with self.assertRaisesRegex( + ValueError, 'room_min_size.*less than or equal to.*room_max_size'): + labmaze.RandomMaze(room_min_size=4, room_max_size=3) +- with self.assertRaisesRegexp(ValueError, 'retry_count.*integer'): ++ with self.assertRaisesRegex(ValueError, 'retry_count.*integer'): + labmaze.RandomMaze(retry_count=5.4) +- with self.assertRaisesRegexp(ValueError, 'retry_count.*positive'): ++ with self.assertRaisesRegex(ValueError, 'retry_count.*positive'): + labmaze.RandomMaze(retry_count=-7) +- with self.assertRaisesRegexp( ++ with self.assertRaisesRegex( + ValueError, 'extra_connection_probability.*between 0.0 and 1.0'): + labmaze.RandomMaze(extra_connection_probability=1.1) +- with self.assertRaisesRegexp(ValueError, 'max_variations.*integer'): ++ with self.assertRaisesRegex(ValueError, 'max_variations.*integer'): + labmaze.RandomMaze(max_variations=6.7) +- with self.assertRaisesRegexp( ++ with self.assertRaisesRegex( + ValueError, 'max_variations.*between 0 and 26'): + labmaze.RandomMaze(max_variations=27) +- with self.assertRaisesRegexp(ValueError, 'spawn_token.*single character'): ++ with self.assertRaisesRegex(ValueError, 'spawn_token.*single character'): + labmaze.RandomMaze(spawn_token='foo') +- with self.assertRaisesRegexp(ValueError, 'object_token.*single character'): ++ with self.assertRaisesRegex(ValueError, 'object_token.*single character'): + labmaze.RandomMaze(object_token='bar') + + if __name__ == '__main__': +-- +2.50.1 (Apple Git-155) + diff --git a/patches/labmaze/1.0.6/0003-bazel-BUILD-use-platforms-os-constraints-not-the-re.patch b/patches/labmaze/1.0.6/0003-bazel-BUILD-use-platforms-os-constraints-not-the-re.patch new file mode 100644 index 000000000..606a394a0 --- /dev/null +++ b/patches/labmaze/1.0.6/0003-bazel-BUILD-use-platforms-os-constraints-not-the-re.patch @@ -0,0 +1,42 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 11 Sep 2026 13:35:00 +0200 +Subject: [PATCH 3/3] bazel/BUILD: use @platforms//os constraints, not the + removed @bazel_tools//platforms ones + +bazel_tools stopped bundling a platforms package years ago (the OS/CPU +constraint_values moved out to the separate @platforms repo); resolving +//bazel:linux/apple/windows against @bazel_tools//platforms:* now fails +with "no such package '@@bazel_tools//platforms'" on any current bazel +release, well before riscv64 host detection is even reached. + +Upstream-Status: To upstream [google-deepmind/labmaze has had no commits since 2022-11-03 and issue #5 is unanswered since 2023; this port must not file issues/PRs against third-party repos] +--- + bazel/BUILD | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/bazel/BUILD b/bazel/BUILD +index 0000000..0000000 100644 +--- a/bazel/BUILD ++++ b/bazel/BUILD +@@ -24,15 +24,15 @@ licenses(["notice"]) + + config_setting( + name = "linux", +- constraint_values = ["@bazel_tools//platforms:linux"], ++ constraint_values = ["@platforms//os:linux"], + ) + + config_setting( + name = "apple", +- constraint_values = ["@bazel_tools//platforms:osx"], ++ constraint_values = ["@platforms//os:macos"], + ) + + config_setting( + name = "windows", +- constraint_values = ["@bazel_tools//platforms:windows"], ++ constraint_values = ["@platforms//os:windows"], + ) +-- +2.43.0