Last active
May 22, 2024 00:53
-
-
Save m00sey/9a904b3b28684ec65205df0e0312a13f to your computer and use it in GitHub Desktop.
Signs a Flet macOS app for distribution outside of the Mac App Store.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Signs a macOS app for distribution outside of the Mac App Store. | |
Requires environment variables: | |
DEVELOPER_CERTIFICATE | |
This can be set in the shell before running the script: | |
export DEVELOPER_CERTIFICATE="Developer ID Application: Your Name (ID)" | |
To get a list of available certificates: | |
security find-identity -v -p codesigning | |
Example usage: | |
python build_app.py --app_name "YourAppName" --app_display_name "YourAppDisplayName" --app_identifier "com.yourcompany.yourapp" --app_version "1.0.0" --app_icon "appicon.icns" | |
""" | |
import os | |
import subprocess | |
import argparse | |
import shutil | |
parser = argparse.ArgumentParser(description="Build and sign a macOS app.") | |
parser.add_argument("--build_dir", default="build", | |
help="Build directory (default: build).") | |
parser.add_argument("--app_name", required=True, help="Application name.") | |
parser.add_argument("--app_display_name", required=True, | |
help="Application display name.") | |
parser.add_argument("--app_identifier", required=True, | |
help="Application identifier.") | |
parser.add_argument("--app_version", required=True, | |
help="Application version.") | |
parser.add_argument("--app_icon", required=True, | |
help="Application icon file name.") | |
def run_command(command): | |
""" | |
Runs a command in the shell. | |
Args: | |
command (str): The command to be executed. | |
Raises: | |
subprocess.CalledProcessError: If the command execution fails. | |
Returns: | |
None | |
""" | |
subprocess.run(command, shell=True, check=True) | |
def main(args): | |
""" | |
Sign and package the application for distribution on macOS. | |
Args: | |
args (Namespace): Command-line arguments passed to the script. | |
Returns: | |
None | |
""" | |
build_dir = args.build_dir | |
app_name = args.app_name | |
app_display_name = args.app_display_name | |
app_identifier = args.app_identifier | |
app_version = args.app_version | |
app_icon = args.app_icon | |
dmg_name = f"{app_name}.dmg" | |
working_dir = os.getcwd() | |
cert = os.getenv("DEVELOPER_CERTIFICATE") | |
# Unpack, sign, and repack the zip file within the framework | |
# Should this path ever change, this script will need to be updated | |
app_framework_dir = f"{build_dir}/{args.app_name}.app/Contents/Frameworks/App.framework/Versions/A/Resources/flutter_assets/app/" | |
os.chdir(app_framework_dir) | |
shutil.unpack_archive("app.zip", "tmp") | |
for root, _, files in os.walk("tmp"): | |
for file in files: | |
if file.endswith(".so"): | |
run_command(f"codesign --force --verify --verbose --sign \"{cert}\" {os.path.join(root, file)}") | |
os.chdir("tmp") | |
shutil.make_archive("app", 'zip', "../tmp") | |
os.chdir("..") | |
shutil.rmtree("tmp") | |
os.chdir(working_dir) | |
# Sign all .so files | |
for root, _, files in os.walk(f"{build_dir}/{args.app_name}.app"): | |
for file in files: | |
if file.endswith(".so"): | |
run_command(f"codesign --force --verify --verbose --sign \"{cert}\" {os.path.join(root, file)}") | |
# Create entitlements.plist | |
entitlements = """ | |
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>com.apple.security.app-sandbox</key> | |
<true/> | |
<key>com.apple.security.cs.allow-jit</key> | |
<true/> | |
<key>com.apple.security.cs.allow-unsigned-executable-memory</key> | |
<true/> | |
<key>com.apple.security.cs.allow-dyld-environment-variables</key> | |
<true/> | |
<key>com.apple.security.network.client</key> | |
<true/> | |
<key>com.apple.security.network.server</key> | |
<true/> | |
<key>com.apple.security.device.audio-input</key> | |
<true/> | |
<key>com.apple.security.device.camera</key> | |
<true/> | |
<key>com.apple.security.device.microphone</key> | |
<true/> | |
</dict> | |
</plist> | |
""" | |
with open("entitlements.plist", "w", encoding="utf-8") as file: | |
file.write(entitlements) | |
# Sign the main executable | |
run_command(f"codesign --force --verify --verbose --sign \"{cert}\" --entitlements entitlements.plist {build_dir}/{app_name}.app/Contents/MacOS/{app_name}") | |
# Sign any frameworks | |
for root, _, files in os.walk(f"{build_dir}/{app_name}.app/Contents/Frameworks"): | |
for file in files: | |
if file.endswith(".framework"): | |
run_command(f"codesign --force --verify --verbose --sign \"{cert}\" {os.path.join(root, file)}") | |
# Sign the entire .app bundle | |
run_command(f"codesign --force --verify --verbose --sign \"{cert}\" --options runtime {build_dir}/{app_name}.app") | |
# Create Info.plist | |
info_plist = f""" | |
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>CFBundleName</key> | |
<string>{app_name}</string> | |
<key>CFBundleExecutable</key> | |
<string>{app_name}</string> | |
<key>CFBundleDisplayName</key> | |
<string>{app_display_name}</string> | |
<key>CFBundleIdentifier</key> | |
<string>{app_identifier}</string> | |
<key>CFBundleVersion</key> | |
<string>{app_version}</string> | |
<key>CFBundleShortVersionString</key> | |
<string>{app_version}</string> | |
<key>BuildMachineOSBuild</key> | |
<string>{subprocess.getoutput("sw_vers -buildVersion")}</string> | |
<key>CFBundleIconFile</key> | |
<string>{app_icon}</string> | |
<key>CFBundleIconName</key> | |
<string>{app_icon}</string> | |
<key>NSPrincipalClass</key> | |
<string>NSApplication</string> | |
<key>CFBundleDevelopmentRegion</key> | |
<string>en</string> | |
<key>CFBundleInfoDictionaryVersion</key> | |
<string>6.0</string> | |
<key>CFBundlePackageType</key> | |
<string>APPL</string> | |
<key>CFBundleSupportedPlatforms</key> | |
<array> | |
<string>MacOSX</string> | |
</array> | |
</dict> | |
</plist> | |
""" | |
with open(f"{build_dir}/{app_name}.app/Contents/Info.plist", "w", encoding="utf-8") as file: | |
file.write(info_plist) | |
# Create the DMG | |
run_command(f"hdiutil create -volname \"{app_name}\" -srcfolder \"{build_dir}/{app_name}.app\" -ov -format UDZO \"{build_dir}/{dmg_name}\"") | |
# Sign the DMG | |
run_command(f"codesign --force --verify --verbose --sign \"{cert}\" \"{build_dir}/{dmg_name}\"") | |
# Notarize the DMG | |
run_command(f"xcrun notarytool submit \"{build_dir}/{dmg_name}\" --keychain-profile \"citadel-notary\" --wait") | |
# Staple the notarization ticket to the DMG | |
run_command(f"xcrun stapler staple \"{build_dir}/{dmg_name}\"") | |
# Verify the notarization | |
run_command(f"xcrun stapler validate \"{build_dir}/{dmg_name}\"") | |
if __name__ == "__main__": | |
main(parser.parse_args()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment