From cf1fd92c6d8a6faf572c378155aeb6fcacc0564b Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 27 Oct 2026 03:41:20 -0500 Subject: [PATCH] fix(desktop): harden multi-platform packaging and macOS notarization --- mateclaw-desktop/README.md | 12 + mateclaw-desktop/electron-builder.cjs | 16 +- .../__tests__/sign-jar-natives.test.cjs | 66 ++++++ mateclaw-desktop/scripts/after-pack.cjs | 28 +++ .../scripts/build-all-platforms.sh | 131 +++++++++-- mateclaw-desktop/scripts/build.sh | 80 +++++-- mateclaw-desktop/scripts/download-jre.sh | 206 ++++++++++++------ mateclaw-desktop/scripts/publish-github.sh | 35 ++- mateclaw-desktop/scripts/sign-jar-natives.cjs | 68 ++++++ mateclaw-desktop/vite.config.ts | 2 +- mateclaw-ui/pnpm-workspace.yaml | 2 + 11 files changed, 529 insertions(+), 117 deletions(-) create mode 100644 mateclaw-desktop/scripts/__tests__/sign-jar-natives.test.cjs create mode 100644 mateclaw-desktop/scripts/after-pack.cjs create mode 100644 mateclaw-desktop/scripts/sign-jar-natives.cjs diff --git a/mateclaw-desktop/README.md b/mateclaw-desktop/README.md index bbdd697a..f05c8024 100644 --- a/mateclaw-desktop/README.md +++ b/mateclaw-desktop/README.md @@ -202,6 +202,18 @@ bash scripts/build-all-platforms.sh --mac-only --publish=always `electron-builder` 会自动完成签名 → 公证 → 装订(staple)→ 上传。 +本地完整版的 `afterPack` 会先裁剪 Playwright,再递归签署 `app.jar` 内的 macOS +原生二进制(包括依赖 JAR 中的 Intel/ARM 动态库和 Playwright Node)。该步骤复用 +electron-builder 发现的 Developer ID Application 证书及钥匙串,并启用可信时间戳。 +修改后的依赖 JAR 会去掉失效的上游 JAR 签名文件,Spring Boot 嵌套依赖保持 +`STORED` 格式;外层 App 签名在这些修改完成后执行。远程轻量版跳过 JAR 处理。 + +若公证日志出现 `app.jar/BOOT-INF/lib/...` 下的 `The binary is not signed`、 +`not signed with a valid Developer ID certificate` 或缺少 secure timestamp, +请确认打包日志包含 `[sign-jar-natives] Signed and verified ...` 并重新打包。 +仅重签外层 App 无法补签嵌套 JAR 中的二进制。日志显示 +`skipped macOS notarization` 时仅表示未执行公证,不能视为公证通过。 + > **注意**:不要设置 `CSC_LINK` 环境变量,否则 electron-builder 会创建临时钥匙串,可能导致签名卡死。详见 [CODESIGNING.md](./CODESIGNING.md) 故障排查章节。 ### CI/CD 签名构建 diff --git a/mateclaw-desktop/electron-builder.cjs b/mateclaw-desktop/electron-builder.cjs index cbc2aba6..f1aff9fb 100644 --- a/mateclaw-desktop/electron-builder.cjs +++ b/mateclaw-desktop/electron-builder.cjs @@ -26,8 +26,13 @@ const { loadBrandConfig } = require('./scripts/branding.cjs') const mode = process.env.BUILD_MODE === 'remote' ? 'remote' : 'local' +const buildArch = process.env.BUILD_ARCH const brand = loadBrandConfig(__dirname) +function archList(defaultArchs) { + return defaultArchs.includes(buildArch) ? [buildArch] : defaultArchs +} + // Derive a short slug from the brand name for artifact file names. // "MyAI" → "MyAI", "Cool App" → "Cool_App" const brandSlug = brand.name.replace(/\s+/g, '_') @@ -55,7 +60,7 @@ const config = { }, ], files: ['dist-electron', 'dist'], - afterPack: 'scripts/trim-playwright-driver.cjs', + afterPack: 'scripts/after-pack.cjs', // extraResources: only bundle JRE + JAR in local mode. // In remote mode this array is empty — the packaged app contains only the @@ -78,8 +83,8 @@ const config = { mac: { category: 'public.app-category.productivity', target: [ - { target: 'dmg', arch: ['arm64', 'x64'] }, - { target: 'zip', arch: ['arm64', 'x64'] }, + { target: 'dmg', arch: archList(['arm64', 'x64']) }, + { target: 'zip', arch: archList(['arm64', 'x64']) }, ], icon: 'build/icon.icns', hardenedRuntime: true, @@ -102,10 +107,7 @@ const config = { }, win: { - target: [ - { target: 'nsis', arch: 'x64' }, - { target: 'nsis', arch: 'arm64' }, - ], + target: archList(['x64', 'arm64']).map((arch) => ({ target: 'nsis', arch })), icon: 'build/icon.ico', artifactName: mode === 'remote' diff --git a/mateclaw-desktop/scripts/__tests__/sign-jar-natives.test.cjs b/mateclaw-desktop/scripts/__tests__/sign-jar-natives.test.cjs new file mode 100644 index 00000000..91fb759f --- /dev/null +++ b/mateclaw-desktop/scripts/__tests__/sign-jar-natives.test.cjs @@ -0,0 +1,66 @@ +const assert = require('node:assert/strict') +const test = require('node:test') +const { isMachO, rewriteArchive } = require('../sign-jar-natives.cjs') +const { createZipBuffer, readZipEntries, ZIP_STORED, ZIP_DEFLATED } = require('../trim-playwright-driver.cjs') + +const native = Buffer.from('cffaedfe00000001', 'hex') +const entry = (name, data, method = ZIP_DEFLATED) => ({ name, data: Buffer.from(data), method }) + +test('detects thin and universal Mach-O without mistaking Java classes for binaries', () => { + for (const magic of ['feedface', 'cefaedfe', 'feedfacf', 'cffaedfe']) { + assert.equal(isMachO(Buffer.from(`${magic}00000001`, 'hex')), true) + } + assert.equal(isMachO(Buffer.from('cafebabe00000002', 'hex')), true) + assert.equal(isMachO(Buffer.from('bebafeca02000000', 'hex')), true) + assert.equal(isMachO(Buffer.from('cafebabe00000041', 'hex')), false) + assert.equal(isMachO(Buffer.from('MZ')), false) +}) + +test('signs all architectures and extensionless executables, preserves Spring Boot storage and removes stale JAR signatures', async () => { + const dependency = createZipBuffer([ + entry('META-INF/MANIFEST.MF', 'Manifest-Version: 1.0\n'), + entry('META-INF/VENDOR.SF', 'old digest'), + entry('META-INF/VENDOR.RSA', 'old signature'), + entry('mac/arm64/lib.dylib', native), + entry('mac/x64/lib.jnilib', native), + entry('driver/mac/node', native), + entry('Example.class', Buffer.from('cafebabe00000041', 'hex')), + entry('linux/lib.so', 'ELF'), + ]) + const untouched = createZipBuffer([entry('hello.txt', 'unchanged')]) + const app = createZipBuffer([ + entry('BOOT-INF/lib/native.jar', dependency, ZIP_STORED), + entry('BOOT-INF/lib/other.jar', untouched, ZIP_STORED), + ]) + const signed = [] + const result = await rewriteArchive(app, async (data, name) => { + signed.push(name) + return Buffer.concat([data, Buffer.from('signed')]) + }) + assert.equal(result.count, 3) + assert.equal(signed.length, 3) + const outer = readZipEntries(result.buffer) + assert.equal(outer[0].method, ZIP_STORED) + assert.deepEqual(outer[1].data, untouched) + const inner = readZipEntries(outer[0].data) + assert.equal(inner.some(e => /\.(SF|RSA)$/.test(e.name)), false) + assert.equal(inner.find(e => e.name === 'linux/lib.so').data.toString(), 'ELF') + assert.equal(inner.find(e => e.name === 'mac/x64/lib.jnilib').data.subarray(-6).toString(), 'signed') +}) + +test('signing errors propagate without producing an archive', async () => { + await assert.rejects(rewriteArchive(createZipBuffer([entry('lib.dylib', native)]), async () => { + throw new Error('timestamp unavailable') + }), /timestamp unavailable/) +}) + +test('remote builds skip local JAR processing', async () => { + const previous = process.env.BUILD_MODE + process.env.BUILD_MODE = 'remote' + try { + await require('../after-pack.cjs')({}) + } finally { + if (previous === undefined) delete process.env.BUILD_MODE + else process.env.BUILD_MODE = previous + } +}) diff --git a/mateclaw-desktop/scripts/after-pack.cjs b/mateclaw-desktop/scripts/after-pack.cjs new file mode 100644 index 00000000..777582fc --- /dev/null +++ b/mateclaw-desktop/scripts/after-pack.cjs @@ -0,0 +1,28 @@ +const trimDriver = require('./trim-playwright-driver.cjs') +const { signAppJar } = require('./sign-jar-natives.cjs') +const path = require('node:path') + +module.exports = async function afterPack(context) { + if (process.env.BUILD_MODE === 'remote') return + await trimDriver(context) + if (context.electronPlatformName !== 'darwin') return + + // Use electron-builder's identity discovery, including its temporary CI keychain. + const { findIdentity, isSignAllowed } = require('app-builder-lib/out/codeSign/macCodeSign') + const options = context.packager.platformSpecificBuildOptions + if (!isSignAllowed(false) || options.identity === null) return + const { keychainFile } = await context.packager.codeSigningInfo.value + const identity = await findIdentity('Developer ID Application', options.identity, keychainFile) + if (!identity) { + if (context.packager.forceCodeSigning) throw new Error('No Developer ID Application identity for JAR signing') + console.log('[sign-jar-natives] No Developer ID Application identity; skipping nested signing') + return + } + const appJar = path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`, 'Contents', 'Resources', 'app.jar') + const count = await signAppJar(appJar, { + identity: identity.hash || identity.name, + keychain: keychainFile, + entitlements: path.join(context.packager.projectDir, 'build/entitlements.mac.inherit.plist'), + }) + console.log(`[sign-jar-natives] Signed and verified ${count} Mach-O binaries inside app.jar`) +} diff --git a/mateclaw-desktop/scripts/build-all-platforms.sh b/mateclaw-desktop/scripts/build-all-platforms.sh index 14f388eb..03cbb901 100755 --- a/mateclaw-desktop/scripts/build-all-platforms.sh +++ b/mateclaw-desktop/scripts/build-all-platforms.sh @@ -1,35 +1,122 @@ #!/usr/bin/env bash # -# scripts/build-all-platforms.sh — Build MateClaw desktop packages for all -# platforms (macOS + Windows) in the specified build mode. +# Build MateClaw Desktop packages. +# This script: +# 1. Builds backend JAR once +# 2. Downloads/verifies JREs for selected platforms +# 3. Clears stale frontend/package output +# 4. Builds the Electron/Vue frontend +# 5. Packages macOS and/or Windows installers # # Usage: -# scripts/build-all-platforms.sh --all # local mode (default), both platforms -# scripts/build-all-platforms.sh --local # local mode, both platforms -# scripts/build-all-platforms.sh --remote # remote mode, both platforms +# bash scripts/build-all-platforms.sh --all +# bash scripts/build-all-platforms.sh --mac-only +# bash scripts/build-all-platforms.sh --win-only +# bash scripts/build-all-platforms.sh --all --publish=always +# BUILD_MODE=remote bash scripts/build-all-platforms.sh --mac-only # + set -euo pipefail -MODE="--all" -case "${1:-}" in - --all) MODE="local" ;; - --local) MODE="local" ;; - --remote) MODE="remote" ;; - *) echo "Usage: $0 [--all|--local|--remote]"; exit 1 ;; +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DESKTOP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +BUILD_TYPE="all" +BUILD_MODE="${BUILD_MODE:-local}" +PUBLISH_FLAG="${PUBLISH_FLAG:---publish=never}" + +for arg in "$@"; do + case "$arg" in + --mac-only|mac) BUILD_TYPE="mac" ;; + --win-only|win) BUILD_TYPE="win" ;; + --all|all) BUILD_TYPE="all" ;; + --local|local) BUILD_MODE="local" ;; + --remote|remote) BUILD_MODE="remote" ;; + --publish=*) PUBLISH_FLAG="$arg" ;; + *) + echo "Usage: $0 [--mac-only|--win-only|--all|--local|--remote] [--publish=never|always|onTagOrDraft]" + exit 1 + ;; + esac +done + +case "$BUILD_TYPE" in + mac) PLATFORMS=("mac-arm64" "mac-x64") ;; + win) PLATFORMS=("win-x64" "win-arm64") ;; + all) PLATFORMS=("mac-arm64" "mac-x64" "win-x64" "win-arm64") ;; esac -echo "==> Building all platforms, BUILD_MODE=$MODE" +echo "╔════════════════════════════════════════════════════════╗" +echo "║ MateClaw Desktop - Multi-Platform Build ║" +echo "╚════════════════════════════════════════════════════════╝" +echo "" +echo " Build mode: $BUILD_MODE" +echo " Publish: $PUBLISH_FLAG" +echo "" -if [ "$MODE" = "remote" ]; then - echo "==> macOS (remote/lite)" - BUILD_MODE=remote npx electron-builder --mac - echo "==> Windows (remote/lite)" - BUILD_MODE=remote npx electron-builder --win +cd "$DESKTOP_DIR" + +if [ "$BUILD_MODE" = "local" ]; then + echo "📦 [1/4] Building backend JAR..." + bash "$SCRIPT_DIR/build.sh" + echo "" + + echo "📥 [2/4] Downloading/verifying JREs..." + for platform in "${PLATFORMS[@]}"; do + echo " → $platform" + bash "$SCRIPT_DIR/download-jre.sh" "$platform" + done + echo "" else - echo "==> macOS (local/full)" - BUILD_MODE=local npx electron-builder --mac - echo "==> Windows (local/full)" - BUILD_MODE=local npx electron-builder --win + echo "📦 [1/4] Skipping backend JAR for remote/lite build." + echo "📥 [2/4] Skipping bundled JREs for remote/lite build." + echo "" fi -echo "==> All builds complete." +echo "🧹 [3/4] Clearing stale desktop build output..." +rm -rf "$DESKTOP_DIR/dist" "$DESKTOP_DIR/dist-electron" "$DESKTOP_DIR/release" +echo "" + +echo "📦 [4/4] Building Electron frontend and packaging..." +npm run build +echo "" + +echo " Packaging for:" +if [[ " ${PLATFORMS[*]} " =~ " mac-arm64 " ]] || [[ " ${PLATFORMS[*]} " =~ " mac-x64 " ]]; then + echo " • macOS (arm64 + x64)" + if [[ " ${PLATFORMS[*]} " =~ " mac-arm64 " ]]; then + BUILD_MODE="$BUILD_MODE" BUILD_ARCH=arm64 npx electron-builder --mac $PUBLISH_FLAG + fi + if [[ " ${PLATFORMS[*]} " =~ " mac-x64 " ]]; then + BUILD_MODE="$BUILD_MODE" BUILD_ARCH=x64 npx electron-builder --mac $PUBLISH_FLAG + fi +fi + +if [[ " ${PLATFORMS[*]} " =~ " win-x64 " ]] || [[ " ${PLATFORMS[*]} " =~ " win-arm64 " ]]; then + echo " • Windows (x64 + arm64)" + if [[ " ${PLATFORMS[*]} " =~ " win-x64 " ]]; then + BUILD_MODE="$BUILD_MODE" BUILD_ARCH=x64 npx electron-builder --win $PUBLISH_FLAG + fi + if [[ " ${PLATFORMS[*]} " =~ " win-arm64 " ]]; then + BUILD_MODE="$BUILD_MODE" BUILD_ARCH=arm64 npx electron-builder --win $PUBLISH_FLAG + fi +fi + +echo "" +echo "╔════════════════════════════════════════════════════════╗" +echo "║ Build Complete! ║" +echo "╚════════════════════════════════════════════════════════╝" +echo "" +echo " 📁 Output directory: $DESKTOP_DIR/release/" +echo "" +echo " Files:" +if [[ " ${PLATFORMS[*]} " =~ " mac-" ]]; then + ls -lh "$DESKTOP_DIR/release/"MateClaw*.dmg 2>/dev/null || true + ls -lh "$DESKTOP_DIR/release/"MateClaw*.zip 2>/dev/null || true + ls -lh "$DESKTOP_DIR/release/"latest-mac.yml 2>/dev/null || true +fi +if [[ " ${PLATFORMS[*]} " =~ " win-" ]]; then + ls -lh "$DESKTOP_DIR/release/"MateClaw*Setup*.exe 2>/dev/null || true + ls -lh "$DESKTOP_DIR/release/"latest.yml 2>/dev/null || true +fi +echo "" diff --git a/mateclaw-desktop/scripts/build.sh b/mateclaw-desktop/scripts/build.sh index 35d6ee73..12ce5230 100755 --- a/mateclaw-desktop/scripts/build.sh +++ b/mateclaw-desktop/scripts/build.sh @@ -1,30 +1,78 @@ #!/usr/bin/env bash # -# scripts/build.sh — Build the MateClaw Spring Boot backend JAR and place it -# at resources/app.jar so electron-builder can bundle it into the desktop app. +# Build the MateClaw backend JAR and place it at resources/app.jar so +# electron-builder can bundle it into the desktop app. # + set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -SERVER_DIR="$(cd "$PROJECT_ROOT/../mateclaw-server" && pwd)" -RESOURCES_DIR="$PROJECT_ROOT/resources" +DESKTOP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +PROJECT_ROOT="$(cd "$DESKTOP_DIR/.." && pwd)" +RESOURCES_DIR="$DESKTOP_DIR/resources" +UI_DIR="$PROJECT_ROOT/mateclaw-ui" -echo "==> Building mateclaw-server JAR from $SERVER_DIR" +echo "╔══════════════════════════════════════════════════╗" +echo "║ MateClaw Desktop - Build Script ║" +echo "╚══════════════════════════════════════════════════╝" +echo "" -# Build the Spring Boot fat JAR (skip tests for packaging speed) -cd "$SERVER_DIR" -mvn clean package -DskipTests -Dmaven.test.skip=true -q +echo "📦 Building MateClaw web UI..." -# Locate the built JAR -JAR_FILE=$(ls "$SERVER_DIR"/target/mateclaw-server-*.jar 2>/dev/null | head -1) -if [ -z "$JAR_FILE" ]; then - echo "ERROR: Could not find built JAR in $SERVER_DIR/target/" - exit 1 +if [ ! -d "$UI_DIR" ]; then + echo "❌ Frontend directory not found: $UI_DIR" + exit 1 fi -echo "==> Copying $JAR_FILE → $RESOURCES_DIR/app.jar" +cd "$UI_DIR" +if [ ! -d "node_modules" ]; then + echo " Installing frontend dependencies..." + if command -v pnpm &>/dev/null; then + pnpm install + elif command -v npm &>/dev/null; then + npm install + else + echo "❌ Neither pnpm nor npm found" + exit 1 + fi +fi + +if command -v pnpm &>/dev/null; then + pnpm build +else + npm run build +fi + +echo "" +echo "📦 Building Spring Boot JAR with Maven reactor..." + +cd "$PROJECT_ROOT" + +if command -v mvn &>/dev/null; then + MVN="mvn" +elif [ -f "./mvnw" ]; then + MVN="./mvnw" + chmod +x ./mvnw +else + echo "❌ Maven not found. Install Maven or use Maven wrapper." + exit 1 +fi + +$MVN -pl mateclaw-server -am clean package -DskipTests -Dmaven.test.skip=true -q + +JAR_FILE=$(find mateclaw-server/target -maxdepth 1 -type f -name 'mateclaw-server-*.jar' ! -name '*sources*' ! -name '*.original' | sort | tail -1) +if [ -z "$JAR_FILE" ]; then + echo "❌ JAR file not found in mateclaw-server/target/" + exit 1 +fi + +echo "✅ JAR built: $JAR_FILE" +echo "" + +echo "📦 Copying JAR to desktop resources..." mkdir -p "$RESOURCES_DIR" cp "$JAR_FILE" "$RESOURCES_DIR/app.jar" -echo "==> Done. JAR size: $(du -h "$RESOURCES_DIR/app.jar" | cut -f1)" +JAR_SIZE=$(du -h "$RESOURCES_DIR/app.jar" | cut -f1) +echo "✅ JAR copied to: $RESOURCES_DIR/app.jar ($JAR_SIZE)" +echo "" diff --git a/mateclaw-desktop/scripts/download-jre.sh b/mateclaw-desktop/scripts/download-jre.sh index 5fca8087..556df368 100755 --- a/mateclaw-desktop/scripts/download-jre.sh +++ b/mateclaw-desktop/scripts/download-jre.sh @@ -1,86 +1,156 @@ #!/usr/bin/env bash # -# scripts/download-jre.sh — Download Eclipse Temurin JRE 21 for the current -# macOS architecture (or both) and extract into resources/jre//. +# Download Adoptium JRE 21 for the requested platform. # # Usage: -# scripts/download-jre.sh # auto-detect current arch -# scripts/download-jre.sh arm64 # arm64 only -# scripts/download-jre.sh x64 # x64 only -# scripts/download-jre.sh all # both arches +# bash scripts/download-jre.sh [platform-arch] +# bash scripts/download-jre.sh mac-arm64 +# bash scripts/download-jre.sh mac-x64 +# bash scripts/download-jre.sh win-x64 +# bash scripts/download-jre.sh win-arm64 # + set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -JRE_DIR="$PROJECT_ROOT/resources/jre" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +RESOURCES_DIR="$PROJECT_DIR/resources" -# Temurin 21 (LTS) JRE downloads via Adoptium API -ADOPTIUM_BASE="https://api.adoptium.net/v3/binary/latest/21/ga/mac" +JRE_VERSION="21" +ADOPTIUM_API="https://api.adoptium.net/v3/binary/latest" -download_and_extract() { - local arch="$1" - local folder="$2" - local url="$ADOPTIUM_BASE/$arch/jre/hotspot/normal/eclipse?project=jdk" - local tmpfile="$JRE_DIR/jre-mac-$arch.tar.gz" +detect_platform() { + local os arch - echo "==> Downloading Temurin 21 JRE for macOS $arch" - mkdir -p "$JRE_DIR" - curl -L --fail -o "$tmpfile" "$url" + case "$(uname -s)" in + Darwin) os="mac" ;; + Linux) os="linux" ;; + MINGW*|MSYS*|CYGWIN*) os="win32" ;; + *) echo "Unsupported OS: $(uname -s)"; exit 1 ;; + esac - echo "==> Extracting to $JRE_DIR/$folder" - rm -rf "$JRE_DIR/$folder" - mkdir -p "$JRE_DIR/$folder" + case "$(uname -m)" in + x86_64|amd64) arch="x64" ;; + arm64|aarch64) arch="arm64" ;; + *) echo "Unsupported arch: $(uname -m)"; exit 1 ;; + esac - # Temurin macOS tar.gz extracts to: jdk-21.x.x+jre/Contents/Home/... - # We want $folder/Contents/Home/... so move the inner Contents up. - local extract_tmp="$JRE_DIR/.tmp-$arch" - rm -rf "$extract_tmp" - mkdir -p "$extract_tmp" - tar -xzf "$tmpfile" -C "$extract_tmp" - - # Find the extracted top-level directory and move its Contents - local extracted_dir - extracted_dir=$(find "$extract_tmp" -maxdepth 1 -type d -name "jdk-*" | head -1) - if [ -z "$extracted_dir" ]; then - # Fallback: some tarballs extract Contents directly - if [ -d "$extract_tmp/Contents" ]; then - mv "$extract_tmp/Contents" "$JRE_DIR/$folder/Contents" - else - echo "ERROR: Could not find extracted JDK directory" - exit 1 - fi - else - mv "$extracted_dir/Contents" "$JRE_DIR/$folder/Contents" - fi - - rm -rf "$extract_tmp" "$tmpfile" - - # Verify java binary exists - local java_bin="$JRE_DIR/$folder/Contents/Home/bin/java" - if [ -f "$java_bin" ]; then - echo "==> OK: $java_bin" - else - echo "ERROR: java binary not found at $java_bin" - exit 1 - fi + echo "${os}-${arch}" } -TARGET="${1:-auto}" -if [ "$TARGET" = "auto" ]; then - case "$(uname -m)" in - arm64) TARGET="arm64" ;; - x86_64) TARGET="x64" ;; - *) echo "Unsupported arch: $(uname -m)"; exit 1 ;; - esac -fi +PLATFORM="${1:-$(detect_platform)}" +echo "🔧 Platform: $PLATFORM" -case "$TARGET" in - arm64) download_and_extract "aarch64" "mac-arm64" ;; - x64) download_and_extract "x64" "mac-x64" ;; - all) download_and_extract "aarch64" "mac-arm64" - download_and_extract "x64" "mac-x64" ;; - *) echo "Usage: $0 [arm64|x64|all]"; exit 1 ;; +case "$PLATFORM" in + mac-arm64) + API_OS="mac" + API_ARCH="aarch64" + ARCHIVE_EXT="tar.gz" + ;; + mac-x64) + API_OS="mac" + API_ARCH="x64" + ARCHIVE_EXT="tar.gz" + ;; + win-x64|win32-x64) + API_OS="windows" + API_ARCH="x64" + ARCHIVE_EXT="zip" + ;; + win-arm64) + API_OS="windows" + API_ARCH="aarch64" + ARCHIVE_EXT="zip" + ;; + linux-x64) + API_OS="linux" + API_ARCH="x64" + ARCHIVE_EXT="tar.gz" + ;; + linux-arm64) + API_OS="linux" + API_ARCH="aarch64" + ARCHIVE_EXT="tar.gz" + ;; + *) + echo "❌ Unsupported platform: $PLATFORM" + exit 1 + ;; esac -echo "==> JRE setup complete." +TARGET_DIR="$RESOURCES_DIR/jre/$PLATFORM" + +case "$PLATFORM" in + mac-*) JRE_NATIVE_LIB="Contents/Home/lib/server/libjvm.dylib"; JAVA_REL="Contents/Home/bin/java" ;; + win-*|win32-*) JRE_NATIVE_LIB="bin/server/jvm.dll"; JAVA_REL="bin/java.exe" ;; + linux-*) JRE_NATIVE_LIB="lib/server/libjvm.so"; JAVA_REL="bin/java" ;; +esac + +if [ -d "$TARGET_DIR" ] && [ -f "$TARGET_DIR/$JRE_NATIVE_LIB" ]; then + echo "✅ JRE already exists and looks complete at $TARGET_DIR" + chmod -R u+w "$TARGET_DIR" + exit 0 +elif [ -d "$TARGET_DIR" ] && [ -n "$(ls -A "$TARGET_DIR" 2>/dev/null)" ]; then + echo "⚠️ Existing JRE at $TARGET_DIR is incomplete (missing $JRE_NATIVE_LIB) — re-downloading." + rm -rf "$TARGET_DIR" +fi + +mkdir -p "$TARGET_DIR" + +DOWNLOAD_URL="${ADOPTIUM_API}/${JRE_VERSION}/ga/${API_OS}/${API_ARCH}/jre/hotspot/normal/eclipse" + +echo "📥 Downloading JRE ${JRE_VERSION} from Adoptium..." +echo " URL: $DOWNLOAD_URL" + +TEMP_FILE="$(mktemp)" + +if command -v curl &>/dev/null; then + curl -fSL --progress-bar -o "$TEMP_FILE" "$DOWNLOAD_URL" +elif command -v wget &>/dev/null; then + wget -q --show-progress -O "$TEMP_FILE" "$DOWNLOAD_URL" +else + echo "❌ Neither curl nor wget found" + exit 1 +fi + +echo "📦 Extracting JRE..." + +if [ "$ARCHIVE_EXT" = "tar.gz" ]; then + tar -xzf "$TEMP_FILE" -C "$TARGET_DIR" --strip-components=1 +elif [ "$ARCHIVE_EXT" = "zip" ]; then + TEMP_EXTRACT="$(mktemp -d)" + unzip -q "$TEMP_FILE" -d "$TEMP_EXTRACT" + EXTRACTED_DIR=$(ls "$TEMP_EXTRACT") + mv "$TEMP_EXTRACT/$EXTRACTED_DIR"/* "$TARGET_DIR/" + rm -rf "$TEMP_EXTRACT" +fi + +rm -f "$TEMP_FILE" + +# codesign --force needs write permission on upstream read-only files. +chmod -R u+w "$TARGET_DIR" + +JAVA_BIN="$TARGET_DIR/$JAVA_REL" +chmod +x "$JAVA_BIN" 2>/dev/null || true + +if [ ! -f "$JAVA_BIN" ] || [ ! -f "$TARGET_DIR/$JRE_NATIVE_LIB" ]; then + echo "❌ JRE is incomplete — missing java launcher or native runtime ($JRE_NATIVE_LIB)" + echo " Removing $TARGET_DIR so the next run re-downloads." + rm -rf "$TARGET_DIR" + exit 1 +fi + +HOST_PLATFORM="$(detect_platform | sed 's/^win32-/win-/')" +NORM_PLATFORM="$(echo "$PLATFORM" | sed 's/^win32-/win-/')" +if [ "$NORM_PLATFORM" = "$HOST_PLATFORM" ]; then + if ! "$JAVA_BIN" -version >/dev/null 2>&1; then + echo "❌ JRE installed but 'java -version' failed — the runtime is broken" + "$JAVA_BIN" -version 2>&1 | head -5 || true + rm -rf "$TARGET_DIR" + exit 1 + fi +fi + +echo "✅ JRE installed successfully" +"$JAVA_BIN" -version 2>&1 | head -1 || true +echo "📁 JRE location: $TARGET_DIR" diff --git a/mateclaw-desktop/scripts/publish-github.sh b/mateclaw-desktop/scripts/publish-github.sh index 777db90a..4da1ef86 100755 --- a/mateclaw-desktop/scripts/publish-github.sh +++ b/mateclaw-desktop/scripts/publish-github.sh @@ -123,7 +123,11 @@ echo "" echo " Version: $VERSION" echo " Tag: $TAG" echo " Mode: ${DRAFT:-release}" -echo " Proxy: ${USE_PROXY:+${PROXY_ADDR}}${USE_PROXY:-off}" +if [ "$USE_PROXY" = true ]; then + echo " Proxy: $PROXY_ADDR" +else + echo " Proxy: off" +fi echo " Retry: $RETRY" echo "" @@ -275,19 +279,44 @@ if [ -f "$LATEST_YML" ]; then echo "🧹 Rewriting latest.yml to drop the merged installer ..." (cd "$DESKTOP_DIR" && node -e " const fs = require('fs'); + const path = require('path'); + const crypto = require('crypto'); const yaml = require('js-yaml'); const file = '$LATEST_YML'; + const releaseDir = '$RELEASE_DIR'; const merged = '$MERGED_EXE_NAME'; const x64 = '$X64_EXE_NAME'; const arm64 = '$ARM64_EXE_NAME'; const doc = yaml.load(fs.readFileSync(file, 'utf8')); const before = (doc.files || []).length; doc.files = (doc.files || []).filter(f => f.url !== merged); - const x64Entry = doc.files.find(f => f.url === x64); - const arm64Entry = doc.files.find(f => f.url === arm64); + + function sha512Base64(filePath) { + return crypto.createHash('sha512').update(fs.readFileSync(filePath)).digest('base64'); + } + + function ensureInstallerEntry(name) { + let entry = doc.files.find(f => f.url === name); + if (entry) return entry; + + const installerPath = path.join(releaseDir, name); + if (!fs.existsSync(installerPath)) return null; + + entry = { + url: name, + sha512: sha512Base64(installerPath), + size: fs.statSync(installerPath).size, + }; + doc.files.push(entry); + return entry; + } + + const x64Entry = ensureInstallerEntry(x64); + const arm64Entry = ensureInstallerEntry(arm64); if (!x64Entry || !arm64Entry) { console.error('FATAL: per-arch installer entries missing from latest.yml'); console.error(' files:', doc.files.map(f => f.url)); + console.error(' expected:', [x64, arm64]); process.exit(1); } doc.path = x64Entry.url; diff --git a/mateclaw-desktop/scripts/sign-jar-natives.cjs b/mateclaw-desktop/scripts/sign-jar-natives.cjs new file mode 100644 index 00000000..d8777e2f --- /dev/null +++ b/mateclaw-desktop/scripts/sign-jar-natives.cjs @@ -0,0 +1,68 @@ +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') +const { execFileSync } = require('node:child_process') +const { readZipEntries, createZipBuffer, ZIP_STORED } = require('./trim-playwright-driver.cjs') + +function isMachO(data) { + if (data.length < 8) return false + const magic = data.readUInt32BE(0) + if ([0xfeedface, 0xcefaedfe, 0xfeedfacf, 0xcffaedfe].includes(magic)) return true + // Java class files share CAFEBABE with fat Mach-O, but contain a class version here. + if ([0xcafebabe, 0xcafebabf].includes(magic)) return data.readUInt32BE(4) > 0 && data.readUInt32BE(4) < 32 + if ([0xbebafeca, 0xbfbafeca].includes(magic)) return data.readUInt32LE(4) > 0 && data.readUInt32LE(4) < 32 + return false +} + +async function rewriteArchive(buffer, signBinary, label = 'app.jar') { + let count = 0 + const entries = readZipEntries(buffer) + for (const entry of entries) { + if (isMachO(entry.data)) { + entry.data = await signBinary(entry.data, `${label}/${entry.name}`) + count++ + } else if (entry.name.endsWith('.jar')) { + const nested = await rewriteArchive(entry.data, signBinary, `${label}/${entry.name}`) + if (nested.count) { + entry.data = nested.buffer + count += nested.count + // Spring Boot requires nested dependencies to be stored without compression. + if (entry.name.startsWith('BOOT-INF/lib/')) entry.method = ZIP_STORED + } + } + } + if (!count) return { buffer, count } + // Upstream JAR signatures become invalid when their native resources change. + const unsigned = entries.filter(entry => !/^META-INF\/[^/]+\.(SF|RSA|DSA|EC)$/i.test(entry.name)) + return { buffer: createZipBuffer(unsigned), count } +} + +async function signAppJar(appJar, options) { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'mateclaw-jar-sign-')) + const output = `${appJar}.signed-tmp` + try { + const result = await rewriteArchive(fs.readFileSync(appJar), async (data, label) => { + // Never extract archive-controlled paths onto the filesystem. + const binary = path.join(temp, 'native') + fs.writeFileSync(binary, data, { mode: 0o755 }) + const args = ['--force', '--sign', options.identity, '--timestamp', '--options', 'runtime'] + if (options.keychain) args.push('--keychain', options.keychain) + if (options.entitlements) args.push('--entitlements', options.entitlements) + args.push(binary) + console.log(`[sign-jar-natives] ${label}`) + execFileSync('/usr/bin/codesign', args, { stdio: 'pipe' }) + execFileSync('/usr/bin/codesign', ['--verify', '--strict', binary], { stdio: 'pipe' }) + return fs.readFileSync(binary) + }) + if (result.count) { + fs.writeFileSync(output, result.buffer) + fs.renameSync(output, appJar) + } + return result.count + } finally { + fs.rmSync(temp, { recursive: true, force: true }) + fs.rmSync(output, { force: true }) + } +} + +module.exports = { isMachO, rewriteArchive, signAppJar } diff --git a/mateclaw-desktop/vite.config.ts b/mateclaw-desktop/vite.config.ts index 5dd690f7..7131400d 100644 --- a/mateclaw-desktop/vite.config.ts +++ b/mateclaw-desktop/vite.config.ts @@ -34,7 +34,7 @@ export default defineConfig(({ command }) => { minify: isBuild, outDir: 'dist-electron/main', rollupOptions: { - external: ['electron', 'electron-updater'], + external: ['electron', 'electron-updater', 'ws'], }, }, }, diff --git a/mateclaw-ui/pnpm-workspace.yaml b/mateclaw-ui/pnpm-workspace.yaml index a23eae08..918fb579 100644 --- a/mateclaw-ui/pnpm-workspace.yaml +++ b/mateclaw-ui/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +packages: + - '.' allowBuilds: esbuild: true vue-demi: true