diff --git a/CHANGELOG.md b/CHANGELOG.md index fa719390..392bf649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,20 @@ ## Master +## 1.3.0 + ### Added - generate output files for Carthage copy-frameworks script [#84](https://github.com/yonaskolb/XcodeGen/pull/84) @mironal +- added options.settingPreset to choose which setting presets get applied [#100](https://github.com/yonaskolb/XcodeGen/pull/101) @yonaskolb +- added `link` option for target dependencies [#109](https://github.com/yonaskolb/XcodeGen/pull/109) @keith ### Changed - updated to xcproj 0.4.1 [#85](https://github.com/yonaskolb/XcodeGen/pull/85) @enmiller - don't copy base settings if config type has been left out [#100](https://github.com/yonaskolb/XcodeGen/pull/100) @yonaskolb - generate localised files under a single variant group [#70](https://github.com/yonaskolb/XcodeGen/pull/70) @ryohey +- don't apply common project settings to configs with no type [#100](https://github.com/yonaskolb/XcodeGen/pull/100) @yonaskolb +- config references in settings can now be partially matched and are case insensitive [#111](https://github.com/yonaskolb/XcodeGen/pull/111) @yonaskolb +- other small internal changes @yonaskolb ### Fixed - embed Carthage frameworks for macOS [#82](https://github.com/yonaskolb/XcodeGen/pull/82) @toshi0383 @@ -17,10 +24,10 @@ - change make build to a PHONY task [#98](https://github.com/yonaskolb/XcodeGen/pull/98) @keith - allow copying of resource files from dependant targets [#95](https://github.com/yonaskolb/XcodeGen/pull/95) @keith - fixed library linking [#93](https://github.com/yonaskolb/XcodeGen/pull/93) @keith +- fixed duplicate carthage file references [#107](https://github.com/yonaskolb/XcodeGen/pull/107) @yonaskolb +- an error is now shown if you try and generate a target scheme and don't have debug and release builds @yonaskolb - - - +[Commits](https://github.com/yonaskolb/XcodeGen/compare/1.2.4...1.3.0) ## 1.2.4 diff --git a/Fixtures/TestProject/GeneratedProject.xcodeproj/project.pbxproj b/Fixtures/TestProject/GeneratedProject.xcodeproj/project.pbxproj index ce7e60b9..e3d1e4c9 100644 --- a/Fixtures/TestProject/GeneratedProject.xcodeproj/project.pbxproj +++ b/Fixtures/TestProject/GeneratedProject.xcodeproj/project.pbxproj @@ -1,6 +1,5 @@ // !$*UTF8*$! { - archiveVersion = 1; classes = { }; objectVersion = 46; @@ -44,13 +43,11 @@ /* Begin PBXCopyFilesBuildPhase section */ CFBP50493301 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( BF3862341101 /* MyFramework.framework in CopyFiles */, ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ @@ -191,7 +188,6 @@ buildActionMask = 2147483647; files = ( ); - runOnlyForDeploymentPostprocessing = 0; }; HBP299349701 /* Frameworks */ = { isa = PBXHeadersBuildPhase; @@ -199,14 +195,12 @@ files = ( BF3515549501 /* MyFramework.h in Headers */, ); - runOnlyForDeploymentPostprocessing = 0; }; HBP652326301 /* Frameworks */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ @@ -297,14 +291,12 @@ buildActionMask = 2147483647; files = ( ); - runOnlyForDeploymentPostprocessing = 0; }; RBP299349701 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); - runOnlyForDeploymentPostprocessing = 0; }; RBP652326301 /* Resources */ = { isa = PBXResourcesBuildPhase; @@ -315,7 +307,6 @@ BF2513089601 /* LocalizedStoryboard.storyboard in Resources */, BF2250910101 /* Main.storyboard in Resources */, ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ @@ -387,7 +378,6 @@ files = ( BF9001417701 /* TestProjectTests.swift in Sources */, ); - runOnlyForDeploymentPostprocessing = 0; }; SBP299349701 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -395,7 +385,6 @@ files = ( BF9155249601 /* FrameworkFile.swift in Sources */, ); - runOnlyForDeploymentPostprocessing = 0; }; SBP652326301 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -404,7 +393,6 @@ BF1073850101 /* AppDelegate.swift in Sources */, BF1744565901 /* ViewController.swift in Sources */, ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ @@ -704,7 +692,6 @@ XCBC89077001 /* Debug */, XCBC60448901 /* Release */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = ""; }; XCCL29934901 /* Build configuration list for PBXNativeTarget "MyFramework" */ = { @@ -713,7 +700,6 @@ XCBC19846901 /* Debug */, XCBC89204001 /* Release */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = ""; }; XCCL65232601 /* Build configuration list for PBXNativeTarget "TestProject" */ = { @@ -722,7 +708,6 @@ XCBC37128501 /* Debug */, XCBC86437501 /* Release */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = ""; }; XCCL81399401 /* Build configuration list for PBXProject "GeneratedProject" */ = { diff --git a/Formula/xcodegen.rb b/Formula/xcodegen.rb index 8cb16be6..b657f5e8 100644 --- a/Formula/xcodegen.rb +++ b/Formula/xcodegen.rb @@ -1,8 +1,8 @@ class Xcodegen < Formula desc "Tool that generates your Xcode project from a project spec" homepage "https://github.com/yonaskolb/XcodeGen" - url "https://github.com/yonaskolb/XcodeGen/archive/1.2.4.tar.gz" - sha256 "4490df6f2e1442a0817f1a7e8e2eb34c1f173f2c92405870e62de1de87a6772d" + url "https://github.com/yonaskolb/XcodeGen/archive/1.3.0.tar.gz" + sha256 "6a78b0eb6768122d0c71a8ceaba877f431cd1650bb5caa06b53c33a8cd512b20" head "https://github.com/yonaskolb/XcodeGen.git" depends_on :xcode diff --git a/Makefile b/Makefile index 3fc7958a..ebec0517 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ TOOL_NAME = XcodeGen -VERSION = 1.2.0 +VERSION = 1.3.0 PREFIX = /usr/local INSTALL_PATH = $(PREFIX)/bin/$(TOOL_NAME) diff --git a/Package.resolved b/Package.resolved index 58535b03..0fd79d68 100644 --- a/Package.resolved +++ b/Package.resolved @@ -69,8 +69,8 @@ "repositoryURL": "https://github.com/xcodeswift/xcproj.git", "state": { "branch": null, - "revision": "cd1c329c6e092fdf5afbddf10dd59da564293aa6", - "version": "0.4.1" + "revision": "fc8cbfce8a6bc1a98773181afe8da97c2516b722", + "version": "1.2.0" } } ] diff --git a/Package.swift b/Package.swift index d1e588f1..2a7a0ff3 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,7 @@ let package = Package( .package(url: "https://github.com/yonaskolb/JSONUtilities.git", from: "3.3.0"), .package(url: "https://github.com/kylef/Spectre.git", from: "0.7.0"), .package(url: "https://github.com/onevcat/Rainbow.git", from: "2.1.0"), - .package(url: "https://github.com/xcodeswift/xcproj.git", from: "0.1.2"), + .package(url: "https://github.com/xcodeswift/xcproj.git", from: "1.0.0"), ], targets: [ .target(name: "XcodeGen", dependencies: [ diff --git a/Sources/ProjectSpec/ProjectSpec.swift b/Sources/ProjectSpec/ProjectSpec.swift index a5d17731..7789f9fe 100644 --- a/Sources/ProjectSpec/ProjectSpec.swift +++ b/Sources/ProjectSpec/ProjectSpec.swift @@ -14,6 +14,7 @@ import Yams public struct ProjectSpec { + public var basePath: Path public var name: String public var targets: [Target] public var settings: Settings @@ -29,12 +30,35 @@ public struct ProjectSpec { public struct Options { public var carthageBuildPath: String? public var bundleIdPrefix: String? + public var settingPresets: SettingPresets = .all + + public enum SettingPresets: String { + case all + case none + case project + case targets + + public var applyTarget: Bool { + switch self { + case .all, .targets: return true + default: return false + } + } + + public var applyProject: Bool { + switch self { + case .all, .project: return true + default: return false + } + } + } public init() { } } - public init(name: String, configs: [Config] = [], targets: [Target] = [], settings: Settings = .empty, settingGroups: [String: Settings] = [:], schemes: [Scheme] = [], options: Options = Options(), fileGroups: [String] = [], configFiles: [String: String] = [:], attributes: [String: Any] = [:]) { + public init(basePath: Path, name: String, configs: [Config] = [], targets: [Target] = [], settings: Settings = .empty, settingGroups: [String: Settings] = [:], schemes: [Scheme] = [], options: Options = Options(), fileGroups: [String] = [], configFiles: [String: String] = [:], attributes: [String: Any] = [:]) { + self.basePath = basePath self.name = name self.targets = targets self.configs = configs @@ -97,13 +121,15 @@ extension ProjectSpec.Options: Equatable { public static func ==(lhs: ProjectSpec.Options, rhs: ProjectSpec.Options) -> Bool { return lhs.carthageBuildPath == rhs.carthageBuildPath && - lhs.bundleIdPrefix == rhs.bundleIdPrefix + lhs.bundleIdPrefix == rhs.bundleIdPrefix && + lhs.settingPresets == rhs.settingPresets } } -extension ProjectSpec: JSONObjectConvertible { +extension ProjectSpec { - public init(jsonDictionary: JSONDictionary) throws { + public init(basePath: Path, jsonDictionary: JSONDictionary) throws { + self.basePath = basePath let jsonDictionary = try ProjectSpec.filterJSON(jsonDictionary: jsonDictionary) name = try jsonDictionary.json(atKeyPath: "name") settings = jsonDictionary.json(atKeyPath: "settings") ?? .empty @@ -133,5 +159,6 @@ extension ProjectSpec.Options: JSONObjectConvertible { public init(jsonDictionary: JSONDictionary) throws { carthageBuildPath = jsonDictionary.json(atKeyPath: "carthageBuildPath") bundleIdPrefix = jsonDictionary.json(atKeyPath: "bundleIdPrefix") + settingPresets = jsonDictionary.json(atKeyPath: "settingPresets") ?? .all } } diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index 006e140b..d0277b98 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -16,7 +16,6 @@ public struct Target { public var platform: Platform public var settings: Settings public var sources: [String] - public var sourceExludes: [String] public var dependencies: [Dependency] public var prebuildScripts: [BuildScript] public var postbuildScripts: [BuildScript] @@ -31,14 +30,13 @@ public struct Target { return name } - public init(name: String, type: PBXProductType, platform: Platform, settings: Settings = .empty, configFiles: [String: String] = [:], sources: [String] = [], sourceExludes: [String] = [], dependencies: [Dependency] = [], prebuildScripts: [BuildScript] = [], postbuildScripts: [BuildScript] = [], scheme: TargetScheme? = nil) { + public init(name: String, type: PBXProductType, platform: Platform, settings: Settings = .empty, configFiles: [String: String] = [:], sources: [String] = [], dependencies: [Dependency] = [], prebuildScripts: [BuildScript] = [], postbuildScripts: [BuildScript] = [], scheme: TargetScheme? = nil) { self.name = name self.type = type self.platform = platform self.settings = settings self.configFiles = configFiles self.sources = sources - self.sourceExludes = sourceExludes self.dependencies = dependencies self.prebuildScripts = prebuildScripts self.postbuildScripts = postbuildScripts @@ -49,7 +47,7 @@ public struct Target { extension Target: CustomStringConvertible { public var description: String { - return "\(platform.emoji) \(type): \(name)" + return "\(platform.emoji) \(name) - \(type)" } } @@ -129,7 +127,6 @@ extension Target: Equatable { lhs.settings == rhs.settings && lhs.configFiles == rhs.configFiles && lhs.sources == rhs.sources && - lhs.sourceExludes == rhs.sourceExludes && lhs.dependencies == rhs.dependencies && lhs.prebuildScripts == rhs.prebuildScripts && lhs.postbuildScripts == rhs.postbuildScripts && @@ -186,7 +183,6 @@ extension Target: NamedJSONDictionaryConvertible { } else { sources = jsonDictionary.json(atKeyPath: "sources") ?? [] } - sourceExludes = jsonDictionary.json(atKeyPath: "sourceExludes") ?? [] if jsonDictionary["dependencies"] == nil { dependencies = [] } else { @@ -205,6 +201,7 @@ public struct Dependency: Equatable { public var embed: Bool? public var codeSign: Bool = true public var removeHeaders: Bool = true + public var link: Bool = true public init(type: DependencyType, reference: String, embed: Bool? = nil) { self.type = type @@ -223,7 +220,8 @@ public struct Dependency: Equatable { lhs.type == rhs.type && lhs.codeSign == rhs.codeSign && lhs.removeHeaders == rhs.removeHeaders && - lhs.embed == rhs.embed + lhs.embed == rhs.embed && + lhs.link == rhs.link } public var buildSettings: [String: Any] { @@ -256,6 +254,9 @@ extension Dependency: JSONObjectConvertible { embed = jsonDictionary.json(atKeyPath: "embed") + if let bool: Bool = jsonDictionary.json(atKeyPath: "link") { + link = bool + } if let bool: Bool = jsonDictionary.json(atKeyPath: "codeSign") { codeSign = bool } diff --git a/Sources/XcodeGen/main.swift b/Sources/XcodeGen/main.swift index 4934dcfc..4a37ea08 100644 --- a/Sources/XcodeGen/main.swift +++ b/Sources/XcodeGen/main.swift @@ -15,11 +15,18 @@ import ProjectSpec import JSONUtilities import Rainbow +let version = "1.3.0" + func generate(spec: String, project: String) { let specPath = Path(spec).normalize() let projectPath = Path(project).normalize() + if !specPath.exists { + print("No project spec found at \(specPath.absolute())".red) + exit(1) + } + let spec: ProjectSpec do { spec = try SpecLoader.loadSpec(path: specPath) @@ -33,7 +40,7 @@ func generate(spec: String, project: String) { } do { - let projectGenerator = ProjectGenerator(spec: spec, path: specPath.parent()) + let projectGenerator = ProjectGenerator(spec: spec) let project = try projectGenerator.generateProject() print("⚙️ Generated project") @@ -42,6 +49,7 @@ func generate(spec: String, project: String) { print("💾 Saved project to \(projectFile.string)".green) } catch let error as SpecValidationError { print(error.description.red) + exit(1) } catch { print("Generation failed: \(error.localizedDescription)".red) exit(1) @@ -52,4 +60,4 @@ command( Option("spec", "project.yml", flag: "s", description: "The path to the spec file"), Option("project", "", flag: "p", description: "The path to the folder where the project should be generated"), generate) - .run() + .run(version) diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index 699adfab..4848bb30 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -17,7 +17,6 @@ import ProjectSpec public class PBXProjGenerator { let spec: ProjectSpec - let basePath: Path let currentXcodeVersion: String var fileReferencesByPath: [Path: String] = [:] @@ -28,7 +27,7 @@ public class PBXProjGenerator { var targetBuildFiles: [String: PBXBuildFile] = [:] var targetFileReferences: [String: String] = [:] var topLevelGroups: [PBXGroup] = [] - var carthageFrameworksByPlatform: [String: [String]] = [:] + var carthageFrameworksByPlatform: [String: Set] = [:] var frameworkFiles: [String] = [] var uuids: Set = [] @@ -38,10 +37,9 @@ public class PBXProjGenerator { return spec.options.carthageBuildPath ?? "Carthage/Build" } - public init(spec: ProjectSpec, path: Path, currentXcodeVersion: String) { + public init(spec: ProjectSpec, currentXcodeVersion: String) { self.currentXcodeVersion = currentXcodeVersion self.spec = spec - basePath = path } public func generateUUID(_ element: T.Type, _ id: String) -> String { @@ -64,17 +62,17 @@ public class PBXProjGenerator { public func generate() throws -> PBXProj { uuids = [] - project = PBXProj(archiveVersion: 1, objectVersion: 46, rootObject: generateUUID(PBXProject.self, spec.name)) + project = PBXProj(objectVersion: 46, rootObject: generateUUID(PBXProject.self, spec.name)) for group in spec.fileGroups { - _ = try getGroups(path: basePath + group) + _ = try getGroups(path: spec.basePath + group) } let buildConfigs: [XCBuildConfiguration] = spec.configs.map { config in let buildSettings = spec.getProjectBuildSettings(basePath: basePath, config: config) var baseConfigurationReference: String? if let configPath = spec.configFiles[config.name] { - baseConfigurationReference = getFileReference(path: basePath + configPath, inPath: basePath) + baseConfigurationReference = getFileReference(path: spec.basePath + configPath, inPath: spec.basePath) } return XCBuildConfiguration(reference: generateUUID(XCBuildConfiguration.self, config.name), name: config.name, baseConfigurationReference: baseConfigurationReference, buildSettings: buildSettings) } @@ -105,11 +103,11 @@ public class PBXProjGenerator { if !carthageFrameworksByPlatform.isEmpty { var platforms: [PBXGroup] = [] for (platform, fileReferences) in carthageFrameworksByPlatform { - let platformGroup = PBXGroup(reference: generateUUID(PBXGroup.self, platform), children: fileReferences, sourceTree: .group, name: platform, path: platform) + let platformGroup = PBXGroup(reference: generateUUID(PBXGroup.self, platform), children: fileReferences.sorted(), sourceTree: .group, name: platform, path: platform) addObject(platformGroup) platforms.append(platformGroup) } - let carthageGroup = PBXGroup(reference: generateUUID(PBXGroup.self, "Carthage"), children: platforms.references, sourceTree: .group, name: "Carthage", path: carthageBuildPath) + let carthageGroup = PBXGroup(reference: generateUUID(PBXGroup.self, "Carthage"), children: platforms.references.sorted(), sourceTree: .group, name: "Carthage", path: carthageBuildPath) addObject(carthageGroup) frameworkFiles.append(carthageGroup.reference) } @@ -159,7 +157,7 @@ public class PBXProjGenerator { let carthageDependencies = getAllCarthageDependencies(target: target) - let sourcePaths = target.sources.map { basePath + $0 } + let sourcePaths = target.sources.map { spec.basePath + $0 } var sourceFiles: [SourceFile] = [] for source in sourcePaths { @@ -177,13 +175,13 @@ public class PBXProjGenerator { // automatically set INFOPLIST_FILE path if let plistPath = infoPlists.first, - !spec.targetHasBuildSetting("INFOPLIST_FILE", basePath: basePath, target: target, config: config) { - buildSettings["INFOPLIST_FILE"] = plistPath.byRemovingBase(path: basePath) + !spec.targetHasBuildSetting("INFOPLIST_FILE", basePath: spec.basePath, target: target, config: config) { + buildSettings["INFOPLIST_FILE"] = plistPath.byRemovingBase(path: spec.basePath) } // automatically calculate bundle id if let bundleIdPrefix = spec.options.bundleIdPrefix, - !spec.targetHasBuildSetting("PRODUCT_BUNDLE_IDENTIFIER", basePath: basePath, target: target, config: config) { + !spec.targetHasBuildSetting("PRODUCT_BUNDLE_IDENTIFIER", basePath: spec.basePath, target: target, config: config) { let characterSet = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-.")).inverted let escapedTargetName = target.name.replacingOccurrences(of: "_", with: "-").components(separatedBy: characterSet).joined(separator: "") buildSettings["PRODUCT_BUNDLE_IDENTIFIER"] = bundleIdPrefix + "." + escapedTargetName @@ -191,7 +189,7 @@ public class PBXProjGenerator { // automatically set test target name if target.type == .uiTestBundle, - !spec.targetHasBuildSetting("TEST_TARGET_NAME", basePath: basePath, target: target, config: config) { + !spec.targetHasBuildSetting("TEST_TARGET_NAME", basePath: spec.basePath, target: target, config: config) { for dependency in target.dependencies { if dependency.type == .target, let dependencyTarget = spec.getTarget(dependency.reference), @@ -219,7 +217,7 @@ public class PBXProjGenerator { var baseConfigurationReference: String? if let configPath = target.configFiles[config.name] { - baseConfigurationReference = getFileReference(path: basePath + configPath, inPath: basePath) + baseConfigurationReference = getFileReference(path: spec.basePath + configPath, inPath: spec.basePath) } return XCBuildConfiguration(reference: generateUUID(XCBuildConfiguration.self, config.name + target.name), name: config.name, baseConfigurationReference: baseConfigurationReference, buildSettings: buildSettings) } @@ -250,9 +248,9 @@ public class PBXProjGenerator { addObject(targetDependency) dependencies.append(targetDependency.reference) - if dependencyTarget.type.isLibrary || dependencyTarget.type.isFramework { + if (dependencyTarget.type.isLibrary || dependencyTarget.type.isFramework) && dependency.link { let dependencyBuildFile = targetBuildFiles[dependencyTargetName]! - let buildFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, dependencyBuildFile.reference + target.name), fileRef: dependencyBuildFile.fileRef) + let buildFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, dependencyBuildFile.reference + target.name), fileRef: dependencyBuildFile.fileRef!) addObject(buildFile) targetFrameworkBuildFiles.append(buildFile.reference) } @@ -277,7 +275,7 @@ public class PBXProjGenerator { case .framework: - let fileReference = getFileReference(path: Path(dependency.reference), inPath: basePath) + let fileReference = getFileReference(path: Path(dependency.reference), inPath: spec.basePath) let buildFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, fileReference + target.name), fileRef: fileReference) addObject(buildFile) @@ -293,9 +291,6 @@ public class PBXProjGenerator { copyFrameworksReferences.append(embedFile.reference) } case .carthage: - if carthageFrameworksByPlatform[target.platform.carthageDirectoryName] == nil { - carthageFrameworksByPlatform[target.platform.carthageDirectoryName] = [] - } var platformPath = Path(getCarthageBuildPath(platform: target.platform)) var frameworkPath = platformPath + dependency.reference if frameworkPath.extension == nil { @@ -305,7 +300,7 @@ public class PBXProjGenerator { let buildFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, fileReference + target.name), fileRef: fileReference) addObject(buildFile) - carthageFrameworksByPlatform[target.platform.carthageDirectoryName]?.append(fileReference) + carthageFrameworksByPlatform[target.platform.carthageDirectoryName, default: []].insert(fileReference) targetFrameworkBuildFiles.append(buildFile.reference) if target.platform == .macOS && target.type.isApp { @@ -330,7 +325,7 @@ public class PBXProjGenerator { var shellScript: String switch buildScript.script { case let .path(path): - shellScript = try (basePath + path).read() + shellScript = try (spec.basePath + path).read() case let .script(script): shellScript = script } @@ -430,11 +425,11 @@ public class PBXProjGenerator { let nativeTarget = PBXNativeTarget( reference: targetNativeReferences[target.name]!, + name: target.name, buildConfigurationList: buildConfigList.reference, buildPhases: buildPhases, buildRules: [], dependencies: dependencies, - name: target.name, productReference: fileReference, productType: target.type) addObject(nativeTarget) @@ -549,13 +544,11 @@ public class PBXProjGenerator { for localisedDirectory in localisedDirectories { let localisationName = localisedDirectory.lastComponentWithoutExtension for path in try localisedDirectory.children().sorted { $0.lastComponent < $1.lastComponent } { - - let filePath = "\(localisedDirectory.lastComponent)/\(path.lastComponent)" // find base localisation variant group let name = path.lastComponentWithoutExtension - let variantGroup = baseLocalisationVariantGroups.first { Path($0.name).lastComponentWithoutExtension == name } + let variantGroup = baseLocalisationVariantGroups.first { Path($0.name!).lastComponentWithoutExtension == name } let fileReference: String if let cachedFileReference = fileReferencesByPath[path] { @@ -571,7 +564,9 @@ public class PBXProjGenerator { } if let variantGroup = variantGroup { - variantGroup.children.append(fileReference) + if !variantGroup.children.contains(fileReference) { + variantGroup.children.append(fileReference) + } } else { // add SourceFile to group if there is no Base.lproj directory let buildFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, fileReference), @@ -583,7 +578,7 @@ public class PBXProjGenerator { } } - let groupPath: String = depth == 0 ? path.byRemovingBase(path: basePath).string : path.lastComponent + let groupPath: String = depth == 0 ? path.byRemovingBase(path: spec.basePath).string : path.lastComponent let group: PBXGroup if let cachedGroup = groupsByPath[path] { group = cachedGroup diff --git a/Sources/XcodeGenKit/ProjectGenerator.swift b/Sources/XcodeGenKit/ProjectGenerator.swift index cd804ec5..1670e7a8 100644 --- a/Sources/XcodeGenKit/ProjectGenerator.swift +++ b/Sources/XcodeGenKit/ProjectGenerator.swift @@ -16,12 +16,10 @@ import ProjectSpec public class ProjectGenerator { var spec: ProjectSpec - var path: Path let currentXcodeVersion = "0900" - public init(spec: ProjectSpec, path: Path) { + public init(spec: ProjectSpec) { self.spec = spec - self.path = path } var defaultDebugConfig: Config { @@ -33,8 +31,8 @@ public class ProjectGenerator { } public func generateProject() throws -> XcodeProj { - try spec.validate(path: path) - let pbxProjGenerator = PBXProjGenerator(spec: spec, path: path, currentXcodeVersion: currentXcodeVersion) + try spec.validate() + let pbxProjGenerator = PBXProjGenerator(spec: spec, currentXcodeVersion: currentXcodeVersion) let pbxProject = try pbxProjGenerator.generate() let workspace = try generateWorkspace() let sharedData = try generateSharedData(pbxProject: pbxProject) diff --git a/Sources/XcodeGenKit/SettingsBuilder.swift b/Sources/XcodeGenKit/SettingsBuilder.swift index cda32f6d..e9d085c9 100644 --- a/Sources/XcodeGenKit/SettingsBuilder.swift +++ b/Sources/XcodeGenKit/SettingsBuilder.swift @@ -18,7 +18,7 @@ extension ProjectSpec { public func getProjectBuildSettings(basePath: Path, config: Config) -> BuildSettings { var buildSettings: BuildSettings = [:] - if let type = config.type { + if let type = config.type, options.settingPresets.applyProject { buildSettings += SettingsPresetFile.base.getBuildSettings() buildSettings += SettingsPresetFile.config(type).getBuildSettings() } @@ -40,10 +40,12 @@ extension ProjectSpec { public func getTargetBuildSettings(basePath: Path, target: Target, config: Config) -> BuildSettings { var buildSettings = BuildSettings() - - buildSettings += SettingsPresetFile.platform(target.platform).getBuildSettings() - buildSettings += SettingsPresetFile.product(target.type).getBuildSettings() - buildSettings += SettingsPresetFile.productPlatform(target.type, target.platform).getBuildSettings() + + if options.settingPresets.applyTarget { + buildSettings += SettingsPresetFile.platform(target.platform).getBuildSettings() + buildSettings += SettingsPresetFile.product(target.type).getBuildSettings() + buildSettings += SettingsPresetFile.productPlatform(target.type, target.platform).getBuildSettings() + } buildSettings += getBuildSettings(settings: target.settings, config: config) // Do not overwrite target xcconfig's values. if let configPath = target.configFiles[config.name] { @@ -79,8 +81,10 @@ extension ProjectSpec { buildSettings += settings.buildSettings - if let configSettings = settings.configSettings[config.name] { - buildSettings += getBuildSettings(settings: configSettings, config: config) + for (configVariant, settings) in settings.configSettings { + if config.name.lowercased().contains(configVariant.lowercased()) { + buildSettings += getBuildSettings(settings: settings, config: config) + } } return buildSettings diff --git a/Sources/XcodeGenKit/SpecLoader.swift b/Sources/XcodeGenKit/SpecLoader.swift index cee23066..51f10c29 100644 --- a/Sources/XcodeGenKit/SpecLoader.swift +++ b/Sources/XcodeGenKit/SpecLoader.swift @@ -16,7 +16,7 @@ public struct SpecLoader { public static func loadSpec(path: Path) throws -> ProjectSpec { let dictionary = try loadDictionary(path: path) - return try ProjectSpec(jsonDictionary: dictionary) + return try ProjectSpec(basePath: path.parent(), jsonDictionary: dictionary) } private static func loadDictionary(path: Path) throws -> JSONDictionary { diff --git a/Sources/XcodeGenKit/SpecValidation.swift b/Sources/XcodeGenKit/SpecValidation.swift index e27375b3..d2841cdc 100644 --- a/Sources/XcodeGenKit/SpecValidation.swift +++ b/Sources/XcodeGenKit/SpecValidation.swift @@ -11,7 +11,7 @@ import PathKit extension ProjectSpec { - public mutating func validate(path: Path) throws { + public mutating func validate() throws { if configs.isEmpty { configs = [Config(name: "Debug", type: .debug), Config(name: "Release", type: .release)] @@ -28,17 +28,22 @@ extension ProjectSpec { errors.append(.invalidSettingsGroup(group)) } } + for config in settings.configSettings.keys { + if !configs.contains(where: { $0.name.lowercased().contains(config.lowercased())}) { + errors.append(.invalidConfigReference(config)) + } + } return errors } for fileGroup in fileGroups { - if !(path + fileGroup).exists { + if !(basePath + fileGroup).exists { errors.append(.invalidFileGroup(fileGroup)) } } for (config, configFile) in configFiles { - if !(path + configFile).exists { + if !(basePath + configFile).exists { errors.append(.invalidConfigFile(configFile: configFile, config: config)) } } @@ -55,7 +60,7 @@ extension ProjectSpec { } for (config, configFile) in target.configFiles { - if !(path + configFile).exists { + if !(basePath + configFile).exists { errors.append(.invalidTargetConfigFile(configFile: configFile, config: config, target: target.name)) } } @@ -67,7 +72,7 @@ extension ProjectSpec { } for source in target.sources { - let sourcePath = path + source + let sourcePath = basePath + source if !sourcePath.exists { errors.append(.missingTargetSource(target: target.name, source: sourcePath.string)) } @@ -84,6 +89,15 @@ extension ProjectSpec { } } + if scheme.configVariants.isEmpty { + if !configs.contains(where: { $0.type == .debug }) { + errors.append(.missingConfigTypeForGeneratedTargetScheme(target: target.name, configType: .debug)) + } + if !configs.contains(where: { $0.type == .release }) { + errors.append(.missingConfigTypeForGeneratedTargetScheme(target: target.name, configType: .release)) + } + } + for testTarget in scheme.testTargets { if getTarget(testTarget) == nil { errors.append(.invalidTargetSchemeTest(target: target.name, testTarget: testTarget)) @@ -94,7 +108,7 @@ extension ProjectSpec { let scripts = target.prebuildScripts + target.postbuildScripts for script in scripts { if case let .path(pathString) = script.script { - let scriptPath = path + pathString + let scriptPath = basePath + pathString if !scriptPath.exists { errors.append(.invalidBuildScriptPath(target: target.name, path: pathString)) } @@ -150,6 +164,8 @@ public struct SpecValidationError: Error, CustomStringConvertible { case invalidTargetSchemeConfigVariant(target: String, configVariant: String, configType: ConfigType) case invalidTargetSchemeTest(target: String, testTarget: String) case invalidFileGroup(String) + case invalidConfigReference(String) + case missingConfigTypeForGeneratedTargetScheme(target: String, configType: ConfigType) public var description: String { switch self { @@ -162,9 +178,11 @@ public struct SpecValidationError: Error, CustomStringConvertible { case let .missingTargetSource(target, source): return "Target \(target.quoted) has a missing source directory \(source.quoted)" case let .invalidSettingsGroup(group): return "Invalid settings group \(group.quoted)" case let .invalidBuildScriptPath(target, path): return "Target \(target.quoted) has a script path that doesn't exist \(path.quoted)" - case let .invalidTargetSchemeConfigVariant(target, configVariant, configType): return "Target \(target.quoted) has invalid scheme config varians which requires a config that has a \(configType.rawValue.quoted) type and contains the name \(configVariant.quoted)" + case let .invalidTargetSchemeConfigVariant(target, configVariant, configType): return "Target \(target.quoted) has an invalid scheme config variant which requires a config that has a \(configType.rawValue.quoted) type and contains the name \(configVariant.quoted)" case let .invalidTargetSchemeTest(target, test): return "Target \(target.quoted) scheme has invalid test \(test.quoted)" case let .invalidFileGroup(group): return "Invalid file group \(group.quoted)" + case let .invalidConfigReference(config): return "Invalid config reference \(config.quoted)" + case let .missingConfigTypeForGeneratedTargetScheme(target, configType): return "Target \(target.quoted) is missing a config of type \(configType.rawValue) to generate its scheme" } } } diff --git a/Tests/XcodeGenKitTests/FixtureTests.swift b/Tests/XcodeGenKitTests/FixtureTests.swift index 8fad8542..fe808cbc 100644 --- a/Tests/XcodeGenKitTests/FixtureTests.swift +++ b/Tests/XcodeGenKitTests/FixtureTests.swift @@ -7,8 +7,8 @@ import ProjectSpec let fixturePath = Path(#file).parent().parent().parent() + "Fixtures" func generate(specPath: Path, projectPath: Path) throws -> XcodeProj { - let spec = try ProjectSpec(path: specPath) - let generator = ProjectGenerator(spec: spec, path: specPath.parent()) + let spec = try SpecLoader.loadSpec(path: specPath) + let generator = ProjectGenerator(spec: spec) let project = try generator.generateProject() let oldProject = try XcodeProj(path: projectPath) try project.write(path: projectPath, override: true) diff --git a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift index 603434d2..19ca62a1 100644 --- a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift @@ -7,7 +7,7 @@ import ProjectSpec func projectGeneratorTests() { func getProject(_ spec: ProjectSpec) throws -> XcodeProj { - let generator = ProjectGenerator(spec: spec, path: Path("")) + let generator = ProjectGenerator(spec: spec) return try generator.generateProject() } @@ -31,22 +31,33 @@ func projectGeneratorTests() { $0.it("generates bundle id") { var options = ProjectSpec.Options() options.bundleIdPrefix = "com.test" - let spec = ProjectSpec(name: "test", targets: [framework], options: options) + let spec = ProjectSpec(basePath: "", name: "test", targets: [framework], options: options) let project = try getProject(spec) guard let target = project.pbxproj.nativeTargets.first, - let buildConfigs = project.pbxproj.configurationLists.getReference(target.buildConfigurationList), + let buildConfigList = target.buildConfigurationList, + let buildConfigs = project.pbxproj.configurationLists.getReference(buildConfigList), let buildConfigReference = buildConfigs.buildConfigurations.first, let buildConfig = project.pbxproj.buildConfigurations.getReference(buildConfigReference) else { - throw failure("Build Config not found") + throw failure("Build Config not found") } try expect(buildConfig.buildSettings["PRODUCT_BUNDLE_IDENTIFIER"] as? String) == "com.test.MyFramework" } + + $0.it("clears setting presets") { + var options = ProjectSpec.Options() + options.settingPresets = .none + let spec = ProjectSpec(basePath: "", name: "test", targets: [framework], options: options) + let project = try getProject(spec) + let allSettings = project.pbxproj.buildConfigurations.reduce([:]) { $0.merged($1.buildSettings)}.keys.sorted() + try expect(allSettings) == ["SETTING_2"] + } + } $0.describe("Config") { $0.it("generates config defaults") { - let spec = ProjectSpec(name: "test") + let spec = ProjectSpec(basePath: "", name: "test") let project = try getProject(spec) let configs = project.pbxproj.buildConfigurations try expect(configs.count) == 2 @@ -55,7 +66,7 @@ func projectGeneratorTests() { } $0.it("generates configs") { - let spec = ProjectSpec(name: "test", configs: [Config(name: "config1"), Config(name: "config2")]) + let spec = ProjectSpec(basePath: "", name: "test", configs: [Config(name: "config1"), Config(name: "config2")]) let project = try getProject(spec) let configs = project.pbxproj.buildConfigurations try expect(configs.count) == 2 @@ -64,7 +75,7 @@ func projectGeneratorTests() { } $0.it("clears config settings when missing type") { - let spec = ProjectSpec(name: "test", configs: [Config(name: "config")]) + let spec = ProjectSpec(basePath: "", name: "test", configs: [Config(name: "config")]) let project = try getProject(spec) guard let config = project.pbxproj.buildConfigurations.first else { throw failure("configuration not found") @@ -73,13 +84,12 @@ func projectGeneratorTests() { } $0.it("merges settings") { - let spec = try ProjectSpec(path: fixturePath + "settings_test.yml") - let basePath = Path(".") + let spec = try SpecLoader.loadSpec(path: fixturePath + "settings_test.yml") guard let config = spec.getConfig("config1") else { throw failure("Couldn't find config1") } - let debugProjectSettings = spec.getProjectBuildSettings(basePath: basePath, config: config) + let debugProjectSettings = spec.getProjectBuildSettings(config: config) guard let target = spec.getTarget("Target") else { throw failure("Couldn't find Target") } - let targetDebugSettings = spec.getTargetBuildSettings(basePath: basePath, target: target, config: config) + let targetDebugSettings = spec.getTargetBuildSettings(target: target, config: config) var buildSettings = BuildSettings() buildSettings += SettingsPresetFile.base.getBuildSettings() @@ -100,11 +110,22 @@ func projectGeneratorTests() { try expect(targetDebugSettings.equals(expectedTargetDebugSettings)).beTrue() } + + $0.it("applies partial config settings") { + let spec = ProjectSpec(basePath: "", name: "test", configs: [ + Config(name: "Staging Debug", type: .debug), + Config(name: "Staging Release", type: .release)], + settings: Settings(configSettings: ["staging": ["SETTING1": "VALUE1"], "debug": ["SETTING2": "VALUE2"]])) + + var buildSettings = spec.getProjectBuildSettings(config: spec.configs.first!) + try expect(buildSettings["SETTING1"] as? String) == "VALUE1" + try expect(buildSettings["SETTING2"] as? String) == "VALUE2" + } } $0.describe("Targets") { - let spec = ProjectSpec(name: "test", targets: targets) + let spec = ProjectSpec(basePath: "", name: "test", targets: targets) $0.it("generates targets") { let pbxProject = try getPbxProj(spec) @@ -155,7 +176,7 @@ func projectGeneratorTests() { let buildTarget = Scheme.BuildTarget(target: application.name) $0.it("generates scheme") { let scheme = Scheme(name: "MyScheme", build: Scheme.Build(targets: [buildTarget])) - let spec = ProjectSpec(name: "test", targets: [application, framework], schemes: [scheme]) + let spec = ProjectSpec(basePath: "", name: "test", targets: [application, framework], schemes: [scheme]) let project = try getProject(spec) guard let target = project.pbxproj.nativeTargets.first(where: { $0.name == application.name }) else { throw failure("Target not found") } guard let xcscheme = project.sharedData?.schemes.first else { throw failure("Scheme not found") } @@ -194,7 +215,7 @@ func projectGeneratorTests() { Config(name: "Production Release", type: .release), ] - let spec = ProjectSpec(name: "test", configs: configs, targets: [target, framework]) + let spec = ProjectSpec(basePath: "", name: "test", configs: configs, targets: [target, framework]) let project = try getProject(spec) try expect(project.sharedData?.schemes.count) == 2 diff --git a/Tests/XcodeGenKitTests/SpecLoadingTests.swift b/Tests/XcodeGenKitTests/SpecLoadingTests.swift index d7319601..60279410 100644 --- a/Tests/XcodeGenKitTests/SpecLoadingTests.swift +++ b/Tests/XcodeGenKitTests/SpecLoadingTests.swift @@ -12,7 +12,7 @@ func specLoadingTests() { for (key, value) in spec { specDictionary[key] = value } - return try ProjectSpec(jsonDictionary: specDictionary) + return try ProjectSpec(basePath: "", jsonDictionary: specDictionary) } func expectProjectSpecError(_ spec: [String: Any], _ expectedError: ProjectSpecError) throws { @@ -82,7 +82,7 @@ func specLoadingTests() { try expect(target.dependencies[2]) == Dependency(type: .framework, reference: "path") } - $0.it("parsed cross platform targets") { + $0.it("parses cross platform targets") { let targetDictionary: [String: Any] = [ "platform": ["iOS", "tvOS"], "type": "framework", @@ -128,7 +128,7 @@ func specLoadingTests() { } $0.it("parses settings") { - let spec = try ProjectSpec(path: fixturePath + "settings_test.yml") + let spec = try SpecLoader.loadSpec(path: fixturePath + "settings_test.yml") let buildSettings: BuildSettings = ["SETTING": "value"] let configSettings: [String: Settings] = ["config1": Settings(buildSettings: ["SETTING1": "value"])] let groups = ["preset1"] @@ -176,7 +176,7 @@ func specLoadingTests() { var options = ProjectSpec.Options() options.carthageBuildPath = "../Carthage/Build" options.bundleIdPrefix = "com.test" - let expected = ProjectSpec(name: "test", options: options) + let expected = ProjectSpec(basePath: "", name: "test", options: options) let parsedSpec = try getProjectSpec(["options": ["carthageBuildPath": "../Carthage/Build", "bundleIdPrefix": "com.test"]]) try expect(parsedSpec) == expected } diff --git a/docs/ProjectSpec.md b/docs/ProjectSpec.md index 2b1c8d97..05001f7a 100644 --- a/docs/ProjectSpec.md +++ b/docs/ProjectSpec.md @@ -64,9 +64,13 @@ Note that target names can also be changed by adding a `name` property to a targ ### Options - ⚪️ **carthageBuildPath**: `String` - The path to the carthage build directory. Defaults to `Carthage/Build`. This is used when specifying target carthage dependencies - ⚪️ **bundleIdPrefix**: `String` - If this is specified then any target that doesn't have an `PRODUCT_BUNDLE_IDENTIFIER` (via all levels of build settings) will get an autogenerated one by combining `bundleIdPrefix` and the target name: `bundleIdPrefix.name`. The target name will be stripped of all characters that aren't alphanumerics, hyphens, or periods. Underscores will be replace with hyphens. - +- ⚪️ **settingPresets**: `String` - This controls the settings that are automatically applied to the project and its targets. These are the same build settings that Xcode would add when creating a new project. Project settings are applied by config type. Target settings are applied by the product type and platform. By default this is set to `all` + - `all`: project and target settings + - `project`: only project settings + - `targets`: only target settings + - `none`: no settings are automatically applied ### Configs -Each config maps to a build type of either `debug` or `release` which will then apply default build settings. Any value other than `debug` or `release` (for example "none"), will mean no default build settings will be applied. +Each config maps to a build type of either `debug` or `release` which will then apply default build settings to the project. Any value other than `debug` or `release` (for example `none`), will mean no default build settings will be applied to the project. ```yaml configs: @@ -99,7 +103,7 @@ settingGroups: Settings can either be a simple map of build settings `[String: String]`, or can be more advanced with the following properties: - ⚪️ **groups**: `[String]` - List of setting groups to include and merge -- ⚪️ **configs**: [String: [Settings](#settings)] - Mapping of config name to a settings spec. These settings will only be applied for that config +- ⚪️ **configs**: [String: [Settings](#settings)] - Mapping of config name to a settings spec. These settings will only be applied for that config. Each key will be matched to any configs that contain the key and is case insensitive. So if you had `Staging Debug` and `Staging Release`, you could apply settings to both of them using `staging`. - ⚪️ **base**: `[String: String]` - Used to specify default settings that apply to any config ```yaml @@ -215,6 +219,7 @@ A dependency can be one of a 3 types: These only applied to `target` and `framework` dependencies. - ⚪️ **embed**: `Bool` - Whether to embed the dependency. Defaults to true for application target and false for non application targets. +- ⚪️ **link**: `Bool` - Whether to link the dependency. Defaults to true but only static library and dynamic frameworks are linked. This only applies for target dependencies. - ⚪️ **codeSign**: `Bool` - Whether the `codeSignOnCopy` setting is applied when embedding framework. Defaults to true - ⚪️ **removeHeaders**: `Bool` - Whether the `removeHeadersOnCopy` setting is applied when embedding the framework. Defaults to true