Skip to content

Commit

Permalink
Add a swift-run tool to run a package executable
Browse files Browse the repository at this point in the history
  • Loading branch information
hartbit committed May 31, 2017
1 parent 5fbef73 commit 66f7067
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Package.swift
Expand Up @@ -126,6 +126,10 @@ let package = Package(
/** Runs package tests */
name: "swift-test",
dependencies: ["Commands"]),
.target(
/** Runs an executable product */
name: "swift-run",
dependencies: ["Commands"]),
.target(
/** Shim tool to find test names on OS X */
name: "swiftpm-xctest-helper",
Expand Down
148 changes: 148 additions & 0 deletions Sources/Commands/SwiftRunTool.swift
@@ -0,0 +1,148 @@
/*
This source file is part of the Swift.org open source project
Copyright 2015 - 2016 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 class Foundation.ProcessInfo

import Basic
import Build
import Utility
import PackageGraph
import PackageModel

import func POSIX.chdir
import func POSIX.getcwd

private enum RunError: Swift.Error {
case noExecutableFound
case executableNotFound(String)
case multipleExecutables
}

extension RunError: CustomStringConvertible {
var description: String {
switch self {
case .noExecutableFound:
return "no executable product found"
case .executableNotFound(let executable):
return "could not find executable '\(executable)'"
case .multipleExecutables:
return "multiple products defined"
}
}
}

public class RunToolOptions: ToolOptions {
/// Returns the mode in with the tool command should run.
var mode: RunMode {
// If we got version option, just print the version and exit.
if shouldPrintVersion {
return .version
}

return .run
}

/// If the executable product should be built before running.
var shouldBuild = true

/// The custom working directory the executable should be run in.
var workingDirectory: AbsolutePath?

/// The executable product to run.
var executable: String?

/// The arguments to pass to the executable.
var arguments: [String] = []
}

public enum RunMode {
case version
case run
}

/// swift-run tool namespace
public class SwiftRunTool: SwiftTool<RunToolOptions> {

public convenience init(args: [String]) {
self.init(
toolName: "run",
usage: "[options] [executable <arguments>]",
overview: "Build and run an executable product",
args: args
)
}

override func runImpl() throws {
switch options.mode {
case .version:
print(Versioning.currentVersion.completeDisplayString)

case .run:
let plan = try buildPlan()

if options.shouldBuild {
try build(plan: plan, includingTests: false)
}

let product = try findExecutable(in: plan)
let path = plan.buildParameters.buildPath.appending(component: product.name)
try run(at: path)
}
}

/// Find executable product based on options.
private func findExecutable(in plan: BuildPlan) throws -> ResolvedProduct {
let executableProducts = plan.graph.products.filter({ $0.type == .executable })

guard executableProducts.count > 0 else {
throw RunError.noExecutableFound
}

if let executable = options.executable {
guard let executableProduct = executableProducts.first(where: { $0.name == executable }) else {
throw RunError.executableNotFound(executable)
}

return executableProduct
} else {
guard executableProducts.count == 1 else {
throw RunError.multipleExecutables
}

return executableProducts.first!
}
}

/// Executes and returns execution status. Prints output on standard streams.
private func run(at path: AbsolutePath) throws {
if let workingDirectory = options.workingDirectory {
try POSIX.chdir(workingDirectory.asString)
}

let relativePath = path.relative(to: currentWorkingDirectory)
try exec(path: path.asString, args: [relativePath.asString] + options.arguments)
}

override class func defineArguments(parser: ArgumentParser, binder: ArgumentBinder<RunToolOptions>) {
binder.bind(
option: parser.add(option: "--skip-build", kind: Bool.self,
usage: "Skip building the executable product"),
to: { $0.shouldBuild = !$1 })

binder.bindArray(
positional: parser.add(positional: "executable", kind: [String].self, optional: true, strategy: .remaining,
usage: "The executable to run"),
to: {
$0.executable = $1.first!
$0.arguments = Array($1.dropFirst())
})
}
}

3 changes: 3 additions & 0 deletions Sources/TestSupport/SwiftPMProduct.swift
Expand Up @@ -31,6 +31,7 @@ public enum SwiftPMProduct {
case SwiftBuild
case SwiftPackage
case SwiftTest
case SwiftRun
case XCTestHelper
case TestSupportExecutable

Expand All @@ -56,6 +57,8 @@ public enum SwiftPMProduct {
return RelativePath("swift-package")
case .SwiftTest:
return RelativePath("swift-test")
case .SwiftRun:
return RelativePath("swift-run")
case .XCTestHelper:
return RelativePath("swiftpm-xctest-helper")
case .TestSupportExecutable:
Expand Down
13 changes: 13 additions & 0 deletions Sources/Utility/ArgumentParser.swift
Expand Up @@ -538,6 +538,7 @@ public final class ArgumentParser {

/// Get a positional argument's value.
public func get<T>(_ argument: PositionalArgument<T>) -> T? {
print(argument)
return results[AnyArgument(argument)] as? T
}

Expand Down Expand Up @@ -929,6 +930,18 @@ public final class ArgumentBinder<Options> {
}
}

/// Bind an array positional argument.
public func bindArray<T>(
positional: PositionalArgument<[T]>,
to body: @escaping (inout Options, [T]) -> Void
) {
addBody {
// All the positional argument will always be present.
guard let result = $1.get(positional) else { return }
body(&$0, result)
}
}

/// Bind two positional arguments.
public func bindPositional<T, U>(
_ first: PositionalArgument<T>,
Expand Down
14 changes: 14 additions & 0 deletions Sources/swift-run/main.swift
@@ -0,0 +1,14 @@
/*
This source file is part of the Swift.org open source project
Copyright (c) 2014 - 2017 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 Commands

let tool = SwiftRunTool(args: Array(CommandLine.arguments.dropFirst()))
tool.run()
33 changes: 33 additions & 0 deletions Tests/CommandsTests/RunToolTests.swift
@@ -0,0 +1,33 @@
/*
This source file is part of the Swift.org open source project
Copyright (c) 2014 - 2017 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 XCTest

import TestSupport
import Commands

final class RunToolTests: XCTestCase {
private func execute(_ args: [String]) throws -> String {
return try SwiftPMProduct.SwiftRun.execute(args, printIfError: true)
}

func testUsage() throws {
XCTAssert(try execute(["--help"]).contains("USAGE: swift run [options] [executable <arguments>]"))
}

func testVersion() throws {
XCTAssert(try execute(["--version"]).contains("Swift Package Manager"))
}

static var allTests = [
("testUsage", testUsage),
("testVersion", testVersion),
]
}

0 comments on commit 66f7067

Please sign in to comment.