@@ -247,48 +247,92 @@ public async Task<OperationResult<LibraryInstallationGoalState>> GetInstallation
247247
248248 private OperationResult < LibraryInstallationGoalState > GenerateGoalState ( ILibraryInstallationState desiredState , ILibrary library )
249249 {
250+ var mappings = new List < FileMapping > ( desiredState . FileMappings ?? [ ] ) ;
250251 List < IError > errors = null ;
251-
252- if ( string . IsNullOrEmpty ( desiredState . DestinationPath ) )
253- {
254- return OperationResult < LibraryInstallationGoalState > . FromError ( PredefinedErrors . DestinationNotSpecified ( desiredState . Name ) ) ;
255- }
256-
257- IEnumerable < string > outFiles ;
258- if ( desiredState . Files == null || desiredState . Files . Count == 0 )
252+ if ( desiredState . Files is { Count : > 0 } )
259253 {
260- outFiles = library . Files . Keys ;
254+ mappings . Add ( new FileMapping { Destination = desiredState . DestinationPath , Files = desiredState . Files } ) ;
261255 }
262- else
256+ else if ( desiredState . FileMappings is null or { Count : 0 } )
263257 {
264- outFiles = FileGlobbingUtility . ExpandFileGlobs ( desiredState . Files , library . Files . Keys ) ;
258+ // no files specified and no file mappings => include all files
259+ mappings . Add ( new FileMapping { Destination = desiredState . DestinationPath } ) ;
265260 }
266261
267262 Dictionary < string , string > installFiles = new ( ) ;
268- if ( library . GetInvalidFiles ( outFiles . ToList ( ) ) is IReadOnlyList < string > invalidFiles
269- && invalidFiles . Count > 0 )
270- {
271- errors ??= [ ] ;
272- errors . Add ( PredefinedErrors . InvalidFilesInLibrary ( desiredState . Name , invalidFiles , library . Files . Keys ) ) ;
273- }
274263
275- foreach ( string outFile in outFiles )
264+ foreach ( FileMapping fileMapping in mappings )
276265 {
277- // strip the source prefix
278- string destinationFile = Path . Combine ( HostInteraction . WorkingDirectory , desiredState . DestinationPath , outFile ) ;
279- if ( ! FileHelpers . IsUnderRootDirectory ( destinationFile , HostInteraction . WorkingDirectory ) )
266+ // if Root is not specified, assume it's the root of the library
267+ string mappingRoot = fileMapping . Root ?? string . Empty ;
268+ // if Destination is not specified, inherit from the library entry
269+ string destination = fileMapping . Destination ?? desiredState . DestinationPath ;
270+
271+ if ( destination is null )
272+ {
273+ errors ??= [ ] ;
274+ string libraryId = LibraryNamingScheme . GetLibraryId ( desiredState . Name , desiredState . Version ) ;
275+ errors . Add ( PredefinedErrors . DestinationNotSpecified ( libraryId ) ) ;
276+ continue ;
277+ }
278+
279+ IReadOnlyList < string > fileFilters ;
280+ if ( fileMapping . Files is { Count : > 0 } )
281+ {
282+ fileFilters = fileMapping . Files ;
283+ }
284+ else
285+ {
286+ fileFilters = [ "**" ] ;
287+ }
288+
289+ if ( mappingRoot . Length > 0 )
290+ {
291+ // prefix mappingRoot to each fileFilter item
292+ fileFilters = fileFilters . Select ( f => $ "{ mappingRoot } /{ f } ") . ToList ( ) ;
293+ }
294+
295+ List < string > outFiles = FileGlobbingUtility . ExpandFileGlobs ( fileFilters , library . Files . Keys ) . ToList ( ) ;
296+
297+ if ( library . GetInvalidFiles ( outFiles ) is IReadOnlyList < string > invalidFiles
298+ && invalidFiles . Count > 0 )
280299 {
281300 errors ??= [ ] ;
282- errors . Add ( PredefinedErrors . PathOutsideWorkingDirectory ( ) ) ;
301+ errors . Add ( PredefinedErrors . InvalidFilesInLibrary ( desiredState . Name , invalidFiles , library . Files . Keys ) ) ;
283302 }
284- destinationFile = FileHelpers . NormalizePath ( destinationFile ) ;
285303
286- // don't forget to include the cache folder in the path
287- string sourceFile = GetCachedFileLocalPath ( desiredState , outFile ) ;
288- sourceFile = FileHelpers . NormalizePath ( sourceFile ) ;
304+ foreach ( string outFile in outFiles )
305+ {
306+ // strip the source prefix
307+ string relativeOutFile = mappingRoot . Length > 0 ? outFile . Substring ( mappingRoot . Length + 1 ) : outFile ;
308+ string destinationFile = Path . Combine ( HostInteraction . WorkingDirectory , destination , relativeOutFile ) ;
309+ destinationFile = FileHelpers . NormalizePath ( destinationFile ) ;
310+
311+ if ( ! FileHelpers . IsUnderRootDirectory ( destinationFile , HostInteraction . WorkingDirectory ) )
312+ {
313+ errors ??= [ ] ;
314+ errors . Add ( PredefinedErrors . PathOutsideWorkingDirectory ( ) ) ;
315+ continue ;
316+ }
289317
290- // map destination back to the library-relative file it originated from
291- installFiles . Add ( destinationFile , sourceFile ) ;
318+ // include the cache folder in the path
319+ string sourceFile = GetCachedFileLocalPath ( desiredState , outFile ) ;
320+ sourceFile = FileHelpers . NormalizePath ( sourceFile ) ;
321+
322+ // map destination back to the library-relative file it originated from
323+ if ( installFiles . ContainsKey ( destinationFile ) )
324+ {
325+ // this file is already being installed from another mapping
326+ errors ??= [ ] ;
327+ string libraryId = LibraryNamingScheme . GetLibraryId ( desiredState . Name , desiredState . Version ) ;
328+ errors . Add ( PredefinedErrors . LibraryCannotBeInstalledDueToConflicts ( destinationFile , [ libraryId ] ) ) ;
329+ continue ;
330+ }
331+ else
332+ {
333+ installFiles . Add ( destinationFile , sourceFile ) ;
334+ }
335+ }
292336 }
293337
294338 if ( errors is not null )
0 commit comments