diff --git a/.github/workflows/build-pyre-check.yml b/.github/workflows/build-pyre-check.yml new file mode 100644 index 00000000000..1274577d373 --- /dev/null +++ b/.github/workflows/build-pyre-check.yml @@ -0,0 +1,220 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow mirrors Pysa's own `pysa` workflow +# (https://github.com/facebook/Pysa/blob/v0.10.0/.github/workflows/pysa.yml), +# followed by the wheel packaging of +# https://github.com/facebook/Pysa/blob/v0.10.0/scripts/pypi/build_pypi_package.py +name: Build pyre-check wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/pyre-check.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-pyre-check.yml' + - 'docs/packages/pyre-check.yaml' + push: + branches: [main] + paths: + - '.github/workflows/build-pyre-check.yml' + - 'docs/packages/pyre-check.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + # Upstream builds on ubuntu-latest with Ubuntu's own opam; riscv64/ubuntu:24.04 + # is the same userland, and its glibc 2.39 matches the runner. + BUILD_IMAGE: docker.io/riscv64/ubuntu:24.04 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: pyre-check + version: ${{ inputs.version }} + + build_wheel: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + name: Build pyre-check ${{ matrix.version }} manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + # The OCaml compiler (flambda), ~100 opam packages and Pyre itself are all + # compiled from source on the riscv64 runner. + timeout-minutes: 1440 + + env: + PYRE_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout Pysa v${{ env.PYRE_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: facebook/Pysa + ref: v${{ env.PYRE_VERSION }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Apply riscv64 patches + run: git apply -v python-wheels/patches/pyre-check/${{ env.PYRE_VERSION }}/*.patch + + - name: Build pyre.bin and the wheel + shell: bash + run: | + cat > riscv64-build.sh <<'EOF' + set -eux + + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y --no-install-recommends \ + opam \ + build-essential bzip2 ca-certificates curl file git m4 make \ + patch pkg-config python3 python3-venv rsync unzip xz-utils + + git config --global --add safe.directory /workspace + export OPAMYES=1 OPAMROOTISOK=1 + + python3 -m venv /tmp/buildenv + # Ubuntu 24.04's pip 24.0 matches no manylinux_*_riscv64 tag. + /tmp/buildenv/bin/pip install --quiet --upgrade pip + export PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ PIP_PREFER_BINARY=1 + /tmp/buildenv/bin/pip install --quiet -r requirements.txt + export PATH="/tmp/buildenv/bin:${HOME}/.opam/pyre-4.14.2/bin:${PATH}" + + ./scripts/setup.sh --local --release --no-tests + + # The released wheels' WHEEL file names bdist_wheel 0.38.4 as generator. + pip install --quiet 'setuptools<70' 'wheel==0.38.4' twine + mkdir -p dist + + # build_pypi_package.py builds the wheel under tempfile.mkdtemp() (which + # defaults to /tmp) and then os.replace()s it into --output-dir. /tmp is + # the container's own overlay/tmpfs, a different device from /workspace + # (bind-mounted from the runner host), so that replace() fails with + # "Invalid cross-device link" (os.replace/rename(2) can't cross devices). + # Point TMPDIR at a directory on the same bind-mounted filesystem as + # ../dist so the final move is same-device. + mkdir -p /workspace/tmp + export TMPDIR=/workspace/tmp + + cd scripts + python -m pypi \ + --typeshed-path ../stubs/typeshed/typeshed \ + --version "${PYRE_VERSION}" \ + --output-dir ../dist + cd .. + rm dist/*.tar.gz + EOF + + # `opam switch create` builds the compiler; tee the whole run to an + # artifact since a multi-hour job's log is sometimes dropped. + podman run \ + --log-driver=none \ + --network=host \ + -v "$(pwd)":/workspace \ + --workdir /workspace \ + -e PYRE_VERSION="${PYRE_VERSION}" \ + --pull=newer \ + "${BUILD_IMAGE}" \ + bash riscv64-build.sh 2>&1 | tee build.log + + - name: Upload build log + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pyre-check-${{ env.PYRE_VERSION }}-build-log + path: build.log + + - name: Store wheel + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pyre-check-${{ env.PYRE_VERSION }}-py3-manylinux_riscv64 + path: dist/*.whl + if-no-files-found: error + retention-days: 1 + compression-level: 0 + + - name: Test the wheel + shell: bash + run: | + cat > riscv64-test.sh <<'EOF' + set -eux + + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq + apt-get install -y --no-install-recommends ca-certificates file python3 python3-venv + + file dist/*.whl + python3 -m venv /tmp/testenv + /tmp/testenv/bin/pip install --quiet --upgrade pip + export PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ PIP_PREFER_BINARY=1 + /tmp/testenv/bin/pip install --quiet dist/*.whl + export PATH="/tmp/testenv/bin:${PATH}" + + file "$(command -v pyre.bin)" + pyre --version + + # Upstream's own `pysa` workflow test, run against the installed wheel. + # v0.10.0 removed the Pyre1 backend, but the app still ships only Pyre1 + # expectations (result.json, no result.pyrefly.json), so upstream's runner + # cannot pass on any arch; run its analysis and require taint issues in app.py. + # The pyrefly.toml scopes Pyrefly to the app instead of the whole checkout. + cd documentation/deliberately_vulnerable_flask_app + . ./setup.sh + touch pyrefly.toml + python -mPysa.client.pyre --noninteractive analyze --use-pyrefly --no-verify > result.pyrefly.actual + python3 - <<'PY' + import json + found = {(i["code"], i["define"]) for i in json.load(open("result.pyrefly.actual"))} + expected = {(i["code"], i["define"]) for i in json.load(open("result.json"))} + print(f"{len(found)} issues found, {len(found & expected)} of the {len(expected)} Pyre1 ones") + for issue in sorted(found): + print(*issue) + assert any(define.startswith("app.") for _, define in found) + PY + EOF + + podman run -t \ + --log-driver=none \ + --network=host \ + -v "$(pwd)":/workspace \ + --workdir /workspace \ + --pull=newer \ + "${BUILD_IMAGE}" \ + bash riscv64-test.sh + + publish: + name: Publish pyre-check ${{ matrix.version }} + needs: [setup, build_wheel] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + artifact-pattern: pyre-check-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/pyre-check.yaml b/docs/packages/pyre-check.yaml new file mode 100644 index 00000000000..c277276dc2b --- /dev/null +++ b/docs/packages/pyre-check.yaml @@ -0,0 +1,5 @@ +package-name: pyre-check +source-code: https://github.com/facebook/Pysa +license: MIT +versions: +- version: 0.10.0 diff --git a/patches/pyre-check/0.10.0/0001-scripts-pypi-package-the-client-as-Pysa.patch b/patches/pyre-check/0.10.0/0001-scripts-pypi-package-the-client-as-Pysa.patch new file mode 100644 index 00000000000..71ede208a9d --- /dev/null +++ b/patches/pyre-check/0.10.0/0001-scripts-pypi-package-the-client-as-Pysa.patch @@ -0,0 +1,53 @@ +From: RISE Project +Subject: [PATCH] scripts/pypi: package the client as Pysa, as released in 0.10.0 + +The pyre-check 0.10.0 wheels on PyPI (uploaded 2026-08-06) were cut after +the v0.10.0 tag, from f3057a5 "Update references of facebook/pyre-check.git +to facebook/Pysa.git" (2026-08-05), the only commit in between. It touches +nothing but packaging: the client ships as the `Pysa` package instead of +`pyre_check` (the `pyre` console script is `Pysa.client.pyre:main`, which is +also what tools/pysa_integration_tests' `--run-from=python-package` runs), +and the metadata names facebook/Pysa and pysa@meta.com. + +Backport its two scripts/pypi hunks so the riscv64 wheel has the same +layout and metadata as the released ones. + +Upstream-Status: Backport [https://github.com/facebook/Pysa/commit/f3057a5f4f9ff354a76e72af47bb7d9d7b35673d] + +diff --git a/scripts/pypi/build_pypi_package.py b/scripts/pypi/build_pypi_package.py +index 3d42918..baf7a02 100644 +--- a/scripts/pypi/build_pypi_package.py ++++ b/scripts/pypi/build_pypi_package.py +@@ -27,7 +27,7 @@ from twine.commands.check import check as twine_check + + from .setup import run as run_setup + +-MODULE_NAME = "pyre_check" ++MODULE_NAME = "Pysa" + EXPECTED_LD_PATH = "/lib64/ld-linux-x86-64.so.2" + + LOG: logging.Logger = logging.getLogger(__name__) +diff --git a/scripts/pypi/setup.py b/scripts/pypi/setup.py +index 0de9b66..6bc54d6 100644 +--- a/scripts/pypi/setup.py ++++ b/scripts/pypi/setup.py +@@ -84,15 +84,15 @@ def run( + setup( + name=package_name, + version=package_version, +- description="A performant type checker for Python", ++ description="A performant type checker and security-focused static analyzer for Python", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://pyre-check.org/", +- download_url="https://github.com/facebook/pyre-check", ++ download_url="https://github.com/facebook/Pysa", + author="Facebook", +- author_email="pyre@fb.com", ++ author_email="pysa@meta.com", + maintainer="Facebook", +- maintainer_email="pyre@fb.com", ++ maintainer_email="pysa@meta.com", + license="MIT", + classifiers=[ + "Development Status :: 5 - Production/Stable", diff --git a/patches/pyre-check/0.10.0/0002-Build-and-package-pyre.bin-on-riscv64.patch b/patches/pyre-check/0.10.0/0002-Build-and-package-pyre.bin-on-riscv64.patch new file mode 100644 index 00000000000..df4711cb4a6 --- /dev/null +++ b/patches/pyre-check/0.10.0/0002-Build-and-package-pyre.bin-on-riscv64.patch @@ -0,0 +1,80 @@ +From: RISE Project +Subject: [PATCH] Build and package pyre.bin on riscv64 + +Three x86_64/aarch64 assumptions stop a riscv64 build or make its binary +unusable: + +- hh_shared.c's spin-wait in hh_mem_inner falls through to x86's `pause` + for everything that is not aarch64/ppc64. On riscv64 `pause` is the + Zihintpause mnemonic, which the assembler rejects for the default + rv64gc ("instruction requires the following: 'Zihintpause'"), so the + build fails. Emit its encoding with `.insn` instead: it is a FENCE hint, + so cores without Zihintpause execute it as a no-op. + +- SHARED_MEM_INIT maps the shared heap with MAP_FIXED at 0x500000000000 + (80 TiB). riscv64 hardware commonly implements only Sv39, whose user + address space ends at 256 GiB (0x4000000000), so that mmap fails with + ENOMEM and every pyre.bin command that initializes shared memory exits + with "Error initializing". Use 0x1000000000 (64 GiB) on riscv64: it + lies below the 2/3*TASK_SIZE PIE load address and far from the + top-down mmap area on Sv39, and is valid on Sv48/Sv57 as well. The + address stays fixed, as the comment requires. + +- scripts/pypi/build_pypi_package.py only accepts a dynamically linked + binary whose interpreter is x86_64's ld.so, and always names the + wheel manylinux1_x86_64. Accept riscv64's ld.so and tag the wheel + manylinux_2_39_riscv64 (glibc 2.39, Ubuntu 24.04, the build image). + +Upstream-Status: To upstream [facebook/Pysa only builds release binaries on x86_64 Linux and arm64 macOS] + +diff --git a/scripts/pypi/build_pypi_package.py b/scripts/pypi/build_pypi_package.py +index baf7a02..b19abeb 100644 +--- a/scripts/pypi/build_pypi_package.py ++++ b/scripts/pypi/build_pypi_package.py +@@ -28,7 +28,9 @@ from twine.commands.check import check as twine_check + from .setup import run as run_setup + + MODULE_NAME = "Pysa" +-EXPECTED_LD_PATH = "/lib64/ld-linux-x86-64.so.2" ++EXPECTED_LD_PATH = { ++ "riscv64": "/lib/ld-linux-riscv64-lp64d.so.1", ++}.get(platform.machine(), "/lib64/ld-linux-x86-64.so.2") + + LOG: logging.Logger = logging.getLogger(__name__) + +@@ -53,6 +55,8 @@ def get_source_distribution_and_wheel(artifact_directory: Path) -> BuildArtifact + def _distribution_platform() -> str: + system = platform.system() + if system == "Linux": ++ if platform.machine() == "riscv64": ++ return "-manylinux_2_39_riscv64" + # Currently we only ever build on Intel Linux machines. + return "-manylinux1_x86_64" + elif system == "Darwin": +diff --git a/source/hack_parallel/hack_parallel/heap/hh_shared.c b/source/hack_parallel/hack_parallel/heap/hh_shared.c +index e386387..c0a2d4d 100644 +--- a/source/hack_parallel/hack_parallel/heap/hh_shared.c ++++ b/source/hack_parallel/hack_parallel/heap/hh_shared.c +@@ -161,7 +161,12 @@ typedef struct { + + /* Fix the location of our shared memory so we can save and restore the + * hashtable easily */ ++#if defined(__riscv) && __riscv_xlen == 64 ++/* Sv39 caps user space at 256 GiB, so 80 TiB is unmappable there. */ ++#define SHARED_MEM_INIT ((char*)0x1000000000ll) ++#else + #define SHARED_MEM_INIT ((char*)0x500000000000ll) ++#endif + + /* As a sanity check when loading from a file */ + static const uint64_t MAGIC_CONSTANT = 0xfacefacefaceb000ull; +@@ -1390,6 +1395,9 @@ int hh_mem_inner(value key) { + while (hashtbl[slot].addr == HASHTBL_WRITE_IN_PROGRESS) { + #if defined(__aarch64__) || defined(__powerpc64__) + asm volatile("yield" : : : "memory"); ++#elif defined(__riscv) ++ /* Zihintpause `pause`, a plain FENCE hint where it is not implemented. */ ++ asm volatile(".insn i 0x0f, 0, x0, x0, 0x010" : : : "memory"); + #else + asm volatile("pause" : : : "memory"); + #endif