diff --git a/.github/workflows/publish-versions.yml b/.github/workflows/publish-versions.yml deleted file mode 100644 index 54a0a1ba8..000000000 --- a/.github/workflows/publish-versions.yml +++ /dev/null @@ -1,104 +0,0 @@ -# Publish python-build-standalone version information to the versions repository. -name: publish-versions - -on: - workflow_call: - inputs: - tag: - required: true - type: string - workflow_dispatch: - inputs: - tag: - description: "Release tag to publish (e.g. 20260127)" - required: true - type: string - dry-run: - description: "Only generate metadata, skip PR creation" - required: false - type: boolean - default: true - -permissions: {} - -jobs: - publish-versions: - runs-on: ubuntu-latest - env: - TAG: ${{ inputs.tag }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: "Install uv" - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 - - - name: "Download SHA256SUMS" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - mkdir -p dist - gh release download "$TAG" --dir dist --pattern "SHA256SUMS" - - - name: "Generate versions metadata" - env: - GITHUB_EVENT_INPUTS_TAG: ${{ inputs.tag }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: uv run generate-version-metadata.py > dist/versions.ndjson - - - name: "Validate metadata" - run: | - echo "Generated $(wc -l < dist/versions.ndjson) version entries" - head -c 1000 dist/versions.ndjson - - - name: "Set branch name" - if: inputs.dry-run != true - run: echo "BRANCH_NAME=update-versions-$TAG-$(date +%s)" >> $GITHUB_ENV - - - name: "Clone versions repo" - if: inputs.dry-run != true - run: git clone https://${{ secrets.ASTRAL_VERSIONS_PAT }}@github.com/astral-sh/versions.git astral-versions - - - name: "Update versions" - if: inputs.dry-run != true - run: cat dist/versions.ndjson | uv run astral-versions/scripts/insert-versions.py --name python-build-standalone - - - name: "Commit versions" - if: inputs.dry-run != true - working-directory: astral-versions - run: | - git config user.name "astral-versions-bot" - git config user.email "176161322+astral-versions-bot@users.noreply.github.com" - - git checkout -b "$BRANCH_NAME" - git add -A - git commit -m "Update python-build-standalone to $TAG" - - - name: "Create Pull Request" - if: inputs.dry-run != true - working-directory: astral-versions - env: - GITHUB_TOKEN: ${{ secrets.ASTRAL_VERSIONS_PAT }} - run: | - pull_request_title="Update python-build-standalone versions for $TAG" - - gh pr list --state open --json title --jq ".[] | select(.title == \"$pull_request_title\") | .number" | \ - xargs -I {} gh pr close {} - - git push origin "$BRANCH_NAME" - - gh pr create --base main --head "$BRANCH_NAME" \ - --title "$pull_request_title" \ - --body "Automated versions update for $TAG" \ - --label "automation" - - - name: "Merge Pull Request" - if: inputs.dry-run != true - working-directory: astral-versions - env: - GITHUB_TOKEN: ${{ secrets.ASTRAL_VERSIONS_PAT }} - run: | - # Wait for PR to be created before merging - sleep 10 - gh pr merge --squash "$BRANCH_NAME" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e56846f4..20ad2176e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,11 +21,20 @@ env: permissions: {} jobs: + release-gate: + name: Release gate + runs-on: ubuntu-latest + environment: release + steps: + - run: echo "Release approved" + release: name: Release + needs: release-gate env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} runs-on: ubuntu-24.04 + environment: release permissions: contents: write @@ -59,32 +68,13 @@ jobs: git config --global user.name "$GITHUB_ACTOR" git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com" - # Fetch the commit so that it exists locally. - - name: Fetch commit - if: ${{ github.event.inputs.dry-run == 'false' }} - run: git fetch origin ${GITHUB_EVENT_INPUTS_SHA} - env: - GITHUB_EVENT_INPUTS_SHA: ${{ github.event.inputs.sha }} - - # Associate the commit with the tag. - - name: Create tag - if: ${{ github.event.inputs.dry-run == 'false' }} - run: git tag ${GITHUB_EVENT_INPUTS_TAG} ${GITHUB_EVENT_INPUTS_SHA} - env: - GITHUB_EVENT_INPUTS_TAG: ${{ github.event.inputs.tag }} - GITHUB_EVENT_INPUTS_SHA: ${{ github.event.inputs.sha }} - - name: Push tag - if: ${{ github.event.inputs.dry-run == 'false' }} - run: git push origin ${GITHUB_EVENT_INPUTS_TAG} - env: - GITHUB_EVENT_INPUTS_TAG: ${{ github.event.inputs.tag }} - - # Create a GitHub release. + # Create a GitHub draft release for the target commit. - name: Create GitHub Release if: ${{ github.event.inputs.dry-run == 'false' }} - run: just release-create ${GITHUB_EVENT_INPUTS_TAG} + run: just release-create ${GITHUB_EVENT_INPUTS_TAG} ${GITHUB_EVENT_INPUTS_SHA} env: GITHUB_EVENT_INPUTS_TAG: ${{ github.event.inputs.tag }} + GITHUB_EVENT_INPUTS_SHA: ${{ github.event.inputs.sha }} # Uploading the relevant artifact to the GitHub release. - run: just release-run ${GH_TOKEN} ${GITHUB_EVENT_INPUTS_SHA} ${GITHUB_EVENT_INPUTS_TAG} diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d4bbaa39e..c7e0193b5 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -37,9 +37,10 @@ Releases To cut a release, wait for the "MacOS Python build", "Linux Python build", and "Windows Python build" GitHub Actions to complete successfully on the target commit. -Then, run the "Release" GitHub Action to create the release, populate the release artifacts (by -downloading the artifacts from each workflow, and uploading them to the GitHub Release), and promote -the SHA via the `latest-release` branch. +Then, run the "Release" GitHub Action to create a draft release for the target commit, +populate the release artifacts (by downloading the artifacts from each workflow, and uploading +them to the GitHub Release), publish the release, and promote the SHA via the `latest-release` +branch. The "Release" GitHub Action takes, as input, a tag (assumed to be a date in `YYYYMMDD` format) and the commit SHA referenced above. @@ -49,8 +50,8 @@ with the tag `20240419` and the commit SHA `29abc56954fbf5ea812f7fbc3e42d87787d4 once the "MacOS Python build", "Linux Python build", and "Windows Python build" workflows have run to completion on `29abc56`. -When the "Release" workflow is complete, populate the release notes in the GitHub UI and promote -the pre-release to a full release, again in the GitHub UI. +When the "Release" workflow is complete, the release will have been published and version metadata +will have been updated. You can then refine the release notes in the GitHub UI. At any stage, you can run the "Release" workflow in dry-run mode to avoid uploading artifacts to GitHub. Dry-run mode can be executed before or after creating the release itself. diff --git a/Justfile b/Justfile index ae1feff7f..c05e889c7 100644 --- a/Justfile +++ b/Justfile @@ -71,28 +71,35 @@ release-set-latest-release tag: git switch main -# Create a GitHub release object, or reuse an existing prerelease. -release-create tag: +# Create a GitHub draft release for the target commit, or reuse an existing draft release. +release-create tag commit: #!/usr/bin/env bash set -euo pipefail - prerelease_exists=$(gh release view {{tag}} --json isPrerelease -t '{{{{.isPrerelease}}' 2>&1 || true) - case "$prerelease_exists" in + draft_exists=$(gh release view {{tag}} --json isDraft -t '{{{{.isDraft}}' 2>&1 || true) + case "$draft_exists" in true) - echo "note: updating existing prerelease {{tag}}" + echo "note: updating existing draft release {{tag}}" ;; false) - echo "error: release {{tag}} already exists" + echo "error: release {{tag}} already exists and is not a draft" exit 1 ;; "release not found") - gh release create {{tag}} --prerelease --notes TBD --verify-tag + gh release create {{tag}} --draft --title {{tag}} --notes TBD --target {{commit}} ;; *) - echo "error: unexpected gh cli output: $prerelease_exists" + echo "error: unexpected gh cli output: $draft_exists" exit 1 ;; esac +# Publish the draft GitHub release and promote the tag to latest-release. +release-finalize tag: + #!/usr/bin/env bash + set -euo pipefail + gh release edit {{tag}} --draft=false --latest + just release-set-latest-release {{tag}} + # Upload release artifacts to an S3-compatible mirror bucket with the correct release names. # AWS credentials are read from the standard AWS_* environment variables. # Requires `release-run` to have been run so that dist/SHA256SUMS exists. @@ -122,7 +129,7 @@ release-run token commit tag: just release-download-distributions {{token}} {{commit}} datetime=$(ls dist/cpython-3.10.*-x86_64-unknown-linux-gnu-install_only-*.tar.gz | awk -F- '{print $8}' | awk -F. '{print $1}') just release-upload-distributions {{token}} ${datetime} {{tag}} - just release-set-latest-release {{tag}} + just release-finalize {{tag}} # Perform a release in dry-run mode. release-dry-run token commit tag: diff --git a/cpython-unix/build-tcl.sh b/cpython-unix/build-tcl.sh index 6a67b6da8..140aaeb76 100755 --- a/cpython-unix/build-tcl.sh +++ b/cpython-unix/build-tcl.sh @@ -67,12 +67,20 @@ rm -rf pkgs/sqlite* pkgs/tdbc* pushd unix CFLAGS="${EXTRA_TARGET_CFLAGS} -fPIC -I${TOOLS_PATH}/deps/include" -LDFLAGS="${EXTRA_TARGET_CFLAGS} -L${TOOLS_PATH}/deps/lib" +LDFLAGS="${EXTRA_TARGET_LDFLAGS} -L${TOOLS_PATH}/deps/lib" if [[ "${PYBUILD_PLATFORM}" != macos* ]]; then LDFLAGS="${LDFLAGS} -Wl,--exclude-libs,ALL" fi -CFLAGS="${CFLAGS}" CPPFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" ./configure \ +# Tcl configures and builds packages (itcl, threads, ...) as make targets. +# These do not pick up environment variables passed to ./configure +# Export compiler flags to make them available when configuring and building +# these packages. +# An alternative is to include these when calling ./configure AND make +export CFLAGS LDFLAGS +export CPPFLAGS="${CFLAGS}" + +./configure \ --build="${BUILD_TRIPLE}" \ --host="${TARGET_TRIPLE}" \ --prefix=/tools/deps \ diff --git a/cpython-unix/targets.yml b/cpython-unix/targets.yml index 50ffbf308..588c2db79 100644 --- a/cpython-unix/targets.yml +++ b/cpython-unix/targets.yml @@ -185,6 +185,9 @@ armv7-unknown-linux-gnueabi: host_cxx: /usr/bin/x86_64-linux-gnu-g++ target_cc: /usr/bin/arm-linux-gnueabi-gcc target_cxx: /usr/bin/arm-linux-gnueabi-g++ + target_ldflags: + # Hardening + - '-Wl,-z,noexecstack' needs: - autoconf - bdb @@ -226,6 +229,9 @@ armv7-unknown-linux-gnueabihf: host_cxx: /usr/bin/x86_64-linux-gnu-g++ target_cc: /usr/bin/arm-linux-gnueabihf-gcc target_cxx: /usr/bin/arm-linux-gnueabihf-g++ + target_ldflags: + # Hardening + - '-Wl,-z,noexecstack' needs: - autoconf - bdb @@ -266,6 +272,9 @@ loongarch64-unknown-linux-gnu: host_cxx: /usr/bin/x86_64-linux-gnu-g++ target_cc: /usr/bin/loongarch64-linux-gnu-gcc target_cxx: /usr/bin/loongarch64-linux-gnu-g++ + target_ldflags: + # Hardening + - '-Wl,-z,noexecstack' needs: - autoconf - bdb @@ -307,6 +316,9 @@ mips-unknown-linux-gnu: host_cxx: /usr/bin/x86_64-linux-gnu-g++ target_cc: /usr/bin/mips-linux-gnu-gcc target_cxx: /usr/bin/mips-linux-gnu-g++ + target_ldflags: + # Hardening + - '-Wl,-z,noexecstack' needs: - autoconf - bdb @@ -348,6 +360,9 @@ mipsel-unknown-linux-gnu: host_cxx: /usr/bin/x86_64-linux-gnu-g++ target_cc: /usr/bin/mipsel-linux-gnu-gcc target_cxx: /usr/bin/mipsel-linux-gnu-g++ + target_ldflags: + # Hardening + - '-Wl,-z,noexecstack' needs: - autoconf - bdb @@ -389,6 +404,9 @@ ppc64le-unknown-linux-gnu: host_cxx: /usr/bin/x86_64-linux-gnu-g++ target_cc: /usr/bin/powerpc64le-linux-gnu-gcc target_cxx: /usr/bin/powerpc64le-linux-gnu-g++ + target_ldflags: + # Hardening + - '-Wl,-z,noexecstack' needs: - autoconf - bdb @@ -430,6 +448,9 @@ riscv64-unknown-linux-gnu: host_cxx: /usr/bin/x86_64-linux-gnu-g++ target_cc: /usr/bin/riscv64-linux-gnu-gcc target_cxx: /usr/bin/riscv64-linux-gnu-g++ + target_ldflags: + # Hardening + - '-Wl,-z,noexecstack' needs: - autoconf - bdb @@ -474,6 +495,9 @@ s390x-unknown-linux-gnu: target_cflags: # set the minimum compatibility level to z10 (released 2008) - '-march=z10' + target_ldflags: + # Hardening + - '-Wl,-z,noexecstack' needs: - autoconf - bdb diff --git a/pythonbuild/disttests/__init__.py b/pythonbuild/disttests/__init__.py index 4dd187fb6..7b92f43f2 100644 --- a/pythonbuild/disttests/__init__.py +++ b/pythonbuild/disttests/__init__.py @@ -314,7 +314,7 @@ def test_ssl(self): if os.name == "nt" and sys.version_info[0:2] < (3, 11): wanted_version = (1, 1, 1, 23, 15) else: - wanted_version = (3, 5, 0, 5, 0) + wanted_version = (3, 5, 0, 6, 0) self.assertEqual(ssl.OPENSSL_VERSION_INFO, wanted_version) @@ -405,7 +405,7 @@ def assertPythonWorks(path: Path, argv0: Optional[str] = None): ) self.assertEqual(output.strip(), "42") - with tempfile.TemporaryDirectory(prefix="verify-distribution-") as t: + with tempfile.TemporaryDirectory(prefix="disttests-") as t: tmpdir = Path(t) symlink = tmpdir / "python" symlink.symlink_to(sys.executable) diff --git a/pythonbuild/downloads.py b/pythonbuild/downloads.py index 93381f94b..8a28180e8 100644 --- a/pythonbuild/downloads.py +++ b/pythonbuild/downloads.py @@ -178,31 +178,31 @@ }, # Remember to update LLVM_URL in src/release.rs whenever upgrading. "llvm-aarch64-linux": { - "url": "https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260312/llvm-22.1.1+20260312-gnu_only-aarch64-unknown-linux-gnu.tar.zst", - "size": 237665010, - "sha256": "0f7ef68a9d6239336aa073366edd5377a47192c32d40c96ff72b416a15259999", - "version": "22.1.1+20260312", + "url": "https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260410/llvm-22.1.3+20260410-gnu_only-aarch64-unknown-linux-gnu.tar.zst", + "size": 237655768, + "sha256": "9cb4b562323a3d899fffe6393148e92447e17b69a2501d7c6e7f2a86c32cddc1", + "version": "22.1.3+20260410", }, # Remember to update LLVM_URL in src/release.rs whenever upgrading. "llvm-x86_64-linux": { - "url": "https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260312/llvm-22.1.1+20260312-gnu_only-x86_64-unknown-linux-gnu.tar.zst", - "size": 281108139, - "sha256": "2a2f2e51bb9c621c6b00a42d8349f27edcb3aacaa750040000ac95a0298e97c1", - "version": "22.1.1+20260312", + "url": "https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260410/llvm-22.1.3+20260410-gnu_only-x86_64-unknown-linux-gnu.tar.zst", + "size": 281109065, + "sha256": "0ee8e4f89c20983d62547b1e665147ee08a7413f478e9e443fa991548da1030d", + "version": "22.1.3+20260410", }, # Remember to update LLVM_URL in src/release.rs whenever upgrading. "llvm-aarch64-macos": { - "url": "https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260312/llvm-22.1.1+20260312-aarch64-apple-darwin.tar.zst", - "size": 159781503, - "sha256": "04df05eed3dde711a5962f031f3075e3d46bad92eb0520bc4c2814ac0210c56f", - "version": "22.1.1+20260312", + "url": "https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260410/llvm-22.1.3+20260410-aarch64-apple-darwin.tar.zst", + "size": 159775425, + "sha256": "98171836c31c04edec074e5f3fee67fcace4bf3859b68a770dd9ff2039ea127d", + "version": "22.1.3+20260410", }, # Remember to update LLVM_URL in src/release.rs whenever upgrading. "llvm-x86_64-macos": { - "url": "https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260312/llvm-22.1.1+20260312-x86_64-apple-darwin.tar.zst", - "size": 167381561, - "sha256": "aef8508bef17a2d40b07ff955917ff7f36c1a609116ee8c052e12307ec748314", - "version": "22.1.1+20260312", + "url": "https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260410/llvm-22.1.3+20260410-x86_64-apple-darwin.tar.zst", + "size": 167376011, + "sha256": "15bfda1bc8bc920658c09ef1ebb4593f0705b3314beab23989f14775f2e3f3f0", + "version": "22.1.3+20260410", }, "m4": { # Mirrored from https://ftp.gnu.org/gnu/m4/m4-1.4.19.tar.xz @@ -212,7 +212,8 @@ "version": "1.4.19", }, "mpdecimal": { - "url": "https://www.bytereef.org/software/mpdecimal/releases/mpdecimal-4.0.0.tar.gz", + # Mirrored from https://www.bytereef.org/software/mpdecimal/releases/mpdecimal-4.0.0.tar.gz + "url": "https://astral-sh.github.io/mirror/files/mpdecimal-4.0.0.tar.gz", "size": 315325, "sha256": "942445c3245b22730fd41a67a7c5c231d11cb1b9936b9c0f76334fb7d0b4468c", "version": "4.0.0", @@ -244,7 +245,7 @@ "licenses": ["X11"], "license_file": "LICENSE.ncurses.txt", }, - # Remember to update OPENSSL_VERSION_INFO in verify_distribution.py whenever upgrading. + # Remember to update OPENSSL_VERSION_INFO in pythonbuild/disttests/ whenever upgrading. "openssl-1.1": { "url": "https://www.openssl.org/source/openssl-1.1.1w.tar.gz", "size": 9893384, @@ -254,12 +255,12 @@ "licenses": ["OpenSSL"], "license_file": "LICENSE.openssl-1.1.txt", }, - # Remember to update OPENSSL_VERSION_INFO in verify_distribution.py whenever upgrading. + # Remember to update OPENSSL_VERSION_INFO in pythonbuild/disttests/ whenever upgrading. "openssl-3.5": { - "url": "https://github.com/openssl/openssl/releases/download/openssl-3.5.5/openssl-3.5.5.tar.gz", - "size": 53104821, - "sha256": "b28c91532a8b65a1f983b4c28b7488174e4a01008e29ce8e69bd789f28bc2a89", - "version": "3.5.5", + "url": "https://github.com/openssl/openssl/releases/download/openssl-3.5.6/openssl-3.5.6.tar.gz", + "size": 53121812, + "sha256": "deae7c80cba99c4b4f940ecadb3c3338b13cb77418409238e57d7f31f2a3b736", + "version": "3.5.6", "library_names": ["crypto", "ssl"], "licenses": ["Apache-2.0"], "license_file": "LICENSE.openssl-3.txt", @@ -299,7 +300,7 @@ "sha256": "a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", "version": "82.0.1", }, - # Remember to update verify_distribution.py when version changed. + # Remember to update pythonbuild/disttests/ when version changed. "sqlite": { "url": "https://www.sqlite.org/2025/sqlite-autoconf-3500400.tar.gz", "size": 3173050, diff --git a/src/github.rs b/src/github.rs index 40403694a..6f7dcfe21 100644 --- a/src/github.rs +++ b/src/github.rs @@ -7,7 +7,7 @@ use { RELEASE_TRIPLES, bootstrap_llvm, build_wanted_filenames, produce_install_only, produce_install_only_stripped, }, - anyhow::{Result, anyhow}, + anyhow::{Context, Result, anyhow}, bytes::Bytes, clap::ArgMatches, futures::StreamExt, @@ -170,6 +170,44 @@ fn new_github_client(args: &ArgMatches) -> Result<(Octocrab, String)> { Ok((builder.build()?, token)) } +async fn get_draft_release_by_tag( + client: &Octocrab, + organization: &str, + repo: &str, + tag: &str, +) -> Result { + let mut page = client + .repos(organization, repo) + .releases() + .list() + .per_page(1) + .send() + .await?; + + let release = loop { + if let Some(release) = page + .take_items() + .into_iter() + .find(|release| release.tag_name == tag) + { + break Some(release); + } + + page = match client.get_page::(&page.next).await? { + Some(page) => page, + None => break None, + }; + }; + + let release = release.ok_or_else(|| anyhow!("release {tag} does not exist"))?; + + if !release.draft { + return Err(anyhow!("release {tag} exists but is not a draft")); + } + + Ok(release) +} + pub async fn command_fetch_release_distributions(args: &ArgMatches) -> Result<()> { let dest_dir = args .get_one::("dest") @@ -486,12 +524,7 @@ pub async fn command_upload_release_distributions(args: &ArgMatches) -> Result<( } let (client, token) = new_github_client(args)?; - let repo_handler = client.repos(organization, repo); - let releases = repo_handler.releases(); - let release = releases - .get_by_tag(tag) - .await - .map_err(|_| anyhow!("release {tag} does not exist; create it via GitHub web UI"))?; + let release = get_draft_release_by_tag(&client, organization, repo, tag).await?; let retry_policy = ExponentialBackoff::builder().build_with_max_retries(5); let raw_client = Client::new(); @@ -538,10 +571,9 @@ pub async fn command_upload_release_distributions(args: &ArgMatches) -> Result<( // Check that content wasn't munged as part of uploading. This once happened // and created a busted release. Never again. - let release = releases - .get_by_tag(tag) + let release = get_draft_release_by_tag(&client, organization, repo, tag) .await - .map_err(|_| anyhow!("could not find release; this should not happen!"))?; + .with_context(|| format!("could not find draft release {tag}; this should not happen"))?; let shasums_asset = release .assets .into_iter() diff --git a/src/github_api_tester.py b/src/github_api_tester.py index 314a70294..18e1b2719 100755 --- a/src/github_api_tester.py +++ b/src/github_api_tester.py @@ -138,6 +138,8 @@ class Release: release_id: int tag_name: str assets: list = dataclasses.field(default_factory=list) + draft: bool = True + prerelease: bool = False # fault0 and fault1 are called before and after receiving the first # chunk of a PUT request, respectively. Each is called once per # release - the first upload that hits it will disarm it. @@ -157,14 +159,15 @@ def render(self) -> dict: "node_id": "fakenode", "tag_name": self.tag_name, "target_commitish": "main", - "draft": False, - "prerelease": True, + "draft": self.draft, + "prerelease": self.prerelease, "assets": [i.render() for i in self.assets], } releases = [ Release(1, "basic"), + Release(2, "draft"), Release(11, "early-drop", fault0=drop_connection), Release(12, "late-drop", fault1=drop_connection), Release(4011, "early-401", fault0=lambda: quart.abort(401)), @@ -195,7 +198,17 @@ def get_release(*, tag=None, release=None) -> Release: @app.route("/repos///releases/tags/") async def get_release_by_tag(org, repo, tag): - return get_release(tag=tag).render() + release = get_release(tag=tag) + if release.draft: + quart.abort( + 404, response=quart.jsonify({"message": "Not Found", "status": "404"}) + ) + return release.render() + + +@app.route("/repos///releases") +async def list_releases(org, repo): + return quart.jsonify([release.render() for release in releases]) @app.route("/repos///releases/") @@ -311,7 +324,7 @@ async def upload_release_distributions(*args): # TODO: test all of [r.tag_name for r in releases] -TAGS_TO_TEST = ["basic", "early-drop", "late-drop", "early-403", "late-403"] +TAGS_TO_TEST = ["basic", "draft", "early-drop", "late-drop", "early-403", "late-403"] @pytest.mark.parametrize("tag", TAGS_TO_TEST) diff --git a/src/macho.rs b/src/macho.rs index 5b3155164..2173ee7b6 100644 --- a/src/macho.rs +++ b/src/macho.rs @@ -4,7 +4,7 @@ use { crate::validation::ValidationContext, - anyhow::{anyhow, Context, Result}, + anyhow::{Context, Result, anyhow}, apple_sdk::{AppleSdk, SdkSearch, SdkSearchLocation, SdkSorting, SdkVersion, SimpleSdk}, semver::Version, std::{ diff --git a/src/release.rs b/src/release.rs index 7a752bbde..de074b32a 100644 --- a/src/release.rs +++ b/src/release.rs @@ -749,14 +749,14 @@ pub fn produce_install_only_stripped(tar_gz_path: &Path, llvm_dir: &Path) -> Res static LLVM_URL: Lazy = Lazy::new(|| { if cfg!(target_os = "macos") { if std::env::consts::ARCH == "aarch64" { - Url::parse("https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260312/llvm-22.1.1+20260312-aarch64-apple-darwin.tar.zst").unwrap() + Url::parse("https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260410/llvm-22.1.3+20260410-aarch64-apple-darwin.tar.zst").unwrap() } else if std::env::consts::ARCH == "x86_64" { - Url::parse("https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260312/llvm-22.1.1+20260312-x86_64-apple-darwin.tar.zst").unwrap() + Url::parse("https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260410/llvm-22.1.3+20260410-x86_64-apple-darwin.tar.zst").unwrap() } else { panic!("unsupported macOS architecture"); } } else if cfg!(target_os = "linux") { - Url::parse("https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260312/llvm-22.1.1+20260312-gnu_only-x86_64-unknown-linux-gnu.tar.zst").unwrap() + Url::parse("https://github.com/indygreg/toolchain-tools/releases/download/toolchain-bootstrap%2F20260410/llvm-22.1.3+20260410-gnu_only-x86_64-unknown-linux-gnu.tar.zst").unwrap() } else { panic!("unsupported platform"); } diff --git a/src/validation.rs b/src/validation.rs index 6ef0cdbd0..7339885ec 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -10,12 +10,12 @@ use { object::{ Architecture, Endianness, FileKind, Object, ObjectSection, SectionIndex, SymbolScope, elf::{ - ET_DYN, ET_EXEC, FileHeader32, FileHeader64, SHN_UNDEF, STB_GLOBAL, STB_WEAK, - STV_DEFAULT, STV_HIDDEN, + ET_DYN, ET_EXEC, FileHeader32, FileHeader64, PF_X, PT_GNU_STACK, SHN_UNDEF, STB_GLOBAL, + STB_WEAK, STV_DEFAULT, STV_HIDDEN, }, macho::{LC_CODE_SIGNATURE, MH_OBJECT, MH_TWOLEVEL, MachHeader32, MachHeader64}, read::{ - elf::{Dyn, FileHeader, SectionHeader, Sym}, + elf::{Dyn, FileHeader, ProgramHeader, SectionHeader, Sym}, macho::{LoadCommandVariant, MachHeader, Nlist, Section, Segment}, pe::{ImageNtHeaders, PeFile, PeFile32, PeFile64}, }, @@ -1231,6 +1231,39 @@ fn validate_elf>( } } + // Verify that objects are not requesting an executable stack. For backwards compatibility, + // Linux (the kernel when loading an executable, and glibc when loading a shared library) + // assumes you need an executable stack unless you request otherwise. In linked outputs + // (executables and shared libraries) this is in the program header: the flags of a + // PT_GNU_STACK entry specify stack permissions, and the default if unspecified is RWX. In + // intermediate objects (.o files) this is conveyed via the presence of an empty-length + // .note.GNU-stack, which is marked as an executable section (SHF_EXECINSTR) if the object + // needs an executable stack. + // + // For now we only check binaries because of an LLVM bug that causes .o files to be missing a + // .note.GNU-stack section, which we are overriding with -Wl,-z,noexecstack. + + if matches!(elf.e_type(endian), ET_EXEC | ET_DYN) { + let mut found_pt_gnu_stack = false; + for phdr in elf.program_headers(endian, data)? { + if phdr.p_type(endian) != PT_GNU_STACK { + continue; + } + found_pt_gnu_stack = true; + if (phdr.p_flags(endian) & PF_X) != 0 { + context + .errors + .push(format!("{} requests executable stack", path.display())); + } + } + if !found_pt_gnu_stack { + context.errors.push(format!( + "{} missing PT_GNU_STACK header (defaults to executable stack)", + path.display(), + )); + } + } + Ok(()) }