mirror of
https://github.com/yonaskolb/XcodeGen.git
synced 2026-03-18 20:02:25 +00:00
a0afcefaed
* Handle membership exceptions for synchronized root groups Adds logic to detect and register membership exceptions for PBXFileSystemSynchronizedRootGroup objects, specifically excluding Info.plist files from group membership when necessary. Also ensures resources build phase is added if synchronized root groups are present. * Refactor synced folder membership exceptions with glob support Extract configureMembershipExceptions into its own method, use Set for dedup, resolve excludes via glob expansion, and add a no-op test case. Incorporates glob support and tests from macguru@baf1108. * Update UUID * Comment out excludes in project.yml Comment out excludes for ExcludedFile.swift due to CI issue. * Clean up project.pbxproj by removing exception set Removed PBXFileSystemSynchronizedBuildFileExceptionSet section and its references. * Remove comment * Update SourceGeneratorTests.swift * Update project.pbxproj * Retrigger CI * Add info.plist exclusion
1516 lines
71 KiB
Swift
1516 lines
71 KiB
Swift
import PathKit
|
|
import ProjectSpec
|
|
import Spectre
|
|
@testable import XcodeGenKit
|
|
import XcodeProj
|
|
import XCTest
|
|
import Yams
|
|
import TestSupport
|
|
|
|
class SourceGeneratorTests: XCTestCase {
|
|
|
|
func testSourceGenerator() throws {
|
|
try skipIfNecessary()
|
|
describe {
|
|
|
|
let directoryPath = Path("TestDirectory")
|
|
let outOfRootPath = Path("OtherDirectory")
|
|
|
|
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 createFile(at relativePath: Path, content: String) throws -> Path {
|
|
let filePath = directoryPath + relativePath
|
|
try filePath.parent().mkpath()
|
|
try filePath.write(content)
|
|
return filePath
|
|
}
|
|
|
|
func removeDirectories() {
|
|
try? directoryPath.delete()
|
|
try? outOfRootPath.delete()
|
|
}
|
|
|
|
$0.before {
|
|
removeDirectories()
|
|
}
|
|
|
|
$0.after {
|
|
removeDirectories()
|
|
}
|
|
|
|
$0.it("generates source groups") {
|
|
let directories = """
|
|
Sources:
|
|
A:
|
|
- a.swift
|
|
- B:
|
|
- b.swift
|
|
- C2.0:
|
|
- c.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["Sources", "A", "a.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["Sources", "A", "B", "b.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["Sources", "A", "C2.0", "c.swift"], buildPhase: .sources)
|
|
}
|
|
|
|
$0.it("generates synced folder") {
|
|
let directories = """
|
|
Sources:
|
|
A:
|
|
- a.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [.init(path: "Sources", type: .syncedFolder)])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
|
|
let syncedFolder = try unwrap(syncedFolders.first)
|
|
|
|
try expect([syncedFolder]) == pbxProj.nativeTargets.first?.fileSystemSynchronizedGroups
|
|
}
|
|
|
|
$0.it("respects defaultSourceDirectoryType") {
|
|
let directories = """
|
|
Sources:
|
|
A:
|
|
- a.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: .init(defaultSourceDirectoryType: .syncedFolder))
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
|
|
let syncedFolder = try unwrap(syncedFolders.first)
|
|
|
|
try expect([syncedFolder]) == pbxProj.nativeTargets.first?.fileSystemSynchronizedGroups
|
|
}
|
|
|
|
$0.it("adds excludes as membership exceptions for synced folder") {
|
|
let directories = """
|
|
Sources:
|
|
- a.swift
|
|
- b.swift
|
|
- Generated:
|
|
- c.generated.swift
|
|
- d.generated.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let source = TargetSource(path: "Sources", excludes: ["b.swift", "Generated/*.generated.swift"], type: .syncedFolder)
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [source])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
|
|
let syncedFolder = try unwrap(syncedFolders.first)
|
|
|
|
let exceptionSets = syncedFolder.exceptions?.compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet }
|
|
let exceptionSet = try unwrap(exceptionSets?.first)
|
|
let exceptions = try unwrap(exceptionSet.membershipExceptions)
|
|
|
|
try expect(exceptions.contains("b.swift")) == true
|
|
try expect(exceptions.contains("Generated/c.generated.swift")) == true
|
|
try expect(exceptions.contains("Generated/d.generated.swift")) == true
|
|
try expect(exceptions.contains("a.swift")) == false
|
|
}
|
|
|
|
$0.it("auto-excludes Info.plist from synced folder membership") {
|
|
let directories = """
|
|
Sources:
|
|
- a.swift
|
|
- Info.plist
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let source = TargetSource(path: "Sources", type: .syncedFolder)
|
|
let target = Target(
|
|
name: "Test",
|
|
type: .application,
|
|
platform: .iOS,
|
|
settings: try Settings(jsonDictionary: ["INFOPLIST_FILE": "Sources/Info.plist"]),
|
|
sources: [source]
|
|
)
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
|
|
let syncedFolder = try unwrap(syncedFolders.first)
|
|
|
|
let exceptionSets = syncedFolder.exceptions?.compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet }
|
|
let exceptionSet = try unwrap(exceptionSets?.first)
|
|
let exceptions = try unwrap(exceptionSet.membershipExceptions)
|
|
|
|
try expect(exceptions.contains("Info.plist")) == true
|
|
}
|
|
|
|
$0.it("creates no exception set for synced folder without excludes") {
|
|
let directories = """
|
|
Sources:
|
|
- a.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let source = TargetSource(path: "Sources", type: .syncedFolder)
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [source])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
|
|
let syncedFolder = try unwrap(syncedFolders.first)
|
|
|
|
try expect(syncedFolder.exceptions?.isEmpty ?? true) == true
|
|
}
|
|
|
|
$0.it("adds empty resources build phase for synced folder") {
|
|
let directories = """
|
|
Sources:
|
|
- a.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let source = TargetSource(path: "Sources", type: .syncedFolder)
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [source])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let nativeTarget = try unwrap(pbxProj.nativeTargets.first)
|
|
let hasResourcesPhase = nativeTarget.buildPhases.contains { $0 is PBXResourcesBuildPhase }
|
|
try expect(hasResourcesPhase) == true
|
|
}
|
|
|
|
$0.it("supports frameworks in sources") {
|
|
let directories = """
|
|
Sources:
|
|
- Foo.framework
|
|
- Bar.swift
|
|
"""
|
|
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["Sources", "Bar.swift"], buildPhase: .sources)
|
|
let buildPhase = pbxProj.copyFilesBuildPhases.first
|
|
try expect(buildPhase?.dstSubfolderSpec) == .frameworks
|
|
let fileReference = pbxProj.getFileReference(
|
|
paths: ["Sources", "Foo.framework"],
|
|
names: ["Sources", "Foo.framework"]
|
|
)
|
|
let buildFile = try unwrap(pbxProj.buildFiles
|
|
.first(where: { $0.file == fileReference }))
|
|
try expect(buildPhase?.files?.count) == 1
|
|
try expect(buildPhase?.files?.contains(buildFile)) == true
|
|
}
|
|
|
|
$0.it("generates core data models") {
|
|
let directories = """
|
|
Sources:
|
|
model.xcdatamodeld:
|
|
- .xccurrentversion
|
|
- model.xcdatamodel
|
|
- model1.xcdatamodel
|
|
- model2.xcdatamodel
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let fileReference = try unwrap(pbxProj.fileReferences.first(where: { $0.nameOrPath == "model2.xcdatamodel" }))
|
|
let versionGroup = try unwrap(pbxProj.versionGroups.first)
|
|
try expect(versionGroup.currentVersion) == fileReference
|
|
try expect(versionGroup.children.count) == 3
|
|
try expect(versionGroup.path) == "model.xcdatamodeld"
|
|
try expect(fileReference.path) == "model2.xcdatamodel"
|
|
}
|
|
|
|
$0.it("generates core data mapping models") {
|
|
let directories = """
|
|
Sources:
|
|
model.xcmappingmodel:
|
|
- xcmapping.xml
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["Sources", "model.xcmappingmodel"], buildPhase: .sources)
|
|
}
|
|
|
|
$0.it("generates variant groups") {
|
|
let directories = """
|
|
Sources:
|
|
Base.lproj:
|
|
- LocalizedStoryboard.storyboard
|
|
en.lproj:
|
|
- LocalizedStoryboard.strings
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
|
|
func getFileReferences(_ path: String) -> [PBXFileReference] {
|
|
pbxProj.fileReferences.filter { $0.path == path }
|
|
}
|
|
|
|
func getVariableGroups(_ name: String?) -> [PBXVariantGroup] {
|
|
pbxProj.variantGroups.filter { $0.name == name }
|
|
}
|
|
|
|
let resourceName = "LocalizedStoryboard.storyboard"
|
|
let baseResource = "Base.lproj/LocalizedStoryboard.storyboard"
|
|
let localizedResource = "en.lproj/LocalizedStoryboard.strings"
|
|
|
|
let variableGroup = try unwrap(getVariableGroups(resourceName).first)
|
|
|
|
do {
|
|
let refs = getFileReferences(baseResource)
|
|
try expect(refs.count) == 1
|
|
try expect(variableGroup.children.filter { $0 == refs.first }.count) == 1
|
|
}
|
|
|
|
do {
|
|
let refs = getFileReferences(localizedResource)
|
|
try expect(refs.count) == 1
|
|
try expect(variableGroup.children.filter { $0 == refs.first }.count) == 1
|
|
}
|
|
}
|
|
|
|
$0.it("handles localized resources") {
|
|
let directories = """
|
|
App:
|
|
Resources:
|
|
en-CA.lproj:
|
|
- empty.json
|
|
- Localizable.strings
|
|
en-US.lproj:
|
|
- empty.json
|
|
- Localizable.strings
|
|
en.lproj:
|
|
- empty.json
|
|
- Localizable.strings
|
|
fonts:
|
|
SFUI:
|
|
- SFUILight.ttf
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [TargetSource(path: "App/Resources")])
|
|
|
|
let options = SpecOptions(createIntermediateGroups: true)
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: options)
|
|
|
|
let outputXcodeProj = try project.generateXcodeProject()
|
|
try outputXcodeProj.write(path: directoryPath)
|
|
|
|
let inputXcodeProj = try XcodeProj(path: directoryPath)
|
|
let pbxProj = inputXcodeProj.pbxproj
|
|
|
|
func getFileReferences(_ path: String) -> [PBXFileReference] {
|
|
pbxProj.fileReferences.filter { $0.path == path }
|
|
}
|
|
|
|
func getVariableGroups(_ name: String?) -> [PBXVariantGroup] {
|
|
pbxProj.variantGroups.filter { $0.name == name }
|
|
}
|
|
|
|
let stringsResourceName = "Localizable.strings"
|
|
let jsonResourceName = "empty.json"
|
|
|
|
let stringsVariableGroup = try unwrap(getVariableGroups(stringsResourceName).first)
|
|
|
|
let jsonVariableGroup = try unwrap(getVariableGroups(jsonResourceName).first)
|
|
|
|
let stringsResource = "en.lproj/Localizable.strings"
|
|
let jsonResource = "en-CA.lproj/empty.json"
|
|
|
|
do {
|
|
let refs = getFileReferences(stringsResource)
|
|
try expect(refs.count) == 1
|
|
try expect(refs.first!.uuid.hasPrefix("TEMP")) == false
|
|
try expect(stringsVariableGroup.children.filter { $0 == refs.first }.count) == 1
|
|
}
|
|
|
|
do {
|
|
let refs = getFileReferences(jsonResource)
|
|
try expect(refs.count) == 1
|
|
try expect(refs.first!.uuid.hasPrefix("TEMP")) == false
|
|
try expect(jsonVariableGroup.children.filter { $0 == refs.first }.count) == 1
|
|
}
|
|
}
|
|
|
|
$0.it("handles duplicate names") {
|
|
let directories = """
|
|
Sources:
|
|
- a.swift
|
|
- a:
|
|
- a.swift
|
|
- a:
|
|
- a.swift
|
|
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"])
|
|
let project = Project(
|
|
basePath: directoryPath,
|
|
name: "Test",
|
|
targets: [target],
|
|
fileGroups: ["Sources"]
|
|
)
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["Sources", "a.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["Sources", "a", "a.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["Sources", "a", "a", "a.swift"], buildPhase: .sources)
|
|
}
|
|
|
|
$0.it("renames sources") {
|
|
let directories = """
|
|
Sources:
|
|
- a.swift
|
|
OtherSource:
|
|
- b.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
|
|
TargetSource(path: "Sources", name: "NewSource"),
|
|
TargetSource(path: "OtherSource/b.swift", name: "c.swift"),
|
|
])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["Sources", "a.swift"], names: ["NewSource", "a.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["OtherSource", "b.swift"], names: ["OtherSource", "c.swift"], buildPhase: .sources)
|
|
}
|
|
|
|
$0.it("excludes sources") {
|
|
let directories = """
|
|
Sources:
|
|
- A:
|
|
- a.swift
|
|
- B:
|
|
- b.swift
|
|
- b.ignored
|
|
- b.alsoIgnored
|
|
- a.ignored
|
|
- a.alsoIgnored
|
|
- B:
|
|
- b.swift
|
|
- D:
|
|
- d.h
|
|
- d.m
|
|
- E:
|
|
- e.jpg
|
|
- e.h
|
|
- e.m
|
|
- F:
|
|
- f.swift
|
|
- G:
|
|
- H:
|
|
- h.swift
|
|
- types:
|
|
- a.swift
|
|
- a.m
|
|
- a.h
|
|
- a.x
|
|
- numbers:
|
|
- file1.a
|
|
- file2.a
|
|
- file3.a
|
|
- file4.a
|
|
- partial:
|
|
- file_part
|
|
- ignore.file
|
|
- a.ignored
|
|
- project.xcodeproj:
|
|
- project.pbxproj
|
|
- a.playground:
|
|
- Sources:
|
|
- a.swift
|
|
- Resources
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let excludes = [
|
|
"B",
|
|
"d.m",
|
|
"E/F/*.swift",
|
|
"G/H/",
|
|
"types/*.[hx]",
|
|
"numbers/file[2-3].a",
|
|
"partial/*_part",
|
|
"ignore.file",
|
|
"*.ignored",
|
|
"*.xcodeproj",
|
|
"*.playground",
|
|
"**/*.ignored",
|
|
"A/B/**/*.alsoIgnored",
|
|
]
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [TargetSource(path: "Sources", excludes: excludes)])
|
|
|
|
func test(generateEmptyDirectories: Bool) throws {
|
|
let options = SpecOptions(generateEmptyDirectories: generateEmptyDirectories)
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: options)
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["Sources", "A", "a.swift"])
|
|
try pbxProj.expectFile(paths: ["Sources", "A", "a.alsoIgnored"])
|
|
try pbxProj.expectFile(paths: ["Sources", "D", "d.h"])
|
|
try pbxProj.expectFile(paths: ["Sources", "D", "d.m"])
|
|
try pbxProj.expectFile(paths: ["Sources", "E", "e.jpg"])
|
|
try pbxProj.expectFile(paths: ["Sources", "E", "e.m"])
|
|
try pbxProj.expectFile(paths: ["Sources", "E", "e.h"])
|
|
try pbxProj.expectFile(paths: ["Sources", "types", "a.swift"])
|
|
try pbxProj.expectFile(paths: ["Sources", "numbers", "file1.a"])
|
|
try pbxProj.expectFile(paths: ["Sources", "numbers", "file4.a"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "B", "b.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "E", "F", "f.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "G", "H", "h.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "types", "a.h"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "types", "a.x"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "numbers", "file2.a"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "numbers", "file3.a"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "partial", "file_part"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "a.ignored"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "ignore.file"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "project.xcodeproj"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "a.playground"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "A", "a.ignored"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "A", "B", "b.ignored"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "A", "B", "b.alsoIgnored"])
|
|
}
|
|
|
|
try test(generateEmptyDirectories: false)
|
|
try test(generateEmptyDirectories: true)
|
|
}
|
|
|
|
$0.it("excludes certain ignored files") {
|
|
let directories = """
|
|
Sources:
|
|
A:
|
|
- a.swift
|
|
- .DS_Store
|
|
- a.swift.orig
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [TargetSource(path: "Sources")])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["Sources", "A", "a.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "A", ".DS_Store"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "A", "a.swift.orig"])
|
|
}
|
|
|
|
$0.it("generates file sources") {
|
|
let directories = """
|
|
Sources:
|
|
A:
|
|
- a.swift
|
|
- Assets.xcassets
|
|
- B:
|
|
- b.swift
|
|
- c.jpg
|
|
- D2.0:
|
|
- d.swift
|
|
- E.bundle:
|
|
- e.json
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
|
|
"Sources/A/a.swift",
|
|
"Sources/A/B/b.swift",
|
|
"Sources/A/D2.0/d.swift",
|
|
"Sources/A/Assets.xcassets",
|
|
"Sources/A/E.bundle/e.json",
|
|
"Sources/A/B/c.jpg",
|
|
])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["Sources/A", "a.swift"], names: ["A", "a.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["Sources/A/B", "b.swift"], names: ["B", "b.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["Sources/A/D2.0", "d.swift"], names: ["D2.0", "d.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["Sources/A/B", "c.jpg"], names: ["B", "c.jpg"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["Sources/A", "Assets.xcassets"], names: ["A", "Assets.xcassets"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["Sources/A/E.bundle", "e.json"], names: ["E.bundle", "e.json"], buildPhase: .resources)
|
|
}
|
|
|
|
$0.it("generates shared sources") {
|
|
let directories = """
|
|
Sources:
|
|
A:
|
|
- a.swift
|
|
- B:
|
|
- b.swift
|
|
- c.jpg
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target1 = Target(name: "Test1", type: .framework, platform: .iOS, sources: ["Sources"])
|
|
let target2 = Target(name: "Test2", type: .framework, platform: .tvOS, sources: ["Sources"])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target1, target2])
|
|
|
|
_ = try project.generatePbxProj()
|
|
// TODO: check there are build files for both targets
|
|
}
|
|
|
|
$0.it("generates intermediate groups") {
|
|
|
|
let directories = """
|
|
Sources:
|
|
A:
|
|
- b.swift
|
|
F:
|
|
- G:
|
|
- h.swift
|
|
B:
|
|
- b.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
let outOfSourceFile = outOfRootPath + "C/D/e.swift"
|
|
try outOfSourceFile.parent().mkpath()
|
|
try outOfSourceFile.write("")
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
|
|
"Sources/A/b.swift",
|
|
"Sources/F/G/h.swift",
|
|
"../OtherDirectory/C/D/e.swift",
|
|
TargetSource(path: "Sources/B", createIntermediateGroups: false),
|
|
])
|
|
let options = SpecOptions(createIntermediateGroups: true)
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: options)
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["Sources", "A", "b.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["Sources", "F", "G", "h.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["..", "OtherDirectory", "C", "D", "e.swift"], names: [".", "OtherDirectory", "C", "D", "e.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["Sources/B", "b.swift"], names: ["B", "b.swift"], buildPhase: .sources)
|
|
}
|
|
|
|
$0.it("generates custom groups") {
|
|
|
|
let directories = """
|
|
- Sources:
|
|
- a.swift
|
|
- A:
|
|
- b.swift
|
|
- F:
|
|
- G:
|
|
- h.swift
|
|
- i.swift
|
|
- B:
|
|
- b.swift
|
|
- C:
|
|
- c.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
|
|
TargetSource(path: "Sources/a.swift", group: "CustomGroup1"),
|
|
TargetSource(path: "Sources/A/b.swift", group: "CustomGroup1"),
|
|
TargetSource(path: "Sources/F/G/h.swift", group: "CustomGroup1"),
|
|
TargetSource(path: "Sources/B", group: "CustomGroup2", createIntermediateGroups: false),
|
|
TargetSource(path: "Sources/F/G/i.swift", group: "Sources/F/G/CustomGroup3"),
|
|
])
|
|
|
|
let options = SpecOptions(createIntermediateGroups: true)
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: options)
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["CustomGroup1", "Sources/a.swift"], names: ["CustomGroup1", "a.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["CustomGroup1", "Sources/A/b.swift"], names: ["CustomGroup1", "b.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["CustomGroup1", "Sources/F/G/h.swift"], names: ["CustomGroup1", "h.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["Sources", "F", "G", "CustomGroup3", "i.swift"], names: ["Sources", "F", "G", "CustomGroup3", "i.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["CustomGroup2", "Sources/B", "b.swift"], names: ["CustomGroup2", "B", "b.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["CustomGroup2", "Sources/B", "C", "c.swift"], names: ["CustomGroup2", "B", "C", "c.swift"], buildPhase: .sources)
|
|
}
|
|
|
|
$0.it("generates folder references") {
|
|
let directories = """
|
|
Sources:
|
|
A:
|
|
- a.resource
|
|
- b.resource
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
|
|
TargetSource(path: "Sources/A", type: .folder),
|
|
])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["Sources/A"], names: ["A"], buildPhase: .resources)
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "A", "a.swift"])
|
|
}
|
|
|
|
$0.it("adds files to correct build phase") {
|
|
let directories = """
|
|
A:
|
|
- file.swift
|
|
- file.xcassets
|
|
- file.h
|
|
- GoogleService-Info.plist
|
|
- file.xcconfig
|
|
- Localizable.xcstrings
|
|
B:
|
|
- file.swift
|
|
- file.xcassets
|
|
- file.h
|
|
- Sample.plist
|
|
- file.xcconfig
|
|
C:
|
|
- file.swift
|
|
- file.m
|
|
- file.mm
|
|
- file.cpp
|
|
- file.c
|
|
- file.S
|
|
- file.h
|
|
- file.hh
|
|
- file.hpp
|
|
- file.ipp
|
|
- file.tpp
|
|
- file.hxx
|
|
- file.def
|
|
- file.xcconfig
|
|
- file.entitlements
|
|
- file.gpx
|
|
- file.apns
|
|
- file.123
|
|
- file.xcassets
|
|
- file.metal
|
|
- file.mlmodel
|
|
- file.mlpackage
|
|
- file.mlmodelc
|
|
- Info.plist
|
|
- Intent.intentdefinition
|
|
- Configuration.storekit
|
|
- Settings.bundle:
|
|
- en.lproj:
|
|
- Root.strings
|
|
- Root.plist
|
|
- WithPeriod2.0:
|
|
- file.swift
|
|
- Documentation.docc
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .framework, platform: .iOS, sources: [
|
|
TargetSource(path: "A", buildPhase: .resources),
|
|
TargetSource(path: "B", buildPhase: BuildPhaseSpec.none),
|
|
TargetSource(path: "C", buildPhase: nil),
|
|
])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["A", "file.swift"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["A", "file.xcassets"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["A", "file.h"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["A", "GoogleService-Info.plist"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["A", "file.xcconfig"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["A", "Localizable.xcstrings"], buildPhase: .resources)
|
|
|
|
try pbxProj.expectFile(paths: ["B", "file.swift"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["B", "file.xcassets"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["B", "file.h"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["B", "Sample.plist"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["B", "file.xcconfig"], buildPhase: BuildPhaseSpec.none)
|
|
|
|
try pbxProj.expectFile(paths: ["C", "file.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["C", "file.m"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["C", "file.mm"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["C", "file.cpp"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["C", "file.c"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["C", "file.S"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["C", "file.h"], buildPhase: .headers)
|
|
try pbxProj.expectFile(paths: ["C", "file.hh"], buildPhase: .headers)
|
|
try pbxProj.expectFile(paths: ["C", "file.hpp"], buildPhase: .headers)
|
|
try pbxProj.expectFile(paths: ["C", "file.ipp"], buildPhase: .headers)
|
|
try pbxProj.expectFile(paths: ["C", "file.tpp"], buildPhase: .headers)
|
|
try pbxProj.expectFile(paths: ["C", "file.hxx"], buildPhase: .headers)
|
|
try pbxProj.expectFile(paths: ["C", "file.def"], buildPhase: .headers)
|
|
try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["C", "file.entitlements"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["C", "file.gpx"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["C", "file.apns"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["C", "file.xcassets"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["C", "file.123"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["C", "Info.plist"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["C", "file.metal"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["C", "file.mlmodel"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["C", "file.mlpackage"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["C", "file.mlmodelc"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["C", "Intent.intentdefinition"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["C", "Configuration.storekit"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["C", "Settings.bundle"], buildPhase: .resources)
|
|
try pbxProj.expectFileMissing(paths: ["C", "Settings.bundle", "en.lproj"])
|
|
try pbxProj.expectFileMissing(paths: ["C", "Settings.bundle", "en.lproj", "Root.strings"])
|
|
try pbxProj.expectFileMissing(paths: ["C", "Settings.bundle", "Root.plist"])
|
|
try pbxProj.expectFileMissing(paths: ["C", "WithPeriod2.0"])
|
|
try pbxProj.expectFile(paths: ["C", "WithPeriod2.0", "file.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["C", "Documentation.docc"], buildPhase: .sources)
|
|
}
|
|
|
|
$0.it("only omits the defined Info.plist from resource build phases but not other plists") {
|
|
try createDirectories("""
|
|
A:
|
|
- A-Info.plist
|
|
B:
|
|
- Info.plist
|
|
- GoogleServices-Info.plist
|
|
C:
|
|
- Info.plist
|
|
- Info-Production.plist
|
|
D:
|
|
- Info-Staging.plist
|
|
- Info-Production.plist
|
|
""")
|
|
|
|
// Explicit plist.path value is respected
|
|
let targetA = Target(
|
|
name: "A",
|
|
type: .application,
|
|
platform: .iOS,
|
|
sources: ["A"],
|
|
info: Plist(path: "A/A-Info.plist")
|
|
)
|
|
|
|
// Automatically picks first 'Info.plist' at the top-level
|
|
let targetB = Target(
|
|
name: "B",
|
|
type: .application,
|
|
platform: .iOS,
|
|
sources: ["B"]
|
|
)
|
|
|
|
// Also respects INFOPLIST_FILE, ignores other files named Info.plist
|
|
let targetC = Target(
|
|
name: "C",
|
|
type: .application,
|
|
platform: .iOS,
|
|
settings: Settings(buildSettings: [
|
|
"INFOPLIST_FILE": "C/Info-Production.plist"
|
|
]),
|
|
sources: ["C"]
|
|
)
|
|
|
|
// Does not support INFOPLIST_FILE value that requires expanding
|
|
let targetD = Target(
|
|
name: "D",
|
|
type: .application,
|
|
platform: .iOS,
|
|
settings: Settings(buildSettings: [
|
|
"ENVIRONMENT": "Production",
|
|
"INFOPLIST_FILE": "D/Info-${ENVIRONMENT}.plist"
|
|
]),
|
|
sources: ["D"]
|
|
)
|
|
|
|
let project = Project(basePath: directoryPath.absolute(), name: "Test", targets: [targetA, targetB, targetC, targetD])
|
|
let pbxProj = try project.generatePbxProj()
|
|
|
|
try pbxProj.expectFile(paths: ["A", "A-Info.plist"], buildPhase: BuildPhaseSpec.none)
|
|
|
|
try pbxProj.expectFile(paths: ["B", "Info.plist"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["B", "GoogleServices-Info.plist"], buildPhase: .resources)
|
|
|
|
try pbxProj.expectFile(paths: ["C", "Info.plist"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["C", "Info-Production.plist"], buildPhase: BuildPhaseSpec.none)
|
|
|
|
try pbxProj.expectFile(paths: ["D", "Info-Staging.plist"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["D", "Info-Production.plist"], buildPhase: .resources)
|
|
}
|
|
|
|
$0.it("sets file type properties") {
|
|
let directories = """
|
|
A:
|
|
- file.resource1
|
|
- file.source1
|
|
- file.abc:
|
|
- file.a
|
|
- file.exclude1
|
|
- file.unphased1
|
|
- ignored.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .framework, platform: .iOS, sources: [
|
|
TargetSource(path: "A"),
|
|
])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: .init(fileTypes: [
|
|
"abc": FileType(buildPhase: .sources),
|
|
"source1": FileType(buildPhase: .sources, attributes: ["a1", "a2"], resourceTags: ["r1", "r2"], compilerFlags: ["-c1", "-c2"]),
|
|
"resource1": FileType(buildPhase: .resources, attributes: ["a1", "a2"], resourceTags: ["r1", "r2"], compilerFlags: ["-c1", "-c2"]),
|
|
"unphased1": FileType(buildPhase: BuildPhaseSpec.none),
|
|
"swift": FileType(buildPhase: .resources),
|
|
]))
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["A", "file.abc"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["A", "file.source1"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["A", "file.resource1"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["A", "file.unphased1"], buildPhase: BuildPhaseSpec.none)
|
|
try pbxProj.expectFile(paths: ["A", "ignored.swift"], buildPhase: .resources)
|
|
|
|
do {
|
|
let fileReference = try unwrap(pbxProj.getFileReference(paths: ["A", "file.resource1"], names: ["A", "file.resource1"]))
|
|
let buildFile = try unwrap(pbxProj.buildFiles.first(where: { $0.file === fileReference }))
|
|
let settings = buildFile.settings ?? [:]
|
|
try expect(settings["ATTRIBUTES"]?.arrayValue) == ["a1", "a2"]
|
|
try expect(settings["ASSET_TAGS"]?.arrayValue) == ["r1", "r2"]
|
|
}
|
|
do {
|
|
let fileReference = try unwrap(pbxProj.getFileReference(paths: ["A", "file.source1"], names: ["A", "file.source1"]))
|
|
let buildFile = try unwrap(pbxProj.buildFiles.first(where: { $0.file === fileReference }))
|
|
let settings = buildFile.settings ?? [:]
|
|
try expect(settings["ATTRIBUTES"]?.arrayValue) == ["a1", "a2"]
|
|
try expect(settings["COMPILER_FLAGS"]?.stringValue) == "-c1 -c2"
|
|
}
|
|
}
|
|
|
|
$0.it("duplicate TargetSource is included once in sources build phase") {
|
|
let directories = """
|
|
Sources:
|
|
A:
|
|
- a.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
|
|
"Sources/A/a.swift",
|
|
"Sources/A/a.swift",
|
|
])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["Sources/A", "a.swift"], names: ["A", "a.swift"], buildPhase: .sources)
|
|
|
|
let sourcesBuildPhase = pbxProj.buildPhases.first(where: { $0.buildPhase == BuildPhase.sources })!
|
|
|
|
try expect(sourcesBuildPhase.files?.count) == 1
|
|
}
|
|
|
|
$0.it("add only carthage dependencies with same platform") {
|
|
let directories = """
|
|
A:
|
|
- file.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let watchTarget = Target(name: "Watch", type: .watch2App, platform: .watchOS, sources: ["A"], dependencies: [Dependency(type: .carthage(findFrameworks: false, linkType: .dynamic), reference: "Alamofire_watch")])
|
|
let watchDependency = Dependency(type: .target, reference: "Watch")
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["A"], dependencies: [Dependency(type: .carthage(findFrameworks: false, linkType: .dynamic), reference: "Alamofire"), watchDependency])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target, watchTarget])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let carthagePhase = pbxProj.nativeTargets.first(where: { $0.name == "Test" })?.buildPhases.first(where: { $0 is PBXShellScriptBuildPhase }) as? PBXShellScriptBuildPhase
|
|
try expect(carthagePhase?.inputPaths) == ["$(SRCROOT)/Carthage/Build/iOS/Alamofire.framework"]
|
|
}
|
|
|
|
$0.it("derived directories are sorted last") {
|
|
let directories = """
|
|
A:
|
|
- file.swift
|
|
P:
|
|
- file.swift
|
|
S:
|
|
- file.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["A", "P", "S"], dependencies: [Dependency(type: .carthage(findFrameworks: false, linkType: .dynamic), reference: "Alamofire")])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let groups = try pbxProj.getMainGroup().children.map { $0.nameOrPath }
|
|
try expect(groups) == ["A", "P", "S", "Frameworks", "Products"]
|
|
}
|
|
|
|
$0.it("sorts files") {
|
|
let directories = """
|
|
A:
|
|
- A.swift
|
|
Source:
|
|
- file.swift
|
|
Sources:
|
|
- file3.swift
|
|
- file.swift
|
|
- 10file.a
|
|
- 1file.a
|
|
- file2.swift
|
|
- group2:
|
|
- file.swift
|
|
- group:
|
|
- file.swift
|
|
Z:
|
|
- A:
|
|
- file.swift
|
|
B:
|
|
- file.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
|
|
"Sources",
|
|
TargetSource(path: "Source", name: "S"),
|
|
"A",
|
|
TargetSource(path: "Z/A", name: "B"),
|
|
"B",
|
|
], dependencies: [Dependency(type: .carthage(findFrameworks: false, linkType: .dynamic), reference: "Alamofire")])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
let mainGroup = try pbxProj.getMainGroup()
|
|
let mainGroupNames = mainGroup.children.prefix(5).map { $0.name }
|
|
try expect(mainGroupNames) == [
|
|
nil,
|
|
nil,
|
|
"B",
|
|
"S",
|
|
nil,
|
|
]
|
|
let mainGroupPaths = mainGroup.children.prefix(5).map { $0.path }
|
|
try expect(mainGroupPaths) == [
|
|
"A",
|
|
"B",
|
|
"Z/A",
|
|
"Source",
|
|
"Sources",
|
|
]
|
|
|
|
let group = mainGroup.children.compactMap { $0 as? PBXGroup }.first { $0.path == "Sources" }!
|
|
let names = group.children.map { $0.name }
|
|
try expect(names) == [
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
]
|
|
let paths = group.children.map { $0.path }
|
|
try expect(paths) == [
|
|
"1file.a",
|
|
"10file.a",
|
|
"file.swift",
|
|
"file2.swift",
|
|
"file3.swift",
|
|
"group",
|
|
"group2",
|
|
]
|
|
}
|
|
|
|
$0.it("adds missing optional files and folders") {
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
|
|
TargetSource(path: "File1.swift", optional: true),
|
|
TargetSource(path: "File2.swift", type: .file, optional: true),
|
|
TargetSource(path: "Group", type: .folder, optional: true),
|
|
])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["File1.swift"])
|
|
try pbxProj.expectFile(paths: ["File2.swift"])
|
|
}
|
|
|
|
$0.it("allows missing optional groups") {
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
|
|
TargetSource(path: "Group1", optional: true),
|
|
TargetSource(path: "Group2", type: .group, optional: true),
|
|
TargetSource(path: "Group3", type: .group, optional: true),
|
|
])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
_ = try project.generatePbxProj()
|
|
}
|
|
|
|
$0.it("relative path items outside base path are grouped together") {
|
|
let directories = """
|
|
Sources:
|
|
- Inside:
|
|
- a.swift
|
|
- Inside2:
|
|
- b.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let outOfSourceFile1 = outOfRootPath + "Outside/a.swift"
|
|
try outOfSourceFile1.parent().mkpath()
|
|
try outOfSourceFile1.write("")
|
|
|
|
let outOfSourceFile2 = outOfRootPath + "Outside/Outside2/b.swift"
|
|
try outOfSourceFile2.parent().mkpath()
|
|
try outOfSourceFile2.write("")
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [
|
|
"Sources",
|
|
"../OtherDirectory",
|
|
])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
try pbxProj.expectFile(paths: ["Sources", "Inside", "a.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["Sources", "Inside", "Inside2", "b.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["../OtherDirectory", "Outside", "a.swift"], names: ["OtherDirectory", "Outside", "a.swift"], buildPhase: .sources)
|
|
try pbxProj.expectFile(paths: ["../OtherDirectory", "Outside", "Outside2", "b.swift"], names: ["OtherDirectory", "Outside", "Outside2", "b.swift"], buildPhase: .sources)
|
|
}
|
|
|
|
$0.it("correctly adds target source attributes") {
|
|
let directories = """
|
|
A:
|
|
- Intent.intentdefinition
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let definition: String = "Intent.intentdefinition"
|
|
|
|
let target = Target(name: "Test", type: .framework, platform: .iOS, sources: [
|
|
TargetSource(path: "A/\(definition)", buildPhase: .sources, attributes: ["no_codegen"]),
|
|
])
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
|
|
let fileReference = pbxProj.getFileReference(
|
|
paths: ["A", definition],
|
|
names: ["A", definition]
|
|
)
|
|
let buildFile = try unwrap(pbxProj.buildFiles.first(where: { $0.file == fileReference }))
|
|
|
|
try pbxProj.expectFile(paths: ["A", definition], buildPhase: .sources)
|
|
|
|
if buildFile.settings?["ATTRIBUTES"]?.arrayValue != ["no_codegen"] {
|
|
throw failure("File does not contain no_codegen attribute")
|
|
}
|
|
}
|
|
|
|
$0.it("includes only the specified files when includes is present") {
|
|
let directories = """
|
|
Sources:
|
|
- file3.swift
|
|
- file3Tests.swift
|
|
- file2.swift
|
|
- file2Tests.swift
|
|
- group2:
|
|
- file.swift
|
|
- fileTests.swift
|
|
- group:
|
|
- file.swift
|
|
- group3:
|
|
- group4:
|
|
- group5:
|
|
- file.swift
|
|
- file5Tests.swift
|
|
- file6Tests.m
|
|
- file6Tests.h
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let includes = [
|
|
"**/*Tests.*",
|
|
]
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [TargetSource(path: "Sources", includes: includes)])
|
|
|
|
let options = SpecOptions(createIntermediateGroups: true, generateEmptyDirectories: true)
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: options)
|
|
let pbxProj = try project.generatePbxProj()
|
|
|
|
try pbxProj.expectFile(paths: ["Sources", "file2Tests.swift"])
|
|
try pbxProj.expectFile(paths: ["Sources", "file3Tests.swift"])
|
|
try pbxProj.expectFile(paths: ["Sources", "group2", "fileTests.swift"])
|
|
try pbxProj.expectFile(paths: ["Sources", "group3", "group4", "group5", "file5Tests.swift"])
|
|
try pbxProj.expectFile(paths: ["Sources", "group3", "group4", "group5", "file6Tests.h"])
|
|
try pbxProj.expectFile(paths: ["Sources", "group3", "group4", "group5", "file6Tests.m"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "file2.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "file3.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "group2", "file.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "group", "file.swift"])
|
|
}
|
|
|
|
$0.it("handles includes with no matches correctly") {
|
|
let directories = """
|
|
Sources:
|
|
- file3.swift
|
|
- file3Tests.swift
|
|
- file2.swift
|
|
- file2Tests.swift
|
|
- group2:
|
|
- file.swift
|
|
- fileTests.swift
|
|
- group:
|
|
- file.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let includes = [
|
|
"**/*NonExistent.*",
|
|
]
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [TargetSource(path: "Sources", includes: includes)])
|
|
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
let pbxProj = try project.generatePbxProj()
|
|
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "file2.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "file3.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "file2Tests.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "file3Tests.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "group2", "file.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "group2", "fileTests.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "group", "file.swift"])
|
|
}
|
|
|
|
$0.it("prioritizes excludes over includes when both are present") {
|
|
let directories = """
|
|
Sources:
|
|
- file3.swift
|
|
- file3Tests.swift
|
|
- file2.swift
|
|
- file2Tests.swift
|
|
- group2:
|
|
- file.swift
|
|
- fileTests.swift
|
|
- group:
|
|
- file.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let includes = [
|
|
"**/*Tests.*",
|
|
]
|
|
|
|
let excludes = [
|
|
"group2",
|
|
]
|
|
|
|
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [TargetSource(path: "Sources", excludes: excludes, includes: includes)])
|
|
|
|
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
|
|
let pbxProj = try project.generatePbxProj()
|
|
|
|
try pbxProj.expectFile(paths: ["Sources", "file2Tests.swift"])
|
|
try pbxProj.expectFile(paths: ["Sources", "file3Tests.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "group2", "fileTests.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "file2.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "file3.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "group2", "file.swift"])
|
|
try pbxProj.expectFileMissing(paths: ["Sources", "group", "file.swift"])
|
|
}
|
|
|
|
$0.describe("Localized sources") {
|
|
$0.context("With localized sources") {
|
|
$0.it("*.intentdefinition should be added to source phase") {
|
|
let directories = """
|
|
Sources:
|
|
Base.lproj:
|
|
- Intents.intentdefinition
|
|
en.lproj:
|
|
- Intents.strings
|
|
ja.lproj:
|
|
- Intents.strings
|
|
"""
|
|
try createDirectories(directories)
|
|
let directoryPath = Path("TestDirectory")
|
|
|
|
let target = Target(name: "IntentDefinitions",
|
|
type: .application,
|
|
platform: .iOS,
|
|
sources: [TargetSource(path: "Sources")])
|
|
let project = Project(basePath: directoryPath,
|
|
name: "IntendDefinitions",
|
|
targets: [target])
|
|
let pbxProj = try project.generatePbxProj()
|
|
let sourceBuildPhase = try unwrap(pbxProj.buildPhases.first { $0.buildPhase == .sources })
|
|
try expect(sourceBuildPhase.files?.compactMap { $0.file?.nameOrPath }) == ["Intents.intentdefinition"]
|
|
}
|
|
}
|
|
|
|
$0.context("With localized sources with buildPhase") {
|
|
$0.it("*.intentdefinition with buildPhase should be added to resource phase") {
|
|
let directories = """
|
|
Sources:
|
|
Base.lproj:
|
|
- Intents.intentdefinition
|
|
en.lproj:
|
|
- Intents.strings
|
|
ja.lproj:
|
|
- Intents.strings
|
|
"""
|
|
try createDirectories(directories)
|
|
let directoryPath = Path("TestDirectory")
|
|
|
|
let target = Target(name: "IntentDefinitions",
|
|
type: .application,
|
|
platform: .iOS,
|
|
sources: [TargetSource(path: "Sources", buildPhase: .resources)])
|
|
let project = Project(basePath: directoryPath,
|
|
name: "IntendDefinitions",
|
|
targets: [target])
|
|
let pbxProj = try project.generatePbxProj()
|
|
let sourceBuildPhase = try unwrap(pbxProj.buildPhases.first { $0.buildPhase == .sources })
|
|
let resourcesBuildPhase = try unwrap(pbxProj.buildPhases.first { $0.buildPhase == .resources })
|
|
try expect(sourceBuildPhase.files) == []
|
|
try expect(resourcesBuildPhase.files?.compactMap { $0.file?.nameOrPath }) == ["Intents.intentdefinition"]
|
|
}
|
|
}
|
|
|
|
$0.it("generates resource tags") {
|
|
let directories = """
|
|
A:
|
|
- resourceFile.mp4
|
|
- resourceFile2.mp4
|
|
- sourceFile.swift
|
|
"""
|
|
try createDirectories(directories)
|
|
|
|
let target = Target(
|
|
name: "Test",
|
|
type: .application,
|
|
platform: .iOS,
|
|
sources: [
|
|
TargetSource(path: "A/resourceFile.mp4", buildPhase: .resources, resourceTags: ["tag1", "tag2"]),
|
|
TargetSource(path: "A/resourceFile2.mp4", buildPhase: .resources, resourceTags: ["tag2", "tag3"]),
|
|
TargetSource(path: "A/sourceFile.swift", buildPhase: .sources, resourceTags: ["tag1", "tag2"]),
|
|
]
|
|
)
|
|
|
|
let project = Project(basePath: directoryPath,
|
|
name: "Test",
|
|
targets: [target])
|
|
|
|
let pbxProj = try project.generatePbxProj()
|
|
|
|
let resourceFileReference = try unwrap(pbxProj.getFileReference(
|
|
paths: ["A", "resourceFile.mp4"],
|
|
names: ["A", "resourceFile.mp4"]
|
|
))
|
|
|
|
let resourceFileReference2 = try unwrap(pbxProj.getFileReference(
|
|
paths: ["A", "resourceFile2.mp4"],
|
|
names: ["A", "resourceFile2.mp4"]
|
|
))
|
|
|
|
let sourceFileReference = try unwrap(pbxProj.getFileReference(
|
|
paths: ["A", "sourceFile.swift"],
|
|
names: ["A", "sourceFile.swift"]
|
|
))
|
|
|
|
try pbxProj.expectFile(paths: ["A", "resourceFile.mp4"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["A", "resourceFile2.mp4"], buildPhase: .resources)
|
|
try pbxProj.expectFile(paths: ["A", "sourceFile.swift"], buildPhase: .sources)
|
|
|
|
let resourceBuildFile = try unwrap(pbxProj.buildFiles.first(where: { $0.file == resourceFileReference }))
|
|
let resourceBuildFile2 = try unwrap(pbxProj.buildFiles.first(where: { $0.file == resourceFileReference2 }))
|
|
let sourceBuildFile = try unwrap(pbxProj.buildFiles.first(where: { $0.file == sourceFileReference }))
|
|
|
|
if resourceBuildFile.settings?["ASSET_TAGS"]?.arrayValue != ["tag1", "tag2"] {
|
|
throw failure("File does not contain tag1 and tag2 ASSET_TAGS")
|
|
}
|
|
|
|
if resourceBuildFile2.settings?["ASSET_TAGS"]?.arrayValue != ["tag2", "tag3"] {
|
|
throw failure("File does not contain tag2 and tag3 ASSET_TAGS")
|
|
}
|
|
|
|
if sourceBuildFile.settings != nil {
|
|
throw failure("File that buildPhase is source contain settings")
|
|
}
|
|
|
|
if !pbxProj.rootObject!.attributes.keys.contains("knownAssetTags") {
|
|
throw failure("PBXProject does not contain knownAssetTags")
|
|
}
|
|
|
|
try expect(pbxProj.rootObject!.attributes["knownAssetTags"]?.arrayValue) == ["tag1", "tag2", "tag3"]
|
|
}
|
|
|
|
$0.it("Detects all locales present in a String Catalog") {
|
|
/// This is a catalog with gaps:
|
|
/// - String "foo" is translated into English (en) and Spanish (es)
|
|
/// - String "bar" is translated into English (en) and Italian (it)
|
|
///
|
|
/// It is aimed at representing real world scenarios where translators have not finished translating all strings into their respective languages.
|
|
/// The expectation in this kind of cases is that `includedLocales` returns all locales found at least once in the catalog.
|
|
/// In this example, `includedLocales` is expected to be a set only containing "en", "es" and "it".
|
|
let stringCatalogContent = """
|
|
{
|
|
"sourceLanguage" : "en",
|
|
"strings" : {
|
|
"foo" : {
|
|
"comment" : "Sample string in an asset catalog",
|
|
"extractionState" : "manual",
|
|
"localizations" : {
|
|
"en" : {
|
|
"stringUnit" : {
|
|
"state" : "translated",
|
|
"value" : "Foo English"
|
|
}
|
|
},
|
|
"es" : {
|
|
"stringUnit" : {
|
|
"state" : "translated",
|
|
"value" : "Foo Spanish"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"bar" : {
|
|
"comment" : "Another sample string in an asset catalog",
|
|
"extractionState" : "manual",
|
|
"localizations" : {
|
|
"en" : {
|
|
"stringUnit" : {
|
|
"state" : "translated",
|
|
"value" : "Bar English"
|
|
}
|
|
},
|
|
"it" : {
|
|
"stringUnit" : {
|
|
"state" : "translated",
|
|
"value" : "Bar Italian"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"version" : "1.0"
|
|
}
|
|
"""
|
|
|
|
let testStringCatalogRelativePath = Path("Localizable.xcstrings")
|
|
let testStringCatalogPath = try createFile(at: testStringCatalogRelativePath, content: stringCatalogContent)
|
|
|
|
guard let stringCatalog = StringCatalog(from: testStringCatalogPath) else {
|
|
throw failure("Failed decoding string catalog from \(testStringCatalogPath)")
|
|
}
|
|
|
|
try expect(stringCatalog.includedLocales.sorted(by: { $0 < $1 })) == ["en", "es", "it"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PBXProj {
|
|
|
|
/// expect a file within groups of the paths, using optional different names
|
|
func expectFile(paths: [String], names: [String]? = nil, buildPhase: BuildPhaseSpec? = nil, file: String = #file, line: Int = #line) throws {
|
|
guard let fileReference = getFileReference(paths: paths, names: names ?? paths) else {
|
|
var error = "Could not find file at path \(paths.joined(separator: "/").quoted)"
|
|
if let names = names, names != paths {
|
|
error += " and name \(names.joined(separator: "/").quoted)"
|
|
}
|
|
error += "\n\(self.printGroups())"
|
|
throw failure(error, file: file, line: line)
|
|
}
|
|
|
|
if let buildPhase = buildPhase {
|
|
let buildFile = buildFiles
|
|
.first(where: { $0.file === fileReference })
|
|
let actualBuildPhase = buildFile
|
|
.flatMap { buildFile in buildPhases.first { $0.files?.contains(buildFile) ?? false } }?.buildPhase
|
|
|
|
var error: String?
|
|
if let buildPhase = buildPhase.buildPhase {
|
|
if actualBuildPhase != buildPhase {
|
|
if let actualBuildPhase = actualBuildPhase {
|
|
error = "is in the \(actualBuildPhase.rawValue) build phase instead of the expected \(buildPhase.rawValue.quoted)"
|
|
} else {
|
|
error = "isn't in a build phase when it's expected to be in \(buildPhase.rawValue.quoted)"
|
|
}
|
|
}
|
|
} else if let actualBuildPhase = actualBuildPhase {
|
|
error = "is in the \(actualBuildPhase.rawValue.quoted) build phase when it's expected to not be in any"
|
|
}
|
|
if let error = error {
|
|
throw failure("File \(paths.joined(separator: "/").quoted) \(error)", file: file, line: line)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// expect a missing file within groups of the paths, using optional different names
|
|
func expectFileMissing(paths: [String], names: [String]? = nil, file: String = #file, line: Int = #line) throws {
|
|
let names = names ?? paths
|
|
if getFileReference(paths: paths, names: names) != nil {
|
|
throw failure("Found unexpected file at path \(paths.joined(separator: "/").quoted) and name \(paths.joined(separator: "/").quoted)", file: file, line: line)
|
|
}
|
|
}
|
|
|
|
func getFileReference(paths: [String], names: [String], file: String = #file, line: Int = #line) -> PBXFileReference? {
|
|
guard let mainGroup = projects.first?.mainGroup else { return nil }
|
|
|
|
return getFileReference(group: mainGroup, paths: paths, names: names)
|
|
}
|
|
|
|
private func getFileReference(group: PBXGroup, paths: [String], names: [String]) -> PBXFileReference? {
|
|
guard !paths.isEmpty else {
|
|
return nil
|
|
}
|
|
|
|
let path = paths.first!
|
|
let name = names.first!
|
|
let restOfPath = Array(paths.dropFirst())
|
|
let restOfName = Array(names.dropFirst())
|
|
if restOfPath.isEmpty {
|
|
let fileReferences: [PBXFileReference] = group.children.compactMap { $0 as? PBXFileReference }
|
|
return fileReferences.first { ($0.path == nil || $0.path == path) && $0.nameOrPath == name }
|
|
} else {
|
|
let groups = group.children.compactMap { $0 as? PBXGroup }
|
|
guard let group = groups.first(where: { ($0.path == nil || $0.path == path) && $0.nameOrPath == name }) else {
|
|
return nil
|
|
}
|
|
return getFileReference(group: group, paths: restOfPath, names: restOfName)
|
|
}
|
|
}
|
|
}
|