Skip to content

Merge installation logic into swiftly as an init subcommand #127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9eb8c91
DRAFT: Merge installation logic into swiftly as an init subcommand
cmcgee1024 Jun 19, 2024
c272921
Design rework based on feedback, removing the swiftly-init accelerato…
cmcgee1024 Jun 26, 2024
2801b53
Add a note about the case when swiftly is copied during init
cmcgee1024 Jul 2, 2024
f2609dc
Separate the swiftly version changes from this PR
cmcgee1024 Jul 5, 2024
3af19ac
Provide more specific details about SWIFTLY_BIN_DIR, and moving the s…
cmcgee1024 Jul 5, 2024
eeb9d36
Update the design based on feedback
cmcgee1024 Jul 9, 2024
b64f4bd
Merge branch 'main' of github.com:cmcgee1024/swiftly into self-install
cmcgee1024 Jul 22, 2024
dd15324
DRAFT: Add init implementation and tests
cmcgee1024 Jul 24, 2024
1d3bb26
Fix formatting problems and remove problematic check in e2e install test
cmcgee1024 Jul 24, 2024
1cd01be
Output the shell and version in the e2e test
cmcgee1024 Jul 24, 2024
c8cb477
Force a hash recalculation in the e2e test
cmcgee1024 Jul 24, 2024
763b284
Provide more details of the failed URL when trying to install a toolc…
cmcgee1024 Jul 24, 2024
72b7025
Fix the URL assembly for Linux x86 toolchain downloads
cmcgee1024 Jul 24, 2024
a2a6d4b
Directly source the env script in e2e test rather than relying on the…
cmcgee1024 Jul 25, 2024
ef9ee05
Capture more information about what happens on Amazon Linux 2
cmcgee1024 Jul 25, 2024
7f9fb9e
Use type shell builtin to find which swift in bash for better resilie…
cmcgee1024 Jul 25, 2024
56672ec
Add init tests
cmcgee1024 Jul 25, 2024
6bf4ab9
Auto-detect the platform when running the tests
cmcgee1024 Jul 25, 2024
c4f87cb
Fix toolchain version source file name
cmcgee1024 Jul 25, 2024
4e0ed6e
Produce post installation instructions and add post install script file
cmcgee1024 Jul 26, 2024
11d112a
Add swiftly system prerequisite checks for ca-certificates on Linux
cmcgee1024 Jul 26, 2024
373be6a
Fallback to copy if move of swiftly binary fails
cmcgee1024 Jul 26, 2024
886865f
Fix error message for the error case in the system managed binary check
cmcgee1024 Jul 29, 2024
0aa21e0
Review feedback and fixes
cmcgee1024 Aug 2, 2024
7081738
Fix the tests
cmcgee1024 Aug 2, 2024
361bc2f
Review feedback
cmcgee1024 Aug 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 23 additions & 15 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,32 @@ This document contains the high level design of swiftly. Not all features have b
- [Implementation sketch - macOS](#implementation-sketch---macos)
- [`config.json` schema](#configjson-schema)

## Linux
## Installation of swiftly

The installation of swiftly is divided into two phases: delivery and initialization. Delivery of the swiftly binary can be accomplished using different methods:

* Shell "one-liner" with a string of commands that can be copy/pasted into the user's shell to securely download from the trusted website and proceed to initialization
* Direct download from a trusted website with guidance on the correct binary for the user's platform to download and then how move on to initialization
* System-level package (e.g. homebrew, pkg, apt-get, rpm) that downloads and places the swiftly binary in a system location outside of the user's home directory, often runnable from the user's path
* Manual compilation of the swiftly binary from this git repository (e.g. from a development environment)

We'll need an initialization phase, 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 has its own configuration stored in the form of a `config.json` file, which will be created as part of initialization. None of the delivery methods can perform all of these steps on their own. System package managers don't normally update all users' environment or update the user's home directory structure directly.

### Installation of swiftly
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 delivered to the user. The trigger for the initialization is done via an `init` subcommand with some initialization detection for the other subcommands to help guide users who have gone off track.

```
swiftly init
```

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:
- Ubuntu 16.04
- Ubuntu 18.04
- Ubuntu 20.04
- CentOS 7
- CentOS 8
- Amazon Linux 2
Also, the swiftly binary itself is copied into this location if it is not run from a system location, managed by a system package manager. In this case the user is notified that a copy of swiftly has been made in the swiftly bin directory (where the symlinks to the toolchain commands will be managed) so that they can remove the temporary copy that they used to initialize.

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.
## Swiftly upgrade

As part of swiftly's regular operations it can detect that the current configuration is out of date and error out. The `config.json` file contains a version at the moment it was created or last upgraded. In the case of an older version it will direct the user to run init to perform the upgrade. If a downgrade situation is detected then swiflty will fail with an error.

There is also a self-update mechanism that will automate the delivery of the new swiftly binary, verifies it and runs the init subcommand to initiate the upgrade procedure. Note that the self-update will error out without performing any operations if swiftly is installed in the system, outside of the user's home directory.

## Linux

### Installation of a Swift toolchain

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

## macOS
### Installation of swiftly

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`.

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`.

### Installation of a Swift toolchain

Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftlyCore/SwiftlyCore.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

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

/// A separate home directory to use for testing purposes. This overrides swiftly's default
/// home directory location logic.
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftlyCore/SwiftlyVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import Foundation

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

public let major: Int
public let minor: Int
Expand Down
68 changes: 68 additions & 0 deletions Tests/SwiftlyTests/SwiftlyVersionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Foundation
import SwiftlyCore
import XCTest

final class SwiftlyVersionTests: SwiftlyTests {
func testSanity() throws {
// GIVEN: a simple valid swiftly version string with major, minor, and patch
let vs = "0.3.0"
// WHEN: that version is parsed
let ver = try! SwiftlyVersion(parsing: vs)
// THEN: it succeeds and the major, minor, and patch parts match
XCTAssertEqual(0, ver.major)
XCTAssertEqual(3, ver.minor)
XCTAssertEqual(0, ver.patch)
XCTAssertEqual(nil, ver.suffix)

// GIVEN: two different swiftly versions
let vs040 = "0.4.0"
let ver040 = try! SwiftlyVersion(parsing: vs040)
// WHEN: the versions are compared
let cmp = ver040 > ver
// THEN: the comparison highlights the larger version
XCTAssertTrue(cmp)
}

func testPreRelease() throws {
// GIVEN: a swiftly version string with major, minor, patch, and suffix (pre-release)
let preRelease = "0.4.0-dev"
// WHEN: that version is parsed
let preVer = try! SwiftlyVersion(parsing: preRelease)
// THEN: it succeeds and the major, minor, patch, and suffix parts match
XCTAssertEqual(0, preVer.major)
XCTAssertEqual(4, preVer.minor)
XCTAssertEqual(0, preVer.patch)
XCTAssertEqual("dev", preVer.suffix)

// GIVEN: a swiftly pre release version and the final release version
let releaseVer = try! SwiftlyVersion(parsing: "0.4.0")
// WHEN: the versions are compared
let cmp = releaseVer > preVer
// THEN: the released version is consider larger
XCTAssertTrue(cmp)

// GIVEN: a swiftly pre release version and the previous release
let oldReleaseVer = try! SwiftlyVersion(parsing: "0.3.0")
// WHEN: the versions are compared
let cmpOldRelease = oldReleaseVer < preVer
// THEN: the older version is considered smaller than the pre release
XCTAssertTrue(cmpOldRelease)

// GIVEN: two pre release versions that are identical except for their suffix
let preVer2 = try! SwiftlyVersion(parsing: "0.4.0-pre")
// WHEN: the versions are compared
let cmpPreVers = preVer2 > preVer
// THEN: the lexicographically larger one is considered larger
XCTAssertTrue(cmpPreVers)

// GIVEN: a pre-release version with dots in it
let preReleaseDot = "1.5.0-alpha.1"
// WHEN: that version is parsed
let preVerDot = try! SwiftlyVersion(parsing: preReleaseDot)
// THEN: it succeeds and the major, minor, patch, and suffix parts match
XCTAssertEqual(1, preVerDot.major)
XCTAssertEqual(5, preVerDot.minor)
XCTAssertEqual(0, preVerDot.patch)
XCTAssertEqual("alpha.1", preVerDot.suffix)
}
}