Skip to content

macOS Signing Workflow

Thomas Höllt edited this page Sep 6, 2024 · 14 revisions

This is the currently tested workflow to create a fully notarized ManiVault Studio app

Before Building: xCode entitlements

In xCode add the Hardened runtime and Sandbox entitlements (file is also below) https://developer.apple.com/documentation/xcode/adding-capabilities-to-your-app#Perform-additional-configuration-steps

Building

We assume building individual repositories (i.e., not the dev bundle) and working and configured CMake build

run in the build folder generated by CMAKE (the folder containing the with xcode project file, e.g., mv-main.xcodeproj for the ManiVault Core)

xcodebuild -scheme ALL_BUILD -configuration Release build https://developer.apple.com/library/archive/technotes/tn2339/_index.html

This will install into the regular folder structure (license, Plugins, and app bundle will be copied in the packaging into a dmg inside a folder with the app name, the other ones can optionally be added in this script. note all executables need to be signed, so if e.g., the libraries folder is added the dylib in there needs to be signed, too):

      Release
         |
   ------------------------ ...
   |         |            |
license   Plugins   ManiVault Studio.app

After building, if not done automatically, collect all plugins in the above folder structure under Plugins

Signing Prerequisites

For adding signing certificates etc. to the Github runner see Github documentation: https://docs.github.com/en/actions/deployment/deploying-xcode-applications/installing-an-apple-certificate-on-macos-runners-for-xcode-development

If you do not already have those (e.g., in your local keychain on your Mac, create the certificates first:

  1. create app id (currently we use com.biovault.mv, I wonder if we want to change this to studio.manivault) I think a wild card will work but for testing i created the explicit id in my account https://developer.apple.com/help/account/manage-identifiers/register-an-app-id
  2. create developer id certificate https://developer.apple.com/help/account/create-certificates/create-developer-id-certificates
  3. along this process you will need to create a certificate signing request (it is also linked in the description) https://developer.apple.com/help/account/create-certificates/create-a-certificate-signing-request
  4. there are some things not fully correct in the build. See Open Issues below and manually fix them before signing.
Signing

all following steps are run in the MV_INSTALL/Release folder as indicated above

FIRSTNAME LASTNAME: Name in the Developer account (I assume this is usually Firstname Lastname, but doublecheck)
TEAM_ID: 10 digit alphanumeric team id, see your apple developer welcome mail
APPLE_ID: apple id used for the developer account (possibly any that is added to that account under appstore connect is also possible, many of the certificate steps above list a required role in the description, that role can be assigned to team members)
PASSWORD: the password for that apple id (use an app password if two factor is enabled on the apple id)
SUBMISSION_ID: created by submitting the app for notarization\

  1. Deep signing the app
    codesign --verbose --force --timestamp --verify --deep --sign "Developer ID Application: FIRSTNAME LASTNAME (TEAM_ID)" --entitlements ManiVault\ Studio.app/Contents/Resources/ManiVault\ Studio.entitlements --options runtime ManiVault\ Studio.app

  2. Webengine (see below for entitlements file), this is one line! the entitlements file is expected to be in the same folder in this command
    codesign --verbose --force --timestamp --verify --sign "Developer ID Application: FIRSTNAME LASTNAME (TEAM_ID)" --entitlements webengine.entitlements --options runtime ManiVault\ Studio.app/Contents/Frameworks/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess

  3. Resign the app executable
    codesign --verbose --force --timestamp --verify --sign "Developer ID Application: FIRSTNAME LASTNAME (TEAM_ID)" --entitlements ManiVault\ Studio.app/Contents/Resources/ManiVault\ Studio.entitlements --options runtime ManiVault\ Studio.app/Contents/MacOS/ManiVault\ Studio

  4. Sign all plugins in one batch
    codesign --verbose --force --deep --timestamp -o runtime --sign "Developer ID Application: FIRSTNAME LASTNAME (TEAM_ID)" Plugins/*

Packaging

Package as dmg with package.command script. Run from within the Release folder (next to the app bundle)

Background.png needs to be in the same folder while running currently, script should be adjusted to the CI/CD environment (changing the ending to .sh should also create a regular bash script, it is currently .command for convenience when run manually)

Notarization
  1. submit
    xcrun notarytool submit --apple-id APPLE_ID --password PASSWORD --team-id TEAM_ID ManiVault\ Studio.1.0.0.dmg This will print thje SUBMISSION_ID to the command line

  2. check status
    xcrun notarytool info --apple-id APPLE_ID --password PASSWORD --team-id TEAM_ID SUBMISSION_ID

  3. Once status shows "Accepted": Staple
    xcrun stapler staple ManiVault\ Studio.1.0.0.dmg

OPEN ISSUES:
  1. The embedded libquazip is missing a .0 at the end of the version
    mv ManiVault\ Studio.app/Contents/Frameworks/libquazip1-qt6.1.4.dylib ManiVault\ Studio.app/Contents/Frameworks/libquazip1-qt6.1.4.0.dylib

  2. Not all Qt frameworks are embedded, needs decision what to do, support part of Qt only or embedd all or external solution (if that is even possible. General solution for plugins with dynamically linked libraries might be needed,too). Temporary solution for a fixed set of plugins is to rerun macdeploy with the plugins copied into the app bundle:
    cp -R Plugins ManiVault\ Studio.app/Contents/Frameworks/MVPlugins
    macdeployqt ManiVault\ Studio.app
    rm -rf ManiVault\ Studio.app/Contents/Frameworks/MVPlugins

  3. Some plugins need other plugins but cannot find them. Before Signing add the plugin folder to their rpath
    cd Plugins
    ls | xargs -n1 install_name_tool -add_rpath @loader_path

Files embedded for convenience

MV_Application.entitlements (should be added to the app bundle as under Contents/Resources/ManiVault Studio.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.cs.allow-dyld-environment-variables</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.disable-executable-page-protection</key>
	<true/>
	<key>com.apple.security.cs.disable-library-validation</key>
	<true/>
</dict>
</plist>
webengine.entitlements

this needs to be available next to the app bundle during step 2 of signing

(we can probably just reuse the above entitlements file, as it includes the page protection, but this should be cleaner)

<?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.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.disable-executable-page-protection</key>
    <true/>
</dict>
</plist>
package.command
#!/bin/bash
# by Andy Maloney
# http://asmaloney.com/2013/07/howto/packaging-a-mac-os-x-application-using-a-dmg/

# make sure we are in the correct dir when we double-click a .command file
dir=${0%/*}
if [ -d "$dir" ]; then
  cd "$dir"
fi

# set up your app name, version number, and background image file name
APP_NAME="ManiVault Studio"
VERSION="1.0.0"
DMG_BACKGROUND_IMG="Background.png"

# you should not need to change these
APP_EXE="${APP_NAME}.app/Contents/MacOS/${APP_NAME}"

VOL_NAME="${APP_NAME}.${VERSION}"   # volume name will be "ManiVault Studio 1.0.0"
DMG_TMP="${VOL_NAME}-temp.dmg"
DMG_FINAL="${VOL_NAME}.dmg"         # final DMG name will be "ManiVault Studio.x.x.x.dmg"
STAGING_DIR="./Install"             # we temporarily copy all our stuff into this dir

# Check the background image DPI and convert it if it isn't 72x72
_BACKGROUND_IMAGE_DPI_H=`sips -g dpiHeight ${DMG_BACKGROUND_IMG} | grep -Eo '[0-9]+\.[0-9]+'`
_BACKGROUND_IMAGE_DPI_W=`sips -g dpiWidth ${DMG_BACKGROUND_IMG} | grep -Eo '[0-9]+\.[0-9]+'`

if [ $(echo " $_BACKGROUND_IMAGE_DPI_H != 72.0 " | bc) -eq 1 -o $(echo " $_BACKGROUND_IMAGE_DPI_W != 72.0 " | bc) -eq 1 ]; then
   echo "WARNING: The background image's DPI is not 72.  This will result in distorted backgrounds on Mac OS X 10.7+."
   echo "         I will convert it to 72 DPI for you."
   
   _DMG_BACKGROUND_TMP="${DMG_BACKGROUND_IMG%.*}"_dpifix."${DMG_BACKGROUND_IMG##*.}"

   sips -s dpiWidth 72 -s dpiHeight 72 ${DMG_BACKGROUND_IMG} --out ${_DMG_BACKGROUND_TMP}
   
   DMG_BACKGROUND_IMG="${_DMG_BACKGROUND_TMP}"
fi

# clear out any old data
rm -rf "${STAGING_DIR}" "${DMG_TMP}" "${DMG_FINAL}"

# copy over the stuff we want in the final disk image to our staging dir
mkdir -p "${STAGING_DIR}"
mkdir -p "${STAGING_DIR}/${APP_NAME}"
cp -Rpf "${APP_NAME}.app" "${STAGING_DIR}/${APP_NAME}"
cp -Rpf "Plugins" "${STAGING_DIR}/${APP_NAME}"
cp -Rpf "license" "${STAGING_DIR}/${APP_NAME}"
# ... cp anything else you want in the DMG - documentation, etc.

# figure out how big our DMG needs to be
#  assumes our contents are at least 1M!
SIZE=`du -sh "${STAGING_DIR}" | sed 's/\([0-9\.]*\)M\(.*\)/\1/'` 
SIZE=`echo "${SIZE} + 5.0" | bc | awk '{print int($1+0.5)}'`

if [ $? -ne 0 ]; then
   echo "Error: Cannot compute size of staging dir"
   exit
fi

# create the temp DMG file
hdiutil create -srcfolder "${STAGING_DIR}" -volname "${VOL_NAME}" -fs HFS+ \
      -fsargs "-c c=64,a=16,e=16" -format UDRW -size ${SIZE}M "${DMG_TMP}"

echo "Created DMG: ${DMG_TMP}"

# mount it and save the device
DEVICE=$(hdiutil attach -readwrite -noverify "${DMG_TMP}" | \
         egrep '^/dev/' | sed 1q | awk '{print $1}')

sleep 2

# add a link to the Applications dir
echo "Add link to /Applications\n"
pushd /Volumes/"${VOL_NAME}"
ln -s /Applications
popd

# add a background image
mkdir /Volumes/"${VOL_NAME}"/.background
cp "${DMG_BACKGROUND_IMG}" /Volumes/"${VOL_NAME}"/.background/

# tell the Finder to resize the window, set the background,
#  change the icon size, place the icons in the right position, etc.
echo '
   tell application "Finder"
     tell disk "'${VOL_NAME}'"
           open
           set current view of container window to icon view
           set toolbar visible of container window to false
           set statusbar visible of container window to false
           set the bounds of container window to {300, 300, 825, 760}
           set viewOptions to the icon view options of container window
           set arrangement of viewOptions to not arranged
           set icon size of viewOptions to 72
           set background picture of viewOptions to file ".background:'${DMG_BACKGROUND_IMG}'"
           set position of item "'${APP_NAME}'" of container window to {160, 205}
           set position of item "Applications" of container window to {360, 205}
           close
           open
           update without registering applications
           delay 2
     end tell
   end tell
' | osascript

sync

# unmount it
hdiutil detach "${DEVICE}"

# now make the final image a compressed disk image
echo "Creating compressed image\n"
hdiutil convert "${DMG_TMP}" -format UDZO -imagekey zlib-level=9 -o "${DMG_FINAL}"

# clean up
rm -rf "${DMG_TMP}"
rm -rf "${STAGING_DIR}"

echo 'Signing.\n'
codesign -s "Developer ID Application: INSERT DEVELOPER ID HERE" "${DMG_FINAL}"

echo 'Done.'

exit