diff --git a/docs/superpowers/plans/2026-07-17-linux-embedded-mpv-frame-copy-packaging.md b/docs/superpowers/plans/2026-07-17-linux-embedded-mpv-frame-copy-packaging.md index 78b0a67d2..a090863f3 100644 --- a/docs/superpowers/plans/2026-07-17-linux-embedded-mpv-frame-copy-packaging.md +++ b/docs/superpowers/plans/2026-07-17-linux-embedded-mpv-frame-copy-packaging.md @@ -15,74 +15,74 @@ ### Runtime contracts and staging - Create `tools/embedded-mpv/linux-runtime-manifest.cjs` - - Parse, normalize, and validate Linux frame-copy runtime manifests. + - 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. + - 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. + - 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. + - 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. + - Keep helper RPATH relative and remove build-host RPATH. - Modify `package.json` - - Expose the Linux runtime build command. + - 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. + - 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. + - Verify profile/target/dependency mapping and invalid combinations. - Modify `electron-builder.json` - - Declare DEB/RPM/Pacman libmpv dependencies. + - 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. + - 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. + - 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. + - Cover system, bundled, malformed, and foreign-architecture layouts. - Modify `tools/packaging/electron-after-pack.cjs` - - Pass the required Linux profile into preparation and validation. + - 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. + - Verify the expected profile for every unpacked layout. - Modify `tools/packaging/project.json` - - Add new tests and source inputs. + - 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. + - 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. + - 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. + - 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. + - 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. + - 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. + - 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. + - 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. + - 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. + - 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. + - 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. + - 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. + - Document the final contract and verification matrix. ## Task 1: Define Linux Packaging Profiles @@ -115,10 +115,9 @@ assert.deepEqual(LINUX_SYSTEM_PACKAGE_DEPENDENCIES, { rpm: 'mpv-libs', pacman: 'mpv', }); -assert.deepEqual( - validateLinuxProfileTargets('system', ['deb', 'AppImage']), - ['Linux frame-copy profile "system" cannot build target "appimage".'] -); +assert.deepEqual(validateLinuxProfileTargets('system', ['deb', 'AppImage']), [ + 'Linux frame-copy profile "system" cannot build target "appimage".', +]); ``` - [ ] **Step 2: Run RED** @@ -220,7 +219,13 @@ Linux mpv flags must include: ``` Record downloaded SHA-256 values, git commits/submodules, exact flags, runtime -file names/sizes, and source-distribution obligations. +file names/sizes, and source-distribution obligations. Pin the hwdata v0.409 +archive and record its `pnp.ids` as a build input to libdisplay-info 0.1.1. +Stage private `hwdata.pc` metadata and run libdisplay-info's Meson setup with a +prefix-only pkg-config environment so the upstream +`/usr/share/hwdata/pnp.ids` fallback is unreachable. Include the exact hwdata +archive and its `GPL-2.0-or-later OR XFree86-1.0` notice in the release source +bundle. - [ ] **Step 4: Implement Linux staging** @@ -422,7 +427,7 @@ Expected: FAIL because the runtime probe module does not exist. Emit exactly one line: ```json -{"protocol":1,"usable":true,"libmpv":"2.x","renderApi":"egl"} +{ "protocol": 1, "usable": true, "libmpv": "2.x", "renderApi": "egl" } ``` Exit nonzero with a JSON `reason` when `mpv_create`, `mpv_initialize`, or the @@ -651,4 +656,3 @@ Report separately: 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. - diff --git a/docs/superpowers/specs/2026-07-17-linux-embedded-mpv-frame-copy-packaging-design.md b/docs/superpowers/specs/2026-07-17-linux-embedded-mpv-frame-copy-packaging-design.md index 697c2f594..115176c01 100644 --- a/docs/superpowers/specs/2026-07-17-linux-embedded-mpv-frame-copy-packaging-design.md +++ b/docs/superpowers/specs/2026-07-17-linux-embedded-mpv-frame-copy-packaging-design.md @@ -43,11 +43,11 @@ carry an explicit unavailable marker and must not contain x64 native binaries. Linux packaging is split into explicit profiles: -| Profile | Formats | libmpv strategy | -| --- | --- | --- | -| `system` | DEB, RPM, Pacman | Depend on the distribution package and resolve `libmpv.so.2` from the host | -| `portable` | AppImage, Snap | Bundle the pinned LGPL-compatible runtime closure under `native/lib` | -| `flatpak` | Flatpak | Bundle the same pinned LGPL-compatible runtime closure under `native/lib` | +| Profile | Formats | libmpv strategy | +| ---------- | ---------------- | -------------------------------------------------------------------------- | +| `system` | DEB, RPM, Pacman | Depend on the distribution package and resolve `libmpv.so.2` from the host | +| `portable` | AppImage, Snap | Bundle the pinned LGPL-compatible runtime closure under `native/lib` | +| `flatpak` | Flatpak | Bundle the same pinned LGPL-compatible runtime closure under `native/lib` | The official CI matrix must run these profiles independently. The packaging hook receives the profile through a required environment value and validates @@ -75,6 +75,16 @@ demuxers, protocols, and software scaling/resampling plus libass text subtitles. Hardware decoding remains opportunistic through host Mesa/driver interfaces and must fall back to software decoding. +The source build also pins the `hwdata` v0.409 archive and its SHA-256 because +libdisplay-info 0.1.1 compiles `pnp.ids` into its generated vendor lookup +table. The builder stages that file with private `hwdata.pc` metadata and +restricts libdisplay-info's native pkg-config search to the staged prefix, so +Meson's `/usr/share/hwdata/pnp.ids` fallback cannot make the runtime depend on +unrecorded host data. The runtime manifest records this build-input +relationship. Release source bundles must include the exact hwdata archive and +its dual-license notice (`GPL-2.0-or-later OR XFree86-1.0`) alongside the +MIT-licensed libdisplay-info source. + ## Runtime Layout And Linkage The x64 packaged native directory is: @@ -115,6 +125,7 @@ The manifest records: - libmpv SONAME and either system package requirements or bundled files; - source package versions, URLs/checksums, license identifiers, and exact FFmpeg/mpv build flags for bundled profiles; +- the pinned hwdata `pnp.ids` build input consumed by libdisplay-info; - runtime closure and total byte size; - the native-view backend contract and the fact that only the helper links libmpv. @@ -203,4 +214,3 @@ status. Release artifacts must publish the generated runtime manifest and exact source archives/metadata required by the recorded LGPL source-distribution statement. No publication, push, pull request, or merge is part of this task. - diff --git a/tools/embedded-mpv/build-linux-runtime.cjs b/tools/embedded-mpv/build-linux-runtime.cjs index 05b9c3a23..dc1a4a6f2 100644 --- a/tools/embedded-mpv/build-linux-runtime.cjs +++ b/tools/embedded-mpv/build-linux-runtime.cjs @@ -4,6 +4,12 @@ const crypto = require('node:crypto'); const fs = require('node:fs'); const path = require('node:path'); +const HWDATA_BUILD_INPUT = Object.freeze({ + consumer: 'libdisplay-info', + relativePath: 'pnp.ids', + purpose: 'PNP vendor lookup table compiled into libdisplay-info.', +}); + const SOURCE_PACKAGES = Object.freeze( [ { @@ -94,6 +100,17 @@ const SOURCE_PACKAGES = Object.freeze( expectedGitCommit: 'cee9b076f2c63104ccfd497fa79c39a867293ec4', license: 'LGPL-2.1-or-later', }, + { + id: 'hwdata', + version: '0.409', + sourceKind: 'archive', + sourceUrl: + 'https://github.com/vcrhonek/hwdata/archive/refs/tags/v0.409.tar.gz', + expectedSha256: + '23006accc0f931dd5187d0307a57d0744e2b8feb85e73c37bc0f5229fb31eadd', + license: 'GPL-2.0-or-later OR XFree86-1.0', + buildInput: HWDATA_BUILD_INPUT, + }, { id: 'libdisplay-info', version: '0.1.1', @@ -388,6 +405,11 @@ const BUILD_RECIPES = Object.freeze({ '-Dxxhash=disabled', ]), }), + hwdata: Object.freeze({ + buildSystem: 'data', + sharedOnly: false, + args: Object.freeze([]), + }), 'libdisplay-info': Object.freeze({ buildSystem: 'meson', sharedOnly: true, @@ -875,6 +897,148 @@ function createBuildEnvironment({ }; } +function createPinnedHwdataPkgConfigEnvironment({ buildEnvironment, prefix }) { + if (!buildEnvironment || typeof buildEnvironment !== 'object') { + throw new TypeError( + 'Pinned hwdata requires the Linux runtime build environment.' + ); + } + const prefixPkgConfigDirs = [ + path.join(prefix, 'lib', 'pkgconfig'), + path.join(prefix, 'share', 'pkgconfig'), + ]; + const pinnedPkgConfigPath = prefixPkgConfigDirs.join(path.delimiter); + return { + ...buildEnvironment, + PKG_CONFIG_PATH: pinnedPkgConfigPath, + PKG_CONFIG_LIBDIR: pinnedPkgConfigPath, + }; +} + +function assertPinnedHwdataResolution({ + pcFileDir, + pkgDataDir, + prefix, + version, +}) { + const expectedPcFileDir = path.join(prefix, 'share', 'pkgconfig'); + const expectedPkgDataDir = path.join(prefix, 'share', 'hwdata'); + if (path.resolve(pcFileDir) !== path.resolve(expectedPcFileDir)) { + throw new Error( + `Pinned hwdata pkg-config metadata resolved outside the staged prefix: ${pcFileDir}.` + ); + } + if (path.resolve(pkgDataDir) !== path.resolve(expectedPkgDataDir)) { + throw new Error( + `Pinned hwdata data resolved outside the staged prefix: ${pkgDataDir}.` + ); + } + const hwdataPackage = SOURCE_PACKAGES.find(({ id }) => id === 'hwdata'); + if (version !== hwdataPackage.version) { + throw new Error( + `Pinned hwdata version mismatch: expected ${hwdataPackage.version}, received ${version}.` + ); + } +} + +function preparePinnedHwdataBuildInput({ + buildEnvironment, + fileSystem = fs, + prefix, + runCapture, + sourcePath, +}) { + if (typeof runCapture !== 'function') { + throw new TypeError( + 'Pinned hwdata preparation requires a command capture function.' + ); + } + const hwdataPackage = SOURCE_PACKAGES.find(({ id }) => id === 'hwdata'); + const sourceRoot = fileSystem.realpathSync(sourcePath); + const sourceInputPath = path.join( + sourcePath, + hwdataPackage.buildInput.relativePath + ); + const sourceInputStat = fileSystem.lstatSync(sourceInputPath); + if (!sourceInputStat.isFile() || sourceInputStat.isSymbolicLink()) { + throw new Error( + `Pinned hwdata build input must be a regular file: ${sourceInputPath}.` + ); + } + const realSourceInputPath = fileSystem.realpathSync(sourceInputPath); + assertPathInside( + sourceRoot, + realSourceInputPath, + 'Pinned hwdata build input' + ); + const pnpIds = fileSystem.readFileSync(realSourceInputPath); + if (pnpIds.length === 0) { + throw new Error('Pinned hwdata pnp.ids build input must not be empty.'); + } + + const pkgDataDir = path.join(prefix, 'share', 'hwdata'); + const pcFileDir = path.join(prefix, 'share', 'pkgconfig'); + fileSystem.mkdirSync(pkgDataDir, { recursive: true }); + fileSystem.mkdirSync(pcFileDir, { recursive: true }); + fileSystem.writeFileSync(path.join(pkgDataDir, 'pnp.ids'), pnpIds, { + mode: 0o644, + }); + fileSystem.writeFileSync( + path.join(pcFileDir, 'hwdata.pc'), + [ + `prefix=${prefix}`, + 'datadir=${prefix}/share', + `pkgdatadir=${pkgDataDir}`, + '', + 'Name: hwdata', + 'Description: Pinned PNP hardware identification data', + `Version: ${hwdataPackage.version}`, + '', + ].join('\n'), + { mode: 0o644 } + ); + + const pinnedEnvironment = createPinnedHwdataPkgConfigEnvironment({ + buildEnvironment, + prefix, + }); + const captureOptions = { env: pinnedEnvironment }; + const resolvedPcFileDir = runCapture( + 'pkg-config', + ['--variable=pcfiledir', 'hwdata'], + captureOptions + ); + const resolvedPkgDataDir = runCapture( + 'pkg-config', + ['--variable=pkgdatadir', 'hwdata'], + captureOptions + ); + const resolvedVersion = runCapture( + 'pkg-config', + ['--modversion', 'hwdata'], + captureOptions + ); + assertPinnedHwdataResolution({ + pcFileDir: resolvedPcFileDir, + pkgDataDir: resolvedPkgDataDir, + prefix, + version: resolvedVersion, + }); + return pinnedEnvironment; +} + +function resolveLinuxPackageBuildEnvironment(packageId, context) { + if (packageId !== 'libdisplay-info') { + return context.buildEnvironment; + } + if (!context.hwdataBuildEnvironment) { + throw new Error( + 'libdisplay-info requires the staged pinned hwdata build environment.' + ); + } + return context.hwdataBuildEnvironment; +} + function sha256Buffer(contents) { return crypto.createHash('sha256').update(contents).digest('hex'); } @@ -1294,6 +1458,9 @@ function sourceManifestMetadata(sourceRecord) { ...(sourceRecord.sourceSubmodules ? { sourceSubmodules: [...sourceRecord.sourceSubmodules] } : {}), + ...(sourceRecord.buildInput + ? { buildInput: { ...sourceRecord.buildInput } } + : {}), license: sourceRecord.license, }; @@ -1392,7 +1559,7 @@ function createLinuxRuntimeManifest({ ), buildHost, sourceDistribution: - 'Attach a source archive to the corresponding Linux binary release containing the exact downloaded source archives, including the pinned MIT-licensed libdisplay-info source archive, a checkout or git bundle of the recorded libplacebo commit and submodules, tools/embedded-mpv/build-linux-runtime.mjs, tools/embedded-mpv/build-linux-runtime.cjs, this runtime manifest, and any local patches.', + 'Attach a source archive to the corresponding Linux binary release containing the exact downloaded source archives, including the pinned dual-licensed hwdata archive whose pnp.ids is compiled into the MIT-licensed libdisplay-info source archive, a checkout or git bundle of the recorded libplacebo commit and submodules, tools/embedded-mpv/build-linux-runtime.mjs, tools/embedded-mpv/build-linux-runtime.cjs, this runtime manifest, and any local patches.', }; } @@ -1429,7 +1596,9 @@ module.exports = { parseReadelfDynamic, parseReadelfVersionInfo, parseVersion, + preparePinnedHwdataBuildInput, resolveSystemPkgConfigDirs, + resolveLinuxPackageBuildEnvironment, runtimeLibraryNames, sha256Buffer, publishOwnedOutput, diff --git a/tools/embedded-mpv/build-linux-runtime.mjs b/tools/embedded-mpv/build-linux-runtime.mjs index 4dd678629..a761bbbd0 100644 --- a/tools/embedded-mpv/build-linux-runtime.mjs +++ b/tools/embedded-mpv/build-linux-runtime.mjs @@ -32,7 +32,9 @@ const { parseCliInvocation, parseReadelfDynamic, parseReadelfVersionInfo, + preparePinnedHwdataBuildInput, retainRuntimeLibraries, + resolveLinuxPackageBuildEnvironment, resolveSystemPkgConfigDirs, runtimeLibraryNames, selectReachableRuntimeLibraryNames, @@ -342,18 +344,35 @@ function mesonSetupArgs(prefix, recipeArgs) { function mesonInstall(packageId, recipe, context) { const sourcePath = sourcePathFor(packageId, context.sourceRoot); const buildPath = path.join(sourcePath, 'build-iptvnator'); + const buildEnvironment = resolveLinuxPackageBuildEnvironment( + packageId, + context + ); fs.rmSync(buildPath, { recursive: true, force: true }); context.run( 'meson', ['setup', buildPath, ...mesonSetupArgs(context.prefix, recipe.args)], - { cwd: sourcePath } + { cwd: sourcePath, env: buildEnvironment } ); context.run( 'meson', ['compile', '--jobs', context.parallelism, '-C', buildPath], - { cwd: sourcePath } + { cwd: sourcePath, env: buildEnvironment } ); - context.run('meson', ['install', '-C', buildPath], { cwd: sourcePath }); + context.run('meson', ['install', '-C', buildPath], { + cwd: sourcePath, + env: buildEnvironment, + }); +} + +function prepareHwdata(context) { + context.hwdataBuildEnvironment = preparePinnedHwdataBuildInput({ + buildEnvironment: context.buildEnvironment, + prefix: context.prefix, + runCapture: (command, args, options) => + context.runCapture(command, args, options), + sourcePath: sourcePathFor('hwdata', context.sourceRoot), + }); } function pathInsideDestdir(destdir, absolutePath) { @@ -506,6 +525,9 @@ function buildRuntime(context) { continue; } switch (recipe.buildSystem) { + case 'data': + prepareHwdata(context); + break; case 'configure': configureInstall(packageId, recipe, context); break; diff --git a/tools/embedded-mpv/build-linux-runtime.test.mjs b/tools/embedded-mpv/build-linux-runtime.test.mjs index b97b73515..0ea7dc064 100644 --- a/tools/embedded-mpv/build-linux-runtime.test.mjs +++ b/tools/embedded-mpv/build-linux-runtime.test.mjs @@ -42,8 +42,10 @@ const { parseCliInvocation, parseReadelfDynamic, parseReadelfVersionInfo, + preparePinnedHwdataBuildInput, publishOwnedOutput, retainRuntimeLibraries, + resolveLinuxPackageBuildEnvironment, resolveSystemPkgConfigDirs, selectReachableRuntimeLibraryNames, validateRuntimeDependencyClosure, @@ -91,6 +93,7 @@ test('pins the complete source stack and preserves dependency build order', () = { id: 'openssl', version: '3.5.7' }, { id: 'ffmpeg', version: '8.1' }, { id: 'libplacebo', version: '7.360.1' }, + { id: 'hwdata', version: '0.409' }, { id: 'libdisplay-info', version: '0.1.1' }, { id: 'mpv', version: '0.41.0' }, ] @@ -129,6 +132,21 @@ test('pins the complete source stack and preserves dependency build order', () = byId.get('openssl').sourceUrl, 'https://github.com/openssl/openssl/releases/download/openssl-3.5.7/openssl-3.5.7.tar.gz' ); + assert.deepEqual(byId.get('hwdata'), { + id: 'hwdata', + version: '0.409', + sourceKind: 'archive', + sourceUrl: + 'https://github.com/vcrhonek/hwdata/archive/refs/tags/v0.409.tar.gz', + expectedSha256: + '23006accc0f931dd5187d0307a57d0744e2b8feb85e73c37bc0f5229fb31eadd', + license: 'GPL-2.0-or-later OR XFree86-1.0', + buildInput: { + consumer: 'libdisplay-info', + relativePath: 'pnp.ids', + purpose: 'PNP vendor lookup table compiled into libdisplay-info.', + }, + }); assert.deepEqual(byId.get('libdisplay-info'), { id: 'libdisplay-info', version: '0.1.1', @@ -139,6 +157,9 @@ test('pins the complete source stack and preserves dependency build order', () = '0d8731588e9f82a9cac96324a3d7c82e2ba5b1b5e006143fefe692c74069fb60', license: 'MIT', }); + assert.ok( + BUILD_ORDER.indexOf('hwdata') < BUILD_ORDER.indexOf('libdisplay-info') + ); assert.ok( BUILD_ORDER.indexOf('libdisplay-info') < BUILD_ORDER.indexOf('mpv') ); @@ -156,6 +177,7 @@ test('hardcodes the verified official archive digests and libplacebo commit', () '1b1cde5b235d40479e91be2f0e88a309e3214c8ab470ec8a2744d82a5a9ea05c', harfbuzz: '77e4f7f98f3d86bf8788b53e6832fb96279956e1c3961988ea3d4b7ca41ddc27', + hwdata: '23006accc0f931dd5187d0307a57d0744e2b8feb85e73c37bc0f5229fb31eadd', libass: 'eae425da50f0015c21f7b3a9c7262a910f0218af469e22e2931462fed3c50959', 'libdisplay-info': '0d8731588e9f82a9cac96324a3d7c82e2ba5b1b5e006143fefe692c74069fb60', @@ -303,12 +325,125 @@ test('defines shared-only source recipes with font discovery before playback', ( BUILD_ORDER.indexOf('fontconfig') < BUILD_ORDER.indexOf('libass') ); assert.ok(BUILD_ORDER.indexOf('openssl') < BUILD_ORDER.indexOf('ffmpeg')); + assert.equal(BUILD_RECIPES.hwdata.buildSystem, 'data'); + assert.equal(BUILD_RECIPES.hwdata.sharedOnly, false); assert.equal(BUILD_RECIPES['libdisplay-info'].buildSystem, 'meson'); - for (const packageId of BUILD_ORDER) { + for (const packageId of BUILD_ORDER.filter( + (packageId) => packageId !== 'hwdata' + )) { assert.equal(BUILD_RECIPES[packageId].sharedOnly, true, packageId); } }); +test('stages pinned hwdata and excludes host pkg-config fallback', (t) => { + const root = fs.mkdtempSync( + path.join(os.tmpdir(), 'iptvnator-pinned-hwdata-') + ); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + const sourcePath = path.join(root, 'sources', 'hwdata'); + const prefix = path.join(root, 'runtime'); + fs.mkdirSync(sourcePath, { recursive: true }); + const pnpIds = 'ABC Example Display Vendor\n'; + fs.writeFileSync(path.join(sourcePath, 'pnp.ids'), pnpIds); + + const invocations = []; + const buildEnvironment = { + PATH: '/usr/bin', + PKG_CONFIG_PATH: '/host/pkgconfig', + PKG_CONFIG_LIBDIR: + '/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig', + }; + const pinnedEnvironment = preparePinnedHwdataBuildInput({ + buildEnvironment, + prefix, + runCapture(command, args, options) { + invocations.push({ args, command, env: options.env }); + assert.equal(options.env.PKG_CONFIG_PATH.includes('/host'), false); + assert.equal(options.env.PKG_CONFIG_LIBDIR.includes('/usr'), false); + if (args[0] === '--variable=pcfiledir') { + return path.join(prefix, 'share', 'pkgconfig'); + } + if (args[0] === '--variable=pkgdatadir') { + return path.join(prefix, 'share', 'hwdata'); + } + if (args[0] === '--modversion') { + return '0.409'; + } + throw new Error(`Unexpected pkg-config query: ${args.join(' ')}`); + }, + sourcePath, + }); + + assert.equal( + fs.readFileSync( + path.join(prefix, 'share', 'hwdata', 'pnp.ids'), + 'utf8' + ), + pnpIds + ); + const pkgConfig = fs.readFileSync( + path.join(prefix, 'share', 'pkgconfig', 'hwdata.pc'), + 'utf8' + ); + assert.match(pkgConfig, /^Version: 0\.409$/m); + assert.match( + pkgConfig, + new RegExp( + `^pkgdatadir=${path + .join(prefix, 'share', 'hwdata') + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, + 'm' + ) + ); + assert.doesNotMatch(pkgConfig, /\/usr\/share\/hwdata/); + assert.equal(pinnedEnvironment.PKG_CONFIG_PATH.includes('/host'), false); + assert.equal(pinnedEnvironment.PKG_CONFIG_LIBDIR.includes('/usr'), false); + assert.equal( + resolveLinuxPackageBuildEnvironment('libdisplay-info', { + buildEnvironment, + hwdataBuildEnvironment: pinnedEnvironment, + }), + pinnedEnvironment + ); + assert.equal( + resolveLinuxPackageBuildEnvironment('mpv', { + buildEnvironment, + hwdataBuildEnvironment: pinnedEnvironment, + }), + buildEnvironment + ); + assert.throws( + () => + resolveLinuxPackageBuildEnvironment('libdisplay-info', { + buildEnvironment, + }), + /libdisplay-info.*pinned hwdata/i + ); + assert.deepEqual( + invocations.map(({ args, command }) => [command, ...args]), + [ + ['pkg-config', '--variable=pcfiledir', 'hwdata'], + ['pkg-config', '--variable=pkgdatadir', 'hwdata'], + ['pkg-config', '--modversion', 'hwdata'], + ] + ); + + assert.throws( + () => + preparePinnedHwdataBuildInput({ + buildEnvironment, + prefix: path.join(root, 'rejected-runtime'), + runCapture(_command, args) { + return args[0] === '--modversion' + ? '0.409' + : '/usr/share/hwdata'; + }, + sourcePath, + }), + /pinned hwdata.*host|host.*hwdata|resolved outside/i + ); +}); + test('rejects duplicate option assignments in every Meson recipe', () => { assert.doesNotThrow(() => assertUniqueMesonOptionAssignments(BUILD_RECIPES) @@ -1309,6 +1444,13 @@ test('generates a hash-complete manifest accepted by the Linux validator', (t) = assert.doesNotMatch(manifest.sourceDistribution, /TBD|TODO/i); assert.match(manifest.sourceDistribution, /source archives/i); assert.match(manifest.sourceDistribution, /libdisplay-info/i); + assert.match(manifest.sourceDistribution, /hwdata/i); + assert.match(manifest.sourceDistribution, /pnp\.ids/i); + assert.deepEqual(manifest.packages.hwdata.buildInput, { + consumer: 'libdisplay-info', + relativePath: 'pnp.ids', + purpose: 'PNP vendor lookup table compiled into libdisplay-info.', + }); }); test('guards the CLI to Linux x64 and requires exactly one output prefix', () => { diff --git a/tools/embedded-mpv/linux-runtime-manifest.cjs b/tools/embedded-mpv/linux-runtime-manifest.cjs index db967e823..fed9a20ab 100644 --- a/tools/embedded-mpv/linux-runtime-manifest.cjs +++ b/tools/embedded-mpv/linux-runtime-manifest.cjs @@ -192,6 +192,24 @@ function validatePackages(errors, packages) { ); } + if (pinnedPackage.buildInput) { + if ( + !isObject(packageMetadata.buildInput) || + !isDeepStrictEqual( + packageMetadata.buildInput, + pinnedPackage.buildInput + ) + ) { + errors.push( + `Linux runtime manifest ${label}.buildInput must equal the pinned build input.` + ); + } + } else if (packageMetadata.buildInput !== undefined) { + errors.push( + `Linux runtime manifest ${label} must not include buildInput.` + ); + } + if (pinnedPackage.sourceKind === 'archive') { if ( typeof packageMetadata.sourceSha256 === 'string' && @@ -880,10 +898,18 @@ function validateLinuxRuntimeManifest(manifest) { errors.push( 'Linux runtime manifest sourceDistribution must be a non-empty string.' ); - } else if (!/\blibdisplay-info\b/i.test(manifest.sourceDistribution)) { - errors.push( - 'Linux runtime manifest sourceDistribution must explicitly include libdisplay-info.' - ); + } else { + for (const [sourceName, sourcePattern] of [ + ['hwdata', /\bhwdata\b/i], + ['pnp.ids', /\bpnp\.ids\b/i], + ['libdisplay-info', /\blibdisplay-info\b/i], + ]) { + if (!sourcePattern.test(manifest.sourceDistribution)) { + errors.push( + `Linux runtime manifest sourceDistribution must explicitly include ${sourceName}.` + ); + } + } } validateRuntimeFiles(errors, manifest.runtimeFiles); diff --git a/tools/embedded-mpv/linux-runtime-manifest.test.mjs b/tools/embedded-mpv/linux-runtime-manifest.test.mjs index 4071bbd47..517dd10e1 100644 --- a/tools/embedded-mpv/linux-runtime-manifest.test.mjs +++ b/tools/embedded-mpv/linux-runtime-manifest.test.mjs @@ -54,6 +54,9 @@ function sourcePackageRecord(sourcePackage) { ...(sourcePackage.sourceTag ? { sourceTag: sourcePackage.sourceTag } : {}), + ...(sourcePackage.buildInput + ? { buildInput: structuredClone(sourcePackage.buildInput) } + : {}), ...(sourcePackage.sourceKind === 'archive' ? { sourceSha256: sourcePackage.expectedSha256 } : { @@ -131,7 +134,7 @@ function createValidManifest( mesonFlags: ['-Dlibmpv=true', '-Dgpl=false'], }, sourceDistribution: - 'Publish the exact source archives, including the pinned MIT-licensed libdisplay-info source archive.', + 'Publish the exact source archives, including pinned hwdata pnp.ids and the MIT-licensed libdisplay-info source archive.', runtimeFiles, runtimeTotalBytes: runtimeFiles.reduce( (total, runtimeEntry) => total + (runtimeEntry?.size ?? 0), @@ -322,6 +325,14 @@ test('requires pinned archive hashes and exact libplacebo git provenance', () => /packages\.libplacebo\.sourceGitCommit must equal the pinned commit/ ); + const hostHwdataInput = createValidManifest(); + hostHwdataInput.packages.hwdata.buildInput.relativePath = + '/usr/share/hwdata/pnp.ids'; + assert.match( + validateLinuxRuntimeManifest(hostHwdataInput).join('\n'), + /packages\.hwdata\.buildInput must equal the pinned build input/ + ); + const validSubmoduleRecord = `${'a'.repeat(40)} 3rdparty/example`; for (const sourceSubmodules of [ [], @@ -583,12 +594,15 @@ test('requires exact portable ABI and external configuration records', () => { ); }); -test('requires the source-distribution statement to name libdisplay-info', () => { +test('requires the source-distribution statement to name pinned display data', () => { const manifest = createValidManifest(); manifest.sourceDistribution = 'Publish all of the other source archives with the binary release.'; + const errors = validateLinuxRuntimeManifest(manifest).join('\n'); + assert.match(errors, /sourceDistribution must explicitly include hwdata/); + assert.match(errors, /sourceDistribution must explicitly include pnp\.ids/); assert.match( - validateLinuxRuntimeManifest(manifest).join('\n'), + errors, /sourceDistribution must explicitly include libdisplay-info/ ); });