-
Notifications
You must be signed in to change notification settings - Fork 51
macOS support for swiftly #121
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
Changes from all commits
5d11297
84f969d
43fb8b8
5d86512
472d6ed
c307f5e
61b2e8c
e2d25fb
76d1a08
aab5f99
5ec3bbf
5e073d0
5facaef
8fd3d39
4658330
ece63e7
bad95c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,4 @@ xcuserdata/ | |
DerivedData/ | ||
.swiftpm/ | ||
.vscode/ | ||
**/*.swp |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
5.7 | ||
5.10 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,12 @@ | ||
// swift-tools-version:5.7 | ||
// swift-tools-version:5.10 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we're bumping to 5.10 we should probably switch on strict concurrency checking There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, I'm definitely interested in enabling better checks. How does one turn on the strict concurrency checking? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In your target definition in Package.swift add swiftSettings: [.enableExperimentalFeature("StrictConcurrency=complete")] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, I tried enabling this and there were a non-trivial number of warnings (errors in Swift 6). I've raised an issue #124 to address the concurrency problems and enable the check. |
||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "swiftly", | ||
platforms: [.macOS(.v13)], | ||
platforms: [ | ||
.macOS(.v13), | ||
], | ||
products: [ | ||
.executable( | ||
name: "swiftly", | ||
|
@@ -24,6 +26,7 @@ let package = Package( | |
.product(name: "ArgumentParser", package: "swift-argument-parser"), | ||
.target(name: "SwiftlyCore"), | ||
.target(name: "LinuxPlatform", condition: .when(platforms: [.linux])), | ||
.target(name: "MacOSPlatform", condition: .when(platforms: [.macOS])), | ||
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), | ||
] | ||
), | ||
|
@@ -44,6 +47,12 @@ let package = Package( | |
.linkedLibrary("z"), | ||
] | ||
), | ||
.target( | ||
name: "MacOSPlatform", | ||
dependencies: [ | ||
"SwiftlyCore", | ||
] | ||
), | ||
.systemLibrary( | ||
name: "CLibArchive", | ||
pkgConfig: "libarchive", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
import Foundation | ||
import SwiftlyCore | ||
|
||
public struct SwiftPkgInfo: Codable { | ||
public var CFBundleIdentifier: String | ||
|
||
public init(CFBundleIdentifier: String) { | ||
self.CFBundleIdentifier = CFBundleIdentifier | ||
} | ||
} | ||
|
||
/// `Platform` implementation for macOS systems. | ||
public struct MacOS: Platform { | ||
public init() {} | ||
|
||
public var appDataDirectory: URL { | ||
FileManager.default.homeDirectoryForCurrentUser | ||
.appendingPathComponent("Library/Application Support", isDirectory: true) | ||
} | ||
|
||
public var swiftlyBinDir: URL { | ||
SwiftlyCore.mockedHomeDir.map { $0.appendingPathComponent("bin", isDirectory: true) } | ||
?? ProcessInfo.processInfo.environment["SWIFTLY_BIN_DIR"].map { URL(fileURLWithPath: $0) } | ||
?? FileManager.default.homeDirectoryForCurrentUser | ||
.appendingPathComponent("Library/Application Support/swiftly/bin", isDirectory: true) | ||
} | ||
|
||
public var swiftlyToolchainsDir: URL { | ||
SwiftlyCore.mockedHomeDir.map { $0.appendingPathComponent("Toolchains", isDirectory: true) } | ||
// The toolchains are always installed here by the installer. We bypass the installer in the case of test mocks | ||
?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Developer/Toolchains", isDirectory: true) | ||
} | ||
|
||
public var toolchainFileExtension: String { | ||
"pkg" | ||
} | ||
|
||
public func isSystemDependencyPresent(_: SystemDependency) -> Bool { | ||
// All system dependencies on macOS should be present | ||
true | ||
} | ||
|
||
public func verifySystemPrerequisitesForInstall(requireSignatureValidation _: Bool) throws { | ||
// All system prerequisites should be there for macOS | ||
} | ||
|
||
public func install(from tmpFile: URL, version: ToolchainVersion) throws { | ||
guard tmpFile.fileExists() else { | ||
throw Error(message: "\(tmpFile) doesn't exist") | ||
} | ||
|
||
if !self.swiftlyToolchainsDir.fileExists() { | ||
try FileManager.default.createDirectory(at: self.swiftlyToolchainsDir, withIntermediateDirectories: false) | ||
adam-fowler marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
if SwiftlyCore.mockedHomeDir == nil { | ||
SwiftlyCore.print("Installing package in user home directory...") | ||
try runProgram("installer", "-pkg", tmpFile.path, "-target", "CurrentUserHomeDirectory") | ||
} else { | ||
// In the case of a mock for testing purposes we won't use the installer, perferring a manual process because | ||
// the installer will not install to an arbitrary path, only a volume or user home directory. | ||
let tmpDir = self.getTempFilePath() | ||
let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent("\(version.identifier).xctoolchain", isDirectory: true) | ||
if !toolchainDir.fileExists() { | ||
try FileManager.default.createDirectory(at: toolchainDir, withIntermediateDirectories: false) | ||
} | ||
try runProgram("pkgutil", "--expand", tmpFile.path, tmpDir.path) | ||
// There's a slight difference in the location of the special Payload file between official swift packages | ||
// and the ones that are mocked here in the test framework. | ||
var payload = tmpDir.appendingPathComponent("Payload") | ||
if !payload.fileExists() { | ||
payload = tmpDir.appendingPathComponent("\(version.identifier)-osx-package.pkg/Payload") | ||
} | ||
try runProgram("tar", "-C", toolchainDir.path, "-xf", payload.path) | ||
} | ||
} | ||
|
||
public func uninstall(_ toolchain: ToolchainVersion) throws { | ||
SwiftlyCore.print("Uninstalling package in user home directory...") | ||
|
||
let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent("\(toolchain.identifier).xctoolchain", isDirectory: true) | ||
|
||
let decoder = PropertyListDecoder() | ||
let infoPlist = toolchainDir.appendingPathComponent("Info.plist") | ||
guard let data = try? Data(contentsOf: infoPlist) else { | ||
throw Error(message: "could not open \(infoPlist)") | ||
} | ||
|
||
guard let pkgInfo = try? decoder.decode(SwiftPkgInfo.self, from: data) else { | ||
throw Error(message: "could not decode plist at \(infoPlist)") | ||
} | ||
|
||
try FileManager.default.removeItem(at: toolchainDir) | ||
|
||
let homedir = ProcessInfo.processInfo.environment["HOME"]! | ||
try? runProgram("pkgutil", "--volume", homedir, "--forget", pkgInfo.CFBundleIdentifier) | ||
} | ||
|
||
public func use(_ toolchain: ToolchainVersion, currentToolchain: ToolchainVersion?) throws -> Bool { | ||
patrickfreed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let toolchainBinURL = self.swiftlyToolchainsDir | ||
.appendingPathComponent(toolchain.identifier + ".xctoolchain", isDirectory: true) | ||
.appendingPathComponent("usr", isDirectory: true) | ||
.appendingPathComponent("bin", isDirectory: true) | ||
|
||
// Delete existing symlinks from previously in-use toolchain. | ||
if let currentToolchain { | ||
try self.unUse(currentToolchain: currentToolchain) | ||
} | ||
|
||
// Ensure swiftly doesn't overwrite any existing executables without getting confirmation first. | ||
let swiftlyBinDirContents = try FileManager.default.contentsOfDirectory(atPath: self.swiftlyBinDir.path) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if it's worth us migration to NIOFileSystem, since we already have NIO as a dependency. Aside from the nicer API, it would be one less thing to use when depending on new Foundation which should make for a smaller binary There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can raise this as a separate issue. I believe that there are Foundation dependencies all over the code. Pulling all of that out would be a significant sweep. I'm not familiar with NIOFileSystem. Something that I really like about FileManager API is that it appears to be very mockable so that we could refactor the tests to operate on a full mock of the filesystem. Is that capability available with NIOFS? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be clear I'm not suggesting we pull out all of Foundation, but minimising it to just Which specific parts would you use for mocking (not getting into a discussion about that word 🤣 )? NIOFileSystem has a similar API in that you have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general, I agree that NIOFileSystem is more appropriate here since it uses async code to do the work. However, I would defer that from this PR since it is something we can do separately. WDYT @0xTim ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes let's defer this to another PR. I've added an issue #125 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah absolutely shouldn't be a blocker for this PR |
||
let toolchainBinDirContents = try FileManager.default.contentsOfDirectory(atPath: toolchainBinURL.path) | ||
let willBeOverwritten = Set(toolchainBinDirContents).intersection(swiftlyBinDirContents) | ||
if !willBeOverwritten.isEmpty { | ||
SwiftlyCore.print("The following existing executables will be overwritten:") | ||
|
||
for executable in willBeOverwritten { | ||
SwiftlyCore.print(" \(self.swiftlyBinDir.appendingPathComponent(executable).path)") | ||
} | ||
|
||
let proceed = SwiftlyCore.readLine(prompt: "Proceed? (y/n)") ?? "n" | ||
|
||
guard proceed == "y" else { | ||
SwiftlyCore.print("Aborting use") | ||
return false | ||
} | ||
} | ||
|
||
for executable in toolchainBinDirContents { | ||
let linkURL = self.swiftlyBinDir.appendingPathComponent(executable) | ||
let executableURL = toolchainBinURL.appendingPathComponent(executable) | ||
|
||
// Deletion confirmed with user above. | ||
try linkURL.deleteIfExists() | ||
|
||
try FileManager.default.createSymbolicLink( | ||
atPath: linkURL.path, | ||
withDestinationPath: executableURL.path | ||
) | ||
} | ||
|
||
SwiftlyCore.print(""" | ||
NOTE: On macOS it is possible that the shell will pick up the system Swift on the path | ||
instead of the one that swiftly has installed for you. You can run the 'hash -r' | ||
command to update the shell with the latest PATHs. | ||
|
||
hash -r | ||
|
||
""" | ||
) | ||
patrickfreed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return true | ||
} | ||
|
||
public func unUse(currentToolchain: ToolchainVersion) throws { | ||
let currentToolchainBinURL = self.swiftlyToolchainsDir | ||
.appendingPathComponent(currentToolchain.identifier + ".xctoolchain", isDirectory: true) | ||
.appendingPathComponent("usr", isDirectory: true) | ||
.appendingPathComponent("bin", isDirectory: true) | ||
|
||
for existingExecutable in try FileManager.default.contentsOfDirectory(atPath: currentToolchainBinURL.path) { | ||
guard existingExecutable != "swiftly" else { | ||
continue | ||
} | ||
|
||
let url = self.swiftlyBinDir.appendingPathComponent(existingExecutable) | ||
let vals = try url.resourceValues(forKeys: [URLResourceKey.isSymbolicLinkKey]) | ||
|
||
guard let islink = vals.isSymbolicLink, islink else { | ||
throw Error(message: "Found executable not managed by swiftly in SWIFTLY_BIN_DIR: \(url.path)") | ||
} | ||
let symlinkDest = url.resolvingSymlinksInPath() | ||
guard symlinkDest.deletingLastPathComponent() == currentToolchainBinURL else { | ||
throw Error(message: "Found symlink that points to non-swiftly managed executable: \(symlinkDest.path)") | ||
} | ||
|
||
try self.swiftlyBinDir.appendingPathComponent(existingExecutable).deleteIfExists() | ||
} | ||
} | ||
|
||
public func listAvailableSnapshots(version _: String?) async -> [Snapshot] { | ||
[] | ||
} | ||
|
||
public func getExecutableName(forArch: String) -> String { | ||
"swiftly-\(forArch)-macos-osx" | ||
} | ||
|
||
public func currentToolchain() throws -> ToolchainVersion? { nil } | ||
|
||
public func getTempFilePath() -> URL { | ||
FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID()).pkg") | ||
} | ||
|
||
public func verifySignature(httpClient _: SwiftlyHTTPClient, archiveDownloadURL _: URL, archive _: URL) async throws { | ||
// No signature verification is required on macOS since the pkg files have their own signing | ||
// mechanism and the swift.org downloadables are trusted by stock macOS installations. | ||
} | ||
|
||
public static let currentPlatform: any Platform = MacOS() | ||
} |
Uh oh!
There was an error while loading. Please reload this page.