aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/WixToolset.Core.ExtensionCache/CachedExtension.cs20
-rw-r--r--src/WixToolset.Core.ExtensionCache/ExtensionCacheManager.cs252
-rw-r--r--src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerCommand.cs170
-rw-r--r--src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerExtensionCommandLine.cs39
-rw-r--r--src/WixToolset.Core.ExtensionCache/ExtensionCacheManagerExtensionFactory.cs30
-rw-r--r--src/WixToolset.Core.ExtensionCache/WixToolset.Core.ExtensionCache.csproj26
-rw-r--r--src/WixToolset.Core.ExtensionCache/WixToolsetCoreServiceProviderExtensions.cs28
-rw-r--r--src/WixToolset.Core/ExtensibilityServices/ExtensionManager.cs182
-rw-r--r--src/WixToolset.Core/WixToolset.Core.csproj1
-rw-r--r--src/test/WixToolsetTest.CoreIntegration/ExtensionFixture.cs42
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
3namespace 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
3namespace 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
3namespace 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
3namespace 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
3namespace 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
3namespace 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)