From bb6bb04c49a5b168029cb4fff0b180f12762e7e0 Mon Sep 17 00:00:00 2001 From: sudacode Date: Wed, 27 May 2026 11:40:55 -0700 Subject: [PATCH] fix: ensure macOS mpv window helper is built and bundled correctly - create dist/scripts dir before swiftc output in prepare-build-assets - add after-pack verification that helper binary exists and is executable - remove stale Windows PS1 extra-resource entry (harmless missing-file warnings) - add tests for new verifyMacOSWindowHelper and prepare-build-assets behavior --- changes/fix-macos-helper-packaging.md | 5 ++ package.json | 6 +-- scripts/electron-builder-after-pack.cjs | 55 +++++++++++++++++++++ scripts/electron-builder-after-pack.test.ts | 54 ++++++++++++++++++++ scripts/prepare-build-assets.mjs | 2 + scripts/prepare-build-assets.test.ts | 20 ++++++++ src/release-workflow.test.ts | 8 +++ 7 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 changes/fix-macos-helper-packaging.md create mode 100644 scripts/prepare-build-assets.test.ts diff --git a/changes/fix-macos-helper-packaging.md b/changes/fix-macos-helper-packaging.md new file mode 100644 index 00000000..fad482e1 --- /dev/null +++ b/changes/fix-macos-helper-packaging.md @@ -0,0 +1,5 @@ +type: fixed +area: release + +- Fixed macOS packaging so the compiled mpv window helper is built into `dist/scripts` and required in the app bundle, preventing the overlay from falling back to slow Swift source startup. +- Removed a stale Windows helper resource entry that produced harmless missing-file warnings during packaging. diff --git a/package.json b/package.json index 5a86444c..d90d44f8 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "test:launcher": "bun run test:launcher:src", "test:core": "bun run test:core:src", "test:subtitle": "bun run test:subtitle:src", - "test:fast": "bun run test:config:src && bun run test:core:src && bun run test:docs:kb && bun test src/main-entry-runtime.test.ts src/anki-integration.test.ts src/anki-integration/card-creation-manual-update.test.ts src/anki-integration/anki-connect-proxy.test.ts src/anki-integration/field-grouping-workflow.test.ts src/anki-integration/field-grouping.test.ts src/anki-integration/field-grouping-merge.test.ts src/release-workflow.test.ts src/prerelease-workflow.test.ts src/ci-workflow.test.ts scripts/docs-versioning.test.ts scripts/docs-versioned-assets.test.ts scripts/build-changelog.test.ts scripts/electron-builder-after-pack.test.ts scripts/get-mpv-window-macos.test.ts scripts/mkv-to-readme-video.test.ts scripts/run-coverage-lane.test.ts scripts/update-aur-package.test.ts && bun test src/core/services/immersion-tracker/__tests__/query.test.ts src/core/services/immersion-tracker/__tests__/query-split-modules.test.ts && bun run tsc && bun test dist/main/runtime/registry.test.js", + "test:fast": "bun run test:config:src && bun run test:core:src && bun run test:docs:kb && bun test src/main-entry-runtime.test.ts src/anki-integration.test.ts src/anki-integration/card-creation-manual-update.test.ts src/anki-integration/anki-connect-proxy.test.ts src/anki-integration/field-grouping-workflow.test.ts src/anki-integration/field-grouping.test.ts src/anki-integration/field-grouping-merge.test.ts src/release-workflow.test.ts src/prerelease-workflow.test.ts src/ci-workflow.test.ts scripts/docs-versioning.test.ts scripts/docs-versioned-assets.test.ts scripts/build-changelog.test.ts scripts/electron-builder-after-pack.test.ts scripts/get-mpv-window-macos.test.ts scripts/prepare-build-assets.test.ts scripts/mkv-to-readme-video.test.ts scripts/run-coverage-lane.test.ts scripts/update-aur-package.test.ts && bun test src/core/services/immersion-tracker/__tests__/query.test.ts src/core/services/immersion-tracker/__tests__/query-split-modules.test.ts && bun run tsc && bun test dist/main/runtime/registry.test.js", "generate:config-example": "bun run src/generate-config-example.ts", "verify:config-example": "bun run src/verify-config-example.ts", "start": "bun run build && electron . --start", @@ -257,10 +257,6 @@ { "from": "dist/launcher/subminer", "to": "launcher/subminer" - }, - { - "from": "dist/scripts/get-mpv-window-windows.ps1", - "to": "scripts/get-mpv-window-windows.ps1" } ] } diff --git a/scripts/electron-builder-after-pack.cjs b/scripts/electron-builder-after-pack.cjs index 54a9795b..4153f760 100644 --- a/scripts/electron-builder-after-pack.cjs +++ b/scripts/electron-builder-after-pack.cjs @@ -1,7 +1,24 @@ const fs = require('node:fs/promises'); +const fsSync = require('node:fs'); const path = require('node:path'); const LINUX_FFMPEG_LIBRARY = 'libffmpeg.so'; +const MACOS_WINDOW_HELPER = 'get-mpv-window-macos'; +const DEFAULT_MACOS_APP_BUNDLE = 'SubMiner.app'; + +function resolveMacOSAppBundlePath(context) { + if (context.appOutDir.endsWith('.app')) { + return context.appOutDir; + } + + const productFilename = context.packager?.appInfo?.productFilename; + const bundleName = + typeof productFilename === 'string' && productFilename.trim() + ? `${productFilename.trim()}.app` + : DEFAULT_MACOS_APP_BUNDLE; + + return path.join(context.appOutDir, bundleName); +} async function stageLinuxAppImageSharedLibrary( context, @@ -35,12 +52,50 @@ async function stageLinuxAppImageSharedLibrary( return true; } +async function verifyMacOSWindowHelper( + context, + deps = { + access: (filePath, mode) => fs.access(filePath, mode), + }, +) { + if (context.electronPlatformName !== 'darwin') return false; + + const appBundlePath = resolveMacOSAppBundlePath(context); + const helperPath = path.join( + appBundlePath, + 'Contents', + 'Resources', + 'scripts', + MACOS_WINDOW_HELPER, + ); + + try { + await deps.access(helperPath, fsSync.constants.X_OK); + } catch (error) { + if (error && typeof error === 'object' && error.code === 'ENOENT') { + throw new Error( + `macOS packaging requires ${MACOS_WINDOW_HELPER} at ${helperPath}. Run bun run build:assets on macOS with swiftc available before packaging.`, + ); + } + if (error && typeof error === 'object' && error.code === 'EACCES') { + throw new Error(`macOS window helper is not executable: ${helperPath}`); + } + throw error; + } + + return true; +} + async function afterPack(context) { await stageLinuxAppImageSharedLibrary(context); + await verifyMacOSWindowHelper(context); } module.exports = { LINUX_FFMPEG_LIBRARY, + MACOS_WINDOW_HELPER, + resolveMacOSAppBundlePath, stageLinuxAppImageSharedLibrary, + verifyMacOSWindowHelper, default: afterPack, }; diff --git a/scripts/electron-builder-after-pack.test.ts b/scripts/electron-builder-after-pack.test.ts index 605d6bc6..5470b62c 100644 --- a/scripts/electron-builder-after-pack.test.ts +++ b/scripts/electron-builder-after-pack.test.ts @@ -6,15 +6,22 @@ import test from 'node:test'; const { LINUX_FFMPEG_LIBRARY, + MACOS_WINDOW_HELPER, default: afterPack, stageLinuxAppImageSharedLibrary, + verifyMacOSWindowHelper, } = require('./electron-builder-after-pack.cjs') as { LINUX_FFMPEG_LIBRARY: string; + MACOS_WINDOW_HELPER: string; default: (context: { appOutDir: string; electronPlatformName: string }) => Promise; stageLinuxAppImageSharedLibrary: (context: { appOutDir: string; electronPlatformName: string; }) => Promise; + verifyMacOSWindowHelper: (context: { + appOutDir: string; + electronPlatformName: string; + }) => Promise; }; function createWorkspace(name: string): string { @@ -65,6 +72,53 @@ test('stageLinuxAppImageSharedLibrary skips non-Linux packaging contexts', async } }); +test('verifyMacOSWindowHelper accepts packaged macOS helper binary', async () => { + const workspace = createWorkspace('subminer-after-pack-macos-helper'); + const appOutDir = path.join(workspace, 'SubMiner-darwin-arm64'); + const helperPath = path.join( + appOutDir, + 'SubMiner.app', + 'Contents', + 'Resources', + 'scripts', + MACOS_WINDOW_HELPER, + ); + + fs.mkdirSync(path.dirname(helperPath), { recursive: true }); + fs.writeFileSync(helperPath, 'compiled helper', 'utf8'); + fs.chmodSync(helperPath, 0o755); + + try { + const verified = await verifyMacOSWindowHelper({ + appOutDir, + electronPlatformName: 'darwin', + }); + + assert.equal(verified, true); + } finally { + fs.rmSync(workspace, { recursive: true, force: true }); + } +}); + +test('verifyMacOSWindowHelper throws when macOS helper binary is missing', async () => { + const workspace = createWorkspace('subminer-after-pack-macos-helper-missing'); + const appOutDir = path.join(workspace, 'SubMiner-darwin-arm64'); + + fs.mkdirSync(path.join(appOutDir, 'SubMiner.app', 'Contents', 'Resources'), { recursive: true }); + + try { + await assert.rejects( + verifyMacOSWindowHelper({ + appOutDir, + electronPlatformName: 'darwin', + }), + /macOS packaging requires get-mpv-window-macos/, + ); + } finally { + fs.rmSync(workspace, { recursive: true, force: true }); + } +}); + test('stageLinuxAppImageSharedLibrary throws when Linux packaging is missing libffmpeg.so', async () => { const workspace = createWorkspace('subminer-after-pack-missing-library'); const appOutDir = path.join(workspace, 'SubMiner-linux-x64'); diff --git a/scripts/prepare-build-assets.mjs b/scripts/prepare-build-assets.mjs index c5a3901e..974af944 100644 --- a/scripts/prepare-build-assets.mjs +++ b/scripts/prepare-build-assets.mjs @@ -63,6 +63,8 @@ function buildMacosHelper() { return; } + ensureDir(scriptsOutputDir); + try { execFileSync('swiftc', ['-O', macosHelperSourcePath, '-o', macosHelperBinaryPath], { stdio: 'inherit', diff --git a/scripts/prepare-build-assets.test.ts b/scripts/prepare-build-assets.test.ts new file mode 100644 index 00000000..13f64fe2 --- /dev/null +++ b/scripts/prepare-build-assets.test.ts @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const source = readFileSync('scripts/prepare-build-assets.mjs', 'utf8'); + +test('macOS helper build creates dist scripts directory before swiftc output', () => { + const buildFunctionIndex = source.indexOf('function buildMacosHelper()'); + assert.notEqual(buildFunctionIndex, -1); + + const swiftcIndex = source.indexOf("execFileSync('swiftc'", buildFunctionIndex); + assert.notEqual(swiftcIndex, -1); + + const ensureDirIndex = source.lastIndexOf('ensureDir(scriptsOutputDir)', swiftcIndex); + + assert.ok( + ensureDirIndex > buildFunctionIndex, + 'buildMacosHelper must create dist/scripts before swiftc writes the helper binary', + ); +}); diff --git a/src/release-workflow.test.ts b/src/release-workflow.test.ts index 7e152bf4..cc678f1b 100644 --- a/src/release-workflow.test.ts +++ b/src/release-workflow.test.ts @@ -190,6 +190,14 @@ test('release packaging stages generated launcher as an app resource', () => { assert.match(packageJson.scripts['build:launcher'] ?? '', /--banner='#!\/usr\/bin\/env bun'/); }); +test('release packaging does not reference removed Windows window helper script', () => { + assert.ok( + !(packageJson.build?.extraResources ?? []).some((resource) => + [resource.from, resource.to].some((value) => value?.includes('get-mpv-window-windows.ps1')), + ), + ); +}); + test('Makefile clean preserves committed prerelease notes', () => { assert.match(makefile, /PRERELEASE_NOTES_BACKUP/); assert.match(makefile, /release\/prerelease-notes\.md/);