mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-05 12:12:05 -07:00
Compare commits
3 Commits
main
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
49ef910e52
|
|||
|
1a70a59ab1
|
|||
|
fa89c43f74
|
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
id: TASK-279
|
||||||
|
title: Fix Linux AppImage child-process libffmpeg resolution
|
||||||
|
status: Done
|
||||||
|
assignee:
|
||||||
|
- '@codex'
|
||||||
|
created_date: '2026-04-05 17:17'
|
||||||
|
updated_date: '2026-04-05 17:56'
|
||||||
|
labels: []
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- 'https://github.com/ksyasuda/SubMiner/issues/41'
|
||||||
|
documentation:
|
||||||
|
- docs/workflow/verification.md
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Fix the Linux AppImage packaging so Chromium child processes relaunched from the bundled binary can resolve the packaged libffmpeg shared library and SubMiner starts cleanly instead of crash-looping on network-service restarts.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Linux AppImage packaging ensures bundled Chromium child processes can resolve the packaged libffmpeg shared library during relaunch.
|
||||||
|
- [x] #2 Regression coverage exercises the Linux packaging/build configuration that provides the AppImage shared-library path.
|
||||||
|
- [x] #3 Release notes/changelog reflect the Linux AppImage startup fix.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
<!-- SECTION:PLAN:BEGIN -->
|
||||||
|
1. Add focused regression tests for Linux release packaging that assert the build config invokes an `afterPack` hook and that the hook stages bundled `libffmpeg.so` into `usr/lib` for AppImage runtime lookup.
|
||||||
|
2. Implement a small electron-builder `afterPack` hook that runs only for Linux, copies `libffmpeg.so` from the packaged app root into `usr/lib`, and no-ops when the source library is absent.
|
||||||
|
3. Wire the hook into `package.json` build config and add a changelog fragment for the Linux AppImage startup fix.
|
||||||
|
4. Run the focused test lane first, then the default handoff gate because the change touches release-sensitive packaging behavior.
|
||||||
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Chose a repo-local electron-builder `afterPack` hook instead of patching/forking `electron-builder`. The hook copies bundled `libffmpeg.so` from the packaged Linux app root into `usr/lib`, matching the AppImage runtime's existing `LD_LIBRARY_PATH` search path.
|
||||||
|
|
||||||
|
Added regression coverage for both config wiring (`src/release-workflow.test.ts`) and the hook behavior (`scripts/electron-builder-after-pack.test.ts`), then wired the new script test into `test:fast` so the maintained lane keeps exercising the fix.
|
||||||
|
|
||||||
|
Verification passed: `bun test scripts/electron-builder-after-pack.test.ts src/release-workflow.test.ts`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run test:smoke:dist`.
|
||||||
|
|
||||||
|
Addressed PR #45 CodeRabbit review thread: Linux `afterPack` staging now hard-fails when `libffmpeg.so` is missing instead of silently no-oping. Updated focused hook tests to assert the new failure contract and that `afterPack` propagates Linux staging errors.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Added a shared electron-builder `afterPack` hook at `scripts/electron-builder-after-pack.cjs` and wired it into `package.json` so Linux packaging stages the bundled `libffmpeg.so` into `usr/lib` inside the packaged app. This keeps Chromium child relaunches compatible with the AppImage runtime's existing `LD_LIBRARY_PATH` layout without forking or patching upstream `electron-builder`.
|
||||||
|
|
||||||
|
Regression coverage now checks both the packaging config and the hook behavior: `src/release-workflow.test.ts` asserts the hook stays wired into release config, and `scripts/electron-builder-after-pack.test.ts` verifies Linux copies `libffmpeg.so` into `usr/lib` while non-Linux and missing-library cases no-op safely. The new script test is included in `test:fast`, and a changelog fragment was added under `changes/fix-appimage-libffmpeg-path.md`.
|
||||||
|
|
||||||
|
Verification passed with `bun test scripts/electron-builder-after-pack.test.ts src/release-workflow.test.ts`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, and `bun run test:smoke:dist`.
|
||||||
|
|
||||||
|
Follow-up review fix on PR #45: Linux packaging now throws when `libffmpeg.so` is missing from the packaged app root, preventing silent shipment of a broken AppImage. Focused regression coverage was updated so the missing-library case rejects and `afterPack` propagates the failure.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
4
changes/fix-appimage-libffmpeg-path.md
Normal file
4
changes/fix-appimage-libffmpeg-path.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: release
|
||||||
|
|
||||||
|
- Fixed Linux AppImage startup packaging so Chromium child relaunches can resolve the bundled `libffmpeg.so` instead of crash-looping on startup.
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"test:launcher": "bun run test:launcher:src",
|
"test:launcher": "bun run test:launcher:src",
|
||||||
"test:core": "bun run test:core:src",
|
"test:core": "bun run test:core:src",
|
||||||
"test:subtitle": "bun run test:subtitle: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/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/ci-workflow.test.ts scripts/build-changelog.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/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/ci-workflow.test.ts scripts/build-changelog.test.ts scripts/electron-builder-after-pack.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",
|
"generate:config-example": "bun run src/generate-config-example.ts",
|
||||||
"verify:config-example": "bun run src/verify-config-example.ts",
|
"verify:config-example": "bun run src/verify-config-example.ts",
|
||||||
"start": "bun run build && electron . --start",
|
"start": "bun run build && electron . --start",
|
||||||
@@ -128,6 +128,7 @@
|
|||||||
"productName": "SubMiner",
|
"productName": "SubMiner",
|
||||||
"executableName": "SubMiner",
|
"executableName": "SubMiner",
|
||||||
"artifactName": "SubMiner-${version}.${ext}",
|
"artifactName": "SubMiner-${version}.${ext}",
|
||||||
|
"afterPack": "scripts/electron-builder-after-pack.cjs",
|
||||||
"icon": "assets/SubMiner-square.png",
|
"icon": "assets/SubMiner-square.png",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "release"
|
"output": "release"
|
||||||
|
|||||||
46
scripts/electron-builder-after-pack.cjs
Normal file
46
scripts/electron-builder-after-pack.cjs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
const fs = require('node:fs/promises');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const LINUX_FFMPEG_LIBRARY = 'libffmpeg.so';
|
||||||
|
|
||||||
|
async function stageLinuxAppImageSharedLibrary(
|
||||||
|
context,
|
||||||
|
deps = {
|
||||||
|
access: (filePath) => fs.access(filePath),
|
||||||
|
mkdir: (dirPath) => fs.mkdir(dirPath, { recursive: true }),
|
||||||
|
copyFile: (from, to) => fs.copyFile(from, to),
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (context.electronPlatformName !== 'linux') return false;
|
||||||
|
|
||||||
|
const sourceLibraryPath = path.join(context.appOutDir, LINUX_FFMPEG_LIBRARY);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deps.access(sourceLibraryPath);
|
||||||
|
} catch (error) {
|
||||||
|
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
||||||
|
throw new Error(
|
||||||
|
`Linux packaging requires ${LINUX_FFMPEG_LIBRARY} at ${sourceLibraryPath} so AppImage child processes can resolve it.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetLibraryDir = path.join(context.appOutDir, 'usr', 'lib');
|
||||||
|
const targetLibraryPath = path.join(targetLibraryDir, LINUX_FFMPEG_LIBRARY);
|
||||||
|
|
||||||
|
await deps.mkdir(targetLibraryDir);
|
||||||
|
await deps.copyFile(sourceLibraryPath, targetLibraryPath);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function afterPack(context) {
|
||||||
|
await stageLinuxAppImageSharedLibrary(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
LINUX_FFMPEG_LIBRARY,
|
||||||
|
stageLinuxAppImageSharedLibrary,
|
||||||
|
default: afterPack,
|
||||||
|
};
|
||||||
104
scripts/electron-builder-after-pack.test.ts
Normal file
104
scripts/electron-builder-after-pack.test.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
const {
|
||||||
|
LINUX_FFMPEG_LIBRARY,
|
||||||
|
default: afterPack,
|
||||||
|
stageLinuxAppImageSharedLibrary,
|
||||||
|
} = require('./electron-builder-after-pack.cjs') as {
|
||||||
|
LINUX_FFMPEG_LIBRARY: string;
|
||||||
|
default: (context: { appOutDir: string; electronPlatformName: string }) => Promise<void>;
|
||||||
|
stageLinuxAppImageSharedLibrary: (context: {
|
||||||
|
appOutDir: string;
|
||||||
|
electronPlatformName: string;
|
||||||
|
}) => Promise<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createWorkspace(name: string): string {
|
||||||
|
return fs.mkdtempSync(path.join(os.tmpdir(), `${name}-`));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('stageLinuxAppImageSharedLibrary copies libffmpeg.so into usr/lib for Linux packaging', async () => {
|
||||||
|
const workspace = createWorkspace('subminer-after-pack-linux');
|
||||||
|
const appOutDir = path.join(workspace, 'SubMiner-linux-x64');
|
||||||
|
const sourceLibraryPath = path.join(appOutDir, LINUX_FFMPEG_LIBRARY);
|
||||||
|
const targetLibraryPath = path.join(appOutDir, 'usr', 'lib', LINUX_FFMPEG_LIBRARY);
|
||||||
|
|
||||||
|
fs.mkdirSync(appOutDir, { recursive: true });
|
||||||
|
fs.writeFileSync(sourceLibraryPath, 'bundled ffmpeg', 'utf8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const staged = await stageLinuxAppImageSharedLibrary({
|
||||||
|
appOutDir,
|
||||||
|
electronPlatformName: 'linux',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(staged, true);
|
||||||
|
assert.equal(fs.readFileSync(targetLibraryPath, 'utf8'), 'bundled ffmpeg');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(workspace, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stageLinuxAppImageSharedLibrary skips non-Linux packaging contexts', async () => {
|
||||||
|
const workspace = createWorkspace('subminer-after-pack-non-linux');
|
||||||
|
const appOutDir = path.join(workspace, 'SubMiner-darwin-arm64');
|
||||||
|
const sourceLibraryPath = path.join(appOutDir, LINUX_FFMPEG_LIBRARY);
|
||||||
|
const targetLibraryPath = path.join(appOutDir, 'usr', 'lib', LINUX_FFMPEG_LIBRARY);
|
||||||
|
|
||||||
|
fs.mkdirSync(appOutDir, { recursive: true });
|
||||||
|
fs.writeFileSync(sourceLibraryPath, 'bundled ffmpeg', 'utf8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const staged = await stageLinuxAppImageSharedLibrary({
|
||||||
|
appOutDir,
|
||||||
|
electronPlatformName: 'darwin',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(staged, false);
|
||||||
|
assert.equal(fs.existsSync(targetLibraryPath), false);
|
||||||
|
} 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');
|
||||||
|
|
||||||
|
fs.mkdirSync(appOutDir, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assert.rejects(
|
||||||
|
stageLinuxAppImageSharedLibrary({
|
||||||
|
appOutDir,
|
||||||
|
electronPlatformName: 'linux',
|
||||||
|
}),
|
||||||
|
new RegExp(`Linux packaging requires ${LINUX_FFMPEG_LIBRARY} at .*${LINUX_FFMPEG_LIBRARY}`),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(workspace, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('afterPack propagates Linux staging failures', async () => {
|
||||||
|
const workspace = createWorkspace('subminer-after-pack-propagates-linux-failure');
|
||||||
|
const appOutDir = path.join(workspace, 'SubMiner-linux-x64');
|
||||||
|
|
||||||
|
fs.mkdirSync(appOutDir, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assert.rejects(
|
||||||
|
afterPack({
|
||||||
|
appOutDir,
|
||||||
|
electronPlatformName: 'linux',
|
||||||
|
}),
|
||||||
|
/Linux packaging requires libffmpeg\.so/,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(workspace, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -13,6 +13,7 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
|
|||||||
productName?: string;
|
productName?: string;
|
||||||
scripts: Record<string, string>;
|
scripts: Record<string, string>;
|
||||||
build?: {
|
build?: {
|
||||||
|
afterPack?: string;
|
||||||
files?: string[];
|
files?: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -77,6 +78,10 @@ test('release package scripts disable implicit electron-builder publishing', ()
|
|||||||
assert.match(packageJson.scripts['build:win:unsigned'] ?? '', /build-win-unsigned\.mjs/);
|
assert.match(packageJson.scripts['build:win:unsigned'] ?? '', /build-win-unsigned\.mjs/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('release packaging wires a shared afterPack hook for Linux AppImage library staging', () => {
|
||||||
|
assert.equal(packageJson.build?.afterPack, 'scripts/electron-builder-after-pack.cjs');
|
||||||
|
});
|
||||||
|
|
||||||
test('top-level package metadata keeps Linux Electron runtime app identity canonical', () => {
|
test('top-level package metadata keeps Linux Electron runtime app identity canonical', () => {
|
||||||
assert.equal(packageJson.productName, 'SubMiner');
|
assert.equal(packageJson.productName, 'SubMiner');
|
||||||
assert.equal(packageJson.desktopName, 'SubMiner.desktop');
|
assert.equal(packageJson.desktopName, 'SubMiner.desktop');
|
||||||
|
|||||||
Reference in New Issue
Block a user