| 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.Reflection; |
| 9 | using System.Runtime.InteropServices; |
| 10 | using WixToolset.Data; |
| 11 | using WixToolset.Data.Burn; |
| 12 | using WixToolset.Data.Symbols; |
| 13 | using WixToolset.Dtf.Resources; |
| 14 | using WixToolset.Extensibility.Data; |
| 15 | using WixToolset.Extensibility.Services; |
| 16 | using WixToolset.Versioning; |
| 17 | |
| 18 | internal class CreateBundleExeCommand |
| 19 | { |
| 20 | public CreateBundleExeCommand(IMessaging messaging, IFileSystem fileSystem, IBackendHelper backendHelper, string intermediateFolder, string outputPath, WixBundleSymbol bundleSymbol, WixBundleContainerSymbol uxContainer, IEnumerable<WixBundleContainerSymbol> containers) |
| 21 | { |
| 22 | this.Messaging = messaging; |
| 23 | this.FileSystem = fileSystem; |
| 24 | this.BackendHelper = backendHelper; |
| 25 | this.IntermediateFolder = intermediateFolder; |
| 26 | this.OutputPath = outputPath; |
| 27 | this.BundleSymbol = bundleSymbol; |
| 28 | this.UXContainer = uxContainer; |
| 29 | this.Containers = containers; |
| 30 | } |
| 31 | |
| 32 | public IFileTransfer Transfer { get; private set; } |
| 33 | |
| 34 | private IMessaging Messaging { get; } |
| 35 | |
| 36 | private IFileSystem FileSystem { get; } |
| 37 | |
| 38 | private IBackendHelper BackendHelper { get; } |
| 39 | |
| 40 | private string IntermediateFolder { get; } |
| 41 | |
| 42 | private string OutputPath { get; } |
| 43 | |
| 44 | private WixBundleSymbol BundleSymbol { get; } |
| 45 | |
| 46 | private WixBundleContainerSymbol UXContainer { get; } |
| 47 | |
| 48 | private IEnumerable<WixBundleContainerSymbol> Containers { get; } |
| 49 | |
| 50 | public void Execute() |
| 51 | { |
| 52 | var bundleFilename = Path.GetFileName(this.OutputPath); |
| 53 | |
| 54 | // Copy the burn.exe to a writable location then mark it to be moved to its final build location. |
| 55 | |
| 56 | var stubPlatform = this.BundleSymbol.Platform.ToString(); |
| 57 | var stubFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), stubPlatform, "burn.exe"); |
| 58 | var bundleTempPath = Path.Combine(this.IntermediateFolder, bundleFilename); |
| 59 | |
| 60 | this.Messaging.Write(VerboseMessages.GeneratingBundle(bundleTempPath, stubFile)); |
| 61 | |
| 62 | if ("setup.exe".Equals(bundleFilename, StringComparison.OrdinalIgnoreCase)) |
| 63 | { |
| 64 | this.Messaging.Write(ErrorMessages.InsecureBundleFilename(bundleFilename)); |
| 65 | } |
| 66 | |
| 67 | this.Transfer = this.BackendHelper.CreateFileTransfer(bundleTempPath, this.OutputPath, true, this.BundleSymbol.SourceLineNumbers); |
| 68 | |
| 69 | this.FileSystem.CopyFile(this.BundleSymbol.SourceLineNumbers, stubFile, bundleTempPath, allowHardlink: false); |
| 70 | File.SetAttributes(bundleTempPath, FileAttributes.Normal); |
| 71 | |
| 72 | var fourPartVersion = this.GetFourPartVersion(this.BundleSymbol); |
| 73 | |
| 74 | this.UpdateBurnResources(bundleTempPath, this.OutputPath, this.BundleSymbol, fourPartVersion); |
| 75 | |
| 76 | // Update the .wixburn section to point to at the UX and attached container(s) then attach the containers |
| 77 | // if they should be attached. |
| 78 | using (var writer = BurnWriter.Open(this.Messaging, this.FileSystem, bundleTempPath)) |
| 79 | { |
| 80 | var burnStubFile = new FileInfo(bundleTempPath); |
| 81 | writer.InitializeBundleSectionData(burnStubFile.Length, this.BundleSymbol.BundleId); |
| 82 | |
| 83 | // Always attach the UX container first |
| 84 | writer.AppendContainer(this.UXContainer.SourceLineNumbers, this.UXContainer.WorkingPath, BurnWriter.Container.UX); |
| 85 | |
| 86 | // Now append all other attached containers |
| 87 | foreach (var container in this.Containers) |
| 88 | { |
| 89 | if (ContainerType.Attached == container.Type) |
| 90 | { |
| 91 | // The container was only created if it had payloads. |
| 92 | if (!String.IsNullOrEmpty(container.WorkingPath) && BurnConstants.BurnUXContainerName != container.Id.Id) |
| 93 | { |
| 94 | writer.AppendContainer(container.SourceLineNumbers, container.WorkingPath, BurnWriter.Container.Attached); |
| 95 | } |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | private Version GetFourPartVersion(WixBundleSymbol bundleSymbol) |
| 102 | { |
| 103 | // Ensure the bundle info provides a full four-part version. |
| 104 | |
| 105 | if (!WixVersion.TryParse(bundleSymbol.Version, out var wixVersion)) |
| 106 | { |
| 107 | // Display an error message indicating that we will require a four-part version number |
| 108 | // not just a WixVersion. |
| 109 | this.Messaging.Write(ErrorMessages.IllegalVersionValue(bundleSymbol.SourceLineNumbers, "Bundle", "Version", bundleSymbol.Version)); |
| 110 | return new Version(0, 0); |
| 111 | } |
| 112 | |
| 113 | var major = wixVersion.Major; |
| 114 | var minor = wixVersion.Minor; |
| 115 | var build = wixVersion.Patch; |
| 116 | var revision = wixVersion.Revision; |
| 117 | |
| 118 | if (UInt16.MaxValue < major || UInt16.MaxValue < minor || UInt16.MaxValue < build || UInt16.MaxValue < revision) |
| 119 | { |
| 120 | major = Math.Max(major, UInt16.MaxValue); |
| 121 | minor = Math.Max(minor, UInt16.MaxValue); |
| 122 | build = Math.Max(build, UInt16.MaxValue); |
| 123 | revision = Math.Max(revision, UInt16.MaxValue); |
| 124 | |
| 125 | this.Messaging.Write(BurnBackendWarnings.CannotParseBundleVersionAsFourPartVersion(bundleSymbol.SourceLineNumbers, bundleSymbol.Version)); |
| 126 | } |
| 127 | |
| 128 | return new Version((int)major, (int)minor, (int)build, (int)revision); |
| 129 | } |
| 130 | |
| 131 | private void UpdateBurnResources(string bundleTempPath, string outputPath, WixBundleSymbol bundleInfo, Version fourPartVersion) |
| 132 | { |
| 133 | const int burnLocale = 1033; |
| 134 | var resources = new ResourceCollection(); |
| 135 | var version = new VersionResource("#1", burnLocale); |
| 136 | |
| 137 | version.Load(bundleTempPath); |
| 138 | resources.Add(version); |
| 139 | |
| 140 | version.FileVersion = fourPartVersion; |
| 141 | version.ProductVersion = fourPartVersion; |
| 142 | |
| 143 | var strings = version[burnLocale] ?? version.Add(burnLocale); |
| 144 | strings["LegalCopyright"] = bundleInfo.Copyright; |
| 145 | strings["OriginalFilename"] = Path.GetFileName(outputPath); |
| 146 | strings["FileVersion"] = bundleInfo.Version; // string versions do not have to be four parts. |
| 147 | strings["ProductVersion"] = bundleInfo.Version; // string versions do not have to be four parts. |
| 148 | |
| 149 | if (!String.IsNullOrEmpty(bundleInfo.Name)) |
| 150 | { |
| 151 | strings["ProductName"] = bundleInfo.Name; |
| 152 | strings["FileDescription"] = bundleInfo.Name; |
| 153 | } |
| 154 | |
| 155 | if (!String.IsNullOrEmpty(bundleInfo.Manufacturer)) |
| 156 | { |
| 157 | strings["CompanyName"] = bundleInfo.Manufacturer; |
| 158 | } |
| 159 | else |
| 160 | { |
| 161 | strings["CompanyName"] = String.Empty; |
| 162 | } |
| 163 | |
| 164 | if (bundleInfo.IconSourceFile != null) |
| 165 | { |
| 166 | var iconGroup = new GroupIconResource("#1", burnLocale); |
| 167 | iconGroup.ReadFromFile(bundleInfo.IconSourceFile.Path); |
| 168 | resources.Add(iconGroup); |
| 169 | |
| 170 | foreach (var icon in iconGroup.Icons) |
| 171 | { |
| 172 | resources.Add(icon); |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | var splashScreenType = BURN_SPLASH_SCREEN_TYPE.BURN_SPLASH_SCREEN_TYPE_NONE; |
| 177 | |
| 178 | if (bundleInfo.SplashScreenSourceFile != null) |
| 179 | { |
| 180 | var bitmap = new BitmapResource("#1", burnLocale); |
| 181 | bitmap.ReadFromFile(bundleInfo.SplashScreenSourceFile.Path); |
| 182 | resources.Add(bitmap); |
| 183 | |
| 184 | splashScreenType = BURN_SPLASH_SCREEN_TYPE.BURN_SPLASH_SCREEN_TYPE_BITMAP_RESOURCE; |
| 185 | } |
| 186 | |
| 187 | var splashScreenConfig = new BURN_SPLASH_SCREEN_CONFIGURATION |
| 188 | { |
| 189 | Type = splashScreenType, |
| 190 | ResourceId = 1, |
| 191 | }; |
| 192 | |
| 193 | var splashScreenConfigResource = new Resource(ResourceType.RCData, "#1", burnLocale, splashScreenConfig.ToBytes()); |
| 194 | resources.Add(splashScreenConfigResource); |
| 195 | |
| 196 | try |
| 197 | { |
| 198 | this.FileSystem.ExecuteWithRetries(() => resources.Save(bundleTempPath)); |
| 199 | } |
| 200 | catch (IOException e) |
| 201 | { |
| 202 | this.Messaging.Write(BurnBackendErrors.FailedToUpdateBundleResources(bundleInfo.SourceLineNumbers, bundleInfo.IconSourceFile?.Path, bundleInfo.SplashScreenSourceFile?.Path, e.Message)); |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | enum BURN_SPLASH_SCREEN_TYPE |
| 207 | { |
| 208 | BURN_SPLASH_SCREEN_TYPE_NONE, |
| 209 | BURN_SPLASH_SCREEN_TYPE_BITMAP_RESOURCE, |
| 210 | } |
| 211 | |
| 212 | [StructLayout(LayoutKind.Sequential)] |
| 213 | struct BURN_SPLASH_SCREEN_CONFIGURATION |
| 214 | { |
| 215 | [MarshalAs(UnmanagedType.I4)] |
| 216 | public BURN_SPLASH_SCREEN_TYPE Type; |
| 217 | |
| 218 | [MarshalAs(UnmanagedType.U2)] |
| 219 | public ushort ResourceId; |
| 220 | |
| 221 | public byte[] ToBytes() |
| 222 | { |
| 223 | var cb = Marshal.SizeOf(this); |
| 224 | var data = new byte[cb]; |
| 225 | var pBuffer = Marshal.AllocHGlobal(cb); |
| 226 | |
| 227 | try |
| 228 | { |
| 229 | Marshal.StructureToPtr(this, pBuffer, true); |
| 230 | Marshal.Copy(pBuffer, data, 0, cb); |
| 231 | return data; |
| 232 | } |
| 233 | finally |
| 234 | { |
| 235 | Marshal.FreeHGlobal(pBuffer); |
| 236 | } |
| 237 | } |
| 238 | } |
| 239 | } |
| 240 | } |