diff --git a/Fixtures/Miscellaneous/LibraryEvolutionLinuxXCF/SwiftFramework/Package.swift b/Fixtures/Miscellaneous/LibraryEvolutionLinuxXCF/SwiftFramework/Package.swift new file mode 100644 index 00000000000..70af89cbeeb --- /dev/null +++ b/Fixtures/Miscellaneous/LibraryEvolutionLinuxXCF/SwiftFramework/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "SwiftFramework", + products: [ + .library(name: "SwiftFramework", type: .dynamic, targets: ["SwiftFramework"]), + ], + targets: [ + .target( + name: "SwiftFramework", + swiftSettings: [.unsafeFlags(["-enable-library-evolution"])] + ), + ] +) diff --git a/Fixtures/Miscellaneous/LibraryEvolutionLinuxXCF/SwiftFramework/Sources/SwiftFramework/SwiftFramework.swift b/Fixtures/Miscellaneous/LibraryEvolutionLinuxXCF/SwiftFramework/Sources/SwiftFramework/SwiftFramework.swift new file mode 100644 index 00000000000..5ad86872cd1 --- /dev/null +++ b/Fixtures/Miscellaneous/LibraryEvolutionLinuxXCF/SwiftFramework/Sources/SwiftFramework/SwiftFramework.swift @@ -0,0 +1,8 @@ +public enum SwiftFrameworkWithEvolution +{ + case v1 + case v2 + + public + static var latest:Self { .v2 } +} \ No newline at end of file diff --git a/Fixtures/Miscellaneous/LibraryEvolutionLinuxXCF/TestBinary/Package.swift b/Fixtures/Miscellaneous/LibraryEvolutionLinuxXCF/TestBinary/Package.swift new file mode 100644 index 00000000000..4cbb13f39c2 --- /dev/null +++ b/Fixtures/Miscellaneous/LibraryEvolutionLinuxXCF/TestBinary/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package(name: "TestBinary", + products: [ + .executable(name: "TestBinary", targets: ["TestBinary"]), + ], + targets: [ + .binaryTarget(name: "SwiftFramework", path: "SwiftFramework.xcframework"), + .executableTarget(name: "TestBinary", + dependencies: [ + .target(name: "SwiftFramework"), + ] + ), + ] +) \ No newline at end of file diff --git a/Fixtures/Miscellaneous/LibraryEvolutionLinuxXCF/TestBinary/Sources/TestBinary/Main.swift b/Fixtures/Miscellaneous/LibraryEvolutionLinuxXCF/TestBinary/Sources/TestBinary/Main.swift new file mode 100644 index 00000000000..b088848fa17 --- /dev/null +++ b/Fixtures/Miscellaneous/LibraryEvolutionLinuxXCF/TestBinary/Sources/TestBinary/Main.swift @@ -0,0 +1,3 @@ +import SwiftFramework + +print("Latest Framework with LibraryEvolution version: \(SwiftFrameworkWithEvolution.latest)") \ No newline at end of file diff --git a/Sources/Build/BuildPlan/BuildPlan+Clang.swift b/Sources/Build/BuildPlan/BuildPlan+Clang.swift index fe9bc8f7619..01306cdcf7d 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Clang.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Clang.swift @@ -65,7 +65,11 @@ extension BuildPlan { clangTarget.libraryBinaryPaths.insert(library.libraryPath) } case .xcframework: - let libraries = try self.parseXCFramework(for: target, triple: clangTarget.buildParameters.triple) + let libraries = try self.parseXCFramework( + for: target, + triple: clangTarget.buildParameters.triple, + enableXCFrameworksOnLinux: clangTarget.buildParameters.enableXCFrameworksOnLinux + ) for library in libraries { library.headersPaths.forEach { clangTarget.additionalFlags += ["-I", $0.pathString] diff --git a/Sources/Build/BuildPlan/BuildPlan+Product.swift b/Sources/Build/BuildPlan/BuildPlan+Product.swift index f7b1bb41231..b290f9e3fd3 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Product.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Product.swift @@ -298,7 +298,8 @@ extension BuildPlan { case .xcframework: let libraries = try self.parseXCFramework( for: binaryTarget, - triple: productDescription.buildParameters.triple + triple: productDescription.buildParameters.triple, + enableXCFrameworksOnLinux: productDescription.buildParameters.enableXCFrameworksOnLinux ) for library in libraries { libraryBinaryPaths.insert(library.libraryPath) diff --git a/Sources/Build/BuildPlan/BuildPlan+Swift.swift b/Sources/Build/BuildPlan/BuildPlan+Swift.swift index 9e16e02f0e0..b5acf6149d1 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Swift.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Swift.swift @@ -61,7 +61,11 @@ extension BuildPlan { swiftTarget.libraryBinaryPaths.insert(library.libraryPath) } case .xcframework: - let libraries = try self.parseXCFramework(for: target, triple: swiftTarget.buildParameters.triple) + let libraries = try self.parseXCFramework( + for: target, + triple: swiftTarget.buildParameters.triple, + enableXCFrameworksOnLinux: swiftTarget.buildParameters.enableXCFrameworksOnLinux + ) for library in libraries { library.headersPaths.forEach { swiftTarget.additionalFlags += ["-I", $0.pathString, "-Xcc", "-I", "-Xcc", $0.pathString] diff --git a/Sources/Build/BuildPlan/BuildPlan.swift b/Sources/Build/BuildPlan/BuildPlan.swift index b54f7b939c6..bef12b95fa8 100644 --- a/Sources/Build/BuildPlan/BuildPlan.swift +++ b/Sources/Build/BuildPlan/BuildPlan.swift @@ -653,9 +653,12 @@ public class BuildPlan: SPMBuildCore.BuildPlan { } /// Extracts the library information from an XCFramework. - func parseXCFramework(for binaryTarget: BinaryModule, triple: Basics.Triple) throws -> [LibraryInfo] { + func parseXCFramework(for binaryTarget: BinaryModule, triple: Basics.Triple, enableXCFrameworksOnLinux: Bool) throws -> [LibraryInfo] { try self.externalLibrariesCache.memoize(key: binaryTarget) { - try binaryTarget.parseXCFrameworks(for: triple, fileSystem: self.fileSystem) + if !enableXCFrameworksOnLinux && triple.os == .linux { + return [] + } + return try binaryTarget.parseXCFrameworks(for: triple, fileSystem: self.fileSystem) } } diff --git a/Sources/CoreCommands/Options.swift b/Sources/CoreCommands/Options.swift index ab0db1df264..3b7411d8ce9 100644 --- a/Sources/CoreCommands/Options.swift +++ b/Sources/CoreCommands/Options.swift @@ -515,6 +515,13 @@ public struct BuildOptions: ParsableArguments { @Flag(name: .customLong("experimental-prepare-for-indexing-no-lazy"), help: .hidden) var prepareForIndexingNoLazy: Bool = false + /// Hidden option to allow XCFrameworks on Linux + @Flag( + name: .customLong("experimental-xcframeworks-on-linux"), + help: .hidden + ) + public var enableXCFrameworksOnLinux: Bool = false + /// Whether to enable generation of `.swiftinterface`s alongside `.swiftmodule`s. @Flag(name: .customLong("enable-parseable-module-interfaces")) public var shouldEnableParseableModuleInterfaces: Bool = false diff --git a/Sources/CoreCommands/SwiftCommandState.swift b/Sources/CoreCommands/SwiftCommandState.swift index ceacbf39b79..b4a6158274b 100644 --- a/Sources/CoreCommands/SwiftCommandState.swift +++ b/Sources/CoreCommands/SwiftCommandState.swift @@ -941,6 +941,7 @@ public final class SwiftCommandState { sanitizers: options.build.enabledSanitizers, indexStoreMode: options.build.indexStoreMode.buildParameter, prepareForIndexing: prepareForIndexingMode, + enableXCFrameworksOnLinux: options.build.enableXCFrameworksOnLinux, debuggingParameters: .init( debugInfoFormat: self.options.build.debugInfoFormat.buildParameter, triple: triple, diff --git a/Sources/SPMBuildCore/BinaryTarget+Extensions.swift b/Sources/SPMBuildCore/BinaryTarget+Extensions.swift index 902e51089c0..d5286127a9e 100644 --- a/Sources/SPMBuildCore/BinaryTarget+Extensions.swift +++ b/Sources/SPMBuildCore/BinaryTarget+Extensions.swift @@ -45,7 +45,7 @@ extension BinaryModule { let metadata = try XCFrameworkMetadata.parse(fileSystem: fileSystem, rootPath: self.artifactPath) // Filter the libraries that are relevant to the triple. guard let library = metadata.libraries.first(where: { - $0.platform == triple.os?.asXCFrameworkPlatformString && + $0.platform == triple.asXCFrameworkPlatformString && $0.variant == triple.environment?.asXCFrameworkPlatformVariantString && $0.architectures.contains(triple.archName) }) else { @@ -117,13 +117,11 @@ extension Triple { return self } } -} -extension Triple.OS { /// Returns a representation of the receiver that can be compared with platform strings declared in an XCFramework. fileprivate var asXCFrameworkPlatformString: String? { - switch self { - case .darwin, .linux, .wasi, .win32, .openbsd, .freebsd, .noneOS: + switch self.os { + case .darwin, .wasi, .win32, .openbsd, .freebsd, .noneOS: return nil // XCFrameworks do not support any of these platforms today. case .macosx: return "macos" @@ -133,6 +131,11 @@ extension Triple.OS { return "tvos" case .watchos: return "watchos" + case .linux: + if environment == .android { + return nil + } + return "linux" // Only if --experimental-xcframeworks-on-linux has been passed default: return nil // XCFrameworks do not support any of these platforms today. } diff --git a/Sources/SPMBuildCore/BuildParameters/BuildParameters.swift b/Sources/SPMBuildCore/BuildParameters/BuildParameters.swift index 035f4911c41..e5d9802b79f 100644 --- a/Sources/SPMBuildCore/BuildParameters/BuildParameters.swift +++ b/Sources/SPMBuildCore/BuildParameters/BuildParameters.swift @@ -132,6 +132,9 @@ public struct BuildParameters: Encodable { /// Do minimal build to prepare for indexing public var prepareForIndexing: PrepareForIndexingMode + /// Support Experimental XCF on Linux + public var enableXCFrameworksOnLinux: Bool + /// Build parameters related to debugging. public var debuggingParameters: Debugging @@ -166,6 +169,7 @@ public struct BuildParameters: Encodable { indexStoreMode: IndexStoreMode = .auto, shouldSkipBuilding: Bool = false, prepareForIndexing: PrepareForIndexingMode = .off, + enableXCFrameworksOnLinux: Bool = false, debuggingParameters: Debugging? = nil, driverParameters: Driver = .init(), linkingParameters: Linking = .init(), @@ -229,6 +233,7 @@ public struct BuildParameters: Encodable { self.indexStoreMode = indexStoreMode self.shouldSkipBuilding = shouldSkipBuilding self.prepareForIndexing = prepareForIndexing + self.enableXCFrameworksOnLinux = enableXCFrameworksOnLinux self.driverParameters = driverParameters self.linkingParameters = linkingParameters self.outputParameters = outputParameters diff --git a/Sources/_InternalBuildTestSupport/MockBuildTestHelper.swift b/Sources/_InternalBuildTestSupport/MockBuildTestHelper.swift index af0bfac2369..8354b6e19ce 100644 --- a/Sources/_InternalBuildTestSupport/MockBuildTestHelper.swift +++ b/Sources/_InternalBuildTestSupport/MockBuildTestHelper.swift @@ -66,6 +66,7 @@ public func mockBuildPlan( commonFlags: PackageModel.BuildFlags = .init(), indexStoreMode: BuildParameters.IndexStoreMode = .off, omitFramePointers: Bool? = nil, + enableXCFrameworksOnLinux: Bool = false, driverParameters: BuildParameters.Driver = .init(), linkingParameters: BuildParameters.Linking = .init(), targetSanitizers: EnabledSanitizers = .init(), @@ -105,7 +106,8 @@ public func mockBuildPlan( toolchain: toolchain, flags: commonFlags, triple: inferredTriple, - indexStoreMode: indexStoreMode + indexStoreMode: indexStoreMode, + enableXCFrameworksOnLinux: enableXCFrameworksOnLinux ) destinationParameters.debuggingParameters = commonDebuggingParameters destinationParameters.driverParameters = driverParameters @@ -119,7 +121,8 @@ public func mockBuildPlan( toolchain: toolchain, flags: commonFlags, triple: inferredTriple, - indexStoreMode: indexStoreMode + indexStoreMode: indexStoreMode, + enableXCFrameworksOnLinux: enableXCFrameworksOnLinux ) hostParameters.debuggingParameters = commonDebuggingParameters hostParameters.driverParameters = driverParameters diff --git a/Sources/_InternalTestSupport/MockBuildTestHelper.swift b/Sources/_InternalTestSupport/MockBuildTestHelper.swift index 78820551777..024726107c9 100644 --- a/Sources/_InternalTestSupport/MockBuildTestHelper.swift +++ b/Sources/_InternalTestSupport/MockBuildTestHelper.swift @@ -91,6 +91,7 @@ public func mockBuildParameters( linkerDeadStrip: Bool = true, linkTimeOptimizationMode: BuildParameters.LinkTimeOptimizationMode? = nil, omitFramePointers: Bool? = nil, + enableXCFrameworksOnLinux: Bool = false, prepareForIndexing: BuildParameters.PrepareForIndexingMode = .off ) -> BuildParameters { try! BuildParameters( @@ -105,6 +106,7 @@ public func mockBuildParameters( workers: 3, indexStoreMode: indexStoreMode, prepareForIndexing: prepareForIndexing, + enableXCFrameworksOnLinux: enableXCFrameworksOnLinux, debuggingParameters: .init( triple: triple, shouldEnableDebuggingEntitlement: config == .debug, diff --git a/Tests/BuildTests/BuildPlanTests.swift b/Tests/BuildTests/BuildPlanTests.swift index c0f61204e73..62abd5aa3db 100644 --- a/Tests/BuildTests/BuildPlanTests.swift +++ b/Tests/BuildTests/BuildPlanTests.swift @@ -6807,6 +6807,114 @@ class BuildPlanTestCase: BuildSystemProviderTestCase { XCTAssertMatch(dynamicLibraryPathExtension, "dylib") } + func testXCFrameworkBinaryTargetsLinux(platform: String = "linux", arch: String, targetTriple: Basics.Triple) async throws { + let Pkg: AbsolutePath = "/Pkg" + + let fs = InMemoryFileSystem( + emptyFiles: + Pkg.appending(components: "Sources", "exe", "main.swift").pathString, + Pkg.appending(components: "Sources", "Library", "Library.swift").pathString + ) + + try fs.createDirectory("/Pkg/Framework.xcframework", recursive: true) + try fs.writeFileContents( + "/Pkg/Framework.xcframework/Info.plist", + string: """ + + + + + AvailableLibraries + + + LibraryIdentifier + \(platform)-\(arch) + LibraryPath + Framework.framework + SupportedArchitectures + + \(arch) + + SupportedPlatform + \(platform) + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + + """ + ) + + let observability = ObservabilitySystem.makeForTesting() + + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: [ + Manifest.createRootManifest( + displayName: "Pkg", + path: .init(validating: Pkg.pathString), + products: [ + ProductDescription(name: "exe", type: .executable, targets: ["exe"]), + ProductDescription(name: "Library", type: .library(.dynamic), targets: ["Library"]), + ], + targets: [ + TargetDescription(name: "exe", dependencies: ["Library"]), + TargetDescription(name: "Library", dependencies: ["Framework"]), + TargetDescription(name: "Framework", path: "Framework.xcframework", type: .binary), + ] + ), + ], + binaryArtifacts: [ + .plain("pkg"): [ + "Framework": .init(kind: .xcframework, originURL: nil, path: "/Pkg/Framework.xcframework"), + ], + ], + observabilityScope: observability.topScope + ) + XCTAssertNoDiagnostics(observability.diagnostics) + + let result = try await BuildPlanResult(plan: mockBuildPlan( + triple: targetTriple, + graph: graph, + enableXCFrameworksOnLinux: true, + fileSystem: fs, + observabilityScope: observability.topScope + )) + XCTAssertNoDiagnostics(observability.diagnostics) + + result.checkProductsCount(2) + result.checkTargetsCount(2) + + let buildPath = result.plan.productsBuildPath + + let libraryBasicArguments = try result.moduleBuildDescription(for: "Library").swift().compileArguments() + XCTAssertMatch( + libraryBasicArguments, + [.anySequence, "-I", "\(Pkg.appending(components: "Framework.xcframework", "\(platform)-\(arch)"))", .anySequence] + ) + + let libraryLinkArguments = try result.buildProduct(for: "Library").linkArguments() + XCTAssertMatch(libraryLinkArguments, [.anySequence, "-L", "\(buildPath)", .anySequence]) + + let exeCompileArguments = try result.moduleBuildDescription(for: "exe").swift().compileArguments() + XCTAssertMatch( + exeCompileArguments, + [.anySequence, "-I", "\(Pkg.appending(components: "Framework.xcframework", "\(platform)-\(arch)"))", .anySequence] + ) + + let exeLinkArguments = try result.buildProduct(for: "exe").linkArguments() + XCTAssertMatch(exeLinkArguments, [.anySequence, "-L", "\(buildPath)", .anySequence]) + + let executablePathExtension = try result.buildProduct(for: "exe").binaryPath.extension ?? "" + XCTAssertMatch(executablePathExtension, "") + + let dynamicLibraryPathExtension = try result.buildProduct(for: "Library").binaryPath.extension + XCTAssertMatch(dynamicLibraryPathExtension, "so") + } + func testXCFrameworkBinaryTargets() async throws { try await self.testXCFrameworkBinaryTargets(platform: "macos", arch: "x86_64", targetTriple: .x86_64MacOS) @@ -6815,6 +6923,12 @@ class BuildPlanTestCase: BuildSystemProviderTestCase { let arm64eTriple = try Basics.Triple("arm64e-apple-macosx") try await self.testXCFrameworkBinaryTargets(platform: "macos", arch: "arm64e", targetTriple: arm64eTriple) + + let x86_64Linux = try Basics.Triple("x86_64-unknown-linux-gnu") + try await self.testXCFrameworkBinaryTargetsLinux(arch: "x86_64", targetTriple: x86_64Linux) + + let aarch64Linux = try Basics.Triple("aarch64-unknown-linux-gnu") + try await self.testXCFrameworkBinaryTargetsLinux(arch: "aarch64", targetTriple: aarch64Linux) } func testArtifactsArchiveBinaryTargets( diff --git a/Tests/FunctionalTests/LibraryEvolutionXCFLinuxTests.swift b/Tests/FunctionalTests/LibraryEvolutionXCFLinuxTests.swift new file mode 100644 index 00000000000..b441c1a83f9 --- /dev/null +++ b/Tests/FunctionalTests/LibraryEvolutionXCFLinuxTests.swift @@ -0,0 +1,109 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2024 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 _InternalTestSupport +import Basics +import Testing + +private struct SwiftPMTests { + @Test( + .requireSwift6_2, + .requireHostOS(.linux) + ) + func libraryEvolutionLinuxXCFramework() async throws { + try await fixture(name: "Miscellaneous/LibraryEvolutionLinuxXCF") { fixturePath in + let swiftFramework = "SwiftFramework" + try await withTemporaryDirectory(removeTreeOnDeinit: false) { tmpDir in + let scratchPath = tmpDir.appending(component: ".build") + try await executeSwiftBuild( + fixturePath.appending(component: swiftFramework), + configuration: .debug, + extraArgs: ["--scratch-path", scratchPath.pathString], + buildSystem: .native + ) + + #if arch(arm64) + let arch = "aarch64" + #elseif arch(x86_64) + let arch = "x86_64" + #endif + + let platform = "linux" + let libraryExtension = "so" + + let xcframeworkPath = fixturePath.appending( + components: "TestBinary", + "\(swiftFramework).xcframework" + ) + let libraryName = "lib\(swiftFramework).\(libraryExtension)" + let artifactsPath = xcframeworkPath.appending(component: "\(platform)-\(arch)") + + try localFileSystem.createDirectory(artifactsPath, recursive: true) + + try localFileSystem.copy( + from: scratchPath.appending(components: "debug", libraryName), + to: artifactsPath.appending(component: libraryName) + ) + + try localFileSystem.copy( + from: scratchPath.appending(components: "debug", "Modules", "\(swiftFramework).swiftinterface"), + to: artifactsPath.appending(component: "\(swiftFramework).swiftinterface") + ) + + try localFileSystem.writeFileContents( + xcframeworkPath.appending(component: "Info.plist"), + string: """ + + + + + AvailableLibraries + + + BinaryPath + \(libraryName) + LibraryIdentifier + \(platform)-\(arch) + LibraryPath + \(libraryName) + SupportedArchitectures + + \(arch) + + SupportedPlatform + \(platform) + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + + """ + ) + } + + let packagePath = fixturePath.appending(component: "TestBinary") + let scratchPath = packagePath.appending(component: ".build-test") + let runOutput = try await executeSwiftRun( + packagePath, "TestBinary", + extraArgs: [ + "--scratch-path", scratchPath.pathString, "--experimental-xcframeworks-on-linux", + ], + buildSystem: .native + ) + #expect(!runOutput.stderr.contains("error:")) + #expect(runOutput.stdout.contains("Latest Framework with LibraryEvolution version: v2")) + } + } +}