Skip to content

Commit f44be3a

Browse files
authored
Merge pull request #132 from thisisthepy/release
Release: merge release into main
2 parents 8865de6 + 8460830 commit f44be3a

669 files changed

Lines changed: 62660 additions & 15788 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/settings.json‎

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python3
22
"""Checks the bilingual GitHub Pages guide in docs/guide/.
33
4-
python3 tools/check_guide.py [guide-dir]
4+
python3 .github/scripts/check_guide.py [guide-dir]
55
66
Fails (exit 1) when:
77
1. an HTML file does not parse into balanced elements;
@@ -153,7 +153,7 @@ def check(guide):
153153

154154
def main():
155155
here = os.path.dirname(os.path.abspath(__file__))
156-
guide = sys.argv[1] if len(sys.argv) > 1 else os.path.join(here, "..", "docs", "guide")
156+
guide = sys.argv[1] if len(sys.argv) > 1 else os.path.join(here, "..", "..", "docs", "guide")
157157
failures = check(guide)
158158
for f in failures:
159159
print("FAIL", f)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env bash
2+
# Build free-threaded CPython (--disable-gil) for aarch64-linux-android with
3+
# CPython's own tooling (Platforms/Android). Usage: build-android.sh [VERSION]
4+
# Needs ANDROID_HOME (SDK with cmdline-tools/latest) and a JDK 17, 21 or 25.
5+
set -euo pipefail
6+
here="$(cd "$(dirname "$0")" && pwd)"
7+
# shellcheck source=common.sh
8+
source "$here/common.sh"
9+
10+
PY_VERSION="${1:-3.15.0}"
11+
HOST=aarch64-linux-android
12+
: "${ANDROID_HOME:?ANDROID_HOME must point at the Android SDK}"
13+
14+
src="$(fetch_source "$PY_VERSION")"
15+
cd "$src"
16+
17+
# `Platforms/Android package` lists the files to ship with `git ls-files`, so
18+
# the unpacked tarball needs an index that tracks Platforms/Android.
19+
if [ ! -d .git ]; then
20+
git init -q .
21+
git add Platforms/Android
22+
fi
23+
24+
# Install the NDK version the tooling pins (android-env.sh would do it on first
25+
# use; doing it here makes a missing cmdline-tools fail early and visibly).
26+
ndk_version="$(sed -n 's/^ndk_version=//p' Platforms/Android/android-env.sh)"
27+
[ -n "$ndk_version" ] || die "no ndk_version in Platforms/Android/android-env.sh"
28+
log "NDK required by CPython $PY_VERSION: $ndk_version"
29+
if [ ! -e "$ANDROID_HOME/ndk/$ndk_version/package.xml" ]; then
30+
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;$ndk_version" >/dev/null || true
31+
fi
32+
[ -e "$ANDROID_HOME/ndk/$ndk_version/package.xml" ] || die "NDK $ndk_version not installed"
33+
34+
# --disable-gil goes to configure after `--`, for the build Python and the host.
35+
python3 Platforms/Android configure-build -- --disable-gil
36+
python3 Platforms/Android make-build
37+
python3 Platforms/Android pythoninfo-build
38+
python3 Platforms/Android configure-host "$HOST" -- --disable-gil
39+
python3 Platforms/Android make-host "$HOST"
40+
python3 Platforms/Android package "$HOST"
41+
42+
# ---- verification -------------------------------------------------------
43+
minor="$(py_minor "$PY_VERSION")"
44+
archive="$(ls cross-build/$HOST/dist/python-*-$HOST.tar.gz)"
45+
log "package: $archive"
46+
47+
# The build Python must itself be free-threaded.
48+
cross-build/build/python -c 'import sys, sysconfig; assert sysconfig.get_config_var("Py_GIL_DISABLED") == 1; print("build python free-threaded:", sys.version)'
49+
50+
check="$FT_WORK/android-check"
51+
rm -rf "$check"; mkdir -p "$check"
52+
tar -xzf "$archive" -C "$check"
53+
54+
require_gil_disabled "$check/prefix/include/python${minor}t/pyconfig.h"
55+
lib="$(ls "$check"/prefix/lib/libpython${minor}t*.so* 2>/dev/null | head -n 1 || true)"
56+
[ -n "$lib" ] || die "no libpython${minor}t*.so in the package"
57+
log "ok: $lib"
58+
file "$lib" | tee /dev/stderr | grep -q 'ARM aarch64' || die "$lib is not an aarch64 ELF"
59+
[ -d "$check/prefix/lib/python${minor}t" ] || die "no lib/python${minor}t in the package"
60+
61+
cp "$archive" "$FT_OUT/"
62+
echo "Android archive:" >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
63+
record_sha256 "$FT_OUT/$(basename "$archive")"
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#!/usr/bin/env bash
2+
# Build free-threaded CPython (--disable-gil) as an iOS XCframework with
3+
# CPython's own tooling (Platforms/Apple). Usage: build-ios.sh [VERSION]
4+
# Needs macOS with a full Xcode and python3 >= 3.10.
5+
set -euo pipefail
6+
here="$(cd "$(dirname "$0")" && pwd)"
7+
# shellcheck source=common.sh
8+
source "$here/common.sh"
9+
10+
PY_VERSION="${1:-3.15.0}"
11+
# The XCframework needs every slice Platforms/Apple declares: one device host
12+
# and two simulator hosts (arm64, x86_64).
13+
HOSTS=(arm64-apple-ios arm64-apple-ios-simulator x86_64-apple-ios-simulator)
14+
15+
src="$(fetch_source "$PY_VERSION")"
16+
cd "$src"
17+
xcodebuild -version
18+
19+
# Platforms/Apple names the standard library directory "python<major.minor>"
20+
# when it packages and in the Xcode build helper, but a free-threaded build
21+
# installs "python<major.minor>t" (LIBDEST has $(ABI_THREAD)). Without these two
22+
# edits `package` fails copying the xcprivacy files and the helper script would
23+
# not find the standard library. Each edit is asserted so a changed upstream
24+
# script fails loudly instead of silently skipping the patch.
25+
python3 -I - <<'PY'
26+
import pathlib
27+
def patch(path, old, new):
28+
p = pathlib.Path(path)
29+
s = p.read_text()
30+
assert s.count(old) == 1, f"{path}: expected exactly one {old!r}"
31+
p.write_text(s.replace(old, new))
32+
patch("Platforms/Apple/__main__.py",
33+
'version_tag = ".".join(version.split(".")[:2])',
34+
'version_tag = ".".join(version.split(".")[:2]) + "t"')
35+
patch("Platforms/Apple/testbed/Python.xcframework/build/utils.sh",
36+
'grep -E "^python3\\.\\d+$"',
37+
'grep -E "^python3\\.[0-9]+t?$"')
38+
PY
39+
40+
# --disable-gil goes to configure after `--`, for the build Python and each host.
41+
python3 Platforms/Apple configure-build -- --disable-gil
42+
python3 Platforms/Apple make-build
43+
python3 Platforms/Apple pythoninfo-build
44+
for host in "${HOSTS[@]}"; do
45+
python3 Platforms/Apple configure-host iOS "$host" -- --disable-gil
46+
python3 Platforms/Apple make-host iOS "$host"
47+
done
48+
python3 Platforms/Apple package iOS
49+
50+
# ---- verification -------------------------------------------------------
51+
minor="$(py_minor "$PY_VERSION")"
52+
archive="$(ls cross-build/dist/python-*-iOS-XCframework.tar.gz)"
53+
log "package: $archive"
54+
55+
# On macOS the build interpreter is python.exe (cross-build/build/python is a directory).
56+
build_py=cross-build/build/python.exe; [ -x "$build_py" ] || build_py=cross-build/build/python
57+
"$build_py" -c 'import sys, sysconfig; assert sysconfig.get_config_var("Py_GIL_DISABLED") == 1; print("build python free-threaded:", sys.version)'
58+
59+
check="$FT_WORK/ios-check"
60+
rm -rf "$check"; mkdir -p "$check"
61+
tar -xzf "$archive" -C "$check"
62+
xcf="$check/Python.xcframework"
63+
[ -d "$xcf" ] || die "no Python.xcframework in the package"
64+
65+
[ -d "$xcf/lib/python${minor}t" ] || die "no lib/python${minor}t in the XCframework"
66+
slices=0
67+
for slice in "$xcf"/ios-*; do
68+
slices=$((slices + 1))
69+
# Headers/pyconfig.h is a wrapper that includes pyconfig-<arch>.h.
70+
found=0
71+
for h in "$slice"/Python.framework/Headers/pyconfig-*.h; do
72+
require_gil_disabled "$h"; found=$((found + 1))
73+
done
74+
[ "$found" -gt 0 ] || die "no pyconfig-<arch>.h in $slice"
75+
[ -e "$slice/lib/libpython${minor}t.dylib" ] || die "no libpython${minor}t.dylib in $slice"
76+
lipo -info "$slice/Python.framework/Python"
77+
done
78+
[ "$slices" -eq 2 ] || die "expected 2 slices (device, simulator), found $slices"
79+
80+
cp "$archive" "$FT_OUT/"
81+
echo "iOS archive:" >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
82+
record_sha256 "$FT_OUT/$(basename "$archive")"
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env bash
2+
# Helpers shared by build-android.sh and build-ios.sh. Source this file, do not run it.
3+
4+
# FT_WORK: scratch directory (source tree and cross-build output).
5+
# FT_OUT : directory that receives the archives the workflow uploads.
6+
FT_WORK="${FT_WORK:-$PWD/.tmp/ft-mobile}"
7+
FT_OUT="${FT_OUT:-$PWD/.tmp/ft-mobile-out}"
8+
9+
log() { echo "[ft-mobile] $*" >&2; }
10+
die() { echo "[ft-mobile] ERROR: $*" >&2; exit 1; }
11+
12+
# "3.15.0" -> "3.15" (also "3.15.0rc1" -> "3.15").
13+
py_minor() { echo "$1" | sed -E 's/^([0-9]+\.[0-9]+).*/\1/'; }
14+
15+
# Download and unpack the CPython source tarball; print the source directory.
16+
# CPython's mobile tooling needs the directory to be a git work tree only for
17+
# `Platforms/Android package` (it runs `git ls-files` there), so the caller
18+
# decides whether to `git init`.
19+
fetch_source() {
20+
local version="$1"
21+
local dirver="${version%%[a-z]*}" # the ftp directory has no rc/alpha suffix
22+
local tgz="$FT_WORK/Python-$version.tgz"
23+
mkdir -p "$FT_WORK" "$FT_OUT"
24+
if [ ! -f "$tgz" ]; then
25+
log "downloading CPython $version"
26+
curl -fL --retry 5 --retry-all-errors -o "$tgz" \
27+
"https://www.python.org/ftp/python/$dirver/Python-$version.tgz"
28+
fi
29+
if [ ! -d "$FT_WORK/Python-$version" ]; then
30+
tar -xzf "$tgz" -C "$FT_WORK"
31+
fi
32+
echo "$FT_WORK/Python-$version"
33+
}
34+
35+
# Fail unless FILE contains a line matching `#define Py_GIL_DISABLED 1`.
36+
require_gil_disabled() {
37+
local file="$1"
38+
grep -Eq '^#define Py_GIL_DISABLED 1$' "$file" \
39+
|| die "$file does not define Py_GIL_DISABLED 1"
40+
log "ok: Py_GIL_DISABLED 1 in $file"
41+
}
42+
43+
# Print "sha256 name" lines for the given files, to stdout and the job summary.
44+
record_sha256() {
45+
local f line
46+
for f in "$@"; do
47+
line="$(cd "$(dirname "$f")" && shasum -a 256 "$(basename "$f")")"
48+
echo "$line"
49+
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
50+
echo "$line" >> "$GITHUB_STEP_SUMMARY"
51+
fi
52+
done
53+
}
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ between runs, and any existing `release` (e.g. a stale one from an older workflo
77
ignored and overwritten.
88

99
1. Every push to `develop` triggers `.github/workflows/release-sync.yml`, which runs
10-
`tools/release/sync-release.sh --push`. It rebuilds `release` from scratch as exactly
10+
`.github/scripts/release/sync-release.sh --push`. It rebuilds `release` from scratch as exactly
1111
one commit (`Release: sync from develop <sha>`) whose parent is that develop commit and
1212
whose tree is the develop tree minus the files below. This happens even when nothing
1313
is dropped. It is a no-op only if `release` already has that tree and that parent.
@@ -31,10 +31,10 @@ Only Markdown is ever dropped.
3131

3232
## Local use
3333

34-
tools/release/sync-release.sh --dry-run # list paths that would be dropped
35-
tools/release/sync-release.sh # regenerate local release branch
36-
tools/release/sync-release.sh --push # also push to origin
37-
tools/release/test-sync-release.sh # tests (throwaway repo in .tmp/)
34+
.github/scripts/release/sync-release.sh --dry-run # list paths that would be dropped
35+
.github/scripts/release/sync-release.sh # regenerate local release branch
36+
.github/scripts/release/sync-release.sh --push # also push to origin
37+
.github/scripts/release/test-sync-release.sh # tests (throwaway repo in .tmp/)
3838

3939
The script uses plumbing and a private index, so your working tree, index and HEAD are untouched.
4040
It refuses to run while `release` is checked out.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env bash
2+
# Assembles the assets of a desktop-only GitHub Release (no Maven publishing): run from the repository root.
3+
# .github/scripts/release/assemble-desktop-release.sh <out-dir>
4+
# Produces in <out-dir>:
5+
# python-multiplatform-maven-repo-<version>.zip a Maven repository (org/thisisthepy/...): the library's
6+
# desktop variant and KMP root, the KSP processor with its
7+
# typedpython-gate zip, the Gradle plugin and its marker
8+
# typedpython-<pyversion>-py3-none-any.whl the TypedPython compiler (pure Python + its C runtime header)
9+
# python-multiplatform-ksp-<version>-typedpython-gate.zip
10+
# python-multiplatform-examples-<version>.zip docs/release/desktop (the examples below, verified per release)
11+
# SHA256SUMS
12+
set -euo pipefail
13+
out="$(mkdir -p "${1:?usage: assemble-desktop-release.sh <out-dir>}" && cd "$1" && pwd)"
14+
root="$(pwd)"
15+
version="$(sed -nE 's/^pythonVersion=(.*)$/\1/p' gradle.properties)-$(sed -nE 's/^libraryVersionSuffix=(.*)$/\1/p' gradle.properties)"
16+
repo="$out/maven-repo"
17+
rm -rf "$repo"
18+
mkdir -p "$repo"
19+
20+
# The desktop variant and the KMP root metadata of the library, the KSP processor, and the Gradle plugin
21+
# (an included build: its own invocation), all into one repository directory, never ~/.m2.
22+
./gradlew --console=plain -Dmaven.repo.local="$repo" \
23+
:python-multiplatform:publishDesktopPublicationToMavenLocal \
24+
:python-multiplatform:publishKotlinMultiplatformPublicationToMavenLocal \
25+
:python-multiplatform-ksp:publishToMavenLocal
26+
./gradlew --console=plain -p python-multiplatform-gradle-plugin -Dmaven.repo.local="$repo" publishToMavenLocal
27+
28+
test -f "$repo/org/thisisthepy/python-multiplatform-desktop/$version/python-multiplatform-desktop-$version.jar"
29+
test -f "$repo/org/thisisthepy/python-multiplatform-ksp/$version/python-multiplatform-ksp-$version-typedpython-gate.zip"
30+
test -d "$repo/org/thisisthepy/python/multiplatform/bindings/org.thisisthepy.python.multiplatform.bindings.gradle.plugin"
31+
if find "$repo" -path '*io/github/thisisthepy*' | grep -q .; then
32+
echo "error: an io.github.thisisthepy coordinate was published (#95)" >&2; exit 1
33+
fi
34+
(cd "$out" && rm -f "python-multiplatform-maven-repo-$version.zip" && zip -qr "python-multiplatform-maven-repo-$version.zip" maven-repo)
35+
cp "$repo/org/thisisthepy/python-multiplatform-ksp/$version/python-multiplatform-ksp-$version-typedpython-gate.zip" "$out/"
36+
37+
# The TypedPython wheel.
38+
rm -f "$out"/typedpython-*.whl
39+
uv build --wheel --out-dir "$out" python-multiplatform-ksp >/dev/null
40+
41+
# The examples, with the release version written into them.
42+
ex="$out/examples"
43+
rm -rf "$ex"
44+
cp -R docs/release/desktop "$ex"
45+
find "$ex" -type f \( -name '*.kts' -o -name '*.md' -o -name '*.properties' \) -exec sed -i.bak "s/@VERSION@/$version/g" {} +
46+
find "$ex" -name '*.bak' -delete
47+
(cd "$out" && rm -f "python-multiplatform-examples-$version.zip" && zip -qr "python-multiplatform-examples-$version.zip" examples)
48+
49+
(cd "$out" && rm -f SHA256SUMS && shasum -a 256 python-multiplatform-maven-repo-*.zip typedpython-*.whl \
50+
python-multiplatform-ksp-*-typedpython-gate.zip python-multiplatform-examples-*.zip > SHA256SUMS)
51+
echo "version=$version"
52+
ls -1 "$out" | grep -v '^maven-repo$\|^examples$'

tools/release/test-sync-release.sh renamed to .github/scripts/release/test-sync-release.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Tests for sync-release.sh. Builds a throwaway repo under <repo>/.tmp/.
33
set -u
44
HERE="$(cd "$(dirname "$0")" && pwd)"
5-
ROOT="$(cd "$HERE/../.." && pwd)"
5+
ROOT="$(cd "$HERE/../../.." && pwd)"
66
SYNC="$HERE/sync-release.sh"
77
SCRATCH="$ROOT/.tmp/release-sync-test.$$"
88
FAILS=0; PASSES=0
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
#!/usr/bin/env bash
2-
# Type-checks tools/stubs/consumer.py against a directory of generated Kotlin-name stubs (issue #31).
2+
# Type-checks .github/scripts/stubs/consumer.py against a directory of generated Kotlin-name stubs (issue #31).
33
#
4-
# tools/stubs/check-stubs.sh <stub-dir> [venv-dir]
4+
# .github/scripts/stubs/check-stubs.sh <stub-dir> [venv-dir]
55
#
66
# <stub-dir> is what `generatePythonStubs` wrote, e.g.
7-
# ksp-fixtures/compose/build/generated/pythonStubs/desktopMain
7+
# python-multiplatform-compose/fixtures/compose/build/generated/pythonStubs/desktopMain
88
# The venv defaults to <repo>/.tmp/stubs-venv (git-ignored) and is created on first use.
99
set -euo pipefail
1010

11-
root="$(cd "$(dirname "$0")/../.." && pwd)"
11+
root="$(cd "$(dirname "$0")/../../.." && pwd)"
1212
stubs="$(cd "${1:?usage: check-stubs.sh <stub-dir> [venv-dir]}" && pwd)"
1313
venv="${2:-$root/.tmp/stubs-venv}"
1414

@@ -17,10 +17,10 @@ if [ ! -x "$venv/bin/mypy" ]; then
1717
"$venv/bin/pip" install --quiet mypy
1818
fi
1919

20-
# tools/stubs/mypy.ini: `strict`, and `warn_unused_ignores` for the consumer so that the lines it
20+
# .github/scripts/stubs/mypy.ini: `strict`, and `warn_unused_ignores` for the consumer so that the lines it
2121
# expects to be rejected fail the check when they stop being. Errors inside the stubs themselves are
2222
# reported too: MYPYPATH modules are checked, not silenced.
2323
MYPYPATH="$stubs" "$venv/bin/mypy" \
24-
--config-file "$root/tools/stubs/mypy.ini" \
24+
--config-file "$root/.github/scripts/stubs/mypy.ini" \
2525
--cache-dir "$root/.tmp/mypy-cache" \
26-
"$root/tools/stubs/consumer.py"
26+
"$root/.github/scripts/stubs/consumer.py"

0 commit comments

Comments
 (0)