Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for "fileMappings" #756

Merged
merged 6 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/LibraryManager.Contracts/FileMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System.Collections.Generic;

namespace Microsoft.Web.LibraryManager.Contracts
{
/// <summary>
///
/// </summary>
public class FileMapping
{
/// <summary>
/// Root path within the library content for this file mapping entry.
/// </summary>
public string? Root { get; set; }

/// <summary>
/// Destination folder within the project.
/// </summary>
public string? Destination { get; set; }

/// <summary>
/// The file patterns to match for this mapping, relative to <see cref="Root"/>. Accepts glob patterns.
/// </summary>
public IReadOnlyList<string>? Files { get; set; }
}
}
5 changes: 5 additions & 0 deletions src/LibraryManager.Contracts/ILibraryInstallationState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public interface ILibraryInstallationState
/// </summary>
IReadOnlyList<string> Files { get; }

/// <summary>
/// List of mappings of a portion of library assets to a unique destination.
/// </summary>
IReadOnlyList<FileMapping> FileMappings { get; }

/// <summary>
/// The path relative to the working directory to copy the files to.
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions src/LibraryManager/Json/FileMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using Newtonsoft.Json;

#nullable enable

namespace Microsoft.Web.LibraryManager.Json
{
internal class FileMapping
{
[JsonProperty(ManifestConstants.Root)]
public string? Root { get; set; }

[JsonProperty(ManifestConstants.Destination)]
public string? Destination { get; set; }

[JsonProperty(ManifestConstants.Files)]
public IReadOnlyList<string>? Files { get; set; }
}
}
5 changes: 3 additions & 2 deletions src/LibraryManager/Json/LibraryInstallationStateOnDisk.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;

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

[JsonProperty(ManifestConstants.Files)]
public IReadOnlyList<string> Files { get; set; }

[JsonProperty(ManifestConstants.FileMappings)]
public IReadOnlyList<FileMapping> FileMappings { get; set; }
}
}
17 changes: 14 additions & 3 deletions src/LibraryManager/Json/LibraryStateToFileConverter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Microsoft.Web.LibraryManager.Contracts;
Expand Down Expand Up @@ -39,7 +40,8 @@ public ILibraryInstallationState ConvertToLibraryInstallationState(LibraryInstal
IsUsingDefaultProvider = string.IsNullOrEmpty(stateOnDisk.ProviderId),
ProviderId = provider,
DestinationPath = destination,
Files = stateOnDisk.Files
Files = stateOnDisk.Files,
FileMappings = stateOnDisk.FileMappings?.Select(f => new Contracts.FileMapping { Destination = f.Destination, Root = f.Root, Files = f.Files }).ToList(),
};

return state;
Expand Down Expand Up @@ -78,13 +80,22 @@ public LibraryInstallationStateOnDisk ConvertToLibraryInstallationStateOnDisk(IL
}

string provider = string.IsNullOrEmpty(state.ProviderId) ? _defaultProvider : state.ProviderId;
return new LibraryInstallationStateOnDisk()
var serializeState = new LibraryInstallationStateOnDisk()
{
ProviderId = state.IsUsingDefaultProvider ? null : state.ProviderId,
DestinationPath = state.IsUsingDefaultDestination ? null : state.DestinationPath,
Files = state.Files,
LibraryId = LibraryIdToNameAndVersionConverter.Instance.GetLibraryId(state.Name, state.Version, provider)
LibraryId = LibraryIdToNameAndVersionConverter.Instance.GetLibraryId(state.Name, state.Version, provider),
FileMappings = state.FileMappings?.Select(f => new FileMapping { Destination = f.Destination, Root = f.Root, Files = f.Files }).ToList(),
};

if (serializeState is { FileMappings: { Count: 0} })
{
// if FileMappings is empty, omit it from serialization
serializeState.FileMappings = null;
}

return serializeState;
}
}
}
5 changes: 5 additions & 0 deletions src/LibraryManager/LibraryInstallationState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ internal class LibraryInstallationState : ILibraryInstallationState
/// </summary>
public string Version { get; set; }

/// <summary>
/// Mappings for multiple different files within the library to different destinations.
/// </summary>
public IReadOnlyList<FileMapping> FileMappings { get; set; }

/// <summary>Internal use only</summary>
public static LibraryInstallationState FromInterface(ILibraryInstallationState state,
string defaultProviderId = null,
Expand Down
2 changes: 1 addition & 1 deletion src/LibraryManager/Manifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class Manifest
/// <summary>
/// Supported versions of Library Manager
/// </summary>
public static readonly Version[] SupportedVersions = { new Version("1.0") };
public static readonly Version[] SupportedVersions = { new Version("1.0"), new Version("3.0") };
private IHostInteraction _hostInteraction;
private readonly List<ILibraryInstallationState> _libraries;
private IDependencies _dependencies;
Expand Down
26 changes: 18 additions & 8 deletions src/LibraryManager/ManifestConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,55 @@ namespace Microsoft.Web.LibraryManager
public static class ManifestConstants
{
/// <summary>
/// libman.json libraries element
/// libman.json libraries element
/// </summary>
public const string Version = "version";

/// <summary>
/// libman.json libraries element
/// libman.json libraries element
/// </summary>
public const string Libraries = "libraries";

/// <summary>
/// libman.json library element
/// libman.json library element
/// </summary>
public const string Library = "library";

/// <summary>
/// libman.json destination element
/// libman.json destination element
/// </summary>
public const string Destination = "destination";

/// <summary>
/// libman.json defaultDestination element
/// libman.json defaultDestination element
/// </summary>
public const string DefaultDestination = "defaultDestination";

/// <summary>
/// libman.json provider element
/// libman.json provider element
/// </summary>
public const string Provider = "provider";

/// <summary>
/// libman.json defaultProvider element
/// libman.json defaultProvider element
/// </summary>
public const string DefaultProvider = "defaultProvider";

/// <summary>
/// libman.json files element
/// libman.json files element
/// </summary>
public const string Files = "files";

/// <summary>
/// libman.json fileMappings element
/// </summary>
public const string FileMappings = "fileMappings";

/// <summary>
/// libman.json root element
/// </summary>
public const string Root = "root";

/// <summary>
/// For providers that support versioned libraries, this represents the evergreen latest version
/// </summary>
Expand Down
100 changes: 72 additions & 28 deletions src/LibraryManager/Providers/BaseProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,48 +247,92 @@ public async Task<OperationResult<LibraryInstallationGoalState>> GetInstallation

private OperationResult<LibraryInstallationGoalState> GenerateGoalState(ILibraryInstallationState desiredState, ILibrary library)
{
var mappings = new List<FileMapping>(desiredState.FileMappings ?? []);
List<IError> errors = null;

if (string.IsNullOrEmpty(desiredState.DestinationPath))
{
return OperationResult<LibraryInstallationGoalState>.FromError(PredefinedErrors.DestinationNotSpecified(desiredState.Name));
}

IEnumerable<string> outFiles;
if (desiredState.Files == null || desiredState.Files.Count == 0)
if (desiredState.Files is { Count: > 0 })
{
outFiles = library.Files.Keys;
mappings.Add(new FileMapping { Destination = desiredState.DestinationPath, Files = desiredState.Files });
}
else
else if (desiredState.FileMappings is null or { Count: 0 })
{
outFiles = FileGlobbingUtility.ExpandFileGlobs(desiredState.Files, library.Files.Keys);
// no files specified and no file mappings => include all files
mappings.Add(new FileMapping { Destination = desiredState.DestinationPath });
}

Dictionary<string, string> installFiles = new();
if (library.GetInvalidFiles(outFiles.ToList()) is IReadOnlyList<string> invalidFiles
&& invalidFiles.Count > 0)
{
errors ??= [];
errors.Add(PredefinedErrors.InvalidFilesInLibrary(desiredState.Name, invalidFiles, library.Files.Keys));
}

foreach (string outFile in outFiles)
foreach (FileMapping fileMapping in mappings)
{
// strip the source prefix
string destinationFile = Path.Combine(HostInteraction.WorkingDirectory, desiredState.DestinationPath, outFile);
if (!FileHelpers.IsUnderRootDirectory(destinationFile, HostInteraction.WorkingDirectory))
// if Root is not specified, assume it's the root of the library
string mappingRoot = fileMapping.Root ?? string.Empty;
// if Destination is not specified, inherit from the library entry
string destination = fileMapping.Destination ?? desiredState.DestinationPath;

if (destination is null)
{
errors ??= [];
string libraryId = LibraryNamingScheme.GetLibraryId(desiredState.Name, desiredState.Version);
errors.Add(PredefinedErrors.DestinationNotSpecified(libraryId));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a case for v1 manifests where (if they followed the schema) they could encounter this error?

Copy link
Contributor Author

@jimmylewis jimmylewis Jul 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v1 schema was declared such that either a defaultDestination must be set, or every library must set a destination. But it got too complicated to be oneOf (defaultDestination, or every library sets a destination, or (every library without fileMappings sets a destination or every fileMapping under a library without a destination includes a fileMapping destination)).

continue;
}

IReadOnlyList<string> fileFilters;
if (fileMapping.Files is { Count: > 0 })
{
fileFilters = fileMapping.Files;
}
else
{
fileFilters = ["**"];
}

if (mappingRoot.Length > 0)
{
// prefix mappingRoot to each fileFilter item
fileFilters = fileFilters.Select(f => $"{mappingRoot}/{f}").ToList();
}

List<string> outFiles = FileGlobbingUtility.ExpandFileGlobs(fileFilters, library.Files.Keys).ToList();

if (library.GetInvalidFiles(outFiles) is IReadOnlyList<string> invalidFiles
&& invalidFiles.Count > 0)
{
errors ??= [];
errors.Add(PredefinedErrors.PathOutsideWorkingDirectory());
errors.Add(PredefinedErrors.InvalidFilesInLibrary(desiredState.Name, invalidFiles, library.Files.Keys));
}
destinationFile = FileHelpers.NormalizePath(destinationFile);

// don't forget to include the cache folder in the path
string sourceFile = GetCachedFileLocalPath(desiredState, outFile);
sourceFile = FileHelpers.NormalizePath(sourceFile);
foreach (string outFile in outFiles)
{
// strip the source prefix
string relativeOutFile = mappingRoot.Length > 0 ? outFile.Substring(mappingRoot.Length + 1) : outFile;
string destinationFile = Path.Combine(HostInteraction.WorkingDirectory, destination, relativeOutFile);
destinationFile = FileHelpers.NormalizePath(destinationFile);

if (!FileHelpers.IsUnderRootDirectory(destinationFile, HostInteraction.WorkingDirectory))
{
errors ??= [];
errors.Add(PredefinedErrors.PathOutsideWorkingDirectory());
continue;
}

// map destination back to the library-relative file it originated from
installFiles.Add(destinationFile, sourceFile);
// include the cache folder in the path
string sourceFile = GetCachedFileLocalPath(desiredState, outFile);
sourceFile = FileHelpers.NormalizePath(sourceFile);

// map destination back to the library-relative file it originated from
if (installFiles.ContainsKey(destinationFile))
{
// this file is already being installed from another mapping
errors ??= [];
string libraryId = LibraryNamingScheme.GetLibraryId(desiredState.Name, desiredState.Version);
errors.Add(PredefinedErrors.LibraryCannotBeInstalledDueToConflicts(destinationFile, [libraryId]));
continue;
}
else
{
installFiles.Add(destinationFile, sourceFile);
}
}
}

if (errors is not null)
Expand Down
Loading