diff options
Diffstat (limited to 'src')
10 files changed, 753 insertions, 37 deletions
diff --git a/src/WixToolset.Core.ExtensionCache/CachedExtension.cs b/src/WixToolset.Core.ExtensionCache/CachedExtension.cs new file mode 100644 index 00000000..9ed874d9 --- /dev/null +++ b/src/WixToolset.Core.ExtensionCache/CachedExtension.cs | |||
| @@ -0,0 +1,20 @@ | |||
| 1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
| 2 | |||
| 3 | namespace WixToolset.Core.ExtensionCache | ||
| 4 | { | ||
| 5 | public class CachedExtension | ||
| 6 | { | ||
| 7 | internal CachedExtension(string id, string version, bool damaged) | ||
| 8 | { | ||
| 9 | this.Id = id; | ||
| 10 | this.Version = version; | ||
| 11 | this.Damaged = damaged; | ||
| 12 | } | ||
| 13 | |||
| 14 | public string Id { get; } | ||
| 15 | |||
| 16 | public string Version { get; } | ||
| 17 | |||
| 18 | public bool Damaged { get; } | ||
| 19 | } | ||
| 20 | } | ||
diff --git a/src/WixToolset.Core.ExtensionCache/ExtensionCacheManager.cs b/src/WixToolset.Core.ExtensionCache/ExtensionCacheManager.cs new file mode 100644 index 00000000..3ec6451e --- /dev/null +++ b/src/WixToolset.Core.ExtensionCache/ExtensionCacheManager.cs | |||
| @@ -0,0 +1,252 @@ | |||
| 1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
| 2 | |||
| 3 | namespace WixToolset.Core.ExtensionCache | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.Collections.Generic; | ||
| 7 | using System.IO; | ||
| 8 | using System.Linq; | ||
| 9 | using System.Threading; | ||
| 10 | using System.Threading.Tasks; | ||
| 11 | using NuGet.Common; | ||
| 12 | using NuGet.Configuration; | ||
| 13 | using NuGet.Credentials; | ||
| 14 | using NuGet.Packaging; | ||
| 15 | using NuGet.Protocol; | ||
| 16 | using NuGet.Protocol.Core.Types; | ||
| 17 | using NuGet.Versioning; | ||
| 18 | |||
| 19 | /// <summary> | ||
| 20 | /// Extension cache manager. | ||
| 21 | /// </summary> | ||
| 22 | public class ExtensionCacheManager | ||
| 23 | { | ||
| 24 | public string CacheFolder(bool global) => global ? this.GlobalCacheFolder() : this.LocalCacheFolder(); | ||
| 25 | |||
| 26 | public string LocalCacheFolder() => Path.Combine(Environment.CurrentDirectory, @".wix\extensions\"); | ||
| 27 | |||
| 28 | public string GlobalCacheFolder() | ||
| 29 | { | ||
| 30 | var baseFolder = Environment.GetEnvironmentVariable("WIX_EXTENSIONS") ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); | ||
| 31 | return Path.Combine(baseFolder, @".wix\extensions\"); | ||
| 32 | } | ||
| 33 | |||
| 34 | public async Task<bool> AddAsync(bool global, string extension, CancellationToken cancellationToken) | ||
| 35 | { | ||
| 36 | if (String.IsNullOrEmpty(extension)) | ||
| 37 | { | ||
| 38 | throw new ArgumentNullException(nameof(extension)); | ||
| 39 | } | ||
| 40 | |||
| 41 | (var extensionId, var extensionVersion) = ParseExtensionReference(extension); | ||
| 42 | |||
| 43 | var result = await this.DownloadAndExtractAsync(global, extensionId, extensionVersion, cancellationToken); | ||
| 44 | |||
| 45 | return result; | ||
| 46 | } | ||
| 47 | |||
| 48 | public Task<bool> RemoveAsync(bool global, string extension, CancellationToken cancellationToken) | ||
| 49 | { | ||
| 50 | if (String.IsNullOrEmpty(extension)) | ||
| 51 | { | ||
| 52 | throw new ArgumentNullException(nameof(extension)); | ||
| 53 | } | ||
| 54 | |||
| 55 | (var extensionId, var extensionVersion) = ParseExtensionReference(extension); | ||
| 56 | |||
| 57 | var cacheFolder = this.CacheFolder(global); | ||
| 58 | |||
| 59 | cacheFolder = Path.Combine(cacheFolder, extensionId, extensionVersion); | ||
| 60 | |||
| 61 | if (Directory.Exists(cacheFolder)) | ||
| 62 | { | ||
| 63 | cancellationToken.ThrowIfCancellationRequested(); | ||
| 64 | |||
| 65 | Directory.Delete(cacheFolder, true); | ||
| 66 | return Task.FromResult(true); | ||
| 67 | } | ||
| 68 | |||
| 69 | return Task.FromResult(false); | ||
| 70 | } | ||
| 71 | |||
| 72 | public Task<IEnumerable<CachedExtension>> ListAsync(bool global, string extension, CancellationToken cancellationToken) | ||
| 73 | { | ||
| 74 | var found = new List<CachedExtension>(); | ||
| 75 | |||
| 76 | (var extensionId, var extensionVersion) = ParseExtensionReference(extension); | ||
| 77 | |||
| 78 | var cacheFolder = this.CacheFolder(global); | ||
| 79 | |||
| 80 | var searchFolder = Path.Combine(cacheFolder, extensionId, extensionVersion); | ||
| 81 | |||
| 82 | if (!Directory.Exists(searchFolder)) | ||
| 83 | { | ||
| 84 | } | ||
| 85 | else if (!String.IsNullOrEmpty(extensionVersion)) // looking for an explicit version of an extension. | ||
| 86 | { | ||
| 87 | var extensionFolder = Path.Combine(cacheFolder, extensionId, extensionVersion); | ||
| 88 | if (Directory.Exists(extensionFolder)) | ||
| 89 | { | ||
| 90 | var present = ExtensionFileExists(cacheFolder, extensionId, extensionVersion); | ||
| 91 | found.Add(new CachedExtension(extensionId, extensionVersion, !present)); | ||
| 92 | } | ||
| 93 | } | ||
| 94 | else // looking for all versions of an extension or all versions of all extensions. | ||
| 95 | { | ||
| 96 | IEnumerable<string> foundExtensionIds; | ||
| 97 | |||
| 98 | if (String.IsNullOrEmpty(extensionId)) | ||
| 99 | { | ||
| 100 | // Looking for all versions of all extensions. | ||
| 101 | foundExtensionIds = Directory.GetDirectories(cacheFolder).Select(folder => Path.GetFileName(folder)).ToList(); | ||
| 102 | } | ||
| 103 | else | ||
| 104 | { | ||
| 105 | // Looking for all versions of a single extension. | ||
| 106 | var extensionFolder = Path.Combine(cacheFolder, extensionId); | ||
| 107 | foundExtensionIds = Directory.Exists(extensionFolder) ? new[] { extensionId } : Array.Empty<string>(); | ||
| 108 | } | ||
| 109 | |||
| 110 | foreach (var foundExtensionId in foundExtensionIds) | ||
| 111 | { | ||
| 112 | var extensionFolder = Path.Combine(cacheFolder, foundExtensionId); | ||
| 113 | |||
| 114 | foreach (var folder in Directory.GetDirectories(extensionFolder)) | ||
| 115 | { | ||
| 116 | cancellationToken.ThrowIfCancellationRequested(); | ||
| 117 | |||
| 118 | var foundExtensionVersion = Path.GetFileName(folder); | ||
| 119 | |||
| 120 | if (!NuGetVersion.TryParse(foundExtensionVersion, out _)) | ||
| 121 | { | ||
| 122 | continue; | ||
| 123 | } | ||
| 124 | |||
| 125 | var present = ExtensionFileExists(cacheFolder, foundExtensionId, foundExtensionVersion); | ||
| 126 | found.Add(new CachedExtension(foundExtensionId, foundExtensionVersion, !present)); | ||
| 127 | } | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | return Task.FromResult((IEnumerable<CachedExtension>)found); | ||
| 132 | } | ||
| 133 | |||
| 134 | private async Task<bool> DownloadAndExtractAsync(bool global, string id, string version, CancellationToken cancellationToken) | ||
| 135 | { | ||
| 136 | var logger = NullLogger.Instance; | ||
| 137 | |||
| 138 | DefaultCredentialServiceUtility.SetupDefaultCredentialService(logger, nonInteractive: false); | ||
| 139 | |||
| 140 | var settings = Settings.LoadDefaultSettings(root: Environment.CurrentDirectory); | ||
| 141 | var sources = PackageSourceProvider.LoadPackageSources(settings).Where(s => s.IsEnabled); | ||
| 142 | |||
| 143 | using (var cache = new SourceCacheContext()) | ||
| 144 | { | ||
| 145 | PackageSource versionSource = null; | ||
| 146 | |||
| 147 | var nugetVersion = String.IsNullOrEmpty(version) ? null : new NuGetVersion(version); | ||
| 148 | |||
| 149 | if (nugetVersion is null) | ||
| 150 | { | ||
| 151 | foreach (var source in sources) | ||
| 152 | { | ||
| 153 | var repository = Repository.Factory.GetCoreV3(source.Source); | ||
| 154 | var resource = await repository.GetResourceAsync<FindPackageByIdResource>(); | ||
| 155 | |||
| 156 | var availableVersions = await resource.GetAllVersionsAsync(id, cache, logger, cancellationToken); | ||
| 157 | foreach (var availableVersion in availableVersions) | ||
| 158 | { | ||
| 159 | if (nugetVersion is null || nugetVersion < availableVersion) | ||
| 160 | { | ||
| 161 | nugetVersion = availableVersion; | ||
| 162 | versionSource = source; | ||
| 163 | } | ||
| 164 | } | ||
| 165 | } | ||
| 166 | |||
| 167 | if (nugetVersion is null) | ||
| 168 | { | ||
| 169 | return false; | ||
| 170 | } | ||
| 171 | } | ||
| 172 | |||
| 173 | var searchSources = versionSource is null ? sources : new[] { versionSource }; | ||
| 174 | |||
| 175 | var extensionFolder = Path.Combine(this.CacheFolder(global), id, nugetVersion.ToString()); | ||
| 176 | |||
| 177 | foreach (var source in searchSources) | ||
| 178 | { | ||
| 179 | var repository = Repository.Factory.GetCoreV3(source.Source); | ||
| 180 | var resource = await repository.GetResourceAsync<FindPackageByIdResource>(); | ||
| 181 | |||
| 182 | using (var stream = new MemoryStream()) | ||
| 183 | { | ||
| 184 | var downloaded = await resource.CopyNupkgToStreamAsync(id, nugetVersion, stream, cache, logger, cancellationToken); | ||
| 185 | |||
| 186 | if (downloaded) | ||
| 187 | { | ||
| 188 | stream.Position = 0; | ||
| 189 | |||
| 190 | using (var archive = new PackageArchiveReader(stream)) | ||
| 191 | { | ||
| 192 | var files = PackagingConstants.Folders.Known.SelectMany(folder => archive.GetFiles(folder)).Distinct(StringComparer.OrdinalIgnoreCase); | ||
| 193 | await archive.CopyFilesAsync(extensionFolder, files, this.ExtractProgress, logger, cancellationToken); | ||
| 194 | } | ||
| 195 | |||
| 196 | return true; | ||
| 197 | } | ||
| 198 | } | ||
| 199 | } | ||
| 200 | } | ||
| 201 | |||
| 202 | return false; | ||
| 203 | } | ||
| 204 | |||
| 205 | private string ExtractProgress(string sourceFile, string targetPath, Stream fileStream) => fileStream.CopyToFile(targetPath); | ||
| 206 | |||
| 207 | private static (string extensionId, string extensionVersion) ParseExtensionReference(string extensionReference) | ||
| 208 | { | ||
| 209 | var extensionId = extensionReference ?? String.Empty; | ||
| 210 | var extensionVersion = String.Empty; | ||
| 211 | |||
| 212 | var index = extensionId.LastIndexOf('/'); | ||
| 213 | if (index > 0) | ||
| 214 | { | ||
| 215 | extensionVersion = extensionReference.Substring(index + 1); | ||
| 216 | extensionId = extensionReference.Substring(0, index); | ||
| 217 | |||
| 218 | if (!NuGetVersion.TryParse(extensionVersion, out _)) | ||
| 219 | { | ||
| 220 | throw new ArgumentException($"Invalid extension version in {extensionReference}"); | ||
| 221 | } | ||
| 222 | |||
| 223 | if (String.IsNullOrEmpty(extensionId)) | ||
| 224 | { | ||
| 225 | throw new ArgumentException($"Invalid extension id in {extensionReference}"); | ||
| 226 | } | ||
| 227 | } | ||
| 228 | |||
| 229 | return (extensionId, extensionVersion); | ||
| 230 | } | ||
| 231 | |||
| 232 | private static bool ExtensionFileExists(string baseFolder, string extensionId, string extensionVersion) | ||
| 233 | { | ||
| 234 | var toolsFolder = Path.Combine(baseFolder, extensionId, extensionVersion, "tools"); | ||
| 235 | if (!Directory.Exists(toolsFolder)) | ||
| 236 | { | ||
| 237 | return false; | ||
| 238 | } | ||
| 239 | |||
| 240 | var extensionAssembly = Path.Combine(toolsFolder, extensionId + ".dll"); | ||
| 241 | |||
| 242 | var present = File.Exists(extensionAssembly); | ||
| 243 | if (!present) | ||
| 244 | { | ||
| 245 | extensionAssembly = Path.Combine(toolsFolder, extensionId + ".exe"); | ||
| 246 | present = File.Exists(extensionAssembly); | ||
| 247 | } | ||
| 248 | |||
| 249 | return present; | ||
| 250 | } | ||
| 251 | } | ||
| 252 | } | ||
diff --git a/src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerCommand.cs b/src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerCommand.cs new file mode 100644 index 00000000..5016f430 --- /dev/null +++ b/src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerCommand.cs | |||
| @@ -0,0 +1,170 @@ | |||
| 1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
| 2 | |||
| 3 | namespace WixToolset.Core.ExtensionCache | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.Collections.Generic; | ||
| 7 | using System.Linq; | ||
| 8 | using System.Threading; | ||
| 9 | using System.Threading.Tasks; | ||
| 10 | using WixToolset.Extensibility.Data; | ||
| 11 | using WixToolset.Extensibility.Services; | ||
| 12 | |||
| 13 | /// <summary> | ||
| 14 | /// Extension cache manager command. | ||
| 15 | /// </summary> | ||
| 16 | internal class ExtensionCacheManagerCommand : ICommandLineCommand | ||
| 17 | { | ||
| 18 | private enum CacheSubcommand | ||
| 19 | { | ||
| 20 | Add, | ||
| 21 | Remove, | ||
| 22 | List | ||
| 23 | } | ||
| 24 | |||
| 25 | public ExtensionCacheManagerCommand(IWixToolsetServiceProvider serviceProvider) | ||
| 26 | { | ||
| 27 | this.Messaging = serviceProvider.GetService<IMessaging>(); | ||
| 28 | this.ExtensionReferences = new List<string>(); | ||
| 29 | } | ||
| 30 | |||
| 31 | private IMessaging Messaging { get; } | ||
| 32 | |||
| 33 | public bool ShowLogo { get; private set; } | ||
| 34 | |||
| 35 | public bool StopParsing { get; private set; } | ||
| 36 | |||
| 37 | private bool ShowHelp { get; set; } | ||
| 38 | |||
| 39 | private bool Global { get; set; } | ||
| 40 | |||
| 41 | private CacheSubcommand? Subcommand { get; set; } | ||
| 42 | |||
| 43 | private List<string> ExtensionReferences { get; } | ||
| 44 | |||
| 45 | public async Task<int> ExecuteAsync(CancellationToken cancellationToken) | ||
| 46 | { | ||
| 47 | if (this.ShowHelp || !this.Subcommand.HasValue) | ||
| 48 | { | ||
| 49 | DisplayHelp(); | ||
| 50 | return 1; | ||
| 51 | } | ||
| 52 | |||
| 53 | var success = false; | ||
| 54 | var cacheManager = new ExtensionCacheManager(); | ||
| 55 | |||
| 56 | switch (this.Subcommand) | ||
| 57 | { | ||
| 58 | case CacheSubcommand.Add: | ||
| 59 | success = await this.AddExtensions(cacheManager, cancellationToken); | ||
| 60 | break; | ||
| 61 | |||
| 62 | case CacheSubcommand.Remove: | ||
| 63 | success = await this.RemoveExtensions(cacheManager, cancellationToken); | ||
| 64 | break; | ||
| 65 | |||
| 66 | case CacheSubcommand.List: | ||
| 67 | success = await this.ListExtensions(cacheManager, cancellationToken); | ||
| 68 | break; | ||
| 69 | } | ||
| 70 | |||
| 71 | return success ? 0 : 2; | ||
| 72 | } | ||
| 73 | |||
| 74 | public bool TryParseArgument(ICommandLineParser parser, string argument) | ||
| 75 | { | ||
| 76 | if (!parser.IsSwitch(argument)) | ||
| 77 | { | ||
| 78 | if (!this.Subcommand.HasValue) | ||
| 79 | { | ||
| 80 | if (!Enum.TryParse(argument, true, out CacheSubcommand subcommand)) | ||
| 81 | { | ||
| 82 | return false; | ||
| 83 | } | ||
| 84 | |||
| 85 | this.Subcommand = subcommand; | ||
| 86 | } | ||
| 87 | else | ||
| 88 | { | ||
| 89 | this.ExtensionReferences.Add(argument); | ||
| 90 | } | ||
| 91 | |||
| 92 | return true; | ||
| 93 | } | ||
| 94 | |||
| 95 | var parameter = argument.Substring(1); | ||
| 96 | switch (parameter.ToLowerInvariant()) | ||
| 97 | { | ||
| 98 | case "?": | ||
| 99 | this.ShowHelp = true; | ||
| 100 | this.ShowLogo = true; | ||
| 101 | this.StopParsing = true; | ||
| 102 | return true; | ||
| 103 | |||
| 104 | case "nologo": | ||
| 105 | this.ShowLogo = false; | ||
| 106 | return true; | ||
| 107 | |||
| 108 | case "g": | ||
| 109 | case "-global": | ||
| 110 | this.Global = true; | ||
| 111 | return true; | ||
| 112 | } | ||
| 113 | |||
| 114 | return false; | ||
| 115 | } | ||
| 116 | |||
| 117 | private async Task<bool> AddExtensions(ExtensionCacheManager cacheManager, CancellationToken cancellationToken) | ||
| 118 | { | ||
| 119 | var success = true; | ||
| 120 | |||
| 121 | foreach (var extensionRef in this.ExtensionReferences) | ||
| 122 | { | ||
| 123 | var added = await cacheManager.AddAsync(this.Global, extensionRef, cancellationToken); | ||
| 124 | success |= added; | ||
| 125 | } | ||
| 126 | |||
| 127 | return success; | ||
| 128 | } | ||
| 129 | |||
| 130 | private async Task<bool> RemoveExtensions(ExtensionCacheManager cacheManager, CancellationToken cancellationToken) | ||
| 131 | { | ||
| 132 | var success = true; | ||
| 133 | |||
| 134 | foreach (var extensionRef in this.ExtensionReferences) | ||
| 135 | { | ||
| 136 | var removed = await cacheManager.RemoveAsync(this.Global, extensionRef, cancellationToken); | ||
| 137 | success |= removed; | ||
| 138 | } | ||
| 139 | |||
| 140 | return success; | ||
| 141 | } | ||
| 142 | |||
| 143 | private async Task<bool> ListExtensions(ExtensionCacheManager cacheManager, CancellationToken cancellationToken) | ||
| 144 | { | ||
| 145 | var found = false; | ||
| 146 | var extensionRef = this.ExtensionReferences.FirstOrDefault(); | ||
| 147 | |||
| 148 | var extensions = await cacheManager.ListAsync(this.Global, extensionRef, cancellationToken); | ||
| 149 | |||
| 150 | foreach (var extension in extensions) | ||
| 151 | { | ||
| 152 | this.Messaging.Write($"{extension.Id} {extension.Version}{(extension.Damaged ? " (damaged)" : String.Empty)}"); | ||
| 153 | found = true; | ||
| 154 | } | ||
| 155 | |||
| 156 | return found; | ||
| 157 | } | ||
| 158 | |||
| 159 | private static void DisplayHelp() | ||
| 160 | { | ||
| 161 | Console.WriteLine(" usage: wix.exe extension add|remove|list [extensionRef]"); | ||
| 162 | Console.WriteLine(); | ||
| 163 | Console.WriteLine(" -g add/remove the extension for the current user"); | ||
| 164 | Console.WriteLine(" -nologo suppress displaying the logo information"); | ||
| 165 | Console.WriteLine(" -? this help information"); | ||
| 166 | Console.WriteLine(); | ||
| 167 | Console.WriteLine(" extensionRef format: extensionId/version (the version is optional)"); | ||
| 168 | } | ||
| 169 | } | ||
| 170 | } | ||
diff --git a/src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerExtensionCommandLine.cs b/src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerExtensionCommandLine.cs new file mode 100644 index 00000000..81e96718 --- /dev/null +++ b/src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerExtensionCommandLine.cs | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
| 2 | |||
| 3 | namespace WixToolset.Core.ExtensionCache | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.Collections.Generic; | ||
| 7 | using WixToolset.Extensibility; | ||
| 8 | using WixToolset.Extensibility.Data; | ||
| 9 | using WixToolset.Extensibility.Services; | ||
| 10 | |||
| 11 | /// <summary> | ||
| 12 | /// Parses the "extension" command-line command. See <c>ExtensionCacheManagerCommand</c> | ||
| 13 | /// for the bulk of the command-line processing. | ||
| 14 | /// </summary> | ||
| 15 | internal class ExtensionCacheManagerExtensionCommandLine : BaseExtensionCommandLine | ||
| 16 | { | ||
| 17 | public ExtensionCacheManagerExtensionCommandLine(IWixToolsetServiceProvider serviceProvider) | ||
| 18 | { | ||
| 19 | this.ServiceProvider = serviceProvider; | ||
| 20 | } | ||
| 21 | |||
| 22 | private IWixToolsetServiceProvider ServiceProvider { get; } | ||
| 23 | |||
| 24 | // TODO: Do something with CommandLineSwitches | ||
| 25 | public override IEnumerable<ExtensionCommandLineSwitch> CommandLineSwitches => base.CommandLineSwitches; | ||
| 26 | |||
| 27 | public override bool TryParseCommand(ICommandLineParser parser, string argument, out ICommandLineCommand command) | ||
| 28 | { | ||
| 29 | command = null; | ||
| 30 | |||
| 31 | if ("extension".Equals(argument, StringComparison.OrdinalIgnoreCase)) | ||
| 32 | { | ||
| 33 | command = new ExtensionCacheManagerCommand(this.ServiceProvider); | ||
| 34 | } | ||
| 35 | |||
| 36 | return command != null; | ||
| 37 | } | ||
| 38 | } | ||
| 39 | } | ||
diff --git a/src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerExtensionFactory.cs b/src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerExtensionFactory.cs new file mode 100644 index 00000000..44fc4b86 --- /dev/null +++ b/src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerExtensionFactory.cs | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
| 2 | |||
| 3 | namespace WixToolset.Core.ExtensionCache | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using WixToolset.Extensibility; | ||
| 7 | using WixToolset.Extensibility.Services; | ||
| 8 | |||
| 9 | internal class ExtensionCacheManagerExtensionFactory : IExtensionFactory | ||
| 10 | { | ||
| 11 | public ExtensionCacheManagerExtensionFactory(IWixToolsetServiceProvider serviceProvider) | ||
| 12 | { | ||
| 13 | this.ServiceProvider = serviceProvider; | ||
| 14 | } | ||
| 15 | |||
| 16 | private IWixToolsetServiceProvider ServiceProvider { get; } | ||
| 17 | |||
| 18 | public bool TryCreateExtension(Type extensionType, out object extension) | ||
| 19 | { | ||
| 20 | extension = null; | ||
| 21 | |||
| 22 | if (extensionType == typeof(IExtensionCommandLine)) | ||
| 23 | { | ||
| 24 | extension = new ExtensionCacheManagerExtensionCommandLine(this.ServiceProvider); | ||
| 25 | } | ||
| 26 | |||
| 27 | return extension != null; | ||
| 28 | } | ||
| 29 | } | ||
| 30 | } | ||
diff --git a/src/WixToolset.Core.ExtensionCache/WixToolset.Core.ExtensionCache.csproj b/src/WixToolset.Core.ExtensionCache/WixToolset.Core.ExtensionCache.csproj new file mode 100644 index 00000000..7ae5cdbb --- /dev/null +++ b/src/WixToolset.Core.ExtensionCache/WixToolset.Core.ExtensionCache.csproj | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. --> | ||
| 3 | |||
| 4 | <Project Sdk="Microsoft.NET.Sdk"> | ||
| 5 | <PropertyGroup> | ||
| 6 | <TargetFrameworks>netstandard2.0;net461;net472</TargetFrameworks> | ||
| 7 | <Description>Extension Cache</Description> | ||
| 8 | <Title>WiX Toolset Extension Cache</Title> | ||
| 9 | <DebugType>embedded</DebugType> | ||
| 10 | <PublishRepositoryUrl>true</PublishRepositoryUrl> | ||
| 11 | </PropertyGroup> | ||
| 12 | |||
| 13 | <ItemGroup> | ||
| 14 | <ProjectReference Include="..\WixToolset.Core\WixToolset.Core.csproj" /> | ||
| 15 | </ItemGroup> | ||
| 16 | |||
| 17 | <ItemGroup> | ||
| 18 | <PackageReference Include="NuGet.Credentials" Version="5.6.0" /> | ||
| 19 | <PackageReference Include="NuGet.Protocol" Version="5.6.0" /> | ||
| 20 | </ItemGroup> | ||
| 21 | |||
| 22 | <ItemGroup> | ||
| 23 | <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> | ||
| 24 | <PackageReference Include="Nerdbank.GitVersioning" Version="2.1.65" PrivateAssets="All" /> | ||
| 25 | </ItemGroup> | ||
| 26 | </Project> | ||
diff --git a/src/WixToolset.Core.ExtensionCache/WixToolsetCoreServiceProviderExtensions.cs b/src/WixToolset.Core.ExtensionCache/WixToolsetCoreServiceProviderExtensions.cs new file mode 100644 index 00000000..c1579330 --- /dev/null +++ b/src/WixToolset.Core.ExtensionCache/WixToolsetCoreServiceProviderExtensions.cs | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
| 2 | |||
| 3 | namespace WixToolset.Core.ExtensionCache | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.Collections.Generic; | ||
| 7 | using WixToolset.Extensibility.Services; | ||
| 8 | |||
| 9 | public static class WixToolsetCoreServiceProviderExtensions | ||
| 10 | { | ||
| 11 | public static IWixToolsetCoreServiceProvider AddExtensionCacheManager(this IWixToolsetCoreServiceProvider serviceProvider) | ||
| 12 | { | ||
| 13 | var extensionManager = serviceProvider.GetService<IExtensionManager>(); | ||
| 14 | extensionManager.Add(typeof(ExtensionCacheManagerExtensionFactory).Assembly); | ||
| 15 | |||
| 16 | serviceProvider.AddService(CreateExtensionCacheManager); | ||
| 17 | return serviceProvider; | ||
| 18 | } | ||
| 19 | |||
| 20 | private static ExtensionCacheManager CreateExtensionCacheManager(IWixToolsetCoreServiceProvider provider, Dictionary<Type, object> singletons) | ||
| 21 | { | ||
| 22 | var extensionCacheManager = new ExtensionCacheManager(); | ||
| 23 | singletons.Add(typeof(ExtensionCacheManager), extensionCacheManager); | ||
| 24 | |||
| 25 | return extensionCacheManager; | ||
| 26 | } | ||
| 27 | } | ||
| 28 | } | ||
diff --git a/src/WixToolset.Core/ExtensibilityServices/ExtensionManager.cs b/src/WixToolset.Core/ExtensibilityServices/ExtensionManager.cs index 97216479..f71c0fd1 100644 --- a/src/WixToolset.Core/ExtensibilityServices/ExtensionManager.cs +++ b/src/WixToolset.Core/ExtensibilityServices/ExtensionManager.cs | |||
| @@ -28,59 +28,65 @@ namespace WixToolset.Core.ExtensibilityServices | |||
| 28 | var types = extensionAssembly.GetTypes().Where(t => !t.IsAbstract && !t.IsInterface && typeof(IExtensionFactory).IsAssignableFrom(t)); | 28 | var types = extensionAssembly.GetTypes().Where(t => !t.IsAbstract && !t.IsInterface && typeof(IExtensionFactory).IsAssignableFrom(t)); |
| 29 | var factories = types.Select(this.CreateExtensionFactory).ToList(); | 29 | var factories = types.Select(this.CreateExtensionFactory).ToList(); |
| 30 | 30 | ||
| 31 | this.extensionFactories.AddRange(factories); | 31 | if (!factories.Any()) |
| 32 | } | ||
| 33 | |||
| 34 | private IExtensionFactory CreateExtensionFactory(Type type) | ||
| 35 | { | ||
| 36 | var constructor = type.GetConstructor(new[] { typeof(IWixToolsetCoreServiceProvider) }); | ||
| 37 | if (constructor != null) | ||
| 38 | { | 32 | { |
| 39 | return (IExtensionFactory)constructor.Invoke(new[] { this.ServiceProvider }); | 33 | var path = Path.GetFullPath(new Uri(extensionAssembly.CodeBase).LocalPath); |
| 34 | throw new WixException(ErrorMessages.InvalidExtension(path, "The extension does not implement IExtensionFactory. All extensions must have at least one implementation of IExtensionFactory.")); | ||
| 40 | } | 35 | } |
| 41 | 36 | ||
| 42 | return (IExtensionFactory)Activator.CreateInstance(type); | 37 | this.extensionFactories.AddRange(factories); |
| 43 | } | 38 | } |
| 44 | 39 | ||
| 45 | public void Load(string extensionPath) | 40 | public void Load(string extensionPath) |
| 46 | { | 41 | { |
| 42 | var checkPath = extensionPath; | ||
| 43 | var checkedPaths = new List<string> { checkPath }; | ||
| 47 | try | 44 | try |
| 48 | { | 45 | { |
| 49 | Assembly assembly; | 46 | if (!TryLoadFromPath(checkPath, out var assembly) && !Path.IsPathRooted(extensionPath)) |
| 50 | |||
| 51 | // Absolute path to an assembly which means only "load from" will work even though we'd prefer to | ||
| 52 | // use Assembly.Load (see the documentation for Assembly.LoadFrom why). | ||
| 53 | if (Path.IsPathRooted(extensionPath)) | ||
| 54 | { | 47 | { |
| 55 | assembly = Assembly.LoadFrom(extensionPath); | 48 | if (TryParseExtensionReference(extensionPath, out var extensionId, out var extensionVersion)) |
| 56 | } | 49 | { |
| 57 | else if (ExtensionManager.TryExtensionLoad(extensionPath, out assembly)) | 50 | foreach (var cachePath in this.CacheLocations()) |
| 58 | { | 51 | { |
| 59 | // Loaded the assembly by name from the probing path. | 52 | var extensionFolder = Path.Combine(cachePath, extensionId); |
| 60 | } | 53 | |
| 61 | else if (ExtensionManager.TryExtensionLoad(Path.GetFileNameWithoutExtension(extensionPath), out assembly)) | 54 | var versionFolder = extensionVersion; |
| 62 | { | 55 | if (String.IsNullOrEmpty(versionFolder) && !TryFindLatestVersionInFolder(extensionFolder, out versionFolder)) |
| 63 | // Loaded the assembly by filename alone along the probing path. | 56 | { |
| 57 | checkedPaths.Add(extensionFolder); | ||
| 58 | continue; | ||
| 59 | } | ||
| 60 | |||
| 61 | checkPath = Path.Combine(extensionFolder, versionFolder, "tools", extensionId + ".dll"); | ||
| 62 | checkedPaths.Add(checkPath); | ||
| 63 | |||
| 64 | if (TryLoadFromPath(checkPath, out assembly)) | ||
| 65 | { | ||
| 66 | break; | ||
| 67 | } | ||
| 68 | } | ||
| 69 | } | ||
| 64 | } | 70 | } |
| 65 | else // relative path to an assembly | 71 | |
| 72 | if (assembly == null) | ||
| 66 | { | 73 | { |
| 67 | // We want to use Assembly.Load when we can because it has some benefits over Assembly.LoadFrom | 74 | throw new WixException(ErrorMessages.CouldNotFindExtensionInPaths(extensionPath, checkedPaths)); |
| 68 | // (see the documentation for Assembly.LoadFrom). However, it may fail when the path is a relative | ||
| 69 | // path, so we should try Assembly.LoadFrom one last time. We could have detected a directory | ||
| 70 | // separator character and used Assembly.LoadFrom directly, but dealing with path canonicalization | ||
| 71 | // issues is something we don't want to deal with if we don't have to. | ||
| 72 | assembly = Assembly.LoadFrom(extensionPath); | ||
| 73 | } | 75 | } |
| 74 | 76 | ||
| 75 | this.Add(assembly); | 77 | this.Add(assembly); |
| 76 | } | 78 | } |
| 77 | catch (ReflectionTypeLoadException rtle) | 79 | catch (ReflectionTypeLoadException rtle) |
| 78 | { | 80 | { |
| 79 | throw new WixException(ErrorMessages.InvalidExtension(extensionPath, String.Join(Environment.NewLine, rtle.LoaderExceptions.Select(le => le.ToString())))); | 81 | throw new WixException(ErrorMessages.InvalidExtension(checkPath, String.Join(Environment.NewLine, rtle.LoaderExceptions.Select(le => le.ToString())))); |
| 82 | } | ||
| 83 | catch (WixException) | ||
| 84 | { | ||
| 85 | throw; | ||
| 80 | } | 86 | } |
| 81 | catch (Exception e) | 87 | catch (Exception e) |
| 82 | { | 88 | { |
| 83 | throw new WixException(ErrorMessages.InvalidExtension(extensionPath, e.Message), e); | 89 | throw new WixException(ErrorMessages.InvalidExtension(checkPath, e.Message), e); |
| 84 | } | 90 | } |
| 85 | } | 91 | } |
| 86 | 92 | ||
| @@ -104,18 +110,120 @@ namespace WixToolset.Core.ExtensibilityServices | |||
| 104 | return extensions.Cast<T>().ToList(); | 110 | return extensions.Cast<T>().ToList(); |
| 105 | } | 111 | } |
| 106 | 112 | ||
| 107 | private static bool TryExtensionLoad(string assemblyName, out Assembly assembly) | 113 | private IExtensionFactory CreateExtensionFactory(Type type) |
| 114 | { | ||
| 115 | var constructor = type.GetConstructor(new[] { typeof(IWixToolsetCoreServiceProvider) }); | ||
| 116 | if (constructor != null) | ||
| 117 | { | ||
| 118 | return (IExtensionFactory)constructor.Invoke(new[] { this.ServiceProvider }); | ||
| 119 | } | ||
| 120 | |||
| 121 | return (IExtensionFactory)Activator.CreateInstance(type); | ||
| 122 | } | ||
| 123 | |||
| 124 | private IEnumerable<string> CacheLocations() | ||
| 125 | { | ||
| 126 | var path = Path.Combine(Environment.CurrentDirectory, @".wix\extensions\"); | ||
| 127 | if (Directory.Exists(path)) | ||
| 128 | { | ||
| 129 | yield return path; | ||
| 130 | } | ||
| 131 | |||
| 132 | path = Environment.GetEnvironmentVariable("WIX_EXTENSIONS") ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); | ||
| 133 | path = Path.Combine(path, @".wix\extensions\"); | ||
| 134 | if (Directory.Exists(path)) | ||
| 135 | { | ||
| 136 | yield return path; | ||
| 137 | } | ||
| 138 | |||
| 139 | if (Environment.Is64BitOperatingSystem) | ||
| 140 | { | ||
| 141 | path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles), @"WixToolset\extensions\"); | ||
| 142 | if (Directory.Exists(path)) | ||
| 143 | { | ||
| 144 | yield return path; | ||
| 145 | } | ||
| 146 | } | ||
| 147 | |||
| 148 | path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFilesX86), @"WixToolset\extensions\"); | ||
| 149 | if (Directory.Exists(path)) | ||
| 150 | { | ||
| 151 | yield return path; | ||
| 152 | } | ||
| 153 | |||
| 154 | path = Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetCallingAssembly().CodeBase).LocalPath), @"extensions\"); | ||
| 155 | if (Directory.Exists(path)) | ||
| 156 | { | ||
| 157 | yield return path; | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | private static bool TryParseExtensionReference(string extensionReference, out string extensionId, out string extensionVersion) | ||
| 162 | { | ||
| 163 | extensionId = extensionReference ?? String.Empty; | ||
| 164 | extensionVersion = String.Empty; | ||
| 165 | |||
| 166 | var index = extensionId.LastIndexOf('/'); | ||
| 167 | if (index > 0) | ||
| 168 | { | ||
| 169 | extensionVersion = extensionReference.Substring(index + 1); | ||
| 170 | extensionId = extensionReference.Substring(0, index); | ||
| 171 | |||
| 172 | if (!NuGet.Versioning.NuGetVersion.TryParse(extensionVersion, out _)) | ||
| 173 | { | ||
| 174 | return false; | ||
| 175 | } | ||
| 176 | |||
| 177 | if (String.IsNullOrEmpty(extensionId)) | ||
| 178 | { | ||
| 179 | return false; | ||
| 180 | } | ||
| 181 | } | ||
| 182 | |||
| 183 | return true; | ||
| 184 | } | ||
| 185 | |||
| 186 | private static bool TryFindLatestVersionInFolder(string basePath, out string foundVersionFolder) | ||
| 187 | { | ||
| 188 | foundVersionFolder = null; | ||
| 189 | |||
| 190 | try | ||
| 191 | { | ||
| 192 | NuGet.Versioning.NuGetVersion version = null; | ||
| 193 | foreach (var versionPath in Directory.GetDirectories(basePath)) | ||
| 194 | { | ||
| 195 | var versionFolder = Path.GetFileName(versionPath); | ||
| 196 | if (NuGet.Versioning.NuGetVersion.TryParse(versionFolder, out var checkVersion) && | ||
| 197 | (version == null || version < checkVersion)) | ||
| 198 | { | ||
| 199 | foundVersionFolder = versionFolder; | ||
| 200 | version = checkVersion; | ||
| 201 | } | ||
| 202 | } | ||
| 203 | } | ||
| 204 | catch (IOException) | ||
| 205 | { | ||
| 206 | } | ||
| 207 | |||
| 208 | return !String.IsNullOrEmpty(foundVersionFolder); | ||
| 209 | } | ||
| 210 | |||
| 211 | private static bool TryLoadFromPath(string extensionPath, out Assembly assembly) | ||
| 108 | { | 212 | { |
| 109 | try | 213 | try |
| 110 | { | 214 | { |
| 111 | assembly = Assembly.Load(assemblyName); | 215 | if (File.Exists(extensionPath)) |
| 112 | return true; | 216 | { |
| 217 | assembly = Assembly.LoadFrom(extensionPath); | ||
| 218 | return true; | ||
| 219 | } | ||
| 113 | } | 220 | } |
| 114 | catch (IOException e) when (e is FileLoadException || e is FileNotFoundException) | 221 | catch (IOException e) when (e is FileLoadException || e is FileNotFoundException) |
| 115 | { | 222 | { |
| 116 | assembly = null; | ||
| 117 | return false; | ||
| 118 | } | 223 | } |
| 224 | |||
| 225 | assembly = null; | ||
| 226 | return false; | ||
| 119 | } | 227 | } |
| 120 | } | 228 | } |
| 121 | } | 229 | } |
diff --git a/src/WixToolset.Core/WixToolset.Core.csproj b/src/WixToolset.Core/WixToolset.Core.csproj index 3e7bea3b..41ab626e 100644 --- a/src/WixToolset.Core/WixToolset.Core.csproj +++ b/src/WixToolset.Core/WixToolset.Core.csproj | |||
| @@ -22,6 +22,7 @@ | |||
| 22 | <ItemGroup> | 22 | <ItemGroup> |
| 23 | <PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.6.0" /> | 23 | <PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.6.0" /> |
| 24 | <PackageReference Include="System.Text.Encoding.CodePages" Version="4.6.0" /> | 24 | <PackageReference Include="System.Text.Encoding.CodePages" Version="4.6.0" /> |
| 25 | <PackageReference Include="NuGet.Versioning" Version="5.6.0" /> | ||
| 25 | </ItemGroup> | 26 | </ItemGroup> |
| 26 | 27 | ||
| 27 | <ItemGroup> | 28 | <ItemGroup> |
diff --git a/src/test/WixToolsetTest.CoreIntegration/ExtensionFixture.cs b/src/test/WixToolsetTest.CoreIntegration/ExtensionFixture.cs index ca7ce0c0..bad7f3ef 100644 --- a/src/test/WixToolsetTest.CoreIntegration/ExtensionFixture.cs +++ b/src/test/WixToolsetTest.CoreIntegration/ExtensionFixture.cs | |||
| @@ -102,6 +102,48 @@ namespace WixToolsetTest.CoreIntegration | |||
| 102 | } | 102 | } |
| 103 | } | 103 | } |
| 104 | 104 | ||
| 105 | [Fact] | ||
| 106 | public void CannotBuildWithMissingExtension() | ||
| 107 | { | ||
| 108 | var folder = TestData.Get(@"TestData\ExampleExtension"); | ||
| 109 | |||
| 110 | using (var fs = new DisposableFileSystem()) | ||
| 111 | { | ||
| 112 | var intermediateFolder = fs.GetFolder(); | ||
| 113 | |||
| 114 | var exception = Assert.Throws<WixException>(() => | ||
| 115 | WixRunner.Execute(new[] | ||
| 116 | { | ||
| 117 | "build", | ||
| 118 | Path.Combine(folder, "Package.wxs"), | ||
| 119 | "-ext", "ExampleExtension.DoesNotExist" | ||
| 120 | })); | ||
| 121 | |||
| 122 | Assert.StartsWith("The extension 'ExampleExtension.DoesNotExist' could not be found. Checked paths: ", exception.Message); | ||
| 123 | } | ||
| 124 | } | ||
| 125 | |||
| 126 | [Fact] | ||
| 127 | public void CannotBuildWithMissingVersionedExtension() | ||
| 128 | { | ||
| 129 | var folder = TestData.Get(@"TestData\ExampleExtension"); | ||
| 130 | |||
| 131 | using (var fs = new DisposableFileSystem()) | ||
| 132 | { | ||
| 133 | var intermediateFolder = fs.GetFolder(); | ||
| 134 | |||
| 135 | var exception = Assert.Throws<WixException>(() => | ||
| 136 | WixRunner.Execute(new[] | ||
| 137 | { | ||
| 138 | "build", | ||
| 139 | Path.Combine(folder, "Package.wxs"), | ||
| 140 | "-ext", "ExampleExtension.DoesNotExist/1.0.0" | ||
| 141 | })); | ||
| 142 | |||
| 143 | Assert.StartsWith("The extension 'ExampleExtension.DoesNotExist/1.0.0' could not be found. Checked paths: ", exception.Message); | ||
| 144 | } | ||
| 145 | } | ||
| 146 | |||
| 105 | private static void Build(string[] args) | 147 | private static void Build(string[] args) |
| 106 | { | 148 | { |
| 107 | var result = WixRunner.Execute(args) | 149 | var result = WixRunner.Execute(args) |
