Skip to content

Commit 0783c4c

Browse files
committed
Replace custom path type with URL in plugin API
Plugins introduced their own custom path type which doesn't have Windows support, causing several issues with plugins on Windows. We should rectify this situation by using Foundation's URL type instead which already works fine on Windows. rdar://117230149
1 parent d2eb0ca commit 0783c4c

File tree

27 files changed

+577
-98
lines changed

27 files changed

+577
-98
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// swift-tools-version: 5.11
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "MySourceGenPlugin",
6+
products: [
7+
// The product that vends MySourceGenBuildToolPlugin to client packages.
8+
.plugin(
9+
name: "MySourceGenBuildToolPlugin",
10+
targets: ["MySourceGenBuildToolPlugin"]
11+
),
12+
// The product that vends the MySourceGenBuildTool executable to client packages.
13+
.executable(
14+
name: "MySourceGenBuildTool",
15+
targets: ["MySourceGenBuildTool"]
16+
),
17+
// The product that vends MySourceGenPrebuildPlugin to client packages.
18+
.plugin(
19+
name: "MySourceGenPrebuildPlugin",
20+
targets: ["MySourceGenPrebuildPlugin"]
21+
),
22+
],
23+
targets: [
24+
// A local tool that uses a build tool plugin.
25+
.executableTarget(
26+
name: "MyLocalTool",
27+
plugins: [
28+
"MySourceGenBuildToolPlugin",
29+
]
30+
),
31+
// A local tool that uses a prebuild plugin.
32+
.executableTarget(
33+
name: "MyOtherLocalTool",
34+
plugins: [
35+
"MySourceGenPrebuildPlugin",
36+
]
37+
),
38+
// The plugin that generates build tool commands to invoke MySourceGenBuildTool.
39+
.plugin(
40+
name: "MySourceGenBuildToolPlugin",
41+
capability: .buildTool(),
42+
dependencies: [
43+
"MySourceGenBuildTool",
44+
]
45+
),
46+
// The plugin that generates prebuild commands (currently to invoke a system tool).
47+
.plugin(
48+
name: "MySourceGenPrebuildPlugin",
49+
capability: .buildTool()
50+
),
51+
// The command line tool that generates source files.
52+
.executableTarget(
53+
name: "MySourceGenBuildTool",
54+
dependencies: [
55+
"MySourceGenBuildToolLib",
56+
]
57+
),
58+
// A library used by MySourceGenBuildTool (not the client).
59+
.target(
60+
name: "MySourceGenBuildToolLib"
61+
),
62+
// A runtime library that the client needs to link against.
63+
.target(
64+
name: "MySourceGenRuntimeLib"
65+
),
66+
// Unit tests for the plugin.
67+
.testTarget(
68+
name: "MySourceGenPluginTests",
69+
dependencies: [
70+
"MySourceGenRuntimeLib",
71+
],
72+
plugins: [
73+
"MySourceGenBuildToolPlugin",
74+
"MySourceGenPrebuildPlugin",
75+
]
76+
)
77+
]
78+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import PackagePlugin
2+
3+
@main
4+
struct MyPlugin: BuildToolPlugin {
5+
#if USE_CREATE
6+
let verb = "Creating"
7+
#else
8+
let verb = "Generating"
9+
#endif
10+
11+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
12+
print("Hello from the Build Tool Plugin!")
13+
guard let target = target as? SourceModuleTarget else { return [] }
14+
return try target.sourceFiles.map{ $0.url }.compactMap {
15+
guard $0.pathExtension == "dat" else { return .none }
16+
let outputName = $0.deletingPathExtension().lastPathComponent + ".swift"
17+
let outputPath = context.pluginWorkDirectoryURL.appendingPathComponent(outputName)
18+
return .buildCommand(
19+
displayName:
20+
"\(verb) \(outputName) from \($0.lastPathComponent)",
21+
executable:
22+
try context.tool(named: "MySourceGenBuildTool").url,
23+
arguments: [
24+
"\($0)",
25+
"\(outputPath.path)"
26+
],
27+
inputFiles: [
28+
$0,
29+
],
30+
outputFiles: [
31+
outputPath
32+
]
33+
)
34+
}
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Foundation
2+
import PackagePlugin
3+
4+
@main
5+
struct MyPlugin: BuildToolPlugin {
6+
7+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
8+
print("Hello from the Prebuild Plugin!")
9+
guard let target = target as? SourceModuleTarget else { return [] }
10+
let outputPaths: [URL] = target.sourceFiles.filter{ $0.url.pathExtension == "dat" }.map { file in
11+
context.pluginWorkDirectoryURL.appendingPathComponent(file.url.lastPathComponent + ".swift")
12+
}
13+
var commands: [Command] = []
14+
if !outputPaths.isEmpty {
15+
commands.append(.prebuildCommand(
16+
displayName:
17+
"Running prebuild command for target \(target.name)",
18+
executable:
19+
URL(fileURLWithPath: "/usr/bin/touch"),
20+
arguments:
21+
outputPaths.map{ $0.path },
22+
outputFilesDirectory:
23+
context.pluginWorkDirectoryURL
24+
))
25+
}
26+
return commands
27+
}
28+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
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)'")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
I am Bar!
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
I am Baz!
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// print("Generated string Bar: '\(bar)'")
2+
// print("Generated string Baz: '\(baz)'")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Foundation
2+
import MySourceGenBuildToolLib
3+
4+
// Sample source generator tool that emits a Swift variable declaration of a string containing the hex representation of the contents of a file as a quoted string. The variable name is the base name of the input file. The input file is the first argument and the output file is the second.
5+
if ProcessInfo.processInfo.arguments.count != 3 {
6+
print("usage: MySourceGenBuildTool <input> <output>")
7+
exit(1)
8+
}
9+
let inputFile = ProcessInfo.processInfo.arguments[1]
10+
let outputFile = ProcessInfo.processInfo.arguments[2]
11+
12+
let variableName = URL(fileURLWithPath: inputFile).deletingPathExtension().lastPathComponent
13+
14+
let inputData = FileManager.default.contents(atPath: inputFile) ?? Data()
15+
let dataAsHex = inputData.map { String(format: "%02hhx", $0) }.joined()
16+
let outputString = "public var \(variableName) = \(dataAsHex.quotedForSourceCode)\n"
17+
let outputData = outputString.data(using: .utf8)
18+
FileManager.default.createFile(atPath: outputFile, contents: outputData)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
3+
extension String {
4+
5+
public var quotedForSourceCode: String {
6+
return "\"" + self
7+
.replacingOccurrences(of: "\\", with: "\\\\")
8+
.replacingOccurrences(of: "\"", with: "\\\"")
9+
+ "\""
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public func GetLibraryName() -> String {
2+
return "MySourceGenRuntimeLib"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import XCTest
2+
import class Foundation.Bundle
3+
4+
final class SwiftyProtobufTests: XCTestCase {
5+
func testExample() throws {
6+
// This is an example of a functional test case.
7+
// Use XCTAssert and related functions to verify your tests produce the correct
8+
// results.
9+
10+
// Some of the APIs that we use below are available in macOS 10.13 and above.
11+
guard #available(macOS 10.13, *) else {
12+
return
13+
}
14+
15+
// Mac Catalyst won't have `Process`, but it is supported for executables.
16+
#if !targetEnvironment(macCatalyst)
17+
18+
let fooBinary = productsDirectory.appendingPathComponent("MySourceGenTool")
19+
20+
let process = Process()
21+
process.executableURL = fooBinary
22+
23+
let pipe = Pipe()
24+
process.standardOutput = pipe
25+
26+
try process.run()
27+
process.waitUntilExit()
28+
29+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
30+
let output = String(data: data, encoding: .utf8)
31+
32+
XCTAssertEqual(output, "Hello, world!\n")
33+
#endif
34+
}
35+
36+
/// Returns path to the built products directory.
37+
var productsDirectory: URL {
38+
#if os(macOS)
39+
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
40+
return bundle.bundleURL.deletingLastPathComponent()
41+
}
42+
fatalError("couldn't find the products directory")
43+
#else
44+
return Bundle.main.bundleURL
45+
#endif
46+
}
47+
}

Sources/Basics/SwiftVersion.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public struct SwiftVersion {
5858
extension SwiftVersion {
5959
/// The current version of the package manager.
6060
public static let current = SwiftVersion(
61-
version: (5, 10, 0),
61+
version: (5, 11, 0),
6262
isDevelopment: true,
6363
buildIdentifier: getBuildIdentifier()
6464
)

Sources/PackagePlugin/Command.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,82 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import Foundation
14+
1315
/// A command to run during the build, including executable, command lines,
1416
/// environment variables, initial working directory, etc. All paths should be
1517
/// based on the ones passed to the plugin in the target build context.
1618
public enum Command {
1719

20+
/// Returns a command that runs when any of its ouput files are needed by
21+
/// the build, but out-of-date.
22+
///
23+
/// An output file is out-of-date if it doesn't exist, or if any input files
24+
/// have changed since the command was last run.
25+
///
26+
/// - Note: the paths in the list of output files may depend on the list of
27+
/// input file paths, but **must not** depend on reading the contents of
28+
/// any input files. Such cases must be handled using a `prebuildCommand`.
29+
///
30+
/// - parameters:
31+
/// - displayName: An optional string to show in build logs and other
32+
/// status areas.
33+
/// - executable: The absolute path to the executable to be invoked.
34+
/// - arguments: Command-line arguments to be passed to the executable.
35+
/// - environment: Environment variable assignments visible to the
36+
/// executable.
37+
/// - inputFiles: Files on which the contents of output files may depend.
38+
/// Any paths passed as `arguments` should typically be passed here as
39+
/// well.
40+
/// - outputFiles: Files to be generated or updated by the executable.
41+
/// Any files recognizable by their extension as source files
42+
/// (e.g. `.swift`) are compiled into the target for which this command
43+
/// was generated as if in its source directory; other files are treated
44+
/// as resources as if explicitly listed in `Package.swift` using
45+
/// `.process(...)`.
46+
@available(_PackageDescription, introduced: 5.11)
47+
case buildCommand(
48+
displayName: String?,
49+
executable: URL,
50+
arguments: [String],
51+
environment: [String: String] = [:],
52+
inputFiles: [URL] = [],
53+
outputFiles: [URL] = []
54+
)
55+
56+
/// Returns a command that runs unconditionally before every build.
57+
///
58+
/// Prebuild commands can have a significant performance impact
59+
/// and should only be used when there would be no way to know the
60+
/// list of output file paths without first reading the contents
61+
/// of one or more input files. Typically there is no way to
62+
/// determine this list without first running the command, so
63+
/// instead of encoding that list, the caller supplies an
64+
/// `outputFilesDirectory` parameter, and all files in that
65+
/// directory after the command runs are treated as output files.
66+
///
67+
/// - parameters:
68+
/// - displayName: An optional string to show in build logs and other
69+
/// status areas.
70+
/// - executable: The absolute path to the executable to be invoked.
71+
/// - arguments: Command-line arguments to be passed to the executable.
72+
/// - environment: Environment variable assignments visible to the executable.
73+
/// - outputFilesDirectory: A directory into which the command writes its
74+
/// output files. Any files there recognizable by their extension as
75+
/// source files (e.g. `.swift`) are compiled into the target for which
76+
/// this command was generated as if in its source directory; other
77+
/// files are treated as resources as if explicitly listed in
78+
/// `Package.swift` using `.process(...)`.
79+
@available(_PackageDescription, introduced: 5.11)
80+
case prebuildCommand(
81+
displayName: String?,
82+
executable: URL,
83+
arguments: [String],
84+
environment: [String: String] = [:],
85+
outputFilesDirectory: URL
86+
)
87+
88+
@available(_PackageDescription, deprecated: 5.11)
1889
case _buildCommand(
1990
displayName: String?,
2091
executable: Path,
@@ -24,6 +95,7 @@ public enum Command {
2495
inputFiles: [Path] = [],
2596
outputFiles: [Path] = [])
2697

98+
@available(_PackageDescription, deprecated: 5.11)
2799
case _prebuildCommand(
28100
displayName: String?,
29101
executable: Path,
@@ -61,6 +133,7 @@ public extension Command {
61133
/// was generated as if in its source directory; other files are treated
62134
/// as resources as if explicitly listed in `Package.swift` using
63135
/// `.process(...)`.
136+
@available(_PackageDescription, deprecated: 5.11)
64137
static func buildCommand(
65138
displayName: String?,
66139
executable: Path,
@@ -150,6 +223,7 @@ public extension Command {
150223
/// this command was generated as if in its source directory; other
151224
/// files are treated as resources as if explicitly listed in
152225
/// `Package.swift` using `.process(...)`.
226+
@available(_PackageDescription, deprecated: 5.11)
153227
static func prebuildCommand(
154228
displayName: String?,
155229
executable: Path,

0 commit comments

Comments
 (0)