atllbuild.swift 15.8 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
/**The ATllbuild tool builds a swift module via llbuild.
For more information on this tool, see `docs/attllbuild.md` */
Drew's avatar
Drew committed
24 25
final class ATllbuild : Tool {
    
26 27 28 29 30 31 32 33 34
    /**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"
35
        s += "    var allTests : [(String, () throws -> Void)] { get }\n"
36 37 38 39 40 41 42 43 44 45
        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"
46
        s += "        print(\"    var allTests : [(String, () throws -> Void)] {\")\n"
47 48
        s += "        print(\"        return [\")\n"
        s += "        print(\"        (\\\"testFoo\\\", testFoo)\")\n"
Drew's avatar
Drew committed
49
        s += "        print(\"        ]\")\n"
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
        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
    }()
    
76 77 78 79 80
    enum OutputType {
        case Executable
        case StaticLibrary
    }
    
81 82 83 84 85 86 87
    /**
     * 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
88
    func llbuildyaml(sources: [String], workdir: String, modulename: String, linkSDK: Bool, compileOptions: [String], linkOptions: [String], outputType: OutputType, linkWithProduct:[String], swiftCPath: String) -> String {
89
        let productPath = workdir + "products/"
Drew's avatar
Drew committed
90 91
        //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
92 93 94 95
        
        yaml += "tools: {}\n\n"

        
Drew's avatar
Drew committed
96
        yaml += "targets:\n"
Drew's avatar
Drew committed
97 98 99
        yaml += "  \"\": [<atllbuild>]\n"
        yaml += "  atllbuild: [<atllbuild>]\n"
        
Drew's avatar
Drew committed
100
        //this is the "compile" command
Drew's avatar
Drew committed
101 102 103 104
        
        yaml += "commands:\n"
        yaml += "  <atllbuild-swiftc>:\n"
        yaml += "     tool: swift-compiler\n"
Drew's avatar
Drew committed
105
        yaml += "     executable: \"\(swiftCPath)\"\n"
Drew's avatar
Drew committed
106 107 108 109 110
        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
111
            workdir + "objects/" + source.toNSString.lastPathComponent + ".o"
Drew's avatar
Drew committed
112 113
        }
        yaml += "     objects: \(objects)\n"
Drew's avatar
Drew committed
114 115 116 117
        //this crazy syntax is how llbuild specifies outputs
        var llbuild_outputs = ["<atllbuild-swiftc>"]
        llbuild_outputs.appendContentsOf(objects)
        yaml += "     outputs: \(llbuild_outputs)\n"
118 119 120 121 122 123 124
        
        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
125 126
        
        yaml += "     module-name: \(modulename)\n"
127 128
        let swiftModulePath = "\(productPath + modulename).swiftmodule"
        yaml += "     module-output-path: \(swiftModulePath)\n"
Drew's avatar
Drew committed
129 130 131
        yaml += "     temps-path: \(workdir)/llbuildtmp\n"
        
        var args : [String] = []
Drew's avatar
Drew committed
132
        args.appendContentsOf(["-j8", "-D","ATBUILD","-I",workdir+"products/"])
133 134
        
        if linkSDK {
Drew's avatar
Drew committed
135
            #if os(OSX) //we don't have SDKPath on linux
136
            args.appendContentsOf(["-sdk", SDKPath])
Drew's avatar
Drew committed
137
            #endif
138
        }
Drew's avatar
Drew committed
139
        args.appendContentsOf(compileOptions)
Drew's avatar
Drew committed
140 141
        
        yaml += "     other-args: \(args)\n"
Drew's avatar
Drew committed
142 143 144
        
        //and this is the "link" command
        yaml += "  <atllbuild>:\n"
145 146 147 148 149 150
        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
151 152
            let builtProducts = linkWithProduct.map {workdir+"products/"+$0}
            llbuild_inputs.appendContentsOf(builtProducts)
153
            let executablePath = productPath+modulename
154
            yaml += "    inputs: \(llbuild_inputs)\n"
155
            yaml += "    outputs: [\"<atllbuild>\", \"\(executablePath)\"]\n"
156
            //and now we have the crazy 'args'
Drew's avatar
Drew committed
157
            args = [swiftCPath, "-o",executablePath]
158
            args.appendContentsOf(objects)
Drew's avatar
Drew committed
159
            args.appendContentsOf(builtProducts)
Drew's avatar
Drew committed
160
            args.appendContentsOf(linkOptions)
161
            yaml += "    args: \(args)\n"
162 163
            yaml += "    description: Linking executable \(executablePath)\n"
            return yaml
Drew's avatar
Drew committed
164

165 166 167 168 169 170
        
        case .StaticLibrary:
            yaml += "    tool: shell\n"
            var llbuild_inputs = ["<atllbuild-swiftc>"]
            llbuild_inputs.appendContentsOf(objects)
            yaml += "    inputs: \(llbuild_inputs)\n"
171
            let libPath = productPath + modulename + ".a"
172 173 174 175 176 177 178 179 180 181
            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)\""
182
            return yaml
183
        }
184
     }
Drew's avatar
Drew committed
185
    
Drew's avatar
Drew committed
186 187 188 189
    private enum Options: String {
        case Tool = "tool"
        case Name = "name"
        case Dependencies = "dependencies"
190 191 192
        case OutputType = "output-type"
        case Source = "sources"
        case BootstrapOnly = "bootstrap-only"
Drew's avatar
Drew committed
193
        case llBuildYaml = "llbuildyaml"
194 195 196 197 198
        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
199
        case XCTestify = "xctestify"
200 201
        case XCTestStrict = "xctest-strict"
        case PublishProduct = "publish-product"
Drew's avatar
Drew committed
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
        
        static var allOptions : [Options] {
            return [
                Name,
                Dependencies,
                OutputType,
                Source,
                BootstrapOnly,
                llBuildYaml,
                CompileOptions,
                LinkOptions,
                LinkSDK,
                LinkWithProduct,
                SwiftCPath,
                XCTestify,
                XCTestStrict,
                PublishProduct
            ]
        }
    }
    
223
    func run(task: Task) {
224 225
        
        //warn if we don't understand an option
226 227 228 229
        var knownOptions = Options.allOptions.map({$0.rawValue})
        for option in Task.Option.allOptions.map({$0.rawValue}) {
            knownOptions.append(option)
        }
230
        for key in task.allKeys {
231
            if !knownOptions.contains(key) {
Drew's avatar
Drew committed
232
                print("Warning: unknown option \(key) for task \(task.qualifiedName)")
233 234 235
            }
        }
        
Drew's avatar
Drew committed
236 237 238
        //create the working directory
        let workDirectory = ".atllbuild/"
        let manager = NSFileManager.defaultManager()
Drew's avatar
Drew committed
239 240 241 242 243 244 245 246 247
        
        //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")
        let _ = try? manager.createDirectoryAtPath(workDirectory, withIntermediateDirectories: false, attributes: nil)
        let _ = try? manager.createDirectoryAtPath(workDirectory + "/products", withIntermediateDirectories: false, attributes: nil)
248
        let _ = try? manager.createDirectoryAtPath(workDirectory + "/objects", withIntermediateDirectories: false, attributes: nil)
Drew's avatar
Drew committed
249

Drew's avatar
Drew committed
250
        //parse arguments
Drew's avatar
Drew committed
251
        var linkWithProduct: [String] = []
Drew's avatar
Drew committed
252
        if let arr = task[Options.LinkWithProduct.rawValue]?.vector {
Drew's avatar
Drew committed
253
            for product in arr {
254
                guard let p = product.string else { fatalError("non-string product \(product)") }
Drew's avatar
Drew committed
255 256 257
                linkWithProduct.append(p)
            }
        }
258
        let outputType: OutputType
Drew's avatar
Drew committed
259
        if task[Options.OutputType.rawValue]?.string == "static-library" {
260 261
            outputType = .StaticLibrary
        }
Drew's avatar
Drew committed
262
        else if task[Options.OutputType.rawValue]?.string == "executable" {
263 264 265
            outputType = .Executable
        }
        else {
266
            fatalError("Unknown \(Options.OutputType.rawValue) \(task["outputType"])")
267 268
        }
        
Drew's avatar
Drew committed
269
        var compileOptions: [String] = []
Drew's avatar
Drew committed
270
        if let opts = task[Options.CompileOptions.rawValue]?.vector {
Drew's avatar
Drew committed
271
            for o in opts {
272
                guard let os = o.string else { fatalError("Compile option \(o) is not a string") }
Drew's avatar
Drew committed
273 274 275
                compileOptions.append(os)
            }
        }
Drew's avatar
Drew committed
276
        var linkOptions: [String] = []
Drew's avatar
Drew committed
277
        if let opts = task[Options.LinkOptions.rawValue]?.vector {
Drew's avatar
Drew committed
278 279 280 281 282
            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
283
        
Drew's avatar
Drew committed
284
        guard let sourceDescriptions = task[Options.Source.rawValue]?.vector?.flatMap({$0.string}) else { fatalError("Can't find sources for atllbuild.") }
Drew's avatar
Drew committed
285
        var sources = collectSources(sourceDescriptions, taskForCalculatingPath: task)
286
        
Drew's avatar
Drew committed
287
        //xctestify
Drew's avatar
Drew committed
288
        if task[Options.XCTestify.rawValue]?.bool == true {
289
            precondition(outputType == .Executable, "You must use :\(Options.OutputType.rawValue) executable with xctestify.")
Drew's avatar
Drew committed
290 291
            //inject platform-specific flags
            #if os(OSX)
292 293 294 295
                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
296
        if task[Options.XCTestStrict.rawValue]?.bool == true {
297 298 299 300 301 302 303 304
            #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
305 306
            #endif
        }
Drew's avatar
Drew committed
307

Drew's avatar
Drew committed
308
        guard let name = task[Options.Name.rawValue]?.string else { fatalError("No name for atllbuild task") }
Drew's avatar
Drew committed
309 310
        
        let bootstrapOnly: Bool
Drew's avatar
Drew committed
311

Drew's avatar
Drew committed
312
        if task[Options.BootstrapOnly.rawValue]?.bool == true {
Drew's avatar
Drew committed
313 314 315 316 317 318
            bootstrapOnly = true
        }
        else {
            bootstrapOnly = false
        }
        
319
        let sdk: Bool
Drew's avatar
Drew committed
320
        if task[Options.LinkSDK.rawValue]?.bool == false {
321 322 323 324
            sdk = false
        }
        else { sdk = true }
        
Drew's avatar
Drew committed
325
        let llbuildyamlpath : String
Drew's avatar
Drew committed
326

Drew's avatar
Drew committed
327
        if let value = task[Options.llBuildYaml.rawValue]?.string {
328
            llbuildyamlpath = value
Drew's avatar
Drew committed
329 330 331 332
        }
        else {
            llbuildyamlpath = workDirectory + "llbuild.yaml"
        }
Drew's avatar
Drew committed
333 334

        let swiftCPath: String
Drew's avatar
Drew committed
335
        if let c = task[Options.SwiftCPath.rawValue]?.string {
Drew's avatar
Drew committed
336 337 338 339 340
            swiftCPath = c
        }
        else {
            swiftCPath = SwiftCPath
        }
Drew's avatar
Drew committed
341
        
Drew's avatar
Drew committed
342
        let yaml = llbuildyaml(sources, workdir: workDirectory, modulename: name, linkSDK: sdk, compileOptions: compileOptions, linkOptions: linkOptions, outputType: outputType, linkWithProduct: linkWithProduct, swiftCPath: swiftCPath)
343
        let _ = try? yaml.writeToFile(llbuildyamlpath, atomically: false, encoding: NSUTF8StringEncoding)
Drew's avatar
Drew committed
344
        if bootstrapOnly { return }
Drew's avatar
Drew committed
345
        
Drew's avatar
Drew committed
346 347 348 349
        //SR-566
        let cmd = "\(SwiftBuildToolpath) -f \(llbuildyamlpath)"
        if system(cmd) != 0 {
            fatalError(cmd)
Drew's avatar
Drew committed
350
        }
Drew's avatar
Drew committed
351
        if task[Options.PublishProduct.rawValue]?.bool == true {
Drew's avatar
Drew committed
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
            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")
            }
        }
    }
}

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
370
    }
371
    try! manager.copyItemAtPath_SWIFTBUG(fromPath, toPath: toPath)
Drew's avatar
Drew committed
372
}