mirror of
https://github.com/yonaskolb/XcodeGen.git
synced 2026-03-18 20:02:25 +00:00
167d11998a
* Add explicitFolders support to syncedFolder Adds an `explicitFolders` property to `TargetSource` that is expanded from Glob patterns and passed through to `PBXFileSystemSynchronizedRootGroup`. * Fix syncedFolder sources ignoring createIntermediateGroups When createIntermediateGroups was enabled and a syncedFolder source had a multi-component path (e.g. SyncedParent/SyncedChild), two things went wrong: 1. The synced folder was unconditionally added to rootGroups, causing it to appear both at the project root and inside the correct intermediate parent group. 2. The synced folder kept its full project-relative path instead of being made relative to its parent group, so Xcode concatenated them into a wrong path (e.g. SyncedParent/SyncedParent/SyncedChild). * Enhance PBXFileElement to recognize synced folders as groups that can be sorted * Fix membership exceptions for nested synced folder with intermediate groups * Update Changelog
554 lines
22 KiB
Swift
554 lines
22 KiB
Swift
import Yams
|
|
import XCTest
|
|
import Spectre
|
|
import PathKit
|
|
import XcodeProj
|
|
import ProjectSpec
|
|
import XcodeGenKit
|
|
import TestSupport
|
|
|
|
extension Project {
|
|
|
|
func generateXcodeProject(validate: Bool = true, file: String = #file, line: Int = #line) throws -> XcodeProj {
|
|
try doThrowing(file: file, line: line) {
|
|
if validate {
|
|
try self.validate()
|
|
}
|
|
let generator = ProjectGenerator(project: self)
|
|
return try generator.generateXcodeProject(userName: "someUser")
|
|
}
|
|
}
|
|
|
|
func generatePbxProj(specValidate: Bool = true, projectValidate: Bool = true, file: String = #file, line: Int = #line) throws -> PBXProj {
|
|
try doThrowing(file: file, line: line) {
|
|
let xcodeProject = try generateXcodeProject(validate: specValidate).pbxproj
|
|
if projectValidate {
|
|
try xcodeProject.validate()
|
|
}
|
|
return xcodeProject
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension PBXProj {
|
|
|
|
// validates that a PBXProj is correct
|
|
// TODO: Use xclint?
|
|
func validate() throws {
|
|
let mainGroup = try getMainGroup()
|
|
|
|
func validateGroup(_ group: PBXGroup) throws {
|
|
|
|
// check for duplicte children
|
|
let dictionary = Dictionary(grouping: group.children) { $0.hashValue }
|
|
let mostChildren = dictionary.sorted { $0.value.count > $1.value.count }
|
|
if let first = mostChildren.first, first.value.count > 1 {
|
|
throw failure("Group \"\(group.nameOrPath)\" has duplicated children:\n - \(group.children.map { $0.nameOrPath }.joined(separator: "\n - "))")
|
|
}
|
|
for child in group.children {
|
|
if let group = child as? PBXGroup {
|
|
try validateGroup(group)
|
|
}
|
|
}
|
|
}
|
|
try validateGroup(mainGroup)
|
|
}
|
|
|
|
func getMainGroup(function: String = #function, file: String = #file, line: Int = #line) throws -> PBXGroup {
|
|
guard let mainGroup = projects.first?.mainGroup else {
|
|
throw failure("Couldn't find main group", file: file, line: line)
|
|
}
|
|
return mainGroup
|
|
}
|
|
|
|
}
|
|
|
|
class PBXProjGeneratorTests: XCTestCase {
|
|
|
|
func testGroupOrdering() {
|
|
describe {
|
|
let directoryPath = Path("TestDirectory")
|
|
|
|
func createDirectories(_ directories: String) throws {
|
|
let yaml = try Yams.load(yaml: directories)!
|
|
|
|
func getFiles(_ file: Any, path: Path) -> [Path] {
|
|
if let array = file as? [Any] {
|
|
return array.flatMap { getFiles($0, path: path) }
|
|
} else if let string = file as? String {
|
|
return [path + string]
|
|
} else if let dictionary = file as? [String: Any] {
|
|
var array: [Path] = []
|
|
for (key, value) in dictionary {
|
|
array += getFiles(value, path: path + key)
|
|
}
|
|
return array
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
|
|
let files = getFiles(yaml, path: directoryPath).filter { $0.extension != nil }
|
|
for file in files {
|
|
try file.parent().mkpath()
|
|
try file.write("")
|
|
}
|
|
}
|
|
|
|
func removeDirectories() {
|
|
try? directoryPath.delete()
|
|
}
|
|
|
|
$0.before {
|
|
removeDirectories()
|
|
}
|
|
|
|
$0.after {
|
|
removeDirectories()
|
|
}
|
|
|
|
$0.it("setups group ordering with groupSortPosition = .top") {
|
|
var options = SpecOptions()
|
|
options.groupSortPosition = .top
|
|
options.groupOrdering = [
|
|
GroupOrdering(
|
|
order: [
|
|
"Sources",
|
|
"Resources",
|
|
"Tests",
|
|
"Support files",
|
|
"Configurations",
|
|
]
|
|
),
|
|
GroupOrdering(
|
|
pattern: "^.*Screen$",
|
|
order: [
|
|
"View",
|
|
"Presenter",
|
|
"Interactor",
|
|
"Entities",
|
|
"Assembly",
|
|
]
|
|
),
|
|
]
|
|
|
|
let directories = """
|
|
Configurations:
|
|
- file.swift
|
|
Resources:
|
|
- file.swift
|
|
Sources:
|
|
- MainScreen:
|
|
- mainScreen1.swift
|
|
- mainScreen2.swift
|
|
- Assembly:
|
|
- file.swift
|
|
- Entities:
|
|
- file.swift
|
|
- Interactor:
|
|
- file.swift
|
|
- Presenter:
|
|
- file.swift
|
|
- View:
|
|
- file.swift
|
|
Support files:
|
|
- file.swift
|
|
Tests:
|
|
- file.swift
|
|
UITests:
|
|
- file.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Configurations", "Resources", "Sources", "Support files", "Tests", "UITests"])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: options)
|
|
let projGenerator = PBXProjGenerator(project: project)
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let group = try pbxProj.getMainGroup()
|
|
|
|
projGenerator.setupGroupOrdering(group: group)
|
|
|
|
let mainGroups = group.children.map { $0.nameOrPath }
|
|
try expect(mainGroups) == ["Sources", "Resources", "Tests", "Support files", "Configurations", "UITests", "Products"]
|
|
|
|
let screenGroups = group.children
|
|
.first { $0.nameOrPath == "Sources" }
|
|
.flatMap { $0 as? PBXGroup }?
|
|
.children
|
|
.first { $0.nameOrPath == "MainScreen" }
|
|
.flatMap { $0 as? PBXGroup }?
|
|
.children
|
|
.map { $0.nameOrPath }
|
|
try expect(screenGroups) == ["View", "Presenter", "Interactor", "Entities", "Assembly", "mainScreen1.swift", "mainScreen2.swift"]
|
|
}
|
|
|
|
$0.it("setups group ordering with groupSortPosition = .bottom") {
|
|
var options = SpecOptions()
|
|
options.groupSortPosition = .bottom
|
|
options.groupOrdering = [
|
|
GroupOrdering(
|
|
order: [
|
|
"Sources",
|
|
"Resources",
|
|
"Tests",
|
|
"Support files",
|
|
"Configurations",
|
|
]
|
|
),
|
|
GroupOrdering(
|
|
pattern: "^.*Screen$",
|
|
order: [
|
|
"View",
|
|
"Presenter",
|
|
"Interactor",
|
|
"Entities",
|
|
"Assembly",
|
|
]
|
|
),
|
|
]
|
|
|
|
let directories = """
|
|
Configurations:
|
|
- file.swift
|
|
Resources:
|
|
- file.swift
|
|
Sources:
|
|
- MainScreen:
|
|
- mainScreen1.swift
|
|
- mainScreen2.swift
|
|
- Assembly:
|
|
- file.swift
|
|
- Entities:
|
|
- file.swift
|
|
- Interactor:
|
|
- file.swift
|
|
- Presenter:
|
|
- file.swift
|
|
- View:
|
|
- file.swift
|
|
Support files:
|
|
- file.swift
|
|
Tests:
|
|
- file.swift
|
|
UITests:
|
|
- file.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Configurations", "Resources", "Sources", "Support files", "Tests", "UITests"])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: options)
|
|
let projGenerator = PBXProjGenerator(project: project)
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let group = try pbxProj.getMainGroup()
|
|
|
|
projGenerator.setupGroupOrdering(group: group)
|
|
|
|
let mainGroups = group.children.map { $0.nameOrPath }
|
|
try expect(mainGroups) == ["Sources", "Resources", "Tests", "Support files", "Configurations", "UITests", "Products"]
|
|
|
|
let screenGroups = group.children
|
|
.first { $0.nameOrPath == "Sources" }
|
|
.flatMap { $0 as? PBXGroup }?
|
|
.children
|
|
.first { $0.nameOrPath == "MainScreen" }
|
|
.flatMap { $0 as? PBXGroup }?
|
|
.children
|
|
.map { $0.nameOrPath }
|
|
try expect(screenGroups) == ["mainScreen1.swift", "mainScreen2.swift", "View", "Presenter", "Interactor", "Entities", "Assembly"]
|
|
}
|
|
|
|
$0.it("sorts SPM packages") {
|
|
var options = SpecOptions()
|
|
options.groupSortPosition = .top
|
|
options.groupOrdering = [
|
|
GroupOrdering(
|
|
order: [
|
|
"Sources",
|
|
"Resources",
|
|
"Tests",
|
|
"Packages",
|
|
"Support files",
|
|
"Configurations",
|
|
]
|
|
),
|
|
GroupOrdering(
|
|
pattern: "Packages",
|
|
order: [
|
|
"FeatureA",
|
|
"FeatureB",
|
|
"Common",
|
|
]
|
|
),
|
|
]
|
|
|
|
let directories = """
|
|
Configurations:
|
|
- file.swift
|
|
Resources:
|
|
- file.swift
|
|
Sources:
|
|
- MainScreen:
|
|
- mainScreen1.swift
|
|
- mainScreen2.swift
|
|
- Assembly:
|
|
- file.swift
|
|
- Entities:
|
|
- file.swift
|
|
- Interactor:
|
|
- file.swift
|
|
- Presenter:
|
|
- file.swift
|
|
- View:
|
|
- file.swift
|
|
Support files:
|
|
- file.swift
|
|
Packages:
|
|
- Common:
|
|
- Package.swift
|
|
- FeatureA:
|
|
- Package.swift
|
|
- FeatureB:
|
|
- Package.swift
|
|
Tests:
|
|
- file.swift
|
|
UITests:
|
|
- file.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Configurations", "Resources", "Sources", "Support files", "Tests", "UITests"])
|
|
let project = Project(
|
|
basePath: directoryPath,
|
|
name: "Test",
|
|
targets: [target],
|
|
packages: [
|
|
"Common": .local(path: "Packages/Common", group: nil, excludeFromProject: false),
|
|
"FeatureA": .local(path: "Packages/FeatureA", group: nil, excludeFromProject: false),
|
|
"FeatureB": .local(path: "Packages/FeatureB", group: nil, excludeFromProject: false),
|
|
],
|
|
options: options
|
|
)
|
|
let projGenerator = PBXProjGenerator(project: project)
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let group = try pbxProj.getMainGroup()
|
|
|
|
projGenerator.setupGroupOrdering(group: group)
|
|
|
|
let mainGroups = group.children.map { $0.nameOrPath }
|
|
try expect(mainGroups) == ["Sources", "Resources", "Tests", "Packages", "Support files", "Configurations", "UITests", "Products"]
|
|
|
|
let packages = group.children
|
|
.first { $0.nameOrPath == "Packages" }
|
|
.flatMap { $0 as? PBXGroup }?
|
|
.children
|
|
.map(\.nameOrPath)
|
|
|
|
try expect(packages) == ["FeatureA", "FeatureB", "Common"]
|
|
}
|
|
|
|
$0.it("sorts synced folders alongside groups") {
|
|
var options = SpecOptions()
|
|
options.groupSortPosition = .top
|
|
options.groupOrdering = [
|
|
GroupOrdering(
|
|
order: [
|
|
"Sources",
|
|
"SyncedSources",
|
|
"Resources",
|
|
]
|
|
),
|
|
]
|
|
|
|
let directories = """
|
|
Resources:
|
|
- file.swift
|
|
Sources:
|
|
- file.swift
|
|
SyncedSources:
|
|
- file.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(
|
|
name: "Test",
|
|
type: .application,
|
|
platform: .iOS,
|
|
sources: [
|
|
"Sources",
|
|
.init(path: "SyncedSources", type: .syncedFolder),
|
|
"Resources",
|
|
]
|
|
)
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: options)
|
|
let projGenerator = PBXProjGenerator(project: project)
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let group = try pbxProj.getMainGroup()
|
|
|
|
projGenerator.setupGroupOrdering(group: group)
|
|
|
|
let mainGroups = group.children.map { $0.nameOrPath }
|
|
try expect(mainGroups) == ["Sources", "SyncedSources", "Resources", "Products"]
|
|
}
|
|
}
|
|
}
|
|
|
|
func testDefaultLastUpgradeCheckWhenUserDidSpecifyInvalidValue() throws {
|
|
let lastUpgradeKey = "LastUpgradeCheck"
|
|
let attributes: [String: Any] = [lastUpgradeKey: 1234]
|
|
let project = Project(name: "Test", attributes: attributes)
|
|
let projGenerator = PBXProjGenerator(project: project)
|
|
|
|
let pbxProj = try projGenerator.generate()
|
|
|
|
for pbxProject in pbxProj.projects {
|
|
XCTAssertEqual(pbxProject.attributes[lastUpgradeKey]?.stringValue, project.xcodeVersion)
|
|
}
|
|
}
|
|
|
|
func testOverrideLastUpgradeCheckWhenUserDidSpecifyValue() throws {
|
|
let lastUpgradeKey = "LastUpgradeCheck"
|
|
let lastUpgradeValue = "1234"
|
|
let attributes: [String: Any] = [lastUpgradeKey: lastUpgradeValue]
|
|
let project = Project(name: "Test", attributes: attributes)
|
|
let projGenerator = PBXProjGenerator(project: project)
|
|
|
|
let pbxProj = try projGenerator.generate()
|
|
|
|
for pbxProject in pbxProj.projects {
|
|
XCTAssertEqual(pbxProject.attributes[lastUpgradeKey]?.stringValue, lastUpgradeValue)
|
|
}
|
|
}
|
|
|
|
func testDefaultLastUpgradeCheckWhenUserDidNotSpecifyValue() throws {
|
|
let lastUpgradeKey = "LastUpgradeCheck"
|
|
let project = Project(name: "Test")
|
|
let projGenerator = PBXProjGenerator(project: project)
|
|
|
|
let pbxProj = try projGenerator.generate()
|
|
|
|
for pbxProject in pbxProj.projects {
|
|
XCTAssertEqual(pbxProject.attributes[lastUpgradeKey]?.stringValue, project.xcodeVersion)
|
|
}
|
|
}
|
|
|
|
func testPlatformDependencies() {
|
|
describe {
|
|
let directoryPath = Path("TestDirectory")
|
|
|
|
func createDirectories(_ directories: String) throws {
|
|
let yaml = try Yams.load(yaml: directories)!
|
|
|
|
func getFiles(_ file: Any, path: Path) -> [Path] {
|
|
if let array = file as? [Any] {
|
|
return array.flatMap { getFiles($0, path: path) }
|
|
} else if let string = file as? String {
|
|
return [path + string]
|
|
} else if let dictionary = file as? [String: Any] {
|
|
var array: [Path] = []
|
|
for (key, value) in dictionary {
|
|
array += getFiles(value, path: path + key)
|
|
}
|
|
return array
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
|
|
let files = getFiles(yaml, path: directoryPath).filter { $0.extension != nil }
|
|
for file in files {
|
|
try file.parent().mkpath()
|
|
try file.write("")
|
|
}
|
|
}
|
|
|
|
func removeDirectories() {
|
|
try? directoryPath.delete()
|
|
}
|
|
|
|
$0.before {
|
|
removeDirectories()
|
|
}
|
|
|
|
$0.after {
|
|
removeDirectories()
|
|
}
|
|
|
|
$0.it("setups target with different dependencies") {
|
|
let directories = """
|
|
Sources:
|
|
- MainScreen:
|
|
- Entities:
|
|
- file.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
let target1 = Target(name: "TestAll", type: .application, platform: .iOS, sources: ["Sources"])
|
|
let target2 = Target(name: "TestiOS", type: .application, platform: .iOS, sources: ["Sources"])
|
|
let target3 = Target(name: "TestmacOS", type: .application, platform: .iOS, sources: ["Sources"])
|
|
let dependency1 = Dependency(type: .target, reference: "TestAll", platformFilter: .all)
|
|
let dependency2 = Dependency(type: .target, reference: "TestiOS", platformFilter: .iOS)
|
|
let dependency3 = Dependency(type: .target, reference: "TestmacOS", platformFilter: .macOS)
|
|
let dependency4 = Dependency(type: .package(products: ["Swinject"]), reference: "Swinject", platformFilter: .iOS)
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"], dependencies: [dependency1, dependency2, dependency3, dependency4])
|
|
let swinjectPackage = SwiftPackage.remote(url: "https://github.com/Swinject/Swinject", versionRequirement: .exact("2.8.0"))
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target, target1, target2, target3], packages: ["Swinject": swinjectPackage])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
|
|
let targets = pbxProj.projects.first?.targets
|
|
let testTarget = pbxProj.projects.first?.targets.first(where: { $0.name == "Test" })
|
|
let testTargetDependencies = testTarget?.dependencies
|
|
try expect(targets?.count) == 4
|
|
try expect(testTargetDependencies?.count) == 3
|
|
try expect(testTargetDependencies?[0].platformFilter).beNil()
|
|
try expect(testTargetDependencies?[1].platformFilter) == "ios"
|
|
try expect(testTargetDependencies?[2].platformFilter) == "maccatalyst"
|
|
try expect(testTarget?.frameworksBuildPhase()?.files?.count) == 1
|
|
try expect(testTarget?.frameworksBuildPhase()?.files?[0].platformFilter) == "ios"
|
|
}
|
|
|
|
$0.it("places resources before sources buildPhase") {
|
|
let directories = """
|
|
Sources:
|
|
- MainScreen:
|
|
- Entities:
|
|
- file.swift
|
|
- image.jpg
|
|
"""
|
|
try createDirectories(directories)
|
|
let target1 = Target(
|
|
name: "TestAll",
|
|
type: .application,
|
|
platform: .iOS,
|
|
sources: ["Sources"],
|
|
putResourcesBeforeSourcesBuildPhase: true
|
|
)
|
|
let target2 = Target(
|
|
name: "TestiOS",
|
|
type: .application,
|
|
platform: .iOS,
|
|
sources: ["Sources"],
|
|
putResourcesBeforeSourcesBuildPhase: false
|
|
)
|
|
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target1, target2])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
|
|
let targets = pbxProj.projects.first?.targets
|
|
try expect(targets?.count) == 2
|
|
try expect(targets?.first?.buildPhases.first).to.beOfType(PBXResourcesBuildPhase.self)
|
|
try expect(targets?.first?.buildPhases.last).to.beOfType(PBXSourcesBuildPhase.self)
|
|
|
|
try expect(targets?.last?.buildPhases.first).to.beOfType(PBXSourcesBuildPhase.self)
|
|
try expect(targets?.last?.buildPhases.last).to.beOfType(PBXResourcesBuildPhase.self)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|