[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/macadmins/python/main/build_python_framework_pkgs.zsh [Back]  [Original]

#!/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 &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"
}

install_sitecustomize() {
    # Drop a sitecustomize.py that points OpenSSL at certifi's CA bundle via
    # SSL_CERT_FILE at interpreter startup. Works around macadmins/python#38:
    # python.org's framework has the OpenSSL CA path compiled in to
    # /Library/Frameworks/... which doesn't match our relocated install.
    local site_packages="$TOOLSDIR/$TYPE/payload${FRAMEWORKDIR}/Python3.framework/Versions/${PYTHON_BIN_VERSION}/lib/python${PYTHON_BIN_VERSION}/site-packages"
    /bin/cp "${TOOLSDIR}/managed_python_sitecustomize.py" "$site_packages/sitecustomize.py"
}

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}"
    local nested_frameworks_dir="$versioned/Frameworks"

    if [[ "$identity" == "-" ]]; then
        echo "Ad-hoc signing framework"
    else
        echo "Signing framework with identity: $identity"
    fi

    # Codesign notes:
    #   - Use --options=runtime to force-enable hardened runtime (required for
    #     notarization on macOS 13+). Do NOT include `runtime` in
    #     --preserve-metadata: install_name_tool just invalidated the prior
    #     signature, so there is nothing reliable to inherit.
    #   - Do NOT sign Versions/Current/Python  it's a symlink to
    #     Versions/X.Y/Python which we already signed. Double-signing through
    #     the symlink corrupts the signature on newer Python frameworks.
    #   - Sign nested .framework bundles (Tcl, Tk in Python 3.13+) with --deep,
    #     not via per-binary find. The bundle's _CodeSignature/CodeResources
    #     file must be regenerated to match the re-signed inner binary;
    #     otherwise notarytool reports "nested code is modified or invalid".

    local -a cs_args
    if [[ "$identity" == "-" ]]; then
        cs_args=(--options=runtime --preserve-metadata=identifier,entitlements,flags -f)
    else
        cs_args=(--timestamp --options=runtime --preserve-metadata=identifier,entitlements,flags -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[@]}" {} \;

    if [[ -d "$nested_frameworks_dir" ]]; then
        local nested_fw
        for nested_fw in "$nested_frameworks_dir"/*.framework; do
            [[ -d "$nested_fw" ]] || continue
            if [[ "$identity" == "-" ]]; then
                /usr/bin/codesign -s "$identity" --options=runtime --force --deep "$nested_fw"
            else
                /usr/bin/codesign -s "$identity" --timestamp --options=runtime --force --deep "$nested_fw"
            fi
        done
    fi

    /usr/bin/codesign -s "$identity" --deep "${cs_args[@]}" "$versioned/Resources/Python.app"
    /usr/bin/codesign -s "$identity" "${cs_args[@]}" "$versioned/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 

Web Proxy Viewer  |  New URL  |  Original Page