Skip to content
Permalink
Browse files

Workspace Improvements (#298)

Resolves #258

### Short description 

Workspace manifests allow users to customize which project are included in their workspace however doesn't yet offer the ability to add arbitrary files and folder references (to support included things like Documentation and README files).

### Solution 

Extend the workspace manifest to allow specifying `additionalFiles`

e.g.

```swift
import ProjectDescription

let workspace = Workspace(name: "Workspace",
                          projects: [
                              "App", 
                              "Frameworks/FrameworkA", 
                              "Frameworks/FrameworkB", 
                            ],
                            additionalFiles: [
                                "Documentation/**",
                                .folderReference(path: "Website")
                            ])
```

As an added bonus of these changes, we can add the ability to specify glob patterns for `projects` to make it easier to include new projects without needing to constantly update the workspace manifest.

e.g.

```swift
import ProjectDescription

let workspace = Workspace(name: "Workspace",
                          projects: [
                              "App", 
                              "Frameworks/**", 
                            ],
                            additionalFiles: [
                                "Documentation/**",
                                .folderReference(path: "Website")
                            ])
```

### Implementation 

- [x] Update workspace manifests
- [x] Parse workspace manifests > models
- [x] Add workspace structure generator
- [x] Integrate workspace structure
- [x] Include fixture
- [x] Update documentation

### Test Plan 

- Run `tuist generate` on the following fixtures:
  - `fixture/ios_app_with_custom_workspace`
   
<img width="254" alt="custom-workspace" src="https://user-images.githubusercontent.com/11914919/54780257-36db2f80-4c11-11e9-8fda-5f59554533c0.png">

  - `fixture/ios_app_with_custom_workspace/App`

<img width="161" alt="custom-workspace-app" src="https://user-images.githubusercontent.com/11914919/54780299-4a869600-4c11-11e9-9637-cdfaa20bb8c8.png">

  - `fixture/ios_app_with_setup`

<img width="175" alt="setup" src="https://user-images.githubusercontent.com/11914919/54780315-540ffe00-4c11-11e9-99fd-e9175ee4de27.png">

  - `fixture/ios_app_with_frameworks`

<img width="184" alt="frameworks" src="https://user-images.githubusercontent.com/11914919/54780335-5d00cf80-4c11-11e9-906c-b53f26c36007.png">

Verify the generated workspace structure

### Notes 

All this work is based on group effort and iteration from #262 - thanks @ollieatkinson, @pepibumur! πŸ‘
  • Loading branch information...
kwridan committed Mar 23, 2019
1 parent 2153c7d commit fe44584b2b6f55934966c19f9ee5163f870dd617
Showing with 1,887 additions and 205 deletions.
  1. +1 βˆ’0 CHANGELOG.md
  2. +76 βˆ’12 Sources/ProjectDescription/Workspace.swift
  3. +23 βˆ’0 Sources/TuistCore/Extensions/AbsolutePath+Extras.swift
  4. +44 βˆ’19 Sources/TuistKit/Generator/Generator.swift
  5. +66 βˆ’6 Sources/TuistKit/Generator/GeneratorModelLoader.swift
  6. +86 βˆ’28 Sources/TuistKit/Generator/WorkspaceGenerator.swift
  7. +214 βˆ’0 Sources/TuistKit/Generator/WorkspaceStructureGenerator.swift
  8. +5 βˆ’1 Sources/TuistKit/Graph/Graph.swift
  9. +6 βˆ’6 Sources/TuistKit/Graph/GraphLoader.swift
  10. +39 βˆ’1 Sources/TuistKit/Models/Workspace.swift
  11. +39 βˆ’1 Tests/ProjectDescriptionTests/WorkspaceTests.swift
  12. +50 βˆ’1 Tests/TuistCoreTests/Extensions/AbsolutePath+ExtrasTests.swift
  13. +2 βˆ’2 Tests/TuistKitTests/Commands/FocusCommandTests.swift
  14. +4 βˆ’4 Tests/TuistKitTests/Commands/GenerateCommandTests.swift
  15. +188 βˆ’3 Tests/TuistKitTests/Generator/GeneratorModelLoaderTest.swift
  16. +152 βˆ’0 Tests/TuistKitTests/Generator/GeneratorTests.swift
  17. +6 βˆ’6 Tests/TuistKitTests/Generator/Mocks/MockGenerator.swift
  18. +9 βˆ’3 Tests/TuistKitTests/Generator/Mocks/MockWorkspaceGenerator.swift
  19. +4 βˆ’2 Tests/TuistKitTests/Generator/TestData/ProjectDescription+TestData.swift
  20. +111 βˆ’60 Tests/TuistKitTests/Generator/WorkspaceGeneratorTests.swift
  21. +344 βˆ’0 Tests/TuistKitTests/Generator/WorkspaceStructureGeneratorTests.swift
  22. +6 βˆ’6 Tests/TuistKitTests/Graph/Mocks/MockGraphLoader.swift
  23. +1 βˆ’1 Tests/TuistKitTests/Models/TestData/Target+TestData.swift
  24. +13 βˆ’0 Tests/TuistKitTests/Models/TestData/Workspace+TestData.swift
  25. +20 βˆ’6 docs/usage/manifest.md
  26. +53 βˆ’37 fixtures/README.md
  27. +43 βˆ’0 fixtures/ios_app_with_custom_workspace/App/Config/App-Info.plist
  28. +22 βˆ’0 fixtures/ios_app_with_custom_workspace/App/Config/AppTests-Info.plist
  29. +24 βˆ’0 fixtures/ios_app_with_custom_workspace/App/Project.swift
  30. +22 βˆ’0 fixtures/ios_app_with_custom_workspace/App/Sources/AppDelegate.swift
  31. +12 βˆ’0 fixtures/ios_app_with_custom_workspace/App/Tests/AppDelegateTests.swift
  32. +1 βˆ’0 fixtures/ios_app_with_custom_workspace/Documentation/Components/UIComponents.md
  33. +1 βˆ’0 fixtures/ios_app_with_custom_workspace/Documentation/README.md
  34. +24 βˆ’0 fixtures/ios_app_with_custom_workspace/Frameworks/Framework1/Config/Framework1-Info.plist
  35. +22 βˆ’0 fixtures/ios_app_with_custom_workspace/Frameworks/Framework1/Config/Framework1Tests-Info.plist
  36. +24 βˆ’0 fixtures/ios_app_with_custom_workspace/Frameworks/Framework1/Project.swift
  37. +16 βˆ’0 fixtures/ios_app_with_custom_workspace/Frameworks/Framework1/Sources/Framework1File.swift
  38. +16 βˆ’0 fixtures/ios_app_with_custom_workspace/Frameworks/Framework1/Tests/Framework1FileTests.swift
  39. +24 βˆ’0 fixtures/ios_app_with_custom_workspace/Frameworks/Framework2/Config/Framework2-Info.plist
  40. +22 βˆ’0 fixtures/ios_app_with_custom_workspace/Frameworks/Framework2/Config/Framework2Tests-Info.plist
  41. +22 βˆ’0 fixtures/ios_app_with_custom_workspace/Frameworks/Framework2/Project.swift
  42. +9 βˆ’0 fixtures/ios_app_with_custom_workspace/Frameworks/Framework2/Sources/Framework2File.swift
  43. +10 βˆ’0 fixtures/ios_app_with_custom_workspace/Frameworks/Framework2/Tests/Framework2FileTests.swift
  44. 0 fixtures/ios_app_with_custom_workspace/Website/about.html
  45. 0 fixtures/ios_app_with_custom_workspace/Website/index.html
  46. +11 βˆ’0 fixtures/ios_app_with_custom_workspace/Workspace.swift
@@ -12,6 +12,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
- Create a Setup.swift file when running the init command https://github.com/tuist/tuist/pull/283 by @pepibumur
- Update `tuistenv` when running `tuist update` https://github.com/tuist/tuist/pull/288 by @pepibumur.
- Allow linking of static products into dynamic frameworks https://github.com/tuist/tuist/pull/299 by @ollieatkinson
- Workspace improvements https://github.com/tuist/tuist/pull/298 by @ollieatkinson & @kwridan.

### Removed

@@ -3,24 +3,88 @@ import Foundation
// MARK: - Workspace
public class Workspace: Codable {
// MARK: - Codable
enum CodingKeys: String, CodingKey {
case name
case projects
}

// Workspace name
/// Name of the workspace
public let name: String

// Relative paths to the projects.
// Note: The paths are relative from the folder that contains the workspace.
/// List of project relative paths (or glob patterns) to generate and include
public let projects: [String]

public init(name: String,
projects: [String]) {
/// List of files to include in the workspace (e.g. Documentation)
public let additionalFiles: [Element]

/// Workspace
///
/// This can be used to customize the generated workspace.
///
/// - Parameters:
/// - 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] = []) {
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)
}
}
@@ -32,4 +32,27 @@ extension AbsolutePath {
public func removingLastComponent() -> AbsolutePath {
return AbsolutePath("/\(components.dropLast().joined(separator: "/"))")
}

/// Returns the common ancestor path with another path
///
/// e.g.
/// /path/to/a
/// /path/another/b
///
/// common ancestor: /path
///
/// - Parameter path: The other path to find a common path with
/// - Returns: An absolute path to the common ancestor
public func commonAncestor(with path: AbsolutePath) -> AbsolutePath {
var ancestorPath = AbsolutePath("/")
for component in components.dropFirst() {
let nextPath = ancestorPath.appending(component: component)
if path.contains(nextPath) {
ancestorPath = nextPath
} else {
break
}
}
return ancestorPath
}
}
@@ -16,19 +16,22 @@ struct GeneratorConfig {
}

protocol Generating {
func generateProject(at path: AbsolutePath, config: GeneratorConfig) throws -> AbsolutePath
func generateWorkspace(at path: AbsolutePath, config: GeneratorConfig) throws -> AbsolutePath
func generateProject(at path: AbsolutePath, config: GeneratorConfig, workspaceFiles: [AbsolutePath]) throws -> AbsolutePath
func generateWorkspace(at path: AbsolutePath, config: GeneratorConfig, workspaceFiles: [AbsolutePath]) throws -> AbsolutePath
}

extension Generating {
func generate(at path: AbsolutePath,
config: GeneratorConfig,
manifestLoader: GraphManifestLoading) throws -> AbsolutePath {
let manifests = manifestLoader.manifests(at: path)
let workspaceFiles: [AbsolutePath] = [Manifest.workspace, Manifest.setup]
.compactMap { try? manifestLoader.manifestPath(at: path, manifest: $0) }

if manifests.contains(.workspace) {
return try generateWorkspace(at: path, config: config)
return try generateWorkspace(at: path, config: config, workspaceFiles: workspaceFiles)
} else if manifests.contains(.project) {
return try generateProject(at: path, config: config)
return try generateProject(at: path, config: config, workspaceFiles: workspaceFiles)
} else {
throw GraphManifestLoaderError.manifestNotFound(path)
}
@@ -39,30 +42,52 @@ class Generator: Generating {
private let graphLoader: GraphLoading
private let workspaceGenerator: WorkspaceGenerating

init(system: Systeming = System(),
printer: Printing = Printer(),
fileHandler: FileHandling = FileHandler(),
modelLoader: GeneratorModelLoading) {
graphLoader = GraphLoader(printer: printer, modelLoader: modelLoader)
workspaceGenerator = WorkspaceGenerator(system: system,
printer: printer,
projectDirectoryHelper: ProjectDirectoryHelper(),
fileHandler: fileHandler)
convenience init(system: Systeming = System(),
printer: Printing = Printer(),
fileHandler: FileHandling = FileHandler(),
modelLoader: GeneratorModelLoading) {
let graphLoader = GraphLoader(printer: printer, modelLoader: modelLoader)
let workspaceGenerator = WorkspaceGenerator(system: system,
printer: printer,
projectDirectoryHelper: ProjectDirectoryHelper(),
fileHandler: fileHandler)
self.init(graphLoader: graphLoader,
workspaceGenerator: workspaceGenerator)
}

init(graphLoader: GraphLoading,
workspaceGenerator: WorkspaceGenerating) {
self.graphLoader = graphLoader
self.workspaceGenerator = workspaceGenerator
}

func generateProject(at path: AbsolutePath, config: GeneratorConfig) throws -> AbsolutePath {
let graph = try graphLoader.loadProject(path: path)
func generateProject(at path: AbsolutePath,
config: GeneratorConfig,
workspaceFiles: [AbsolutePath]) throws -> AbsolutePath {
let (graph, project) = try graphLoader.loadProject(path: path)

return try workspaceGenerator.generate(path: path,
let workspace = Workspace(name: project.name,
projects: graph.projectPaths,
additionalFiles: workspaceFiles.map(Workspace.Element.file))

return try workspaceGenerator.generate(workspace: workspace,
path: path,
graph: graph,
options: config.options,
directory: config.directory)
}

func generateWorkspace(at path: AbsolutePath, config: GeneratorConfig) throws -> AbsolutePath {
let graph = try graphLoader.loadWorkspace(path: path)
func generateWorkspace(at path: AbsolutePath,
config: GeneratorConfig,
workspaceFiles: [AbsolutePath]) throws -> AbsolutePath {
let (graph, workspace) = try graphLoader.loadWorkspace(path: path)

let updatedWorkspace = workspace
.merging(projects: graph.projectPaths)
.adding(files: workspaceFiles)

return try workspaceGenerator.generate(path: path,
return try workspaceGenerator.generate(workspace: updatedWorkspace,
path: path,
graph: graph,
options: config.options,
directory: config.directory)
@@ -25,13 +25,15 @@ class GeneratorModelLoader: GeneratorModelLoading {
private let fileHandler: FileHandling
private let manifestLoader: GraphManifestLoading
private let manifestTargetGenerator: ManifestTargetGenerating

private let printer: Printing
init(fileHandler: FileHandling,
manifestLoader: GraphManifestLoading,
manifestTargetGenerator: ManifestTargetGenerating) {
manifestTargetGenerator: ManifestTargetGenerating,
printer: Printing = Printer()) {
self.fileHandler = fileHandler
self.manifestLoader = manifestLoader
self.manifestTargetGenerator = manifestTargetGenerator
self.printer = printer
}

func loadProject(at path: AbsolutePath) throws -> Project {
@@ -46,16 +48,74 @@ class GeneratorModelLoader: GeneratorModelLoading {

func loadWorkspace(at path: AbsolutePath) throws -> Workspace {
let manifest = try manifestLoader.loadWorkspace(at: path)
let workspace = try TuistKit.Workspace.from(manifest: manifest, path: path)
let workspace = try TuistKit.Workspace.from(manifest: manifest,
path: path,
fileHandler: fileHandler,
manifestLoader: manifestLoader,
printer: printer)
return workspace
}
}

extension TuistKit.Workspace {
static func from(manifest: ProjectDescription.Workspace,
path: AbsolutePath) throws -> TuistKit.Workspace {
return Workspace(name: manifest.name,
projects: manifest.projects.map { path.appending(RelativePath($0)) })
path: AbsolutePath,
fileHandler: FileHandling,
manifestLoader: GraphManifestLoading,
printer: Printing) throws -> TuistKit.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
.filter(fileHandler.isFolder)
.filter {
manifestLoader.manifests(at: $0).contains(.project)
}

if projects.isEmpty {
printer.print(warning: "No projects found at: \(string)")
}

return Array(projects)
}

func folderReferences(_ relativePath: String) -> [AbsolutePath] {
let folderReferencePath = path.appending(RelativePath(relativePath))

guard fileHandler.exists(folderReferencePath) else {
printer.print(warning: "\(relativePath) does not exist")
return []
}

guard fileHandler.isFolder(folderReferencePath) else {
printer.print(warning: "\(relativePath) is not a directory - folder reference paths need to point to directories")
return []
}

return [folderReferencePath]
}

func workspaceElement(from element: ProjectDescription.Workspace.Element) -> [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)
}
}

return TuistKit.Workspace(name: manifest.name,
projects: manifest.projects.flatMap(globProjects),
additionalFiles: manifest.additionalFiles.flatMap(workspaceElement))
}
}

Oops, something went wrong.

0 comments on commit fe44584

Please sign in to comment.
You can’t perform that action at this time.