@@ -20,21 +20,25 @@ internal struct Init: SwiftlyCommand {
2020 var skipInstall : Bool = false
2121 @Flag ( help: " Quiet shell follow up commands " )
2222 var quietShellFollowup : Bool = false
23+ @Flag ( help: " Run sudo if there are post-installation packages to install (Linux only) " )
24+ var sudoInstallPackages : Bool = false
2325
2426 @OptionGroup var root : GlobalOptions
2527
28+ internal static var allowedInstallCommands : Regex < ( Substring , Substring , Substring ) > { try ! Regex ( " ^(apt-get|yum) -y install( [A-Za-z0-9: \\ - \\ +]+)+$ " ) }
29+
2630 private enum CodingKeys : String , CodingKey {
27- case noModifyProfile, overwrite, platform, skipInstall, root, quietShellFollowup
31+ case noModifyProfile, overwrite, platform, skipInstall, root, quietShellFollowup, sudoInstallPackages
2832 }
2933
3034 public mutating func validate( ) throws { }
3135
3236 internal mutating func run( ) async throws {
33- 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)
37+ 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 )
3438 }
3539
3640 /// Initialize the installation of swiftly.
37- internal static func execute( assumeYes: Bool , noModifyProfile: Bool , overwrite: Bool , platform: String ? , verbose: Bool , skipInstall: Bool , quietShellFollowup: Bool ) async throws {
41+ internal static func execute( assumeYes: Bool , noModifyProfile: Bool , overwrite: Bool , platform: String ? , verbose: Bool , skipInstall: Bool , quietShellFollowup: Bool , sudoInstallPackages : Bool ) async throws {
3842 try Swiftly . currentPlatform. verifySwiftlySystemPrerequisites ( )
3943
4044 var config = try ? Config . load ( )
@@ -290,6 +294,12 @@ internal struct Init: SwiftlyCommand {
290294 }
291295
292296 if let postInstall {
297+ #if !os(Linux)
298+ if sudoInstallPackages {
299+ SwiftlyCore . print ( " Sudo installing missing packages has no effect on non-Linux platforms. " )
300+ }
301+ #endif
302+
293303 SwiftlyCore . print ( """
294304 There are some dependencies that should be installed before using this toolchain.
295305 You can run the following script as the system administrator (e.g. root) to prepare
@@ -298,6 +308,42 @@ internal struct Init: SwiftlyCommand {
298308 \( postInstall)
299309
300310 """ )
311+
312+ if sudoInstallPackages {
313+ // This is very security sensitive code here and that's why there's special process handling
314+ // and an allow-list of what we will attempt to run as root. Also, the sudo binary is run directly
315+ // with a fully-qualified path without any checking in order to avoid TOCTOU.
316+
317+ guard try Self . allowedInstallCommands. wholeMatch ( in: postInstall) != nil else {
318+ fatalError ( " post installation command \( postInstall) does not match allowed patterns for sudo " )
319+ }
320+
321+ let p = Process ( )
322+ p. executableURL = URL ( fileURLWithPath: " /usr/bin/sudo " )
323+ p. arguments = [ " -k " ] + [ " -p " , " Enter your sudo password to run it right away (Ctrl-C aborts): " ] + postInstall. split ( separator: " " ) . map { String ( $0) }
324+
325+ do {
326+ try p. run ( )
327+
328+ // Attach this process to our process group so that Ctrl-C and other signals work
329+ let pgid = tcgetpgrp ( STDOUT_FILENO)
330+ if pgid != - 1 {
331+ tcsetpgrp ( STDOUT_FILENO, p. processIdentifier)
332+ }
333+
334+ defer { if pgid != - 1 {
335+ tcsetpgrp ( STDOUT_FILENO, pgid)
336+ } }
337+
338+ p. waitUntilExit ( )
339+
340+ guard p. terminationStatus == 0 else {
341+ throw SwiftlyError ( message: " sudo could not be run to install the packages " )
342+ }
343+ } catch {
344+ throw SwiftlyError ( message: " sudo could not be run to install the packages " )
345+ }
346+ }
301347 }
302348 }
303349}
0 commit comments