diff --git a/RELEASE.md b/RELEASE.md index 209b611b..23e033f0 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -121,18 +121,18 @@ Set up the development environment described in `README.md`. Then build an executable, make it a macOS app bundle, and package it in a dmg: ```sh -poetry run python setup-freeze.py bdist_dmg +poetry run ./package/build-mac.py ``` -The will create `build/OnionShare.dmg`. +The will create `dist/OnionShare-$VERSION.dmg`. Now, notarize the release. You must have an app-specific Apple ID password saved in the login keychain called `onionshare-notarize`. -- Notarize it: `xcrun altool --notarize-app --primary-bundle-id "com.micahflee.onionshare" -u "micah@micahflee.com" -p "@keychain:onionshare-notarize" --file build/OnionShare.dmg` +- Notarize it: `xcrun altool --notarize-app --primary-bundle-id "com.micahflee.onionshare" -u "micah@micahflee.com" -p "@keychain:onionshare-notarize" --file dist/OnionShare-$VERSION.dmg` - Wait for it to get approved, check status with: `xcrun altool --notarization-history 0 -u "micah@micahflee.com" -p "@keychain:onionshare-notarize"` -- After it's approved, staple the ticket: `xcrun stapler staple build/OnionShare.dmg` +- After it's approved, staple the ticket: `xcrun stapler staple dist/OnionShare-$VERSION.dmg` -This will create `desktop/build/OnionShare.dmg`, signed and notarized. +This will create `desktop/dist/OnionShare-$VERSION.dmg`, signed and notarized. ## Source package diff --git a/desktop/package/build-mac.py b/desktop/package/build-mac.py new file mode 100755 index 00000000..7d08e9b3 --- /dev/null +++ b/desktop/package/build-mac.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python3 +import os +import inspect +import subprocess +import shutil +import itertools +import glob + +root = os.path.dirname( + os.path.dirname( + os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) + ) +) + + +def run(cmd, cwd=None, error_ok=False): + print(cmd) + subprocess.run(cmd, cwd=cwd, check=True) + + +def get_size(dir): + size = 0 + for path, dirs, files in os.walk(dir): + for f in files: + fp = os.path.join(path, f) + size += os.path.getsize(fp) + return size + + +def codesign(path, entitlements, identity): + run( + [ + "codesign", + "--sign", + identity, + "--entitlements", + str(entitlements), + "--timestamp", + "--deep", + str(path), + "--force", + "--options", + "runtime,library", + ] + ) + + +def main(): + desktop_dir = os.path.join(root, "desktop") + + print("○ Clean up from last build") + if os.path.exists(os.path.join(desktop_dir, "build")): + shutil.rmtree(os.path.join(desktop_dir, "build")) + if os.path.exists(os.path.join(desktop_dir, "dist")): + shutil.rmtree(os.path.join(desktop_dir, "dist")) + + print("○ Building binaries") + run( + [ + shutil.which("python"), + "setup-freeze.py", + "bdist_mac", + ], + desktop_dir, + ) + before_size = get_size( + os.path.join(desktop_dir, "build", "OnionShare.app") + ) + + print("○ Delete unused PySide2 stuff to save space") + for dirname in [ + "PySide2/Designer.app", + "PySide2/examples", + "PySide2/glue", + "PySide2/Qt/qml", + "shiboken2/files.dir", + ]: + shutil.rmtree( + os.path.join( + desktop_dir, + "build", + "OnionShare.app", + "Contents", + "MacOS", + "lib", + dirname, + ) + ) + shutil.rmtree( + os.path.join( + desktop_dir, + "build", + "OnionShare.app", + "Contents", + "MacOS", + "lib", + "shiboken2", + "docs", + ) + ) + for framework in [ + "Qt3DAnimation", + "Qt3DCore", + "Qt3DExtras", + "Qt3DInput", + "Qt3DLogic", + "Qt3DQuick", + "Qt3DQuickAnimation", + "Qt3DQuickExtras", + "Qt3DQuickInput", + "Qt3DQuickRender", + "Qt3DQuickScene2D", + "Qt3DRender", + "QtBluetooth", + "QtBodymovin", + "QtCharts", + "QtConcurrent", + "QtDataVisualization", + "QtDesigner", + "QtDesignerComponents", + "QtGamepad", + "QtHelp", + "QtLocation", + "QtMultimedia", + "QtMultimediaQuick", + "QtMultimediaWidgets", + "QtNfc", + "QtOpenGL", + "QtPdf", + "QtPdfWidgets", + "QtPositioning", + "QtPositioningQuick", + "QtPurchasing", + "QtQml", + "QtQuick", + "QtQuick3D", + "QtQuick3DAssetImport", + "QtQuick3DRender", + "QtQuick3DRuntimeRender", + "QtQuick3DUtils", + "QtQuickControls2", + "QtQuickParticles", + "QtQuickShapes", + "QtQuickTemplates2", + "QtQuickTest", + "QtQuickWidgets", + "QtRemoteObjects", + "QtRepParser", + "QtScript", + "QtScriptTools", + "QtScxml", + "QtSensors", + "QtSerialBus", + "QtSerialPort", + "QtSql", + "QtSvg", + "QtTest", + "QtTextToSpeech", + "QtUiPlugin", + "QtVirtualKeyboard", + "QtWebChannel", + "QtWebEngine", + "QtWebEngineCore", + "QtWebEngineWidgets", + "QtWebSockets", + "QtWebView", + "QtXml", + "QtXmlPatterns", + ]: + shutil.rmtree( + os.path.join( + desktop_dir, + "build", + "OnionShare.app", + "Contents", + "MacOS", + "lib", + "PySide2", + "Qt", + "lib", + f"{framework}.framework", + ) + ) + try: + os.remove( + os.path.join( + desktop_dir, + "build", + "OnionShare.app", + "Contents", + "MacOS", + "lib", + "PySide2", + f"{framework}.abi3.so", + ) + ) + os.remove( + os.path.join( + desktop_dir, + "build", + "OnionShare.app", + "Contents", + "MacOS", + "lib", + "PySide2", + f"{framework}.pyi", + ) + ) + except FileNotFoundError: + pass + + after_size = get_size( + os.path.join(desktop_dir, "build", "OnionShare.app") + ) + freed_bytes = before_size - after_size + freed_mb = int(freed_bytes / 1024 / 1024) + print(f"○ Freed {freed_mb} mb") + + print("○ Sign app bundle") + identity_name_application = "Developer ID Application: Micah Lee (N9B95FDWH4)" + entitlements_plist_path = os.path.join(desktop_dir, "package", "Entitlements.plist") + + for path in itertools.chain( + glob.glob( + f"{desktop_dir}/build/OnionShare.app/Contents/MacOS/**/*.dylib", + recursive=True, + ), + glob.glob( + f"{desktop_dir}/build/OnionShare.app/Contents/MacOS/**/*.so", recursive=True + ), + [ + f"{desktop_dir}/build/OnionShare.app/Contents/MacOS/lib/PySide2/pyside2-lupdate", + f"{desktop_dir}/build/OnionShare.app/Contents/MacOS/lib/PySide2/rcc", + f"{desktop_dir}/build/OnionShare.app/Contents/MacOS/lib/PySide2/uic", + ], + ): + codesign(path, entitlements_plist_path, identity_name_application) + codesign( + f"{desktop_dir}/build/OnionShare.app", + entitlements_plist_path, + identity_name_application, + ) + print(f"○ Signed app bundle: {desktop_dir}/build/OnionShare.app") + + if not os.path.exists("/usr/local/bin/create-dmg"): + print("○ Error: create-dmg is not installed") + return + + print("○ Create DMG") + version_filename = os.path.join( + root, "cli", "onionshare_cli", "resources", "version.txt" + ) + with open(version_filename) as f: + version = f.read().strip() + + os.makedirs(os.path.join(desktop_dir, "dist"), exist_ok=True) + dmg_path = os.path.join(desktop_dir, "dist", f"OnionShare-{version}.dmg") + run( + [ + "create-dmg", + "--volname", + "OnionShare", + "--volicon", + os.path.join( + desktop_dir, "onionshare", "resources", "onionshare.icns" + ), + "--window-size", + "400", + "200", + "--icon-size", + "100", + "--icon", + "OnionShare.app", + "100", + "70", + "--hide-extension", + "OnionShare.app", + "--app-drop-link", + "300", + "70", + dmg_path, + f"{desktop_dir}/build/OnionShare.app", + "--identity", + identity_name_application, + ] + ) + + print(f"○ Finished building DMG: {dmg_path}") + +if __name__ == "__main__": + main() diff --git a/desktop/setup-freeze.py b/desktop/setup-freeze.py index 14b05a7e..6026a9cd 100644 --- a/desktop/setup-freeze.py +++ b/desktop/setup-freeze.py @@ -214,10 +214,7 @@ setup( # bdist_mac, making the macOS app bundle "bdist_mac": { "iconfile": os.path.join("onionshare", "resources", "onionshare.icns"), - "bundle_name": "OnionShare", - "codesign_identity": "Developer ID Application: Micah Lee (N9B95FDWH4)", - "codesign_entitlements": os.path.join("package", "Entitlements.plist"), - "codesign_deep": True, + "bundle_name": "OnionShare" }, }, executables=[