From 02cdf55197d599d4d1fd611ad749d01f5c47a01f Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Mon, 8 Jun 2020 16:26:59 -0700 Subject: Add "extension" command --- .../CachedExtension.cs | 20 ++ .../ExtensionCacheManager.cs | 252 +++++++++++++++++++++ .../ExtensionCacheManagerCommand.cs | 170 ++++++++++++++ .../ExtensionCacheManagerExtensionCommandLine.cs | 39 ++++ .../ExtensionCacheManagerExtensionFactory.cs | 30 +++ .../WixToolset.Core.ExtensionCache.csproj | 26 +++ .../WixToolsetCoreServiceProviderExtensions.cs | 28 +++ .../ExtensibilityServices/ExtensionManager.cs | 182 ++++++++++++--- src/WixToolset.Core/WixToolset.Core.csproj | 1 + .../ExtensionFixture.cs | 42 ++++ 10 files changed, 753 insertions(+), 37 deletions(-) create mode 100644 src/WixToolset.Core.ExtensionCache/CachedExtension.cs create mode 100644 src/WixToolset.Core.ExtensionCache/ExtensionCacheManager.cs create mode 100644 src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerCommand.cs create mode 100644 src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerExtensionCommandLine.cs create mode 100644 src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerExtensionFactory.cs create mode 100644 src/WixToolset.Core.ExtensionCache/WixToolset.Core.ExtensionCache.csproj create mode 100644 src/WixToolset.Core.ExtensionCache/WixToolsetCoreServiceProviderExtensions.cs 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 @@ +// 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. + +namespace WixToolset.Core.ExtensionCache +{ + public class CachedExtension + { + internal CachedExtension(string id, string version, bool damaged) + { + this.Id = id; + this.Version = version; + this.Damaged = damaged; + } + + public string Id { get; } + + public string Version { get; } + + public bool Damaged { get; } + } +} 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 @@ +// 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. + +namespace WixToolset.Core.ExtensionCache +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using NuGet.Common; + using NuGet.Configuration; + using NuGet.Credentials; + using NuGet.Packaging; + using NuGet.Protocol; + using NuGet.Protocol.Core.Types; + using NuGet.Versioning; + + /// + /// Extension cache manager. + /// + public class ExtensionCacheManager + { + public string CacheFolder(bool global) => global ? this.GlobalCacheFolder() : this.LocalCacheFolder(); + + public string LocalCacheFolder() => Path.Combine(Environment.CurrentDirectory, @".wix\extensions\"); + + public string GlobalCacheFolder() + { + var baseFolder = Environment.GetEnvironmentVariable("WIX_EXTENSIONS") ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + return Path.Combine(baseFolder, @".wix\extensions\"); + } + + public async Task AddAsync(bool global, string extension, CancellationToken cancellationToken) + { + if (String.IsNullOrEmpty(extension)) + { + throw new ArgumentNullException(nameof(extension)); + } + + (var extensionId, var extensionVersion) = ParseExtensionReference(extension); + + var result = await this.DownloadAndExtractAsync(global, extensionId, extensionVersion, cancellationToken); + + return result; + } + + public Task RemoveAsync(bool global, string extension, CancellationToken cancellationToken) + { + if (String.IsNullOrEmpty(extension)) + { + throw new ArgumentNullException(nameof(extension)); + } + + (var extensionId, var extensionVersion) = ParseExtensionReference(extension); + + var cacheFolder = this.CacheFolder(global); + + cacheFolder = Path.Combine(cacheFolder, extensionId, extensionVersion); + + if (Directory.Exists(cacheFolder)) + { + cancellationToken.ThrowIfCancellationRequested(); + + Directory.Delete(cacheFolder, true); + return Task.FromResult(true); + } + + return Task.FromResult(false); + } + + public Task> ListAsync(bool global, string extension, CancellationToken cancellationToken) + { + var found = new List(); + + (var extensionId, var extensionVersion) = ParseExtensionReference(extension); + + var cacheFolder = this.CacheFolder(global); + + var searchFolder = Path.Combine(cacheFolder, extensionId, extensionVersion); + + if (!Directory.Exists(searchFolder)) + { + } + else if (!String.IsNullOrEmpty(extensionVersion)) // looking for an explicit version of an extension. + { + var extensionFolder = Path.Combine(cacheFolder, extensionId, extensionVersion); + if (Directory.Exists(extensionFolder)) + { + var present = ExtensionFileExists(cacheFolder, extensionId, extensionVersion); + found.Add(new CachedExtension(extensionId, extensionVersion, !present)); + } + } + else // looking for all versions of an extension or all versions of all extensions. + { + IEnumerable foundExtensionIds; + + if (String.IsNullOrEmpty(extensionId)) + { + // Looking for all versions of all extensions. + foundExtensionIds = Directory.GetDirectories(cacheFolder).Select(folder => Path.GetFileName(folder)).ToList(); + } + else + { + // Looking for all versions of a single extension. + var extensionFolder = Path.Combine(cacheFolder, extensionId); + foundExtensionIds = Directory.Exists(extensionFolder) ? new[] { extensionId } : Array.Empty(); + } + + foreach (var foundExtensionId in foundExtensionIds) + { + var extensionFolder = Path.Combine(cacheFolder, foundExtensionId); + + foreach (var folder in Directory.GetDirectories(extensionFolder)) + { + cancellationToken.ThrowIfCancellationRequested(); + + var foundExtensionVersion = Path.GetFileName(folder); + + if (!NuGetVersion.TryParse(foundExtensionVersion, out _)) + { + continue; + } + + var present = ExtensionFileExists(cacheFolder, foundExtensionId, foundExtensionVersion); + found.Add(new CachedExtension(foundExtensionId, foundExtensionVersion, !present)); + } + } + } + + return Task.FromResult((IEnumerable)found); + } + + private async Task DownloadAndExtractAsync(bool global, string id, string version, CancellationToken cancellationToken) + { + var logger = NullLogger.Instance; + + DefaultCredentialServiceUtility.SetupDefaultCredentialService(logger, nonInteractive: false); + + var settings = Settings.LoadDefaultSettings(root: Environment.CurrentDirectory); + var sources = PackageSourceProvider.LoadPackageSources(settings).Where(s => s.IsEnabled); + + using (var cache = new SourceCacheContext()) + { + PackageSource versionSource = null; + + var nugetVersion = String.IsNullOrEmpty(version) ? null : new NuGetVersion(version); + + if (nugetVersion is null) + { + foreach (var source in sources) + { + var repository = Repository.Factory.GetCoreV3(source.Source); + var resource = await repository.GetResourceAsync(); + + var availableVersions = await resource.GetAllVersionsAsync(id, cache, logger, cancellationToken); + foreach (var availableVersion in availableVersions) + { + if (nugetVersion is null || nugetVersion < availableVersion) + { + nugetVersion = availableVersion; + versionSource = source; + } + } + } + + if (nugetVersion is null) + { + return false; + } + } + + var searchSources = versionSource is null ? sources : new[] { versionSource }; + + var extensionFolder = Path.Combine(this.CacheFolder(global), id, nugetVersion.ToString()); + + foreach (var source in searchSources) + { + var repository = Repository.Factory.GetCoreV3(source.Source); + var resource = await repository.GetResourceAsync(); + + using (var stream = new MemoryStream()) + { + var downloaded = await resource.CopyNupkgToStreamAsync(id, nugetVersion, stream, cache, logger, cancellationToken); + + if (downloaded) + { + stream.Position = 0; + + using (var archive = new PackageArchiveReader(stream)) + { + var files = PackagingConstants.Folders.Known.SelectMany(folder => archive.GetFiles(folder)).Distinct(StringComparer.OrdinalIgnoreCase); + await archive.CopyFilesAsync(extensionFolder, files, this.ExtractProgress, logger, cancellationToken); + } + + return true; + } + } + } + } + + return false; + } + + private string ExtractProgress(string sourceFile, string targetPath, Stream fileStream) => fileStream.CopyToFile(targetPath); + + private static (string extensionId, string extensionVersion) ParseExtensionReference(string extensionReference) + { + var extensionId = extensionReference ?? String.Empty; + var extensionVersion = String.Empty; + + var index = extensionId.LastIndexOf('/'); + if (index > 0) + { + extensionVersion = extensionReference.Substring(index + 1); + extensionId = extensionReference.Substring(0, index); + + if (!NuGetVersion.TryParse(extensionVersion, out _)) + { + throw new ArgumentException($"Invalid extension version in {extensionReference}"); + } + + if (String.IsNullOrEmpty(extensionId)) + { + throw new ArgumentException($"Invalid extension id in {extensionReference}"); + } + } + + return (extensionId, extensionVersion); + } + + private static bool ExtensionFileExists(string baseFolder, string extensionId, string extensionVersion) + { + var toolsFolder = Path.Combine(baseFolder, extensionId, extensionVersion, "tools"); + if (!Directory.Exists(toolsFolder)) + { + return false; + } + + var extensionAssembly = Path.Combine(toolsFolder, extensionId + ".dll"); + + var present = File.Exists(extensionAssembly); + if (!present) + { + extensionAssembly = Path.Combine(toolsFolder, extensionId + ".exe"); + present = File.Exists(extensionAssembly); + } + + return present; + } + } +} 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 @@ +// 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. + +namespace WixToolset.Core.ExtensionCache +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using WixToolset.Extensibility.Data; + using WixToolset.Extensibility.Services; + + /// + /// Extension cache manager command. + /// + internal class ExtensionCacheManagerCommand : ICommandLineCommand + { + private enum CacheSubcommand + { + Add, + Remove, + List + } + + public ExtensionCacheManagerCommand(IWixToolsetServiceProvider serviceProvider) + { + this.Messaging = serviceProvider.GetService(); + this.ExtensionReferences = new List(); + } + + private IMessaging Messaging { get; } + + public bool ShowLogo { get; private set; } + + public bool StopParsing { get; private set; } + + private bool ShowHelp { get; set; } + + private bool Global { get; set; } + + private CacheSubcommand? Subcommand { get; set; } + + private List ExtensionReferences { get; } + + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + if (this.ShowHelp || !this.Subcommand.HasValue) + { + DisplayHelp(); + return 1; + } + + var success = false; + var cacheManager = new ExtensionCacheManager(); + + switch (this.Subcommand) + { + case CacheSubcommand.Add: + success = await this.AddExtensions(cacheManager, cancellationToken); + break; + + case CacheSubcommand.Remove: + success = await this.RemoveExtensions(cacheManager, cancellationToken); + break; + + case CacheSubcommand.List: + success = await this.ListExtensions(cacheManager, cancellationToken); + break; + } + + return success ? 0 : 2; + } + + public bool TryParseArgument(ICommandLineParser parser, string argument) + { + if (!parser.IsSwitch(argument)) + { + if (!this.Subcommand.HasValue) + { + if (!Enum.TryParse(argument, true, out CacheSubcommand subcommand)) + { + return false; + } + + this.Subcommand = subcommand; + } + else + { + this.ExtensionReferences.Add(argument); + } + + return true; + } + + var parameter = argument.Substring(1); + switch (parameter.ToLowerInvariant()) + { + case "?": + this.ShowHelp = true; + this.ShowLogo = true; + this.StopParsing = true; + return true; + + case "nologo": + this.ShowLogo = false; + return true; + + case "g": + case "-global": + this.Global = true; + return true; + } + + return false; + } + + private async Task AddExtensions(ExtensionCacheManager cacheManager, CancellationToken cancellationToken) + { + var success = true; + + foreach (var extensionRef in this.ExtensionReferences) + { + var added = await cacheManager.AddAsync(this.Global, extensionRef, cancellationToken); + success |= added; + } + + return success; + } + + private async Task RemoveExtensions(ExtensionCacheManager cacheManager, CancellationToken cancellationToken) + { + var success = true; + + foreach (var extensionRef in this.ExtensionReferences) + { + var removed = await cacheManager.RemoveAsync(this.Global, extensionRef, cancellationToken); + success |= removed; + } + + return success; + } + + private async Task ListExtensions(ExtensionCacheManager cacheManager, CancellationToken cancellationToken) + { + var found = false; + var extensionRef = this.ExtensionReferences.FirstOrDefault(); + + var extensions = await cacheManager.ListAsync(this.Global, extensionRef, cancellationToken); + + foreach (var extension in extensions) + { + this.Messaging.Write($"{extension.Id} {extension.Version}{(extension.Damaged ? " (damaged)" : String.Empty)}"); + found = true; + } + + return found; + } + + private static void DisplayHelp() + { + Console.WriteLine(" usage: wix.exe extension add|remove|list [extensionRef]"); + Console.WriteLine(); + Console.WriteLine(" -g add/remove the extension for the current user"); + Console.WriteLine(" -nologo suppress displaying the logo information"); + Console.WriteLine(" -? this help information"); + Console.WriteLine(); + Console.WriteLine(" extensionRef format: extensionId/version (the version is optional)"); + } + } +} 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 @@ +// 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. + +namespace WixToolset.Core.ExtensionCache +{ + using System; + using System.Collections.Generic; + using WixToolset.Extensibility; + using WixToolset.Extensibility.Data; + using WixToolset.Extensibility.Services; + + /// + /// Parses the "extension" command-line command. See ExtensionCacheManagerCommand + /// for the bulk of the command-line processing. + /// + internal class ExtensionCacheManagerExtensionCommandLine : BaseExtensionCommandLine + { + public ExtensionCacheManagerExtensionCommandLine(IWixToolsetServiceProvider serviceProvider) + { + this.ServiceProvider = serviceProvider; + } + + private IWixToolsetServiceProvider ServiceProvider { get; } + + // TODO: Do something with CommandLineSwitches + public override IEnumerable CommandLineSwitches => base.CommandLineSwitches; + + public override bool TryParseCommand(ICommandLineParser parser, string argument, out ICommandLineCommand command) + { + command = null; + + if ("extension".Equals(argument, StringComparison.OrdinalIgnoreCase)) + { + command = new ExtensionCacheManagerCommand(this.ServiceProvider); + } + + return command != null; + } + } +} 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 @@ +// 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. + +namespace WixToolset.Core.ExtensionCache +{ + using System; + using WixToolset.Extensibility; + using WixToolset.Extensibility.Services; + + internal class ExtensionCacheManagerExtensionFactory : IExtensionFactory + { + public ExtensionCacheManagerExtensionFactory(IWixToolsetServiceProvider serviceProvider) + { + this.ServiceProvider = serviceProvider; + } + + private IWixToolsetServiceProvider ServiceProvider { get; } + + public bool TryCreateExtension(Type extensionType, out object extension) + { + extension = null; + + if (extensionType == typeof(IExtensionCommandLine)) + { + extension = new ExtensionCacheManagerExtensionCommandLine(this.ServiceProvider); + } + + return extension != null; + } + } +} 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 @@ + + + + + + netstandard2.0;net461;net472 + Extension Cache + WiX Toolset Extension Cache + embedded + true + + + + + + + + + + + + + + + + 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 @@ +// 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. + +namespace WixToolset.Core.ExtensionCache +{ + using System; + using System.Collections.Generic; + using WixToolset.Extensibility.Services; + + public static class WixToolsetCoreServiceProviderExtensions + { + public static IWixToolsetCoreServiceProvider AddExtensionCacheManager(this IWixToolsetCoreServiceProvider serviceProvider) + { + var extensionManager = serviceProvider.GetService(); + extensionManager.Add(typeof(ExtensionCacheManagerExtensionFactory).Assembly); + + serviceProvider.AddService(CreateExtensionCacheManager); + return serviceProvider; + } + + private static ExtensionCacheManager CreateExtensionCacheManager(IWixToolsetCoreServiceProvider provider, Dictionary singletons) + { + var extensionCacheManager = new ExtensionCacheManager(); + singletons.Add(typeof(ExtensionCacheManager), extensionCacheManager); + + return extensionCacheManager; + } + } +} 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 var types = extensionAssembly.GetTypes().Where(t => !t.IsAbstract && !t.IsInterface && typeof(IExtensionFactory).IsAssignableFrom(t)); var factories = types.Select(this.CreateExtensionFactory).ToList(); - this.extensionFactories.AddRange(factories); - } - - private IExtensionFactory CreateExtensionFactory(Type type) - { - var constructor = type.GetConstructor(new[] { typeof(IWixToolsetCoreServiceProvider) }); - if (constructor != null) + if (!factories.Any()) { - return (IExtensionFactory)constructor.Invoke(new[] { this.ServiceProvider }); + var path = Path.GetFullPath(new Uri(extensionAssembly.CodeBase).LocalPath); + throw new WixException(ErrorMessages.InvalidExtension(path, "The extension does not implement IExtensionFactory. All extensions must have at least one implementation of IExtensionFactory.")); } - return (IExtensionFactory)Activator.CreateInstance(type); + this.extensionFactories.AddRange(factories); } public void Load(string extensionPath) { + var checkPath = extensionPath; + var checkedPaths = new List { checkPath }; try { - Assembly assembly; - - // Absolute path to an assembly which means only "load from" will work even though we'd prefer to - // use Assembly.Load (see the documentation for Assembly.LoadFrom why). - if (Path.IsPathRooted(extensionPath)) + if (!TryLoadFromPath(checkPath, out var assembly) && !Path.IsPathRooted(extensionPath)) { - assembly = Assembly.LoadFrom(extensionPath); - } - else if (ExtensionManager.TryExtensionLoad(extensionPath, out assembly)) - { - // Loaded the assembly by name from the probing path. - } - else if (ExtensionManager.TryExtensionLoad(Path.GetFileNameWithoutExtension(extensionPath), out assembly)) - { - // Loaded the assembly by filename alone along the probing path. + if (TryParseExtensionReference(extensionPath, out var extensionId, out var extensionVersion)) + { + foreach (var cachePath in this.CacheLocations()) + { + var extensionFolder = Path.Combine(cachePath, extensionId); + + var versionFolder = extensionVersion; + if (String.IsNullOrEmpty(versionFolder) && !TryFindLatestVersionInFolder(extensionFolder, out versionFolder)) + { + checkedPaths.Add(extensionFolder); + continue; + } + + checkPath = Path.Combine(extensionFolder, versionFolder, "tools", extensionId + ".dll"); + checkedPaths.Add(checkPath); + + if (TryLoadFromPath(checkPath, out assembly)) + { + break; + } + } + } } - else // relative path to an assembly + + if (assembly == null) { - // We want to use Assembly.Load when we can because it has some benefits over Assembly.LoadFrom - // (see the documentation for Assembly.LoadFrom). However, it may fail when the path is a relative - // path, so we should try Assembly.LoadFrom one last time. We could have detected a directory - // separator character and used Assembly.LoadFrom directly, but dealing with path canonicalization - // issues is something we don't want to deal with if we don't have to. - assembly = Assembly.LoadFrom(extensionPath); + throw new WixException(ErrorMessages.CouldNotFindExtensionInPaths(extensionPath, checkedPaths)); } this.Add(assembly); } catch (ReflectionTypeLoadException rtle) { - throw new WixException(ErrorMessages.InvalidExtension(extensionPath, String.Join(Environment.NewLine, rtle.LoaderExceptions.Select(le => le.ToString())))); + throw new WixException(ErrorMessages.InvalidExtension(checkPath, String.Join(Environment.NewLine, rtle.LoaderExceptions.Select(le => le.ToString())))); + } + catch (WixException) + { + throw; } catch (Exception e) { - throw new WixException(ErrorMessages.InvalidExtension(extensionPath, e.Message), e); + throw new WixException(ErrorMessages.InvalidExtension(checkPath, e.Message), e); } } @@ -104,18 +110,120 @@ namespace WixToolset.Core.ExtensibilityServices return extensions.Cast().ToList(); } - private static bool TryExtensionLoad(string assemblyName, out Assembly assembly) + private IExtensionFactory CreateExtensionFactory(Type type) + { + var constructor = type.GetConstructor(new[] { typeof(IWixToolsetCoreServiceProvider) }); + if (constructor != null) + { + return (IExtensionFactory)constructor.Invoke(new[] { this.ServiceProvider }); + } + + return (IExtensionFactory)Activator.CreateInstance(type); + } + + private IEnumerable CacheLocations() + { + var path = Path.Combine(Environment.CurrentDirectory, @".wix\extensions\"); + if (Directory.Exists(path)) + { + yield return path; + } + + path = Environment.GetEnvironmentVariable("WIX_EXTENSIONS") ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + path = Path.Combine(path, @".wix\extensions\"); + if (Directory.Exists(path)) + { + yield return path; + } + + if (Environment.Is64BitOperatingSystem) + { + path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles), @"WixToolset\extensions\"); + if (Directory.Exists(path)) + { + yield return path; + } + } + + path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFilesX86), @"WixToolset\extensions\"); + if (Directory.Exists(path)) + { + yield return path; + } + + path = Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetCallingAssembly().CodeBase).LocalPath), @"extensions\"); + if (Directory.Exists(path)) + { + yield return path; + } + } + + private static bool TryParseExtensionReference(string extensionReference, out string extensionId, out string extensionVersion) + { + extensionId = extensionReference ?? String.Empty; + extensionVersion = String.Empty; + + var index = extensionId.LastIndexOf('/'); + if (index > 0) + { + extensionVersion = extensionReference.Substring(index + 1); + extensionId = extensionReference.Substring(0, index); + + if (!NuGet.Versioning.NuGetVersion.TryParse(extensionVersion, out _)) + { + return false; + } + + if (String.IsNullOrEmpty(extensionId)) + { + return false; + } + } + + return true; + } + + private static bool TryFindLatestVersionInFolder(string basePath, out string foundVersionFolder) + { + foundVersionFolder = null; + + try + { + NuGet.Versioning.NuGetVersion version = null; + foreach (var versionPath in Directory.GetDirectories(basePath)) + { + var versionFolder = Path.GetFileName(versionPath); + if (NuGet.Versioning.NuGetVersion.TryParse(versionFolder, out var checkVersion) && + (version == null || version < checkVersion)) + { + foundVersionFolder = versionFolder; + version = checkVersion; + } + } + } + catch (IOException) + { + } + + return !String.IsNullOrEmpty(foundVersionFolder); + } + + private static bool TryLoadFromPath(string extensionPath, out Assembly assembly) { try { - assembly = Assembly.Load(assemblyName); - return true; + if (File.Exists(extensionPath)) + { + assembly = Assembly.LoadFrom(extensionPath); + return true; + } } catch (IOException e) when (e is FileLoadException || e is FileNotFoundException) { - assembly = null; - return false; } + + assembly = null; + return false; } } } 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 @@ + 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 } } + [Fact] + public void CannotBuildWithMissingExtension() + { + var folder = TestData.Get(@"TestData\ExampleExtension"); + + using (var fs = new DisposableFileSystem()) + { + var intermediateFolder = fs.GetFolder(); + + var exception = Assert.Throws(() => + WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "Package.wxs"), + "-ext", "ExampleExtension.DoesNotExist" + })); + + Assert.StartsWith("The extension 'ExampleExtension.DoesNotExist' could not be found. Checked paths: ", exception.Message); + } + } + + [Fact] + public void CannotBuildWithMissingVersionedExtension() + { + var folder = TestData.Get(@"TestData\ExampleExtension"); + + using (var fs = new DisposableFileSystem()) + { + var intermediateFolder = fs.GetFolder(); + + var exception = Assert.Throws(() => + WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "Package.wxs"), + "-ext", "ExampleExtension.DoesNotExist/1.0.0" + })); + + Assert.StartsWith("The extension 'ExampleExtension.DoesNotExist/1.0.0' could not be found. Checked paths: ", exception.Message); + } + } + private static void Build(string[] args) { var result = WixRunner.Execute(args) -- cgit v1.2.3-55-g6feb