Files
python/build_python_framework_pkgs.zsh
T
Erik GomezandClaude Opus 4.7 7773030dd3 Drop --no-unsign and bump RP_SHA to 8ee72fe
The --no-unsign flag was disabling relocatable-python's own
fix_broken_signatures step (locallibs/fix.py), which ad-hoc re-signs
every binary modified by install_name_tool before ensurepip runs.
That's the exact code path needed to satisfy Apple Silicon Gatekeeper.

The flag's stated purpose ('don't touch the python.org signature') was
never achievable — install_name_tool invalidates the signature whether
you tell relocatable-python to handle it or not. Keeping --no-unsign
just left the binary in a broken signed state, which Gatekeeper SIGKILLs.

Bumping RP_SHA to 8ee72fe simultaneously, since the prior pin
(fb4dd9b) had the same code path and was only kept while investigating.
The newer SHA additionally sets CPPFLAGS for pip, useful for packages
with native extensions.

Closes the local equivalent of gregneagle/relocatable-python#32 for our
build pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 12:21:26 -05:00

257 lines
8.6 KiB
Bash
Executable File

#!/bin/zsh
#
# Build the macadmins Python 3 framework.
# Produces an installable .pkg (when signing identities are supplied) and a
# portable framework zip targeting Apple Silicon.
#
# Adapted from https://github.com/munki/munki/blob/Munki3dev/code/tools/build_python_framework.sh
set -eu
# --- Pinned upstream commits ---
RP_SHA="8ee72fe3a5dbef733365370ebf44f25022b895ef" # gregneagle/relocatable-python
MP_SHA="bbd07730d1b93ed3828246575ef5676bba74b5d1" # munki/munki-pkg
# --- Paths and constants ---
TYPE="recommended"
FRAMEWORKDIR="/Library/ManagedFrameworks/Python"
PYTHON_BIN_NEW="$FRAMEWORKDIR/Python3.framework/Versions/Current/Resources/Python.app/Contents/MacOS/Python"
PYTHON_BASEURL="https://www.python.org/ftp/python/%s/python-%s-macos%s.pkg"
TOOLSDIR="$(/usr/bin/dirname "$0")"
OUTPUTSDIR="$TOOLSDIR/outputs"
RP_BINDIR="/tmp/relocatable-python"
MP_BINDIR="/tmp/munki-pkg"
RP_ZIP="/tmp/relocatable-python.zip"
MP_ZIP="/tmp/munki-pkg.zip"
CONSOLEUSER="$(/usr/bin/stat -f "%Su" /dev/console)"
PIPCACHEDIR="/Users/${CONSOLEUSER}/Library/Caches/pip"
# --- CLI arguments (set by parse_args) ---
PYTHON_VERSION=""
INSTALLER_ID=""
APPLICATION_ID=""
NOTARY_PASSWORD=""
XCODE_PATH=""
usage() {
cat <<EOF
Usage: $(/usr/bin/basename "$0") --python-version X.Y.Z [options]
Required:
--python-version Full Python version, e.g. 3.13.13
Optional (omit for an unsigned local build):
--installer-id Developer ID Installer identity
--application-id Developer ID Application identity
--notary-password App-specific password for notarytool
--xcode-path Path to Xcode.app (CI only)
EOF
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--python-version) PYTHON_VERSION="$2"; shift 2 ;;
--installer-id) INSTALLER_ID="$2"; shift 2 ;;
--application-id) APPLICATION_ID="$2"; shift 2 ;;
--notary-password) NOTARY_PASSWORD="$2"; shift 2 ;;
--xcode-path) XCODE_PATH="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "$PYTHON_VERSION" ]]; then
echo "error: --python-version is required" >&2
usage
exit 1
fi
PYTHON_MAJOR_VERSION="${PYTHON_VERSION%.*}" # 3.13.13 -> 3.13
PYTHON_BIN_VERSION="$PYTHON_MAJOR_VERSION"
}
is_ci() {
[[ -n "${CI:-}" || -n "${GITHUB_ACTIONS:-}" ]]
}
derive_build_version() {
local rev_count
rev_count="$(/usr/bin/git -C "$TOOLSDIR" rev-list --count HEAD)"
NEWSUBBUILD=$((80620 + rev_count))
AUTOMATED_PYTHON_BUILD="$PYTHON_VERSION.$NEWSUBBUILD"
echo "$AUTOMATED_PYTHON_BUILD" > "$TOOLSDIR/build_info.txt"
echo "Build version: $AUTOMATED_PYTHON_BUILD"
}
prepare_ci_env() {
if ! is_ci; then
return
fi
echo "CI detected — clearing Homebrew and selecting Xcode."
/usr/local/bin/brew remove --force "$(/usr/local/bin/brew list)" || true
if [[ -n "$XCODE_PATH" && -d "$XCODE_PATH" ]]; then
/usr/bin/sudo /usr/bin/xcode-select -s "$XCODE_PATH"
fi
}
prepare_build_dirs() {
/usr/bin/sudo /bin/mkdir -p "$FRAMEWORKDIR"
# mkdir -m only applies to newly created dirs; ensure existing dirs are writable.
/usr/bin/sudo /bin/chmod 777 "$FRAMEWORKDIR"
if [[ -d "$FRAMEWORKDIR/Python.framework" ]]; then
/usr/bin/sudo /bin/rm -rf "$FRAMEWORKDIR/Python.framework"
fi
if [[ -d "$PIPCACHEDIR" ]]; then
echo "Removing pip cache to reduce build errors"
/usr/bin/sudo /bin/rm -rf "$PIPCACHEDIR"
fi
/bin/rm -rf "$TOOLSDIR/$TYPE"
/bin/mkdir -p "$TOOLSDIR/$TYPE/scripts"
/bin/mkdir -p "$TOOLSDIR/$TYPE/payload${FRAMEWORKDIR}"
/bin/mkdir -p "$TOOLSDIR/$TYPE/payload/usr/local/bin"
/usr/bin/sudo /usr/sbin/chown -R "${CONSOLEUSER}":wheel "$TOOLSDIR/$TYPE"
/bin/ln -s "$PYTHON_BIN_NEW" "$TOOLSDIR/$TYPE/payload/usr/local/bin/managed_python3"
}
download_tool() {
local name="$1" sha="$2" url="$3" zip_path="$4" dest="$5"
echo "Downloading $name @ $sha"
/bin/rm -rf "$zip_path" "$dest"
/usr/bin/curl -fL "$url" -o "$zip_path"
/usr/bin/unzip -q "$zip_path" -d "$dest"
}
build_framework() {
export C_INCLUDE_PATH="/Library/ManagedFrameworks/Python/Python.framework/Versions/Current/Headers/"
local rp_extract="${RP_BINDIR}/relocatable-python-${RP_SHA}"
"${rp_extract}/make_relocatable_python_framework.py" \
--baseurl "${PYTHON_BASEURL}" \
--python-version "${PYTHON_VERSION}" \
--os-version 11 \
--upgrade-pip \
--pip-requirements "${TOOLSDIR}/requirements_${TYPE}.txt" \
--destination "${FRAMEWORKDIR}"
/bin/mv "${FRAMEWORKDIR}/Python.framework" \
"$TOOLSDIR/$TYPE/payload${FRAMEWORKDIR}/Python3.framework"
}
codesign_framework() {
local identity="${APPLICATION_ID:--}" # `-` means ad-hoc
local framework_root="$TOOLSDIR/$TYPE/payload${FRAMEWORKDIR}/Python3.framework"
local versioned="$framework_root/Versions/${PYTHON_BIN_VERSION}"
if [[ "$identity" == "-" ]]; then
echo "Ad-hoc signing framework"
else
echo "Signing framework with identity: $identity"
fi
local -a cs_args
if [[ "$identity" == "-" ]]; then
cs_args=(--preserve-metadata=identifier,entitlements,flags,runtime -f)
else
cs_args=(--timestamp --preserve-metadata=identifier,entitlements,flags,runtime -f)
fi
/usr/bin/find "$versioned/bin" -type f -perm -u=x -exec \
/usr/bin/codesign -s "$identity" "${cs_args[@]}" {} \;
/usr/bin/find "$versioned/lib" -type f -perm -u=x -exec \
/usr/bin/codesign -s "$identity" "${cs_args[@]}" {} \;
/usr/bin/find "$versioned/lib" -type f -name "*dylib" -exec \
/usr/bin/codesign -s "$identity" "${cs_args[@]}" {} \;
/usr/bin/codesign -s "$identity" --deep "${cs_args[@]}" "$versioned/Resources/Python.app"
/usr/bin/codesign -s "$identity" "${cs_args[@]}" "$versioned/Python"
/usr/bin/codesign -s "$identity" "${cs_args[@]}" "$framework_root/Versions/Current/Python"
/usr/sbin/spctl -a -vvvv "$versioned/Python" || true
}
build_pkg() {
/bin/mkdir -p "$OUTPUTSDIR"
/bin/cp "${TOOLSDIR}/preinstall-cleanup" "$TOOLSDIR/$TYPE/scripts/preinstall"
if [[ -z "$INSTALLER_ID" ]]; then
echo "No installer identity provided; skipping signed pkg"
return
fi
/bin/cat <<JSON > "$TOOLSDIR/$TYPE/build-info.json"
{
"ownership": "recommended",
"suppress_bundle_relocation": true,
"identifier": "io.macadmins.python.$TYPE",
"postinstall_action": "none",
"distribution_style": true,
"version": "$AUTOMATED_PYTHON_BUILD",
"name": "python_${TYPE}_signed-$AUTOMATED_PYTHON_BUILD.pkg",
"install_location": "/",
"preserve_xattr": true,
"signing_info": {
"identity": "$INSTALLER_ID",
"timestamp": true
}
}
JSON
"${MP_BINDIR}/munki-pkg-${MP_SHA}/munkipkg" "$TOOLSDIR/$TYPE"
local pkg_built="$TOOLSDIR/$TYPE/build/python_${TYPE}_signed-$AUTOMATED_PYTHON_BUILD.pkg"
/bin/mv "$pkg_built" "$OUTPUTSDIR"
}
notarize_and_staple() {
if [[ -z "$NOTARY_PASSWORD" || -z "$INSTALLER_ID" ]]; then
echo "Skipping notarization (no notary password or installer id)"
return
fi
local xcode_dev xcode_notary xcode_stapler pkg
xcode_dev="$(/usr/bin/xcode-select -p)"
xcode_notary="$xcode_dev/usr/bin/notarytool"
xcode_stapler="$xcode_dev/usr/bin/stapler"
pkg="$OUTPUTSDIR/python_${TYPE}_signed-$AUTOMATED_PYTHON_BUILD.pkg"
"$xcode_notary" store-credentials \
--apple-id "opensource@macadmins.io" \
--team-id "T4SK8ZXCXG" \
--password "$NOTARY_PASSWORD" \
macadminpython
"$xcode_notary" submit "$pkg" --keychain-profile macadminpython --wait
"$xcode_stapler" staple "$pkg"
}
zip_framework() {
local zipfile="Python3.framework_$TYPE-$AUTOMATED_PYTHON_BUILD.zip"
/usr/bin/ditto -c -k --sequesterRsrc \
"$TOOLSDIR/$TYPE/payload${FRAMEWORKDIR}/" "$zipfile"
/bin/mv "$zipfile" "$OUTPUTSDIR"
/usr/bin/sudo /usr/sbin/chown -R "${CONSOLEUSER}":wheel "$OUTPUTSDIR"
}
cleanup() {
/usr/bin/sudo /bin/rm -rf "$TOOLSDIR/$TYPE"
/usr/bin/sudo /bin/rm -rf "$FRAMEWORKDIR"
}
# --- Main ---
parse_args "$@"
echo "Building Python framework — $PYTHON_VERSION"
prepare_ci_env
derive_build_version
prepare_build_dirs
download_tool relocatable-python "$RP_SHA" \
"https://github.com/gregneagle/relocatable-python/archive/${RP_SHA}.zip" \
"$RP_ZIP" "$RP_BINDIR"
download_tool munki-pkg "$MP_SHA" \
"https://github.com/munki/munki-pkg/archive/${MP_SHA}.zip" \
"$MP_ZIP" "$MP_BINDIR"
build_framework
codesign_framework
build_pkg
notarize_and_staple
zip_framework
cleanup
echo "Done."