Skip to content

Commit 8dc6b7e

Browse files
committed
Model the pkgbuild command
Model the pkgbuild command with the usual structure used for other commands. Provide a custom string convertible implementation for configuration so that the configuration can be easily converted into a string for logging and comparison purposes. Create a runEcho() implementation for the build swiftly release script so that commands can output their command-line to the log for reproducibility.
1 parent df0e91c commit 8dc6b7e

File tree

5 files changed

+157
-63
lines changed

5 files changed

+157
-63
lines changed

Sources/SwiftlyCore/Commands.swift

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ extension SystemCommand.DsclCommand.ReadCommand: Output {
9191
}
9292
}
9393

94+
// Create or operate on universal files
95+
// See lipo(1) for details
9496
extension SystemCommand {
9597
public static func lipo(executable: Executable = LipoCommand.defaultExecutable, inputFiles: FilePath...) -> LipoCommand {
9698
Self.lipo(executable: executable, inputFiles: inputFiles)
@@ -106,7 +108,7 @@ extension SystemCommand {
106108
var executable: Executable
107109
var inputFiles: [FilePath]
108110

109-
public init(executable: Executable, inputFiles: [FilePath]) {
111+
internal init(executable: Executable, inputFiles: [FilePath]) {
110112
self.executable = executable
111113
self.inputFiles = inputFiles
112114
}
@@ -150,3 +152,72 @@ extension SystemCommand {
150152
}
151153

152154
extension SystemCommand.LipoCommand.CreateCommand: Runnable {}
155+
156+
// Build a macOS Installer component package from on-disk files
157+
// See pkgbuild(1) for more details
158+
extension SystemCommand {
159+
public static func pkgbuild(executable: Executable = PkgbuildCommand.defaultExecutable, _ options: PkgbuildCommand.Option..., root: FilePath, packageOutputPath: FilePath) -> PkgbuildCommand {
160+
Self.pkgbuild(executable: executable, options: options, root: root, packageOutputPath: packageOutputPath)
161+
}
162+
163+
public static func pkgbuild(executable: Executable = PkgbuildCommand.defaultExecutable, options: [PkgbuildCommand.Option], root: FilePath, packageOutputPath: FilePath) -> PkgbuildCommand {
164+
PkgbuildCommand(executable: executable, options, root: root, packageOutputPath: packageOutputPath)
165+
}
166+
167+
public struct PkgbuildCommand {
168+
public static var defaultExecutable: Executable { .name("pkgbuild") }
169+
170+
var executable: Executable
171+
172+
var options: [Option]
173+
174+
var root: FilePath
175+
var packageOutputPath: FilePath
176+
177+
internal init(executable: Executable, _ options: [Option], root: FilePath, packageOutputPath: FilePath) {
178+
self.executable = executable
179+
self.options = options
180+
self.root = root
181+
self.packageOutputPath = packageOutputPath
182+
}
183+
184+
public enum Option {
185+
case installLocation(FilePath)
186+
case version(String)
187+
case identifier(String)
188+
case sign(String)
189+
190+
func args() -> [String] {
191+
switch self {
192+
case let .installLocation(installLocation):
193+
return ["--install-location", installLocation.string]
194+
case let .version(version):
195+
return ["--version", version]
196+
case let .identifier(identifier):
197+
return ["--identifier", identifier]
198+
case let .sign(identityName):
199+
return ["--sign", identityName]
200+
}
201+
}
202+
}
203+
204+
public func config() -> Configuration {
205+
var args: [String] = []
206+
207+
for option in self.options {
208+
args += option.args()
209+
}
210+
211+
args += ["--root", "\(self.root)"]
212+
args += ["\(self.packageOutputPath)"]
213+
214+
return Configuration(
215+
executable: self.executable,
216+
arguments: Arguments(args),
217+
environment: .inherit
218+
)
219+
}
220+
}
221+
}
222+
223+
extension SystemCommand.PkgbuildCommand: Runnable {}

Sources/SwiftlyCore/ModeledCommandLine.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,38 @@ public struct Arguments: Sendable, ExpressibleByArrayLiteral, Hashable {
121121
}
122122
}
123123

124+
// Provide string representations of Configuration
125+
extension Executable: CustomStringConvertible {
126+
public var description: String {
127+
switch self.storage {
128+
case let .executable(name):
129+
name
130+
case let .path(path):
131+
path.string
132+
}
133+
}
134+
}
135+
136+
extension Arguments: CustomStringConvertible {
137+
public var description: String {
138+
let normalized: [String] = self.storage.map(\.description).map {
139+
if $0.contains(" ") {
140+
return "\"\($0)\""
141+
} else {
142+
return String($0)
143+
}
144+
}
145+
146+
return normalized.joined(separator: " ")
147+
}
148+
}
149+
150+
extension Configuration: CustomStringConvertible {
151+
public var description: String {
152+
"\(self.executable) \(self.arguments)"
153+
}
154+
}
155+
124156
public protocol Runnable {
125157
func config() -> Configuration
126158
}

Tests/SwiftlyTests/CommandLineTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,12 @@ public struct CommandLineTests {
3939
#expect(config.executable == .name("lipo"))
4040
#expect(config.arguments.storage.map(\.description) == ["swiftly", "-create", "-output", "swiftly-universal-with-one-arch"])
4141
}
42+
43+
@Test func testPkgbuild() {
44+
var config = sys.pkgbuild(root: "mypath", packageOutputPath: "outputDir").config()
45+
#expect(String(describing: config) == "pkgbuild --root mypath outputDir")
46+
47+
config = sys.pkgbuild(.version("1234"), root: "somepath", packageOutputPath: "output").config()
48+
#expect(String(describing: config) == "pkgbuild --version 1234 --root somepath output")
49+
}
4250
}

Tests/SwiftlyTests/SwiftlyTests.swift

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -940,22 +940,14 @@ public final actor MockToolchainDownloader: HTTPRequestExecutor {
940940

941941
let pkg = tmp / "swiftly.pkg"
942942

943-
let task = Process()
944-
task.executableURL = URL(fileURLWithPath: "/usr/bin/env")
945-
task.arguments = [
946-
"pkgbuild",
947-
"--root",
948-
"\(swiftlyDir)",
949-
"--install-location",
950-
".swiftly",
951-
"--version",
952-
"\(self.latestSwiftlyVersion)",
953-
"--identifier",
954-
"org.swift.swiftly",
955-
"\(pkg)",
956-
]
957-
try task.run()
958-
task.waitUntilExit()
943+
try await sys.pkgbuild(
944+
.installLocation("swiftly"),
945+
.version("\(self.latestSwiftlyVersion)"),
946+
.identifier("org.swift.swiftly"),
947+
root: swiftlyDir,
948+
packageOutputPath: pkg
949+
)
950+
.run(Swiftly.currentPlatform)
959951

960952
let data = try Data(contentsOf: pkg)
961953
try await fs.remove(atPath: tmp)
@@ -993,24 +985,16 @@ public final actor MockToolchainDownloader: HTTPRequestExecutor {
993985
let data = try encoder.encode(pkgInfo)
994986
try data.write(to: toolchainDir.appending("Info.plist"))
995987

996-
let pkg = tmp.appending("toolchain.pkg")
988+
let pkg = tmp / "toolchain.pkg"
997989

998-
let task = Process()
999-
task.executableURL = URL(fileURLWithPath: "/usr/bin/env")
1000-
task.arguments = [
1001-
"pkgbuild",
1002-
"--root",
1003-
"\(toolchainDir)",
1004-
"--install-location",
1005-
"Library/Developer/Toolchains/\(toolchain.identifier).xctoolchain",
1006-
"--version",
1007-
"\(toolchain.name)",
1008-
"--identifier",
1009-
pkgInfo.CFBundleIdentifier,
1010-
"\(pkg)",
1011-
]
1012-
try task.run()
1013-
task.waitUntilExit()
990+
try await sys.pkgbuild(
991+
.installLocation(FilePath("Library/Developer/Toolchains/\(toolchain.identifier).xctoolchain")),
992+
.version("\(toolchain.name)"),
993+
.identifier(pkgInfo.CFBundleIdentifier),
994+
root: toolchainDir,
995+
packageOutputPath: pkg
996+
)
997+
.run(Swiftly.currentPlatform)
1014998

1015999
let pkgData = try Data(contentsOf: pkg)
10161000
try await fs.remove(atPath: tmp)

Tools/build-swiftly-release/BuildSwiftlyRelease.swift

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ let currentPlatform = Linux()
1818
typealias fs = FileSystem
1919
typealias sys = SystemCommand
2020

21+
extension Runnable {
22+
// Runs the command while echoing the full command-line to stdout for logging and reproduction
23+
func runEcho(_ platform: Platform, quiet: Bool = false) async throws {
24+
let config = self.config()
25+
// if !quiet { print("\(args.joined(separator: " "))") }
26+
if !quiet { print("\(config)") }
27+
28+
try await self.run(platform)
29+
}
30+
}
31+
2132
public struct SwiftPlatform: Codable {
2233
public var name: String?
2334
public var checksum: String?
@@ -396,7 +407,6 @@ struct BuildSwiftlyRelease: AsyncParsableCommand {
396407

397408
try await self.checkGitRepoStatus(git)
398409

399-
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.")
400410
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.")
401411

402412
let tar = try await self.assertTool("tar", message: "In order to produce archives there needs to be the `tar` tool that is installed on macOS.")
@@ -415,7 +425,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand {
415425
inputFiles: ".build/x86_64-apple-macosx/release/swiftly", ".build/arm64-apple-macosx/release/swiftly"
416426
)
417427
.create(output: swiftlyBinDir / "swiftly")
418-
.run(currentPlatform)
428+
.runEcho(currentPlatform)
419429

420430
let swiftlyLicenseDir = FileManager.default.currentDirectoryPath + "/.build/release/.swiftly/license"
421431
try? FileManager.default.createDirectory(atPath: swiftlyLicenseDir, withIntermediateDirectories: true)
@@ -427,33 +437,22 @@ struct BuildSwiftlyRelease: AsyncParsableCommand {
427437
let pkgFile = releaseDir.appendingPathComponent("/swiftly-\(self.version).pkg")
428438

429439
if let cert {
430-
try runProgram(
431-
pkgbuild,
432-
"--root",
433-
"\(swiftlyBinDir.parent)",
434-
"--install-location",
435-
".swiftly",
436-
"--version",
437-
self.version,
438-
"--identifier",
439-
identifier,
440-
"--sign",
441-
cert,
442-
".build/release/swiftly-\(self.version).pkg"
443-
)
440+
try await sys.pkgbuild(
441+
.installLocation(".swiftly"),
442+
.version(self.version),
443+
.identifier(identifier),
444+
.sign(cert),
445+
root: swiftlyBinDir.parent,
446+
packageOutputPath: FilePath(".build/release/swiftly-\(self.version).pkg")
447+
).runEcho(currentPlatform)
444448
} else {
445-
try runProgram(
446-
pkgbuild,
447-
"--root",
448-
"\(swiftlyBinDir.parent)",
449-
"--install-location",
450-
".swiftly",
451-
"--version",
452-
self.version,
453-
"--identifier",
454-
identifier,
455-
".build/release/swiftly-\(self.version).pkg"
456-
)
449+
try await sys.pkgbuild(
450+
.installLocation(".swiftly"),
451+
.version(self.version),
452+
.identifier(identifier),
453+
root: swiftlyBinDir.parent,
454+
packageOutputPath: FilePath(".build/release/swiftly-\(self.version).pkg")
455+
).runEcho(currentPlatform)
457456
}
458457

459458
// Re-configure the pkg to prefer installs into the current user's home directory with the help of productbuild.
@@ -490,7 +489,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand {
490489
inputFiles: ".build/x86_64-apple-macosx/debug/test-swiftly", ".build/arm64-apple-macosx/debug/test-swiftly"
491490
)
492491
.create(output: swiftlyBinDir / "swiftly")
493-
.run(currentPlatform)
492+
.runEcho(currentPlatform)
494493

495494
try runProgram(tar, "--directory=.build/x86_64-apple-macosx/debug", "-czf", testArchive.path, "test-swiftly")
496495

0 commit comments

Comments
 (0)