From 6774934a1ba09f96d3dd6bfe7ae0e5456423aed4 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Wed, 30 Aug 2017 12:58:10 +0200 Subject: [PATCH 1/2] support include array in spec that merges other specs --- .../project.pbxproj | 6 ++ Fixtures/include_test.yml | 12 ++++ Fixtures/included.yml | 9 +++ Sources/ProjectSpec/ProjectSpec.swift | 16 +----- Sources/ProjectSpec/Target.swift | 4 +- Sources/XcodeGen/main.swift | 2 +- Sources/XcodeGenKit/PBXProjGenerator.swift | 2 +- Sources/XcodeGenKit/SpecLoader.swift | 55 +++++++++++++++++++ Tests/XcodeGenKitTests/SpecLoadingTests.swift | 26 +++++++-- 9 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 Fixtures/include_test.yml create mode 100644 Fixtures/included.yml create mode 100644 Sources/XcodeGenKit/SpecLoader.swift diff --git a/Fixtures/TestProject/GeneratedProject.xcodeproj/project.pbxproj b/Fixtures/TestProject/GeneratedProject.xcodeproj/project.pbxproj index b5eb094be..e8a2838b9 100644 --- a/Fixtures/TestProject/GeneratedProject.xcodeproj/project.pbxproj +++ b/Fixtures/TestProject/GeneratedProject.xcodeproj/project.pbxproj @@ -386,6 +386,7 @@ XCBC19846901 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -406,6 +407,7 @@ XCBC37128501 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = TestProject/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -472,6 +474,7 @@ XCBC60448901 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = TestProjectTests/Info.plist; @@ -489,6 +492,7 @@ XCBC86437501 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = TestProject/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -547,6 +551,7 @@ XCBC89077001 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = TestProjectTests/Info.plist; @@ -564,6 +569,7 @@ XCBC89204001 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; diff --git a/Fixtures/include_test.yml b/Fixtures/include_test.yml new file mode 100644 index 000000000..1db837564 --- /dev/null +++ b/Fixtures/include_test.yml @@ -0,0 +1,12 @@ +include: [included.yml] +name: NewName +settingGroups: + test: + MY_SETTING1: NEW VALUE + MY_SETTING3: VALUE3 + new: + MY_SETTING: VALUE +targets: + - name: NewTarget + type: application + platform: iOS diff --git a/Fixtures/included.yml b/Fixtures/included.yml new file mode 100644 index 000000000..6f5cd6dbf --- /dev/null +++ b/Fixtures/included.yml @@ -0,0 +1,9 @@ +name: Included +settingGroups: + test: + MY_SETTING1: VALUE1 + MY_SETTING2: VALUE2 +targets: + - name: IncludedTarget + type: application + platform: iOS diff --git a/Sources/ProjectSpec/ProjectSpec.swift b/Sources/ProjectSpec/ProjectSpec.swift index f6204268a..95959c7f5 100644 --- a/Sources/ProjectSpec/ProjectSpec.swift +++ b/Sources/ProjectSpec/ProjectSpec.swift @@ -69,19 +69,7 @@ extension ProjectSpec.Options: Equatable { } } -extension ProjectSpec { - - public init(path: Path) throws { - let string: String = try path.read() - try self.init(path: path, string: string) - } - - public init(path: Path, string: String) throws { - let yaml = try Yams.load(yaml: string) - let json = yaml as! JSONDictionary - - try self.init(jsonDictionary: json) - } +extension ProjectSpec: JSONObjectConvertible { public init(jsonDictionary: JSONDictionary) throws { name = try jsonDictionary.json(atKeyPath: "name") @@ -89,7 +77,7 @@ extension ProjectSpec { settingGroups = jsonDictionary.json(atKeyPath: "settingGroups") ?? jsonDictionary.json(atKeyPath: "settingPresets") ?? [:] let configs: [String: String] = jsonDictionary.json(atKeyPath: "configs") ?? [:] self.configs = configs.map { Config(name: $0, type: ConfigType(rawValue: $1)) } - self.targets = try Target.decodeTargets(jsonDictionary: jsonDictionary) + targets = try Target.decodeTargets(jsonDictionary: jsonDictionary) schemes = try jsonDictionary.json(atKeyPath: "schemes") if jsonDictionary["options"] != nil { options = try jsonDictionary.json(atKeyPath: "options") diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index bf49c10ab..01de3d212 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -48,7 +48,7 @@ public struct Target { extension Target { - static func decodeTargets(jsonDictionary: JSONDictionary) throws -> [Target] { + static func decodeTargets(jsonDictionary: JSONDictionary) throws -> [Target] { guard jsonDictionary["targets"] != nil else { return [] } @@ -251,7 +251,7 @@ extension Dependency: JSONObjectConvertible { } embed = jsonDictionary.json(atKeyPath: "embed") - + if let bool: Bool = jsonDictionary.json(atKeyPath: "codeSign") { codeSign = bool } diff --git a/Sources/XcodeGen/main.swift b/Sources/XcodeGen/main.swift index c43715c79..5ab027c47 100644 --- a/Sources/XcodeGen/main.swift +++ b/Sources/XcodeGen/main.swift @@ -30,7 +30,7 @@ func generate(spec: String, project: String?) { let spec: ProjectSpec do { - spec = try ProjectSpec(path: specPath) + spec = try SpecLoader.loadSpec(path: specPath) print("Loaded spec: \(spec.targets.count) targets, \(spec.schemes.count) schemes, \(spec.configs.count) configs") } catch let error as DecodingError { print("Parsing spec failed: \(error.description)") diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index a8fe74297..995195d97 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -476,7 +476,7 @@ public class PBXProjGenerator { allFilePaths.append(path) } } - + let groupPath: String = depth == 0 ? path.byRemovingBase(path: basePath).string : path.lastComponent let group: PBXGroup if let cachedGroup = groupsByPath[path] { diff --git a/Sources/XcodeGenKit/SpecLoader.swift b/Sources/XcodeGenKit/SpecLoader.swift new file mode 100644 index 000000000..c17556bff --- /dev/null +++ b/Sources/XcodeGenKit/SpecLoader.swift @@ -0,0 +1,55 @@ +// +// SpecLoader.swift +// XcodeGen +// +// Created by Yonas Kolb on 30/8/17. +// +// + +import Foundation +import ProjectSpec +import PathKit +import Yams +import JSONUtilities + +public struct SpecLoader { + + public static func loadSpec(path: Path) throws -> ProjectSpec { + let dictionary = try loadDictionary(path: path) + return try ProjectSpec(jsonDictionary: dictionary) + } + + private static func loadDictionary(path: Path) throws -> JSONDictionary { + let string: String = try path.read() + let yaml = try Yams.load(yaml: string) + guard var json = yaml as? JSONDictionary else { + throw JSONUtilsError.fileNotAJSONDictionary + } + + if let includes = json["include"] as? [String] { + var includeDictionary: JSONDictionary = [:] + for include in includes { + let includePath = path.parent() + include + let dictionary = try loadDictionary(path: includePath) + includeDictionary = merge(dictionary: dictionary, onto: includeDictionary) + } + json = merge(dictionary: json, onto: includeDictionary) + } + return json + } + + private static func merge(dictionary: JSONDictionary, onto base: JSONDictionary) -> JSONDictionary { + var merged = base + + for (key, value) in dictionary { + if let dictionary = value as? JSONDictionary, let base = merged[key] as? JSONDictionary { + merged[key] = merge(dictionary: dictionary, onto: base) + } else if let array = value as? [Any], let base = merged[key] as? [Any] { + merged[key] = base + array + } else { + merged[key] = value + } + } + return merged + } +} diff --git a/Tests/XcodeGenKitTests/SpecLoadingTests.swift b/Tests/XcodeGenKitTests/SpecLoadingTests.swift index f82c39eef..27bebc00b 100644 --- a/Tests/XcodeGenKitTests/SpecLoadingTests.swift +++ b/Tests/XcodeGenKitTests/SpecLoadingTests.swift @@ -30,6 +30,23 @@ func specLoadingTests() { let validTarget: [String: Any] = ["name": "test", "type": "application", "platform": "iOS"] let invalid = "invalid" + describe("Spec Loader") { + $0.it("merges includes") { + let path = fixturePath + "include_test.yml" + let spec = try SpecLoader.loadSpec(path: path) + + try expect(spec.name) == "NewName" + try expect(spec.settingGroups) == [ + "test": Settings(dictionary: ["MY_SETTING1": "NEW VALUE", "MY_SETTING2": "VALUE2", "MY_SETTING3": "VALUE3"]), + "new": Settings(dictionary: ["MY_SETTING": "VALUE"]), + ] + try expect(spec.targets) == [ + Target(name: "IncludedTarget", type: .application, platform: .iOS), + Target(name: "NewTarget", type: .application, platform: .iOS), + ] + } + } + describe("Project Spec") { $0.it("fails with incorrect platform") { @@ -66,19 +83,19 @@ func specLoadingTests() { $0.it("parsed cross platform targets") { let targetDictionary: [String: Any] = [ - "name":"Framework", + "name": "Framework", "platform": ["iOS", "tvOS"], "type": "framework", "sources": ["Framework", "Framework $platform"], "settings": ["SETTING": "value_$platform"], ] - let spec = try getProjectSpec(["targets":[targetDictionary]]) + let spec = try getProjectSpec(["targets": [targetDictionary]]) var target_iOS = Target(name: "Framework_iOS", type: .framework, platform: .iOS) var target_tvOS = Target(name: "Framework_tvOS", type: .framework, platform: .tvOS) - target_iOS.sources = ["Framework","Framework iOS"] - target_tvOS.sources = ["Framework","Framework tvOS"] + target_iOS.sources = ["Framework", "Framework iOS"] + target_tvOS.sources = ["Framework", "Framework tvOS"] target_iOS.settings = ["PRODUCT_NAME": "Framework", "SETTING": "value_iOS"] target_tvOS.settings = ["PRODUCT_NAME": "Framework", "SETTING": "value_tvOS"] @@ -160,6 +177,5 @@ func specLoadingTests() { let parsedSpec = try getProjectSpec(["options": ["carthageBuildPath": "../Carthage/Build"]]) try expect(parsedSpec) == expected } - } } From 61fd9bafb8ec3ad635a6c42298964fb094f9d57b Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Wed, 30 Aug 2017 13:03:05 +0200 Subject: [PATCH 2/2] spec include documentation --- README.md | 1 + docs/ProjectSpec.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index f4f4f9611..167e929cb 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ The project spec is a YAML or JSON file that defines your targets, configuration - ✅ Automatically generate Schemes for **different environments** like test and production - ✅ Easily **create new projects** with complicated setups on demand without messing around with Xcode - ✅ Generate from anywhere including **Continuous Delivery** servers +- ✅ Distribute your spec amongst multiple files for easy **sharing** and overriding Given a very simple project spec file like this: diff --git a/docs/ProjectSpec.md b/docs/ProjectSpec.md index c1c326fb7..16f156262 100644 --- a/docs/ProjectSpec.md +++ b/docs/ProjectSpec.md @@ -25,6 +25,7 @@ Required properties are marked 🔵 and optional properties with ⚪️. ## Project - 🔵 **name**: `String` - Name of the generated project +- ⚪️ **include**: `[String]` - The paths to other specs. They will be merged in order and then the current spec will be merged on top - ⚪️ **options**: [Options](#options) - Various options to override default behaviour - ⚪️ **configs**: [Configs](#configs) - Project build configurations. Defaults to `Debug` and `Release` configs - ⚪️ **settings**: [Settings](#settings) - Project specific settings. Default base and config type settings will be applied first before any settings defined here