| 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.Burn.Bundles |
| 4 | { |
| 5 | using System; |
| 6 | using System.Collections.Generic; |
| 7 | using System.IO; |
| 8 | using System.Linq; |
| 9 | using System.Xml; |
| 10 | using WixToolset.Data; |
| 11 | using WixToolset.Data.Symbols; |
| 12 | using WixToolset.Extensibility; |
| 13 | using WixToolset.Extensibility.Data; |
| 14 | using WixToolset.Extensibility.Services; |
| 15 | |
| 16 | internal class HarvestBundlePackageCommand |
| 17 | { |
| 18 | public HarvestBundlePackageCommand(IServiceProvider serviceProvider, IEnumerable<IBurnBackendBinderExtension> backendExtensions, string intermediateFolder, WixBundlePayloadSymbol payloadSymbol, WixBundleBundlePackagePayloadSymbol packagePayloadSymbol, Dictionary<string, WixBundlePayloadSymbol> packagePayloadsById) |
| 19 | { |
| 20 | this.Messaging = serviceProvider.GetService<IMessaging>(); |
| 21 | this.FileSystem = serviceProvider.GetService<IFileSystem>(); |
| 22 | this.BackendHelper = serviceProvider.GetService<IBackendHelper>(); |
| 23 | this.BackendExtensions = backendExtensions; |
| 24 | this.IntermediateFolder = intermediateFolder; |
| 25 | |
| 26 | this.PackagePayload = payloadSymbol; |
| 27 | this.BundlePackagePayload = packagePayloadSymbol; |
| 28 | this.PackagePayloadsById = packagePayloadsById; |
| 29 | } |
| 30 | |
| 31 | private IMessaging Messaging { get; } |
| 32 | |
| 33 | private IFileSystem FileSystem { get; } |
| 34 | |
| 35 | private IBackendHelper BackendHelper { get; } |
| 36 | |
| 37 | private IEnumerable<IBurnBackendBinderExtension> BackendExtensions { get; } |
| 38 | |
| 39 | private string IntermediateFolder { get; } |
| 40 | |
| 41 | private WixBundlePayloadSymbol PackagePayload { get; } |
| 42 | |
| 43 | private WixBundleBundlePackagePayloadSymbol BundlePackagePayload { get; } |
| 44 | |
| 45 | private Dictionary<string, WixBundlePayloadSymbol> PackagePayloadsById { get; } |
| 46 | |
| 47 | public WixBundleHarvestedBundlePackageSymbol HarvestedBundlePackage { get; private set; } |
| 48 | |
| 49 | public WixBundleHarvestedDependencyProviderSymbol HarvestedDependencyProvider { get; private set; } |
| 50 | |
| 51 | public List<WixBundlePayloadSymbol> Payloads { get; } = new List<WixBundlePayloadSymbol>(); |
| 52 | |
| 53 | public List<WixBundlePackageRelatedBundleSymbol> RelatedBundles { get; } = new List<WixBundlePackageRelatedBundleSymbol>(); |
| 54 | |
| 55 | public List<ITrackedFile> TrackedFiles { get; } = new List<ITrackedFile>(); |
| 56 | |
| 57 | public void Execute() |
| 58 | { |
| 59 | bool win64; |
| 60 | string bundleId; |
| 61 | string engineVersion; |
| 62 | int protocolVersion; |
| 63 | string manifestNamespace; |
| 64 | bool perMachine; |
| 65 | string version; |
| 66 | string displayName; |
| 67 | long installSize; |
| 68 | |
| 69 | var sourcePath = this.PackagePayload.SourceFile.Path; |
| 70 | var sourceLineNumbers = this.PackagePayload.SourceLineNumbers; |
| 71 | |
| 72 | using (var burnReader = BurnReader.Open(this.Messaging, this.FileSystem, sourcePath)) |
| 73 | { |
| 74 | if (burnReader.Invalid) |
| 75 | { |
| 76 | return; |
| 77 | } |
| 78 | |
| 79 | var baFolderPath = Path.Combine(this.IntermediateFolder, burnReader.BundleId.ToString()); |
| 80 | |
| 81 | if (!burnReader.ExtractUXContainer(baFolderPath, baFolderPath)) |
| 82 | { |
| 83 | this.Messaging.Write(BurnBackendErrors.BundleMissingBootstrapperApplicationContainer(sourceLineNumbers, sourcePath)); |
| 84 | return; |
| 85 | } |
| 86 | |
| 87 | foreach (var filePath in Directory.EnumerateFiles(baFolderPath, "*.*", SearchOption.AllDirectories)) |
| 88 | { |
| 89 | this.TrackedFiles.Add(this.BackendHelper.TrackFile(filePath, TrackedFileType.Temporary, sourceLineNumbers)); |
| 90 | } |
| 91 | |
| 92 | bundleId = burnReader.BundleId.ToString("B").ToUpperInvariant(); |
| 93 | |
| 94 | try |
| 95 | { |
| 96 | var document = new XmlDocument(); |
| 97 | document.Load(Path.Combine(baFolderPath, "manifest.xml")); |
| 98 | var namespaceManager = new XmlNamespaceManager(document.NameTable); |
| 99 | |
| 100 | if (document.DocumentElement.LocalName != "BurnManifest") |
| 101 | { |
| 102 | this.Messaging.Write(BurnBackendErrors.InvalidBundleManifest(sourceLineNumbers, sourcePath, $"Expected root element to be 'BurnManifest' but was '{document.DocumentElement.LocalName}'.")); |
| 103 | return; |
| 104 | } |
| 105 | |
| 106 | engineVersion = document.DocumentElement.GetAttribute("EngineVersion"); |
| 107 | protocolVersion = this.ProcessProtocolVersion(burnReader, document); |
| 108 | win64 = this.ProcessWin64(burnReader, document, sourceLineNumbers, sourcePath); |
| 109 | |
| 110 | manifestNamespace = document.DocumentElement.NamespaceURI; |
| 111 | |
| 112 | namespaceManager.AddNamespace("burn", document.DocumentElement.NamespaceURI); |
| 113 | var registrationElement = document.SelectSingleNode("/burn:BurnManifest/burn:Registration", namespaceManager) as XmlElement; |
| 114 | var arpElement = document.SelectSingleNode("/burn:BurnManifest/burn:Registration/burn:Arp", namespaceManager) as XmlElement; |
| 115 | |
| 116 | perMachine = registrationElement.GetAttribute("PerMachine") == "yes"; |
| 117 | |
| 118 | version = registrationElement.GetAttribute("Version"); |
| 119 | |
| 120 | var providerKey = registrationElement.GetAttribute("ProviderKey"); |
| 121 | var depId = new Identifier(AccessModifier.Section, this.BackendHelper.GenerateIdentifier("dep", this.PackagePayload.Id.Id, providerKey)); |
| 122 | this.HarvestedDependencyProvider = new WixBundleHarvestedDependencyProviderSymbol(sourceLineNumbers, depId) |
| 123 | { |
| 124 | PackagePayloadRef = this.PackagePayload.Id.Id, |
| 125 | ProviderKey = providerKey, |
| 126 | Version = version, |
| 127 | }; |
| 128 | |
| 129 | displayName = arpElement.GetAttribute("DisplayName"); |
| 130 | |
| 131 | installSize = this.ProcessPackages(document, namespaceManager); |
| 132 | |
| 133 | this.ProcessPayloads(document, namespaceManager, this.BundlePackagePayload.PayloadGeneration); |
| 134 | |
| 135 | this.ProcessRelatedBundles(document, namespaceManager, sourcePath); |
| 136 | } |
| 137 | catch (Exception e) |
| 138 | { |
| 139 | this.Messaging.Write(BurnBackendErrors.InvalidBundleManifest(sourceLineNumbers, sourcePath, e.ToString())); |
| 140 | return; |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | this.HarvestedBundlePackage = new WixBundleHarvestedBundlePackageSymbol(this.PackagePayload.SourceLineNumbers, this.PackagePayload.Id) |
| 145 | { |
| 146 | Win64 = win64, |
| 147 | BundleId = bundleId, |
| 148 | EngineVersion = engineVersion, |
| 149 | ManifestNamespace = manifestNamespace, |
| 150 | ProtocolVersion = protocolVersion, |
| 151 | PerMachine = perMachine, |
| 152 | Version = version, |
| 153 | DisplayName = displayName, |
| 154 | InstallSize = installSize, |
| 155 | }; |
| 156 | } |
| 157 | |
| 158 | private int ProcessProtocolVersion(BurnReader burnReader, XmlDocument document) |
| 159 | { |
| 160 | var protocolVersionValue = document.DocumentElement.GetAttribute("ProtocolVersion"); |
| 161 | |
| 162 | if (Int32.TryParse(protocolVersionValue, out var protocolVersion)) |
| 163 | { |
| 164 | return protocolVersion; |
| 165 | } |
| 166 | |
| 167 | // Assume that the .wixburn section version will change when the Burn protocol changes. |
| 168 | // This should be a safe assumption since only old bundles should be missing the ProtocolVersion from the manifest. |
| 169 | return burnReader.Version == 2 ? 1 : 0; |
| 170 | } |
| 171 | |
| 172 | private bool ProcessWin64(BurnReader burnReader, XmlDocument document, SourceLineNumber sourceLineNumbers, string sourcePath) |
| 173 | { |
| 174 | var win64Value = document.DocumentElement.GetAttribute("Win64"); |
| 175 | |
| 176 | switch (win64Value) |
| 177 | { |
| 178 | case "yes": |
| 179 | return true; |
| 180 | case "no": |
| 181 | return false; |
| 182 | } |
| 183 | |
| 184 | switch (burnReader.MachineType) |
| 185 | { |
| 186 | case BurnCommon.IMAGE_FILE_MACHINE_ARM: |
| 187 | case BurnCommon.IMAGE_FILE_MACHINE_ARMNT: |
| 188 | case BurnCommon.IMAGE_FILE_MACHINE_I386: |
| 189 | case BurnCommon.IMAGE_FILE_MACHINE_LOONGARCH32: |
| 190 | return false; |
| 191 | case BurnCommon.IMAGE_FILE_MACHINE_AMD64: |
| 192 | case BurnCommon.IMAGE_FILE_MACHINE_ARM64: |
| 193 | case BurnCommon.IMAGE_FILE_MACHINE_IA64: |
| 194 | case BurnCommon.IMAGE_FILE_MACHINE_LOONGARCH64: |
| 195 | return true; |
| 196 | case BurnCommon.IMAGE_FILE_MACHINE_AM33: |
| 197 | case BurnCommon.IMAGE_FILE_MACHINE_EBC: |
| 198 | case BurnCommon.IMAGE_FILE_MACHINE_M32R: |
| 199 | case BurnCommon.IMAGE_FILE_MACHINE_MIPS16: |
| 200 | case BurnCommon.IMAGE_FILE_MACHINE_MIPSFPU: |
| 201 | case BurnCommon.IMAGE_FILE_MACHINE_MIPSFPU16: |
| 202 | case BurnCommon.IMAGE_FILE_MACHINE_POWERPC: |
| 203 | case BurnCommon.IMAGE_FILE_MACHINE_POWERPCFP: |
| 204 | case BurnCommon.IMAGE_FILE_MACHINE_R4000: |
| 205 | case BurnCommon.IMAGE_FILE_MACHINE_RISCV32: |
| 206 | case BurnCommon.IMAGE_FILE_MACHINE_RISCV64: |
| 207 | case BurnCommon.IMAGE_FILE_MACHINE_RISCV128: |
| 208 | case BurnCommon.IMAGE_FILE_MACHINE_SH3: |
| 209 | case BurnCommon.IMAGE_FILE_MACHINE_SH3DSP: |
| 210 | case BurnCommon.IMAGE_FILE_MACHINE_SH4: |
| 211 | case BurnCommon.IMAGE_FILE_MACHINE_SH5: |
| 212 | case BurnCommon.IMAGE_FILE_MACHINE_THUMB: |
| 213 | case BurnCommon.IMAGE_FILE_MACHINE_WCEMIPSV2: |
| 214 | default: |
| 215 | this.Messaging.Write(BurnBackendWarnings.UnknownCoffMachineType(sourceLineNumbers, sourcePath, burnReader.MachineType)); |
| 216 | return false; |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | private long ProcessPackages(XmlDocument document, XmlNamespaceManager namespaceManager) |
| 221 | { |
| 222 | long packageInstallSize = 0; |
| 223 | |
| 224 | foreach (XmlElement packageElement in document.SelectNodes("/burn:BurnManifest/burn:Chain/*", namespaceManager)) |
| 225 | { |
| 226 | if (!packageElement.Name.EndsWith("Package")) |
| 227 | { |
| 228 | continue; |
| 229 | } |
| 230 | |
| 231 | if (Int64.TryParse(packageElement.GetAttribute("InstallSize"), out var installSize)) |
| 232 | { |
| 233 | packageInstallSize += installSize; |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | return packageInstallSize; |
| 238 | } |
| 239 | |
| 240 | private void ProcessPayloads(XmlDocument document, XmlNamespaceManager namespaceManager, BundlePackagePayloadGenerationType payloadGenerationType) |
| 241 | { |
| 242 | if (payloadGenerationType == BundlePackagePayloadGenerationType.None) |
| 243 | { |
| 244 | return; |
| 245 | } |
| 246 | |
| 247 | var payloadNames = new HashSet<string>(this.PackagePayloadsById.Values.Select(p => p.Name), StringComparer.OrdinalIgnoreCase); |
| 248 | |
| 249 | var containersById = new Dictionary<string, ManifestContainer>(); |
| 250 | |
| 251 | foreach (XmlElement containerElement in document.SelectNodes("/burn:BurnManifest/burn:Container", namespaceManager)) |
| 252 | { |
| 253 | var container = new ManifestContainer(); |
| 254 | container.Attached = containerElement.GetAttribute("Attached") == "yes"; |
| 255 | container.DownloadUrl = containerElement.GetAttribute("DownloadUrl"); |
| 256 | container.FilePath = containerElement.GetAttribute("FilePath"); |
| 257 | container.Id = containerElement.GetAttribute("Id"); |
| 258 | containersById.Add(container.Id, container); |
| 259 | |
| 260 | if (container.Attached) |
| 261 | { |
| 262 | continue; |
| 263 | } |
| 264 | |
| 265 | switch (payloadGenerationType) |
| 266 | { |
| 267 | case BundlePackagePayloadGenerationType.ExternalWithoutDownloadUrl: |
| 268 | if (!String.IsNullOrEmpty(container.DownloadUrl)) |
| 269 | { |
| 270 | continue; |
| 271 | } |
| 272 | break; |
| 273 | } |
| 274 | |
| 275 | // If we didn't find the Payload as an existing child of the package, we need to |
| 276 | // add it. We expect the file to exist on-disk in the same relative location as |
| 277 | // the bundle expects to find it... |
| 278 | container.IncludedAsPayload = true; |
| 279 | var containerName = container.FilePath; |
| 280 | var containerFullName = Path.Combine(Path.GetDirectoryName(this.PackagePayload.Name), containerName); |
| 281 | |
| 282 | if (!payloadNames.Contains(containerFullName)) |
| 283 | { |
| 284 | var generatedId = this.BackendHelper.GenerateIdentifier("hcp", this.PackagePayload.Id.Id, containerName); |
| 285 | var payloadSourceFile = this.ResolveRelatedFile(this.PackagePayload.SourceFile.Path, this.PackagePayload.UnresolvedSourceFile, containerName, "container", this.PackagePayload.SourceLineNumbers); |
| 286 | |
| 287 | this.Payloads.Add(new WixBundlePayloadSymbol(this.PackagePayload.SourceLineNumbers, new Identifier(AccessModifier.Section, generatedId)) |
| 288 | { |
| 289 | Name = containerFullName, |
| 290 | SourceFile = new IntermediateFieldPathValue { Path = payloadSourceFile }, |
| 291 | Compressed = this.PackagePayload.Compressed, |
| 292 | UnresolvedSourceFile = containerFullName, |
| 293 | ContainerRef = this.PackagePayload.ContainerRef, |
| 294 | DownloadUrl = this.PackagePayload.DownloadUrl, |
| 295 | Packaging = this.PackagePayload.Packaging, |
| 296 | ParentPackagePayloadRef = this.PackagePayload.Id.Id, |
| 297 | }); |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | foreach (XmlElement payloadElement in document.SelectNodes("/burn:BurnManifest/burn:Payload", namespaceManager)) |
| 302 | { |
| 303 | var payload = new ManifestPayload(); |
| 304 | payload.Container = payloadElement.GetAttribute("Container"); |
| 305 | payload.DownloadUrl = payloadElement.GetAttribute("DownloadUrl"); |
| 306 | payload.FilePath = payloadElement.GetAttribute("FilePath"); |
| 307 | payload.Id = payloadElement.GetAttribute("Id"); |
| 308 | |
| 309 | if (payload.Container == null || !containersById.TryGetValue(payload.Container, out var container)) |
| 310 | { |
| 311 | container = null; |
| 312 | } |
| 313 | |
| 314 | if (container != null && container.IncludedAsPayload) |
| 315 | { |
| 316 | // Don't include payload if it's in a container that's already included. |
| 317 | continue; |
| 318 | } |
| 319 | |
| 320 | switch (payloadGenerationType) |
| 321 | { |
| 322 | case BundlePackagePayloadGenerationType.ExternalWithoutDownloadUrl: |
| 323 | if (container != null || !String.IsNullOrEmpty(payload.DownloadUrl)) |
| 324 | { |
| 325 | continue; |
| 326 | } |
| 327 | break; |
| 328 | case BundlePackagePayloadGenerationType.External: |
| 329 | if (container != null) |
| 330 | { |
| 331 | continue; |
| 332 | } |
| 333 | break; |
| 334 | } |
| 335 | |
| 336 | // If we didn't find the Payload as an existing child of the package, we need to |
| 337 | // add it. We expect the file to exist on-disk in the same relative location as |
| 338 | // the bundle expects to find it... |
| 339 | var payloadName = payload.FilePath; |
| 340 | var payloadFullName = Path.Combine(Path.GetDirectoryName(this.PackagePayload.Name), payloadName); |
| 341 | |
| 342 | if (!payloadNames.Contains(payloadFullName)) |
| 343 | { |
| 344 | var generatedId = this.BackendHelper.GenerateIdentifier("hpp", this.PackagePayload.Id.Id, payloadName); |
| 345 | var payloadSourceFile = this.ResolveRelatedFile(this.PackagePayload.SourceFile.Path, this.PackagePayload.UnresolvedSourceFile, payloadName, "payload", this.PackagePayload.SourceLineNumbers); |
| 346 | |
| 347 | this.Payloads.Add(new WixBundlePayloadSymbol(this.PackagePayload.SourceLineNumbers, new Identifier(AccessModifier.Section, generatedId)) |
| 348 | { |
| 349 | Name = payloadFullName, |
| 350 | SourceFile = new IntermediateFieldPathValue { Path = payloadSourceFile }, |
| 351 | Compressed = this.PackagePayload.Compressed, |
| 352 | UnresolvedSourceFile = payloadFullName, |
| 353 | ContainerRef = this.PackagePayload.ContainerRef, |
| 354 | DownloadUrl = this.PackagePayload.DownloadUrl, |
| 355 | Packaging = this.PackagePayload.Packaging, |
| 356 | ParentPackagePayloadRef = this.PackagePayload.Id.Id, |
| 357 | }); |
| 358 | } |
| 359 | } |
| 360 | } |
| 361 | |
| 362 | private string ResolveRelatedFile(string resolvedSource, string unresolvedSource, string relatedSource, string type, SourceLineNumber sourceLineNumbers) |
| 363 | { |
| 364 | var checkedPaths = new List<string>(); |
| 365 | |
| 366 | foreach (var extension in this.BackendExtensions) |
| 367 | { |
| 368 | var resolved = extension.ResolveRelatedFile(unresolvedSource, relatedSource, type, sourceLineNumbers); |
| 369 | |
| 370 | if (resolved?.CheckedPaths != null) |
| 371 | { |
| 372 | checkedPaths.AddRange(resolved.CheckedPaths); |
| 373 | } |
| 374 | |
| 375 | if (!String.IsNullOrEmpty(resolved?.Path)) |
| 376 | { |
| 377 | return resolved?.Path; |
| 378 | } |
| 379 | } |
| 380 | |
| 381 | var resolvedPath = Path.Combine(Path.GetDirectoryName(resolvedSource), relatedSource); |
| 382 | |
| 383 | if (!File.Exists(resolvedPath)) |
| 384 | { |
| 385 | checkedPaths.Add(resolvedPath); |
| 386 | this.Messaging.Write(ErrorMessages.FileNotFound(sourceLineNumbers, resolvedPath, type, checkedPaths)); |
| 387 | } |
| 388 | |
| 389 | return resolvedPath; |
| 390 | } |
| 391 | |
| 392 | private void ProcessRelatedBundles(XmlDocument document, XmlNamespaceManager namespaceManager, string sourcePath) |
| 393 | { |
| 394 | var sourceLineNumbers = this.PackagePayload.SourceLineNumbers; |
| 395 | |
| 396 | foreach (XmlElement relatedBundleElement in document.SelectNodes("/burn:BurnManifest/burn:RelatedBundle", namespaceManager)) |
| 397 | { |
| 398 | var id = relatedBundleElement.GetAttribute("Id"); |
| 399 | var actionValue = relatedBundleElement.GetAttribute("Action"); |
| 400 | |
| 401 | if (!Enum.TryParse(actionValue, out RelatedBundleActionType action)) |
| 402 | { |
| 403 | this.Messaging.Write(BurnBackendWarnings.UnknownBundleRelationAction(sourceLineNumbers, sourcePath, actionValue)); |
| 404 | continue; |
| 405 | } |
| 406 | |
| 407 | this.RelatedBundles.Add(new WixBundlePackageRelatedBundleSymbol(sourceLineNumbers) |
| 408 | { |
| 409 | PackagePayloadRef = this.PackagePayload.Id.Id, |
| 410 | BundleId = id, |
| 411 | Action = action, |
| 412 | }); |
| 413 | } |
| 414 | } |
| 415 | |
| 416 | private class ManifestContainer |
| 417 | { |
| 418 | public bool Attached { get; set; } |
| 419 | public string DownloadUrl { get; set; } |
| 420 | public string FilePath { get; set; } |
| 421 | public string Id { get; set; } |
| 422 | |
| 423 | public bool IncludedAsPayload { get; set; } |
| 424 | } |
| 425 | |
| 426 | private class ManifestPayload |
| 427 | { |
| 428 | public string Container { get; set; } |
| 429 | public string DownloadUrl { get; set; } |
| 430 | public string FilePath { get; set; } |
| 431 | public string Id { get; set; } |
| 432 | } |
| 433 | } |
| 434 | } |