Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,8 @@ cd packages/core && bun run bench
| Windows | x86_64 | ✅ Supported |
| Windows | aarch64 | ✅ Supported |

On Linux, the launcher detects glibc vs musl automatically (via `process.report`, the musl dynamic loader at `/lib/ld-musl-*.so.1`, and `ldd`). If detection ever picks the wrong flavor — e.g. in minimal containers — set `TOKSCALE_LIBC=musl` (or `TOKSCALE_LIBC=gnu`) to force it.

### Windows Support

Tokscale fully supports Windows. The TUI and CLI work the same as on macOS/Linux.
Expand Down
57 changes: 52 additions & 5 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { spawnSync, execSync } from "node:child_process";
import { existsSync, realpathSync } from "node:fs";
import { existsSync, readdirSync, realpathSync } from "node:fs";
import { resolve, join, basename } from "node:path";
import { fileURLToPath } from "node:url";

Expand All @@ -25,9 +25,16 @@ const workspaceRoot = resolve(scopeDir, "..");
type LibcKind = "gnu" | "musl";

function detectLibcKind(): LibcKind {
const override = process.env.TOKSCALE_LIBC?.trim().toLowerCase();
if (override === "musl") return "musl";
if (override === "gnu" || override === "glibc") return "gnu";

const report = process.report?.getReport?.() as
| {
header?: { glibcVersionRuntime?: string };
header?: {
glibcVersionRuntime?: string;
release?: { sourceUrl?: string };
};
sharedObjects?: string[];
}
| undefined;
Expand All @@ -43,15 +50,55 @@ function detectLibcKind(): LibcKind {
return "musl";
}

// Bun reports neither glibcVersionRuntime nor sharedObjects, but its
// release.sourceUrl names the build flavor (e.g. bun-linux-x64-musl-baseline.zip).
if (report?.header?.release?.sourceUrl?.toLowerCase().includes("musl")) {
return "musl";
}

try {
const output = execSync("ldd --version", {
encoding: "utf-8",
stdio: ["ignore", "pipe", "pipe"],
}).toLowerCase();
return output.includes("musl") ? "musl" : "gnu";
} catch {
return "gnu";
if (output.includes("musl")) return "musl";
if (output.includes("glibc") || output.includes("gnu")) return "gnu";
} catch (error) {
// musl's ldd rejects --version: it prints "musl libc" to stderr and
// exits non-zero, so the answer is in the error, not the output.
const { stdout, stderr } = (error ?? {}) as { stdout?: unknown; stderr?: unknown };
const combined = `${stdout ?? ""}\n${stderr ?? ""}`.toLowerCase();
if (combined.includes("musl")) return "musl";
if (combined.includes("glibc") || combined.includes("gnu")) return "gnu";
}

// ldd missing or inconclusive: look for dynamic loaders. Either loader
// can coexist with the other's libc (Debian's musl package installs
// ld-musl-*; Alpine's gcompat installs ld-linux-*), so when both are
// present, let the distro break the tie.
const hasGnuLoader = loaderPresent("ld-linux-");
const hasMuslLoader = loaderPresent("ld-musl-");
if (hasGnuLoader !== hasMuslLoader) return hasMuslLoader ? "musl" : "gnu";
if (hasGnuLoader && hasMuslLoader) {
return existsSync("/etc/alpine-release") ? "musl" : "gnu";
}

return "gnu";
}

// Glibc ships ld-linux-*.so.* in /lib64 (or /lib on some arches); musl
// distros (Alpine, Void-musl, ...) ship /lib/ld-musl-<arch>.so.1.
function loaderPresent(prefix: string): boolean {
for (const dir of ["/lib", "/lib64"]) {
try {
if (readdirSync(dir).some((entry) => entry.startsWith(prefix))) {
return true;
}
} catch {
// Directory unreadable or missing; try the next one.
}
}
return false;
}

function resolveTargetPackageName(): string | null {
Expand Down
42 changes: 39 additions & 3 deletions scripts/test-package-launchers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,18 @@ esac

PLATFORM_PACKAGE="$(node --input-type=module <<'NODE'
import { execSync } from "node:child_process";
import { existsSync, readdirSync } from "node:fs";

// Keep in sync with detectLibcKind() in packages/cli/src/index.ts.
function detectLibcKind() {
if (process.platform !== "linux") {
return null;
}

const override = process.env.TOKSCALE_LIBC?.trim().toLowerCase();
if (override === "musl") return "musl";
if (override === "gnu" || override === "glibc") return "gnu";

const report = process.report?.getReport?.();
if (report?.header?.glibcVersionRuntime) {
return "gnu";
Expand All @@ -54,15 +60,45 @@ function detectLibcKind() {
return "musl";
}

if (report?.header?.release?.sourceUrl?.toLowerCase().includes("musl")) {
return "musl";
}

try {
const output = execSync("ldd --version", {
encoding: "utf-8",
stdio: ["ignore", "pipe", "pipe"],
}).toLowerCase();
return output.includes("musl") ? "musl" : "gnu";
} catch {
throw new Error("Unable to determine Linux libc kind for launcher smoke tests");
if (output.includes("musl")) return "musl";
if (output.includes("glibc") || output.includes("gnu")) return "gnu";
} catch (error) {
// musl's ldd prints "musl libc" to stderr and exits non-zero on --version.
const combined = `${error?.stdout ?? ""}\n${error?.stderr ?? ""}`.toLowerCase();
if (combined.includes("musl")) return "musl";
if (combined.includes("glibc") || combined.includes("gnu")) return "gnu";
}

// ldd missing or inconclusive: look for dynamic loaders. Either loader can
// coexist with the other's libc (Debian's musl package installs ld-musl-*;
// Alpine's gcompat installs ld-linux-*), so the distro breaks ties.
const loaderPresent = (prefix) => {
for (const dir of ["/lib", "/lib64"]) {
try {
if (readdirSync(dir).some((entry) => entry.startsWith(prefix))) {
return true;
}
} catch {}
}
return false;
};
const hasGnuLoader = loaderPresent("ld-linux-");
const hasMuslLoader = loaderPresent("ld-musl-");
if (hasGnuLoader !== hasMuslLoader) return hasMuslLoader ? "musl" : "gnu";
if (hasGnuLoader && hasMuslLoader) {
return existsSync("/etc/alpine-release") ? "musl" : "gnu";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Tie-break logic misclassifies non-Alpine musl systems as gnu when both loaders exist.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/test-package-launchers.sh, line 98:

<comment>Tie-break logic misclassifies non-Alpine musl systems as gnu when both loaders exist.</comment>

<file context>
@@ -90,8 +91,12 @@ function detectLibcKind() {
+  const hasMuslLoader = loaderPresent("ld-musl-");
+  if (hasGnuLoader !== hasMuslLoader) return hasMuslLoader ? "musl" : "gnu";
+  if (hasGnuLoader && hasMuslLoader) {
+    return existsSync("/etc/alpine-release") ? "musl" : "gnu";
+  }
 
</file context>

}

return "gnu";
}

const arch = process.arch;
Expand Down