Skip to content

Commit e639b87

Browse files
Merge pull request #756 from jimmylewis/fileMapping
Implement support for "fileMappings"
2 parents 91c6660 + 9697b44 commit e639b87

15 files changed

+552
-105
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#nullable enable
5+
6+
using System.Collections.Generic;
7+
8+
namespace Microsoft.Web.LibraryManager.Contracts
9+
{
10+
/// <summary>
11+
///
12+
/// </summary>
13+
public class FileMapping
14+
{
15+
/// <summary>
16+
/// Root path within the library content for this file mapping entry.
17+
/// </summary>
18+
public string? Root { get; set; }
19+
20+
/// <summary>
21+
/// Destination folder within the project.
22+
/// </summary>
23+
public string? Destination { get; set; }
24+
25+
/// <summary>
26+
/// The file patterns to match for this mapping, relative to <see cref="Root"/>. Accepts glob patterns.
27+
/// </summary>
28+
public IReadOnlyList<string>? Files { get; set; }
29+
}
30+
}

src/LibraryManager.Contracts/ILibraryInstallationState.cs

+5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ public interface ILibraryInstallationState
3030
/// </summary>
3131
IReadOnlyList<string> Files { get; }
3232

33+
/// <summary>
34+
/// List of mappings of a portion of library assets to a unique destination.
35+
/// </summary>
36+
IReadOnlyList<FileMapping> FileMappings { get; }
37+
3338
/// <summary>
3439
/// The path relative to the working directory to copy the files to.
3540
/// </summary>
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using Newtonsoft.Json;
6+
7+
#nullable enable
8+
9+
namespace Microsoft.Web.LibraryManager.Json
10+
{
11+
internal class FileMapping
12+
{
13+
[JsonProperty(ManifestConstants.Root)]
14+
public string? Root { get; set; }
15+
16+
[JsonProperty(ManifestConstants.Destination)]
17+
public string? Destination { get; set; }
18+
19+
[JsonProperty(ManifestConstants.Files)]
20+
public IReadOnlyList<string>? Files { get; set; }
21+
}
22+
}

src/LibraryManager/Json/LibraryInstallationStateOnDisk.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
54
using System.Collections.Generic;
6-
using System.Text;
75
using Newtonsoft.Json;
86

97
namespace Microsoft.Web.LibraryManager.Json
@@ -21,5 +19,8 @@ internal class LibraryInstallationStateOnDisk
2119

2220
[JsonProperty(ManifestConstants.Files)]
2321
public IReadOnlyList<string> Files { get; set; }
22+
23+
[JsonProperty(ManifestConstants.FileMappings)]
24+
public IReadOnlyList<FileMapping> FileMappings { get; set; }
2425
}
2526
}

src/LibraryManager/Json/LibraryStateToFileConverter.cs

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Linq;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Text;
67
using Microsoft.Web.LibraryManager.Contracts;
@@ -39,7 +40,8 @@ public ILibraryInstallationState ConvertToLibraryInstallationState(LibraryInstal
3940
IsUsingDefaultProvider = string.IsNullOrEmpty(stateOnDisk.ProviderId),
4041
ProviderId = provider,
4142
DestinationPath = destination,
42-
Files = stateOnDisk.Files
43+
Files = stateOnDisk.Files,
44+
FileMappings = stateOnDisk.FileMappings?.Select(f => new Contracts.FileMapping { Destination = f.Destination, Root = f.Root, Files = f.Files }).ToList(),
4345
};
4446

4547
return state;
@@ -78,13 +80,22 @@ public LibraryInstallationStateOnDisk ConvertToLibraryInstallationStateOnDisk(IL
7880
}
7981

8082
string provider = string.IsNullOrEmpty(state.ProviderId) ? _defaultProvider : state.ProviderId;
81-
return new LibraryInstallationStateOnDisk()
83+
var serializeState = new LibraryInstallationStateOnDisk()
8284
{
8385
ProviderId = state.IsUsingDefaultProvider ? null : state.ProviderId,
8486
DestinationPath = state.IsUsingDefaultDestination ? null : state.DestinationPath,
8587
Files = state.Files,
86-
LibraryId = LibraryIdToNameAndVersionConverter.Instance.GetLibraryId(state.Name, state.Version, provider)
88+
LibraryId = LibraryIdToNameAndVersionConverter.Instance.GetLibraryId(state.Name, state.Version, provider),
89+
FileMappings = state.FileMappings?.Select(f => new FileMapping { Destination = f.Destination, Root = f.Root, Files = f.Files }).ToList(),
8790
};
91+
92+
if (serializeState is { FileMappings: { Count: 0} })
93+
{
94+
// if FileMappings is empty, omit it from serialization
95+
serializeState.FileMappings = null;
96+
}
97+
98+
return serializeState;
8899
}
89100
}
90101
}

src/LibraryManager/LibraryInstallationState.cs

+5
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ internal class LibraryInstallationState : ILibraryInstallationState
4646
/// </summary>
4747
public string Version { get; set; }
4848

49+
/// <summary>
50+
/// Mappings for multiple different files within the library to different destinations.
51+
/// </summary>
52+
public IReadOnlyList<FileMapping> FileMappings { get; set; }
53+
4954
/// <summary>Internal use only</summary>
5055
public static LibraryInstallationState FromInterface(ILibraryInstallationState state,
5156
string defaultProviderId = null,

src/LibraryManager/Manifest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class Manifest
2626
/// <summary>
2727
/// Supported versions of Library Manager
2828
/// </summary>
29-
public static readonly Version[] SupportedVersions = { new Version("1.0") };
29+
public static readonly Version[] SupportedVersions = { new Version("1.0"), new Version("3.0") };
3030
private IHostInteraction _hostInteraction;
3131
private readonly List<ILibraryInstallationState> _libraries;
3232
private IDependencies _dependencies;

src/LibraryManager/ManifestConstants.cs

+18-8
Original file line numberDiff line numberDiff line change
@@ -13,45 +13,55 @@ namespace Microsoft.Web.LibraryManager
1313
public static class ManifestConstants
1414
{
1515
/// <summary>
16-
/// libman.json libraries element
16+
/// libman.json libraries element
1717
/// </summary>
1818
public const string Version = "version";
1919

2020
/// <summary>
21-
/// libman.json libraries element
21+
/// libman.json libraries element
2222
/// </summary>
2323
public const string Libraries = "libraries";
2424

2525
/// <summary>
26-
/// libman.json library element
26+
/// libman.json library element
2727
/// </summary>
2828
public const string Library = "library";
2929

3030
/// <summary>
31-
/// libman.json destination element
31+
/// libman.json destination element
3232
/// </summary>
3333
public const string Destination = "destination";
3434

3535
/// <summary>
36-
/// libman.json defaultDestination element
36+
/// libman.json defaultDestination element
3737
/// </summary>
3838
public const string DefaultDestination = "defaultDestination";
3939

4040
/// <summary>
41-
/// libman.json provider element
41+
/// libman.json provider element
4242
/// </summary>
4343
public const string Provider = "provider";
4444

4545
/// <summary>
46-
/// libman.json defaultProvider element
46+
/// libman.json defaultProvider element
4747
/// </summary>
4848
public const string DefaultProvider = "defaultProvider";
4949

5050
/// <summary>
51-
/// libman.json files element
51+
/// libman.json files element
5252
/// </summary>
5353
public const string Files = "files";
5454

55+
/// <summary>
56+
/// libman.json fileMappings element
57+
/// </summary>
58+
public const string FileMappings = "fileMappings";
59+
60+
/// <summary>
61+
/// libman.json root element
62+
/// </summary>
63+
public const string Root = "root";
64+
5565
/// <summary>
5666
/// For providers that support versioned libraries, this represents the evergreen latest version
5767
/// </summary>

src/LibraryManager/Providers/BaseProvider.cs

+72-28
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)