[Pitch] Bringing Swiftly to Windows

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:

  1. Change the upstream installer build or create a separate one for Swiftly. The /layout option normally downloads the files needed for installation without installing the software. However, with the current Swift Windows installer, it only copies the .exe because all MSIs and .cab files 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 /layout works as expected. If swift.org published the MSIs separately, Swiftly could download the files it needs and install the MSIs normally.
  2. Write our own parser with no new dependencies (swift-burn-extract). This option works with existing releases, unlike option 1, and requires no dependencies.
  3. Use WiX's Burn reader. This is a simple, robust solution, but it adds WiX and .NET dependencies:

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 .exe files, one per tool (swift.exe, swiftc.exe, and so on). The stub reads its own file name and runs swiftly run <name> <args>. All the selection logic stays in swiftly.exe. A small demo.
  • Windows has no exec, so the proxy starts the real swift.exe as a child
  • Path: Prepend the toolchain's binary directory to Path.
    • Path: [Vendor]\Toolchains\[Version]+[Variant]\usr\bin
    • Example: Swift\Toolchains\6.4.0+Asserts\usr\bin
  • SDKROOT: Set SDKROOT to 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
10 Likes

Hi AbdAlRahman, I'm Shubham. Would an independent Windows argument-forwarding test pass be useful for your shim probe? Its README lists embedded quotes as untested, and the quote function has separate handling for backslashes before a quote and at the end of an argument.

I'd propose a small matrix of empty arguments, spaces, embedded quotes and trailing backslashes, using a helper that prints the received arguments and returns a chosen exit code. That would separate shell parsing from the shim's forwarding and check exit-code propagation without needing a full compiler build. I haven't run these cases yet.

If you and the Swiftly maintainers want this contribution, could you delegate that validation scope and point me to the preferred tracking issue? I'd coordinate with your existing work before making changes.