Skip to content

Commit 1ec5919

Browse files
authored
Support using a Swift file as a build tool plugin executable (#8807)
A Swift file can be executed directly in the shell. Typically the file begins with a `#/usr/bin/swift` or an equivalent `xcrun` incantation, and then simply running `swift MyFile.swift` will automatically compile and execute it. Build tool plugins often leverage Swift, but to do so they add two targets, a `.plugin` and an `.executableTarget`. The executable target builds a binary that the plugin tool then calls during a build. Often this is more heavyweight than needed. By passing the path to a Swift file to the `executable` argument of the `.buildCommand` returned from your build tool plugin, you could bypass the need to create a separate executable target and just execute the file directly. However, because SwiftPM executes build tool plugins within a sandbox the sandbox script needs to be ammended with the clang module cache temporary directory so that the swift compiler can write the modules it needs to execute the file. Ammend the sandbox script to add write permissions to the root tmp directory that LLVM uses to write clang modules. With this addition you can now use a .swift file as a build tool plugin executable. rdar://152874736
1 parent ffd78a7 commit 1ec5919

File tree

7 files changed

+75
-10
lines changed

7 files changed

+75
-10
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// swift-tools-version: 6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "SwiftFilePluginFixture",
7+
products: [
8+
.library(
9+
name: "SwiftFilePluginFixture",
10+
targets: ["SwiftFilePluginFixture"]
11+
),
12+
],
13+
targets: [
14+
.target(
15+
name: "SwiftFilePluginFixture",
16+
plugins: [
17+
.plugin(name: "MyCustomBuildTool")
18+
]
19+
),
20+
.plugin(
21+
name: "MyCustomBuildTool",
22+
capability: .buildTool()
23+
)
24+
]
25+
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Foundation
2+
import PackagePlugin
3+
4+
@main
5+
struct SwiftToolsBuildPlugin: BuildToolPlugin {
6+
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
7+
let formatExecutable = context.package.directoryURL.appending(components: "SimpleSwiftScript.swift")
8+
return [.buildCommand(
9+
displayName: "Run a swift script",
10+
executable: formatExecutable,
11+
arguments: [],
12+
inputFiles: [formatExecutable],
13+
outputFiles: []
14+
)]
15+
}
16+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env swift
2+
3+
print("Hello, Build Tool Plugin!")

Fixtures/Miscellaneous/Plugins/SwiftFilePlugin/Sources/SwiftFilePluginFixture/SwiftFilePluginFixture.swift

Whitespace-only changes.

Sources/Basics/Sandbox.swift

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,16 @@ fileprivate func macOSSandboxProfile(
188188
}
189189
// Optionally allow writing to temporary directories (a lot of use of Foundation requires this).
190190
else if strictness == .writableTemporaryDirectory {
191-
// Add `subpath` expressions for the regular and the Foundation temporary directories.
192-
for tmpDir in ["/tmp", NSTemporaryDirectory()] {
193-
writableDirectoriesExpression += try [
194-
"(subpath \(resolveSymlinks(AbsolutePath(validating: tmpDir)).quotedAsSubpathForSandboxProfile))",
195-
]
191+
var stableCacheDirectories: [AbsolutePath] = []
192+
// Add `subpath` expressions for the regular, Foundation and clang module cache temporary directories.
193+
for tmpDir in (["/tmp"] + threadSafeDarwinCacheDirectories.map(\.pathString)) {
194+
let resolved = try resolveSymlinks(AbsolutePath(validating: tmpDir))
195+
if !stableCacheDirectories.contains(where: { $0.isAncestorOfOrEqual(to: resolved) }) {
196+
stableCacheDirectories.append(resolved)
197+
writableDirectoriesExpression += [
198+
"(subpath \(resolved.quotedAsSubpathForSandboxProfile))",
199+
]
200+
}
196201
}
197202
}
198203

@@ -217,16 +222,17 @@ fileprivate func macOSSandboxProfile(
217222
// Emit rules for paths under which writing is allowed, even if they are descendants directories that are otherwise read-only.
218223
if writableDirectories.count > 0 {
219224
contents += "(allow file-write*\n"
225+
var stableItemReplacementDirectories: [AbsolutePath] = []
220226
for path in writableDirectories {
221227
contents += " (subpath \(try resolveSymlinks(path).quotedAsSubpathForSandboxProfile))\n"
222228

223229
// `itemReplacementDirectories` may return a combination of stable directory paths, and subdirectories which are unique on every call. Avoid including unnecessary subdirectories in the Sandbox profile which may lead to nondeterminism in its construction.
224230
if let itemReplacementDirectories = try? fileSystem.itemReplacementDirectories(for: path).sorted(by: { $0.pathString.count < $1.pathString.count }) {
225-
var stableItemReplacementDirectories: [AbsolutePath] = []
226231
for directory in itemReplacementDirectories {
227-
if !stableItemReplacementDirectories.contains(where: { $0.isAncestorOfOrEqual(to: directory) }) {
228-
stableItemReplacementDirectories.append(directory)
229-
contents += " (subpath \(try resolveSymlinks(directory).quotedAsSubpathForSandboxProfile))\n"
232+
let resolved = try resolveSymlinks(directory)
233+
if !stableItemReplacementDirectories.contains(where: { $0.isAncestorOfOrEqual(to: resolved) }) {
234+
stableItemReplacementDirectories.append(resolved)
235+
contents += " (subpath \(resolved.quotedAsSubpathForSandboxProfile))\n"
230236
}
231237
}
232238
}

Sources/SwiftBuildSupport/PIFBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ public final class PIFBuilder {
381381
self.parameters.disableSandbox ?
382382
nil :
383383
.init(
384-
strictness: .default,
384+
strictness: .writableTemporaryDirectory,
385385
writableDirectories: writableDirectories,
386386
readOnlyDirectories: buildCommand.inputFiles
387387
)

Tests/FunctionalTests/PluginTests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,21 @@ final class PluginTests: XCTestCase {
12061206
}
12071207
}
12081208

1209+
#if os(macOS)
1210+
func testBuildToolPluginSwiftFileExecutable() async throws {
1211+
for buildSystem in ["native", "swiftbuild"] {
1212+
try await fixture(name: "Miscellaneous/Plugins") { fixturePath in
1213+
let (stdout, stderr) = try await executeSwiftBuild(fixturePath.appending("SwiftFilePlugin"), configuration: .Debug, extraArgs: ["--build-system", buildSystem, "--verbose"])
1214+
if buildSystem == "native" {
1215+
XCTAssertTrue(stdout.contains("Hello, Build Tool Plugin!"), "stdout:\n\(stdout)")
1216+
} else {
1217+
XCTAssertTrue(stderr.contains("Hello, Build Tool Plugin!"), "stderr:\n\(stderr)")
1218+
}
1219+
}
1220+
}
1221+
}
1222+
#endif
1223+
12091224
func testTransitivePluginOnlyDependency() async throws {
12101225
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
12111226
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")

0 commit comments

Comments
 (0)