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

Edit command #703

Merged
merged 10 commits into from Nov 26, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -10,6 +10,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
- `ProjectEditor` utility https://github.com/tuist/tuist/pull/702 by @pepibumur.
- Fix warnings in the project, refactor SHA256 diegest code https://github.com/tuist/tuist/pull/704 by @rowwingman.
- Define `ArchiveAction` on `Scheme` https://github.com/tuist/tuist/pull/697 by @grsouza.
- `tuist edit` command https://github.com/tuist/tuist/pull/703 by @pepibumur.

## 0.19.0

Expand Down
9 changes: 9 additions & 0 deletions Package.resolved
Expand Up @@ -10,6 +10,15 @@
"version": "4.4.0"
}
},
{
"package": "Signals",
"repositoryURL": "https://github.com/IBM-Swift/BlueSignals",
"state": {
"branch": null,
"revision": "b7331c8bef913f5f8b3cffa6ecfcce679f7c2531",
"version": "1.0.21"
}
},
{
"package": "PathKit",
"repositoryURL": "https://github.com/kylef/PathKit",
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Expand Up @@ -28,6 +28,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/tuist/XcodeProj", .upToNextMajor(from: "7.5.0")),
.package(url: "https://github.com/apple/swift-package-manager", .upToNextMajor(from: "0.5.0")),
.package(url: "https://github.com/IBM-Swift/BlueSignals", .upToNextMajor(from: "1.0.21")),
],
targets: [
.target(
Expand All @@ -48,7 +49,7 @@ let package = Package(
),
.target(
name: "TuistKit",
dependencies: ["XcodeProj", "SPMUtility", "TuistSupport", "TuistGenerator", "ProjectDescription"]
dependencies: ["XcodeProj", "SPMUtility", "TuistSupport", "TuistGenerator", "ProjectDescription", "Signals"]
),
.testTarget(
name: "TuistKitTests",
Expand Down
1 change: 1 addition & 0 deletions Sources/TuistKit/Commands/CommandRegistry.swift
Expand Up @@ -26,6 +26,7 @@ public final class CommandRegistry {
register(command: FocusCommand.self)
register(command: UpCommand.self)
register(command: GraphCommand.self)
register(command: EditCommand.self)
register(rawCommand: BuildCommand.self)
}

Expand Down
87 changes: 87 additions & 0 deletions Sources/TuistKit/Commands/EditCommand.swift
@@ -0,0 +1,87 @@
import Basic
import Foundation
import Signals
import SPMUtility
import TuistGenerator
import TuistSupport

class EditCommand: NSObject, Command {
// MARK: - Static

static let command = "edit"
static let overview = "Generates a temporary project to edit the project in the current directory"

// MARK: - Attributes

private let projectEditor: ProjectEditing
private let opener: Opening
private let pathArgument: OptionArgument<String>
private let permanentArgument: OptionArgument<Bool>

// MARK: - Init

required convenience init(parser: ArgumentParser) {
self.init(parser: parser, projectEditor: ProjectEditor(), opener: Opener())
}

init(parser: ArgumentParser, projectEditor: ProjectEditing, opener: Opening) {
let subparser = parser.add(subparser: EditCommand.command, overview: EditCommand.overview)
pathArgument = subparser.add(option: "--path",
shortName: "-p",
kind: String.self,
usage: "The path to the directory whose project will be edited.",
completion: .filename)
permanentArgument = subparser.add(option: "--permanent",
shortName: "-P",
kind: Bool.self,
usage: "It creates the project in the current directory or the one indicated by -p and doesn't block the process.")
self.projectEditor = projectEditor
self.opener = opener
}

func run(with arguments: ArgumentParser.Result) throws {
let path = self.path(arguments: arguments)
let permanent = self.permanent(arguments: arguments)
let generationDirectory = permanent ? path : EditCommand.temporaryDirectory.path
let xcodeprojPath = try projectEditor.edit(at: path, in: generationDirectory)

if !permanent {
Signals.trap(signals: [.int, .abrt]) { _ in
// swiftlint:disable:next force_try
try! FileHandler.shared.delete(EditCommand.temporaryDirectory.path)
exit(0)
}
Printer.shared.print(success: "Opening Xcode to edit the project. Press CTRL + C once you are done editing")
try opener.open(path: xcodeprojPath, wait: true)
} else {
Printer.shared.print(success: "Xcode project generated at \(xcodeprojPath.pathString)")
}
}

// MARK: - Fileprivate

fileprivate static var _temporaryDirectory: TemporaryDirectory?
fileprivate static var temporaryDirectory: TemporaryDirectory {
// swiftlint:disable:next identifier_name
if let _temporaryDirectory = _temporaryDirectory { return _temporaryDirectory }
// swiftlint:disable:next force_try
_temporaryDirectory = try! TemporaryDirectory(removeTreeOnDeinit: true)
return _temporaryDirectory!
}

private func path(arguments: ArgumentParser.Result) -> AbsolutePath {
if let path = arguments.get(pathArgument) {
return AbsolutePath(path, relativeTo: FileHandler.shared.currentPath)
} else {
return FileHandler.shared.currentPath
}
}

private func permanent(arguments: ArgumentParser.Result) -> Bool {
if let permanent = arguments.get(permanentArgument) {
return permanent
} else {
return false
}
}
}
30 changes: 27 additions & 3 deletions Sources/TuistKit/ProjectEditor/ProjectEditor.swift
Expand Up @@ -3,12 +3,31 @@ import Foundation
import TuistGenerator
import TuistSupport

enum ProjectEditorError: FatalError, Equatable {
/// This error is thrown when we try to edit in a project in a directory that has no editable files.
case noEditableFiles(AbsolutePath)

var type: ErrorType {
switch self {
case .noEditableFiles: return .abort
}
}

var description: String {
switch self {
case let .noEditableFiles(path):
return "There are no editable files at \(path.pathString)"
}
}
}

protocol ProjectEditing: AnyObject {
/// Generates an Xcode project to edit the Project defined in the given directory.
/// - Parameters:
/// - at: Directory whose project will be edited.
/// - destinationDirectory: Directory in which the Xcode project will be generated.
func edit(at: AbsolutePath, in destinationDirectory: AbsolutePath) throws
/// - Returns: The path to the generated Xcode project.
func edit(at: AbsolutePath, in destinationDirectory: AbsolutePath) throws -> AbsolutePath
}

final class ProjectEditor: ProjectEditing {
Expand Down Expand Up @@ -39,18 +58,23 @@ final class ProjectEditor: ProjectEditing {
self.helpersDirectoryLocator = helpersDirectoryLocator
}

func edit(at: AbsolutePath, in destinationDirectory: AbsolutePath) throws {
func edit(at: AbsolutePath, in destinationDirectory: AbsolutePath) throws -> AbsolutePath {
let projectDesciptionPath = try resourceLocator.projectDescription()
let manifests = manifestFilesLocator.locate(at: at)
var helpers: [AbsolutePath] = []
if let helpersDirectory = self.helpersDirectoryLocator.locate(at: at) {
helpers = FileHandler.shared.glob(helpersDirectory, glob: "**/*.swift")
}

/// We error if the user tries to edit a project in a directory where there are no editable files.
if manifests.isEmpty, helpers.isEmpty {
throw ProjectEditorError.noEditableFiles(at)
}

let (project, graph) = projectEditorMapper.map(sourceRootPath: destinationDirectory,
manifests: manifests.map { $0.1 },
helpers: helpers,
projectDescriptionPath: projectDesciptionPath)
_ = try generator.generateProject(project, graph: graph)
return try generator.generateProject(project, graph: graph)
}
}
12 changes: 11 additions & 1 deletion Sources/TuistSupport/Utils/Opener.swift
Expand Up @@ -28,6 +28,7 @@ enum OpeningError: FatalError, Equatable {

public protocol Opening: AnyObject {
func open(path: AbsolutePath) throws
func open(path: AbsolutePath, wait: Bool) throws
}

public class Opener: Opening {
Expand All @@ -36,9 +37,18 @@ public class Opener: Opening {
// MARK: - Opening

public func open(path: AbsolutePath) throws {
try open(path: path, wait: true)
}

public func open(path: AbsolutePath, wait: Bool) throws {
if !FileHandler.shared.exists(path) {
throw OpeningError.notFound(path)
}
try System.shared.runAndPrint("/usr/bin/open", path.pathString)
var arguments: [String] = []
arguments.append(contentsOf: ["/usr/bin/open"])
if wait { arguments.append("-W") }
arguments.append(path.pathString)

try System.shared.run(arguments)
}
}
10 changes: 7 additions & 3 deletions Sources/TuistSupportTesting/Utils/MockOpener.swift
Expand Up @@ -4,12 +4,16 @@ import TuistSupport

public final class MockOpener: Opening {
var openStub: Error?
var openArgs: [AbsolutePath] = []
var openArgs: [(AbsolutePath, Bool)] = []
var openCallCount: UInt = 0

public func open(path: AbsolutePath) throws {
public func open(path: AbsolutePath, wait: Bool) throws {
openCallCount += 1
openArgs.append(path)
openArgs.append((path, wait))
if let openStub = openStub { throw openStub }
}

public func open(path: AbsolutePath) throws {
try open(path: path, wait: true)
}
}
17 changes: 17 additions & 0 deletions Tests/TuistKitTests/Commands/EditCommandTests.swift
@@ -0,0 +1,17 @@
import Basic
import Foundation
import SPMUtility
import XcodeProj
import XCTest
@testable import TuistKit
@testable import TuistSupportTesting

final class EditCommandTests: TuistUnitTestCase {
func test_command() {
XCTAssertEqual(EditCommand.command, "edit")
}

func test_overview() {
XCTAssertEqual(EditCommand.overview, "Generates a temporary project to edit the project in the current directory")
}
}
2 changes: 1 addition & 1 deletion Tests/TuistKitTests/Commands/FocusCommandTests.swift
Expand Up @@ -68,6 +68,6 @@ final class FocusCommandTests: TuistUnitTestCase {
}
try subject.run(with: result)

XCTAssertEqual(opener.openArgs.last, workspacePath)
XCTAssertEqual(opener.openArgs.last?.0, workspacePath)
}
}
30 changes: 29 additions & 1 deletion Tests/TuistKitTests/ProjectEditor/ProjectEditorTests.swift
Expand Up @@ -7,6 +7,16 @@ import XCTest
@testable import TuistKit
@testable import TuistSupportTesting

final class ProjectEditorErrorTests: TuistUnitTestCase {
func test_type() {
XCTAssertEqual(ProjectEditorError.noEditableFiles(AbsolutePath.root).type, .abort)
}

func test_description() {
XCTAssertEqual(ProjectEditorError.noEditableFiles(AbsolutePath.root).description, "There are no editable files at \(AbsolutePath.root.pathString)")
}
}

final class ProjectEditorTests: TuistUnitTestCase {
var generator: MockGenerator!
var projectEditorMapper: MockProjectEditorMapper!
Expand Down Expand Up @@ -45,7 +55,7 @@ final class ProjectEditorTests: TuistUnitTestCase {
let projectDescriptionPath = directory.appending(component: "ProjectDescription.framework")
let project = Project.test(path: directory, name: "Edit")
let graph = Graph.test(name: "Edit")
let helpersDirectory = directory.appending(component: "ProjectDDescriptionHelpers")
let helpersDirectory = directory.appending(component: "ProjectDescriptionHelpers")
try FileHandler.shared.createFolder(helpersDirectory)
let helpers = ["A.swift", "B.swift"].map { helpersDirectory.appending(component: $0) }
try helpers.forEach { try FileHandler.shared.touch($0) }
Expand Down Expand Up @@ -73,4 +83,22 @@ final class ProjectEditorTests: TuistUnitTestCase {

XCTAssertEqual(generatedProject, project)
}

func test_edit_when_there_are_no_editable_files() throws {
// Given
let directory = try temporaryPath()
let projectDescriptionPath = directory.appending(component: "ProjectDescription.framework")
let project = Project.test(path: directory, name: "Edit")
let graph = Graph.test(name: "Edit")
let helpersDirectory = directory.appending(component: "ProjectDescriptionHelpers")
try FileHandler.shared.createFolder(helpersDirectory)

resourceLocator.projectDescriptionStub = { projectDescriptionPath }
manifestFilesLocator.locateStub = []
helpersDirectoryLocator.locateStub = helpersDirectory
projectEditorMapper.mapStub = (project, graph)

// When
XCTAssertThrowsSpecific(try subject.edit(at: directory, in: directory), ProjectEditorError.noEditableFiles(directory))
}
}
10 changes: 9 additions & 1 deletion Tests/TuistSupportTests/Utils/OpenerTests.swift
Expand Up @@ -41,7 +41,15 @@ final class OpenerTests: TuistUnitTestCase {
let temporaryPath = try self.temporaryPath()
let path = temporaryPath.appending(component: "tool")
try FileHandler.shared.touch(path)
system.succeedCommand("/usr/bin/open", path.pathString)
system.succeedCommand("/usr/bin/open", "-W", path.pathString)
try subject.open(path: path)
}

func test_open_when_wait_is_false() throws {
let temporaryPath = try self.temporaryPath()
let path = temporaryPath.appending(component: "tool")
try FileHandler.shared.touch(path)
system.succeedCommand("/usr/bin/open", path.pathString)
try subject.open(path: path, wait: false)
}
}
9 changes: 9 additions & 0 deletions features/edit.feature
@@ -0,0 +1,9 @@
Feature: Edit an existing project using Tuist

Scenario: The project is an application with helpers (ios_app_with_helpers)
Given that tuist is available
And I have a working directory
Then I copy the fixture ios_app_with_helpers into the working directory
Then tuist edits the project
Then I should be able to build for macOS the scheme ProjectDescriptionHelpers
Then I should be able to build for macOS the scheme Manifests
5 changes: 5 additions & 0 deletions features/step_definitions/shared/tuist.rb
Expand Up @@ -10,6 +10,11 @@
@xcodeproj_path = Dir.glob(File.join(@dir, "*.xcodeproj")).first
end

Then(/tuist edits the project/) do
system("swift", "run", "tuist", "edit", "--path", @dir, "--permanent")
@xcodeproj_path = Dir.glob(File.join(@dir, "*.xcodeproj")).first
end

Then(/tuist sets up the project/) do
system("swift", "run", "tuist", "up", "--path", @dir)
@workspace_path = Dir.glob(File.join(@dir, "*.xcworkspace")).first
Expand Down
10 changes: 8 additions & 2 deletions features/step_definitions/shared/xcode.rb
Expand Up @@ -8,9 +8,13 @@

args = [
"-scheme", scheme,
"-workspace", @workspace_path,
"-derivedDataPath", @derived_data_path
]
unless @workspace_path.nil?
args.concat(["-workspace", @workspace_path]) unless @workspace_path.nil?
else
args.concat(["-project", @xcodeproj_path]) unless @xcodeproj_path.nil?
end

if action == "test" && platform == "iOS"
args << "-destination\ \'name=iPhone 11\'"
Expand All @@ -23,7 +27,9 @@
args << "clean"
args << action
args << "CODE_SIGNING_ALLOWED=NO"
args << "CODE_SIGNING_IDENTITY=\"iPhone Developer\""
args << "CODE_SIGNING_IDENTITY=\"\""
args << "CODE_SIGNING_REQUIRED=NO"
args << "CODE_SIGN_ENTITLEMENTS=\"\""

xcodebuild(*args)
end
Expand Down
4 changes: 4 additions & 0 deletions fixtures/README.md
Expand Up @@ -268,3 +268,7 @@ An example of a workspace that has a dependency cycle between targets in differe
## ios_app_with_carthage_frameworks

An example of an iOS app that contains Carthage frameworks \*(fat frameworks with device & simulators architectures)\*. This fixture is useful to test the script that embeds them stripping the architectures that are not necessary.

## ios_app_with_helpers

A basic iOS app that has some manifest bits extracted into helpers.