diff --git a/.github/actions/import-macos-cert/action.yml b/.github/actions/import-macos-cert/action.yml new file mode 100644 index 0000000000..e9ecb02761 --- /dev/null +++ b/.github/actions/import-macos-cert/action.yml @@ -0,0 +1,52 @@ +name: Import macOS Developer ID certificate +description: > + Import a Developer ID Application certificate (base64-encoded .p12) into a + throwaway keychain so codesign/jpackage can find it during the job. Soft: + when no certificate is supplied it is a no-op and reports signing=false, so + callers build UNSIGNED artifacts exactly as before. + +inputs: + certificate-p12-base64: + description: Base64 of the Developer ID Application .p12 (cert + private key) + required: true + certificate-password: + description: Password used when the .p12 was exported + required: true + +outputs: + signing: + description: "'true' if a certificate was imported, else 'false'" + value: ${{ steps.import.outputs.signing }} + +runs: + using: composite + steps: + - id: import + shell: bash + env: + CERT_P12: ${{ inputs.certificate-p12-base64 }} + CERT_PASSWORD: ${{ inputs.certificate-password }} + run: | + set -euo pipefail + if [[ -z "${CERT_P12:-}" ]]; then + echo "::notice::No macOS signing certificate configured — artifacts will be UNSIGNED." + echo "signing=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + KEYCHAIN="$RUNNER_TEMP/amethyst-signing.keychain-db" + KEYCHAIN_PWD="$(openssl rand -base64 24)" + CERT_PATH="$RUNNER_TEMP/developer_id.p12" + security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" + security set-keychain-settings -lut 21600 "$KEYCHAIN" + security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" + echo "$CERT_P12" | base64 --decode > "$CERT_PATH" + security import "$CERT_PATH" -P "$CERT_PASSWORD" \ + -k "$KEYCHAIN" -T /usr/bin/codesign -T /usr/bin/productsign + # Let codesign use the private key without an interactive UI prompt. + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$KEYCHAIN_PWD" "$KEYCHAIN" >/dev/null + # Prepend our keychain to the user search list so codesign sees it. + security list-keychains -d user -s "$KEYCHAIN" \ + $(security list-keychains -d user | sed -e 's/[\"[:space:]]//g') + rm -f "$CERT_PATH" + echo "signing=true" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 83547b56e1..e923e01ac2 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -101,8 +101,29 @@ jobs: fi chmod +x desktopApp/packaging/appimage/appimagetool-x86_64.AppImage + # macOS only: import the Developer ID Application cert into a throwaway + # keychain so jpackage's codesign pass can find it. Soft — if the + # MAC_CERTIFICATE_P12 secret isn't set (forks, or before Apple creds are + # provisioned) the DMG is built UNSIGNED, exactly as before. notarytool + # runs as part of the gradle task when the identity env is exported below. + - name: Import Apple Developer ID certificate (macOS leg, if configured) + if: matrix.family == 'macos' + id: mac_keychain + uses: ./.github/actions/import-macos-cert + with: + certificate-p12-base64: ${{ secrets.MAC_CERTIFICATE_P12 }} + certificate-password: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} + - name: Build desktop artifacts uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 + env: + # Empty on non-macOS legs and on the macOS leg when no cert is + # configured — the gradle macOS{} block skips signing when the + # identity is blank. + AMETHYST_MAC_SIGN_IDENTITY: ${{ steps.mac_keychain.outputs.signing == 'true' && secrets.MAC_SIGN_IDENTITY || '' }} + AMETHYST_NOTARY_APPLE_ID: ${{ secrets.MAC_NOTARY_APPLE_ID }} + AMETHYST_NOTARY_PASSWORD: ${{ secrets.MAC_NOTARY_PASSWORD }} + AMETHYST_NOTARY_TEAM_ID: ${{ secrets.MAC_NOTARY_TEAM_ID }} with: max_attempts: 2 timeout_minutes: 15 @@ -212,7 +233,7 @@ jobs: - { os: macos-14, arch: arm64, family: macos, tasks: "amyImage" } - { os: ubuntu-latest, arch: x64, family: linux, tasks: "amyImage jpackageDeb jpackageRpm" } runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 45 # macOS leg also codesigns + notarizes the jlink image defaults: run: shell: bash @@ -261,6 +282,91 @@ jobs: timeout_minutes: 15 command: ./gradlew --no-daemon :cli:${{ matrix.tasks }} + # amy is headless: the Compose UI render stack (skiko + its native dylibs, + # foundation/material/material3/ui/animation) must never reach the CLI + # image. cli/build.gradle.kts excludes it from runtimeClasspath; this + # guards against a transitive dep silently dragging it back (size + macOS + # notarization-surface regression). compose.runtime is CLI-safe and stays. + - name: Assert no Compose UI in the amy image + run: | + set -euo pipefail + LIB="cli/build/install/amy/lib" + leak="$(ls "$LIB" | grep -iE 'skiko|foundation(-layout)?-desktop|material3?-desktop|material-ripple|ui-desktop|animation(-core)?-desktop' || true)" + if [ -n "$leak" ]; then + echo "::error::Compose UI render stack leaked into the amy CLI image:" + echo "$leak" | sed 's/^/ /' + echo "Exclude it in cli/build.gradle.kts (configurations.runtimeClasspath)." + exit 1 + fi + echo "OK: no skiko / Compose UI render jars in the amy image ($(du -sh "$LIB" | cut -f1))." + + # macOS only: import the Developer ID cert (no-op without the secret) so + # the next step can codesign the jlink image. The jvm bundle for + # Homebrew-core is NOT signed here — Homebrew strips quarantine itself. + - name: Import Apple Developer ID certificate (macOS leg, if configured) + if: matrix.family == 'macos' + id: mac_keychain + uses: ./.github/actions/import-macos-cert + with: + certificate-p12-base64: ${{ secrets.MAC_CERTIFICATE_P12 }} + certificate-password: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} + + # Codesign + notarize the macOS jlink image (amy--macos-arm64.tar.gz) + # for users who download it directly. A loose tarball can't be stapled + # (stapler only does .app/.dmg/.pkg), so Gatekeeper verifies notarization + # online on first run. Runs before "Collect" so the tarred image is signed. + - name: Sign + notarize amy image (macOS leg, if configured) + if: matrix.family == 'macos' && steps.mac_keychain.outputs.signing == 'true' + env: + SIGN_IDENTITY: ${{ secrets.MAC_SIGN_IDENTITY }} + NOTARY_APPLE_ID: ${{ secrets.MAC_NOTARY_APPLE_ID }} + NOTARY_PASSWORD: ${{ secrets.MAC_NOTARY_PASSWORD }} + NOTARY_TEAM_ID: ${{ secrets.MAC_NOTARY_TEAM_ID }} + run: | + set -euo pipefail + IMG="cli/build/amy-image/amy" + ENTITLEMENTS="cli/packaging/macos/amy.entitlements" + # Sign every Mach-O binary in the bundled JRE. Each is signed + # independently (no enclosing .app seals them), so order is irrelevant. + # Executables get the hardened-runtime entitlements; dylibs don't. + while IFS= read -r f; do + case "$(file -b "$f")" in + *Mach-O*executable*) + codesign --force --options runtime --timestamp \ + --entitlements "$ENTITLEMENTS" --sign "$SIGN_IDENTITY" "$f" ;; + *Mach-O*) + codesign --force --options runtime --timestamp \ + --sign "$SIGN_IDENTITY" "$f" ;; + esac + done < <(find "$IMG" -type f) + codesign --verify --strict --verbose=2 "$IMG/runtime/bin/java" + # Notarize: zip the signed image, submit, wait for Apple's verdict. + # The notary service recursively inspects the lib/*.jar files, so any + # unsigned Mach-O embedded in them (secp256k1/jna/sqlite/skiko natives) + # can come back Invalid. Surface the per-file log so the first real run + # is diagnostic rather than a bare failure. + ZIP="$RUNNER_TEMP/amy-notarize.zip" + OUT="$RUNNER_TEMP/notary-submit.json" + ditto -c -k --keepParent "$IMG" "$ZIP" + if ! xcrun notarytool submit "$ZIP" \ + --apple-id "$NOTARY_APPLE_ID" --password "$NOTARY_PASSWORD" \ + --team-id "$NOTARY_TEAM_ID" --wait --output-format json > "$OUT"; then + echo "::warning::notarytool submit exited non-zero" + fi + cat "$OUT" + STATUS="$(jq -r '.status // "Unknown"' "$OUT" 2>/dev/null || echo Unknown)" + SUBMISSION_ID="$(jq -r '.id // empty' "$OUT" 2>/dev/null || true)" + if [ "$STATUS" != "Accepted" ]; then + echo "::error::Notarization status: $STATUS" + if [ -n "$SUBMISSION_ID" ]; then + echo "----- notary log -----" + xcrun notarytool log "$SUBMISSION_ID" \ + --apple-id "$NOTARY_APPLE_ID" --password "$NOTARY_PASSWORD" \ + --team-id "$NOTARY_TEAM_ID" || true + fi + exit 1 + fi + # jpackage pins libicu to the build host's version (libicu74 on # ubuntu-24.04). Rewrite the .deb so it installs across Debian/Ubuntu. - name: Relax libicu dependency in .deb @@ -277,6 +383,23 @@ jobs: source scripts/asset-name.sh collect_cli_assets "${{ matrix.family }}" "${{ matrix.arch }}" "${{ steps.ver.outputs.version }}" dist + # Homebrew-core ships a no-JRE jar bundle and depends_on "openjdk" — it + # cannot use the jlink tarball above (bundled runtime) nor build from + # source (its sandbox blocks Gradle's Maven downloads). installDist + # (bin/amy + lib/*.jar, no runtime/) is exactly that bundle. It is pure + # JVM bytecode, so one platform-independent asset serves every OS; we cut + # it on the linux leg only. amyImage depends on installDist, so the + # cli/build/install/amy tree already exists here. + - name: Package no-JRE jvm bundle for Homebrew (linux leg only) + if: matrix.family == 'linux' + run: | + set -euo pipefail + VER="${{ steps.ver.outputs.version }}" + SRC="cli/build/install/amy" + test -x "$SRC/bin/amy" + ( cd "$SRC" && tar czf "$OLDPWD/dist/amy-${VER}-jvm.tar.gz" bin lib ) + echo "Collected: dist/amy-${VER}-jvm.tar.gz" + - name: Enforce CLI size budget (200 MB per asset) run: | set -euo pipefail diff --git a/BUILDING.md b/BUILDING.md index 941bf4b3f1..3363d10b46 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -250,13 +250,58 @@ provided automatically; everything else you set yourself.) | `SONATYPE_PASSWORD` | Maven Central user token password | Same | | `SIGNING_PRIVATE_KEY` | **GPG/PGP** private key, ASCII-armored | Signs the Maven artifacts (Central requires it) | | `SIGNING_PASSWORD` | Passphrase for that GPG key | Same | +| `MAC_CERTIFICATE_P12` | Base64 of your **Apple Developer ID Application** cert (`.p12`, includes the private key) | Signs the macOS desktop **DMG** and the macOS **amy** jlink tarball | +| `MAC_CERTIFICATE_PASSWORD` | Password set when exporting the `.p12` | Imports the cert into the CI keychain | +| `MAC_SIGN_IDENTITY` | Full identity string, e.g. `Developer ID Application: Your Name (TEAMID)` | The `codesign` identity to sign with | +| `MAC_NOTARY_APPLE_ID` | Apple ID email of the notarization account | Apple notarization (`notarytool`) | +| `MAC_NOTARY_PASSWORD` | **App-specific** password for that Apple ID (not the login password) | Same | +| `MAC_NOTARY_TEAM_ID` | 10-char Apple Developer **Team ID** | Same | | `HOMEBREW_TOKEN` | PAT for `Homebrew/homebrew-cask` | Desktop cask bump (stable tags) | | `WINGET_TOKEN` | PAT for `microsoft/winget-pkgs` | Desktop winget bump (stable tags) | | `CROWDIN_PERSONAL_TOKEN`, `CROWDIN_PROJECT_ID` | Crowdin API creds | Translation sync (separate workflow, not the release) | -Note the **two distinct signing identities** people often conflate: +Note the **three distinct signing identities** people often conflate: `SIGNING_KEY` + `KEY_*` is the **Android keystore**; `SIGNING_PRIVATE_KEY` + -`SIGNING_PASSWORD` is the **GPG key** for Maven Central. They are unrelated. +`SIGNING_PASSWORD` is the **GPG key** for Maven Central; `MAC_CERTIFICATE_*` + +`MAC_SIGN_IDENTITY` + `MAC_NOTARY_*` is the **Apple Developer ID** for the macOS +desktop DMG. They are unrelated — each comes from a different authority. + +The macOS signing secrets are **optional**: if `MAC_CERTIFICATE_P12` is unset +the release workflow still builds the DMG **and** the macOS `amy` tarball, just +**unsigned** (the previous behavior). Provision all six to switch signing + +notarization on for both. Obtaining them requires Apple Developer Program +membership ($99/yr). The same one certificate signs both artifacts. + +The macOS `amy` tarball is the jlink image (bundled JRE), so signing it means +codesigning every Mach-O binary in that runtime with hardened-runtime +entitlements (`cli/packaging/macos/amy.entitlements` — needed so the JVM can +load the secp256k1 native library it extracts at runtime). A loose `.tar.gz` +cannot be **stapled** (Apple's `stapler` only handles `.app`/`.dmg`/`.pkg`), so +Gatekeeper verifies notarization **online** on first run — fine for a CLI. +Note the Homebrew-core jvm bundle (`amy--jvm.tar.gz`) is **not** signed: +Homebrew removes the quarantine attribute on its own downloads. + +> **Validated (Developer ID `D77MCV9NZ7`):** signing every Mach-O in the bundled +> JRE with hardened runtime + `amy.entitlements` lets `amy init` derive a key via +> secp256k1 with no library-validation crash. Dropping `disable-library-validation` +> reproduces `UnsatisfiedLinkError: … different Team IDs` on the runtime-extracted +> `libsecp256k1-jni.dylib` — so that entitlement is load-bearing, not decorative. +> +> **Open risk — embedded jar natives.** The notary service unpacks `lib/*.jar` +> recursively and checks every Mach-O for a signature + hardened runtime. Our +> sign loop only touches loose files, so 9 unsigned natives ride along inside +> jars on a macOS build: `secp256k1` (1, required at runtime), `jna` (2), +> `sqlite` (2), and `skiko` (4, dead weight — Compose UI the CLI never renders). +> Whether `notarytool` returns `Accepted` or `Invalid` on these is **unverified** +> (the local validation had no notary creds). **Decide it with one run:** set the +> six `MAC_*` secrets and trigger `create-release.yml` via `workflow_dispatch` +> with `dry_run=true` — the sign+notarize step runs regardless of `dry_run` and +> now prints the per-file notary log on a non-`Accepted` verdict. If it comes +> back `Invalid`, the fix is to codesign the dylibs *inside* those jars before +> zipping (and/or strip the unused `skiko`/Compose jars from the CLI image — the +> `:commons` core/ui split the size budget already flags). The **desktop** app +> bundles the same jars through Compose/jpackage notarization, so run a desktop +> dry-run too; its in-jar handling differs and is likewise unverified. Generating the values: @@ -269,6 +314,14 @@ base64 -i upload.jks | tr -d '\n' # paste output into SIGNING_KEY # GPG key → armored private key for SIGNING_PRIVATE_KEY gpg --full-generate-key # create the key (once) gpg --armor --export-secret-keys # paste output into SIGNING_PRIVATE_KEY + +# Apple Developer ID Application cert → base64 for MAC_CERTIFICATE_P12. +# In Keychain Access, export the "Developer ID Application: ..." cert (with its +# private key) as a .p12, setting an export password (-> MAC_CERTIFICATE_PASSWORD). +base64 -i developer_id.p12 | tr -d '\n' # paste output into MAC_CERTIFICATE_P12 +security find-identity -v -p codesigning # shows the exact MAC_SIGN_IDENTITY string +# MAC_NOTARY_PASSWORD is an app-specific password from https://appleid.apple.com +# (Sign-In and Security -> App-Specific Passwords), NOT your Apple ID login. ``` `SONATYPE_USERNAME`/`SONATYPE_PASSWORD` are a **user token** from @@ -339,6 +392,51 @@ The cask filename is `amethyst-nostr` (not `amethyst` — that's taken by a tiling window manager). After the first PR is merged, `bump-homebrew.yml` auto-submits new version bumps on each stable release. +> **The desktop app is already on mainline Homebrew.** `homebrew/cask` *is* the +> mainline cask repo — GUI apps live in homebrew-**cask**, CLIs in +> homebrew-**core**; both are "mainline." A private tap is only the *fallback* +> if Homebrew ever rejects the (now signed + notarized) cask. + +### Homebrew-core formula for the `amy` CLI (one-time initial PR) + +The CLI goes to **homebrew-core** (mainline formulae), not homebrew-cask — +casks are for GUI apps. homebrew-core builds in a **network-sandboxed** +environment, so a from-source Gradle build can't resolve its Maven +dependencies there. Instead the formula downloads the pre-built **no-JRE jar +bundle** `amy--jvm.tar.gz` (published by `create-release.yml`) and +`depends_on "openjdk"`. The reference formula lives at +[`cli/packaging/homebrew/amy.rb`](cli/packaging/homebrew/amy.rb). + +To submit: + +```bash +# 1. Grab the published asset's sha256 +curl -fsSL -o amy-jvm.tar.gz \ + https://github.com/vitorpamplona/amethyst/releases/download/v1.12.1/amy-1.12.1-jvm.tar.gz +shasum -a 256 amy-jvm.tar.gz + +# 2. Fill the url + sha256 into cli/packaging/homebrew/amy.rb, then open the PR +brew create --set-name amy --tap homebrew/core \ + https://github.com/vitorpamplona/amethyst/releases/download/v1.12.1/amy-1.12.1-jvm.tar.gz +# (paste the reference formula body, run `brew audit --new amy`, +# `brew install --build-from-source amy`, `brew test amy`, then PR it.) +``` + +Caveats that the maintainer must weigh before submitting: + +- **Name collision.** `amy` may already exist in homebrew-core — check with + `brew search amy` first. If taken, fall back to `amethyst-cli`. +- **Pre-built-jar scrutiny.** homebrew-core prefers source builds; downloading + a jar bundle is an accepted-but-reviewed pattern for JVM tools. Be ready to + justify it (sandboxed Gradle can't fetch Maven deps). +- **Bundle size.** The bundle is ~70 MB today because `:commons` leaks + Compose/Skiko jars onto the CLI classpath. Trimming that (a `:commons` + core/ui split) would shrink it and smooth review — tracked as a follow-up. + +After the formula merges, the `livecheck` block lets homebrew-core's BrewTestBot +auto-open version-bump PRs on each stable release — no token or workflow on our +side (unlike the cask/winget bumps). + ### Winget (one-time initial submission) ```bash @@ -480,9 +578,17 @@ for the deprecation date. When it hits: Homebrew has committed to disabling unsigned casks in `Homebrew/homebrew-cask` on 2026-09-01. Before that date: -**Option A**: Commit budget to Apple Developer Program ($99/yr), add -`signing { sign.set(true) }` + `notarization {}` blocks to -`desktopApp/build.gradle.kts`, wire Developer ID + notary creds into CI. +**Option A (wiring done — needs Apple creds)**: The `signing { sign.set(true) }` ++ `notarization {}` blocks are already in `desktopApp/build.gradle.kts` (gated on +the `AMETHYST_MAC_SIGN_IDENTITY` env var), and the macOS leg of +`create-release.yml` imports a Developer ID cert into a throwaway keychain and +exports the signing/notary env. It all stays a **no-op until the six +`MAC_*`/notary secrets are provisioned** (see [§ Secrets the CI +needs](#secrets-the-ci-needs)) — until then the DMG builds unsigned. To turn it +on: join the Apple Developer Program ($99/yr), create a *Developer ID +Application* certificate, generate an app-specific password, and set the six +secrets. The first signed+notarized DMG is best validated with a +`workflow_dispatch` dry-run before a real tag. **Option B**: Pivot to a private Homebrew tap: diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index 9296e49d5b..5fdf00d2fd 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -30,6 +30,28 @@ dependencies { implementation(libs.slf4j.nop) } +// amy is headless. It compiles against zero Compose UI (the Compose deps are +// `implementation` in :commons, so they never reach the CLI compile classpath — +// verified), yet they still ride the *runtime* classpath into the shipped image: +// ~29 MB of Compose desktop render stack, including skiko's native .dylibs that +// needlessly enlarge the macOS notarization surface. None of it is reachable +// from a CLI command, so drop the whole UI layer from the runtime image. Keep +// `androidx.compose.runtime` — snapshot state + the @Stable/@Immutable +// annotations ARE CLI-safe and used by commons models/state holders (see +// commons/ARCHITECTURE.md §1). A regression here is caught by the "no Compose UI +// in the amy image" assertion in .github/workflows/create-release.yml. +configurations.named("runtimeClasspath") { + // skiko (native renderer + its .dylibs) and the Compose UI bytecode layer. + // NB: Compose Multiplatform publishes UI under org.jetbrains.compose.* — only + // `runtime` relocates to androidx.compose.runtime, which we deliberately keep. + exclude(group = "org.jetbrains.skiko") + exclude(group = "org.jetbrains.compose.ui") + exclude(group = "org.jetbrains.compose.foundation") + exclude(group = "org.jetbrains.compose.material") + exclude(group = "org.jetbrains.compose.material3") + exclude(group = "org.jetbrains.compose.animation") +} + application { mainClass.set("com.vitorpamplona.amethyst.cli.MainKt") applicationName = "amy" diff --git a/cli/packaging/homebrew/amy.rb b/cli/packaging/homebrew/amy.rb new file mode 100644 index 0000000000..dc1526a931 --- /dev/null +++ b/cli/packaging/homebrew/amy.rb @@ -0,0 +1,47 @@ +# Reference Homebrew formula for `amy`, the Amethyst CLI. +# +# This file is NOT consumed by any build in this repo. It is the artifact you +# submit to Homebrew/homebrew-core (`brew bump-formula-pr` / a new-formula PR). +# Once accepted, homebrew-core's copy is the source of truth; keep this in sync +# for reference and to make version bumps a copy-paste. +# +# Why a pre-built jar bundle instead of building from source: +# homebrew-core builds inside a network sandbox, so a Gradle build cannot +# resolve its Maven dependencies there. The accepted pattern for JVM tools is +# to download a pre-built, no-JRE jar bundle and depend on the system openjdk. +# We publish exactly that as `amy--jvm.tar.gz` (bin/amy + lib/*.jar, +# no bundled runtime) from .github/workflows/create-release.yml. +# +# Before submitting: replace the version in the url and the sha256 with the +# values for the actual published release asset: +# curl -fsSL -o amy-jvm.tar.gz \ +# https://github.com/vitorpamplona/amethyst/releases/download/vX.Y.Z/amy-X.Y.Z-jvm.tar.gz +# shasum -a 256 amy-jvm.tar.gz +class Amy < Formula + desc "Command-line Nostr client from the Amethyst project" + homepage "https://github.com/vitorpamplona/amethyst" + url "https://github.com/vitorpamplona/amethyst/releases/download/v1.12.1/amy-1.12.1-jvm.tar.gz" + sha256 "REPLACE_WITH_RELEASE_ASSET_SHA256" + license "MIT" + + # Lets homebrew-core's BrewTestBot auto-open version-bump PRs when a new + # stable GitHub release appears. + livecheck do + url :stable + strategy :github_latest + end + + depends_on "openjdk" + + def install + # Tarball top level is bin/ and lib/ (the Gradle installDist layout). + libexec.install Dir["*"] + # Wrapper on PATH that pins JAVA_HOME to Homebrew's openjdk so amy runs + # regardless of the user's own Java setup. + (bin/"amy").write_env_script libexec/"bin/amy", JAVA_HOME: Formula["openjdk"].opt_prefix + end + + test do + assert_match "Amethyst command-line interface", shell_output("#{bin}/amy --help 2>&1") + end +end diff --git a/cli/packaging/macos/amy.entitlements b/cli/packaging/macos/amy.entitlements new file mode 100644 index 0000000000..08352a00a9 --- /dev/null +++ b/cli/packaging/macos/amy.entitlements @@ -0,0 +1,26 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-dyld-environment-variables + + + diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 730ab5c59f..d8cf817781 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -135,6 +135,36 @@ compose.desktop { macOS { bundleID = "com.vitorpamplona.amethyst.desktop" iconFile.set(project.file("src/jvmMain/resources/icon.icns")) + + // --- Developer ID code signing + notarization --- + // Required for Homebrew's main cask (unsigned casks rejected after + // 2026-09-01) and to clear macOS Gatekeeper without the right-click + // dance. Gated on the signing-identity env var so local dev builds + // and PR CI keep producing plain UNSIGNED DMGs exactly as before — + // signing only kicks in when the release workflow exports these + // (which it does only when the Apple secrets are present): + // + // AMETHYST_MAC_SIGN_IDENTITY "Developer ID Application: NAME (TEAMID)" + // AMETHYST_NOTARY_APPLE_ID Apple ID email of the notary account + // AMETHYST_NOTARY_PASSWORD app-specific password for that Apple ID + // AMETHYST_NOTARY_TEAM_ID 10-char Apple Developer Team ID + // + // The Developer ID Application certificate must already be in the + // build host's keychain (CI imports it from a base64 .p12 secret). + // Compose ships default hardened-runtime entitlements that permit + // the JVM's JIT, so no custom entitlements file is needed. + val macSignIdentity = System.getenv("AMETHYST_MAC_SIGN_IDENTITY") + if (!macSignIdentity.isNullOrBlank()) { + signing { + sign.set(true) + identity.set(macSignIdentity) + } + notarization { + appleID.set(System.getenv("AMETHYST_NOTARY_APPLE_ID")) + password.set(System.getenv("AMETHYST_NOTARY_PASSWORD")) + teamID.set(System.getenv("AMETHYST_NOTARY_TEAM_ID")) + } + } } windows { diff --git a/scripts/asset-name.sh b/scripts/asset-name.sh index aac7b3147f..6e82288085 100755 --- a/scripts/asset-name.sh +++ b/scripts/asset-name.sh @@ -32,6 +32,13 @@ # amy-1.08.0-linux-x64.tar.gz # amy-1.08.0-linux-x64.deb # amy-1.08.0-linux-x64.rpm +# +# One CLI asset breaks the family/arch shape on purpose: the no-JRE jar bundle +# for Homebrew-core is pure JVM bytecode (no bundled runtime), so a single +# platform-independent artifact serves every OS: +# amy-1.08.0-jvm.tar.gz +# It is packaged inline in create-release.yml (linux leg), not via the helpers +# below, since it has no / dimension. set -euo pipefail