diff --git a/contrib/osx/README.md b/contrib/osx/README.md index cfd8ac73ee6c..372f61e229a0 100644 --- a/contrib/osx/README.md +++ b/contrib/osx/README.md @@ -13,8 +13,16 @@ This guide explains how to build Electrum binaries for macOS systems. This needs to be done on a system running macOS or OS X. -The script is only tested on Intel-based (x86_64) Macs, and the binary built -targets `x86_64` currently. +The script is only tested on Intel-based (x86_64) Macs. +By default, the binary built targets `x86_64`. To build the binary for +Apple Silicon (`arm64`) Macs, set the `ELECTRUM_MACOS_ARCH` env var: + + ELECTRUM_MACOS_ARCH=arm64 ./contrib/osx/make_osx.sh + +Note that the arm64 binary is *cross-compiled*: the build host stays x86_64 +for both targets (see [#7557](https://github.com/spesmilo/electrum/issues/7557)). +The arm64 `.dmg` gets an `-arm64` suffix in its file name; the file name of the +x86_64 `.dmg` is unchanged. Notes about compatibility with different macOS versions: - In general the binary is not guaranteed to run on an older version of macOS diff --git a/contrib/osx/apply_sigs.sh b/contrib/osx/apply_sigs.sh index c79d70374816..a2200c10b415 100755 --- a/contrib/osx/apply_sigs.sh +++ b/contrib/osx/apply_sigs.sh @@ -17,7 +17,6 @@ CP=gcp UNSIGNED="$1" SIGNATURE="$2" -ARCH=x86_64 OUTDIR="/tmp/electrum_compare_dmg/signed_app" if [ -z "$UNSIGNED" ]; then @@ -38,10 +37,18 @@ find ${OUTDIR} -name "*.sign" | while read i; do SIZE=$(gstat -c %s "${i}") TARGET_FILE="$(echo "${i}" | sed 's/\.sign$//')" + ARCH=$(lipo -archs "${TARGET_FILE}") + case "${ARCH}" in + *" "*) + echo "Unexpected fat (multi-arch) binary: ${TARGET_FILE} (archs: ${ARCH})" + exit 1 + ;; + esac + if [ -z ${QUIET} ]; then - echo "Allocating space for the signature of size ${SIZE} in ${TARGET_FILE}" + echo "Allocating space for the signature of size ${SIZE} in ${TARGET_FILE} (arch: ${ARCH})" fi - codesign_allocate -i "${TARGET_FILE}" -a ${ARCH} ${SIZE} -o "${i}.tmp" + codesign_allocate -i "${TARGET_FILE}" -a "${ARCH}" ${SIZE} -o "${i}.tmp" OFFSET=$(pagestuff "${i}.tmp" -p | tail -2 | grep offset | sed 's/[^0-9]*//g') if [ -z ${QUIET} ]; then diff --git a/contrib/osx/make_osx.sh b/contrib/osx/make_osx.sh index 608806c7bfcd..b3ee07364d42 100755 --- a/contrib/osx/make_osx.sh +++ b/contrib/osx/make_osx.sh @@ -15,11 +15,29 @@ export PYTHONDONTWRITEBYTECODE=1 # don't create __pycache__/ folders with .pyc . "$(dirname "$0")/../build_tools_util.sh" +# Which CPU architecture to build for ("x86_64" or "arm64"). +# Official release binaries for *both* targets are built on an x86_64 host; +# for the arm64 target we cross-compile. (see #7557) +ELECTRUM_MACOS_ARCH="${ELECTRUM_MACOS_ARCH:-x86_64}" +export ELECTRUM_MACOS_ARCH # (also read by pyinstaller.spec) +case "$ELECTRUM_MACOS_ARCH" in + x86_64) + ARCH_DMG_SUFFIX="" + ;; + arm64) + ARCH_DMG_SUFFIX="-arm64" + ;; + *) + fail "unsupported ELECTRUM_MACOS_ARCH: '$ELECTRUM_MACOS_ARCH'. supported values: x86_64, arm64." + ;; +esac + + CONTRIB_OSX="$(dirname "$(realpath "$0")")" CONTRIB="$CONTRIB_OSX/.." PROJECT_ROOT="$CONTRIB/.." CACHEDIR="$CONTRIB_OSX/.cache" -export DLL_TARGET_DIR="$CACHEDIR/dlls" +export DLL_TARGET_DIR="$CACHEDIR/dlls-$ELECTRUM_MACOS_ARCH" PIP_CACHE_DIR="$CACHEDIR/pip_cache" mkdir -p "$CACHEDIR" "$DLL_TARGET_DIR" "$PIP_CACHE_DIR" @@ -64,9 +82,26 @@ source "$VENV_DIR/bin/activate" # see additional "strip" pass on built files later in the file. export CFLAGS="-g0" -# Do not build universal binaries. The default on macos 11+ and xcode 12+ is "-arch arm64 -arch x86_64" -# but with that e.g. "hid.cpython-310-darwin.so" is not reproducible as built by clang. -export ARCHFLAGS="-arch x86_64" +if [ "$ELECTRUM_MACOS_ARCH" = "arm64" ]; then + # When targeting arm64, C extensions and dylibs are compiled as universal2 ("fat") + # binaries, even though the app bundle will be arm64-only: + # - pyinstaller must be able to *import* the modules on the (x86_64) build host + # during its Analysis step, so the host's slice is needed in the venv, + # - the bundled app needs the arm64 slice. + # pyinstaller thins all collected binaries to $ELECTRUM_MACOS_ARCH when + # assembling the .app (see "target_arch" in pyinstaller.spec). + export ARCHFLAGS="-arch x86_64 -arch arm64" + # DYLIB_ARCHFLAGS is appended to CFLAGS for the libsecp256k1/zbar/libusb builds. + DYLIB_ARCHFLAGS="-arch x86_64 -arch arm64" + # libsecp's x86_64 asm cannot be compiled into the arm64 slice of a fat binary: + LIBSECP_AUTOCONF_FLAGS="--with-asm=no" +else + # Do not build universal binaries. The default on macos 11+ and xcode 12+ is "-arch arm64 -arch x86_64" + # but with that e.g. "hid.cpython-310-darwin.so" is not reproducible as built by clang. + export ARCHFLAGS="-arch x86_64" + DYLIB_ARCHFLAGS="" + LIBSECP_AUTOCONF_FLAGS="" +fi info "Installing build dependencies" # note: re pip installing from PyPI, @@ -92,7 +127,8 @@ PYINSTALLER_REPO="https://github.com/pyinstaller/pyinstaller.git" PYINSTALLER_COMMIT="306d4d92580fea7be7ff2c89ba112cdc6f73fac1" # ^ tag "v6.13.0" ( - if [ -f "$CACHEDIR/pyinstaller/PyInstaller/bootloader/Darwin-64bit/runw" ]; then + if [ -f "$CACHEDIR/pyinstaller/PyInstaller/bootloader/Darwin-64bit/runw" ] \ + && lipo "$CACHEDIR/pyinstaller/PyInstaller/bootloader/Darwin-64bit/runw" -verify_arch "$ELECTRUM_MACOS_ARCH" ; then info "pyinstaller already built, skipping" exit 0 fi @@ -117,6 +153,10 @@ PYINSTALLER_COMMIT="306d4d92580fea7be7ff2c89ba112cdc6f73fac1" popd # sanity check bootloader is there: [[ -e "PyInstaller/bootloader/Darwin-64bit/runw" ]] || fail "Could not find runw in target dir!" + # the bootloader is expected to be built as universal2 by default (see "--universal2" + # in pyinstaller's bootloader/wscript). sanity check it contains the target arch: + lipo "PyInstaller/bootloader/Darwin-64bit/runw" -verify_arch "$ELECTRUM_MACOS_ARCH" \ + || fail "bootloader was not built for $ELECTRUM_MACOS_ARCH. (xcode too old to build universal2 binaries?)" ) info "Installing PyInstaller." python3 -m pip install --no-build-isolation --no-dependencies \ @@ -151,13 +191,14 @@ if ls "$DLL_TARGET_DIR"/libsecp256k1.*.dylib 1> /dev/null 2>&1; then info "libsecp256k1 already built, skipping" else info "Building libsecp256k1 dylib..." - "$CONTRIB"/make_libsecp256k1.sh || fail "Could not build libsecp" + CFLAGS="$CFLAGS $DYLIB_ARCHFLAGS" AUTOCONF_FLAGS="$LIBSECP_AUTOCONF_FLAGS" \ + "$CONTRIB"/make_libsecp256k1.sh || fail "Could not build libsecp" fi cp -f "$DLL_TARGET_DIR"/libsecp256k1.*.dylib "$PROJECT_ROOT/electrum" || fail "Could not copy libsecp256k1 dylib" if [ ! -f "$DLL_TARGET_DIR/libzbar.0.dylib" ]; then info "Building ZBar dylib..." - "$CONTRIB"/make_zbar.sh || fail "Could not build ZBar dylib" + CFLAGS="$CFLAGS $DYLIB_ARCHFLAGS" "$CONTRIB"/make_zbar.sh || fail "Could not build ZBar dylib" else info "Skipping ZBar build: reusing already built dylib." fi @@ -165,7 +206,7 @@ cp -f "$DLL_TARGET_DIR/libzbar.0.dylib" "$PROJECT_ROOT/electrum/" || fail "Could if [ ! -f "$DLL_TARGET_DIR/libusb-1.0.dylib" ]; then info "Building libusb dylib..." - "$CONTRIB"/make_libusb.sh || fail "Could not build libusb dylib" + CFLAGS="$CFLAGS $DYLIB_ARCHFLAGS" "$CONTRIB"/make_libusb.sh || fail "Could not build libusb dylib" else info "Skipping libusb build: reusing already built dylib." fi @@ -196,6 +237,18 @@ python3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: -Ir ./contrib/deterministic-build/requirements-binaries-mac.txt \ || fail "Could not install dependencies specific to binaries" +if [ "$ELECTRUM_MACOS_ARCH" = "arm64" ]; then + # PyQt6-Qt6 does not ship universal2 wheels, only thin x86_64 and arm64 ones, + # and pip installed the wheel matching the *build host*. Replace the Qt libs + # with a universal2 merge of both thin wheels. (see make_universal_qt.py) + info "Merging PyQt6-Qt6 x86_64+arm64 wheels into universal2 Qt libs..." + python3 "$CONTRIB_OSX/make_universal_qt.py" \ + --requirements ./contrib/deterministic-build/requirements-binaries-mac.txt \ + --site-packages "$VENV_DIR/lib/python$PY_VER_MAJOR/site-packages" \ + --cache-dir "$CACHEDIR/qt_wheels" \ + || fail "Could not create universal2 Qt libs" +fi + info "Building $PACKAGE..." python3 -m pip install --no-build-isolation --no-dependencies \ --cache-dir "$PIP_CACHE_DIR" --no-warn-script-location . > /dev/null || fail "Could not build $PACKAGE" @@ -217,11 +270,25 @@ VERSION=$(git describe --tags --always) info "Building binary" ELECTRUM_VERSION=$VERSION pyinstaller --noconfirm --clean contrib/osx/pyinstaller.spec || fail "Could not build binary" +if [ "$ELECTRUM_MACOS_ARCH" = "arm64" ]; then + # We compiled the venv binaries as universal2 (see ARCHFLAGS above) and rely on + # pyinstaller to thin them. Sanity check every Mach-O file in the bundle is + # thin and matches the target arch: + info "Verifying architecture of Mach-O files in the app bundle..." + find "dist/${PACKAGE}.app" -type f | while IFS= read -r f; do + archs=$(lipo -archs "$f" 2>/dev/null) || continue # not a Mach-O file + if [ "$archs" != "$ELECTRUM_MACOS_ARCH" ]; then + echo "unexpected architectures ('$archs') for file: $f" + exit 1 + fi + done || fail "app bundle contains files with unexpected architectures" +fi + info "Finished building unsigned dist/${PACKAGE}.app. This hash should be reproducible:" find "dist/${PACKAGE}.app" -type f -print0 | sort -z | xargs -0 shasum -a 256 | shasum -a 256 info "Creating unsigned .DMG" -hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION-unsigned.dmg || fail "Could not create .DMG" +hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-${VERSION}${ARCH_DMG_SUFFIX}-unsigned.dmg || fail "Could not create .DMG" info "App was built successfully but was not code signed. Users may get security warnings from macOS." info "Now you also need to run sign_osx.sh to codesign/notarize the binary." diff --git a/contrib/osx/make_universal_qt.py b/contrib/osx/make_universal_qt.py new file mode 100644 index 000000000000..b2333edca12a --- /dev/null +++ b/contrib/osx/make_universal_qt.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +"""Create universal2 (x86_64+arm64) PyQt6-Qt6 libraries in site-packages. + +PyQt6-Qt6 does not ship universal2 wheels on PyPI, only thin x86_64 and arm64 ones. +When cross-building the arm64 app on an x86_64 host, the Qt libraries in the venv +need to contain + - the build host's slice (x86_64), as pyinstaller imports PyQt6.QtCore during + its Analysis step, and + - the target's slice (arm64), which ends up in the app bundle (pyinstaller + thins all collected binaries to the target arch). + +This script downloads both thin wheels (pip verifies them against the hashes pinned +in the requirements file), merges each Mach-O file using lipo, and replaces +/PyQt6/Qt6 with the merged tree. The merged tree is constructed +deterministically and does not depend on the build host architecture. +""" + +import argparse +import hashlib +import os +import platform +import re +import shutil +import subprocess +import sys +import zipfile + +PACKAGE_NAME = "PyQt6-Qt6" +# the minimum macOS version we request wheels for. arm64 wheels are macos 11+, +# so this is a hard floor; the actual request is bumped up to the build host's +# macOS version (see requested_platform_tag). +MIN_MACOS_MAJOR = 11 +MACHO_MAGICS = ( + b"\xfe\xed\xfa\xce", b"\xce\xfa\xed\xfe", # MH_MAGIC (32-bit) + b"\xfe\xed\xfa\xcf", b"\xcf\xfa\xed\xfe", # MH_MAGIC_64 + b"\xca\xfe\xba\xbe", b"\xca\xfe\xba\xbf", # FAT_MAGIC, FAT_MAGIC_64 +) + + +def requested_platform_tag(arch: str) -> str: + """Build a pip '--platform' tag permissive enough to match the pinned wheel. + + 'pip download --platform macosx_X_Y_' matches wheels whose min macOS + version is <= X.Y. Rather than hardcoding X.Y (which would silently stop + matching once PyQt6-Qt6 raises its wheels' min macOS above it), we request + the build host's macOS major version, floored at MIN_MACOS_MAJOR. The host is + necessarily new enough to provide whatever Qt we are bundling, so this keeps + matching across future Qt upgrades without edits. + """ + major = MIN_MACOS_MAJOR + host_ver = platform.mac_ver()[0] # e.g. "11.7.10"; "" when not on macOS + if host_ver: + major = max(major, int(host_ver.split(".")[0])) + return f"macosx_{major}_0_{arch}" + + +def extract_requirement_block(requirements_path: str) -> str: + """Return the requirement lines (incl. pinned hashes) for PACKAGE_NAME.""" + block_lines = [] + in_block = False + with open(requirements_path, encoding="utf-8") as f: + for line in f: + line = line.rstrip("\n") + if not in_block: + if re.match(rf"^{re.escape(PACKAGE_NAME)}==", line, re.IGNORECASE): + in_block = True + if in_block: + block_lines.append(line) + if not line.endswith("\\"): + break + if not block_lines: + raise Exception(f"could not find {PACKAGE_NAME} in {requirements_path}") + return "\n".join(block_lines) + "\n" + + +def is_macho(path: str) -> bool: + with open(path, "rb") as f: + return f.read(4) in MACHO_MAGICS + + +def sha256_of_file(path: str) -> str: + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1 << 20), b""): + h.update(chunk) + return h.hexdigest() + + +def is_expected_wheel_name(filename: str, arch: str) -> bool: + # note: the pinned hashes span ALL platform wheels (incl. linux/windows), so a + # hash match alone does not prove we have the right (thin, macos, $arch) wheel. + return bool(re.search(rf"-macosx_\d+_\d+_{re.escape(arch)}\.whl$", filename)) + + +def download_wheel(req_block: str, pinned_hashes: set, arch: str, cache_dir: str) -> str: + """Download (or reuse) the PACKAGE_NAME wheel for the given arch. Returns wheel path.""" + dest_dir = os.path.join(cache_dir, arch) + if os.path.isdir(dest_dir): + cached = [fn for fn in sorted(os.listdir(dest_dir)) if fn.endswith(".whl")] + for fn in cached: + path = os.path.join(dest_dir, fn) + if is_expected_wheel_name(fn, arch) and sha256_of_file(path) in pinned_hashes: + print(f"reusing cached wheel: {path}") + return path + # stale/corrupt cache: start over + shutil.rmtree(dest_dir) + os.makedirs(dest_dir, exist_ok=True) + req_file = os.path.join(dest_dir, "requirements.txt") + with open(req_file, "w", encoding="utf-8") as f: + f.write(req_block) + subprocess.run( + [ + sys.executable, "-m", "pip", "download", + "--require-hashes", "--no-deps", "--only-binary", ":all:", + "--platform", requested_platform_tag(arch), + "-r", req_file, "-d", dest_dir, + ], + check=True, + ) + wheels = [fn for fn in sorted(os.listdir(dest_dir)) if fn.endswith(".whl")] + if len(wheels) != 1: + raise Exception(f"expected exactly one downloaded wheel in {dest_dir}, found: {wheels}") + if not is_expected_wheel_name(wheels[0], arch): + raise Exception( + f"downloaded wheel does not look like a thin macos {arch} wheel: {wheels[0]}. " + f"If {PACKAGE_NAME} now ships 'universal2' wheels, this whole merge step is " + f"likely obsolete and should be removed (see #7557).") + path = os.path.join(dest_dir, wheels[0]) + # paranoia: pip already checked the hash, but verify again ourselves + if sha256_of_file(path) not in pinned_hashes: + raise Exception(f"sha256 mismatch for downloaded wheel: {path}") + return path + + +def unpack_wheel(wheel_path: str, dest_dir: str) -> None: + if os.path.isdir(dest_dir): + shutil.rmtree(dest_dir) + os.makedirs(dest_dir) + with zipfile.ZipFile(wheel_path) as zf: + for zinfo in zf.infolist(): + extracted = zf.extract(zinfo, dest_dir) + # zipfile does not preserve unix permissions; restore them: + unix_mode = (zinfo.external_attr >> 16) & 0o7777 + if unix_mode: + os.chmod(extracted, unix_mode) + + +def merge_trees(x86_tree: str, arm_tree: str, out_tree: str) -> None: + """Merge the "PyQt6/Qt6" dirs of both unpacked wheels into out_tree. + + The arm64 tree is used as the base (its file set defines the output), and for + each Mach-O file the x86_64 slice is merged in with lipo. + """ + x86_qt = os.path.join(x86_tree, "PyQt6", "Qt6") + arm_qt = os.path.join(arm_tree, "PyQt6", "Qt6") + for d in (x86_qt, arm_qt): + if not os.path.isdir(d): + raise Exception(f"missing PyQt6/Qt6 dir in unpacked wheel: {d}") + if os.path.isdir(out_tree): + shutil.rmtree(out_tree) + num_fat = 0 + for dirpath, dirnames, filenames in os.walk(arm_qt): + dirnames.sort() + rel_dir = os.path.relpath(dirpath, arm_qt) + os.makedirs(os.path.normpath(os.path.join(out_tree, rel_dir)), exist_ok=True) + for fn in sorted(filenames): + arm_file = os.path.join(dirpath, fn) + rel_file = os.path.normpath(os.path.join(rel_dir, fn)) + x86_file = os.path.join(x86_qt, rel_file) + out_file = os.path.join(out_tree, rel_file) + if is_macho(arm_file): + if not os.path.isfile(x86_file): + raise Exception(f"Mach-O file missing from x86_64 wheel: {rel_file}") + subprocess.run( + ["lipo", "-create", x86_file, arm_file, "-output", out_file], + check=True, + ) + subprocess.run( + ["lipo", out_file, "-verify_arch", "x86_64", "arm64"], + check=True, + ) + shutil.copymode(arm_file, out_file) + num_fat += 1 + else: + # ".a" static archives (plugins/permissions/) contain per-arch objects + # and are expected to differ between the wheels. They are link-time-only + # and do not get collected into the app bundle, so no warning for them. + if os.path.isfile(x86_file) and not fn.endswith(".a"): + with open(arm_file, "rb") as f1, open(x86_file, "rb") as f2: + if f1.read() != f2.read(): + print(f"warning: non-Mach-O file differs between wheels " + f"(using arm64 wheel's copy): {rel_file}") + shutil.copy2(arm_file, out_file) + # warn about x86_64-only files we are dropping: + for dirpath, dirnames, filenames in os.walk(x86_qt): + dirnames.sort() + for fn in sorted(filenames): + rel_file = os.path.normpath( + os.path.join(os.path.relpath(dirpath, x86_qt), fn)) + if not os.path.isfile(os.path.join(arm_qt, rel_file)): + print(f"warning: file only present in x86_64 wheel (dropped): {rel_file}") + if num_fat == 0: + raise Exception("merged zero Mach-O files. Did the PyQt6/Qt6 wheel layout change?") + print(f"merged {num_fat} Mach-O files into universal2 binaries.") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--requirements", required=True, + help="path to requirements file with pinned PyQt6-Qt6 hashes") + parser.add_argument("--site-packages", required=True, + help="path to the venv's site-packages dir") + parser.add_argument("--cache-dir", required=True, + help="dir to store downloaded wheels and temp files") + args = parser.parse_args() + + installed_qt = os.path.join(args.site_packages, "PyQt6", "Qt6") + if not os.path.isdir(installed_qt): + raise Exception(f"{PACKAGE_NAME} does not appear to be installed: {installed_qt}") + + req_block = extract_requirement_block(args.requirements) + pinned_hashes = set(re.findall(r"--hash=sha256:([0-9a-f]{64})", req_block)) + if not pinned_hashes: + raise Exception(f"no pinned hashes found for {PACKAGE_NAME}") + + # guard against a stale venv: the Qt libs we merge must be the same version as + # the installed PyQt6 bindings expect, or the app only crashes at runtime. + # (cannot happen in a full make_osx.sh run, but can on manual/partial reruns.) + m = re.match(rf"^{re.escape(PACKAGE_NAME)}==([^\s\\;]+)", req_block, re.IGNORECASE) + pinned_version = m.group(1) + dist_prefix = PACKAGE_NAME.replace("-", "_") + "-" + installed_versions = [ + fn[len(dist_prefix):-len(".dist-info")] + for fn in sorted(os.listdir(args.site_packages)) + if fn.startswith(dist_prefix) and fn.endswith(".dist-info") + ] + if installed_versions != [pinned_version]: + raise Exception( + f"installed {PACKAGE_NAME} version {installed_versions or 'not found'} does not " + f"match the pinned version {pinned_version}. Stale venv? (rerun the full build)") + + x86_whl = download_wheel(req_block, pinned_hashes, "x86_64", args.cache_dir) + arm_whl = download_wheel(req_block, pinned_hashes, "arm64", args.cache_dir) + + x86_tree = os.path.join(args.cache_dir, "unpacked_x86_64") + arm_tree = os.path.join(args.cache_dir, "unpacked_arm64") + unpack_wheel(x86_whl, x86_tree) + unpack_wheel(arm_whl, arm_tree) + + merged_tree = os.path.join(args.cache_dir, "merged_Qt6") + merge_trees(x86_tree, arm_tree, merged_tree) + + shutil.rmtree(installed_qt) + shutil.move(merged_tree, installed_qt) + shutil.rmtree(x86_tree) + shutil.rmtree(arm_tree) + print(f"replaced {installed_qt} with universal2 merge.") + + +if __name__ == "__main__": + main() diff --git a/contrib/osx/pyinstaller.spec b/contrib/osx/pyinstaller.spec index 682807d1eeef..2c993c759343 100644 --- a/contrib/osx/pyinstaller.spec +++ b/contrib/osx/pyinstaller.spec @@ -118,7 +118,9 @@ exe = EXE( upx=True, icon=ICONS_FILE, console=False, - target_arch='x86_64', # TODO investigate building 'universal2' + # note: ELECTRUM_MACOS_ARCH is set by make_osx.sh. All collected binaries + # (which might be universal2/"fat") get thinned to this single arch. + target_arch=os.environ.get('ELECTRUM_MACOS_ARCH') or 'x86_64', ) app = BUNDLE( diff --git a/contrib/osx/sign_osx.sh b/contrib/osx/sign_osx.sh index 62730f932445..931264a8f72b 100755 --- a/contrib/osx/sign_osx.sh +++ b/contrib/osx/sign_osx.sh @@ -73,7 +73,14 @@ if [ ! -z "$CODESIGN_CERT" ]; then fi fi +APP_ARCHS=$(lipo -archs "dist/${PACKAGE}.app/Contents/MacOS/run_electrum") +case "$APP_ARCHS" in + "x86_64") ARCH_DMG_SUFFIX="" ;; + "arm64") ARCH_DMG_SUFFIX="-arm64" ;; + *) fail "unexpected architectures in app binary: '$APP_ARCHS'" ;; +esac + info "Creating .DMG" -hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG" +hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-${VERSION}${ARCH_DMG_SUFFIX}.dmg || fail "Could not create .DMG" -DoCodeSignMaybe ".DMG" "dist/electrum-${VERSION}.dmg" +DoCodeSignMaybe ".DMG" "dist/electrum-${VERSION}${ARCH_DMG_SUFFIX}.dmg"