Skip to content

Commit 73d53c2

Browse files
committed
Abstraction for running external programs, logging
1 parent e495279 commit 73d53c2

File tree

1 file changed

+77
-54
lines changed

1 file changed

+77
-54
lines changed

Plugins/Link/Link.swift

Lines changed: 77 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import PackagePlugin
33

44
@main
55
struct Link: CommandPlugin {
6+
static let pluginName: String = "link"
7+
68
func performCommand(
79
context: PluginContext,
810
arguments: [String]
@@ -12,14 +14,13 @@ struct Link: CommandPlugin {
1214
fileURLWithPath: clang.path.string,
1315
isDirectory: false
1416
)
17+
Diagnostics.remark("[\(Self.pluginName)] clang: \(clang.path.string)")
1518
let commonClangArgs = [
1619
"--target=armv6m-none-eabi",
1720
"-mfloat-abi=soft",
1821
"-march=armv6m",
1922
"-O3",
20-
"-DNDEBUG",
2123
"-nostdlib",
22-
"-Wl,--build-id=none",
2324
]
2425

2526
let buildParams = PackageManager.BuildParameters(
@@ -29,13 +30,14 @@ struct Link: CommandPlugin {
2930

3031
// Build RP2040 second-stage bootloader (boot2)
3132
let boot2Product = try context.package.products(named: ["RP2040Boot2"])[0]
33+
Diagnostics.remark("[\(Self.pluginName)] Building product '\(boot2Product.name)'")
3234
let boot2BuildResult = try packageManager.build(
3335
.product(boot2Product.name),
3436
parameters: buildParams
3537
)
3638
guard boot2BuildResult.succeeded else {
3739
print(boot2BuildResult.logText)
38-
Diagnostics.error("Building product '\(boot2Product.name)' failed")
40+
Diagnostics.error("[\(Self.pluginName)] Building product '\(boot2Product.name)' failed")
3941
// TODO: Exit with error code
4042
return
4143
}
@@ -44,13 +46,13 @@ struct Link: CommandPlugin {
4446
// Create directory for intermediate files
4547
let intermediatesDir = context.pluginWorkDirectory
4648
.appending(subpath: "intermediates")
47-
let intermediatesDirURL = URL(fileURLWithPath: intermediatesDir.string, isDirectory: true)
4849
try FileManager.default.createDirectory(
49-
at: intermediatesDirURL,
50+
at: URL(fileURLWithPath: intermediatesDir.string, isDirectory: true),
5051
withIntermediateDirectories: true
5152
)
5253

5354
// Postprocess boot2
55+
Diagnostics.remark("[\(Self.pluginName)] Boot2 processing")
5456
//
5557
// 1. Extract .o file from static library build product (.a)
5658
// For some reason, if I try to link the .a file with Clang, it doesn't work.
@@ -61,24 +63,13 @@ struct Link: CommandPlugin {
6163
// info is back (according to `file`).
6264
// How does SwiftPM create the .a file? Anything suspicious?
6365
let ar = try context.tool(named: "ar")
64-
let arURL = URL(fileURLWithPath: ar.path.string, isDirectory: false)
6566
let boot2ObjFile = intermediatesDir.appending(subpath: "compile_time_choice.S.o")
6667
let arArgs = [
6768
"x",
6869
boot2StaticLib.path.string,
6970
boot2ObjFile.lastComponent // ar always extracts to the current dir
7071
]
71-
let arProcess = Process()
72-
arProcess.executableURL = arURL
73-
arProcess.arguments = arArgs
74-
arProcess.currentDirectoryURL = intermediatesDirURL
75-
try arProcess.run()
76-
arProcess.waitUntilExit()
77-
guard arProcess.terminationStatus == 0 else {
78-
Diagnostics.error("ar failed")
79-
// TODO: Exit with error code
80-
return
81-
}
72+
try runProgram(ar.path, arguments: arArgs, workingDirectory: intermediatesDir)
8273

8374
// 2. Apply boot2 linker script
8475
let boot2ELF = intermediatesDir.appending(subpath: "bs2_default.elf")
@@ -88,18 +79,14 @@ struct Link: CommandPlugin {
8879
.first(where: { $0.type == .resource && $0.path.lastComponent == "boot_stage2.ld" })!
8980
var boot2ELFClangArgs = commonClangArgs
9081
boot2ELFClangArgs.append(contentsOf: [
82+
"-DNDEBUG",
83+
"-Wl,--build-id=none",
9184
"-Xlinker", "--script=\(boot2LinkerScript.path.string)",
9285
boot2ObjFile.string,
9386
"-o", boot2ELF.string
9487
])
9588
boot2ELFClangArgs.append(contentsOf: boot2BuildResult.builtArtifacts.map(\.path.string))
96-
let boot2ELFProcess = try Process.run(clangURL, arguments: boot2ELFClangArgs)
97-
boot2ELFProcess.waitUntilExit()
98-
guard boot2ELFProcess.terminationStatus == 0 else {
99-
Diagnostics.error("Clang failed linking boot2 elf file before checksumming")
100-
// TODO: Exit with error code
101-
return
102-
}
89+
try runProgram(clang.path, arguments: boot2ELFClangArgs)
10390

10491
// 3. Convert boot2.elf to boot2.bin
10592
let boot2Bin = intermediatesDir.appending(subpath: "bs2_default.bin")
@@ -110,13 +97,7 @@ struct Link: CommandPlugin {
11097
boot2ELF.string,
11198
boot2Bin.string
11299
]
113-
let objcopyProcess = try Process.run(objcopyURL, arguments: objcopyArgs)
114-
objcopyProcess.waitUntilExit()
115-
guard objcopyProcess.terminationStatus == 0 else {
116-
Diagnostics.error("objcopy failed")
117-
// TODO: Exit with error code
118-
return
119-
}
100+
try runProgram(objcopy.path, arguments: objcopyArgs)
120101

121102
// 4. Calculate checksum and write into assembly file
122103
let boot2ChecksummedAsm = intermediatesDir
@@ -125,19 +106,12 @@ struct Link: CommandPlugin {
125106
.sourceModules[0]
126107
.sourceFiles
127108
.first(where: { $0.type == .resource && $0.path.lastComponent == "pad_checksum" })!
128-
let padChecksumURL = URL(fileURLWithPath: padChecksumScript.path.string, isDirectory: false)
129109
let padChecksumArgs = [
130110
"-s", "0xffffffff",
131111
boot2Bin.string,
132112
boot2ChecksummedAsm.string
133113
]
134-
let padChecksumProcess = try Process.run(padChecksumURL, arguments: padChecksumArgs)
135-
padChecksumProcess.waitUntilExit()
136-
guard padChecksumProcess.terminationStatus == 0 else {
137-
Diagnostics.error("pad_checksum failed")
138-
// TODO: Exit with error code
139-
return
140-
}
114+
try runProgram(padChecksumScript.path, arguments: padChecksumArgs)
141115

142116
// 5. Assemble checksummed boot2 loader
143117
let boot2ChecksummedObj = intermediatesDir.appending(subpath: "bs2_default_padded_checksummed.s.o")
@@ -146,13 +120,7 @@ struct Link: CommandPlugin {
146120
"-c", boot2ChecksummedAsm.string,
147121
"-o", boot2ChecksummedObj.string
148122
])
149-
let boot2ObjProcess = try Process.run(clangURL, arguments: boot2ObjClangArgs)
150-
boot2ObjProcess.waitUntilExit()
151-
guard boot2ObjProcess.terminationStatus == 0 else {
152-
Diagnostics.error("Clang failed linking boot2 obj file")
153-
// TODO: Exit with error code
154-
return
155-
}
123+
try runProgram(clang.path, arguments: boot2ObjClangArgs)
156124

157125
// Build the app
158126
let appProduct = try context.package.products(named: ["App"])[0]
@@ -162,7 +130,7 @@ struct Link: CommandPlugin {
162130
)
163131
guard appBuildResult.succeeded else {
164132
print(appBuildResult.logText)
165-
Diagnostics.error("Building product '\(appProduct.name)' failed")
133+
Diagnostics.error("[\(Self.pluginName)] Building product '\(appProduct.name)' failed")
166134
// TODO: Exit with error code
167135
return
168136
}
@@ -181,6 +149,8 @@ struct Link: CommandPlugin {
181149
.first(where: { $0.type == .resource && $0.path.lastComponent == "memmap_default.ld" })!
182150
var appClangArgs = commonClangArgs
183151
appClangArgs.append(contentsOf: [
152+
"-DNDEBUG",
153+
"-Wl,--build-id=none",
184154
"-Xlinker", "--gc-sections",
185155
"-Xlinker", "--script=\(appLinkerScript.path.string)",
186156
"-Xlinker", "-z", "-Xlinker", "max-page-size=4096",
@@ -189,14 +159,67 @@ struct Link: CommandPlugin {
189159
boot2ChecksummedObj.string,
190160
"-o", linkedExecutable.string,
191161
])
192-
let appClangProcess = try Process.run(clangURL, arguments: appClangArgs)
193-
appClangProcess.waitUntilExit()
194-
guard appClangProcess.terminationStatus == 0 else {
195-
Diagnostics.error("Clang failed linking app executable")
196-
// TODO: Exit with error code
197-
return
198-
}
162+
try runProgram(clang.path, arguments: appClangArgs)
199163

200164
print("Executable: \(linkedExecutable)")
201165
}
166+
167+
/// Runs an external program and waits for it to finish.
168+
///
169+
/// Emits SwiftPM diagnostics:
170+
/// - `remark` with the invocation (exectuable + arguments)
171+
/// - `error` on non-zero exit code
172+
///
173+
/// - Throws:
174+
/// - When the program cannot be launched.
175+
/// - Throws `ExitCode` when the program completes with a non-zero status.
176+
private func runProgram(
177+
_ executable: Path,
178+
arguments: [String],
179+
workingDirectory: Path? = nil
180+
) throws {
181+
// If the command is longer than approx. one line, format it neatly
182+
// on multiple lines for logging.
183+
let fullCommand = "\(executable.string) \(arguments.joined(separator: " "))"
184+
let logMessage = if fullCommand.count < 70 {
185+
fullCommand
186+
} else {
187+
"""
188+
\(executable.string) \\
189+
\(arguments.joined(separator: " \\\n "))
190+
"""
191+
}
192+
Diagnostics.remark("[\(Self.pluginName)] \(logMessage)")
193+
194+
let process = Process()
195+
process.executableURL = URL(
196+
fileURLWithPath: executable.string,
197+
isDirectory: false
198+
)
199+
process.arguments = arguments
200+
if let workingDirectory {
201+
process.currentDirectoryURL = URL(
202+
fileURLWithPath: workingDirectory.string,
203+
isDirectory: true
204+
)
205+
}
206+
try process.run()
207+
process.waitUntilExit()
208+
guard process.terminationStatus == 0 else {
209+
Diagnostics.error("[\(Self.pluginName)] \(executable.lastComponent) exited with code \(process.terminationStatus)")
210+
throw ExitCode(process.terminationStatus)
211+
}
212+
}
213+
}
214+
215+
struct ExitCode: RawRepresentable, Error {
216+
var rawValue: Int32
217+
218+
init(rawValue: Int32) {
219+
self.rawValue = rawValue
220+
}
221+
222+
init(_ code: Int32) {
223+
self.init(rawValue: code)
224+
}
202225
}

0 commit comments

Comments
 (0)