Skip to content

Commit d6f9f0b

Browse files
Add Python 3.15 builds and compatible native dependencies (#96)
1 parent 69b38db commit d6f9f0b

4 files changed

Lines changed: 123 additions & 5 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: Build Python 3.15
2+
3+
env:
4+
TYPE: "recommended"
5+
DEV_INSTALLER_ID: "Developer ID Installer: Mac Admins Open Source (T4SK8ZXCXG)"
6+
DEV_APPLICATION_ID: "Developer ID Application: Mac Admins Open Source (T4SK8ZXCXG)"
7+
NOTARY_APP_PASSWORD: ${{ secrets.NOTARY_APP_PASSWORD_MAOS }}
8+
PYTHON_VERSION: "3.15.1"
9+
PYTHON_MAJOR_VERSION: "3.15"
10+
11+
on:
12+
workflow_dispatch:
13+
pull_request:
14+
15+
jobs:
16+
build:
17+
runs-on: macos-26
18+
19+
steps:
20+
- name: Checkout python repo
21+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
22+
with:
23+
fetch-depth: 0
24+
25+
- name: Install Apple Developer ID Application certificates
26+
uses: apple-actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7.0.0
27+
with:
28+
keychain-password: ${{ github.run_id }}
29+
p12-file-base64: ${{ secrets.APP_CERTIFICATES_P12_MAOS }}
30+
p12-password: ${{ secrets.APP_CERTIFICATES_P12_PASSWORD_MAOS }}
31+
32+
- name: Install Apple Developer ID Installer certificates
33+
uses: apple-actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7.0.0
34+
with:
35+
create-keychain: false # do not create a new keychain for this value
36+
keychain-password: ${{ github.run_id }}
37+
p12-file-base64: ${{ secrets.PKG_CERTIFICATES_P12_MAOS }}
38+
p12-password: ${{ secrets.PKG_CERTIFICATES_P12_PASSWORD_MAOS }}
39+
40+
- name: Run build package script
41+
run: |
42+
./build_python_framework_pkgs.zsh \
43+
--python-version "$PYTHON_VERSION" \
44+
--installer-id "$DEV_INSTALLER_ID" \
45+
--application-id "$DEV_APPLICATION_ID" \
46+
--notary-password "$NOTARY_APP_PASSWORD"
47+
48+
- name: get environment variables
49+
id: get_env_var
50+
run: |
51+
echo "PYTHON_BUILD_VERSION=$(/bin/cat ./build_info.txt)" >> $GITHUB_ENV
52+
53+
- name: Generate changelog
54+
if: github.event_name == 'workflow_dispatch'
55+
id: changelog
56+
uses: metcalfc/changelog-generator@0440d0932f9a0dd1cc9ecd8412830761351323bd # v4.7.0
57+
with:
58+
myToken: ${{ secrets.GITHUB_TOKEN }}
59+
reverse: 'true'
60+
61+
- name: Create Release
62+
if: github.event_name == 'workflow_dispatch'
63+
id: create_release
64+
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
65+
with:
66+
name: Python ${{env.PYTHON_BUILD_VERSION}}
67+
tag_name: v${{env.PYTHON_BUILD_VERSION}}
68+
draft: false
69+
prerelease: true
70+
token: ${{ secrets.GITHUB_TOKEN }}
71+
body: |
72+
# Notes
73+
Python ${{env.PYTHON_VERSION}} Framework
74+
75+
## Changes
76+
- Upgraded Python to 3.15.1
77+
**Note: Some of these updates may have breaking changes. Always test your code before deploying to production!**
78+
79+
Please see the `requirements_recommended.txt` for the current libraries being used.
80+
81+
${{ steps.changelog.outputs.changelog }}
82+
83+
# Flavors of Python
84+
At this time, the automated build process will **only** create the Recommended package
85+
86+
## Recommended
87+
This is a Python.framework with a recommended set of libraries for tools like Autopkg, Munki, and InstallApplications.
88+
89+
## Signing/Notarization
90+
The signed package is fully notarized, including the Python.framework file
91+
files: ${{github.workspace}}/outputs/*.pkg
92+
93+
- name: Upload packages
94+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
95+
with:
96+
name: packages
97+
path: outputs/

‎README.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ This is an intended replacement for `/usr/bin/python`, which Apple removed in ma
88
## Apple Silicon Only
99
Builds and packages target Apple Silicon (arm64). Universal2 outputs are no longer produced. Build hosts and target machines must be Apple Silicon Macs.
1010

11+
CI builds Python 3.11.9, 3.12.10, 3.13.17, 3.14.9, and 3.15.1 with the shared
12+
recommended dependencies, including PyObjC 12.2.2 and CFFI 2.1.1 for Python
13+
3.15 compatibility.
14+
1115
## Why use this instead of a package from python.org?
1216
- Ships with PyObjC and other modules useful for Mac admins, similar in spirit to the Apple Python it replaces
1317
- Installs to a location less likely to be overwritten, removed, or modified by other Python installations
@@ -47,7 +51,7 @@ Not supported.
4751
Build an unsigned framework on Apple Silicon with:
4852

4953
```
50-
./build_python_framework_pkgs.zsh --python-version 3.13.17
54+
./build_python_framework_pkgs.zsh --python-version 3.15.1
5155
```
5256

5357
Pass `--installer-id`, `--application-id`, and `--notary-password` to produce a signed and notarized `.pkg`.
@@ -56,8 +60,8 @@ The build bootstraps pip 26.2.2 from a SHA-256-verified wheel before accessing
5660
PyPI, updates the pip bundled with `ensurepip`, and installs certifi 2026.10.19
5761
and virtualenv 21.14.13. These updates address the
5862
[eMudhra/emSign certificate security incident](https://discuss.python.org/t/109576)
59-
for the framework and newly created Python 3.11–3.14 virtual environments.
60-
Python 3.13 and 3.14 builds use the updated 3.13.17 and 3.14.9 installers;
63+
for the framework and newly created Python 3.11–3.15 virtual environments.
64+
Python 3.13, 3.14, and 3.15 builds use the 3.13.17, 3.14.9, and 3.15.1 installers;
6165
3.11 and 3.12 retain their last upstream macOS installers with patched `ensurepip`.
6266

6367
Before packaging, the build verifies package dependencies, CA stores, offline

‎requirements_recommended.txt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ aspy.yaml==1.3.0
33
attrs==26.1.0
44
black==26.3.1
55
certifi==2026.10.19
6-
cffi==2.0.0
6+
cffi==2.1.1
77
cfgv==3.5.0
88
charset-normalizer==3.4.7
99
click==8.3.3
@@ -28,7 +28,7 @@ pycodestyle==2.14.0
2828
pycparser==3.0
2929
pyflakes==3.4.0
3030
python-discovery==1.6.4
31-
pyobjc==12.1
31+
pyobjc==12.2.2
3232
PyYAML==6.0.3
3333
requests==2.34.0
3434
six==1.17.0

‎verify_framework.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
from pathlib import Path
1212

1313
import certifi
14+
import cffi
1415
import objc
1516
import requests
1617
import xattr
1718
import yaml
19+
from Foundation import NSObject
1820
from packaging.version import Version
1921

2022

@@ -50,6 +52,20 @@ def check_context(context):
5052

5153
def main():
5254
print(f"Verifying framework: {sys.version}", flush=True)
55+
# Exercise native extensions as well as their package metadata. This also
56+
# covers source builds when wheels are not yet available for a new Python.
57+
if cffi.FFI().sizeof("int") != 4:
58+
raise RuntimeError("CFFI native extension check failed")
59+
if NSObject.alloc().init() is None:
60+
raise RuntimeError("PyObjC Foundation bridge check failed")
61+
if yaml.safe_load("enabled: true") != {"enabled": True}:
62+
raise RuntimeError("PyYAML check failed")
63+
with tempfile.NamedTemporaryFile(prefix="macadmins-xattr-verify-") as temporary:
64+
attributes = xattr.xattr(temporary.name)
65+
attributes["io.macadmins.python.verify"] = b"working"
66+
if attributes["io.macadmins.python.verify"] != b"working":
67+
raise RuntimeError("xattr native extension check failed")
68+
print("Verified CFFI, PyObjC, PyYAML, and xattr", flush=True)
5369
for package, minimum in (("certifi", "2026.10.19"), ("pip", "26.2.2")):
5470
version = importlib.metadata.version(package)
5571
if Version(version) < Version(minimum):
@@ -65,6 +81,7 @@ def main():
6581
# Exercise relocated console scripts as well as python -m entry points.
6682
run(str(Path(sys.prefix) / "bin/pip"), "--version")
6783
run(str(Path(sys.prefix) / "bin/virtualenv"), "--version")
84+
run(str(Path(sys.prefix) / "bin/black"), "--version")
6885

6986
with tempfile.TemporaryDirectory(prefix="macadmins-python-verify-") as temporary:
7087
root = Path(temporary)

0 commit comments

Comments
 (0)