Skip to content

Instantly share code, notes, and snippets.

@m00sey
Last active May 22, 2024 00:53
Show Gist options
  • Save m00sey/9a904b3b28684ec65205df0e0312a13f to your computer and use it in GitHub Desktop.
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.
"""
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