Skip to content

Add support for sudo installation missing packages after init #350

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

Open
wants to merge 3 commits into
base: release/1.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ written to this file as commands that can be run after the installation.
Perform swiftly initialization into your user account.

```
swiftly init [--no-modify-profile] [--overwrite] [--platform=<platform>] [--skip-install] [--quiet-shell-followup] [--assume-yes] [--verbose] [--version] [--help]
swiftly init [--no-modify-profile] [--overwrite] [--platform=<platform>] [--skip-install] [--quiet-shell-followup] [--sudo-install-packages] [--assume-yes] [--verbose] [--version] [--help]
```

**--no-modify-profile:**
Expand Down Expand Up @@ -433,6 +433,11 @@ swiftly init [--no-modify-profile] [--overwrite] [--platform=<platform>] [--skip
*Quiet shell follow up commands*


**--sudo-install-packages:**

*Run sudo if there are post-installation packages to install (Linux only)*


**--assume-yes:**

*Disable confirmation prompts by assuming 'yes'*
Expand Down
52 changes: 49 additions & 3 deletions Sources/Swiftly/Init.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,25 @@ internal struct Init: SwiftlyCommand {
var skipInstall: Bool = false
@Flag(help: "Quiet shell follow up commands")
var quietShellFollowup: Bool = false
@Flag(help: "Run sudo if there are post-installation packages to install (Linux only)")
var sudoInstallPackages: Bool = false

@OptionGroup var root: GlobalOptions

internal static var allowedInstallCommands: Regex<(Substring, Substring, Substring)> { try! Regex("^(apt-get|yum) -y install( [A-Za-z0-9:\\-\\+]+)+$") }

private enum CodingKeys: String, CodingKey {
case noModifyProfile, overwrite, platform, skipInstall, root, quietShellFollowup
case noModifyProfile, overwrite, platform, skipInstall, root, quietShellFollowup, sudoInstallPackages
}

public mutating func validate() throws {}

internal mutating func run() async throws {
try await Self.execute(assumeYes: self.root.assumeYes, noModifyProfile: self.noModifyProfile, overwrite: self.overwrite, platform: self.platform, verbose: self.root.verbose, skipInstall: self.skipInstall, quietShellFollowup: self.quietShellFollowup)
try await Self.execute(assumeYes: self.root.assumeYes, noModifyProfile: self.noModifyProfile, overwrite: self.overwrite, platform: self.platform, verbose: self.root.verbose, skipInstall: self.skipInstall, quietShellFollowup: self.quietShellFollowup, sudoInstallPackages: self.sudoInstallPackages)
}

/// Initialize the installation of swiftly.
internal static func execute(assumeYes: Bool, noModifyProfile: Bool, overwrite: Bool, platform: String?, verbose: Bool, skipInstall: Bool, quietShellFollowup: Bool) async throws {
internal static func execute(assumeYes: Bool, noModifyProfile: Bool, overwrite: Bool, platform: String?, verbose: Bool, skipInstall: Bool, quietShellFollowup: Bool, sudoInstallPackages: Bool) async throws {
try Swiftly.currentPlatform.verifySwiftlySystemPrerequisites()

var config = try? Config.load()
Expand Down Expand Up @@ -290,6 +294,12 @@ internal struct Init: SwiftlyCommand {
}

if let postInstall {
#if !os(Linux)
if sudoInstallPackages {
SwiftlyCore.print("Sudo installing missing packages has no effect on non-Linux platforms.")
}
#endif

SwiftlyCore.print("""
There are some dependencies that should be installed before using this toolchain.
You can run the following script as the system administrator (e.g. root) to prepare
Expand All @@ -298,6 +308,42 @@ internal struct Init: SwiftlyCommand {
\(postInstall)

""")

if sudoInstallPackages {
// This is very security sensitive code here and that's why there's special process handling
// and an allow-list of what we will attempt to run as root. Also, the sudo binary is run directly
// with a fully-qualified path without any checking in order to avoid TOCTOU.

guard try Self.allowedInstallCommands.wholeMatch(in: postInstall) != nil else {
fatalError("post installation command \(postInstall) does not match allowed patterns for sudo")
}

let p = Process()
p.executableURL = URL(fileURLWithPath: "/usr/bin/sudo")
p.arguments = ["-k"] + ["-p", "Enter your sudo password to run it right away (Ctrl-C aborts): "] + postInstall.split(separator: " ").map { String($0) }

do {
try p.run()

// Attach this process to our process group so that Ctrl-C and other signals work
let pgid = tcgetpgrp(STDOUT_FILENO)
if pgid != -1 {
tcsetpgrp(STDOUT_FILENO, p.processIdentifier)
}

defer { if pgid != -1 {
tcsetpgrp(STDOUT_FILENO, pgid)
}}

p.waitUntilExit()

if p.terminationStatus == 0 {
SwiftlyCore.print("sudo could not be run to install the packages")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could go in the common messages struct

}
} catch {
SwiftlyCore.print("sudo could not be run to install the packages")
}
}
}
}
}
4 changes: 2 additions & 2 deletions Sources/Swiftly/Proxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public enum Proxy {

if CommandLine.arguments.count == 1 {
// User ran swiftly with no extra arguments in an uninstalled environment, so we jump directly into
// an simple init.
try await Init.execute(assumeYes: false, noModifyProfile: false, overwrite: false, platform: nil, verbose: false, skipInstall: false, quietShellFollowup: false)
// a simple init.
try await Init.execute(assumeYes: false, noModifyProfile: false, overwrite: false, platform: nil, verbose: false, skipInstall: false, quietShellFollowup: false, sudoInstallPackages: false)
return
} else if CommandLine.arguments.count >= 2 && CommandLine.arguments[1] == "init" {
// Let the user run the init command with their arguments, if any.
Expand Down
11 changes: 11 additions & 0 deletions Tests/SwiftlyTests/InitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,15 @@ final class InitTests: SwiftlyTests {
XCTAssertTrue(Swiftly.currentPlatform.swiftlyToolchainsDir.appendingPathComponent("foo.txt").fileExists())
}
}

func testAllowedInstalledCommands() async throws {
XCTAssertTrue(try Init.allowedInstallCommands.wholeMatch(in: "apt-get -y install python3 libsqlite3") != nil)
XCTAssertTrue(try Init.allowedInstallCommands.wholeMatch(in: "yum -y install python3 libsqlite3") != nil)
XCTAssertTrue(try Init.allowedInstallCommands.wholeMatch(in: "yum -y install python3 libsqlite3-dev") != nil)
XCTAssertTrue(try Init.allowedInstallCommands.wholeMatch(in: "yum -y install libstdc++-dev:i386") != nil)

XCTAssertTrue(try Init.allowedInstallCommands.wholeMatch(in: "SOME_ENV_VAR=abcde yum -y install libstdc++-dev:i386") == nil)
XCTAssertTrue(try Init.allowedInstallCommands.wholeMatch(in: "apt-get -y install libstdc++-dev:i386; rm -rf /") == nil)
XCTAssertTrue(try Init.allowedInstallCommands.wholeMatch(in: "apt-get -y install libstdc++-dev:i386\nrm -rf /") == nil)
}
}