Skip to content

Commit 5508505

Browse files
authored
Generate command models from argument parser info JSON files (#345)
Instead of hand-crafting the helper functions, options, and structs for each command, describe the command structure using an argument parser compatible JSON format for each command. Introduce a generator plugin that generates all of the Swift types in a manner that is similar to the way that they were hand-written before. There are certain limitations in the JSON schema for the Swift argument parser that prevent the generator from knowing certain important details. For example, there is no information on which arguments contain file paths, which is important information for type safety in this context. For this information, and more potentially in the future introduce a "-ext.json" file with an extension schema to hold the extra information.
1 parent 0c6ae4f commit 5508505

39 files changed

+53922
-1633
lines changed

Package.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ let package = Package(
6868
.product(name: "OpenAPIAsyncHTTPClient", package: "swift-openapi-async-http-client"),
6969
.product(name: "SystemPackage", package: "swift-system"),
7070
],
71-
swiftSettings: swiftSettings
71+
swiftSettings: swiftSettings,
72+
plugins: ["GenerateCommandModels"]
7273
),
7374
.target(
7475
name: "SwiftlyDownloadAPI",
@@ -107,13 +108,28 @@ let package = Package(
107108
),
108109
dependencies: ["generate-docs-reference"]
109110
),
111+
.plugin(
112+
name: "GenerateCommandModels",
113+
capability: .buildTool(),
114+
dependencies: [
115+
"generate-command-models",
116+
]
117+
),
110118
.executableTarget(
111119
name: "generate-docs-reference",
112120
dependencies: [
113121
.product(name: "ArgumentParser", package: "swift-argument-parser"),
114122
],
115123
path: "Tools/generate-docs-reference"
116124
),
125+
.executableTarget(
126+
name: "generate-command-models",
127+
dependencies: [
128+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
129+
.product(name: "SystemPackage", package: "swift-system"),
130+
],
131+
path: "Tools/generate-command-models"
132+
),
117133
.executableTarget(
118134
name: "build-swiftly-release",
119135
dependencies: [
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import PackagePlugin
2+
3+
@main
4+
struct GenerateCommandModelsPlugin: BuildToolPlugin {
5+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
6+
guard let target = target as? SourceModuleTarget else { return [] }
7+
8+
let jsonSources = target.sourceFiles.map(\.path).filter { $0.extension == "json" }
9+
10+
guard jsonSources.count > 0 else { return [] }
11+
12+
let outputPath = context.pluginWorkDirectory.appending("Commands.swift")
13+
14+
return [
15+
.buildCommand(
16+
displayName: "Generating Command Models from dumped JSON help",
17+
executable: try context.tool(named: "generate-command-models").path,
18+
arguments: ["--output-file", outputPath] + jsonSources,
19+
inputFiles: jsonSources,
20+
outputFiles: [outputPath]
21+
),
22+
]
23+
}
24+
}

Sources/LinuxPlatform/Linux.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,9 @@ public struct Linux: Platform {
277277
if let mockedHomeDir = ctx.mockedHomeDir {
278278
var env = ProcessInfo.processInfo.environment
279279
env["GNUPGHOME"] = (mockedHomeDir / ".gnupg").string
280-
try await sys.gpg()._import(keys: tmpFile).run(self, env: env, quiet: true)
280+
try await sys.gpg()._import(key: tmpFile).run(self, env: env, quiet: true)
281281
} else {
282-
try await sys.gpg()._import(keys: tmpFile).run(self, quiet: true)
282+
try await sys.gpg()._import(key: tmpFile).run(self, quiet: true)
283283
}
284284
}
285285
}
@@ -418,9 +418,9 @@ public struct Linux: Platform {
418418
if let mockedHomeDir = ctx.mockedHomeDir {
419419
var env = ProcessInfo.processInfo.environment
420420
env["GNUPGHOME"] = (mockedHomeDir / ".gnupg").string
421-
try await sys.gpg().verify(detachedSignature: sigFile, signedData: archive).run(self, env: env, quiet: false)
421+
try await sys.gpg().verify(detached_signature: sigFile, signed_data: archive).run(self, env: env, quiet: false)
422422
} else {
423-
try await sys.gpg().verify(detachedSignature: sigFile, signedData: archive).run(self, quiet: !verbose)
423+
try await sys.gpg().verify(detached_signature: sigFile, signed_data: archive).run(self, quiet: !verbose)
424424
}
425425
} catch {
426426
throw SwiftlyError(message: "Signature verification failed: \(error).")
@@ -447,9 +447,9 @@ public struct Linux: Platform {
447447
if let mockedHomeDir = ctx.mockedHomeDir {
448448
var env = ProcessInfo.processInfo.environment
449449
env["GNUPGHOME"] = (mockedHomeDir / ".gnupg").string
450-
try await sys.gpg().verify(detachedSignature: sigFile, signedData: archive).run(self, env: env, quiet: false)
450+
try await sys.gpg().verify(detached_signature: sigFile, signed_data: archive).run(self, env: env, quiet: false)
451451
} else {
452-
try await sys.gpg().verify(detachedSignature: sigFile, signedData: archive).run(self, quiet: !verbose)
452+
try await sys.gpg().verify(detached_signature: sigFile, signed_data: archive).run(self, quiet: !verbose)
453453
}
454454
} catch {
455455
throw SwiftlyError(message: "Signature verification failed: \(error).")
@@ -603,7 +603,7 @@ public struct Linux: Platform {
603603

604604
public func getShell() async throws -> String {
605605
let userName = ProcessInfo.processInfo.userName
606-
if let entry = try await sys.getent(database: "passwd", keys: userName).entries(self).first {
606+
if let entry = try await sys.getent(database: "passwd", key: userName).entries(self).first {
607607
if let shell = entry.last { return shell }
608608
}
609609

Sources/MacOSPlatform/MacOS.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public struct MacOS: Platform {
7171
// If the toolchains go into the default user location then we use the installer to install them
7272
await ctx.print("Installing package in user home directory...")
7373

74-
try await sys.installer(.verbose, pkg: tmpFile, target: "CurrentUserHomeDirectory").run(self, quiet: !verbose)
74+
try await sys.installer(.verbose, .pkg(tmpFile), .target("CurrentUserHomeDirectory")).run(self, quiet: !verbose)
7575
} else {
7676
// Otherwise, we extract the pkg into the requested toolchains directory.
7777
await ctx.print("Expanding pkg...")
@@ -84,7 +84,7 @@ public struct MacOS: Platform {
8484

8585
await ctx.print("Checking package signature...")
8686
do {
87-
try await sys.pkgutil().checkSignature(pkgPath: tmpFile).run(self, quiet: !verbose)
87+
try await sys.pkgutil().checksignature(pkg_path: tmpFile).run(self, quiet: !verbose)
8888
} catch {
8989
// If this is not a test that uses mocked toolchains then we must throw this error and abort installation
9090
guard ctx.mockedHomeDir != nil else {
@@ -94,7 +94,7 @@ public struct MacOS: Platform {
9494
// We permit the signature verification to fail during testing
9595
await ctx.print("Signature verification failed, which is allowable during testing with mocked toolchains")
9696
}
97-
try await sys.pkgutil(.verbose).expand(pkgPath: tmpFile, dirPath: tmpDir).run(self, quiet: !verbose)
97+
try await sys.pkgutil(.verbose).expand(pkg_path: tmpFile, dir_path: tmpDir).run(self, quiet: !verbose)
9898

9999
// There's a slight difference in the location of the special Payload file between official swift packages
100100
// and the ones that are mocked here in the test framework.
@@ -118,18 +118,18 @@ public struct MacOS: Platform {
118118
if ctx.mockedHomeDir == nil {
119119
await ctx.print("Extracting the swiftly package...")
120120
try await sys.installer(
121-
pkg: archive,
122-
target: "CurrentUserHomeDirectory"
121+
.pkg(archive),
122+
.target("CurrentUserHomeDirectory")
123123
)
124-
try? await sys.pkgutil(.volume(userHomeDir)).forget(packageId: "org.swift.swiftly").run(self)
124+
try? await sys.pkgutil(.volume(userHomeDir)).forget(pkg_id: "org.swift.swiftly").run(self)
125125
} else {
126126
let installDir = userHomeDir / ".swiftly"
127127
try await fs.mkdir(.parents, atPath: installDir)
128128

129129
// In the case of a mock for testing purposes we won't use the installer, perferring a manual process because
130130
// the installer will not install to an arbitrary path, only a volume or user home directory.
131131
let tmpDir = fs.mktemp()
132-
try await sys.pkgutil().expand(pkgPath: archive, dirPath: tmpDir).run(self)
132+
try await sys.pkgutil().expand(pkg_path: archive, dir_path: tmpDir).run(self)
133133

134134
// There's a slight difference in the location of the special Payload file between official swift packages
135135
// and the ones that are mocked here in the test framework.
@@ -164,7 +164,7 @@ public struct MacOS: Platform {
164164

165165
try await fs.remove(atPath: toolchainDir)
166166

167-
try? await sys.pkgutil(.volume(fs.home)).forget(packageId: pkgInfo.CFBundleIdentifier).run(self, quiet: !verbose)
167+
try? await sys.pkgutil(.volume(fs.home)).forget(pkg_id: pkgInfo.CFBundleIdentifier).run(self, quiet: !verbose)
168168
}
169169

170170
public func getExecutableName() -> String {
@@ -193,7 +193,7 @@ public struct MacOS: Platform {
193193
}
194194

195195
public func getShell() async throws -> String {
196-
for (key, value) in try await sys.dscl(datasource: ".").read(path: fs.home, keys: "UserShell").properties(self) {
196+
for (key, value) in try await sys.dscl(datasource: ".").read(path: fs.home, key: ["UserShell"]).properties(self) {
197197
return value
198198
}
199199

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import Foundation
2+
import SystemPackage
3+
4+
extension SystemCommand.dsclCommand.readCommand: Output {
5+
public func properties(_ p: Platform) async throws -> [(key: String, value: String)] {
6+
let output = try await self.output(p)
7+
guard let output else { return [] }
8+
9+
var props: [(key: String, value: String)] = []
10+
for line in output.components(separatedBy: "\n") {
11+
if case let comps = line.components(separatedBy: ": "), comps.count == 2 {
12+
props.append((key: comps[0], value: comps[1]))
13+
}
14+
}
15+
return props
16+
}
17+
}
18+
19+
extension SystemCommand.lipoCommand.createCommand: Runnable {}
20+
21+
extension SystemCommand.pkgbuildCommand: Runnable {}
22+
23+
extension SystemCommand.getentCommand: Output {
24+
public func entries(_ platform: Platform) async throws -> [[String]] {
25+
let output = try await output(platform)
26+
guard let output else { return [] }
27+
28+
var entries: [[String]] = []
29+
for line in output.components(separatedBy: "\n") {
30+
entries.append(line.components(separatedBy: ":"))
31+
}
32+
return entries
33+
}
34+
}
35+
36+
extension SystemCommand.gitCommand.logCommand: Output {}
37+
extension SystemCommand.gitCommand.diffindexCommand: Runnable {}
38+
extension SystemCommand.gitCommand.initCommand: Runnable {}
39+
extension SystemCommand.gitCommand.commitCommand: Runnable {}
40+
41+
extension SystemCommand.tarCommand.createCommand: Runnable {}
42+
extension SystemCommand.tarCommand.extractCommand: Runnable {}
43+
44+
extension SystemCommand.swiftCommand.packageCommand.resetCommand: Runnable {}
45+
extension SystemCommand.swiftCommand.packageCommand.cleanCommand: Runnable {}
46+
extension SystemCommand.swiftCommand.packageCommand.initCommand: Runnable {}
47+
extension SystemCommand.swiftCommand.sdkCommand.installCommand: Runnable {}
48+
extension SystemCommand.swiftCommand.sdkCommand.removeCommand: Runnable {}
49+
extension SystemCommand.swiftCommand.buildCommand: Runnable {}
50+
51+
extension SystemCommand.makeCommand: Runnable {}
52+
extension SystemCommand.makeCommand.installCommand: Runnable {}
53+
54+
extension SystemCommand.stripCommand: Runnable {}
55+
56+
extension SystemCommand.sha256sumCommand: Output {}
57+
58+
extension SystemCommand.productbuildCommand: Runnable {}
59+
60+
extension SystemCommand.gpgCommand.importCommand: Runnable {}
61+
extension SystemCommand.gpgCommand.verifyCommand: Runnable {}
62+
63+
extension SystemCommand.pkgutilCommand.checksignatureCommand: Runnable {}
64+
extension SystemCommand.pkgutilCommand.expandCommand: Runnable {}
65+
extension SystemCommand.pkgutilCommand.forgetCommand: Runnable {}
66+
67+
extension SystemCommand.installerCommand: Runnable {}

0 commit comments

Comments
 (0)