@@ -202,14 +202,25 @@ func (r *changeTrackingDirectorySymlinkFollowingResolver[TReference, TMetadata])
202202 return r , nil
203203}
204204
205- func copyFileAndDependencies [TReference object.BasicReference , TMetadata model_core.ReferenceMetadata ](
205+ // targetActionResultCopier is responsible for copying files out of
206+ // the output root directories of target action results to a new
207+ // directory hierarchy. This is used to extract individual outputs out
208+ // of target action results (e.g., just "foo.o", even though the action
209+ // also yields a "foo.d").
210+ type targetActionResultCopier [TReference object.BasicReference , TMetadata model_core.ReferenceMetadata ] struct {
211+ loadOptions * changeTrackingDirectoryLoadOptions [TReference ]
212+ directoriesScanned map [* changeTrackingDirectory [TReference , TMetadata ]]struct {}
213+ }
214+
215+ // copyFileAndDependencies extracts a single file or directory from the
216+ // output root directory and places it in a new directory hierarchy.
217+ func (c * targetActionResultCopier [TReference , TMetadata ]) copyFileAndDependencies (
206218 originalOutputDirectory * changeTrackingDirectory [TReference , TMetadata ],
207219 filePath string ,
208220 fileType model_starlark_pb.File_Type ,
209- loadOptions * changeTrackingDirectoryLoadOptions [TReference ],
210221) (* changeTrackingDirectory [TReference , TMetadata ], error ) {
211222 resolver := changeTrackingDirectorySymlinkFollowingResolver [TReference , TMetadata ]{
212- loadOptions : loadOptions ,
223+ loadOptions : c . loadOptions ,
213224 stack : util .NewNonEmptyStack (originalOutputDirectory ),
214225 }
215226 var trimmedOutputRootDirectory changeTrackingDirectory [TReference , TMetadata ]
@@ -235,7 +246,7 @@ func copyFileAndDependencies[TReference object.BasicReference, TMetadata model_c
235246 return nil , errors .New ("target action output is a directory, while a file was expected" )
236247 }
237248 if err := symlinkRecordingComponentWalker .stack .Peek ().setFile (
238- loadOptions ,
249+ c . loadOptions ,
239250 * symlinkRecordingComponentWalker .terminalName ,
240251 resolver .file ,
241252 ); err != nil {
@@ -245,10 +256,9 @@ func copyFileAndDependencies[TReference object.BasicReference, TMetadata model_c
245256 if resolver .file != nil {
246257 return nil , errors .New ("target action output is a file, while a directory was expected" )
247258 }
248- if err := symlinkRecordingComponentWalker .stack .Peek ().setDirectory (
249- loadOptions ,
250- * symlinkRecordingComponentWalker .terminalName ,
251- resolver .stack .Peek (),
259+ if err := c .copyDirectoryAndDependencies (
260+ resolver .stack ,
261+ & symlinkRecordingComponentWalker ,
252262 ); err != nil {
253263 return nil , err
254264 }
@@ -258,6 +268,126 @@ func copyFileAndDependencies[TReference object.BasicReference, TMetadata model_c
258268 return & trimmedOutputRootDirectory , nil
259269}
260270
271+ // copyDirectoryAndDependencies is invoked by copyFileAndDependencies()
272+ // to copy a directory from the target action output root into the new
273+ // directory hierarchy. It also traverses the resulting directory to
274+ // copy any files referenced via symbolic links contained within the
275+ // directory.
276+ func (c * targetActionResultCopier [TReference , TMetadata ]) copyDirectoryAndDependencies (
277+ originalOutputDirectories util.NonEmptyStack [* changeTrackingDirectory [TReference , TMetadata ]],
278+ trimmedOutputComponentWalker * symlinkRecordingComponentWalker [TReference , TMetadata ],
279+ ) error {
280+ trimmedOutputDirectories := trimmedOutputComponentWalker .stack
281+ var newDirectory * changeTrackingDirectory [TReference , TMetadata ]
282+ if terminalName := trimmedOutputComponentWalker .terminalName ; terminalName == nil {
283+ // Symbolic link pointing to this directory contained a
284+ // trailing slash, meaning we're already within the
285+ // right directory.
286+ newDirectory = trimmedOutputDirectories .Peek ()
287+ } else {
288+ // Symbolic link pointing to this directory did not
289+ // contain a trailing slash. This means the target
290+ // directory did not get created for us.
291+ var err error
292+ newDirectory , err = trimmedOutputComponentWalker .stack .Peek ().getOrCreateDirectory (* trimmedOutputComponentWalker .terminalName )
293+ if err != nil {
294+ return err
295+ }
296+ trimmedOutputDirectories = trimmedOutputDirectories .Copy ()
297+ trimmedOutputDirectories .Push (newDirectory )
298+ }
299+ if err := newDirectory .mergeDirectory (originalOutputDirectories .Peek (), c .loadOptions ); err != nil {
300+ return err
301+ }
302+ return c .scanDirectoryDependencies (originalOutputDirectories , trimmedOutputDirectories , 0 )
303+ }
304+
305+ // scanDirectoryDependencies visits all symbolic links in a directory
306+ // hierarchy and ensures their targets are copied over into the newly
307+ // created directory hierarchy as well.
308+ func (c * targetActionResultCopier [TReference , TMetadata ]) scanDirectoryDependencies (
309+ originalOutputDirectories util.NonEmptyStack [* changeTrackingDirectory [TReference , TMetadata ]],
310+ trimmedOutputDirectories util.NonEmptyStack [* changeTrackingDirectory [TReference , TMetadata ]],
311+ maximumEscapementLevels uint32 ,
312+ ) error {
313+ // Prevent cyclic scanning of directories.
314+ trimmedOutputDirectory := trimmedOutputDirectories .Peek ()
315+ if _ , ok := c .directoriesScanned [trimmedOutputDirectory ]; ok {
316+ return nil
317+ }
318+ c .directoriesScanned [trimmedOutputDirectory ] = struct {}{}
319+
320+ if trimmedOutputDirectory .maximumSymlinkEscapementLevelsAtMost (maximumEscapementLevels ) {
321+ // This directory does not contain any symlinks that
322+ // escape the directory that was copied. This means that
323+ // there is no need to traverse it.
324+ return nil
325+ }
326+
327+ originalOutputDirectory := originalOutputDirectories .Peek ()
328+ if err := originalOutputDirectory .maybeLoadContents (c .loadOptions ); err != nil {
329+ return err
330+ }
331+ if err := trimmedOutputDirectory .maybeLoadContents (c .loadOptions ); err != nil {
332+ return err
333+ }
334+
335+ for name , trimmedOutputChildDirectory := range trimmedOutputDirectory .directories {
336+ originalOutputDirectories .Push (originalOutputDirectory .directories [name ])
337+ trimmedOutputDirectories .Push (trimmedOutputChildDirectory )
338+ if err := c .scanDirectoryDependencies (
339+ originalOutputDirectories ,
340+ trimmedOutputDirectories ,
341+ maximumEscapementLevels + 1 ,
342+ ); err != nil {
343+ return err
344+ }
345+ if _ , ok := originalOutputDirectories .PopSingle (); ! ok {
346+ panic ("bad directory stack handling" )
347+ }
348+ if _ , ok := trimmedOutputDirectories .PopSingle (); ! ok {
349+ panic ("bad directory stack handling" )
350+ }
351+ }
352+
353+ for name , target := range trimmedOutputDirectory .symlinks {
354+ resolver := changeTrackingDirectorySymlinkFollowingResolver [TReference , TMetadata ]{
355+ loadOptions : c .loadOptions ,
356+ stack : originalOutputDirectories .Copy (),
357+ }
358+ symlinkRecordingComponentWalker := symlinkRecordingComponentWalker [TReference , TMetadata ]{
359+ base : & resolver ,
360+ stack : trimmedOutputDirectories .Copy (),
361+ }
362+ if err := path .Resolve (
363+ target ,
364+ path .NewLoopDetectingScopeWalker (
365+ path .NewRelativeScopeWalker (& symlinkRecordingComponentWalker ),
366+ ),
367+ ); err != nil {
368+ return fmt .Errorf ("failed to resolve symlink %#v: %w" , name .String (), err )
369+ }
370+
371+ if resolver .file != nil {
372+ if err := symlinkRecordingComponentWalker .stack .Peek ().setFile (
373+ c .loadOptions ,
374+ * symlinkRecordingComponentWalker .terminalName ,
375+ resolver .file ,
376+ ); err != nil {
377+ return err
378+ }
379+ } else {
380+ if err := c .copyDirectoryAndDependencies (
381+ resolver .stack ,
382+ & symlinkRecordingComponentWalker ,
383+ ); err != nil {
384+ return err
385+ }
386+ }
387+ }
388+ return nil
389+ }
390+
261391func (c * baseComputer [TReference , TMetadata ]) ComputeFileRootValue (ctx context.Context , key model_core.Message [* model_analysis_pb.FileRoot_Key , TReference ], e FileRootEnvironment [TReference , TMetadata ]) (PatchedFileRootValue , error ) {
262392 f := model_core .Nested (key , key .Message .File )
263393 if f .Message == nil {
@@ -337,7 +467,11 @@ func (c *baseComputer[TReference, TMetadata]) ComputeFileRootValue(ctx context.C
337467 directoryContentsReader : directoryReaders .DirectoryContents ,
338468 leavesReader : directoryReaders .Leaves ,
339469 }
340- trimmedOutputRootDirectory , err := copyFileAndDependencies (& originalOutputRootDirectory , filePath , f .Message .Type , loadOptions )
470+ copier := targetActionResultCopier [TReference , TMetadata ]{
471+ loadOptions : loadOptions ,
472+ directoriesScanned : map [* changeTrackingDirectory [TReference , TMetadata ]]struct {}{},
473+ }
474+ trimmedOutputRootDirectory , err := copier .copyFileAndDependencies (& originalOutputRootDirectory , filePath , f .Message .Type )
341475 if err != nil {
342476 if ! errors .Is (err , errChangeTrackingDirectorySymlinkFollowingResolverFileNotFound ) {
343477 return PatchedFileRootValue {}, err
@@ -379,7 +513,11 @@ func (c *baseComputer[TReference, TMetadata]) ComputeFileRootValue(ctx context.C
379513 return PatchedFileRootValue {}, err
380514 }
381515
382- trimmedOutputRootDirectory , err = copyFileAndDependencies (& originalOutputRootDirectory , filePath , f .Message .Type , loadOptions )
516+ copier := targetActionResultCopier [TReference , TMetadata ]{
517+ loadOptions : loadOptions ,
518+ directoriesScanned : map [* changeTrackingDirectory [TReference , TMetadata ]]struct {}{},
519+ }
520+ trimmedOutputRootDirectory , err = copier .copyFileAndDependencies (& originalOutputRootDirectory , filePath , f .Message .Type )
383521 if err != nil {
384522 return PatchedFileRootValue {}, err
385523 }
0 commit comments