Motivation
Installing Swift on Windows today means downloading an installer from swift.org and running it. That works for one toolchain, but anyone who needs a second version, say to test a package against 6.1 and 6.2, or to follow a project’s .swift-version file, ends up uninstalling and reinstalling by hand. On Linux and macOS, swiftly solves this with swiftly install and swiftly use. I also remember how painful Linux setup was before it existed.
Benefits to Community
This project makes it easier for developers to try Swift on Windows and to keep using it. A new user gets a working toolchain with one command. Someone who maintains a package can test against several compiler versions without reinstalling each time.
It also brings Windows in line with Linux and macOS. Once swiftly works the same way on all three, existing Swift users on macOS and Linux can move to Windows without learning a new workflow.
Overview
- Downloading a toolchain
- Installing a toolchain
- Enumerate existing toolchains
- Selecting & Invoking a toolchain
Downloading a toolchain
Using the same mechanism swiftly uses (OpenAPI), but replaces the transport layer with openapi-winhttp-transport (A Windows transport for Swift OpenAPI Runtime using asynchronous WinHTTP) that @compnerd implemented.
Installing a toolchain
We need to run the packages through Windows Installer to extract and install them.
The Swift toolchain is bundled in an .exe file. An .exe can contain one or more .msi packages. MSI packages are the native package format for Windows Installer. An MSI file is a database that lists files, registry keys, and other changes, which Windows Installer applies.
Windows maintains a record of every installed MSI product, identified by a ProductCode GUID, which we can use to enumerate existing products and uninstall them.
We can install a downloaded Swift toolchain using MSI APIs such as MsiInstallProductW().
Before installing the MSIs we need to extract .exe to the MSIs first, we can do that with a few ways:
- Change the upstream installer build or create a separate one for Swiftly. The
/layoutoption normally downloads the files needed for installation without installing the software. However, with the current Swift Windows installer, it only copies the.exebecause all MSIs and.cabfiles are bundled inside it. The installer scripts already support an online flavor that keeps these files separate. The Browser Company uses this flavor in its builds, where/layoutworks as expected. If swift.org published the MSIs separately, Swiftly could download the files it needs and install the MSIs normally. - Write our own parser with no new dependencies (swift-burn-extract). This option works with existing releases, unlike option 1, and requires no dependencies.
- Use WiX's Burn reader. This is a simple, robust solution, but it adds WiX and .NET dependencies:
- Call
wix burn extractfrom Swift (wix-shellout-probe). - Write a separate C# program using WiX as a library (wix-library-probe).
- Call
Enumerate existing toolchains
In this phase we need to enumerate the existing toolchains and get their locations, which we can use later in when listing toolchains and invoking a toolchain.
We can get a list of all installed products using an MSI API like MsiEnumProductsExW, and keep the ones whose publisher is swift.org.
We can get the version from MsiGetProductInfoExW. To get the installation location, we enumerate the installed components (MsiEnumComponentsExW), identify the ones associated with Swift products (MsiEnumClientsExW), and ask MsiGetComponentPathExW for the path to swift.exe. This gives us the actual path, even when the user has changed the installation folder.
A small demo of the functionality.
Selecting & Invoking a toolchain
On macOS and Linux, swiftly init adds .swiftly/bin to PATH through the shell’s startup file. Windows has no equivalent startup file. Path is a registry value, with a user copy and a system copy. On Windows, Swiftly could ship as an MSI that installs swiftly.exe and adds its bin directory to the user’s Path.
Then when running a toolchain it has proxies (files all linking to swiftly binary) with names like: swift and swiftc that runs swiftly with these names as argv[0](the program name) and swiftly runs in proxy mode with the binary (swift) as a subprocess with the selected toolchain version.
Swiftly picks the toolchain from .swift-version or the global default (inUse in config.json which Swiftly creates in .swiftly) and runs that toolchain’s binary.
This can also be run with swiftly run [clang++] for example.
On Windows we can do similar functionality with a few differences:
- Proxies are small stub
.exefiles, one per tool (swift.exe,swiftc.exe, and so on). The stub reads its own file name and runsswiftly run <name> <args>. All the selection logic stays inswiftly.exe. A small demo. - Windows has no
exec, so the proxy starts the realswift.exeas a child Path: Prepend the toolchain's binary directory toPath.- Path:
[Vendor]\Toolchains\[Version]+[Variant]\usr\bin - Example:
Swift\Toolchains\6.4.0+Asserts\usr\bin
- Path:
SDKROOT: SetSDKROOTto the Windows SDK path, or to the Android SDK path when cross-compiling for Android.- Add a flag to select the target SDK. It defaults to the build host’s platform (macOS SDK on macOS, Windows SDK on Windows), and users can pass a different SDK explicitly when cross-compiling.
- Path:
[Vendor]\Platforms\[Version]\[Platform].platform\Developer\SDKs\[Platform].sdk - Example:
Swift\Platforms\6.4.0\Windows.platform\Developer\SDKs\Windows.sdk