Updates
This commit is contained in:
@@ -38,7 +38,14 @@ jobs:
|
|||||||
npm install --no-audit --no-fund --loglevel=info
|
npm install --no-audit --no-fund --loglevel=info
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
|
env:
|
||||||
|
SKIP_INTEGRATION: '1'
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
||||||
- name: Healthcheck script loads
|
- name: Syntax check packaging entrypoints
|
||||||
run: node scripts/healthcheck.js
|
run: |
|
||||||
|
node --check bin/peardata-server.mjs
|
||||||
|
node --check scripts/make.cjs
|
||||||
|
node --check scripts/bare-standalone.cjs
|
||||||
|
node --check forge.config.cjs
|
||||||
|
node --check electron/main.cjs
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
# Rolling release for Gitea (mirrors peardock-style forge pipelines).
|
# peardata rolling release — Linux Bare servers + Electron clients (all 64-bit hosts).
|
||||||
# Runs on every push to main/master — always rebuilds and republishes the `rolling` tag.
|
|
||||||
#
|
#
|
||||||
# Secrets:
|
# Secrets:
|
||||||
# RELEASE_TOKEN — Gitea PAT with repo release write (required)
|
# RELEASE_TOKEN — Gitea PAT with repo release write (required)
|
||||||
# GITEA_URL — optional forge base URL (defaults to origin / GITHUB_SERVER_URL)
|
# GITEA_URL — optional forge base URL
|
||||||
|
#
|
||||||
|
# Server hosts: linux-x64, linux-arm64 only
|
||||||
|
# Client hosts: linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64, win32-arm64
|
||||||
name: Release rolling
|
name: Release rolling
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -11,6 +13,18 @@ on:
|
|||||||
branches: [main, master]
|
branches: [main, master]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
|
skip_client:
|
||||||
|
description: 'Skip Electron client builds (server only)'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
server_hosts:
|
||||||
|
description: 'Comma-separated Linux server hosts'
|
||||||
|
required: false
|
||||||
|
default: 'linux-x64,linux-arm64'
|
||||||
|
client_hosts:
|
||||||
|
description: 'Comma-separated 64-bit client hosts'
|
||||||
|
required: false
|
||||||
|
default: 'linux-x64,linux-arm64,darwin-x64,darwin-arm64,win32-x64,win32-arm64'
|
||||||
dry_run:
|
dry_run:
|
||||||
description: 'Build artifacts without uploading'
|
description: 'Build artifacts without uploading'
|
||||||
required: false
|
required: false
|
||||||
@@ -26,29 +40,89 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 45
|
timeout-minutes: 180
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js 22
|
- name: Use Node.js 22
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
timeout-minutes: 5
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Install + test
|
- name: Install system deps (electron + cross packaging)
|
||||||
|
timeout-minutes: 8
|
||||||
|
env:
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
APT_OPTS=(
|
||||||
|
-o Acquire::ForceIPv4=true
|
||||||
|
-o Acquire::Retries=3
|
||||||
|
-o Acquire::http::Timeout=30
|
||||||
|
-o Acquire::https::Timeout=30
|
||||||
|
)
|
||||||
|
if [ -f /etc/apt/sources.list.d/microsoft-prod.list ]; then
|
||||||
|
sudo mv /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/microsoft-prod.list.bak || true
|
||||||
|
fi
|
||||||
|
sudo apt-get update -qq "${APT_OPTS[@]}" || true
|
||||||
|
sudo apt-get install -y -qq "${APT_OPTS[@]}" \
|
||||||
|
libnss3 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libgtk-3-0 \
|
||||||
|
libgbm1 \
|
||||||
|
libasound2t64 \
|
||||||
|
zip \
|
||||||
|
unzip \
|
||||||
|
ca-certificates
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
env:
|
env:
|
||||||
|
NODE_OPTIONS: '--dns-result-order=ipv4first'
|
||||||
GIT_TERMINAL_PROMPT: '0'
|
GIT_TERMINAL_PROMPT: '0'
|
||||||
|
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
|
||||||
npm_config_fetch_retries: '3'
|
npm_config_fetch_retries: '3'
|
||||||
npm_config_fetch_timeout: '120000'
|
npm_config_fetch_timeout: '120000'
|
||||||
|
npm_config_fund: 'false'
|
||||||
|
npm_config_audit: 'false'
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
git config --global url."https://github.com/".insteadOf "ssh://[email protected]/"
|
git config --global url."https://github.com/".insteadOf "ssh://[email protected]/"
|
||||||
git config --global url."https://github.com/".insteadOf "[email protected]:"
|
git config --global url."https://github.com/".insteadOf "[email protected]:"
|
||||||
npm install --no-audit --no-fund
|
echo "==> npm ci (ignore lifecycle scripts — avoid electron hangs)"
|
||||||
SKIP_INTEGRATION=1 npm test
|
npm ci --ignore-scripts --no-audit --no-fund --loglevel=info
|
||||||
|
echo "==> esbuild binary"
|
||||||
|
node node_modules/esbuild/install.js
|
||||||
|
node -e "console.log('node', process.version); console.log('esbuild', require('esbuild').version)"
|
||||||
|
|
||||||
- name: Build + publish rolling release
|
- name: Unit tests
|
||||||
|
timeout-minutes: 10
|
||||||
|
env:
|
||||||
|
SKIP_INTEGRATION: '1'
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Install rcodesign (macOS codesign on Linux)
|
||||||
|
timeout-minutes: 2
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ARCH="$(uname -m)"
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64|amd64) HOST=linux-x64 ;;
|
||||||
|
aarch64|arm64) HOST=linux-arm64 ;;
|
||||||
|
*) echo "unsupported arch $ARCH for rcodesign"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
SRC="tools/rcodesign/${HOST}/rcodesign"
|
||||||
|
if [ ! -f "$SRC" ]; then
|
||||||
|
echo "ERROR: vendored rcodesign missing at $SRC"
|
||||||
|
echo "See tools/rcodesign/README.md"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sudo install -m 0755 "$SRC" /usr/local/bin/rcodesign
|
||||||
|
echo "Installed from $SRC ($(cat "tools/rcodesign/${HOST}/VERSION" 2>/dev/null || echo unknown))"
|
||||||
|
rcodesign --version
|
||||||
|
|
||||||
|
- name: Build all hosts + publish rolling release
|
||||||
env:
|
env:
|
||||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
GITEA_URL: ${{ secrets.GITEA_URL }}
|
GITEA_URL: ${{ secrets.GITEA_URL }}
|
||||||
@@ -56,8 +130,20 @@ jobs:
|
|||||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||||
GITHUB_SHA: ${{ github.sha }}
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
GITEA_SHA: ${{ github.sha }}
|
GITEA_SHA: ${{ github.sha }}
|
||||||
RELEASE_TAG: rolling
|
NODE_OPTIONS: '--dns-result-order=ipv4first'
|
||||||
|
PEARDATA_SERVER_HOSTS: ${{ github.event.inputs.server_hosts || 'linux-x64,linux-arm64' }}
|
||||||
|
PEARDATA_CLIENT_HOSTS: ${{ github.event.inputs.client_hosts || 'linux-x64,linux-arm64,darwin-x64,darwin-arm64,win32-x64,win32-arm64' }}
|
||||||
|
PEARDATA_SKIP_CLIENT: ${{ github.event.inputs.skip_client == 'true' && '1' || '0' }}
|
||||||
DRY_RUN: ${{ github.event.inputs.dry_run == 'true' && '1' || '0' }}
|
DRY_RUN: ${{ github.event.inputs.dry_run == 'true' && '1' || '0' }}
|
||||||
|
RELEASE_TAG: rolling
|
||||||
|
npm_config_build_from_source: 'false'
|
||||||
|
PEARDATA_SKIP_REBUILD: '1'
|
||||||
|
ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron
|
||||||
|
electron_config_cache: ${{ github.workspace }}/.cache/electron
|
||||||
|
PEARDATA_ELECTRON_DOWNLOAD_TIMEOUT_MS: '180000'
|
||||||
|
PEARDATA_CLIENT_TIMEOUT_MS: '480000'
|
||||||
|
ELECTRON_GET_USE_PROXY: '0'
|
||||||
|
CI: 'true'
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
if [ "${DRY_RUN:-0}" != "1" ] && [ -z "${RELEASE_TOKEN:-}" ]; then
|
if [ "${DRY_RUN:-0}" != "1" ] && [ -z "${RELEASE_TOKEN:-}" ]; then
|
||||||
@@ -71,5 +157,9 @@ jobs:
|
|||||||
if [ -z "${GITEA_URL:-}" ]; then
|
if [ -z "${GITEA_URL:-}" ]; then
|
||||||
export GITEA_URL="${GITHUB_SERVER_URL:-}"
|
export GITEA_URL="${GITHUB_SERVER_URL:-}"
|
||||||
fi
|
fi
|
||||||
chmod +x scripts/gitea-rolling-release.sh scripts/release.sh
|
mkdir -p "${ELECTRON_CACHE:-$GITHUB_WORKSPACE/.cache/electron}"
|
||||||
|
mkdir -p "$GITHUB_WORKSPACE/.cache/electron-zips"
|
||||||
|
echo "Server hosts (Linux only): $PEARDATA_SERVER_HOSTS"
|
||||||
|
echo "Client hosts: $PEARDATA_CLIENT_HOSTS"
|
||||||
|
chmod +x scripts/gitea-rolling-release.sh scripts/bare-standalone.cjs scripts/make.cjs scripts/predownload-electron.cjs
|
||||||
bash scripts/gitea-rolling-release.sh
|
bash scripts/gitea-rolling-release.sh
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Versioned GitHub Release — same host matrix as Gitea rolling (Linux servers + all client arches).
|
||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -5,6 +6,10 @@ on:
|
|||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
|
skip_client:
|
||||||
|
description: 'Skip Electron client builds (server only)'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
dry_run:
|
dry_run:
|
||||||
description: 'Build artifacts without uploading'
|
description: 'Build artifacts without uploading'
|
||||||
required: false
|
required: false
|
||||||
@@ -13,10 +18,17 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: release-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--dns-result-order=ipv4first'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 180
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -24,27 +36,73 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Install + test
|
- name: Install system deps
|
||||||
run: |
|
timeout-minutes: 8
|
||||||
npm install --no-audit --no-fund
|
env:
|
||||||
SKIP_INTEGRATION=1 npm test
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
|
||||||
- name: Pack source tarball
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
VERSION="${GITHUB_REF_NAME:-manual}"
|
APT_OPTS=(-o Acquire::ForceIPv4=true -o Acquire::Retries=3)
|
||||||
NAME="peardata-${VERSION}"
|
sudo apt-get update -qq "${APT_OPTS[@]}" || true
|
||||||
mkdir -p dist
|
sudo apt-get install -y -qq "${APT_OPTS[@]}" \
|
||||||
tar --exclude=node_modules --exclude=.git --exclude=data --exclude=dist \
|
libnss3 libatk-bridge2.0-0 libgtk-3-0 libgbm1 libasound2t64 zip unzip ca-certificates
|
||||||
-czf "dist/${NAME}.tar.gz" .
|
|
||||||
(cd dist && sha256sum "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256")
|
- name: Install dependencies
|
||||||
ls -la dist
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
|
||||||
|
GIT_TERMINAL_PROMPT: '0'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config --global url."https://github.com/".insteadOf "ssh://[email protected]/"
|
||||||
|
git config --global url."https://github.com/".insteadOf "[email protected]:"
|
||||||
|
npm ci --ignore-scripts --no-audit --no-fund
|
||||||
|
node node_modules/esbuild/install.js
|
||||||
|
|
||||||
|
- name: Unit tests
|
||||||
|
env:
|
||||||
|
SKIP_INTEGRATION: '1'
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Install rcodesign
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ARCH="$(uname -m)"
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64|amd64) HOST=linux-x64 ;;
|
||||||
|
aarch64|arm64) HOST=linux-arm64 ;;
|
||||||
|
*) exit 1 ;;
|
||||||
|
esac
|
||||||
|
sudo install -m 0755 "tools/rcodesign/${HOST}/rcodesign" /usr/local/bin/rcodesign
|
||||||
|
rcodesign --version
|
||||||
|
|
||||||
|
- name: Build server + client archives
|
||||||
|
env:
|
||||||
|
PEARDATA_SERVER_HOSTS: linux-x64,linux-arm64
|
||||||
|
PEARDATA_CLIENT_HOSTS: linux-x64,linux-arm64,darwin-x64,darwin-arm64,win32-x64,win32-arm64
|
||||||
|
PEARDATA_SKIP_CLIENT: ${{ github.event.inputs.skip_client == 'true' && '1' || '0' }}
|
||||||
|
DRY_RUN: '1'
|
||||||
|
RELEASE_TAG: ${{ github.ref_name || 'manual' }}
|
||||||
|
npm_config_build_from_source: 'false'
|
||||||
|
PEARDATA_SKIP_REBUILD: '1'
|
||||||
|
ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron
|
||||||
|
electron_config_cache: ${{ github.workspace }}/.cache/electron
|
||||||
|
PEARDATA_CLIENT_TIMEOUT_MS: '480000'
|
||||||
|
CI: 'true'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p "$ELECTRON_CACHE" .cache/electron-zips
|
||||||
|
chmod +x scripts/gitea-rolling-release.sh scripts/bare-standalone.cjs scripts/make.cjs
|
||||||
|
# Stage into dist/release without uploading (DRY_RUN=1)
|
||||||
|
bash scripts/gitea-rolling-release.sh
|
||||||
|
ls -la dist/release
|
||||||
|
|
||||||
- name: Upload GitHub Release
|
- name: Upload GitHub Release
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.event.inputs.dry_run != 'true'
|
if: startsWith(github.ref, 'refs/tags/') && github.event.inputs.dry_run != 'true'
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: dist/*
|
files: dist/release/*
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
body_path: dist/release/RELEASE_NOTES.md
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ tmp-test-data/
|
|||||||
tmp-hyperdb-test/
|
tmp-hyperdb-test/
|
||||||
*.seed
|
*.seed
|
||||||
.cache/
|
.cache/
|
||||||
|
electron/app.bundle.cjs
|
||||||
|
electron/app.bundle.cjs.map
|
||||||
tmp/
|
tmp/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ peardata/
|
|||||||
|--------|---------|
|
|--------|---------|
|
||||||
| `npm start` | Pear desktop UI |
|
| `npm start` | Pear desktop UI |
|
||||||
| `npm run start:server` | PearMonitor agent (P2P + REST) |
|
| `npm run start:server` | PearMonitor agent (P2P + REST) |
|
||||||
|
| `npm run start:client` | Electron desktop (packaged UI) |
|
||||||
|
| `npm run make` | Build Linux servers + all client arches |
|
||||||
|
| `npm run make:server` | Bare `peardata-server` (`linux-x64` / `linux-arm64`) |
|
||||||
|
| `npm run make:client` | Electron clients (all 64-bit hosts) |
|
||||||
| `npm test` | brittle unit + integration |
|
| `npm test` | brittle unit + integration |
|
||||||
| `npm run mint-invite -- [role]` | Offline `pd1.` invite |
|
| `npm run mint-invite -- [role]` | Offline `pd1.` invite |
|
||||||
| `npm run build:db` | Regenerate HyperDB `spec/` |
|
| `npm run build:db` | Regenerate HyperDB `spec/` |
|
||||||
@@ -107,8 +111,10 @@ peardata/
|
|||||||
| [Protocol](./docs/PROTOCOL.md) | RPC methods & pushes |
|
| [Protocol](./docs/PROTOCOL.md) | RPC methods & pushes |
|
||||||
| [Data model](./docs/DATA-MODEL.md) | Metrics, anomalies, health |
|
| [Data model](./docs/DATA-MODEL.md) | Metrics, anomalies, health |
|
||||||
| [REST API](./docs/REST-API.md) | `/api/v1\|v2\|v3` |
|
| [REST API](./docs/REST-API.md) | `/api/v1\|v2\|v3` |
|
||||||
| [Tech choices](./docs/TECH-CHOICES.md) | Collector, charts, libraries |
|
| [Tech choices](./docs/TECH-CHOICES.md) | Collector, charts, Bare maps |
|
||||||
| [HyperDB storage](./docs/STORAGE-HYPERDB.md) | Warm history, peer links, swarm sync |
|
| [HyperDB storage](./docs/STORAGE-HYPERDB.md) | Warm history, peer links, swarm sync |
|
||||||
|
| [Release](./docs/RELEASE.md) | Binary host matrix + rolling CI |
|
||||||
|
| [CI](./docs/CI.md) | Gitea / GitHub pipelines |
|
||||||
| [Security](./docs/SECURITY.md) | Threat model & hardening |
|
| [Security](./docs/SECURITY.md) | Threat model & hardening |
|
||||||
| [Configuration](./docs/CONFIGURATION.md) | Environment reference |
|
| [Configuration](./docs/CONFIGURATION.md) | Environment reference |
|
||||||
|
|
||||||
|
|||||||
+57
-58
@@ -1,81 +1,80 @@
|
|||||||
# CI & pipelines
|
# CI & pipelines
|
||||||
|
|
||||||
## GitHub Actions
|
PearData mirrors the PearDock-style **cross-compile from one Linux runner** pipeline: Bare servers + Electron clients, published as rolling / tagged releases.
|
||||||
|
|
||||||
| Workflow | Trigger | Jobs |
|
## Host matrix
|
||||||
|----------|---------|------|
|
|
||||||
| `.github/workflows/ci.yml` | push / PR / manual | **test** (Node 20 + 22 matrix), **lint-docs** |
|
|
||||||
| `.github/workflows/release.yml` | `v*` tags / manual | install, test (`SKIP_INTEGRATION=1`), pack tarball + sha256, GitHub Release |
|
|
||||||
|
|
||||||
### CI job details (`ci.yml`)
|
| Product | Hosts |
|
||||||
|
|---------|-------|
|
||||||
**test**
|
| Server (Bare) | `linux-x64`, `linux-arm64` **only** |
|
||||||
|
| Client (Electron) | `linux-x64`, `linux-arm64`, `darwin-x64`, `darwin-arm64`, `win32-x64`, `win32-arm64` |
|
||||||
- `actions/checkout@v4` + `setup-node` matrix `20` / `22`
|
|
||||||
- HTTPS rewrite for GitHub git deps
|
|
||||||
- `npm install --no-audit --no-fund`
|
|
||||||
- `npm test` (integration enabled by default)
|
|
||||||
- `node --check` on key entrypoints: `server/server.js`, `client/connection.js`, `app.js`, `shared/crypto-auth.js`
|
|
||||||
- `NODE_OPTIONS=--dns-result-order=ipv4first`
|
|
||||||
|
|
||||||
**lint-docs**
|
|
||||||
|
|
||||||
- Asserts required documentation files exist (README, architecture, protocol, data model, REST API, roadmap, tech choices, getting started, security, desktop, configuration, LICENSE, etc.)
|
|
||||||
|
|
||||||
### Release job details (`release.yml`)
|
|
||||||
|
|
||||||
- Node 22
|
|
||||||
- `SKIP_INTEGRATION=1 npm test` (avoids flaky UDP on some runners)
|
|
||||||
- Source tarball under `dist/`
|
|
||||||
- Upload via `softprops/action-gh-release` when ref is a tag
|
|
||||||
|
|
||||||
## Gitea Actions
|
## Gitea Actions
|
||||||
|
|
||||||
| Workflow | Trigger | Job |
|
| Workflow | Trigger | Job |
|
||||||
|----------|---------|-----|
|
|----------|---------|-----|
|
||||||
| `.gitea/workflows/ci.yml` | push / PR / manual | install, `npm test`, `node scripts/healthcheck.js` (liveness) |
|
| `.gitea/workflows/ci.yml` | push / PR / manual | install, `npm test` |
|
||||||
| `.gitea/workflows/release-rolling.yml` | **every** push to `main`/`master` + manual | test, `scripts/gitea-rolling-release.sh` → Gitea `rolling` prerelease |
|
| `.gitea/workflows/release-rolling.yml` | **every** push to `main`/`master` + manual | test → build matrix → Gitea tag **`rolling`** |
|
||||||
|
|
||||||
Mirrors patterns from peardock-class forge pipelines (IPv4-first DNS, HTTPS rewrite for GitHub deps, always-on rolling release).
|
Rolling job (ubuntu-latest, ~180m timeout):
|
||||||
|
|
||||||
|
1. System deps for Electron packaging
|
||||||
|
2. `npm ci --ignore-scripts` + esbuild install
|
||||||
|
3. `SKIP_INTEGRATION=1 npm test`
|
||||||
|
4. Install vendored `tools/rcodesign` (darwin client seal-sign)
|
||||||
|
5. `scripts/gitea-rolling-release.sh`
|
||||||
|
- `make.cjs server` → `peardata-server-*.tar.gz`
|
||||||
|
- `make.cjs client` → `peardata-client-*.tar.gz`
|
||||||
|
- upload to `rolling` prerelease
|
||||||
|
|
||||||
|
Manual inputs: `skip_client`, `server_hosts`, `client_hosts`, `dry_run`.
|
||||||
|
|
||||||
|
**Secret:** `RELEASE_TOKEN` (required to publish). Optional `GITEA_URL`.
|
||||||
|
|
||||||
|
## GitHub Actions
|
||||||
|
|
||||||
|
| Workflow | Trigger | Jobs |
|
||||||
|
|----------|---------|------|
|
||||||
|
| `.github/workflows/ci.yml` | push / PR / manual | **test** (Node 20+22), **lint-docs** |
|
||||||
|
| `.github/workflows/release.yml` | `v*` tags / manual | Same binary matrix → GitHub Release assets |
|
||||||
|
|
||||||
## Local parity
|
## Local parity
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm test
|
SKIP_INTEGRATION=1 npm test
|
||||||
node --check server/server.js
|
|
||||||
node --check client/connection.js
|
# Server (Linux Bare)
|
||||||
node --check app.js
|
npm run make:server:linux-x64
|
||||||
node --check shared/crypto-auth.js
|
|
||||||
bash scripts/release.sh
|
# Client (Electron)
|
||||||
|
npm run make:client:linux-x64
|
||||||
|
|
||||||
|
# Full matrix (slow)
|
||||||
|
npm run make
|
||||||
|
|
||||||
|
# Stage release archives without upload
|
||||||
|
DRY_RUN=1 bash scripts/gitea-rolling-release.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Key scripts
|
||||||
|
|
||||||
|
| Script | Role |
|
||||||
|
|--------|------|
|
||||||
|
| `scripts/hosts.cjs` | Host lists (`SERVER_LINUX`, `ALL_64`) |
|
||||||
|
| `scripts/make.cjs` | Orchestrate server/client builds |
|
||||||
|
| `scripts/bare-standalone.cjs` | Pack Bare `peardata-server` |
|
||||||
|
| `scripts/build-client-bundle.cjs` | esbuild GUI → `electron/app.bundle.cjs` |
|
||||||
|
| `scripts/predownload-electron.cjs` | Pre-fetch Electron zips for CI |
|
||||||
|
| `scripts/sign-macos-app.cjs` | Darwin client codesign |
|
||||||
|
| `scripts/gitea-rolling-release.sh` | Build + stage + Gitea upload |
|
||||||
|
| `forge.config.cjs` | Electron Forge packaging |
|
||||||
|
|
||||||
## Integration tests in CI
|
## Integration tests in CI
|
||||||
|
|
||||||
Integration spins a real HyperDHT listener. If a runner blocks UDP/DHT:
|
Release workflows set `SKIP_INTEGRATION=1`. Unit CI may run integration when the runner allows DHT/UDP. See [TESTING.md](./TESTING.md).
|
||||||
|
|
||||||
```yaml
|
|
||||||
env:
|
|
||||||
SKIP_INTEGRATION: '1'
|
|
||||||
```
|
|
||||||
|
|
||||||
Unit tests (`crypto-auth`, `protocol`, `acl`) always run. See [TESTING.md](./TESTING.md).
|
|
||||||
|
|
||||||
## Secrets (forge)
|
|
||||||
|
|
||||||
| Secret | Used by |
|
|
||||||
|--------|---------|
|
|
||||||
| `GITHUB_TOKEN` | GitHub Release (automatic) |
|
|
||||||
| `RELEASE_TOKEN` | Gitea rolling release (**required** for publish) |
|
|
||||||
| `GITEA_URL` | Forge base URL (optional; defaults to runner `GITHUB_SERVER_URL`) |
|
|
||||||
|
|
||||||
Do not store `SERVER_SEED` in CI unless a dedicated deploy workflow needs it.
|
|
||||||
|
|
||||||
## Required docs check
|
|
||||||
|
|
||||||
When adding documentation, update the `lint-docs` step in `.github/workflows/ci.yml` so the file is asserted present.
|
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [RELEASE.md](./RELEASE.md)
|
- [RELEASE.md](./RELEASE.md)
|
||||||
- [TESTING.md](./TESTING.md)
|
- [TECH-CHOICES.md](./TECH-CHOICES.md) — Bare import maps
|
||||||
|
|||||||
+81
-82
@@ -1,106 +1,105 @@
|
|||||||
# Release process
|
# Release process
|
||||||
|
|
||||||
## Preconditions
|
## Host matrix
|
||||||
|
|
||||||
- [ ] `npm test` passes (use `SKIP_INTEGRATION=1` only if the runner cannot do DHT)
|
| Product | Toolchain | Hosts |
|
||||||
- [ ] Version bumped in `package.json`
|
|---------|-----------|-------|
|
||||||
- [ ] Docs updated if protocol / env / UX changed
|
| **Server** | Bare standalone (`scripts/bare-standalone.cjs`) | **`linux-x64`**, **`linux-arm64` only** |
|
||||||
- [ ] No secrets in the tree (`.env`, `data/`, identity files)
|
| **Client** | Electron Forge (`forge.config.cjs`) | `linux-x64`, `linux-arm64`, `darwin-x64`, `darwin-arm64`, `win32-x64`, `win32-arm64` |
|
||||||
- [ ] `git status` clean except intended changes
|
|
||||||
|
|
||||||
## Version & tag
|
Same layout as PearDock’s rolling pipeline, except PearData agents ship **Linux-only**.
|
||||||
|
|
||||||
|
## Local builds
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Bump version in package.json (semver)
|
npm install
|
||||||
# 2. Commit
|
|
||||||
git add package.json
|
|
||||||
git commit -m "Release vX.Y.Z"
|
|
||||||
|
|
||||||
# 3. Tag
|
# Everything CI builds
|
||||||
|
npm run make
|
||||||
|
|
||||||
|
# Or separately
|
||||||
|
npm run make:server # both Linux arches
|
||||||
|
npm run make:server:linux-x64
|
||||||
|
npm run make:client # all client hosts
|
||||||
|
npm run make:client:darwin-arm64
|
||||||
|
|
||||||
|
# Dev Electron UI (not Pear runtime)
|
||||||
|
npm run start:client
|
||||||
|
```
|
||||||
|
|
||||||
|
Outputs land under `out/`:
|
||||||
|
|
||||||
|
```
|
||||||
|
out/peardata-server-linux-x64/peardata-server
|
||||||
|
out/peardata-linux-x64/peardata-client
|
||||||
|
out/peardata-darwin-arm64/peardata.app # macOS
|
||||||
|
```
|
||||||
|
|
||||||
|
Env knobs:
|
||||||
|
|
||||||
|
| Env | Default | Meaning |
|
||||||
|
|-----|---------|---------|
|
||||||
|
| `PEARDATA_SERVER_HOSTS` | `linux-x64,linux-arm64` | Server host list |
|
||||||
|
| `PEARDATA_CLIENT_HOSTS` | all 64-bit | Client host list |
|
||||||
|
| `PEARDATA_SKIP_CLIENT=1` | off | Server-only release |
|
||||||
|
| `PEARDATA_SKIP_REBUILD=1` | on in CI | Skip `@electron/rebuild` |
|
||||||
|
| `DRY_RUN=1` | off | Stage archives, skip upload |
|
||||||
|
|
||||||
|
## CI / forge pipelines
|
||||||
|
|
||||||
|
| Forge | Workflow | Trigger | Output |
|
||||||
|
|-------|----------|---------|--------|
|
||||||
|
| **Gitea** | `.gitea/workflows/release-rolling.yml` | push `main`/`master` + manual | Build matrix → prerelease tag **`rolling`** |
|
||||||
|
| **GitHub** | `.github/workflows/release.yml` | `v*` tags + manual | Same matrix → GitHub Release assets |
|
||||||
|
| Both | `.gitea/workflows/ci.yml` / `.github/workflows/ci.yml` | PR / push | Unit tests |
|
||||||
|
|
||||||
|
Rolling script: [`scripts/gitea-rolling-release.sh`](../scripts/gitea-rolling-release.sh)
|
||||||
|
Orchestrator: [`scripts/make.cjs`](../scripts/make.cjs)
|
||||||
|
Hosts: [`scripts/hosts.cjs`](../scripts/hosts.cjs)
|
||||||
|
|
||||||
|
### Secrets
|
||||||
|
|
||||||
|
| Secret | Used by |
|
||||||
|
|--------|---------|
|
||||||
|
| `RELEASE_TOKEN` | Gitea rolling publish (**required**) |
|
||||||
|
| `GITEA_URL` | Optional forge base URL |
|
||||||
|
| `GITHUB_TOKEN` | GitHub Release (automatic) |
|
||||||
|
|
||||||
|
### macOS codesign
|
||||||
|
|
||||||
|
Darwin **clients** built on Linux are seal-signed with vendored `tools/rcodesign/` (see that README). Avoids Gatekeeper “damaged” false positives. Not notarized unless you set Developer ID credentials.
|
||||||
|
|
||||||
|
## Version & tag (semver)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Bump version in package.json
|
||||||
|
# 2. Commit + tag
|
||||||
git tag -a vX.Y.Z -m "vX.Y.Z"
|
git tag -a vX.Y.Z -m "vX.Y.Z"
|
||||||
|
|
||||||
# 4. Push
|
|
||||||
git push origin main
|
git push origin main
|
||||||
git push origin vX.Y.Z
|
git push origin vX.Y.Z
|
||||||
```
|
```
|
||||||
|
|
||||||
Tag pattern **`v*`** triggers versioned GitHub releases. On Gitea, **every push to `main`/`master`** rebuilds the rolling release:
|
- **Gitea:** every `main` push republishes `rolling` (latest main binaries).
|
||||||
|
- **GitHub:** `v*` tags upload versioned archives from the release workflow.
|
||||||
|
|
||||||
| Forge | Workflow | Trigger | Output |
|
## Source-only tarball (optional)
|
||||||
|-------|----------|---------|--------|
|
|
||||||
| GitHub | `.github/workflows/release.yml` | `v*` tags / manual | Source tarball + sha256 → GitHub Release |
|
|
||||||
| Gitea | `.gitea/workflows/release-rolling.yml` | push to `main`/`master` / manual | `scripts/gitea-rolling-release.sh` → prerelease tag **`rolling`** |
|
|
||||||
|
|
||||||
The rolling job always runs after a successful install+test on the default branch. It deletes and recreates the `rolling` tag/release so operators can always download the latest main build.
|
|
||||||
|
|
||||||
## Local artifacts
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash scripts/release.sh
|
bash scripts/release.sh # dist/peardata-vX.Y.Z.tar.gz (source tree)
|
||||||
```
|
```
|
||||||
|
|
||||||
Produces:
|
Prefer the binary matrix for operators.
|
||||||
|
|
||||||
```
|
|
||||||
dist/
|
|
||||||
├── peardata-vX.Y.Z.tar.gz
|
|
||||||
├── peardata-vX.Y.Z.tar.gz.sha256
|
|
||||||
└── RELEASE_NOTES.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd dist
|
|
||||||
sha256sum -c peardata-vX.Y.Z.tar.gz.sha256
|
|
||||||
# or: shasum -a 256 -c …
|
|
||||||
```
|
|
||||||
|
|
||||||
Tarball **excludes** `node_modules/`, `.git/`, `data/`, `dist/`.
|
|
||||||
|
|
||||||
## What ships
|
|
||||||
|
|
||||||
| Included | Not included |
|
|
||||||
|----------|--------------|
|
|
||||||
| Source (server, client, shared, UI) | `node_modules` |
|
|
||||||
| CI workflows | `.env` / secrets |
|
|
||||||
| Docs + systemd unit | Runtime `data/` |
|
|
||||||
| Scripts | Generated release dist |
|
|
||||||
|
|
||||||
Consumers install with `npm install` after unpacking.
|
|
||||||
|
|
||||||
## Changelog guidance
|
## Changelog guidance
|
||||||
|
|
||||||
For each release note:
|
1. **Protocol** — RPC / push changes
|
||||||
|
2. **Security** — auth / roles
|
||||||
1. **Protocol** — method / push / version bumps
|
3. **Desktop** — Electron / Pear UI
|
||||||
2. **Security** — auth or default role changes
|
4. **Ops** — env, systemd, collectors
|
||||||
3. **Desktop** — pear-ctrl / window / Pear dependency bumps
|
5. **Breaking** — re-dial / invite requirements
|
||||||
4. **Ops** — env vars, systemd, healthcheck
|
|
||||||
5. **Breaking** — call out re-dial / re-invite requirements
|
|
||||||
|
|
||||||
## Hotfix without retag
|
|
||||||
|
|
||||||
Push commits to `main` for CI only. Prefer a new patch tag for anything operators must download.
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
|
|
||||||
- **App code**: redeploy previous tag tarball / checkout
|
|
||||||
- **Server seed**: only if compromised — generate new seed (new public key); re-issue all invites; clients must dial the new key
|
|
||||||
- **Peer policy**: restore `data/peer-policy.json` from backup if revoke tables were corrupted
|
|
||||||
|
|
||||||
## Secrets for forges
|
|
||||||
|
|
||||||
| Secret | Where | Purpose |
|
|
||||||
|--------|-------|---------|
|
|
||||||
| `GITHUB_TOKEN` | GitHub (automatic) | Upload release assets |
|
|
||||||
| `RELEASE_TOKEN` | Gitea (**required**) | Publish/update `rolling` release |
|
|
||||||
| `GITEA_URL` | Gitea (optional) | Forge API base (defaults from runner) |
|
|
||||||
|
|
||||||
Never put `SERVER_SEED` in CI secrets unless a dedicated deploy job needs it — prefer generating seeds on the target host.
|
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [CI.md](./CI.md)
|
- [CI.md](./CI.md)
|
||||||
- [SECURITY.md](./SECURITY.md)
|
- [TECH-CHOICES.md](./TECH-CHOICES.md) (Bare import maps)
|
||||||
- [CONFIGURATION.md](./CONFIGURATION.md)
|
- PearDock reference: `docs/RELEASE.md` in the peardock repo
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
/**
|
||||||
|
* PearData desktop client — Electron shell.
|
||||||
|
*
|
||||||
|
* - Electron owns the window chrome
|
||||||
|
* - Renderer loads index.html over localhost; GUI code is require()'d as
|
||||||
|
* electron/app.bundle.cjs (esbuild CJS of app.js + local modules)
|
||||||
|
* - Pear polyfill provides config.storage, exit, teardown for UI code
|
||||||
|
*
|
||||||
|
* Dev: npm run start:client (builds bundle then launches)
|
||||||
|
* Pack: npm run make:client
|
||||||
|
*/
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { app, BrowserWindow, ipcMain, Menu } = require('electron')
|
||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
const http = require('http')
|
||||||
|
|
||||||
|
const pkg = require('../package.json')
|
||||||
|
const appName = pkg.productName || pkg.name || 'PearData'
|
||||||
|
|
||||||
|
// ---- CLI flags ----
|
||||||
|
const argv = app.isPackaged ? process.argv.slice(1) : process.argv.slice(2)
|
||||||
|
let storageOverride = null
|
||||||
|
let noSandbox = false
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
if (argv[i] === '--storage') storageOverride = argv[++i]
|
||||||
|
if (argv[i] === '--no-sandbox') noSandbox = true
|
||||||
|
}
|
||||||
|
if (noSandbox || process.platform === 'linux') {
|
||||||
|
app.commandLine.appendSwitch('no-sandbox')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storageOverride) {
|
||||||
|
app.setPath('userData', storageOverride)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = () => app.getPath('userData')
|
||||||
|
const storageDir = () => path.join(userData(), 'storage')
|
||||||
|
|
||||||
|
/** @type {import('http').Server|null} */
|
||||||
|
let staticServer = null
|
||||||
|
/** @type {number} */
|
||||||
|
let staticPort = 0
|
||||||
|
|
||||||
|
function ensureDir(p) {
|
||||||
|
fs.mkdirSync(p, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal static file server rooted at the app directory.
|
||||||
|
* index.html is rewritten so the GUI loads via require('…/electron/app.bundle.cjs').
|
||||||
|
*/
|
||||||
|
function startStaticServer(rootDir) {
|
||||||
|
const bundlePath = path.join(rootDir, 'electron', 'app.bundle.cjs')
|
||||||
|
if (!fs.existsSync(bundlePath)) {
|
||||||
|
throw new Error(
|
||||||
|
`electron/app.bundle.cjs not found at ${bundlePath}.\n` +
|
||||||
|
`Run: npm run build:client-bundle\n` +
|
||||||
|
`(packaged builds run this in the forge prePackage hook.)`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const mime = {
|
||||||
|
'.html': 'text/html; charset=utf-8',
|
||||||
|
'.js': 'text/javascript; charset=utf-8',
|
||||||
|
'.mjs': 'text/javascript; charset=utf-8',
|
||||||
|
'.cjs': 'text/javascript; charset=utf-8',
|
||||||
|
'.css': 'text/css; charset=utf-8',
|
||||||
|
'.json': 'application/json',
|
||||||
|
'.webmanifest': 'application/manifest+json',
|
||||||
|
'.xml': 'application/xml',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.woff': 'font/woff',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
'.map': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
try {
|
||||||
|
const u = new URL(req.url || '/', 'http://127.0.0.1')
|
||||||
|
let rel = decodeURIComponent(u.pathname)
|
||||||
|
if (rel === '/') rel = '/index.html'
|
||||||
|
const filePath = path.normalize(path.join(rootDir, rel))
|
||||||
|
if (!filePath.startsWith(rootDir)) {
|
||||||
|
res.writeHead(403)
|
||||||
|
res.end('Forbidden')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
|
||||||
|
res.writeHead(404)
|
||||||
|
res.end('Not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const ext = path.extname(filePath).toLowerCase()
|
||||||
|
|
||||||
|
if (rel === '/index.html' || filePath.endsWith(`${path.sep}index.html`)) {
|
||||||
|
let html = fs.readFileSync(filePath, 'utf8')
|
||||||
|
html = html.replace(
|
||||||
|
/<script\s+type=["']module["']\s+src=["'][^"']*app\.js["']\s*>\s*<\/script>/i,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
const boot = `
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var bundlePath = ${JSON.stringify(bundlePath)};
|
||||||
|
console.log('[peardata] loading GUI via require:', bundlePath);
|
||||||
|
try {
|
||||||
|
require(bundlePath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[peardata] failed to load app.bundle.cjs', err);
|
||||||
|
var el = document.createElement('pre');
|
||||||
|
el.style.cssText = 'color:#f88;padding:2rem;white-space:pre-wrap;font:14px monospace';
|
||||||
|
el.textContent = 'PearData failed to start:\\n' + (err && err.stack ? err.stack : err);
|
||||||
|
document.body.appendChild(el);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>`
|
||||||
|
if (html.includes('</body>')) {
|
||||||
|
html = html.replace('</body>', boot + '\n</body>')
|
||||||
|
} else {
|
||||||
|
html += boot
|
||||||
|
}
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'text/html; charset=utf-8',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
})
|
||||||
|
res.end(html)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': mime[ext] || 'application/octet-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
})
|
||||||
|
fs.createReadStream(filePath).pipe(res)
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500)
|
||||||
|
res.end(String(err && err.message ? err.message : err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(0, '127.0.0.1', () => {
|
||||||
|
const addr = server.address()
|
||||||
|
staticPort = typeof addr === 'object' && addr ? addr.port : 0
|
||||||
|
staticServer = server
|
||||||
|
resolve(staticPort)
|
||||||
|
})
|
||||||
|
server.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPreloadPath() {
|
||||||
|
return path.join(__dirname, 'preload.cjs')
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAppIcon() {
|
||||||
|
const appRoot = app.isPackaged ? app.getAppPath() : path.resolve(__dirname, '..')
|
||||||
|
const candidates = [
|
||||||
|
path.join(appRoot, 'build', process.platform === 'win32' ? 'icon.ico' : 'icon.png'),
|
||||||
|
path.join(appRoot, 'build', 'icon.png'),
|
||||||
|
]
|
||||||
|
for (const p of candidates) {
|
||||||
|
if (fs.existsSync(p)) return p
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shared window chrome: hiddenInset (macOS) / frameless (win/linux) + pear-ctrl. */
|
||||||
|
function windowChromeOpts() {
|
||||||
|
const isDarwin = process.platform === 'darwin'
|
||||||
|
return isDarwin
|
||||||
|
? {
|
||||||
|
titleBarStyle: 'hiddenInset',
|
||||||
|
trafficLightPosition: { x: 16, y: 13 },
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
frame: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultWebPreferences() {
|
||||||
|
return {
|
||||||
|
preload: buildPreloadPath(),
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: false,
|
||||||
|
sandbox: false,
|
||||||
|
spellcheck: false,
|
||||||
|
webSecurity: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
const icon = resolveAppIcon()
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: pkg.pear?.gui?.width || 1280,
|
||||||
|
height: pkg.pear?.gui?.height || 860,
|
||||||
|
minWidth: pkg.pear?.gui?.minWidth || 900,
|
||||||
|
minHeight: pkg.pear?.gui?.minHeight || 560,
|
||||||
|
backgroundColor: pkg.pear?.gui?.backgroundColor || '#0b1020',
|
||||||
|
title: appName,
|
||||||
|
...(icon ? { icon } : {}),
|
||||||
|
autoHideMenuBar: process.platform !== 'darwin',
|
||||||
|
...windowChromeOpts(),
|
||||||
|
webPreferences: defaultWebPreferences(),
|
||||||
|
show: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
win.once('ready-to-show', () => win.show())
|
||||||
|
|
||||||
|
const url = `http://127.0.0.1:${staticPort}/index.html`
|
||||||
|
win.loadURL(url).catch((err) => {
|
||||||
|
console.error('Failed to load UI:', err)
|
||||||
|
app.quit()
|
||||||
|
})
|
||||||
|
|
||||||
|
win.webContents.on('did-fail-load', (_e, code, desc) => {
|
||||||
|
console.error('did-fail-load', code, desc)
|
||||||
|
})
|
||||||
|
|
||||||
|
win.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))
|
||||||
|
|
||||||
|
return win
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC for Pear polyfill
|
||||||
|
ipcMain.on('peardata:get-pear-config', (evt) => {
|
||||||
|
evt.returnValue = {
|
||||||
|
storage: storageDir(),
|
||||||
|
name: appName,
|
||||||
|
version: pkg.version,
|
||||||
|
platform: process.platform,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('peardata:exit', () => {
|
||||||
|
app.quit()
|
||||||
|
})
|
||||||
|
|
||||||
|
function windowFromEvent(evt) {
|
||||||
|
try {
|
||||||
|
return BrowserWindow.fromWebContents(evt.sender)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusedWindow() {
|
||||||
|
return BrowserWindow.getFocusedWindow() || BrowserWindow.getAllWindows()[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('peardata:window-minimize', (evt) => {
|
||||||
|
;(windowFromEvent(evt) || focusedWindow())?.minimize()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('peardata:window-maximize', (evt) => {
|
||||||
|
const win = windowFromEvent(evt) || focusedWindow()
|
||||||
|
if (!win) return
|
||||||
|
if (win.isMaximized()) win.unmaximize()
|
||||||
|
else win.maximize()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('peardata:window-close', (evt) => {
|
||||||
|
;(windowFromEvent(evt) || focusedWindow())?.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('peardata:window-is-maximized', (evt) => {
|
||||||
|
return Boolean((windowFromEvent(evt) || focusedWindow())?.isMaximized())
|
||||||
|
})
|
||||||
|
|
||||||
|
const teardownFns = []
|
||||||
|
ipcMain.on('peardata:teardown-register', () => {
|
||||||
|
// Renderer registers via preload bridge; actual teardown on before-quit
|
||||||
|
})
|
||||||
|
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
ensureDir(storageDir())
|
||||||
|
|
||||||
|
const appRoot = app.isPackaged ? app.getAppPath() : path.resolve(__dirname, '..')
|
||||||
|
|
||||||
|
const dockIcon = resolveAppIcon()
|
||||||
|
if (dockIcon && process.platform === 'darwin' && app.dock) {
|
||||||
|
try {
|
||||||
|
app.dock.setIcon(dockIcon)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
Menu.setApplicationMenu(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
await startStaticServer(appRoot)
|
||||||
|
|
||||||
|
process.env.PEARDATA_STORAGE = storageDir()
|
||||||
|
process.env.PEARDATA_APP_NAME = appName
|
||||||
|
|
||||||
|
createWindow()
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') app.quit()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('before-quit', async () => {
|
||||||
|
if (staticServer) {
|
||||||
|
try {
|
||||||
|
staticServer.close()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const fn of teardownFns) {
|
||||||
|
try {
|
||||||
|
await fn()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const gotLock = app.requestSingleInstanceLock()
|
||||||
|
if (!gotLock) {
|
||||||
|
app.quit()
|
||||||
|
} else {
|
||||||
|
app.on('second-instance', () => {
|
||||||
|
const wins = BrowserWindow.getAllWindows()
|
||||||
|
if (wins[0]) {
|
||||||
|
if (wins[0].isMinimized()) wins[0].restore()
|
||||||
|
wins[0].focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Preload + Pear polyfill for the PearData GUI under Electron.
|
||||||
|
* Runs with contextIsolation: false / nodeIntegration: true so app.js can
|
||||||
|
* import HyperDHT packages (pear-electron UI equivalent).
|
||||||
|
*/
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { ipcRenderer } = require('electron')
|
||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
const os = require('os')
|
||||||
|
|
||||||
|
const cfg = ipcRenderer.sendSync('peardata:get-pear-config') || {}
|
||||||
|
const storage =
|
||||||
|
cfg.storage ||
|
||||||
|
process.env.PEARDATA_STORAGE ||
|
||||||
|
path.join(os.homedir(), '.config', 'peardata', 'storage')
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(storage, { recursive: true })
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const teardownHooks = []
|
||||||
|
|
||||||
|
/** Pear surface used by PearData UI */
|
||||||
|
const Pear = {
|
||||||
|
config: {
|
||||||
|
storage,
|
||||||
|
name: cfg.name || 'PearData',
|
||||||
|
version: cfg.version || '0.0.0',
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
storage,
|
||||||
|
name: cfg.name || 'PearData',
|
||||||
|
},
|
||||||
|
exit(code = 0) {
|
||||||
|
try {
|
||||||
|
ipcRenderer.invoke('peardata:exit', code)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
teardown(fn) {
|
||||||
|
if (typeof fn === 'function') teardownHooks.push(fn)
|
||||||
|
},
|
||||||
|
constructor: {
|
||||||
|
IPC: null,
|
||||||
|
UI: null,
|
||||||
|
CUTOVER: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.Pear = Pear
|
||||||
|
try {
|
||||||
|
window.Pear = Pear
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// pear-ctrl: platform chrome for frameless / hiddenInset titlebar
|
||||||
|
try {
|
||||||
|
if (typeof customElements !== 'undefined' && !customElements.get('pear-ctrl')) {
|
||||||
|
const platform =
|
||||||
|
(cfg && cfg.platform) ||
|
||||||
|
(typeof process !== 'undefined' && process.platform) ||
|
||||||
|
'darwin'
|
||||||
|
|
||||||
|
class PearCtrl extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
this.setAttribute('data-platform', platform)
|
||||||
|
this.setAttribute('role', 'group')
|
||||||
|
this.setAttribute('aria-label', 'Window controls')
|
||||||
|
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
this.replaceChildren()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.replaceChildren()
|
||||||
|
this.classList.add('pd-electron-ctrl')
|
||||||
|
const mkBtn = (action, label, pathD) => {
|
||||||
|
const btn = document.createElement('button')
|
||||||
|
btn.type = 'button'
|
||||||
|
btn.className = `pd-win-btn pd-win-btn--${action}`
|
||||||
|
btn.title = label
|
||||||
|
btn.setAttribute('aria-label', label)
|
||||||
|
btn.innerHTML = `<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><path fill="currentColor" d="${pathD}"/></svg>`
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
try {
|
||||||
|
ipcRenderer.invoke(`peardata:window-${action}`)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return btn
|
||||||
|
}
|
||||||
|
this.append(
|
||||||
|
mkBtn('minimize', 'Minimize', 'M0 5h10v1H0z'),
|
||||||
|
mkBtn('maximize', 'Maximize', 'M1 1h8v8H1V1zm1 1v6h6V2H2z'),
|
||||||
|
mkBtn(
|
||||||
|
'close',
|
||||||
|
'Close',
|
||||||
|
'M1.2 0.5L5 4.3 8.8.5l.7.7L5.7 5l3.8 3.8-.7.7L5 5.7 1.2 9.5l-.7-.7L4.3 5 .5 1.2l.7-.7z'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('pear-ctrl', PearCtrl)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
for (const fn of teardownHooks) {
|
||||||
|
try {
|
||||||
|
const r = fn()
|
||||||
|
if (r && typeof r.then === 'function') r.catch(() => {})
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('[peardata] Pear polyfill ready, storage=', storage)
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
/**
|
||||||
|
* Electron Forge config for peardata-client.
|
||||||
|
*
|
||||||
|
* CI note: asar keeps finalize fast; ignore list strips server/tooling deps.
|
||||||
|
* Only ship prebuilds for the package target platform/arch.
|
||||||
|
*/
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
const pkg = require('./package.json')
|
||||||
|
const appName = pkg.productName || pkg.name || 'PearData'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve packaging target from forge CLI args or env (set by make.cjs).
|
||||||
|
* @returns {{ platform: string, arch: string }}
|
||||||
|
*/
|
||||||
|
function resolvePackageTarget() {
|
||||||
|
const argv = process.argv
|
||||||
|
const flag = (name) => {
|
||||||
|
const i = argv.indexOf(name)
|
||||||
|
return i >= 0 && argv[i + 1] ? argv[i + 1] : null
|
||||||
|
}
|
||||||
|
const platform =
|
||||||
|
process.env.PEARDATA_PACKAGE_PLATFORM ||
|
||||||
|
flag('--platform') ||
|
||||||
|
process.platform
|
||||||
|
const arch =
|
||||||
|
process.env.PEARDATA_PACKAGE_ARCH || flag('--arch') || process.arch
|
||||||
|
return { platform, arch }
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageTarget = resolvePackageTarget()
|
||||||
|
const packageHost = `${packageTarget.platform}-${packageTarget.arch}`
|
||||||
|
|
||||||
|
/** Path prefixes (packager paths start with /) to exclude from the app bundle */
|
||||||
|
const IGNORE_PREFIXES = [
|
||||||
|
'/.git',
|
||||||
|
'/.gitea',
|
||||||
|
'/.github',
|
||||||
|
'/out',
|
||||||
|
'/dist',
|
||||||
|
'/deploy',
|
||||||
|
'/test',
|
||||||
|
'/docs',
|
||||||
|
'/spec',
|
||||||
|
'/server',
|
||||||
|
'/bin',
|
||||||
|
'/scripts',
|
||||||
|
'/tools',
|
||||||
|
'/.cache',
|
||||||
|
'/build/stubs',
|
||||||
|
'/build/shims',
|
||||||
|
'/README.md',
|
||||||
|
'/LICENSE',
|
||||||
|
// Packaging / Bare server toolchain (not needed at Electron runtime)
|
||||||
|
'/node_modules/electron',
|
||||||
|
'/node_modules/electron-',
|
||||||
|
'/node_modules/@electron',
|
||||||
|
'/node_modules/@electron-forge',
|
||||||
|
'/node_modules/bare-build',
|
||||||
|
'/node_modules/bare-runtime',
|
||||||
|
'/node_modules/bare-sidecar',
|
||||||
|
'/node_modules/bare-link',
|
||||||
|
'/node_modules/bare-lief',
|
||||||
|
'/node_modules/bare-apk',
|
||||||
|
'/node_modules/bare-app-image',
|
||||||
|
'/node_modules/bare-make',
|
||||||
|
'/node_modules/bare-pack',
|
||||||
|
'/node_modules/bare-dev',
|
||||||
|
'/node_modules/bare-bundle',
|
||||||
|
'/node_modules/bare-module-traverse',
|
||||||
|
'/node_modules/bare-sqlite',
|
||||||
|
'/node_modules/postject',
|
||||||
|
'/node_modules/@inquirer',
|
||||||
|
'/node_modules/terser',
|
||||||
|
'/node_modules/pear-runtime/',
|
||||||
|
'/node_modules/pear-electron',
|
||||||
|
// Heavy / unused tooling
|
||||||
|
'/node_modules/typescript',
|
||||||
|
'/node_modules/prettier',
|
||||||
|
'/node_modules/webpack',
|
||||||
|
'/node_modules/caniuse-lite',
|
||||||
|
'/node_modules/brittle',
|
||||||
|
'/node_modules/@types',
|
||||||
|
'/node_modules/esbuild',
|
||||||
|
'/node_modules/@esbuild',
|
||||||
|
'/node_modules/node-gyp',
|
||||||
|
'/node_modules/node-addon-api',
|
||||||
|
]
|
||||||
|
|
||||||
|
const IGNORE_REGEX = [
|
||||||
|
/^\/node_modules\/bare-build-/,
|
||||||
|
/^\/node_modules\/bare-runtime-/,
|
||||||
|
/^\/node_modules\/bare-pack-/,
|
||||||
|
/^\/node_modules\/@esbuild\//,
|
||||||
|
/\.md$/i,
|
||||||
|
/\.map$/,
|
||||||
|
/\.d\.ts$/,
|
||||||
|
/^\/peardata-.*\.json$/,
|
||||||
|
/^\/\.env$/,
|
||||||
|
/^\/package-lock\.json$/,
|
||||||
|
/^\/node_modules\/[^/]+\/test\//,
|
||||||
|
/^\/node_modules\/[^/]+\/tests\//,
|
||||||
|
/^\/node_modules\/[^/]+\/docs\//,
|
||||||
|
/^\/node_modules\/[^/]+\/example\//,
|
||||||
|
/^\/node_modules\/[^/]+\/examples\//,
|
||||||
|
/^\/node_modules\/[^/]+\/\.github\//,
|
||||||
|
]
|
||||||
|
|
||||||
|
function isForeignPrebuild(file) {
|
||||||
|
const marker = '/prebuilds/'
|
||||||
|
const idx = file.indexOf(marker)
|
||||||
|
if (idx === -1) return false
|
||||||
|
const host = file.slice(idx + marker.length).split('/')[0]
|
||||||
|
if (!host) return false
|
||||||
|
if (
|
||||||
|
host.startsWith('android') ||
|
||||||
|
host.startsWith('ios') ||
|
||||||
|
host.includes('simulator')
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return host !== packageHost
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIgnore(file) {
|
||||||
|
if (!file) return false
|
||||||
|
if (file === '/package.json') return false
|
||||||
|
if (file === '/electron/app.bundle.cjs') return false
|
||||||
|
if (file === '/electron/app.bundle.cjs.map') {
|
||||||
|
return process.env.PEARDATA_KEEP_SOURCEMAP !== '1'
|
||||||
|
}
|
||||||
|
for (const p of IGNORE_PREFIXES) {
|
||||||
|
if (file === p || file.startsWith(p + '/') || file.startsWith(p)) return true
|
||||||
|
}
|
||||||
|
for (const re of IGNORE_REGEX) {
|
||||||
|
if (re.test(file)) return true
|
||||||
|
}
|
||||||
|
if (isForeignPrebuild(file)) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const skipRebuild =
|
||||||
|
process.env.PEARDATA_FORCE_REBUILD !== '1' &&
|
||||||
|
process.env.PEARDATA_SKIP_REBUILD !== '0'
|
||||||
|
|
||||||
|
const electronZipDir = path.join(__dirname, '.cache', 'electron-zips')
|
||||||
|
const useElectronZipDir =
|
||||||
|
process.env.PEARDATA_USE_ELECTRON_ZIP_DIR !== '0' &&
|
||||||
|
fs.existsSync(electronZipDir)
|
||||||
|
|
||||||
|
function stripBuildPath(buildPath) {
|
||||||
|
const t0 = Date.now()
|
||||||
|
let removed = 0
|
||||||
|
|
||||||
|
function rm(rel) {
|
||||||
|
const p = path.join(buildPath, rel)
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(p)) {
|
||||||
|
fs.rmSync(p, { recursive: true, force: true })
|
||||||
|
removed++
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const junk = [
|
||||||
|
'node_modules/bare-sidecar',
|
||||||
|
'node_modules/electron',
|
||||||
|
'node_modules/@electron-forge',
|
||||||
|
'node_modules/bare-build',
|
||||||
|
'node_modules/bare-runtime',
|
||||||
|
'node_modules/pear-runtime',
|
||||||
|
'node_modules/pear-electron',
|
||||||
|
'node_modules/esbuild',
|
||||||
|
'node_modules/bare-sqlite',
|
||||||
|
'node_modules/postject',
|
||||||
|
'node_modules/@inquirer',
|
||||||
|
'node_modules/terser',
|
||||||
|
'server',
|
||||||
|
'bin',
|
||||||
|
'scripts',
|
||||||
|
'spec',
|
||||||
|
'deploy',
|
||||||
|
'out',
|
||||||
|
'test',
|
||||||
|
'docs',
|
||||||
|
'tools',
|
||||||
|
'.cache',
|
||||||
|
'README.md',
|
||||||
|
'LICENSE',
|
||||||
|
]
|
||||||
|
for (const rel of junk) rm(rel)
|
||||||
|
|
||||||
|
const nm = path.join(buildPath, 'node_modules')
|
||||||
|
if (fs.existsSync(nm)) {
|
||||||
|
const stack = [nm]
|
||||||
|
const seen = new Set()
|
||||||
|
while (stack.length) {
|
||||||
|
const dir = stack.pop()
|
||||||
|
let real
|
||||||
|
try {
|
||||||
|
real = fs.realpathSync(dir)
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (seen.has(real)) continue
|
||||||
|
seen.add(real)
|
||||||
|
let entries
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (const ent of entries) {
|
||||||
|
const full = path.join(dir, ent.name)
|
||||||
|
if (ent.isSymbolicLink()) continue
|
||||||
|
if (!ent.isDirectory()) continue
|
||||||
|
if (ent.name === 'prebuilds') {
|
||||||
|
let hosts
|
||||||
|
try {
|
||||||
|
hosts = fs.readdirSync(full)
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (const host of hosts) {
|
||||||
|
if (host === packageHost) continue
|
||||||
|
try {
|
||||||
|
fs.rmSync(path.join(full, host), { recursive: true, force: true })
|
||||||
|
removed++
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ent.name === '.bin') continue
|
||||||
|
stack.push(full)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.PEARDATA_KEEP_SOURCEMAP !== '1') {
|
||||||
|
rm('electron/app.bundle.cjs.map')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[forge] packageAfterCopy target=${packageHost} stripped=${removed} in ${Date.now() - t0}ms`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
packagerConfig: {
|
||||||
|
name: appName,
|
||||||
|
executableName: 'peardata-client',
|
||||||
|
appBundleId: 'com.peardata.app',
|
||||||
|
icon: fs.existsSync(path.join(__dirname, 'build', 'icon.png'))
|
||||||
|
? path.join(__dirname, 'build', 'icon')
|
||||||
|
: undefined,
|
||||||
|
asar: {
|
||||||
|
unpack: '**/*.{node,bare,dll,dylib,so}',
|
||||||
|
},
|
||||||
|
ignore: shouldIgnore,
|
||||||
|
derefSymlinks: false,
|
||||||
|
prune: false,
|
||||||
|
...(useElectronZipDir ? { electronZipDir } : {}),
|
||||||
|
quiet: process.env.CI ? false : true,
|
||||||
|
osxSign: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
rebuildConfig: skipRebuild
|
||||||
|
? { onlyModules: [], force: false }
|
||||||
|
: {
|
||||||
|
force: false,
|
||||||
|
onlyModules: [
|
||||||
|
'udx-native',
|
||||||
|
'sodium-native',
|
||||||
|
'rocksdb-native',
|
||||||
|
'fs-native-extensions',
|
||||||
|
'quickbit-native',
|
||||||
|
'simdle-native',
|
||||||
|
'bare-fs',
|
||||||
|
'bare-os',
|
||||||
|
'bare-crypto',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
makers: [
|
||||||
|
{
|
||||||
|
name: '@electron-forge/maker-zip',
|
||||||
|
platforms: ['darwin', 'linux', 'win32'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
plugins: [],
|
||||||
|
|
||||||
|
hooks: {
|
||||||
|
prePackage: async () => {
|
||||||
|
console.log(`[forge] packaging target host: ${packageHost}`)
|
||||||
|
console.log(
|
||||||
|
`[forge] electronZipDir: ${useElectronZipDir ? electronZipDir : '(none — packager will download)'}`
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
`[forge] rebuild: ${skipRebuild ? 'skip (onlyModules:[])' : 'enabled'}`
|
||||||
|
)
|
||||||
|
if (process.env.PEARDATA_SKIP_PREPACKAGE_BUNDLE === '1') {
|
||||||
|
const bundle = path.join(__dirname, 'electron', 'app.bundle.cjs')
|
||||||
|
if (fs.existsSync(bundle)) {
|
||||||
|
console.log('[forge] prePackage: skip bundle (already built)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require('child_process').execFileSync(
|
||||||
|
process.execPath,
|
||||||
|
[path.join(__dirname, 'scripts', 'build-client-bundle.cjs')],
|
||||||
|
{ stdio: 'inherit', cwd: __dirname }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
preMake: async () => {
|
||||||
|
fs.rmSync(path.join(__dirname, 'out', 'make'), { recursive: true, force: true })
|
||||||
|
},
|
||||||
|
packageAfterCopy: async (_forgeConfig, buildPath) => {
|
||||||
|
const pkgPath = path.join(buildPath, 'package.json')
|
||||||
|
const appPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
||||||
|
appPkg.main = 'electron/main.cjs'
|
||||||
|
delete appPkg.devDependencies
|
||||||
|
delete appPkg.scripts
|
||||||
|
if (appPkg.dependencies) {
|
||||||
|
for (const name of [
|
||||||
|
'bare-build',
|
||||||
|
'bare-runtime',
|
||||||
|
'bare-sqlite',
|
||||||
|
'pear-electron',
|
||||||
|
]) {
|
||||||
|
delete appPkg.dependencies[name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.writeFileSync(pkgPath, JSON.stringify(appPkg, null, 2) + '\n')
|
||||||
|
|
||||||
|
stripBuildPath(buildPath)
|
||||||
|
},
|
||||||
|
postPackage: async (_forgeConfig, options) => {
|
||||||
|
const platform = options.platform || process.platform
|
||||||
|
if (platform !== 'darwin') return
|
||||||
|
const { signApp, findApps } = require('./scripts/sign-macos-app.cjs')
|
||||||
|
const paths = options.outputPaths || []
|
||||||
|
for (const outPath of paths) {
|
||||||
|
const apps = findApps(outPath)
|
||||||
|
for (const app of apps) {
|
||||||
|
console.log('[forge] postPackage codesign', app)
|
||||||
|
await signApp(app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
Generated
+7454
-5
File diff suppressed because it is too large
Load Diff
+31
-3
@@ -46,7 +46,25 @@
|
|||||||
"mint-invite": "node scripts/mint-invite.js",
|
"mint-invite": "node scripts/mint-invite.js",
|
||||||
"build:db": "node scripts/build-db.js",
|
"build:db": "node scripts/build-db.js",
|
||||||
"rename": "bash scripts/rename-template.sh",
|
"rename": "bash scripts/rename-template.sh",
|
||||||
"release:notes": "node -e \"console.log('See docs/RELEASE.md')\""
|
"release:notes": "node -e \"console.log('See docs/RELEASE.md')\"",
|
||||||
|
"build:client-bundle": "node scripts/build-client-bundle.cjs",
|
||||||
|
"start:client": "npm run build:client-bundle && electron electron/main.cjs",
|
||||||
|
"package:client": "electron-forge package",
|
||||||
|
"make": "node scripts/make.cjs both",
|
||||||
|
"make:server": "node scripts/make.cjs server",
|
||||||
|
"make:client": "node scripts/make.cjs client",
|
||||||
|
"make:all": "node scripts/make.cjs all",
|
||||||
|
"make:bin": "node scripts/bare-standalone.cjs",
|
||||||
|
"make:server:linux-x64": "node scripts/bare-standalone.cjs --product server --host linux-x64",
|
||||||
|
"make:server:linux-arm64": "node scripts/bare-standalone.cjs --product server --host linux-arm64",
|
||||||
|
"make:client:linux-x64": "electron-forge package --platform linux --arch x64",
|
||||||
|
"make:client:linux-arm64": "electron-forge package --platform linux --arch arm64",
|
||||||
|
"make:client:darwin-arm64": "electron-forge package --platform darwin --arch arm64",
|
||||||
|
"make:client:darwin-x64": "electron-forge package --platform darwin --arch x64",
|
||||||
|
"make:client:win32-x64": "electron-forge package --platform win32 --arch x64",
|
||||||
|
"make:client:win32-arm64": "electron-forge package --platform win32 --arch arm64",
|
||||||
|
"sign:macos": "node scripts/sign-macos-app.cjs",
|
||||||
|
"release:rolling": "bash scripts/gitea-rolling-release.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.8.1",
|
"b4a": "^1.8.1",
|
||||||
@@ -97,11 +115,21 @@
|
|||||||
"bare-utils": "^1.5.1",
|
"bare-utils": "^1.5.1",
|
||||||
"bare-worker": "^4.0.0",
|
"bare-worker": "^4.0.0",
|
||||||
"bare-zlib": "^1.3.1",
|
"bare-zlib": "^1.3.1",
|
||||||
"which-runtime": "^1.4.0"
|
"which-runtime": "^1.4.0",
|
||||||
|
"bare-pack": "^1.0.0",
|
||||||
|
"bare-module-traverse": "^1.0.0",
|
||||||
|
"bare-bundle-id": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"brittle": "^4.1.0",
|
"brittle": "^4.1.0",
|
||||||
"pear-interface": "^1.1.0"
|
"pear-interface": "^1.1.0",
|
||||||
|
"@electron-forge/cli": "^7.11.2",
|
||||||
|
"@electron-forge/maker-zip": "^7.11.2",
|
||||||
|
"@electron/get": "^3.1.0",
|
||||||
|
"bare-build": "^1.0.2",
|
||||||
|
"bare-runtime": "1.30.3",
|
||||||
|
"electron": "^33.4.11",
|
||||||
|
"esbuild": "^0.25.0"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"assert": {
|
"assert": {
|
||||||
|
|||||||
@@ -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
|
#!/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:
|
# Required:
|
||||||
# RELEASE_TOKEN — Gitea PAT with repository release write
|
# RELEASE_TOKEN — Gitea PAT with repository release write
|
||||||
# Optional:
|
# Optional:
|
||||||
# GITEA_URL / GITEA_OWNER / GITEA_REPO
|
# 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)
|
# RELEASE_TAG (default: rolling)
|
||||||
# DRY_RUN=1 — build + stage only, no upload
|
# DRY_RUN=1 — build + stage only, no upload
|
||||||
# GITHUB_SHA / GITEA_SHA — target commit for the release tag
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
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)"
|
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)}}"
|
FULL_SHA="${GITHUB_SHA:-${GITEA_SHA:-$(git rev-parse HEAD 2>/dev/null || echo main)}}"
|
||||||
TAG="${RELEASE_TAG:-rolling}"
|
TAG="${RELEASE_TAG:-rolling}"
|
||||||
PKG_NAME="$(node -p "require('./package.json').name")"
|
RELEASE_TITLE="${RELEASE_NAME:-peardata rolling}"
|
||||||
RELEASE_TITLE="${RELEASE_NAME:-${PKG_NAME} rolling}"
|
DIST="$ROOT/dist/release"
|
||||||
DIST="$ROOT/dist"
|
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] $*"; }
|
log() { echo "[release] $*"; }
|
||||||
|
|
||||||
@@ -39,45 +49,133 @@ detect_remote() {
|
|||||||
read -r DETECTED_URL DETECTED_OWNER DETECTED_REPO <<<"$(detect_remote)"
|
read -r DETECTED_URL DETECTED_OWNER DETECTED_REPO <<<"$(detect_remote)"
|
||||||
GITEA_URL="${GITEA_URL:-${DETECTED_URL:-}}"
|
GITEA_URL="${GITEA_URL:-${DETECTED_URL:-}}"
|
||||||
GITEA_OWNER="${GITEA_OWNER:-${DETECTED_OWNER:-}}"
|
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
|
if [[ -z "${GITEA_URL}" ]]; then
|
||||||
log "WARN: could not detect GITEA_URL — set GITEA_URL for upload"
|
log "WARN: could not detect GITEA_URL — set GITEA_URL for upload"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- build artifacts (source tarball + checksum + notes) ---
|
export PEARDATA_SERVER_HOSTS="${PEARDATA_SERVER_HOSTS:-$DEFAULT_SERVER}"
|
||||||
log "building release artifacts via scripts/release.sh"
|
export PEARDATA_CLIENT_HOSTS="${PEARDATA_CLIENT_HOSTS:-$DEFAULT_CLIENT}"
|
||||||
bash scripts/release.sh
|
|
||||||
|
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
|
cat >"$DIST/RELEASE_NOTES.md" <<EOF
|
||||||
# ${PKG_NAME} ${VERSION} (${TAG})
|
# peardata ${VERSION} (${TAG})
|
||||||
|
|
||||||
- Commit: \`${SHA}\` (\`${FULL_SHA}\`)
|
- Commit: \`${SHA}\` (\`${FULL_SHA}\`)
|
||||||
- Built: ${STAMP}
|
- 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
|
| Product | Hosts |
|
||||||
- Node server (\`npm run start:server\`)
|
|---------|-------|
|
||||||
- Pear desktop client (\`npm start\` / \`pear run -d .\`)
|
| **Server** (Bare) | \`linux-x64\`, \`linux-arm64\` only |
|
||||||
- Demo room (messages + presence + invites)
|
| **Client** (Electron) | \`linux-x64\`, \`linux-arm64\`, \`darwin-x64\`, \`darwin-arm64\`, \`win32-x64\`, \`win32-arm64\` |
|
||||||
|
|
||||||
## Install
|
## Server (Bare standalone)
|
||||||
|
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
mkdir -p app && tar -xzf ${PKG_NAME}-v${VERSION}.tar.gz -C app
|
tar -xzf peardata-server-${VERSION}-linux-x64.tar.gz
|
||||||
cd app
|
./peardata-server
|
||||||
npm install
|
# REST: http://127.0.0.1:19999/api/v3/info
|
||||||
npm run start:server
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
## Verify
|
## Client (Electron)
|
||||||
|
|
||||||
\`\`\`bash
|
\`\`\`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
|
## Checksums
|
||||||
|
|
||||||
See \`*.sha256\` beside each archive.
|
See \`*.sha256\` beside each archive.
|
||||||
@@ -85,11 +183,10 @@ EOF
|
|||||||
|
|
||||||
log "artifacts in $DIST:"
|
log "artifacts in $DIST:"
|
||||||
ls -la "$DIST" || true
|
ls -la "$DIST" || true
|
||||||
|
log "summary: server ok=${SERVER_OK} fail=${SERVER_FAIL} | client ok=${CLIENT_OK} fail=${CLIENT_FAIL}"
|
||||||
|
|
||||||
shopt -s nullglob
|
if [[ "$SERVER_OK" -eq 0 ]]; then
|
||||||
ARTIFACTS=("$DIST"/*.tar.gz)
|
log "ERROR: no server artifacts staged"
|
||||||
if [[ ${#ARTIFACTS[@]} -eq 0 ]]; then
|
|
||||||
log "ERROR: no tarball artifacts in $DIST"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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")"
|
REL_ID="$(node -e "console.log(JSON.parse(process.argv[1]).id)" "$CREATE_RESP")"
|
||||||
log "created release id=$REL_ID"
|
log "created release id=$REL_ID"
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
for f in "$DIST"/*; do
|
for f in "$DIST"/*; do
|
||||||
[[ -f "$f" ]] || continue
|
[[ -f "$f" ]] || continue
|
||||||
base="$(basename "$f")"
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Vendored `rcodesign` (apple-codesign)
|
||||||
|
|
||||||
|
Used by CI on Linux to **seal-sign** macOS **client** artifacts produced on Linux:
|
||||||
|
|
||||||
|
- Electron `.app` bundles (forge `postPackage` → `scripts/sign-macos-app.cjs`)
|
||||||
|
|
||||||
|
so Gatekeeper does not report them as “damaged / move to Trash”.
|
||||||
|
|
||||||
|
PearData **server** binaries are Linux-only and do not need macOS codesign.
|
||||||
|
|
||||||
|
| Path | Binary |
|
||||||
|
|------|--------|
|
||||||
|
| `linux-x64/rcodesign` | x86_64 musl (ubuntu-latest runners) |
|
||||||
|
| `linux-arm64/rcodesign` | aarch64 musl |
|
||||||
|
|
||||||
|
Upstream: [indygreg/apple-platform-rs](https://github.com/indygreg/apple-platform-rs)
|
||||||
|
Release tag: `apple-codesign/0.29.0`
|
||||||
|
|
||||||
|
Do **not** re-download in CI — the rolling release workflow installs from these paths.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
apple-codesign 0.29.0 (aarch64-unknown-linux-musl)
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
apple-codesign 0.29.0 (x86_64-unknown-linux-musl)
|
||||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user