mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-11-02 14:11:49 +08:00
fix(desktop): harden multi-platform packaging and macOS notarization
This commit is contained in:
parent
e99da0a257
commit
cf1fd92c6d
@ -202,6 +202,18 @@ bash scripts/build-all-platforms.sh --mac-only --publish=always
|
|||||||
|
|
||||||
`electron-builder` 会自动完成签名 → 公证 → 装订(staple)→ 上传。
|
`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) 故障排查章节。
|
> **注意**:不要设置 `CSC_LINK` 环境变量,否则 electron-builder 会创建临时钥匙串,可能导致签名卡死。详见 [CODESIGNING.md](./CODESIGNING.md) 故障排查章节。
|
||||||
|
|
||||||
### CI/CD 签名构建
|
### CI/CD 签名构建
|
||||||
|
|||||||
@ -26,8 +26,13 @@
|
|||||||
const { loadBrandConfig } = require('./scripts/branding.cjs')
|
const { loadBrandConfig } = require('./scripts/branding.cjs')
|
||||||
|
|
||||||
const mode = process.env.BUILD_MODE === 'remote' ? 'remote' : 'local'
|
const mode = process.env.BUILD_MODE === 'remote' ? 'remote' : 'local'
|
||||||
|
const buildArch = process.env.BUILD_ARCH
|
||||||
const brand = loadBrandConfig(__dirname)
|
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.
|
// Derive a short slug from the brand name for artifact file names.
|
||||||
// "MyAI" → "MyAI", "Cool App" → "Cool_App"
|
// "MyAI" → "MyAI", "Cool App" → "Cool_App"
|
||||||
const brandSlug = brand.name.replace(/\s+/g, '_')
|
const brandSlug = brand.name.replace(/\s+/g, '_')
|
||||||
@ -55,7 +60,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
files: ['dist-electron', 'dist'],
|
files: ['dist-electron', 'dist'],
|
||||||
afterPack: 'scripts/trim-playwright-driver.cjs',
|
afterPack: 'scripts/after-pack.cjs',
|
||||||
|
|
||||||
// extraResources: only bundle JRE + JAR in local mode.
|
// extraResources: only bundle JRE + JAR in local mode.
|
||||||
// In remote mode this array is empty — the packaged app contains only the
|
// In remote mode this array is empty — the packaged app contains only the
|
||||||
@ -78,8 +83,8 @@ const config = {
|
|||||||
mac: {
|
mac: {
|
||||||
category: 'public.app-category.productivity',
|
category: 'public.app-category.productivity',
|
||||||
target: [
|
target: [
|
||||||
{ target: 'dmg', arch: ['arm64', 'x64'] },
|
{ target: 'dmg', arch: archList(['arm64', 'x64']) },
|
||||||
{ target: 'zip', arch: ['arm64', 'x64'] },
|
{ target: 'zip', arch: archList(['arm64', 'x64']) },
|
||||||
],
|
],
|
||||||
icon: 'build/icon.icns',
|
icon: 'build/icon.icns',
|
||||||
hardenedRuntime: true,
|
hardenedRuntime: true,
|
||||||
@ -102,10 +107,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
win: {
|
win: {
|
||||||
target: [
|
target: archList(['x64', 'arm64']).map((arch) => ({ target: 'nsis', arch })),
|
||||||
{ target: 'nsis', arch: 'x64' },
|
|
||||||
{ target: 'nsis', arch: 'arm64' },
|
|
||||||
],
|
|
||||||
icon: 'build/icon.ico',
|
icon: 'build/icon.ico',
|
||||||
artifactName:
|
artifactName:
|
||||||
mode === 'remote'
|
mode === 'remote'
|
||||||
|
|||||||
66
mateclaw-desktop/scripts/__tests__/sign-jar-natives.test.cjs
Normal file
66
mateclaw-desktop/scripts/__tests__/sign-jar-natives.test.cjs
Normal file
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
28
mateclaw-desktop/scripts/after-pack.cjs
Normal file
28
mateclaw-desktop/scripts/after-pack.cjs
Normal file
@ -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`)
|
||||||
|
}
|
||||||
@ -1,35 +1,122 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# scripts/build-all-platforms.sh — Build MateClaw desktop packages for all
|
# Build MateClaw Desktop packages.
|
||||||
# platforms (macOS + Windows) in the specified build mode.
|
# 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:
|
# Usage:
|
||||||
# scripts/build-all-platforms.sh --all # local mode (default), both platforms
|
# bash scripts/build-all-platforms.sh --all
|
||||||
# scripts/build-all-platforms.sh --local # local mode, both platforms
|
# bash scripts/build-all-platforms.sh --mac-only
|
||||||
# scripts/build-all-platforms.sh --remote # remote mode, both platforms
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
MODE="--all"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
case "${1:-}" in
|
DESKTOP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
--all) MODE="local" ;;
|
|
||||||
--local) MODE="local" ;;
|
BUILD_TYPE="all"
|
||||||
--remote) MODE="remote" ;;
|
BUILD_MODE="${BUILD_MODE:-local}"
|
||||||
*) echo "Usage: $0 [--all|--local|--remote]"; exit 1 ;;
|
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
|
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
|
cd "$DESKTOP_DIR"
|
||||||
echo "==> macOS (remote/lite)"
|
|
||||||
BUILD_MODE=remote npx electron-builder --mac
|
if [ "$BUILD_MODE" = "local" ]; then
|
||||||
echo "==> Windows (remote/lite)"
|
echo "📦 [1/4] Building backend JAR..."
|
||||||
BUILD_MODE=remote npx electron-builder --win
|
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
|
else
|
||||||
echo "==> macOS (local/full)"
|
echo "📦 [1/4] Skipping backend JAR for remote/lite build."
|
||||||
BUILD_MODE=local npx electron-builder --mac
|
echo "📥 [2/4] Skipping bundled JREs for remote/lite build."
|
||||||
echo "==> Windows (local/full)"
|
echo ""
|
||||||
BUILD_MODE=local npx electron-builder --win
|
|
||||||
fi
|
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 ""
|
||||||
|
|||||||
@ -1,30 +1,78 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# scripts/build.sh — Build the MateClaw Spring Boot backend JAR and place it
|
# Build the MateClaw backend JAR and place it at resources/app.jar so
|
||||||
# at resources/app.jar so electron-builder can bundle it into the desktop app.
|
# electron-builder can bundle it into the desktop app.
|
||||||
#
|
#
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
DESKTOP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
SERVER_DIR="$(cd "$PROJECT_ROOT/../mateclaw-server" && pwd)"
|
PROJECT_ROOT="$(cd "$DESKTOP_DIR/.." && pwd)"
|
||||||
RESOURCES_DIR="$PROJECT_ROOT/resources"
|
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)
|
echo "📦 Building MateClaw web UI..."
|
||||||
cd "$SERVER_DIR"
|
|
||||||
mvn clean package -DskipTests -Dmaven.test.skip=true -q
|
|
||||||
|
|
||||||
# Locate the built JAR
|
if [ ! -d "$UI_DIR" ]; then
|
||||||
JAR_FILE=$(ls "$SERVER_DIR"/target/mateclaw-server-*.jar 2>/dev/null | head -1)
|
echo "❌ Frontend directory not found: $UI_DIR"
|
||||||
if [ -z "$JAR_FILE" ]; then
|
exit 1
|
||||||
echo "ERROR: Could not find built JAR in $SERVER_DIR/target/"
|
|
||||||
exit 1
|
|
||||||
fi
|
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"
|
mkdir -p "$RESOURCES_DIR"
|
||||||
cp "$JAR_FILE" "$RESOURCES_DIR/app.jar"
|
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 ""
|
||||||
|
|||||||
@ -1,86 +1,156 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# scripts/download-jre.sh — Download Eclipse Temurin JRE 21 for the current
|
# Download Adoptium JRE 21 for the requested platform.
|
||||||
# macOS architecture (or both) and extract into resources/jre/<platform>/.
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# scripts/download-jre.sh # auto-detect current arch
|
# bash scripts/download-jre.sh [platform-arch]
|
||||||
# scripts/download-jre.sh arm64 # arm64 only
|
# bash scripts/download-jre.sh mac-arm64
|
||||||
# scripts/download-jre.sh x64 # x64 only
|
# bash scripts/download-jre.sh mac-x64
|
||||||
# scripts/download-jre.sh all # both arches
|
# bash scripts/download-jre.sh win-x64
|
||||||
|
# bash scripts/download-jre.sh win-arm64
|
||||||
#
|
#
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
JRE_DIR="$PROJECT_ROOT/resources/jre"
|
RESOURCES_DIR="$PROJECT_DIR/resources"
|
||||||
|
|
||||||
# Temurin 21 (LTS) JRE downloads via Adoptium API
|
JRE_VERSION="21"
|
||||||
ADOPTIUM_BASE="https://api.adoptium.net/v3/binary/latest/21/ga/mac"
|
ADOPTIUM_API="https://api.adoptium.net/v3/binary/latest"
|
||||||
|
|
||||||
download_and_extract() {
|
detect_platform() {
|
||||||
local arch="$1"
|
local os arch
|
||||||
local folder="$2"
|
|
||||||
local url="$ADOPTIUM_BASE/$arch/jre/hotspot/normal/eclipse?project=jdk"
|
|
||||||
local tmpfile="$JRE_DIR/jre-mac-$arch.tar.gz"
|
|
||||||
|
|
||||||
echo "==> Downloading Temurin 21 JRE for macOS $arch"
|
case "$(uname -s)" in
|
||||||
mkdir -p "$JRE_DIR"
|
Darwin) os="mac" ;;
|
||||||
curl -L --fail -o "$tmpfile" "$url"
|
Linux) os="linux" ;;
|
||||||
|
MINGW*|MSYS*|CYGWIN*) os="win32" ;;
|
||||||
|
*) echo "Unsupported OS: $(uname -s)"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
echo "==> Extracting to $JRE_DIR/$folder"
|
case "$(uname -m)" in
|
||||||
rm -rf "$JRE_DIR/$folder"
|
x86_64|amd64) arch="x64" ;;
|
||||||
mkdir -p "$JRE_DIR/$folder"
|
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/...
|
echo "${os}-${arch}"
|
||||||
# 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TARGET="${1:-auto}"
|
PLATFORM="${1:-$(detect_platform)}"
|
||||||
if [ "$TARGET" = "auto" ]; then
|
echo "🔧 Platform: $PLATFORM"
|
||||||
case "$(uname -m)" in
|
|
||||||
arm64) TARGET="arm64" ;;
|
|
||||||
x86_64) TARGET="x64" ;;
|
|
||||||
*) echo "Unsupported arch: $(uname -m)"; exit 1 ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$TARGET" in
|
case "$PLATFORM" in
|
||||||
arm64) download_and_extract "aarch64" "mac-arm64" ;;
|
mac-arm64)
|
||||||
x64) download_and_extract "x64" "mac-x64" ;;
|
API_OS="mac"
|
||||||
all) download_and_extract "aarch64" "mac-arm64"
|
API_ARCH="aarch64"
|
||||||
download_and_extract "x64" "mac-x64" ;;
|
ARCHIVE_EXT="tar.gz"
|
||||||
*) echo "Usage: $0 [arm64|x64|all]"; exit 1 ;;
|
;;
|
||||||
|
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
|
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"
|
||||||
|
|||||||
@ -123,7 +123,11 @@ echo ""
|
|||||||
echo " Version: $VERSION"
|
echo " Version: $VERSION"
|
||||||
echo " Tag: $TAG"
|
echo " Tag: $TAG"
|
||||||
echo " Mode: ${DRAFT:-release}"
|
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 " Retry: $RETRY"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
@ -275,19 +279,44 @@ if [ -f "$LATEST_YML" ]; then
|
|||||||
echo "🧹 Rewriting latest.yml to drop the merged installer ..."
|
echo "🧹 Rewriting latest.yml to drop the merged installer ..."
|
||||||
(cd "$DESKTOP_DIR" && node -e "
|
(cd "$DESKTOP_DIR" && node -e "
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
const yaml = require('js-yaml');
|
const yaml = require('js-yaml');
|
||||||
const file = '$LATEST_YML';
|
const file = '$LATEST_YML';
|
||||||
|
const releaseDir = '$RELEASE_DIR';
|
||||||
const merged = '$MERGED_EXE_NAME';
|
const merged = '$MERGED_EXE_NAME';
|
||||||
const x64 = '$X64_EXE_NAME';
|
const x64 = '$X64_EXE_NAME';
|
||||||
const arm64 = '$ARM64_EXE_NAME';
|
const arm64 = '$ARM64_EXE_NAME';
|
||||||
const doc = yaml.load(fs.readFileSync(file, 'utf8'));
|
const doc = yaml.load(fs.readFileSync(file, 'utf8'));
|
||||||
const before = (doc.files || []).length;
|
const before = (doc.files || []).length;
|
||||||
doc.files = (doc.files || []).filter(f => f.url !== merged);
|
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) {
|
if (!x64Entry || !arm64Entry) {
|
||||||
console.error('FATAL: per-arch installer entries missing from latest.yml');
|
console.error('FATAL: per-arch installer entries missing from latest.yml');
|
||||||
console.error(' files:', doc.files.map(f => f.url));
|
console.error(' files:', doc.files.map(f => f.url));
|
||||||
|
console.error(' expected:', [x64, arm64]);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
doc.path = x64Entry.url;
|
doc.path = x64Entry.url;
|
||||||
|
|||||||
68
mateclaw-desktop/scripts/sign-jar-natives.cjs
Normal file
68
mateclaw-desktop/scripts/sign-jar-natives.cjs
Normal file
@ -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 }
|
||||||
@ -34,7 +34,7 @@ export default defineConfig(({ command }) => {
|
|||||||
minify: isBuild,
|
minify: isBuild,
|
||||||
outDir: 'dist-electron/main',
|
outDir: 'dist-electron/main',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ['electron', 'electron-updater'],
|
external: ['electron', 'electron-updater', 'ws'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
packages:
|
||||||
|
- '.'
|
||||||
allowBuilds:
|
allowBuilds:
|
||||||
esbuild: true
|
esbuild: true
|
||||||
vue-demi: true
|
vue-demi: true
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user