Generate project when editing (#958)

* Pin XcodeProj to a branch until it's merged

* Add a way to specify PathRunnable in custom scheme

* Make scheme buildable when `tuist edit`-ing

* Added changelog entry

* Style correct

* Update Xcodeproj

* Using same path to tuist executable that was used to invoke the edit command

* Update documentation

* style update
This commit is contained in:
Vytis 2020-02-17 23:11:04 +01:00 committed by GitHub
parent e1c122d2c7
commit 350a53ee08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 170 additions and 82 deletions

View File

@ -5,6 +5,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
## Next
### Added
- When using `tuist edit` it's possible to run `tuist generate` from Xcode by simply running the target https://github.com/tuist/tuist/pull/958 by @vytis
- Add FAQ section by @mollyIV
- Add benchmarking helper tool https://github.com/tuist/tuist/pull/957 by @kwridan.

View File

@ -6,15 +6,18 @@ public struct RunAction: Equatable {
public let configurationName: String
public let executable: TargetReference?
public let filePath: AbsolutePath?
public let arguments: Arguments?
// MARK: - Init
public init(configurationName: String,
executable: TargetReference? = nil,
filePath: AbsolutePath? = nil,
arguments: Arguments? = nil) {
self.configurationName = configurationName
self.executable = executable
self.filePath = filePath
self.arguments = arguments
}
}

View File

@ -5,9 +5,11 @@ import Foundation
public extension RunAction {
static func test(configurationName: String = BuildConfiguration.debug.name,
executable: TargetReference? = TargetReference(projectPath: "/Project", name: "App"),
filePath: AbsolutePath? = nil,
arguments: Arguments? = Arguments.test()) -> RunAction {
RunAction(configurationName: configurationName,
executable: executable,
filePath: filePath,
arguments: arguments)
}
}

View File

@ -41,7 +41,7 @@ class ProjectGroups {
frameworks: PBXGroup,
playgrounds: PBXGroup?,
pbxproj: PBXProj) {
self.sortedMain = main
sortedMain = main
self.projectGroups = Dictionary(uniqueKeysWithValues: projectGroups)
self.products = products
self.frameworks = frameworks

View File

@ -330,19 +330,26 @@ final class SchemesGenerator: SchemesGenerating {
target = executable
}
guard let targetNode = try graph.target(path: target.projectPath, name: target.name) else { return nil }
guard let buildableReference = try createBuildableReference(targetReference: target,
graph: graph,
rootPath: rootPath,
generatedProjects: generatedProjects) else { return nil }
var buildableProductRunnable: XCScheme.BuildableProductRunnable?
var macroExpansion: XCScheme.BuildableReference?
var pathRunnable: XCScheme.PathRunnable?
var defaultBuildConfiguration = BuildConfiguration.debug.name
if targetNode.target.product.runnable {
buildableProductRunnable = XCScheme.BuildableProductRunnable(buildableReference: buildableReference, runnableDebuggingMode: "0")
if let filePath = scheme.runAction?.filePath {
pathRunnable = XCScheme.PathRunnable(filePath: filePath.pathString)
} else {
macroExpansion = buildableReference
guard let targetNode = try graph.target(path: target.projectPath, name: target.name) else { return nil }
defaultBuildConfiguration = defaultDebugBuildConfigurationName(in: targetNode.project)
guard let buildableReference = try createBuildableReference(targetReference: target,
graph: graph,
rootPath: rootPath,
generatedProjects: generatedProjects) else { return nil }
if targetNode.target.product.runnable {
buildableProductRunnable = XCScheme.BuildableProductRunnable(buildableReference: buildableReference, runnableDebuggingMode: "0")
} else {
macroExpansion = buildableReference
}
}
var commandlineArguments: XCScheme.CommandLineArguments?
@ -353,10 +360,11 @@ final class SchemesGenerator: SchemesGenerating {
environments = environmentVariables(arguments.environment)
}
let buildConfiguration = scheme.runAction?.configurationName ?? defaultDebugBuildConfigurationName(in: targetNode.project)
let buildConfiguration = scheme.runAction?.configurationName ?? defaultBuildConfiguration
return XCScheme.LaunchAction(runnable: buildableProductRunnable,
buildConfiguration: buildConfiguration,
macroExpansion: macroExpansion,
pathRunnable: pathRunnable,
commandlineArguments: commandlineArguments,
environmentVariables: environments)
}

View File

@ -4,7 +4,7 @@ import XcodeProj
@propertyWrapper
class SortedPBXGroup {
var value: PBXGroup
var wrappedValue: PBXGroup {
get {
value.childGroups.forEach(sort) // We preserve the order of the root level groups and files
@ -14,11 +14,11 @@ class SortedPBXGroup {
value = newValue
}
}
init(wrappedValue: PBXGroup) {
self.value = wrappedValue
value = wrappedValue
}
// The sorting implementation was taken from https://github.com/yonaskolb/XcodeGen/blob/d64cfff8a1ca01fd8f18cbb41f72230983c4a192/Sources/XcodeGenKit/PBXProjGenerator.swift
// We require exactly the same sort which places groups over files while using the PBXGroup from Xcodeproj.
private func sort(with group: PBXGroup) {
@ -52,7 +52,7 @@ private extension PBXFileElement {
}
static func sortByNameThenPath(_ lhs: PBXFileElement, _ rhs: PBXFileElement) -> Bool {
return lhs.namePathSortString.localizedStandardCompare(rhs.namePathSortString) == .orderedAscending
lhs.namePathSortString.localizedStandardCompare(rhs.namePathSortString) == .orderedAscending
}
var namePathSortString: String {

View File

@ -74,7 +74,11 @@ final class ProjectEditor: ProjectEditing {
throw ProjectEditorError.noEditableFiles(at)
}
let (project, graph) = projectEditorMapper.map(sourceRootPath: at,
// To be sure that we are using the same binary of Tuist that invoked `edit`
let tuistPath = AbsolutePath(CommandRegistry.processArguments().first!)
let (project, graph) = projectEditorMapper.map(tuistPath: tuistPath,
sourceRootPath: at,
manifests: manifests.map { $0.1 },
helpers: helpers,
projectDescriptionPath: projectDesciptionPath)

View File

@ -4,7 +4,8 @@ import TuistCore
import TuistSupport
protocol ProjectEditorMapping: AnyObject {
func map(sourceRootPath: AbsolutePath,
func map(tuistPath: AbsolutePath,
sourceRootPath: AbsolutePath,
manifests: [AbsolutePath],
helpers: [AbsolutePath],
projectDescriptionPath: AbsolutePath) -> (Project, Graph)
@ -12,7 +13,8 @@ protocol ProjectEditorMapping: AnyObject {
final class ProjectEditorMapper: ProjectEditorMapping {
// swiftlint:disable:next function_body_length
func map(sourceRootPath: AbsolutePath,
func map(tuistPath: AbsolutePath,
sourceRootPath: AbsolutePath,
manifests: [AbsolutePath],
helpers: [AbsolutePath],
projectDescriptionPath: AbsolutePath) -> (Project, Graph) {
@ -54,12 +56,20 @@ final class ProjectEditorMapper: ProjectEditorMapping {
targets.append(manifestsTarget)
if let helpersTarget = helpersTarget { targets.append(helpersTarget) }
// Run Scheme
let buildAction = BuildAction(targets: targets.map { TargetReference(projectPath: sourceRootPath, name: $0.name) })
let arguments = Arguments(launch: ["generate --path \(sourceRootPath)": true])
let runAction = RunAction(configurationName: "Debug", filePath: tuistPath, arguments: arguments)
let scheme = Scheme(name: "Manifests", shared: true, buildAction: buildAction, runAction: runAction)
// Project
let project = Project(path: sourceRootPath,
name: "Manifests",
settings: projectSettings,
filesGroup: .group(name: "Manifests"),
targets: targets)
targets: targets,
schemes: [scheme])
// Graph
let cache = GraphLoaderCache()

View File

@ -459,6 +459,31 @@ final class SchemesGeneratorTests: XCTestCase {
let target = Target.test(name: "Library", platform: .iOS, product: .dynamicLibrary)
let buildAction = BuildAction.test(targets: [TargetReference(projectPath: projectPath, name: "Library")])
let launchAction = RunAction.test(configurationName: "Debug", filePath: "/usr/bin/foo")
let scheme = Scheme.test(name: "Library", buildAction: buildAction, runAction: launchAction)
let project = Project.test(path: projectPath, targets: [target])
let graph = Graph.create(dependencies: [(project: project, target: target, dependencies: [])])
// When
let got = try subject.schemeLaunchAction(scheme: scheme,
graph: graph,
rootPath: projectPath,
generatedProjects: createGeneratedProjects(projects: [project]))
// Then
let result = try XCTUnwrap(got)
XCTAssertNil(result.runnable?.buildableReference)
XCTAssertEqual(result.buildConfiguration, "Debug")
XCTAssertEqual(result.pathRunnable?.filePath, "/usr/bin/foo")
}
func test_schemeLaunchAction_with_path() throws {
let projectPath = AbsolutePath("/somepath/Project")
let target = Target.test(name: "Library", platform: .iOS, product: .dynamicLibrary)
let buildAction = BuildAction.test(targets: [TargetReference(projectPath: projectPath, name: "Library")])
let testAction = TestAction.test(targets: [TestableTarget(target: TargetReference(projectPath: projectPath, name: "Library"))])

View File

@ -1,7 +1,7 @@
import XcodeProj
@testable import TuistSupportTesting
@testable import TuistGenerator
@testable import TuistSupportTesting
class SortedPBXGroupTests: TuistTestCase {
var subject: SortedPBXGroup!
@ -159,7 +159,7 @@ class SortedPBXGroupTests: TuistTestCase {
file("file2"),
file("file3"),
file("file4.swift"),
])
]),
]))
}

View File

@ -7,13 +7,15 @@ import TuistCore
final class MockProjectEditorMapper: ProjectEditorMapping {
var mapStub: (Project, Graph)?
var mapArgs: [(sourceRootPath: AbsolutePath, manifests: [AbsolutePath], helpers: [AbsolutePath], projectDescriptionPath: AbsolutePath)] = []
var mapArgs: [(tuistPath: AbsolutePath, sourceRootPath: AbsolutePath, manifests: [AbsolutePath], helpers: [AbsolutePath], projectDescriptionPath: AbsolutePath)] = []
func map(sourceRootPath: AbsolutePath,
func map(tuistPath: AbsolutePath,
sourceRootPath: AbsolutePath,
manifests: [AbsolutePath],
helpers: [AbsolutePath],
projectDescriptionPath: AbsolutePath) -> (Project, Graph) {
mapArgs.append((sourceRootPath: sourceRootPath,
mapArgs.append((tuistPath: tuistPath,
sourceRootPath: sourceRootPath,
manifests: manifests,
helpers: helpers,
projectDescriptionPath: projectDescriptionPath))

View File

@ -16,8 +16,8 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
}
override func tearDown() {
super.tearDown()
subject = nil
super.tearDown()
}
func test_edit_when_there_are_helpers() throws {
@ -26,48 +26,65 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
let manifestPaths = [sourceRootPath].map { $0.appending(component: "Project.swift") }
let helperPaths = [sourceRootPath].map { $0.appending(component: "Project+Template.swift") }
let projectDescriptionPath = sourceRootPath.appending(component: "ProjectDescription.framework")
let tuistPath = AbsolutePath("/usr/bin/foo/bar/tuist")
// When
let (project, graph) = subject.map(sourceRootPath: sourceRootPath,
let (project, graph) = subject.map(tuistPath: tuistPath,
sourceRootPath: sourceRootPath,
manifests: manifestPaths,
helpers: helperPaths,
projectDescriptionPath: projectDescriptionPath)
// Then
let manifestsTarget = project.targets.first
let helpersTarget = project.targets.last
let expectedManifestsTarget = Target(name: "Manifests",
platform: .macOS,
product: .staticFramework,
productName: "Manifests",
bundleId: "io.tuist.${PRODUCT_NAME:rfc1034identifier}",
settings: expectedSettings(sourceRootPath: sourceRootPath),
sources: manifestPaths.map { (path: $0, compilerFlags: nil) },
filesGroup: .group(name: "Manifests"),
dependencies: [.target(name: "ProjectDescriptionHelpers")])
let expectedHelpersTarget = Target(name: "ProjectDescriptionHelpers",
platform: .macOS,
product: .staticFramework,
productName: "ProjectDescriptionHelpers",
bundleId: "io.tuist.${PRODUCT_NAME:rfc1034identifier}",
settings: expectedSettings(sourceRootPath: sourceRootPath),
sources: helperPaths.map { (path: $0, compilerFlags: nil) },
filesGroup: .group(name: "Manifests"),
dependencies: [])
let expectedProject = Project(path: sourceRootPath,
name: "Manifests",
settings: Settings(base: [:],
configurations: Settings.default.configurations,
defaultSettings: .recommended),
filesGroup: .group(name: "Manifests"),
targets: [expectedManifestsTarget, expectedHelpersTarget])
XCTAssertEqual(project, expectedProject)
let targetNodes = graph.targets.sorted(by: { $0.target.name < $1.target.name })
XCTAssertEqual(targetNodes.count, 2)
XCTAssertEqual(targetNodes.first?.target, manifestsTarget)
XCTAssertEqual(targetNodes.last?.target, helpersTarget)
XCTAssertEqual(targetNodes.first?.dependencies, [targetNodes.last!])
// Generated Manifests target
let manifestsTarget = try XCTUnwrap(project.targets.first)
XCTAssertEqual(targetNodes.first?.target, manifestsTarget)
XCTAssertEqual(manifestsTarget.name, "Manifests")
XCTAssertEqual(manifestsTarget.platform, .macOS)
XCTAssertEqual(manifestsTarget.product, .staticFramework)
XCTAssertEqual(manifestsTarget.settings, expectedSettings(sourceRootPath: sourceRootPath))
XCTAssertEqual(manifestsTarget.sources.map { $0.path }, manifestPaths)
XCTAssertEqual(manifestsTarget.filesGroup, .group(name: "Manifests"))
XCTAssertEqual(manifestsTarget.dependencies, [.target(name: "ProjectDescriptionHelpers")])
// Generated Helpers target
let helpersTarget = try XCTUnwrap(project.targets.last)
XCTAssertEqual(targetNodes.last?.target, helpersTarget)
XCTAssertEqual(helpersTarget.name, "ProjectDescriptionHelpers")
XCTAssertEqual(helpersTarget.platform, .macOS)
XCTAssertEqual(helpersTarget.product, .staticFramework)
XCTAssertEqual(helpersTarget.settings, expectedSettings(sourceRootPath: sourceRootPath))
XCTAssertEqual(helpersTarget.sources.map { $0.path }, helperPaths)
XCTAssertEqual(helpersTarget.filesGroup, .group(name: "Manifests"))
XCTAssertEqual(helpersTarget.dependencies, [])
// Generated Project
XCTAssertEqual(project.path, sourceRootPath)
XCTAssertEqual(project.name, "Manifests")
XCTAssertEqual(project.settings, Settings(base: [:],
configurations: Settings.default.configurations,
defaultSettings: .recommended))
XCTAssertEqual(project.filesGroup, .group(name: "Manifests"))
XCTAssertEqual(project.targets, targetNodes.map { $0.target })
// Generated Scheme
XCTAssertEqual(project.schemes.count, 1)
let scheme = try XCTUnwrap(project.schemes.first)
XCTAssertEqual(scheme.name, "Manifests")
let buildAction = try XCTUnwrap(scheme.buildAction)
XCTAssertEqual(buildAction.targets.map { $0.name }, targetNodes.map { $0.name })
let runAction = try XCTUnwrap(scheme.runAction)
XCTAssertEqual(runAction.filePath, tuistPath)
let generateArgument = "generate --path \(sourceRootPath)"
XCTAssertEqual(runAction.arguments, Arguments(launch: [generateArgument: true]))
}
func test_edit_when_there_are_no_helpers() throws {
@ -76,39 +93,53 @@ final class ProjectEditorMapperTests: TuistUnitTestCase {
let manifestPaths = [sourceRootPath].map { $0.appending(component: "Project.swift") }
let helperPaths: [AbsolutePath] = []
let projectDescriptionPath = sourceRootPath.appending(component: "ProjectDescription.framework")
let tuistPath = AbsolutePath("/usr/bin/foo/bar/tuist")
// When
let (project, graph) = subject.map(sourceRootPath: sourceRootPath,
let (project, graph) = subject.map(tuistPath: tuistPath,
sourceRootPath: sourceRootPath,
manifests: manifestPaths,
helpers: helperPaths,
projectDescriptionPath: projectDescriptionPath)
// Then
let manifestsTarget = project.targets.first
XCTAssertEqual(project.targets.count, 1)
let expectedManifestsTarget = Target(name: "Manifests",
platform: .macOS,
product: .staticFramework,
productName: "Manifests",
bundleId: "io.tuist.${PRODUCT_NAME:rfc1034identifier}",
settings: expectedSettings(sourceRootPath: sourceRootPath),
sources: manifestPaths.map { (path: $0, compilerFlags: nil) },
filesGroup: .group(name: "Manifests"),
dependencies: [])
let expectedProject = Project(path: sourceRootPath,
name: "Manifests",
settings: Settings(base: [:],
configurations: Settings.default.configurations,
defaultSettings: .recommended),
filesGroup: .group(name: "Manifests"),
targets: [expectedManifestsTarget])
XCTAssertEqual(project, expectedProject)
let targetNodes = graph.targets.sorted(by: { $0.target.name < $1.target.name })
XCTAssertEqual(targetNodes.count, 1)
XCTAssertEqual(targetNodes.first?.target, manifestsTarget)
XCTAssertEqual(targetNodes.first?.dependencies, [])
// Generated Manifests target
let manifestsTarget = try XCTUnwrap(project.targets.first)
XCTAssertEqual(targetNodes.first?.target, manifestsTarget)
XCTAssertEqual(manifestsTarget.name, "Manifests")
XCTAssertEqual(manifestsTarget.platform, .macOS)
XCTAssertEqual(manifestsTarget.product, .staticFramework)
XCTAssertEqual(manifestsTarget.settings, expectedSettings(sourceRootPath: sourceRootPath))
XCTAssertEqual(manifestsTarget.sources.map { $0.path }, manifestPaths)
XCTAssertEqual(manifestsTarget.filesGroup, .group(name: "Manifests"))
XCTAssertEqual(manifestsTarget.dependencies, [])
// Generated Project
XCTAssertEqual(project.path, sourceRootPath)
XCTAssertEqual(project.name, "Manifests")
XCTAssertEqual(project.settings, Settings(base: [:],
configurations: Settings.default.configurations,
defaultSettings: .recommended))
XCTAssertEqual(project.filesGroup, .group(name: "Manifests"))
XCTAssertEqual(project.targets, targetNodes.map { $0.target })
// Generated Scheme
XCTAssertEqual(project.schemes.count, 1)
let scheme = try XCTUnwrap(project.schemes.first)
XCTAssertEqual(scheme.name, "Manifests")
let buildAction = try XCTUnwrap(scheme.buildAction)
XCTAssertEqual(buildAction.targets.map { $0.name }, targetNodes.map { $0.name })
let runAction = try XCTUnwrap(scheme.runAction)
XCTAssertEqual(runAction.filePath, tuistPath)
let generateArgument = "generate --path \(sourceRootPath)"
XCTAssertEqual(runAction.arguments, Arguments(launch: [generateArgument: true]))
}
fileprivate func expectedSettings(sourceRootPath: AbsolutePath) -> Settings {

View File

@ -62,6 +62,7 @@ final class ProjectEditorTests: TuistUnitTestCase {
let helpers = ["A.swift", "B.swift"].map { helpersDirectory.appending(component: $0) }
try helpers.forEach { try FileHandler.shared.touch($0) }
let manifests: [(Manifest, AbsolutePath)] = [(.project, directory.appending(component: "Project.swift"))]
let tuistPath = AbsolutePath(ProcessInfo.processInfo.arguments.first!)
resourceLocator.projectDescriptionStub = { projectDescriptionPath }
manifestFilesLocator.locateStub = manifests
@ -79,6 +80,7 @@ final class ProjectEditorTests: TuistUnitTestCase {
// Then
XCTAssertEqual(projectEditorMapper.mapArgs.count, 1)
let mapArgs = projectEditorMapper.mapArgs.first
XCTAssertEqual(mapArgs?.tuistPath, tuistPath)
XCTAssertEqual(mapArgs?.helpers, helpers)
XCTAssertEqual(mapArgs?.sourceRootPath, directory)
XCTAssertEqual(mapArgs?.projectDescriptionPath, projectDescriptionPath)

View File

@ -14,7 +14,7 @@ Editing your projects is easy; position yourself in a directory where there's a
tuist edit
```
It'll generate a temporary Xcode project with the manfiests and the project description helpers. Please note that the project doesn't get generated as the files are edited. Once you are done with editing, you can close the project and run `tuist generate`.
It will open a temporary Xcode project with the manifests and the project description helpers. After making changes you can run the target from Xcode and it will call `tuist generate` for you.
The project is deleted automatically once you are done with editing. If you wish to generate and keep the project in the current directory, you can run the command passing the `--permanent` argument: