Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Universal2 build #108

Merged
merged 20 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,31 @@ jobs:
- name: Git checkout
uses: actions/checkout@v4

- name: Set up Python 3.10 for macOS
- name: Set up Python 3.12 for macOS
# We use Python from python.org instead of from actions/setup-python, as the app
# built with the latter does not work on macOS 10.15
run: |
curl https://www.python.org/ftp/python/3.10.9/python-3.10.9-macos11.pkg --output python-installer.pkg
curl https://www.python.org/ftp/python/3.12.7/python-3.12.7-macos11.pkg --output python-installer.pkg
sudo installer -pkg python-installer.pkg -target /
python3.10-intel64 --version
python3.10-intel64 -c "import platform; print('macOS version:', platform.mac_ver()[0])"

- name: Setup Virtual Environment
run: |
python3.10-intel64 -m venv venv
# Somehow using plain "python3" gives us the runner's homebrew Python,
# so let's be explicit about the path:
ourpython=/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12
ls -l $ourpython
$ourpython --version
$ourpython -c "import platform; print('platform:', platform.platform())"
$ourpython -c "import platform; print('macOS version:', platform.mac_ver()[0])"
$ourpython -m venv venv
source venv/bin/activate
python -c "import sys; print('\n'.join(sys.path))"

- name: Install dependencies
run: |
source venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install -r requirements-dev.txt
pip install -r requirements.txt | tee pip_log.txt
python macos/ensure_universal_wheels.py pip_log.txt
pip install --force build/universal_wheels/*.whl
pip install -r requirements-dev.txt

- name: Run pre-commit
run: |
Expand Down
2 changes: 1 addition & 1 deletion FontraPak.spec
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ if sys.platform == "darwin":
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
target_arch="universal2",
codesign_identity=None,
entitlements_file=None,
icon="icon/FontraIcon.ico",
Expand Down
2 changes: 1 addition & 1 deletion macos/build_dmg.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"-fs",
"HFS+",
"-size",
"200m",
"300m",
"-srcfolder",
imgPath,
"-volname",
Expand Down
157 changes: 157 additions & 0 deletions macos/ensure_universal_wheels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import argparse
import json
import pathlib
import re
import sys
from tempfile import TemporaryDirectory
from urllib.request import urlopen

from delocate.fuse import fuse_wheels
from packaging.utils import parse_wheel_filename

#
# The problem we are trying to solve is:
# - To build a universal2 app with py2app or PyInstaller, we need _all_ compiled packages
# to be universal2
# - This is hard for two reasons:
# - Not all packages offer universal2 wheels
# - When running on x86, pip will serve x86, even if universal2 is available
#
# We take the following approach:
# - Run `pip install -r requirements.txt` and capture the output
# - Find and parse all wheel filenames
# - Any wheel that is x86 or arm64 needs attention (eg. we ignore "any" and "universal2"):
# - Check the pypi json for the package + version
# - If there is a universal2 wheel, download it, write to `build/universal_wheels/*.whl`
# - Else if there are x86 and arm64 wheels, download both and merge, write to
# `build/universal_wheels/*.whl`
# - Else: error
# - `pip install --force build/universal_wheels/*.whl`
#

python_versions = [
f"cp{sys.version_info.major}{minor}"
for minor in range(8, sys.version_info.minor + 1)
]
python_versions.reverse()


class IncompatibleWheelError(Exception):
pass


def url_filename(url):
return url.rsplit("/", 1)[-1]


def download_file(url, dest_dir):
filename = url_filename(url)
print("downloading wheel", filename)
response = urlopen(url)
with open(dest_dir / filename, "wb") as f:
f.write(response.read())


def merge_wheels(url1, url2, dest_dir):
wheel_name1 = url_filename(url1)
wheel_name2 = url_filename(url2)
print("merging wheels", wheel_name1, "and", wheel_name2)

with TemporaryDirectory() as tmpdir:
tmpdir = pathlib.Path(tmpdir)

download_file(url1, tmpdir)
download_file(url2, tmpdir)

wheel_names = [wheel_name1, wheel_name2]
wheel_names.sort()

assert any("x86" in name for name in wheel_names)
assert any("arm64" in name for name in wheel_names)

package, version, build, tags = parse_wheel_filename(wheel_names[0])

wheel_base, platform = wheel_names[0].rsplit("-", 1)
platform_base_parts = platform.split("_")
platform_base = "_".join(platform_base_parts[:3])

universal_wheel_path = (
dest_dir / f"{wheel_base.lower()}-{platform_base}_universal2.whl"
)
print("writing universal wheel", universal_wheel_path.name)
fuse_wheels(tmpdir / wheel_name1, tmpdir / wheel_name2, universal_wheel_path)


wheel_filename_pattern = re.compile(r"[^\s=]+.whl")


def main():
parser = argparse.ArgumentParser()
parser.add_argument("pip_log")
parser.add_argument("--wheels-dir", default="build/universal_wheels")

args = parser.parse_args()

wheels_dir = pathlib.Path(args.wheels_dir).resolve()
wheels_dir.mkdir(exist_ok=True, parents=True)

pip_log_path = args.pip_log
with open(pip_log_path) as f:
pip_log = f.read()

non_portable_wheels = {}

for wheel_filename in wheel_filename_pattern.findall(pip_log):
package, version, build, tags = parse_wheel_filename(wheel_filename)
package = package.lower()
if any("x86" in tag.platform or "arm64" in tag.platform for tag in tags):
assert non_portable_wheels.get(package, wheel_filename) == wheel_filename
non_portable_wheels[package] = wheel_filename

for wheel_filename in non_portable_wheels.values():
package, version, build, tags = parse_wheel_filename(wheel_filename)
response = urlopen(f"https://pypi.org/pypi/{package}/{version}/json")
data = json.load(response)

universal_wheels = []
platform_wheels = []

for python_version in python_versions:
file_descriptors = [
file_descriptor
for file_descriptor in data["urls"]
if file_descriptor["python_version"] == python_version
]
if file_descriptors:
break

if not file_descriptors:
file_descriptors = [
file_descriptor
for file_descriptor in data["urls"]
if file_descriptor["python_version"] == "py3"
]

for file_descriptor in file_descriptors:
wheel_filename = file_descriptor["filename"]
package, version, build, tags = parse_wheel_filename(wheel_filename)
if any("macosx" in tag.platform for tag in tags):
if any("universal2" in tag.platform for tag in tags):
universal_wheels.append(file_descriptor["url"])
else:
platform_wheels.append(file_descriptor["url"])

if universal_wheels:
assert len(universal_wheels) == 1
download_file(universal_wheels[0], wheels_dir)
elif platform_wheels:
assert len(platform_wheels) == 2
merge_wheels(platform_wheels[0], platform_wheels[1], wheels_dir)
else:
raise IncompatibleWheelError(
f"No universal2 solution found for non-portable wheel {wheel_filename}"
)


if __name__ == "__main__":
main()
11 changes: 6 additions & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ git+https://github.com/googlefonts/fontra-rcjk.git
git+https://github.com/googlefonts/fontra-glyphs.git
aiohttp==3.10.8
pyinstaller==6.10.0
# PyQt6 6.5.0 does not support macOS 10.15 anymore, so for now
# we'll stick to these:
PyQt6==6.4.2
PyQt6-Qt6==6.4.3
PyQt6-sip==13.5.0 # 13.6.0 works, but issues a DeprecationWarning that makes our test fail
PyQt6==6.7.1
PyQt6-Qt6==6.7.3
PyQt6-sip==13.8.0
# Use patched fork to work around https://github.com/matthew-brett/delocate/issues/228
git+https://github.com/justvanrossum/delocate.git
# delocate==0.12.0