Adding support for project additional files (#314)
Part of https://github.com/tuist/tuist/issues/202 ### Short description Often projects may include additional files or folder references that don't need to be part of any build phase (e.g. design references, documentation etc...) ### Solution `Workspace.swift` manifest now supports `additionalFiles`, the same concept can be added to the `Project.swift` manifest with the same rules. - `.glob(pattern: "Documentation/**")`: glob pattern to one or more files - `.folderReference(path: "Designs")`: single path to a directory that is added as a folder reference Example: ```swift let project = Project(name: "App", targets: [ // ... ], additionalFiles: [ "Dangerfile.swift" "Documentation/**", .folderReference(path: "Designs") ]) ``` ### Implementation - [x] Update `Project` manifest to include `additionalFiles` - [x] Update models to translate globs to paths - [x] Update `ProjectFileElements` to include `additionalFiles` - [x] Include fixture - [x] Update documentation - [x] Update changelog ### Test Plan - Run `tuist generate` within `fixtures/ios_app_with_custom_workspace/App` - Open the generated workspace - Verify `Dangerfile.swift` is included in the project but isn't part of the resource phase - Verify `Documentation` files are included (with the appropriate group structure that reflects the file system) and that the files aren't copied to resources - Verify `Server` folder reference is included
This commit is contained in:
parent
2726304e29
commit
8d7f15f8d9
|
@ -6,6 +6,9 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
|
|||
|
||||
### Changed
|
||||
### Added
|
||||
|
||||
- Adding support for project additional files https://github.com/tuist/tuist/pull/314 by @kwridan
|
||||
|
||||
### Removed
|
||||
### Fixed
|
||||
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import Foundation
|
||||
|
||||
/// File element
|
||||
///
|
||||
/// - glob: a glob pattern for files to include
|
||||
/// - folderReference: a single path to a directory
|
||||
///
|
||||
/// Note: For convenience, an element can be represented as a string literal
|
||||
/// `"some/pattern/**"` is the equivalent of `FileElement.glob(pattern: "some/pattern/**")`
|
||||
public enum FileElement: Codable {
|
||||
/// A glob pattern of files to include
|
||||
case glob(pattern: String)
|
||||
|
||||
/// Relative path to a directory to include
|
||||
/// as a folder reference
|
||||
case folderReference(path: String)
|
||||
|
||||
private enum TypeName: String, Codable {
|
||||
case glob
|
||||
case folderReference
|
||||
}
|
||||
|
||||
private var typeName: TypeName {
|
||||
switch self {
|
||||
case .glob:
|
||||
return .glob
|
||||
case .folderReference:
|
||||
return .folderReference
|
||||
}
|
||||
}
|
||||
|
||||
public enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case pattern
|
||||
case path
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(TypeName.self, forKey: .type)
|
||||
switch type {
|
||||
case .glob:
|
||||
let pattern = try container.decode(String.self, forKey: .pattern)
|
||||
self = .glob(pattern: pattern)
|
||||
case .folderReference:
|
||||
let path = try container.decode(String.self, forKey: .path)
|
||||
self = .folderReference(path: path)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(typeName, forKey: .type)
|
||||
switch self {
|
||||
case let .glob(pattern: pattern):
|
||||
try container.encode(pattern, forKey: .pattern)
|
||||
case let .folderReference(path: path):
|
||||
try container.encode(path, forKey: .path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FileElement: ExpressibleByStringLiteral {
|
||||
public init(stringLiteral value: String) {
|
||||
self = .glob(pattern: value)
|
||||
}
|
||||
}
|
|
@ -6,19 +6,16 @@ public class Project: Codable {
|
|||
public let name: String
|
||||
public let targets: [Target]
|
||||
public let settings: Settings?
|
||||
|
||||
public enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case targets
|
||||
case settings
|
||||
}
|
||||
public let additionalFiles: [FileElement]
|
||||
|
||||
public init(name: String,
|
||||
settings: Settings? = nil,
|
||||
targets: [Target] = []) {
|
||||
targets: [Target] = [],
|
||||
additionalFiles: [FileElement] = []) {
|
||||
self.name = name
|
||||
self.targets = targets
|
||||
self.settings = settings
|
||||
self.additionalFiles = additionalFiles
|
||||
dumpIfNeeded(self)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ public class Workspace: Codable {
|
|||
public let projects: [String]
|
||||
|
||||
/// List of files to include in the workspace (e.g. Documentation)
|
||||
public let additionalFiles: [Element]
|
||||
public let additionalFiles: [FileElement]
|
||||
|
||||
/// Workspace
|
||||
///
|
||||
|
@ -20,71 +20,10 @@ public class Workspace: Codable {
|
|||
/// - name: Name of the workspace.
|
||||
/// - projects: List of project relative paths (or glob patterns) to generate and include.
|
||||
/// - additionalFiles: List of files to include in the workspace (e.g. Documentation)
|
||||
public init(name: String, projects: [String], additionalFiles: [Element] = []) {
|
||||
public init(name: String, projects: [String], additionalFiles: [FileElement] = []) {
|
||||
self.name = name
|
||||
self.projects = projects
|
||||
self.additionalFiles = additionalFiles
|
||||
dumpIfNeeded(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Workspace {
|
||||
public enum Element: Codable {
|
||||
/// A glob pattern of files to include
|
||||
case glob(pattern: String)
|
||||
|
||||
/// Relative path to a directory to include
|
||||
/// as a folder reference
|
||||
case folderReference(path: String)
|
||||
|
||||
private enum TypeName: String, Codable {
|
||||
case glob
|
||||
case folderReference
|
||||
}
|
||||
|
||||
private var typeName: TypeName {
|
||||
switch self {
|
||||
case .glob:
|
||||
return .glob
|
||||
case .folderReference:
|
||||
return .folderReference
|
||||
}
|
||||
}
|
||||
|
||||
public enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case pattern
|
||||
case path
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(TypeName.self, forKey: .type)
|
||||
switch type {
|
||||
case .glob:
|
||||
let pattern = try container.decode(String.self, forKey: .pattern)
|
||||
self = .glob(pattern: pattern)
|
||||
case .folderReference:
|
||||
let path = try container.decode(String.self, forKey: .path)
|
||||
self = .folderReference(path: path)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(typeName, forKey: .type)
|
||||
switch self {
|
||||
case let .glob(pattern: pattern):
|
||||
try container.encode(pattern, forKey: .pattern)
|
||||
case let .folderReference(path: path):
|
||||
try container.encode(path, forKey: .path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Workspace.Element: ExpressibleByStringLiteral {
|
||||
public init(stringLiteral value: String) {
|
||||
self = .glob(pattern: value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ public class Generator: Generating {
|
|||
|
||||
let workspace = Workspace(name: project.name,
|
||||
projects: graph.projectPaths,
|
||||
additionalFiles: workspaceFiles.map(Workspace.Element.file))
|
||||
additionalFiles: workspaceFiles.map(FileElement.file))
|
||||
|
||||
return try workspaceGenerator.generate(workspace: workspace,
|
||||
path: path,
|
||||
|
|
|
@ -5,9 +5,16 @@ import xcodeproj
|
|||
|
||||
// swiftlint:disable:next type_body_length
|
||||
class ProjectFileElements {
|
||||
struct FileElement: Hashable {
|
||||
struct GroupFileElement: Hashable {
|
||||
var path: AbsolutePath
|
||||
var group: ProjectGroup
|
||||
var isReference: Bool
|
||||
|
||||
init(path: AbsolutePath, group: ProjectGroup, isReference: Bool = false) {
|
||||
self.path = path
|
||||
self.group = group
|
||||
self.isReference = isReference
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
@ -41,24 +48,21 @@ class ProjectFileElements {
|
|||
groups: ProjectGroups,
|
||||
pbxproj: PBXProj,
|
||||
sourceRootPath: AbsolutePath) throws {
|
||||
var files = Set<FileElement>()
|
||||
var files = Set<GroupFileElement>()
|
||||
var products = Set<String>()
|
||||
project.targets.forEach { target in
|
||||
let targetFilePaths = targetFiles(target: target)
|
||||
let targetFileElements = targetFilePaths.map {
|
||||
FileElement(path: $0, group: target.filesGroup)
|
||||
GroupFileElement(path: $0, group: target.filesGroup)
|
||||
}
|
||||
files.formUnion(targetFileElements)
|
||||
products.formUnion(targetProducts(target: target))
|
||||
}
|
||||
let projectFilePaths = projectFiles(project: project)
|
||||
let projectFileElements = projectFilePaths.map {
|
||||
FileElement(path: $0, group: project.filesGroup)
|
||||
}
|
||||
let projectFileElements = projectFiles(project: project)
|
||||
files.formUnion(projectFileElements)
|
||||
|
||||
let pathsSort = filesSortener.sort
|
||||
let filesSort: (FileElement, FileElement) -> Bool = {
|
||||
let filesSort: (GroupFileElement, GroupFileElement) -> Bool = {
|
||||
pathsSort($0.path, $1.path)
|
||||
}
|
||||
|
||||
|
@ -90,17 +94,27 @@ class ProjectFileElements {
|
|||
filesGroup: project.filesGroup)
|
||||
}
|
||||
|
||||
func projectFiles(project: Project) -> Set<AbsolutePath> {
|
||||
var files = Set<AbsolutePath>()
|
||||
func projectFiles(project: Project) -> Set<GroupFileElement> {
|
||||
var fileElements = Set<GroupFileElement>()
|
||||
|
||||
/// Config files
|
||||
if let debugConfigFile = project.settings?.debug?.xcconfig {
|
||||
files.insert(debugConfigFile)
|
||||
}
|
||||
if let releaseConfigFile = project.settings?.release?.xcconfig {
|
||||
files.insert(releaseConfigFile)
|
||||
}
|
||||
return files
|
||||
let configFiles = [
|
||||
project.settings?.debug?.xcconfig,
|
||||
project.settings?.release?.xcconfig,
|
||||
].compactMap { $0 }
|
||||
|
||||
fileElements.formUnion(configFiles.map {
|
||||
GroupFileElement(path: $0, group: project.filesGroup)
|
||||
})
|
||||
|
||||
// Additional files
|
||||
fileElements.formUnion(project.additionalFiles.map {
|
||||
GroupFileElement(path: $0.path,
|
||||
group: project.filesGroup,
|
||||
isReference: $0.isReference)
|
||||
})
|
||||
|
||||
return fileElements
|
||||
}
|
||||
|
||||
func targetProducts(target: Target) -> Set<String> {
|
||||
|
@ -141,7 +155,7 @@ class ProjectFileElements {
|
|||
return files
|
||||
}
|
||||
|
||||
func generate(files: [FileElement],
|
||||
func generate(files: [GroupFileElement],
|
||||
groups: ProjectGroups,
|
||||
pbxproj: PBXProj,
|
||||
sourceRootPath: AbsolutePath) throws {
|
||||
|
@ -212,8 +226,8 @@ class ProjectFileElements {
|
|||
self.products[productName] = fileReference
|
||||
|
||||
} else if let precompiledNode = node as? PrecompiledNode {
|
||||
let fileElement = FileElement(path: precompiledNode.path,
|
||||
group: filesGroup)
|
||||
let fileElement = GroupFileElement(path: precompiledNode.path,
|
||||
group: filesGroup)
|
||||
try generate(fileElement: fileElement,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj,
|
||||
|
@ -222,7 +236,7 @@ class ProjectFileElements {
|
|||
}
|
||||
}
|
||||
|
||||
func generate(fileElement: FileElement,
|
||||
func generate(fileElement: GroupFileElement,
|
||||
groups: ProjectGroups,
|
||||
pbxproj: PBXProj,
|
||||
sourceRootPath: AbsolutePath) throws {
|
||||
|
@ -239,6 +253,7 @@ class ProjectFileElements {
|
|||
group = try groups.projectGroup(named: groupName)
|
||||
}
|
||||
guard let firstElement = addElement(relativePath: closestRelativeRelativePath,
|
||||
isReference: fileElement.isReference,
|
||||
from: sourceRootPath,
|
||||
toGroup: group,
|
||||
pbxproj: pbxproj) else {
|
||||
|
@ -256,6 +271,7 @@ class ProjectFileElements {
|
|||
for component in fileElement.path.relative(to: lastPath).components {
|
||||
if lastGroup == nil { return }
|
||||
guard let element = addElement(relativePath: RelativePath(component),
|
||||
isReference: fileElement.isReference,
|
||||
from: lastPath,
|
||||
toGroup: lastGroup!,
|
||||
pbxproj: pbxproj) else {
|
||||
|
@ -269,6 +285,7 @@ class ProjectFileElements {
|
|||
// MARK: - Internal
|
||||
|
||||
@discardableResult func addElement(relativePath: RelativePath,
|
||||
isReference: Bool,
|
||||
from: AbsolutePath,
|
||||
toGroup: PBXGroup,
|
||||
pbxproj: PBXProj) -> (element: PBXFileElement, path: AbsolutePath)? {
|
||||
|
@ -300,7 +317,7 @@ class ProjectFileElements {
|
|||
name: name,
|
||||
toGroup: toGroup,
|
||||
pbxproj: pbxproj)
|
||||
} else if isGroup(path: absolutePath) {
|
||||
} else if isGroup(path: absolutePath), !isReference {
|
||||
return addGroupElement(from: from,
|
||||
folderAbsolutePath: absolutePath,
|
||||
folderRelativePath: relativePath,
|
||||
|
@ -387,7 +404,7 @@ class ProjectFileElements {
|
|||
name: String?,
|
||||
toGroup: PBXGroup,
|
||||
pbxproj: PBXProj) {
|
||||
let lastKnownFileType = Xcode.filetype(extension: fileAbsolutePath.extension!)
|
||||
let lastKnownFileType = fileAbsolutePath.extension.flatMap { Xcode.filetype(extension: $0) }
|
||||
let file = PBXFileReference(sourceTree: .group, name: name, lastKnownFileType: lastKnownFileType, path: fileRelativePath.asString)
|
||||
pbxproj.add(object: file)
|
||||
toGroup.children.append(file)
|
||||
|
|
|
@ -55,7 +55,7 @@ private class DirectoryStructure {
|
|||
let fileHandler: FileHandling
|
||||
|
||||
let projects: [AbsolutePath]
|
||||
let files: [Workspace.Element]
|
||||
let files: [FileElement]
|
||||
|
||||
private let containers: [String] = [
|
||||
".playground",
|
||||
|
@ -65,7 +65,7 @@ private class DirectoryStructure {
|
|||
init(path: AbsolutePath,
|
||||
fileHandler: FileHandling,
|
||||
projects: [AbsolutePath],
|
||||
files: [Workspace.Element]) {
|
||||
files: [FileElement]) {
|
||||
self.path = path
|
||||
self.fileHandler = fileHandler
|
||||
self.projects = projects
|
||||
|
@ -100,7 +100,7 @@ private class DirectoryStructure {
|
|||
return root
|
||||
}
|
||||
|
||||
private func fileNode(from element: Workspace.Element) -> Node {
|
||||
private func fileNode(from element: FileElement) -> Node {
|
||||
switch element {
|
||||
case let .file(path: path):
|
||||
return .file(path)
|
||||
|
@ -113,7 +113,7 @@ private class DirectoryStructure {
|
|||
return .project(path)
|
||||
}
|
||||
|
||||
private func isFileOrFolderReference(element: Workspace.Element) -> Bool {
|
||||
private func isFileOrFolderReference(element: FileElement) -> Bool {
|
||||
switch element {
|
||||
case .folderReference:
|
||||
return true
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
|
||||
public enum FileElement: Equatable {
|
||||
case file(path: AbsolutePath)
|
||||
case folderReference(path: AbsolutePath)
|
||||
|
||||
var path: AbsolutePath {
|
||||
switch self {
|
||||
case let .file(path):
|
||||
return path
|
||||
case let .folderReference(path):
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
var isReference: Bool {
|
||||
switch self {
|
||||
case .file:
|
||||
return false
|
||||
case .folderReference:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,9 @@ public class Project: Equatable, CustomStringConvertible {
|
|||
/// The group to place project files within
|
||||
public let filesGroup: ProjectGroup
|
||||
|
||||
/// Additional files to include in the project
|
||||
public let additionalFiles: [FileElement]
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
/// Initializes the project with its attributes.
|
||||
|
@ -27,17 +30,23 @@ public class Project: Equatable, CustomStringConvertible {
|
|||
/// - Parameters:
|
||||
/// - path: Path to the folder that contains the project manifest.
|
||||
/// - name: Project name.
|
||||
/// - targets: Project settings.
|
||||
/// - settings: The settings to apply at the project level
|
||||
/// - filesGroup: The root group to place project files within
|
||||
/// - targets: The project targets
|
||||
/// - additionalFiles: The additional files to include in the project
|
||||
/// *(Those won't be included in any build phases)*
|
||||
public init(path: AbsolutePath,
|
||||
name: String,
|
||||
settings: Settings? = nil,
|
||||
filesGroup: ProjectGroup,
|
||||
targets: [Target]) {
|
||||
targets: [Target],
|
||||
additionalFiles: [FileElement] = []) {
|
||||
self.path = path
|
||||
self.name = name
|
||||
self.targets = targets
|
||||
self.settings = settings
|
||||
self.filesGroup = filesGroup
|
||||
self.additionalFiles = additionalFiles
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
|
|
|
@ -7,11 +7,11 @@ public class Workspace: Equatable {
|
|||
|
||||
public let name: String
|
||||
public let projects: [AbsolutePath]
|
||||
public let additionalFiles: [Element]
|
||||
public let additionalFiles: [FileElement]
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(name: String, projects: [AbsolutePath], additionalFiles: [Element] = []) {
|
||||
public init(name: String, projects: [AbsolutePath], additionalFiles: [FileElement] = []) {
|
||||
self.name = name
|
||||
self.projects = projects
|
||||
self.additionalFiles = additionalFiles
|
||||
|
@ -43,19 +43,3 @@ extension Workspace {
|
|||
additionalFiles: additionalFiles)
|
||||
}
|
||||
}
|
||||
|
||||
extension Workspace {
|
||||
public enum Element: Equatable {
|
||||
case file(path: AbsolutePath)
|
||||
case folderReference(path: AbsolutePath)
|
||||
|
||||
var path: AbsolutePath {
|
||||
switch self {
|
||||
case let .file(path):
|
||||
return path
|
||||
case let .folderReference(path):
|
||||
return path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,10 @@ class GeneratorModelLoader: GeneratorModelLoading {
|
|||
|
||||
func loadProject(at path: AbsolutePath) throws -> TuistGenerator.Project {
|
||||
let manifest = try manifestLoader.loadProject(at: path)
|
||||
let project = try TuistGenerator.Project.from(manifest: manifest, path: path, fileHandler: fileHandler)
|
||||
let project = try TuistGenerator.Project.from(manifest: manifest,
|
||||
path: path,
|
||||
fileHandler: fileHandler,
|
||||
printer: printer)
|
||||
|
||||
let manifestTarget = try manifestTargetGenerator.generateManifestTarget(for: project.name,
|
||||
at: path)
|
||||
|
@ -68,16 +71,6 @@ extension TuistGenerator.Workspace {
|
|||
fileHandler: FileHandling,
|
||||
manifestLoader: GraphManifestLoading,
|
||||
printer: Printing) throws -> TuistGenerator.Workspace {
|
||||
func globFiles(_ string: String) -> [AbsolutePath] {
|
||||
let files = fileHandler.glob(path, glob: string)
|
||||
|
||||
if files.isEmpty {
|
||||
printer.print(warning: "No files found at: \(string)")
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
func globProjects(_ string: String) -> [AbsolutePath] {
|
||||
let projects = fileHandler.glob(path, glob: string)
|
||||
.lazy
|
||||
|
@ -93,6 +86,34 @@ extension TuistGenerator.Workspace {
|
|||
return Array(projects)
|
||||
}
|
||||
|
||||
let additionalFiles = manifest.additionalFiles.flatMap {
|
||||
TuistGenerator.FileElement.from(manifest: $0,
|
||||
path: path,
|
||||
fileHandler: fileHandler,
|
||||
printer: printer)
|
||||
}
|
||||
|
||||
return TuistGenerator.Workspace(name: manifest.name,
|
||||
projects: manifest.projects.flatMap(globProjects),
|
||||
additionalFiles: additionalFiles)
|
||||
}
|
||||
}
|
||||
|
||||
extension TuistGenerator.FileElement {
|
||||
static func from(manifest: ProjectDescription.FileElement,
|
||||
path: AbsolutePath,
|
||||
fileHandler: FileHandling,
|
||||
printer: Printing) -> [TuistGenerator.FileElement] {
|
||||
func globFiles(_ string: String) -> [AbsolutePath] {
|
||||
let files = fileHandler.glob(path, glob: string)
|
||||
|
||||
if files.isEmpty {
|
||||
printer.print(warning: "No files found at: \(string)")
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
func folderReferences(_ relativePath: String) -> [AbsolutePath] {
|
||||
let folderReferencePath = path.appending(RelativePath(relativePath))
|
||||
|
||||
|
@ -109,34 +130,37 @@ extension TuistGenerator.Workspace {
|
|||
return [folderReferencePath]
|
||||
}
|
||||
|
||||
func workspaceElement(from element: ProjectDescription.Workspace.Element) -> [TuistGenerator.Workspace.Element] {
|
||||
switch element {
|
||||
case let .glob(pattern: pattern):
|
||||
return globFiles(pattern).map(Workspace.Element.file)
|
||||
case let .folderReference(path: folderReferencePath):
|
||||
return folderReferences(folderReferencePath).map(Workspace.Element.folderReference)
|
||||
}
|
||||
switch manifest {
|
||||
case let .glob(pattern: pattern):
|
||||
return globFiles(pattern).map(FileElement.file)
|
||||
case let .folderReference(path: folderReferencePath):
|
||||
return folderReferences(folderReferencePath).map(FileElement.folderReference)
|
||||
}
|
||||
|
||||
return TuistGenerator.Workspace(name: manifest.name,
|
||||
projects: manifest.projects.flatMap(globProjects),
|
||||
additionalFiles: manifest.additionalFiles.flatMap(workspaceElement))
|
||||
}
|
||||
}
|
||||
|
||||
extension TuistGenerator.Project {
|
||||
static func from(manifest: ProjectDescription.Project,
|
||||
path: AbsolutePath,
|
||||
fileHandler: FileHandling) throws -> TuistGenerator.Project {
|
||||
fileHandler: FileHandling,
|
||||
printer: Printing) throws -> TuistGenerator.Project {
|
||||
let name = manifest.name
|
||||
let settings = manifest.settings.map { TuistGenerator.Settings.from(manifest: $0, path: path) }
|
||||
let targets = try manifest.targets.map { try TuistGenerator.Target.from(manifest: $0, path: path, fileHandler: fileHandler) }
|
||||
|
||||
let additionalFiles = manifest.additionalFiles.flatMap {
|
||||
TuistGenerator.FileElement.from(manifest: $0,
|
||||
path: path,
|
||||
fileHandler: fileHandler,
|
||||
printer: printer)
|
||||
}
|
||||
|
||||
return Project(path: path,
|
||||
name: name,
|
||||
settings: settings,
|
||||
filesGroup: .group(name: "Project"),
|
||||
targets: targets)
|
||||
targets: targets,
|
||||
additionalFiles: additionalFiles)
|
||||
}
|
||||
|
||||
func adding(target: TuistGenerator.Target) -> TuistGenerator.Project {
|
||||
|
@ -144,7 +168,8 @@ extension TuistGenerator.Project {
|
|||
name: name,
|
||||
settings: settings,
|
||||
filesGroup: filesGroup,
|
||||
targets: targets + [target])
|
||||
targets: targets + [target],
|
||||
additionalFiles: additionalFiles)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import XCTest
|
|||
@testable import TuistGenerator
|
||||
|
||||
final class ProjectFileElementsTests: XCTestCase {
|
||||
typealias FileElement = ProjectFileElements.FileElement
|
||||
typealias FileElement = ProjectFileElements.GroupFileElement
|
||||
|
||||
var subject: ProjectFileElements!
|
||||
var fileHandler: MockFileHandler!
|
||||
|
@ -20,16 +20,27 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
}
|
||||
|
||||
func test_projectFiles() {
|
||||
let settings = Settings(base: [:],
|
||||
debug: Configuration(settings: [:],
|
||||
xcconfig: AbsolutePath("/project/debug.xcconfig")),
|
||||
release: Configuration(settings: [:],
|
||||
xcconfig: AbsolutePath("/project/release.xcconfig")))
|
||||
// Given
|
||||
let settings = Settings(debug: Configuration(xcconfig: AbsolutePath("/project/debug.xcconfig")),
|
||||
release: Configuration(xcconfig: AbsolutePath("/project/release.xcconfig")))
|
||||
|
||||
let project = Project.test(path: AbsolutePath("/project/"), settings: settings)
|
||||
let project = Project.test(path: AbsolutePath("/project/"),
|
||||
settings: settings,
|
||||
additionalFiles: [
|
||||
.file(path: "/path/to/file"),
|
||||
.folderReference(path: "/path/to/folder"),
|
||||
])
|
||||
|
||||
// When
|
||||
let files = subject.projectFiles(project: project)
|
||||
XCTAssertTrue(files.contains(AbsolutePath("/project/debug.xcconfig")))
|
||||
XCTAssertTrue(files.contains(AbsolutePath("/project/release.xcconfig")))
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(files.isSuperset(of: [
|
||||
FileElement(path: "/project/debug.xcconfig", group: project.filesGroup),
|
||||
FileElement(path: "/project/release.xcconfig", group: project.filesGroup),
|
||||
FileElement(path: "/path/to/file", group: project.filesGroup),
|
||||
FileElement(path: "/path/to/folder", group: project.filesGroup, isReference: true),
|
||||
]))
|
||||
}
|
||||
|
||||
func test_targetProducts() {
|
||||
|
|
|
@ -43,7 +43,7 @@ final class WorkspaceGeneratorTests: XCTestCase {
|
|||
"Website/about.html",
|
||||
])
|
||||
|
||||
let additionalFiles: [Workspace.Element] = [
|
||||
let additionalFiles: [FileElement] = [
|
||||
.file(path: path.appending(RelativePath("README.md"))),
|
||||
.file(path: path.appending(RelativePath("Documentation/README.md"))),
|
||||
.folderReference(path: path.appending(RelativePath("Website"))),
|
||||
|
|
|
@ -94,7 +94,7 @@ class WorkspaceStructureGeneratorTests: XCTestCase {
|
|||
"/path/to/workspace/README.md",
|
||||
])
|
||||
|
||||
let additionalFiles: [Workspace.Element] = [
|
||||
let additionalFiles: [FileElement] = [
|
||||
.folderReference(path: "/path/to/workspace/Documentation/Guides"),
|
||||
.folderReference(path: "/path/to/workspace/Documentation/Proposals"),
|
||||
.file(path: "/path/to/workspace/README.md"),
|
||||
|
@ -200,7 +200,7 @@ class WorkspaceStructureGeneratorTests: XCTestCase {
|
|||
"/path/to/workspace/Modules/A",
|
||||
])
|
||||
|
||||
let files: [Workspace.Element] = [
|
||||
let files: [FileElement] = [
|
||||
.folderReference(path: "/path/to/workspace/Modules/A"),
|
||||
]
|
||||
let workspace = Workspace.test(projects: projects,
|
||||
|
|
|
@ -7,11 +7,13 @@ extension Project {
|
|||
name: String = "Project",
|
||||
settings: Settings? = Settings.test(),
|
||||
filesGroup: ProjectGroup = .group(name: "Project"),
|
||||
targets: [Target] = [Target.test()]) -> Project {
|
||||
targets: [Target] = [Target.test()],
|
||||
additionalFiles: [FileElement] = []) -> Project {
|
||||
return Project(path: path,
|
||||
name: name,
|
||||
settings: settings,
|
||||
filesGroup: filesGroup,
|
||||
targets: targets)
|
||||
targets: targets,
|
||||
additionalFiles: additionalFiles)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import Foundation
|
|||
extension Workspace {
|
||||
static func test(name: String = "test",
|
||||
projects: [AbsolutePath] = [],
|
||||
additionalFiles: [Workspace.Element] = []) -> Workspace {
|
||||
additionalFiles: [FileElement] = []) -> Workspace {
|
||||
return Workspace(name: name,
|
||||
projects: projects,
|
||||
additionalFiles: additionalFiles)
|
||||
|
|
|
@ -64,7 +64,7 @@ final class DumpCommandTests: XCTestCase {
|
|||
encoding: .utf8)
|
||||
let result = try parser.parse([DumpCommand.command, "-p", tmpDir.path.asString])
|
||||
try subject.run(with: result)
|
||||
let expected = "{\n \"name\": \"tuist\",\n \"targets\": [\n\n ]\n}\n"
|
||||
let expected = "{\n \"additionalFiles\": [\n\n ],\n \"name\": \"tuist\",\n \"targets\": [\n\n ]\n}\n"
|
||||
XCTAssertEqual(printer.printArgs.first, expected)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,6 +89,57 @@ class GeneratorModelLoaderTest: XCTestCase {
|
|||
XCTAssertEqual(model.targets[2].name, "Project-Manifest")
|
||||
}
|
||||
|
||||
func test_loadProject_withAdditionalFiles() throws {
|
||||
// Given
|
||||
let files = try createFiles([
|
||||
"Documentation/README.md",
|
||||
"Documentation/guide.md",
|
||||
])
|
||||
|
||||
let manifests = [
|
||||
path: ProjectManifest.test(name: "SomeProject",
|
||||
additionalFiles: [
|
||||
"Documentation/**/*.md",
|
||||
]),
|
||||
]
|
||||
|
||||
let manifestLoader = createManifestLoader(with: manifests)
|
||||
let subject = GeneratorModelLoader(fileHandler: fileHandler,
|
||||
manifestLoader: manifestLoader,
|
||||
manifestTargetGenerator: manifestTargetGenerator)
|
||||
|
||||
// When
|
||||
let model = try subject.loadProject(at: path)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(model.additionalFiles, files.map { .file(path: $0) })
|
||||
}
|
||||
|
||||
func test_loadProject_withFolderReferences() throws {
|
||||
// Given
|
||||
let files = try createFolders([
|
||||
"Stubs",
|
||||
])
|
||||
|
||||
let manifests = [
|
||||
path: ProjectManifest.test(name: "SomeProject",
|
||||
additionalFiles: [
|
||||
.folderReference(path: "Stubs"),
|
||||
]),
|
||||
]
|
||||
|
||||
let manifestLoader = createManifestLoader(with: manifests)
|
||||
let subject = GeneratorModelLoader(fileHandler: fileHandler,
|
||||
manifestLoader: manifestLoader,
|
||||
manifestTargetGenerator: manifestTargetGenerator)
|
||||
|
||||
// When
|
||||
let model = try subject.loadProject(at: path)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(model.additionalFiles, files.map { .folderReference(path: $0) })
|
||||
}
|
||||
|
||||
func test_loadWorkspace() throws {
|
||||
// Given
|
||||
let manifests = [
|
||||
|
|
|
@ -4,7 +4,7 @@ import Foundation
|
|||
extension Workspace {
|
||||
static func test(name: String = "Workspace",
|
||||
projects: [String] = [],
|
||||
additionalFiles: [Element] = []) -> Workspace {
|
||||
additionalFiles: [FileElement] = []) -> Workspace {
|
||||
return Workspace(name: name,
|
||||
projects: projects,
|
||||
additionalFiles: additionalFiles)
|
||||
|
@ -14,10 +14,12 @@ extension Workspace {
|
|||
extension Project {
|
||||
static func test(name: String = "Project",
|
||||
settings: Settings? = nil,
|
||||
targets: [Target] = []) -> Project {
|
||||
targets: [Target] = [],
|
||||
additionalFiles: [FileElement] = []) -> Project {
|
||||
return Project(name: name,
|
||||
settings: settings,
|
||||
targets: targets)
|
||||
targets: targets,
|
||||
additionalFiles: additionalFiles)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,12 @@ let project = Project(name: "MyProject",
|
|||
.project(target: "Framework1", path: "../Framework1"),
|
||||
.project(target: "Framework2", path: "../Framework2"),
|
||||
])
|
||||
])
|
||||
],
|
||||
additionalFiles: [
|
||||
"Dangerfile.swift",
|
||||
"Documentation/**",
|
||||
.folderReference(path: "Website")
|
||||
])
|
||||
```
|
||||
|
||||
### Project
|
||||
|
@ -31,6 +36,7 @@ A `Project.swift` should initialize a variable of type `Project`. It can take an
|
|||
- **Name:** Name of the project. It's used to determine the name of the generated Xcode project.
|
||||
- **Targets:** It contains the list of targets that belong to the project. Read more [about targets](#target)
|
||||
- **Settings (optional):** Read more about [settings](#settings)
|
||||
- **AdditionalFiles (optional):**: List of [FileElement](#FileElement)s to include in the project - these won't be included in any of the build phases.
|
||||
|
||||
> **Note:** All the relative paths in the project manifest are relative to the folder that contains the manifest file.
|
||||
|
||||
|
@ -96,6 +102,15 @@ A `Settings` object contains an optional dictionary with build settings and rela
|
|||
- **Debug (optional):** The debug configuration settings. The settings are initialized with `.settings([String: String], xcconfig: String)` where the first argument are the build settings and the second a relative path to an xcconfig file. Both arguments are optionals.
|
||||
- **Release (optional):** Same as debug but for the release configuration.
|
||||
|
||||
### FileElement
|
||||
|
||||
A `FileElement` can be one of the following options:
|
||||
|
||||
- `.glob(pattern: String)`: A file path (or glob pattern) to include.
|
||||
- *For convenience, a string literal can be used as an alternate way to specify this option.*
|
||||
- `.folderReference(path: String)`: A directory path to include as a folder reference.
|
||||
|
||||
|
||||
## Workspace.swift
|
||||
|
||||
By default, `tuist generate` generates an Xcode workspace that has the same name as the current project. It includes the project and all its dependencies. Tuist allows customizing this behaviour by defining a workspace manifest within a `Workspace.swift` file.
|
||||
|
@ -128,9 +143,6 @@ A `Workspace.swift` should initialize a variable of type `Workspace`. It can tak
|
|||
|
||||
- **Name:** Name of the workspace. It’s used to determine the name of the generated Xcode workspace.
|
||||
- **Projects:** List of paths (or glob patterns) to projects to generate and include within the generated Xcode workspace.
|
||||
- **AdditionalFiles:**: List of one of the following options
|
||||
- `.glob(pattern: String)`: A file path (or glob pattern) to include in the workspace.
|
||||
- *For convenience, a string literal can be used as an alternate way to specify this option.*
|
||||
- `.folderReference(path: String)`: A directory path to include as a folder reference in the workspace.
|
||||
- **AdditionalFiles (optional):**: List of [FileElement](#FileElement)s to include in the workspace - these won't be included in any of the projects or their build phases.
|
||||
|
||||
>***Note:***All the relative paths in the workspace manifest are relative to the folder that contains the manifest file.*
|
||||
|
|
|
@ -14,8 +14,14 @@ Contains a few projects and a `Workspace.swift` manifest file.
|
|||
The workspace manifest defines:
|
||||
|
||||
- glob patterns to list projects
|
||||
- glob patterns to list additional files
|
||||
- folder references
|
||||
- glob patterns to include documentation files
|
||||
- folder reference to directory with html files
|
||||
|
||||
The App's project manifest leverages `additionalFiles` tha defines:
|
||||
|
||||
- glob patterns to include documentation files
|
||||
- Includes a swift `Danger.swift` file that shouldn't get included in any buid phase
|
||||
- folder references to a directory with json files
|
||||
|
||||
## ios_app_with_tests
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
import Danger
|
||||
|
||||
// This should fail to build if it
|
||||
// accidentally gets added to the project
|
||||
// sources build phases
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
# Item
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
# List
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
# App Documentation
|
||||
|
|
@ -20,5 +20,9 @@ let project = Project(name: "MainApp",
|
|||
sources: "Tests/**",
|
||||
dependencies: [
|
||||
.target(name: "App"),
|
||||
]),
|
||||
])
|
||||
])],
|
||||
additionalFiles: [
|
||||
"Dangerfile.swift",
|
||||
"Documentation/**",
|
||||
.folderReference(path: "Responses"),
|
||||
])
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"id": "a",
|
||||
"description": "Item a"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"items": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
|
@ -20,5 +20,5 @@ let project = Project(name: "Framework1",
|
|||
sources: "Tests/**",
|
||||
dependencies: [
|
||||
.target(name: "Framework1"),
|
||||
]),
|
||||
])
|
||||
])
|
||||
])
|
||||
|
|
Loading…
Reference in New Issue