docs: plan Linux frame-copy packaging

This commit is contained in:
4gray committed 2026-07-17 22:42:36 +02:00
1 parent 2877ec59e5
commit a3fbfb1bdc
1 file changed
+654
@@ -0,0 +1,654 @@
# Linux Embedded MPV Frame-Copy Packaging Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Ship a verified Linux x64 frame-copy runtime in AppImage, DEB, RPM, Pacman, Snap, and Flatpak while preserving out-of-process libmpv isolation and honest native-view fallback.
**Architecture:** Split official Linux packaging into system-runtime and bundled-runtime passes because Electron Builder reuses one unpacked layout per pass. A versioned manifest plus a real helper `--runtime-probe` gates frame-copy before BrowserWindow creation; only the helper may link libmpv, and foreign-architecture packages retain the unavailable marker.
**Tech Stack:** Electron 41, Node/TypeScript, C++17/N-API, libmpv render API, EGL/OpenGL/GBM, ELF/RPATH tooling, electron-builder 26, Nx/Jest/node:test, Playwright, GitHub Actions, AppImage/DEB/RPM/Pacman/Snap/Flatpak.
---
## File Map
### Runtime contracts and staging
- Create `tools/embedded-mpv/linux-runtime-manifest.cjs`
- Parse, normalize, and validate Linux frame-copy runtime manifests.
- Create `tools/embedded-mpv/build-linux-runtime.mjs`
- Build the pinned LGPL-compatible FFmpeg/libass/libplacebo/libmpv prefix.
- Modify `tools/embedded-mpv/stage-runtime.mjs`
- Stage Linux shared libraries and reject incomplete release manifests.
- Modify `apps/electron-backend/build-embedded-mpv.js`
- Build against staged Linux libmpv, copy the bundled closure, and emit the
profile-neutral build manifest.
- Modify `apps/electron-backend/native/binding.gyp`
- Keep helper RPATH relative and remove build-host RPATH.
- Modify `package.json`
- Expose the Linux runtime build command.
### Packaging profiles and validation
- Create `tools/packaging/linux-frame-copy-profile.cjs`
- Own profile names, target sets, manifest origins, and package dependencies.
- Create `tools/packaging/linux-frame-copy-profile.test.mjs`
- Verify profile/target/dependency mapping and invalid combinations.
- Modify `electron-builder.json`
- Declare DEB/RPM/Pacman libmpv dependencies.
- Modify `tools/packaging/embedded-mpv-frame-copy-files.cjs`
- Package Linux helper/reader and select/remove private runtime by profile.
- Modify `tools/packaging/embedded-mpv-packaging.cjs`
- Validate Linux ELF linkage, manifest, files, modes, RPATH, and isolation.
- Modify `tools/packaging/embedded-mpv-arch.test.mjs`
- Cover system, bundled, malformed, and foreign-architecture layouts.
- Modify `tools/packaging/electron-after-pack.cjs`
- Pass the required Linux profile into preparation and validation.
- Modify `tools/packaging/verify-electron-package-layout.mjs`
- Verify the expected profile for every unpacked layout.
- Modify `tools/packaging/project.json`
- Add new tests and source inputs.
### Runtime capability probe
- Modify `apps/electron-backend/native/helper/frame_helper_gl.h`
- Provide a context-only probe that does not create a playback session.
- Modify `apps/electron-backend/native/helper/mpv_frame_helper.cpp`
- Implement the versioned `--runtime-probe` JSON protocol.
- Create `apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime.ts`
- Validate manifest/files and run/cache the bounded helper probe.
- Create `apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime.spec.ts`
- Cover all fail-closed paths and success caching.
- Modify `apps/electron-backend/src/app/services/embedded-mpv-frame-copy-platform.util.ts`
- Resolve an artifact set and delegate usability to the runtime probe.
- Modify `apps/electron-backend/src/app/services/embedded-mpv-frame-copy-platform.util.spec.ts`
- Keep path/security coverage and add manifest/probe integration cases.
- Modify `apps/electron-backend/src/app/services/embedded-mpv-native.service.ts`
- Surface stable fallback diagnostics without changing native-view safety.
### Linux CI, package smoke, and documentation
- Create `tools/packaging/verify-linux-frame-copy-runtime.mjs`
- Inspect real package payload ELF/modes/manifest and invoke the helper probe.
- Create `tools/packaging/verify-linux-frame-copy-runtime.test.mjs`
- Unit-test verifier parsing and failure reporting with fixtures.
- Create `apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged.e2e.ts`
- Exercise packaged capability, a deterministic media frame, and fallback.
- Modify `.github/workflows/build-and-make.yaml`
- Build/cache runtime, split profiles, inspect every format, and run sandbox
and container smoke coverage.
- Modify `docs/architecture/embedded-mpv-native.md`
- Modify `tools/embedded-mpv/README.md`
- Modify `vendor/embedded-mpv/README.md`
- Modify `AGENTS.md`
- Modify `CLAUDE.md`
- Document the final contract and verification matrix.
## Task 1: Define Linux Packaging Profiles
**Files:**
- Create: `tools/packaging/linux-frame-copy-profile.cjs`
- Create: `tools/packaging/linux-frame-copy-profile.test.mjs`
- Modify: `electron-builder.json`
- Modify: `tools/packaging/project.json`
- [ ] **Step 1: Write failing profile tests**
Cover the exact public API:
```js
assert.deepEqual(resolveLinuxFrameCopyProfile('system'), {
name: 'system',
runtimeMode: 'system',
targets: ['deb', 'rpm', 'pacman'],
manifestOrigin: 'system-libmpv-frame-copy',
});
assert.deepEqual(resolveLinuxFrameCopyProfile('portable').targets, [
'appimage',
'snap',
]);
assert.deepEqual(resolveLinuxFrameCopyProfile('flatpak').targets, ['flatpak']);
assert.throws(() => resolveLinuxFrameCopyProfile('standard'), /Unsupported/);
assert.deepEqual(LINUX_SYSTEM_PACKAGE_DEPENDENCIES, {
deb: 'libmpv2',
rpm: 'mpv-libs',
pacman: 'mpv',
});
assert.deepEqual(
validateLinuxProfileTargets('system', ['deb', 'AppImage']),
['Linux frame-copy profile "system" cannot build target "appimage".']
);
```
- [ ] **Step 2: Run RED**
```bash
node --test tools/packaging/linux-frame-copy-profile.test.mjs
```
Expected: FAIL because the profile module does not exist.
- [ ] **Step 3: Implement the profile module and package dependencies**
Export immutable `LINUX_FRAME_COPY_PROFILES`,
`LINUX_SYSTEM_PACKAGE_DEPENDENCIES`, `resolveLinuxFrameCopyProfile()`, and
`validateLinuxProfileTargets()`. Add `deb.depends += libmpv2`,
`rpm.depends += mpv-libs`, and `pacman.depends += mpv` without replacing
Electron Builder's existing defaults.
- [ ] **Step 4: Register and run GREEN**
Add the new test to `packaging:test`, then run:
```bash
pnpm nx test packaging --skip-nx-cache
```
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add electron-builder.json tools/packaging
git commit -m "feat(packaging): define Linux frame-copy profiles"
```
## Task 2: Stage A Pinned LGPL Linux Runtime
**Files:**
- Create: `tools/embedded-mpv/linux-runtime-manifest.cjs`
- Create: `tools/embedded-mpv/linux-runtime-manifest.test.mjs`
- Create: `tools/embedded-mpv/build-linux-runtime.mjs`
- Modify: `tools/embedded-mpv/stage-runtime.mjs`
- Modify: `package.json`
- Modify: `tools/packaging/project.json`
- [ ] **Step 1: Write failing manifest/staging tests**
Use temporary prefixes to prove:
```js
assert.equal(validateLinuxRuntimeManifest(validManifest).length, 0);
assert.match(
validateLinuxRuntimeManifest({
...validManifest,
ffmpeg: { configureFlags: ['--enable-gpl'] },
})[0],
/--enable-gpl/
);
assert.match(
validateLinuxRuntimeManifest({
...validManifest,
mpv: { mesonFlags: ['-Dgpl=true'] },
})[0],
/-Dgpl=false/
);
```
Exercise `stage-runtime.mjs linux x64 <prefix>` and assert it copies
`libmpv.so.2` plus all manifest-declared `.so` files and records byte sizes.
- [ ] **Step 2: Run RED**
```bash
node --test tools/embedded-mpv/linux-runtime-manifest.test.mjs
```
Expected: FAIL because the validator/build/staging contract is absent.
- [ ] **Step 3: Implement the source builder**
Reuse the pinned package versions already used by the macOS builder. Linux
FFmpeg flags must include:
```text
--enable-shared --disable-static --disable-programs --disable-doc
--disable-debug --disable-autodetect --disable-gpl --disable-nonfree
--enable-pic --enable-pthreads
```
Linux mpv flags must include:
```text
-Dgpl=false -Dlibmpv=true -Dcplayer=false -Dtests=false
-Dlua=disabled -Djavascript=disabled -Dcplugins=disabled
-Dlibarchive=disabled -Dlibbluray=disabled -Ddvdnav=disabled
-Dcdda=disabled -Ddvbin=disabled -Dvulkan=disabled
-Dplain-gl=enabled -Degl=enabled -Dgbm=enabled
```
Record downloaded SHA-256 values, git commits/submodules, exact flags, runtime
file names/sizes, and source-distribution obligations.
- [ ] **Step 4: Implement Linux staging**
Require `include/mpv/client.h`, a versioned `libmpv.so.*`, and a valid manifest.
Copy only manifest-declared shared libraries and preserve SONAME symlinks as
materialized regular files so Electron Builder cannot lose them.
- [ ] **Step 5: Run GREEN and static policy checks**
```bash
pnpm nx test packaging --skip-nx-cache
node --check tools/embedded-mpv/build-linux-runtime.mjs
node --check tools/embedded-mpv/stage-runtime.mjs
```
Expected: PASS and no GPL/nonfree-enabling flag in generated policy fixtures.
- [ ] **Step 6: Commit**
```bash
git add package.json tools/embedded-mpv tools/packaging/project.json
git commit -m "feat(embedded-mpv): stage LGPL Linux runtime"
```
## Task 3: Build A Relocatable Isolated Helper
**Files:**
- Modify: `apps/electron-backend/native/binding.gyp`
- Modify: `apps/electron-backend/build-embedded-mpv.js`
- Test: `apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts`
- [ ] **Step 1: Extend source-policy tests**
Assert the Linux helper has `$ORIGIN/lib` and no absolute build-host RPATH,
the addon does not use `-lmpv`, the staged closure is copied, and the build
manifest identifies both allowed package modes.
- [ ] **Step 2: Run RED**
```bash
pnpm nx test electron-backend --skip-nx-cache --runInBand \
--testPathPatterns=embedded-mpv-native-source.spec
```
Expected: FAIL on the current absolute `LINUX_NATIVE_LIBRARY_DIR` RPATH and
`external-mpv-process`-only manifest.
- [ ] **Step 3: Update native build integration**
Link the helper against staged `libmpv.so`, retain only:
```text
-Wl,--enable-new-dtags
-Wl,-rpath,$ORIGIN/lib
```
Copy the staged shared-library closure to build output for downstream bundled
profiles, but keep `embedded_mpv.node` dynamically independent of libmpv.
Write a build manifest that carries the runtime metadata without prematurely
choosing `system` versus `portable`.
- [ ] **Step 4: Run GREEN**
```bash
pnpm nx test electron-backend --skip-nx-cache --runInBand \
--testPathPatterns=embedded-mpv-native-source.spec
```
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add apps/electron-backend/native/binding.gyp \
apps/electron-backend/build-embedded-mpv.js \
apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts
git commit -m "feat(embedded-mpv): build relocatable Linux helper"
```
## Task 4: Package And Validate Each Runtime Mode
**Files:**
- Modify: `tools/packaging/embedded-mpv-frame-copy-files.cjs`
- Modify: `tools/packaging/embedded-mpv-packaging.cjs`
- Modify: `tools/packaging/embedded-mpv-arch.test.mjs`
- Modify: `tools/packaging/electron-after-pack.cjs`
- Modify: `tools/packaging/verify-electron-package-layout.mjs`
- [ ] **Step 1: Write failing package-layout tests**
Create realistic temp layouts and prove:
- system mode requires executable helper, reader, system manifest, no
`native/lib/libmpv*`;
- bundled mode requires all manifest files and rejects missing/runtime-prefix
links;
- helper mode `0644` is rejected;
- reader symlinks/directories are rejected;
- Linux addon or Electron `DT_NEEDED libmpv` is rejected;
- helper without `DT_NEEDED libmpv.so.2` is rejected;
- foreign-arch layout contains only the unavailable marker.
- [ ] **Step 2: Run RED**
```bash
pnpm nx test packaging --skip-nx-cache
```
Expected: FAIL because Linux helpers are deleted and validation forbids them.
- [ ] **Step 3: Implement profile-aware preparation**
For x64:
- restore helper mode `0755`;
- always retain the frame reader;
- `system`: remove private runtime and write normalized system manifest;
- `portable`/`flatpak`: retain only manifest-declared closure and write bundled
manifest;
- reject missing `IPTVNATOR_LINUX_FRAME_COPY_PROFILE` when Embedded MPV is
required.
For foreign architectures, keep the current unavailable marker behavior and
remove every native artifact.
- [ ] **Step 4: Implement ELF/package validation**
Use `readelf -d` for `NEEDED` and RPATH/RUNPATH inspection. Resolve bundled
closure recursively from the private directory and permit only a documented
glibc/driver/system allowlist outside it. Keep validation host-aware: pure
manifest/mode checks run everywhere; ELF inspection is required on Linux CI.
- [ ] **Step 5: Run GREEN**
```bash
pnpm nx test packaging --skip-nx-cache
pnpm nx test electron-backend --skip-nx-cache --runInBand \
--testPathPatterns=embedded-mpv-native-source.spec
```
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
git add tools/packaging
git commit -m "feat(packaging): ship Linux frame-copy artifacts"
```
## Task 5: Add The Real Runtime Capability Probe
**Files:**
- Modify: `apps/electron-backend/native/helper/frame_helper_gl.h`
- Modify: `apps/electron-backend/native/helper/mpv_frame_helper.cpp`
- Create: `apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime.ts`
- Create: `apps/electron-backend/src/app/services/embedded-mpv-frame-copy-runtime.spec.ts`
- Modify: `apps/electron-backend/src/app/services/embedded-mpv-frame-copy-platform.util.ts`
- Modify: `apps/electron-backend/src/app/services/embedded-mpv-frame-copy-platform.util.spec.ts`
- Modify: `apps/electron-backend/src/app/services/embedded-mpv-native.service.ts`
- [ ] **Step 1: Write failing TypeScript probe tests**
Inject filesystem and `spawnSync` collaborators. Cover:
```ts
expect(probeRuntime(validArtifacts, successSpawn).usable).toBe(true);
expect(probeRuntime(validArtifacts, timeoutSpawn).reason).toBe(
'helper-probe-timeout'
);
expect(probeRuntime(validArtifacts, nonzeroSpawn).reason).toBe(
'helper-probe-failed'
);
expect(probeRuntime(validArtifacts, invalidJsonSpawn).reason).toBe(
'helper-probe-invalid-output'
);
expect(probeRuntime(missingManifest, successSpawn).reason).toBe(
'runtime-manifest-missing'
);
expect(successSpawn).toHaveBeenCalledTimes(1);
```
The last assertion calls the public probe twice and proves process-lifetime
caching.
- [ ] **Step 2: Run RED**
```bash
pnpm nx test electron-backend --skip-nx-cache --runInBand \
--testPathPatterns=embedded-mpv-frame-copy-runtime.spec
```
Expected: FAIL because the runtime probe module does not exist.
- [ ] **Step 3: Implement helper `--runtime-probe`**
Emit exactly one line:
```json
{"protocol":1,"usable":true,"libmpv":"2.x","renderApi":"egl"}
```
Exit nonzero with a JSON `reason` when `mpv_create`, `mpv_initialize`, or the
EGL/OpenGL context probe fails. Do not create shared memory, open a URL, or
enter the normal command loop.
- [ ] **Step 4: Implement fail-closed main-process probing**
Validate manifest/artifacts first, then run:
```ts
spawnSync(helperPath, ['--runtime-probe'], {
encoding: 'utf8',
timeout: 3000,
windowsHide: true,
env: probeEnvironment,
});
```
Cache by helper/manifest identity. Never throw across startup; return a stable
reason and make `isFrameCopyRuntimeUsable()` depend on `.usable`.
- [ ] **Step 5: Run GREEN and related regression tests**
```bash
pnpm nx test electron-backend --skip-nx-cache --runInBand \
--testPathPatterns='embedded-mpv-frame-copy-(runtime|platform).util.spec|app.spec|embedded-mpv-native.service.spec'
```
Expected: PASS; unavailable runtime keeps sandbox enabled and selects native.
- [ ] **Step 6: Commit**
```bash
git add apps/electron-backend/native/helper \
apps/electron-backend/src/app/services
git commit -m "feat(embedded-mpv): probe Linux frame-copy runtime"
```
## Task 6: Split Linux CI And Inspect Every Artifact
**Files:**
- Create: `tools/packaging/verify-linux-frame-copy-runtime.mjs`
- Create: `tools/packaging/verify-linux-frame-copy-runtime.test.mjs`
- Modify: `.github/workflows/build-and-make.yaml`
- Modify: `tools/packaging/project.json`
- [ ] **Step 1: Write failing verifier tests**
Test payload discovery for `.AppImage`, `.deb`, `.rpm`, Pacman archive,
`.snap`, and `.flatpak`, plus clear errors for a missing helper, wrong mode,
wrong profile, direct addon libmpv linkage, and unresolved helper dependency.
- [ ] **Step 2: Run RED**
```bash
node --test tools/packaging/verify-linux-frame-copy-runtime.test.mjs
```
Expected: FAIL because the verifier does not exist.
- [ ] **Step 3: Implement artifact verification**
Provide `--artifact <path> --profile <name>`. Extract/mount into a temp
directory, find the native layout, run manifest/mode/ELF checks, and execute
`iptvnator_mpv_helper --runtime-probe` in the package's intended environment.
Always clean temporary mounts/directories.
- [ ] **Step 4: Split and harden CI**
Change Linux matrix entries to:
```yaml
- os: linux
linux_profile: system
- os: linux
linux_profile: portable
- os: linux
linux_profile: flatpak
```
Filter targets exactly per profile and set
`IPTVNATOR_LINUX_FRAME_COPY_PROFILE`. Build/cache the pinned runtime once per
source/tool hash. Add format-specific extraction/installation tools and invoke
the verifier for every produced artifact.
Run system formats in matching containers with `libmpv2`, `mpv-libs`, or
`mpv`; run AppImage directly with extraction fallback; install and probe Snap
and Flatpak inside their sandboxes.
- [ ] **Step 5: Run GREEN and workflow source regressions**
```bash
pnpm nx test packaging --skip-nx-cache
pnpm nx test electron-backend --skip-nx-cache --runInBand \
--testPathPatterns=embedded-mpv-native-source.spec
```
Expected: PASS and source tests prove all three profiles and six formats.
- [ ] **Step 6: Commit**
```bash
git add .github/workflows/build-and-make.yaml tools/packaging
git commit -m "ci: verify Linux frame-copy packages"
```
## Task 7: Add Packaged Playback And Fallback Smoke Coverage
**Files:**
- Create: `apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged.e2e.ts`
- Modify: `.github/workflows/build-and-make.yaml`
- [ ] **Step 1: Write the packaged E2E**
Use the existing Electron test fixtures and a generated two-second local media
fixture. Assert the support response reports `frameCopyAvailable: true`,
activate the engine, load the fixture, observe a nonzero frame generation and
playing/paused snapshot, then relaunch with the runtime hidden and assert
native engine selection without a main-process crash.
- [ ] **Step 2: Run the closest local parse/list check**
```bash
pnpm nx lint electron-backend-e2e
pnpm nx show project electron-backend-e2e
```
Expected: PASS on macOS; the actual test is Linux-packaged-only.
- [ ] **Step 3: Wire the Linux packaged smoke**
Run the spec against the unpacked x64 bundled layout under software EGL
(`LIBGL_ALWAYS_SOFTWARE=1`) and keep a hardware-enabled smoke as a separate
non-blocking diagnostic when the CI runner exposes DRI.
- [ ] **Step 4: Commit**
```bash
git add apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged.e2e.ts \
.github/workflows/build-and-make.yaml
git commit -m "test(embedded-mpv): smoke packaged Linux frame-copy"
```
## Task 8: Update Canonical Documentation
**Files:**
- Modify: `docs/architecture/embedded-mpv-native.md`
- Modify: `tools/embedded-mpv/README.md`
- Modify: `vendor/embedded-mpv/README.md`
- Modify: `AGENTS.md`
- Modify: `CLAUDE.md`
- [ ] **Step 1: Replace the dev-only Linux contract**
Document x64 support across all six formats, the three profiles, dependency
names, runtime manifest/probe, codec baseline, source obligations, fallback,
and ARM unavailable behavior. Keep `AGENTS.md` and `CLAUDE.md` synchronized.
- [ ] **Step 2: Verify documentation consistency**
```bash
rg -n "dev-build-only|stripped from packages|must not bundle libmpv" \
AGENTS.md CLAUDE.md docs/architecture/embedded-mpv-native.md \
tools/embedded-mpv/README.md vendor/embedded-mpv/README.md
git diff --check
```
Expected: no stale Linux frame-copy shipping claim; any remaining
“must not bundle” text applies specifically to Electron/addon or system
profiles.
- [ ] **Step 3: Commit**
```bash
git add AGENTS.md CLAUDE.md docs/architecture/embedded-mpv-native.md \
tools/embedded-mpv/README.md vendor/embedded-mpv/README.md
git commit -m "docs: document Linux frame-copy packages"
```
## Task 9: Final Verification Matrix
**Files:** No production edits unless verification exposes a defect.
- [ ] **Step 1: Run local project discovery and affected checks**
```bash
pnpm nx show projects
pnpm nx test packaging --skip-nx-cache
pnpm nx test electron-backend --skip-nx-cache --runInBand
pnpm nx lint packaging --skip-nx-cache
pnpm nx lint electron-backend --skip-nx-cache
pnpm nx lint electron-backend-e2e --skip-nx-cache
pnpm nx build electron-backend --configuration=production --skip-nx-cache
```
Expected: PASS or an explicitly recorded platform-only native-build skip on
macOS without a vendored runtime.
- [ ] **Step 2: Verify isolation source and local artifacts**
```bash
rg -n -- '-lmpv|libmpv' apps/electron-backend/native/binding.gyp \
tools/packaging apps/electron-backend/build-embedded-mpv.js
git diff --check origin/master...HEAD
git status --short
```
Expected: only the helper target links libmpv; no unstaged/unexplained files.
- [ ] **Step 3: Record the exact evidence matrix**
Report separately:
- verified locally on macOS: Node/Jest/lint/build/static/package-policy tests;
- structurally verified but not executable locally: Linux runtime builder and
artifact extraction code;
- requires Linux CI: ELF resolution, actual six-format packages, package
managers, Snap/Flatpak confinement, EGL/GBM, frame production, and fallback
with libmpv removed.
- [ ] **Step 4: Stop before publication**
Do not push, open a PR, publish artifacts, or merge. Leave the fully checked
local branch ready for explicit user confirmation in a new task.