Skip to content

Commit 02f2f9f

Browse files
committed
Add Snippet support with SwiftBuild
Snippets are treated as executable targets with the native build system. This change updates the PIF Builder to support snippet, giving Snippet support with the Swift Build build system. Depends on: swiftlang/swift-build#775 Fixes: swiftlang#9040 issue: rdar://158630024 issue: rdar://147705448
1 parent a834937 commit 02f2f9f

File tree

14 files changed

+1653
-503
lines changed

14 files changed

+1653
-503
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// The Swift Programming Language
2+
// https://docs.swift.org/swift-book
3+
4+
@main
5+
struct foo {
6+
static func main() {
7+
print("hello, snippets. File: \(#file)")
8+
}
9+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("hello, snippets. File: \(#file)")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("hello, snippets! File: \(#file)")

Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,34 @@ import struct TSCBasic.ByteString
3434
@available(*, deprecated, renamed: "SwiftModuleBuildDescription")
3535
public typealias SwiftTargetBuildDescription = SwiftModuleBuildDescription
3636

37+
// looking into the file content to see if it is using the @main annotation
38+
// this is not bullet-proof since theoretically the file can contain the @main string for other reasons
39+
// but it is the closest to accurate we can do at this point
40+
package func containsAtMain(fileSystem: FileSystem, path: AbsolutePath) throws -> Bool {
41+
let content: String = try fileSystem.readFileContents(path)
42+
let lines = content.split(whereSeparator: { $0.isNewline }).map { $0.trimmingCharacters(in: .whitespaces) }
43+
44+
var multilineComment = false
45+
for line in lines {
46+
if line.hasPrefix("//") {
47+
continue
48+
}
49+
if line.hasPrefix("/*") {
50+
multilineComment = true
51+
}
52+
if line.hasSuffix("*/") {
53+
multilineComment = false
54+
}
55+
if multilineComment {
56+
continue
57+
}
58+
if line.hasPrefix("@main") {
59+
return true
60+
}
61+
}
62+
return false
63+
}
64+
3765
/// Build description for a Swift module.
3866
public final class SwiftModuleBuildDescription {
3967
/// The package this target belongs to.
@@ -216,40 +244,12 @@ public final class SwiftModuleBuildDescription {
216244
return false
217245
}
218246
// looking into the file content to see if it is using the @main annotation which requires parse-as-library
219-
return (try? self.containsAtMain(fileSystem: self.fileSystem, path: self.sources[0])) ?? false
247+
return (try? containsAtMain(fileSystem: self.fileSystem, path: self.sources[0])) ?? false
220248
default:
221249
return false
222250
}
223251
}
224252

225-
// looking into the file content to see if it is using the @main annotation
226-
// this is not bullet-proof since theoretically the file can contain the @main string for other reasons
227-
// but it is the closest to accurate we can do at this point
228-
func containsAtMain(fileSystem: FileSystem, path: AbsolutePath) throws -> Bool {
229-
let content: String = try self.fileSystem.readFileContents(path)
230-
let lines = content.split(whereSeparator: { $0.isNewline }).map { $0.trimmingCharacters(in: .whitespaces) }
231-
232-
var multilineComment = false
233-
for line in lines {
234-
if line.hasPrefix("//") {
235-
continue
236-
}
237-
if line.hasPrefix("/*") {
238-
multilineComment = true
239-
}
240-
if line.hasSuffix("*/") {
241-
multilineComment = false
242-
}
243-
if multilineComment {
244-
continue
245-
}
246-
if line.hasPrefix("@main") {
247-
return true
248-
}
249-
}
250-
return false
251-
}
252-
253253
/// The filesystem to operate on.
254254
let fileSystem: FileSystem
255255

Sources/SwiftBuildSupport/PackagePIFBuilder.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -451,13 +451,13 @@ public final class PackagePIFBuilder {
451451
try projectBuilder.makeLibraryProduct(product, type: libraryType)
452452
}
453453

454-
case .executable, .test:
454+
case .executable, .test, .snippet:
455455
try projectBuilder.makeMainModuleProduct(product)
456456

457457
case .plugin:
458458
try projectBuilder.makePluginProduct(product)
459459

460-
case .snippet, .macro:
460+
case .macro:
461461
break // TODO: Double-check what's going on here as we skip snippet modules too (rdar://147705448)
462462
}
463463
}

Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Products.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import TSCUtility
1717
import struct Basics.AbsolutePath
1818
import class Basics.ObservabilitySystem
1919
import struct Basics.SourceControlURL
20+
import func Build.containsAtMain
2021

2122
import class PackageModel.BinaryModule
2223
import class PackageModel.Manifest
@@ -55,7 +56,7 @@ extension PackagePIFProjectBuilder {
5556
let synthesizedResourceGeneratingPluginInvocationResults: [PackagePIFBuilder.BuildToolPluginInvocationResult] =
5657
[]
5758

58-
if product.type == .executable {
59+
if [.executable, .snippet].contains(product.type) {
5960
if let customPIFProductType = pifBuilder.delegate.customProductType(forExecutable: product.underlying) {
6061
pifProductType = customPIFProductType
6162
moduleOrProductType = PackagePIFBuilder.ModuleOrProductType(from: customPIFProductType)
@@ -138,6 +139,17 @@ extension PackagePIFProjectBuilder {
138139
settings[.INSTALL_PATH] = "/usr/local/bin"
139140
settings[.LD_RUNPATH_SEARCH_PATHS] = ["$(inherited)", "@executable_path/../lib"]
140141
}
142+
} else if mainModule.type == .snippet {
143+
let hasMainModule: Bool
144+
if let mainModule = product.mainModule {
145+
// Check if any source file in the main module contains @main
146+
hasMainModule = mainModule.sources.paths.contains { (sourcePath: AbsolutePath) in
147+
(try? containsAtMain(fileSystem: pifBuilder.fileSystem, path: sourcePath)) ?? false
148+
}
149+
} else {
150+
hasMainModule = false
151+
}
152+
settings[.SWIFT_DISABLE_PARSE_AS_LIBRARY] = hasMainModule ? "NO" : "YES"
141153
}
142154

143155
let mainTargetDeploymentTargets = mainModule.deploymentTargets(using: pifBuilder.delegate)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import Foundation
2+
import Basics
3+
4+
func getFiles(atPath path: String, matchingExtension fileExtension: String) -> [URL] {
5+
let fileManager = FileManager.default
6+
var matchingFiles: [URL] = []
7+
8+
guard
9+
let enumerator = fileManager.enumerator(
10+
at: URL(fileURLWithPath: path),
11+
includingPropertiesForKeys: [.isRegularFileKey],
12+
options: [.skipsHiddenFiles, .skipsPackageDescendants]
13+
)
14+
else {
15+
print("Error: Could not create enumerator for path: \(path)")
16+
return []
17+
}
18+
19+
for case let fileURL as URL in enumerator {
20+
do {
21+
let resourceValues = try fileURL.resourceValues(forKeys: [.isRegularFileKey])
22+
if let isRegularFile = resourceValues.isRegularFile, isRegularFile {
23+
if fileURL.pathExtension.lowercased() == fileExtension.lowercased() {
24+
matchingFiles.append(fileURL)
25+
}
26+
}
27+
} catch {
28+
print("Error retrieving resource values for \(fileURL.lastPathComponent): \(error.localizedDescription)")
29+
}
30+
}
31+
return matchingFiles
32+
}
33+
34+
/// Returns all files that match the given extension in the specified directory.
35+
///
36+
/// - Parameters:
37+
/// - directory: The directory to search in (AbsolutePath)
38+
/// - extension: The file extension to match (without the leading dot)
39+
/// - recursive: Whether to search subdirectories recursively (default: true)
40+
/// - fileSystem: The file system to use for operations (defaults to localFileSystem)
41+
/// - Returns: An array of AbsolutePath objects
42+
/// - Throws: FileSystemError if the directory cannot be accessed or enumerated
43+
public func getFiles(
44+
in directory: AbsolutePath,
45+
matchingExtension extension: String,
46+
recursive: Bool = true,
47+
fileSystem: FileSystem = localFileSystem
48+
) throws -> [AbsolutePath] {
49+
var matchingFiles: [AbsolutePath] = []
50+
let normalizedExtension = `extension`.lowercased()
51+
52+
guard fileSystem.exists(directory) else {
53+
throw StringError("Directory does not exist: \(directory)")
54+
}
55+
56+
guard fileSystem.isDirectory(directory) else {
57+
throw StringError("Path is not a directory: \(directory)")
58+
}
59+
60+
if recursive {
61+
try fileSystem.enumerate(directory: directory) { filePath in
62+
if fileSystem.isFile(filePath) {
63+
if let fileExtension = filePath.extension?.lowercased(),
64+
fileExtension == normalizedExtension {
65+
matchingFiles.append(filePath)
66+
}
67+
}
68+
}
69+
} else {
70+
// Non-recursive: only check direct children
71+
let contents = try fileSystem.getDirectoryContents(directory)
72+
for item in contents {
73+
let itemPath = directory.appending(component: item)
74+
if fileSystem.isFile(itemPath) {
75+
if let fileExtension = itemPath.extension?.lowercased(),
76+
fileExtension == normalizedExtension {
77+
matchingFiles.append(itemPath)
78+
}
79+
}
80+
}
81+
}
82+
83+
return matchingFiles
84+
}
85+
86+
/// Returns all files that match the given extension in the specified directory.
87+
///
88+
/// - Parameters:
89+
/// - directory: The directory to search in (RelativePath)
90+
/// - extension: The file extension to match (without the leading dot)
91+
/// - recursive: Whether to search subdirectories recursively (default: true)
92+
/// - fileSystem: The file system to use for operations (defaults to localFileSystem)
93+
/// - Returns: An array of RelativePath objects
94+
/// - Throws: FileSystemError if the directory cannot be accessed or enumerated
95+
public func getFiles(
96+
in directory: RelativePath,
97+
matchingExtension extension: String,
98+
recursive: Bool = true,
99+
fileSystem: FileSystem = localFileSystem
100+
) throws -> [RelativePath] {
101+
// Convert RelativePath to AbsolutePath for enumeration
102+
guard let currentWorkingDirectory = fileSystem.currentWorkingDirectory else {
103+
throw StringError("Cannot determine current working directory")
104+
}
105+
106+
let absoluteDirectory = currentWorkingDirectory.appending(directory)
107+
let absoluteResults = try getFiles(
108+
in: absoluteDirectory,
109+
matchingExtension: `extension`,
110+
recursive: recursive,
111+
fileSystem: fileSystem
112+
)
113+
114+
// Convert results back to RelativePath
115+
return absoluteResults.map { absolutePath in
116+
absolutePath.relative(to: currentWorkingDirectory)
117+
}
118+
}

Sources/_InternalTestSupport/SwiftTesting+Tags.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ extension Tag.Feature {
3333
@Tag public static var NetRc: Tag
3434
@Tag public static var Resource: Tag
3535
@Tag public static var SpecialCharacters: Tag
36+
@Tag public static var Snippets: Tag
3637
@Tag public static var Traits: Tag
3738

3839
}

0 commit comments

Comments
 (0)