Skip to content
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
15 changes: 11 additions & 4 deletions Fixtures/Miscellaneous/Plugins/PluginsAndSnippets/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ let package = Package(
name: "PluginScriptProduct",
targets: [
"PluginScriptTarget"
]
],
),
.library(
name: "MyLib",
targets: [
"MyLib",
],
),
],
targets: [
Expand All @@ -17,9 +23,10 @@ let package = Package(
capability: .command(
intent: .custom(
verb: "do-something",
description: "Do something"
)
)
description: "Do something",
),
),
),
.target(name: "MyLib"),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book

@main
struct foo {
static func main() {
print("hello, snippets. File: \(#file)")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import MyLib

libraryCall()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("hello, snippets. File: \(#file)")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("hello, snippets! File: \(#file)")
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public func libraryCall() {
print("From library")
print("hello, snippets. File: \(#file)")
}
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ let package = Package(
.target(
name: "SwiftBuildSupport",
dependencies: [
"Build",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just taking a quick look, and I see it's still in draft. The "Build" target is the native build system. We shouldn't have SwiftBuildSupport depending on it since it'll be removed eventually. Common things should go in SPMBuildCore.

"SPMBuildCore",
"PackageGraph",
],
Expand Down
16 changes: 14 additions & 2 deletions Sources/Basics/FileSystem/InMemoryFileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,12 +226,24 @@ public final class InMemoryFileSystem: FileSystem {
}

/// Virtualized current working directory.
private var _currentWorkingDirectory: TSCBasic.AbsolutePath = try! .init(validating: "/")

public var currentWorkingDirectory: TSCBasic.AbsolutePath? {
return try? .init(validating: "/")
return _currentWorkingDirectory
}

public func changeCurrentWorkingDirectory(to path: TSCBasic.AbsolutePath) throws {
throw FileSystemError(.unsupported, path)
return try lock.withLock {
// Verify the path exists and is a directory
guard let node = try getNode(path) else {
throw FileSystemError(.noEntry, path)
}

guard case .directory = node.contents else {
throw FileSystemError(.notDirectory, path)
}
_currentWorkingDirectory = path
}
}

public var homeDirectory: TSCBasic.AbsolutePath {
Expand Down
58 changes: 29 additions & 29 deletions Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,34 @@ import struct TSCBasic.ByteString
@available(*, deprecated, renamed: "SwiftModuleBuildDescription")
public typealias SwiftTargetBuildDescription = SwiftModuleBuildDescription

// looking into the file content to see if it is using the @main annotation
// this is not bullet-proof since theoretically the file can contain the @main string for other reasons
// but it is the closest to accurate we can do at this point
package func containsAtMain(fileSystem: FileSystem, path: AbsolutePath) throws -> Bool {
let content: String = try fileSystem.readFileContents(path)
let lines = content.split(whereSeparator: { $0.isNewline }).map { $0.trimmingCharacters(in: .whitespaces) }

var multilineComment = false
for line in lines {
if line.hasPrefix("//") {
continue
}
if line.hasPrefix("/*") {
multilineComment = true
}
if line.hasSuffix("*/") {
multilineComment = false
}
if multilineComment {
continue
}
if line.hasPrefix("@main") {
return true
}
}
return false
}

/// Build description for a Swift module.
public final class SwiftModuleBuildDescription {
/// The package this target belongs to.
Expand Down Expand Up @@ -216,40 +244,12 @@ public final class SwiftModuleBuildDescription {
return false
}
// looking into the file content to see if it is using the @main annotation which requires parse-as-library
return (try? self.containsAtMain(fileSystem: self.fileSystem, path: self.sources[0])) ?? false
return (try? containsAtMain(fileSystem: self.fileSystem, path: self.sources[0])) ?? false
default:
return false
}
}

// looking into the file content to see if it is using the @main annotation
// this is not bullet-proof since theoretically the file can contain the @main string for other reasons
// but it is the closest to accurate we can do at this point
func containsAtMain(fileSystem: FileSystem, path: AbsolutePath) throws -> Bool {
let content: String = try self.fileSystem.readFileContents(path)
let lines = content.split(whereSeparator: { $0.isNewline }).map { $0.trimmingCharacters(in: .whitespaces) }

var multilineComment = false
for line in lines {
if line.hasPrefix("//") {
continue
}
if line.hasPrefix("/*") {
multilineComment = true
}
if line.hasSuffix("*/") {
multilineComment = false
}
if multilineComment {
continue
}
if line.hasPrefix("@main") {
return true
}
}
return false
}

/// The filesystem to operate on.
let fileSystem: FileSystem

Expand Down
6 changes: 3 additions & 3 deletions Sources/Commands/PackageCommands/CompletionCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,18 @@ extension SwiftPackageCommand {
case .listExecutables:
let graph = try await swiftCommandState.loadPackageGraph()
let package = graph.rootPackages[graph.rootPackages.startIndex].underlying
let executables = package.products.filter { $0.type == .executable }
let executables = package.products.filter { $0.type == .executable }.sorted()
for executable in executables {
print(executable.name)
}
case .listSnippets:
let graph = try await swiftCommandState.loadPackageGraph()
let package = graph.rootPackages[graph.rootPackages.startIndex].underlying
let executables = package.modules.filter { $0.type == .snippet }
let executables = package.modules.filter { $0.type == .snippet }.sorted()
for executable in executables {
print(executable.name)
}
}
}
}
}
}
6 changes: 6 additions & 0 deletions Sources/PackageModel/Module/Module.swift
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,12 @@ extension Module: Hashable {
}
}

extension Module: Comparable {
public static func < (lhs: Module, rhs: Module) -> Bool {
return lhs.name < rhs.name
}
}

extension Module: CustomStringConvertible {
public var description: String {
return "<\(Swift.type(of: self)): \(name)>"
Expand Down
6 changes: 6 additions & 0 deletions Sources/PackageModel/Product.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ extension Product: Hashable {
}
}

extension Product: Comparable {
public static func < (lhs: Product, rhs: Product) -> Bool {
lhs.name < rhs.name
}
}

/// The type of product.
public enum ProductType: Equatable, Hashable, Sendable {

Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftBuildSupport/PackagePIFBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -451,13 +451,13 @@ public final class PackagePIFBuilder {
try projectBuilder.makeLibraryProduct(product, type: libraryType)
}

case .executable, .test:
case .executable, .test, .snippet:
try projectBuilder.makeMainModuleProduct(product)

case .plugin:
try projectBuilder.makePluginProduct(product)

case .snippet, .macro:
case .macro:
break // TODO: Double-check what's going on here as we skip snippet modules too (rdar://147705448)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import TSCUtility
import struct Basics.AbsolutePath
import class Basics.ObservabilitySystem
import struct Basics.SourceControlURL
import func Build.containsAtMain

import class PackageModel.BinaryModule
import class PackageModel.Manifest
Expand Down Expand Up @@ -55,7 +56,7 @@ extension PackagePIFProjectBuilder {
let synthesizedResourceGeneratingPluginInvocationResults: [PackagePIFBuilder.BuildToolPluginInvocationResult] =
[]

if product.type == .executable {
if [.executable, .snippet].contains(product.type) {
if let customPIFProductType = pifBuilder.delegate.customProductType(forExecutable: product.underlying) {
pifProductType = customPIFProductType
moduleOrProductType = PackagePIFBuilder.ModuleOrProductType(from: customPIFProductType)
Expand Down Expand Up @@ -138,6 +139,17 @@ extension PackagePIFProjectBuilder {
settings[.INSTALL_PATH] = "/usr/local/bin"
settings[.LD_RUNPATH_SEARCH_PATHS] = ["$(inherited)", "@executable_path/../lib"]
}
} else if mainModule.type == .snippet {
let hasMainModule: Bool
if let mainModule = product.mainModule {
// Check if any source file in the main module contains @main
hasMainModule = mainModule.sources.paths.contains { (sourcePath: AbsolutePath) in
(try? containsAtMain(fileSystem: pifBuilder.fileSystem, path: sourcePath)) ?? false
}
} else {
hasMainModule = false
}
settings[.SWIFT_DISABLE_PARSE_AS_LIBRARY] = hasMainModule ? "NO" : "YES"
}

let mainTargetDeploymentTargets = mainModule.deploymentTargets(using: pifBuilder.delegate)
Expand Down
118 changes: 118 additions & 0 deletions Sources/_InternalTestSupport/FileSystemHelpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import Foundation
import Basics

func getFiles(atPath path: String, matchingExtension fileExtension: String) -> [URL] {
let fileManager = FileManager.default
var matchingFiles: [URL] = []

guard
let enumerator = fileManager.enumerator(
at: URL(fileURLWithPath: path),
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles, .skipsPackageDescendants]
)
else {
print("Error: Could not create enumerator for path: \(path)")
return []
}

for case let fileURL as URL in enumerator {
do {
let resourceValues = try fileURL.resourceValues(forKeys: [.isRegularFileKey])
if let isRegularFile = resourceValues.isRegularFile, isRegularFile {
if fileURL.pathExtension.lowercased() == fileExtension.lowercased() {
matchingFiles.append(fileURL)
}
}
} catch {
print("Error retrieving resource values for \(fileURL.lastPathComponent): \(error.localizedDescription)")
}
}
return matchingFiles
}

/// Returns all files that match the given extension in the specified directory.
///
/// - Parameters:
/// - directory: The directory to search in (AbsolutePath)
/// - extension: The file extension to match (without the leading dot)
/// - recursive: Whether to search subdirectories recursively (default: true)
/// - fileSystem: The file system to use for operations (defaults to localFileSystem)
/// - Returns: An array of AbsolutePath objects
/// - Throws: FileSystemError if the directory cannot be accessed or enumerated
public func getFiles(
in directory: AbsolutePath,
matchingExtension extension: String,
recursive: Bool = true,
fileSystem: FileSystem = localFileSystem
) throws -> [AbsolutePath] {
var matchingFiles: [AbsolutePath] = []
let normalizedExtension = `extension`.lowercased()

guard fileSystem.exists(directory) else {
throw StringError("Directory does not exist: \(directory)")
}

guard fileSystem.isDirectory(directory) else {
throw StringError("Path is not a directory: \(directory)")
}

if recursive {
try fileSystem.enumerate(directory: directory) { filePath in
if fileSystem.isFile(filePath) {
if let fileExtension = filePath.extension?.lowercased(),
fileExtension == normalizedExtension {
matchingFiles.append(filePath)
}
}
}
} else {
// Non-recursive: only check direct children
let contents = try fileSystem.getDirectoryContents(directory)
for item in contents {
let itemPath = directory.appending(component: item)
if fileSystem.isFile(itemPath) {
if let fileExtension = itemPath.extension?.lowercased(),
fileExtension == normalizedExtension {
matchingFiles.append(itemPath)
}
}
}
}

return matchingFiles
}

/// Returns all files that match the given extension in the specified directory.
///
/// - Parameters:
/// - directory: The directory to search in (RelativePath)
/// - extension: The file extension to match (without the leading dot)
/// - recursive: Whether to search subdirectories recursively (default: true)
/// - fileSystem: The file system to use for operations (defaults to localFileSystem)
/// - Returns: An array of RelativePath objects
/// - Throws: FileSystemError if the directory cannot be accessed or enumerated
public func getFiles(
in directory: RelativePath,
matchingExtension extension: String,
recursive: Bool = true,
fileSystem: FileSystem = localFileSystem
) throws -> [RelativePath] {
// Convert RelativePath to AbsolutePath for enumeration
guard let currentWorkingDirectory = fileSystem.currentWorkingDirectory else {
throw StringError("Cannot determine current working directory")
}

let absoluteDirectory = currentWorkingDirectory.appending(directory)
let absoluteResults = try getFiles(
in: absoluteDirectory,
matchingExtension: `extension`,
recursive: recursive,
fileSystem: fileSystem
)

// Convert results back to RelativePath
return absoluteResults.map { absolutePath in
absolutePath.relative(to: currentWorkingDirectory)
}
}
1 change: 1 addition & 0 deletions Sources/_InternalTestSupport/SwiftTesting+Tags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ extension Tag.Feature {
@Tag public static var NetRc: Tag
@Tag public static var Resource: Tag
@Tag public static var SpecialCharacters: Tag
@Tag public static var Snippets: Tag
@Tag public static var Traits: Tag

}
Expand Down
Loading