diff --git a/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-filesystem.tests.ts b/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-filesystem.tests.ts new file mode 100644 index 000000000..191f7688d --- /dev/null +++ b/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-filesystem.tests.ts @@ -0,0 +1,288 @@ +import assert = require('node:assert/strict'); +import { + chmodSync, + existsSync, + lstatSync, + mkdtempSync, + mkdirSync, + readFileSync, + readlinkSync, + rmSync, + statSync, + symlinkSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, it } from 'node:test'; +import { + createDisposablePackagedLinuxApp, + createPackagedEntryGuard, + readPackagedRuntimeIdentity, +} from './embedded-mpv-frame-copy-packaged-filesystem'; + +const temporaryDirectories = new Set(); + +type PackageFixture = { + executablePath: string; + libmpvAliasPath: string; + libmpvSonamePath: string; + nativeDir: string; + packageRoot: string; + payloadPath: string; + runtimeManifestPath: string; +}; + +function createPackageFixture(): PackageFixture { + const packageRoot = mkdtempSync( + join(tmpdir(), 'iptvnator-packaged-fixture-source-') + ); + temporaryDirectories.add(packageRoot); + const executablePath = join(packageRoot, 'IPTVnator'); + const nativeDir = join( + packageRoot, + 'resources', + 'app.asar.unpacked', + 'electron-backend', + 'native' + ); + const payloadPath = join(packageRoot, 'resources', 'payload.bin'); + const runtimeManifestPath = join(nativeDir, 'embedded-mpv-runtime.json'); + const runtimeLibraryDir = join(nativeDir, 'lib'); + const libmpvSonamePath = join(runtimeLibraryDir, 'libmpv.so.2'); + const libmpvAliasPath = join(runtimeLibraryDir, 'libmpv.so'); + + mkdirSync(runtimeLibraryDir, { recursive: true }); + writeFileSync(executablePath, '#!/bin/sh\nexit 0\n'); + chmodSync(executablePath, 0o755); + writeFileSync(payloadPath, 'packaged payload'); + chmodSync(payloadPath, 0o640); + writeFileSync(libmpvSonamePath, 'packaged libmpv'); + symlinkSync('libmpv.so.2', libmpvAliasPath); + writeFileSync( + runtimeManifestPath, + JSON.stringify({ + arch: 'x64', + libmpvSoname: 'libmpv.so.2', + platform: 'linux', + profile: 'portable', + runtimeMode: 'bundled', + }) + ); + + if (process.platform !== 'win32') { + symlinkSync( + 'payload.bin', + join(packageRoot, 'resources', 'payload-link') + ); + } + + return { + executablePath, + libmpvAliasPath, + libmpvSonamePath, + nativeDir, + packageRoot, + payloadPath, + runtimeManifestPath, + }; +} + +function entryExists(entryPath: string): boolean { + try { + lstatSync(entryPath); + return true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return false; + } + throw error; + } +} + +afterEach(() => { + for (const directory of temporaryDirectories) { + rmSync(directory, { force: true, recursive: true }); + } + temporaryDirectories.clear(); +}); + +describe('disposable unpacked package clone', () => { + it('requires a safe versioned libmpv target in the packaged manifest', () => { + const source = createPackageFixture(); + + assert.equal( + readPackagedRuntimeIdentity(source.nativeDir).libmpvSoname, + 'libmpv.so.2' + ); + + writeFileSync( + source.runtimeManifestPath, + JSON.stringify({ + arch: 'x64', + libmpvSoname: '../libmpv.so.2', + platform: 'linux', + profile: 'portable', + runtimeMode: 'bundled', + }) + ); + assert.throws( + () => readPackagedRuntimeIdentity(source.nativeDir), + /libmpvSoname/ + ); + }); + + it('hides and restores a cloned regular dependency without changing the source package', () => { + const source = createPackageFixture(); + const clone = createDisposablePackagedLinuxApp(source.executablePath); + temporaryDirectories.add(clone.temporaryRoot); + + const clonedLibmpvPath = join(clone.nativeDir, 'lib', 'libmpv.so.2'); + const guard = createPackagedEntryGuard(clonedLibmpvPath, { + expectedKind: 'regular-file', + hiddenDirectory: clone.temporaryRoot, + }); + + try { + assert.equal(lstatSync(clonedLibmpvPath).isFile(), true); + assert.equal( + statSync(clonedLibmpvPath).ino, + statSync(source.libmpvSonamePath).ino + ); + + guard.hide(); + + assert.equal(entryExists(clonedLibmpvPath), false); + assert.equal(lstatSync(source.libmpvSonamePath).isFile(), true); + assert.equal( + readFileSync(source.libmpvSonamePath, 'utf8'), + 'packaged libmpv' + ); + + guard.restore(); + + assert.equal(lstatSync(clonedLibmpvPath).isFile(), true); + assert.equal( + statSync(clonedLibmpvPath).ino, + statSync(source.libmpvSonamePath).ino + ); + } finally { + guard.restore(); + clone.cleanup(); + temporaryDirectories.delete(clone.temporaryRoot); + } + + assert.equal(entryExists(source.libmpvSonamePath), true); + }); + + it('hides and restores a cloned symbolic link without dereferencing it', () => { + const source = createPackageFixture(); + const clone = createDisposablePackagedLinuxApp(source.executablePath); + temporaryDirectories.add(clone.temporaryRoot); + + const clonedAliasPath = join(clone.nativeDir, 'lib', 'libmpv.so'); + const guard = createPackagedEntryGuard(clonedAliasPath, { + expectedKind: 'symbolic-link', + hiddenDirectory: clone.temporaryRoot, + }); + + try { + guard.hide(); + assert.equal(entryExists(clonedAliasPath), false); + assert.equal( + lstatSync(source.libmpvAliasPath).isSymbolicLink(), + true + ); + assert.equal(readlinkSync(source.libmpvAliasPath), 'libmpv.so.2'); + + guard.restore(); + assert.equal(lstatSync(clonedAliasPath).isSymbolicLink(), true); + assert.equal(readlinkSync(clonedAliasPath), 'libmpv.so.2'); + } finally { + guard.restore(); + clone.cleanup(); + temporaryDirectories.delete(clone.temporaryRoot); + } + }); + + it('hardlinks regular files and preserves modes, symlinks, and the source manifest', () => { + const source = createPackageFixture(); + const clone = createDisposablePackagedLinuxApp(source.executablePath); + temporaryDirectories.add(clone.temporaryRoot); + + try { + assert.notEqual(clone.packageRoot, source.packageRoot); + assert.equal( + statSync(clone.executablePath).mode & 0o777, + statSync(source.executablePath).mode & 0o777 + ); + + const clonedPayloadPath = join( + clone.packageRoot, + 'resources', + 'payload.bin' + ); + assert.equal( + statSync(clonedPayloadPath).ino, + statSync(source.payloadPath).ino + ); + assert.equal(statSync(clonedPayloadPath).mode & 0o777, 0o640); + + if (process.platform !== 'win32') { + const clonedLinkPath = join( + clone.packageRoot, + 'resources', + 'payload-link' + ); + assert.equal(lstatSync(clonedLinkPath).isSymbolicLink(), true); + assert.equal(readlinkSync(clonedLinkPath), 'payload.bin'); + } + + const clonedRuntimeManifestPath = join( + clone.nativeDir, + 'embedded-mpv-runtime.json' + ); + assert.equal(existsSync(clonedRuntimeManifestPath), true); + assert.equal(existsSync(source.runtimeManifestPath), true); + } finally { + clone.cleanup(); + temporaryDirectories.delete(clone.temporaryRoot); + } + + assert.equal(existsSync(clone.temporaryRoot), false); + assert.equal(existsSync(source.runtimeManifestPath), true); + }); + + it('copies a regular file when hardlinking is unavailable', () => { + const source = createPackageFixture(); + const clone = createDisposablePackagedLinuxApp(source.executablePath, { + linkFile() { + throw Object.assign(new Error('cross-device hardlink'), { + code: 'EXDEV', + }); + }, + }); + temporaryDirectories.add(clone.temporaryRoot); + + try { + assert.equal(statSync(clone.executablePath).mode & 0o777, 0o755); + const clonedPayloadPath = join( + clone.packageRoot, + 'resources', + 'payload.bin' + ); + assert.equal( + readFileSync(clonedPayloadPath, 'utf8'), + readFileSync(source.payloadPath, 'utf8') + ); + assert.notEqual( + statSync(clonedPayloadPath).ino, + statSync(source.payloadPath).ino + ); + assert.equal(statSync(clonedPayloadPath).mode & 0o777, 0o640); + } finally { + clone.cleanup(); + temporaryDirectories.delete(clone.temporaryRoot); + } + }); +}); diff --git a/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-filesystem.ts b/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-filesystem.ts index ce1e954df..69d0c0a19 100644 --- a/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-filesystem.ts +++ b/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-filesystem.ts @@ -1,7 +1,6 @@ import { chmodSync, copyFileSync, - existsSync, linkSync, lstatSync, mkdirSync, @@ -11,8 +10,8 @@ import { readlinkSync, renameSync, rmSync, - statSync, symlinkSync, + type Stats, } from 'fs'; import { tmpdir } from 'os'; import { basename, dirname, join, resolve } from 'path'; @@ -32,16 +31,24 @@ export type DisposablePackagedLinuxAppOptions = { export type PackagedRuntimeIdentity = { arch: string; + libmpvSoname: string; platform: string; profile: string; runtimeMode: string; }; -export type RuntimeManifestGuard = { +export type PackagedEntryKind = 'regular-file' | 'symbolic-link'; + +export type PackagedEntryGuard = { hide: () => void; restore: () => void; }; +export type PackagedEntryGuardOptions = { + expectedKind: PackagedEntryKind; + hiddenDirectory: string; +}; + const HARDLINK_COPY_FALLBACK_CODES = new Set([ 'EACCES', 'EMLINK', @@ -50,6 +57,28 @@ const HARDLINK_COPY_FALLBACK_CODES = new Set([ 'EPERM', 'EXDEV', ]); +const VERSIONED_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/; + +function entryKindMatches( + stats: Stats, + expectedKind: PackagedEntryKind +): boolean { + return expectedKind === 'regular-file' + ? stats.isFile() && !stats.isSymbolicLink() + : stats.isSymbolicLink(); +} + +function entryExistsByLstat(entryPath: string): boolean { + try { + lstatSync(entryPath); + return true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return false; + } + throw error; + } +} function clonePackagedEntry( sourcePath: string, @@ -136,43 +165,58 @@ export function createDisposablePackagedLinuxApp( }; } -export function createRuntimeManifestGuard( - nativeDir: string -): RuntimeManifestGuard { - const runtimeManifestPath = join(nativeDir, 'embedded-mpv-runtime.json'); - const hiddenRuntimeManifestPath = `${runtimeManifestPath}.e2e-hidden-${process.pid}`; +export function createPackagedEntryGuard( + entryPath: string, + options: PackagedEntryGuardOptions +): PackagedEntryGuard { + const guardedEntryPath = resolve(entryPath); + const hiddenDirectory = resolve(options.hiddenDirectory); + const hiddenEntryPath = join( + hiddenDirectory, + `.${basename(guardedEntryPath)}.e2e-hidden-${process.pid}` + ); let hidden = false; return { hide() { - if (!statSync(runtimeManifestPath).isFile()) { + const entryStats = lstatSync(guardedEntryPath); + if (!entryKindMatches(entryStats, options.expectedKind)) { throw new Error( - `Packaged runtime manifest is not a regular file: ${runtimeManifestPath}` + `Packaged entry is not a ${options.expectedKind}: ${guardedEntryPath}` ); } - if (existsSync(hiddenRuntimeManifestPath)) { + const hiddenDirectoryStats = lstatSync(hiddenDirectory); + if ( + hiddenDirectoryStats.isSymbolicLink() || + !hiddenDirectoryStats.isDirectory() + ) { throw new Error( - `Stale hidden runtime manifest exists: ${hiddenRuntimeManifestPath}` + `Packaged entry stash is not a regular directory: ${hiddenDirectory}` ); } - renameSync(runtimeManifestPath, hiddenRuntimeManifestPath); + if (entryExistsByLstat(hiddenEntryPath)) { + throw new Error( + `Stale hidden packaged entry exists: ${hiddenEntryPath}` + ); + } + renameSync(guardedEntryPath, hiddenEntryPath); hidden = true; }, restore() { if (!hidden) { return; } - if (!existsSync(hiddenRuntimeManifestPath)) { + if (!entryExistsByLstat(hiddenEntryPath)) { throw new Error( - `Hidden runtime manifest disappeared before restore: ${hiddenRuntimeManifestPath}` + `Hidden packaged entry disappeared before restore: ${hiddenEntryPath}` ); } - if (existsSync(runtimeManifestPath)) { + if (entryExistsByLstat(guardedEntryPath)) { throw new Error( - `Refusing to overwrite runtime manifest during restore: ${runtimeManifestPath}` + `Refusing to overwrite packaged entry during restore: ${guardedEntryPath}` ); } - renameSync(hiddenRuntimeManifestPath, runtimeManifestPath); + renameSync(hiddenEntryPath, guardedEntryPath); hidden = false; }, }; @@ -198,6 +242,14 @@ export function readPackagedRuntimeIdentity( ); } } + if ( + typeof parsed.libmpvSoname !== 'string' || + !VERSIONED_LIBMPV_PATTERN.test(parsed.libmpvSoname) + ) { + throw new Error( + `Packaged runtime manifest libmpvSoname is invalid at ${runtimeManifestPath}.` + ); + } return parsed as PackagedRuntimeIdentity; } diff --git a/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-fixtures.spec.ts b/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-fixtures.spec.ts index cd58f0869..55e419e5f 100644 --- a/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-fixtures.spec.ts +++ b/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-fixtures.spec.ts @@ -1,92 +1,13 @@ import assert = require('node:assert/strict'); -import { - chmodSync, - existsSync, - lstatSync, - mkdtempSync, - mkdirSync, - readFileSync, - readlinkSync, - rmSync, - statSync, - symlinkSync, - writeFileSync, -} from 'node:fs'; -import { tmpdir } from 'node:os'; +import { readFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; -import { afterEach, describe, it } from 'node:test'; +import { describe, it } from 'node:test'; import { isMeaningfulNativePlaybackSnapshot } from './embedded-mpv-frame-copy-packaged-fixtures'; -import { - createDisposablePackagedLinuxApp, - createRuntimeManifestGuard, -} from './embedded-mpv-frame-copy-packaged-filesystem'; +import './embedded-mpv-frame-copy-packaged-filesystem.tests'; import { resolvePackagedElectronLaunchArgs } from './electron-test-fixtures'; import packagedPlaywrightConfig from '../playwright.packaged.config'; const projectRoot = resolve(__dirname, '..'); -const temporaryDirectories = new Set(); - -type PackageFixture = { - executablePath: string; - nativeDir: string; - packageRoot: string; - payloadPath: string; - runtimeManifestPath: string; -}; - -function createPackageFixture(): PackageFixture { - const packageRoot = mkdtempSync( - join(tmpdir(), 'iptvnator-packaged-fixture-source-') - ); - temporaryDirectories.add(packageRoot); - const executablePath = join(packageRoot, 'IPTVnator'); - const nativeDir = join( - packageRoot, - 'resources', - 'app.asar.unpacked', - 'electron-backend', - 'native' - ); - const payloadPath = join(packageRoot, 'resources', 'payload.bin'); - const runtimeManifestPath = join(nativeDir, 'embedded-mpv-runtime.json'); - - mkdirSync(nativeDir, { recursive: true }); - writeFileSync(executablePath, '#!/bin/sh\nexit 0\n'); - chmodSync(executablePath, 0o755); - writeFileSync(payloadPath, 'packaged payload'); - chmodSync(payloadPath, 0o640); - writeFileSync( - runtimeManifestPath, - JSON.stringify({ - arch: 'x64', - platform: 'linux', - profile: 'portable', - runtimeMode: 'bundled', - }) - ); - - if (process.platform !== 'win32') { - symlinkSync( - 'payload.bin', - join(packageRoot, 'resources', 'payload-link') - ); - } - - return { - executablePath, - nativeDir, - packageRoot, - payloadPath, - runtimeManifestPath, - }; -} - -afterEach(() => { - for (const directory of temporaryDirectories) { - rmSync(directory, { force: true, recursive: true }); - } - temporaryDirectories.clear(); -}); describe('packaged Electron launch arguments', () => { it('disables the Chromium sandbox only when running as root', () => { @@ -113,92 +34,6 @@ describe('packaged Electron launch arguments', () => { }); }); -describe('disposable unpacked package clone', () => { - it('hardlinks regular files, preserves modes and symlinks, and never hides the source manifest', () => { - const source = createPackageFixture(); - const clone = createDisposablePackagedLinuxApp(source.executablePath); - temporaryDirectories.add(clone.temporaryRoot); - - try { - assert.notEqual(clone.packageRoot, source.packageRoot); - assert.equal( - statSync(clone.executablePath).mode & 0o777, - statSync(source.executablePath).mode & 0o777 - ); - - const clonedPayloadPath = join( - clone.packageRoot, - 'resources', - 'payload.bin' - ); - assert.equal( - statSync(clonedPayloadPath).ino, - statSync(source.payloadPath).ino - ); - assert.equal(statSync(clonedPayloadPath).mode & 0o777, 0o640); - - if (process.platform !== 'win32') { - const clonedLinkPath = join( - clone.packageRoot, - 'resources', - 'payload-link' - ); - assert.equal(lstatSync(clonedLinkPath).isSymbolicLink(), true); - assert.equal(readlinkSync(clonedLinkPath), 'payload.bin'); - } - - const clonedRuntimeManifestPath = join( - clone.nativeDir, - 'embedded-mpv-runtime.json' - ); - const guard = createRuntimeManifestGuard(clone.nativeDir); - guard.hide(); - assert.equal(existsSync(clonedRuntimeManifestPath), false); - assert.equal(existsSync(source.runtimeManifestPath), true); - guard.restore(); - } finally { - clone.cleanup(); - temporaryDirectories.delete(clone.temporaryRoot); - } - - assert.equal(existsSync(clone.temporaryRoot), false); - assert.equal(existsSync(source.runtimeManifestPath), true); - }); - - it('copies a regular file when hardlinking is unavailable', () => { - const source = createPackageFixture(); - const clone = createDisposablePackagedLinuxApp(source.executablePath, { - linkFile() { - throw Object.assign(new Error('cross-device hardlink'), { - code: 'EXDEV', - }); - }, - }); - temporaryDirectories.add(clone.temporaryRoot); - - try { - assert.equal(statSync(clone.executablePath).mode & 0o777, 0o755); - const clonedPayloadPath = join( - clone.packageRoot, - 'resources', - 'payload.bin' - ); - assert.equal( - readFileSync(clonedPayloadPath, 'utf8'), - readFileSync(source.payloadPath, 'utf8') - ); - assert.notEqual( - statSync(clonedPayloadPath).ino, - statSync(source.payloadPath).ino - ); - assert.equal(statSync(clonedPayloadPath).mode & 0o777, 0o640); - } finally { - clone.cleanup(); - temporaryDirectories.delete(clone.temporaryRoot); - } - }); -}); - describe('native-view playback proof', () => { it('requires the loaded URL, a playing or paused state, and positive duration', () => { const expectedUrl = 'http://127.0.0.1:3210/fixture.y4m'; @@ -276,6 +111,42 @@ describe('native-view playback proof', () => { assert.ok(exitCodeIndex > proofIndex); assert.ok(disposeIndex > exitCodeIndex); }); + + it('removes the manifest-declared libmpv target and expects the stable missing-library reason', () => { + const source = readFileSync( + join(projectRoot, 'src', 'embedded-mpv-frame-copy-packaged.e2e.ts'), + 'utf8' + ); + const manifestIdentityIndex = source.indexOf( + 'const runtimeIdentity = readPackagedRuntimeIdentity' + ); + const guardIndex = source.indexOf( + 'createPackagedEntryGuard(', + manifestIdentityIndex + ); + const sonameIndex = source.indexOf( + 'runtimeIdentity.libmpvSoname', + guardIndex + ); + const hideIndex = source.indexOf('.hide()', sonameIndex); + const fallbackStart = source.indexOf( + 'const launchedFallbackApp', + hideIndex + ); + const missingReasonIndex = source.indexOf( + "frameCopyUnavailableReason: 'runtime-library-missing'", + fallbackStart + ); + + assert.ok(manifestIdentityIndex >= 0); + assert.ok(guardIndex > manifestIdentityIndex); + assert.ok(sonameIndex > guardIndex); + assert.ok(hideIndex > sonameIndex); + assert.ok(fallbackStart > hideIndex); + assert.ok(missingReasonIndex > fallbackStart); + assert.doesNotMatch(source, /createRuntimeManifestGuard/); + assert.doesNotMatch(source, /runtimeManifest\.hide/); + }); }); describe('dedicated packaged smoke target', () => { diff --git a/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-fixtures.ts b/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-fixtures.ts index dc11e2266..31c888631 100644 --- a/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-fixtures.ts +++ b/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged-fixtures.ts @@ -12,7 +12,7 @@ import { } from './electron-test-fixtures'; import type { DisposablePackagedLinuxApp, - RuntimeManifestGuard, + PackagedEntryGuard, } from './embedded-mpv-frame-copy-packaged-filesystem'; declare global { @@ -291,9 +291,9 @@ export async function closeAndWaitForExit( export async function cleanupPackagedFrameCopySmoke(options: { apps: Array; + hiddenRuntimeEntry?: PackagedEntryGuard; media?: LocalMediaServer; packageClone?: DisposablePackagedLinuxApp; - runtimeManifest?: RuntimeManifestGuard; }): Promise { const errors: unknown[] = []; @@ -308,9 +308,9 @@ export async function cleanupPackagedFrameCopySmoke(options: { } } - if (options.runtimeManifest) { + if (options.hiddenRuntimeEntry) { try { - options.runtimeManifest.restore(); + options.hiddenRuntimeEntry.restore(); } catch (error) { errors.push(error); } diff --git a/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged.e2e.ts b/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged.e2e.ts index b6b282063..b9a43f860 100644 --- a/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged.e2e.ts +++ b/apps/electron-backend-e2e/src/embedded-mpv-frame-copy-packaged.e2e.ts @@ -5,6 +5,7 @@ import { test, type LaunchedElectronApp, } from './electron-test-fixtures'; +import { join } from 'path'; import { assertNativeFallbackPrerequisites, cleanupPackagedFrameCopySmoke, @@ -20,10 +21,10 @@ import { } from './embedded-mpv-frame-copy-packaged-fixtures'; import { createDisposablePackagedLinuxApp, - createRuntimeManifestGuard, + createPackagedEntryGuard, readPackagedRuntimeIdentity, type DisposablePackagedLinuxApp, - type RuntimeManifestGuard, + type PackagedEntryGuard, } from './embedded-mpv-frame-copy-packaged-filesystem'; const PACKAGED_FRAME_COPY_REQUIRED_ENV = @@ -68,7 +69,7 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => { const sourceExecutablePath = packagedExecutable as string; assertNativeFallbackPrerequisites(); let packageClone: DisposablePackagedLinuxApp | undefined; - let runtimeManifest: RuntimeManifestGuard | undefined; + let hiddenRuntimeEntry: PackagedEntryGuard | undefined; let media: LocalMediaServer | undefined; let frameCopyApp: LaunchedElectronApp | undefined; let fallbackApp: LaunchedElectronApp | undefined; @@ -79,7 +80,13 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => { const executablePath = packageClone.executablePath; const nativeDir = packageClone.nativeDir; const runtimeIdentity = readPackagedRuntimeIdentity(nativeDir); - runtimeManifest = createRuntimeManifestGuard(nativeDir); + hiddenRuntimeEntry = createPackagedEntryGuard( + join(nativeDir, 'lib', runtimeIdentity.libmpvSoname), + { + expectedKind: 'regular-file', + hiddenDirectory: packageClone.temporaryRoot, + } + ); const mediaServer = await createLocalMediaServer(); media = mediaServer; @@ -219,7 +226,10 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => { await closeAndWaitForExit(launchedFrameCopyApp); frameCopyApp = undefined; - runtimeManifest.hide(); + hiddenRuntimeEntry.hide(); + expect(readPackagedRuntimeIdentity(nativeDir)).toEqual( + runtimeIdentity + ); const launchedFallbackApp = await launchPackagedElectronApp( executablePath, @@ -237,7 +247,7 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => { expect(fallbackSupport).toMatchObject({ engine: 'native', frameCopyAvailable: false, - frameCopyUnavailableReason: 'runtime-manifest-missing', + frameCopyUnavailableReason: 'runtime-library-missing', platform: 'linux', supported: true, }); @@ -292,9 +302,9 @@ test.describe('Packaged Linux embedded MPV frame-copy runtime', () => { } finally { await cleanupPackagedFrameCopySmoke({ apps: [frameCopyApp, fallbackApp], + hiddenRuntimeEntry, media, packageClone, - runtimeManifest, }); } });