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
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,11 @@ public final class SwiftModuleBuildDescription {
fileSystem: FileSystem,
observabilityScope: ObservabilityScope
) throws {
// It's an error to contain mixed language source files.
if target.sources.containsMixedLanguage {
throw StringError("\(target.name): mixed language source files in Swift targets are not supported by the native build system.")
}

guard let swiftTarget = target.underlying as? SwiftModule else {
throw InternalError("underlying target type mismatch \(target)")
}
Expand Down Expand Up @@ -318,9 +323,9 @@ public final class SwiftModuleBuildDescription {
)
self.pluginDerivedResources = pluginGeneratedFiles.resources.values.map(\.self)

let nonSwiftSources = pluginDerivedSources.relativePaths.filter({ $0.extension != "swift" })
if !nonSwiftSources.isEmpty {
for source in nonSwiftSources {
let nonSwiftDerivedSources = pluginDerivedSources.relativePaths.filter({ $0.extension != "swift" })
if !nonSwiftDerivedSources.isEmpty {
for source in nonSwiftDerivedSources {
let absPath = pluginDerivedSources.root.appending(source)
observabilityScope.emit(warning: "Only Swift is supported for generated plugin source files: \(absPath)")
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/PackageLoading/PackageBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1833,15 +1833,15 @@ extension Manifest {
}

extension Sources {
var hasSwiftSources: Bool {
public var hasSwiftSources: Bool {
paths.contains { path in
guard let ext = path.extension else { return false }

return FileRuleDescription.swift.fileTypes.contains(ext)
}
}

var hasClangSources: Bool {
public var hasClangSources: Bool {
let supportedClangFileExtensions = FileRuleDescription.clang.fileTypes.union(FileRuleDescription.asm.fileTypes)

return paths.contains { path in
Expand All @@ -1851,7 +1851,7 @@ extension Sources {
}
}

var containsMixedLanguage: Bool {
public var containsMixedLanguage: Bool {
self.hasSwiftSources && self.hasClangSources
}

Expand Down
5 changes: 3 additions & 2 deletions Sources/PackageLoading/TargetSourcesBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,9 @@ public struct TargetSourcesBuilder {
try diagnoseInfoPlistConflicts(in: resources)
diagnoseInvalidResource(in: target.resources)

// It's an error to contain mixed language source files.
if sources.containsMixedLanguage {
// It's an error to contain mixed language source files
// Unless experimental flag is turned on
if sources.containsMixedLanguage && !toolsVersion.experimentalMultiLang {
throw Module.Error.mixedSources(targetPath)
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/PackageLoading/ToolsVersionParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -358,12 +358,12 @@ public struct ToolsVersionParser {
/// - Note: For a misspelt Swift tools version specification `"// swift-too1s-version:5.3"`, the first `"1"` is considered as the first character of the version specifier, and so `"1s-version:5.3"` is taken as the version specifier.
let versionSpecifier = specificationSnippetFromLabelToLineTerminator[startIndexOfVersionSpecifier..<indexOfVersionSpecifierTerminator]

/// Look for experimental features. They are space separated values contained in parenthases right after the ";", e.g. "// swift-tools-version: 6.3;(experimentalCGen)"
/// Look for experimental features. They are comma separated values contained in parenthases right after the ";", e.g. "// swift-tools-version: 6.4;(experimentalCGen,experimentalMultiLang)"
var experimentalFeatures: Set<ToolsVersion.ExperimentalFeature> = []
if indexOfVersionSpecifierTerminator < specificationWithIgnoredTrailingContents.endIndex, specificationWithIgnoredTrailingContents[indexOfVersionSpecifierTerminator...].hasPrefix(";(") {
let startOfExperimentalFeatures = specificationWithIgnoredTrailingContents.index(indexOfVersionSpecifierTerminator, offsetBy: 2)
if let endOfExperimentalFeatures = specificationWithIgnoredTrailingContents[startOfExperimentalFeatures...].firstIndex(where: { $0 == ")" }) {
for featureString in specificationWithIgnoredTrailingContents[startOfExperimentalFeatures..<endOfExperimentalFeatures].split(separator: " ", omittingEmptySubsequences: true) {
for featureString in specificationWithIgnoredTrailingContents[startOfExperimentalFeatures..<endOfExperimentalFeatures].split(separator: ",", omittingEmptySubsequences: true) {
if let feature = ToolsVersion.ExperimentalFeature(rawValue: String(featureString)) {
experimentalFeatures.insert(feature)
}
Expand Down
5 changes: 5 additions & 0 deletions Sources/PackageModel/ToolsVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public struct ToolsVersion: Equatable, Hashable, Codable, Sendable {
/// Experimental features
public enum ExperimentalFeature: String, Sendable, Codable {
case experimentalCGen
case experimentalMultiLang
}
public let experimentalFeatures: Set<ExperimentalFeature>?

Expand All @@ -104,6 +105,10 @@ public struct ToolsVersion: Equatable, Hashable, Codable, Sendable {
self >= .v6_3 && experimentalFeatures?.contains(.experimentalCGen) == true
}

public var experimentalMultiLang: Bool {
self >= .v6_4 && experimentalFeatures?.contains(.experimentalMultiLang) == true
}

/// Create an instance of tools version from a given string.
public init?(string: String, experimentalFeatures: Set<ExperimentalFeature>) {
guard let match = ToolsVersion.toolsVersionRegex.firstMatch(
Expand Down
57 changes: 57 additions & 0 deletions Tests/BuildTests/NativeBuildPlanTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Basics
@_spi(DontAdoptOutsideOfSwiftPMExposedForBenchmarksAndTestsOnly)
import PackageGraph
import PackageLoading
import PackageModel
import SPMBuildCore
import _InternalBuildTestSupport
import _InternalTestSupport
import Testing

/// Test suite for Native only tests.
/// TODO: Almost all the BuildPlanTests are native only and should be cleaned up.
struct NativeBuildPlanTests {
// Test that native build still fails for a multi-lang target when flag turned on
@Test func testMixedSource() async throws {
let fs = InMemoryFileSystem(
emptyFiles:
"/Pkg/Sources/lib/file1.swift",
"/Pkg/Sources/lib/file2.c"
)
let observability = ObservabilitySystem.makeForTesting()
let graph = try loadModulesGraph(
fileSystem: fs,
manifests: [
Manifest.createRootManifest(
displayName: "Pkg",
path: "/Pkg",
toolsVersion: #require(ToolsVersion(string: "6.4.0", experimentalFeatures: [.experimentalMultiLang])),
targets: [
TargetDescription(name: "lib"),
]
),
],
observabilityScope: observability.topScope
)
#expect(observability.diagnostics.isEmpty)

do {
_ = try await mockBuildPlan(graph: graph, fileSystem: fs, observabilityScope: observability.topScope)
Issue.record("Should have raised an error")
} catch {
#expect(error.localizedDescription == "lib: mixed language source files in Swift targets are not supported by the native build system.")
}
}
}
38 changes: 35 additions & 3 deletions Tests/PackageLoadingTests/PackageBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,50 @@ struct PackageBuilderTests {
let foo: AbsolutePath = "/Sources/foo"

let fs = InMemoryFileSystem(emptyFiles:
foo.appending(components: "main.swift").pathString,
foo.appending(components: "main.c").pathString
foo.appending(components: "Foo.swift").pathString,
foo.appending(components: "CFoo.c").pathString
)

let manifest = Manifest.createRootManifest(
displayName: "pkg",
path: .root,
toolsVersion: try #require(ToolsVersion(string: "6.4.0", experimentalFeatures: [.experimentalMultiLang])),
targets: [
try TargetDescription(name: "foo"),
]
)
try PackageBuilderTester(manifest, in: fs) { _, diagnostics in
try PackageBuilderTester(manifest, in: fs) { package, diagnostics in
// Mixed language targets no long raise an error at package load time.
// The Native build system will error instead as it's the only one that doesn't support it.
diagnostics.checkIsEmpty()
try package.checkModule("foo") { module in
module.check(c99name: "foo")
module.checkSources(paths:
"Foo.swift",
"CFoo.c"
)
module.checkResources(resources: [])
}
}
}

@Test
func testMixedSourcesDisabled() throws {
let foo: AbsolutePath = "/Sources/foo"

let fs = InMemoryFileSystem(emptyFiles:
foo.appending(components: "Foo.swift").pathString,
foo.appending(components: "CFoo.c").pathString
)

let manifest = Manifest.createRootManifest(
displayName: "pkg",
path: .root,
targets: [
try TargetDescription(name: "foo"),
]
)
try PackageBuilderTester(manifest, in: fs) { package, diagnostics in
diagnostics.check(diagnostic: "target at '\(foo)' contains mixed language source files; feature not supported", severity: .error)
}
}
Expand Down
27 changes: 19 additions & 8 deletions Tests/PackageLoadingTests/ToolsVersionParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -838,13 +838,24 @@ final class ToolsVersionParserTests: XCTestCase {
XCTAssertEqual(version.description, "5.0.0")
}

func testExperimentalFlag() throws {
let version = try ToolsVersionParser.parse(utf8String: "// swift-tools-version: 6.3;(experimentalCGen)")
XCTAssertEqual(version, ToolsVersion(version: .init(6, 3, 0)))
XCTAssertTrue(version.experimentalFeatures?.contains(.experimentalCGen) == true)

let version2 = try ToolsVersionParser.parse(utf8String: "// swift-tools-version: 6.3;(experimentalIgnored)")
XCTAssertEqual(version2, ToolsVersion(version: .init(6, 3, 0)))
XCTAssertNil(version2.experimentalFeatures)
func testExperimentalFlags() throws {
let cgen = try ToolsVersionParser.parse(utf8String: "// swift-tools-version: 6.3;(experimentalCGen)")
XCTAssertEqual(cgen, ToolsVersion(version: .init(6, 3, 0)))
XCTAssertTrue(cgen.experimentalFeatures?.contains(.experimentalCGen) == true)
XCTAssertTrue(cgen.experimentalFeatures?.contains(.experimentalMultiLang) ?? false == false)

let multiLang = try ToolsVersionParser.parse(utf8String: "// swift-tools-version: 6.4;(experimentalMultiLang)")
XCTAssertEqual(multiLang, ToolsVersion(version: .init(6, 4, 0)))
XCTAssertTrue(multiLang.experimentalFeatures?.contains(.experimentalMultiLang) == true)
XCTAssertTrue(multiLang.experimentalFeatures?.contains(.experimentalCGen) ?? false == false)

let both = try ToolsVersionParser.parse(utf8String: "// swift-tools-version: 6.4;(experimentalCGen,experimentalMultiLang)")
XCTAssertEqual(both, ToolsVersion(version: .init(6, 4, 0)))
XCTAssertTrue(both.experimentalFeatures?.contains(.experimentalMultiLang) == true)
XCTAssertTrue(both.experimentalFeatures?.contains(.experimentalCGen) == true)

let invalid = try ToolsVersionParser.parse(utf8String: "// swift-tools-version: 6.3;(experimentalIgnored)")
XCTAssertEqual(invalid, ToolsVersion(version: .init(6, 3, 0)))
XCTAssertNil(invalid.experimentalFeatures)
}
}
2 changes: 2 additions & 0 deletions Tests/PackageModelTests/ToolsVersionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation
import PackageLoading
import PackageModel
import Testing

Expand Down
25 changes: 0 additions & 25 deletions Tests/SwiftBuildSupportTests/CGenPIFTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -329,28 +329,3 @@ extension HostToPluginMessage.InputContext {
return path
}
}

extension ProjectModel.Group {
func findSource(ref: GUID) throws -> Basics.AbsolutePath? {
for child in subitems {
switch child {
case .file(let file):
if file.id == ref {
if let file = try? Basics.AbsolutePath(validating: file.path) {
return file
}
guard self.pathBase == .absolute else {
return nil
}
let groupPath = try Basics.AbsolutePath(validating: self.path)
return groupPath.appending(file.path)
}
case .group(let group):
if let file = try group.findSource(ref: ref) {
return file
}
}
}
return nil
}
}
87 changes: 87 additions & 0 deletions Tests/SwiftBuildSupportTests/PIFBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1025,4 +1025,91 @@ struct PIFBuilderTests {
}
}
}

@Test func mixedSourceTarget() async throws {
let fs = InMemoryFileSystem(
emptyFiles:
"/Pkg/Sources/lib/file1.swift",
"/Pkg/Sources/lib/file2.c"
)
let observability = ObservabilitySystem.makeForTesting()
let graph = try loadModulesGraph(
fileSystem: fs,
manifests: [
Manifest.createRootManifest(
displayName: "Pkg",
path: "/Pkg",
toolsVersion: try #require(ToolsVersion(string: "6.4.0", experimentalFeatures: [.experimentalMultiLang])),
targets: [
TargetDescription(name: "lib"),
]
),
],
observabilityScope: observability.topScope
)
#expect(observability.diagnostics.isEmpty)

let pifBuilder = PIFBuilder(
graph: graph,
parameters: try PIFBuilderParameters.constructDefaultParametersForTesting(
temporaryDirectory: AbsolutePath.root.appending("tmp"),
addLocalRpaths: true
),
fileSystem: fs,
observabilityScope: observability.topScope
)

let pif = try await pifBuilder.constructPIF(
buildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild),
hostBuildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild)
)

let project = try pif.workspace.project(named: "Pkg")
let lib = try project.target(named: "lib")

// Ensure both sources are included
let sourcesPhase: ProjectModel.SourcesBuildPhase = try #require(lib.common.buildPhases.compactMap({
guard case let .sources(sourcesBuildPhase) = $0 else {
return nil
}
return sourcesBuildPhase
}).only)

let sources: [Basics.AbsolutePath] = sourcesPhase.files.compactMap({
guard case .reference(id: let refId) = $0.ref else {
return nil
}
return try? project.underlying.mainGroup.findSource(ref: refId)
}).sorted()
let expected: [Basics.AbsolutePath] = [
"/Pkg/Sources/lib/file1.swift",
"/Pkg/Sources/lib/file2.c",
]
#expect(sources == expected)
}
}

extension ProjectModel.Group {
func findSource(ref: GUID) throws -> Basics.AbsolutePath? {
for child in subitems {
switch child {
case .file(let file):
if file.id == ref {
if let file = try? Basics.AbsolutePath(validating: file.path) {
return file
}
guard self.pathBase == .absolute else {
return nil
}
let groupPath = try Basics.AbsolutePath(validating: self.path)
return groupPath.appending(file.path)
}
case .group(let group):
if let file = try group.findSource(ref: ref) {
return file
}
}
}
return nil
}
}
Loading