atllbuild.swift 19.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13
// Copyright (c) 2016 Anarchy Tools Contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Drew's avatar
Drew committed
14

Drew's avatar
Drew committed
15
import Foundation
16
import atpkg
Drew's avatar
Drew committed
17

Drew's avatar
Drew committed
18 19 20 21
#if os(Linux)
    import Glibc //need sleep
#endif

Drew's avatar
Drew committed
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
/**Synthesize a module map.
- parameter name: The name of the module to synthesize
- parameter umbrellaHeader: A path to the umbrella header.  The path must be relative to the exported module map file.
- returns String contents of the synthesized modulemap
*/
private func synthesizeModuleMap(name: String, umbrellaHeader: String?) -> String {
    var s = ""
    s += "module \(name) {\n"
    if let u = umbrellaHeader {
        s += "  umbrella header \"\(u)\"\n"
    }
    s += "\n"
    s += "}\n"
    return s
}

Drew's avatar
Drew committed
38 39
/**The ATllbuild tool builds a swift module via llbuild.
For more information on this tool, see `docs/attllbuild.md` */
Drew's avatar
Drew committed
40 41
final class ATllbuild : Tool {
    
42 43 44 45 46 47 48 49 50
    /**We inject this sourcefile in xctestify=true on OSX
    On Linux, the API requires you to explicitly list tests
    which is not required on OSX.  Injecting this file into test targets
    will enforce that API on OSX as well */
    private static let xcTestCaseProvider: String = { () -> String in
        var s = ""
        s += "import XCTest\n"
        s += "\n"
        s += "protocol XCTestCaseProvider {\n"
51
        s += "    var allTests : [(String, () throws -> Void)] { get }\n"
52 53 54 55 56 57 58 59 60 61
        s += "}\n"
        s += "\n"
        s += "public func XCTMain(testCases: [XCTestCase]) {\n"
        s += "    fatalError(\"Can't get here.\")\n"
        s += "}\n"
        s += "\n"
        s += "extension XCTestCase {\n"
        s += "    private func implementAllTests() {\n"
        s += "        print(\"Make sure to implement allTests via\")\n"
        s += "        print(\"extension \\(self.dynamicType) : XCTestCaseProvider {\")\n"
62
        s += "        print(\"    var allTests : [(String, () throws -> Void)] {\")\n"
63 64
        s += "        print(\"        return [\")\n"
        s += "        print(\"        (\\\"testFoo\\\", testFoo)\")\n"
Drew's avatar
Drew committed
65
        s += "        print(\"        ]\")\n"
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
        s += "        print(\"    }\")\n"
        s += "        print(\"}\")\n"
        s += "        print(\"(Or disable xctestStrict.)\")\n"
        s += "        print(\"Cheers! -- Anarchy Tools Team\")\n"
        s += "    }\n"
        s += "    override public func tearDown() {\n"
        s += "        if let provider = self as? XCTestCaseProvider {\n"
        s += "            let contains = provider.allTests.contains({ test in\n"
        s += "                return test.0 == invocation!.selector.description\n"
        s += "            })\n"
        s += "            if !contains {\n"
        s += "               XCTFail(\"Test \\(name) is missing from \\(self.dynamicType)\")\n"
        s += "               implementAllTests()\n"
        s += "            }\n"
        s += "        }\n"
        s += "        else {\n"
        s += "            XCTFail(\"Whoops!  \\(self.dynamicType) doesn't conform to XCTestCaseProvider.\")\n"
        s += "            implementAllTests()\n"
        s += "        }\n"
        s += "        super.tearDown()\n"
        s += "    }\n"
        s += "}\n"
        s += "\n"
        return s
    }()
    
92 93 94 95
    enum OutputType {
        case Executable
        case StaticLibrary
    }
Drew's avatar
Drew committed
96 97 98 99 100

    enum ModuleMapType {
        case None
        case Synthesized
    }
101
    
102 103 104 105 106 107 108
    /**
     * Calculates the llbuild.yaml contents for the given configuration options
     *   - parameter sources: A resolved list of swift sources
     *   - parameter workdir: A temporary working directory for `atllbuild` to use
     *   - parameter modulename: The name of the module to be built.
     *   - returns: The string contents for llbuild.yaml suitable for processing by swift-build-tool
     */
Drew's avatar
Drew committed
109
    func llbuildyaml(sources: [String], workdir: String, modulename: String, linkSDK: Bool, compileOptions: [String], linkOptions: [String], outputType: OutputType, linkWithProduct:[String], swiftCPath: String) -> String {
110
        let productPath = workdir + "products/"
Drew's avatar
Drew committed
111 112
        //this format is largely undocumented, but I reverse-engineered it from SwiftPM.
        var yaml = "client:\n  name: swift-build\n\n"
Drew's avatar
Drew committed
113 114 115 116
        
        yaml += "tools: {}\n\n"

        
Drew's avatar
Drew committed
117
        yaml += "targets:\n"
Drew's avatar
Drew committed
118 119 120
        yaml += "  \"\": [<atllbuild>]\n"
        yaml += "  atllbuild: [<atllbuild>]\n"
        
Drew's avatar
Drew committed
121
        //this is the "compile" command
Drew's avatar
Drew committed
122 123 124 125
        
        yaml += "commands:\n"
        yaml += "  <atllbuild-swiftc>:\n"
        yaml += "     tool: swift-compiler\n"
Drew's avatar
Drew committed
126
        yaml += "     executable: \"\(swiftCPath)\"\n"
Drew's avatar
Drew committed
127 128 129 130 131
        yaml += "     inputs: \(sources)\n"
        yaml += "     sources: \(sources)\n"
        
        //swiftPM wants "objects" which is just a list of %.swift.o files.  We have to put them in a temp directory though.
        let objects = sources.map { (source) -> String in
Drew's avatar
Drew committed
132
            workdir + "objects/" + source.toNSString.lastPathComponent + ".o"
Drew's avatar
Drew committed
133 134
        }
        yaml += "     objects: \(objects)\n"
Drew's avatar
Drew committed
135 136 137 138
        //this crazy syntax is how llbuild specifies outputs
        var llbuild_outputs = ["<atllbuild-swiftc>"]
        llbuild_outputs.appendContentsOf(objects)
        yaml += "     outputs: \(llbuild_outputs)\n"
139 140 141 142 143 144 145
        
        switch(outputType) {
        case .Executable:
            break
        case .StaticLibrary:
            yaml += "     is-library: true\n" //I have no idea what the effect of this is, but swiftPM does it, so I'm including it.
        }
Drew's avatar
Drew committed
146 147
        
        yaml += "     module-name: \(modulename)\n"
148 149
        let swiftModulePath = "\(productPath + modulename).swiftmodule"
        yaml += "     module-output-path: \(swiftModulePath)\n"
Drew's avatar
Drew committed
150 151 152
        yaml += "     temps-path: \(workdir)/llbuildtmp\n"
        
        var args : [String] = []
Drew's avatar
Drew committed
153
        args.appendContentsOf(["-j8", "-D","ATBUILD","-I",workdir+"products/"])
154 155
        
        if linkSDK {
Drew's avatar
Drew committed
156
            #if os(OSX) //we don't have SDKPath on linux
157
            args.appendContentsOf(["-sdk", SDKPath])
Drew's avatar
Drew committed
158
            #endif
159
        }
Drew's avatar
Drew committed
160
        args.appendContentsOf(compileOptions)
Drew's avatar
Drew committed
161 162
        
        yaml += "     other-args: \(args)\n"
Drew's avatar
Drew committed
163 164 165
        
        //and this is the "link" command
        yaml += "  <atllbuild>:\n"
166 167 168 169 170 171
        switch(outputType) {
        case .Executable:
            yaml += "    tool: shell\n"
            //this crazy syntax is how sbt declares a dependency
            var llbuild_inputs = ["<atllbuild-swiftc>"]
            llbuild_inputs.appendContentsOf(objects)
Drew's avatar
Drew committed
172 173
            let builtProducts = linkWithProduct.map {workdir+"products/"+$0}
            llbuild_inputs.appendContentsOf(builtProducts)
174
            let executablePath = productPath+modulename
175
            yaml += "    inputs: \(llbuild_inputs)\n"
176
            yaml += "    outputs: [\"<atllbuild>\", \"\(executablePath)\"]\n"
177
            //and now we have the crazy 'args'
Drew's avatar
Drew committed
178
            args = [swiftCPath, "-o",executablePath]
179
            args.appendContentsOf(objects)
Drew's avatar
Drew committed
180
            args.appendContentsOf(builtProducts)
Drew's avatar
Drew committed
181
            args.appendContentsOf(linkOptions)
182
            yaml += "    args: \(args)\n"
183 184
            yaml += "    description: Linking executable \(executablePath)\n"
            return yaml
Drew's avatar
Drew committed
185

186 187 188 189 190 191
        
        case .StaticLibrary:
            yaml += "    tool: shell\n"
            var llbuild_inputs = ["<atllbuild-swiftc>"]
            llbuild_inputs.appendContentsOf(objects)
            yaml += "    inputs: \(llbuild_inputs)\n"
192
            let libPath = productPath + modulename + ".a"
193 194 195 196 197 198 199 200 201 202
            yaml += "    outputs: [\"<atllbuild>\", \"\(libPath)\"]\n"
            
            //build the crazy args, mostly consisting of an `ar` shell command
            var shellCmd = "rm -rf \(libPath); ar cr '\(libPath)'"
            for obj in objects {
                shellCmd += " '\(obj)'"
            }
            let args = "[\"/bin/sh\",\"-c\",\(shellCmd)]"
            yaml += "    args: \(args)\n"
            yaml += "    description: \"Linking Library:  \(libPath)\""
203
            return yaml
204
        }
205
     }
Drew's avatar
Drew committed
206
    
Drew's avatar
Drew committed
207 208 209 210
    private enum Options: String {
        case Tool = "tool"
        case Name = "name"
        case Dependencies = "dependencies"
211 212 213
        case OutputType = "output-type"
        case Source = "sources"
        case BootstrapOnly = "bootstrap-only"
Drew's avatar
Drew committed
214
        case llBuildYaml = "llbuildyaml"
215 216 217 218 219
        case CompileOptions = "compile-options"
        case LinkOptions = "link-options"
        case LinkSDK = "link-sdk"
        case LinkWithProduct = "link-with"
        case SwiftCPath = "swiftc-path"
Drew's avatar
Drew committed
220
        case XCTestify = "xctestify"
221 222
        case XCTestStrict = "xctest-strict"
        case PublishProduct = "publish-product"
223
        case UmbrellaHeader = "umbrella-header"
Drew's avatar
Drew committed
224
        case ModuleMap = "module-map"
225

Drew's avatar
Drew committed
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
        
        static var allOptions : [Options] {
            return [
                Tool,
                Name,
                Dependencies,
                OutputType,
                Source,
                BootstrapOnly,
                llBuildYaml,
                CompileOptions,
                LinkOptions,
                LinkSDK,
                LinkWithProduct,
                SwiftCPath,
                XCTestify,
                XCTestStrict,
243
                PublishProduct,
Drew's avatar
Drew committed
244 245
                UmbrellaHeader,
                ModuleMap
Drew's avatar
Drew committed
246 247 248 249
            ]
        }
    }
    
250
    func run(task: Task) {
251 252 253
        
        //warn if we don't understand an option
        for key in task.allKeys {
Drew's avatar
Drew committed
254
            if !Options.allOptions.map({$0.rawValue}).contains(key) {
Drew's avatar
Drew committed
255
                print("Warning: unknown option \(key) for task \(task.qualifiedName)")
256 257 258
            }
        }
        
Drew's avatar
Drew committed
259 260 261
        //create the working directory
        let workDirectory = ".atllbuild/"
        let manager = NSFileManager.defaultManager()
Drew's avatar
Drew committed
262 263 264 265 266 267 268
        
        //NSFileManager is pretty anal about throwing errors if we try to remove something that doesn't exist, etc.
        //We just want to create a state where .atllbuild/objects and .atllbuild/llbuildtmp and .atllbuild/products exists.
        //and in particular, without erasing the product directory, since that accumulates build products across
        //multiple invocations of atllbuild.
        let _ = try? manager.removeItemAtPath(workDirectory + "/objects")
        let _ = try? manager.removeItemAtPath(workDirectory + "/llbuildtmp")
269 270
        let _ = try? manager.removeItemAtPath(workDirectory + "/include")

Drew's avatar
Drew committed
271 272
        let _ = try? manager.createDirectoryAtPath(workDirectory, withIntermediateDirectories: false, attributes: nil)
        let _ = try? manager.createDirectoryAtPath(workDirectory + "/products", withIntermediateDirectories: false, attributes: nil)
273
        let _ = try? manager.createDirectoryAtPath(workDirectory + "/objects", withIntermediateDirectories: false, attributes: nil)
274
        let _ = try? manager.createDirectoryAtPath(workDirectory + "/include", withIntermediateDirectories: false, attributes: nil)
Drew's avatar
Drew committed
275

Drew's avatar
Drew committed
276
        //parse arguments
Drew's avatar
Drew committed
277
        var linkWithProduct: [String] = []
Drew's avatar
Drew committed
278
        if let arr = task[Options.LinkWithProduct.rawValue]?.vector {
Drew's avatar
Drew committed
279
            for product in arr {
280
                guard let p = product.string else { fatalError("non-string product \(product)") }
Drew's avatar
Drew committed
281 282 283
                linkWithProduct.append(p)
            }
        }
284
        let outputType: OutputType
Drew's avatar
Drew committed
285
        if task[Options.OutputType.rawValue]?.string == "static-library" {
286 287
            outputType = .StaticLibrary
        }
Drew's avatar
Drew committed
288
        else if task[Options.OutputType.rawValue]?.string == "executable" {
289 290 291
            outputType = .Executable
        }
        else {
292
            fatalError("Unknown \(Options.OutputType.rawValue) \(task["outputType"])")
293 294
        }
        
Drew's avatar
Drew committed
295
        var compileOptions: [String] = []
Drew's avatar
Drew committed
296
        if let opts = task[Options.CompileOptions.rawValue]?.vector {
Drew's avatar
Drew committed
297
            for o in opts {
298
                guard let os = o.string else { fatalError("Compile option \(o) is not a string") }
Drew's avatar
Drew committed
299 300 301
                compileOptions.append(os)
            }
        }
Drew's avatar
Drew committed
302
        var linkOptions: [String] = []
Drew's avatar
Drew committed
303
        if let opts = task[Options.LinkOptions.rawValue]?.vector {
Drew's avatar
Drew committed
304 305 306 307 308
            for o in opts {
                guard let os = o.string else { fatalError("Link option \(o) is not a string") }
                linkOptions.append(os)
            }
        }
Drew's avatar
Drew committed
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327

        //check for modulemaps
        for product in linkWithProduct {
            let productName = product.componentsSeparatedByString(".")[0]
            let moduleMapPath = workDirectory + "/products/\(productName).modulemap"
            if manager.fileExistsAtPath(moduleMapPath) {
                        print("a")

                /*per http://clang.llvm.org/docs/Modules.html#command-line-parameters, pretty much
                the only way to do this is to create a file called `module.modulemap`.  That
                potentially conflicts with other modulemaps, so we give it its own directory, namespaced
                by the product name. */
                let pathName = workDirectory + "/include/\(productName)"
                try! manager.createDirectoryAtPath(pathName, withIntermediateDirectories:false, attributes: nil)
                try! manager.copyItemAtPath_SWIFTBUG(moduleMapPath, toPath: pathName + "/module.modulemap")
                compileOptions.appendContentsOf(["-I",pathName])
            }
        }

Drew's avatar
Drew committed
328
        guard let sourceDescriptions = task[Options.Source.rawValue]?.vector?.flatMap({$0.string}) else { fatalError("Can't find sources for atllbuild.") }
Drew's avatar
Drew committed
329
        var sources = collectSources(sourceDescriptions, taskForCalculatingPath: task)
330
        
Drew's avatar
Drew committed
331
        //xctestify
Drew's avatar
Drew committed
332
        if task[Options.XCTestify.rawValue]?.bool == true {
333
            precondition(outputType == .Executable, "You must use :\(Options.OutputType.rawValue) executable with xctestify.")
Drew's avatar
Drew committed
334 335
            //inject platform-specific flags
            #if os(OSX)
336 337 338 339
                compileOptions.appendContentsOf(["-F", "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks/"])
                linkOptions.appendContentsOf(["-F", "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks/", "-target", "x86_64-apple-macosx10.11", "-Xlinker", "-rpath", "-Xlinker", "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks/", "-Xlinker", "-bundle"])
            #endif
        }
Drew's avatar
Drew committed
340
        if task[Options.XCTestStrict.rawValue]?.bool == true {
341 342 343 344 345 346 347 348
            #if os(OSX)
            //inject XCTestCaseProvider.swift
            var xcTestCaseProviderPath = "/tmp/XXXXXXX"
            var template = xcTestCaseProviderPath.cStringUsingEncoding(NSUTF8StringEncoding)!
            xcTestCaseProviderPath = String(CString: mkdtemp(&template), encoding: NSUTF8StringEncoding)!
            xcTestCaseProviderPath += "/XCTestCaseProvider.swift"
            try! ATllbuild.xcTestCaseProvider.writeToFile(xcTestCaseProviderPath, atomically: false, encoding: NSUTF8StringEncoding)
            sources.append(xcTestCaseProviderPath)
Drew's avatar
Drew committed
349 350
            #endif
        }
Drew's avatar
Drew committed
351 352 353 354 355 356 357
        let moduleMap: ModuleMapType
        if task[Options.ModuleMap.rawValue]?.string == "synthesized" {
            moduleMap = .Synthesized
        }
        else {
            moduleMap = .None
        }
Drew's avatar
Drew committed
358

Drew's avatar
Drew committed
359
        guard let name = task[Options.Name.rawValue]?.string else { fatalError("No name for atllbuild task") }
Drew's avatar
Drew committed
360
        
361
        if let umbrellaHeader = task[Options.UmbrellaHeader.rawValue]?.string {
Drew's avatar
Drew committed
362 363
            precondition(moduleMap == .Synthesized, ":\(Options.UmbrellaHeader.rawValue) \"synthesized\" must be used with the \(Options.UmbrellaHeader.rawValue) option")
            let s = synthesizeModuleMap(name, umbrellaHeader: "Umbrella.h")
364
            try! s.writeToFile(workDirectory+"/include/module.modulemap", atomically: false, encoding: NSUTF8StringEncoding)
Drew's avatar
Drew committed
365
            try! manager.copyItemAtPath_SWIFTBUG(task.importedPath + umbrellaHeader, toPath: workDirectory + "/include/Umbrella.h")
366 367 368 369 370
            compileOptions.append("-I")
            compileOptions.append(workDirectory + "/include/")
            compileOptions.append("-import-underlying-module")
        }
        
Drew's avatar
Drew committed
371
        let bootstrapOnly: Bool
Drew's avatar
Drew committed
372

Drew's avatar
Drew committed
373
        if task[Options.BootstrapOnly.rawValue]?.bool == true {
Drew's avatar
Drew committed
374 375 376 377 378 379
            bootstrapOnly = true
        }
        else {
            bootstrapOnly = false
        }
        
380
        let sdk: Bool
Drew's avatar
Drew committed
381
        if task[Options.LinkSDK.rawValue]?.bool == false {
382 383 384 385
            sdk = false
        }
        else { sdk = true }
        
Drew's avatar
Drew committed
386
        let llbuildyamlpath : String
Drew's avatar
Drew committed
387

Drew's avatar
Drew committed
388
        if let value = task[Options.llBuildYaml.rawValue]?.string {
389
            llbuildyamlpath = value
Drew's avatar
Drew committed
390 391 392 393
        }
        else {
            llbuildyamlpath = workDirectory + "llbuild.yaml"
        }
Drew's avatar
Drew committed
394 395

        let swiftCPath: String
Drew's avatar
Drew committed
396
        if let c = task[Options.SwiftCPath.rawValue]?.string {
Drew's avatar
Drew committed
397 398 399 400 401
            swiftCPath = c
        }
        else {
            swiftCPath = SwiftCPath
        }
Drew's avatar
Drew committed
402
        
Drew's avatar
Drew committed
403
        let yaml = llbuildyaml(sources, workdir: workDirectory, modulename: name, linkSDK: sdk, compileOptions: compileOptions, linkOptions: linkOptions, outputType: outputType, linkWithProduct: linkWithProduct, swiftCPath: swiftCPath)
404
        let _ = try? yaml.writeToFile(llbuildyamlpath, atomically: false, encoding: NSUTF8StringEncoding)
Drew's avatar
Drew committed
405
        if bootstrapOnly { return }
Drew's avatar
Drew committed
406 407 408 409 410 411 412 413

        switch moduleMap {
                case .None:
                break
                case .Synthesized:
                let s = synthesizeModuleMap(name, umbrellaHeader: nil)
                try! s.writeToFile(workDirectory + "/products/\(name).modulemap", atomically: false, encoding: NSUTF8StringEncoding)
        }
Drew's avatar
Drew committed
414
        
Drew's avatar
Drew committed
415 416 417 418
        //SR-566
        let cmd = "\(SwiftBuildToolpath) -f \(llbuildyamlpath)"
        if system(cmd) != 0 {
            fatalError(cmd)
Drew's avatar
Drew committed
419
        }
Drew's avatar
Drew committed
420
        if task[Options.PublishProduct.rawValue]?.bool == true {
Drew's avatar
Drew committed
421 422 423 424 425 426 427 428 429 430
            if !manager.fileExistsAtPath("bin") {
                try! manager.createDirectoryAtPath("bin", withIntermediateDirectories: false, attributes: nil)
            }
            try! copyByOverwriting("\(workDirectory)/products/\(name).swiftmodule", toPath: "bin/\(name).swiftmodule")
            switch outputType {
            case .Executable:
                try! copyByOverwriting("\(workDirectory)/products/\(name)", toPath: "bin/\(name)")
            case .StaticLibrary:
                try! copyByOverwriting("\(workDirectory)/products/\(name).a", toPath: "bin/\(name).a")
            }
Drew's avatar
Drew committed
431 432 433 434 435 436 437 438

            switch moduleMap {
                case .None:
                break
                case .Synthesized:
                try! copyByOverwriting("\(workDirectory)/products/\(name).modulemap", toPath: "bin/\(name).modulemap")
            }

Drew's avatar
Drew committed
439 440 441 442 443 444 445 446
        }
    }
}

private func copyByOverwriting(fromPath: String, toPath: String) throws {
    let manager = NSFileManager.defaultManager()
    if manager.fileExistsAtPath(toPath) {
        try manager.removeItemAtPath(toPath)
Drew's avatar
Drew committed
447
    }
448
    try! manager.copyItemAtPath_SWIFTBUG(fromPath, toPath: toPath)
Drew's avatar
Drew committed
449
}