Skip to content

Instantly share code, notes, and snippets.

@tresf
Last active July 6, 2021 18:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tresf/11038234bff464b301e62f21c96a7c77 to your computer and use it in GitHub Desktop.
Save tresf/11038234bff464b301e62f21c96a7c77 to your computer and use it in GitHub Desktop.
#!/bin/bash
# Downloads, extracts and bundles the JDK into a runnable DMG for MacOS
#
# Dependencies:
# homebrew
# brew install node
# npm install -g appdmg
# xcode 12.0+
# command line utilities
# keychain access
# a valid signing i.d. from apple
SIGNING_ID="P5DMU6659X" # tres@qz.io
# Calulate the download URL
if [ "$(uname -m)" = "arm64" ]; then
# aarch64
JDK_URL="https://download.bell-sw.com/java/11.0.11+10/bellsoft-jdk11.0.11+10-macos-aarch64.zip"
else
# amd64
JDK_URL="https://download.bell-sw.com/java/11.0.11+9/bellsoft-jdk11.0.11+9-macos-amd64.zip"
fi
# Download and extract
mkdir -p jdk
if [ "$1" = "-f" ]; then
rm -rf jdk/extracted
fi
echo
if [ -d jdk/extracted ]; then
echo "- JDK already downloaded (-f will force a re-download)"
else
echo "- Downloading the JDK from $JDK_URL..."
curl -o jdk/jdk.zip "$JDK_URL"
echo "- Unzipping the JDK to jdk/extracted..."
unzip jdk/jdk.zip -d jdk/extracted
fi
# Calculate JDK_HOME
# TODO: Some JDKs use different structure
pushd jdk/extracted > /dev/null
jdkfiles=(*)
JDK_RELATIVE="jdk/extracted/${jdkfiles[0]}"
popd > /dev/null
JDK_HOME="$(pwd)/$JDK_RELATIVE"
echo
echo "- Calculated JDK_HOME=$JDK_HOME"
echo " - Testing JDK..."
JDK_VERSION_RAW=$("$JDK_HOME/bin/java" --version 2>&1)
# TODO: Some JDKs return double quotes
JDK_VERSION="$(echo $JDK_VERSION_RAW | sed 's/ /\n/g' | sed -n 2p)"
JDK_VERSION_MAJOR=$(echo $JDK_VERSION | cut -d. -f1)
JDK_VERSION_MINOR=$(echo $JDK_VERSION | cut -d. -f2)
JDK_VERSION_PATCH=$(echo $JDK_VERSION | cut -d. -f3)
if [ -n "JDK_VERSION_PATCH" ]; then
echo " - Found version $JDK_VERSION_MAJOR.$JDK_VERSION_MINOR.$JDK_VERSION_PATCH"
else
echo " - BROKEN"; exit 1
fi
# Create a sample Java application
echo
echo "- Creating a sample Java application..."
mkdir -p src
pushd src > /dev/null
cat >Hello.java <<EOL
import javax.print.*;
import javax.swing.*;
public class Hello {
public static void main(String ... args) throws Throwable {
SwingUtilities.invokeAndWait(() -> {
JOptionPane.showMessageDialog(null, "Hello world!" +
"\n JAVA_HOME: " + System.getProperty("java.home") +
"\n SANDBOXED: " + (System.getenv("APP_SANDBOX_CONTAINER_ID") != null) +
"\n PRINTER: " + PrintServiceLookup.lookupDefaultPrintService() +
"\n TOTAL: " + PrintServiceLookup.lookupPrintServices(null, null).length);
System.exit(0);
});
}
}
EOL
if "$JDK_HOME/bin/javac" Hello.java ; then
echo " - Compiled successfully"
else
echo " - Compilation failed"; exit 1
fi
echo "Main-Class: Hello" > MANIFEST.MF
rm -rf hello.jar
JAR_PATH="$(pwd)/hello.jar"
if "$JDK_HOME/bin/jar" cfm "$JAR_PATH" MANIFEST.MF Hello.class ; then
echo " - $JAR_PATH created"
else
echo " - $JAR_PATH creation failed"; exit 1
fi
popd > /dev/null
# Setup some jlink tools
echo
echo "- Calculating some jlink tools..."
JMODS="$JDK_HOME/jmods"
JDEPS="$JDK_HOME/bin/jdeps"
JLINK="$JDK_HOME/bin/jlink"
# Make sure they work
echo " - JMODS=$JMODS"
if [ -d "$JMODS" ]; then
echo " - EXISTS"
else
echo " - MISSING"; exit 1
fi
echo " - JDEPS=$JDEPS"
if "$JDEPS" --help >/dev/null; then
echo " - WORKS"
else
echo " - BROKEN"; exit 1
fi
echo " - JLINK=$JLINK"
if "$JLINK" --help >/dev/null; then
echo " - WORKS"
else
echo " - BROKEN"; exit 1
fi
# Start constructing a runtime from our jar
echo
echo "- Creating a suitable runtime in out/jlink for $JAR_PATH..."
# TODO: --ignore-missing-deps should be JDK 11.0.11+ only
echo " - Calculating dependencies..."
# TODO: For most apps, this will spew out unusable modules which need to be filtered
NEEDED_MODULES=$("$JDEPS" --list-deps --ignore-missing-deps "$JAR_PATH" |tr '\n' ',' |tr -d '[:space:]')
TARGET_APP=out/Hello.app
TARGET_RUNTIME="$TARGET_APP/Contents/PlugIns/Java.runtime"
echo " - Building $TARGET_RUNTIME..."
rm -rf "$TARGET_APP"
mkdir -p "$TARGET_RUNTIME/Contents"
"$JLINK" --strip-debug --compress=2 --no-header-files --no-man-pages --module-path "$JMODS" --add-modules $NEEDED_MODULES --output out/Hello.app/Contents/PlugIns/Java.runtime/Contents/Home
# Fix some layout nuances
echo
echo "- Fixing some Java.runtime layout nuances..."
echo " - Copying lib/jli/libjli.dylib to $TARGET_RUNTIME/Contents/MacOS..."
# TODO: Some JDKs offer this file in MacOS already, but jlink still won't deploy it. Copy from lib/jli is a guess?
mkdir -p "$TARGET_RUNTIME/Contents/MacOS"
if ! cp "$TARGET_RUNTIME/Contents/Home/lib/jli/libjli.dylib" "$TARGET_RUNTIME/Contents/MacOS" ; then
echo " - Failed"; exit 1
fi
echo " - Removing unused files from $TARGET_RUNTIME/Contents/Home/bin..."
rm -f "$TARGET_RUNTIME/Contents/Home/bin/jfr"
rm -f "$TARGET_RUNTIME/Contents/Home/bin/rmid"
rm -f "$TARGET_RUNTIME/Contents/Home/bin/rmiregistry"
rm -f "$TARGET_RUNTIME/Contents/Home/bin/jrunscript"
rm -f "$TARGET_RUNTIME/Contents/Home/bin/keytool"
# Start building the app structure
echo
echo " - Building the main bundle..."
mkdir "$TARGET_APP/Contents/Resources"
echo " - Creating resources..."
cp /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/Notifications.icns "$TARGET_APP/Contents/Resources/hello.icns"
cp "$JAR_PATH" "$TARGET_APP/Contents/Resources"
echo " - Creating launcher..."
mkdir "$TARGET_APP/Contents/MacOS"
cat >"$TARGET_APP/Contents/MacOS/Hello" <<EOL
#!/bin/bash
SCRIPT_DIR="\$( cd "\$( dirname "\${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
pushd "\$SCRIPT_DIR/../PlugIns/Java.runtime/Contents/home/bin"
./java -jar "\$SCRIPT_DIR/../Resources/hello.jar"
popd
EOL
chmod +x "$TARGET_APP/Contents/MacOS/Hello"
echo " - Writing Info.plist..."
cat>"$TARGET_APP/Contents/Info.plist" <<EOL
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleDevelopmentRegion</key><string>English</string>
<key>CFBundleIconFile</key><string>hello</string>
<key>CFBundleIdentifier</key><string>io.qz.hello</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleGetInfoString</key><string>Hello 1.0.0</string>
<key>CFBundleSignature</key><string>Hello</string>
<key>CFBundleExecutable</key><string>Hello</string>
<key>CFBundleVersion</key><string>1.0.0</string>
<key>CFBundleShortVersionString</key><string>1.0.0</string>
<key>CFBundleName</key><string>Hello</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
</dict></plist>
EOL
echo " - You should have a usable app now, but we're going to try to sign and hopefully sandbox it."
echo
echo "- Preparing the entitlement files..."
echo " - Writing bundle entitlements to src/entitlements.plist..."
cat>"src/entitlements.plist" <<EOL
<?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.network.client</key><true/>
<key>com.apple.security.network.server</key><true/>
<key>com.apple.security.files.all</key><true/>
<key>com.apple.security.print</key><true/>
<key>com.apple.security.device.usb</key><true/>
<key>com.apple.security.device.bluetooth</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-library-validation</key><true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key><true/>
<key>com.apple.security.cs.debugger</key><true/>
</dict>
</plist>
EOL
echo " - Writing Info.plist for $TARGET_RUNTIME..."
# TODO: Write vendor and version information dynamically
cat>"$TARGET_RUNTIME/Contents/Info.plist" <<EOL
<?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>CFBundleDevelopmentRegion</key><string>English</string>
<key>CFBundleExecutable</key><string>libjli.dylib</string>
<key>CFBundleGetInfoString</key><string>BellSoft 11.0.11+10</string>
<key>CFBundleIdentifier</key><string>io.qz.hello.jre</string>
<key>CFBundleInfoDictionaryVersion</key><string>7.0</string>
<key>CFBundleName</key><string>Java Runtime Image</string>
<key>CFBundlePackageType</key><string>BNDL</string>
<key>CFBundleShortVersionString</key><string>11.0.11</string>
<key>CFBundleSignature</key><string>????</string>
<key>CFBundleVersion</key><string>11.0.11</string>
<key>JavaVM</key>
<dict>
<key>JVMCapabilities</key>
<array>
<string>CommandLine</string>
<string>JNI</string>
<string>BundledApp</string>
</array>
<key>JVMMinimumFrameworkVersion</key><string>17.0.0</string>
<key>JVMMinimumSystemVersion</key><string>10.6.0</string>
<key>JVMPlatformVersion</key><string>11.0.11+10</string>
<key>JVMVendor</key><string>BellSoft</string>
<key>JVMVersion</key><string>11.0.11</string>
</dict>
</dict>
</plist>
EOL
echo "- Starting the signing process..."
echo " - Signing individual files to overwrite any lingering signatures..."
find -X "$TARGET_APP" -type f -not -path "*/Contents/MacOS/*" -exec sh -c 'file -I "{}" |grep -m1 "x-mach-binary"|cut -f 1 -d \:' \; |xargs codesign --force -s "$SIGNING_ID" --timestamp --options runtime --entitlement src/entitlements.plist
if [ $? -ne 0 ]; then
echo " - Failed."; exit 1
fi
echo " - Signing the Java.runtime bundle using src/entitlements.plist..."
if ! codesign --force -s $SIGNING_ID --timestamp --options runtime --entitlement src/entitlements.plist "$TARGET_RUNTIME" ; then
echo " - Failed."; exit 1
fi
echo " - Signing the main bundle using src/entitlements.plist..."
if ! codesign --force -s $SIGNING_ID --timestamp --options runtime --entitlement src/entitlements.plist "$TARGET_APP" ; then
echo " - Failed."; exit 1
fi
echo " - The signed app is available here: $TARGET_APP"
echo "- Packaging the DMG..."
# TODO: ../$TARGET_APP needed because appdmg works relative to json file
cat>"src/dmg.json" <<EOL
{
"title": "Hello",
"contents": [
{ "x": 407, "y": 244, "type": "link", "path": "/Applications" },
{ "x": 133, "y": 244, "type": "file", "path": "../$TARGET_APP" }
]
}
EOL
rm -f "out/Hello 1.0.0.dmg"
if ! appdmg "src/dmg.json" "out/Hello 1.0.0.dmg" ; then
echo " - Failed."; exit 1
fi
echo " - The DMG is available at out/Hello 1.0.0.dmg"
echo
echo "- Finished"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment