From e195b3a1e2844567177406814ece422d30f5f8e2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 18 Dec 2021 13:51:44 +0530 Subject: [PATCH] Build universal binaries on macOS --- bypy/macos/__main__.py | 19 +++++++++++++++---- setup/build.py | 12 ++++++++++-- setup/build_environment.py | 3 +++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/bypy/macos/__main__.py b/bypy/macos/__main__.py index 1b99263bfd..ee53fe51c3 100644 --- a/bypy/macos/__main__.py +++ b/bypy/macos/__main__.py @@ -25,7 +25,9 @@ from bypy.constants import ( from bypy.freeze import ( extract_extension_modules, fix_pycryptodome, freeze_python, path_to_freeze_dir ) -from bypy.utils import current_dir, mkdtemp, py_compile, timeit, walk +from bypy.utils import ( + current_dir, get_arches_in_binary, mkdtemp, py_compile, timeit, walk +) abspath, join, basename, dirname = os.path.abspath, os.path.join, os.path.basename, os.path.dirname iv = globals()['init_env'] @@ -35,7 +37,7 @@ py_ver = '.'.join(map(str, python_major_minor_version())) sign_app = runpy.run_path(join(dirname(abspath(__file__)), 'sign.py'))['sign_app'] QT_PREFIX = join(PREFIX, 'qt') -QT_FRAMEWORKS = [x.replace('5', '') for x in QT_DLLS] +QT_FRAMEWORKS = [x.replace('6', '') for x in QT_DLLS] ENV = dict( FONTCONFIG_PATH='@executable_path/../Resources/fonts', @@ -56,7 +58,7 @@ def compile_launcher_lib(contents_dir, gcc, base, pyver, inc_dir): dest = join(contents_dir, 'Frameworks', 'calibre-launcher.dylib') src = join(base, 'util.c') - cmd = [gcc] + '-Wall -dynamiclib -std=gnu99'.split() + [src] + \ + cmd = [gcc] + '-arch x86_64 -arch arm64 -Wall -dynamiclib -std=gnu99'.split() + [src] + \ ['-I' + base] + '-DPY_VERSION_MAJOR={} -DPY_VERSION_MINOR={}'.format(*pyver.split('.')).split() + \ [f'-I{path_to_freeze_dir()}', f'-I{inc_dir}'] + \ [f'-DENV_VARS={env}', f'-DENV_VAR_VALS={env_vals}'] + \ @@ -87,7 +89,8 @@ def compile_launchers(contents_dir, inc_dir, xprograms, pyver): programs.append(out) is_gui = 'true' if ptype == 'gui' else 'false' cmd = [ - gcc, '-Wall', f'-DPROGRAM=L"{program}"', f'-DMODULE=L"{module}"', f'-DFUNCTION=L"{func}"', f'-DIS_GUI={is_gui}', + gcc, '-Wall', '-arch', 'x86_64', '-arch', 'arm64', + f'-DPROGRAM=L"{program}"', f'-DMODULE=L"{module}"', f'-DFUNCTION=L"{func}"', f'-DIS_GUI={is_gui}', '-I' + base, src, lib, '-o', out, '-headerpad_max_install_names' ] # print('\t'+' '.join(cmd)) @@ -108,7 +111,14 @@ def flipwritable(fn, mode=None): return old_mode +def check_universal(path): + arches = get_arches_in_binary(path) + if arches != EXPECTED_ARCHES: + raise SystemExit(f'The file {path} is not a universal binary, it only has arches: {", ".join(arches)}') + + STRIPCMD = ['/usr/bin/strip', '-x', '-S', '-'] +EXPECTED_ARCHES = {'x86_64', 'arm64'} def strip_files(files, argv_max=(256 * 1024)): @@ -272,6 +282,7 @@ class Freeze: @flush def fix_dependencies_in_lib(self, path_to_lib): + check_universal(path_to_lib) self.to_strip.append(path_to_lib) old_mode = flipwritable(path_to_lib) for dep, bname, is_id in self.get_local_dependencies(path_to_lib): diff --git a/setup/build.py b/setup/build.py index 7fdf07b6ef..36d7bae463 100644 --- a/setup/build.py +++ b/setup/build.py @@ -178,6 +178,9 @@ def get_python_include_paths(): return sorted(frozenset(filter(None, map(gp, sorted(ans))))) +is_macos_universal_build = ismacos and 'universal2' in sysconfig.get_platform() + + def init_env(debug=False, sanitize=False): from setup.build_environment import win_ld, is64bit, win_inc, win_lib, NMAKE, win_cc linker = None @@ -222,6 +225,9 @@ def init_env(debug=False, sanitize=False): ldflags += (sysconfig.get_config_var('LINKFORSHARED') or '').split() if ismacos: + if is_macos_universal_build: + cflags.extend(['-arch', 'x86_64', '-arch', 'arm64']) + ldflags.extend(['-arch', 'x86_64', '-arch', 'arm64']) cflags.append('-D_OSX') ldflags.extend('-bundle -undefined dynamic_lookup'.split()) cflags.extend(['-fno-common', '-dynamic']) @@ -443,7 +449,7 @@ class Build(Command): def check_call(self, *args, **kwargs): """print cmdline if an error occurred - If something is missing (qmake e.g.) you get a non-informative error + If something is missing (cmake e.g.) you get a non-informative error self.check_call(qmc + [ext.name+'.pro']) so you would have to look at the source to see the actual command. """ @@ -481,6 +487,8 @@ class Build(Command): if os.path.exists(bdir): shutil.rmtree(bdir) cmd = [CMAKE] + if is_macos_universal_build: + cmd += ['-DCMAKE_OSX_ARCHITECTURES=x86_64;arm64'] if sw and os.path.exists(os.path.join(sw, 'qt')): cmd += ['-DCMAKE_SYSTEM_PREFIX_PATH=' + os.path.join(sw, 'qt').replace(os.sep, '/')] os.makedirs(bdir) @@ -491,7 +499,7 @@ class Build(Command): self.check_call([self.env.make] + ['-j%d'%(cpu_count or 1)]) finally: os.chdir(cwd) - os.rename(self.j(bdir, 'libheadless.' + ('dylib' if ismacos else 'so')), target) + os.rename(self.j(bdir, 'libheadless.so'), target) def create_sip_build_skeleton(self, src_dir, ext): from setup.build_environment import pyqt_sip_abi_version diff --git a/setup/build_environment.py b/setup/build_environment.py index ab27bfb769..39e88bd56e 100644 --- a/setup/build_environment.py +++ b/setup/build_environment.py @@ -159,6 +159,7 @@ elif ismacos: sw = os.environ.get('SW', os.path.expanduser('~/sw')) sw_inc_dir = os.path.join(sw, 'include') sw_lib_dir = os.path.join(sw, 'lib') + sw_bin_dir = os.path.join(sw, 'bin') podofo_inc = os.path.join(sw_inc_dir, 'podofo') hunspell_inc_dirs = [os.path.join(sw_inc_dir, 'hunspell')] podofo_lib = sw_lib_dir @@ -167,6 +168,8 @@ elif ismacos: SSL = os.environ.get('OPENSSL_DIR', os.path.join(sw, 'private', 'ssl')) openssl_inc_dirs = [os.path.join(SSL, 'include')] openssl_lib_dirs = [os.path.join(SSL, 'lib')] + if os.path.exists(os.path.join(sw_bin_dir, 'cmake')): + CMAKE = os.path.join(sw_bin_dir, 'cmake') else: ft_inc_dirs = pkgconfig_include_dirs('freetype2', 'FT_INC_DIR', '/usr/include/freetype2')