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
200 changes: 200 additions & 0 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
name: pypi-publish

# Publish the `mcpp-bin` wheels to PyPI (`pip install mcpp-bin`).
#
# Downstream of `release`, like aur-publish.yml and homebrew-publish.yml: the
# wheels are built from the release's own mcpp-release.json and payloads, so
# this runs only once the release workflow has completed.
#
# CREDENTIALS: none stored. Publishing uses PyPI Trusted Publishing (OIDC):
# PyPI trusts this repository + workflow file + the `pypi` environment, and
# the job exchanges its GitHub OIDC token for a short-lived upload token.
# One-time setup is in scripts/pypi/README.md.
#
# ARMING: the automatic trigger builds, verifies and reports, and publishes
# only when the repository variable PYPI_AUTOPUBLISH is `true`, for the reason
# aur-publish.yml gives: an unattended push to a third-party service must be
# armed by a human who has watched one publish succeed, not inherited from a
# merge. `workflow_dispatch` carries its own explicit `publish` switch.

on:
workflow_run:
workflows: [release]
types: [completed]
# Changes to the packaging itself build and pip-install the wheels of the
# latest release on every platform. A pull request never publishes.
pull_request:
paths:
- scripts/pypi/**
- tests/scripts/test_pypi_wheels.py
- .github/workflows/pypi-publish.yml
workflow_dispatch:
inputs:
publish:
description: 'Upload to PyPI (false builds and verifies only)'
type: boolean
required: true
default: false
tag:
description: 'Release tag, e.g. v2026.9.21.3 (default: the latest release)'
type: string
required: false

concurrency:
group: pypi-mcpp-bin
cancel-in-progress: false

permissions:
contents: read

jobs:
build:
name: build wheels
if: >-
github.event_name != 'workflow_run' ||
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-24.04
timeout-minutes: 20
outputs:
version: ${{ steps.resolve.outputs.version }}
publish: ${{ steps.resolve.outputs.publish }}
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha || github.ref }}

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Builder contract tests
run: python3 tests/scripts/test_pypi_wheels.py

- name: Resolve the release and whether to publish
id: resolve
env:
TRIGGER: ${{ github.event_name }}
INPUT_TAG: ${{ inputs.tag }}
MANUAL_PUBLISH: ${{ inputs.publish }}
AUTOPUBLISH: ${{ vars.PYPI_AUTOPUBLISH }}
run: |
set -euo pipefail
if [[ -n "${INPUT_TAG:-}" ]]; then
tag="$INPUT_TAG"
elif [[ "$TRIGGER" == "workflow_run" ]]; then
# The released commit's mcpp.toml carries the released version.
tag="v$(grep -m1 -E '^\s*version\s*=' mcpp.toml | sed -E 's/.*"([^"]+)".*/\1/')"
else
tag="$(gh release view -R "$GITHUB_REPOSITORY" --json tagName --jq .tagName)"
fi
version="${tag#v}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"

# PyPI never accepts the same file twice, so an existing version is
# a finished job rather than something to retry.
code=$(curl -s -o /dev/null -w '%{http_code}' --retry 3 --retry-all-errors \
"https://pypi.org/pypi/mcpp-bin/$version/json")
if [[ "$code" == "200" ]]; then
echo "::notice::mcpp-bin $version is already on PyPI; nothing to publish."
publish=false
elif [[ "$TRIGGER" == "workflow_run" ]]; then
if [[ "${AUTOPUBLISH:-}" == "true" ]]; then
publish=true
else
publish=false
echo "::notice::PYPI_AUTOPUBLISH is not set — building and verifying $tag without publishing."
fi
elif [[ "$TRIGGER" == "workflow_dispatch" ]]; then
publish="${MANUAL_PUBLISH:-false}"
else
publish=false
fi
echo "publish=$publish" >> "$GITHUB_OUTPUT"
echo "mcpp-bin $version from $tag; publish=$publish" >> "$GITHUB_STEP_SUMMARY"

- name: Build wheels from the release manifest
run: python3 scripts/pypi/build_wheels.py --tag "${{ steps.resolve.outputs.tag }}" --out dist

- name: Check metadata
run: |
python3 -m pip install --quiet twine
python3 -m twine check --strict dist/*.whl

- uses: actions/upload-artifact@v4
with:
name: mcpp-bin-wheels
path: dist/*.whl
if-no-files-found: error

# pip, not this workflow, picks the wheel: each runner installs from the
# directory of all four, so a wrong platform tag fails here rather than on a
# user's machine. The run then checks the two properties the launcher exists
# for: the per-user home is outside the Python environment, and the bundled
# xlings is the one seeded into it.
smoke:
name: pip install (${{ matrix.os }})
needs: build
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-14, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/download-artifact@v4
with:
name: mcpp-bin-wheels
path: dist
- name: Install and run
shell: bash
env:
VERSION: ${{ needs.build.outputs.version }}
run: |
set -euo pipefail
python -m venv venv
if [[ -x venv/Scripts/python.exe ]]; then py=venv/Scripts/python.exe; bin=venv/Scripts; else py=venv/bin/python; bin=venv/bin; fi
"$py" -m pip install --quiet --no-index --find-links dist mcpp-bin
home="$RUNNER_TEMP/home"; mkdir -p "$home"
export HOME="$home" USERPROFILE="$home"
unset MCPP_HOME MCPP_VENDORED_XLINGS
out="$("$bin/mcpp" --version)"
echo "$out"
[[ "$out" == *"$VERSION"* ]] || { echo "::error::expected $VERSION, got: $out"; exit 1; }
"$bin/mcpp" self env | tee env.txt
grep -F "MCPP_HOME" env.txt | grep -F ".mcpp" \
|| { echo "::error::MCPP_HOME is not the per-user home"; exit 1; }
if grep -F "MCPP_HOME" env.txt | grep -qF "site-packages"; then
echo "::error::MCPP_HOME resolved into the Python environment"; exit 1
fi
# On Windows mcpp runs the vendored xlings in place (src/config.cppm,
# make_xlings_env), so the seeded copy is checked on POSIX only.
if [[ "$RUNNER_OS" != "Windows" ]]; then
ls "$home/.mcpp/registry/bin/" | grep -q '^xlings' \
|| { echo "::error::the bundled xlings was not seeded into the home"; exit 1; }
fi

publish:
name: publish to PyPI
needs: [build, smoke]
if: needs.build.outputs.publish == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 15
environment:
name: pypi
url: https://pypi.org/project/mcpp-bin/${{ needs.build.outputs.version }}/
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: mcpp-bin-wheels
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,12 @@
|:---:|
| [Package index mcpp-index](https://mcpplibs.github.io/mcpp-index/) · [Module libraries mcpplibs](https://github.com/mcpplibs) · [Community Forum](https://forum.d2learn.org/category/20) · [Issues](https://github.com/mcpp-community/mcpp/issues) · [Releases](https://github.com/mcpp-community/mcpp/releases) |
| [![ci-linux](https://github.com/mcpp-community/mcpp/actions/workflows/ci-linux.yml/badge.svg?branch=main)](https://github.com/mcpp-community/mcpp/actions/workflows/ci-linux.yml) [![ci-macos](https://github.com/mcpp-community/mcpp/actions/workflows/ci-macos.yml/badge.svg?branch=main)](https://github.com/mcpp-community/mcpp/actions/workflows/ci-macos.yml) [![ci-windows](https://github.com/mcpp-community/mcpp/actions/workflows/ci-windows.yml/badge.svg?branch=main)](https://github.com/mcpp-community/mcpp/actions/workflows/ci-windows.yml) |
| Plugins · [mcpp-language-server (mcppls)](https://github.com/Sunrisepeak/mcpp-language-server) — a C++20/23 modules language server for VS Code, Zed, CLion, Neovim, AI agents (MCP) and CI |

<p align="center">
<img src="https://github.com/user-attachments/assets/6c85896e-9a37-4f62-acfb-d37a4eae2363" alt="mcpp demo" width="720">
</p>

> **Note (2026.9.20.1):** the `[c-abi]` verification probe now selects the
> target it is verifying. On a freestanding target it selected none and
> answered for the build host, which on a Linux host passed for the wrong
> reason and on a Windows host failed for one. The `hostStripMacros`
> compensation 2026.9.18.3 added is removed with it. This release also adds
> `[kernel-abi] provides-interfaces` / `requires-interfaces`, answered at
> dependency resolution, and `[c-abi-absent]`, which states what a C library
> does not supply and in what shape. See CHANGELOG and docs/22.

## Highlights

- **Modular build system** — C++ modules first: `import std` handled automatically, file-level incremental builds, automatic dependency analysis, nothing to configure
Expand Down Expand Up @@ -174,7 +166,23 @@ remain manually maintained and may intentionally lag.
</details>

<details>
<summary><b>Option 4</b> — let an AI assistant install it for you</summary>
<summary><b>Option 4</b> — pip (PyPI)</summary>

```bash
pip install mcpp-bin
```

Installs the `mcpp` command into the active Python environment; `pipx install
mcpp-bin` gives it an environment of its own. The wheels carry the same
prebuilt release binary for Linux x86_64 / aarch64, macOS 14+ on Apple silicon
and Windows x86_64. Per-user data still lives in `~/.mcpp/`, outside the Python
environment. On PyPI the name `mcpp` belongs to an unrelated project, hence
`mcpp-bin` (see [`scripts/pypi/`](scripts/pypi/)).

</details>

<details>
<summary><b>Option 5</b> — let an AI assistant install it for you</summary>

Copy the following prompt to your AI coding assistant (Claude Code / Cursor / Copilot, etc.):

Expand Down
Loading
Loading