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

[Xcodeproj] Add support for automatic project generation #1604

Merged
merged 1 commit into from
Jun 16, 2018
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
3 changes: 3 additions & 0 deletions Sources/Basic/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,16 @@ public enum FileMode {

case userUnWritable
case userWritable
case executable

public var cliArgument: String {
switch self {
case .userUnWritable:
return "u-w"
case .userWritable:
return "u+w"
case .executable:
return "+x"
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/Basic/Process.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ public final class Process: ObjectIdentifierProtocol {
/// Cache of validated executables.
///
/// Key: Executable name or path.
/// Value: If key was found in the search paths and is executable.
static private var validatedExecutablesMap = [String: Bool]()
/// Value: Path to the executable, if found.
static private var validatedExecutablesMap = [String: AbsolutePath?]()

/// Create a new process instance.
///
Expand All @@ -195,7 +195,7 @@ public final class Process: ObjectIdentifierProtocol {
/// Returns true if the given program is present and executable in search path.
Copy link
Contributor

Choose a reason for hiding this comment

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

We probably want to update to documentation to say a path is returned.

///
/// The program can be executable name, relative path or absolute path.
func findExecutable(_ program: String) -> Bool {
public static func findExecutable(_ program: String) -> AbsolutePath? {
return Process.executablesQueue.sync {
// Check if we already have a value for the program.
if let value = Process.validatedExecutablesMap[program] {
Expand All @@ -206,9 +206,9 @@ public final class Process: ObjectIdentifierProtocol {
pathString: getenv("PATH"),
currentWorkingDirectory: localFileSystem.currentWorkingDirectory
)
// Lookup the executable.
// Lookup and cache the executable path.
let value = lookupExecutablePath(
filename: program, searchPaths: envSearchPaths) != nil
filename: program, searchPaths: envSearchPaths)
Process.validatedExecutablesMap[program] = value
return value
}
Expand All @@ -229,7 +229,7 @@ public final class Process: ObjectIdentifierProtocol {
}

// Look for executable.
guard findExecutable(arguments[0]) else {
guard Process.findExecutable(arguments[0]) != nil else {
throw Process.Error.missingExecutableProgram(program: arguments[0])
}

Expand Down
17 changes: 15 additions & 2 deletions Sources/Commands/SwiftPackageTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ public class SwiftPackageTool: SwiftTool<PackageToolOptions> {

print("generated:", xcodeprojPath.prettyPath(cwd: originalWorkingDirectory))

// Run the file watcher if requested.
if options.xcodeprojOptions.enableAutogeneration {
try WatchmanHelper(
diagnostics: diagnostics,
watchmanScriptsDir: buildPath.appending(component: "watchman"),
packageRoot: packageRoot!
).runXcodeprojWatcher(options.xcodeprojOptions)
}

case .describe:
let graph = try loadPackageGraph()
describe(graph.rootPackages[0].underlyingPackage, in: options.describeMode, on: stdoutStream)
Expand Down Expand Up @@ -314,11 +323,15 @@ public class SwiftPackageTool: SwiftTool<PackageToolOptions> {
$0.outputPath = $3?.path
})
binder.bind(
option: generateXcodeParser.add(
generateXcodeParser.add(
option: "--legacy-scheme-generator", kind: Bool.self,
usage: "Use the legacy scheme generator"),
generateXcodeParser.add(
option: "--watch", kind: Bool.self,
usage: "Watch the filesystem and autogenerate the Xcode project if needed"),
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be worth being more specific?

Watch for changes to the Package manifest to regenerate the Xcode project

to: {
$0.xcodeprojOptions.useLegacySchemeGenerator = $1
$0.xcodeprojOptions.useLegacySchemeGenerator = $1 ?? false
$0.xcodeprojOptions.enableAutogeneration = $2 ?? false
})

let completionToolParser = parser.add(
Expand Down
103 changes: 103 additions & 0 deletions Sources/Commands/WatchmanHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2018 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Basic
import Utility
import POSIX
import Xcodeproj

struct WatchmanMissingDiagnostic: DiagnosticData {
static let id = DiagnosticID(
type: WatchmanMissingDiagnostic.self,
name: "org.swift.diags.watchman-missing",
description: {
$0 <<< "this feature requires 'watchman' to work"
$0 <<< "\n\n installation instructions for 'watchman' are available at https://facebook.github.io/watchman/docs/install.html#buildinstall"
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the double-newlines?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks a bit "pretty".

}
)
}

final class WatchmanHelper {

/// Name of the watchman-make tool.
static let watchmanMakeTool: String = "watchman-make"

/// Directory where watchman script should be created.
let watchmanScriptsDir: AbsolutePath

/// The package root.
let packageRoot: AbsolutePath

/// The filesystem to operator on.
let fs: FileSystem

let diagnostics: DiagnosticsEngine

init(
diagnostics: DiagnosticsEngine,
watchmanScriptsDir: AbsolutePath,
packageRoot: AbsolutePath,
fs: FileSystem = localFileSystem
) {
self.watchmanScriptsDir = watchmanScriptsDir
self.diagnostics = diagnostics
self.packageRoot = packageRoot
self.fs = fs
}

func runXcodeprojWatcher(_ options: XcodeprojOptions) throws {
let scriptPath = try createXcodegenScript(options)
try run(scriptPath)
}

func createXcodegenScript(_ options: XcodeprojOptions) throws -> AbsolutePath {
let scriptPath = watchmanScriptsDir.appending(component: "gen-xcodeproj.sh")

let stream = BufferedOutputByteStream()
stream <<< "#!/usr/bin/env bash" <<< "\n\n\n"
stream <<< "# Autogenerated by SwiftPM. Do not edit!" <<< "\n\n\n"
stream <<< "set -eu" <<< "\n\n"
stream <<< "swift package generate-xcodeproj"
if let xcconfigOverrides = options.xcconfigOverrides {
stream <<< " --xcconfig-overrides " <<< xcconfigOverrides.asString
}
stream <<< "\n"

try fs.createDirectory(scriptPath.parentDirectory, recursive: true)
try fs.writeFileContents(scriptPath, bytes: stream.bytes)
try fs.chmod(.executable, path: scriptPath)

return scriptPath
}

private func run(_ scriptPath: AbsolutePath) throws {
// Construct the arugments.
var args = [String]()
args += ["--settle", "2"]
args += ["-p", "Package.swift", "Package.resolved"]
args += ["--run", scriptPath.asString.shellEscaped()]

// Find and execute watchman.
let watchmanMakeToolPath = try self.watchmanMakeToolPath()

print("Starting:", watchmanMakeToolPath.asString, args.joined(separator: " "))

let pathRelativeToWorkingDirectory = watchmanMakeToolPath.relative(to: packageRoot)
try exec(path: watchmanMakeToolPath.asString, args: [pathRelativeToWorkingDirectory.asString] + args)
}

private func watchmanMakeToolPath() throws -> AbsolutePath {
if let toolPath = Process.findExecutable(WatchmanHelper.watchmanMakeTool) {
return toolPath
}
diagnostics.emit(data: WatchmanMissingDiagnostic())
throw Error.hasFatalDiagnostics
}
}
7 changes: 6 additions & 1 deletion Sources/Xcodeproj/generate().swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,21 @@ public struct XcodeprojOptions {
/// Whether to use legacy scheme generation logic.
public var useLegacySchemeGenerator: Bool

/// Run watchman to auto-generate the project file on changes.
public var enableAutogeneration: Bool

public init(
flags: BuildFlags = BuildFlags(),
xcconfigOverrides: AbsolutePath? = nil,
isCodeCoverageEnabled: Bool? = nil,
useLegacySchemeGenerator: Bool? = nil
useLegacySchemeGenerator: Bool? = nil,
enableAutogeneration: Bool? = nil
) {
self.flags = flags
self.xcconfigOverrides = xcconfigOverrides
self.isCodeCoverageEnabled = isCodeCoverageEnabled ?? false
self.useLegacySchemeGenerator = useLegacySchemeGenerator ?? false
self.enableAutogeneration = enableAutogeneration ?? false
}
}

Expand Down
8 changes: 4 additions & 4 deletions Tests/BasicTests/ProcessTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ class ProcessTests: XCTestCase {
func testFindExecutable() throws {
mktmpdir { path in
// This process should always work.
XCTAssertTrue(Process().findExecutable("ls"))
XCTAssertTrue(Process.findExecutable("ls") != nil)

XCTAssertFalse(Process().findExecutable("nonExistantProgram"))
XCTAssertFalse(Process().findExecutable(""))
XCTAssertEqual(Process.findExecutable("nonExistantProgram"), nil)
XCTAssertEqual(Process.findExecutable(""), nil)

// Create a local nonexecutable file to test.
let tempExecutable = path.appending(component: "nonExecutableProgram")
Expand All @@ -85,7 +85,7 @@ class ProcessTests: XCTestCase {
""")

try withCustomEnv(["PATH": path.asString]) {
XCTAssertFalse(Process().findExecutable("nonExecutableProgram"))
XCTAssertEqual(Process.findExecutable("nonExecutableProgram"), nil)
}
}
}
Expand Down
35 changes: 34 additions & 1 deletion Tests/CommandsTests/PackageToolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import XCTest
import Foundation

import Basic
import Commands
@testable import Commands
import Xcodeproj
import PackageModel
import SourceControl
import TestSupport
Expand Down Expand Up @@ -489,6 +490,37 @@ final class PackageToolTests: XCTestCase {
}
}

func testWatchmanXcodeprojgen() {
mktmpdir { path in
let fs = localFileSystem
let diagnostics = DiagnosticsEngine()

let scriptsDir = path.appending(component: "scripts")
let packageRoot = path.appending(component: "root")

let helper = WatchmanHelper(
diagnostics: diagnostics,
watchmanScriptsDir: scriptsDir,
packageRoot: packageRoot)

let script = try helper.createXcodegenScript(
XcodeprojOptions())

XCTAssertEqual(try fs.readFileContents(script), """
#!/usr/bin/env bash


# Autogenerated by SwiftPM. Do not edit!


set -eu

swift package generate-xcodeproj
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we also want a test that makes such that we pass --xcconfig-overrides when necessary?


""")
}
}

static var allTests = [
("testDescribe", testDescribe),
("testUsage", testUsage),
Expand All @@ -507,5 +539,6 @@ final class PackageToolTests: XCTestCase {
("testPinning", testPinning),
("testPinningBranchAndRevision", testPinningBranchAndRevision),
("testSymlinkedDependency", testSymlinkedDependency),
("testWatchmanXcodeprojgen", testWatchmanXcodeprojgen),
]
}