|
| 1 | +import Foundation |
| 2 | +import SwiftlyCore |
| 3 | + |
| 4 | +struct SwiftPkgInfo: Codable { |
| 5 | + var CFBundleIdentifier: String |
| 6 | +} |
| 7 | + |
| 8 | +/// `Platform` implementation for macOS systems. |
| 9 | +public struct MacOS: Platform { |
| 10 | + public init() {} |
| 11 | + |
| 12 | + public var appDataDirectory: URL { |
| 13 | + return FileManager.default.homeDirectoryForCurrentUser |
| 14 | + .appendingPathComponent("Library/Application Support", isDirectory: true) |
| 15 | + } |
| 16 | + |
| 17 | + public var swiftlyBinDir: URL { |
| 18 | + SwiftlyCore.mockedHomeDir.map { $0.appendingPathComponent("bin", isDirectory: true) } |
| 19 | + ?? ProcessInfo.processInfo.environment["SWIFTLY_BIN_DIR"].map { URL(fileURLWithPath: $0) } |
| 20 | + ?? FileManager.default.homeDirectoryForCurrentUser |
| 21 | + .appendingPathComponent("Library/Application Support/swiftly/bin", isDirectory: true) |
| 22 | + } |
| 23 | + |
| 24 | + public var swiftlyToolchainsDir: URL { |
| 25 | + SwiftlyCore.mockedHomeDir.map { $0.appendingPathComponent("Library/Developer/Toolchains", isDirectory: true) } |
| 26 | + ?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Developer/Toolchains", isDirectory: true) |
| 27 | + } |
| 28 | + |
| 29 | + public var toolchainFileExtension: String { |
| 30 | + "pkg" |
| 31 | + } |
| 32 | + |
| 33 | + public func isSystemDependencyPresent(_: SystemDependency) -> Bool { |
| 34 | + // All system dependencies on macOS should be present |
| 35 | + true |
| 36 | + } |
| 37 | + |
| 38 | + public func verifySystemPrerequisitesForInstall(requireSignatureValidation: Bool) throws { |
| 39 | + // All system prerequisites should be there for macOS |
| 40 | + } |
| 41 | + |
| 42 | + public func install(from tmpFile: URL, version: ToolchainVersion) throws { |
| 43 | + guard tmpFile.fileExists() else { |
| 44 | + throw Error(message: "\(tmpFile) doesn't exist") |
| 45 | + } |
| 46 | + |
| 47 | + if !self.swiftlyToolchainsDir.fileExists() { |
| 48 | + try FileManager.default.createDirectory(at: self.swiftlyToolchainsDir, withIntermediateDirectories: false) |
| 49 | + } |
| 50 | + |
| 51 | + SwiftlyCore.print("Installing package in user home directory...") |
| 52 | + try runProgram("installer", "-pkg", tmpFile.path, "-target", "CurrentUserHomeDirectory") |
| 53 | + } |
| 54 | + |
| 55 | + public func uninstall(_ toolchain: ToolchainVersion) throws { |
| 56 | + SwiftlyCore.print("Uninstalling package in user home directory...") |
| 57 | + |
| 58 | + let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent("\(toolchain.identifier).xctoolchain", isDirectory: true) |
| 59 | + |
| 60 | + let decoder = PropertyListDecoder() |
| 61 | + let infoPlist = toolchainDir.appendingPathComponent("Info.plist") |
| 62 | + guard let data = try? Data(contentsOf: infoPlist) else { |
| 63 | + throw Error(message: "could not open \(infoPlist)") |
| 64 | + } |
| 65 | + |
| 66 | + guard let pkgInfo = try? decoder.decode(SwiftPkgInfo.self, from: data) else { |
| 67 | + throw Error(message: "could not decode plist at \(infoPlist)") |
| 68 | + } |
| 69 | + |
| 70 | + try FileManager.default.removeItem(at: toolchainDir) |
| 71 | + |
| 72 | + let homedir = ProcessInfo.processInfo.environment["HOME"]! |
| 73 | + try runProgram("pkgutil", "--volume", homedir, "--forget", pkgInfo.CFBundleIdentifier) |
| 74 | + } |
| 75 | + |
| 76 | + public func use(_ toolchain: ToolchainVersion, currentToolchain: ToolchainVersion?) throws -> Bool { |
| 77 | + let toolchainBinURL = self.swiftlyToolchainsDir |
| 78 | + .appendingPathComponent(toolchain.identifier + ".xctoolchain", isDirectory: true) |
| 79 | + .appendingPathComponent("usr", isDirectory: true) |
| 80 | + .appendingPathComponent("bin", isDirectory: true) |
| 81 | + |
| 82 | + // Delete existing symlinks from previously in-use toolchain. |
| 83 | + if let currentToolchain { |
| 84 | + try self.unUse(currentToolchain: currentToolchain) |
| 85 | + } |
| 86 | + |
| 87 | + // Ensure swiftly doesn't overwrite any existing executables without getting confirmation first. |
| 88 | + let swiftlyBinDirContents = try FileManager.default.contentsOfDirectory(atPath: self.swiftlyBinDir.path) |
| 89 | + let toolchainBinDirContents = try FileManager.default.contentsOfDirectory(atPath: toolchainBinURL.path) |
| 90 | + let willBeOverwritten = Set(toolchainBinDirContents).intersection(swiftlyBinDirContents) |
| 91 | + if !willBeOverwritten.isEmpty { |
| 92 | + SwiftlyCore.print("The following existing executables will be overwritten:") |
| 93 | + |
| 94 | + for executable in willBeOverwritten { |
| 95 | + SwiftlyCore.print(" \(self.swiftlyBinDir.appendingPathComponent(executable).path)") |
| 96 | + } |
| 97 | + |
| 98 | + let proceed = SwiftlyCore.readLine(prompt: "Proceed? (y/n)") ?? "n" |
| 99 | + |
| 100 | + guard proceed == "y" else { |
| 101 | + SwiftlyCore.print("Aborting use") |
| 102 | + return false |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + for executable in toolchainBinDirContents { |
| 107 | + let linkURL = self.swiftlyBinDir.appendingPathComponent(executable) |
| 108 | + let executableURL = toolchainBinURL.appendingPathComponent(executable) |
| 109 | + |
| 110 | + // Deletion confirmed with user above. |
| 111 | + try linkURL.deleteIfExists() |
| 112 | + |
| 113 | + try FileManager.default.createSymbolicLink( |
| 114 | + atPath: linkURL.path, |
| 115 | + withDestinationPath: executableURL.path |
| 116 | + ) |
| 117 | + } |
| 118 | + |
| 119 | + SwiftlyCore.print(""" |
| 120 | + NOTE: On macOS it is possible that the shell will pick up the system Swift on the path |
| 121 | + instead of the one that swiftly has installed for you. You can run the 'hash -r' |
| 122 | + command to update the shell with the latest PATHs. |
| 123 | +
|
| 124 | + hash -r |
| 125 | +
|
| 126 | + """ |
| 127 | + ) |
| 128 | + |
| 129 | + return true |
| 130 | + } |
| 131 | + |
| 132 | + public func unUse(currentToolchain: ToolchainVersion) throws { |
| 133 | + let currentToolchainBinURL = self.swiftlyToolchainsDir |
| 134 | + .appendingPathComponent(currentToolchain.identifier + ".xctoolchain", isDirectory: true) |
| 135 | + .appendingPathComponent("usr", isDirectory: true) |
| 136 | + .appendingPathComponent("bin", isDirectory: true) |
| 137 | + |
| 138 | + for existingExecutable in try FileManager.default.contentsOfDirectory(atPath: currentToolchainBinURL.path) { |
| 139 | + guard existingExecutable != "swiftly" else { |
| 140 | + continue |
| 141 | + } |
| 142 | + |
| 143 | + let url = self.swiftlyBinDir.appendingPathComponent(existingExecutable) |
| 144 | + let vals = try url.resourceValues(forKeys: [URLResourceKey.isSymbolicLinkKey]) |
| 145 | + |
| 146 | + guard let islink = vals.isSymbolicLink, islink else { |
| 147 | + throw Error(message: "Found executable not managed by swiftly in SWIFTLY_BIN_DIR: \(url.path)") |
| 148 | + } |
| 149 | + let symlinkDest = url.resolvingSymlinksInPath() |
| 150 | + guard symlinkDest.deletingLastPathComponent() == currentToolchainBinURL else { |
| 151 | + throw Error(message: "Found symlink that points to non-swiftly managed executable: \(symlinkDest.path)") |
| 152 | + } |
| 153 | + |
| 154 | + try self.swiftlyBinDir.appendingPathComponent(existingExecutable).deleteIfExists() |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + public func listAvailableSnapshots(version _: String?) async -> [Snapshot] { |
| 159 | + [] |
| 160 | + } |
| 161 | + |
| 162 | + public func getExecutableName(forArch: String) -> String { |
| 163 | + "swiftly-\(forArch)-macos-osx" |
| 164 | + } |
| 165 | + |
| 166 | + public func currentToolchain() throws -> ToolchainVersion? { nil } |
| 167 | + |
| 168 | + public func getTempFilePath() -> URL { |
| 169 | + FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID()).pkg") |
| 170 | + } |
| 171 | + |
| 172 | + public func verifySignature(httpClient: SwiftlyHTTPClient, archiveDownloadURL: URL, archive: URL) async throws { |
| 173 | + // No signature verification is required on macOS since the pkg files have their own signing |
| 174 | + // mechanism and the swift.org downloadables are trusted by stock macOS installations. |
| 175 | + } |
| 176 | + |
| 177 | + public static let currentPlatform: any Platform = MacOS() |
| 178 | +} |
0 commit comments