# Utility methods for use in an Xcode project.
#
# An iOS XCframework cannot include any content other than the library binary
# and relevant metadata. However, Python requires a standard library at runtime.
# Therefore, it is necessary to add a build step to an Xcode app target that
# processes the standard library and puts the content into the final app.
#
# In general, these tools will be invoked after bundle resources have been
# copied into the app, but before framework embedding (and signing).
#
# The following is an example script, assuming that:
# * Python.xcframework is in the root of the project
# * There is an `app` folder that contains the app code
# * There is an `app_packages` folder that contains installed Python packages.
# -----
#   set -e
#   source $PROJECT_DIR/Python.xcframework/build/build_utils.sh
#   install_python Python.xcframework app app_packages
# -----

# Copy the standard library from the XCframework into the app bundle.
#
# Accepts one argument:
# 1. The path, relative to the root of the Xcode project, where the Python
#    XCframework can be found.
install_stdlib() {
    PYTHON_XCFRAMEWORK_PATH=$1

    mkdir -p "$CODESIGNING_FOLDER_PATH/python/lib"
    if [ "$EFFECTIVE_PLATFORM_NAME" = "-iphonesimulator" ]; then
        echo "Installing Python modules for iOS Simulator"
        if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/ios-arm64-simulator" ]; then
            SLICE_FOLDER="ios-arm64-simulator"
        else
            SLICE_FOLDER="ios-arm64_x86_64-simulator"
        fi
    else
        echo "Installing Python modules for iOS Device"
        SLICE_FOLDER="ios-arm64"
    fi

    # If the XCframework has a shared lib folder, then it's a full framework.
    # Copy both the common and slice-specific part of the lib directory.
    # Otherwise, it's a single-arch framework; use the "full" lib folder.
    if [ -d "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib" ]; then
        rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/"
        rsync -au "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib-$ARCHS/" "$CODESIGNING_FOLDER_PATH/python/lib/"
    else
        # A single-arch framework will have a libpython symlink; that can't be included at runtime
        rsync -au --delete "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/$SLICE_FOLDER/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" --exclude 'libpython*.dylib'
    fi
}

# Convert a single .so library into a framework that iOS can load.
#
# Accepts three arguments:
# 1. The path, relative to the root of the Xcode project, where the Python
#    XCframework can be found.
# 2. The base path, relative to the installed location in the app bundle, that
#    needs to be processed. Any .so file found in this path (or a subdirectory
#    of it) will be processed.
# 2. The full path to a single .so file to process. This path should include
#    the base path.
install_dylib () {
    PYTHON_XCFRAMEWORK_PATH=$1
    INSTALL_BASE=$2
    FULL_EXT=$3

    # The name of the extension file
    EXT=$(basename "$FULL_EXT")
    # The name and location of the module
    MODULE_PATH=$(dirname "$FULL_EXT")
    MODULE_NAME=$(echo $EXT | cut -d "." -f 1)
    # The location of the extension file, relative to the bundle
    RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/}
    # The path to the extension file, relative to the install base
    PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/}
    # The full dotted name of the extension module, constructed from the file path.
    FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d "." -f 1 | tr "/" ".");
    # A bundle identifier; not actually used, but required by Xcode framework packaging
    FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr "_" "-")
    # The name of the framework folder.
    FRAMEWORK_FOLDER="Frameworks/$FULL_MODULE_NAME.framework"

    # If the framework folder doesn't exist, create it.
    if [ ! -d "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" ]; then
        echo "Creating framework for $RELATIVE_EXT"
        mkdir -p "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER"
        cp "$PROJECT_DIR/$PYTHON_XCFRAMEWORK_PATH/build/$PLATFORM_FAMILY_NAME-dylib-Info-template.plist" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist"
        plutil -replace CFBundleExecutable -string "$FULL_MODULE_NAME" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist"
        plutil -replace CFBundleIdentifier -string "$FRAMEWORK_BUNDLE_ID" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist"
    fi

    echo "Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME"
    mv "$FULL_EXT" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME"
    # Create a placeholder .fwork file where the .so was
    echo "$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" > ${FULL_EXT%.so}.fwork
    # Create a back reference to the .so file location in the framework
    echo "${RELATIVE_EXT%.so}.fwork" > "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin"

    # If the framework provides an xcprivacy file, install it.
    if [ -e "$MODULE_PATH/$MODULE_NAME.xcprivacy" ]; then
        echo "Installing XCPrivacy file for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME"
        XCPRIVACY_FILE="$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/PrivacyInfo.xcprivacy"
        if [ -e "$XCPRIVACY_FILE" ]; then
          rm -rf "$XCPRIVACY_FILE"
        fi
        mv "$MODULE_PATH/$MODULE_NAME.xcprivacy" "$XCPRIVACY_FILE"
    fi

    echo "Signing framework as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)..."
    /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER"
}

# Process all the dynamic libraries in a path into Framework format.
#
# Accepts two arguments:
# 1. The path, relative to the root of the Xcode project, where the Python
#    XCframework can be found.
# 2. The base path, relative to the installed location in the app bundle, that
#    needs to be processed. Any .so file found in this path (or a subdirectory
#    of it) will be processed.
process_dylibs () {
    PYTHON_XCFRAMEWORK_PATH=$1
    LIB_PATH=$2
    find "$CODESIGNING_FOLDER_PATH/$LIB_PATH" -name "*.so" | while read FULL_EXT; do
        install_dylib $PYTHON_XCFRAMEWORK_PATH "$LIB_PATH/" "$FULL_EXT"
    done
}

# The entry point for post-processing a Python XCframework.
#
# Accepts 1 or more arguments:
# 1. The path, relative to the root of the Xcode project, where the Python
#    XCframework can be found. If the XCframework is in the root of the project,
# 2+. The path of a package, relative to the root of the packaged app, that contains
#     library content that should be processed for binary libraries.
install_python() {
    PYTHON_XCFRAMEWORK_PATH=$1
    shift

    install_stdlib $PYTHON_XCFRAMEWORK_PATH
    PYTHON_VER=$(ls -1 "$CODESIGNING_FOLDER_PATH/python/lib")
    echo "Install Python $PYTHON_VER standard library extension modules..."
    process_dylibs $PYTHON_XCFRAMEWORK_PATH python/lib/$PYTHON_VER/lib-dynload

    for package_path in $@; do
        echo "Installing $package_path extension modules ..."
        process_dylibs $PYTHON_XCFRAMEWORK_PATH $package_path
    done
}
