Skip to content

Commit 2a03c79

Browse files
committed
Assert the Linkage of SwiftSyntax and SwiftParser
We want to keep this set as minimal as possible. To that end, provide a test on Darwin that the set of link-time dependencies remains known to us. If this set expands, this test will fail and we'll be able to investigate the dependency growth.
1 parent 23e54c1 commit 2a03c79

File tree

1 file changed

+130
-0
lines changed

1 file changed

+130
-0
lines changed

Tests/SwiftParserTest/Linkage.swift

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#if canImport(Darwin)
2+
import Darwin
3+
import XCTest
4+
5+
final class LinkageTest: XCTestCase {
6+
// Assert that SwiftSyntax and SwiftParser do not introduce more link-time
7+
// dependencies than are strictly necessary. We want to minimize our link-time
8+
// dependencies. If this set changes - in particular, if it grows - consult
9+
// a SwiftSyntax maintainer to see if there's a way to avoid adding the
10+
// dependency.
11+
func testLinkage() throws {
12+
for i in 0..<_dyld_image_count() {
13+
let name = try XCTUnwrap(_dyld_get_image_name(i))
14+
let path = String(cString: name)
15+
guard path.hasSuffix("SwiftParserTest") else {
16+
continue
17+
}
18+
19+
var baseURL = URL(fileURLWithPath: path)
20+
while !baseURL.pathComponents.isEmpty, baseURL.pathExtension != "xctest" {
21+
baseURL = baseURL.deletingLastPathComponent()
22+
}
23+
24+
if baseURL.pathComponents.isEmpty {
25+
XCTFail("Unable to determine path to enclosing xctest bundle")
26+
return
27+
}
28+
29+
baseURL = baseURL.deletingLastPathComponent()
30+
let swiftSyntaxURL = baseURL.appendingPathComponent("SwiftSyntax.o")
31+
try assertLinkage(of: swiftSyntaxURL) { linkages in
32+
XCTAssertEqual(linkages, [
33+
.library("-lobjc"),
34+
.library("-lswiftCompatibilityConcurrency"),
35+
.library("-lswiftCore"),
36+
.library("-lswiftDarwin"),
37+
.library("-lswiftSwiftOnoneSupport"),
38+
.library("-lswift_Concurrency"),
39+
])
40+
}
41+
let swiftParserURL = baseURL.appendingPathComponent("SwiftParser.o")
42+
try assertLinkage(of: swiftParserURL) { linkages in
43+
XCTAssertEqual(linkages, [
44+
.library("-lobjc"),
45+
.library("-lswiftCompatibilityConcurrency"),
46+
.library("-lswiftCore"),
47+
.library("-lswiftDarwin"),
48+
.library("-lswiftSwiftOnoneSupport"),
49+
.library("-lswift_Concurrency"),
50+
])
51+
}
52+
}
53+
}
54+
55+
private enum Linkage: Comparable {
56+
case library(String)
57+
case framework(String)
58+
}
59+
60+
private func readLoadCommands(in object: URL) throws -> [String] {
61+
let result = Process()
62+
result.executableURL = try XCTUnwrap(URL(fileURLWithPath: "/usr/bin/xcrun"))
63+
result.arguments = [
64+
"otool", "-l", object.path,
65+
]
66+
let outputPipe = Pipe()
67+
let errorPipe = Pipe()
68+
69+
result.standardOutput = outputPipe
70+
result.standardError = errorPipe
71+
try result.run()
72+
result.waitUntilExit()
73+
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
74+
let output = String(decoding: outputData, as: UTF8.self)
75+
return output.components(separatedBy: .newlines)
76+
}
77+
78+
private func assertLinkage(of object: URL, assertion: ([Linkage]) throws -> Void) throws {
79+
var linkages = [Linkage]()
80+
var lines = try self.readLoadCommands(in: object).makeIterator()
81+
while let line = lines.next() {
82+
guard line.starts(with: "Load command") else {
83+
continue
84+
}
85+
86+
guard
87+
let command = lines.next(),
88+
command.hasSuffix("LC_LINKER_OPTION"),
89+
let _ = lines.next(),
90+
let count = lines.next()
91+
else {
92+
continue
93+
}
94+
95+
let countString = count.trimmingCharacters(in: .whitespaces)
96+
.suffix(from: count.index(count.startIndex, offsetBy: "count ".count))
97+
guard let count = Int(countString), count == 1 || count == 2 else {
98+
XCTFail("Malformed load command: \(line)")
99+
return
100+
}
101+
102+
if count == 1 {
103+
guard let library = lines.next() else {
104+
XCTFail("No load command payload: \(line)")
105+
return
106+
}
107+
108+
let linkLibrary = library.trimmingCharacters(in: .whitespaces)
109+
.suffix(from: library.index(library.startIndex, offsetBy: "string #1 ".count))
110+
linkages.append(.library(String(linkLibrary)))
111+
} else {
112+
assert(count == 2)
113+
guard
114+
let frameworkArg = lines.next(),
115+
frameworkArg.trimmingCharacters(in: .whitespaces) == "string #1 -framework",
116+
let framework = lines.next()
117+
else {
118+
XCTFail("No load command payload: \(line)")
119+
return
120+
}
121+
122+
let linkedFramework = framework.trimmingCharacters(in: .whitespaces)
123+
.suffix(from: framework.index(framework.startIndex, offsetBy: "string #2 ".count))
124+
linkages.append(.framework(String(linkedFramework)))
125+
}
126+
}
127+
return try assertion(linkages.sorted())
128+
}
129+
}
130+
#endif

0 commit comments

Comments
 (0)