From e66179c262182d87518d2a9be14a1047228e8e76 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 22 Feb 2026 17:56:43 -0800 Subject: [PATCH] build & publish wheels on master pushes (#3) --- .github/workflows/release.yml | 202 ++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..eb3856c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,202 @@ +name: release wheels + +on: + push: + branches: [master] + +jobs: + build: + name: build - ${{ matrix.runs-on }} + runs-on: ${{ matrix.runs-on }} + permissions: + contents: write + strategy: + fail-fast: false + matrix: + runs-on: [ubuntu-latest, ubuntu-24.04-arm, macos-latest] + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: "3.14" + + - name: build and release wheels + env: + GH_TOKEN: ${{ github.token }} + run: | + pip install build + + for toml in */pyproject.toml; do + [ -f "$toml" ] || continue + pkg="$(dirname "$toml")" + version=$(python3 -c "import tomllib; print(tomllib.load(open('$toml', 'rb'))['project']['version'])") + tag="${pkg}/v${version}" + + echo "=========================================" + echo "Building: $pkg v$version" + echo "=========================================" + python -m build --wheel "$pkg" + + whl=$(ls "$pkg"/dist/*.whl) + echo "Uploading: $whl" + if gh release view "$tag" --repo "$GITHUB_REPOSITORY" &>/dev/null; then + gh release upload "$tag" "$whl" --repo "$GITHUB_REPOSITORY" --clobber + else + gh release create "$tag" "$whl" \ + --repo "$GITHUB_REPOSITORY" \ + --title "$pkg v$version" \ + --notes "Platform wheels for $pkg $version" + fi + done + + publish-shims: + name: publish releases branch + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + + - name: build and push shim packages + run: | + REPO_URL="https://github.com/${GITHUB_REPOSITORY}" + + # set up a temp dir for the releases branch + tmp="$(mktemp -d)" + trap 'rm -rf "$tmp"' EXIT + + for toml in */pyproject.toml; do + [ -f "$toml" ] || continue + pkg="$(dirname "$toml")" + version=$(python3 -c "import tomllib; print(tomllib.load(open('$toml', 'rb'))['project']['version'])") + tag="${pkg}/v${version}" + module="${pkg//-/_}" + + echo "=========================================" + echo "Creating shim: $pkg v$version" + echo "=========================================" + + pkg_dir="$tmp/$pkg" + mod_dir="$pkg_dir/$module" + mkdir -p "$mod_dir" + + # copy __init__.py from source + cp "$pkg/$module/__init__.py" "$mod_dir/" + + # pyproject.toml — same metadata, no package-data (toolchain comes from wheel) + cat > "$pkg_dir/pyproject.toml" << TOML + [build-system] + requires = ["setuptools>=64", "wheel"] + build-backend = "setuptools.build_meta" + + [project] + name = "$pkg" + version = "$version" + description = "ARM GCC toolchain for bare-metal targets (pre-built)" + requires-python = ">=3.8" + + [project.scripts] + $(grep -A100 '^\[project\.scripts\]' "$pkg/pyproject.toml" | tail -n +2 | sed '/^\[/q' | grep -v '^\[') + + [tool.setuptools.packages.find] + include = ["${module}*"] + + [tool.setuptools.package-data] + $module = ["toolchain/**/*"] + TOML + + # setup.py — downloads pre-built wheel from GH releases and extracts toolchain + cat > "$pkg_dir/setup.py" << 'SETUP' + import os + import platform + import zipfile + from io import BytesIO + from urllib.request import urlopen + + from setuptools.command.build_py import build_py + + REPO_URL = "REPO_URL_PLACEHOLDER" + TAG = "TAG_PLACEHOLDER" + VERSION = "VERSION_PLACEHOLDER" + MODULE = "MODULE_PLACEHOLDER" + + PLATFORM_MAP = { + ("Linux", "x86_64"): "linux_x86_64", + ("Linux", "aarch64"): "linux_aarch64", + ("Darwin", "arm64"): "macosx_11_0_arm64", + } + + + class InstallPrebuilt(build_py): + """Download pre-built wheel from GitHub Releases and extract the toolchain.""" + + def run(self): + pkg_dir = os.path.dirname(os.path.abspath(__file__)) + toolchain_dir = os.path.join(pkg_dir, MODULE, "toolchain") + + if not os.path.exists(os.path.join(toolchain_dir, "bin")): + key = (platform.system(), platform.machine()) + plat = PLATFORM_MAP.get(key) + if plat is None: + raise RuntimeError(f"unsupported platform: {key}") + + whl_name = f"{MODULE}-{VERSION}-py3-none-{plat}.whl" + url = f"{REPO_URL}/releases/download/{TAG}/{whl_name}" + + print(f"Downloading {url} ...") + data = urlopen(url).read() + + print("Extracting toolchain ...") + with zipfile.ZipFile(BytesIO(data)) as zf: + prefix = f"{MODULE}/toolchain/" + # also handle .data/purelib/ layout + alt_prefix = f"{MODULE}-{VERSION}.data/purelib/{MODULE}/toolchain/" + for info in zf.infolist(): + for p in (prefix, alt_prefix): + if info.filename.startswith(p): + rel = info.filename[len(p):] + if not rel: + continue + dest = os.path.join(toolchain_dir, rel) + if info.is_dir(): + os.makedirs(dest, exist_ok=True) + else: + os.makedirs(os.path.dirname(dest), exist_ok=True) + with open(dest, "wb") as f: + f.write(zf.read(info)) + # preserve executable bit + if info.external_attr >> 16 & 0o111: + os.chmod(dest, 0o755) + break + + super().run() + + + def setup(): + from setuptools import setup as _setup + _setup(cmdclass={"build_py": InstallPrebuilt}) + + + if __name__ == "__main__": + setup() + SETUP + + # template in the actual values + sed -i "s|REPO_URL_PLACEHOLDER|${REPO_URL}|" "$pkg_dir/setup.py" + sed -i "s|TAG_PLACEHOLDER|${tag}|" "$pkg_dir/setup.py" + sed -i "s|VERSION_PLACEHOLDER|${version}|" "$pkg_dir/setup.py" + sed -i "s|MODULE_PLACEHOLDER|${module}|" "$pkg_dir/setup.py" + done + + # push to releases branch + cd "$tmp" + git init + git checkout -b releases + git add . + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git commit -m "update shim packages" + git remote add origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git" + git push -f origin releases