mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
fix(packaging): harden Linux frame-copy delivery
This commit is contained in:
1 parent
4367fb595f
commit
482630dd2a
16 files changed
+367
-28
No files matched your search
@@ -827,9 +827,12 @@ jobs:
|
||||
|
||||
cp "${RUNNER_TEMP}/electron-builder.base.json" electron-builder.json
|
||||
node tools/packaging/configure-linux-frame-copy-build.mjs --foreign-deb
|
||||
pnpm nx run electron-backend:make \
|
||||
--outputPath=dist/executables-linux-foreign \
|
||||
--publishPolicy=never
|
||||
for foreign_arch in armv7l arm64; do
|
||||
pnpm nx run electron-backend:make \
|
||||
--arch="${foreign_arch}" \
|
||||
--outputPath=dist/executables-linux-foreign \
|
||||
--publishPolicy=never
|
||||
done
|
||||
find dist/executables-linux-foreign -maxdepth 1 -type f -name '*.deb' \
|
||||
-exec mv '{}' dist/executables/ ';'
|
||||
|
||||
|
||||
@@ -240,9 +240,9 @@ Key files:
|
||||
sandbox. Any failure reports a stable reason and falls back to native-view
|
||||
without crashing; an environment flag never bypasses this gate.
|
||||
- Snap uses an exact private `shared-memory` plug. The probe and playback
|
||||
helper share one sanitized loader environment: ambient preload/library
|
||||
paths are removed, the validated private closure wins, and trusted Snap GL
|
||||
roots precede generic in-snap library roots.
|
||||
helper share one sanitized loader environment: ambient audit, preload, and
|
||||
library paths are removed, the validated private closure wins, and trusted
|
||||
Snap GL roots precede generic in-snap library roots.
|
||||
- Bundled Linux releases must publish the exact source archives/git records,
|
||||
checksums, licenses, flags, patches, build scripts, and the pinned hwdata
|
||||
`pnp.ids` input. Each bundled package carries
|
||||
|
||||
@@ -636,9 +636,10 @@ engine` (restart required) or
|
||||
packaged manifest/file/hash gate and bounded `--runtime-probe`; any failure
|
||||
keeps the sandbox enabled, records a stable reason, and falls back to
|
||||
native-view without crashing. Snap uses an exact private `shared-memory`
|
||||
plug; probe and playback share a sanitized loader environment in which the
|
||||
validated private closure and trusted Snap GL roots have explicit
|
||||
precedence. Bundled Linux packages carry hash-validated
|
||||
plug; probe and playback share a sanitized loader environment in which
|
||||
ambient audit, preload, and library paths are removed and the validated
|
||||
private closure and trusted Snap GL roots have explicit precedence. Bundled
|
||||
Linux packages carry hash-validated
|
||||
`embedded-mpv-notices.json`, `THIRD_PARTY_NOTICES.txt`, and `licenses/**`.
|
||||
CI caches the staged runtime plus immutable source inputs, never finished
|
||||
notices or the compliance tarball; it regenerates those notices and the
|
||||
|
||||
@@ -17,6 +17,7 @@ const {
|
||||
validateLinuxSystemBuildInputManifest,
|
||||
} = require('../../tools/embedded-mpv/linux-runtime-manifest.cjs');
|
||||
const {
|
||||
resolveLinuxFrameCopyLinkageInputs,
|
||||
resolveVerifiedLinuxLibMpvSoname,
|
||||
runWithCleanup,
|
||||
validateLinuxFrameCopyLinkage,
|
||||
@@ -316,9 +317,9 @@ function findWindowsLibMpv(runtimeRoot) {
|
||||
}
|
||||
|
||||
/* Debian/Ubuntu install linker targets under the multiarch triple dir. The
|
||||
* compiler's built-in search paths cover it for -l resolution either way;
|
||||
* this keeps the -L flag and the helper's baked rpath pointing somewhere
|
||||
* real. */
|
||||
* compiler's built-in search paths cover it for -l resolution either way.
|
||||
* This directory is a link-time input only; the helper runtime intentionally
|
||||
* stays on the sanitized system loader contract. */
|
||||
function defaultLinuxSystemLibDir() {
|
||||
const multiarchTriples = {
|
||||
arm: 'arm-linux-gnueabihf',
|
||||
@@ -381,8 +382,9 @@ function resolveRuntime() {
|
||||
if (targetPlatform === 'linux') {
|
||||
// Dev-first Linux flow (frame-copy helper links system libmpv): a
|
||||
// distro libmpv-dev install is a full runtime — no staging needed.
|
||||
// LIBMPV_INCLUDE_DIR / LINUX_NATIVE_LIBRARY_DIR override the system
|
||||
// paths for machines with a local (non-root) libmpv prefix.
|
||||
// LIBMPV_INCLUDE_DIR overrides the header path.
|
||||
// LINUX_NATIVE_LIBRARY_DIR overrides the link-time library directory,
|
||||
// which must already be visible to the system dynamic loader.
|
||||
const systemIncludeDir =
|
||||
process.env.LIBMPV_INCLUDE_DIR || '/usr/include';
|
||||
if (fs.existsSync(path.join(systemIncludeDir, 'mpv', 'client.h'))) {
|
||||
@@ -765,6 +767,16 @@ function main() {
|
||||
: targetPlatform === 'linux'
|
||||
? writeLinuxFrameCopyBuildManifest(runtime)
|
||||
: copyGenericRuntimeToNativeBuild(runtime);
|
||||
const linuxLinkageInputs =
|
||||
targetPlatform === 'linux'
|
||||
? resolveLinuxFrameCopyLinkageInputs({
|
||||
buildInputMode: runtime.buildInputMode,
|
||||
outputLibDir,
|
||||
packagedLibmpvSoname: runtimeManifest.libmpvSoname,
|
||||
readDynamicSection: readLinuxDynamicSection,
|
||||
runtimeLibDir: runtime.libDir,
|
||||
})
|
||||
: null;
|
||||
|
||||
const electronPackageJson = require(
|
||||
path.join(workspaceRoot, 'node_modules', 'electron', 'package.json')
|
||||
@@ -781,7 +793,8 @@ function main() {
|
||||
LIBMPV_INCLUDE_DIR: runtime.includeDir,
|
||||
...(targetPlatform === 'linux'
|
||||
? {
|
||||
LINUX_VERIFIED_RUNTIME_LIBRARY_DIR: outputLibDir,
|
||||
LINUX_VERIFIED_RUNTIME_LIBRARY_DIR:
|
||||
linuxLinkageInputs.linkerLibraryDir,
|
||||
}
|
||||
: { LIBMPV_LIBRARY_DIR: outputLibDir }),
|
||||
...(runtime.windowsImportLib
|
||||
@@ -807,7 +820,7 @@ function main() {
|
||||
|
||||
if (targetPlatform === 'linux') {
|
||||
validateLinuxFrameCopyLinkage({
|
||||
expectedLibmpvSoname: runtimeManifest.libmpvSoname,
|
||||
expectedLibmpvSoname: linuxLinkageInputs.expectedLibmpvSoname,
|
||||
outputDir,
|
||||
readDynamicSection: readLinuxDynamicSection,
|
||||
});
|
||||
|
||||
@@ -201,6 +201,51 @@ function resolveVerifiedLinuxLibMpvSoname({
|
||||
return expectedSoname;
|
||||
}
|
||||
|
||||
function resolveLinuxFrameCopyLinkageInputs({
|
||||
buildInputMode,
|
||||
outputLibDir,
|
||||
packagedLibmpvSoname,
|
||||
readDynamicSection,
|
||||
runtimeLibDir,
|
||||
}) {
|
||||
const systemDevelopment = buildInputMode === 'system-dev';
|
||||
const linkerLibraryDir = systemDevelopment ? runtimeLibDir : outputLibDir;
|
||||
if (
|
||||
typeof linkerLibraryDir !== 'string' ||
|
||||
linkerLibraryDir.trim().length === 0
|
||||
) {
|
||||
throw new Error(
|
||||
'Linux frame-copy linkage requires a non-empty linker library directory.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!systemDevelopment) {
|
||||
return {
|
||||
expectedLibmpvSoname: packagedLibmpvSoname,
|
||||
linkerLibraryDir,
|
||||
};
|
||||
}
|
||||
if (typeof readDynamicSection !== 'function') {
|
||||
throw new TypeError('Linux readelf dynamic reader is required.');
|
||||
}
|
||||
|
||||
const linkerInputPath = path.join(linkerLibraryDir, 'libmpv.so');
|
||||
const dynamic = parseReadelfDynamic(readDynamicSection(linkerInputPath));
|
||||
if (
|
||||
dynamic.soname.length !== 1 ||
|
||||
!VERSIONED_LIBMPV_PATTERN.test(dynamic.soname[0])
|
||||
) {
|
||||
throw new Error(
|
||||
'The system-development libmpv linker input must contain exactly one versioned libmpv SONAME.'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
expectedLibmpvSoname: dynamic.soname[0],
|
||||
linkerLibraryDir,
|
||||
};
|
||||
}
|
||||
|
||||
function assertRegularArtifact(filePath, label) {
|
||||
let stat;
|
||||
try {
|
||||
@@ -288,6 +333,7 @@ function runWithCleanup(operation, cleanup) {
|
||||
|
||||
module.exports = {
|
||||
parseReadelfDynamic,
|
||||
resolveLinuxFrameCopyLinkageInputs,
|
||||
resolveVerifiedLinuxLibMpvSoname,
|
||||
runWithCleanup,
|
||||
validateLinuxFrameCopyLinkage,
|
||||
|
||||
+3
@@ -8,6 +8,7 @@ describe('createLinuxFrameCopyHelperEnvironment', () => {
|
||||
{
|
||||
PATH: '/usr/bin',
|
||||
HOME: '/home/user',
|
||||
LD_AUDIT: '/tmp/audit.so',
|
||||
LD_LIBRARY_PATH: '/tmp/hostile-libs',
|
||||
LD_PRELOAD: '/tmp/inject.so',
|
||||
},
|
||||
@@ -41,6 +42,7 @@ describe('createLinuxFrameCopyHelperEnvironment', () => {
|
||||
'/var/lib/snapd/lib/gl/nvidia',
|
||||
'/var/lib/snapd/lib/gl-evil',
|
||||
].join(':'),
|
||||
LD_AUDIT: '/tmp/audit.so',
|
||||
LD_LIBRARY_PATH: '/tmp/hostile-libs',
|
||||
LD_PRELOAD: '/tmp/inject.so',
|
||||
},
|
||||
@@ -75,6 +77,7 @@ describe('createLinuxFrameCopyHelperEnvironment', () => {
|
||||
PATH: '/usr/bin',
|
||||
SNAP: '/snap/iptvnator/42',
|
||||
SNAP_LIBRARY_PATH: '/var/lib/snapd/lib/gl:/tmp/hostile-gl',
|
||||
LD_AUDIT: '/tmp/audit.so',
|
||||
LD_LIBRARY_PATH: '/tmp/hostile-libs',
|
||||
LD_PRELOAD: '/tmp/inject.so',
|
||||
},
|
||||
|
||||
+1
@@ -86,6 +86,7 @@ export function createLinuxFrameCopyHelperEnvironment(
|
||||
runtimeMode: EmbeddedMpvFrameCopyRuntimeMode
|
||||
): NodeJS.ProcessEnv {
|
||||
const helperEnvironment = { ...environment };
|
||||
delete helperEnvironment.LD_AUDIT;
|
||||
delete helperEnvironment.LD_LIBRARY_PATH;
|
||||
delete helperEnvironment.LD_PRELOAD;
|
||||
|
||||
|
||||
+1
@@ -53,6 +53,7 @@ export function createRuntimeTestContext(): RuntimeTestContext {
|
||||
arch: 'x64',
|
||||
env: {
|
||||
PATH: '/usr/bin',
|
||||
LD_AUDIT: '/tmp/audit.so',
|
||||
LD_LIBRARY_PATH: '/ambient/libs',
|
||||
LD_PRELOAD: '/tmp/inject.so',
|
||||
},
|
||||
|
||||
@@ -117,6 +117,7 @@ describe('EmbeddedMpvFrameCopyAdapter', () => {
|
||||
environment: {
|
||||
PATH: '/usr/bin',
|
||||
HOME: '/home/user',
|
||||
LD_AUDIT: '/tmp/audit.so',
|
||||
LD_LIBRARY_PATH: '/tmp/hostile-libs',
|
||||
LD_PRELOAD: '/tmp/inject.so',
|
||||
},
|
||||
@@ -148,6 +149,7 @@ describe('EmbeddedMpvFrameCopyAdapter', () => {
|
||||
PATH: '/snap/bin:/usr/bin',
|
||||
SNAP: snapRoot,
|
||||
SNAP_LIBRARY_PATH: '/var/lib/snapd/lib/gl:/tmp/hostile-gl',
|
||||
LD_AUDIT: '/tmp/audit.so',
|
||||
LD_LIBRARY_PATH: '/tmp/hostile-libs',
|
||||
LD_PRELOAD: '/tmp/inject.so',
|
||||
},
|
||||
|
||||
@@ -52,6 +52,16 @@ function loadLinkageModule(): {
|
||||
runtimeDependencyClosure: SonameFixture['runtimeDependencyClosure'];
|
||||
runtimeFiles: RuntimeFileRecord[];
|
||||
}) => string;
|
||||
resolveLinuxFrameCopyLinkageInputs: (options: {
|
||||
buildInputMode: string;
|
||||
outputLibDir: string;
|
||||
packagedLibmpvSoname: string | null;
|
||||
readDynamicSection: (filePath: string) => string;
|
||||
runtimeLibDir: string;
|
||||
}) => {
|
||||
expectedLibmpvSoname: string | null;
|
||||
linkerLibraryDir: string;
|
||||
};
|
||||
runWithCleanup: <T>(operation: () => T, cleanup: () => void) => T;
|
||||
validateLinuxFrameCopyLinkage: (options: {
|
||||
expectedLibmpvSoname: string;
|
||||
@@ -287,6 +297,93 @@ describe('Linux Embedded MPV linkage verification', () => {
|
||||
).toThrow(/size|SHA-256/i);
|
||||
});
|
||||
|
||||
it('uses and identifies the unmanaged system libmpv only for system development', () => {
|
||||
const { resolveLinuxFrameCopyLinkageInputs } = loadLinkageModule();
|
||||
const readDynamicSection = jest.fn((filePath: string) => {
|
||||
expect(filePath).toBe('/opt/libmpv/lib/libmpv.so');
|
||||
return readelfDynamic([['SONAME', 'libmpv.so.2']]);
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveLinuxFrameCopyLinkageInputs({
|
||||
buildInputMode: 'system-dev',
|
||||
outputLibDir: '/native/build/Release/lib',
|
||||
packagedLibmpvSoname: null,
|
||||
readDynamicSection,
|
||||
runtimeLibDir: '/opt/libmpv/lib',
|
||||
})
|
||||
).toEqual({
|
||||
expectedLibmpvSoname: 'libmpv.so.2',
|
||||
linkerLibraryDir: '/opt/libmpv/lib',
|
||||
});
|
||||
expect(readDynamicSection).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('keeps bundled and untrusted build modes on the copied runtime directory', () => {
|
||||
const { resolveLinuxFrameCopyLinkageInputs } = loadLinkageModule();
|
||||
const readDynamicSection = jest.fn(() => {
|
||||
throw new Error('must not inspect the ambient system runtime');
|
||||
});
|
||||
|
||||
for (const buildInputMode of [
|
||||
'bundled-runtime',
|
||||
'system-build-inputs',
|
||||
'unexpected-mode',
|
||||
]) {
|
||||
expect(
|
||||
resolveLinuxFrameCopyLinkageInputs({
|
||||
buildInputMode,
|
||||
outputLibDir: '/native/build/Release/lib',
|
||||
packagedLibmpvSoname:
|
||||
buildInputMode === 'bundled-runtime'
|
||||
? 'libmpv.so.2'
|
||||
: null,
|
||||
readDynamicSection,
|
||||
runtimeLibDir: '/usr/lib/x86_64-linux-gnu',
|
||||
})
|
||||
).toEqual({
|
||||
expectedLibmpvSoname:
|
||||
buildInputMode === 'bundled-runtime' ? 'libmpv.so.2' : null,
|
||||
linkerLibraryDir: '/native/build/Release/lib',
|
||||
});
|
||||
}
|
||||
expect(readDynamicSection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects ambiguous or unversioned system-development libmpv identities', () => {
|
||||
const { resolveLinuxFrameCopyLinkageInputs } = loadLinkageModule();
|
||||
const options = {
|
||||
buildInputMode: 'system-dev',
|
||||
outputLibDir: '/native/build/Release/lib',
|
||||
packagedLibmpvSoname: null,
|
||||
runtimeLibDir: '/usr/lib/x86_64-linux-gnu',
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
resolveLinuxFrameCopyLinkageInputs({
|
||||
...options,
|
||||
readDynamicSection: () =>
|
||||
readelfDynamic([['SONAME', 'libmpv.so']]),
|
||||
})
|
||||
).toThrow(/system-development.*exactly one versioned libmpv SONAME/i);
|
||||
expect(() =>
|
||||
resolveLinuxFrameCopyLinkageInputs({
|
||||
...options,
|
||||
readDynamicSection: () => readelfDynamic([]),
|
||||
})
|
||||
).toThrow(/system-development.*exactly one versioned libmpv SONAME/i);
|
||||
expect(() =>
|
||||
resolveLinuxFrameCopyLinkageInputs({
|
||||
...options,
|
||||
readDynamicSection: () =>
|
||||
readelfDynamic([
|
||||
['SONAME', 'libmpv.so.1'],
|
||||
['SONAME', 'libmpv.so.2'],
|
||||
]),
|
||||
})
|
||||
).toThrow(/system-development.*exactly one versioned libmpv SONAME/i);
|
||||
});
|
||||
|
||||
function createArtifactFixture(): {
|
||||
outputDir: string;
|
||||
readDynamicSection: (filePath: string) => string;
|
||||
|
||||
@@ -604,7 +604,14 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
'SHA-256 mismatch for staged Linux runtime file'
|
||||
);
|
||||
expect(buildScriptSource).toContain(
|
||||
'LINUX_VERIFIED_RUNTIME_LIBRARY_DIR: outputLibDir'
|
||||
'resolveLinuxFrameCopyLinkageInputs({'
|
||||
);
|
||||
expect(buildScriptSource).toContain(
|
||||
'LINUX_VERIFIED_RUNTIME_LIBRARY_DIR:\n' +
|
||||
' linuxLinkageInputs.linkerLibraryDir'
|
||||
);
|
||||
expect(buildScriptSource).toContain(
|
||||
'expectedLibmpvSoname: linuxLinkageInputs.expectedLibmpvSoname'
|
||||
);
|
||||
expect(buildScriptSource).not.toContain(
|
||||
'process.env.LINUX_NATIVE_LIBRARY_DIR || runtime.libDir'
|
||||
|
||||
@@ -254,10 +254,11 @@ JSON line and return zero.
|
||||
|
||||
The startup probe and every playback helper session use the same sanitized
|
||||
loader environment selected by the validated manifest's cached `runtimeMode`.
|
||||
Both remove ambient `LD_PRELOAD` and `LD_LIBRARY_PATH`; the system profile then
|
||||
uses the default system loader without a private path. Bundled profiles put the
|
||||
validated packaged `native/lib` first. AppImage and Flatpak resolve the declared
|
||||
external graphics/audio interfaces through their normal host or sandbox loader.
|
||||
Both remove ambient `LD_AUDIT`, `LD_PRELOAD`, and `LD_LIBRARY_PATH`; the system
|
||||
profile then uses the default system loader without a private path. Bundled
|
||||
profiles put the validated packaged `native/lib` first. AppImage and Flatpak
|
||||
resolve the declared external graphics/audio interfaces through their normal
|
||||
host or sandbox loader.
|
||||
Inside a genuine Snap mount, filtered absolute `SNAP_LIBRARY_PATH` entries below
|
||||
`/var/lib/snapd/lib/gl` follow `native/lib` and precede the generic
|
||||
`$SNAP/lib`, `$SNAP/usr/lib`, and x64 multiarch roots. This preserves the pinned
|
||||
@@ -558,7 +559,7 @@ The Electron main process holds an `electron.powerSaveBlocker` of type `prevent-
|
||||
Current development behavior:
|
||||
|
||||
- The addon build supports `darwin`, `win32`, and `linux`; Windows and Linux builds require running on that target OS.
|
||||
- The build script first looks for staged inputs at `vendor/embedded-mpv/<platform>-<arch>/`. On Linux, local development can fall back to distribution `libmpv-dev` headers and libraries; `LIBMPV_INCLUDE_DIR` and `LINUX_NATIVE_LIBRARY_DIR` override the default system paths.
|
||||
- The build script first looks for staged inputs at `vendor/embedded-mpv/<platform>-<arch>/`. On Linux, local development can fall back to distribution `libmpv-dev` headers and libraries. `LIBMPV_INCLUDE_DIR` overrides the header root. `LINUX_NATIVE_LIBRARY_DIR` is a link-time override and must name a directory already visible to the system dynamic loader; it is never inherited as helper `LD_LIBRARY_PATH`.
|
||||
- When the staged-input path is used, it must contain `include/mpv/client.h`,
|
||||
`runtime-manifest.json`, and the platform runtime/build files. The Linux
|
||||
source builder also stages the complete declared `.so` closure.
|
||||
|
||||
@@ -126,8 +126,9 @@ POSIX shm namespace without granting global cross-snap shared-memory access.
|
||||
|
||||
The bounded probe and every playback helper share one sanitized loader
|
||||
environment derived from the validated, cached runtime mode. Ambient
|
||||
`LD_PRELOAD` and `LD_LIBRARY_PATH` are removed. System packages then use the
|
||||
default loader; bundled packages put their validated `native/lib` first.
|
||||
`LD_AUDIT`, `LD_PRELOAD`, and `LD_LIBRARY_PATH` are removed. System packages
|
||||
then use the default loader; bundled packages put their validated `native/lib`
|
||||
first.
|
||||
AppImage and Flatpak use normal host/sandbox lookup for the declared external
|
||||
interfaces. In a genuine Snap mount, filtered `SNAP_LIBRARY_PATH` GL roots
|
||||
under `/var/lib/snapd/lib/gl` come next, ahead of generic `$SNAP` library and
|
||||
@@ -230,8 +231,10 @@ license notices with the binary.
|
||||
|
||||
Linux can use distribution development packages for an unshipped local build
|
||||
(`libmpv-dev`, EGL/OpenGL/GBM development files, and X11 headers). Overrides:
|
||||
`LIBMPV_INCLUDE_DIR` and `LINUX_NATIVE_LIBRARY_DIR`. Required/release package
|
||||
builds must use the pinned staged runtime and manifest.
|
||||
`LIBMPV_INCLUDE_DIR` selects the header root. `LINUX_NATIVE_LIBRARY_DIR`
|
||||
selects a link-time library directory that must already be visible to the
|
||||
system dynamic loader; it is not inherited as a helper `LD_LIBRARY_PATH`.
|
||||
Required/release package builds must use the pinned staged runtime and manifest.
|
||||
|
||||
On macOS:
|
||||
|
||||
|
||||
@@ -362,6 +362,18 @@ test('Linux CI verifies every package family and exercises intended environments
|
||||
assert.doesNotMatch(buildWorkflow, /\bldd\b/);
|
||||
});
|
||||
|
||||
test('foreign DEB CI explicitly selects both marker-only ARM architectures', () => {
|
||||
const foreignDebStep = workflowStep(
|
||||
'Make marker-only foreign-architecture DEB packages'
|
||||
);
|
||||
|
||||
assert.match(foreignDebStep, /for foreign_arch in armv7l arm64; do/);
|
||||
assert.match(foreignDebStep, /--arch="\$\{foreign_arch\}"/);
|
||||
assert.match(foreignDebStep, /IPTVNATOR_LINUX_FRAME_COPY_PROFILE: ''/);
|
||||
assert.match(foreignDebStep, /IPTVNATOR_REQUIRE_EMBEDDED_MPV: '1'/);
|
||||
assert.doesNotMatch(foreignDebStep, /IPTVNATOR_REQUIRE_EMBEDDED_MPV: '0'/);
|
||||
});
|
||||
|
||||
test('dedicated packaged x64 smoke cannot silently skip', () => {
|
||||
const linuxDependencies = workflowStep('Install Linux system dependencies');
|
||||
assert.match(linuxDependencies, /--no-install-recommends/);
|
||||
|
||||
@@ -179,7 +179,8 @@ export function extractLinuxArtifact({
|
||||
destination,
|
||||
runCommand = defaultRunCommand,
|
||||
}) {
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
fs.rmSync(destination, { recursive: true, force: true });
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
||||
const run = (command, args, options = {}) =>
|
||||
assertCommandSucceeded(
|
||||
command,
|
||||
@@ -198,7 +199,6 @@ export function extractLinuxArtifact({
|
||||
const failures = [];
|
||||
for (const offset of squashfsOffsets) {
|
||||
fs.rmSync(destination, { recursive: true, force: true });
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
const args = [
|
||||
'-no-progress',
|
||||
'-offset',
|
||||
@@ -226,10 +226,12 @@ export function extractLinuxArtifact({
|
||||
);
|
||||
}
|
||||
case 'deb':
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
run('dpkg-deb', ['--extract', artifactPath, destination]);
|
||||
return destination;
|
||||
case 'rpm':
|
||||
case 'pacman':
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
run('bsdtar', [
|
||||
'--extract',
|
||||
'--file',
|
||||
@@ -247,9 +249,15 @@ export function extractLinuxArtifact({
|
||||
]);
|
||||
return destination;
|
||||
case 'flatpak': {
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
const repository = path.join(destination, '.ostree-repository');
|
||||
const checkout = path.join(destination, 'checkout');
|
||||
fs.mkdirSync(repository, { recursive: true });
|
||||
run('ostree', [
|
||||
`--repo=${repository}`,
|
||||
'init',
|
||||
'--mode=archive-z2',
|
||||
]);
|
||||
run('flatpak', ['build-import-bundle', repository, artifactPath]);
|
||||
const refsResult = run('ostree', ['refs', `--repo=${repository}`]);
|
||||
const refs = String(refsResult.stdout ?? '')
|
||||
@@ -265,6 +273,7 @@ export function extractLinuxArtifact({
|
||||
}
|
||||
run('ostree', [
|
||||
'checkout',
|
||||
'-U',
|
||||
`--repo=${repository}`,
|
||||
refs[0],
|
||||
checkout,
|
||||
@@ -1122,6 +1131,7 @@ export function createRuntimeProbeEnvironment({
|
||||
runtimeMode,
|
||||
}) {
|
||||
const probeEnvironment = { ...(environment ?? {}) };
|
||||
delete probeEnvironment.LD_AUDIT;
|
||||
delete probeEnvironment.LD_LIBRARY_PATH;
|
||||
delete probeEnvironment.LD_PRELOAD;
|
||||
if (runtimeMode === 'bundled') {
|
||||
|
||||
@@ -378,6 +378,7 @@ test('extracts every payload format with argument arrays and no shell', () => {
|
||||
'bsdtar',
|
||||
'bsdtar',
|
||||
'unsquashfs',
|
||||
'ostree',
|
||||
'flatpak',
|
||||
'ostree',
|
||||
'ostree',
|
||||
@@ -402,6 +403,142 @@ test('extracts every payload format with argument arrays and no shell', () => {
|
||||
'-no-progress',
|
||||
'-dest',
|
||||
]);
|
||||
assert.equal(invocations[5].args[1], 'init');
|
||||
assert.ok(invocations[5].args.includes('--mode=archive-z2'));
|
||||
assert.equal(invocations[8].args[0], 'checkout');
|
||||
assert.ok(invocations[8].args.includes('-U'));
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('gives unsquashfs a fresh destination for every AppImage and Snap extraction', () => {
|
||||
const root = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'iptvnator-verifier-unsquashfs-')
|
||||
);
|
||||
const appImagePath = path.join(root, 'IPTVnator.AppImage');
|
||||
const snapPath = path.join(root, 'IPTVnator.snap');
|
||||
const appImageDestination = path.join(root, 'appimage-payload');
|
||||
const snapDestination = path.join(root, 'snap-payload');
|
||||
fs.writeFileSync(
|
||||
appImagePath,
|
||||
Buffer.concat([
|
||||
Buffer.alloc(128),
|
||||
Buffer.from('hsqs'),
|
||||
Buffer.alloc(64),
|
||||
Buffer.from('hsqs'),
|
||||
])
|
||||
);
|
||||
fs.writeFileSync(snapPath, 'snap fixture');
|
||||
fs.mkdirSync(appImageDestination);
|
||||
fs.mkdirSync(snapDestination);
|
||||
let appImageAttempt = 0;
|
||||
|
||||
try {
|
||||
extractLinuxArtifact({
|
||||
artifactPath: appImagePath,
|
||||
format: 'appimage',
|
||||
destination: appImageDestination,
|
||||
runCommand: (command) => {
|
||||
assert.equal(command, 'unsquashfs');
|
||||
assert.equal(fs.existsSync(appImageDestination), false);
|
||||
appImageAttempt += 1;
|
||||
fs.mkdirSync(appImageDestination);
|
||||
return {
|
||||
status: appImageAttempt === 1 ? 1 : 0,
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.equal(appImageAttempt, 2);
|
||||
|
||||
extractLinuxArtifact({
|
||||
artifactPath: snapPath,
|
||||
format: 'snap',
|
||||
destination: snapDestination,
|
||||
runCommand: (command) => {
|
||||
assert.equal(command, 'unsquashfs');
|
||||
assert.equal(fs.existsSync(snapDestination), false);
|
||||
fs.mkdirSync(snapDestination);
|
||||
return { status: 0, stdout: '', stderr: '' };
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('initializes a user-checkout OSTree repository before importing Flatpak bundles', () => {
|
||||
const root = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'iptvnator-verifier-flatpak-repo-')
|
||||
);
|
||||
const artifactPath = path.join(root, 'IPTVnator.flatpak');
|
||||
const destination = path.join(root, 'flatpak-payload');
|
||||
const invocations = [];
|
||||
let initializedRepository = null;
|
||||
fs.writeFileSync(artifactPath, 'flatpak fixture');
|
||||
|
||||
try {
|
||||
extractLinuxArtifact({
|
||||
artifactPath,
|
||||
format: 'flatpak',
|
||||
destination,
|
||||
runCommand: (command, args) => {
|
||||
invocations.push([command, ...args]);
|
||||
if (command === 'ostree' && args.includes('init')) {
|
||||
initializedRepository = args
|
||||
.find((argument) => argument.startsWith('--repo='))
|
||||
?.slice('--repo='.length);
|
||||
assert.ok(args.includes('--mode=archive-z2'));
|
||||
return { status: 0, stdout: '', stderr: '' };
|
||||
}
|
||||
if (command === 'flatpak') {
|
||||
assert.equal(args[0], 'build-import-bundle');
|
||||
assert.equal(args[1], initializedRepository);
|
||||
if (!initializedRepository) {
|
||||
return {
|
||||
status: 1,
|
||||
stdout: '',
|
||||
stderr: 'error: opening repo: opendir(objects): No such file or directory',
|
||||
};
|
||||
}
|
||||
return { status: 0, stdout: '', stderr: '' };
|
||||
}
|
||||
if (command === 'ostree' && args[0] === 'refs') {
|
||||
return {
|
||||
status: 0,
|
||||
stdout: 'app/com.fourgray.iptvnator/x86_64/stable\n',
|
||||
stderr: '',
|
||||
};
|
||||
}
|
||||
if (command === 'ostree' && args[0] === 'checkout') {
|
||||
assert.ok(args.includes('-U'));
|
||||
return { status: 0, stdout: '', stderr: '' };
|
||||
}
|
||||
throw new Error(`Unexpected command: ${command}`);
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
invocations.map(([command, ...args]) => [
|
||||
command,
|
||||
args.find((argument) =>
|
||||
[
|
||||
'init',
|
||||
'build-import-bundle',
|
||||
'refs',
|
||||
'checkout',
|
||||
].includes(argument)
|
||||
),
|
||||
]),
|
||||
[
|
||||
['ostree', 'init'],
|
||||
['flatpak', 'build-import-bundle'],
|
||||
['ostree', 'refs'],
|
||||
['ostree', 'checkout'],
|
||||
]
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -602,6 +739,7 @@ test('validates an x64 system payload and executes one bounded helper probe', ()
|
||||
},
|
||||
environment: {
|
||||
PATH: '/usr/bin',
|
||||
LD_AUDIT: '/host/can-inject-audit.so',
|
||||
LD_LIBRARY_PATH: '/host/can-mask-missing-dependencies',
|
||||
LD_PRELOAD: '/host/can-inject.so',
|
||||
},
|
||||
@@ -976,6 +1114,7 @@ test('bundled probes use only the packaged library directory', () => {
|
||||
createRuntimeProbeEnvironment({
|
||||
environment: {
|
||||
PATH: '/usr/bin',
|
||||
LD_AUDIT: '/host/can-inject-audit.so',
|
||||
LD_LIBRARY_PATH: '/host/can-mask-missing-dependencies',
|
||||
LD_PRELOAD: '/host/can-inject.so',
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user