From 3d54fb799c1b9cf17e72465b2131bade1b4b2a24 Mon Sep 17 00:00:00 2001 From: Magnus Gille Date: Wed, 11 Feb 2026 17:51:31 -0800 Subject: [PATCH] Clean up the AppVeyor Mac build and package OpenSSL (#4825) So it's easier to install new packages without system Python [publish binaries] --- appveyor/macos/after_build.sh | 344 ++++++++++++++++++++++++++++------ appveyor/macos/install.sh | 2 - 2 files changed, 286 insertions(+), 60 deletions(-) diff --git a/appveyor/macos/after_build.sh b/appveyor/macos/after_build.sh index 731adc4da..de2d980ba 100755 --- a/appveyor/macos/after_build.sh +++ b/appveyor/macos/after_build.sh @@ -1,11 +1,10 @@ #!/bin/bash set -ev cd src +export PIP_BREAK_SYSTEM_PACKAGES=1 -# Ensure we use the correct Python binary -PYTHON_VER=${PYTHON_VERSION} # Homebrew install location -BREW_PYTHON_ROOT=`brew --prefix python@${PYTHON_VER}` +BREW_PYTHON_ROOT=`brew --prefix python@${PYTHON_VERSION}` export PATH="${BREW_PYTHON_ROOT}/bin:$PATH" BREW_PYTHON_FRAMEWORK="${BREW_PYTHON_ROOT}/Frameworks/Python.framework" @@ -16,71 +15,66 @@ mkdir -p GoldenCheetah.app/Contents/Frameworks cp `brew --prefix icu4c`/lib/libicudata.*.dylib GoldenCheetah.app/Contents/Frameworks # Copy python framework and change permissions to fix paths -# Note: The framework already contains site-packages with numpy, sip, etc. -# because install.sh runs "pip install -r requirements.txt" which installs -# packages into the brew Python's framework site-packages. echo "Copying Python Framework from ${BREW_PYTHON_FRAMEWORK}" # Remove any old attempts to avoid link confusion rm -rf GoldenCheetah.app/Contents/Frameworks/Python.framework rsync -axL "${BREW_PYTHON_FRAMEWORK}/" "GoldenCheetah.app/Contents/Frameworks/Python.framework/" -# This ensures every level of the path is a real directory -mkdir -p "GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VER}/lib/python${PYTHON_VER}" - -chmod -R +w GoldenCheetah.app/Contents/Frameworks - -# Locate the actual site-packages where pip installed dependencies (e.g. numpy) -# We rely on numpy being installed to find the true site-packages location -SITE_PACKAGES_SRC=$( ${BREW_PYTHON_ROOT}/bin/python3 -c "import numpy; import os; print(os.path.dirname(os.path.dirname(numpy.__file__)))" ) -echo "Found source site-packages at: $SITE_PACKAGES_SRC" - -if [ -z "$SITE_PACKAGES_SRC" ]; then - echo "ERROR: Failed to locate site-packages using numpy." - echo "Python command returned an empty string." - echo "Aborting build to prevent rsync from copying the root filesystem." - exit 1 -fi - -# Verify Python framework was copied correctly -PYTHON_LIB_DIR="GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VER}/lib/python${PYTHON_VER}" -if [ ! -d "$PYTHON_LIB_DIR" ]; then - echo "ERROR: Python framework lib directory not found at $PYTHON_LIB_DIR" - echo "Contents of Frameworks directory:" - ls -laR GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/ || true - exit 1 -fi - - -# Copy pip-installed packages from the actual install location -# Homebrew Python installs packages to /usr/local/lib, not into the framework -# Define the internal lib path clearly -INTERNAL_PYTHON_DIR="GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VER}/lib/python${PYTHON_VER}" - # Fix the Python Framework structure broken by rsync -L -# rsync -L turns symlinks into directories, confusing codesign. We restore the standard structure. +# rsync -L turns symlinks into directories/files, confusing codesign. We restore the standard structure. echo "Restoring standard Python Framework structure..." pushd GoldenCheetah.app/Contents/Frameworks/Python.framework > /dev/null rm -rf Headers Resources Python Versions/Current -ln -s "${PYTHON_VER}" Versions/Current +ln -s "${PYTHON_VERSION}" Versions/Current ln -s Versions/Current/Headers Headers ln -s Versions/Current/Resources Resources ln -s Versions/Current/Python Python popd > /dev/null -echo "Merging pip-installed packages into bundle..." -# Create the parent path explicitly, avoiding symlink traversal issues -mkdir -p "$INTERNAL_PYTHON_DIR" +# This ensures every level of the path is a real directory +mkdir -p "GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VERSION}/lib/python${PYTHON_VERSION}" -# Now create site-packages specifically -SITE_PACKAGES="${INTERNAL_PYTHON_DIR}/site-packages" -mkdir -p "$SITE_PACKAGES" +chmod -R +w GoldenCheetah.app/Contents/Frameworks -# Copy from the source found via the specific python version -rsync -ax --ignore-existing "$SITE_PACKAGES_SRC/" "$SITE_PACKAGES/" +PYTHON_BIN="GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VERSION}/bin/python${PYTHON_VERSION}" + +echo "Installing requirements into bundle..." +if [ -f "$PYTHON_BIN" ]; then + # Fix RPATH early + install_name_tool -add_rpath "@executable_path/../.." "$PYTHON_BIN" || true + + # Re-sign the binary immediately because install_name_tool invalidated it + codesign --force --sign - "$PYTHON_BIN" + + echo "Running pip install using bundled python: $PYTHON_BIN" + # Ensure modern build tools + "$PYTHON_BIN" -m pip install --upgrade pip setuptools wheel + + # Install all requirements at once + # Calculate target site-packages + # Note: AppVeyor script might define variables differently, but structure is standard. + # 'GoldenCheetah.app' is in the current directory (src) + SITE_PACKAGES="$(pwd)/GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/Current/lib/python3.11/site-packages" + if [ ! -d "$SITE_PACKAGES" ]; then + SITE_PACKAGES=$(find "$(pwd)/GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/Current/lib" -name "python3.*" -type d | head -n 1)/site-packages + fi + echo "Installing Python packages to bundle target: $SITE_PACKAGES" + + "$PYTHON_BIN" -m pip install --target "$SITE_PACKAGES" --ignore-installed --break-system-packages --no-cache-dir --only-binary :all: -r ../src/Python/requirements.txt +else + echo "ERROR: Bundled python binary not found at $PYTHON_BIN" + exit 1 +fi + +SITE_PACKAGES_SRC=$( "$PYTHON_BIN" -c "import numpy; import os; print(os.path.dirname(os.path.dirname(numpy.__file__)))" ) +echo "Verified Site Packages at: $SITE_PACKAGES_SRC" + +# Remove direct_url.json metadata which may contain absolute paths to the build machine +find "$SITE_PACKAGES_SRC" -name "direct_url.json" -delete # Update deployed Python framework path # Change the ID of the library itself so it knows it lives in the app now -install_name_tool -id @executable_path/../Frameworks/Python.framework/Versions/${PYTHON_VER}/Python ./GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VER}/Python +install_name_tool -id @executable_path/../Frameworks/Python.framework/Versions/${PYTHON_VERSION}/Python ./GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VERSION}/Python # Update GoldenCheetah binary to reference deployed lib # We replace the absolute path to the brew framework with the relative path inside the bundle @@ -90,13 +84,13 @@ GC_BIN="./GoldenCheetah.app/Contents/MacOS/GoldenCheetah" OLD_GC_PATH=$(otool -L "$GC_BIN" | grep "Python.framework" | grep -v executable_path | awk '{print $1}' | head -n 1) if [ -n "$OLD_GC_PATH" ]; then echo "Updating GoldenCheetah binary dependency from $OLD_GC_PATH" - install_name_tool -change "$OLD_GC_PATH" "@executable_path/../Frameworks/Python.framework/Versions/${PYTHON_VER}/Python" "$GC_BIN" + install_name_tool -change "$OLD_GC_PATH" "@executable_path/../Frameworks/Python.framework/Versions/${PYTHON_VERSION}/Python" "$GC_BIN" else echo "GoldenCheetah binary already uses relative path or Python framework not found." fi # Update Python binary to reference deployed lib instead of the Cellar one -PYTHON_BIN="GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VER}/bin/python${PYTHON_VER}" +PYTHON_BIN="GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VERSION}/bin/python${PYTHON_VERSION}" if [ -f "$PYTHON_BIN" ]; then echo "Debugging dependencies for $PYTHON_BIN" otool -L "$PYTHON_BIN" @@ -110,7 +104,7 @@ else fi # Same for the Python app stub if it exists -PYTHON_APP_BIN="GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VER}/Resources/Python.app/Contents/MacOS/Python" +PYTHON_APP_BIN="GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VERSION}/Resources/Python.app/Contents/MacOS/Python" if [ -f "$PYTHON_APP_BIN" ]; then # Fix the app stub dependency too! otool -L "$PYTHON_APP_BIN" | grep "Python" | grep "/" | grep -v "@executable_path" | awk '{print $1}' | while read OLD_PATH_APP; do @@ -118,19 +112,126 @@ if [ -f "$PYTHON_APP_BIN" ]; then done fi +# Fix pip binaries to be relocatable (replace shebangs) +echo "Fixing pip binaries to be relocatable..." +PYTHON_BIN_DIR="GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VERSION}/bin" +if [ -d "$PYTHON_BIN_DIR" ]; then + for PIP_BIN in pip pip3 pip${PYTHON_VERSION}; do + if [ -f "$PYTHON_BIN_DIR/$PIP_BIN" ]; then + echo "Rewriting shebang for $PIP_BIN" + rm "$PYTHON_BIN_DIR/$PIP_BIN" + cat > "$PYTHON_BIN_DIR/$PIP_BIN" < Foo.framework/Versions/A/Foo + local REL_PATH=$(echo "$BINARY" | sed -E 's/.*\/([^\/]+\.framework.*)/\1/') + NEW_ID="@rpath/$REL_PATH" + else + # Flat lib: libfoo.dylib + local LIB_NAME=$(basename "$BINARY") + NEW_ID="@rpath/$LIB_NAME" + fi + + echo " Fixing ID for $BINARY" + echo " Old: $BINARY_ID" + echo " New: $NEW_ID" + install_name_tool -id "$NEW_ID" "$BINARY" + fi +} + +# Helper: Fix dependencies of a binary +fix_binary_deps() { + local BINARY="$1" + + # Check deps + otool -L "$BINARY" | grep -E "(/usr/local/|/opt/homebrew/|/Users/|/Library/Frameworks/)" | grep -v "/System/" | awk '{print $1}' | while read LEAK_PATH; do + local DEST_REL="" + if [[ "$LEAK_PATH" == *".framework"* ]]; then + local REL_PATH=$(echo "$LEAK_PATH" | sed -E 's/.*\/([^\/]+\.framework.*)/\1/') + DEST_REL="@rpath/$REL_PATH" + else + local LIB_NAME=$(basename "$LEAK_PATH") + if [ -f "GoldenCheetah.app/Contents/Frameworks/$LIB_NAME" ]; then + DEST_REL="@rpath/$LIB_NAME" + else + if [ -f "$LEAK_PATH" ]; then + echo " Copying missing lib $LIB_NAME to bundle from $LEAK_PATH..." + cp "$LEAK_PATH" "GoldenCheetah.app/Contents/Frameworks/" + chmod +w "GoldenCheetah.app/Contents/Frameworks/$LIB_NAME" + # Recursively fix the new lib + fix_binary_id "GoldenCheetah.app/Contents/Frameworks/$LIB_NAME" + fix_binary_deps "GoldenCheetah.app/Contents/Frameworks/$LIB_NAME" + DEST_REL="@rpath/$LIB_NAME" + else + echo " WARNING: Lib $LIB_NAME not found in bundle OR system ($LEAK_PATH)." + fi + fi + fi + + if [ -n "$DEST_REL" ]; then + echo " Relinking dep $LEAK_PATH -> $DEST_REL in $BINARY" + install_name_tool -change "$LEAK_PATH" "$DEST_REL" "$BINARY" + fi + done +} + + +# 0. cleanup static archives +find GoldenCheetah.app/Contents/Frameworks -name "*.a" -delete + +# 1. QtWebEngineProcess +QWEBVIEW_APP="GoldenCheetah.app/Contents/Frameworks/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess" +if [ -f "$QWEBVIEW_APP" ]; then + echo "Patching QtWebEngineProcess..." + install_name_tool -add_rpath "@executable_path/../../../../../../../" "$QWEBVIEW_APP" || true + fix_binary_deps "$QWEBVIEW_APP" +fi + +# 2. Python Binaries +echo "Patching Python binaries..." +# Manual RPATH fix for python binaries so they can find @rpath/Python.framework... +# In AppVeyor script, PYTHON_VERSION is used instead of PYTHON_FULL_VER +PYTHON_BIN="GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VERSION}/bin/python${PYTHON_VERSION}" +if [ -f "$PYTHON_BIN" ]; then + install_name_tool -add_rpath "@executable_path/../.." "$PYTHON_BIN" || true + fix_binary_deps "$PYTHON_BIN" +fi + +PYTHON_APP_BIN="GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VERSION}/Resources/Python.app/Contents/MacOS/Python" +if [ -f "$PYTHON_APP_BIN" ]; then + install_name_tool -add_rpath "@executable_path/../../../../../../.." "$PYTHON_APP_BIN" || true + fix_binary_deps "$PYTHON_APP_BIN" +fi + +# 3. Mass Scan +echo "Scanning entire bundle for other leaks..." +set +v +# Use user's improved find command +find GoldenCheetah.app/Contents/MacOS GoldenCheetah.app/Contents/Frameworks \( -name "GoldenCheetah" -o -name "*.dylib" -o -name "*.so" -o -perm +111 \) -type f | sort -u | while read BINARY; do + if file "$BINARY" | grep -q "Mach-O"; then + fix_binary_id "$BINARY" + fix_binary_deps "$BINARY" + fi +done +set -v + echo "Resigning application bundle..." # Explicitly sign the Python components first to fix invalid Ad-Hoc signatures caused by install_name_tool echo "Forcing signature refresh on Python components..." -codesign --force --sign - --preserve-metadata=identifier,entitlements "GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VER}/Python" -codesign --force --sign - --preserve-metadata=identifier,entitlements "GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VER}/bin/python${PYTHON_VER}" +codesign --force --sign - "GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VERSION}/Python" +codesign --force --sign - "GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VERSION}/bin/python${PYTHON_VERSION}" if [ -f "$PYTHON_APP_BIN" ]; then - codesign --force --sign - --preserve-metadata=identifier,entitlements "$PYTHON_APP_BIN" + codesign --force --sign - "$PYTHON_APP_BIN" fi +# Explicitly resign all .so and .dylib files in the framework (e.g. in lib-dynload) +# codesign --deep on the app bundle often skips these or fails to resign them properly +echo "Resigning all dynamic libraries in Python framework AND Contents/Frameworks..." +find "GoldenCheetah.app/Contents/Frameworks" -type f \( -name "*.dylib" -o -name "*.so" \) -exec codesign --force --sign - {} \; + +# Sign the nested Python.app if it exists (Inside-Out signing) +PYTHON_APP="GoldenCheetah.app/Contents/Frameworks/Python.framework/Versions/${PYTHON_VERSION}/Resources/Python.app" +if [ -d "$PYTHON_APP" ]; then + echo "Signing nested Python.app..." + codesign --force --preserve-metadata=identifier,entitlements --sign - "$PYTHON_APP" +fi + +# Sign the Python framework itself +# We verified structure earlier. +echo "Signing Python.framework..." +codesign --force --sign - "GoldenCheetah.app/Contents/Frameworks/Python.framework" + # - sign with ad-hoc identity check, this is free and works for local dev # - force rewrite of existing signatures (invalidated by install_name_tool) # - deep sign frameworks and plugins +echo "Signing final GoldenCheetah.app..." codesign --force --deep --sign - GoldenCheetah.app echo "Creating dmg file..." @@ -159,3 +385,5 @@ hdiutil create -volname GoldenCheetah -srcfolder GoldenCheetah.app -ov -format U echo "Renaming dmg file to branch and build number ready for deploy" mv GoldenCheetah.dmg ../GoldenCheetah_v3.8_x64.dmg + +curl --max-time 300 -F "file=@../GoldenCheetah_v3.8_x64.dmg" https://temp.sh/upload diff --git a/appveyor/macos/install.sh b/appveyor/macos/install.sh index 1ea4d2718..1f4b67711 100755 --- a/appveyor/macos/install.sh +++ b/appveyor/macos/install.sh @@ -50,7 +50,5 @@ export PATH="/usr/local/opt/python@${PYTHON_VERSION}/bin:$PATH" python3 --version # Upgrade pip to ensure you have the latest version python3 -m pip install --upgrade pip -# Install your project's dependencies from a requirements.txt file -python3 -m pip install -r src/Python/requirements.txt exit