Skip to content

Commit 9eb8c91

Browse files
committed
DRAFT: Merge installation logic into swiftly as an init subcommand
Instead of relying on a separate shell script with certain issues swiftly can perform its own user initialization with many of the features of the script, including prompts, flags, and options. It also prepares swiftly for possible delivery through system package managers. Provide a design for the new installation system in the DESIGN.md. Change SwiftlyVersion to make it more conformant to the semantic version specification (https://semver.org) with respect to pre releases. Add test cases that cover typical scenarios. Update the swiftly version to a "dev" pre-release.
1 parent ab38db0 commit 9eb8c91

File tree

4 files changed

+97
-18
lines changed

4 files changed

+97
-18
lines changed

DESIGN.md

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,35 @@ This document contains the high level design of swiftly. Not all features have b
1919
- [Implementation sketch - macOS](#implementation-sketch---macos)
2020
- [`config.json` schema](#configjson-schema)
2121

22-
## Linux
23-
2422
### Installation of swiftly
2523

26-
We'll need a bootstrapping script which detects information about the OS and downloads the correct pre-built swiftly executable. We can use [rustup-init.sh](https://github.com/rust-lang/rustup/blob/master/rustup-init.sh) as a general guide on implementing such a script, though it is more complicated and supports far more systems than I think we need to. At least for the initial release, I think we'll only need to support the platforms listed on [Swift.org - Getting Started](http://swift.org/getting-started), namely:
27-
- Ubuntu 16.04
28-
- Ubuntu 18.04
29-
- Ubuntu 20.04
30-
- CentOS 7
31-
- CentOS 8
32-
- Amazon Linux 2
24+
We'll need an initialization mode which, detects information about the OS and distribution in the case of Linux. The initialization mode is also responsible for setting up the directory structure for the toolchains (if necessary), setting up the shell environment for the user, and determining any operating system level dependencies that are required, but missing. Swiftly can perform these tasks itself with the capabilities provided by the Swift language and libraries, such as rich argument parsing, and launching system processes provided that the binary is somehow delivered to the user.
25+
26+
The delivery of the binary onto the user's system can be accomplished in a number of ways, such as direct download from a trusted website with some guidance for the user to pick the right one for their OS and architecture. There might also be a copy/paste of a simple shell command-line for some more automation. Swiftly might be provided as a system-level package (e.g. homebrew, apt, yum, macOS pkg, etc). In some cases delivery might come from manual compilation from this git repository and the "swift build" command, useful for swiftly development and testing.
27+
28+
In any case the delivery mechanism transitions to the initialization and these two parts form the complete installation process. Initialization is still needed since none of the delivery mechanisms typically modify the users' home directory and environment and it will be very useful to manage one workflow for everyone.
29+
30+
The trigger for initialization is primarily swiftly's init subcommand, which can be invoked at any time to ensure that swiftly is initialized for the user, guiding them through the process if it isn't. This also serves as an entry point to upgrade if the user has an older swiftly installation. The init subcommand is responsible for setting up an initial directory structure for storing toolchains, the swiftly configuration `config.json`, and a shell environment `env.sh` (or similar for the user's shell). Operating system depenencies are checked, and if any are missing then the user is prompted to either install them automatically, or they are given the package installation commands to do so themselves. Note that the init can be invoked without any prompts, with supporting flags to provide needed information, to support scripted installations.
31+
32+
```
33+
swiftly init
34+
```
35+
36+
Since an end user may not be aware that swiftly has an initialiation step, its possible that they will invoke it in an uninitialized state on their account. If this happens then swiftly validates its own installation every time it is invoked and helps the user to discover the init subcommand. A system package, or website, can use an accelerator to help guide the user to the init subcommand by naming the swiftly binary as `swiftly-init*`. Swiftly discovers that it is being invoked in this way and automatically runs the initialization right away. If the system package installs swiftly-init in a location that's on the user's default path, tab completion of `swiftly` will usually complete to the full name and the user is directed to run the initialization by default. However, if swiftly is already on the user's path then the completion will be unintrusive.
37+
38+
```
39+
swiftly-init-0.4.0 # Runs 'swiftly init' subcommand to initialize swiftly
40+
```
41+
42+
### Swiftly upgrades
43+
44+
It is possible that initialization will detect an existing swiftly installation at an older version, which then performs an upgrade procedure. If the existing installation is newer (using the `--version flag`) then the initialization is aborted. This gives the user the opportunity to upgrade to a newer version of swiftly by manually downloading it from a trusted website or ad-hoc test a pre-release.
45+
46+
Another upgrade mechanism is through a system package manager that updates the swiftly-init package. Whenever you run swiftly for any reason it will check if there's a newer swiftly-init binary and warn that a new release is available.
3347

34-
Once it has detected which platform the user is running, the script will then create `$HOME/.local/share/swiftly` (or a different path, if the user provides one. For an initial MVP, I think we can always install there). It'll also create `$HOME/.local/bin` if needed, download the prebuilt swiftly executable appropriate for the platform, and drop it in there.
48+
Regardless of the delivery mechanism, swiftly has its own self-update subcommand that downloads and verifies swiftly from the trusted source, and delegates to its init to perform the upgrade. This gives the user an escape hatch to get the latest independently of the delivery mechanism that they chose initially.
49+
50+
## Linux
3551

3652
### Installation of a Swift toolchain
3753

@@ -52,11 +68,6 @@ The `~/.local/bin` directory would include symlinks pointing to the `bin` direct
5268
This is all very similar to how rustup does things, but I figure there's no need to reinvent the wheel here.
5369

5470
## macOS
55-
### Installation of swiftly
56-
57-
Similar to Linux, the bootstrapping script for macOS will create a `~/.swiftly/bin` directory and drop the swiftly executable in it. A similar `~/.swiftly/env` file will be created and a message will be printed suggesting users add source `~/.swiftly/env` to their `.bash_profile` or `.zshrc`.
58-
59-
The bootstrapping script will detect if xcode is installed and prompt the user to install it if it isn’t. We could also ask the user if they’d like us to install the xcode command line tools for them via `xcode-select --install`.
6071

6172
### Installation of a Swift toolchain
6273

Sources/SwiftlyCore/SwiftlyCore.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
22

3-
public let version = SwiftlyVersion(major: 0, minor: 3, patch: 0)
3+
public let version = SwiftlyVersion(major: 0, minor: 4, patch: 0, suffix: "dev")
44

55
/// A separate home directory to use for testing purposes. This overrides swiftly's default
66
/// home directory location logic.

Sources/SwiftlyCore/SwiftlyVersion.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import Foundation
33

44
/// Struct modeling a version of swiftly itself.
55
public struct SwiftlyVersion: Equatable, Comparable, CustomStringConvertible {
6-
/// Regex matching versions like "a.b.c", "a.b.c-alpha", and "a.b.c-alpha2".
6+
/// Regex matching versions like "a.b.c", "a.b.c-alpha", and "a.b.c-alpha.2".
77
static let regex: Regex<(Substring, Substring, Substring, Substring, Substring?)> =
8-
try! Regex("^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([a-zA-Z0-9]+))?$")
8+
try! Regex("^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([a-zA-Z0-9\\.]+))?$")
99

1010
public let major: Int
1111
public let minor: Int
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import Foundation
2+
import SwiftlyCore
3+
import XCTest
4+
5+
final class SwiftlyVersionTests: SwiftlyTests {
6+
func testSanity() throws {
7+
// GIVEN: a simple valid swiftly version string with major, minor, and patch
8+
let vs = "0.3.0"
9+
// WHEN: that version is parsed
10+
let ver = try! SwiftlyVersion(parsing: vs)
11+
// THEN: it succeeds and the major, minor, and patch parts match
12+
XCTAssertEqual(0, ver.major)
13+
XCTAssertEqual(3, ver.minor)
14+
XCTAssertEqual(0, ver.patch)
15+
XCTAssertEqual(nil, ver.suffix)
16+
17+
// GIVEN: two different swiftly versions
18+
let vs040 = "0.4.0"
19+
let ver040 = try! SwiftlyVersion(parsing: vs040)
20+
// WHEN: the versions are compared
21+
let cmp = ver040 > ver
22+
// THEN: the comparison highlights the larger version
23+
XCTAssertTrue(cmp)
24+
}
25+
26+
func testPreRelease() throws {
27+
// GIVEN: a swiftly version string with major, minor, patch, and suffix (pre-release)
28+
let preRelease = "0.4.0-dev"
29+
// WHEN: that version is parsed
30+
let preVer = try! SwiftlyVersion(parsing: preRelease)
31+
// THEN: it succeeds and the major, minor, patch, and suffix parts match
32+
XCTAssertEqual(0, preVer.major)
33+
XCTAssertEqual(4, preVer.minor)
34+
XCTAssertEqual(0, preVer.patch)
35+
XCTAssertEqual("dev", preVer.suffix)
36+
37+
// GIVEN: a swiftly pre release version and the final release version
38+
let releaseVer = try! SwiftlyVersion(parsing: "0.4.0")
39+
// WHEN: the versions are compared
40+
let cmp = releaseVer > preVer
41+
// THEN: the released version is consider larger
42+
XCTAssertTrue(cmp)
43+
44+
// GIVEN: a swiftly pre release version and the previous release
45+
let oldReleaseVer = try! SwiftlyVersion(parsing: "0.3.0")
46+
// WHEN: the versions are compared
47+
let cmpOldRelease = oldReleaseVer < preVer
48+
// THEN: the older version is considered smaller than the pre release
49+
XCTAssertTrue(cmpOldRelease)
50+
51+
// GIVEN: two pre release versions that are identical except for their suffix
52+
let preVer2 = try! SwiftlyVersion(parsing: "0.4.0-pre")
53+
// WHEN: the versions are compared
54+
let cmpPreVers = preVer2 > preVer
55+
// THEN: the lexicographically larger one is considered larger
56+
XCTAssertTrue(cmpPreVers)
57+
58+
// GIVEN: a pre-release version with dots in it
59+
let preReleaseDot = "1.5.0-alpha.1"
60+
// WHEN: that version is parsed
61+
let preVerDot = try! SwiftlyVersion(parsing: preReleaseDot)
62+
// THEN: it succeeds and the major, minor, patch, and suffix parts match
63+
XCTAssertEqual(1, preVerDot.major)
64+
XCTAssertEqual(5, preVerDot.minor)
65+
XCTAssertEqual(0, preVerDot.patch)
66+
XCTAssertEqual("alpha.1", preVerDot.suffix)
67+
}
68+
}

0 commit comments

Comments
 (0)