Skip to content

Commit 4e7a85b

Browse files
committed
fix(win): ship the compositor addon beside its ffmpeg DLLs
The 1.9.0 Store build loads no compositor at all: the editor opens with a permanently blank preview while audio keeps playing. Audio comes from the renderer, every frame comes from the addon, so the symptom is exactly what an addon that failed to load produces — the service falls back to a silent no-op. The addon dlopens avcodec/avformat/avutil at require() time. It shipped from app.asar.unpacked, one directory away from electron/native/bin/win32-x64/*.dll, and the gap was bridged at runtime by ensureFfmpegSharedDllsOnPath prepending the DLL directory to PATH. That works for NSIS and does not work under MSIX, which resolves an addon's dependent DLLs through the package graph and ignores PATH. Measured inside a registered package, with the directory verifiably present and correctly prepended: dllDir existsSync : true require BEFORE PATH : FAILED: The specified module could not be found. require AFTER PATH : FAILED: The specified module could not be found. and with the addon beside those same DLLs, no PATH involved: require BEFORE PATH : LOADED OK Node loads .node files with LOAD_WITH_ALTERED_SEARCH_PATH, so the addon's own directory is searched for its dependencies. Colocating removes the PATH mechanism rather than repairing it, and works on every Windows packaging format. macOS has always done this; Windows was the outlier. No loader change: buildCandidatePaths already probes bin/<tag> first. before-pack now refuses to package unless the addon and each of avcodec, avformat and avutil are in that directory, on Windows as it already did on macOS and Linux. One requirement per library rather than a count over a combined regex — the trap LINUX_REQUIRED already documents, where several versioned copies of one library satisfy the count while another is missing.
1 parent aacdefb commit 4e7a85b

4 files changed

Lines changed: 146 additions & 12 deletions

File tree

electron-builder.json5

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -192,16 +192,21 @@
192192
"filter": ["win32-*/*", "!win32-*/ffmpeg.exe"]
193193
}
194194
],
195-
// Native D3D11 compositor addon (Windows-only). Built by
196-
// `npm run build:native:compositor` (scripts/build-windows-compositor-addon.mjs)
197-
// before packaging. Packed into asar, then auto-unpacked by the top-level
198-
// `asarUnpack: ["**/*.node"]` rule to app.asar.unpacked/... — matches the
199-
// `.asar` -> `.asar.unpacked` rewrite compositorViewService.ts's
200-
// buildCandidatePaths() already applies for packaged builds. Appends to
201-
// (doesn't replace) the top-level `files` list.
202-
"files": [
203-
"electron/native/compositor-view/build"
204-
]
195+
// The D3D11 compositor addon deliberately does NOT travel through `files` into
196+
// app.asar. It ships with the ffmpeg DLLs it links against, via the
197+
// `extraResources` entry above, because it has to sit in the SAME directory as
198+
// them — exactly like the Metal addon on macOS.
199+
//
200+
// It used to be listed here and unpacked to app.asar.unpacked/…, one directory
201+
// away from `electron/native/bin/win32-x64/*.dll`. That only ever worked because
202+
// `ensureFfmpegSharedDllsOnPath` prepends the DLL directory to PATH before the
203+
// require. MSIX ignores PATH when resolving dependent DLLs — it resolves through
204+
// the package graph — so the Store build of 1.9.0 loaded no compositor at all and
205+
// the editor ran with no preview while audio kept playing.
206+
//
207+
// Node loads .node files with LOAD_WITH_ALTERED_SEARCH_PATH, so the addon's own
208+
// directory is searched for its dependencies; colocation makes the load work with
209+
// no PATH at all, on every Windows packaging format.
205210
},
206211
"nsis": {
207212
"oneClick": false,

scripts/before-pack.cjs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,56 @@ function checkNativePayload({ dir, required, osLabel, bundleNoun, emptyDirFix })
202202
);
203203
}
204204

205+
/**
206+
* The Windows addon must sit in the SAME directory as the ffmpeg DLLs it links
207+
* against, because that is the only arrangement that loads under MSIX.
208+
*
209+
* The addon dlopens avcodec/avformat/avutil at require() time. While it shipped from
210+
* app.asar.unpacked — one directory away from the DLLs — loading it depended on
211+
* `ensureFfmpegSharedDllsOnPath` prepending their directory to PATH. That works for
212+
* the NSIS installer and does not work under MSIX, which resolves dependent DLLs
213+
* through the package graph and ignores PATH. Measured inside a registered package,
214+
* with the directory correctly on PATH: `require` failed both before and after the
215+
* PATH was set; with the addon beside its DLLs it loaded with no PATH at all.
216+
*
217+
* That shipped as 1.9.0 on the Store: no compositor loaded, so the editor showed no
218+
* preview at all while audio kept playing — and it looked like an app bug, not a
219+
* packaging one, because every file was present and the NSIS build of the same commit
220+
* was fine.
221+
*
222+
* `win.extraResources` ships this directory wholesale (filter `win32-*​/*`), so
223+
* "together here" is the same thing as "together in the installed app".
224+
*/
225+
const WIN_REQUIRED = [
226+
{
227+
match: (name) => name === "compositor_view.node",
228+
what: "the D3D11 compositor addon",
229+
breaks: "the preview renders nothing and every export falls back to the no-op compositor",
230+
fix: FIX,
231+
},
232+
// One requirement per library, not `atLeast: 3` over a combined regex — the same
233+
// trap LINUX_REQUIRED documents above. Several versioned copies of one library
234+
// (avcodec-60/61/62.dll left by an earlier fetch) would satisfy a combined count
235+
// while another library was missing entirely, and the addon would still fail to
236+
// load.
237+
...["avcodec", "avformat", "avutil"].map((library) => ({
238+
match: (name) => new RegExp(`^${library}-\\d+\\.dll$`).test(name),
239+
what: `the ${library} DLL the compositor links`,
240+
breaks: "the addon cannot be loaded at all under MSIX, which ignores PATH",
241+
fix: "Fetch them with:\n\n npm run fetch:ffmpeg",
242+
})),
243+
];
244+
245+
function checkWinNativePayload() {
246+
checkNativePayload({
247+
dir: path.join(ROOT, "electron", "native", "bin", "win32-x64"),
248+
required: WIN_REQUIRED,
249+
osLabel: "Windows",
250+
bundleNoun: "the installer",
251+
emptyDirFix: `${FIX}\n\nThe STT helper and the capture helper are separate builds — see\ntechnical-documentation/engineering/build-and-packaging.md.`,
252+
});
253+
}
254+
205255
function checkMacNativePayload(context) {
206256
checkNativePayload({
207257
dir: path.join(ROOT, "electron", "native", "bin", `darwin-${archTagFor(context)}`),
@@ -313,7 +363,19 @@ function checkCompositorAddonFreshness(
313363
exports.default = async function beforePack(context) {
314364
const platform = context?.electronPlatformName ?? process.platform;
315365
if (platform === "win32") {
316-
checkCompositorAddonFreshness();
366+
// The copy that ships is the arch-tagged one under electron/native/bin/
367+
// (win.extraResources), beside its ffmpeg DLLs — not the dev copy this hook
368+
// used to be the sole guardian of. Same reasoning as the darwin branch below.
369+
const shipped = path.join(
370+
ROOT,
371+
"electron",
372+
"native",
373+
"bin",
374+
"win32-x64",
375+
"compositor_view.node",
376+
);
377+
checkWinNativePayload();
378+
checkCompositorAddonFreshness(shipped, FIX, "D3D11");
317379
return;
318380
}
319381
if (platform === "darwin") {
@@ -376,6 +438,20 @@ if (require.main === module) {
376438
HELPER_SOURCE_PATHS,
377439
);
378440
console.log(`Linux native payload complete in electron/native/bin/${tag}, addon up to date.`);
441+
} else if (process.platform === "win32") {
442+
const shipped = path.join(
443+
ROOT,
444+
"electron",
445+
"native",
446+
"bin",
447+
"win32-x64",
448+
"compositor_view.node",
449+
);
450+
checkWinNativePayload();
451+
checkCompositorAddonFreshness(shipped, FIX, "D3D11");
452+
console.log(
453+
"Windows native payload complete in electron/native/bin/win32-x64 (addon beside its ffmpeg DLLs), addon up to date.",
454+
);
379455
} else {
380456
checkCompositorAddonFreshness();
381457
console.log("compositor addon is up to date with its Rust sources.");

scripts/build-windows-compositor-addon.mjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,34 @@ fs.mkdirSync(BUILD_OUT_DIR, { recursive: true });
9595
const dest = path.join(BUILD_OUT_DIR, "compositor_view.node");
9696
fs.copyFileSync(builtDll, dest);
9797

98+
// Also install next to the ffmpeg DLLs the addon links against, which is the copy
99+
// that actually ships (win `extraResources`, filter `win32-*/*`). macOS has always
100+
// done this — see build-macos-compositor-addon.mjs's archBinDir — and Windows not
101+
// doing it is what broke the Store build of 1.9.0:
102+
//
103+
// The addon dlopens avcodec/avformat/avutil at require() time. Shipped from inside
104+
// app.asar.unpacked it sat in a different directory from those DLLs, so loading it
105+
// depended on `ensureFfmpegSharedDllsOnPath` prepending their directory to PATH.
106+
// That works for the NSIS installer and does NOT work under MSIX: a packaged app
107+
// resolves dependent DLLs through its package graph and ignores PATH. Measured
108+
// inside a registered package, with the directory correctly on PATH:
109+
//
110+
// require BEFORE PATH: FAILED: The specified module could not be found.
111+
// require AFTER PATH: FAILED: The specified module could not be found.
112+
//
113+
// and with the addon sitting beside its DLLs, no PATH involved:
114+
//
115+
// require BEFORE PATH: LOADED OK
116+
//
117+
// Node loads .node files with LOAD_WITH_ALTERED_SEARCH_PATH, so the addon's own
118+
// directory is searched for its dependencies. Colocating removes the PATH mechanism
119+
// rather than repairing it. `buildCandidatePaths` already probes this location
120+
// first, so no loader change is needed.
121+
const archBinDir = path.join(ROOT, "electron", "native", "bin", "win32-x64");
122+
fs.mkdirSync(archBinDir, { recursive: true });
123+
const archDest = path.join(archBinDir, "compositor_view.node");
124+
fs.copyFileSync(builtDll, archDest);
125+
98126
console.log(`Built ${builtDll}`);
99127
console.log(`Copied ${dest}`);
128+
console.log(`Copied ${archDest}`);

technical-documentation/engineering/build-and-packaging.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,31 @@ A usable full package depends on generated artifacts that are not committed:
3636
| Native Metal compositor addon | `electron/native/bin/darwin-<arch>/compositor_view.node` (plus a dev copy under `electron/native/compositor-view/build/`) | Rust, Xcode, and the LGPL FFmpeg tree from `fetch:ffmpeg:mac` |
3737
| FFmpeg runtime files | matching `electron/native/bin/<platform>-<arch>/` directory | Downloaded by `fetch:ffmpeg` on Windows; **built from source** by `fetch:ffmpeg:mac` on macOS (~5 min) — BtbN publishes no macOS target and every circulating macOS build is GPL, which would relicense this MIT app |
3838

39-
Electron-builder copies only the matching `electron/native/bin/<platform>-<arch>/` directory into each package. The compositor `.node` file is included by the Windows `files` rule and unpacked from ASAR because native addons cannot be loaded from inside the archive.
39+
Electron-builder copies only the matching `electron/native/bin/<platform>-<arch>/` directory into each package. On both Windows and macOS the compositor `.node` ships from inside that directory, beside the ffmpeg libraries it links against, and never travels through ASAR.
40+
41+
### The compositor addon must sit beside its ffmpeg libraries
42+
43+
This is a hard requirement on Windows, not a tidiness preference.
44+
45+
The addon dlopens `avcodec`/`avformat`/`avutil` at `require()` time. Until 1.9.0 the Windows build shipped it inside `app.asar.unpacked/electron/native/compositor-view/build/`, one directory away from `electron/native/bin/win32-x64/*.dll`, and the gap was bridged at runtime by `ensureFfmpegSharedDllsOnPath` prepending the DLL directory to `PATH` before the require.
46+
47+
That works for the NSIS installer. **It does not work under MSIX**, which resolves an addon's dependent DLLs through the package graph and ignores `PATH`. Measured inside a registered package, with the directory verifiably present and correctly prepended to `PATH`:
48+
49+
```
50+
dllDir existsSync : true
51+
require BEFORE PATH : FAILED: The specified module could not be found.
52+
require AFTER PATH : FAILED: The specified module could not be found.
53+
```
54+
55+
and with the addon sitting beside those same DLLs, no `PATH` involved:
56+
57+
```
58+
require BEFORE PATH : LOADED OK
59+
```
60+
61+
Node loads `.node` files with `LOAD_WITH_ALTERED_SEARCH_PATH`, so the addon's own directory is searched for its dependencies. Colocating removes the `PATH` mechanism rather than repairing it, and works on every Windows packaging format.
62+
63+
This shipped: the 1.9.0 Store build loaded no compositor at all, so the editor opened with a permanently blank preview while audio kept playing — audio comes from the renderer, every frame comes from the addon. It read as an application bug rather than a packaging one, because every file was present in the package and the NSIS build of the same commit was fine. `scripts/before-pack.cjs` now refuses to package unless the addon and at least `avcodec`/`avformat`/`avutil` are in the same directory, on Windows as it already did on macOS.
4064

4165
`electron/native/bin/`, local native build directories, the compositor build output, models, and caches are gitignored. Rebuilding from a source checkout therefore requires the complete platform toolchain and third-party SDKs; running the generic `npm run build` alone does not manufacture missing native artifacts. The Windows compositor's D3D11/FFmpeg prerequisites are described by the source POC in `crates/README.md`, while capture helper lookup and output conventions are documented in `electron/native/README.md`.
4266

0 commit comments

Comments
 (0)