Updates
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build peardata-server as a Bare standalone binary (Linux hosts).
|
||||
*
|
||||
* Flow (Holepunch bare-build + bare-node-runtime):
|
||||
* 1. bare-pack the entry with global imports (package.json + bare-node-runtime)
|
||||
* 2. Embed the bundle into a bare-runtime prebuild via bare-build platform hooks
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/bare-standalone.cjs --product server --host linux-x64
|
||||
* node scripts/bare-standalone.cjs --product server --host all
|
||||
*
|
||||
* Output:
|
||||
* out/peardata-server-<host>/peardata-server
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const { pathToFileURL } = require('url')
|
||||
const pack = require('bare-pack')
|
||||
const { readModule, listPrefix } = require('bare-pack/fs')
|
||||
const traverse = require('bare-module-traverse')
|
||||
const id = require('bare-bundle-id')
|
||||
|
||||
const root = path.resolve(__dirname, '..')
|
||||
const pkg = require(path.join(root, 'package.json'))
|
||||
const { SERVER_LINUX } = require('./hosts.cjs')
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = {
|
||||
product: 'server',
|
||||
hosts: [],
|
||||
outRoot: path.join(root, 'out'),
|
||||
}
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '--product') out.product = argv[++i]
|
||||
else if (a === '--host') {
|
||||
const h = argv[++i]
|
||||
if (h === 'all') out.hosts.push(...SERVER_LINUX)
|
||||
else out.hosts.push(h)
|
||||
} else if (a === '--out') out.outRoot = path.resolve(argv[++i])
|
||||
else if (a === '--help' || a === '-h') out.help = true
|
||||
}
|
||||
if (!out.hosts.length) {
|
||||
const thisHost = `${process.platform}-${process.arch}`
|
||||
out.hosts.push(SERVER_LINUX.includes(thisHost) ? thisHost : 'linux-x64')
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Global imports map for bare-pack.
|
||||
*/
|
||||
function buildImportsMap() {
|
||||
let bnr = {}
|
||||
try {
|
||||
bnr = require('bare-node-runtime/imports')
|
||||
} catch {
|
||||
console.warn(
|
||||
'[bare-standalone] bare-node-runtime/imports not found — relying on package.json imports'
|
||||
)
|
||||
}
|
||||
return { ...bnr, ...(pkg.imports || {}) }
|
||||
}
|
||||
|
||||
function platformForHost(host) {
|
||||
const bareBuildRoot = path.dirname(require.resolve('bare-build/package'))
|
||||
const load = (name) => require(path.join(bareBuildRoot, 'lib', 'platform', name))
|
||||
switch (host) {
|
||||
case 'linux-arm64':
|
||||
case 'linux-x64':
|
||||
return load('linux')
|
||||
default:
|
||||
throw new Error(
|
||||
`peardata-server only builds Linux hosts (got '${host}'). ` +
|
||||
`Allowed: ${SERVER_LINUX.join(', ')}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} host
|
||||
* @param {string} outRoot
|
||||
*/
|
||||
async function buildOne(host, outRoot) {
|
||||
if (!SERVER_LINUX.includes(host)) {
|
||||
throw new Error(
|
||||
`Refusing non-Linux server host '${host}'. Allowed: ${SERVER_LINUX.join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
const name = 'peardata-server'
|
||||
const outDir = path.join(outRoot, `${name}-${host}`)
|
||||
fs.rmSync(outDir, { recursive: true, force: true })
|
||||
fs.mkdirSync(outDir, { recursive: true })
|
||||
|
||||
const entryPath = path.join(root, 'bin', 'peardata-server.mjs')
|
||||
if (!fs.existsSync(entryPath)) throw new Error(`Missing entry ${entryPath}`)
|
||||
|
||||
const imports = buildImportsMap()
|
||||
console.log(`[bare-standalone] packing ${name} for ${host}…`)
|
||||
|
||||
let entry = await pack(
|
||||
pathToFileURL(entryPath),
|
||||
{
|
||||
hosts: [host],
|
||||
linked: false,
|
||||
resolve: traverse.resolve.bare,
|
||||
imports,
|
||||
},
|
||||
readModule,
|
||||
listPrefix
|
||||
)
|
||||
|
||||
const baseURL = pathToFileURL(root + path.sep)
|
||||
entry = entry.unmount(baseURL)
|
||||
entry.id = id(entry).toString('hex')
|
||||
|
||||
const platform = platformForHost(host)
|
||||
const opts = {
|
||||
name,
|
||||
version: pkg.version || '0.0.0',
|
||||
description: pkg.description || 'peardata server',
|
||||
author: pkg.author || '',
|
||||
identifier: 'com.peardata.server',
|
||||
hosts: [host],
|
||||
out: outDir,
|
||||
standalone: true,
|
||||
package: false,
|
||||
base: root,
|
||||
}
|
||||
|
||||
console.log(`[bare-standalone] embedding bare-runtime for ${host}…`)
|
||||
for await (const resource of platform(root, entry, null, opts)) {
|
||||
if (resource && resource.path) {
|
||||
console.log(`[bare-standalone] resource ${resource.path}`)
|
||||
}
|
||||
}
|
||||
|
||||
const binName = name
|
||||
let binary = path.join(outDir, binName)
|
||||
if (!fs.existsSync(binary)) {
|
||||
const found = walkFind(outDir, (f) => {
|
||||
const base = path.basename(f)
|
||||
return base === name || base === 'peardata-server'
|
||||
})
|
||||
if (found) binary = found
|
||||
}
|
||||
|
||||
if (fs.existsSync(binary)) {
|
||||
try {
|
||||
fs.chmodSync(binary, 0o755)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
const flat = path.join(outDir, path.basename(binary))
|
||||
if (path.resolve(binary) !== path.resolve(flat)) {
|
||||
fs.copyFileSync(binary, flat)
|
||||
binary = flat
|
||||
}
|
||||
console.log(`[bare-standalone] wrote ${binary}`)
|
||||
} else {
|
||||
console.warn(`[bare-standalone] WARN: expected binary not found under ${outDir}`)
|
||||
console.warn(
|
||||
' contents:',
|
||||
fs.readdirSync(outDir, { recursive: true }).slice(0, 30).join(', ')
|
||||
)
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(outDir, 'build-info.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
product: 'server',
|
||||
host,
|
||||
name,
|
||||
version: pkg.version,
|
||||
builtAt: new Date().toISOString(),
|
||||
entry: 'bin/peardata-server.mjs',
|
||||
bundleId: entry.id,
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + '\n'
|
||||
)
|
||||
|
||||
return outDir
|
||||
}
|
||||
|
||||
function walkFind(dir, pred) {
|
||||
const stack = [dir]
|
||||
while (stack.length) {
|
||||
const d = stack.pop()
|
||||
let entries
|
||||
try {
|
||||
entries = fs.readdirSync(d, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
for (const ent of entries) {
|
||||
const p = path.join(d, ent.name)
|
||||
if (ent.isDirectory()) stack.push(p)
|
||||
else if (pred(p)) return p
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const opts = parseArgs(process.argv.slice(2))
|
||||
if (opts.help) {
|
||||
console.log(`Usage: node scripts/bare-standalone.cjs [--product server] [--host <host>|all] [--out dir]
|
||||
Server hosts (Linux only): ${SERVER_LINUX.join(', ')}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (opts.product !== 'server') {
|
||||
throw new Error(
|
||||
`bare-standalone only builds product=server. For client use: npm run make:client:<host>`
|
||||
)
|
||||
}
|
||||
|
||||
const results = []
|
||||
for (const host of opts.hosts) {
|
||||
results.push(await buildOne(host, opts.outRoot))
|
||||
}
|
||||
console.log('[bare-standalone] done:', results.join(', '))
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Bundle the PearData GUI (app.js + local modules) to CJS for Electron.
|
||||
*
|
||||
* CJS require() works with nodeIntegration. esbuild inlines local sources and
|
||||
* leaves node_modules as external require()s resolved from electron/../node_modules.
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const { build } = require('esbuild')
|
||||
|
||||
const root = path.resolve(__dirname, '..')
|
||||
const outfile = path.join(root, 'electron', 'app.bundle.cjs')
|
||||
|
||||
async function main() {
|
||||
await build({
|
||||
entryPoints: [path.join(root, 'app.js')],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile,
|
||||
packages: 'external',
|
||||
sourcemap: true,
|
||||
logLevel: 'info',
|
||||
banner: {
|
||||
js: '/* peardata electron GUI bundle — generated by scripts/build-client-bundle.cjs */\n',
|
||||
},
|
||||
})
|
||||
console.log('[build-client-bundle] wrote', outfile)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,13 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build source release artifacts and publish a rolling Gitea release.
|
||||
# Build peardata server (Linux) + client (all arches) and publish a rolling Gitea release.
|
||||
#
|
||||
# Cross-compile (default — single Linux CI runner is enough):
|
||||
# Server: bare-build + bare-runtime prebuilds (linux-x64, linux-arm64 only)
|
||||
# Client: electron-forge --platform/--arch (all 64-bit hosts)
|
||||
#
|
||||
# Required:
|
||||
# RELEASE_TOKEN — Gitea PAT with repository release write
|
||||
# Optional:
|
||||
# GITEA_URL / GITEA_OWNER / GITEA_REPO
|
||||
# PEARDATA_SERVER_HOSTS — default: linux-x64,linux-arm64
|
||||
# PEARDATA_CLIENT_HOSTS — default: all 64-bit (see scripts/hosts.cjs)
|
||||
# PEARDATA_SKIP_CLIENT=1
|
||||
# RELEASE_TAG (default: rolling)
|
||||
# DRY_RUN=1 — build + stage only, no upload
|
||||
# GITHUB_SHA / GITEA_SHA — target commit for the release tag
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
@@ -18,9 +24,13 @@ STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
SHA="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
FULL_SHA="${GITHUB_SHA:-${GITEA_SHA:-$(git rev-parse HEAD 2>/dev/null || echo main)}}"
|
||||
TAG="${RELEASE_TAG:-rolling}"
|
||||
PKG_NAME="$(node -p "require('./package.json').name")"
|
||||
RELEASE_TITLE="${RELEASE_NAME:-${PKG_NAME} rolling}"
|
||||
DIST="$ROOT/dist"
|
||||
RELEASE_TITLE="${RELEASE_NAME:-peardata rolling}"
|
||||
DIST="$ROOT/dist/release"
|
||||
DEFAULT_SERVER="$(node -p "require('./scripts/hosts.cjs').SERVER_LINUX.join(',')")"
|
||||
DEFAULT_CLIENT="$(node -p "require('./scripts/hosts.cjs').ALL_64.join(',')")"
|
||||
|
||||
rm -rf "$DIST"
|
||||
mkdir -p "$DIST"
|
||||
|
||||
log() { echo "[release] $*"; }
|
||||
|
||||
@@ -39,45 +49,133 @@ detect_remote() {
|
||||
read -r DETECTED_URL DETECTED_OWNER DETECTED_REPO <<<"$(detect_remote)"
|
||||
GITEA_URL="${GITEA_URL:-${DETECTED_URL:-}}"
|
||||
GITEA_OWNER="${GITEA_OWNER:-${DETECTED_OWNER:-}}"
|
||||
GITEA_REPO="${GITEA_REPO:-${DETECTED_REPO:-${PKG_NAME}}}"
|
||||
GITEA_REPO="${GITEA_REPO:-${DETECTED_REPO:-peardata}}"
|
||||
|
||||
if [[ -z "${GITEA_URL}" ]]; then
|
||||
log "WARN: could not detect GITEA_URL — set GITEA_URL for upload"
|
||||
fi
|
||||
|
||||
# --- build artifacts (source tarball + checksum + notes) ---
|
||||
log "building release artifacts via scripts/release.sh"
|
||||
bash scripts/release.sh
|
||||
export PEARDATA_SERVER_HOSTS="${PEARDATA_SERVER_HOSTS:-$DEFAULT_SERVER}"
|
||||
export PEARDATA_CLIENT_HOSTS="${PEARDATA_CLIENT_HOSTS:-$DEFAULT_CLIENT}"
|
||||
|
||||
export npm_config_build_from_source="${npm_config_build_from_source:-false}"
|
||||
export PEARDATA_SKIP_REBUILD="${PEARDATA_SKIP_REBUILD:-1}"
|
||||
|
||||
# --- server: Linux only ---
|
||||
log "building server binaries for: $PEARDATA_SERVER_HOSTS"
|
||||
node scripts/make.cjs server
|
||||
|
||||
# --- client: all hosts ---
|
||||
if [[ "${PEARDATA_SKIP_CLIENT:-0}" != "1" ]]; then
|
||||
log "building client binaries for: $PEARDATA_CLIENT_HOSTS"
|
||||
node scripts/make.cjs client
|
||||
else
|
||||
log "skipping client (PEARDATA_SKIP_CLIENT=1)"
|
||||
fi
|
||||
|
||||
# --- stage artifacts ---
|
||||
sha_file() {
|
||||
local f="$1"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$f"
|
||||
else
|
||||
shasum -a 256 "$f"
|
||||
fi
|
||||
}
|
||||
|
||||
stage_server() {
|
||||
local host="$1"
|
||||
local dir="$ROOT/out/peardata-server-$host"
|
||||
local bin="peardata-server"
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
log "WARN: missing server dir $dir"
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -f "$dir/$bin" ]]; then
|
||||
local f
|
||||
f="$(find "$dir" -maxdepth 2 -type f -name 'peardata-server' | head -1 || true)"
|
||||
if [[ -n "$f" ]]; then
|
||||
bin="$(basename "$f")"
|
||||
cp -f "$f" "$dir/$bin" 2>/dev/null || true
|
||||
else
|
||||
log "WARN: no server binary in $dir"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
local archive="peardata-server-${VERSION}-${host}.tar.gz"
|
||||
tar -C "$dir" -czf "$DIST/$archive" .
|
||||
(cd "$DIST" && sha_file "$archive" >"${archive}.sha256")
|
||||
log "staged $archive"
|
||||
}
|
||||
|
||||
stage_client() {
|
||||
local host="$1"
|
||||
local dir="$ROOT/out/peardata-$host"
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
log "WARN: missing client dir $dir"
|
||||
return 1
|
||||
fi
|
||||
local archive="peardata-client-${VERSION}-${host}.tar.gz"
|
||||
tar -C "$ROOT/out" -czf "$DIST/$archive" "peardata-$host"
|
||||
(cd "$DIST" && sha_file "$archive" >"${archive}.sha256")
|
||||
log "staged $archive"
|
||||
}
|
||||
|
||||
SERVER_OK=0
|
||||
CLIENT_OK=0
|
||||
SERVER_FAIL=0
|
||||
CLIENT_FAIL=0
|
||||
|
||||
IFS=',' read -ra SHOSTS <<<"$PEARDATA_SERVER_HOSTS"
|
||||
for h in "${SHOSTS[@]}"; do
|
||||
h="$(echo "$h" | xargs)"
|
||||
if stage_server "$h"; then SERVER_OK=$((SERVER_OK + 1)); else SERVER_FAIL=$((SERVER_FAIL + 1)); fi
|
||||
done
|
||||
|
||||
if [[ "${PEARDATA_SKIP_CLIENT:-0}" != "1" ]]; then
|
||||
IFS=',' read -ra CHOSTS <<<"$PEARDATA_CLIENT_HOSTS"
|
||||
for h in "${CHOSTS[@]}"; do
|
||||
h="$(echo "$h" | xargs)"
|
||||
if stage_client "$h"; then CLIENT_OK=$((CLIENT_OK + 1)); else CLIENT_FAIL=$((CLIENT_FAIL + 1)); fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Enrich notes with rolling metadata (release.sh writes a base file)
|
||||
cat >"$DIST/RELEASE_NOTES.md" <<EOF
|
||||
# ${PKG_NAME} ${VERSION} (${TAG})
|
||||
# peardata ${VERSION} (${TAG})
|
||||
|
||||
- Commit: \`${SHA}\` (\`${FULL_SHA}\`)
|
||||
- Built: ${STAMP}
|
||||
- Server archives staged: ${SERVER_OK} (failed: ${SERVER_FAIL})
|
||||
- Client archives staged: ${CLIENT_OK} (failed: ${CLIENT_FAIL})
|
||||
|
||||
HyperDHT + protomux-rpc application template.
|
||||
## Host matrix
|
||||
|
||||
## Contents
|
||||
- Node server (\`npm run start:server\`)
|
||||
- Pear desktop client (\`npm start\` / \`pear run -d .\`)
|
||||
- Demo room (messages + presence + invites)
|
||||
| Product | Hosts |
|
||||
|---------|-------|
|
||||
| **Server** (Bare) | \`linux-x64\`, \`linux-arm64\` only |
|
||||
| **Client** (Electron) | \`linux-x64\`, \`linux-arm64\`, \`darwin-x64\`, \`darwin-arm64\`, \`win32-x64\`, \`win32-arm64\` |
|
||||
|
||||
## Install
|
||||
## Server (Bare standalone)
|
||||
|
||||
\`\`\`bash
|
||||
mkdir -p app && tar -xzf ${PKG_NAME}-v${VERSION}.tar.gz -C app
|
||||
cd app
|
||||
npm install
|
||||
npm run start:server
|
||||
tar -xzf peardata-server-${VERSION}-linux-x64.tar.gz
|
||||
./peardata-server
|
||||
# REST: http://127.0.0.1:19999/api/v3/info
|
||||
\`\`\`
|
||||
|
||||
## Verify
|
||||
## Client (Electron)
|
||||
|
||||
\`\`\`bash
|
||||
sha256sum -c ${PKG_NAME}-v${VERSION}.tar.gz.sha256
|
||||
tar -xzf peardata-client-${VERSION}-darwin-arm64.tar.gz
|
||||
open peardata-darwin-arm64/peardata.app # macOS
|
||||
# Linux: ./peardata-linux-x64/peardata-client
|
||||
# Windows: peardata-win32-x64\\\\peardata-client.exe
|
||||
\`\`\`
|
||||
|
||||
macOS clients are codesigned in CI (ad-hoc / self-signed via \`rcodesign\` on Linux)
|
||||
to avoid Gatekeeper "**damaged**" false positives. First open may still need
|
||||
right-click → Open unless notarized with Developer ID.
|
||||
|
||||
## Checksums
|
||||
|
||||
See \`*.sha256\` beside each archive.
|
||||
@@ -85,11 +183,10 @@ EOF
|
||||
|
||||
log "artifacts in $DIST:"
|
||||
ls -la "$DIST" || true
|
||||
log "summary: server ok=${SERVER_OK} fail=${SERVER_FAIL} | client ok=${CLIENT_OK} fail=${CLIENT_FAIL}"
|
||||
|
||||
shopt -s nullglob
|
||||
ARTIFACTS=("$DIST"/*.tar.gz)
|
||||
if [[ ${#ARTIFACTS[@]} -eq 0 ]]; then
|
||||
log "ERROR: no tarball artifacts in $DIST"
|
||||
if [[ "$SERVER_OK" -eq 0 ]]; then
|
||||
log "ERROR: no server artifacts staged"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -140,6 +237,7 @@ CREATE_RESP="$(curl -fsSL -X POST -H "$AUTH" -H 'Content-Type: application/json'
|
||||
REL_ID="$(node -e "console.log(JSON.parse(process.argv[1]).id)" "$CREATE_RESP")"
|
||||
log "created release id=$REL_ID"
|
||||
|
||||
shopt -s nullglob
|
||||
for f in "$DIST"/*; do
|
||||
[[ -f "$f" ]] || continue
|
||||
base="$(basename "$f")"
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Canonical PearData binary host list (64-bit only — no ia32 / armv7).
|
||||
* Used by make scripts, predownload-electron, and release tooling.
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
/** @type {readonly string[]} */
|
||||
const SERVER_LINUX = Object.freeze(['linux-x64', 'linux-arm64'])
|
||||
|
||||
/** @type {readonly string[]} */
|
||||
const ALL_64 = Object.freeze([
|
||||
'linux-x64',
|
||||
'linux-arm64',
|
||||
'darwin-x64',
|
||||
'darwin-arm64',
|
||||
'win32-x64',
|
||||
'win32-arm64',
|
||||
])
|
||||
|
||||
/**
|
||||
* @param {string|undefined} envVal comma-separated override
|
||||
* @param {readonly string[]} fallback
|
||||
*/
|
||||
function parseHostList(envVal, fallback = ALL_64) {
|
||||
if (!envVal || !String(envVal).trim()) return [...fallback]
|
||||
return String(envVal)
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* electron-forge platform/arch for a host triple
|
||||
* @param {string} host
|
||||
* @returns {{ platform: string, arch: string }}
|
||||
*/
|
||||
function hostToElectron(host) {
|
||||
const [platform, arch] = host.split('-')
|
||||
if (!platform || !arch) throw new Error(`Invalid host: ${host}`)
|
||||
return { platform, arch }
|
||||
}
|
||||
|
||||
/**
|
||||
* npm script name for client package, or null
|
||||
* @param {string} host
|
||||
*/
|
||||
function clientNpmScript(host) {
|
||||
const map = {
|
||||
'linux-x64': 'make:client:linux-x64',
|
||||
'linux-arm64': 'make:client:linux-arm64',
|
||||
'darwin-arm64': 'make:client:darwin-arm64',
|
||||
'darwin-x64': 'make:client:darwin-x64',
|
||||
'win32-x64': 'make:client:win32-x64',
|
||||
'win32-arm64': 'make:client:win32-arm64',
|
||||
}
|
||||
return map[host] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* npm script name for server package, or null
|
||||
* @param {string} host
|
||||
*/
|
||||
function serverNpmScript(host) {
|
||||
// Server is Linux-only
|
||||
const map = {
|
||||
'linux-x64': 'make:server:linux-x64',
|
||||
'linux-arm64': 'make:server:linux-arm64',
|
||||
}
|
||||
return map[host] || null
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SERVER_LINUX,
|
||||
ALL_64,
|
||||
CLIENT_HOSTS: ALL_64,
|
||||
parseHostList,
|
||||
hostToElectron,
|
||||
clientNpmScript,
|
||||
serverNpmScript,
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Orchestrate peardata binary builds.
|
||||
*
|
||||
* node scripts/make.cjs server # Linux server hosts (bare-build)
|
||||
* node scripts/make.cjs client # all client hosts (electron-forge)
|
||||
* node scripts/make.cjs both|all # server + client
|
||||
*
|
||||
* Env:
|
||||
* PEARDATA_SERVER_HOSTS=linux-x64,linux-arm64
|
||||
* PEARDATA_CLIENT_HOSTS=linux-x64,darwin-arm64,…
|
||||
* PEARDATA_CLIENT_TIMEOUT_MS=600000
|
||||
* PEARDATA_SKIP_ELECTRON_PREDOWNLOAD=1
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const { spawnSync } = require('child_process')
|
||||
const {
|
||||
SERVER_LINUX,
|
||||
ALL_64,
|
||||
parseHostList,
|
||||
clientNpmScript,
|
||||
hostToElectron,
|
||||
} = require('./hosts.cjs')
|
||||
|
||||
const root = path.resolve(__dirname, '..')
|
||||
|
||||
const SERVER_HOSTS = parseHostList(process.env.PEARDATA_SERVER_HOSTS, SERVER_LINUX)
|
||||
const CLIENT_HOSTS = parseHostList(process.env.PEARDATA_CLIENT_HOSTS, ALL_64)
|
||||
const CLIENT_TIMEOUT_MS = Number(process.env.PEARDATA_CLIENT_TIMEOUT_MS || 600_000)
|
||||
|
||||
function run(cmd, args, opts = {}) {
|
||||
console.log(`\n$ ${cmd} ${args.join(' ')}\n`)
|
||||
const t0 = Date.now()
|
||||
const res = spawnSync(cmd, args, {
|
||||
cwd: root,
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
shell: process.platform === 'win32',
|
||||
...opts,
|
||||
})
|
||||
if (res.error) throw res.error
|
||||
if (res.status !== 0) process.exit(res.status || 1)
|
||||
console.log(`[make] ok in ${((Date.now() - t0) / 1000).toFixed(1)}s`)
|
||||
}
|
||||
|
||||
function npmRun(script, env) {
|
||||
run(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', script], {
|
||||
env: env ? { ...process.env, ...env } : process.env,
|
||||
})
|
||||
}
|
||||
|
||||
function makeServer(hosts = SERVER_HOSTS) {
|
||||
for (const host of hosts) {
|
||||
if (!SERVER_LINUX.includes(host)) {
|
||||
console.error(`[make] refusing non-Linux server host: ${host}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
console.log(`[make] server hosts (Linux only): ${hosts.join(', ')}`)
|
||||
for (const host of hosts) {
|
||||
run(process.execPath, [
|
||||
path.join(root, 'scripts', 'bare-standalone.cjs'),
|
||||
'--product',
|
||||
'server',
|
||||
'--host',
|
||||
host,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
function predownloadElectron(hosts) {
|
||||
if (process.env.PEARDATA_SKIP_ELECTRON_PREDOWNLOAD === '1') {
|
||||
console.log('[make] skip electron predownload (PEARDATA_SKIP_ELECTRON_PREDOWNLOAD=1)')
|
||||
return
|
||||
}
|
||||
console.log('[make] pre-downloading Electron for client hosts…')
|
||||
run(process.execPath, [path.join(root, 'scripts', 'predownload-electron.cjs')], {
|
||||
env: {
|
||||
...process.env,
|
||||
PEARDATA_CLIENT_HOSTS: hosts.join(','),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function makeClient(hosts = CLIENT_HOSTS) {
|
||||
console.log(`[make] client hosts: ${hosts.join(', ')}`)
|
||||
npmRun('build:client-bundle')
|
||||
predownloadElectron(hosts)
|
||||
|
||||
for (const host of hosts) {
|
||||
const script = clientNpmScript(host)
|
||||
if (!script) {
|
||||
console.error(`[make] no client script for host ${host}`)
|
||||
process.exit(1)
|
||||
}
|
||||
const { platform, arch } = hostToElectron(host)
|
||||
console.log(`[make] client ${host} (filter prebuilds to ${platform}-${arch})`)
|
||||
const env = {
|
||||
...process.env,
|
||||
npm_config_build_from_source: 'false',
|
||||
PEARDATA_SKIP_REBUILD: process.env.PEARDATA_SKIP_REBUILD || '1',
|
||||
PEARDATA_PACKAGE_PLATFORM: platform,
|
||||
PEARDATA_PACKAGE_ARCH: arch,
|
||||
PEARDATA_SKIP_PREPACKAGE_BUNDLE: '1',
|
||||
DEBUG:
|
||||
process.env.DEBUG ||
|
||||
(process.env.CI ? 'electron-packager,electron-forge:lifecycle' : ''),
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[make] client timeout: ${(CLIENT_TIMEOUT_MS / 1000).toFixed(0)}s (PEARDATA_CLIENT_TIMEOUT_MS)`
|
||||
)
|
||||
const t0 = Date.now()
|
||||
const res = spawnSync(
|
||||
process.platform === 'win32' ? 'npm.cmd' : 'npm',
|
||||
['run', script],
|
||||
{
|
||||
cwd: root,
|
||||
stdio: 'inherit',
|
||||
env,
|
||||
shell: process.platform === 'win32',
|
||||
timeout: CLIENT_TIMEOUT_MS,
|
||||
killSignal: 'SIGKILL',
|
||||
}
|
||||
)
|
||||
if (res.error) {
|
||||
if (res.error.code === 'ETIMEDOUT') {
|
||||
console.error(
|
||||
`[make] FATAL: client package ${host} exceeded ${CLIENT_TIMEOUT_MS}ms — killed.`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
throw res.error
|
||||
}
|
||||
if (res.status !== 0) {
|
||||
console.error(`[make] client package ${host} failed with status ${res.status}`)
|
||||
process.exit(res.status || 1)
|
||||
}
|
||||
console.log(`[make] ok ${host} in ${((Date.now() - t0) / 1000).toFixed(1)}s`)
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const mode = process.argv[2] || 'all'
|
||||
const thisHost = `${process.platform}-${process.arch}`
|
||||
|
||||
switch (mode) {
|
||||
case 'server':
|
||||
makeServer()
|
||||
break
|
||||
case 'client':
|
||||
makeClient()
|
||||
break
|
||||
case 'client-this':
|
||||
makeClient([thisHost])
|
||||
break
|
||||
case 'both':
|
||||
case 'all':
|
||||
makeServer()
|
||||
makeClient()
|
||||
break
|
||||
case 'client-all':
|
||||
makeClient(ALL_64)
|
||||
break
|
||||
default:
|
||||
console.error(`Unknown mode: ${mode}`)
|
||||
console.error(
|
||||
'Usage: node scripts/make.cjs [server|client|client-this|both|all|client-all]'
|
||||
)
|
||||
console.error(`Server (Linux): ${SERVER_LINUX.join(', ')}`)
|
||||
console.error(`Client (all): ${ALL_64.join(', ')}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('\n[make] complete')
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Pre-download Electron binaries for every client host so forge packaging
|
||||
* does not silently hang mid-package on cross-arch GitHub downloads.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/predownload-electron.cjs
|
||||
* PEARDATA_CLIENT_HOSTS=linux-x64,darwin-arm64 node scripts/predownload-electron.cjs
|
||||
*
|
||||
* Env:
|
||||
* ELECTRON_CACHE / electron_config_cache — cache dir (recommended in CI)
|
||||
* PEARDATA_ELECTRON_DOWNLOAD_TIMEOUT_MS — per-artifact timeout (default 180000)
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const { downloadArtifact } = require('@electron/get')
|
||||
const { ALL_64, parseHostList, hostToElectron } = require('./hosts.cjs')
|
||||
|
||||
const root = path.resolve(__dirname, '..')
|
||||
const electronVersion = require(path.join(root, 'node_modules/electron/package.json')).version
|
||||
const hosts = parseHostList(process.env.PEARDATA_CLIENT_HOSTS, ALL_64)
|
||||
const timeoutMs = Number(process.env.PEARDATA_ELECTRON_DOWNLOAD_TIMEOUT_MS || 180000)
|
||||
const zipDir = path.join(root, '.cache', 'electron-zips')
|
||||
|
||||
function withTimeout(promise, ms, label) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const t = setTimeout(() => {
|
||||
reject(new Error(`[predownload-electron] timeout after ${ms}ms: ${label}`))
|
||||
}, ms)
|
||||
promise.then(
|
||||
(v) => {
|
||||
clearTimeout(t)
|
||||
resolve(v)
|
||||
},
|
||||
(e) => {
|
||||
clearTimeout(t)
|
||||
reject(e)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(zipDir, { recursive: true })
|
||||
if (process.env.ELECTRON_CACHE || process.env.electron_config_cache) {
|
||||
console.log(
|
||||
'[predownload-electron] cache:',
|
||||
process.env.ELECTRON_CACHE || process.env.electron_config_cache
|
||||
)
|
||||
}
|
||||
console.log(
|
||||
`[predownload-electron] electron@${electronVersion} hosts: ${hosts.join(', ')}`
|
||||
)
|
||||
|
||||
for (const host of hosts) {
|
||||
const { platform, arch } = hostToElectron(host)
|
||||
const label = `${platform}-${arch}`
|
||||
const destName = `electron-v${electronVersion}-${platform}-${arch}.zip`
|
||||
const destPath = path.join(zipDir, destName)
|
||||
if (fs.existsSync(destPath) && fs.statSync(destPath).size > 1_000_000) {
|
||||
console.log(`[predownload-electron] skip (present): ${destName}`)
|
||||
continue
|
||||
}
|
||||
|
||||
console.log(`[predownload-electron] downloading ${label}…`)
|
||||
const t0 = Date.now()
|
||||
const zipPath = await withTimeout(
|
||||
downloadArtifact({
|
||||
version: electronVersion,
|
||||
platform,
|
||||
arch,
|
||||
artifactName: 'electron',
|
||||
}),
|
||||
timeoutMs,
|
||||
label
|
||||
)
|
||||
fs.copyFileSync(zipPath, destPath)
|
||||
const mb = (fs.statSync(destPath).size / 1024 / 1024).toFixed(1)
|
||||
console.log(
|
||||
`[predownload-electron] ok ${label} → ${destName} (${mb} MB) in ${((Date.now() - t0) / 1000).toFixed(1)}s`
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`[predownload-electron] zip dir: ${zipDir}`)
|
||||
console.log('[predownload-electron] done')
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,521 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Codesign PearData macOS artifacts so Gatekeeper does not report
|
||||
* "is damaged and can't be opened. You should move it to the Trash."
|
||||
*
|
||||
* Targets:
|
||||
* - Electron .app bundles (deep sign / nested helpers)
|
||||
* - Standalone peardata-server Mach-O binaries (Bare)
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/sign-macos-app.cjs path/to/PearData.app
|
||||
* node scripts/sign-macos-app.cjs path/to/out/peardata-darwin-arm64
|
||||
* node scripts/sign-macos-app.cjs path/to/out/peardata-server-darwin-arm64
|
||||
* node scripts/sign-macos-app.cjs path/to/peardata-server
|
||||
*
|
||||
* Identity (first match wins):
|
||||
* MAC_CODESIGN_IDENTITY / CSC_NAME — "Developer ID Application: …" or team identity
|
||||
* otherwise ad-hoc (`-`) which is enough to make the artifact *valid* (not "damaged")
|
||||
*
|
||||
* Tools:
|
||||
* macOS: /usr/bin/codesign (required for production identities)
|
||||
* Linux CI: rcodesign (apple-codesign) for self-signed seal when present
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { spawnSync, execFileSync } = require('child_process')
|
||||
|
||||
const ENTITLEMENTS = path.join(__dirname, 'entitlements.mac.plist')
|
||||
const SERVER_BIN_NAMES = new Set(['peardata-server', 'peardata-server.exe'])
|
||||
|
||||
function log(...a) {
|
||||
console.log('[sign-macos]', ...a)
|
||||
}
|
||||
|
||||
function findApps(input) {
|
||||
const st = fs.statSync(input)
|
||||
if (st.isFile() && input.endsWith('.app')) return [input]
|
||||
if (st.isDirectory() && input.endsWith('.app')) return [input]
|
||||
if (st.isDirectory()) {
|
||||
return fs
|
||||
.readdirSync(input)
|
||||
.filter((n) => n.endsWith('.app'))
|
||||
.map((n) => path.join(input, n))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Find standalone peardata-server Mach-O binaries under a path.
|
||||
* @param {string} input
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function findServerBinaries(input) {
|
||||
if (!fs.existsSync(input)) return []
|
||||
const st = fs.statSync(input)
|
||||
if (st.isFile()) {
|
||||
const base = path.basename(input)
|
||||
if (SERVER_BIN_NAMES.has(base) || base === 'peardata-server') return [input]
|
||||
if (!base.endsWith('.app') && !base.endsWith('.dmg') && !base.endsWith('.pkg')) {
|
||||
if (base.includes('peardata-server')) return [input]
|
||||
}
|
||||
return []
|
||||
}
|
||||
if (!st.isDirectory()) return []
|
||||
const found = []
|
||||
const stack = [input]
|
||||
while (stack.length) {
|
||||
const dir = stack.pop()
|
||||
let entries
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
for (const ent of entries) {
|
||||
const p = path.join(dir, ent.name)
|
||||
if (ent.isDirectory()) {
|
||||
if (ent.name === 'node_modules' || ent.name.endsWith('.app') || ent.name.startsWith('.')) {
|
||||
continue
|
||||
}
|
||||
stack.push(p)
|
||||
} else if (ent.isFile() && SERVER_BIN_NAMES.has(ent.name)) {
|
||||
found.push(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
function which(cmd) {
|
||||
try {
|
||||
const r = spawnSync(process.platform === 'win32' ? 'where' : 'which', [cmd], {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
if (r.status === 0) return r.stdout.trim().split(/\r?\n/)[0]
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function identity() {
|
||||
const id = process.env.MAC_CODESIGN_IDENTITY || process.env.CSC_NAME || ''
|
||||
if (id && id !== '-' && id.toLowerCase() !== 'null') return id
|
||||
return '-'
|
||||
}
|
||||
|
||||
function ensureEntitlements() {
|
||||
if (fs.existsSync(ENTITLEMENTS)) return ENTITLEMENTS
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
`
|
||||
fs.writeFileSync(ENTITLEMENTS, xml)
|
||||
return ENTITLEMENTS
|
||||
}
|
||||
|
||||
function listSignTargets(appPath) {
|
||||
const targets = []
|
||||
const walk = (dir) => {
|
||||
let entries
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
for (const ent of entries) {
|
||||
const p = path.join(dir, ent.name)
|
||||
if (ent.isDirectory()) {
|
||||
if (ent.name === 'node_modules' || ent.name.startsWith('.')) {
|
||||
if (ent.name === 'node_modules') walk(p)
|
||||
else if (!ent.name.startsWith('.')) walk(p)
|
||||
continue
|
||||
}
|
||||
if (ent.name.endsWith('.app') || ent.name.endsWith('.framework')) {
|
||||
walk(p)
|
||||
targets.push(p)
|
||||
continue
|
||||
}
|
||||
walk(p)
|
||||
} else if (ent.isFile() || ent.isSymbolicLink()) {
|
||||
const base = ent.name
|
||||
if (
|
||||
base.endsWith('.dylib') ||
|
||||
base.endsWith('.so') ||
|
||||
base.endsWith('.node') ||
|
||||
base.endsWith('.bare') ||
|
||||
base === 'peardata-client' ||
|
||||
base.startsWith('PearData Helper') ||
|
||||
base.startsWith('peardata Helper') ||
|
||||
base === 'Electron Framework' ||
|
||||
base === 'Squirrel' ||
|
||||
base === 'ReactiveObjC' ||
|
||||
base === 'Mantle' ||
|
||||
base === 'chrome_crashpad_handler'
|
||||
) {
|
||||
targets.push(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(appPath)
|
||||
const uniq = [...new Set(targets)]
|
||||
uniq.sort((a, b) => {
|
||||
const da = a.split(path.sep).length
|
||||
const db = b.split(path.sep).length
|
||||
if (da !== db) return db - da
|
||||
return b.length - a.length
|
||||
})
|
||||
const rootIdx = uniq.indexOf(appPath)
|
||||
if (rootIdx >= 0) uniq.splice(rootIdx, 1)
|
||||
uniq.push(appPath)
|
||||
return uniq
|
||||
}
|
||||
|
||||
function codesignDarwin(appPath, id) {
|
||||
const entitlements = ensureEntitlements()
|
||||
const hardened = id !== '-'
|
||||
|
||||
try {
|
||||
execFileSync('xattr', ['-cr', appPath], { stdio: 'pipe' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
log(`deep codesign identity=${id === '-' ? 'ad-hoc' : id}`)
|
||||
const rootArgs = [
|
||||
'--force',
|
||||
'--deep',
|
||||
'--sign',
|
||||
id,
|
||||
'--entitlements',
|
||||
entitlements,
|
||||
]
|
||||
if (hardened) rootArgs.push('--options', 'runtime', '--timestamp')
|
||||
else rootArgs.push('--timestamp=none')
|
||||
rootArgs.push(appPath)
|
||||
|
||||
let root = spawnSync('codesign', rootArgs, { encoding: 'utf8' })
|
||||
if (root.status !== 0) {
|
||||
throw new Error(`codesign failed for app:\n${root.stderr || root.stdout}`)
|
||||
}
|
||||
|
||||
let v = spawnSync('codesign', ['--verify', '--deep', '--strict', '--verbose=2', appPath], {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
if (v.status !== 0) {
|
||||
log('strict verify failed — signing nested code then app…')
|
||||
const targets = listSignTargets(appPath)
|
||||
for (const target of targets) {
|
||||
if (target === appPath) continue
|
||||
const args = ['--force', '--sign', id]
|
||||
if (hardened) args.push('--options', 'runtime', '--timestamp')
|
||||
else args.push('--timestamp=none')
|
||||
args.push(target)
|
||||
spawnSync('codesign', args, { encoding: 'utf8' })
|
||||
}
|
||||
root = spawnSync('codesign', rootArgs, { encoding: 'utf8' })
|
||||
if (root.status !== 0) {
|
||||
throw new Error(`codesign failed for app (retry):\n${root.stderr || root.stdout}`)
|
||||
}
|
||||
v = spawnSync('codesign', ['--verify', '--deep', '--strict', '--verbose=2', appPath], {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
if (v.status !== 0) {
|
||||
throw new Error(`codesign verify failed:\n${v.stderr || v.stdout}`)
|
||||
}
|
||||
}
|
||||
log('verify ok:', (v.stderr || v.stdout || '').trim().split('\n').slice(0, 3).join(' | '))
|
||||
}
|
||||
|
||||
function ensureRcodesignSelfSignedP12(bin) {
|
||||
const certDir =
|
||||
process.env.PEARDATA_RCODESIGN_CERT_DIR ||
|
||||
path.join(__dirname, '..', 'tools', 'rcodesign', 'ci-cert')
|
||||
const p12Path = path.join(certDir, 'peardata-ci.p12')
|
||||
const password = process.env.PEARDATA_RCODESIGN_P12_PASSWORD || 'peardata-ci-sign'
|
||||
fs.mkdirSync(certDir, { recursive: true })
|
||||
if (fs.existsSync(p12Path) && fs.statSync(p12Path).size > 100) {
|
||||
return { p12Path, password }
|
||||
}
|
||||
|
||||
log('generating self-signed signing cert for rcodesign…')
|
||||
const gen = spawnSync(
|
||||
bin,
|
||||
[
|
||||
'generate-self-signed-certificate',
|
||||
'--p12-file',
|
||||
p12Path,
|
||||
'--p12-password',
|
||||
password,
|
||||
'--person-name',
|
||||
'peardata-ci',
|
||||
'--country-name',
|
||||
'US',
|
||||
'--validity-days',
|
||||
'3650',
|
||||
'--team-id',
|
||||
'NONE',
|
||||
'--profile',
|
||||
'developer-id-application',
|
||||
],
|
||||
{ encoding: 'utf8', stdio: 'pipe' }
|
||||
)
|
||||
if (gen.status !== 0 || !fs.existsSync(p12Path)) {
|
||||
const gen2 = spawnSync(
|
||||
bin,
|
||||
[
|
||||
'generate-self-signed-certificate',
|
||||
'--p12-file',
|
||||
p12Path,
|
||||
'--p12-password',
|
||||
password,
|
||||
'--person-name',
|
||||
'peardata-ci',
|
||||
'--country-name',
|
||||
'US',
|
||||
'--validity-days',
|
||||
'3650',
|
||||
],
|
||||
{ encoding: 'utf8', stdio: 'pipe' }
|
||||
)
|
||||
if (gen2.status !== 0 || !fs.existsSync(p12Path)) {
|
||||
throw new Error(
|
||||
`rcodesign generate-self-signed-certificate failed:\n` +
|
||||
`${gen.stderr || gen.stdout}\n${gen2.stderr || gen2.stdout}`
|
||||
)
|
||||
}
|
||||
}
|
||||
log('wrote', p12Path)
|
||||
return { p12Path, password }
|
||||
}
|
||||
|
||||
function codesignRcodesign(targetPath, id) {
|
||||
const bin = which('rcodesign')
|
||||
if (!bin) {
|
||||
throw new Error(
|
||||
'rcodesign not found (needed to sign macOS artifacts on Linux). ' +
|
||||
'CI installs tools/rcodesign/<host>/rcodesign, or build/sign on macOS.'
|
||||
)
|
||||
}
|
||||
if (id !== '-') {
|
||||
log(
|
||||
'WARN: Linux rcodesign path uses self-signed cert only; ' +
|
||||
'Developer ID needs codesign on macOS + Apple certs'
|
||||
)
|
||||
}
|
||||
|
||||
const entitlements = ensureEntitlements()
|
||||
const { p12Path, password } = ensureRcodesignSelfSignedP12(bin)
|
||||
|
||||
const attempts = [
|
||||
[
|
||||
'sign',
|
||||
'--p12-file',
|
||||
p12Path,
|
||||
'--p12-password',
|
||||
password,
|
||||
'--code-signature-flags',
|
||||
'runtime',
|
||||
'--entitlements-xml-file',
|
||||
entitlements,
|
||||
targetPath,
|
||||
],
|
||||
[
|
||||
'sign',
|
||||
'--p12-file',
|
||||
p12Path,
|
||||
'--p12-password',
|
||||
password,
|
||||
'--code-signature-flags',
|
||||
'runtime',
|
||||
targetPath,
|
||||
],
|
||||
['sign', '--p12-file', p12Path, '--p12-password', password, targetPath],
|
||||
]
|
||||
|
||||
let lastErr = ''
|
||||
for (const args of attempts) {
|
||||
log(
|
||||
'rcodesign',
|
||||
args.map((a) => (a === password ? '***' : a)).join(' ')
|
||||
)
|
||||
const r = spawnSync(bin, args, { encoding: 'utf8', stdio: 'pipe' })
|
||||
if (r.status === 0) {
|
||||
log('rcodesign sign complete (self-signed / sealed)')
|
||||
return
|
||||
}
|
||||
lastErr += `${r.stderr || r.stdout || ''}\n`
|
||||
}
|
||||
throw new Error(`rcodesign failed:\n${lastErr}`)
|
||||
}
|
||||
|
||||
function codesignDarwinBinary(binPath, id) {
|
||||
const entitlements = ensureEntitlements()
|
||||
const hardened = id !== '-'
|
||||
|
||||
try {
|
||||
execFileSync('xattr', ['-cr', binPath], { stdio: 'pipe' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
log(`codesign binary identity=${id === '-' ? 'ad-hoc' : id}`)
|
||||
const args = ['--force', '--sign', id, '--entitlements', entitlements]
|
||||
if (hardened) args.push('--options', 'runtime', '--timestamp')
|
||||
else args.push('--timestamp=none')
|
||||
args.push('--identifier', 'com.peardata.server', binPath)
|
||||
|
||||
const r = spawnSync('codesign', args, { encoding: 'utf8' })
|
||||
if (r.status !== 0) {
|
||||
throw new Error(`codesign failed for binary:\n${r.stderr || r.stdout}`)
|
||||
}
|
||||
|
||||
const v = spawnSync('codesign', ['--verify', '--strict', '--verbose=2', binPath], {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
if (v.status !== 0) {
|
||||
throw new Error(`codesign verify failed for binary:\n${v.stderr || v.stdout}`)
|
||||
}
|
||||
log('verify ok:', (v.stderr || v.stdout || '').trim().split('\n').slice(0, 3).join(' | '))
|
||||
}
|
||||
|
||||
function signBinary(binPath) {
|
||||
if (!fs.existsSync(binPath)) throw new Error(`Binary not found: ${binPath}`)
|
||||
const id = identity()
|
||||
log('binary:', binPath)
|
||||
log('identity:', id === '-' ? 'ad-hoc (-)' : id)
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
codesignDarwinBinary(binPath, id)
|
||||
try {
|
||||
execFileSync('xattr', ['-cr', binPath], { stdio: 'pipe' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
codesignRcodesign(binPath, id)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function signApp(appPath) {
|
||||
if (!fs.existsSync(appPath)) throw new Error(`App not found: ${appPath}`)
|
||||
const id = identity()
|
||||
log('app:', appPath)
|
||||
log('identity:', id === '-' ? 'ad-hoc (-)' : id)
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
if (id === '-') {
|
||||
codesignDarwin(appPath, id)
|
||||
try {
|
||||
execFileSync('xattr', ['-cr', appPath], { stdio: 'pipe' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
try {
|
||||
const { signAsync } = require('@electron/osx-sign')
|
||||
return signAsync({
|
||||
app: appPath,
|
||||
identity: id,
|
||||
platform: 'darwin',
|
||||
hardenedRuntime: true,
|
||||
gatekeeperAssess: false,
|
||||
optionsForFile: () => ({
|
||||
entitlements: ensureEntitlements(),
|
||||
hardenedRuntime: true,
|
||||
}),
|
||||
}).then(() => {
|
||||
try {
|
||||
execFileSync('xattr', ['-cr', appPath], { stdio: 'pipe' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
const v = spawnSync(
|
||||
'codesign',
|
||||
['--verify', '--deep', '--strict', '--verbose=2', appPath],
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
if (v.status !== 0) {
|
||||
log('osx-sign verify soft-fail, falling back to codesign deep…')
|
||||
codesignDarwin(appPath, id)
|
||||
} else {
|
||||
log('osx-sign + verify ok')
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
log('osx-sign unavailable or failed, using codesign:', err.message || err)
|
||||
codesignDarwin(appPath, id)
|
||||
try {
|
||||
execFileSync('xattr', ['-cr', appPath], { stdio: 'pipe' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
codesignRcodesign(appPath, id)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const input = process.argv[2]
|
||||
if (!input) {
|
||||
console.error(
|
||||
'Usage: node scripts/sign-macos-app.cjs <path-to.app|peardata-server|dir>'
|
||||
)
|
||||
process.exit(2)
|
||||
}
|
||||
const resolved = path.resolve(input)
|
||||
if (!fs.existsSync(resolved)) {
|
||||
console.error('Path not found:', resolved)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const apps = findApps(resolved)
|
||||
const bins = findServerBinaries(resolved)
|
||||
|
||||
if (!apps.length && !bins.length) {
|
||||
console.error('No .app or peardata-server binary found at', resolved)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
for (const app of apps) {
|
||||
await signApp(app)
|
||||
}
|
||||
for (const bin of bins) {
|
||||
if (apps.some((a) => bin === a || bin.startsWith(a + path.sep))) continue
|
||||
await signBinary(bin)
|
||||
}
|
||||
log('done')
|
||||
}
|
||||
|
||||
module.exports = { signApp, signBinary, findApps, findServerBinaries, identity }
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((err) => {
|
||||
console.error('[sign-macos] FAILED:', err.message || err)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user