Skip to content

Commit 078bc3b

Browse files
committed
fix: locate bundled lame binary in flattened dist builds
1 parent 38fc124 commit 078bc3b

File tree

9 files changed

+401
-24
lines changed

9 files changed

+401
-24
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ jobs:
4343
- name: Build package
4444
run: pnpm run build
4545

46+
- name: Ensure bundled LAME binary
47+
env:
48+
LAME_FORCE_DOWNLOAD: "1"
49+
run: node ./scripts/install-lame.mjs
50+
51+
- name: Diagnose bundled LAME binary
52+
run: node ./scripts/diagnose-lame.mjs
53+
4654
- name: Run examples
4755
run: |
4856
pnpm run example:wav-to-mp3

AI_AGENT_INSTRUCTIONS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,4 @@ You operate as a senior Node.js and TypeScript backend engineer focused on maint
133133
- Document behavioural changes in commit messages following Conventional Commit rules so the release tooling can derive `CHANGELOG.md` for future releases.
134134
- Manual edits to `CHANGELOG.md` are only allowed for the 2.0.0 release; subsequent entries must come from the automated workflow.
135135
- After finishing any feature implementation, include in your final response a Conventional Commit-style message suggestion that downstream tooling can use.
136+
- When referencing Node.js types, import them explicitly (e.g. `import type { ProcessEnv } from "node:process";`) instead of relying on the `NodeJS.*` namespace to keep ESLint satisfied.

scripts/diagnose-lame.mjs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env node
2+
3+
import { existsSync, readdirSync, statSync } from "node:fs";
4+
import { spawnSync } from "node:child_process";
5+
import { platform } from "node:os";
6+
7+
const {
8+
resolveLameBinary,
9+
resolveBundledLibraryDirectory,
10+
} = await import(new URL("../dist/index.cjs", import.meta.url));
11+
12+
function logSection(title) {
13+
console.log(`[lame-diagnostics] ${title}`);
14+
}
15+
16+
const binaryPath = resolveLameBinary();
17+
logSection(`Resolved binary: ${binaryPath}`);
18+
19+
try {
20+
const stats = statSync(binaryPath);
21+
logSection(
22+
`File stats -> size=${stats.size} mode=${stats.mode.toString(8)} mtime=${stats.mtime.toISOString()}`,
23+
);
24+
} catch (error) {
25+
logSection(`stat() failed: ${error instanceof Error ? error.message : error}`);
26+
}
27+
28+
const runCommand = (cmd, args) => {
29+
const result = spawnSync(cmd, args, {
30+
encoding: "utf-8",
31+
});
32+
33+
logSection(
34+
`${cmd} ${args.join(" ")} exited ${result.status} (signal: ${result.signal ?? "none"})`,
35+
);
36+
37+
if (result.stdout) {
38+
console.log(result.stdout.trim());
39+
}
40+
41+
if (result.stderr) {
42+
console.error(result.stderr.trim());
43+
}
44+
};
45+
46+
runCommand(binaryPath, ["--version"]);
47+
48+
if (platform() === "linux") {
49+
runCommand("ldd", [binaryPath]);
50+
}
51+
52+
const libraryDir = resolveBundledLibraryDirectory();
53+
logSection(
54+
`Bundled library directory: ${libraryDir ?? "not found (not expected on this platform?)"}`,
55+
);
56+
if (libraryDir && existsSync(libraryDir)) {
57+
const entries = readdirSync(libraryDir);
58+
if (entries.length === 0) {
59+
logSection("Bundled library directory is empty");
60+
} else {
61+
logSection(
62+
`Bundled libraries:\n${entries.map((entry) => ` - ${entry}`).join("\n")}`,
63+
);
64+
}
65+
}

scripts/install-lame.mjs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import {
22
chmodSync,
33
copyFileSync,
4+
cpSync,
45
existsSync,
56
mkdirSync,
67
mkdtempSync,
78
readdirSync,
9+
rmSync,
810
statSync,
11+
writeFileSync,
912
} from "node:fs";
1013
import { createWriteStream } from "node:fs";
1114
import { pipeline } from "node:stream/promises";
@@ -29,6 +32,9 @@ const INSTALL_BASE = join(
2932
`${PLATFORM}-${ARCH}`,
3033
);
3134
const TARGET_BINARY = join(INSTALL_BASE, `lame${EXECUTABLE_SUFFIX}`);
35+
const LIB_DIRECTORY = join(INSTALL_BASE, "lib");
36+
const LIB_DIRECTORY_MARKER = join(LIB_DIRECTORY, ".installed");
37+
const LIBSNDFILE_VERSION = "1.2.0-1+deb12u1";
3238

3339
const DOWNLOAD_SOURCES = {
3440
"linux-x64": [
@@ -97,6 +103,45 @@ const DOWNLOAD_SOURCES = {
97103
],
98104
};
99105

106+
const LINUX_SHARED_LIBRARY_PACKAGES = {
107+
"linux-x64": [
108+
{
109+
name: "libmp3lame0",
110+
url: `https://deb.debian.org/debian/pool/main/l/lame/libmp3lame0_${LAME_VERSION}-6_amd64.deb`,
111+
libraryRoot: "usr/lib/x86_64-linux-gnu",
112+
},
113+
{
114+
name: "libsndfile1",
115+
url: `https://deb.debian.org/debian/pool/main/libs/libsndfile/libsndfile1_${LIBSNDFILE_VERSION}_amd64.deb`,
116+
libraryRoot: "usr/lib/x86_64-linux-gnu",
117+
},
118+
],
119+
"linux-arm64": [
120+
{
121+
name: "libmp3lame0",
122+
url: `https://deb.debian.org/debian/pool/main/l/lame/libmp3lame0_${LAME_VERSION}-6_arm64.deb`,
123+
libraryRoot: "usr/lib/aarch64-linux-gnu",
124+
},
125+
{
126+
name: "libsndfile1",
127+
url: `https://deb.debian.org/debian/pool/main/libs/libsndfile/libsndfile1_${LIBSNDFILE_VERSION}_arm64.deb`,
128+
libraryRoot: "usr/lib/aarch64-linux-gnu",
129+
},
130+
],
131+
"linux-arm": [
132+
{
133+
name: "libmp3lame0",
134+
url: `https://deb.debian.org/debian/pool/main/l/lame/libmp3lame0_${LAME_VERSION}-6_armhf.deb`,
135+
libraryRoot: "usr/lib/arm-linux-gnueabihf",
136+
},
137+
{
138+
name: "libsndfile1",
139+
url: `https://deb.debian.org/debian/pool/main/libs/libsndfile/libsndfile1_${LIBSNDFILE_VERSION}_armhf.deb`,
140+
libraryRoot: "usr/lib/arm-linux-gnueabihf",
141+
},
142+
],
143+
};
144+
100145
/**
101146
* Logs installer progress messages with a consistent prefix.
102147
*/
@@ -385,6 +430,92 @@ function writeBinaryToVendorDirectory(fromPath) {
385430
logInstallerMessage(`Installed LAME binary to ${TARGET_BINARY}`);
386431
}
387432

433+
function copyLibrariesIntoTarget(sourceDir) {
434+
const entries = readdirSync(sourceDir);
435+
for (const entry of entries) {
436+
const fromPath = join(sourceDir, entry);
437+
const toPath = join(LIB_DIRECTORY, entry);
438+
cpSync(fromPath, toPath, {
439+
recursive: true,
440+
force: true,
441+
errorOnExist: false,
442+
});
443+
}
444+
}
445+
446+
async function downloadAndInstallSharedLibraryPackage(dependency) {
447+
const tmpDir = mkdtempSync(join(tmpdir(), "node-lame-lib-"));
448+
const archivePath = join(tmpDir, basename(new URL(dependency.url).pathname));
449+
450+
await downloadFileTo(dependency.url, archivePath);
451+
452+
const extractDir = join(tmpDir, "extract");
453+
mkdirSync(extractDir, { recursive: true });
454+
455+
await runCommandAndWait("ar", ["x", archivePath, "data.tar.xz"], {
456+
cwd: extractDir,
457+
});
458+
await runCommandAndWait("tar", ["-xf", "data.tar.xz"], {
459+
cwd: extractDir,
460+
});
461+
462+
const sourceDir = join(extractDir, dependency.libraryRoot);
463+
if (!existsSync(sourceDir)) {
464+
throw new Error(
465+
`Unable to locate ${dependency.libraryRoot} in ${dependency.name} package`,
466+
);
467+
}
468+
469+
copyLibrariesIntoTarget(sourceDir);
470+
logInstallerMessage(
471+
`Installed shared libraries for ${dependency.name} from ${dependency.url}`,
472+
);
473+
}
474+
475+
async function installLinuxSharedLibraries() {
476+
const platformKey = `${PLATFORM}-${ARCH}`;
477+
const dependencies = LINUX_SHARED_LIBRARY_PACKAGES[platformKey];
478+
479+
if (!dependencies || dependencies.length === 0) {
480+
logInstallerMessage(
481+
`No shared library dependencies configured for ${platformKey}`,
482+
);
483+
return;
484+
}
485+
486+
if (existsSync(LIB_DIRECTORY_MARKER) && existsSync(LIB_DIRECTORY)) {
487+
logInstallerMessage(
488+
`Bundled shared libraries already present at ${LIB_DIRECTORY}`,
489+
);
490+
return;
491+
}
492+
493+
rmSync(LIB_DIRECTORY, { recursive: true, force: true });
494+
mkdirSync(LIB_DIRECTORY, { recursive: true });
495+
496+
for (const dependency of dependencies) {
497+
await downloadAndInstallSharedLibraryPackage(dependency);
498+
}
499+
500+
writeFileSync(LIB_DIRECTORY_MARKER, String(Date.now()), "utf-8");
501+
logInstallerMessage(
502+
`Shared library installation complete at ${LIB_DIRECTORY}`,
503+
);
504+
}
505+
506+
async function installPlatformSpecificDependencies() {
507+
if (PLATFORM === "linux") {
508+
try {
509+
await installLinuxSharedLibraries();
510+
} catch (error) {
511+
logInstallerMessage(
512+
`Failed to install Linux shared libraries: ${error.message}`,
513+
);
514+
throw error;
515+
}
516+
}
517+
}
518+
388519
/**
389520
* Ensures a suitable LAME binary exists in the vendor directory, downloading it if absent.
390521
*/
@@ -393,6 +524,7 @@ async function ensureBundledBinaryAvailable() {
393524
logInstallerMessage(
394525
`Bundled LAME binary already present at ${TARGET_BINARY}`,
395526
);
527+
await installPlatformSpecificDependencies();
396528
return;
397529
}
398530

@@ -436,6 +568,7 @@ async function ensureBundledBinaryAvailable() {
436568
try {
437569
if (source.type === "zip") {
438570
await downloadAndInstallZipBinary(source);
571+
await installPlatformSpecificDependencies();
439572
return;
440573
}
441574

@@ -448,11 +581,13 @@ async function ensureBundledBinaryAvailable() {
448581
}
449582

450583
await downloadAndInstallDebianBinary(source);
584+
await installPlatformSpecificDependencies();
451585
return;
452586
}
453587

454588
if (source.type === "ghcr") {
455589
await downloadAndInstallGhcrBinary(source);
590+
await installPlatformSpecificDependencies();
456591
return;
457592
}
458593

src/core/lame-process.ts

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2-
3-
import type { LameProgressEmitter, LameStatus } from "../types";
4-
import { resolveLameBinary } from "../internal/binary/resolve-binary";
2+
import type { ProcessEnv } from "node:process";
3+
import { delimiter } from "node:path";
4+
5+
import type {
6+
LameProgressEmitter,
7+
LameStatus,
8+
LameStreamMode,
9+
} from "../types";
10+
import {
11+
resolveBundledLibraryDirectory,
12+
resolveLameBinary,
13+
} from "../internal/binary/resolve-binary";
514
import { LameOptions } from "./lame-options";
615

7-
type ProgressKind = "encode" | "decode";
16+
type ProgressKind = LameStreamMode;
817

918
const LAME_TAG_MESSAGE = "Writing LAME Tag...done";
1019

@@ -168,7 +177,10 @@ function processProgressChunk(
168177
return {};
169178
}
170179

171-
function getExitError(code: number | null): Error | null {
180+
function getExitError(
181+
code: number | null,
182+
executable: string,
183+
): Error | null {
172184
if (code === 0) {
173185
return null;
174186
}
@@ -179,13 +191,51 @@ function getExitError(code: number | null): Error | null {
179191
);
180192
}
181193

194+
if (code === 127) {
195+
return new Error(
196+
`lame: Failed to execute '${executable}'. Exit code 127 usually indicates missing shared libraries or an unreadable binary. Run scripts/diagnose-lame.mjs for details.`,
197+
);
198+
}
199+
182200
if (code !== null) {
183201
return new Error(`lame: Process exited with code ${code}`);
184202
}
185203

186204
return new Error("lame: Process exited unexpectedly");
187205
}
188206

207+
function applyBundledLibraryPath(
208+
env: ProcessEnv,
209+
libraryDir: string | null,
210+
): ProcessEnv {
211+
if (!libraryDir) {
212+
return env;
213+
}
214+
215+
let variable: "LD_LIBRARY_PATH" | "DYLD_LIBRARY_PATH" | "PATH" | null =
216+
null;
217+
218+
if (process.platform === "linux") {
219+
variable = "LD_LIBRARY_PATH";
220+
} else if (process.platform === "darwin") {
221+
variable = "DYLD_LIBRARY_PATH";
222+
} else if (process.platform === "win32") {
223+
variable = "PATH";
224+
}
225+
226+
if (!variable) {
227+
return env;
228+
}
229+
230+
const currentValue = env[variable];
231+
return {
232+
...env,
233+
[variable]: currentValue
234+
? `${libraryDir}${delimiter}${currentValue}`
235+
: libraryDir,
236+
};
237+
}
238+
189239
interface SpawnLameProcessOptions {
190240
binaryPath?: string;
191241
spawnArgs: string[];
@@ -231,7 +281,11 @@ function spawnLameProcess(
231281
status.eta = undefined;
232282

233283
const executable = binaryPath ?? resolveLameBinary();
234-
const child = spawn(executable, spawnArgs);
284+
const libraryDir = resolveBundledLibraryDirectory();
285+
const childEnv = applyBundledLibraryPath({ ...process.env }, libraryDir);
286+
const child = spawn(executable, spawnArgs, {
287+
env: childEnv,
288+
});
235289

236290
const progressTargets = new Set(progressSources);
237291
let hasSeenCliError = false;
@@ -323,7 +377,7 @@ function spawnLameProcess(
323377

324378
child.on("error", emitCliError);
325379
child.on("close", (code) => {
326-
const exitError = getExitError(code);
380+
const exitError = getExitError(code, executable);
327381
if (exitError) {
328382
const bufferedLines = stderrBuffer
329383
.split(/\r?\n/)

0 commit comments

Comments
 (0)