Speed up Electron client packaging by filtering prebuilds
Release rolling / release (push) Has been cancelled
Release rolling / release (push) Has been cancelled
Only ship native prebuilds for the target platform/arch (~80MB instead of ~500MB of multi-arch blobs), skip redundant GUI rebundles, cache Electron downloads across hosts, and strip more packaging-only deps so "Finalizing package" no longer crawls multi-hundred-MB asars six times.
This commit is contained in:
@@ -162,6 +162,9 @@ jobs:
|
|||||||
# Prefer prebuilds; skip electron-rebuild (main "Finalizing package" stall)
|
# Prefer prebuilds; skip electron-rebuild (main "Finalizing package" stall)
|
||||||
npm_config_build_from_source: 'false'
|
npm_config_build_from_source: 'false'
|
||||||
PEARDOCK_SKIP_REBUILD: '1'
|
PEARDOCK_SKIP_REBUILD: '1'
|
||||||
|
# Reuse Electron downloads across the 6 client hosts
|
||||||
|
ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron
|
||||||
|
electron_config_cache: ${{ github.workspace }}/.cache/electron
|
||||||
# electron-packager / forge download target Electron builds
|
# electron-packager / forge download target Electron builds
|
||||||
ELECTRON_GET_USE_PROXY: ${{ env.ELECTRON_GET_USE_PROXY || '' }}
|
ELECTRON_GET_USE_PROXY: ${{ env.ELECTRON_GET_USE_PROXY || '' }}
|
||||||
run: |
|
run: |
|
||||||
@@ -177,6 +180,7 @@ 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
|
||||||
|
mkdir -p "${ELECTRON_CACHE:-$GITHUB_WORKSPACE/.cache/electron}"
|
||||||
echo "Server hosts: $PEARDOCK_SERVER_HOSTS"
|
echo "Server hosts: $PEARDOCK_SERVER_HOSTS"
|
||||||
echo "Client hosts: $PEARDOCK_CLIENT_HOSTS"
|
echo "Client hosts: $PEARDOCK_CLIENT_HOSTS"
|
||||||
chmod +x scripts/gitea-rolling-release.sh scripts/bare-standalone.cjs scripts/make.cjs
|
chmod +x scripts/gitea-rolling-release.sh scripts/bare-standalone.cjs scripts/make.cjs
|
||||||
|
|||||||
+183
-30
@@ -2,9 +2,12 @@
|
|||||||
* Electron Forge config for peardock-client.
|
* Electron Forge config for peardock-client.
|
||||||
*
|
*
|
||||||
* CI note: "Finalizing package" used to take forever because asar:false + prune
|
* CI note: "Finalizing package" used to take forever because asar:false + prune
|
||||||
* copied ~GBs of real files. We now pack with asar (CJS require works inside
|
* copied ~GBs of real files. We pack with asar (CJS require works inside asar;
|
||||||
* asar; only natives are unpacked) and prune:false (ignore list already strips
|
* only natives are unpacked) and prune:false (ignore list already strips tooling).
|
||||||
* the heavy tooling).
|
*
|
||||||
|
* Further win: only ship prebuilds for the *target* platform/arch. Holepunch
|
||||||
|
* natives ship ~500MB of multi-arch prebuilds; keeping one host (~20–40MB)
|
||||||
|
* dominates finalize time.
|
||||||
*/
|
*/
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
@@ -14,6 +17,28 @@ const fs = require('fs')
|
|||||||
const pkg = require('./package.json')
|
const pkg = require('./package.json')
|
||||||
const appName = pkg.productName || pkg.name || 'peardock'
|
const appName = pkg.productName || pkg.name || 'peardock'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.PEARDOCK_PACKAGE_PLATFORM ||
|
||||||
|
flag('--platform') ||
|
||||||
|
process.platform
|
||||||
|
const arch =
|
||||||
|
process.env.PEARDOCK_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 */
|
/** Path prefixes (packager paths start with /) to exclude from the app bundle */
|
||||||
const IGNORE_PREFIXES = [
|
const IGNORE_PREFIXES = [
|
||||||
'/.git',
|
'/.git',
|
||||||
@@ -32,6 +57,8 @@ const IGNORE_PREFIXES = [
|
|||||||
'/peardock-branding', // master brand package; runtime copies live in build/ + assets/
|
'/peardock-branding', // master brand package; runtime copies live in build/ + assets/
|
||||||
'/build/stubs',
|
'/build/stubs',
|
||||||
'/build/shims',
|
'/build/shims',
|
||||||
|
'/ROADMAP.md',
|
||||||
|
'/README.md',
|
||||||
// Packaging / Bare server toolchain (not needed at Electron runtime)
|
// Packaging / Bare server toolchain (not needed at Electron runtime)
|
||||||
'/node_modules/electron',
|
'/node_modules/electron',
|
||||||
'/node_modules/electron-',
|
'/node_modules/electron-',
|
||||||
@@ -49,7 +76,12 @@ const IGNORE_PREFIXES = [
|
|||||||
'/node_modules/bare-dev',
|
'/node_modules/bare-dev',
|
||||||
'/node_modules/bare-bundle',
|
'/node_modules/bare-bundle',
|
||||||
'/node_modules/bare-module-traverse',
|
'/node_modules/bare-module-traverse',
|
||||||
|
'/node_modules/bare-sqlite', // not used by Electron client
|
||||||
|
'/node_modules/postject',
|
||||||
|
'/node_modules/@inquirer',
|
||||||
|
'/node_modules/terser',
|
||||||
'/node_modules/pear-runtime/',
|
'/node_modules/pear-runtime/',
|
||||||
|
'/node_modules/pear-electron', // packaged shell is electron/main.cjs, not pear-electron
|
||||||
// Server-only Docker stack
|
// Server-only Docker stack
|
||||||
'/node_modules/dockerode',
|
'/node_modules/dockerode',
|
||||||
'/node_modules/docker-modem',
|
'/node_modules/docker-modem',
|
||||||
@@ -75,26 +107,51 @@ const IGNORE_REGEX = [
|
|||||||
/^\/node_modules\/bare-runtime-/,
|
/^\/node_modules\/bare-runtime-/,
|
||||||
/^\/node_modules\/bare-pack-/,
|
/^\/node_modules\/bare-pack-/,
|
||||||
/^\/node_modules\/@esbuild\//,
|
/^\/node_modules\/@esbuild\//,
|
||||||
/\.md$/,
|
/\.md$/i,
|
||||||
/\.map$/,
|
/\.map$/,
|
||||||
|
/\.d\.ts$/,
|
||||||
/^\/peardock-.*\.json$/,
|
/^\/peardock-.*\.json$/,
|
||||||
/^\/\.env$/,
|
/^\/\.env$/,
|
||||||
/^\/package-lock\.json$/,
|
/^\/package-lock\.json$/,
|
||||||
// Tests / docs inside deps
|
// Tests / docs / examples inside deps
|
||||||
/^\/node_modules\/[^/]+\/test\//,
|
/^\/node_modules\/[^/]+\/test\//,
|
||||||
/^\/node_modules\/[^/]+\/tests\//,
|
/^\/node_modules\/[^/]+\/tests\//,
|
||||||
/^\/node_modules\/[^/]+\/docs\//,
|
/^\/node_modules\/[^/]+\/docs\//,
|
||||||
/^\/node_modules\/[^/]+\/example\//,
|
/^\/node_modules\/[^/]+\/example\//,
|
||||||
/^\/node_modules\/[^/]+\/examples\//,
|
/^\/node_modules\/[^/]+\/examples\//,
|
||||||
|
/^\/node_modules\/[^/]+\/\.github\//,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop prebuilds for every host except the package target.
|
||||||
|
* Paths look like: /node_modules/rocksdb-native/prebuilds/darwin-arm64/...
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
// Always drop mobile / simulator prebuilds
|
||||||
|
if (
|
||||||
|
host.startsWith('android') ||
|
||||||
|
host.startsWith('ios') ||
|
||||||
|
host.includes('simulator')
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return host !== packageHost
|
||||||
|
}
|
||||||
|
|
||||||
function shouldIgnore(file) {
|
function shouldIgnore(file) {
|
||||||
if (!file) return false
|
if (!file) return false
|
||||||
// Always keep package.json (packager needs it)
|
// Always keep package.json (packager needs it)
|
||||||
if (file === '/package.json') return false
|
if (file === '/package.json') return false
|
||||||
// Keep the GUI bundle (generated)
|
// Keep the GUI bundle (exact path only — do not match .map via prefix)
|
||||||
if (file === '/electron/app.bundle.cjs' || file.startsWith('/electron/app.bundle.cjs')) {
|
if (file === '/electron/app.bundle.cjs') return false
|
||||||
return false
|
// Sourcemap: only when explicitly requested (default drop → faster asar)
|
||||||
|
if (file === '/electron/app.bundle.cjs.map') {
|
||||||
|
return process.env.PEARDOCK_KEEP_SOURCEMAP !== '1'
|
||||||
}
|
}
|
||||||
for (const p of IGNORE_PREFIXES) {
|
for (const p of IGNORE_PREFIXES) {
|
||||||
if (file === p || file.startsWith(p + '/') || file.startsWith(p)) return true
|
if (file === p || file.startsWith(p + '/') || file.startsWith(p)) return true
|
||||||
@@ -102,6 +159,7 @@ function shouldIgnore(file) {
|
|||||||
for (const re of IGNORE_REGEX) {
|
for (const re of IGNORE_REGEX) {
|
||||||
if (re.test(file)) return true
|
if (re.test(file)) return true
|
||||||
}
|
}
|
||||||
|
if (isForeignPrebuild(file)) return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +168,100 @@ const skipRebuild =
|
|||||||
process.env.PEARDOCK_FORCE_REBUILD !== '1' &&
|
process.env.PEARDOCK_FORCE_REBUILD !== '1' &&
|
||||||
process.env.PEARDOCK_SKIP_REBUILD !== '0'
|
process.env.PEARDOCK_SKIP_REBUILD !== '0'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After copy (before asar): belt-and-suspenders strip of foreign prebuilds + junk.
|
||||||
|
* Ignore should already exclude these; this catches nested paths packager still copied.
|
||||||
|
*/
|
||||||
|
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',
|
||||||
|
'scripts',
|
||||||
|
'out',
|
||||||
|
'test',
|
||||||
|
'docs',
|
||||||
|
'peardock-branding',
|
||||||
|
'ROADMAP.md',
|
||||||
|
'README.md',
|
||||||
|
]
|
||||||
|
for (const rel of junk) rm(rel)
|
||||||
|
|
||||||
|
// Walk node_modules for prebuilds/<host> dirs
|
||||||
|
const nm = path.join(buildPath, 'node_modules')
|
||||||
|
if (fs.existsSync(nm)) {
|
||||||
|
const stack = [nm]
|
||||||
|
while (stack.length) {
|
||||||
|
const dir = stack.pop()
|
||||||
|
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.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
|
||||||
|
}
|
||||||
|
// Do not descend into prebuilds we already handled
|
||||||
|
if (ent.name === '.bin') continue
|
||||||
|
stack.push(full)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop GUI sourcemap unless kept
|
||||||
|
if (process.env.PEARDOCK_KEEP_SOURCEMAP !== '1') {
|
||||||
|
rm('electron/app.bundle.cjs.map')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[forge] packageAfterCopy target=${packageHost} stripped=${removed} in ${Date.now() - t0}ms`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
packagerConfig: {
|
packagerConfig: {
|
||||||
name: appName,
|
name: appName,
|
||||||
@@ -162,6 +314,14 @@ module.exports = {
|
|||||||
|
|
||||||
hooks: {
|
hooks: {
|
||||||
prePackage: async () => {
|
prePackage: async () => {
|
||||||
|
console.log(`[forge] packaging target host: ${packageHost}`)
|
||||||
|
if (process.env.PEARDOCK_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(
|
require('child_process').execFileSync(
|
||||||
process.execPath,
|
process.execPath,
|
||||||
[path.join(__dirname, 'scripts', 'build-client-bundle.cjs')],
|
[path.join(__dirname, 'scripts', 'build-client-bundle.cjs')],
|
||||||
@@ -177,30 +337,23 @@ module.exports = {
|
|||||||
appPkg.main = 'electron/main.cjs'
|
appPkg.main = 'electron/main.cjs'
|
||||||
delete appPkg.devDependencies
|
delete appPkg.devDependencies
|
||||||
delete appPkg.scripts
|
delete appPkg.scripts
|
||||||
|
// Drop server-only deps from package.json so nothing tries to resolve them
|
||||||
|
if (appPkg.dependencies) {
|
||||||
|
for (const name of [
|
||||||
|
'dockerode',
|
||||||
|
'docker-modem',
|
||||||
|
'ssh2',
|
||||||
|
'bare-build',
|
||||||
|
'bare-runtime',
|
||||||
|
'bare-sqlite',
|
||||||
|
'pear-electron',
|
||||||
|
]) {
|
||||||
|
delete appPkg.dependencies[name]
|
||||||
|
}
|
||||||
|
}
|
||||||
fs.writeFileSync(pkgPath, JSON.stringify(appPkg, null, 2) + '\n')
|
fs.writeFileSync(pkgPath, JSON.stringify(appPkg, null, 2) + '\n')
|
||||||
|
|
||||||
const junk = [
|
stripBuildPath(buildPath)
|
||||||
'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/esbuild',
|
|
||||||
'server',
|
|
||||||
'scripts',
|
|
||||||
'out',
|
|
||||||
'test',
|
|
||||||
'docs',
|
|
||||||
]
|
|
||||||
for (const rel of junk) {
|
|
||||||
const p = path.join(buildPath, rel)
|
|
||||||
try {
|
|
||||||
fs.rmSync(p, { recursive: true, force: true })
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Always re-sign darwin builds after packager rewrites the Electron .app.
|
* Always re-sign darwin builds after packager rewrites the Electron .app.
|
||||||
|
|||||||
+20
-4
@@ -15,7 +15,12 @@
|
|||||||
|
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { spawnSync } = require('child_process')
|
const { spawnSync } = require('child_process')
|
||||||
const { ALL_64, parseHostList, clientNpmScript } = require('./hosts.cjs')
|
const {
|
||||||
|
ALL_64,
|
||||||
|
parseHostList,
|
||||||
|
clientNpmScript,
|
||||||
|
hostToElectron,
|
||||||
|
} = require('./hosts.cjs')
|
||||||
|
|
||||||
const root = path.resolve(__dirname, '..')
|
const root = path.resolve(__dirname, '..')
|
||||||
|
|
||||||
@@ -24,6 +29,7 @@ const CLIENT_HOSTS = parseHostList(process.env.PEARDOCK_CLIENT_HOSTS, ALL_64)
|
|||||||
|
|
||||||
function run(cmd, args, opts = {}) {
|
function run(cmd, args, opts = {}) {
|
||||||
console.log(`\n$ ${cmd} ${args.join(' ')}\n`)
|
console.log(`\n$ ${cmd} ${args.join(' ')}\n`)
|
||||||
|
const t0 = Date.now()
|
||||||
const res = spawnSync(cmd, args, {
|
const res = spawnSync(cmd, args, {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
@@ -33,10 +39,13 @@ function run(cmd, args, opts = {}) {
|
|||||||
})
|
})
|
||||||
if (res.error) throw res.error
|
if (res.error) throw res.error
|
||||||
if (res.status !== 0) process.exit(res.status || 1)
|
if (res.status !== 0) process.exit(res.status || 1)
|
||||||
|
console.log(`[make] ok in ${((Date.now() - t0) / 1000).toFixed(1)}s`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function npmRun(script) {
|
function npmRun(script, env) {
|
||||||
run(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', script])
|
run(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', script], {
|
||||||
|
env: env ? { ...process.env, ...env } : process.env,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeServer(hosts = SERVER_HOSTS) {
|
function makeServer(hosts = SERVER_HOSTS) {
|
||||||
@@ -54,6 +63,7 @@ function makeServer(hosts = SERVER_HOSTS) {
|
|||||||
|
|
||||||
function makeClient(hosts = CLIENT_HOSTS) {
|
function makeClient(hosts = CLIENT_HOSTS) {
|
||||||
console.log(`[make] client hosts: ${hosts.join(', ')}`)
|
console.log(`[make] client hosts: ${hosts.join(', ')}`)
|
||||||
|
// Build GUI bundle once; forge prePackage skips rebuild when this is set
|
||||||
npmRun('build:client-bundle')
|
npmRun('build:client-bundle')
|
||||||
for (const host of hosts) {
|
for (const host of hosts) {
|
||||||
const script = clientNpmScript(host)
|
const script = clientNpmScript(host)
|
||||||
@@ -61,11 +71,17 @@ function makeClient(hosts = CLIENT_HOSTS) {
|
|||||||
console.error(`[make] no client script for host ${host}`)
|
console.error(`[make] no client script for host ${host}`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
// Prefer prebuilds; skip native rebuild (cross-arch rebuild is slow/fails)
|
const { platform, arch } = hostToElectron(host)
|
||||||
|
console.log(`[make] client ${host} (filter prebuilds to ${platform}-${arch})`)
|
||||||
|
// Prefer prebuilds; skip native rebuild (cross-arch rebuild is slow/fails).
|
||||||
|
// PEARDOCK_PACKAGE_* tells forge.config to ignore foreign prebuilds (~500MB → ~30MB).
|
||||||
const env = {
|
const env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
npm_config_build_from_source: 'false',
|
npm_config_build_from_source: 'false',
|
||||||
PEARDOCK_SKIP_REBUILD: process.env.PEARDOCK_SKIP_REBUILD || '1',
|
PEARDOCK_SKIP_REBUILD: process.env.PEARDOCK_SKIP_REBUILD || '1',
|
||||||
|
PEARDOCK_PACKAGE_PLATFORM: platform,
|
||||||
|
PEARDOCK_PACKAGE_ARCH: arch,
|
||||||
|
PEARDOCK_SKIP_PREPACKAGE_BUNDLE: '1',
|
||||||
}
|
}
|
||||||
run(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', script], { env })
|
run(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', script], { env })
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user