Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .github/workflows/macos-pkg-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: macOS PKG Build & Notarize

on:
push:
branches: [ "release/**" ]
workflow_dispatch:

jobs:
build-pkg:
runs-on: macos-latest

steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'oracle'

- name: Install Ant
run: brew install ant

# Import both Developer ID Application and Developer ID Installer certs
# into a temporary keychain so codesign/productsign never prompt for a password.
- name: Install signing certificates
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

# Decode the base64-encoded .p12 that contains both Developer ID certs
echo -n "$MACOS_CERTIFICATE" | base64 --decode -o /tmp/build_certificate.p12

# Create a temporary keychain and import the certificate
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import /tmp/build_certificate.p12 -P "$MACOS_CERTIFICATE_PWD" \
-A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"

# Grant codesign and productsign access without any UI password prompt
security set-key-partition-list -S apple-tool:,apple: -s \
-k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"

# Add this keychain to the search path
security list-keychain -d user -s "$KEYCHAIN_PATH"

- name: Write private.properties
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
cat > private.properties <<EOF
apple.id=${APPLE_ID}
apple.password=${APPLE_APP_SPECIFIC_PASSWORD}
apple.team.id=${APPLE_TEAM_ID}
signing.identity=Developer ID Application: Tyler Lui (232JY57U23)
mac.package.identifier=tylerlui.pooltesting
mac.installer.signing.identity=Developer ID Installer: Tyler Lui (232JY57U23)
EOF

- name: Build and sign PKG
run: ant create-pkg

- name: Notarize PKG
run: ant notarize-pkg

- name: Staple notarization ticket
run: xcrun stapler staple dist/jdiskmark-*.pkg

- name: Verify with Gatekeeper
run: spctl --assess --type install dist/jdiskmark-*.pkg

- name: Upload PKG artifact
uses: actions/upload-artifact@v4
with:
name: jdiskmark-macos-pkg
path: dist/jdiskmark-*.pkg

- name: Clean up keychain
if: always()
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
/jdm.properties
/tmp
/derbyDB/
/build.sign

developer_log.json
.DS_Store
debug-benchmark.json
*.zip
*.patch
.VSCodeCounter*
build.properties
hs_err_pid*.log
private.properties
.DS_Store

# deb packaging
Expand Down
214 changes: 211 additions & 3 deletions build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,13 @@
<copy file="capacity.ps1" todir="dist"/>
<copy file="disk-model.ps1" todir="dist"/>
<!--<antcall target="buildinfo"/>-->
<copy file="pkg/macos/JDM.icns" todir="${dist.dir}"/>
<copy file="pkg/macos/images/JDiskMark.icns" todir="${dist.dir}"/>
<!-- Also provide icon files named to match the application/launcher names
jpackage for macOS expects an icon file whose filename matches the
application (or launcher) name. Create copies with and without the
version suffix so DMG/PKG generation finds them. -->
<copy file="pkg/macos/images/JDiskMark.icns" tofile="${dist.dir}/${pkg.name}-${version}.icns" failonerror="false"/>
<copy file="pkg/macos/images/JDiskMark.icns" tofile="${dist.dir}/${pkg.name}.icns" failonerror="false"/>
<copy file="pkg/macos/run.sh" todir="${dist.dir}"/>
<!--<copy file="build.properties" todir="dist"/>-->
<!--<copy file="build.properties" todir="src/META-INF"/>-->
Expand Down Expand Up @@ -223,7 +229,7 @@
<echo message="MSI installer created: ${dist.dir}/${jpackage.output.msi.targetname}"/>
</target>

<target name="create-dmg" depends="clean, jar, -post-jar, -prepare-jpackage-input-jar" description="Creates the DMG installer using jpackage">
<target name="create-dmg" depends="clean, -pre-jar, jar, -post-jar, -prepare-jpackage-input-jar" description="Creates the DMG installer using jpackage">
<property name="jpackage.input.dir.relative" value="${release.dir}"/>

<echo message="Deleting existing dmg files: ${dist.dir}/${app.name}-*.dmg"/>
Expand All @@ -242,7 +248,7 @@
<arg line="--name ${pkg.name}-${version}"/>
<arg line="--app-version ${dmg.version}"/>
<arg line="--vendor 'jdiskmark'"/>
<arg line="--icon '${dist.dir}/${release.dir}/JDM.icns'"/>
<arg line="--icon '${dist.dir}/${release.dir}/${app.name}.icns'"/>
<arg line="--dest ${dist.dir}"/>
</exec>

Expand All @@ -257,6 +263,206 @@
<echo message="DMG installer created: ${dist.dir}/${jpackage.output.dmg.targetname}"/>
</target>


<target name="create-pkg" depends="clean, -pre-jar, jar, -post-jar, -prepare-jpackage-input-jar" description="Creates the PKG installer using jpackage">
<property file="private.properties" />
<delete>
<fileset dir="dist" includes="*.pkg"/>
</delete>
<property name="flatlaf.jar.name" value="flatlaf-3.7.jar"/>
<property name="flatlaf.jar.src" value="libs/${flatlaf.jar.name}"/>
<property name="flatlaf.jar.input" value="${dist.dir}/${release.dir}/lib/${flatlaf.jar.name}"/>
<property name="temp.extract.dir" value="build.sign"/>
<delete dir="${temp.extract.dir}"/>
<mkdir dir="${temp.extract.dir}"/>

<property name="app.image.dir" value="${dist.dir}/${pkg.name}-${version}-app-image"/>
<property name="pkg.app.bundle" value="${app.image.dir}/${pkg.name}-${version}.app"/>
<property name="unsigned.pkg" value="${dist.dir}/${pkg.name}-${version}-unsigned.pkg"/>
<property name="jpackage.output.pkg.defaultname" value="${pkg.name}-${version}-${dmg.version}.pkg"/>
<property name="jpackage.output.pkg.targetname" value="${pkg.name}-${version}.pkg"/>

<!-- Remove Windows-only files from the jpackage input dir -->
<delete failonerror="false">
<fileset dir="${dist.dir}/${release.dir}" includes="*.pkg,*.exe,*.msi,*.ps1"/>
</delete>
<!-- Remove any stale app-image dir that may have been swept into the
input dir by a prior -post-jar run; jpackage would bundle it. -->
<delete dir="${dist.dir}/${release.dir}/${pkg.name}-${version}-app-image" failonerror="false"/>

<!-- Step 1: Sign FlatLaf native dylibs BEFORE packaging.
They live inside a JAR so codesign deep cannot reach them at
bundle-signing time. Sign them now and repack into both the
project libs/ copy and the input-dir copy so jpackage bundles
the signed versions. -->
<echo message="Step 1: Signing FlatLaf dylibs..."/>
<unzip src="${flatlaf.jar.src}" dest="${temp.extract.dir}">
<patternset>
<include name="com/formdev/flatlaf/natives/libflatlaf-macos-x86_64.dylib"/>
<include name="com/formdev/flatlaf/natives/libflatlaf-macos-arm64.dylib"/>
</patternset>
</unzip>
<exec executable="codesign" failonerror="true">
<arg value="--force"/>
<arg value="--options"/><arg value="runtime"/>
<arg value="--entitlements"/><arg value="entitlements.plist"/>
<arg value="--timestamp"/>
<arg value="--sign"/><arg value="${signing.identity}"/>
<arg value="${temp.extract.dir}/com/formdev/flatlaf/natives/libflatlaf-macos-x86_64.dylib"/>
</exec>
<exec executable="codesign" failonerror="true">
<arg value="--force"/>
<arg value="--options"/><arg value="runtime"/>
<arg value="--entitlements"/><arg value="entitlements.plist"/>
<arg value="--timestamp"/>
<arg value="--sign"/><arg value="${signing.identity}"/>
<arg value="${temp.extract.dir}/com/formdev/flatlaf/natives/libflatlaf-macos-arm64.dylib"/>
</exec>
<!-- Repack into the project source JAR and the input-dir copy -->
<jar destfile="${flatlaf.jar.src}" update="yes">
<fileset dir="${temp.extract.dir}"/>
</jar>
<jar destfile="${flatlaf.jar.input}" update="yes">
<fileset dir="${temp.extract.dir}"/>
</jar>
<delete dir="${temp.extract.dir}"/>
<echo message="FlatLaf dylibs signed and repacked."/>

<!-- Step 2: Build UNSIGNED app-image.
Do NOT pass mac-sign. Signing is done manually below so we
control entitlements. jpackage's mac-entitlements does not
reliably apply to inner binaries, and passing mac-sign on the
later pkg step causes jpackage to re-sign the bundle, wiping
the entitlements we set here. -->
<echo message="Step 2: Building unsigned app-image..."/>
<delete dir="${app.image.dir}" failonerror="false"/>
<exec executable="jpackage" failonerror="true">
<arg value="--type"/><arg value="app-image"/>
<arg value="--input"/><arg value="${dist.dir}/${release.dir}"/>
<arg value="--main-jar"/><arg value="${pkg.name}.jar"/>
<arg value="--main-class"/><arg value="${pkg.name}.App"/>
<arg value="--name"/><arg value="${pkg.name}-${version}"/>
<arg value="--app-version"/><arg value="${dmg.version}"/>
<arg value="--vendor"/><arg value="jdiskmark"/>
<arg value="--dest"/><arg value="${app.image.dir}"/>
<arg value="--resource-dir"/><arg value="pkg/macos/images"/>
<arg value="--mac-package-identifier"/><arg value="${mac.package.identifier}"/>
<arg value="--mac-package-name"/><arg value="JDiskMark"/>
</exec>

<!-- Step 3a: Sign all dylibs/so files inside the bundled JDK runtime. -->
<echo message="Step 3a: Signing runtime dylibs..."/>
<exec executable="bash" failonerror="true">
<arg value="-c"/>
<arg value="find &quot;${pkg.app.bundle}/Contents/runtime&quot; \( -name '*.dylib' -o -name '*.so' \) | while read f; do codesign --force --options runtime --entitlements entitlements.plist --timestamp --sign &quot;${signing.identity}&quot; &quot;$f&quot;; done"/>
</exec>

<!-- Step 3b: Sign Mach-O executables in the runtime (keytool, java, etc.). -->
<echo message="Step 3b: Signing runtime executables..."/>
<exec executable="bash" failonerror="true">
<arg value="-c"/>
<arg value="find &quot;${pkg.app.bundle}/Contents/runtime&quot; -type f -perm +111 ! -name '*.dylib' ! -name '*.so' | while read f; do if file &quot;$f&quot; | grep -q Mach-O; then codesign --force --options runtime --entitlements entitlements.plist --timestamp --sign &quot;${signing.identity}&quot; &quot;$f&quot;; fi; done"/>
</exec>

<!-- Step 3c: Seal the bundled JDK runtime as a sub-bundle.
The runtime has a Contents/ structure (MacOS/, Home/, Info.plist) that
codesign treats as a nested bundle. Individual dylibs/executables inside
it were signed in 3a/3b, but the runtime sub-bundle itself needs its own
seal before the outer app bundle seal can reference it cleanly. Without
this, the outer bundle seal sees "a sealed resource is missing or invalid"
on Contents/runtime/Contents/MacOS/libjli.dylib. -->
<echo message="Step 3c: Sealing runtime sub-bundle..."/>
<exec executable="codesign" failonerror="true">
<arg value="--force"/>
<arg value="--options"/><arg value="runtime"/>
<arg value="--entitlements"/><arg value="entitlements.plist"/>
<arg value="--timestamp"/>
<arg value="--sign"/><arg value="${signing.identity}"/>
<arg value="${pkg.app.bundle}/Contents/runtime"/>
</exec>

<!-- Step 3d: Sign the main launcher binary. -->
<echo message="Step 3d: Signing main launcher..."/>
<exec executable="codesign" failonerror="true">
<arg value="--force"/>
<arg value="--options"/><arg value="runtime"/>
<arg value="--entitlements"/><arg value="entitlements.plist"/>
<arg value="--timestamp"/>
<arg value="--sign"/><arg value="${signing.identity}"/>
<arg value="${pkg.app.bundle}/Contents/MacOS/${pkg.name}-${version}"/>
</exec>

<!-- Step 3e: Seal the app bundle. -->
<echo message="Step 3e: Sealing app bundle..."/>
<exec executable="codesign" failonerror="true">
<arg value="--force"/>
<arg value="--options"/><arg value="runtime"/>
<arg value="--entitlements"/><arg value="entitlements.plist"/>
<arg value="--timestamp"/>
<arg value="--sign"/><arg value="${signing.identity}"/>
<arg value="${pkg.app.bundle}"/>
</exec>

<!-- Step 4: Build unsigned PKG from the signed app-image using pkgbuild + productbuild.
jpackage type pkg re-signs the entire app bundle ad-hoc (even without mac-sign),
which wipes the Developer ID Application signatures applied in step 3. -->
<echo message="Step 4a: Building component PKG..."/>
<property name="component.pkg" value="${dist.dir}/${pkg.name}-${version}-component.pkg"/>
<delete file="${component.pkg}" failonerror="false"/>
<exec executable="pkgbuild" failonerror="true">
<arg value="--component"/><arg value="${pkg.app.bundle}"/>
<arg value="--install-location"/><arg value="/Applications"/>
<arg value="--identifier"/><arg value="${mac.package.identifier}"/>
<arg value="--version"/><arg value="${dmg.version}"/>
<arg value="--scripts"/><arg value="pkg/macos/scripts"/>
<arg value="${component.pkg}"/>
</exec>
<echo message="Step 4b: Building distribution PKG..."/>
<exec executable="productbuild" failonerror="true">
<arg value="--package"/><arg value="${component.pkg}"/>
<arg value="${unsigned.pkg}"/>
</exec>
<delete file="${component.pkg}"/>

<!-- Step 5: Sign the PKG installer with Developer ID Installer cert.
productsign signs the outer package; the app bundle inside
keeps its Developer ID Application signature from step 3. -->
<echo message="Step 5: Signing PKG installer with Developer ID Installer..."/>
<exec executable="productsign" failonerror="true">
<arg value="--sign"/><arg value="${mac.installer.signing.identity}"/>
<arg value="--timestamp"/>
<arg value="${unsigned.pkg}"/>
<arg value="${dist.dir}/${jpackage.output.pkg.targetname}"/>
</exec>
<delete file="${unsigned.pkg}"/>

<echo message="PKG installer created: ${dist.dir}/${jpackage.output.pkg.targetname}"/>

</target>
<target name="notarize-pkg" description="Submit the package for Apple notarization">
<!-- Populate the private.properties file with apple credentials -->
<property file="private.properties" />

<echo message="Submitting ${dist.dir}/jdiskmark-${version}.pkg for notarization..."/>

<exec executable="xcrun" failonerror="true">
<arg value="notarytool"/>
<arg value="submit"/>
<arg value="${dist.dir}/jdiskmark-${version}.pkg"/>

<arg value="--apple-id"/>
<arg value="${apple.id}"/>

<arg value="--password"/>
<arg value="${apple.password}"/>

<arg value="--team-id"/>
<arg value="${apple.team.id}"/>

<arg value="--wait"/>
</exec>
</target>

<target name="sign-msi" depends="create-msi" description="Signs the MSI installer using signtool.exe (manual invocation)">
<echo message="Signing MSI installer..."/>
<condition property="signtool.is.available">
Expand All @@ -281,8 +487,10 @@
<arg value="/a"/> <arg file="${msi.file.to.sign.path}"/> </exec>
<echo message="MSI file successfully signed: ${msi.file.to.sign.path}"/>
</target>

<target name="package-msi" depends="clean, jar, create-msi, sign-msi" description="Builds JAR and MSI installer"/>
<target name="package-dmg" depends="clean, jar, create-dmg" description="Builds JAR and DMG installer"/>
<target name="package-pkg" depends="clean, jar, create-pkg, notarize-pkg" description="Builds PKG installer"/>

<property name="deb.dir" value="jdiskmark-deb"/>
<property name="deb.version" value="${version}-1"/>
Expand Down
12 changes: 12 additions & 0 deletions entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?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-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
Binary file modified libs/flatlaf-3.7.jar
Binary file not shown.
File renamed without changes.
6 changes: 6 additions & 0 deletions pkg/macos/scripts/preinstall
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
# Remove any existing jdiskmark installation before installing fresh.
# This prevents macOS from skipping the install when the same bundle ID
# and version is already on disk (upgrade-bundle version check behaviour).
rm -rf /Applications/jdiskmark*.app 2>/dev/null
exit 0
Loading