From 1073ce53507b62f0fb5d9f3975b0663be99972b3 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 14 Jun 2026 17:00:17 +0200 Subject: [PATCH] ci(electron): require embedded mpv in windows artifacts --- .github/workflows/build-and-make.yaml | 74 +++- apps/electron-backend/build-embedded-mpv.js | 17 +- .../native/src/embedded_mpv_win32.cc | 2 +- .../embedded-mpv-native-source.spec.ts | 5 +- .../embedded-mpv-native.service.spec.ts | 4 + .../services/embedded-mpv-native.service.ts | 4 + docs/architecture/embedded-mpv-native.md | 14 +- package.json | 1 + tools/embedded-mpv/README.md | 40 +- tools/embedded-mpv/stage-runtime.mjs | 8 +- .../stage-windows-runtime-archive.mjs | 355 ++++++++++++++++++ .../electron-package-identity.test.mjs | 136 +++++++ tools/packaging/embedded-mpv-packaging.cjs | 4 + tools/packaging/project.json | 7 +- 14 files changed, 648 insertions(+), 23 deletions(-) create mode 100644 tools/embedded-mpv/stage-windows-runtime-archive.mjs diff --git a/.github/workflows/build-and-make.yaml b/.github/workflows/build-and-make.yaml index 5df334f90..7b6a80ed6 100644 --- a/.github/workflows/build-and-make.yaml +++ b/.github/workflows/build-and-make.yaml @@ -46,7 +46,7 @@ jobs: embedded_mpv_arch: x64 embedded_mpv_build_runtime: false - os: windows - runner: windows-latest + runner: windows-2022 embedded_mpv_platform: win32 embedded_mpv_arch: x64 embedded_mpv_build_runtime: false @@ -109,6 +109,9 @@ jobs: if: matrix.embedded_mpv_platform && !startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'pull_request' || github.ref == 'refs/heads/master') id: embedded-mpv-runtime-cache-key shell: bash + env: + IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: ${{ vars.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || secrets.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || '' }} + IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: a26e28fa55c15ac5b6209d8a9f9c3c8cb39649631a53d3c4774beeae559a078c run: | set -euo pipefail @@ -150,6 +153,15 @@ jobs: `macos${safeDeploymentTarget}`, `xcode${hash(xcodeVersion)}` ); + } else if (targetPlatform === 'win32') { + sourceHashFiles.push('tools/embedded-mpv/stage-windows-runtime-archive.mjs'); + const windowsRuntimeSha256 = + process.env.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || + process.env.IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || + 'missing'; + cacheKeyParts.push( + `runtime${hash(windowsRuntimeSha256)}` + ); } const sourceHash = hash( sourceHashFiles.map((filePath) => fs.readFileSync(filePath)).join('\n') @@ -204,6 +216,37 @@ jobs: pnpm embedded-mpv:build-runtime -- "${{ matrix.embedded_mpv_arch }}" "${RUNTIME_PREFIX}" pnpm embedded-mpv:stage-runtime -- "${{ matrix.embedded_mpv_platform }}" "${{ matrix.embedded_mpv_arch }}" "${RUNTIME_PREFIX}" + - name: Stage Windows embedded MPV runtime archive + if: matrix.os == 'windows' && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true' + shell: bash + env: + IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL: ${{ vars.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL || secrets.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL || '' }} + IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: ${{ vars.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || secrets.IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 || '' }} + IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_URL: https://github.com/zhongfly/mpv-winbuild/releases/download/2026-06-14-7d245fd100/mpv-dev-lgpl-x86_64-20260614-git-7d245fd100.7z + IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256: a26e28fa55c15ac5b6209d8a9f9c3c8cb39649631a53d3c4774beeae559a078c + run: | + set -euo pipefail + + WINDOWS_RUNTIME_URL="${IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL}" + WINDOWS_RUNTIME_SHA256="${IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256}" + if [ -z "${WINDOWS_RUNTIME_URL}" ] && [ -z "${WINDOWS_RUNTIME_SHA256}" ]; then + case "${GITHUB_REF}" in + refs/tags/v*) + ;; + *) + WINDOWS_RUNTIME_URL="${IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_URL}" + WINDOWS_RUNTIME_SHA256="${IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256}" + ;; + esac + fi + + if [ -z "${WINDOWS_RUNTIME_URL}" ] || [ -z "${WINDOWS_RUNTIME_SHA256}" ]; then + echo "::error::Windows Embedded MPV CI requires IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL and IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 repository variables or secrets." + exit 1 + fi + + pnpm embedded-mpv:stage-runtime:windows-archive -- "${WINDOWS_RUNTIME_URL}" "${WINDOWS_RUNTIME_SHA256}" + - name: Stage Linux embedded MPV build inputs if: matrix.os == 'linux' shell: bash @@ -245,13 +288,13 @@ jobs: env: IPTVNATOR_EMBEDDED_MPV_PLATFORM: ${{ matrix.embedded_mpv_platform || '' }} IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.embedded_mpv_arch || matrix.arch || '' }} - IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (matrix.os == 'linux' || (matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master')) || (steps.embedded-mpv-runtime-cache.outputs.cache-hit == 'true')) && '1' || '0' }} + IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (matrix.os == 'linux' || matrix.os == 'windows' || (matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master'))) && '1' || '0' }} run: pnpm run build:backend - name: Verify embedded MPV build output # TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'` # after the macOS Embedded MPV artifacts are built and manually tested. - if: matrix.os == 'linux' || (matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master')) || steps.embedded-mpv-runtime-cache.outputs.cache-hit == 'true' + if: matrix.os == 'linux' || matrix.os == 'windows' || (matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master')) shell: bash run: | set -euo pipefail @@ -272,7 +315,7 @@ jobs: test -f dist/apps/electron-backend/native/lib/libmpv.2.dylib || test -f dist/apps/electron-backend/native/lib/libmpv.dylib ;; win32) - test -f dist/apps/electron-backend/native/lib/mpv-2.dll || test -f dist/apps/electron-backend/native/lib/mpv.dll + test -f dist/apps/electron-backend/native/lib/mpv-2.dll || test -f dist/apps/electron-backend/native/lib/libmpv-2.dll || test -f dist/apps/electron-backend/native/lib/mpv.dll || test -f dist/apps/electron-backend/native/lib/libmpv.dll ;; linux) node -e "const manifest = require('./dist/apps/electron-backend/native/embedded-mpv-runtime.json'); if (manifest.origin !== 'external-mpv-process') { throw new Error('Linux embedded MPV manifest must use external-mpv-process origin.'); }" @@ -321,6 +364,21 @@ jobs: fs.writeFileSync('electron-builder.json', JSON.stringify(pkg, null, 4)); " + - name: Override Windows arch in electron-builder.json + if: matrix.os == 'windows' + run: | + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('electron-builder.json', 'utf8')); + const targets = Array.isArray(pkg.win?.target) ? pkg.win.target : []; + for (const target of targets) { + if (target && typeof target === 'object') { + target.arch = ['${{ matrix.embedded_mpv_arch }}']; + } + } + fs.writeFileSync('electron-builder.json', JSON.stringify(pkg, null, 4) + '\n'); + " + - name: Validate macOS signing configuration if: matrix.os == 'macos' && github.event_name != 'pull_request' shell: bash @@ -471,23 +529,23 @@ jobs: IPTVNATOR_EMBEDDED_MPV_ARCH: ${{ matrix.embedded_mpv_arch || matrix.arch || '' }} # TEMPORARY PR TEST: change this back to '0' after manually # testing the macOS PR artifact with Embedded MPV included. - IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (matrix.os == 'linux' || (matrix.os == 'macos' && github.event_name == 'pull_request') || (steps.embedded-mpv-runtime-cache.outputs.cache-hit == 'true')) && '1' || '0' }} + IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (matrix.os == 'linux' || matrix.os == 'windows' || (matrix.os == 'macos' && github.event_name == 'pull_request')) && '1' || '0' }} run: pnpm run make:app - name: Verify packaged worker layout shell: bash env: PACKAGE_OS: ${{ matrix.os }} - PACKAGE_ARCH: ${{ matrix.arch || '' }} + PACKAGE_ARCH: ${{ matrix.arch || matrix.embedded_mpv_arch || '' }} # TEMPORARY ARTIFACT TEST: remove `|| github.event_name == 'pull_request' || github.ref == 'refs/heads/master'` # after the macOS Embedded MPV artifacts are built and manually tested. - IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (matrix.os == 'linux' || (matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master')) || (steps.embedded-mpv-runtime-cache.outputs.cache-hit == 'true')) && '1' || '0' }} + IPTVNATOR_REQUIRE_EMBEDDED_MPV: ${{ (matrix.os == 'linux' || matrix.os == 'windows' || (matrix.os == 'macos' && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'pull_request' || github.ref == 'refs/heads/master'))) && '1' || '0' }} run: pnpm run verify:package-layout -- "$PACKAGE_OS" "$PACKAGE_ARCH" - name: Save embedded MPV runtime cache # TEMPORARY ARTIFACT TEST: remove `|| github.ref == 'refs/heads/master'` # after the macOS Embedded MPV artifacts are built and manually tested. - if: matrix.embedded_mpv_build_runtime && !startsWith(github.ref, 'refs/tags/v') && github.repository == '4gray/iptvnator' && github.event_name != 'pull_request' && github.ref == 'refs/heads/master' && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true' + if: matrix.embedded_mpv_platform && matrix.os != 'linux' && !startsWith(github.ref, 'refs/tags/v') && github.repository == '4gray/iptvnator' && github.event_name != 'pull_request' && github.ref == 'refs/heads/master' && steps.embedded-mpv-runtime-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: path: | diff --git a/apps/electron-backend/build-embedded-mpv.js b/apps/electron-backend/build-embedded-mpv.js index 5a6e81f19..eb4b85706 100644 --- a/apps/electron-backend/build-embedded-mpv.js +++ b/apps/electron-backend/build-embedded-mpv.js @@ -59,7 +59,12 @@ function log(message) { function cleanOutput() { fs.rmSync(outputFile, { force: true }); fs.rmSync(outputLibDir, { recursive: true, force: true }); - for (const windowsDllName of ['mpv-2.dll', 'mpv.dll']) { + for (const windowsDllName of [ + 'mpv-2.dll', + 'libmpv-2.dll', + 'mpv.dll', + 'libmpv.dll', + ]) { fs.rmSync(path.join(outputDir, windowsDllName), { force: true }); } fs.rmSync(path.join(outputDir, '.deps'), { recursive: true, force: true }); @@ -129,7 +134,11 @@ function runtimeFilePredicate(filePath) { case 'darwin': return fileName.endsWith('.dylib'); case 'win32': - return fileName.endsWith('.dll') || fileName.endsWith('.lib'); + return ( + fileName.endsWith('.dll') || + fileName.endsWith('.lib') || + fileName.endsWith('.dll.a') + ); case 'linux': return /\.so(?:\.\d+)*$/.test(fileName); default: @@ -152,8 +161,12 @@ function findWindowsLibMpv(runtimeRoot) { for (const candidate of [ path.join(runtimeRoot, 'lib', 'mpv-2.dll'), path.join(runtimeRoot, 'bin', 'mpv-2.dll'), + path.join(runtimeRoot, 'lib', 'libmpv-2.dll'), + path.join(runtimeRoot, 'bin', 'libmpv-2.dll'), path.join(runtimeRoot, 'lib', 'mpv.dll'), path.join(runtimeRoot, 'bin', 'mpv.dll'), + path.join(runtimeRoot, 'lib', 'libmpv.dll'), + path.join(runtimeRoot, 'bin', 'libmpv.dll'), ]) { if (fileExists(candidate)) { return candidate; diff --git a/apps/electron-backend/native/src/embedded_mpv_win32.cc b/apps/electron-backend/native/src/embedded_mpv_win32.cc index 5b419bd5b..bd7b3faf1 100644 --- a/apps/electron-backend/native/src/embedded_mpv_win32.cc +++ b/apps/electron-backend/native/src/embedded_mpv_win32.cc @@ -158,7 +158,7 @@ private: windowClass.lpfnWndProc = &NativeVideoHost::windowProc; windowClass.hInstance = GetModuleHandleW(nullptr); windowClass.lpszClassName = windowClassName(); - windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); + windowClass.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512)); windowClass.hbrBackground = reinterpret_cast(GetStockObject(BLACK_BRUSH)); const ATOM classAtom = RegisterClassExW(&windowClass); diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts b/apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts index 68e13e601..0e4daffac 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native-source.spec.ts @@ -255,13 +255,16 @@ describe('Embedded MPV native source recording invariants', () => { it('copies Windows runtime DLLs next to the addon for Windows loader lookup', () => { expect(buildScriptSource).toContain( - "for (const windowsDllName of ['mpv-2.dll', 'mpv.dll'])" + "'libmpv-2.dll'," ); expect(buildScriptSource).toContain( 'path.join(outputDir, windowsDllName)' ); expect(buildScriptSource).toContain("fileName.endsWith('.dll')"); expect(buildScriptSource).toContain('path.join(outputDir, fileName)'); + expect(stageRuntimeSource).toContain( + "win32: ['mpv-2.dll', 'libmpv-2.dll', 'mpv.dll', 'libmpv.dll']" + ); }); it('checks Win32 window class registration failures explicitly', () => { diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts b/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts index 8044d9401..e07cd14ff 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native.service.spec.ts @@ -477,6 +477,10 @@ describe('EmbeddedMpvNativeService power blocker', () => { platform: 'win32' as NodeJS.Platform, runtimeFile: path.join('lib', 'mpv-2.dll'), }, + { + platform: 'win32' as NodeJS.Platform, + runtimeFile: path.join('lib', 'libmpv-2.dll'), + }, ])( 'loads the addon after validating the $platform runtime file exists', ({ platform, runtimeFile }) => { diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts b/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts index 94a4e5fff..b34485679 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts @@ -968,9 +968,13 @@ export class EmbeddedMpvNativeService { case 'win32': return [ path.join(nativeDir, 'lib', 'mpv-2.dll'), + path.join(nativeDir, 'lib', 'libmpv-2.dll'), path.join(nativeDir, 'mpv-2.dll'), + path.join(nativeDir, 'libmpv-2.dll'), path.join(nativeDir, 'lib', 'mpv.dll'), + path.join(nativeDir, 'lib', 'libmpv.dll'), path.join(nativeDir, 'mpv.dll'), + path.join(nativeDir, 'libmpv.dll'), ]; case 'linux': return []; diff --git a/docs/architecture/embedded-mpv-native.md b/docs/architecture/embedded-mpv-native.md index b34c10f24..08ba44f80 100644 --- a/docs/architecture/embedded-mpv-native.md +++ b/docs/architecture/embedded-mpv-native.md @@ -28,6 +28,12 @@ The build directory contains files such as `Makefile`, `binding.Makefile`, `conf The embedded player renders MPV frames into an app-owned native video surface. macOS uses the libmpv render API in an `NSOpenGLView` because the mpv `wid` path produced a black video surface inside Electron. Windows loads `libmpv` through the native Node addon and uses mpv's `wid` option against an IPTVnator-owned child `HWND`. Linux creates an IPTVnator-owned X11/Xwayland child `Window` and starts an out-of-process `mpv --wid=` instance for that child window. +Windows packaged runtimes must preserve the MPV DLL basename referenced by the +import library used at native-addon link time. For example, an archive that +ships `libmpv.dll.a` and `libmpv-2.dll` must package `libmpv-2.dll`; renaming it +to `mpv-2.dll` leaves `embedded_mpv.node` with an unresolved DLL dependency at +startup, so the Settings support probe hides the Embedded MPV option. + On Linux, `embedded_mpv.node` must not link directly to `libmpv` or load libmpv in-process. Electron loads its own `libffmpeg` and Chromium graphics stack; in-process libmpv can resolve FFmpeg/GL symbols against incompatible Electron symbols, while isolated dynamic-loader namespaces introduce thread/runtime ownership problems. The Linux addon therefore owns only the X11 child-window embedding, process lifecycle, and a private MPV JSON IPC socket. It starts `mpv --wid= --input-ipc-server=`, polls `time-pos`, `duration`, `volume`, and `pause`, and forwards pause/seek/volume/audio-track commands through that socket. The Linux MPV JSON IPC polling runs on an addon-owned background thread; `getSessionSnapshot()` returns the last cached snapshot and must not perform socket round trips on Electron's main thread. Linux MPV process teardown sends `SIGTERM` on the caller path, then waits and escalates to `SIGKILL` on a detached cleanup thread. A healthy Linux build lists X11/Xext as addon dependencies, but `ldd apps/electron-backend/native/build/Release/embedded_mpv.node` must not list `libmpv`. Runtime support also requires an `mpv` executable on `PATH`. Linux native Wayland embedding is not implemented. When Electron is started on Xwayland, the Linux backend also starts the child MPV process with `WAYLAND_DISPLAY` removed, `XDG_SESSION_TYPE=x11`, `--vo=gpu,x11`, and `--gpu-context=x11egl`. This prevents MPV from choosing a Wayland VO in a Wayland desktop session, which would ignore the X11 `--wid` target and open a separate top-level MPV window. @@ -185,7 +191,7 @@ Current development behavior: - The build script first looks for a staged runtime at `vendor/embedded-mpv/-/`. - The staged runtime/build inputs must contain `include/mpv/client.h` and `runtime-manifest.json`. macOS and Windows staging also contains the platform runtime files that are bundled into the app. - The compiled `.node` addon is copied into `dist/apps/electron-backend/native/embedded_mpv.node`. -- Bundled runtime files are copied into `dist/apps/electron-backend/native/lib/` for macOS and Windows. macOS copies `.dylib` and non-`.dylib` Mach-O dependencies; Windows copies `mpv-2.dll`/`mpv.dll` and import libraries. Linux writes an `external-mpv-process` manifest and intentionally leaves `libmpv.so` out of the package. +- Bundled runtime files are copied into `dist/apps/electron-backend/native/lib/` for macOS and Windows. macOS copies `.dylib` and non-`.dylib` Mach-O dependencies; Windows copies the staged `mpv-2.dll`/`libmpv-2.dll`/`mpv.dll`/`libmpv.dll` runtime name plus import libraries. Linux writes an `external-mpv-process` manifest and intentionally leaves `libmpv.so` out of the package. - Linux does not bundle or load `libmpv` in the Electron process. Its native addon still requires staged MPV headers, but runtime support depends on the X11/Xwayland window handle plus an `mpv` executable on `PATH`. - `afterPack` copies `dist/apps/electron-backend/native/` into `app.asar.unpacked/electron-backend/native/` on macOS, Windows, and Linux so the addon, manifest, and runtime libraries are filesystem-addressable. @@ -237,7 +243,7 @@ pnpm embedded-mpv:build-runtime -- arm64 /tmp/embedded-mpv-prefix pnpm embedded-mpv:stage-runtime -- darwin arm64 /tmp/embedded-mpv-prefix ``` -During temporary PR and `master` artifact testing, CI can restore an exact-keyed GitHub Actions cache for the staged `vendor/embedded-mpv/-/` runtime and skip the expensive source build where a source builder exists. The cache only contains `include/`, `lib/`, and `runtime-manifest.json`; it never contains the compiled `embedded_mpv.node` addon because that target depends on Electron headers, ABI, architecture, and build environment. Runtime cache entries are saved only from trusted repository refs, and tagged public macOS release builds continue to rebuild from pinned sources until a dedicated signed and attested runtime artifact flow exists. Windows and Linux currently use staged runtime cache inputs only; adding pinned source builders for those platforms is a separate release-hardening task. +During temporary PR and `master` artifact testing, CI can restore an exact-keyed GitHub Actions cache for the staged `vendor/embedded-mpv/-/` runtime and skip the expensive source build or archive staging path where one exists. The cache only contains `include/`, `lib/`, and `runtime-manifest.json`; it never contains the compiled `embedded_mpv.node` addon because that target depends on Electron headers, ABI, architecture, and build environment. Runtime cache entries are saved only from trusted repository refs, and tagged public macOS release builds continue to rebuild from pinned sources until a dedicated signed and attested runtime artifact flow exists. Windows CI uses a checksum-pinned `win32-x64` runtime archive configured through `IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL` and `IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256` repository variables or secrets on cache miss. Non-tag artifact builds have a pinned `zhongfly/mpv-winbuild` `mpv-dev-lgpl-x86_64` fallback so PR builds can produce a Windows embedded MPV artifact before repository variables are configured; tagged releases still require explicit repository configuration. The Windows archive helper accepts normal `lib/` + `bin/` prefixes and common `mpv-dev-lgpl` flat archives, including `libmpv-2.dll` names, and preserves the DLL basename expected by the import library; when the archive does not include `runtime-manifest.json`, it generates a minimal manifest from the archive URL/path and checksum. Linux stages Ubuntu package build inputs only; adding pinned source builders for Windows and Linux remains a separate release-hardening task. The CI builder pins FFmpeg `8.1`, mpv `0.41.0`, libplacebo `7.360.1`, libass `0.17.3`, FreeType `2.13.3`, FriBidi `1.0.16`, and HarfBuzz `8.5.0`. FFmpeg disables autodetected external libraries so Homebrew libraries cannot silently enter the runtime. Libplacebo is checked out from git with the submodules required by its Meson build because the generated GitHub archive does not include submodule contents. Even with Vulkan disabled, libplacebo still compiles Vulkan stubs and needs `3rdparty/Vulkan-Headers`. The generated manifest records source URLs, archive SHA-256 values where applicable, libplacebo git commit/submodule metadata, FFmpeg configure flags, and mpv Meson flags. The staging step normalizes macOS/Windows manifests to `origin: vendored-lgpl`, which release package validation requires on those platforms. @@ -281,7 +287,9 @@ For tagged macOS builds, CI must: - set `IPTVNATOR_EMBEDDED_MPV_ARCH=${arch}` for backend build and packaging - set `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` for packaging and package-layout verification -For Windows and Linux builds, CI must set `IPTVNATOR_EMBEDDED_MPV_PLATFORM`/`IPTVNATOR_EMBEDDED_MPV_ARCH` for native build and package verification. When an exact-keyed staged runtime cache is restored, the job can set `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` and verify the packaged `.dll` or `.so` runtime. Without a staged runtime, the build script writes an unavailable marker and Settings hides Embedded MPV. +For Windows builds, CI must restore the `win32-x64` staged runtime cache or stage the checksum-pinned runtime archive before `pnpm run build:backend`. The Windows job must set `IPTVNATOR_EMBEDDED_MPV_PLATFORM=win32`, `IPTVNATOR_EMBEDDED_MPV_ARCH=x64`, and `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` for backend build, package make, and package-layout verification. CI narrows `electron-builder.json` to x64 Windows targets while only a `win32-x64` runtime is available. The Windows job is pinned to `windows-2022` until the Electron `node-gyp` toolchain can identify Visual Studio 18 from `windows-latest`. + +For Linux builds, CI must set `IPTVNATOR_EMBEDDED_MPV_PLATFORM=linux`, `IPTVNATOR_EMBEDDED_MPV_ARCH=x64`, and `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` after staging the Ubuntu package build inputs. Linux package verification checks the `external-mpv-process` manifest and confirms that no bundled `libmpv.so` files are present. During temporary artifact tests, CI may also set `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` for PR and `master` push jobs where a runtime is known to exist. After the artifacts are manually validated, remove temporary conditions so ordinary development builds leave `IPTVNATOR_REQUIRE_EMBEDDED_MPV` unset or `0`. This keeps the native feature in-tree without making every non-release build depend on runtime artifacts. diff --git a/package.json b/package.json index 2c7475334..d97a26480 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "embedded-mpv:build-runtime": "node tools/embedded-mpv/build-macos-runtime.mjs", "embedded-mpv:stage-runtime": "node tools/embedded-mpv/stage-runtime.mjs", "embedded-mpv:stage-runtime:macos": "node tools/embedded-mpv/stage-macos-runtime.mjs", + "embedded-mpv:stage-runtime:windows-archive": "node tools/embedded-mpv/stage-windows-runtime-archive.mjs", "serve:backend:embedded-mpv": "pnpm embedded-mpv:build-native:homebrew && IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW=1 IPTVNATOR_ENABLE_EMBEDDED_MPV_EXPERIMENT=1 pnpm serve:backend", "package:app": "nx run electron-backend:make --prepackageOnly", "make:app": "nx run electron-backend:make", diff --git a/tools/embedded-mpv/README.md b/tools/embedded-mpv/README.md index 8440c41df..3e5b74dad 100644 --- a/tools/embedded-mpv/README.md +++ b/tools/embedded-mpv/README.md @@ -29,8 +29,8 @@ vendor/embedded-mpv/ runtime-manifest.json win32-x64/ include/mpv/client.h - lib/mpv-2.dll - lib/mpv.lib + lib/libmpv-2.dll # or mpv-2.dll/mpv.dll/libmpv.dll + lib/libmpv.dll.a # or mpv.lib/mpv-2.lib runtime-manifest.json linux-x64/ include/mpv/client.h @@ -60,7 +60,7 @@ pnpm embedded-mpv:stage-runtime:macos -- x64 /path/to/lgpl-prefix The prefix must contain `include/mpv/client.h` and the platform runtime/build files: - macOS: `lib/libmpv.2.dylib` or `lib/libmpv.dylib` plus all non-system dylib dependencies -- Windows: `lib/mpv.lib` or `lib/mpv-2.lib`, and `bin/mpv-2.dll` or `lib/mpv-2.dll` +- Windows: `lib/mpv.lib`, `lib/mpv-2.lib`, or `libmpv.dll.a`, and `bin/` or `lib/` containing `mpv-2.dll`, `libmpv-2.dll`, `mpv.dll`, or `libmpv.dll` - Linux: `include/mpv/client.h`; CI also records the `libmpv-dev` and `mpv` package versions used as build inputs. Linux runtime playback uses the system `mpv` executable and does not bundle `libmpv.so`. If the prefix contains `runtime-manifest.json`, the staging script copies its build metadata into the vendored manifest. At minimum, record: @@ -85,7 +85,37 @@ to be present. Linux playback does not load or bundle `libmpv` in the Electron process; the addon creates an X11 child window and starts a system `mpv --wid` process at runtime. -During temporary PR and `master` artifact testing, CI restores an exact-keyed GitHub Actions cache for the staged `vendor/embedded-mpv/-/` runtime before falling back to the macOS source build where available. The cache key includes the target platform, architecture, macOS deployment target, Xcode version when available, and hashes of the runtime build/staging scripts. Cache entries are saved only from trusted repository refs and are treated strictly as a speed optimization; tagged macOS release builds continue to rebuild from pinned sources unless a future signed and attested runtime artifact flow is introduced. +Windows CI does not build libmpv from source. It restores an exact-keyed cache +for `vendor/embedded-mpv/win32-x64/`; on cache miss it stages a checksum-pinned +LGPL-compatible archive from repository configuration: + +```text +IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL +IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256 +``` + +The values can be repository variables or secrets. Prefer variables when PR +artifact builds from same-repository branches should include Embedded MPV. For +non-tag artifact builds only, the workflow falls back to a checksum-pinned +`zhongfly/mpv-winbuild` `mpv-dev-lgpl-x86_64` archive when those variables are +unset. Tagged release builds must provide the repository configuration +explicitly. + +The Windows job is pinned to `windows-2022` while the current Electron +`node-gyp` toolchain cannot identify Visual Studio 18 from `windows-latest`. + +The archive must contain a Windows x64 prefix with `include/mpv/client.h`, a +libmpv import library, and `mpv-2.dll`/`mpv.dll` or +`libmpv-2.dll`/`libmpv.dll`. The archive can use either the normal prefix layout +(`lib/` and `bin/`) or the common `mpv-dev-lgpl` flat layout with the import +library and DLL in the archive root. The staged runtime preserves the DLL +basename from the archive because Windows import libraries encode the DLL name +that `embedded_mpv.node` must load at runtime. If +`runtime-manifest.json` is missing, CI generates a minimal manifest from the +archive URL/path and checksum; release-ready runtime archives should still +provide full source/build metadata. + +During temporary PR and `master` artifact testing, CI restores an exact-keyed GitHub Actions cache for the staged `vendor/embedded-mpv/-/` runtime before falling back to the macOS source build or Windows runtime archive where available. The cache key includes the target platform, architecture, macOS deployment target, Xcode version when available, a hash of the Windows runtime checksum when applicable, and hashes of the runtime build/staging scripts. Cache entries are saved only from trusted repository refs and are treated strictly as a speed optimization; tagged macOS release builds continue to rebuild from pinned sources unless a future signed and attested runtime artifact flow is introduced. The builder currently pins: @@ -112,7 +142,7 @@ The `afterPack` hook copies `dist/apps/electron-backend/native/` into `app.asar. During release packaging, `tools/packaging/electron-after-pack.cjs` verifies that macOS/Windows packages use a `vendored-lgpl` runtime/build input set. macOS artifacts additionally verify that Mach-O dependencies have no `/opt/homebrew` or `/usr/local` dynamic links for embedded MPV. Linux artifacts verify that the addon and `external-mpv-process` manifest are present, that no bundled `libmpv.so` files are present, and the runtime support check verifies that `mpv` is available on `PATH`. -Set `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` when packaging a release artifact that must include Embedded MPV. The same variable is temporarily enabled for macOS PR and `master` push artifacts while the bundled runtime is being tested. Linux CI packaging requires Embedded MPV after staging the Ubuntu package build inputs. Windows CI packaging requires Embedded MPV when an exact-keyed staged runtime cache is restored; otherwise the Windows job builds without the native addon and Settings keeps Embedded MPV hidden. +Set `IPTVNATOR_REQUIRE_EMBEDDED_MPV=1` when packaging a release artifact that must include Embedded MPV. The same variable is temporarily enabled for macOS PR and `master` push artifacts while the bundled runtime is being tested. Linux CI packaging requires Embedded MPV after staging the Ubuntu package build inputs. Windows CI packaging now requires Embedded MPV for x64 artifacts: the job restores the staged runtime cache or stages the checksum-pinned runtime archive, then fails backend build, package make, or package-layout verification if the addon/runtime is missing. ## Platform Notes diff --git a/tools/embedded-mpv/stage-runtime.mjs b/tools/embedded-mpv/stage-runtime.mjs index 35836cc5b..81b06e3fa 100644 --- a/tools/embedded-mpv/stage-runtime.mjs +++ b/tools/embedded-mpv/stage-runtime.mjs @@ -94,7 +94,7 @@ function copyDirectory(sourceDir, destinationDir, filter) { function findRuntimeFile(libDir) { const candidatesByPlatform = { darwin: ['libmpv.2.dylib', 'libmpv.dylib'], - win32: ['mpv-2.dll', 'mpv.dll'], + win32: ['mpv-2.dll', 'libmpv-2.dll', 'mpv.dll', 'libmpv.dll'], linux: ['libmpv.so.2', 'libmpv.so.1', 'libmpv.so'], }; const candidates = candidatesByPlatform[platform] ?? []; @@ -127,7 +127,11 @@ function runtimeFileFilter(_sourcePath, entry) { } if (platform === 'win32') { - return entry.name.endsWith('.dll') || entry.name.endsWith('.lib'); + return ( + entry.name.endsWith('.dll') || + entry.name.endsWith('.lib') || + entry.name.endsWith('.dll.a') + ); } if (platform === 'linux') { diff --git a/tools/embedded-mpv/stage-windows-runtime-archive.mjs b/tools/embedded-mpv/stage-windows-runtime-archive.mjs new file mode 100644 index 000000000..abd13e140 --- /dev/null +++ b/tools/embedded-mpv/stage-windows-runtime-archive.mjs @@ -0,0 +1,355 @@ +#!/usr/bin/env node +import crypto from 'crypto'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { spawnSync } from 'child_process'; +import { Readable } from 'stream'; +import { pipeline } from 'stream/promises'; + +const rawArgs = process.argv.slice(2); +const args = rawArgs[0] === '--' ? rawArgs.slice(1) : rawArgs; +const [archiveSource, rawExpectedSha256] = args; +const workspaceRoot = process.cwd(); +const windowsArch = 'x64'; + +if (!archiveSource || !rawExpectedSha256) { + console.error( + [ + 'Usage: node tools/embedded-mpv/stage-windows-runtime-archive.mjs ', + '', + 'Downloads or reads a checksum-pinned LGPL-compatible Windows libmpv runtime archive,', + 'extracts it, and stages it as vendor/embedded-mpv/win32-x64.', + '', + 'The archive must contain a prefix with:', + '- include/mpv/client.h', + '- mpv.lib, mpv-2.lib, or libmpv.dll.a in the prefix root or lib/', + '- mpv-2.dll, libmpv-2.dll, mpv.dll, or libmpv.dll in the prefix root, bin/, or lib/', + '- optional runtime-manifest.json with source/build metadata', + ].join('\n') + ); + process.exit(1); +} + +function log(message) { + process.stdout.write(`[embedded-mpv-windows-runtime] ${message}\n`); +} + +function isHttpsUrl(value) { + try { + const parsedUrl = new URL(value); + return parsedUrl.protocol === 'https:'; + } catch { + return false; + } +} + +function normalizeSha256(value) { + const normalized = value + .trim() + .toLowerCase() + .replace(/^sha256[:=\s-]*/, ''); + + if (!/^[a-f0-9]{64}$/.test(normalized)) { + throw new Error('Expected SHA-256 must be a 64-character hex digest.'); + } + + return normalized; +} + +async function sha256File(filePath) { + const hash = crypto.createHash('sha256'); + for await (const chunk of fs.createReadStream(filePath)) { + hash.update(chunk); + } + return hash.digest('hex'); +} + +function run(command, commandArgs, options = {}) { + log(`${command} ${commandArgs.join(' ')}`); + const result = spawnSync(command, commandArgs, { + cwd: options.cwd ?? workspaceRoot, + env: options.env ?? process.env, + stdio: 'inherit', + ...options, + }); + + if (result.status !== 0) { + throw new Error( + `${command} ${commandArgs.join(' ')} failed with status ${ + result.status ?? 1 + }.` + ); + } +} + +function runResult(command, commandArgs, options = {}) { + log(`${command} ${commandArgs.join(' ')}`); + return spawnSync(command, commandArgs, { + cwd: options.cwd ?? workspaceRoot, + env: options.env ?? process.env, + stdio: 'inherit', + ...options, + }); +} + +async function downloadArchive(sourceUrl, destinationPath) { + const response = await fetch(sourceUrl); + if (!response.ok || !response.body) { + throw new Error( + `Unable to download Windows embedded MPV runtime archive: ${response.status} ${response.statusText}` + ); + } + + await pipeline( + Readable.fromWeb(response.body), + fs.createWriteStream(destinationPath) + ); +} + +function archiveNameForSource(source) { + if (!isHttpsUrl(source)) { + return path.basename(source); + } + + const parsedUrl = new URL(source); + const archiveName = path.basename(parsedUrl.pathname); + return archiveName || 'windows-embedded-mpv-runtime.zip'; +} + +function findWindowsImportLibrary(prefix) { + return ['mpv.lib', 'mpv-2.lib', 'libmpv.dll.a'] + .flatMap((candidate) => [ + path.join(prefix, 'lib', candidate), + path.join(prefix, candidate), + ]) + .find((candidatePath) => fs.existsSync(candidatePath)); +} + +function findWindowsDll(prefix) { + return ['mpv-2.dll', 'libmpv-2.dll', 'mpv.dll', 'libmpv.dll'] + .flatMap((candidate) => [ + path.join(prefix, 'bin', candidate), + path.join(prefix, 'lib', candidate), + path.join(prefix, candidate), + ]) + .find((candidatePath) => fs.existsSync(candidatePath)); +} + +function hasRuntimePrefixLayout(candidateDir) { + return ( + fs.existsSync(path.join(candidateDir, 'include', 'mpv', 'client.h')) && + Boolean(findWindowsImportLibrary(candidateDir)) && + Boolean(findWindowsDll(candidateDir)) + ); +} + +function findRuntimePrefix(extractRoot) { + const queue = [{ directory: extractRoot, depth: 0 }]; + + while (queue.length > 0) { + const current = queue.shift(); + if (!current) { + continue; + } + + if (hasRuntimePrefixLayout(current.directory)) { + return current.directory; + } + + if (current.depth >= 5) { + continue; + } + + for (const entry of fs.readdirSync(current.directory, { + withFileTypes: true, + })) { + if (!entry.isDirectory()) { + continue; + } + + queue.push({ + directory: path.join(current.directory, entry.name), + depth: current.depth + 1, + }); + } + } + + return null; +} + +function copyDirectory(sourceDir, destinationDir) { + fs.mkdirSync(destinationDir, { recursive: true }); + fs.cpSync(sourceDir, destinationDir, { recursive: true }); +} + +function copyFile(sourcePath, destinationPath) { + fs.mkdirSync(path.dirname(destinationPath), { recursive: true }); + fs.copyFileSync(sourcePath, destinationPath); +} + +function writeGeneratedManifest(destinationPath, archiveSha256) { + const manifest = { + sourceDistribution: archiveSource, + archive: { + urlOrPath: archiveSource, + sha256: archiveSha256, + }, + ffmpeg: { + licensePolicy: + 'LGPL-compatible Windows runtime archive supplied to CI.', + configureFlags: + 'Record exact FFmpeg configure flags in the upstream runtime manifest when available.', + }, + mpv: { + licensePolicy: + 'LGPL-compatible libmpv Windows runtime archive supplied to CI.', + mesonFlags: + 'Record exact mpv Meson flags in the upstream runtime manifest when available.', + }, + }; + + fs.writeFileSync(destinationPath, `${JSON.stringify(manifest, null, 2)}\n`); +} + +function normalizeRuntimePrefix(runtimePrefix, tempRoot, archiveSha256) { + const normalizedPrefix = path.join(tempRoot, 'normalized-prefix'); + const importLibrary = findWindowsImportLibrary(runtimePrefix); + const runtimeDll = findWindowsDll(runtimePrefix); + + if (!importLibrary || !runtimeDll) { + throw new Error( + `Incomplete Windows embedded MPV runtime prefix: ${runtimePrefix}` + ); + } + + fs.rmSync(normalizedPrefix, { recursive: true, force: true }); + fs.mkdirSync(normalizedPrefix, { recursive: true }); + copyDirectory( + path.join(runtimePrefix, 'include', 'mpv'), + path.join(normalizedPrefix, 'include', 'mpv') + ); + copyFile( + importLibrary, + path.join(normalizedPrefix, 'lib', path.basename(importLibrary)) + ); + copyFile( + runtimeDll, + path.join(normalizedPrefix, 'bin', path.basename(runtimeDll)) + ); + + const sourceManifestPath = path.join( + runtimePrefix, + 'runtime-manifest.json' + ); + const normalizedManifestPath = path.join( + normalizedPrefix, + 'runtime-manifest.json' + ); + if (fs.existsSync(sourceManifestPath)) { + copyFile(sourceManifestPath, normalizedManifestPath); + } else { + writeGeneratedManifest(normalizedManifestPath, archiveSha256); + } + + return normalizedPrefix; +} + +function commandExists(command) { + const result = spawnSync( + process.platform === 'win32' ? 'where' : 'sh', + process.platform === 'win32' + ? [command] + : ['-lc', `command -v ${command}`], + { stdio: 'ignore' } + ); + return result.status === 0; +} + +function extractArchive(archivePath, extractRoot) { + const tarResult = runResult( + 'tar', + ['-xf', archivePath, '-C', extractRoot], + { + stdio: 'pipe', + } + ); + if (tarResult.status === 0) { + return; + } + + if (commandExists('7z')) { + run('7z', ['x', `-o${extractRoot}`, archivePath]); + return; + } + + throw new Error( + `Unable to extract ${archivePath}. Install tar with archive support or 7z.` + ); +} + +async function main() { + const expectedSha256 = normalizeSha256(rawExpectedSha256); + const tempRoot = fs.mkdtempSync( + path.join(os.tmpdir(), 'iptvnator-windows-embedded-mpv-') + ); + const archivePath = isHttpsUrl(archiveSource) + ? path.join(tempRoot, archiveNameForSource(archiveSource)) + : path.resolve(archiveSource); + const extractRoot = path.join(tempRoot, 'extract'); + + try { + if (isHttpsUrl(archiveSource)) { + log(`Downloading runtime archive from ${archiveSource}`); + await downloadArchive(archiveSource, archivePath); + } + + if (!fs.existsSync(archivePath)) { + throw new Error(`Runtime archive does not exist: ${archivePath}`); + } + + const actualSha256 = await sha256File(archivePath); + if (actualSha256 !== expectedSha256) { + throw new Error( + `Windows embedded MPV runtime checksum mismatch. Expected ${expectedSha256}, received ${actualSha256}.` + ); + } + + fs.mkdirSync(extractRoot, { recursive: true }); + extractArchive(archivePath, extractRoot); + + const runtimePrefix = findRuntimePrefix(extractRoot); + if (!runtimePrefix) { + throw new Error( + [ + 'Unable to find a Windows embedded MPV runtime prefix in the archive.', + 'Expected include/mpv/client.h, a Windows import library, and mpv-2.dll/libmpv-2.dll/mpv.dll/libmpv.dll.', + ].join('\n') + ); + } + const normalizedPrefix = normalizeRuntimePrefix( + runtimePrefix, + tempRoot, + expectedSha256 + ); + + run(process.execPath, [ + path.join( + workspaceRoot, + 'tools', + 'embedded-mpv', + 'stage-runtime.mjs' + ), + 'win32', + windowsArch, + normalizedPrefix, + ]); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/tools/packaging/electron-package-identity.test.mjs b/tools/packaging/electron-package-identity.test.mjs index 43965f45e..1098ef404 100644 --- a/tools/packaging/electron-package-identity.test.mjs +++ b/tools/packaging/electron-package-identity.test.mjs @@ -11,6 +11,10 @@ const require = createRequire(import.meta.url); const packageMetadata = JSON.parse( fs.readFileSync(join(currentDir, '..', '..', 'package.json'), 'utf8') ); +const buildAndMakeWorkflow = fs.readFileSync( + join(currentDir, '..', '..', '.github', 'workflows', 'build-and-make.yaml'), + 'utf8' +); const electronBuilderConfig = JSON.parse( fs.readFileSync( join(currentDir, '..', '..', 'electron-builder.json'), @@ -56,6 +60,42 @@ const electronAfterPackSource = fs.readFileSync( join(currentDir, 'electron-after-pack.cjs'), 'utf8' ); +const embeddedMpvPackagingSource = fs.readFileSync( + join(currentDir, 'embedded-mpv-packaging.cjs'), + 'utf8' +); +const embeddedMpvBuildSource = fs.readFileSync( + join( + currentDir, + '..', + '..', + 'apps', + 'electron-backend', + 'build-embedded-mpv.js' + ), + 'utf8' +); +const embeddedMpvStageRuntimeSource = fs.readFileSync( + join(currentDir, '..', 'embedded-mpv', 'stage-runtime.mjs'), + 'utf8' +); +const embeddedMpvWindowsArchiveStageSource = fs.readFileSync( + join(currentDir, '..', 'embedded-mpv', 'stage-windows-runtime-archive.mjs'), + 'utf8' +); +const embeddedMpvWin32Source = fs.readFileSync( + join( + currentDir, + '..', + '..', + 'apps', + 'electron-backend', + 'native', + 'src', + 'embedded_mpv_win32.cc' + ), + 'utf8' +); const { validatePackagedEmbeddedMpv } = require('./embedded-mpv-packaging.cjs'); test('Linux package identity does not expose the internal Electron backend project name', () => { @@ -197,7 +237,9 @@ test('embedded MPV package validation accepts Windows runtime files and Linux pr try { for (const [platform, runtimeFile] of [ ['windows', 'mpv-2.dll'], + ['windows', 'libmpv-2.dll'], ['windows', join('lib', 'mpv.dll')], + ['windows', join('lib', 'libmpv.dll')], ]) { const resourceDir = join(tempDir, platform); const nativeDir = join( @@ -249,6 +291,46 @@ test('embedded MPV package validation accepts Windows runtime files and Linux pr } }); +test('Windows embedded MPV staging preserves import-library DLL basenames', () => { + assert.match( + embeddedMpvStageRuntimeSource, + /win32:\s*\[\s*'mpv-2\.dll',\s*'libmpv-2\.dll',\s*'mpv\.dll',\s*'libmpv\.dll'\s*\]/ + ); + assert.match( + embeddedMpvWindowsArchiveStageSource, + /path\.join\(normalizedPrefix,\s*'bin',\s*path\.basename\(runtimeDll\)\)/ + ); + assert.doesNotMatch( + embeddedMpvWindowsArchiveStageSource, + /normalizedWindowsDllName/ + ); + assert.match(embeddedMpvBuildSource, /'libmpv-2\.dll'/); + assert.match(embeddedMpvPackagingSource, /libmpv-2\.dll/); +}); + +test('Windows embedded MPV archive staging keeps CI downloads bounded and quiet', () => { + assert.match( + embeddedMpvWindowsArchiveStageSource, + /parsedUrl\.protocol === 'https:'/ + ); + assert.doesNotMatch( + embeddedMpvWindowsArchiveStageSource, + /parsedUrl\.protocol === 'http:'/ + ); + assert.match( + embeddedMpvWindowsArchiveStageSource, + /fs\.createReadStream\(filePath\)/ + ); + assert.doesNotMatch( + embeddedMpvWindowsArchiveStageSource, + /hash\.update\(fs\.readFileSync\(filePath\)\)/ + ); + assert.match( + embeddedMpvWindowsArchiveStageSource, + /runResult\(\s*'tar',\s*\['-xf', archivePath, '-C', extractRoot\],\s*\{\s*stdio: 'pipe',\s*\}\s*\)/s + ); +}); + test('embedded MPV packaging helpers use a cross-platform module name', () => { assert.match( electronAfterPackSource, @@ -260,6 +342,60 @@ test('embedded MPV packaging helpers use a cross-platform module name', () => { ); }); +test('Windows CI packages embedded MPV from a staged x64 runtime', () => { + const requireEmbeddedMpvLines = buildAndMakeWorkflow + .split(/\r?\n/) + .filter((line) => line.includes('IPTVNATOR_REQUIRE_EMBEDDED_MPV:')); + + assert.equal( + packageMetadata.scripts?.['embedded-mpv:stage-runtime:windows-archive'], + 'node tools/embedded-mpv/stage-windows-runtime-archive.mjs' + ); + assert.match( + buildAndMakeWorkflow, + /name:\s+Stage Windows embedded MPV runtime archive/ + ); + assert.match(buildAndMakeWorkflow, /runner:\s+windows-2022/); + assert.match( + buildAndMakeWorkflow, + /IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL/ + ); + assert.match( + buildAndMakeWorkflow, + /IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256/ + ); + assert.match( + buildAndMakeWorkflow, + /IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_URL: https:\/\/github\.com\/zhongfly\/mpv-winbuild\/releases\/download\// + ); + assert.match(buildAndMakeWorkflow, /refs\/tags\/v\*/); + assert.match( + buildAndMakeWorkflow, + /name:\s+Override Windows arch in electron-builder\.json/ + ); + assert.match( + buildAndMakeWorkflow, + /IPTVNATOR_REQUIRE_EMBEDDED_MPV:\s+\$\{\{\s*\(matrix\.os == 'linux' \|\| matrix\.os == 'windows'/ + ); + assert.match(embeddedMpvStageRuntimeSource, /\.dll\.a/); + assert.match(embeddedMpvBuildSource, /\.dll\.a/); + assert.ok(requireEmbeddedMpvLines.length > 0); + for (const line of requireEmbeddedMpvLines) { + assert.doesNotMatch(line, /cache-hit/); + } +}); + +test('Windows embedded MPV native build uses wide Win32 cursor resources', () => { + assert.match( + embeddedMpvWin32Source, + /LoadCursorW\(nullptr,\s*MAKEINTRESOURCEW\(32512\)\)/ + ); + assert.doesNotMatch( + embeddedMpvWin32Source, + /LoadCursorW\(nullptr,\s*IDC_ARROW\)/ + ); +}); + test('embedded MPV package validation rejects missing required Windows runtime', () => { const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-')); diff --git a/tools/packaging/embedded-mpv-packaging.cjs b/tools/packaging/embedded-mpv-packaging.cjs index 42d2c1095..11fcc6dd8 100644 --- a/tools/packaging/embedded-mpv-packaging.cjs +++ b/tools/packaging/embedded-mpv-packaging.cjs @@ -428,9 +428,13 @@ function getPackagedRuntimeCandidates(libDir, platform, nativeDir) { case 'win32': return [ nativeDir ? path.join(nativeDir, 'mpv-2.dll') : null, + nativeDir ? path.join(nativeDir, 'libmpv-2.dll') : null, nativeDir ? path.join(nativeDir, 'mpv.dll') : null, + nativeDir ? path.join(nativeDir, 'libmpv.dll') : null, path.join(libDir, 'mpv-2.dll'), + path.join(libDir, 'libmpv-2.dll'), path.join(libDir, 'mpv.dll'), + path.join(libDir, 'libmpv.dll'), ].filter(Boolean); case 'linux': return []; diff --git a/tools/packaging/project.json b/tools/packaging/project.json index 6cbc84e1e..d9e35412c 100644 --- a/tools/packaging/project.json +++ b/tools/packaging/project.json @@ -11,9 +11,14 @@ "default", "{workspaceRoot}/package.json", "{workspaceRoot}/electron-builder.json", + "{workspaceRoot}/.github/workflows/build-and-make.yaml", + "{workspaceRoot}/apps/electron-backend/build-embedded-mpv.js", "{workspaceRoot}/apps/electron-backend/project.json", + "{workspaceRoot}/apps/electron-backend/native/src/embedded_mpv_win32.cc", "{workspaceRoot}/apps/electron-backend/src/app/options/maker.options.json", - "{workspaceRoot}/tools/packaging/generate-electron-builder-metadata.mjs" + "{workspaceRoot}/tools/packaging/generate-electron-builder-metadata.mjs", + "{workspaceRoot}/tools/embedded-mpv/stage-runtime.mjs", + "{workspaceRoot}/tools/embedded-mpv/stage-windows-runtime-archive.mjs" ], "options": { "command": "node --test tools/packaging/electron-package-identity.test.mjs",