Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 87 additions & 20 deletions Sources/XcodeGenKit/SourceGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ struct SourceFile {

class SourceGenerator {

private enum FileReferenceKey: Hashable {
case project(path: String)
case localized(variantGroup: ObjectIdentifier, path: String)
}

var rootGroups: Set<PBXFileElement> = []
private let projectDirectory: Path?
private var fileReferencesByPath: [String: PBXFileElement] = [:]
private var fileReferencesByKey: [FileReferenceKey: PBXFileElement] = [:]
private var groupsByPath: [Path: PBXGroup] = [:]
private var variantGroupsByPath: [Path: PBXVariantGroup] = [:]
private var syncedGroupsByPath: [String: PBXFileSystemSynchronizedRootGroup] = [:]
Expand Down Expand Up @@ -94,7 +99,9 @@ class SourceGenerator {
/// - sources: The array of sources defined as part of the targets spec.
/// - buildPhases: A dictionary containing any build phases that should be applied to source files at specific paths in the event that the associated `TargetSource` didn't already define a `buildPhase`. Values from this dictionary are used in cases where the project generator knows more about a file than the spec/filesystem does (i.e if the file should be treated as the targets Info.plist and so on).
func getAllSourceFiles(targetType: PBXProductType, sources: [TargetSource], buildPhases: [Path : BuildPhaseSpec]) throws -> [SourceFile] {
try sources.flatMap { try getSourceFiles(targetType: targetType, targetSource: $0, buildPhases: buildPhases) }
// Localized groups can select different languages in each target, so start each target with an empty cache.
variantGroupsByPath.removeAll()
return try sources.flatMap { try getSourceFiles(targetType: targetType, targetSource: $0, buildPhases: buildPhases) }
}

// get groups without build files. Use for Project.fileGroups
Expand All @@ -109,6 +116,10 @@ class SourceGenerator {
return nil
}
}

private func isPlainDirectory(_ path: Path) -> Bool {
path.isDirectory && !Xcode.isDirectoryFileWrapper(path: path)
}

private func makeDestinationFilters(for path: Path, with filters: [SupportedDestination]?, or inferDestinationFiltersByPath: Bool?) -> [String]? {
if let filters = filters, !filters.isEmpty {
Expand All @@ -127,7 +138,7 @@ class SourceGenerator {
}

func generateSourceFile(targetType: PBXProductType, targetSource: TargetSource, path: Path, fileReference: PBXFileElement? = nil, buildPhases: [Path: BuildPhaseSpec]) -> SourceFile {
let fileReference = fileReference ?? fileReferencesByPath[path.string.lowercased()]!
let fileReference = fileReference ?? fileReferencesByKey[.project(path: path.string.lowercased())]!
var settings: [String: BuildFileSetting] = [:]
let fileType = getFileType(path: path)
var attributes: [String] = targetSource.attributes + (fileType?.attributes ?? [])
Expand All @@ -141,6 +152,9 @@ class SourceGenerator {
chosenBuildPhase = buildPhase
} else if resolvedTargetSourceType(for: targetSource, at: path) == .folder {
chosenBuildPhase = .resources
} else if fileReference is PBXVariantGroup, isPlainDirectory(path) {
// A directory discovered inside an .lproj is a folder resource even though its TargetSource is a group.
chosenBuildPhase = .resources
} else if let buildPhase = buildPhases[path] {
chosenBuildPhase = buildPhase
} else {
Expand Down Expand Up @@ -227,9 +241,25 @@ class SourceGenerator {
return fileReference
}

func getFileReference(path: Path, inPath: Path, name: String? = nil, sourceTree: PBXSourceTree = .group, lastKnownFileType: String? = nil) -> PBXFileElement {
let fileReferenceKey = path.string.lowercased()
if let fileReference = fileReferencesByPath[fileReferenceKey] {
func getFileReference(
path: Path,
inPath: Path,
name: String? = nil,
sourceTree: PBXSourceTree = .group,
lastKnownFileType: String? = nil,
localizedIn variantGroup: PBXVariantGroup? = nil
) -> PBXFileElement {
let normalizedPath = path.string.lowercased()
let fileReferenceKey: FileReferenceKey
if let variantGroup {
fileReferenceKey = .localized(
variantGroup: ObjectIdentifier(variantGroup),
path: normalizedPath
)
} else {
fileReferenceKey = .project(path: normalizedPath)
}
if let fileReference = fileReferencesByKey[fileReferenceKey] {
return fileReference
} else {
let fileReferencePath = (try? path.relativePath(from: inPath)) ?? path
Expand Down Expand Up @@ -271,7 +301,7 @@ class SourceGenerator {
versionGroupType: "wrapper.xcdatamodel",
children: modelFileReferences
))
fileReferencesByPath[fileReferenceKey] = versionGroup
fileReferencesByKey[fileReferenceKey] = versionGroup
return versionGroup
} else {
// For all extensions other than `xcdatamodeld`
Expand All @@ -283,7 +313,7 @@ class SourceGenerator {
path: fileReferencePath.string
)
)
fileReferencesByPath[fileReferenceKey] = fileReference
fileReferencesByKey[fileReferenceKey] = fileReference
return fileReference
}
}
Expand Down Expand Up @@ -339,8 +369,16 @@ class SourceGenerator {
var cachedGroupChildren = cachedGroup.children
for child in children {
// only add the children that aren't already in the cachedGroup
// Check equality by path and sourceTree because XcodeProj.PBXObject.== is very slow.
if !cachedGroupChildren.contains(where: { $0.name == child.name && $0.path == child.path && $0.sourceTree == child.sourceTree }) {
// Variant groups with the same name may select different localizations for different
// targets, so only the same variant group object is a duplicate.
let alreadyContainsChild = cachedGroupChildren.contains {
if child is PBXVariantGroup {
return $0 === child
}
// Check equality by path and sourceTree because XcodeProj.PBXObject.== is very slow.
return $0.name == child.name && $0.path == child.path && $0.sourceTree == child.sourceTree
}
if !alreadyContainsChild {
cachedGroupChildren.append(child)
child.parent = cachedGroup
}
Expand Down Expand Up @@ -384,16 +422,18 @@ class SourceGenerator {

/// Creates a variant group or returns an existing one at the path
private func getVariantGroup(path: Path, inPath: Path) -> PBXVariantGroup {
// The base localization can vary by source entry, but the logical resource path remains the same.
let variantGroupPath = inPath + path.lastComponent
let variantGroup: PBXVariantGroup
if let cachedGroup = variantGroupsByPath[path] {
if let cachedGroup = variantGroupsByPath[variantGroupPath] {
variantGroup = cachedGroup
} else {
let group = PBXVariantGroup(
sourceTree: .group,
name: path.lastComponent
)
variantGroup = addObject(group)
variantGroupsByPath[path] = variantGroup
variantGroupsByPath[variantGroupPath] = variantGroup
}
return variantGroup
}
Expand Down Expand Up @@ -600,7 +640,8 @@ class SourceGenerator {
knownRegions.formUnion(stringCatalogsLocales)

// create variant groups of the base localisation first
var baseLocalisationVariantGroups: [PBXVariantGroup] = []
var localisedVariantGroups: [PBXVariantGroup] = []
var folderVariantGroups: Set<ObjectIdentifier> = []

if let baseLocalisedDirectory = baseLocalisedDirectory {
let filePaths = try baseLocalisedDirectory.children()
Expand All @@ -609,7 +650,10 @@ class SourceGenerator {
for filePath in filePaths {
let variantGroup = getVariantGroup(path: filePath, inPath: path)
groupChildren.append(variantGroup)
baseLocalisationVariantGroups.append(variantGroup)
localisedVariantGroups.append(variantGroup)
if isPlainDirectory(filePath) {
folderVariantGroups.insert(ObjectIdentifier(variantGroup))
}

let sourceFile = generateSourceFile(targetType: targetType,
targetSource: targetSource,
Expand All @@ -620,32 +664,55 @@ class SourceGenerator {
}
}

// add references to localised resources into base localisation variant groups
// add references to localised resources into their variant groups
for localisedDirectory in localisedDirectories {
let localisationName = localisedDirectory.lastComponentWithoutExtension
let filePaths = try localisedDirectory.children()
.filter { self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths) }
.sorted { $0.lastComponent < $1.lastComponent }
for filePath in filePaths {
// find base localisation variant group
// find matching localisation variant group
// ex: Foo.strings will be added to Foo.strings or Foo.storyboard variant group
let variantGroup = baseLocalisationVariantGroups
var variantGroup = localisedVariantGroups
.first {
Path($0.name!).lastComponent == filePath.lastComponent

} ?? baseLocalisationVariantGroups.first {
Path($0.name!).lastComponentWithoutExtension == filePath.lastComponentWithoutExtension
} ?? localisedVariantGroups.first {
!isPlainDirectory(filePath) &&
!folderVariantGroups.contains(ObjectIdentifier($0)) &&
Path($0.name!).lastComponentWithoutExtension == filePath.lastComponentWithoutExtension
}

// The folder might not be in the base localization, so create its variant group here if necessary.
if variantGroup == nil, isPlainDirectory(filePath) {
let folderVariantGroup = getVariantGroup(path: filePath, inPath: path)
groupChildren.append(folderVariantGroup)
localisedVariantGroups.append(folderVariantGroup)
folderVariantGroups.insert(ObjectIdentifier(folderVariantGroup))
allSourceFiles.append(
generateSourceFile(
targetType: targetType,
targetSource: targetSource,
path: filePath,
fileReference: folderVariantGroup,
buildPhases: buildPhases
)
)
variantGroup = folderVariantGroup
}

let fileReference = getFileReference(
path: filePath,
inPath: path,
name: variantGroup != nil ? localisationName : filePath.lastComponent
name: variantGroup != nil ? localisationName : filePath.lastComponent,
lastKnownFileType: isPlainDirectory(filePath) ? "folder" : nil,
localizedIn: variantGroup
)

if let variantGroup = variantGroup {
if !variantGroup.children.contains(fileReference) {
variantGroup.children.append(fileReference)
fileReference.parent = variantGroup
}
} else {
// add SourceFile to group if there is no Base.lproj directory
Expand Down
Loading