|
| 1 | +# Windows installers |
| 2 | + |
| 3 | +## Online and offline bundles |
| 4 | + |
| 5 | +The Swift SDK bundle is now available in two flavors: |
| 6 | + |
| 7 | +- **Online**: A small .exe that downloads the packages the user selected at install time. Can download fewer total bytes but must be connected to the Internet. The work to stage the payloads required for the online bundle is left as an exercise for the reader. |
| 8 | +- **Offline**: A big .exe that contains all the packages and are extracted on-demand at install time. Downloads the most bytes but doesn't require live Internet connection. |
| 9 | + |
| 10 | +A third pseudo-flavor is using the online bundle with the `/layout` switch to create a layout that can be placed on a network or USB drive to install offline. |
| 11 | + |
| 12 | +### Download URLs |
| 13 | + |
| 14 | +For the online bundle, we need to provide a download URL for each package and its cabinet files (`*.cab`). (The `SourceFile` attribute is also required, both for the offline bundle and for Burn to get the hash of the packages. Note that the packages and .cabs available for download must exactly match the ones used when building the bundle. Burn validates downloads by their hashes.) |
| 15 | + |
| 16 | +(For the offline bundle, the download URLs are ignored so we can provide any string. That's easier than conditionalizing the `Chain` of packages and omitting the `DownloadUrl` attributes.) |
| 17 | + |
| 18 | +`BaseReleaseDownloadUrl` is a preprocessor variable that provides the URL to the directory containing the packages for that bundle. So, for example, for the bundle at `https://download.swift.org/swift-5.8.1-release/windows10/swift-5.8.1-RELEASE/swift-5.8.1-RELEASE-windows10.exe`, `BaseReleaseDownloadUrl` would be `https://download.swift.org/swift-5.8.1-release/windows10/swift-5.8.1-RELEASE`. |
| 19 | + |
| 20 | +Burn supports some [special syntax](https://wixtoolset.org/docs/schema/wxs/msipackage/) to simplify authoring download URLs. The one we use is `{2}`, which is replaced by the file name of the payload. |
| 21 | + |
| 22 | +Burn automatically expands payload authoring to handle .cab files that are external to their .msi files so `DownloadUrl="$(BaseReleaseDownloadUrl)/{2}"` turns into, for example: |
| 23 | + |
| 24 | +- `$(BaseReleaseDownloadUrl)/bld.msi` |
| 25 | +- `$(BaseReleaseDownloadUrl)/bld.cab` |
| 26 | + |
| 27 | + |
| 28 | +### Install directory and feature selection |
| 29 | + |
| 30 | +The bundle authoring (in `installer.wxs`) drives optional install directory and feature selection via `Variable`s that can be specified by the user at the command line and in the bundle UI on the Options page. |
| 31 | + |
| 32 | +| Variable | Description | |
| 33 | +| -------- | ----------- | |
| 34 | +| InstallRoot | A formatted string variable that specifies the installation root directory. The default value specified in `installer.wxs` should match the equivalent `INSTALLROOT` authoring in `shared.wxs`. The bundle variable is passed to each `MsiPackage` so overwrites the default directory authored in the MSI packages -- but keeping them in sync avoids the confusion if the default directory should change. | |
| 35 | +| OptionsInstallBld | Controls whether bld.msi will be installed. | |
| 36 | +| OptionsInstallCli | Controls whether cli.msi will be installed. | |
| 37 | +| OptionsInstallDbg | Controls whether dbg.msi will be installed. | |
| 38 | +| OptionsInstallIde | Controls whether ide.msi will be installed. | |
| 39 | +| OptionsInstallRtl | Controls whether rtl.msi will be installed. | |
| 40 | +| OptionsInstallSdkX86 | Controls whether the x86 SDK will be installed. | |
| 41 | +| OptionsInstallSdkAMD64 | Controls whether the AMD64 SDK will be installed. | |
| 42 | +| OptionsInstallSdkArm64 | Controls whether the Arm64 SDK will be installed. | |
| 43 | + |
| 44 | +Those variables are tied to controls in the bundle theme (`installer\theme.xml`) on the Options page. For example, the install directory is an `Editbox` control that takes the `InstallRoot` name to tie itself to the `InstallRoot` variable in `installer.wxs`: |
| 45 | + |
| 46 | +```xml |
| 47 | +<Editbox Name="InstallRoot" X="185" Y="46" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" /> |
| 48 | +``` |
| 49 | + |
| 50 | +Likewise, the feature selection controls are all checkboxes tied to the variables that control feature selection: |
| 51 | + |
| 52 | +```xml |
| 53 | +<Checkbox Name="OptionsInstallIde" X="185" Y="170" Width="-11" Height="17" TabStop="yes" FontId="3">#(loc.OptionsInstallIde)</Checkbox> |
| 54 | +``` |
| 55 | + |
| 56 | +`Checkbox` controls set variables to `1` when checked and `0` when unchecked. |
| 57 | + |
| 58 | +The variables are used in `installer.wxs` bundle authoring to control the installation of whole packages using the package `InstallCondition` attribute: |
| 59 | + |
| 60 | +```xml |
| 61 | +<MsiPackage |
| 62 | + SourceFile="!(bindpath.ide)\ide.msi" |
| 63 | + InstallCondition="OptionsInstallIde" |
| 64 | + DownloadUrl="$(BaseReleaseDownloadUrl)/{2}"> |
| 65 | + <MsiProperty Name="INSTALLROOT" Value="[InstallRoot]" /> |
| 66 | +</MsiPackage> |
| 67 | +``` |
| 68 | + |
| 69 | + |
| 70 | +### Bundle command line |
| 71 | + |
| 72 | +All Burn bundles support a common set of command-line switches that, unsurprisingly, mimic [those of Windows Installer](https://learn.microsoft.com/en-us/windows/win32/msi/standard-installer-command-line-options). The Swift bundle theme also implements a standard Help dialog when you run the bundle with a `/?` switch. |
| 73 | + |
| 74 | +For example, to uninstall the bundle showing UI but not requiring user interaction: |
| 75 | + |
| 76 | +```sh |
| 77 | +installer.exe /passive /uninstall |
| 78 | +``` |
| 79 | + |
| 80 | +The same variables that drive install directory and feature selection are available from the command line too: |
| 81 | + |
| 82 | + |
| 83 | +```sh |
| 84 | +installer.exe /passive InstallRoot=X:\Swift OptionsInstallIde=0 OptionsInstallSdkX86=0 OptionsInstallSdkArm64=0 |
| 85 | +``` |
| 86 | + |
| 87 | +To create a layout from a online bundle to allow for offline install: |
| 88 | + |
| 89 | +```sh |
| 90 | +installer.exe /layout path\to\layout\directory |
| 91 | +``` |
| 92 | + |
| 93 | + |
| 94 | +## MSBuild projects |
| 95 | + |
| 96 | +### Directory.Build.props |
| 97 | + |
| 98 | +MSBuild automatically imports Directory.Build.props files in your tree. We use Directory.Build.props to centralize MSBuild properties that are shared among the .wixproj projects for the installers. Here are some of the interesting ones: |
| 99 | + |
| 100 | +| Property | Description | |
| 101 | +| -------- | ----------- | |
| 102 | +| MajorMinorProductVersion | Gets the `major.minor` fields of `ProductVersion`. Used for the side-by-side upgrade strategy. | |
| 103 | +| BaseIntermediateOutputPath, OutputPath | Sets the intermediate and build output directories to handle MSBuild/NuGet restore functionality and support multiplatform output. | |
| 104 | +| SuppressIces | Suppress the ICE validation messages that are erroneously emitted for per-user packages. **ICE38** is about mixing per-user and per-machine resources (not a thing for us). **ICE61** is warning about allowing "same-version" major upgrades, something we want. **ICE64** is documented as not being an issue when packages are always per-user. **ICE91** is about "roaming scenarios," which doesn't apply to our use of `LocalAppDataFolder`. | |
| 105 | +| PackageCompressionLevel, BundleCompressionLevel | Always set to `high` for the smallest downloads (and painfully-long build times). It's a property so it can be overridden for dev builds. See _<user>.props_. | |
| 106 | +| ArePackageCabsEmbedded | Always set to false to keep the .cab files external to the .msi files. This save user disk space: Burn caches packages so it can always uninstall and repair. MSI also caches packages for uninstall. If the cab is embedded, you have two copies and MSI doesn't always use its cached copy as a source for repair. With an external .cab, MSI caches only the tiny .msi file and not the (relatively huge) .cab. | |
| 107 | +| BundleFlavor, IsBundleCompressed | BundleFlavor defaults to `online` to build an online bundle. Set by the invocation of MSBuild to build an online or offline bundle. Controls IsBundleCompressed. | |
| 108 | +| DefineConstants | Passes a subset of MSBuild properties into the WiX build as preprocessor variables. | |
| 109 | +| INCLUDE_SWIFT_FORMAT, INCLUDE_SWIFT_INSPECT | swift-format and swift-inspect are currently conditionalized out. Set these to `true` to include them. The properties `SWIFT_FORMAT_BUILD` and `SWIFT_INSPECT_BUILD` define the directories to find the .exes. | |
| 110 | +| INCLUDE_X86_SDK, INCLUDE_ARM64_SDK | The x86 and Arm64 SDKs are currently conditionalized out, pending build changes. Set these to `true` to include them in the bundles. Note that bundle\theme.xml currently has commented-out checkboxes that need to be restored when the x86 and Arm64 SDKs are brought back. | |
| 111 | + |
| 112 | + |
| 113 | +## SDKs |
| 114 | + |
| 115 | +The SDKs contain architecture-specific payloads like libraries and architecture-neutral payloads like headers. However, as Arm64 packages cannot be installed on an x64 Windows host, all SDKs are built as x86 packages. (Windows Installer doesn't have the concept of an architecture-neutral package, so we use x86 as the package architecture that can be installed on all Windows hosts.) |
| 116 | + |
| 117 | +Because we need 32-bit components in an x86 package, we have to force the shared .wixlib project to an x86 `Platform` at the same using the correct `ProductArchitecture` for the SDK being built. That happens by setting those properties in the `ProjectReference`: |
| 118 | + |
| 119 | +```xml |
| 120 | +<ProjectReference Include="..\shared\shared.wixproj" Properties="ProductArchitecture=arm64;Platform=x86" /> |
| 121 | +``` |
| 122 | + |
| 123 | + |
| 124 | +## User.props |
| 125 | + |
| 126 | +Also imported by Directory.Build.props is a user-specific .props file in same directory as Directory.Build.props: |
| 127 | + |
| 128 | +```xml |
| 129 | +<Import Project="$(USERNAME).props" Condition="Exists('$(USERNAME).props')" /> |
| 130 | +``` |
| 131 | + |
| 132 | +That lets you override settings in Directory.Build.props for local dev builds. For example, you can dramatically increase build speed with a couple of property tweaks: |
| 133 | + |
| 134 | +```xml |
| 135 | +<Project> |
| 136 | + <PropertyGroup> |
| 137 | + <PackageCompressionLevel>none</PackageCompressionLevel> |
| 138 | + <BundleCompressionLevel>none</BundleCompressionLevel> |
| 139 | + </PropertyGroup> |
| 140 | +</Project> |
| 141 | +``` |
| 142 | + |
| 143 | + |
| 144 | +## Building the installers |
| 145 | + |
| 146 | +`ProjectReference`s between the various .wixproj projects establish the correct dependencies and build order, so while you can still build individual .wixproj projects, you can build the bundle and have it automatically build all the MSI packages. |
| 147 | + |
| 148 | +To support the three architecture flavors of the SDK and RTL MSI packages, you need to pass in architecture-specific paths in MSBuild properties. Directory.Build.props translates them to "generic" preprocessor variables in the SDK .wxs authoring: |
| 149 | + |
| 150 | +| MSBuild property | Description | |
| 151 | +| ---------------- | ----------- | |
| 152 | +| PLATFORM_ROOT_X86 | PLATFORM_ROOT when building x86 SDK and RTL | |
| 153 | +| PLATFORM_ROOT_AMD64 | PLATFORM_ROOT when building AMD64 SDK and RTL | |
| 154 | +| PLATFORM_ROOT_ARM64 | PLATFORM_ROOT when building ARM64 SDK and RTL | |
| 155 | +| SDK_ROOT_X86 | SDK_ROOT when building x86 SDK and RTL | |
| 156 | +| SDK_ROOT_AMD64 | SDK_ROOT when building AMD64 SDK and RTL | |
| 157 | +| SDK_ROOT_ARM64 | SDK_ROOT when building ARM64 SDK and RTL | |
| 158 | + |
| 159 | + |
| 160 | +```sh |
| 161 | +msbuild %SourceRoot%\swift-installer-scripts\platforms\Windows\bundle\installer.wixproj ^ |
| 162 | + -m ^ |
| 163 | + -restore ^ |
| 164 | + -p:BundleFlavor=online ^ |
| 165 | + -p:BaseReleaseDownloadUrl=todo://base/release/download/url ^ |
| 166 | + -p:Configuration=Release ^ |
| 167 | + -p:BaseOutputPath=%PackageRoot%\online\ ^ |
| 168 | + -p:DEVTOOLS_ROOT=%BuildRoot%\Library\Developer\Toolchains\unknown-Asserts-development.xctoolchain ^ |
| 169 | + -p:TOOLCHAIN_ROOT=%BuildRoot%\Library\Developer\Toolchains\unknown-Asserts-development.xctoolchain ^ |
| 170 | + -p:PLATFORM_ROOT_X86=path\to\x86\platform ^ |
| 171 | + -p:PLATFORM_ROOT_AMD64=path\to\amd64\platform ^ |
| 172 | + -p:PLATFORM_ROOT_ARM64=path\to\arm64\platform ^ |
| 173 | + -p:SDK_ROOT_X86=path\to\x86\sdk ^ |
| 174 | + -p:SDK_ROOT_AMD64=path\to\amd64\sdk ^ |
| 175 | + -p:SDK_ROOT_ARM64=path\to\arm64\sdk |
| 176 | +``` |
| 177 | + |
| 178 | + |
| 179 | +## Side-by-side upgrade strategy |
| 180 | + |
| 181 | +The Swift side-by-side upgrade strategy lets you retain the latest patch release of each major.minor release of the Swift SDK. So, for example, if you were to install these (hypothetical) versions of the Swift SDK: |
| 182 | + |
| 183 | +- 5.9.0 |
| 184 | +- 5.9.1 |
| 185 | +- 5.9.2 |
| 186 | +- 5.10.0 |
| 187 | +- 5.10.1 |
| 188 | +- 5.10.5 |
| 189 | +- 5.11.2 |
| 190 | +- 6.0.0 |
| 191 | +- 6.0.1 |
| 192 | +- 6.5.1 |
| 193 | +- 6.5.7 |
| 194 | + |
| 195 | +you'd be left with these versions: |
| 196 | + |
| 197 | +- ~~5.9.0~~ |
| 198 | +- ~~5.9.1~~ |
| 199 | +- **5.9.2** |
| 200 | +- ~~5.10.0~~ |
| 201 | +- ~~5.10.1~~ |
| 202 | +- **5.10.5** |
| 203 | +- **5.11.2** |
| 204 | +- ~~6.0.0~~ |
| 205 | +- **6.0.1** |
| 206 | +- ~~6.5.1~~ |
| 207 | +- **6.5.7** |
| 208 | + |
| 209 | + |
| 210 | +### SideBySideUpgradeStrategy.props |
| 211 | + |
| 212 | +SideBySideUpgradeStrategy.props is imported by Directory.Build.props to bring in the GUIDs used to handle side-by-side upgrades. |
| 213 | + |
| 214 | +Note that these GUIDs are substituted at bind time so they skip the normal validation and cleanup that the compiler does and therefore must be "proper" GUIDs: |
| 215 | + |
| 216 | +- All uppercase |
| 217 | +- Surrounded by curly braces |
| 218 | + |
| 219 | +| Property | Description | |
| 220 | +| -------- | ----------- | |
| 221 | +| BldUpgradeCode, CliUpgradeCode, DbgUpgradeCode, IdeUpgradeCode, RtlUpgradeCode, SdkUpgradeCode | Upgrade codes for individual packages. Packages keep the same upgrade codes "forever" because MSI lets you specify version ranges for upgrades, which you can find in `shared/shared.wxs`. | |
| 222 | +| BundleUpgradeCode | Upgrade codes for the bundle. Bundles don't support upgrade version ranges, so the bundle upgrade code must change for every minor version _and_ stay the same for the entire lifetime of that minor version (e.g., v5.10.0 through v5.10.9999). You can keep the history of upgrade codes using a condition like `Condition="'$(MajorMinorProductVersion)' == '5.10'` or just replace BundleUpgradeCode when forking to a new minor version. | |
| 223 | + |
| 224 | + |
| 225 | +### shared\shared.wxs |
| 226 | + |
| 227 | +To support side-by-side installation for each minor release (the latest point release of each minor release), we need to use "old-school" `Upgrade`/`UpgradeVersion` authoring to get the upgrade version ranges, which also requires manually scheduling `RemoveExistingProducts`. (We can no longer use WiX's `MajorUpgrade` element because it's intended to support the way-more-common case of upgrading every version.) To avoid duplication, the upgrade logic is authored in `shared\shared.wxs` and referenced from the `Package` element of each package: |
| 228 | + |
| 229 | +```xml |
| 230 | +<WixVariable Id="SideBySidePackageUpgradeCode" Value="$(SdkUpgradeCode)" /> |
| 231 | +<FeatureGroupRef Id="SideBySideUpgradeStrategy" /> |
| 232 | +``` |
| 233 | + |
| 234 | +`SideBySidePackageUpgradeCode` is a bind-time variable, used here essentially to pass the upgrade code as an argument to the fragment in `shared\shared.wxs`. |
| 235 | + |
| 236 | + |
| 237 | +### Cleaning up |
| 238 | + |
| 239 | +Windows Installer can, under conditions that are both mysterious and undocumented, leave behind empty folders. (The files are removed.) The ICE validator ICE64 says this is a problem for roaming profiles so our use of local AppData shouldn't apply, but, sadly, sometimes it does. ICE64 suggests the user-hostile workaround of manually authoring to remove every directory. Instead, Directory.Build.props suppresses ICE64 and in `shared\shared.wxs`, the `VersionedDirectoryCleanup` component group handles cleaning up any orphaned, empty directories. Every package has a `<ComponentGroupRef Id="VersionedDirectoryCleanup" />` to pull in that component group. |
| 240 | + |
| 241 | + |
| 242 | +## Compression levels, build time, and download size |
| 243 | + |
| 244 | +As expected, the `high` compression level builds the smallest payloads with the benefit that you have enough time to not only go get a coffee after kicking off the build, you can drink it _and_ get a refill with time to spare. |
| 245 | + |
| 246 | +Because both MSI and Burn use cabinets for containers and compression, the results of both packages and bundles are consistent. MSI is almost certainly using cabinets forever but Burn could adopt a different mechanism in the future (e.g., LZMA) that could improve these results. |
| 247 | + |
| 248 | +Until that point, however, the authoring uses the older `Media` element to ensure a single .cab for maximal compression of each .msi file's content. |
| 249 | + |
| 250 | +I tried a few combinations to show the tradeoffs of size and build time. |
| 251 | + |
| 252 | + |
| 253 | +### Offline bundle |
| 254 | + |
| 255 | +| Package compression | Bundle compression | Build time | Bundle size | |
| 256 | +| ------------------- | ------------------ | ---------- | ----------- | |
| 257 | +| High | High | 12m51s | ~470MB | |
| 258 | +| High | None | 9m47s | ~472MB | |
| 259 | +| None | High | 10m0s | ~479MB | |
| 260 | +| None | Medium | 5m46s | ~513MB | |
| 261 | +| None | Low | 4m21s | ~552MB | |
| 262 | +| None | None | 43s | ~1.6GB | |
| 263 | + |
| 264 | + |
| 265 | +### Online bundle |
| 266 | + |
| 267 | +The bundle .exe itself gets a decent amount of compression: |
| 268 | + |
| 269 | +| Compression level | Bundle size | |
| 270 | +| ----------------- | ----------- | |
| 271 | +| High | ~950K | |
| 272 | +| None | ~1.2MB | |
| 273 | + |
| 274 | +The payloads show similar sizes and build times as for the offline bundle, except that building the online bundle compresses only the .exe itself, whereas the offline bundle compresses the payloads and then (tries and mostly fails to) compress them again into the .exe: |
| 275 | + |
| 276 | +| Compression level | Build time | Download size | |
| 277 | +| ----------------- | ---------- | ------------- | |
| 278 | +| High | 9m47s | ~470MB | |
| 279 | +| Medium | 5m37s | ~504MB | |
| 280 | +| Low | 4m10s | ~544MB | |
| 281 | +| None | 36s | ~1.6GB | |
| 282 | + |
| 283 | + |
| 284 | +## Getting files without installing |
| 285 | + |
| 286 | +Windows Installer has what it calls [administrative installations](https://learn.microsoft.com/en-us/windows/win32/msi/administrative-installation) that let you "unzip" an .msi package and get its files without actually installing the package. |
| 287 | + |
| 288 | +```sh |
| 289 | +msiexec /qb /a build\amd64\bld.msi TARGETDIR=X:\swift.bld.admin |
| 290 | +``` |
| 291 | + |
| 292 | +| Argument | Description | |
| 293 | +| -------- | ----------- | |
| 294 | +| /qb | Run a "passive" UI with a progress bar but no user interaction. | |
| 295 | +| /a | Run an administrative installation. | |
| 296 | +| TARGETDIR= | Specify the output path for the package's files. All the Swift packages can specify the same TARGETDIR. | |
| 297 | + |
| 298 | +Administrative installations don't touch the machine (including the registry) outside the directory you specify for `TARGETDIR`. |
| 299 | + |
| 300 | + |
| 301 | +## Testing in Windows Sandbox |
| 302 | + |
| 303 | +Windows Sandbox is a Windows 10-and-later virtualization feature that is convenient and fairly easy to (mostly) automate. The Swift per-user bundles are reliable and low-impact so you don't always need to use virtual machines for testing, if you feel daring. But Sandbox's automatability makes it handy for "bulkier" testing, like the side-by-side feature. The files in samples\tests\SxS are what I used for that testing. |
| 304 | + |
| 305 | +> Note that these files assume that there is a directory `X:\sandbox` directory that holds the test collateral and is shared with the Sandbox VM. This is a convention I use to avoid over-exposing the host machine to Sandbox VMs, which is more important when you're using Sandbox for riskier testing. |
| 306 | +
|
| 307 | +| File | Description | |
| 308 | +| ---- | ----------- | |
| 309 | +| BuildTests.cmd | Batch file that builds many different versions of the online bundle to `X:\sandbox\Swift\builds`. | |
| 310 | +| RunSxSTests.cmd | Batch file that runs in the Sandbox VM when it boots (as specified in SxSTesting.wsb). It opens a couple of useful Explorer windows then executes the side-by-side test bundles. This file is expected to live in the `X:\sandbox\Swift` directory and BuildTests.cmd copies it there. | |
| 311 | +| SxSTesting.wsb | Windows Sandbox configuration that maps `X:\sandbox\Swift` to `C:\sandbox` inside the Sandbox VM. The directory is read/write from the Sandbox VM so the host can build the test collateral into that tree and the Sandbox VM can write logs and test results. That means you can inspect results using your favorite tools on the host machine, rather than just Notepad in the basic Windows image running in the Sandbox VM. This is the file you launch to open the Sandbox VM and can live in any handy directory. | |
| 312 | + |
| 313 | +A cut-down version with a smaller number of bundles being tested is available in the samples\tests\MiniSxS. |
| 314 | + |
| 315 | + |
| 316 | +## Samples |
| 317 | + |
| 318 | +`HelloMergeModule` is a sample WiX project that installs a Swift-built app and the appropriate RTL merge module. To build it, you need to set the `RedistributablesDirectory` property to the redistributables directory for a particular version of Swift: |
| 319 | + |
| 320 | +```sh |
| 321 | +msbuild -Restore -p:RedistributablesDirectory=X:\Swift\Redistributables\0.0.0 -p:Platform=Arm64 hellomm.wixproj |
| 322 | +``` |
0 commit comments