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)