From d1fa7825fb788bbc196cd2d485992de72e51abb6 Mon Sep 17 00:00:00 2001 From: reaper Date: Fri, 14 Aug 2026 17:59:18 -0500 Subject: [PATCH] build: embed pac catalog in application bundles --- docs/ALPHA24_COMPLETION_REPORT.md | 6 ++++-- electron-builder.yml | 18 ++++++++++++++++ package.json | 5 +++-- scripts/package-modules.cjs | 34 +++++++++++++++++++++++++++++++ src/main/moduleRegistry.test.ts | 15 ++++++++++++++ src/main/moduleRegistry.ts | 5 ++++- 6 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 scripts/package-modules.cjs diff --git a/docs/ALPHA24_COMPLETION_REPORT.md b/docs/ALPHA24_COMPLETION_REPORT.md index 3786985..e07551c 100644 --- a/docs/ALPHA24_COMPLETION_REPORT.md +++ b/docs/ALPHA24_COMPLETION_REPORT.md @@ -13,6 +13,7 @@ - Package manifests are validated for identity, version, category, dependencies, conflicts, settings, Core/API compatibility, project contributions, and safe relative entry paths. - Native `.pac` files are ZIP-compressed Aurora packages with `manifest.json` at archive root. - A `.pac` placed beside the AppImage or in its `modules` folder is discovered automatically and appears in the Modules screen. +- Packaged applications also carry a generated `.pac` catalog in their application resources. The macOS DMG layout exposes the same catalog as an **Aurora Modules** folder while the copied `.app` retains its own embedded catalog after the DMG is ejected. - Linux packages use Aurora's 512×512 application icon and a synchronized `aurora-dockside` desktop filename, executable name, icon name, and `StartupWMClass`. - `.pac` inspection rejects encrypted entries, symbolic links, path traversal, absolute/drive paths, excessive entry counts, and expanded archives larger than 256 MiB before extraction. - Installation uses staging plus rollback-safe replacement, so a failed update preserves the currently installed module. @@ -45,7 +46,7 @@ npm run build npx electron-builder --linux AppImage ``` -Automated result: 6 test files and 26 tests passed. Coverage includes: +Automated result: 6 test files and 27 tests passed. Coverage includes: - Empty registry - Available local packages @@ -78,7 +79,7 @@ Live project smoke result for project `24`: SHA-256: ```text -faafa9555072e8de98db1ee65cf329804c0eb37d44cd3bf4e6b50c5216f9f6e6 aurora-dockside-2.0.0-alpha.24.AppImage +ad679c904204b7f8db52b209231a08eea6ba1baf34dd3f24eae90ffcdfae7e56 aurora-dockside-2.0.0-alpha.24.AppImage d2165af4b75ab888c6d35a750a449205123866231ba5e98e1cbe9bcaa8738f46 aurora-module-wordpress-1.2.0.pac ``` @@ -86,3 +87,4 @@ d2165af4b75ab888c6d35a750a449205123866231ba5e98e1cbe9bcaa8738f46 aurora-module- - Clean-registry install/remove behavior was exercised through the real registry implementation in automated temporary-directory tests. The live GUI smoke used the already installed local WordPress package and project `24`. - AppImage systems without working FUSE can use `--appimage-extract` and launch `squashfs-root/AppRun --no-sandbox`. +- The embedded catalog and DMG layout are configured and the identical Linux application-resource layout was verified. A final signed/notarized `.app` and DMG must be built and inspected on macOS or a macOS CI runner. diff --git a/electron-builder.yml b/electron-builder.yml index 9efb011..b3fecc9 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -1,5 +1,6 @@ appId: com.aurora-dockside.app productName: Aurora Dockside +beforePack: scripts/package-modules.cjs directories: buildResources: build files: @@ -11,6 +12,11 @@ files: - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' asarUnpack: - resources/** +extraResources: + - from: dist/module-catalog + to: module-catalog + filter: + - '*.pac' win: executableName: aurora-dockside nsis: @@ -23,6 +29,18 @@ mac: notarize: false dmg: artifactName: ${name}-${version}.${ext} + contents: + - x: 130 + y: 220 + - x: 410 + y: 220 + type: link + path: /Applications + - x: 270 + y: 110 + type: dir + name: Aurora Modules + path: dist/module-catalog linux: executableName: aurora-dockside icon: resources/icon.png diff --git a/package.json b/package.json index 55645f5..50f0938 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,12 @@ "start": "electron-vite preview", "dev": "electron-vite dev", "build": "npm run typecheck && electron-vite build", + "build:modules": "node scripts/package-modules.cjs", "postinstall": "electron-builder install-app-deps", "build:unpack": "npm run build && electron-builder --dir", "build:win": "npm run build && electron-builder --win", - "build:mac": "electron-vite build && electron-builder --mac", - "build:linux": "electron-vite build && electron-builder --linux", + "build:mac": "npm run build && electron-builder --mac", + "build:linux": "npm run build && electron-builder --linux", "test": "vitest", "test:run": "vitest run", "test:coverage": "vitest run --coverage", diff --git a/scripts/package-modules.cjs b/scripts/package-modules.cjs new file mode 100644 index 0000000..0c0b9a9 --- /dev/null +++ b/scripts/package-modules.cjs @@ -0,0 +1,34 @@ +'use strict' + +const { mkdirSync, readFileSync, readdirSync, rmSync } = require('fs') +const { join, resolve } = require('path') +const { spawnSync } = require('child_process') + +function packageModules() { + const projectRoot = resolve(__dirname, '..') + const packagesRoot = join(projectRoot, 'packages') + const catalogRoot = join(projectRoot, 'dist', 'module-catalog') + mkdirSync(catalogRoot, { recursive: true }) + + for (const entry of readdirSync(packagesRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) continue + const source = join(packagesRoot, entry.name) + let manifest + try { + manifest = JSON.parse(readFileSync(join(source, 'manifest.json'), 'utf8')) + } catch { + continue + } + if (!manifest.id || !manifest.version) throw new Error(`Invalid module manifest in ${source}`) + const output = join(catalogRoot, `${manifest.id}-${manifest.version}.pac`) + rmSync(output, { force: true }) + const result = spawnSync('zip', ['-qr', output, '.'], { cwd: source, stdio: 'inherit' }) + if (result.error) throw result.error + if (result.status !== 0) throw new Error(`zip failed for ${manifest.id} with exit code ${result.status}`) + process.stdout.write(` • packaged module ${manifest.id}@${manifest.version} → ${output}\n`) + } +} + +module.exports = packageModules + +if (require.main === module) packageModules() diff --git a/src/main/moduleRegistry.test.ts b/src/main/moduleRegistry.test.ts index 8f56711..dd41ed3 100644 --- a/src/main/moduleRegistry.test.ts +++ b/src/main/moduleRegistry.test.ts @@ -51,6 +51,21 @@ describe('external module registry', () => { expect((await getAvailableModulePackages()).map((item) => item.manifest.id)).toContain('sample-app') delete process.env.APPIMAGE }) + it('discovers a .pac embedded in the packaged application resources', async () => { + const resources = await temp('aurora-resources-') + const catalog = join(resources, 'module-catalog') + await mkdir(catalog) + const pac = await pacFile() + await import('fs/promises').then(({ copyFile }) => copyFile(pac, join(catalog, 'sample-app.pac'))) + const previous = Object.getOwnPropertyDescriptor(process, 'resourcesPath') + Object.defineProperty(process, 'resourcesPath', { value: resources, configurable: true }) + try { + expect((await getAvailableModulePackages()).map((item) => item.manifest.id)).toContain('sample-app') + } finally { + if (previous) Object.defineProperty(process, 'resourcesPath', previous) + else Reflect.deleteProperty(process, 'resourcesPath') + } + }) it('installs a valid local package and refreshes after install', async () => { const userData = await temp('aurora-user-'); const source = await packageDir() await installModulePackage(source, userData) diff --git a/src/main/moduleRegistry.ts b/src/main/moduleRegistry.ts index 9db7c8b..d85db06 100644 --- a/src/main/moduleRegistry.ts +++ b/src/main/moduleRegistry.ts @@ -165,7 +165,10 @@ function catalogDirectories(): string[] { const besideImage = process.env.APPIMAGE ? [dirname(process.env.APPIMAGE), join(dirname(process.env.APPIMAGE), 'modules')] : [] - return [...configured, ...besideImage, join(process.cwd(), 'modules'), join(process.cwd(), 'packages'), join(app.getAppPath(), 'packages')] + const embedded = typeof process.resourcesPath === 'string' + ? [join(process.resourcesPath, 'module-catalog')] + : [] + return [...configured, ...besideImage, ...embedded, join(process.cwd(), 'modules'), join(process.cwd(), 'packages'), join(app.getAppPath(), 'packages')] } export async function getAvailableModulePackages(): Promise {