@@ -9,10 +9,150 @@ final class LinkageTest: XCTestCase {
9
9
// a SwiftSyntax maintainer to see if there's a way to avoid adding the
10
10
// dependency.
11
11
func testLinkage( ) throws {
12
+ guard let baseURL = try self . findEnclosingTestBundle ( ) else {
13
+ XCTFail ( " Unable to determine path to enclosing xctest bundle " )
14
+ return
15
+ }
16
+
17
+ try assertLinkage ( of: " SwiftSyntax " , in: baseURL, assertions: [
18
+ . library( " -lobjc " ) ,
19
+ . library( " -lswiftCompatibility51 " , condition: . mayBeAbsent( " Starting in Xcode 14 this library is not always autolinked " ) ) ,
20
+ . library( " -lswiftCompatibilityConcurrency " ) ,
21
+ . library( " -lswiftCore " ) ,
22
+ . library( " -lswiftDarwin " ) ,
23
+ . library( " -lswiftSwiftOnoneSupport " , condition: . when( configuration: . debug) ) ,
24
+ . library( " -lswift_Concurrency " ) ,
25
+ . library( " -lswift_StringProcessing " , condition: . when( swiftVersionAtLeast: . v5_7) ) ,
26
+ ] )
27
+
28
+ try assertLinkage ( of: " SwiftParser " , in: baseURL, assertions: [
29
+ . library( " -lobjc " ) ,
30
+ . library( " -lswiftCompatibility51 " , condition: . mayBeAbsent( " Starting in Xcode 14 this library is not always autolinked " ) ) ,
31
+ . library( " -lswiftCompatibilityConcurrency " ) ,
32
+ . library( " -lswiftCore " ) ,
33
+ . library( " -lswiftDarwin " ) ,
34
+ . library( " -lswiftSwiftOnoneSupport " , condition: . when( configuration: . debug) ) ,
35
+ . library( " -lswift_Concurrency " ) ,
36
+ . library( " -lswift_StringProcessing " , condition: . when( swiftVersionAtLeast: . v5_7) ) ,
37
+ ] )
38
+ }
39
+ }
40
+
41
+ extension LinkageTest {
42
+ public struct MachHeader {
43
+ var magic : UInt32
44
+ var cputype : UInt32
45
+ var cpusubtype : UInt32
46
+ var filetype : UInt32
47
+ var ncmds : UInt32
48
+ var sizeofcmds : UInt32
49
+ var flags : UInt32
50
+ var reserved : UInt32
51
+
52
+ public struct LoadCommand : OptionSet {
53
+ public var rawValue : UInt32
54
+
55
+ public init ( rawValue: UInt32 ) {
56
+ self . rawValue = rawValue
57
+ }
58
+
59
+ /// A load command that defines a list of linker options strings embedded
60
+ /// directly into this file.
61
+ public static let linkerOption = Self ( rawValue: 0x2D )
62
+ }
63
+ }
64
+
65
+ struct LoadCommand {
66
+ var cmd : MachHeader . LoadCommand
67
+ var cmdsize : UInt32
68
+ }
69
+
70
+ struct LinkerOptionCommand {
71
+ var cmd : MachHeader . LoadCommand
72
+ var cmdsize : UInt32
73
+ var count : UInt32
74
+ /* concatenation of zero terminated UTF8 strings.
75
+ Zero filled at end to align */
76
+ }
77
+ }
78
+
79
+ extension LinkageTest {
80
+ private func assertLinkage(
81
+ of library: String ,
82
+ in bundle: EnclosingTestBundle ,
83
+ assertions: [ Linkage . Assertion ]
84
+ ) throws {
85
+ let linkages = try bundle. objectFiles ( for: library) . reduce ( into: [ ] ) { acc, next in
86
+ acc += try self . extractAutolinkingHints ( in: next)
87
+ }
88
+
89
+ let sortedLinkages = Set ( linkages) . sorted ( )
90
+ var expectedLinkagesIdx = sortedLinkages. startIndex
91
+ var assertions = assertions. makeIterator ( )
92
+ while let assert = assertions. next ( ) {
93
+ // Evaluate the condition first, if any.
94
+ if let condition = assert. condition, !condition. evaluate ( ) {
95
+ continue
96
+ }
97
+
98
+ // Make sure there's a linkage to even assert against. If not, then we've
99
+ // got too many assertions and not enough link libraries. This isn't
100
+ // always a bad thing, but it's worth calling out so we document this
101
+ // with `.mayBeAbsent(...)`.
102
+ guard expectedLinkagesIdx < sortedLinkages. endIndex else {
103
+ XCTFail ( " Expected linkage was not present: \( assert. linkage) " ,
104
+ file: assert. file, line: assert. line)
105
+ continue
106
+ }
107
+
108
+ let linkage = sortedLinkages [ expectedLinkagesIdx]
109
+
110
+ // Check the linkage assertion. If it doesn't hold, the real fun begins.
111
+ guard !assert. matches ( linkage) else {
112
+ expectedLinkagesIdx += 1
113
+ continue
114
+ }
115
+
116
+ // Skip flaky linkages if they're absent.
117
+ if case . flaky = assert. condition {
118
+ continue
119
+ }
120
+
121
+ XCTFail ( " Expected linkage to \( assert. linkage) , but recieved linkage to \( linkage. linkage) ; Perhaps linkage assertions are out of order? " ,
122
+ file: assert. file, line: assert. line)
123
+ expectedLinkagesIdx += 1
124
+ }
125
+
126
+ for superfluousLinkage in sortedLinkages [ expectedLinkagesIdx..< sortedLinkages. endIndex] {
127
+ XCTFail ( " Found unasserted link-time dependency: \( superfluousLinkage. linkage) " )
128
+ }
129
+ }
130
+
131
+ private enum EnclosingTestBundle {
132
+ case incremental( URL )
133
+ case unified( URL )
134
+
135
+ func objectFiles( for library: String ) throws -> [ URL ] {
136
+ switch self {
137
+ case . incremental( let baseURL) :
138
+ return [ baseURL. appendingPathComponent ( library + " .o " ) ]
139
+ case . unified( let baseURL) :
140
+ return try FileManager . default
141
+ . contentsOfDirectory ( at: baseURL. appendingPathComponent ( library + " .build " ) ,
142
+ includingPropertiesForKeys: nil )
143
+ . filter ( { $0. pathExtension == " o " } )
144
+ }
145
+ }
146
+ }
147
+
148
+ private func findEnclosingTestBundle( ) throws -> EnclosingTestBundle ? {
12
149
for i in 0 ..< _dyld_image_count ( ) {
13
150
let name = try XCTUnwrap ( _dyld_get_image_name ( i) )
14
151
let path = String ( cString: name)
15
- guard path. hasSuffix ( " SwiftParserTest " ) else {
152
+ // We can wind up in SwiftParserTest.xctest when built via the IDE or
153
+ // in SwiftSyntaxPackageTests.xctest when built at the command line
154
+ // via the package manager.
155
+ guard path. hasSuffix ( " SwiftParserTest " ) || path. hasSuffix ( " SwiftSyntaxPackageTests " ) else {
16
156
continue
17
157
}
18
158
@@ -22,109 +162,156 @@ final class LinkageTest: XCTestCase {
22
162
}
23
163
24
164
if baseURL. pathComponents. isEmpty {
25
- XCTFail ( " Unable to determine path to enclosing xctest bundle " )
26
- return
165
+ continue
166
+ }
167
+
168
+ let url = baseURL. deletingLastPathComponent ( )
169
+ if path. hasSuffix ( " SwiftParserTest " ) {
170
+ return . incremental( url)
171
+ } else {
172
+ return . unified( url)
27
173
}
174
+ }
175
+ return nil
176
+ }
28
177
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
- ] )
178
+ private func extractAutolinkingHints( in object: URL ) throws -> [ Linkage ] {
179
+ let data = try Data ( contentsOf: object, options: . mappedIfSafe)
180
+ assert ( data. starts ( with: [ 0xcf , 0xfa , 0xed , 0xfe ] ) , " Not a mach object file? " )
181
+ return data. withUnsafeBytes { ( buf: UnsafeRawBufferPointer ) -> [ Linkage ] in
182
+ var result = [ Linkage] ( )
183
+ guard let base = buf. baseAddress else {
184
+ return [ ]
40
185
}
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
- ] )
186
+
187
+ let hdr = base. bindMemory ( to: MachHeader . self, capacity: 1 )
188
+
189
+ var commandStart = base + MemoryLayout< MachHeader> . size
190
+ for _ in 0 ..< Int ( hdr. pointee. ncmds) {
191
+ let command = commandStart. load ( as: LoadCommand . self)
192
+ defer {
193
+ commandStart = commandStart. advanced ( by: Int ( command. cmdsize) )
194
+ }
195
+
196
+ switch command. cmd {
197
+ case . linkerOption:
198
+ let ( namePtr, cmdCount) = commandStart. withMemoryRebound ( to: LinkerOptionCommand . self, capacity: 1 , { cmd in
199
+ return cmd. withMemoryRebound ( to: CChar . self, capacity: 1 ) { p in
200
+ return ( p. advanced ( by: MemoryLayout< LinkerOptionCommand> . size) , Int ( cmd. pointee. count) )
201
+ }
202
+ } )
203
+ if cmdCount == 1 {
204
+ result. append ( . library( String ( cString: namePtr) ) )
205
+ } else if cmdCount == 2 {
206
+ guard String ( cString: namePtr) == " -framework " else {
207
+ continue
208
+ }
209
+
210
+ let frameworkName = String ( cString: namePtr. advanced ( by: " -framework " . utf8. count + 1 ) )
211
+ result. append ( . framework( frameworkName) )
212
+ } else {
213
+ XCTFail ( " Unexpected number of linker options: \( cmdCount) " )
214
+ }
215
+ default :
216
+ continue
217
+ }
51
218
}
219
+ return result
52
220
}
53
221
}
222
+ }
223
+ #endif
54
224
55
- private enum Linkage : Comparable {
56
- case library( String )
57
- case framework( String )
225
+ fileprivate enum Linkage : Comparable , Hashable {
226
+ case library( String )
227
+ case framework( String )
228
+
229
+ var linkage : String {
230
+ switch self {
231
+ case . library( let s) : return s
232
+ case . framework( let s) : return s
233
+ }
58
234
}
59
235
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)
236
+ func hasPrefix( _ prefix: String ) -> Bool {
237
+ return self . linkage. hasPrefix ( prefix)
76
238
}
239
+ }
77
240
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
- }
241
+ extension Linkage {
242
+ fileprivate struct Assertion {
243
+ var linkage : Linkage
244
+ var condition : Condition ?
245
+ var file : StaticString
246
+ var line : UInt
85
247
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
- }
248
+ func matches( _ linkage: Linkage ) -> Bool {
249
+ return self . linkage == linkage
250
+ }
94
251
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
- }
252
+ static func library( _ linkage: String , condition: Condition ? = nil , file: StaticString = #file, line: UInt = #line) -> Assertion {
253
+ return Linkage . Assertion ( linkage: . library( linkage) , condition: condition, file: file, line: line)
254
+ }
101
255
102
- if count == 1 {
103
- guard let library = lines . next ( ) else {
104
- XCTFail ( " No load command payload: \( line ) " )
105
- return
106
- }
256
+ static func framework ( _ linkage : String , condition : Condition ? = nil , file : StaticString = #file , line : UInt = #line ) -> Assertion {
257
+ return Linkage . Assertion ( linkage : . framework ( linkage ) , condition : condition , file : file , line : line )
258
+ }
259
+ }
260
+ }
107
261
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
- }
262
+ extension Linkage . Assertion {
263
+ fileprivate enum Condition {
264
+ case swiftVersionAtLeast( versionBound: SwiftVersion )
265
+ case configuration( ProductConfiguration )
266
+ case flaky
121
267
122
- let linkedFramework = framework. trimmingCharacters ( in: . whitespaces)
123
- . suffix ( from: framework. index ( framework. startIndex, offsetBy: " string #2 " . count) )
124
- linkages. append ( . framework( String ( linkedFramework) ) )
268
+ enum SwiftVersion : Comparable {
269
+ case v5_5
270
+ case v5_6
271
+ case v5_7
272
+ // We don't support compiling with <=5.4
273
+ }
274
+
275
+ enum ProductConfiguration : Equatable {
276
+ case debug
277
+ case release
278
+ }
279
+
280
+ fileprivate static func when( swiftVersionAtLeast version: SwiftVersion ) -> Condition {
281
+ return . swiftVersionAtLeast( versionBound: version)
282
+ }
283
+
284
+ fileprivate static func when( configuration: ProductConfiguration ) -> Condition {
285
+ return . configuration( configuration)
286
+ }
287
+
288
+ fileprivate static func mayBeAbsent( _ reason: StaticString ) -> Condition {
289
+ return . flaky
290
+ }
291
+
292
+ fileprivate func evaluate( ) -> Bool {
293
+ switch self {
294
+ case let . swiftVersionAtLeast( versionBound: bound) :
295
+ #if swift(>=5.7)
296
+ let version : SwiftVersion = . v5_7
297
+ #elseif swift(>=5.6)
298
+ let version : SwiftVersion = . v5_6
299
+ #elseif swift(>=5.5)
300
+ let version : SwiftVersion = . v5_5
301
+ #else
302
+ #error("Swift version is too old!")
303
+ #endif
304
+ return version >= bound
305
+ case let . configuration( expectation) :
306
+ #if DEBUG
307
+ let config : ProductConfiguration = . debug
308
+ #else
309
+ let config : ProductConfiguration = . release
310
+ #endif
311
+ return config == expectation
312
+ case . flaky:
313
+ return true
125
314
}
126
315
}
127
- return try assertion ( linkages. sorted ( ) )
128
316
}
129
317
}
130
- #endif
0 commit comments