Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 220 additions & 0 deletions .github/workflows/build-pyre-check.yml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions docs/packages/pyre-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package-name: pyre-check
source-code: https://github.com/facebook/Pysa
license: MIT
versions:
- version: 0.10.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
From: RISE Project <info@riseproject.dev>
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",
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
From: RISE Project <info@riseproject.dev>
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
Loading