Skip to content
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
8 changes: 8 additions & 0 deletions Documentation/Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* [Swift Tools Version](#swift-tools-version)
* [Prefetching Dependencies](#prefetching-dependencies)
* [Testing](#testing)
* [Running](#running)
* [Build Configurations](#build-configurations)
* [Debug](#debug)
* [Release](#release)
Expand Down Expand Up @@ -720,6 +721,13 @@ $ swift build --enable-prefetching
Use `swift test` tool to run tests of a Swift package. For more information on
the test tool, run `swift test --help`.

## Running

Use `swift run [executable [arguments...]]` tool to run an executable product of a Swift
package. The executable's name is optional when running without arguments and when there
is only one executable product. For more information on the run tool, run
`swift run --help`.

## Build Configurations

SwiftPM allows two build configurations: Debug (default) and Release.
Expand Down
11 changes: 11 additions & 0 deletions Fixtures/Miscellaneous/EchoExecutable/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// swift-tools-version:4.0
import PackageDescription

let package = Package(
name: "EchoExecutable",
products: [
.executable(name: "secho", targets: ["secho"])
],
targets: [
.target(name: "secho", dependencies: [])
])
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let arguments = Array(CommandLine.arguments.dropFirst())
print(arguments.map({ "\"\($0)\"" }).joined(separator: " "))
13 changes: 13 additions & 0 deletions Fixtures/Miscellaneous/MultipleExecutables/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// swift-tools-version:4.0
import PackageDescription

let package = Package(
name: "EchoExecutable",
products: [
.executable(name: "exec1", targets: ["exec1"]),
.executable(name: "exec2", targets: ["exec2"])
],
targets: [
.target(name: "exec1", dependencies: []),
.target(name: "exec2", dependencies: [])
])
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("1")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("2")
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
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
151 changes: 151 additions & 0 deletions Sources/Commands/SwiftRunTool.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
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 Basic
import Build
import Utility
import PackageGraph
import PackageModel

import func POSIX.chdir
import func POSIX.getcwd

/// An enumeration of the errors that can be generated by the run tool.
private enum RunError: Swift.Error {
/// The package manifest has no executable product.
case noExecutableFound

/// Could not find a specific executable in the package manifest.
case executableNotFound(String)

/// There are multiple executables and one must be chosen.
case multipleExecutables([String])
}

extension RunError: CustomStringConvertible {
var description: String {
switch self {
case .noExecutableFound:
return "no executable product in the package"
case .executableNotFound(let executable):
return "could not find executable product '\(executable)' in the package"
case .multipleExecutables(let executables):
let joinedExecutables = executables.joined(separator: ", ")
return "multiple executable products in the package. Use `swift run ` followed by one of: \(joinedExecutables)"
}
}
}

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 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)
}

try run(findExecutable(in: plan))
}
}

/// Returns the path to the correct executable based on options.
private func findExecutable(in plan: BuildPlan) throws -> AbsolutePath {
let executableProducts = plan.graph.products.filter({ $0.type == .executable })

// Error out if the product contains no executable.
guard executableProducts.count > 0 else {
throw RunError.noExecutableFound
}

let product: ResolvedProduct
if let executable = options.executable {
// If the exectuable is explicitly specified, verify that it exists.
guard let executableProduct = executableProducts.first(where: { $0.name == executable }) else {
throw RunError.executableNotFound(executable)
}

product = executableProduct
} else {
// Only implicitly deduce the executable if it is the only one.
guard executableProducts.count == 1 else {
throw RunError.multipleExecutables(executableProducts.map({ $0.name }))
}

product = executableProducts[0]
}

// Build the path to the executable from the build directory.
return plan.buildParameters.buildPath.appending(component: product.name)
}

/// Executes the executable at the specified path.
private func run(_ excutablePath: AbsolutePath) throws {
let pathRelativeToWorkingDirectory = excutablePath.relative(to: originalWorkingDirectory)
try exec(path: excutablePath.asString, args: [pathRelativeToWorkingDirectory.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
Original file line number Diff line number Diff line change
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
14 changes: 14 additions & 0 deletions Sources/swift-run/main.swift
Original file line number Diff line number Diff line change
@@ -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()
66 changes: 66 additions & 0 deletions Tests/CommandsTests/RunToolTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
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
import Basic

final class RunToolTests: XCTestCase {
private func execute(_ args: [String], packagePath: AbsolutePath? = nil) throws -> String {
return try SwiftPMProduct.SwiftRun.execute(args, packagePath: packagePath, 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"))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We should add a basic functional test. We can extend one of the executable related functional test.


func testFunctional() throws {
fixture(name: "Miscellaneous/EchoExecutable") { path in
do {
_ = try execute(["unknown"], packagePath: path)
XCTFail("Unexpected success")
} catch SwiftPMProductError.executionFailure(_, _, let stderr) {
XCTAssertEqual(stderr, "error: could not find executable product 'unknown' in the package\n")
}

let runOutput = try execute(["secho", "1", "--hello", "world"], packagePath: path)
let outputLines = runOutput.split(separator: "\n")
XCTAssertEqual(outputLines.last!, "\"1\" \"--hello\" \"world\"")
}

fixture(name: "Miscellaneous/MultipleExecutables") { path in
do {
_ = try execute([], packagePath: path)
XCTFail("Unexpected success")
} catch SwiftPMProductError.executionFailure(_, _, let stderr) {
XCTAssertEqual(stderr, "error: multiple executable products in the package. Use `swift run ` followed by one of: exec1, exec2\n")
}

var runOutput = try execute(["exec1"], packagePath: path)
var outputLines = runOutput.split(separator: "\n")
XCTAssertEqual(outputLines.last!, "1")
runOutput = try execute(["exec2"], packagePath: path)
outputLines = runOutput.split(separator: "\n")
XCTAssertEqual(outputLines.last!, "2")
}
}

static var allTests = [
("testUsage", testUsage),
("testVersion", testVersion),
("testFunctional", testFunctional)
]
}
1 change: 1 addition & 0 deletions Tests/CommandsTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public func allTests() -> [XCTestCaseEntry] {
testCase(BuildToolTests.allTests),
testCase(PackageToolTests.allTests),
testCase(TestToolTests.allTests),
testCase(RunToolTests.allTests),
]
}
#endif