Skip to content

Commit 0488c60

Browse files
committed
fix(std/wrap): top-level preloads key to merge with existing preloads
1 parent ad1b030 commit 0488c60

File tree

1 file changed

+259
-6
lines changed

1 file changed

+259
-6
lines changed

packages/std/wrap.tg.ts

Lines changed: 259 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export async function wrap(...args: std.Args<wrap.Arg>): Promise<tg.File> {
4949
executable: arg.interpreter ? undefined : arg.executable,
5050
libraryPaths: arg.libraryPaths,
5151
libraryPathStrategy: arg.libraryPathStrategy,
52+
preloads: arg.preloads,
5253
});
5354

5455
// Use existing manifest values as defaults if we're wrapping a wrapper
@@ -122,6 +123,9 @@ export namespace wrap {
122123
/** Which library path strategy should we use? The default is "unfilteredIsolate", which separates libraries into individual directories. */
123124
libraryPathStrategy?: LibraryPathStrategy | undefined;
124125

126+
/** Preloads to include. If the executable is wrapped, they will be merged. */
127+
preloads?: Array<tg.File | tg.Symlink | tg.Template>;
128+
125129
/** Specify how to handle executables that are already Tangram wrappers. When `merge` is true, retain the original executable in the resulting manifest. When `merge` is set to false, produce a manifest pointing to the original wrapper. This option is ignored if the executable being wrapped is not a Tangram wrapper. Default: true. */
126130
merge?: boolean;
127131
};
@@ -226,6 +230,7 @@ export namespace wrap {
226230
merge: merge_ = true,
227231
libraryPaths = [],
228232
libraryPathStrategy,
233+
preloads = [],
229234
} = await std.args.apply<wrap.Arg, wrap.ArgObject>({
230235
args,
231236
map: async (arg) => {
@@ -247,6 +252,7 @@ export namespace wrap {
247252
reduce: {
248253
env: (a, b) => std.env.arg(a, b, { utils: false }),
249254
libraryPaths: "append",
255+
preloads: "append",
250256
args: "append",
251257
},
252258
});
@@ -320,11 +326,23 @@ export namespace wrap {
320326

321327
envs.push(await wrap.envObjectFromManifestEnv(existingManifest.env));
322328

323-
// Only use the existing interpreter if no explicit interpreter was provided
324-
if (interpreter === undefined) {
325-
interpreter = await wrap.interpreterFromManifestInterpreter(
326-
existingManifest.interpreter,
329+
// Merge the existing interpreter with any new interpreter provided
330+
const existingInterpreter = await wrap.interpreterFromManifestInterpreter(
331+
existingManifest.interpreter,
332+
);
333+
if (interpreter !== undefined) {
334+
const newInterpreter = await interpreterFromArg(
335+
interpreter,
336+
buildToolchain,
337+
build,
338+
host,
339+
);
340+
interpreter = await wrap.mergeInterpreters(
341+
existingInterpreter,
342+
newInterpreter,
327343
);
344+
} else {
345+
interpreter = existingInterpreter;
328346
}
329347

330348
executable = await wrap.executableFromManifestExecutable(
@@ -357,6 +375,7 @@ export namespace wrap {
357375
merge,
358376
libraryPaths,
359377
libraryPathStrategy,
378+
preloads,
360379
};
361380
};
362381

@@ -583,6 +602,117 @@ export namespace wrap {
583602
return ret;
584603
};
585604

605+
/** Merge two interpreters, with the new interpreter's properties taking precedence but arrays being concatenated. */
606+
export const mergeInterpreters = async (
607+
existingInterpreter: wrap.Interpreter | undefined,
608+
newInterpreter: wrap.Interpreter | undefined,
609+
): Promise<wrap.Interpreter | undefined> => {
610+
// If no existing interpreter, just return the new one
611+
if (!existingInterpreter) {
612+
return newInterpreter;
613+
}
614+
615+
// If no new interpreter, just return the existing one
616+
if (!newInterpreter) {
617+
return existingInterpreter;
618+
}
619+
620+
// Both interpreters must be the same kind to merge
621+
if (existingInterpreter.kind !== newInterpreter.kind) {
622+
return newInterpreter; // New interpreter completely replaces existing one
623+
}
624+
625+
const kind = existingInterpreter.kind;
626+
627+
switch (kind) {
628+
case "normal": {
629+
const existing = existingInterpreter as wrap.NormalInterpreter;
630+
const new_ = newInterpreter as wrap.NormalInterpreter;
631+
return {
632+
kind,
633+
// New executable takes precedence
634+
executable: new_.executable ?? existing.executable,
635+
// Concatenate args arrays
636+
args:
637+
[...(existing.args ?? []), ...(new_.args ?? [])].length > 0
638+
? [...(existing.args ?? []), ...(new_.args ?? [])]
639+
: undefined,
640+
};
641+
}
642+
case "ld-linux": {
643+
const existing = existingInterpreter as wrap.LdLinuxInterpreter;
644+
const new_ = newInterpreter as wrap.LdLinuxInterpreter;
645+
return {
646+
kind,
647+
// New executable takes precedence
648+
executable: new_.executable ?? existing.executable,
649+
// Concatenate libraryPaths arrays
650+
libraryPaths:
651+
[...(existing.libraryPaths ?? []), ...(new_.libraryPaths ?? [])]
652+
.length > 0
653+
? [...(existing.libraryPaths ?? []), ...(new_.libraryPaths ?? [])]
654+
: undefined,
655+
// Concatenate preloads arrays
656+
preloads:
657+
[...(existing.preloads ?? []), ...(new_.preloads ?? [])].length > 0
658+
? [...(existing.preloads ?? []), ...(new_.preloads ?? [])]
659+
: undefined,
660+
// Concatenate args arrays
661+
args:
662+
[...(existing.args ?? []), ...(new_.args ?? [])].length > 0
663+
? [...(existing.args ?? []), ...(new_.args ?? [])]
664+
: undefined,
665+
};
666+
}
667+
case "ld-musl": {
668+
const existing = existingInterpreter as wrap.LdMuslInterpreter;
669+
const new_ = newInterpreter as wrap.LdMuslInterpreter;
670+
return {
671+
kind,
672+
// New executable takes precedence
673+
executable: new_.executable ?? existing.executable,
674+
// Concatenate libraryPaths arrays
675+
libraryPaths:
676+
[...(existing.libraryPaths ?? []), ...(new_.libraryPaths ?? [])]
677+
.length > 0
678+
? [...(existing.libraryPaths ?? []), ...(new_.libraryPaths ?? [])]
679+
: undefined,
680+
// Concatenate preloads arrays
681+
preloads:
682+
[...(existing.preloads ?? []), ...(new_.preloads ?? [])].length > 0
683+
? [...(existing.preloads ?? []), ...(new_.preloads ?? [])]
684+
: undefined,
685+
// Concatenate args arrays
686+
args:
687+
[...(existing.args ?? []), ...(new_.args ?? [])].length > 0
688+
? [...(existing.args ?? []), ...(new_.args ?? [])]
689+
: undefined,
690+
};
691+
}
692+
case "dyld": {
693+
const existing = existingInterpreter as wrap.DyLdInterpreter;
694+
const new_ = newInterpreter as wrap.DyLdInterpreter;
695+
return {
696+
kind,
697+
// Concatenate libraryPaths arrays
698+
libraryPaths:
699+
[...(existing.libraryPaths ?? []), ...(new_.libraryPaths ?? [])]
700+
.length > 0
701+
? [...(existing.libraryPaths ?? []), ...(new_.libraryPaths ?? [])]
702+
: undefined,
703+
// Concatenate preloads arrays
704+
preloads:
705+
[...(existing.preloads ?? []), ...(new_.preloads ?? [])].length > 0
706+
? [...(existing.preloads ?? []), ...(new_.preloads ?? [])]
707+
: undefined,
708+
};
709+
}
710+
default: {
711+
return tg.unreachable(`Unexpected interpreter kind ${kind}`);
712+
}
713+
}
714+
};
715+
586716
export const executableFromManifestExecutable = async (
587717
manifestExecutable: wrap.Manifest.Executable,
588718
): Promise<tg.Template | tg.File | tg.Symlink> => {
@@ -963,6 +1093,7 @@ type ManifestInterpreterArg = {
9631093
executable?: string | tg.Template | tg.File | tg.Symlink | undefined;
9641094
libraryPaths?: Array<tg.Template.Arg> | undefined;
9651095
libraryPathStrategy?: wrap.LibraryPathStrategy | undefined;
1096+
preloads?: Array<tg.File | tg.Symlink | tg.Template> | undefined;
9661097
};
9671098

9681099
/** Produce the manifest interpreter object given a set of parameters. */
@@ -988,16 +1119,28 @@ const manifestInterpreterFromWrapArgObject = async (
9881119

9891120
// If this is not a "normal" interpreter run the library path optimization, including any additional paths from the user.
9901121
if (interpreter.kind !== "normal") {
991-
const { executable, libraryPaths, libraryPathStrategy } = arg;
1122+
const { executable, libraryPaths, libraryPathStrategy, preloads } = arg;
9921123
interpreter = await optimizeLibraryPaths({
9931124
executable,
9941125
interpreter,
9951126
libraryPaths,
9961127
libraryPathStrategy,
9971128
});
1129+
1130+
// Add any additional preloads from the arg
1131+
if (preloads && preloads.length > 0) {
1132+
// Merge with existing preloads
1133+
const existingPreloads = interpreter.preloads ?? [];
1134+
interpreter = {
1135+
...interpreter,
1136+
preloads: [...existingPreloads, ...preloads],
1137+
};
1138+
}
9981139
}
9991140

1000-
return manifestInterpreterFromWrapInterpreter(interpreter);
1141+
return interpreter
1142+
? manifestInterpreterFromWrapInterpreter(interpreter)
1143+
: undefined;
10011144
};
10021145

10031146
/** Serialize an interpreter into its manifest form. */
@@ -2336,6 +2479,7 @@ export const test = async () => {
23362479
testContentExecutable(),
23372480
testContentExecutableVariadic(),
23382481
testInterpreterSwappingNormal(),
2482+
testInterpreterWrappingPreloads(),
23392483
]);
23402484
return true;
23412485
};
@@ -2667,3 +2811,112 @@ export const testInterpreterSwappingNormal = async () => {
26672811

26682812
return secondWrapper;
26692813
};
2814+
2815+
export const testInterpreterWrappingPreloads = async () => {
2816+
const host = await std.triple.host();
2817+
const os = std.triple.os(host);
2818+
const expectedKind = os === "darwin" ? "dyld" : "ld-musl";
2819+
2820+
const bootstrapSdk = await bootstrap.sdk(host);
2821+
2822+
const testSource = tg.file(`
2823+
#include <stdio.h>
2824+
int main() {
2825+
printf("Hello from test executable\\n");
2826+
return 0;
2827+
}
2828+
`);
2829+
2830+
const testExecutable = await std.build`cc -xc -o $OUTPUT ${testSource}`
2831+
.bootstrap(true)
2832+
.env(bootstrapSdk)
2833+
.then(tg.File.expect);
2834+
2835+
// Create a simple shared library that can be used as a preload.
2836+
const preloadSource = tg.file(`
2837+
#include <stdio.h>
2838+
void __attribute__((constructor)) init() {
2839+
fprintf(stderr, "Custom preload loaded\\n");
2840+
}
2841+
`);
2842+
2843+
const customPreloadLib =
2844+
await std.build`cc -shared -fPIC -xc -o $OUTPUT ${preloadSource}`
2845+
.bootstrap(true)
2846+
.env(bootstrapSdk)
2847+
.then(tg.File.expect);
2848+
2849+
// First, create a wrapper with the default interpreter (will have injection preload)
2850+
const originalWrapper = await wrap(testExecutable, {
2851+
buildToolchain: bootstrapSdk,
2852+
});
2853+
await originalWrapper.store();
2854+
2855+
// Verify it has an ld-linux interpreter with preloads
2856+
const originalManifest = await wrap.Manifest.read(originalWrapper);
2857+
tg.assert(originalManifest);
2858+
tg.assert(originalManifest.interpreter);
2859+
tg.assert(originalManifest.interpreter.kind === expectedKind);
2860+
tg.assert(originalManifest.interpreter.preloads);
2861+
const originalPreloadCount = originalManifest.interpreter.preloads.length;
2862+
tg.assert(
2863+
originalPreloadCount >= 1,
2864+
"Expected at least one default preload (injection library)",
2865+
);
2866+
2867+
// Test adding preloads to an existing wrapper using the top-level preloads field
2868+
const extendedWrapper = await wrap(originalWrapper, {
2869+
preloads: [customPreloadLib],
2870+
});
2871+
await extendedWrapper.store();
2872+
2873+
// Read the extended manifest
2874+
const extendedManifest = await wrap.Manifest.read(extendedWrapper);
2875+
tg.assert(extendedManifest);
2876+
tg.assert(extendedManifest.interpreter);
2877+
tg.assert(extendedManifest.interpreter.kind === expectedKind);
2878+
tg.assert(extendedManifest.interpreter.preloads);
2879+
2880+
// Verify that we have both the original preloads AND the new one,.
2881+
tg.assert(
2882+
extendedManifest.interpreter.preloads.length === originalPreloadCount + 1,
2883+
`Expected ${originalPreloadCount + 1} preloads (${originalPreloadCount} original + 1 new), but got ${extendedManifest.interpreter.preloads.length}`,
2884+
);
2885+
2886+
// Verify that the executable in the extended wrapper is still the original.
2887+
tg.assert(
2888+
JSON.stringify(extendedManifest.executable) ===
2889+
JSON.stringify(originalManifest.executable),
2890+
"Expected the executable to remain the same through re-wrapping",
2891+
);
2892+
2893+
// Verify that all original preloads are still present in the extended wrapper.
2894+
const originalPreloadTemplates = originalManifest.interpreter.preloads;
2895+
const extendedPreloadTemplates = extendedManifest.interpreter.preloads;
2896+
2897+
let foundOriginalPreloads = 0;
2898+
for (const originalPreload of originalPreloadTemplates) {
2899+
const found = extendedPreloadTemplates.some(
2900+
(extendedPreload) =>
2901+
JSON.stringify(originalPreload) === JSON.stringify(extendedPreload),
2902+
);
2903+
if (found) {
2904+
foundOriginalPreloads++;
2905+
}
2906+
}
2907+
2908+
tg.assert(
2909+
foundOriginalPreloads === originalPreloadTemplates.length,
2910+
`Expected all ${originalPreloadTemplates.length} original preloads to be preserved, but only found ${foundOriginalPreloads}`,
2911+
);
2912+
2913+
// Verify that the custom preload was added.
2914+
const customPreloadTemplate = await manifestTemplateFromArg(customPreloadLib);
2915+
const foundCustomPreload = extendedPreloadTemplates.some(
2916+
(extendedPreload) =>
2917+
JSON.stringify(extendedPreload) === JSON.stringify(customPreloadTemplate),
2918+
);
2919+
tg.assert(foundCustomPreload, "Expected the custom preload to be added");
2920+
2921+
return extendedWrapper;
2922+
};

0 commit comments

Comments
 (0)