diff --git a/apps/electron-backend/build-embedded-mpv.js b/apps/electron-backend/build-embedded-mpv.js index 444bf6452..69e4768bf 100644 --- a/apps/electron-backend/build-embedded-mpv.js +++ b/apps/electron-backend/build-embedded-mpv.js @@ -20,6 +20,7 @@ const { const LINUX_PACKAGE_RUNTIME_MODES = Object.freeze(['system', 'bundled']); const LINUX_STAGED_RUNTIME_ORIGIN = 'vendored-lgpl'; const LINUX_SOURCE_RUNTIME_ORIGIN = 'vendored-lgpl-source-build'; +const VERSIONED_LINUX_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/; const workspaceRoot = process.cwd(); const addonRoot = path.join( @@ -233,7 +234,7 @@ function validatedLinuxSourceRuntime(runtimeRoot) { return { buildInputMode, sourceRuntimeManifest, - sourceRuntimeValidated: true, + sourceRuntimeValidated: buildInputMode === 'bundled-runtime', }; } @@ -381,12 +382,6 @@ function resolveRuntime() { const systemIncludeDir = process.env.LIBMPV_INCLUDE_DIR || '/usr/include'; if (fs.existsSync(path.join(systemIncludeDir, 'mpv', 'client.h'))) { - if (embeddedMpvRequired) { - throw new Error( - 'Required Linux builds must use an explicit staged runtime manifest.' - ); - } - const sourceRuntimeManifest = { linuxBackend: 'process-isolated mpv --wid', warning: 'Development-only unmanaged system libmpv toolchain.', @@ -431,6 +426,61 @@ function resolveRuntime() { return null; } +function hasStagedLinuxLibMpvLinkerInput(runtime) { + return ( + Array.isArray(runtime.sourceRuntimeManifest?.runtimeFiles) && + runtime.sourceRuntimeManifest.runtimeFiles.some( + (runtimeFile) => runtimeFile.name === 'libmpv.so' + ) + ); +} + +function assertRequiredLinuxFrameCopyRuntime(runtime) { + if (targetPlatform !== 'linux' || !embeddedMpvRequired) { + return; + } + + if ( + runtime.buildInputMode !== 'bundled-runtime' || + runtime.sourceRuntimeValidated !== true || + !hasStagedLinuxLibMpvLinkerInput(runtime) + ) { + cleanOutput(); + throw new Error( + 'Required Linux builds must use the validated bundled source runtime containing staged libmpv.' + ); + } +} + +function deriveLinuxLibMpvSoname(runtime) { + if ( + runtime.buildInputMode !== 'bundled-runtime' || + runtime.sourceRuntimeValidated !== true || + !Array.isArray(runtime.sourceRuntimeManifest?.runtimeFiles) + ) { + return null; + } + + const candidates = runtime.sourceRuntimeManifest.runtimeFiles + .map((runtimeFile) => runtimeFile.name) + .filter((fileName) => VERSIONED_LINUX_LIBMPV_PATTERN.test(fileName)) + .sort((left, right) => { + const leftVersionParts = left + .slice('libmpv.so.'.length) + .split('.').length; + const rightVersionParts = right + .slice('libmpv.so.'.length) + .split('.').length; + return ( + leftVersionParts - rightVersionParts || + left.length - right.length || + left.localeCompare(right) + ); + }); + + return candidates[0] ?? null; +} + function copyLinuxRuntimeClosureToNativeBuild(runtime) { fs.rmSync(outputLibDir, { recursive: true, force: true }); @@ -490,6 +540,12 @@ function copyLinuxRuntimeClosureToNativeBuild(runtime) { function writeLinuxFrameCopyBuildManifest(runtime) { const copiedRuntimeFiles = copyLinuxRuntimeClosureToNativeBuild(runtime); + const libmpvSoname = deriveLinuxLibMpvSoname(runtime); + const packageRuntimeAvailable = + runtime.sourceRuntimeValidated === true && + runtime.buildInputMode === 'bundled-runtime' && + copiedRuntimeFiles.length > 0 && + libmpvSoname !== null; const manifest = { schemaVersion: 1, origin: 'linux-frame-copy-build', @@ -500,8 +556,8 @@ function writeLinuxFrameCopyBuildManifest(runtime) { sourceRuntimeValidated: runtime.sourceRuntimeValidated, allowedPackageRuntimeModes: [...LINUX_PACKAGE_RUNTIME_MODES], packageRuntimeAvailability: { - system: true, - bundled: copiedRuntimeFiles.length > 0, + system: packageRuntimeAvailable, + bundled: packageRuntimeAvailable, }, artifacts: { addon: 'embedded_mpv.node', @@ -514,7 +570,7 @@ function writeLinuxFrameCopyBuildManifest(runtime) { helperRunpath: ['$ORIGIN/lib'], }, nativeViewFallback: 'process-isolated mpv --wid', - libmpvSoname: 'libmpv.so.2', + libmpvSoname, runtimeFiles: copiedRuntimeFiles, runtimeTotalBytes: copiedRuntimeFiles.reduce( (total, runtimeFile) => total + runtimeFile.size, @@ -685,6 +741,8 @@ function main() { return; } + assertRequiredLinuxFrameCopyRuntime(runtime); + const runtimeManifest = targetPlatform === 'darwin' ? copyRuntimeToNativeBuild({ diff --git a/apps/electron-backend/native/binding.gyp b/apps/electron-backend/native/binding.gyp index 33edd65f0..b6f83bc7e 100644 --- a/apps/electron-backend/native/binding.gyp +++ b/apps/electron-backend/native/binding.gyp @@ -163,7 +163,7 @@ ], "libraries": [ "-L { ); }); + it('requires a validated bundled source runtime for required Linux builds', () => { + expect(buildScriptSource).toContain( + "sourceRuntimeValidated: buildInputMode === 'bundled-runtime'" + ); + expect(buildScriptSource).toContain( + 'assertRequiredLinuxFrameCopyRuntime(runtime);' + ); + expect(buildScriptSource).toContain( + "runtime.buildInputMode !== 'bundled-runtime' ||" + ); + expect(buildScriptSource).toContain( + 'runtime.sourceRuntimeValidated !== true' + ); + expect(buildScriptSource).toContain("runtimeFile.name === 'libmpv.so'"); + expect(buildScriptSource).toContain( + 'Required Linux builds must use the validated bundled source runtime containing staged libmpv.' + ); + }); + + it('derives packaged Linux libmpv identity from the validated runtime', () => { + expect(buildScriptSource).toContain( + 'function deriveLinuxLibMpvSoname(runtime)' + ); + expect(buildScriptSource).toContain( + 'runtime.sourceRuntimeManifest.runtimeFiles' + ); + expect(buildScriptSource).toContain('VERSIONED_LINUX_LIBMPV_PATTERN'); + expect(buildScriptSource).toContain('libmpvSoname,'); + expect(buildScriptSource).not.toContain("libmpvSoname: 'libmpv.so.2'"); + }); + + it('marks both package modes available only for a validated bundled build', () => { + expect(buildScriptSource).toContain( + 'runtime.sourceRuntimeValidated === true &&\n' + + " runtime.buildInputMode === 'bundled-runtime' &&\n" + + ' copiedRuntimeFiles.length > 0 &&\n' + + ' libmpvSoname !== null' + ); + expect(buildScriptSource).toContain('system: packageRuntimeAvailable'); + expect(buildScriptSource).toContain('bundled: packageRuntimeAvailable'); + }); + it('uses platform-specific embedded MPV runtime cache key inputs in CI', () => { expect(buildAndMakeWorkflowSource).toContain( "const targetPlatform = '${{ matrix.embedded_mpv_platform }}';" @@ -672,7 +714,7 @@ describe('Embedded MPV native source recording invariants', () => { "if (!embeddedMpvRequired && runtime.origin === 'system-dev')" ); expect(buildScriptSource).toContain( - 'Required Linux builds must use an explicit staged runtime manifest.' + 'Required Linux builds must use the validated bundled source runtime containing staged libmpv.' ); expect(buildScriptSource).toContain( 'removeStaleFrameCopyArtifacts(outputDir);' @@ -858,14 +900,14 @@ describe('Embedded MPV native build configuration', () => { expect(linuxAddonConfig?.libraries).not.toContain('-lmpv'); expect(JSON.stringify(linuxAddonConfig)).not.toContain('-lmpv'); expect(linuxHelperConfig?.libraries).toEqual( - expect.arrayContaining([ - '-lmpv', - '-lEGL', - '-lOpenGL', - '-lgbm', - '-ldl', - ]) + expect.arrayContaining(['-lEGL', '-lOpenGL', '-lgbm', '-ldl']) ); + expect(linuxHelperConfig?.libraries).not.toContain('-lmpv'); + expect( + linuxHelperConfig?.libraries.find((library: string) => + library.includes("path.join(dir, 'libmpv.so')") + ) + ).toContain('LINUX_NATIVE_LIBRARY_DIR'); const runtimeLinkerFlags = linuxHelperConfig?.ldflags.filter( (flag: string) => flag.startsWith('-Wl,')