@joebigelow / wix / commits / 02cdf551

Add "extension" command

Rob Mensching committed Jun 8, 2020 at 16:26 UTC 02cdf55197d599d4d1fd611ad749d01f5c47a01f
10 files changed +753 -37
src/WixToolset.Core.ExtensionCache/CachedExtension.cs new
+20
@@ -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 +}
src/WixToolset.Core.ExtensionCache/ExtensionCacheManager.cs new
+252
@@ -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 +}
src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerCommand.cs new
+170
@@ -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 +}
src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerExtensionCommandLine.cs new
+39
@@ -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 +}
src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerExtensionFactory.cs new
+30
@@ -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 +}
src/WixToolset.Core.ExtensionCache/WixToolset.Core.ExtensionCache.csproj new
+26
@@ -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>
src/WixToolset.Core.ExtensionCache/WixToolsetCoreServiceProviderExtensions.cs new
+28
@@ -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 +}
src/WixToolset.Core/ExtensibilityServices/ExtensionManager.cs
+145 -37
@@ -28,59 +28,65 @@ namespace WixToolset.Core.ExtensibilityServices
28 var types = extensionAssembly.GetTypes().Where(t => !t.IsAbstract && !t.IsInterface && typeof(IExtensionFactory).IsAssignableFrom(t));
29 var factories = types.Select(this.CreateExtensionFactory).ToList();
30
31 - this.extensionFactories.AddRange(factories);
32 - }
33 -
34 - private IExtensionFactory CreateExtensionFactory(Type type)
35 - {
36 - var constructor = type.GetConstructor(new[] { typeof(IWixToolsetCoreServiceProvider) });
37 - if (constructor != null)
31 + if (!factories.Any())
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."));
35 }
36
42 - return (IExtensionFactory)Activator.CreateInstance(type);
37 + this.extensionFactories.AddRange(factories);
38 }
39
40 public void Load(string extensionPath)
41 {
42 + var checkPath = extensionPath;
43 + var checkedPaths = new List<string> { checkPath };
44 try
45 {
49 - Assembly assembly;
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))
46 + if (!TryLoadFromPath(checkPath, out var assembly) && !Path.IsPathRooted(extensionPath))
47 {
55 - assembly = Assembly.LoadFrom(extensionPath);
56 - }
57 - else if (ExtensionManager.TryExtensionLoad(extensionPath, out assembly))
58 - {
59 - // Loaded the assembly by name from the probing path.
60 - }
61 - else if (ExtensionManager.TryExtensionLoad(Path.GetFileNameWithoutExtension(extensionPath), out assembly))
62 - {
63 - // Loaded the assembly by filename alone along the probing path.
48 + if (TryParseExtensionReference(extensionPath, out var extensionId, out var extensionVersion))
49 + {
50 + foreach (var cachePath in this.CacheLocations())
51 + {
52 + var extensionFolder = Path.Combine(cachePath, extensionId);
53 +
54 + var versionFolder = extensionVersion;
55 + if (String.IsNullOrEmpty(versionFolder) && !TryFindLatestVersionInFolder(extensionFolder, out versionFolder))
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 + }
70 }
65 - else // relative path to an assembly
71 +
72 + if (assembly == null)
73 {
67 - // We want to use Assembly.Load when we can because it has some benefits over Assembly.LoadFrom
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);
74 + throw new WixException(ErrorMessages.CouldNotFindExtensionInPaths(extensionPath, checkedPaths));
75 }
76
77 this.Add(assembly);
78 }
79 catch (ReflectionTypeLoadException rtle)
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;
86 }
87 catch (Exception e)
88 {
83 - throw new WixException(ErrorMessages.InvalidExtension(extensionPath, e.Message), e);
89 + throw new WixException(ErrorMessages.InvalidExtension(checkPath, e.Message), e);
90 }
91 }
92
@@ -104,18 +110,120 @@ namespace WixToolset.Core.ExtensibilityServices
110 return extensions.Cast<T>().ToList();
111 }
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)
212 {
213 try
214 {
111 - assembly = Assembly.Load(assemblyName);
112 - return true;
215 + if (File.Exists(extensionPath))
216 + {
217 + assembly = Assembly.LoadFrom(extensionPath);
218 + return true;
219 + }
220 }
221 catch (IOException e) when (e is FileLoadException || e is FileNotFoundException)
222 {
116 - assembly = null;
117 - return false;
223 }
224 +
225 + assembly = null;
226 + return false;
227 }
228 }
229 }
src/WixToolset.Core/WixToolset.Core.csproj
+1
@@ -22,6 +22,7 @@
22 <ItemGroup>
23 <PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.6.0" />
24 <PackageReference Include="System.Text.Encoding.CodePages" Version="4.6.0" />
25 + <PackageReference Include="NuGet.Versioning" Version="5.6.0" />
26 </ItemGroup>
27
28 <ItemGroup>
src/test/WixToolsetTest.CoreIntegration/ExtensionFixture.cs
+42
@@ -102,6 +102,48 @@ namespace WixToolsetTest.CoreIntegration
102 }
103 }
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 +
147 private static void Build(string[] args)
148 {
149 var result = WixRunner.Execute(args)