I’m sorry this might be long, but it seems that this process is extremely precise and so I want to provide as much detail as possible to help you understand how I got here. I’m really stuck and desperately need some guidance on this. I’ll give you a high-level overview of what I’m trying to achieve, and then show you what I have so far.
So my goal is to take an existing .Net project that’s essentially just security software running a service as a daemon, and effectively to get it running on Mac. This involves building an installer for Mac, because the service that will be running in the background as a daemon needs to check a specific file path for a JSON file that’s created during the installer. The JSON file is created via a Swift app that I wrote. The app simply displays a dialog that asks for the user’s email address, first name and last name, and it also generates a unique device ID. When the user submits this form dialog, the swift app generates a JSON file and stores it at a specific file path that the .Net service knows where to find it when it launches. So to interject the installer with a Swift dialog, I created a preinstall script that basically just launches the .app file I created by archiving my Swift app in Xcode. When I run this script independently, it works as it should and as long as the .app file is where it should be, which is in root of my project directory. When I test the script independently, it launches the app and the dialog is presented, creates the JSON file and exits as it should. I’ve also resolved all signing and notarization issues and have confirmed that each sub-package can be executed without GateKeeper warnings on a fresh Mac. My issue is that when I build my final package using productbuild, the scripts are never executed and the logs found at /var/log/install.log give no indication that the scripts are running or even referenced in any way. Here is my file structure:
The files highlighted in blue are what I started with. Everything EXCEPT NetarxInstallationDialog.app and Poseidon.svc.zip were generated when publishing our .NET project. Poseidon.svc is the service I ultimately need to run as a daemon. NetarxInstallationDialog.app is my Swift project that was archived, signed and notarized already. I’ve confirmed that this was done correctly, as it runs just fine on a fresh Mac. So from this starting point (assume just the files in blue existed at the time):
First I zipped Poseidon.svc so I could notarize it. I ran:
zip -r Poseidon.svc.zip Poseidon.svc
Which produced Poseidon.svc.zip. Then I ran:
xcrun notarytool submit Poseidon.svc.zip --keychain-profile “Netarx” --wait
This succeeded with no issues.
Then I built a package for Poseidon.svc:
pkgbuild --root ./ --identifier "com.netarx.poseidon" --version "1.0.0" --install-location "/Library/Application Support/Netarx" --sign "Developer ID Installer: xxx xxxx (xxxxxxxxxx)" Poseidon.pkg
This produced the file Poseidon.pkg, then to sign the package I ran:
productsign --sign "Developer ID Installer: xxx xxxx (xxxxxxxxxx)" --timestamp Poseidon.pkg Poseidon-signed.pkg
This produced the file Poseidon-signed.pkg, and I deleted the original Poseidon.pkg as it was no longer needed.
Then I notarized Poseidon-signed.pkg:
xcrun notarytool submit Poseidon-Signed.pkg --keychain-profile "Poseidon" --wait
This succeeded without error. Then I stapled the notarization ticket to Poseidon-signed.pkg:
xcrun stapler staple Poseidon-Signed.pkg
This succeeded without error and I’ve verified this package runs an installer with no GateKeeper warnings on a fresh Mac.
Then I built a package for my NetarxInstallationDialog.app (The swift app) so that it could be used in productbuild for the final installer:
pkgbuild --root NetarxInstallationDialog.app
--install-location "/Library/Application Support/Netarx/NetarxInstallationDialog.app"
--identifier "com.netarx.installationdialog"
--version "1.0.0"
--sign "Developer ID Installer: xxx xxxx (xxxxxxxxxx)"
NetarxInstallationDialog.pkg
This produced the file NetarxInstallationDialog.pkg. Since I signed this package directly within the pkgbuild command, I simply checked the signature using pkgutil and confirmed it was good. Then I notarized NetarxInstallationDialog.pkg:
xcrun notarytool submit NetarxInstallationDialog.pkg --keychain-profile "Poseidon" --wait
This also succeeded without error, so I then stapled the notarization ticket to NetarxInstallationDialog.pkg:
xcrun stapler staple NetarxInstallationDialog.pkg
This succeeded without error as well, and I was able to verify that this pkg also runs on a fresh Mac with no GateKeeper warning.
Since I need my swift app to launch at the beginning of the installation, before Poseidon.svc can be installed and run, I added a preinstall script:
#!/bin/sh
# Define log file
USER_NAME="$(whoami)"
LOG_FILE="/Users/${USER_NAME}/Desktop/preinstall_script.log"
echo "Starting preinstall script" > "${LOG_FILE}"
# Path to the application
APP_PATH="/Library/Application Support/Netarx/NetarxInstallationDialog.app"
# Define the JSON path
JSON_PATH="/Users/${USER_NAME}/Library/Application Support/Netarx/Validator/UserConfiguration.json"
echo "Expected JSON path: ${JSON_PATH}" >> "${LOG_FILE}"
# Check if the application exists and run it
if [ -d "$APP_PATH" ]; then
echo "Application found, attempting to open..." >> "${LOG_FILE}"
open "$APP_PATH"
while [ ! -f "${JSON_PATH}" ]; do
echo "Waiting for user input..." >> "${LOG_FILE}"
sleep 2
done
echo "Configuration file found. Continuing installation..." >> "${LOG_FILE}"
else
echo "Application not found at $APP_PATH" >> "${LOG_FILE}"
exit 1
fi
echo "Preinstall script completed successfully" >> "${LOG_FILE}"
exit 0
This script basically is just meant to execute NetarxInstallationDialog.app and wait for the user to submit input, and then the Swift app generates a JSON file and stores it in /Users/${USER_NAME}/Library/Application Support/Netarx/Validator/UserConfiguration.json. After the file is found, the script exits with code 0.
As I said, I’ve tested this script by running it from terminal and I see NetarxInstallationDialog.app launch, and the script exit with code 0 once I submit input within the swift app. That all seems to work as expected.
I’ve also included a postinstall script, which aims to install Poseidon.svc as a launch daemon:
#!/bin/sh
# Define the source path of the .plist file
PLIST_SRC="./resources/com.netarx.poseidon.plist"
# Define the destination path
PLIST_DEST="/Library/LaunchDaemons/com.netarx.poseidon.plist"
# Debug: Check if the .plist file exists
if [ -f "$PLIST_SRC" ]; then
echo ".plist file found, proceeding with copy and permissions settings..."
# Copy the .plist file to the correct directory
sudo cp "$PLIST_SRC" "$PLIST_DEST"
# Set the owner and permissions
sudo chown root:wheel "$PLIST_DEST"
sudo chmod 644 "$PLIST_DEST"
# Load the daemon
sudo launchctl load -w "$PLIST_DEST"
else
echo ".plist file not found at $PLIST_SRC"
fi
Here’s the plist my postinstall script references:
<?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>Label</key>
<string>com.netarx.poseidon</string>
<key>ProgramArguments</key>
<array>
<string>/Library/Application Support/Netarx/Poseidon.svc</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
Then I added a Distribution.xml file to my root directory:
<?xml version="1.0" encoding="UTF-8"?>
<installer-script minSpecVersion="1">
<title>Your Application Installer</title>
<options customize="allow" allow-external-scripts="no"/>
<choices-outline>
<line choice="default">
<line choice="InstallNetarxDialog"/>
<line choice="InstallPoseidon"/>
</line>
</choices-outline>
<choice id="default"/>
<choice id="InstallNetarxDialog">
<pkg-ref id="netarx"/>
</choice>
<choice id="InstallPoseidon">
<pkg-ref id="poseidon"/>
</choice>
<pkg-ref id="netarx" version="1.0.0" auth="Root">NetarxInstallationDialog.pkg</pkg-ref>
<pkg-ref id="poseidon" version="1.0.0" auth="Root">Poseidon-signed.pkg</pkg-ref>
<scripts>
<preinstall file="resources/scripts/preinstall.sh"/>
<postinstall file="resources/scripts/postinstall.sh"/>
</scripts>
</installer-script>
After adding Distribution.xml to my project root directory, adding the plist to a /resources directory, and adding scripts to a /scripts directory within /resources, I ran productbuild to bundle it all together and create the final installer which I aim to distribute:
productbuild --distribution ./Distribution.xml
--resources resources
--package-path .
--sign "Developer ID Installer: xxx xxxx (xxxxxxxxxx)"
/Users/xxxxxxx/Desktop/NewPoseidon/FinalInstaller.pkg
This produces the file FinalInstaller.pkg (not pictured, but it’s in the root directory of my project with the other packages). I also signed and notarized FinalInstaller.pkg without issue and verified that it runs on another Mac without GateKeeper warnings.
The issue is that when I run this FinalInstaller.pkg, none of the scripts appear to be running. I’m not seeing my Swift app launch and not seeing Poseidon.svc installed where it should be to run as a daemon.
With all of this said, do you see anywhere I went wrong with any of this? I’ve never built an installer for Mac and I wouldn’t be surprised at all if I’m going about this incorrectly. Any help would be greatly appreciated!