Files
Leeksov 4647310322 GLEGram 12.5 — Initial public release
Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
2026-04-06 09:48:12 +03:00

1391 lines
65 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("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(dictionary: [
"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(dictionary: [
"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 = NSDictionary(dictionary: buildFile.settings ?? [:])
try expect(settings) == [
"ATTRIBUTES": ["a1", "a2"],
"ASSET_TAGS": ["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 = NSDictionary(dictionary: buildFile.settings ?? [:])
try expect(settings) == [
"ATTRIBUTES": ["a1", "a2"],
"COMPILER_FLAGS": "-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! as NSDictionary) != (["ATTRIBUTES": ["no_codegen"]] as NSDictionary) {
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! as NSDictionary) != (["ASSET_TAGS": ["tag1", "tag2"]] as NSDictionary) {
throw failure("File does not contain tag1 and tag2 ASSET_TAGS")
}
if (resourceBuildFile2.settings! as NSDictionary) != (["ASSET_TAGS": ["tag2", "tag3"]] as NSDictionary) {
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"] as? [String]) == ["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)
}
}
}