Skip to content

Commit ee50121

Browse files
Add a new show-executables package subcommand for all executables (#8005)
The `swift run` subcommand runs executables that come from a variety of places. Typically, users are expecting to be able to execute products and targets for their package, iterating quickly with their code changes. You can also run executable products from packages that are part of the dependency list. It turns out that all of the executable products of the root package's transitive closure are available. Discovery of the available executables is limited to either direct inspection of the Package.swift, the `swift package describe`, and `swift package completion-tool list-executables`. However, these only work to find the executables of the current package, not in the dependent packages. This has real implications for a package maintainer since duplicate executable names are forbidden among the closure of the package's dependencies. Provide the ability to show the complete list of available executables from the package with a new `swift package show-executables` subcommand. Qualify executables that aren't in the current package with their package of origin for traceability purposes. The list can be formatted as JSON so that this information can be used by third-party tools. --------- Co-authored-by: Max Desiatov <[email protected]>
1 parent 58f9aa4 commit ee50121

File tree

8 files changed

+166
-0
lines changed

8 files changed

+166
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// swift-tools-version:5.10
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "Dealer",
6+
products: [
7+
.executable(
8+
name: "dealer",
9+
targets: ["Dealer"]
10+
),
11+
],
12+
dependencies: [
13+
.package(path: "../deck-of-playing-cards"),
14+
],
15+
targets: [
16+
.executableTarget(
17+
name: "Dealer",
18+
path: "./"
19+
),
20+
]
21+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("I'm the dealer")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// swift-tools-version:5.10
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "deck-of-playing-cards",
6+
products: [
7+
.executable(
8+
name: "deck",
9+
targets: ["Deck"]
10+
),
11+
],
12+
targets: [
13+
.executableTarget(
14+
name: "Deck",
15+
path: "./"
16+
),
17+
]
18+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("I'm a deck of cards")

Sources/Commands/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ add_library(Commands
2727
PackageCommands/ResetCommands.swift
2828
PackageCommands/Resolve.swift
2929
PackageCommands/ShowDependencies.swift
30+
PackageCommands/ShowExecutables.swift
3031
PackageCommands/SwiftPackageCommand.swift
3132
PackageCommands/ToolsVersionCommand.swift
3233
PackageCommands/Update.swift
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import ArgumentParser
14+
import Basics
15+
import CoreCommands
16+
import Foundation
17+
import Workspace
18+
19+
struct ShowExecutables: AsyncSwiftCommand {
20+
static let configuration = CommandConfiguration(
21+
abstract: "List the available executables from this package.")
22+
23+
@OptionGroup(visibility: .hidden)
24+
var globalOptions: GlobalOptions
25+
26+
@Option(help: "Set the output format.")
27+
var format: ShowExecutablesMode = .flatlist
28+
29+
func run(_ swiftCommandState: SwiftCommandState) async throws {
30+
let packageGraph = try await swiftCommandState.loadPackageGraph()
31+
let rootPackages = packageGraph.rootPackages.map { $0.identity }
32+
33+
let executables = packageGraph.allProducts.filter({
34+
$0.type == .executable || $0.type == .snippet
35+
}).map { product -> Executable in
36+
if !rootPackages.contains(product.packageIdentity) {
37+
return Executable(package: product.packageIdentity.description, name: product.name)
38+
} else {
39+
return Executable(package: Optional<String>.none, name: product.name)
40+
}
41+
}.sorted(by: {$0.name < $1.name})
42+
43+
switch self.format {
44+
case .flatlist:
45+
for executable in executables {
46+
if let package = executable.package {
47+
print("\(executable.name) (\(package))")
48+
} else {
49+
print(executable.name)
50+
}
51+
}
52+
53+
case .json:
54+
let encoder = JSONEncoder()
55+
let data = try encoder.encode(executables)
56+
if let output = String(data: data, encoding: .utf8) {
57+
print(output)
58+
}
59+
}
60+
}
61+
62+
struct Executable: Codable {
63+
var package: String?
64+
var name: String
65+
}
66+
67+
enum ShowExecutablesMode: String, RawRepresentable, CustomStringConvertible, ExpressibleByArgument, CaseIterable {
68+
case flatlist, json
69+
70+
public init?(rawValue: String) {
71+
switch rawValue.lowercased() {
72+
case "flatlist":
73+
self = .flatlist
74+
case "json":
75+
self = .json
76+
default:
77+
return nil
78+
}
79+
}
80+
81+
public var description: String {
82+
switch self {
83+
case .flatlist: return "flatlist"
84+
case .json: return "json"
85+
}
86+
}
87+
}
88+
}

Sources/Commands/PackageCommands/SwiftPackageCommand.swift

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public struct SwiftPackageCommand: AsyncParsableCommand {
6262
Fetch.self,
6363

6464
ShowDependencies.self,
65+
ShowExecutables.self,
6566
ToolsVersionCommand.self,
6667
ComputeChecksum.self,
6768
ArchiveSource.self,

Tests/CommandsTests/PackageCommandTests.swift

+35
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,41 @@ final class PackageCommandTests: CommandsTestCase {
551551
}
552552
}
553553

554+
func testShowExecutables() async throws {
555+
try await fixture(name: "Miscellaneous/ShowExecutables") { fixturePath in
556+
let packageRoot = fixturePath.appending("app")
557+
let (textOutput, _) = try await SwiftPM.Package.execute(["show-executables", "--format=flatlist"], packagePath: packageRoot)
558+
XCTAssert(textOutput.contains("dealer\n"))
559+
XCTAssert(textOutput.contains("deck (deck-of-playing-cards)\n"))
560+
561+
let (jsonOutput, _) = try await SwiftPM.Package.execute(["show-executables", "--format=json"], packagePath: packageRoot)
562+
let json = try JSON(bytes: ByteString(encodingAsUTF8: jsonOutput))
563+
guard case let .array(contents) = json else { XCTFail("unexpected result"); return }
564+
565+
XCTAssertEqual(2, contents.count)
566+
567+
guard case let first = contents.first else { XCTFail("unexpected result"); return }
568+
guard case let .dictionary(dealer) = first else { XCTFail("unexpected result"); return }
569+
guard case let .string(dealerName)? = dealer["name"] else { XCTFail("unexpected result"); return }
570+
XCTAssertEqual(dealerName, "dealer")
571+
if case let .string(package)? = dealer["package"] {
572+
XCTFail("unexpected package for dealer (should be unset): \(package)")
573+
return
574+
}
575+
576+
guard case let last = contents.last else { XCTFail("unexpected result"); return }
577+
guard case let .dictionary(deck) = last else { XCTFail("unexpected result"); return }
578+
guard case let .string(deckName)? = deck["name"] else { XCTFail("unexpected result"); return }
579+
XCTAssertEqual(deckName, "deck")
580+
if case let .string(package)? = deck["package"] {
581+
XCTAssertEqual("deck-of-playing-cards", package)
582+
} else {
583+
XCTFail("missing package for deck")
584+
return
585+
}
586+
}
587+
}
588+
554589
func testShowDependencies() async throws {
555590
try await fixture(name: "DependencyResolution/External/Complex") { fixturePath in
556591
let packageRoot = fixturePath.appending("app")

0 commit comments

Comments
 (0)