aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core.ExtensionCache
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core.ExtensionCache')
-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
7 files changed, 565 insertions, 0 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}