Skip to content

Commit 47aefe7

Browse files
committed
Support vending products that are backed by binaryTargets
This adds support for vending an executable product that consists solely of a binary target that is backed by an artifact bundle. This allows vending binary executables as their own separate package, independently of the plugins that are using them. rdar://101096803
1 parent db9a253 commit 47aefe7

File tree

20 files changed

+272
-83
lines changed

20 files changed

+272
-83
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"schemaVersion": "1.0",
3+
"artifacts": {
4+
"mytool": {
5+
"type": "executable",
6+
"version": "1.2.3",
7+
"variants": [
8+
{
9+
"path": "mytool-macos/mytool",
10+
"supportedTriples": ["x86_64-apple-macosx", "arm64-apple-macosx"]
11+
},
12+
{
13+
"path": "mytool-linux/mytool",
14+
"supportedTriples": ["x86_64-unknown-linux-gnu"]
15+
}
16+
]
17+
}
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/bin/bash
2+
3+
print_usage() {
4+
echo "usage: ${0##*/} [--verbose] <in> <out>"
5+
}
6+
7+
# Parse arguments until we find '--' or an argument that isn't an option.
8+
until [ $# -eq 0 ]
9+
do
10+
case "$1" in
11+
--verbose) verbose=1; shift;;
12+
--) shift; break;;
13+
-*) echo "unknown option: ${1}"; print_usage; exit 1; shift;;
14+
*) break;;
15+
esac
16+
done
17+
18+
# Print usage and leave if we don't have exactly two arguments.
19+
if [ $# -ne 2 ]; then
20+
print_usage
21+
exit 1
22+
fi
23+
24+
# For our sample tool we just copy from one to the other.
25+
if [ $verbose != 0 ]; then
26+
echo "[${0##*/}-linux] '$1' '$2'"
27+
fi
28+
29+
cp "$1" "$2"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/bin/bash
2+
3+
print_usage() {
4+
echo "usage: ${0##*/} [--verbose] <in> <out>"
5+
}
6+
7+
# Parse arguments until we find '--' or an argument that isn't an option.
8+
until [ $# -eq 0 ]
9+
do
10+
case "$1" in
11+
--verbose) verbose=1; shift;;
12+
--) shift; break;;
13+
-*) echo "unknown option: ${1}"; print_usage; exit 1; shift;;
14+
*) break;;
15+
esac
16+
done
17+
18+
# Print usage and leave if we don't have exactly two arguments.
19+
if [ $# -ne 2 ]; then
20+
print_usage
21+
exit 1
22+
fi
23+
24+
# For our sample tool we just copy from one to the other.
25+
if [ $verbose != 0 ]; then
26+
echo "[${0##*/}-macosx] '$1' '$2'"
27+
fi
28+
29+
cp "$1" "$2"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// swift-tools-version: 5.6
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "MyBinaryProduct",
6+
products: [
7+
.executable(
8+
name: "MyVendedSourceGenBuildTool",
9+
targets: ["MyVendedSourceGenBuildTool"]
10+
),
11+
],
12+
targets: [
13+
.binaryTarget(
14+
name: "MyVendedSourceGenBuildTool",
15+
path: "Binaries/MyVendedSourceGenBuildTool.artifactbundle"
16+
),
17+
]
18+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// swift-tools-version: 5.6
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "MyBinaryToolPlugin",
6+
dependencies: [
7+
.package(path: "Dependency"),
8+
],
9+
targets: [
10+
// A local tool that uses a build tool plugin.
11+
.executableTarget(
12+
name: "MyLocalTool",
13+
plugins: [
14+
"MySourceGenBuildToolPlugin",
15+
]
16+
),
17+
// The plugin that generates build tool commands to invoke MySourceGenBuildTool.
18+
.plugin(
19+
name: "MySourceGenBuildToolPlugin",
20+
capability: .buildTool(),
21+
dependencies: [
22+
.product(name: "MyVendedSourceGenBuildTool", package: "Dependency"),
23+
]
24+
),
25+
]
26+
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import PackagePlugin
2+
3+
@main
4+
struct MyPlugin: BuildToolPlugin {
5+
6+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
7+
print("Hello from the Build Tool Plugin!")
8+
guard let target = target as? SourceModuleTarget else { return [] }
9+
let inputFiles = target.sourceFiles.filter({ $0.path.extension == "dat" })
10+
return try inputFiles.map {
11+
let inputFile = $0
12+
let inputPath = inputFile.path
13+
let outputName = inputPath.stem + ".swift"
14+
let outputPath = context.pluginWorkDirectory.appending(outputName)
15+
return .buildCommand(
16+
displayName:
17+
"Generating \(outputName) from \(inputPath.lastComponent)",
18+
executable:
19+
try context.tool(named: "mytool").path,
20+
arguments: [
21+
"--verbose",
22+
"\(inputPath)",
23+
"\(outputPath)"
24+
],
25+
inputFiles: [
26+
inputPath,
27+
],
28+
outputFiles: [
29+
outputPath
30+
]
31+
)
32+
}
33+
}
34+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let foo = "I am Foo!"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Generated string Foo: '\(foo)'")

Sources/Build/BuildPlan.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,7 +1469,7 @@ public final class ProductBuildDescription {
14691469
// we will instead have generated a source file containing the redirect.
14701470
// Support for linking tests against executables is conditional on the tools
14711471
// version of the package that defines the executable product.
1472-
let executableTarget = try product.executableTarget()
1472+
let executableTarget = try product.executableTarget
14731473
if executableTarget.underlyingTarget is SwiftTarget, toolsVersion >= .v5_5,
14741474
buildParameters.canRenameEntrypointFunctionName {
14751475
if let flags = buildParameters.linkerFlagsForRenamingMainFunction(of: executableTarget) {
@@ -1988,9 +1988,8 @@ public class BuildPlan {
19881988
}
19891989

19901990
var productMap: [ResolvedProduct: ProductBuildDescription] = [:]
1991-
// Create product description for each product we have in the package graph except
1992-
// for automatic libraries and plugins, because they don't produce any output.
1993-
for product in graph.allProducts where product.type != .library(.automatic) && product.type != .plugin {
1991+
// Create product description for each product we have in the package graph that is eligible.
1992+
for product in graph.allProducts where product.shouldCreateProductDescription {
19941993
guard let package = graph.package(for: product) else {
19951994
throw InternalError("unknown package for \(product)")
19961995
}
@@ -2567,3 +2566,22 @@ extension ResolvedPackage {
25672566
}
25682567
}
25692568
}
2569+
2570+
extension ResolvedProduct {
2571+
private var isAutomaticLibrary: Bool {
2572+
return self.type == .library(.automatic)
2573+
}
2574+
2575+
private var isBinaryOnly: Bool {
2576+
return self.targets.filter({ !($0.underlyingTarget is BinaryTarget) }).isEmpty
2577+
}
2578+
2579+
private var isPlugin: Bool {
2580+
return self.type == .plugin
2581+
}
2582+
2583+
// We shouldn't create product descriptions for automatic libraries, plugins or products which consist solely of binary targets, because they don't produce any output.
2584+
fileprivate var shouldCreateProductDescription: Bool {
2585+
return !isAutomaticLibrary && !isBinaryOnly && !isPlugin
2586+
}
2587+
}

Sources/Build/LLBuildManifestBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ extension LLBuildManifestBuilder {
553553
if target.type == .executable {
554554
// FIXME: Optimize.
555555
let _product = try plan.graph.allProducts.first {
556-
try $0.type == .executable && $0.executableTarget() == target
556+
try $0.type == .executable && $0.executableTarget == target
557557
}
558558
if let product = _product {
559559
guard let planProduct = plan.productMap[product] else {

Sources/Commands/SwiftPackageTool.swift

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,7 @@ extension SwiftPackageTool {
10101010
try PluginCommand.run(
10111011
plugin: matchingPlugins[0],
10121012
package: packageGraph.rootPackages[0],
1013+
packageGraph: packageGraph,
10131014
options: pluginOptions,
10141015
arguments: arguments,
10151016
swiftTool: swiftTool)
@@ -1018,6 +1019,7 @@ extension SwiftPackageTool {
10181019
static func run(
10191020
plugin: PluginTarget,
10201021
package: ResolvedPackage,
1022+
packageGraph: PackageGraph,
10211023
options: PluginOptions,
10221024
arguments: [String],
10231025
swiftTool: SwiftTool
@@ -1080,29 +1082,17 @@ extension SwiftPackageTool {
10801082

10811083
// Build or bring up-to-date any executable host-side tools on which this plugin depends. Add them and any binary dependencies to the tool-names-to-path map.
10821084
var toolNamesToPaths: [String: AbsolutePath] = [:]
1083-
for dep in plugin.dependencies(satisfying: try swiftTool.buildParameters().buildEnvironment) {
1085+
for dep in try plugin.accessibleTools(packageGraph: packageGraph, fileSystem: swiftTool.fileSystem, environment: try swiftTool.buildParameters().buildEnvironment, for: try pluginScriptRunner.hostTriple) {
10841086
let buildOperation = try swiftTool.createBuildOperation(cacheBuildManifest: false)
10851087
switch dep {
1086-
case .product(let productRef, _):
1087-
// Build the product referenced by the tool, and add the executable to the tool map.
1088-
try buildOperation.build(subset: .product(productRef.name))
1089-
if let builtTool = buildOperation.buildPlan?.buildProducts.first(where: { $0.product.name == productRef.name}) {
1090-
toolNamesToPaths[productRef.name] = builtTool.binary
1091-
}
1092-
case .target(let target, _):
1093-
if let target = target as? BinaryTarget {
1094-
// Add the executables vended by the binary target to the tool map.
1095-
for exec in try target.parseArtifactArchives(for: pluginScriptRunner.hostTriple, fileSystem: swiftTool.fileSystem) {
1096-
toolNamesToPaths[exec.name] = exec.executablePath
1097-
}
1098-
}
1099-
else {
1100-
// Build the product referenced by the tool, and add the executable to the tool map. Product dependencies are not supported within a package, so we instead find the executable that corresponds to the product. There is always one, because of autogeneration of implicit executables with the same name as the target if there isn't an explicit one.
1101-
try buildOperation.build(subset: .product(target.name))
1102-
if let builtTool = buildOperation.buildPlan?.buildProducts.first(where: { $0.product.name == target.name}) {
1103-
toolNamesToPaths[target.name] = builtTool.binary
1104-
}
1088+
case .builtTool(let name, _):
1089+
// Build the product referenced by the tool, and add the executable to the tool map. Product dependencies are not supported within a package, so if the tool happens to be from the same package, we instead find the executable that corresponds to the product. There is always one, because of autogeneration of implicit executables with the same name as the target if there isn't an explicit one.
1090+
try buildOperation.build(subset: .product(name))
1091+
if let builtTool = buildOperation.buildPlan?.buildProducts.first(where: { $0.product.name == name}) {
1092+
toolNamesToPaths[name] = builtTool.binary
11051093
}
1094+
case .vendedTool(let name, let path):
1095+
toolNamesToPaths[name] = path
11061096
}
11071097
}
11081098

@@ -1536,6 +1526,7 @@ extension SwiftPackageTool {
15361526
try PluginCommand.run(
15371527
plugin: matchingPlugins[0],
15381528
package: packageGraph.rootPackages[0],
1529+
packageGraph: packageGraph,
15391530
options: pluginOptions,
15401531
arguments: Array( remaining.dropFirst()),
15411532
swiftTool: swiftTool)

Sources/PackageGraph/ResolvedProduct.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,16 @@ public final class ResolvedProduct {
4343
/// The main executable target of product.
4444
///
4545
/// Note: This property is only valid for executable products.
46-
public func executableTarget() throws -> ResolvedTarget {
47-
guard type == .executable || type == .snippet else {
48-
throw InternalError("firstExecutableModule should only be called for executable targets")
46+
public var executableTarget: ResolvedTarget {
47+
get throws {
48+
guard type == .executable || type == .snippet else {
49+
throw InternalError("`executableTarget` should only be called for executable targets")
50+
}
51+
guard let underlyingExecutableTarget = targets.map({ $0.underlyingTarget }).executables.first, let executableTarget = targets.first(where: { $0.underlyingTarget == underlyingExecutableTarget }) else {
52+
throw InternalError("could not determine executable target")
53+
}
54+
return executableTarget
4955
}
50-
return targets.first(where: { $0.type == .executable || $0.type == .snippet })!
5156
}
5257

5358
public init(product: Product, targets: [ResolvedTarget]) {

Sources/PackageLoading/ManifestLoader+Validation.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,14 @@ public struct ManifestValidator {
7878
}
7979
}
8080

81-
// Check that products that reference only binary targets don't define a type.
82-
let areTargetsBinary = product.targets.allSatisfy { self.manifest.targetMap[$0]?.type == .binary }
83-
if areTargetsBinary && product.type != .library(.automatic) {
84-
diagnostics.append(.invalidBinaryProductType(productName: product.name))
81+
// Check that products that reference only binary targets don't define an explicit library type.
82+
if product.targets.allSatisfy({ self.manifest.targetMap[$0]?.type == .binary }) {
83+
switch product.type {
84+
case .library(.automatic), .executable:
85+
break
86+
default:
87+
diagnostics.append(.invalidBinaryProductType(productName: product.name))
88+
}
8589
}
8690
}
8791

@@ -262,7 +266,7 @@ extension Basics.Diagnostic {
262266
}
263267

264268
static func invalidBinaryProductType(productName: String) -> Self {
265-
.error("invalid type for binary product '\(productName)'; products referencing only binary targets must have a type of 'library'")
269+
.error("invalid type for binary product '\(productName)'; products referencing only binary targets must be executable or automatic library products")
266270
}
267271

268272
/*static func duplicateDependency(dependencyIdentity: PackageIdentity) -> Self {

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1290,7 +1290,7 @@ public final class PackageBuilder {
12901290
}
12911291

12921292
private func validateExecutableProduct(_ product: ProductDescription, with targets: [Target]) -> Bool {
1293-
let executableTargetCount = targets.filter { $0.type == .executable }.count
1293+
let executableTargetCount = targets.executables.count
12941294
guard executableTargetCount == 1 else {
12951295
if executableTargetCount == 0 {
12961296
if let target = targets.spm_only {

Sources/PackageModel/Product.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class Product: Codable {
4343
throw InternalError("Targets cannot be empty")
4444
}
4545
if type == .executable {
46-
guard targets.filter({ $0.type == .executable }).count == 1 else {
46+
guard targets.executables.count == 1 else {
4747
throw InternalError("Executable products should have exactly one executable target.")
4848
}
4949
}

Sources/PackageModel/Target.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,11 @@ public final class BinaryTarget: Target {
638638
}
639639
}
640640

641+
public var containsExecutable: Bool {
642+
// FIXME: needs to be revisited once libraries are supported in artifact bundles
643+
return self.kind == .artifactsArchive
644+
}
645+
641646
public enum Origin: Equatable, Codable {
642647

643648
/// Represents an artifact that was downloaded from a remote URL.
@@ -799,3 +804,18 @@ public enum PluginPermission: Hashable, Codable {
799804
}
800805
}
801806
}
807+
808+
public extension Sequence where Iterator.Element == Target {
809+
var executables: [Target] {
810+
return filter {
811+
switch $0.type {
812+
case .binary:
813+
return ($0 as? BinaryTarget)?.containsExecutable == true
814+
case .executable:
815+
return true
816+
default:
817+
return false
818+
}
819+
}
820+
}
821+
}

Sources/SPMBuildCore/PluginContextSerializer.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,7 @@ internal struct PluginContextSerializer {
184184
switch product.type {
185185

186186
case .executable:
187-
guard let mainExecTarget = product.targets.first(where: { $0.type == .executable }) else {
188-
throw InternalError("could not determine main executable target for product \(product)")
189-
}
187+
let mainExecTarget = try product.executableTarget
190188
guard let mainExecTargetId = try serialize(target: mainExecTarget) else {
191189
throw InternalError("unable to serialize main executable target \(mainExecTarget) for product \(product)")
192190
}

0 commit comments

Comments
 (0)