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