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