diff --git a/.github/workflows/ccpp.yml b/.github/workflows/ccpp.yml index 39cdac4a..5a5d08c7 100644 --- a/.github/workflows/ccpp.yml +++ b/.github/workflows/ccpp.yml @@ -56,12 +56,16 @@ jobs: bazel test //... --test_output=errors bazel build //:fluxengine_dmg --config=stamp --test_output=errors + - name: Signing package + run: | + cd fluxengine + sh scripts/resign-dmg.sh bazel-bin/java/com/cowlark/fluxengine/fluxengine.dmg fluxengine-signed.dmg - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: ${{ github.event.repository.name }}.${{ github.sha }}.osx.${{ runner.arch }} path: | - fluxengine/bazel-bin/java/com/cowlark/fluxengine/*.dmg + fluxengine/fluxengine-signed.dmg build-windows: runs-on: windows-latest diff --git a/MODULE.bazel b/MODULE.bazel index 5331a7ab..66884d52 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -19,6 +19,7 @@ maven.install( "com.jayway.jsonpath:json-path:3.0.0", "junit:junit:4.13.2", "org.usb4java:usb4java:1.3.0", + "io.github.dsheirer:libusb4java-darwin-aarch64:1.3.1", "com.formdev:flatlaf:3.7.2", "com.formdev:flatlaf-swingx:3.7.2", "org.exbin.bined:bined-core:0.2.2", diff --git a/java/com/cowlark/fluxengine/BUILD.bazel b/java/com/cowlark/fluxengine/BUILD.bazel index 27c681c9..862d86f7 100644 --- a/java/com/cowlark/fluxengine/BUILD.bazel +++ b/java/com/cowlark/fluxengine/BUILD.bazel @@ -16,26 +16,24 @@ java_binary( jpackage( name = "fluxengine_deb", package_name = "fluxengine", - app_version = "1.0.0", - stamp = True, extra_launchers = [":fluxengine-gui.properties"], jar = ":fluxengine_deploy.jar", launcher_icon = "//extras:icon.png", main_class = "com.cowlark.fluxengine.cli.Main", package_type = "deb", + stamp = True, tags = ["manual"], ) jpackage( name = "fluxengine_rpm", package_name = "fluxengine", - app_version = "1.0.0", - stamp = True, extra_launchers = [":fluxengine-gui.properties"], jar = ":fluxengine_deploy.jar", launcher_icon = "//extras:icon.png", main_class = "com.cowlark.fluxengine.cli.Main", package_type = "rpm", + stamp = True, tags = ["manual"], ) @@ -46,8 +44,6 @@ jpackage( jpackage( name = "fluxengine_msi", package_name = "fluxengine", - app_version = "1.0.0", - stamp = True, extra_launchers = [":fluxengine-gui.properties"], jar = ":fluxengine_deploy.jar", launcher_icon = select({ @@ -59,36 +55,38 @@ jpackage( "@platforms//os:windows": "msi", "//conditions:default": "unsupported", }), + stamp = True, tags = ["manual"], ) jpackage( name = "fluxengine_dmg", package_name = "fluxengine", - app_version = "1.0.0", - stamp = True, - extra_launchers = [":fluxengine-gui.properties"], + extra_jars = ["@maven//:io_github_dsheirer_libusb4java_darwin_aarch64"], + extra_jpackage_args = [ + "--mac-package-name FluxEngine", + ], jar = ":fluxengine_deploy.jar", launcher_icon = select({ "@platforms//os:osx": "//extras:fluxengine_icns", "//conditions:default": "//extras:icon.png", }), - main_class = "com.cowlark.fluxengine.cli.Main", + main_class = "com.cowlark.fluxengine.gui.Gui", package_type = select({ "@platforms//os:osx": "dmg", "//conditions:default": "unsupported", }), + stamp = True, tags = ["manual"], ) jpackage_app_image( name = "fluxengine_app_image", package_name = "fluxengine", - app_version = "1.0.0", - stamp = True, extra_launchers = [":fluxengine-gui.properties"], jar = ":fluxengine_deploy.jar", launcher_icon = "//extras:icon.png", main_class = "com.cowlark.fluxengine.cli.Main", + stamp = True, tags = ["manual"], ) diff --git a/scripts/jpackage.bzl b/scripts/jpackage.bzl index 2fccb11c..5da4889e 100644 --- a/scripts/jpackage.bzl +++ b/scripts/jpackage.bzl @@ -66,6 +66,14 @@ def _jpackage_impl(ctx): else: out = ctx.actions.declare_file(ctx.attr.package_name + "_" + ctx.attr.app_version + "." + extension) launchers = _launcher_properties(ctx) + stage_extra_jars = "\n".join([ + (' cp -L "{jar}" workdir/input/\n' + + ' (cd workdir/input && "{jar_tool}" xf "{jar}")').format( + jar_tool = java_runtime.java_home + "/bin/jar", + jar = extra_jar.path, + ) + for extra_jar in ctx.files.extra_jars + ]) # jpackage writes a lot of scratch state (a jlink runtime image and an app # image) and chmods files in it. Do all the scratch work in a plain @@ -91,7 +99,7 @@ def _jpackage_impl(ctx): stamp_inputs = [ctx.info_file] if ctx.attr.stamp else [] ctx.actions.run_shell( outputs = [out], - inputs = [jar] + [f for (_, f) in launchers] +\ + inputs = [jar] + ctx.files.extra_jars + [f for (_, f) in launchers] +\ ([ctx.file.launcher_icon] if ctx.file.launcher_icon else []) + stamp_inputs, tools = [java_runtime.files], use_default_shell_env = True, @@ -99,6 +107,7 @@ def _jpackage_impl(ctx): rm -rf workdir mkdir -p workdir/input workdir/tmp workdir/dest workdir/home workdir/rpmbuild workdir/resources cp -L "{jar}" workdir/input/ +{stage_extra_jars} chmod u+w workdir/input/* printf '[Desktop Entry]\\nName=FluxEngine\\nComment=FluxEngine\\nExec=APPLICATION_LAUNCHER\\nIcon=APPLICATION_ICON\\nTerminal=false\\nType=Application\\nCategories=DEPLOY_BUNDLE_CATEGORY\\n' > workdir/resources/fluxengine-gui.desktop if [ "{package_type}" = "rpm" ]; then @@ -129,6 +138,7 @@ def _jpackage_impl(ctx): --main-jar "{main_jar}" \ --main-class "{main_class}" \ $WIN_CONSOLE \ + {extra_jpackage_args} \ --resource-dir "$(pwd)/workdir/resources" \ {add_launcher_args} \ --jlink-options "--strip-debug --no-header-files --no-man-pages --strip-native-commands" \ @@ -145,8 +155,11 @@ def _jpackage_impl(ctx): info_file = ctx.info_file.path if ctx.attr.stamp else "", jar = jar.path, main_jar = jar.basename, + jar_tool = java_runtime.java_home + "/bin/jar", + stage_extra_jars = stage_extra_jars, main_class = ctx.attr.main_class, add_launcher_args = _add_launcher_args(launchers), + extra_jpackage_args = " ".join(ctx.attr.extra_jpackage_args), out = out.path, ), mnemonic = "Jpackage" + package_type.title(), @@ -167,6 +180,14 @@ def _jpackage_app_image_impl(ctx): else: out = ctx.actions.declare_file(ctx.attr.package_name + "_" + ctx.attr.app_version + ".tar.xz") launchers = _launcher_properties(ctx) + stage_extra_jars = "\n".join([ + (' cp -L "{jar}" workdir/input/\n' + + ' (cd workdir/input && "{jar_tool}" xf "{jar}")').format( + jar_tool = java_runtime.java_home + "/bin/jar", + jar = extra_jar.path, + ) + for extra_jar in ctx.files.extra_jars + ]) # jpackage --type app-image writes a directory (with a jlink runtime image # and the app launcher) and chmods files in it. Do the scratch work in a @@ -180,7 +201,7 @@ def _jpackage_app_image_impl(ctx): stamp_inputs = [ctx.info_file] if ctx.attr.stamp else [] ctx.actions.run_shell( outputs = [out], - inputs = [jar] + [f for (_, f) in launchers] +\ + inputs = [jar] + ctx.files.extra_jars + [f for (_, f) in launchers] +\ ([ctx.file.launcher_icon] if ctx.file.launcher_icon else []) + stamp_inputs, tools = [java_runtime.files], use_default_shell_env = True, @@ -188,6 +209,7 @@ def _jpackage_app_image_impl(ctx): rm -rf workdir mkdir -p workdir/input workdir/tmp workdir/dest workdir/home workdir/resources cp -L "{jar}" workdir/input/ +{stage_extra_jars} chmod u+w workdir/input/* printf '[Desktop Entry]\nName=FluxEngine\nComment=FluxEngine\nExec=APPLICATION_LAUNCHER\nIcon=APPLICATION_ICON\nTerminal=false\nType=Application\nCategories=DEPLOY_BUNDLE_CATEGORY\n' > workdir/resources/fluxengine-gui.desktop TMPDIR="$(pwd)/workdir/tmp" @@ -206,6 +228,7 @@ def _jpackage_app_image_impl(ctx): --input "$(pwd)/workdir/input" \ --main-jar "{main_jar}" \ --main-class "{main_class}" \ + {extra_jpackage_args} \ --resource-dir "$(pwd)/workdir/resources" \ {add_launcher_args} \ --jlink-options "--strip-debug --no-header-files --no-man-pages --strip-native-commands" \ @@ -224,8 +247,11 @@ def _jpackage_app_image_impl(ctx): info_file = ctx.info_file.path if ctx.attr.stamp else "", jar = jar.path, main_jar = jar.basename, + jar_tool = java_runtime.java_home + "/bin/jar", + stage_extra_jars = stage_extra_jars, main_class = ctx.attr.main_class, add_launcher_args = _add_launcher_args(launchers), + extra_jpackage_args = " ".join(ctx.attr.extra_jpackage_args), out = out.path, ), mnemonic = "JpackageAppImage", @@ -248,6 +274,10 @@ _jpackage_attrs = { doc = "jpackage launcher properties files; " + "each launcher is named after the file (minus its .properties suffix).", ), + "extra_jars": attr.label_list( + allow_files = [".jar"], + doc = "Additional runtime JARs copied beside the main application JAR.", + ), "launcher_icon": attr.label( allow_single_file = [".png", ".ico", ".icns"], doc = "Icon for all launchers, added to each launcher's " + @@ -259,7 +289,7 @@ _jpackage_attrs = { doc = "The package name; also used for the output filename.", ), "app_version": attr.string( - mandatory = True, + default = "1.0.0", doc = "Application version, e.g. '1.0.0'. Used as fallback when stamp is off.", ), "stamp": attr.bool( @@ -272,6 +302,10 @@ _jpackage_attrs = { doc = "The jpackage package type: deb/rpm (Linux), msi (Windows), dmg (macOS). " + "Use select() so this is only set to the matching platform.", ), + "extra_jpackage_args": attr.string_list( + default = [], + doc = "Additional arguments to pass to jpackage verbatim.", + ), } jpackage = rule( diff --git a/scripts/resign-dmg.sh b/scripts/resign-dmg.sh new file mode 100644 index 00000000..d6ee81f7 --- /dev/null +++ b/scripts/resign-dmg.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# +# resign-dmg.sh: unpack a dmg, ad-hoc sign the .app inside, and rebuild the dmg. +# +# Usage: ./resign-dmg.sh input.dmg [output.dmg] +# If output.dmg is omitted, the input file is replaced. +# +# Set SIGN_IDENTITY to use a real certificate instead of ad-hoc ("-"): +# SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" ./resign-dmg.sh in.dmg + +set -euo pipefail + +IN_DMG="${1:?Usage: $0 input.dmg [output.dmg]}" +OUT_DMG="${2:-$IN_DMG}" +IDENTITY="${SIGN_IDENTITY:--}" + +[[ -f "$IN_DMG" ]] || { echo "Not found: $IN_DMG" >&2; exit 1; } + +WORK="$(mktemp -d)" +MOUNT="$WORK/mount" +STAGE="$WORK/stage" +mkdir -p "$MOUNT" "$STAGE" + +cleanup() { + hdiutil detach "$MOUNT" -quiet -force 2>/dev/null || true + rm -rf "$WORK" +} +trap cleanup EXIT + +echo "==> Mounting $IN_DMG" +hdiutil attach "$IN_DMG" -mountpoint "$MOUNT" -nobrowse -readonly -quiet + +# Reuse the original volume name +VOLNAME="$(diskutil info "$MOUNT" | sed -n 's/^ *Volume Name: *//p')" +VOLNAME="${VOLNAME:-$(basename "$IN_DMG" .dmg)}" + +echo "==> Copying contents (volume: $VOLNAME)" +# ditto preserves symlinks (e.g. /Applications), xattrs and hidden files +ditto "$MOUNT" "$STAGE" +hdiutil detach "$MOUNT" -quiet + +APP="$(find "$STAGE" -maxdepth 1 -name '*.app' -type d | head -n 1)" +[[ -n "$APP" ]] || { echo "No .app found in dmg" >&2; exit 1; } +echo "==> Found app: $(basename "$APP")" + +# Remove any existing (possibly broken) signatures and quarantine flags +xattr -cr "$APP" || true +find "$APP" -name '_CodeSignature' -type d -prune -exec rm -rf {} + 2>/dev/null || true + +echo "==> Signing nested binaries (identity: $IDENTITY)" +# Sign every Mach-O file (dylibs, jspawnhelper, java, launcher, ...) inside-out. +# Deepest paths first so nested code is signed before what contains it. +find "$APP" -type f -print0 \ + | while IFS= read -r -d '' f; do + if file -b "$f" | grep -q 'Mach-O'; then + printf '%s\n' "$f" + fi + done \ + | awk '{ print length($0) "\t" $0 }' | sort -rn | cut -f2- \ + | while IFS= read -r f; do + codesign --force --sign "$IDENTITY" --timestamp=none "$f" + done + +echo "==> Signing app bundle" +codesign --force --sign "$IDENTITY" --timestamp=none "$APP" + +echo "==> Verifying" +codesign --verify --deep --strict --verbose=2 "$APP" +codesign -dvv "$APP" 2>&1 | grep -E 'Signature|Identifier' || true + +echo "==> Building dmg: $OUT_DMG" +TMP_DMG="$WORK/out.dmg" +hdiutil create -volname "$VOLNAME" -srcfolder "$STAGE" -ov -format UDZO -quiet "$TMP_DMG" +mv -f "$TMP_DMG" "$OUT_DMG" + +echo "Done: $OUT_DMG"