Merge pull request #502 from yonaskolb/refactor_spec

Refactor relative specs slightly
This commit is contained in:
Yonas Kolb
2019-01-28 22:09:05 +11:00
committed by GitHub
9 changed files with 114 additions and 107 deletions
+10 -7
View File
@@ -33,7 +33,7 @@ public struct Project: BuildSettingsContainer {
private var aggregateTargetsMap: [String: AggregateTarget]
public init(
basePath: Path,
basePath: Path = "",
name: String,
configs: [Config] = Config.defaultConfigs,
targets: [Target] = [],
@@ -132,16 +132,19 @@ extension Project: Equatable {
extension Project {
public init(basePath: Path, jsonDictionary: JSONDictionary) throws {
let spec = Spec(relativePath: Path(), jsonDictionary: jsonDictionary)
try self.init(spec: spec, basePath: basePath)
public init(path: Path) throws {
let spec = try SpecFile(path: path)
try self.init(spec: spec)
}
public init(spec: Spec, basePath: Path) throws {
public init(spec: SpecFile) throws {
try self.init(basePath: spec.basePath, jsonDictionary: spec.resolvedDictionary())
}
public init(basePath: Path = "", jsonDictionary: JSONDictionary) throws {
self.basePath = basePath
let spec = spec.resolvingPaths()
let jsonDictionary = try Project.resolveProject(jsonDictionary: spec.resolvedDictionary())
let jsonDictionary = try Project.resolveProject(jsonDictionary: jsonDictionary)
name = try jsonDictionary.json(atKeyPath: "name")
settings = jsonDictionary.json(atKeyPath: "settings") ?? .empty
@@ -2,89 +2,110 @@ import Foundation
import JSONUtilities
import PathKit
public struct Spec {
public struct SpecFile {
public let basePath: Path
public let relativePath: Path
public let jsonDictionary: JSONDictionary
public let subSpecs: [Spec]
public let subSpecs: [SpecFile]
public init(relativePath: Path, jsonDictionary: JSONDictionary, subSpecs: [Spec] = []) {
fileprivate struct Include {
let path: Path
let relativePaths: Bool
static let defaultRelativePaths = true
init?(any: Any) {
if let string = any as? String {
path = Path(string)
relativePaths = Include.defaultRelativePaths
} else if let dictionary = any as? JSONDictionary,
let path = dictionary["path"] as? String {
self.path = Path(path)
self.relativePaths = dictionary["relativePaths"] as? Bool ?? Include.defaultRelativePaths
} else {
return nil
}
}
static func parse(json: Any?) -> [Include] {
if let array = json as? [Any] {
return array.compactMap(Include.init)
} else if let object = json, let include = Include(any: object) {
return [include]
} else {
return []
}
}
}
public init(path: Path) throws {
try self.init(filename: path.lastComponent, basePath: path.parent())
}
public init(jsonDictionary: JSONDictionary, basePath: Path = "", relativePath: Path = "", subSpecs: [SpecFile] = []) {
self.basePath = basePath
self.relativePath = relativePath
self.jsonDictionary = jsonDictionary
self.subSpecs = subSpecs
}
public init(filename: String, basePath: Path, relativePath: Path = Path()) throws {
let path = basePath + relativePath + filename
fileprivate init(include: Include, basePath: Path, relativePath: Path) throws {
let basePath = include.relativePaths ? (basePath + relativePath) : (basePath + relativePath + include.path.parent())
let relativePath = include.relativePaths ? include.path.parent() : Path()
try self.init(filename: include.path.lastComponent, basePath: basePath, relativePath: relativePath)
}
fileprivate init(filename: String, basePath: Path, relativePath: Path = "") throws {
let path = basePath + relativePath + filename
let jsonDictionary = try SpecFile.loadDictionary(path: path)
let includes = Include.parse(json: jsonDictionary["include"])
let subSpecs: [SpecFile] = try includes.map { include in
try SpecFile(include: include, basePath: basePath, relativePath: relativePath)
}
self.init(jsonDictionary: jsonDictionary, basePath: basePath, relativePath: relativePath, subSpecs: subSpecs)
}
static func loadDictionary(path: Path) throws -> JSONDictionary {
// Depending on the extension we will either load the file as YAML or JSON
var json: [String: Any]
if path.extension?.lowercased() == "json" {
let data: Data = try path.read()
let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
guard let jsonDictionary = jsonData as? [String: Any] else {
fatalError("Invalid JSON at path \(path)")
}
json = jsonDictionary
return jsonDictionary
} else {
json = try loadYamlDictionary(path: path)
return try loadYamlDictionary(path: path)
}
let processIncludeOption = { (option: Any) -> (String, Bool)? in
if let option = option as? String {
return (option, true)
} else if let option = option as? JSONDictionary, let path = option["path"] as? String {
return (path, (option["relativePaths"] as? Bool) ?? true)
}
return nil
}
let includeSources: [(String, Bool)]
if let sources = json["include"] as? [Any] {
includeSources = sources.compactMap { processIncludeOption($0) }
} else if let source = json["include"] {
includeSources = [processIncludeOption(source)].compactMap { $0 }
} else {
includeSources = []
}
let includes = try includeSources.map { include -> Spec in
let path = Path(include.0)
let basePath = include.1 ? basePath + relativePath : basePath + relativePath + path.parent()
let relativePath = include.1 ? path.parent() : Path()
return try Spec(filename: path.lastComponent, basePath: basePath, relativePath: relativePath)
}
self.relativePath = relativePath
self.jsonDictionary = json
self.subSpecs = includes
}
public func resolvedDictionary() -> JSONDictionary {
let resolvedSpec = resolvingPaths()
return resolvedSpec.mergedDictionary()
}
func mergedDictionary() -> JSONDictionary {
return jsonDictionary.merged(onto:
subSpecs
.map { $0.resolvedDictionary() }
.map { $0.mergedDictionary() }
.reduce([:]) { $1.merged(onto: $0) }
)
}
}
extension Spec {
func resolvingPaths(relativeTo basePath: Path = Path()) -> Spec {
func resolvingPaths(relativeTo basePath: Path = Path()) -> SpecFile {
let relativePath = (basePath + self.relativePath).normalize()
guard relativePath != Path() else {
return self
}
let jsonDictionary = Project.pathProperties.resolvingPaths(in: self.jsonDictionary, relativeTo: relativePath)
return Spec(
relativePath: self.relativePath,
return SpecFile(
jsonDictionary: jsonDictionary,
subSpecs: self.subSpecs.map { template in
return template.resolvingPaths(relativeTo: relativePath)
}
relativePath: self.relativePath,
subSpecs: self.subSpecs.map { $0.resolvingPaths(relativeTo: relativePath) }
)
}
}
-16
View File
@@ -1,16 +0,0 @@
import Foundation
import JSONUtilities
import PathKit
extension Project {
public init(path: Path) throws {
let basePath = path.parent()
let template = try Spec(filename: path.lastComponent, basePath: basePath)
try self.init(spec: template, basePath: basePath)
}
public static func loadDictionary(path: Path) throws -> JSONDictionary {
return try Spec(filename: path.lastComponent, basePath: path.parent()).jsonDictionary
}
}
-2
View File
@@ -126,8 +126,6 @@ extension SpecOptions: PathContainer {
static var pathProperties: [PathProperty] {
return [
.string("carthageBuildPath"),
.string("carthageExecutablePath"),
.string("defaultConfig"),
]
}
}
+4 -3
View File
@@ -16,11 +16,12 @@ public class SpecLoader {
}
public func loadProject(path: Path) throws -> Project {
let template = try Spec(filename: path.lastComponent, basePath: path.parent())
let project = try Project(spec: template, basePath: path.parent())
let spec = try SpecFile(path: path)
let resolvedDictionary = spec.resolvedDictionary()
let project = try Project(basePath: spec.basePath, jsonDictionary: resolvedDictionary)
self.project = project
projectDictionary = template.jsonDictionary
projectDictionary = resolvedDictionary
return project
}
@@ -45,7 +45,7 @@ class ProjectGeneratorTests: XCTestCase {
$0.it("generates bundle id") {
let options = SpecOptions(bundleIdPrefix: "com.test")
let project = Project(basePath: "", name: "test", targets: [framework], options: options)
let project = Project(name: "test", targets: [framework], options: options)
let pbxProj = try project.generatePbxProj()
guard let target = pbxProj.nativeTargets.first,
let buildConfigList = target.buildConfigurationList,
@@ -57,7 +57,7 @@ class ProjectGeneratorTests: XCTestCase {
$0.it("clears setting presets") {
let options = SpecOptions(settingPresets: .none)
let project = Project(basePath: "", name: "test", targets: [framework], options: options)
let project = Project(name: "test", targets: [framework], options: options)
let pbxProj = try project.generatePbxProj()
let allSettings = pbxProj.buildConfigurations.reduce([:]) { $0.merged($1.buildSettings) }.keys.sorted()
try expect(allSettings) == ["SDKROOT", "SETTING_2"]
@@ -65,7 +65,7 @@ class ProjectGeneratorTests: XCTestCase {
$0.it("generates development language") {
let options = SpecOptions(developmentLanguage: "de")
let project = Project(basePath: "", name: "test", options: options)
let project = Project(name: "test", options: options)
let pbxProj = try project.generatePbxProj()
guard let pbxProject = pbxProj.projects.first else {
throw failure("Could't find PBXProject")
@@ -93,7 +93,7 @@ class ProjectGeneratorTests: XCTestCase {
$0.it("uses the default configuration name") {
let options = SpecOptions(defaultConfig: "Bconfig")
let project = Project(basePath: "", name: "test", configs: [Config(name: "Aconfig"), Config(name: "Bconfig")], targets: [framework], options: options)
let project = Project(name: "test", configs: [Config(name: "Aconfig"), Config(name: "Bconfig")], targets: [framework], options: options)
let pbxProject = try project.generatePbxProj()
guard let projectConfigList = pbxProject.projects.first?.buildConfigurationList,
@@ -111,7 +111,7 @@ class ProjectGeneratorTests: XCTestCase {
describe {
$0.it("generates config defaults") {
let project = Project(basePath: "", name: "test")
let project = Project(name: "test")
let pbxProj = try project.generatePbxProj()
let configs = pbxProj.buildConfigurations
try expect(configs.count) == 2
@@ -121,7 +121,6 @@ class ProjectGeneratorTests: XCTestCase {
$0.it("generates configs") {
let project = Project(
basePath: "",
name: "test",
configs: [Config(name: "config1"), Config(name: "config2")]
)
@@ -134,7 +133,6 @@ class ProjectGeneratorTests: XCTestCase {
$0.it("clears config settings when missing type") {
let project = Project(
basePath: "",
name: "test",
configs: [Config(name: "config")]
)
@@ -176,7 +174,6 @@ class ProjectGeneratorTests: XCTestCase {
$0.it("applies partial config settings") {
let project = Project(
basePath: "",
name: "test",
configs: [
Config(name: "Staging Debug", type: .debug),
@@ -192,7 +189,6 @@ class ProjectGeneratorTests: XCTestCase {
$0.it("sets project SDKROOT if there is only a single platform") {
var project = Project(
basePath: "",
name: "test",
targets: [
Target(name: "1", type: .application, platform: .iOS),
@@ -216,7 +212,7 @@ class ProjectGeneratorTests: XCTestCase {
let otherTarget2 = Target(name: "Other2", type: .framework, platform: .iOS, dependencies: [Dependency(type: .target, reference: "Other")], transitivelyLinkDependencies: true)
let aggregateTarget = AggregateTarget(name: "AggregateTarget", targets: ["MyApp", "MyFramework"])
let aggregateTarget2 = AggregateTarget(name: "AggregateTarget2", targets: ["AggregateTarget"])
let project = Project(basePath: "", name: "test", targets: [app, framework, otherTarget, otherTarget2], aggregateTargets: [aggregateTarget, aggregateTarget2])
let project = Project(name: "test", targets: [app, framework, otherTarget, otherTarget2], aggregateTargets: [aggregateTarget, aggregateTarget2])
$0.it("generates aggregate targets") {
let pbxProject = try project.generatePbxProj()
@@ -246,7 +242,7 @@ class ProjectGeneratorTests: XCTestCase {
func testTargets() {
describe {
let project = Project(basePath: "", name: "test", targets: targets)
let project = Project(name: "test", targets: targets)
$0.it("generates targets") {
let pbxProject = try project.generatePbxProj()
@@ -265,7 +261,7 @@ class ProjectGeneratorTests: XCTestCase {
var testTargetWithAttributes = uiTest
testTargetWithAttributes.settings.buildSettings["CODE_SIGN_STYLE"] = "Manual"
let project = Project(basePath: "", name: "test", targets: [appTargetWithAttributes, framework, optionalFramework, testTargetWithAttributes])
let project = Project(name: "test", targets: [appTargetWithAttributes, framework, optionalFramework, testTargetWithAttributes])
let pbxProject = try project.generatePbxProj()
guard let targetAttributes = pbxProject.projects.first?.targetAttributes else {
@@ -288,7 +284,7 @@ class ProjectGeneratorTests: XCTestCase {
$0.it("generates platform version") {
let target = Target(name: "Target", type: .application, platform: .watchOS, deploymentTarget: "2.0")
let project = Project(basePath: "", name: "", targets: [target], options: .init(deploymentTarget: DeploymentTarget(iOS: "10.0", watchOS: "3.0")))
let project = Project(name: "", targets: [target], options: .init(deploymentTarget: DeploymentTarget(iOS: "10.0", watchOS: "3.0")))
let pbxProject = try project.generatePbxProj()
@@ -568,7 +564,6 @@ class ProjectGeneratorTests: XCTestCase {
let targets = [app, iosFrameworkZ, staticLibrary, resourceBundle, iosFrameworkA, iosFrameworkB, appTest, appTestWithoutTransitive, stickerPack]
let project = Project(
basePath: "",
name: "test",
targets: targets,
options: SpecOptions(transitivelyLinkDependencies: true)
@@ -801,7 +796,6 @@ class ProjectGeneratorTests: XCTestCase {
dependencies: [Dependency(type: .target, reference: "target1")]
)
let project = Project(
basePath: "",
name: "test",
targets: [target1, target2]
)
@@ -860,7 +854,7 @@ class ProjectGeneratorTests: XCTestCase {
]
)
let project = Project(basePath: "", name: "test", targets: [app, framework, optionalFramework, uiTest])
let project = Project(name: "test", targets: [app, framework, optionalFramework, uiTest])
let pbxProject = try project.generatePbxProj()
guard let nativeTarget = pbxProject.nativeTargets.first(where: { $0.name == app.name }) else {
@@ -69,7 +69,7 @@ class ProjectSpecTests: XCTestCase {
func testValidation() {
describe {
let baseProject = Project(basePath: "", name: "", configs: [Config(name: "invalid")])
let baseProject = Project(name: "", configs: [Config(name: "invalid")])
let invalidSettings = Settings(
configSettings: ["invalidConfig": [:]],
groups: ["invalidSettingGroup"]
@@ -45,7 +45,6 @@ class SchemeGeneratorTests: XCTestCase {
build: Scheme.Build(targets: [buildTarget], preActions: [preAction])
)
let project = Project(
basePath: "",
name: "test",
targets: [app, framework],
schemes: [scheme]
@@ -104,7 +103,6 @@ class SchemeGeneratorTests: XCTestCase {
profile: Scheme.Profile(config: "Debug")
)
let project = Project(
basePath: "",
name: "test",
targets: [app, framework],
schemes: [scheme]
@@ -135,7 +133,7 @@ class SchemeGeneratorTests: XCTestCase {
Config(name: "Production Release", type: .release),
]
let project = Project(basePath: "", name: "test", configs: configs, targets: [target, framework])
let project = Project(name: "test", configs: configs, targets: [target, framework])
let xcodeProject = try project.generateXcodeProject()
try expect(xcodeProject.sharedData?.schemes.count) == 2
@@ -162,7 +160,7 @@ class SchemeGeneratorTests: XCTestCase {
var target = app
target.scheme = TargetScheme(environmentVariables: variables)
let project = Project(basePath: "", name: "test", targets: [target, framework])
let project = Project(name: "test", targets: [target, framework])
let xcodeProject = try project.generateXcodeProject()
try expect(xcodeProject.sharedData?.schemes.count) == 1
@@ -183,7 +181,7 @@ class SchemeGeneratorTests: XCTestCase {
postActions: [.init(name: "Run2", script: "post", settingsTarget: "MyApp")]
)
let project = Project(basePath: "", name: "test", targets: [target, framework])
let project = Project(name: "test", targets: [target, framework])
let xcodeProject = try project.generateXcodeProject()
try expect(xcodeProject.sharedData?.schemes.count) == 1
+16 -8
View File
@@ -12,7 +12,7 @@ class SpecLoadingTests: XCTestCase {
describe {
$0.it("merges includes") {
let path = fixturePath + "include_test.yml"
let project = try Project(path: path)
let project = try loadSpec(path: path)
try expect(project.name) == "NewName"
try expect(project.settingGroups) == [
@@ -28,7 +28,7 @@ class SpecLoadingTests: XCTestCase {
$0.it("expands directories") {
let path = fixturePath + "paths_test.yml"
let project = try Project(path: path)
let project = try loadSpec(path: path)
try expect(project.configFiles) == [
"IncludedConfig": "paths_test/config",
@@ -38,7 +38,7 @@ class SpecLoadingTests: XCTestCase {
try expect(project.options) == SpecOptions(
carthageBuildPath: "paths_test/recursive_test/carthage_build",
carthageExecutablePath: "paths_test/recursive_test/carthage_executable"
carthageExecutablePath: "carthage_executable"
)
try expect(project.aggregateTargets) == [
@@ -107,7 +107,7 @@ class SpecLoadingTests: XCTestCase {
$0.it("respects directory expansion preference") {
let path = fixturePath + "legacy_paths_test.yml"
let project = try Project(path: path)
let project = try loadSpec(path: path)
try expect(project.configFiles) == [
"IncludedConfig": "config",
@@ -186,7 +186,7 @@ class SpecLoadingTests: XCTestCase {
describe {
$0.it("merges includes") {
let path = fixturePath + "include_test.json"
let project = try Project(path: path)
let project = try loadSpec(path: path)
try expect(project.name) == "NewName"
try expect(project.settingGroups) == [
@@ -619,7 +619,7 @@ class SpecLoadingTests: XCTestCase {
macOS: "10.12.1"
)
)
let expected = Project(basePath: "", name: "test", options: options)
let expected = Project(name: "test", options: options)
let dictionary: [String: Any] = ["options": [
"carthageBuildPath": "../Carthage/Build",
"carthageExecutablePath": "../bin/carthage",
@@ -657,8 +657,16 @@ fileprivate func getProjectSpec(_ project: [String: Any], file: String = #file,
projectDictionary[key] = value
}
do {
let template = Spec(relativePath: "", jsonDictionary: projectDictionary)
return try Project(spec: template, basePath: "")
return try Project(jsonDictionary: projectDictionary)
} catch {
throw failure("\(error)", file: file, line: line)
}
}
fileprivate func loadSpec(path: Path, file: String = #file, line: Int = #line) throws -> Project {
do {
let specLoader = SpecLoader(version: "1.1.0")
return try specLoader.loadProject(path: path)
} catch {
throw failure("\(error)", file: file, line: line)
}