Skip to content
Open
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
810 changes: 810 additions & 0 deletions Documentation/CMakeIntegration.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -505,11 +505,19 @@ let package = Package(
.unsafeFlags(["-static"]),
]
),
.target(
/** CMake integration for system library targets */
name: "BuildSystemCMake",
swiftSettings: commonExperimentalFeatures + [
.unsafeFlags(["-static"]),
]
),
.target(
/** Builds Modules and Products */
name: "Build",
dependencies: [
"Basics",
"BuildSystemCMake",
"LLBuildManifest",
"PackageGraph",
"SPMBuildCore",
Expand Down
24 changes: 22 additions & 2 deletions Sources/Build/BuildPlan/BuildPlan+Clang.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//===----------------------------------------------------------------------===//

import Basics
import BuildSystemCMake
import PackageGraph
import PackageLoading
import SPMBuildCore
Expand Down Expand Up @@ -46,8 +47,27 @@ extension BuildPlan {
}
}
case let target as SystemLibraryModule:
clangTarget.additionalFlags += ["-fmodule-map-file=\(target.moduleMapPath.pathString)"]
clangTarget.additionalFlags += try pkgConfig(for: target).cFlags
// Check if this is a CMake-based system library
if let cmake = try prepareCMakeIfNeeded(for: target) {
// Add include directory from CMake staging
if let includePath = try? AbsolutePath(validating: cmake.includeDir) {
clangTarget.additionalFlags += ["-I", includePath.pathString]
}

// Add libraries from CMake build
for lib in cmake.libFiles {
if let libPath = try? AbsolutePath(validating: lib) {
clangTarget.libraryBinaryPaths.insert(libPath)
}
}

// Add extra compiler flags (e.g., for overlay mode)
clangTarget.additionalFlags += cmake.extraCFlags
} else {
// Fall back to traditional pkgConfig-based system library
clangTarget.additionalFlags += ["-fmodule-map-file=\(target.moduleMapPath.pathString)"]
clangTarget.additionalFlags += try pkgConfig(for: target).cFlags
}
case let target as BinaryModule:
switch target.kind {
case .unknown:
Expand Down
27 changes: 24 additions & 3 deletions Sources/Build/BuildPlan/BuildPlan+Swift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
//
//===----------------------------------------------------------------------===//

import struct Basics.InternalError
import Basics
import BuildSystemCMake

import class PackageModel.BinaryModule
import class PackageModel.ClangModule
Expand Down Expand Up @@ -40,8 +41,28 @@ extension BuildPlan {
"-Xcc", "-I", "-Xcc", target.clangTarget.includeDir.pathString,
]
case let target as SystemLibraryModule:
swiftTarget.additionalFlags += ["-Xcc", "-fmodule-map-file=\(target.moduleMapPath.pathString)"]
swiftTarget.additionalFlags += try pkgConfig(for: target).cFlags
// Check if this is a CMake-based system library
if let cmake = try prepareCMakeIfNeeded(for: target) {
// Add include directory from CMake staging
if let includePath = try? AbsolutePath(validating: cmake.includeDir) {
swiftTarget.additionalFlags += ["-I", includePath.pathString, "-Xcc", "-I", "-Xcc", includePath.pathString]
}

// Add libraries from CMake build
for lib in cmake.libFiles {
if let libPath = try? AbsolutePath(validating: lib) {
swiftTarget.libraryBinaryPaths.insert(libPath)
}
}

// Add extra compiler flags (e.g., for overlay mode)
// Note: extraCFlags from CMake integration already include -Xcc prefixes where needed
swiftTarget.additionalFlags += cmake.extraCFlags
} else {
// Fall back to traditional pkgConfig-based system library
swiftTarget.additionalFlags += ["-Xcc", "-fmodule-map-file=\(target.moduleMapPath.pathString)"]
swiftTarget.additionalFlags += try pkgConfig(for: target).cFlags
}
case let target as BinaryModule:
switch target.kind {
case .unknown:
Expand Down
111 changes: 111 additions & 0 deletions Sources/Build/BuildPlan/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import _Concurrency
import Basics
import BuildSystemCMake
import Foundation
import LLBuildManifest
import OrderedCollections
Expand Down Expand Up @@ -255,6 +256,9 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
/// Cache for pkgConfig flags.
private var pkgConfigCache = [SystemLibraryModule: (cFlags: [String], libs: [String])]()

/// Cache for CMake build results.
private var cmakeBuildCache = [SystemLibraryModule: CMakeIntegrationResult]()

/// Cache for library information.
var externalLibrariesCache = [BinaryModule: [LibraryInfo]]()

Expand Down Expand Up @@ -706,6 +710,113 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
return result
}

/// Query Swift driver for target info (respects --swift-sdk, destination, toolset)
func querySwiftTargetInfo(triple: String) throws -> SwiftTargetInfo {
let swiftPath = destinationBuildParameters.toolchain.swiftCompilerPath
var args = [swiftPath.pathString, "-print-target-info"]
args += ["-target", triple]

let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
process.arguments = args

let pipe = Pipe()
process.standardOutput = pipe
process.standardError = Pipe()

try process.run()
process.waitUntilExit()

guard process.terminationStatus == 0 else {
throw StringError("Failed to query Swift target info for \(triple)")
}

let data = pipe.fileHandleForReading.readDataToEndOfFile()
let decoder = JSONDecoder()
return try decoder.decode(SwiftTargetInfo.self, from: data)
}

/// Derive CMake toolchain settings from Swift SDK/destination
func deriveCMakeToolchain(targetInfo: SwiftTargetInfo, triple: Basics.Triple, sdkRoot: Basics.AbsolutePath?) -> (defines: [String: String], env: [String: String]) {
let platformConfig = CMakePlatformMapper.mapPlatform(
triple: triple.tripleString,
sysroot: sdkRoot?.pathString,
runtimePaths: targetInfo.paths?.runtimeLibraryPaths
)

var cmakeDefines = CMakePlatformMapper.toCMakeDefines(platformConfig)

// Apple-specific: set RPATH for frameworks
if triple.isDarwin() {
cmakeDefines["CMAKE_INSTALL_RPATH"] = "@rpath"
}

var env: [String: String] = [:]

// Expose toolchain compilers (respects TOOLCHAINS environment)
if let clangPath = try? destinationBuildParameters.toolchain.getClangCompiler() {
env["CC"] = clangPath.pathString
env["CXX"] = clangPath.pathString
}

return (defines: cmakeDefines, env: env)
}

/// Prepare CMake build for a system library target if it contains CMakeLists.txt.
func prepareCMakeIfNeeded(for target: SystemLibraryModule) throws -> CMakeIntegrationResult? {
// Check if we already have a cached result
if let cached = cmakeBuildCache[target] {
return cached
}

// Check if CMakeLists.txt exists
let cmakelistsPath = target.path.appending(component: "CMakeLists.txt")
guard fileSystem.exists(cmakelistsPath) else {
return nil
}

// Derive build/staging dirs under .build/cmake/<hash>/<triple>/<config>/
let targetPathString = target.path.pathString
let hash = String(targetPathString.hashValue)
let triple = destinationBuildParameters.triple.tripleString
let configuration = destinationBuildParameters.configuration.dirname

let buildPath = destinationBuildParameters.buildPath
let cmakeRoot = buildPath.appending(component: "cmake")
let workDir = cmakeRoot.appending(components: [hash, triple, configuration, "build"])
let stagingDir = cmakeRoot.appending(components: [hash, triple, configuration, "staging"])

// Create directories
try fileSystem.createDirectory(workDir, recursive: true)
try fileSystem.createDirectory(stagingDir, recursive: true)

// Query Swift target info (respects --swift-sdk, destination, toolset)
let targetInfo = try querySwiftTargetInfo(triple: triple)

// Derive platform-specific CMake settings
let toolchain = deriveCMakeToolchain(
targetInfo: targetInfo,
triple: destinationBuildParameters.triple,
sdkRoot: destinationBuildParameters.toolchain.sdkRootPath
)

// Build via CMake with SDK-aware configuration
let result = try CMakeIntegration.buildAndPrepare(
targetRoot: targetPathString,
buildDir: workDir.pathString,
stagingDir: stagingDir.pathString,
configuration: configuration,
topLevelModuleName: target.c99name,
triple: triple,
platformDefines: toolchain.defines,
platformEnv: toolchain.env
)

// Cache the result
cmakeBuildCache[target] = result
return result
}

/// Extracts the library information from an XCFramework.
func parseXCFramework(for binaryTarget: BinaryModule, triple: Basics.Triple) throws -> [LibraryInfo] {
try self.externalLibrariesCache.memoize(key: binaryTarget) {
Expand Down
87 changes: 87 additions & 0 deletions Sources/BuildSystemCMake/BuildCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import Foundation

/// Manages build caching to avoid unnecessary CMake rebuilds
public struct BuildCache {
private let cacheDir: String

public init(cacheDir: String) {
self.cacheDir = cacheDir
}

/// Compute hash of all inputs that affect the build
public func computeInputHash(sourceDir: String, config: SPMCMakeConfig) -> String {
var hasher = 0

// Hash CMakeLists.txt
if let cmakeData = try? Data(contentsOf: URL(fileURLWithPath: (sourceDir as NSString).appendingPathComponent("CMakeLists.txt"))) {
hasher ^= cmakeData.hashValue
}

// Hash .spm-cmake.json
if let configData = try? JSONEncoder().encode(config) {
hasher ^= configData.hashValue
}

// Hash CMake version
if let version = getCMakeVersion() {
hasher ^= version.hashValue
}

return String(hasher, radix: 16)
}

/// Check if cached build is still valid
public func isValid(for inputHash: String) -> Bool {
let manifestPath = (cacheDir as NSString).appendingPathComponent("build-manifest.json")
guard let data = try? Data(contentsOf: URL(fileURLWithPath: manifestPath)),
let manifest = try? JSONDecoder().decode(BuildManifest.self, from: data) else {
return false
}
return manifest.inputHash == inputHash
}

/// Save build manifest
public func saveManifest(_ manifest: BuildManifest) throws {
let manifestPath = (cacheDir as NSString).appendingPathComponent("build-manifest.json")
let data = try JSONEncoder().encode(manifest)
try data.write(to: URL(fileURLWithPath: manifestPath))
}

private func getCMakeVersion() -> String? {
// Try to get cmake --version
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/which")
process.arguments = ["cmake"]

let pipe = Pipe()
process.standardOutput = pipe

do {
try process.run()
process.waitUntilExit()
guard process.terminationStatus == 0 else { return nil }

let data = pipe.fileHandleForReading.readDataToEndOfFile()
return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
} catch {
return nil
}
}
}

/// Manifest of a completed build
public struct BuildManifest: Codable {
public let cmakeVersion: String
public let buildDate: Date
public let defines: [String: String]
public let producedLibraries: [String]
public let inputHash: String

public init(cmakeVersion: String, buildDate: Date, defines: [String: String], producedLibraries: [String], inputHash: String) {
self.cmakeVersion = cmakeVersion
self.buildDate = buildDate
self.defines = defines
self.producedLibraries = producedLibraries
self.inputHash = inputHash
}
}
Loading