mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Keep the static hello binary small under GNU ld (#2373)
This commit is contained in:
@@ -75,7 +75,7 @@ export function executableLinkInputs(options: {
|
||||
* links as needed: the runtime defines the libm functions most programs
|
||||
* reach (scr_glibc_math.c) and Math's transcendentals (scr_ieee754.c), so a
|
||||
* program that calls no other libm function, such as pow, starts without
|
||||
* loading libm.so. */
|
||||
* loading libm.so when it links with lld (see inputsWithoutLld). */
|
||||
export function systemLibraryLinkerArgs(
|
||||
target: Pick<NativeTargetSpec, "platform">,
|
||||
names: readonly string[],
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { type NativeArtifactDependency } from "./native/contracts.js";
|
||||
import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js";
|
||||
import type { NativeLinkPlan } from "./link-plan.js";
|
||||
import { lldLinkerArgs } from "./lld.js";
|
||||
import { inputsWithoutLld, lldLinkerArgs } from "./lld.js";
|
||||
import { systemLibraryLinkerArgs } from "./link-plan-core.js";
|
||||
import {
|
||||
createDarwinDebugSymbols,
|
||||
@@ -244,15 +244,19 @@ export async function linkNativeExecutable(
|
||||
stagedRoot = staged.root;
|
||||
// Linux links with lld when the default driver finds it.
|
||||
const lld = explicitLinker === undefined ? lldLinkerArgs(plan.target.platform, linker) : [];
|
||||
const args = [
|
||||
...(explicitLinker === undefined ? plan.target.defaultLinkerArgs : []),
|
||||
...lld,
|
||||
...plan.driverFlags,
|
||||
...plan.inputs.map((input) => staged.replacements.get(input) ?? input),
|
||||
...systemLibraryLinkerArgs(plan.target, plan.systemLibraries),
|
||||
"-o",
|
||||
privateOut,
|
||||
];
|
||||
const linkArgs = (withLld: boolean): string[] => {
|
||||
const inputs = plan.inputs.map((input) => staged.replacements.get(input) ?? input);
|
||||
return [
|
||||
...(explicitLinker === undefined ? plan.target.defaultLinkerArgs : []),
|
||||
...(withLld ? lld : []),
|
||||
...plan.driverFlags,
|
||||
...(withLld ? inputs : inputsWithoutLld(plan.target.platform, inputs)),
|
||||
...systemLibraryLinkerArgs(plan.target, plan.systemLibraries),
|
||||
"-o",
|
||||
privateOut,
|
||||
];
|
||||
};
|
||||
const args = linkArgs(lld.length !== 0);
|
||||
// The pack snapshots bracket both verification passes and private staging;
|
||||
// the program-object snapshot begins before helper emission. Snapshot the
|
||||
// remaining cache-bearing inputs before the linker consumes them, then
|
||||
@@ -285,10 +289,7 @@ export async function linkNativeExecutable(
|
||||
// A failed link with lld runs again with the driver's default linker,
|
||||
// whose error (if any) is the one reported.
|
||||
if (lld.length === 0) throw error;
|
||||
await execFileAsync(
|
||||
linker,
|
||||
args.filter((arg) => !lld.includes(arg)),
|
||||
);
|
||||
await execFileAsync(linker, linkArgs(false));
|
||||
});
|
||||
const output = await stat(privateOut);
|
||||
if (!output.isFile() || output.size === 0) throw new Error("linker produced no executable");
|
||||
|
||||
@@ -2,7 +2,7 @@ import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync }
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { driverFindsLld, lldLinkerArgs } from "./lld.js";
|
||||
import { driverFindsLld, inputsWithoutLld, lldLinkerArgs } from "./lld.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
afterEach(() => {
|
||||
@@ -59,3 +59,10 @@ test.skipIf(process.platform === "win32")(
|
||||
expect(lldLinkerArgs("linux", clang, { ...env, SCRIPTC_LINKER: clang })).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
test("Linux links without lld leave out the glibc math unit", () => {
|
||||
const inputs = ["/pack/scr_lib.o", "/pack/scr_glibc_math.o", "/tmp/program.o"];
|
||||
expect(inputsWithoutLld("linux", inputs)).toEqual(["/pack/scr_lib.o", "/tmp/program.o"]);
|
||||
expect(inputsWithoutLld("darwin", inputs)).toEqual(inputs);
|
||||
expect(inputsWithoutLld("windows", inputs)).toEqual(inputs);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { realpathSync, statSync } from "node:fs";
|
||||
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
|
||||
function isFile(path: string): boolean {
|
||||
try {
|
||||
@@ -47,3 +47,21 @@ export function lldLinkerArgs(
|
||||
? ["-fuse-ld=lld"]
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The link inputs for a Linux link without lld. The runtime's
|
||||
* scr_glibc_math.c defines glibc's floor, fmod, frexp and a few others as
|
||||
* hidden functions so that a program calling no other libm function starts
|
||||
* without libm.so. That works with lld, which garbage-collects the unused
|
||||
* definitions and decides `--as-needed` from the live code. GNU ld decides
|
||||
* `--as-needed` before section GC, so the runtime's unreachable code keeps
|
||||
* libm needed, and it keeps every regular definition of a name that a needed
|
||||
* shared library also defines, hidden or not: the unit would add all of its
|
||||
* code to every executable and save nothing. Its functions answer what
|
||||
* glibc's do, so linking glibc's own instead is equivalent.
|
||||
*/
|
||||
export function inputsWithoutLld(platform: string, inputs: readonly string[]): string[] {
|
||||
return platform === "linux"
|
||||
? inputs.filter((input) => basename(input) !== "scr_glibc_math.o")
|
||||
: [...inputs];
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ import {
|
||||
import { splitLlvmLibraryProgram } from "../backend/llvm/split.js";
|
||||
import { prepareNativeExecutable, prepareNativeLibrary } from "./prepare.js";
|
||||
import { buildSanitizedRuntime, sanitizerDriver, sanitizerFlags } from "./sanitizer.js";
|
||||
import { lldLinkerArgs } from "../backend/lld.js";
|
||||
import { inputsWithoutLld, lldLinkerArgs } from "../backend/lld.js";
|
||||
import { openNativeExecutableCache } from "./executable-cache.js";
|
||||
import { LinkerJobs } from "./linker-job.js";
|
||||
import { nativeFileIdentity } from "./file-identity.js";
|
||||
@@ -569,15 +569,16 @@ export class NativeCompiler {
|
||||
});
|
||||
// Linux links with lld when the driver finds it.
|
||||
const lld = options.sanitize ? [] : lldLinkerArgs(target.platform, toolchain.linker);
|
||||
const linkArgs = [
|
||||
const linkArgsWith = (withLld: boolean): string[] => [
|
||||
...(options.sanitize ? ["-fsanitize=address"] : toolchain.linkerArgs),
|
||||
...lld,
|
||||
...(withLld ? lld : []),
|
||||
...plan.driverFlags,
|
||||
...plan.inputs,
|
||||
...(withLld ? plan.inputs : inputsWithoutLld(target.platform, plan.inputs)),
|
||||
...systemLibraryLinkerArgs(target, plan.systemLibraries),
|
||||
"-o",
|
||||
stagedOutput,
|
||||
];
|
||||
const linkArgs = linkArgsWith(lld.length !== 0);
|
||||
// Host Linux links with lld run the linker job the driver would.
|
||||
const jobs = LinkerJobs.open(this.cache, toolchain, lld, options.sanitize ?? false);
|
||||
const job = jobs?.link([...plan.inputs, stagedOutput], stage);
|
||||
@@ -593,10 +594,7 @@ export class NativeCompiler {
|
||||
// A failed link with lld runs again with the driver's default
|
||||
// linker, whose error (if any) is the one reported.
|
||||
if (lld.length === 0) throw error;
|
||||
runNativeTool(
|
||||
driver,
|
||||
linkArgs.filter((arg) => !lld.includes(arg)),
|
||||
);
|
||||
runNativeTool(driver, linkArgsWith(false));
|
||||
}
|
||||
}
|
||||
timing("native-link");
|
||||
|
||||
@@ -5393,6 +5393,11 @@ static ScrGen *scr_gen_new_common(void (*entry)(ScrFiber *, void *), void *argpa
|
||||
bool stackless) {
|
||||
ScrGen *g = scr_mem_calloc(sizeof *g);
|
||||
if (!g) scr_oom();
|
||||
/* Install the tag reader with each generator rather than in a constructor:
|
||||
* a constructor is a link root, so it would keep scr_gen_tag and this
|
||||
* unit's string literals in every executable, generators or not. Threads
|
||||
* store the same value, relaxed. */
|
||||
__atomic_store_n(&scr_gen_tag_reader, scr_gen_tag, __ATOMIC_RELAXED);
|
||||
g->rc = 1;
|
||||
g->receiver = scr_dyn_this_get();
|
||||
g->state = SCR_GEN_UNSTARTED;
|
||||
@@ -5967,10 +5972,6 @@ const char *scr_gen_tag(const ScrGen *g) {
|
||||
return g->tag ? g->tag : g->is_async ? "AsyncGenerator" : "Generator";
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void scr_gen_tag_reader_init(void) {
|
||||
scr_gen_tag_reader = scr_gen_tag;
|
||||
}
|
||||
|
||||
void scr_gen_close(ScrGen *g) {
|
||||
scr_gen_slot_reset(&g->ret);
|
||||
scr_gen_resume_return(g);
|
||||
|
||||
@@ -82,8 +82,9 @@ void scr_gen_set_tag(ScrGen *g, const char *tag);
|
||||
void scr_gen_copy_tag(ScrGen *g, const ScrGen *from);
|
||||
const char *scr_gen_tag(const ScrGen *g);
|
||||
/* scr_gen_tag for units that also link without the generator runtime (a
|
||||
* library links none): the generator runtime sets it at startup, and only
|
||||
* then can a generator capsule exist. */
|
||||
* library links none): the generator runtime sets it when it creates a
|
||||
* generator, so it is set whenever a generator capsule exists. Read it with
|
||||
* a relaxed atomic load. */
|
||||
extern const char *(*scr_gen_tag_reader)(const ScrGen *g);
|
||||
ScrGen *scr_gen_retain(ScrGen *g);
|
||||
void scr_gen_release(ScrGen *g); /* NULL-tolerant */
|
||||
|
||||
@@ -1613,7 +1613,7 @@ static bool scr_insp_is_generator(const ScrDyn *d) {
|
||||
}
|
||||
|
||||
static ScrStr *scr_insp_generator(const ScrDyn *d) {
|
||||
const char *tag = scr_gen_tag_reader(d->v.typed_ref.ptr);
|
||||
const char *tag = __atomic_load_n(&scr_gen_tag_reader, __ATOMIC_RELAXED)(d->v.typed_ref.ptr);
|
||||
if (tag[0] == '!') {
|
||||
static const char message[] = "util.inspect of this iterator object is not supported yet";
|
||||
scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1);
|
||||
|
||||
@@ -9633,7 +9633,7 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
|
||||
if (d->kind == SCR_DYN_TYPED_REF) {
|
||||
if (scr_dyn_ref_is_error(d)) return scr_str_new("[object Error]", 14);
|
||||
if (scr_dyn_generator(d)) {
|
||||
const char *gen_tag = scr_gen_tag_reader(d->v.typed_ref.ptr);
|
||||
const char *gen_tag = __atomic_load_n(&scr_gen_tag_reader, __ATOMIC_RELAXED)(d->v.typed_ref.ptr);
|
||||
if (gen_tag[0] == '!') {
|
||||
static const char message[] = "Object.prototype.toString of this iterator is not supported yet";
|
||||
scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1);
|
||||
|
||||
@@ -142,6 +142,9 @@ nativeToolchainTest(
|
||||
"scr_url_",
|
||||
"scr_json_parse",
|
||||
"scr_date_",
|
||||
// The generator runtime installs its tag reader with its first
|
||||
// generator; a load-time constructor would retain it everywhere.
|
||||
"scr_gen_tag",
|
||||
]) {
|
||||
expect(helloSymbols, `hello retains ${family}`).not.toContain(family);
|
||||
}
|
||||
@@ -159,8 +162,10 @@ nativeToolchainTest(
|
||||
|
||||
// Symbol absence is the primary reachability contract. Keep a deliberately
|
||||
// roomy, platform-specific hello-world ceiling too: it catches losing
|
||||
// section GC without pinning an exact linker/SDK byte count. The canonical
|
||||
// Linux build is about 84KB and current Mach-O builds are about 70KB. Most
|
||||
// section GC without pinning an exact linker/SDK byte count. The Linux
|
||||
// build is about 65KB with lld (CI's pinned LLVM provides it) and about
|
||||
// 94KB with GNU ld, which keeps a unit's string literals whole once any of
|
||||
// them is reachable; current Mach-O builds are about 70KB. Most
|
||||
// of Linux hello's runtime code is the cycle collector, which its exit
|
||||
// path reaches (scr_lib_cleanup releases an array). ELF segments are page
|
||||
// aligned, so the Linux size moves in 4 KiB steps whenever .text crosses
|
||||
|
||||
Reference in New Issue
Block a user