Package.swift 12 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.
14
import Foundation
15

16
final public class Task {
Drew's avatar
Drew committed
17 18 19 20 21 22 23 24 25 26 27
    ///The unqualified name of the task, not including its package name
    public let unqualifiedName: String

    ///The qualified name of the task, including its package name
    public var qualifiedName: String {
        return package.name + "." + unqualifiedName
    }

    ///The package for the task
    let package: Package

28
    public var dependencies: [String] = []
29
    public var tool: String = "atllbuild"
30
    public var importedPath: String ///the directory at which the task was imported.  This includes a trailing /.
Drew's avatar
Drew committed
31

32 33 34 35
    var overlay: [String] = [] ///The overlays we should apply to this task
    var appliedOverlays: [String] = [] ///The overlays we did apply to this task

    var declaredOverlays: [String: [String: ParseValue]] = [:] ///The overlays this task declares
36
    
37 38
    public var allKeys: [String]
    
39 40
    private var kvp: [String:ParseValue]

Drew's avatar
Drew committed
41 42
    init?(value: ParseValue, unqualifiedName: String, package: Package, importedPath: String) {
        precondition(!unqualifiedName.characters.contains("."), "Task \(unqualifiedName) may not contain a period.")
43
        guard let kvp = value.map else { return nil }
44
        self.importedPath = importedPath.pathWithTrailingSlash
45
        self.kvp = kvp
Drew's avatar
Drew committed
46 47
        self.unqualifiedName = unqualifiedName
        self.package = package
48
        self.allKeys = [String](kvp.keys)
49
        self.tool = kvp["tool"]?.string ?? self.tool
50 51 52 53 54 55 56
        if let ol = kvp["overlay"] {
            guard let overlays = ol.vector else {
                fatalError("Non-vector overlay \(ol); did you mean to use `overlays` instead?")
            }
            for overlay in overlays {
                guard let str = overlay.string else {
                    fatalError("Non-string overlay \(overlay)")
Drew's avatar
Drew committed
57
                }
58 59 60 61 62 63 64 65 66 67
                self.overlay.append(str)
            }
        }
        if let ol = kvp["overlays"] {
            guard let overlays = ol.map else {
                fatalError("Non-map overlays \(ol); did you mean to use `overlay` instead?")
            }
            for (name, overlay) in overlays {

                guard let innerOverlay = overlay.map else {
Drew's avatar
Drew committed
68
                    fatalError("non-map overlay \(overlay)")
69 70
                }
                self.declaredOverlays[name] = innerOverlay
Drew's avatar
Drew committed
71 72
            }
        }
73

74 75
        if let values = kvp["dependencies"]?.vector {
            for value in values {
76
                if let dep = value.string { self.dependencies.append(dep) }
77 78 79
            }
        }
    }
80 81 82 83
    
    public subscript(key: String) -> ParseValue? {
        return kvp[key]
    }
84 85 86 87 88 89 90 91

    /**Apply the overlay to the receiver
- warning: an overlay may itself apply another overlay.  In this case, the overlay for the task should be recalculated.
- return: whether the overlay applied another overlay */
    @warn_unused_result
    private func applyOverlay(name: String, overlay: [String: ParseValue]) -> Bool {
        precondition(!appliedOverlays.contains(name), "Already applied overlay named \(name)")
        for (optionName, optionValue) in overlay {
Drew's avatar
Drew committed
92 93
            switch(optionValue) {
                case ParseValue.Vector(let vectorValue):
94 95 96 97 98 99 100
                let existingValue: [ParseValue]

                if let ev = self[optionName]?.vector  {
                    existingValue = ev
                }
                else {
                    existingValue = []
Drew's avatar
Drew committed
101 102
                }
                var newValue = existingValue
Drew's avatar
Drew committed
103
                newValue.appendContentsOf(vectorValue)
Drew's avatar
Drew committed
104 105 106
                self.kvp[optionName] = ParseValue.Vector(newValue)
                //apply overlays to the model property
                if optionName == "overlay" {
Drew's avatar
Drew committed
107
                    for overlayName in vectorValue {
Drew's avatar
Drew committed
108 109 110 111
                        guard let overlayNameStr = overlayName.string else {
                            fatalError("Non-string overlayname \(overlayName)")
                        }
                        self.overlay.append(overlayNameStr)
112 113
                    }
                }
Drew's avatar
Drew committed
114 115 116

                case ParseValue.StringLiteral(let str):
                if let existingValue = self[optionName] {
Drew's avatar
Drew committed
117
                    fatalError("Can't overlay on \(self.qualifiedName)[\(optionName)] which already has a value \(existingValue)")
Drew's avatar
Drew committed
118 119 120
                }
                self.kvp[optionName] = ParseValue.StringLiteral(str)

Drew's avatar
Drew committed
121 122
                case ParseValue.BoolLiteral(let b):
                if let existingValue = self[optionName] {
Drew's avatar
Drew committed
123
                    fatalError("Can't overlay on \(self.qualifiedName)[\(optionName)] which already has a value \(existingValue)")
Drew's avatar
Drew committed
124 125 126 127
                }
                self.kvp[optionName] = ParseValue.BoolLiteral(b)


Drew's avatar
Drew committed
128 129
                default:
                fatalError("Canot overlay value \(optionValue); please file a bug")
130
            }
Drew's avatar
Drew committed
131 132 133
            

            
134 135 136 137
        }
        appliedOverlays.append(name)
        return overlay.keys.contains("overlay")
    }
138 139
}

140
final public class Package {
141
    // The required properties.
142
    public var name: String
143 144 145
    
    // The optional properties. All optional properties must have a default value.
    public var version: String = ""
Drew's avatar
Drew committed
146 147 148 149

    /**The tasks for the package.  For tasks in this package, they are indexed
    both by qualified and unqualified name.  For tasks in another package, they
    appear only by qualified name. */
150
    public var tasks: [String:Task] = [:]
151

152
    var overlays: [String: [String: ParseValue]] = [:]
Drew's avatar
Drew committed
153 154
    var adjustedImportPath: String = ""

155 156 157 158 159 160 161 162 163 164
    /**Calculate the pruned dependency graph for the given task
- returns: A list of tasks in a reasonable order to be processed. */
    public func prunedDependencyGraph(task: Task) -> [Task] {
        var pruned : [Task] = []
        if let dependencies = task["dependencies"]?.vector {
            for next in dependencies {
                guard let depName = next.string else { fatalError("Non-string dependency \(next)")}
                guard let nextTask = tasks[depName] else { fatalError("Can't find so-called task \(depName)")}
                let nextGraph = prunedDependencyGraph(nextTask)
                for nextItem in nextGraph {
Drew's avatar
Drew committed
165
                    let filteredTasks = pruned.filter() {$0.qualifiedName == nextItem.qualifiedName}
166 167 168 169 170 171 172 173
                    if filteredTasks.count >= 1 { continue }
                    pruned.append(nextItem)
                }
            }
        }
        pruned.append(task)
        return pruned
    }
174 175 176 177
    
    public init(name: String) {
        self.name = name
    }
178
    
179 180 181 182
    /**Create the package.
- parameter filepath: The path to the file to load
- parameter overlay: A list of overlays to apply globally to all tasks in the package. */
    public convenience init?(filepath: String, overlay: [String]) {
183 184 185 186
        guard let parser = Parser(filepath: filepath) else { return nil }
        
        do {
            let result = try parser.parse()
Drew's avatar
Drew committed
187
            let basepath = filepath.toNSString.stringByDeletingLastPathComponent
188
            self.init(type: result, overlay: overlay, pathOnDisk:basepath)
189 190 191 192 193 194 195
        }
        catch {
            print("error: \(error)")
            return nil
        }
    }
    
196
    public init?(type: ParseType, overlay requestedGlobalOverlays: [String], pathOnDisk: String) {
197 198
        if type.name != "package" { return nil }
        
199
        if let value = type.properties["name"]?.string { self.name = value }
200 201 202 203
        else {
            print("ERROR: No name specified for the package.")
            return nil
        }
204
        if let value = type.properties["version"]?.string { self.version = value }
205 206

        if let parsedTasks = type.properties["tasks"]?.map {
Drew's avatar
Drew committed
207 208 209 210
            for (name, value) in parsedTasks {
                if let task = Task(value: value, unqualifiedName: name, package: self, importedPath: pathOnDisk) {
                    self.tasks[task.unqualifiedName] = task
                    self.tasks[task.qualifiedName] = task
211 212 213
                }
            }
        }
Drew's avatar
Drew committed
214

Drew's avatar
Drew committed
215
        var remotePackages: [Package] = []
216

Drew's avatar
Drew committed
217
        //load remote packages
218 219 220
        if let imports = type.properties["import"]?.vector {
            for importFile in imports {
                guard let importFileString = importFile.string else { fatalError("Non-string import \(importFile)")}
Drew's avatar
Drew committed
221 222
                let adjustedImportPath = (pathOnDisk.pathWithTrailingSlash + importFileString).toNSString.stringByDeletingLastPathComponent.pathWithTrailingSlash
                let adjustedFileName = importFileString.toNSString.lastPathComponent
223
                guard let remotePackage = Package(filepath: adjustedImportPath + adjustedFileName, overlay: requestedGlobalOverlays) else {
224
                    fatalError("Can't load remote package \(adjustedImportPath + adjustedFileName)")
225
                }
Drew's avatar
Drew committed
226 227 228 229 230
                remotePackage.adjustedImportPath = adjustedImportPath
                remotePackages.append(remotePackage)
            }
        }

Drew's avatar
Drew committed
231
        //load remote overlays
Drew's avatar
Drew committed
232
        for remotePackage in remotePackages {
233 234
            for (overlayName, value) in remotePackage.overlays {
                self.overlays["\(remotePackage.name).\(overlayName)"] = value
Drew's avatar
Drew committed
235 236
            }
        }
Drew's avatar
Drew committed
237 238 239 240
        if let ol = type.properties["overlays"] {
            guard let overlays = ol.map else {
                fatalError("Non-map overlay \(ol)")
            }
241 242 243 244 245
            for (name, overlay) in overlays {
                guard let innerOverlay = overlay.map else {
                    fatalError("Non-map overlay \(overlay)")
                }
                self.overlays[name] = innerOverlay
Drew's avatar
Drew committed
246 247 248
            }
        }

249
        var usedGlobalOverlays : [String] = []
Drew's avatar
Drew committed
250
        //swap in overlays
251 252 253

        while true {
            var again = false
Drew's avatar
Drew committed
254
            for (_, task) in self.tasks {
255 256 257 258
                //merge task-declared and globally-declared overlays
                var declaredOverlays : [String: [String: ParseValue]] = [:]
                for (k,v) in task.declaredOverlays {
                    declaredOverlays[k] = v
Drew's avatar
Drew committed
259
                }
260 261 262 263 264 265 266
                for (k,v) in overlays {
                    declaredOverlays[k] = v
                }

                for overlayName in task.overlay {
                    if task.appliedOverlays.contains(overlayName) { continue }
                    guard let overlay = declaredOverlays[overlayName] else {
Drew's avatar
Drew committed
267
                        fatalError("Can't find overlay named \(overlayName) in \(declaredOverlays)")
Drew's avatar
Drew committed
268
                    }
269 270 271 272
                    again = again || task.applyOverlay(overlayName, overlay: overlay)
                }
                for overlayName in requestedGlobalOverlays {
                    if task.appliedOverlays.contains(overlayName) { continue }
Drew's avatar
Drew committed
273

274
                    guard let overlay = declaredOverlays[overlayName] else {
Drew's avatar
Drew committed
275
                        print("Warning: Can't apply overlay \(overlayName) to task \(task.qualifiedName)")
Drew's avatar
Drew committed
276
                        continue
Drew's avatar
Drew committed
277
                    }
278 279
                    again = again || task.applyOverlay(overlayName, overlay: overlay)
                    usedGlobalOverlays.append(overlayName)
280 281
                }
            }
282 283 284 285 286 287
            if !again { break }
        }

        //warn about unused global overlays
        for requestedOverlay in requestedGlobalOverlays {
            if !usedGlobalOverlays.contains(requestedOverlay) {
Drew's avatar
Drew committed
288
                print("Warning: overlay \(requestedOverlay) had no effect on package \(name)")
289
            }
290
        }
Drew's avatar
Drew committed
291 292 293

        //load remote tasks
        for remotePackage in remotePackages {
294 295
            for (_, task) in remotePackage.tasks {
                task.importedPath = remotePackage.adjustedImportPath
Drew's avatar
Drew committed
296
                self.tasks[task.qualifiedName] = task
Drew's avatar
Drew committed
297 298 299 300
            }
        }


301
    }
302
}