atllbuild.swift 14.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
    
186
    func run(task: Task) {
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
        
        //warn if we don't understand an option
        let knownOptions = ["tool",
                            "name",
                            "dependencies",
                            "outputType",
                            "source",
                            "bootstrapOnly",
                            "llbuildyaml",
                            "compileOptions",
                            "linkOptions",
                            "linkSDK",
                            "linkWithProduct",
                            "swiftCPath",
                            "xctestify",
Drew's avatar
Drew committed
202 203 204
                            "xctestStrict",
                            "publishProduct"]
        
205 206
        for key in task.allKeys {
            if !knownOptions.contains(key) {
Drew's avatar
Drew committed
207
                print("Warning: unknown option \(key) for task \(task.qualifiedName)")
208 209 210
            }
        }
        
Drew's avatar
Drew committed
211 212 213
        //create the working directory
        let workDirectory = ".atllbuild/"
        let manager = NSFileManager.defaultManager()
Drew's avatar
Drew committed
214 215 216 217 218 219 220 221 222
        
        //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)
223
        let _ = try? manager.createDirectoryAtPath(workDirectory + "/objects", withIntermediateDirectories: false, attributes: nil)
Drew's avatar
Drew committed
224

Drew's avatar
Drew committed
225
        //parse arguments
Drew's avatar
Drew committed
226
        var linkWithProduct: [String] = []
227
        if let arr = task["linkWithProduct"]?.vector {
Drew's avatar
Drew committed
228
            for product in arr {
229
                guard let p = product.string else { fatalError("non-string product \(product)") }
Drew's avatar
Drew committed
230 231 232
                linkWithProduct.append(p)
            }
        }
233
        let outputType: OutputType
234
        if task["outputType"]?.string == "static-library" {
235 236
            outputType = .StaticLibrary
        }
237
        else if task["outputType"]?.string == "executable" {
238 239 240
            outputType = .Executable
        }
        else {
241
            fatalError("Unknown outputType \(task["outputType"])")
242 243
        }
        
Drew's avatar
Drew committed
244
        var compileOptions: [String] = []
245
        if let opts = task["compileOptions"]?.vector {
Drew's avatar
Drew committed
246
            for o in opts {
247
                guard let os = o.string else { fatalError("Compile option \(o) is not a string") }
Drew's avatar
Drew committed
248 249 250
                compileOptions.append(os)
            }
        }
Drew's avatar
Drew committed
251 252 253 254 255 256 257
        var linkOptions: [String] = []
        if let opts = task["linkOptions"]?.vector {
            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
258
        
259
        guard let sourceDescriptions = task["source"]?.vector?.flatMap({$0.string}) else { fatalError("Can't find sources for atllbuild.") }
Drew's avatar
Drew committed
260
        var sources = collectSources(sourceDescriptions, taskForCalculatingPath: task)
261
        
Drew's avatar
Drew committed
262 263 264 265 266
        //xctestify
        if task["xctestify"]?.bool == true {
            precondition(outputType == .Executable, "You must use outputType: executable with xctestify.")
            //inject platform-specific flags
            #if os(OSX)
267 268 269 270 271 272 273 274 275 276 277 278 279
                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
        }
        if task["xctestStrict"]?.bool == true {
            #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
280 281
            #endif
        }
Drew's avatar
Drew committed
282

283
        guard let name = task["name"]?.string else { fatalError("No name for atllbuild task") }
Drew's avatar
Drew committed
284 285
        
        let bootstrapOnly: Bool
Drew's avatar
Drew committed
286

287
        if task["bootstrapOnly"]?.bool == true {
Drew's avatar
Drew committed
288 289 290 291 292 293
            bootstrapOnly = true
        }
        else {
            bootstrapOnly = false
        }
        
294
        let sdk: Bool
295
        if task["linkSDK"]?.bool == false {
296 297 298 299
            sdk = false
        }
        else { sdk = true }
        
Drew's avatar
Drew committed
300
        let llbuildyamlpath : String
Drew's avatar
Drew committed
301

302 303
        if let value = task["llbuildyaml"]?.string {
            llbuildyamlpath = value
Drew's avatar
Drew committed
304 305 306 307
        }
        else {
            llbuildyamlpath = workDirectory + "llbuild.yaml"
        }
Drew's avatar
Drew committed
308 309 310 311 312 313 314 315

        let swiftCPath: String
        if let c = task["swiftCPath"]?.string {
            swiftCPath = c
        }
        else {
            swiftCPath = SwiftCPath
        }
Drew's avatar
Drew committed
316
        
Drew's avatar
Drew committed
317
        let yaml = llbuildyaml(sources, workdir: workDirectory, modulename: name, linkSDK: sdk, compileOptions: compileOptions, linkOptions: linkOptions, outputType: outputType, linkWithProduct: linkWithProduct, swiftCPath: swiftCPath)
318
        let _ = try? yaml.writeToFile(llbuildyamlpath, atomically: false, encoding: NSUTF8StringEncoding)
Drew's avatar
Drew committed
319
        if bootstrapOnly { return }
Drew's avatar
Drew committed
320
        
Drew's avatar
Drew committed
321 322 323 324
        //SR-566
        let cmd = "\(SwiftBuildToolpath) -f \(llbuildyamlpath)"
        if system(cmd) != 0 {
            fatalError(cmd)
Drew's avatar
Drew committed
325
        }
Drew's avatar
Drew committed
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
        if task["publishProduct"]?.bool == true {
            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
345
    }
346
    try! manager.copyItemAtPath_SWIFTBUG(fromPath, toPath: toPath)
Drew's avatar
Drew committed
347
}