mirror of
https://github.com/yonaskolb/XcodeGen.git
synced 2026-03-18 20:02:25 +00:00
e6a0af79a8
Copy Files phases don't stop dependent targets from starting to compile. If some of the files being copied are needed for the dependent target to compile then they need to happen before the Compile Sources phase. This change allows specifying that.
537 lines
22 KiB
Swift
537 lines
22 KiB
Swift
import Foundation
|
|
import PathKit
|
|
import ProjectSpec
|
|
import xcproj
|
|
|
|
struct SourceFile {
|
|
let path: Path
|
|
let fileReference: String
|
|
let buildFile: PBXBuildFile
|
|
let buildPhase: TargetSource.BuildPhase?
|
|
}
|
|
|
|
class SourceGenerator {
|
|
|
|
var rootGroups: Set<String> = []
|
|
private var fileReferencesByPath: [String: String] = [:]
|
|
private var groupsByPath: [Path: ObjectReference<PBXGroup>] = [:]
|
|
private var variantGroupsByPath: [Path: ObjectReference<PBXVariantGroup>] = [:]
|
|
|
|
private let project: Project
|
|
var addObjectClosure: (String, PBXObject) -> String
|
|
var targetSourceExcludePaths: Set<Path> = []
|
|
var defaultExcludedFiles = [
|
|
".DS_Store",
|
|
]
|
|
|
|
var targetName: String = ""
|
|
|
|
private(set) var knownRegions: Set<String> = []
|
|
|
|
init(project: Project, addObjectClosure: @escaping (String, PBXObject) -> String) {
|
|
self.project = project
|
|
self.addObjectClosure = addObjectClosure
|
|
}
|
|
|
|
func addObject(id: String, _ object: PBXObject) -> String {
|
|
return addObjectClosure(id, object)
|
|
}
|
|
|
|
func createObject<T: PBXObject>(id: String, _ object: T) -> ObjectReference<T> {
|
|
let reference = addObject(id: id, object)
|
|
return ObjectReference(reference: reference, object: object)
|
|
}
|
|
|
|
func getAllSourceFiles(targetType: PBXProductType, sources: [TargetSource]) throws -> [SourceFile] {
|
|
return try sources.flatMap { try getSourceFiles(targetType: targetType, targetSource: $0, path: project.basePath + $0.path) }
|
|
}
|
|
|
|
// get groups without build files. Use for Project.fileGroups
|
|
func getFileGroups(path: String) throws {
|
|
let fullPath = project.basePath + path
|
|
_ = try getSourceFiles(targetType: .none, targetSource: TargetSource(path: path), path: fullPath)
|
|
}
|
|
|
|
func generateSourceFile(targetType: PBXProductType, targetSource: TargetSource, path: Path, buildPhase: TargetSource.BuildPhase? = nil) -> SourceFile {
|
|
let fileReference = fileReferencesByPath[path.string.lowercased()]!
|
|
var settings: [String: Any] = [:]
|
|
var chosenBuildPhase: TargetSource.BuildPhase?
|
|
|
|
let headerVisibility = targetSource.headerVisibility ?? .public
|
|
|
|
if let buildPhase = buildPhase {
|
|
chosenBuildPhase = buildPhase
|
|
} else if let buildPhase = targetSource.buildPhase {
|
|
chosenBuildPhase = buildPhase
|
|
} else {
|
|
chosenBuildPhase = getDefaultBuildPhase(for: path, targetType: targetType)
|
|
}
|
|
|
|
if chosenBuildPhase == .headers && targetType == .staticLibrary {
|
|
// Static libraries don't support the header build phase
|
|
// For public headers they need to be copied
|
|
if headerVisibility == .public {
|
|
chosenBuildPhase = .copyFiles(TargetSource.BuildPhase.CopyFilesSettings(
|
|
destination: .productsDirectory,
|
|
subpath: "include/$(PRODUCT_NAME)",
|
|
phaseOrder: .preCompile
|
|
))
|
|
} else {
|
|
chosenBuildPhase = nil
|
|
}
|
|
}
|
|
|
|
if chosenBuildPhase == .headers {
|
|
if headerVisibility != .project {
|
|
// Xcode doesn't write the default of project
|
|
settings["ATTRIBUTES"] = [headerVisibility.settingName]
|
|
}
|
|
}
|
|
if chosenBuildPhase == .sources && targetSource.compilerFlags.count > 0 {
|
|
settings["COMPILER_FLAGS"] = targetSource.compilerFlags.joined(separator: " ")
|
|
}
|
|
|
|
let buildFile = PBXBuildFile(fileRef: fileReference, settings: settings.isEmpty ? nil : settings)
|
|
return SourceFile(
|
|
path: path,
|
|
fileReference: fileReference,
|
|
buildFile: buildFile,
|
|
buildPhase: chosenBuildPhase
|
|
)
|
|
}
|
|
|
|
func getContainedFileReference(path: Path) -> String {
|
|
let createIntermediateGroups = project.options.createIntermediateGroups
|
|
|
|
let parentPath = path.parent()
|
|
let fileReference = getFileReference(path: path, inPath: parentPath)
|
|
let parentGroup = getGroup(
|
|
path: parentPath,
|
|
mergingChildren: [fileReference],
|
|
createIntermediateGroups: createIntermediateGroups,
|
|
isBaseGroup: true
|
|
)
|
|
|
|
if createIntermediateGroups {
|
|
createIntermediaGroups(for: parentGroup.reference, at: parentPath)
|
|
}
|
|
return fileReference
|
|
}
|
|
|
|
func getFileReference(path: Path, inPath: Path, name: String? = nil, sourceTree: PBXSourceTree = .group, lastKnownFileType: String? = nil) -> String {
|
|
let fileReferenceKey = path.string.lowercased()
|
|
if let fileReference = fileReferencesByPath[fileReferenceKey] {
|
|
return fileReference
|
|
} else {
|
|
let fileReferencePath = path.byRemovingBase(path: inPath)
|
|
var fileReferenceName: String? = name ?? fileReferencePath.lastComponent
|
|
if fileReferencePath.string == fileReferenceName {
|
|
fileReferenceName = nil
|
|
}
|
|
let lastKnownFileType = lastKnownFileType ?? PBXFileReference.fileType(path: path)
|
|
|
|
if path.extension == "xcdatamodeld" {
|
|
let versionedModels = (try? path.children()) ?? []
|
|
|
|
// Sort the versions alphabetically
|
|
let sortedPaths = versionedModels
|
|
.filter { $0.extension == "xcdatamodel" }
|
|
.sorted { $0.string.localizedStandardCompare($1.string) == .orderedAscending }
|
|
|
|
let modelFileReference =
|
|
sortedPaths.map { path in
|
|
createObject(
|
|
id: path.byRemovingBase(path: project.basePath).string,
|
|
PBXFileReference(
|
|
sourceTree: .group,
|
|
lastKnownFileType: "wrapper.xcdatamodel",
|
|
path: path.lastComponent
|
|
)
|
|
)
|
|
}
|
|
// If no current version path is found we fall back to alphabetical
|
|
// order by taking the last item in the sortedPaths array
|
|
let currentVersionPath = findCurrentCoreDataModelVersionPath(using: versionedModels) ?? sortedPaths.last
|
|
let currentVersion: ObjectReference<PBXFileReference>? = {
|
|
guard let indexOf = sortedPaths.index(where: { $0 == currentVersionPath }) else { return nil }
|
|
return modelFileReference[indexOf]
|
|
}()
|
|
let versionGroup = addObject(id: fileReferencePath.string, XCVersionGroup(
|
|
currentVersion: currentVersion?.reference,
|
|
path: fileReferencePath.string,
|
|
sourceTree: sourceTree,
|
|
versionGroupType: "wrapper.xcdatamodel",
|
|
children: modelFileReference.map { $0.reference }
|
|
))
|
|
fileReferencesByPath[fileReferenceKey] = versionGroup
|
|
return versionGroup
|
|
} else {
|
|
// For all extensions other than `xcdatamodeld`
|
|
let fileReference = createObject(
|
|
id: path.byRemovingBase(path: project.basePath).string,
|
|
PBXFileReference(
|
|
sourceTree: sourceTree,
|
|
name: fileReferenceName,
|
|
lastKnownFileType: lastKnownFileType,
|
|
path: fileReferencePath.string
|
|
)
|
|
)
|
|
fileReferencesByPath[fileReferenceKey] = fileReference.reference
|
|
return fileReference.reference
|
|
}
|
|
}
|
|
}
|
|
|
|
/// returns a default build phase for a given path. This is based off the filename
|
|
private func getDefaultBuildPhase(for path: Path, targetType: PBXProductType) -> TargetSource.BuildPhase? {
|
|
if path.lastComponent == "Info.plist" {
|
|
return nil
|
|
}
|
|
if let fileExtension = path.extension {
|
|
switch fileExtension {
|
|
case "swift", "m", "mm", "cpp", "c", "cc", "S", "xcdatamodeld", "metal": return .sources
|
|
case "h", "hh", "hpp", "ipp", "tpp", "hxx", "def": return .headers
|
|
case "modulemap":
|
|
guard targetType == .staticLibrary else { return nil }
|
|
return .copyFiles(TargetSource.BuildPhase.CopyFilesSettings(
|
|
destination: .productsDirectory,
|
|
subpath: "include/$(PRODUCT_NAME)",
|
|
phaseOrder: .preCompile
|
|
))
|
|
case "framework": return .frameworks
|
|
case "xpc": return .copyFiles(.xpcServices)
|
|
case "xcconfig", "entitlements", "gpx", "lproj", "apns": return nil
|
|
default: return .resources
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Create a group or return an existing one at the path.
|
|
/// Any merged children are added to a new group or merged into an existing one.
|
|
private func getGroup(path: Path, name: String? = nil, mergingChildren children: [String], createIntermediateGroups: Bool, isBaseGroup: Bool) -> ObjectReference<PBXGroup> {
|
|
let groupReference: ObjectReference<PBXGroup>
|
|
|
|
if let cachedGroup = groupsByPath[path] {
|
|
// only add the children that aren't already in the cachedGroup
|
|
cachedGroup.object.children = Array(Set(cachedGroup.object.children + children))
|
|
groupReference = cachedGroup
|
|
} else {
|
|
|
|
// lives outside the project base path
|
|
let isOutOfBasePath = !path.absolute().string.contains(project.basePath.absolute().string)
|
|
|
|
// has no valid parent paths
|
|
let isRootPath = isOutOfBasePath || path.parent() == project.basePath
|
|
|
|
// is a top level group in the project
|
|
let isTopLevelGroup = (isBaseGroup && !createIntermediateGroups) || isRootPath
|
|
|
|
let groupName = name ?? path.lastComponent
|
|
let groupPath = isTopLevelGroup ?
|
|
path.byRemovingBase(path: project.basePath).string :
|
|
path.lastComponent
|
|
let group = PBXGroup(
|
|
children: children,
|
|
sourceTree: .group,
|
|
name: groupName != groupPath ? groupName : nil,
|
|
path: groupPath
|
|
)
|
|
groupReference = createObject(id: path.byRemovingBase(path: project.basePath).string, group)
|
|
groupsByPath[path] = groupReference
|
|
|
|
if isTopLevelGroup {
|
|
rootGroups.insert(groupReference.reference)
|
|
}
|
|
}
|
|
return groupReference
|
|
}
|
|
|
|
/// Creates a variant group or returns an existing one at the path
|
|
private func getVariantGroup(path: Path, inPath: Path) -> ObjectReference<PBXVariantGroup> {
|
|
let variantGroup: ObjectReference<PBXVariantGroup>
|
|
if let cachedGroup = variantGroupsByPath[path] {
|
|
variantGroup = cachedGroup
|
|
} else {
|
|
let group = PBXVariantGroup(
|
|
children: [],
|
|
sourceTree: .group,
|
|
name: path.lastComponent
|
|
)
|
|
variantGroup = createObject(id: path.byRemovingBase(path: project.basePath).string, group)
|
|
variantGroupsByPath[path] = variantGroup
|
|
}
|
|
return variantGroup
|
|
}
|
|
|
|
/// Collects all the excluded paths within the targetSource
|
|
private func getSourceExcludes(targetSource: TargetSource) -> Set<Path> {
|
|
let rootSourcePath = project.basePath + targetSource.path
|
|
|
|
return Set(
|
|
targetSource.excludes.map {
|
|
Path.glob("\(rootSourcePath)/\($0)")
|
|
.map {
|
|
guard $0.isDirectory else {
|
|
return [$0]
|
|
}
|
|
|
|
return (try? $0.recursiveChildren()) ?? []
|
|
}
|
|
.reduce([], +)
|
|
}
|
|
.reduce([], +)
|
|
)
|
|
}
|
|
|
|
/// Checks whether the path is not in any default or TargetSource excludes
|
|
func isIncludedPath(_ path: Path) -> Bool {
|
|
return !defaultExcludedFiles.contains(where: { path.lastComponent.contains($0) })
|
|
&& !targetSourceExcludePaths.contains(path)
|
|
}
|
|
|
|
/// Gets all the children paths that aren't excluded
|
|
private func getSourceChildren(targetSource: TargetSource, dirPath: Path) throws -> [Path] {
|
|
return try dirPath.children()
|
|
.filter {
|
|
if $0.isDirectory {
|
|
let children = try $0.children().filter(isIncludedPath)
|
|
return !children.isEmpty
|
|
} else if $0.isFile {
|
|
return isIncludedPath($0)
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// creates all the source files and groups they belong to for a given targetSource
|
|
private func getGroupSources(targetType: PBXProductType, targetSource: TargetSource, path: Path, isBaseGroup: Bool)
|
|
throws -> (sourceFiles: [SourceFile], groups: [ObjectReference<PBXGroup>]) {
|
|
|
|
let children = try getSourceChildren(targetSource: targetSource, dirPath: path)
|
|
|
|
let directories = children
|
|
.filter { $0.isDirectory && $0.extension == nil && $0.extension != "lproj" }
|
|
.sorted { $0.lastComponent < $1.lastComponent }
|
|
|
|
let filePaths = children
|
|
.filter { $0.isFile || $0.extension != nil && $0.extension != "lproj" }
|
|
.sorted { $0.lastComponent < $1.lastComponent }
|
|
|
|
let localisedDirectories = children
|
|
.filter { $0.extension == "lproj" }
|
|
.sorted { $0.lastComponent < $1.lastComponent }
|
|
|
|
var groupChildren: [String] = filePaths.map { getFileReference(path: $0, inPath: path) }
|
|
var allSourceFiles: [SourceFile] = filePaths.map {
|
|
generateSourceFile(targetType: targetType, targetSource: targetSource, path: $0)
|
|
}
|
|
var groups: [ObjectReference<PBXGroup>] = []
|
|
|
|
for path in directories {
|
|
let subGroups = try getGroupSources(targetType: targetType, targetSource: targetSource, path: path, isBaseGroup: false)
|
|
|
|
guard !subGroups.sourceFiles.isEmpty else {
|
|
continue
|
|
}
|
|
|
|
allSourceFiles += subGroups.sourceFiles
|
|
|
|
guard let first = subGroups.groups.first else {
|
|
continue
|
|
}
|
|
|
|
groupChildren.append(first.reference)
|
|
groups += subGroups.groups
|
|
}
|
|
|
|
// find the base localised directory
|
|
let baseLocalisedDirectory: Path? = {
|
|
func findLocalisedDirectory(by languageId: String) -> Path? {
|
|
return localisedDirectories.first { $0.lastComponent == "\(languageId).lproj" }
|
|
}
|
|
return findLocalisedDirectory(by: "Base") ??
|
|
findLocalisedDirectory(by: NSLocale.canonicalLanguageIdentifier(from: project.options.developmentLanguage ?? "en"))
|
|
}()
|
|
|
|
knownRegions.formUnion(localisedDirectories.map { $0.lastComponentWithoutExtension })
|
|
|
|
// create variant groups of the base localisation first
|
|
var baseLocalisationVariantGroups: [PBXVariantGroup] = []
|
|
|
|
if let baseLocalisedDirectory = baseLocalisedDirectory {
|
|
for filePath in try baseLocalisedDirectory.children()
|
|
.filter(isIncludedPath)
|
|
.sorted() {
|
|
let variantGroup = getVariantGroup(path: filePath, inPath: path)
|
|
groupChildren.append(variantGroup.reference)
|
|
baseLocalisationVariantGroups.append(variantGroup.object)
|
|
|
|
let sourceFile = SourceFile(
|
|
path: filePath,
|
|
fileReference: variantGroup.reference,
|
|
buildFile: PBXBuildFile(fileRef: variantGroup.reference),
|
|
buildPhase: .resources
|
|
)
|
|
allSourceFiles.append(sourceFile)
|
|
}
|
|
}
|
|
|
|
// add references to localised resources into base localisation variant groups
|
|
for localisedDirectory in localisedDirectories {
|
|
let localisationName = localisedDirectory.lastComponentWithoutExtension
|
|
for filePath in try localisedDirectory.children()
|
|
.filter(isIncludedPath)
|
|
.sorted { $0.lastComponent < $1.lastComponent } {
|
|
// find base localisation variant group
|
|
// ex: Foo.strings will be added to Foo.strings or Foo.storyboard variant group
|
|
let variantGroup = baseLocalisationVariantGroups
|
|
.first {
|
|
Path($0.name!).lastComponent == filePath.lastComponent
|
|
|
|
} ?? baseLocalisationVariantGroups.first {
|
|
Path($0.name!).lastComponentWithoutExtension == filePath.lastComponentWithoutExtension
|
|
}
|
|
|
|
let fileReference = getFileReference(
|
|
path: filePath,
|
|
inPath: path,
|
|
name: variantGroup != nil ? localisationName : filePath.lastComponent
|
|
)
|
|
|
|
if let variantGroup = variantGroup {
|
|
if !variantGroup.children.contains(fileReference) {
|
|
variantGroup.children.append(fileReference)
|
|
}
|
|
} else {
|
|
// add SourceFile to group if there is no Base.lproj directory
|
|
let sourceFile = SourceFile(
|
|
path: filePath,
|
|
fileReference: fileReference,
|
|
buildFile: PBXBuildFile(fileRef: fileReference),
|
|
buildPhase: .resources
|
|
)
|
|
allSourceFiles.append(sourceFile)
|
|
groupChildren.append(fileReference)
|
|
}
|
|
}
|
|
}
|
|
|
|
let group = getGroup(
|
|
path: path,
|
|
mergingChildren: groupChildren,
|
|
createIntermediateGroups: project.options.createIntermediateGroups,
|
|
isBaseGroup: isBaseGroup
|
|
)
|
|
if project.options.createIntermediateGroups {
|
|
createIntermediaGroups(for: group.reference, at: path)
|
|
}
|
|
|
|
groups.insert(group, at: 0)
|
|
return (allSourceFiles, groups)
|
|
}
|
|
|
|
/// creates source files
|
|
private func getSourceFiles(targetType: PBXProductType, targetSource: TargetSource, path: Path) throws -> [SourceFile] {
|
|
|
|
// generate excluded paths
|
|
targetSourceExcludePaths = getSourceExcludes(targetSource: targetSource)
|
|
|
|
let type = targetSource.type ?? (path.isFile || path.extension != nil ? .file : .group)
|
|
let createIntermediateGroups = project.options.createIntermediateGroups
|
|
|
|
var sourceFiles: [SourceFile] = []
|
|
let sourceReference: String
|
|
var sourcePath = path
|
|
switch type {
|
|
case .folder:
|
|
let folderPath = Path(targetSource.path)
|
|
let fileReference = getFileReference(
|
|
path: folderPath,
|
|
inPath: project.basePath,
|
|
name: targetSource.name ?? folderPath.lastComponent,
|
|
sourceTree: .sourceRoot,
|
|
lastKnownFileType: "folder"
|
|
)
|
|
|
|
if !createIntermediateGroups || path.parent() == project.basePath {
|
|
rootGroups.insert(fileReference)
|
|
}
|
|
|
|
let buildPhase: TargetSource.BuildPhase?
|
|
if let targetBuildPhase = targetSource.buildPhase {
|
|
buildPhase = targetBuildPhase
|
|
} else {
|
|
buildPhase = .resources
|
|
}
|
|
|
|
let sourceFile = generateSourceFile(targetType: targetType, targetSource: targetSource, path: folderPath, buildPhase: buildPhase)
|
|
|
|
sourceFiles.append(sourceFile)
|
|
sourceReference = fileReference
|
|
case .file:
|
|
let parentPath = path.parent()
|
|
let fileReference = getFileReference(path: path, inPath: parentPath, name: targetSource.name)
|
|
|
|
let sourceFile = generateSourceFile(targetType: targetType, targetSource: targetSource, path: path)
|
|
|
|
if parentPath == project.basePath {
|
|
sourcePath = path
|
|
sourceReference = fileReference
|
|
rootGroups.insert(fileReference)
|
|
} else {
|
|
let parentGroup = getGroup(path: parentPath, mergingChildren: [fileReference], createIntermediateGroups: createIntermediateGroups, isBaseGroup: true)
|
|
sourcePath = parentPath
|
|
sourceReference = parentGroup.reference
|
|
}
|
|
sourceFiles.append(sourceFile)
|
|
|
|
case .group:
|
|
let (groupSourceFiles, groups) = try getGroupSources(targetType: targetType, targetSource: targetSource, path: path, isBaseGroup: true)
|
|
let group = groups.first!
|
|
if let name = targetSource.name {
|
|
group.object.name = name
|
|
}
|
|
|
|
sourceFiles += groupSourceFiles
|
|
sourceReference = group.reference
|
|
}
|
|
|
|
if createIntermediateGroups {
|
|
createIntermediaGroups(for: sourceReference, at: sourcePath)
|
|
}
|
|
|
|
return sourceFiles
|
|
}
|
|
|
|
// Add groups for all parents recursively
|
|
private func createIntermediaGroups(for groupReference: String, at path: Path) {
|
|
|
|
let parentPath = path.parent()
|
|
guard parentPath != project.basePath && path.string.contains(project.basePath.string) else {
|
|
// we've reached the top or are out of the root directory
|
|
return
|
|
}
|
|
|
|
let hasParentGroup = groupsByPath[parentPath] != nil
|
|
let parentGroup = getGroup(path: parentPath, mergingChildren: [groupReference], createIntermediateGroups: true, isBaseGroup: false)
|
|
|
|
if !hasParentGroup {
|
|
createIntermediaGroups(for: parentGroup.reference, at: parentPath)
|
|
}
|
|
}
|
|
|
|
private func findCurrentCoreDataModelVersionPath(using versionedModels: [Path]) -> Path? {
|
|
// Find and parse the current version model stored in the .xccurrentversion file
|
|
guard
|
|
let versionPath = versionedModels.first(where: { $0.lastComponent == ".xccurrentversion" }),
|
|
let data = try? versionPath.read(),
|
|
let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any],
|
|
let versionString = plist?["_XCCurrentVersionName"] as? String else {
|
|
return nil
|
|
}
|
|
return versionedModels.first(where: { $0.lastComponent == versionString })
|
|
}
|
|
}
|