@@ -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