From beff331eefd7ebe198e4c7bcd012faf608a655e6 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Thu, 24 Apr 2025 10:23:12 -0400 Subject: [PATCH 1/4] Model lipo invocations with a modeled command structure with a test --- Sources/SwiftlyCore/Commands.swift | 80 ++++++++++++++++++- Sources/SwiftlyCore/ModeledCommandLine.swift | 2 +- Tests/SwiftlyTests/CommandLineTests.swift | 7 ++ .../BuildSwiftlyRelease.swift | 24 ++++-- 4 files changed, 103 insertions(+), 10 deletions(-) diff --git a/Sources/SwiftlyCore/Commands.swift b/Sources/SwiftlyCore/Commands.swift index 641fcfbd..ceff40a7 100644 --- a/Sources/SwiftlyCore/Commands.swift +++ b/Sources/SwiftlyCore/Commands.swift @@ -48,7 +48,7 @@ extension SystemCommand { self.read(path: path, keys: keys) } - public struct ReadCommand: Output { + public struct ReadCommand { var dscl: DsclCommand var path: FilePath? var keys: [String] @@ -76,7 +76,7 @@ extension SystemCommand { } } -extension SystemCommand.DsclCommand.ReadCommand { +extension SystemCommand.DsclCommand.ReadCommand: Output { public func properties(_ p: Platform) async throws -> [(key: String, value: String)] { let output = try await self.output(p) guard let output else { return [] } @@ -90,3 +90,79 @@ extension SystemCommand.DsclCommand.ReadCommand { return props } } + +extension SystemCommand { + public static func lipo(executable: Executable = LipoCommand.defaultExecutable, inputFiles: FilePath...) -> LipoCommand { + Self.lipo(executable: executable, inputFiles: inputFiles) + } + + public static func lipo(executable: Executable = LipoCommand.defaultExecutable, inputFiles: [FilePath]) -> LipoCommand { + LipoCommand(executable: executable, inputFiles: inputFiles) + } + + public struct LipoCommand { + public static var defaultExecutable: Executable { .name("lipo") } + + var executable: Executable + var inputFiles: [FilePath] + + public init(executable: Executable, inputFiles: [FilePath]) { + self.executable = executable + self.inputFiles = inputFiles + } + + func config() -> Configuration { + var args: [String] = [] + + args += self.inputFiles.map(\.string) + + return Configuration( + executable: self.executable, + arguments: Arguments(args), + environment: .inherit + ) + } + + public func create(_ options: CreateCommand.Option...) -> CreateCommand { + CreateCommand(self, options) + } + + public struct CreateCommand { + var lipo: LipoCommand + + var options: [Option] + + init(_ lipo: LipoCommand, _ options: [Option]) { + self.lipo = lipo + self.options = options + } + + public enum Option { + case output(FilePath) + + func args() -> [String] { + switch self { + case let .output(output): + return ["-output", output.string] + } + } + } + + public func config() -> Configuration { + var c = self.lipo.config() + + var args = c.arguments.storage.map(\.description) + ["-create"] + + for opt in self.options { + args += opt.args() + } + + c.arguments = .init(args) + + return c + } + } + } +} + +extension SystemCommand.LipoCommand.CreateCommand: Runnable {} diff --git a/Sources/SwiftlyCore/ModeledCommandLine.swift b/Sources/SwiftlyCore/ModeledCommandLine.swift index a7a21a9b..cb07f372 100644 --- a/Sources/SwiftlyCore/ModeledCommandLine.swift +++ b/Sources/SwiftlyCore/ModeledCommandLine.swift @@ -126,7 +126,7 @@ public protocol Runnable { } extension Runnable { - public func run(_ p: Platform, quiet: Bool) async throws { + public func run(_ p: Platform, quiet: Bool = false) async throws { let c = self.config() let executable = switch c.executable.storage { case let .executable(name): diff --git a/Tests/SwiftlyTests/CommandLineTests.swift b/Tests/SwiftlyTests/CommandLineTests.swift index 62e2aecf..c5b44816 100644 --- a/Tests/SwiftlyTests/CommandLineTests.swift +++ b/Tests/SwiftlyTests/CommandLineTests.swift @@ -28,4 +28,11 @@ public struct CommandLineTests { #expect(properties.count == 1) // Only one shell for the current user #expect(properties[0].key == "UserShell") // The one property key should be the one that is requested } + + @Test func testLipo() { + var config = sys.lipo(inputFiles: FilePath("swiftly1"), FilePath("swiftly2")).create(.output(FilePath("swiftly-universal"))).config() + + #expect(config.executable == .name("lipo")) + #expect(config.arguments.storage.map(\.description) == ["swiftly1", "swiftly2", "-create", "-output", "swiftly-universal"]) + } } diff --git a/Tools/build-swiftly-release/BuildSwiftlyRelease.swift b/Tools/build-swiftly-release/BuildSwiftlyRelease.swift index cf6219b0..174cc9ff 100644 --- a/Tools/build-swiftly-release/BuildSwiftlyRelease.swift +++ b/Tools/build-swiftly-release/BuildSwiftlyRelease.swift @@ -1,6 +1,7 @@ import ArgumentParser import Foundation import SwiftlyCore +import SystemPackage #if os(macOS) import MacOSPlatform @@ -14,6 +15,9 @@ let currentPlatform = MacOS() let currentPlatform = Linux() #endif +typealias fs = FileSystem +typealias sys = SystemCommand + public struct SwiftPlatform: Codable { public var name: String? public var checksum: String? @@ -392,7 +396,6 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { try await self.checkGitRepoStatus(git) - let lipo = try await self.assertTool("lipo", message: "In order to make a universal binary there needs to be the `lipo` tool that is installed on macOS.") let pkgbuild = try await self.assertTool("pkgbuild", message: "In order to make pkg installers there needs to be the `pkgbuild` tool that is installed on macOS.") let strip = try await self.assertTool("strip", message: "In order to strip binaries there needs to be the `strip` tool that is installed on macOS.") @@ -405,10 +408,13 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { try runProgram(strip, ".build/\(arch)-apple-macosx/release/swiftly") } - let swiftlyBinDir = FileManager.default.currentDirectoryPath + "/.build/release/.swiftly/bin" - try? FileManager.default.createDirectory(atPath: swiftlyBinDir, withIntermediateDirectories: true) + let swiftlyBinDir = fs.cwd / ".build/release/.swiftly/bin" + try? await fs.mkdir(.parents, atPath: swiftlyBinDir) - try runProgram(lipo, ".build/x86_64-apple-macosx/release/swiftly", ".build/arm64-apple-macosx/release/swiftly", "-create", "-o", "\(swiftlyBinDir)/swiftly") + try await sys.lipo( + inputFiles: FilePath(".build/x86_64-apple-macosx/release/swiftly"), FilePath(".build/arm64-apple-macosx/release/swiftly")) + .create(.output(swiftlyBinDir / "swiftly")) + .run(currentPlatform) let swiftlyLicenseDir = FileManager.default.currentDirectoryPath + "/.build/release/.swiftly/license" try? FileManager.default.createDirectory(atPath: swiftlyLicenseDir, withIntermediateDirectories: true) @@ -423,7 +429,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { try runProgram( pkgbuild, "--root", - swiftlyBinDir + "/..", + "\(swiftlyBinDir.removingLastComponent)", "--install-location", ".swiftly", "--version", @@ -438,7 +444,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { try runProgram( pkgbuild, "--root", - swiftlyBinDir + "/..", + "\(swiftlyBinDir.removingLastComponent)", "--install-location", ".swiftly", "--version", @@ -479,7 +485,11 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { let testArchive = releaseDir.appendingPathComponent("test-swiftly-macos.tar.gz") - try runProgram(lipo, ".build/x86_64-apple-macosx/debug/test-swiftly", ".build/arm64-apple-macosx/debug/test-swiftly", "-create", "-o", "\(swiftlyBinDir)/swiftly") + try await sys.lipo( + inputFiles: FilePath(".build/x86_64-apple-macosx/debug/test-swiftly"), FilePath(".build/arm64-apple-macosx/debug/test-swiftly")) + .create(.output(swiftlyBinDir / "swiftly")) + .run(currentPlatform) + try runProgram(tar, "--directory=.build/x86_64-apple-macosx/debug", "-czf", testArchive.path, "test-swiftly") print(testArchive.path) From d9a1b8b32515c856ca069fabc710e1bc79a3209f Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Thu, 24 Apr 2025 11:21:55 -0400 Subject: [PATCH 2/4] Reformat --- .../BuildSwiftlyRelease.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Tools/build-swiftly-release/BuildSwiftlyRelease.swift b/Tools/build-swiftly-release/BuildSwiftlyRelease.swift index 174cc9ff..585534ad 100644 --- a/Tools/build-swiftly-release/BuildSwiftlyRelease.swift +++ b/Tools/build-swiftly-release/BuildSwiftlyRelease.swift @@ -412,9 +412,10 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { try? await fs.mkdir(.parents, atPath: swiftlyBinDir) try await sys.lipo( - inputFiles: FilePath(".build/x86_64-apple-macosx/release/swiftly"), FilePath(".build/arm64-apple-macosx/release/swiftly")) - .create(.output(swiftlyBinDir / "swiftly")) - .run(currentPlatform) + inputFiles: FilePath(".build/x86_64-apple-macosx/release/swiftly"), FilePath(".build/arm64-apple-macosx/release/swiftly") + ) + .create(.output(swiftlyBinDir / "swiftly")) + .run(currentPlatform) let swiftlyLicenseDir = FileManager.default.currentDirectoryPath + "/.build/release/.swiftly/license" try? FileManager.default.createDirectory(atPath: swiftlyLicenseDir, withIntermediateDirectories: true) @@ -486,9 +487,10 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { let testArchive = releaseDir.appendingPathComponent("test-swiftly-macos.tar.gz") try await sys.lipo( - inputFiles: FilePath(".build/x86_64-apple-macosx/debug/test-swiftly"), FilePath(".build/arm64-apple-macosx/debug/test-swiftly")) - .create(.output(swiftlyBinDir / "swiftly")) - .run(currentPlatform) + inputFiles: FilePath(".build/x86_64-apple-macosx/debug/test-swiftly"), FilePath(".build/arm64-apple-macosx/debug/test-swiftly") + ) + .create(.output(swiftlyBinDir / "swiftly")) + .run(currentPlatform) try runProgram(tar, "--directory=.build/x86_64-apple-macosx/debug", "-czf", testArchive.path, "test-swiftly") From a66acfef97c25785fe7c1950ed32ac9375414f8d Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Thu, 24 Apr 2025 11:38:29 -0400 Subject: [PATCH 3/4] Make required option a required named parameter in Swift --- Sources/SwiftlyCore/Commands.swift | 28 ++++--------------- Tests/SwiftlyTests/CommandLineTests.swift | 6 +++- .../BuildSwiftlyRelease.swift | 8 +++--- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/Sources/SwiftlyCore/Commands.swift b/Sources/SwiftlyCore/Commands.swift index ceff40a7..ad837104 100644 --- a/Sources/SwiftlyCore/Commands.swift +++ b/Sources/SwiftlyCore/Commands.swift @@ -123,39 +123,23 @@ extension SystemCommand { ) } - public func create(_ options: CreateCommand.Option...) -> CreateCommand { - CreateCommand(self, options) + public func create(output: FilePath) -> CreateCommand { + CreateCommand(self, output: output) } public struct CreateCommand { var lipo: LipoCommand + var output: FilePath - var options: [Option] - - init(_ lipo: LipoCommand, _ options: [Option]) { + init(_ lipo: LipoCommand, output: FilePath) { self.lipo = lipo - self.options = options - } - - public enum Option { - case output(FilePath) - - func args() -> [String] { - switch self { - case let .output(output): - return ["-output", output.string] - } - } + self.output = output } public func config() -> Configuration { var c = self.lipo.config() - var args = c.arguments.storage.map(\.description) + ["-create"] - - for opt in self.options { - args += opt.args() - } + var args = c.arguments.storage.map(\.description) + ["-create", "-output", "\(self.output)"] c.arguments = .init(args) diff --git a/Tests/SwiftlyTests/CommandLineTests.swift b/Tests/SwiftlyTests/CommandLineTests.swift index c5b44816..fb478fad 100644 --- a/Tests/SwiftlyTests/CommandLineTests.swift +++ b/Tests/SwiftlyTests/CommandLineTests.swift @@ -30,9 +30,13 @@ public struct CommandLineTests { } @Test func testLipo() { - var config = sys.lipo(inputFiles: FilePath("swiftly1"), FilePath("swiftly2")).create(.output(FilePath("swiftly-universal"))).config() + var config = sys.lipo(inputFiles: "swiftly1", "swiftly2").create(output: "swiftly-universal").config() #expect(config.executable == .name("lipo")) #expect(config.arguments.storage.map(\.description) == ["swiftly1", "swiftly2", "-create", "-output", "swiftly-universal"]) + + config = sys.lipo(inputFiles: "swiftly").create(output: "swiftly-universal-with-one-arch").config() + #expect(config.executable == .name("lipo")) + #expect(config.arguments.storage.map(\.description) == ["swiftly", "-create", "-output", "swiftly-universal-with-one-arch"]) } } diff --git a/Tools/build-swiftly-release/BuildSwiftlyRelease.swift b/Tools/build-swiftly-release/BuildSwiftlyRelease.swift index 585534ad..e00c2fbc 100644 --- a/Tools/build-swiftly-release/BuildSwiftlyRelease.swift +++ b/Tools/build-swiftly-release/BuildSwiftlyRelease.swift @@ -414,7 +414,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { try await sys.lipo( inputFiles: FilePath(".build/x86_64-apple-macosx/release/swiftly"), FilePath(".build/arm64-apple-macosx/release/swiftly") ) - .create(.output(swiftlyBinDir / "swiftly")) + .create(output: swiftlyBinDir / "swiftly") .run(currentPlatform) let swiftlyLicenseDir = FileManager.default.currentDirectoryPath + "/.build/release/.swiftly/license" @@ -430,7 +430,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { try runProgram( pkgbuild, "--root", - "\(swiftlyBinDir.removingLastComponent)", + "\(swiftlyBinDir.removingLastComponent())", "--install-location", ".swiftly", "--version", @@ -445,7 +445,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { try runProgram( pkgbuild, "--root", - "\(swiftlyBinDir.removingLastComponent)", + "\(swiftlyBinDir.removingLastComponent())", "--install-location", ".swiftly", "--version", @@ -489,7 +489,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { try await sys.lipo( inputFiles: FilePath(".build/x86_64-apple-macosx/debug/test-swiftly"), FilePath(".build/arm64-apple-macosx/debug/test-swiftly") ) - .create(.output(swiftlyBinDir / "swiftly")) + .create(output: swiftlyBinDir / "swiftly") .run(currentPlatform) try runProgram(tar, "--directory=.build/x86_64-apple-macosx/debug", "-czf", testArchive.path, "test-swiftly") From 18c14db376f13304dee56de7dbb9db96959e0d6e Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Thu, 24 Apr 2025 11:49:08 -0400 Subject: [PATCH 4/4] Add a computed property for getting the parent of a FilePath --- Sources/SwiftlyCore/FileManager+FilePath.swift | 2 ++ Tools/build-swiftly-release/BuildSwiftlyRelease.swift | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftlyCore/FileManager+FilePath.swift b/Sources/SwiftlyCore/FileManager+FilePath.swift index 7c6779bc..5d1453b4 100644 --- a/Sources/SwiftlyCore/FileManager+FilePath.swift +++ b/Sources/SwiftlyCore/FileManager+FilePath.swift @@ -199,4 +199,6 @@ extension FilePath { public static func / (left: FilePath, right: String) -> FilePath { left.appending(right) } + + public var parent: FilePath { self.removingLastComponent() } } diff --git a/Tools/build-swiftly-release/BuildSwiftlyRelease.swift b/Tools/build-swiftly-release/BuildSwiftlyRelease.swift index e00c2fbc..b5ead917 100644 --- a/Tools/build-swiftly-release/BuildSwiftlyRelease.swift +++ b/Tools/build-swiftly-release/BuildSwiftlyRelease.swift @@ -412,7 +412,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { try? await fs.mkdir(.parents, atPath: swiftlyBinDir) try await sys.lipo( - inputFiles: FilePath(".build/x86_64-apple-macosx/release/swiftly"), FilePath(".build/arm64-apple-macosx/release/swiftly") + inputFiles: ".build/x86_64-apple-macosx/release/swiftly", ".build/arm64-apple-macosx/release/swiftly" ) .create(output: swiftlyBinDir / "swiftly") .run(currentPlatform) @@ -430,7 +430,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { try runProgram( pkgbuild, "--root", - "\(swiftlyBinDir.removingLastComponent())", + "\(swiftlyBinDir.parent)", "--install-location", ".swiftly", "--version", @@ -445,7 +445,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { try runProgram( pkgbuild, "--root", - "\(swiftlyBinDir.removingLastComponent())", + "\(swiftlyBinDir.parent)", "--install-location", ".swiftly", "--version", @@ -487,7 +487,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { let testArchive = releaseDir.appendingPathComponent("test-swiftly-macos.tar.gz") try await sys.lipo( - inputFiles: FilePath(".build/x86_64-apple-macosx/debug/test-swiftly"), FilePath(".build/arm64-apple-macosx/debug/test-swiftly") + inputFiles: ".build/x86_64-apple-macosx/debug/test-swiftly", ".build/arm64-apple-macosx/debug/test-swiftly" ) .create(output: swiftlyBinDir / "swiftly") .run(currentPlatform)