Skip to content

[5.5] Align experimental implementations of SE-0303 and SE-0305 with proposal #3574

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

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
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ print("Hello from the Build Tool Plugin!")
for inputFile in targetBuildContext.inputFiles.filter({ $0.path.extension == "dat" }) {
let inputPath = inputFile.path
let outputName = inputPath.stem + ".swift"
let outputPath = targetBuildContext.outputDirectory.appending(outputName)
commandConstructor.createBuildCommand(
let outputPath = targetBuildContext.pluginWorkDirectory.appending(outputName)
commandConstructor.addBuildCommand(
displayName:
"Generating \(outputName) from \(inputPath.lastComponent)",
executable:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ let package = Package(
// The vended executable that generates source files.
.binaryTarget(
name: "MyVendedSourceGenBuildTool",
path: "Binaries/MyVendedSourceGenBuildTool.arar"
path: "Binaries/MyVendedSourceGenBuildTool.artifactbundle"
),
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ print("Hello from the Build Tool Plugin!")
for inputFile in targetBuildContext.inputFiles.filter({ $0.path.extension == "dat" }) {
let inputPath = inputFile.path
let outputName = inputPath.stem + ".swift"
let outputPath = targetBuildContext.outputDirectory.appending(outputName)
commandConstructor.createBuildCommand(
let outputPath = targetBuildContext.pluginWorkDirectory.appending(outputName)
commandConstructor.addBuildCommand(
displayName:
"Generating \(outputName) from \(inputPath.lastComponent)",
executable:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ print("Hello from the Build Tool Plugin!")
for inputPath in targetBuildContext.inputFiles.map{ $0.path } {
guard inputPath.extension == "dat" else { continue }
let outputName = inputPath.stem + ".swift"
let outputPath = targetBuildContext.outputDirectory.appending(outputName)
commandConstructor.createBuildCommand(
let outputPath = targetBuildContext.pluginWorkDirectory.appending(outputName)
commandConstructor.addBuildCommand(
displayName:
"Generating \(outputName) from \(inputPath.lastComponent)",
executable:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import PackagePlugin
print("Hello from the Prebuild Plugin!")

let outputPaths: [Path] = targetBuildContext.inputFiles.filter{ $0.path.extension == "dat" }.map { file in
targetBuildContext.outputDirectory.appending(file.path.stem + ".swift")
targetBuildContext.pluginWorkDirectory.appending(file.path.stem + ".swift")
}

if !outputPaths.isEmpty {
commandConstructor.createPrebuildCommand(
commandConstructor.addPrebuildCommand(
displayName:
"Running prebuild command for target \(targetBuildContext.targetName)",
executable:
Path("/usr/bin/touch"),
arguments:
outputPaths.map{ $0.string },
outputFilesDirectory:
targetBuildContext.outputDirectory
targetBuildContext.pluginWorkDirectory
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import PackagePlugin
import Foundation

// Check that we can write to the output directory.
let allowedOutputPath = targetBuildContext.outputDirectory.appending("Foo")
let allowedOutputPath = targetBuildContext.pluginWorkDirectory.appending("Foo")
if mkdir(allowedOutputPath.string, 0o777) != 0 {
throw StringError("unexpectedly could not write to '\(allowedOutputPath)': \(String(utf8String: strerror(errno)))")
}
Expand Down
15 changes: 8 additions & 7 deletions Sources/PackageDescription/Target.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,6 @@ public final class Target {
case .plugin:
precondition(
url == nil &&
exclude.isEmpty &&
sources == nil &&
resources == nil &&
publicHeadersPath == nil &&
pkgConfig == nil &&
Expand Down Expand Up @@ -901,14 +899,17 @@ public final class Target {
public static func plugin(
name: String,
capability: PluginCapability,
dependencies: [Dependency] = []
dependencies: [Dependency] = [],
path: String? = nil,
exclude: [String] = [],
sources: [String]? = nil
) -> Target {
return Target(
name: name,
dependencies: dependencies,
path: nil,
exclude: [],
sources: nil,
path: path,
exclude: exclude,
sources: sources,
publicHeadersPath: nil,
type: .plugin,
pluginCapability: capability)
Expand Down Expand Up @@ -1062,7 +1063,7 @@ extension Target.Dependency {

extension Target.PluginCapability {

/// Specifies that the plugin provides a prebuild capability. The plugin
/// Specifies that the plugin provides a build tool capability. The plugin
/// will be applied to each target that uses it and should create commands
/// that will run before or during the build of the target.
@available(_PackageDescription, introduced: 5.5)
Expand Down
26 changes: 21 additions & 5 deletions Sources/PackageLoading/PackageBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ extension ModuleError: CustomStringConvertible {
let packages = packages.joined(separator: "', '")
return "multiple targets named '\(name)' in: '\(packages)'"
case .moduleNotFound(let target, let type):
let folderName = type == .test ? "Tests" : "Sources"
let folderName = (type == .test) ? "Tests" : (type == .plugin) ? "Plugins" : "Sources"
return "Source files for target \(target) should be located under '\(folderName)/\(target)', or a custom sources path can be set with the 'path' property in Package.swift"
case .artifactNotFound(let target):
return "artifact not found for target '\(target)'"
Expand Down Expand Up @@ -535,9 +535,12 @@ public final class PackageBuilder {

/// Predefined test directories, in order of preference.
public static let predefinedTestDirectories = ["Tests", "Sources", "Source", "src", "srcs"]

/// Predefined plugin directories, in order of preference.
public static let predefinedPluginDirectories = ["Plugins"]

/// Finds the predefined directories for regular and test targets.
private func findPredefinedTargetDirectory() -> (targetDir: String, testTargetDir: String) {
/// Finds the predefined directories for regular targets, test targets, and plugin targets.
private func findPredefinedTargetDirectory() -> (targetDir: String, testTargetDir: String, pluginTargetDir: String) {
let targetDir = PackageBuilder.predefinedSourceDirectories.first(where: {
fileSystem.isDirectory(packagePath.appending(component: $0))
}) ?? PackageBuilder.predefinedSourceDirectories[0]
Expand All @@ -546,7 +549,11 @@ public final class PackageBuilder {
fileSystem.isDirectory(packagePath.appending(component: $0))
}) ?? PackageBuilder.predefinedTestDirectories[0]

return (targetDir, testTargetDir)
let pluginTargetDir = PackageBuilder.predefinedPluginDirectories.first(where: {
fileSystem.isDirectory(packagePath.appending(component: $0))
}) ?? PackageBuilder.predefinedPluginDirectories[0]

return (targetDir, testTargetDir, pluginTargetDir)
}

struct PredefinedTargetDirectory {
Expand All @@ -566,6 +573,7 @@ public final class PackageBuilder {

let predefinedTargetDirectory = PredefinedTargetDirectory(fs: fileSystem, path: packagePath.appending(component: predefinedDirs.targetDir))
let predefinedTestTargetDirectory = PredefinedTargetDirectory(fs: fileSystem, path: packagePath.appending(component: predefinedDirs.testTargetDir))
let predefinedPluginTargetDirectory = PredefinedTargetDirectory(fs: fileSystem, path: packagePath.appending(component: predefinedDirs.pluginTargetDir))

/// Returns the path of the given target.
func findPath(for target: TargetDescription) throws -> AbsolutePath {
Expand Down Expand Up @@ -598,7 +606,15 @@ public final class PackageBuilder {
}

// Check if target is present in the predefined directory.
let predefinedDir = target.isTest ? predefinedTestTargetDirectory : predefinedTargetDirectory
let predefinedDir: PredefinedTargetDirectory
switch target.type {
case .test:
predefinedDir = predefinedTestTargetDirectory
case .plugin:
predefinedDir = predefinedPluginTargetDirectory
default:
predefinedDir = predefinedTargetDirectory
}
let path = predefinedDir.path.appending(component: target.name)

// Return the path if the predefined directory contains it.
Expand Down
2 changes: 0 additions & 2 deletions Sources/PackageModel/Manifest/TargetDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,6 @@ public struct TargetDescription: Equatable, Codable {
if pluginUsages != nil { throw Error.disallowedPropertyInTarget(targetName: name, propertyName: "pluginUsages") }
case .plugin:
if url != nil { throw Error.disallowedPropertyInTarget(targetName: name, propertyName: "url") }
if !exclude.isEmpty { throw Error.disallowedPropertyInTarget(targetName: name, propertyName: "exclude") }
if sources != nil { throw Error.disallowedPropertyInTarget(targetName: name, propertyName: "sources") }
if !resources.isEmpty { throw Error.disallowedPropertyInTarget(targetName: name, propertyName: "resources") }
if publicHeadersPath != nil { throw Error.disallowedPropertyInTarget(targetName: name, propertyName: "publicHeadersPath") }
if pkgConfig != nil { throw Error.disallowedPropertyInTarget(targetName: name, propertyName: "pkgConfig") }
Expand Down
2 changes: 1 addition & 1 deletion Sources/PackageModel/Target.swift
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ public final class BinaryTarget: Target {
case .xcframework:
return "xcframework"
case .artifactsArchive:
return "arar"
return "artifactbundle"
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/PackagePlugin/ImplementationDetails.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ struct BuildCommand: Encodable {
let displayName: String?
let executable: Path
let arguments: [String]
let environment: [String: String]?
let environment: [String: String]
let workingDirectory: Path?
let inputFiles: [Path]
let outputFiles: [Path]
Expand All @@ -71,7 +71,7 @@ struct PrebuildCommand: Encodable {
let displayName: String?
let executable: Path
let arguments: [String]
let environment: [String: String]?
let environment: [String: String]
let workingDirectory: Path?
let outputFilesDirectory: Path
}
Expand Down
135 changes: 119 additions & 16 deletions Sources/PackagePlugin/PublicAPI/CommandConstructor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,82 @@
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

/// Constructs commands to run during the build, including full command lines.
/// All paths should be based on the ones passed to the plugin in the target
/// build context.
/// Constructs commands to run during the build, including command lines,
/// environment variables, initial working directory, etc. All paths should
/// be based on the ones passed to the plugin in the target build context.
public final class CommandConstructor {

/// Prevents the CommandConstructor from being instantiated by the script.
internal init() {}

/// Creates a command to run during the build. The executable should be a
/// path returned by `TargetBuildContext.tool(named:)`, the inputs should
/// be the files that are used by the command, and the outputs should be
/// the files that are produced by the command.
public func createBuildCommand(
/// tool returned by `TargetBuildContext.tool(named:)`, and any paths in
/// the arguments list as well as in the input and output lists should be
/// based on the paths provided in the target build context structure.
///
/// The build command will run whenever its outputs are missing or if its
/// inputs have changed since the command was last run. In other words,
/// it is incorporated into the build command graph.
///
/// This is the preferred kind of command to create when the outputs that
/// will be generated are known ahead of time.
///
/// - parameters:
/// - displayName: An optional string to show in build logs and other
/// status areas.
/// - executable: The executable to be invoked; should be a tool looked
/// up using `tool(named:)`, which may reference either a tool provided
/// by a binary target or build from source.
/// - arguments: Arguments to be passed to the tool. Any paths should be
/// based on the paths provided in the target build context.
/// - environment: Any custom environment assignments for the subprocess.
/// - inputFiles: Input files to the build command. Any changes to the
/// input files cause the command to be rerun.
/// - outputFiles: Output files that should be processed further according
/// to the rules defined by the build system.
public func addBuildCommand(
displayName: String?,
executable: Path,
arguments: [String],
environment: [String: String]? = nil,
environment: [String: String] = [:],
inputFiles: [Path] = [],
outputFiles: [Path] = []
) {
output.buildCommands.append(BuildCommand(displayName: displayName, executable: executable, arguments: arguments, environment: environment, workingDirectory: nil, inputFiles: inputFiles, outputFiles: outputFiles))
}

/// Creates a command to run during the build. The executable should be a
/// tool returned by `TargetBuildContext.tool(named:)`, and any paths in
/// the arguments list as well as in the input and output lists should be
/// based on the paths provided in the target build context structure.
///
/// The build command will run whenever its outputs are missing or if its
/// inputs have changed since the command was last run. In other words,
/// it is incorporated into the build command graph.
///
/// This is the preferred kind of command to create when the outputs that
/// will be generated are known ahead of time.
///
/// - parameters:
/// - displayName: An optional string to show in build logs and other
/// status areas.
/// - executable: The executable to be invoked; should be a tool looked
/// up using `tool(named:)`, which may reference either a tool provided
/// by a binary target or build from source.
/// - arguments: Arguments to be passed to the tool. Any paths should be
/// based on the paths provided in the target build context.
/// - environment: Any custom environment assignments for the subprocess.
/// - workingDirectory: Optional initial working directory of the command.
/// - inputFiles: Input files to the build command. Any changes to the
/// input files cause the command to be rerun.
/// - outputFiles: Output files that should be processed further according
/// to the rules defined by the build system.
@available(*, unavailable, message: "specifying the initial working directory for a command is not yet supported")
public func createBuildCommand(
public func addBuildCommand(
displayName: String?,
executable: Path,
arguments: [String],
environment: [String: String]? = nil,
environment: [String: String] = [:],
workingDirectory: Path? = nil,
inputFiles: [Path] = [],
outputFiles: [Path] = []
Expand All @@ -44,25 +92,80 @@ public final class CommandConstructor {
}

/// Creates a command to run before the build. The executable should be a
/// path returned by `TargetBuildContext.tool(named:)`, the output direc-
/// tory should be a directory in which the command will create the output
/// files that should be subject to further processing.
public func createPrebuildCommand(
/// tool returned by `TargetBuildContext.tool(named:)`, and any paths in
/// the arguments list and the output files directory should be based on
/// the paths provided in the target build context structure.
///
/// The build command will run before the build starts, and is allowed to
/// create an arbitrary set of output files based on the contents of the
/// inputs.
///
/// Because prebuild commands are run on every build, they are can have
/// significant performance impact and should only be used when there is
/// no way to know the names of the outputs before the command is run.
///
/// The `outputFilesDirectory` parameter is the path of a directory into
/// which the command will write its output files. Any files that are in
/// that directory after the prebuild command finishes will be interpreted
/// according to same build rules as for sources.
///
/// - parameters:
/// - displayName: An optional string to show in build logs and other
/// status areas.
/// - executable: The executable to be invoked; should be a tool looked
/// up using `tool(named:)`, which may reference either a tool provided
/// by a binary target or build from source.
/// - arguments: Arguments to be passed to the tool. Any paths should be
/// based on the paths provided in the target build context.
/// - environment: Any custom environment assignments for the subprocess.
/// - outputFilesDirectory: A directory into which the command can write
/// output files that should be processed further.
public func addPrebuildCommand(
displayName: String?,
executable: Path,
arguments: [String],
environment: [String: String]? = nil,
environment: [String: String] = [:],
outputFilesDirectory: Path
) {
output.prebuildCommands.append(PrebuildCommand(displayName: displayName, executable: executable, arguments: arguments, environment: environment, workingDirectory: nil, outputFilesDirectory: outputFilesDirectory))
}

/// Creates a command to run before the build. The executable should be a
/// tool returned by `TargetBuildContext.tool(named:)`, and any paths in
/// the arguments list and the output files directory should be based on
/// the paths provided in the target build context structure.
///
/// The build command will run before the build starts, and is allowed to
/// create an arbitrary set of output files based on the contents of the
/// inputs.
///
/// Because prebuild commands are run on every build, they are can have
/// significant performance impact and should only be used when there is
/// no way to know the names of the outputs before the command is run.
///
/// The `outputFilesDirectory` parameter is the path of a directory into
/// which the command will write its output files. Any files that are in
/// that directory after the prebuild command finishes will be interpreted
/// according to same build rules as for sources.
///
/// - parameters:
/// - displayName: An optional string to show in build logs and other
/// status areas.
/// - executable: The executable to be invoked; should be a tool looked
/// up using `tool(named:)`, which may reference either a tool provided
/// by a binary target or build from source.
/// - arguments: Arguments to be passed to the tool. Any paths should be
/// based on the paths provided in the target build context.
/// - environment: Any custom environment assignments for the subprocess.
/// - workingDirectory: Optional initial working directory of the command.
/// - outputFilesDirectory: A directory into which the command can write
/// output files that should be processed further.
@available(*, unavailable, message: "specifying the initial working directory for a command is not yet supported")
public func createPrebuildCommand(
displayName: String?,
executable: Path,
arguments: [String],
environment: [String: String]? = nil,
environment: [String: String] = [:],
workingDirectory: Path? = nil,
outputFilesDirectory: Path
) {
Expand Down
Loading