Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add folder reference support #151

Merged
merged 2 commits into from
Nov 15, 2017
Merged
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
8 changes: 7 additions & 1 deletion Docs/ProjectSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Note that target names can also be changed by adding a `name` property to a targ

### Options
- ⚪️ **carthageBuildPath**: `String` - The path to the carthage build directory. Defaults to `Carthage/Build`. This is used when specifying target carthage dependencies
- ⚪️ **createIntermediateGroups**: `Bool` - If this is specified and set to `true`, then intermediate groups will be created for every path component between the folder containing the source and the base path. For example, when enabled if a source path is specified as `Vendor/Foo/Hello.swift`, the group `Vendor` will created as a parent of the `Foo` group.
- ⚪️ **createIntermediateGroups**: `Bool` - If this is specified and set to `true`, then intermediate groups will be created for every path component between the folder containing the source and next existing group it finds or the base path. For example, when enabled if a source path is specified as `Vendor/Foo/Hello.swift`, the group `Vendor` will created as a parent of the `Foo` group.
- ⚪️ **bundleIdPrefix**: `String` - If this is specified then any target that doesn't have an `PRODUCT_BUNDLE_IDENTIFIER` (via all levels of build settings) will get an autogenerated one by combining `bundleIdPrefix` and the target name: `bundleIdPrefix.name`. The target name will be stripped of all characters that aren't alphanumerics, hyphens, or periods. Underscores will be replace with hyphens.
- ⚪️ **settingPresets**: `String` - This controls the settings that are automatically applied to the project and its targets. These are the same build settings that Xcode would add when creating a new project. Project settings are applied by config type. Target settings are applied by the product type and platform. By default this is set to `all`
- `all`: project and target settings
Expand Down Expand Up @@ -205,6 +205,10 @@ A source can be provided via a string (the path) or an object of the form:
- 🔵 **path**: `String` - The path to the source file or directory.
- ⚪️ **name**: `String` - Can be used to override the name of the source file or directory. By default the last component of the path is used for the name
- ⚪️ **compilerFlags**: `[String]` or `String` - A list of compilerFlags to add to files under this specific path provided as a list or a space delimitted string. Defaults to empty.
- ⚪️ **type**: `String`: This can be one of the following values
- `file`: a file reference with a parent group will be created (Default for files or directories with extensions)
- `group`: a group with all it's containing files. (Default for directories without extensions)
- `folder`: a folder reference.


```yaml
Expand All @@ -221,6 +225,8 @@ targets:
- "-Wextra"
- path: MyOtherTargetSource3
compilerFlags: "-Werror -Wextra"
- path: Resources
type: folder
```

### Dependency
Expand Down
19 changes: 11 additions & 8 deletions Sources/ProjectSpec/TargetSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@ public struct TargetSource {
public var name: String?
public var compilerFlags: [String]
public var excludes: [String]
public var type: SourceType?

public init(path: String, name: String? = nil, compilerFlags: [String] = [], excludes: [String] = []) {
public enum SourceType: String {
case group
case file
case folder
}

public init(path: String, name: String? = nil, compilerFlags: [String] = [], excludes: [String] = [], type: SourceType? = nil) {
self.path = path
self.name = name
self.compilerFlags = compilerFlags
self.excludes = excludes
self.type = type
}
}

Expand Down Expand Up @@ -51,6 +59,7 @@ extension TargetSource: JSONObjectConvertible {
maybeCompilerFlagsString.map { $0.split(separator: " ").map { String($0) } } ?? []

excludes = jsonDictionary.json(atKeyPath: "excludes") ?? []
type = jsonDictionary.json(atKeyPath: "type")
}
}

Expand All @@ -61,13 +70,7 @@ extension TargetSource: Equatable {
&& lhs.name == rhs.name
&& lhs.compilerFlags == rhs.compilerFlags
&& lhs.excludes == rhs.excludes
&& lhs.type == rhs.type
}
}

extension TargetSource: Hashable {
public var hashValue: Int {
return path.hashValue
^ compilerFlags.joined(separator: ":").hashValue
^ excludes.joined(separator: ":").hashValue
}
}
14 changes: 6 additions & 8 deletions Sources/XcodeGenKit/PBXProjGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class PBXProjGenerator {
var targetNativeReferences: [String: String] = [:]
var targetBuildFiles: [String: PBXBuildFile] = [:]
var targetFileReferences: [String: String] = [:]
var topLevelGroups: [PBXGroup] = []
var topLevelGroups: Set<String> = []
var carthageFrameworksByPlatform: [String: Set<String>] = [:]
var frameworkFiles: [String] = []

Expand Down Expand Up @@ -93,7 +93,7 @@ public class PBXProjGenerator {

let productGroup = PBXGroup(reference: referenceGenerator.generate(PBXGroup.self, "Products"), children: Array(targetFileReferences.values), sourceTree: .group, name: "Products")
addObject(productGroup)
topLevelGroups.append(productGroup)
topLevelGroups.insert(productGroup.reference)

if !carthageFrameworksByPlatform.isEmpty {
var platforms: [PBXGroup] = []
Expand All @@ -110,16 +110,14 @@ public class PBXProjGenerator {
if !frameworkFiles.isEmpty {
let group = PBXGroup(reference: referenceGenerator.generate(PBXGroup.self, "Frameworks"), children: frameworkFiles, sourceTree: .group, name: "Frameworks")
addObject(group)
topLevelGroups.append(group)
topLevelGroups.insert(group.reference)
}

for rootGroup in sourceGenerator.rootGroups {
if !topLevelGroups.contains(reference: rootGroup), let group = proj.groups.getReference(rootGroup) {
topLevelGroups.append(group)
}
topLevelGroups.insert(rootGroup)
}

let mainGroup = PBXGroup(reference: referenceGenerator.generate(PBXGroup.self, "Project"), children: topLevelGroups.references, sourceTree: .group)
let mainGroup = PBXGroup(reference: referenceGenerator.generate(PBXGroup.self, "Project"), children: Array(topLevelGroups), sourceTree: .group)
addObject(mainGroup)

sortGroups(group: mainGroup)
Expand Down Expand Up @@ -325,7 +323,7 @@ public class PBXProjGenerator {

func getBuildFilesForPhase(_ buildPhase: BuildPhase) -> [String] {
let files = sourceFiles
.filter { sourceGenerator.getBuildPhaseForPath($0.path) == buildPhase }
.filter { $0.buildPhase == buildPhase }
.sorted { $0.path.lastComponent < $1.path.lastComponent }
files.forEach { addObject($0.buildFile) }
return files.map { $0.buildFile.reference }
Expand Down
160 changes: 104 additions & 56 deletions Sources/XcodeGenKit/SourceGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ struct SourceFile {
let path: Path
let fileReference: String
let buildFile: PBXBuildFile
let buildPhase: BuildPhase?
}

class SourceGenerator {

var rootGroups: [String] = []
var rootGroups: Set<String> = []
private var fileReferencesByPath: [Path: String] = [:]
private var groupsByPath: [Path: PBXGroup] = [:]
private var variantGroupsByPath: [Path: PBXVariantGroup] = [:]
Expand All @@ -36,79 +37,91 @@ class SourceGenerator {
}

func getAllSourceFiles(sources: [TargetSource]) throws -> [SourceFile] {
return try sources.flatMap { try getSources(targetSource: $0, path: spec.basePath + $0.path, isRootSource: true).sourceFiles }
}

func getBuildPhaseForPath(_ path: Path) -> BuildPhase? {
if path.lastComponent == "Info.plist" {
return nil
}
if let fileExtension = path.extension {
switch fileExtension {
case "swift", "m", "mm", "cpp", "c", "S": return .sources
case "h", "hh", "hpp", "ipp", "tpp", "hxx", "def": return .headers
case "xcconfig", "entitlements", "gpx", "lproj", "apns": return nil
default: return .resources
}
}
return nil
return try sources.flatMap { try getSourceFiles(targetSource: $0, path: spec.basePath + $0.path) }
}

// get groups without build files. Use for Project.fileGroups
func getFileGroups(path: String) throws {
// TODO: call a seperate function that only creates groups not source files
_ = try getSources(targetSource: TargetSource(path: path), path: spec.basePath + path, isRootSource: true)
_ = try getGroupSources(targetSource: TargetSource(path: path), path: spec.basePath + path, isBaseGroup: true)
}

func generateSourceFile(targetSource: TargetSource, path: Path) -> SourceFile {
func generateSourceFile(targetSource: TargetSource, path: Path, buildPhase: BuildPhase? = nil) -> SourceFile {
let fileReference = fileReferencesByPath[path]!
var settings: [String: Any] = [:]
if getBuildPhaseForPath(path) == .headers {
let buildPhase = buildPhase ?? getDefaultBuildPhase(for: path)

if buildPhase == .headers {
settings = ["ATTRIBUTES": ["Public"]]
}
if targetSource.compilerFlags.count > 0 {
settings["COMPILER_FLAGS"] = targetSource.compilerFlags.joined(separator: " ")
}

//TODO: add the target name to the reference generator string so shared files don't have same reference (that will be escaped by appending a number)
let buildFile = PBXBuildFile(reference: referenceGenerator.generate(PBXBuildFile.self, fileReference), fileRef: fileReference, settings: settings.isEmpty ? nil : settings)
return SourceFile(path: path, fileReference: fileReference, buildFile: buildFile)
return SourceFile(path: path, fileReference: fileReference, buildFile: buildFile, buildPhase: buildPhase)
}

func getFileReference(path: Path, inPath: Path, name: String? = nil) -> String {
func getFileReference(path: Path, inPath: Path, name: String? = nil, sourceTree: PBXSourceTree = .group) -> String {
if let fileReference = fileReferencesByPath[path] {
return fileReference
} else {
let fileReference = PBXFileReference(reference: referenceGenerator.generate(PBXFileReference.self, path.lastComponent), sourceTree: .group, name: name, path: path.byRemovingBase(path: inPath).string)
let fileReference = PBXFileReference(reference: referenceGenerator.generate(PBXFileReference.self, path.lastComponent), sourceTree: sourceTree, name: name, path: path.byRemovingBase(path: inPath).string)
addObject(fileReference)
fileReferencesByPath[path] = fileReference.reference
return fileReference.reference
}
}

private func getGroup(path: Path, name: String? = nil, mergingChildren children: [String], isRootGroup: Bool) -> PBXGroup {
let group: PBXGroup

private func getDefaultBuildPhase(for path: Path) -> BuildPhase? {
if path.lastComponent == "Info.plist" {
return nil
}
if let fileExtension = path.extension {
switch fileExtension {
case "swift", "m", "mm", "cpp", "c", "S": return .sources
case "h", "hh", "hpp", "ipp", "tpp", "hxx", "def": return .headers
case "xcconfig", "entitlements", "gpx", "lproj", "apns": return nil
default: return .resources
}
}
return nil
}

private func getGroup(path: Path, name: String? = nil, mergingChildren children: [String], createIntermediateGroups: Bool, isBaseGroup: Bool) -> PBXGroup {
let group: PBXGroup

if let cachedGroup = groupsByPath[path] {
// only add the children that aren't already in the cachedGroup
cachedGroup.children = Array(Set(cachedGroup.children + children))
group = cachedGroup
} else {

// lives outside the spec base path
let isOutOfBasePath = !path.string.contains(spec.basePath.string)

// has no valid parent paths
let isRootPath = isOutOfBasePath || path.parent() == spec.basePath

// is a top level group in the project
let isTopLevelGroup = (isBaseGroup && !createIntermediateGroups) || isRootPath

group = PBXGroup(
reference: referenceGenerator.generate(PBXGroup.self, path.lastComponent),
children: children,
sourceTree: .group,
name: name ?? path.lastComponent,
path: isRootGroup ?
path: isTopLevelGroup ?
path.byRemovingBase(path: spec.basePath).string :
path.lastComponent
)
addObject(group)
groupsByPath[path] = group

if isRootGroup {
if !rootGroups.contains(group.reference) {
rootGroups.append(group.reference)
}
if isTopLevelGroup {
rootGroups.insert(group.reference)
}
}
return group
Expand Down Expand Up @@ -176,20 +189,9 @@ class SourceGenerator {
}
}

private func getSources(targetSource: TargetSource, path: Path, isRootSource: Bool) throws -> (sourceFiles: [SourceFile], groups: [PBXGroup]) {

// treat all directories with extensions as files
let isFile = path.isFile || path.extension != nil
let fileName = isFile && isRootSource ? targetSource.name : nil
private func getGroupSources(targetSource: TargetSource, path: Path, isBaseGroup: Bool) throws -> (sourceFiles: [SourceFile], groups: [PBXGroup]) {

// if we have a file, move it to children and use the parent as the path
let (children, path) = isFile ?
([path], path.parent()) :
(try getSourceChildren(targetSource: targetSource, dirPath: path).sorted(), path)

guard children.count > 0 else {
return ([], [])
}
let children = try getSourceChildren(targetSource: targetSource, dirPath: path)

let directories = children
.filter { $0.isDirectory && $0.extension == nil && $0.extension != "lproj" }
Expand All @@ -203,14 +205,14 @@ class SourceGenerator {
.filter { $0.extension == "lproj" }
.sorted { $0.lastComponent < $1.lastComponent }

var groupChildren: [String] = filePaths.map { getFileReference(path: $0, inPath: path, name: fileName) }
var groupChildren: [String] = filePaths.map { getFileReference(path: $0, inPath: path) }
var allSourceFiles: [SourceFile] = filePaths.map {
generateSourceFile(targetSource: targetSource, path: $0)
}
var groups: [PBXGroup] = []

for path in directories {
let subGroups = try getSources(targetSource: targetSource, path: path, isRootSource: false)
let subGroups = try getGroupSources(targetSource: targetSource, path: path, isBaseGroup: false)

guard !subGroups.sourceFiles.isEmpty else {
continue
Expand All @@ -235,7 +237,7 @@ class SourceGenerator {
baseLocalisationVariantGroups.append(variantGroup)

let buildFile = PBXBuildFile(reference: referenceGenerator.generate(PBXBuildFile.self, variantGroup.reference), fileRef: variantGroup.reference, settings: nil)
allSourceFiles.append(SourceFile(path: filePath, fileReference: variantGroup.reference, buildFile: buildFile))
allSourceFiles.append(SourceFile(path: filePath, fileReference: variantGroup.reference, buildFile: buildFile, buildPhase: .resources))
}
}

Expand All @@ -259,26 +261,73 @@ class SourceGenerator {
let buildFile = PBXBuildFile(reference: referenceGenerator.generate(PBXBuildFile.self, fileReference),
fileRef: fileReference,
settings: nil)
allSourceFiles.append(SourceFile(path: filePath, fileReference: fileReference, buildFile: buildFile))
allSourceFiles.append(SourceFile(path: filePath, fileReference: fileReference, buildFile: buildFile, buildPhase: .resources))
groupChildren.append(fileReference)
}
}
}

let isOutOfBasePath = !path.string.contains(spec.basePath.string)
let isRootPath = isOutOfBasePath || path.parent() == spec.basePath
let isRootGroup = (isRootSource && !spec.options.createIntermediateGroups) || isRootPath
let group = getGroup(path: path, name: isRootSource && !isFile ? targetSource.name : nil, mergingChildren: groupChildren, isRootGroup: isRootGroup)
let group = getGroup(path: path, mergingChildren: groupChildren, createIntermediateGroups: spec.options.createIntermediateGroups, isBaseGroup: isBaseGroup)
if spec.options.createIntermediateGroups {
createIntermediaGroups(for: group, at: path)
createIntermediaGroups(for: group.reference, at: path)
}

groups.insert(group, at: 0)
return (allSourceFiles, groups)
}

private func getSourceFiles(targetSource: TargetSource, path: Path) throws -> [SourceFile] {

let type = targetSource.type ?? (path.isFile || path.extension != nil ? .file : .group)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome! This will make our generation way faster since we waited on I/O for the path.isFile check for every file in our project.

let createIntermediateGroups = spec.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: spec.basePath, name: targetSource.name ?? folderPath.lastComponent, sourceTree: .sourceRoot)

if !createIntermediateGroups {
rootGroups.insert(fileReference)
}

let sourceFile = generateSourceFile(targetSource: targetSource, path: folderPath, buildPhase: .resources)

sourceFiles.append(sourceFile)
sourceReference = fileReference
case .file:
let parentPath = path.parent()
let fileReference = getFileReference(path: path, inPath: parentPath, name: targetSource.name)

let sourceFile = generateSourceFile(targetSource: targetSource, path: path)

let parentGroup = getGroup(path: parentPath, mergingChildren: [fileReference], createIntermediateGroups: createIntermediateGroups, isBaseGroup: true)

sourcePath = parentPath
sourceFiles.append(sourceFile)
sourceReference = parentGroup.reference
case .group:
let (groupSourceFiles, groups) = try getGroupSources(targetSource: targetSource, path: path, isBaseGroup: true)
let group = groups.first!
if let name = targetSource.name {
group.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 group: PBXGroup, at path: Path) {
private func createIntermediaGroups(for groupReference: String, at path: Path) {

let parentPath = path.parent()
guard parentPath != spec.basePath && path.string.contains(spec.basePath.string) else {
Expand All @@ -287,11 +336,10 @@ class SourceGenerator {
}

let hasParentGroup = groupsByPath[parentPath] != nil
let isRootGroup = parentPath.parent() == spec.basePath
let parentGroup = getGroup(path: parentPath, mergingChildren: [group.reference], isRootGroup: isRootGroup)
let parentGroup = getGroup(path: parentPath, mergingChildren: [groupReference], createIntermediateGroups: true, isBaseGroup: false)

if !hasParentGroup {
createIntermediaGroups(for: parentGroup, at: parentPath)
createIntermediaGroups(for: parentGroup.reference, at: parentPath)
}
}
}
Loading