diff --git a/.github/workflows/test_macos.yaml b/.github/workflows/test_macos.yaml index 430b64e..938ea15 100644 --- a/.github/workflows/test_macos.yaml +++ b/.github/workflows/test_macos.yaml @@ -15,55 +15,47 @@ jobs: runs-on: ${{ matrix.runs_on }} strategy: matrix: - include: - - runs_on: macos-11 - python: 3.9 - - runs_on: apple-silicon-m1 - python: 3.9.7 + runs_on: ['macos-latest', 'apple-silicon-m1'] name: macOS build dmg ( ${{ matrix.runs_on }} ) steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python 3.x - # Needs to be skipped on our self-hosted runners tagged as 'apple-silicon-m1' - if: ${{ matrix.runs_on != 'apple-silicon-m1' }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python }} + python-version: 3.x - name: Install Platypus run: | source ci/osx_ci.sh - arm64_set_path_and_python_version ${{ matrix.python }} install_platypus + - name: Install build dependencies + run: | + brew install pkg-config cmake ninja - name: Create macOS App Bundle with extras [dev] run: | source ci/osx_ci.sh - arm64_set_path_and_python_version ${{ matrix.python }} cd osx ./create-osx-bundle.sh -e "dev" - name: Test Fix bundle metadata run: | source ci/osx_ci.sh - arm64_set_path_and_python_version ${{ matrix.python }} cd osx ./fix-bundle-metadata.sh build/Kivy.app -v "1.2.3" ./fix-bundle-metadata.sh build/Kivy.app -v "master" - name: Test Relocation run: | source ci/osx_ci.sh - arm64_set_path_and_python_version ${{ matrix.python }} cd osx ./relocate.sh build/Kivy.app - name: Test dmg creation and store it in osx_artifacts run: | source ci/osx_ci.sh - arm64_set_path_and_python_version ${{ matrix.python }} cd osx ./create-osx-dmg.sh build/Kivy.app Kivy cd .. mkdir osx_artifacts mv osx/Kivy.dmg osx_artifacts/${{ matrix.runs_on }}-Kivy.dmg - name: Upload dmg as artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: KivySDKPackager path: osx_artifacts @@ -73,19 +65,19 @@ jobs: runs-on: ${{ matrix.runs_on }} strategy: matrix: - runs_on: [macos-11, apple-silicon-m1] + runs_on: ['macos-latest', 'apple-silicon-m1'] name: macOS test dmg ( ${{ matrix.runs_on }} ) steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Download dmg from artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: KivySDKPackager path: osx_artifacts - - name: Mount build from macos-11 runner on Apple Silicon runner + - name: Mount build from macos-latest runner on apple-silicon-m1 runner if: ${{ matrix.runs_on == 'apple-silicon-m1' }} - run: hdiutil attach osx_artifacts/macos-11-Kivy.dmg -mountroot . - - name: Mount build from Apple Silicon runner on macos-11 runner + run: hdiutil attach osx_artifacts/macos-latest-Kivy.dmg -mountroot . + - name: Mount build from apple-silicon-m1n runner on macos-latest runner if: ${{ matrix.runs_on != 'apple-silicon-m1' }} run: hdiutil attach osx_artifacts/apple-silicon-m1-Kivy.dmg -mountroot . - name: Copy Kivy.app to Applications diff --git a/ci/osx_ci.sh b/ci/osx_ci.sh index f014c0b..acb7e10 100644 --- a/ci/osx_ci.sh +++ b/ci/osx_ci.sh @@ -17,18 +17,6 @@ download_cache_curl() { fi } - -arm64_set_path_and_python_version(){ - python_version="$1" - if [[ $(/usr/bin/arch) = arm64 ]]; then - export PATH=/opt/homebrew/bin:$PATH - eval "$(pyenv init --path)" - pyenv install $python_version -s - pyenv global $python_version - export PATH=$(pyenv prefix)/bin:$PATH - fi -} - install_platypus() { download_cache_curl "platypus5.3.zip" "osx-cache" "https://github.com/sveinbjornt/Platypus/releases/download/5.3" diff --git a/osx/README.md b/osx/README.md index 0f057a9..c4761dc 100644 --- a/osx/README.md +++ b/osx/README.md @@ -1,15 +1,18 @@ -Kivy packaging for OS X -======================= +Kivy packaging for macOS +======================== This repository contains the scripts for packaging a Kivy based app into a installable dmg. **Important notice:** macOS 11 (or greater), with XCode 12.2 (or greater) is required to build a fully working universal2 ``.app`` due to https://bugs.python.org/issue42619 -Kivy versions supported: ``2.0.0+``. For older Kivy versions, use the corresponding stable branch. +Kivy versions supported: ``2.2.0+``. For older Kivy versions, use the corresponding stable branch. -The packaged dmg contains a Python virtualenv, Kivy and its binary dependencies such as SDL2. Kivy -provides an existing dmg containing the virtualenv and Kivy pre-installed that can be used -as a base into which your app can be installed and packaged again as a dmg. Below are the steps: +The packaged ``.app`` contains a Python virtualenv with ``kivy`` and its dependencies pre-installed. + +SDL2 frameworks are included in the app bundle, and the Kivy installation is configured to use them. + +Kivy, on every release, provides a ``Kivy.app`` that can be used as a base for your app, so you don't need to build it from scratch. +Below are the steps to use the ``Kivy.app`` as a base for your app, or to build your app from scratch. * Get the Kivy sdk repo with e.g. ``git clone https://github.com/kivy/kivy-sdk-packager.git`` and ``cd`` into @@ -19,7 +22,8 @@ as a base into which your app can be installed and packaged again as a dmg. Belo ./create-osx-bundle.sh -n MyApp -k ... - See in ``create-osx-bundle.sh`` for all the configuration options. + For all the configuration options, you can run ``./create-osx-bundle.sh -h``. + This will build from scratch all the requirements (openssl, SDL2, SDL2_image, SDL2_mixer, SDL2_ttf, python3). A ``build`` directory is created to contain the``MyApp.app`` directory, where ``MyApp`` is the app's name. * To use the existing Kivy app bundle: @@ -46,7 +50,7 @@ as a base into which your app can be installed and packaged again as a dmg. Belo source kivy_activate popd - On the default mac shell you **must** be in the bin directory containing ``activate`` to be + On the default macOS shell (zsh) you **must** be in the bin directory containing ``activate`` to be able to ``activate`` the virtualenv. ``kivy_activate`` is only necessary if you'll run Kivy in the environment - it sets up the Kivy home, and other Kivy environment variables. @@ -64,13 +68,19 @@ as a base into which your app can be installed and packaged again as a dmg. Belo @executable_path/Contents/Frameworks/MyFramework.framework/Versions/A/MyFramework This should be customized for each framework. See the ``create-osx-bundle.sh`` script for examples. - * Install your dependencies with e.g. ``pip``:: + * By using the ``prepare-wheels.py`` helper, download and prepare wheels before installing them. + This script will download the wheels, accordingly to your ``MACOSX_DEPLOYMENT_TARGET`` and merge them into a single ``universal2`` wheel if one is not available from PyPI. - python -m pip install ... + If no compatible wheel is available, the script will download the source distribution. - * Install your app:: + To see all the available options, and usage instructions, run:: - python -m pip install myapp + python prepare-wheels.py -h + + Since this tool is rapidly evolving, and the process to install the downloaded + wheels is done via ``pip``, consider looking at ``create-osx-bundle.sh`` for examples on how to use the downloaded artifacts. + + :warning: Never install dependencies via ``pip`` from the virtual environment, without using the ``prepare-wheels.py`` helper, as it will install the architecture-specific wheels, and not the ``universal2`` ones. * Deactivate the virtualenv by running ``deactivate`` in the shell. * Reduce app size (optional): @@ -124,8 +134,14 @@ A complete example using ``Kivy.app`` with a entry_point pointing to your app as git clone https://github.com/user/myapp.git git clone https://github.com/kivy/kivy-sdk-packager.git + + python3 -m venv venv + source venv/bin/activate + cd kivy-sdk-packager/osx + pip install -r requirements.txt + curl -O -L https://xxx/Kivy-xxx.dmg hdiutil attach Kivy-xxx.dmg -mountroot . @@ -134,12 +150,17 @@ A complete example using ``Kivy.app`` with a entry_point pointing to your app as ./fix-bundle-metadata.sh MyApp.app -n MyApp -v "0.1.1" -a "Name" -o \ "org.myorg.myapp" -i "../../myapp/doc/source/images/myapp_icon.png" + # Prepare a my-app-requirements.txt file with your app's dependencies (even indirect ones) + # and run the following command to prepare the distributions to later be installed in the + # virtualenv. + python prepare-wheels.py --requirements-file my-app-requirements.txt --output-folder my-app-wheels + pushd MyApp.app/Contents/Resources/venv/bin source activate popd - python -m pip install --upgrade pyobjus plyer ... - python -m pip install ../../myapp/ + SITE_PACKAGES_DIR=$(python -c "import site; print(site.getsitepackages()[0])") + pip install --platform macosx_11_0_universal2 --find-links=./my-app-wheels --no-deps --target $SITE_PACKAGES_DIR -r my-app-requirements.txt # Reduce app size ./cleanup-app.sh MyApp.app @@ -156,58 +177,12 @@ A complete example using ``Kivy.app`` with a entry_point pointing to your app as Example create app from scratch ------------------------------- -A complete example creating a bundle and building a dmg without using the prepared Kivy.app. -Also using a entry_point pointing to your app as described above -(notice the metadata and download URLs need to be replaced with actual metadata and URLs). -The dependencies versions and url should be updated as needed. Note that gstreamer is not -included:: - - # configure kivy - export CC=clang - export CXX=clang - export FFLAGS='-ff2c' - export USE_SDL2=1 - - # get the dependencies - export PLATYPUS=5.3 - - curl -O -L "http://www.sveinbjorn.org/files/software/platypus/platypus$PLATYPUS.zip" - - unzip platypus$PLATYPUS.zip - gunzip Platypus.app/Contents/Resources/platypus_clt.gz - gunzip Platypus.app/Contents/Resources/ScriptExec.gz - mkdir -p /usr/local/bin - mkdir -p /usr/local/share/platypus - cp Platypus.app/Contents/Resources/platypus_clt /usr/local/bin/platypus - cp Platypus.app/Contents/Resources/ScriptExec /usr/local/share/platypus/ScriptExec - cp -a Platypus.app/Contents/Resources/MainMenu.nib /usr/local/share/platypus/MainMenu.nib - chmod -R 755 /usr/local/share/platypus - - # create app - git clone https://github.com/user/myapp.git - git clone https://github.com/kivy/kivy-sdk-packager.git - cd kivy-sdk-packager/osx - - ./create-osx-bundle.sh -k master -n MyApp -v "0.1.1" -a "Name" -o \ - "org.myorg.myapp" -i "../../myapp/doc/source/images/myapp_icon.png" -g 0 - - pushd build/MyApp.app/Contents/Resources/venv/bin - source activate - popd - - python -m pip install --upgrade pyobjus plyer ... - python -m pip install ../../../myapp/ - - # reduce app size - ./cleanup-app.sh MyApp.app - - # the link needs to be created relative to the yourapp path, so go to that directory - pushd build/MyApp.app/Contents/Resources/ - ln -s ./venv/bin/myapp yourapp - popd +``create-osx-bundle.sh`` can be used to create a app bundle from scratch. It will download and +build all the dependencies, and create a app bundle with a virtualenv with the dependencies +installed. - ./relocate.sh build/MyApp.app - ./create-osx-dmg.sh build/MyApp.app MyApp +You can later use the same steps as above to install your app and its dependencies, and create a +dmg. Dev note:: Buildozer uses this repository for its OS X packaging process. diff --git a/osx/create-osx-bundle.sh b/osx/create-osx-bundle.sh index 49afb9a..93ce0c3 100755 --- a/osx/create-osx-bundle.sh +++ b/osx/create-osx-bundle.sh @@ -2,25 +2,6 @@ set -x # verbose set -e # exit on error -USAGE="Creates a Kivy bundle that can be used to build your app into a dmg. See documentation. - -Usage: create-osx-bundle.sh [options] - - -k --kivy The local path to Kivy source or a git tag/branch/commit. - -e --extras The extras selection (base, full, dev ...). - -p --python The Python version to use. - -n --name The name of the app. - -v --version The version of the app. - -a --author The author name. - -o --org The org id used for the app. - -i --icon A icns icon file path. - -s --script The script to run when the user clicks the app. - -Requirements:: - Platypus needs to be installed. Finally, any python3 version must be available for - initial scripting. -" - KIVY_PATH="master" EXTRAS="base" PYVER="3.11.2" @@ -32,6 +13,25 @@ APP_ORG="org.kivy.osxlauncher" ICON_PATH="data/icon.icns" APP_SCRIPT="data/script" +USAGE="Creates a Kivy bundle that can be used to build your app into a dmg. See documentation. + +Usage: create-osx-bundle.sh [options] + + -k --kivy The local path to Kivy source or a git tag/branch/commit. + -e --extras The extras selection (base, full, dev ...). + -p --python The Python version to use. + -n --name The name of the app. + -v --version The version of the app. + -a --author The author name. + -o --org The org id used for the app. + -i --icon A icns icon file path. + -s --script The script to run when the user clicks the app. + +Requirements:: + Platypus needs to be installed. Finally, any python3 version must be available for + initial scripting. +" + while [[ "$#" -gt 0 ]]; do # empty arg? if [ -z "$2" ]; then @@ -126,9 +126,9 @@ else curl -L -O "https://raw.githubusercontent.com/kivy/kivy/master/tools/build_macos_dependencies.sh" fi -echo "-- Set MACOSX_DEPLOYMENT_TARGET=10.9" +echo "-- Set MACOSX_DEPLOYMENT_TARGET=11.0" export SDKROOT=$(xcrun -sdk macosx --show-sdk-path) -export MACOSX_DEPLOYMENT_TARGET=10.9 +export MACOSX_DEPLOYMENT_TARGET=11.0 echo "-- Build OpenSSL (x86_64)" tar -xvf "openssl-${OPENSSL_VERSION}.tar.gz" @@ -169,15 +169,6 @@ echo "-- Build Kivy dependencies via build_macos_dependencies.sh" chmod +x build_macos_dependencies.sh ./build_macos_dependencies.sh -echo "-- Create a virtualenv in ${APP_NAME}.app/Contents/Resources" -pushd "$APP_NAME.app/Contents/Resources/" -$KIVY_APP_PYTHON_BIN -m pip install --upgrade pip virtualenv --user -$KIVY_APP_PYTHON_BIN -m venv venv - -echo "-- Activate the just created virtualenv" -source venv/bin/activate -popd - echo "-- Ensure KIVY_DEPS_ROOT is set to the dependencies folder" export KIVY_DEPS_ROOT=$(pwd)/kivy-dependencies @@ -194,56 +185,27 @@ echo "-- Compile the requirements.txt file via pip-compile, so we have a full li pip install pip-tools pip-compile kivy-app-requirements.in --no-annotate --no-header -o kivy-app-requirements.txt -echo "-- Install the requirements via pip" +echo "-- Call prepare-wheels.py to download (and fuse) all the wheels and source distributions" +pip install -r ../requirements.txt +WHEELS_FOLDER=$(pwd)/wheels +$PYTHON ../prepare-wheels.py --requirements-file kivy-app-requirements.txt --python-version $PYVER --deployment-target $MACOSX_DEPLOYMENT_TARGET --output-folder $WHEELS_FOLDER +echo "-- Create a virtualenv in ${APP_NAME}.app/Contents/Resources" +pushd "$APP_NAME.app/Contents/Resources/" +$KIVY_APP_PYTHON_BIN -m pip install --upgrade pip virtualenv --user +$KIVY_APP_PYTHON_BIN -m venv venv -# This will install macosx_10_9_universal2 and none wheels from PyPI, if available. -# If not, it will install the source distribution and try to build it locally. +echo "-- Activate the just created virtualenv" +source venv/bin/activate +popd +echo "-- Install the requirements via pip" SITE_PACKAGES_DIR=$(python -c "import site; print(site.getsitepackages()[0])") -# check if pillow is in kivy-app-requirements.txt file, if so, install it -if grep -q "pillow" kivy-app-requirements.txt; then - echo "-- Install Pillow via pip (needs to be done separately as it requires specific --global-option flags) and additional dependencies" - - pushd $BUILD_FOLDER +pip install --platform macosx_11_0_universal2 --find-links=$WHEELS_FOLDER --no-deps --target $SITE_PACKAGES_DIR -r kivy-app-requirements.txt - echo "-- Download needed dependencies (libjpeg, zlib)" - curl -L -O "http://www.ijg.org/files/jpegsrc.v9d.tar.gz" - curl -L -O "https://zlib.net/zlib-1.2.13.tar.gz" - - echo "-- Build libjpeg (universal2)" - tar -xvf "jpegsrc.v9d.tar.gz" - pushd jpeg-9d - CFLAGS="-arch x86_64 -arch arm64" ./configure --enable-shared=no --enable-static=yes --prefix=$BUILD_FOLDER/jpeg-9d/build - make clean - make install - popd - - echo "-- Build zlib (universal2)" - tar -xvf "zlib-1.2.13.tar.gz" - pushd zlib-1.2.13 - CFLAGS="-arch x86_64 -arch arm64" ./configure --prefix=$BUILD_FOLDER/zlib-1.2.13/build --static - make clean - make install - popd - - popd - - echo "-- Build Pillow (universal2)" - CFLAGS="-I$BUILD_FOLDER/jpeg-9d/build/include -I$BUILD_FOLDER/zlib-1.2.13/build/include" \ - LDFLAGS="-L$BUILD_FOLDER/jpeg-9d/build/lib -L$BUILD_FOLDER/zlib-1.2.13/build/lib" \ - PKG_CONFIG="" \ - pip install --platform macosx_10_9_universal2 --no-deps --target $SITE_PACKAGES_DIR Pillow --global-option="build_ext" --global-option="--disable-platform-guessing" - - echo "-- Remove Pillow from kivy-app-requirements.txt file" - sed -i '' '/pillow/d' kivy-app-requirements.txt -fi - -pip install --platform macosx_10_9_universal2 --no-deps --target $SITE_PACKAGES_DIR -r kivy-app-requirements.txt - -echo "-- Relocate SDL2 frameworks" -pushd $APP_NAME.app +echo "-- Relocate SDL2 frameworks on Kivy installation folder" +pushd $SITE_PACKAGES_DIR/kivy python3 -m pip install git+https://github.com/tito/osxrelocator osxrelocator -r . @rpath/SDL2.framework/Versions/A/SDL2 @executable_path/../../../../Contents/Frameworks/SDL2.framework/Versions/A/SDL2 osxrelocator -r . @rpath/SDL2_ttf.framework/Versions/A/SDL2_ttf @executable_path/../../../../Contents/Frameworks/SDL2_ttf.framework/Versions/A/SDL2_ttf @@ -270,7 +232,12 @@ cp "${SCRIPT_PATH}/data/kivy_activate" "${APP_NAME}.app/Contents/Resources/venv/ echo "-- Copy Kivy dependencies into $APP_NAME.app/Contents/Frameworks directory" cp -R "$KIVY_DEPS_ROOT/dist/frameworks/." "$APP_NAME.app/Contents/Frameworks" + +echo "-- Change png.framework path in SDL2_ttf" +install_name_tool -change @rpath/png.framework/Versions/1.6.40/png @loader_path/../../../../Frameworks/png.framework/Versions/1.6.40/png "$APP_NAME.app/Contents/Frameworks/SDL2_ttf.framework/Versions/A/SDL2_ttf" + echo "-- Let's fix Frameworks signing." +codesign -fs - "${APP_NAME}.app/Contents/Frameworks/png.framework/Versions/1.6.40/png" codesign -fs - "${APP_NAME}.app/Contents/Frameworks/SDL2.framework/Versions/A/SDL2" codesign -fs - "${APP_NAME}.app/Contents/Frameworks/SDL2_ttf.framework/Versions/A/SDL2_ttf" codesign -fs - "${APP_NAME}.app/Contents/Frameworks/SDL2_image.framework/Versions/A/SDL2_image" diff --git a/osx/prepare-wheels.py b/osx/prepare-wheels.py new file mode 100644 index 0000000..3c9ae91 --- /dev/null +++ b/osx/prepare-wheels.py @@ -0,0 +1,331 @@ +import argparse +import logging +import os +import tempfile +import urllib.request + +from delocate.fuse import fuse_wheels +from distlib.locators import SimpleScrapingLocator +from distlib.wheel import Wheel +from distlib.util import parse_requirement +import packaging.tags +import packaging.version + +parser = argparse.ArgumentParser() +parser.add_argument("--requirements-file", help="Path to the requirements file") +parser.add_argument("--python-version", help="Python version", default="3.12") +parser.add_argument( + "--deployment-target", help="Deployment target", default="11.0" +) +parser.add_argument("--log-level", help="Log level", default="INFO") +parser.add_argument( + "--output-folder", + help="The folder where the wheels which have to be installed will be stored", +) +args = parser.parse_args() + +logging.basicConfig(level=args.log_level) + +deployment_target = packaging.version.parse(args.deployment_target) +deployment_target_tuple = (deployment_target.major, deployment_target.minor) + +python_version = packaging.version.parse(args.python_version) +python_version_tuple = (python_version.major, python_version.minor) + +mac_platforms = { + "universal2": list( + packaging.tags.mac_platforms( + version=deployment_target_tuple, arch="universal2" + ) + ), + "x86_64": list( + packaging.tags.mac_platforms( + version=deployment_target_tuple, arch="x86_64" + ) + ), + "arm64": list( + packaging.tags.mac_platforms( + version=deployment_target_tuple, arch="arm64" + ) + ), +} + +cpython_wheel_tags = { + "universal2": list( + packaging.tags.cpython_tags( + python_version=python_version_tuple, + platforms=mac_platforms["universal2"], + ) + ), + "x86_64": list( + packaging.tags.cpython_tags( + python_version=python_version_tuple, + platforms=mac_platforms["x86_64"], + ) + ), + "arm64": list( + packaging.tags.cpython_tags( + python_version=python_version_tuple, + platforms=mac_platforms["arm64"], + ) + ), +} + +compatible_wheel_tags = { + "universal2": list( + packaging.tags.compatible_tags( + python_version=python_version_tuple, + platforms=mac_platforms["universal2"], + ) + ), + "x86_64": list( + packaging.tags.compatible_tags( + python_version=python_version_tuple, + platforms=mac_platforms["x86_64"], + ) + ), + "arm64": list( + packaging.tags.compatible_tags( + python_version=python_version_tuple, + platforms=mac_platforms["arm64"], + ) + ), +} + + +class WheelLocator(SimpleScrapingLocator): + def __init__(self, *args, wheel_tags=None, **kwargs): + super().__init__(*args, **kwargs) + self.downloadable_extensions = (".whl",) + self.wheel_tags = wheel_tags + + +class SourceLocator(SimpleScrapingLocator): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.downloadable_extensions = self.source_extensions + + +class Universal2Fuser: + def __init__( + self, + arm64_wheel_path: str, + x86_64_wheel_path: str, + universal2_output_dir: str, + ): + self.arm64_wheel_path = arm64_wheel_path + self.x86_64_wheel_path = x86_64_wheel_path + self.universal2_output_dir = universal2_output_dir + + self.arm64_wheel = Wheel(self.arm64_wheel_path) + self.x86_64_wheel = Wheel(self.x86_64_wheel_path) + + def _discover_deployment_target(self): + # The new universal2 wheel need to have the same deployment target as the + # wheel with the highest deployment target + + # Not implemented yet. For now, just return deployment_target_tuple + return deployment_target_tuple + + def fuse(self): + universal2_wheel = Wheel(self.arm64_wheel.filename) + _first_platform = list( + packaging.tags.mac_platforms(version=(11, 0), arch="universal2") + )[0] + universal2_wheel.arch = [_first_platform] + fuse_wheels( + self.arm64_wheel_path, + self.x86_64_wheel_path, + os.path.join(self.universal2_output_dir, universal2_wheel.filename), + ) + + +class DistributionDownloader: + def __init__(self, distribution_url: str, output_folder: str): + self.distribution_url = distribution_url + self.output_folder = output_folder + + def download(self): + if self.distribution_url.endswith(".whl"): + wheel = Wheel(self.distribution_url) + filename = wheel.filename + else: + filename = self.distribution_url.split("/")[-1] + download_path = os.path.join(self.output_folder, filename) + urllib.request.urlretrieve(self.distribution_url, download_path) + return download_path + + +def to_locator_wheel_tag(tag): + return (tag.interpreter, tag.abi, tag.platform) + + +locators = { + "universal2": WheelLocator( + "https://pypi.org/simple/", + scheme="legacy", + wheel_tags=[ + to_locator_wheel_tag(tag) + for tag in cpython_wheel_tags["universal2"] + + compatible_wheel_tags["universal2"] + ], + ), + "x86_64": WheelLocator( + "https://pypi.org/simple/", + scheme="legacy", + wheel_tags=[ + to_locator_wheel_tag(tag) + for tag in cpython_wheel_tags["x86_64"] + + compatible_wheel_tags["x86_64"] + ], + ), + "arm64": WheelLocator( + "https://pypi.org/simple/", + scheme="legacy", + wheel_tags=[ + to_locator_wheel_tag(tag) + for tag in cpython_wheel_tags["arm64"] + + compatible_wheel_tags["arm64"] + ], + ), + "source": SourceLocator("https://pypi.org/simple/", scheme="legacy"), +} + +requirements = [] + +with open(args.requirements_file, encoding="utf-8") as f: + _requirements_lines = f.read().splitlines() + + for _line in _requirements_lines: + # Ensure is a valid requirement + if parse_requirement(_line) is None: + logging.warning( + "Invalid requirement line: %s.", _line + ) + continue + requirements.append( + dict( + description=_line, + distributions={ + "universal2": None, + "x86_64": None, + "arm64": None, + "source": None, + }, + ) + ) + + +for requirement in requirements: + requirement_description = requirement["description"] + _distribution = locators["universal2"].locate(requirement_description) + + if _distribution: + logging.info( + "Found universal2 or non-platform-specific wheel for %s", + requirement_description, + ) + requirement["distributions"]["universal2"] = _distribution + continue + + # If no universal2-compatible wheel was found, try to find a wheel for the 2 other + # architectures + logging.info( + "No universal or non-platform-specific wheel found for %s", + requirement_description, + ) + + for arch_name in ["x86_64", "arm64"]: + _distribution = locators[arch_name].locate(requirement_description) + if _distribution: + logging.info( + "Found %s-compatible wheel for %s", + arch_name, + requirement_description, + ) + requirement["distributions"][arch_name] = _distribution + continue + + logging.info( + "No %s-compatible wheel found for %s", + arch_name, + requirement_description, + ) + + # If wheels are not found for both x86_64 and arm64, try to find a source distribution + if not all( + requirement["distributions"][arch_name] + for arch_name in ["x86_64", "arm64"] + ): + logging.info( + "No wheel found for %s. Trying to find a source distribution.", + requirement_description, + ) + _distribution = locators["source"].locate(requirement_description) + if _distribution: + logging.info( + "Found source distribution for %s", requirement_description + ) + requirement["distributions"]["source"] = _distribution + continue + + logging.error( + "No source distribution found for %s", requirement_description + ) + +output_folder = args.output_folder or "wheels" +# Ensure the output folder exists +os.makedirs(output_folder, exist_ok=True) + +for requirement in requirements: + requirement_description = requirement["description"] + if requirement["distributions"]["universal2"]: + logging.info( + "Downloading universal2 or non-platform-specific wheel for %s", + requirement_description, + ) + # Download the universal2 or non-platform-specific wheel + DistributionDownloader( + requirement["distributions"]["universal2"].download_url, + output_folder, + ).download() + continue + + if all( + requirement["distributions"][arch_name] + for arch_name in ["x86_64", "arm64"] + ): + # Create a temporary folder to download the wheels for the 2 architectures + # and then fuse them + logging.info( + "Downloading x86_64 and arm64 wheels for %s in tmp folder for fusing", + requirement_description, + ) + with tempfile.TemporaryDirectory() as tmp_folder: + fuse_paths = {} + for arch_name in ["x86_64", "arm64"]: + fuse_paths[arch_name] = DistributionDownloader( + requirement["distributions"][arch_name].download_url, + tmp_folder, + ).download() + + logging.info( + "Fusing x86_64 and arm64 wheels for %s", requirement_description + ) + Universal2Fuser( + arm64_wheel_path=fuse_paths["arm64"], + x86_64_wheel_path=fuse_paths["x86_64"], + universal2_output_dir=output_folder, + ).fuse() + continue + + if requirement["distributions"]["source"]: + logging.info( + "Downloading source distribution for %s", requirement_description + ) + + DistributionDownloader( + requirement["distributions"]["source"].download_url, + output_folder, + ).download() + continue diff --git a/osx/requirements.txt b/osx/requirements.txt index 9dd76c1..5c680bf 100644 --- a/osx/requirements.txt +++ b/osx/requirements.txt @@ -1,2 +1,5 @@ docopt==0.6.2 -sh==1.14.2 \ No newline at end of file +sh==1.14.2 +distlib~=0.3.7 +delocate~=0.10.6 +packaging~=23.2 \ No newline at end of file