From acfafa8b19aa16b9016328d5102eacafe2524e49 Mon Sep 17 00:00:00 2001 From: Zamderax Date: Sun, 2 Nov 2025 01:36:14 -0700 Subject: [PATCH 1/4] Initial commit with SPM Cmake addition --- Documentation/CMakeIntegration.md | 607 ++++++++++++++++++ Package.swift | 8 + Sources/Build/BuildPlan/BuildPlan+Clang.swift | 24 +- Sources/Build/BuildPlan/BuildPlan+Swift.swift | 27 +- Sources/Build/BuildPlan/BuildPlan.swift | 46 ++ Sources/BuildSystemCMake/BuildCache.swift | 88 +++ Sources/BuildSystemCMake/CMakeBuilder.swift | 139 ++++ .../BuildSystemCMake/CMakeIntegration.swift | 69 ++ Sources/BuildSystemCMake/HeaderAnalyzer.swift | 114 ++++ .../BuildSystemCMake/ModuleMapGenerator.swift | 45 ++ .../BuildSystemCMake/ModuleMapPolicy.swift | 97 +++ Sources/Commands/CMakeAnalyze.swift | 135 ++++ Sources/Commands/CMakeDiagnose.swift | 141 ++++ .../PackageCommands/SwiftPackageCommand.swift | 3 + 14 files changed, 1538 insertions(+), 5 deletions(-) create mode 100644 Documentation/CMakeIntegration.md create mode 100644 Sources/BuildSystemCMake/BuildCache.swift create mode 100644 Sources/BuildSystemCMake/CMakeBuilder.swift create mode 100644 Sources/BuildSystemCMake/CMakeIntegration.swift create mode 100644 Sources/BuildSystemCMake/HeaderAnalyzer.swift create mode 100644 Sources/BuildSystemCMake/ModuleMapGenerator.swift create mode 100644 Sources/BuildSystemCMake/ModuleMapPolicy.swift create mode 100644 Sources/Commands/CMakeAnalyze.swift create mode 100644 Sources/Commands/CMakeDiagnose.swift diff --git a/Documentation/CMakeIntegration.md b/Documentation/CMakeIntegration.md new file mode 100644 index 00000000000..5beb0769744 --- /dev/null +++ b/Documentation/CMakeIntegration.md @@ -0,0 +1,607 @@ +# Wrapping CMake-based C/C++ Libraries with Swift Package Manager + +This guide shows you how to create a Swift Package that wraps a CMake-based C/C++ library (as a git submodule) with ergonomic Swift APIs. + +## Table of Contents + +* [Quick Start](#quick-start) +* [Step-by-Step Tutorial](#step-by-step-tutorial) +* [Configuration Reference](#configuration-reference) +* [Module Map Modes](#module-map-modes) +* [Advanced Topics](#advanced-topics) +* [Troubleshooting](#troubleshooting) +* [Examples](#examples) + +## Quick Start + +```bash +# 1. Create your Swift package +mkdir MyAwesomeWrapper && cd MyAwesomeWrapper +swift package init --type library + +# 2. Add the C/C++ library as a submodule +git init +git submodule add https://github.com/vendor/awesome-lib ThirdParty/AwesomeLib + +# 3. Check CMake prerequisites +swift package diagnose-cmake + +# 4. Analyze the library and generate configuration +swift package analyze-cmake ThirdParty/AwesomeLib \ + --module-name CAwesomeLib \ + --write + +# 5. Update Package.swift to include the system library target +# 6. Create Swift wrapper code +# 7. Build! +swift build +``` + +## Step-by-Step Tutorial + +### Example: Wrapping SDL3 + +We'll walk through wrapping SDL3 (Simple DirectMedia Layer) as a complete example. + +#### Step 1: Create Your Package + +```bash +mkdir MySDL +cd MySDL +swift package init --type library +git init +``` + +#### Step 2: Add SDL as a Submodule + +```bash +git submodule add https://github.com/libsdl-org/SDL.git ThirdParty/SDL +git commit -m "Add SDL submodule" +``` + +**Why a submodule?** This keeps the C/C++ library separate, tracked at a specific version, and makes updates explicit. + +#### Step 3: Verify CMake Is Available + +```bash +swift package diagnose-cmake +``` + +Expected output: +``` +CMake Diagnostics +================= + +cmake: /usr/local/bin/cmake + version: 3.28.0 + +ninja: /usr/local/bin/ninja + version: 1.11.1 + +PATH directories: + - /usr/local/bin + - /usr/bin + ... +``` + +If CMake is missing, install it: +- **macOS**: `brew install cmake` +- **Ubuntu**: `sudo apt-get install cmake` +- **Windows**: `winget install cmake` + +#### Step 4: Analyze and Configure + +```bash +swift package analyze-cmake ThirdParty/SDL \ + --module-name CSDL \ + --write +``` + +This command: +1. Scans headers in `ThirdParty/SDL/include/` +2. Identifies textual headers (e.g., `begin_code.h`/`close_code.h`) +3. Detects headers requiring external dependencies (e.g., OpenGL, Vulkan) +4. Generates suggested configuration files + +**Output:** Two files are created: +- `ThirdParty/SDL/.spm-cmake.json` - CMake build configuration +- `ThirdParty/SDL/config/CSDL.modulemap` - Clang module map + +#### Step 5: Review and Customize Configuration + +**`.spm-cmake.json`** - Configure the CMake build: + +```json +{ + "defines": { + "SDL_SHARED": "OFF", + "SDL_STATIC": "ON", + "SDL_TESTS": "OFF" + }, + "moduleMap": { + "mode": "provided", + "path": "config/CSDL.modulemap", + "installAt": "include/module.modulemap" + } +} +``` + +**`config/CSDL.modulemap`** - Define how Swift sees the C headers: + +```c +module CSDL [system] { + umbrella "SDL3" + + // Textual headers - included via #include, not compiled into module + textual header "SDL3/SDL_begin_code.h" + textual header "SDL3/SDL_close_code.h" + + // Exclude headers requiring external dependencies + exclude header "SDL3/SDL_main.h" + exclude header "SDL3/SDL_egl.h" + exclude header "SDL3/SDL_vulkan.h" + exclude header "SDL3/SDL_opengl.h" + + export * + module * { export * } + + link "SDL3" +} +``` + +#### Step 6: Update Package.swift + +Add the system library target and your Swift wrapper: + +```swift +// swift-tools-version: 6.2 +import PackageDescription + +let package = Package( + name: "MySDL", + products: [ + .library(name: "MySDL", targets: ["MySDL"]) + ], + targets: [ + // System library target - SwiftPM will build this via CMake + .systemLibrary( + name: "CSDL", + path: "ThirdParty/SDL" + ), + + // Swift wrapper with ergonomic APIs + .target( + name: "MySDL", + dependencies: ["CSDL"] + ), + + .testTarget( + name: "MySDLTests", + dependencies: ["MySDL"] + ) + ] +) +``` + +#### Step 7: Create Swift Wrapper + +**Sources/MySDL/MySDL.swift**: + +```swift +import CSDL + +/// Swift wrapper for SDL3 with ergonomic APIs +public struct SDL { + + /// Get SDL version as a string + public static var version: String { + let v = SDL_GetVersion() + let major = (v >> 24) & 0xFF + let minor = (v >> 16) & 0xFF + let patch = v & 0xFFFF + return "\(major).\(minor).\(patch)" + } + + /// Initialize SDL subsystems + public static func initialize(_ subsystems: Subsystem) throws { + guard SDL_Init(subsystems.rawValue) == 0 else { + throw SDLError.initializationFailed + } + } + + /// Quit SDL + public static func quit() { + SDL_Quit() + } +} + +// MARK: - Type-safe wrappers + +extension SDL { + public struct Subsystem: OptionSet { + public let rawValue: UInt32 + public init(rawValue: UInt32) { self.rawValue = rawValue } + + public static let video = Subsystem(rawValue: UInt32(SDL_INIT_VIDEO)) + public static let audio = Subsystem(rawValue: UInt32(SDL_INIT_AUDIO)) + public static let gamepad = Subsystem(rawValue: UInt32(SDL_INIT_GAMEPAD)) + } +} + +// MARK: - Error handling + +public enum SDLError: Error { + case initializationFailed + + public var localizedDescription: String { + "SDL initialization failed" + } +} +``` + +#### Step 8: Add Tests + +**Tests/MySDLTests/MySDLTests.swift**: + +```swift +import XCTest +@testable import MySDL +import CSDL + +final class MySDLTests: XCTestCase { + + func testVersion() throws { + let version = SDL.version + XCTAssertFalse(version.isEmpty) + print("SDL Version: \(version)") + } + + func testInitQuit() throws { + try SDL.initialize(.video) + SDL.quit() + } +} +``` + +#### Step 9: Build and Test + +```bash +swift build +swift test +``` + +**What happens during build:** + +1. SwiftPM detects `CMakeLists.txt` in `ThirdParty/SDL/` +2. Reads `.spm-cmake.json` for configuration +3. Runs CMake to configure, build, and install SDL3: + ```bash + cmake -S ThirdParty/SDL -B .build/cmake/.../build \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_INSTALL_PREFIX=.build/cmake/.../staging \ + -DSDL_SHARED=OFF -DSDL_STATIC=ON -DSDL_TESTS=OFF + cmake --build .build/cmake/.../build + cmake --install .build/cmake/.../build + ``` +4. Copies the module map to staging area +5. Discovers libraries (`libSDL3.a`) +6. Compiles Swift code linking against SDL3 + +## Configuration Reference + +### .spm-cmake.json + +Complete schema: + +```json +{ + "defines": { + "": "", + "BUILD_SHARED_LIBS": "OFF", + "ENABLE_TESTING": "OFF" + }, + "moduleMap": { + "mode": "auto | provided | overlay | none", + "path": "config/MyModule.modulemap", + "installAt": "include/module.modulemap", + "textualHeaders": [ + "include/prefix.h", + "include/suffix.h" + ], + "excludeHeaders": [ + "include/platform_specific.h", + "include/requires_external_lib.h" + ] + } +} +``` + +#### CMake Defines + +Any CMake cache variables can be set via `defines`: + +```json +{ + "defines": { + "BUILD_SHARED_LIBS": "OFF", // Static linking + "CMAKE_BUILD_TYPE": "Release", // Override build type + "ENABLE_FEATURE_X": "ON", // Enable library feature + "USE_SYSTEM_LIBS": "OFF" // Don't use system libs + } +} +``` + +#### Module Map Configuration + +| Field | Description | +|-------|-------------| +| `mode` | How to generate the module map (see [Module Map Modes](#module-map-modes)) | +| `path` | Path to provided module map (relative to library root) | +| `installAt` | Where to install the module map in staging area | +| `textualHeaders` | Headers to include textually (not compiled into module) | +| `excludeHeaders` | Headers to exclude from the module | + +## Module Map Modes + +### Auto Mode + +SwiftPM generates a module map automatically from staged headers. + +```json +{ + "moduleMap": { + "mode": "auto", + "textualHeaders": ["include/begin.h", "include/end.h"], + "excludeHeaders": ["include/platform.h"] + } +} +``` + +**When to use:** Simple libraries with standard header structure. + +### Provided Mode + +You provide a custom module map. + +```json +{ + "moduleMap": { + "mode": "provided", + "path": "config/MyModule.modulemap", + "installAt": "include/module.modulemap" + } +} +``` + +**When to use:** Complex libraries needing fine-grained control (like SDL3). + +### Overlay Mode + +Use VFS overlays and custom module maps. + +```json +{ + "moduleMap": { + "mode": "overlay", + "overlay": { + "vfs": "config/overlay.yaml", + "moduleMapFile": "config/MyModule.modulemap" + } + } +} +``` + +**When to use:** Libraries with unconventional header layouts. + +### None Mode + +No automatic module map handling - you provide all flags manually. + +```json +{ + "moduleMap": { + "mode": "none" + } +} +``` + +**When to use:** Complete manual control needed. + +## Advanced Topics + +### Textual Headers + +Some libraries use "bracketing headers" that define/undefine macros around other headers: + +```c +// begin_code.h +#define EXPORT __attribute__((visibility("default"))) + +// actual header +#include "begin_code.h" +EXPORT void my_function(); +#include "end_code.h" + +// end_code.h +#undef EXPORT +``` + +These must be marked as `textual header` in the module map: + +```c +module MyLib [system] { + umbrella "include" + textual header "include/begin_code.h" + textual header "include/end_code.h" + export * +} +``` + +### Platform-Specific Configuration + +Different defines per platform (future feature): + +```json +{ + "platforms": { + "macos": { + "defines": { "USE_METAL": "ON" } + }, + "linux": { + "defines": { "USE_WAYLAND": "ON" } + } + } +} +``` + +### Build Caching + +SwiftPM caches CMake builds and only rebuilds when: +- `CMakeLists.txt` changes +- `.spm-cmake.json` changes +- CMake version changes + +Clean cache: `rm -rf .build/cmake/` + +## Troubleshooting + +### "cmake not found" + +**Solution:** Install CMake +```bash +# macOS +brew install cmake + +# Ubuntu/Debian +sudo apt-get install cmake + +# Windows +winget install cmake +``` + +### "Header 'X.h' not found" + +**Cause:** Header requires external dependency or is platform-specific. + +**Solution:** Add to `excludeHeaders`: + +```json +{ + "moduleMap": { + "excludeHeaders": ["include/problematic.h"] + } +} +``` + +### "begin_code.h included without matching end_code.h" + +**Cause:** Bracketing headers being compiled into module. + +**Solution:** Mark as textual: + +```json +{ + "moduleMap": { + "textualHeaders": [ + "include/begin_code.h", + "include/end_code.h" + ] + } +} +``` + +### CMake configuration fails + +**Debug:** Check CMake output: +```bash +swift build -v 2>&1 | grep cmake +``` + +Common issues: +- Missing dependencies +- Incompatible CMake version +- Wrong defines + +### Module not found in Swift code + +**Check:** +1. System library target exists in Package.swift +2. Module map is valid (run `analyze-cmake`) +3. Build succeeded (check `.build/cmake/.../staging/include/`) + +## Examples + +### Example 1: Simple C Library (zlib) + +```json +// ThirdParty/zlib/.spm-cmake.json +{ + "defines": { + "BUILD_SHARED_LIBS": "OFF" + }, + "moduleMap": { + "mode": "auto" + } +} +``` + +```swift +// Package.swift +.systemLibrary(name: "CZlib", path: "ThirdParty/zlib"), +.target(name: "MyZlib", dependencies: ["CZlib"]) +``` + +### Example 2: Complex C++ Library with External Dependencies + +```json +// ThirdParty/opencv/.spm-cmake.json +{ + "defines": { + "BUILD_SHARED_LIBS": "OFF", + "BUILD_EXAMPLES": "OFF", + "BUILD_TESTS": "OFF", + "WITH_CUDA": "OFF", + "WITH_OPENCL": "OFF" + }, + "moduleMap": { + "mode": "provided", + "path": "config/OpenCV.modulemap", + "excludeHeaders": [ + "opencv2/cuda*.hpp", + "opencv2/opencl*.hpp" + ] + } +} +``` + +## Best Practices + +1. **Pin submodule versions**: Always commit a specific commit hash + ```bash + cd ThirdParty/SDL + git checkout release-3.1.0 + cd ../.. + git add ThirdParty/SDL + git commit -m "Pin SDL to 3.1.0" + ``` + +2. **Start with `analyze-cmake`**: Let the tool suggest configuration + +3. **Test incrementally**: Build after each configuration change + +4. **Document requirements**: Note required CMake version, system dependencies + +5. **Provide Swift-friendly APIs**: Don't expose raw C pointers + +6. **Add safety**: Use Swift error handling, not raw return codes + +## Additional Resources + +- [Swift Package Manager Documentation](README.md) +- [Package Manifest Specification](PackageDescription.md) +- [CMake Documentation](https://cmake.org/documentation/) +- [Clang Module Maps](https://clang.llvm.org/docs/Modules.html) + +--- + +**Questions or Issues?** Check `swift package diagnose-cmake` and `swift package analyze-cmake --help` diff --git a/Package.swift b/Package.swift index b06520f092c..d1130aea475 100644 --- a/Package.swift +++ b/Package.swift @@ -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", diff --git a/Sources/Build/BuildPlan/BuildPlan+Clang.swift b/Sources/Build/BuildPlan/BuildPlan+Clang.swift index fe9bc8f7619..1bebea2353c 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Clang.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Clang.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Basics +import BuildSystemCMake import PackageGraph import PackageLoading import SPMBuildCore @@ -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: diff --git a/Sources/Build/BuildPlan/BuildPlan+Swift.swift b/Sources/Build/BuildPlan/BuildPlan+Swift.swift index 9e16e02f0e0..94d50110515 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Swift.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Swift.swift @@ -10,7 +10,8 @@ // //===----------------------------------------------------------------------===// -import struct Basics.InternalError +import Basics +import BuildSystemCMake import class PackageModel.BinaryModule import class PackageModel.ClangModule @@ -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: diff --git a/Sources/Build/BuildPlan/BuildPlan.swift b/Sources/Build/BuildPlan/BuildPlan.swift index ebf692deea1..d1e4e54a07a 100644 --- a/Sources/Build/BuildPlan/BuildPlan.swift +++ b/Sources/Build/BuildPlan/BuildPlan.swift @@ -12,6 +12,7 @@ import _Concurrency import Basics +import BuildSystemCMake import Foundation import LLBuildManifest import OrderedCollections @@ -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]]() @@ -706,6 +710,48 @@ public class BuildPlan: SPMBuildCore.BuildPlan { return result } + /// 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//// + 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) + + // Build via CMake + let result = try CMakeIntegration.buildAndPrepare( + targetRoot: targetPathString, + buildDir: workDir.pathString, + stagingDir: stagingDir.pathString, + configuration: configuration, + topLevelModuleName: target.c99name + ) + + // 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) { diff --git a/Sources/BuildSystemCMake/BuildCache.swift b/Sources/BuildSystemCMake/BuildCache.swift new file mode 100644 index 00000000000..f03c8162ab1 --- /dev/null +++ b/Sources/BuildSystemCMake/BuildCache.swift @@ -0,0 +1,88 @@ +import Foundation +import Crypto + +/// 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 = SHA256() + + // Hash CMakeLists.txt + if let cmakeData = try? Data(contentsOf: URL(fileURLWithPath: (sourceDir as NSString).appendingPathComponent("CMakeLists.txt"))) { + hasher.update(data: cmakeData) + } + + // Hash .spm-cmake.json + let configData = (try? JSONEncoder().encode(config)) ?? Data() + hasher.update(data: configData) + + // Hash CMake version + if let version = getCMakeVersion() { + hasher.update(data: Data(version.utf8)) + } + + let digest = hasher.finalize() + return digest.map { String(format: "%02x", $0) }.joined() + } + + /// 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 + } +} diff --git a/Sources/BuildSystemCMake/CMakeBuilder.swift b/Sources/BuildSystemCMake/CMakeBuilder.swift new file mode 100644 index 00000000000..c59901901ce --- /dev/null +++ b/Sources/BuildSystemCMake/CMakeBuilder.swift @@ -0,0 +1,139 @@ +import Foundation + +public struct CMakeArtifacts { + public let includeDir: String + public let libFiles: [String] +} + +public enum CMakeError: Error, CustomStringConvertible { + case cmakeNotFound + case ninjaNotFound + case failed(String, hint: String?) + case configurationFailed(output: String) + case buildFailed(output: String) + + public var description: String { + switch self { + case .cmakeNotFound: + return """ + CMake not found on PATH + + Install CMake: + macOS: brew install cmake + Ubuntu: sudo apt-get install cmake + Windows: winget install cmake + + Or run: swift package diagnose-cmake + """ + case .ninjaNotFound: + return """ + Ninja build system not found (optional but recommended) + + Install Ninja for faster builds: + macOS: brew install ninja + Ubuntu: sudo apt-get install ninja + Windows: winget install ninja + """ + case .failed(let output, let hint): + var msg = "CMake build failed:\n\(output)" + if let hint = hint { + msg += "\n\nHint: \(hint)" + } + return msg + case .configurationFailed(let output): + return """ + CMake configuration failed + + This usually means: + - Missing dependencies (check .spm-cmake.json defines) + - Incompatible CMake version + - Platform-specific requirements not met + + Output: + \(output) + """ + case .buildFailed(let output): + return """ + CMake build failed + + Common causes: + - Compiler errors in C/C++ code + - Missing system libraries + - Incorrect build flags + + Output: + \(output) + """ + } + } +} + +public final class CMakeBuilder { + private func run(_ args: [String], cwd: String? = nil) throws { + let p = Process() + p.executableURL = URL(fileURLWithPath: args[0]) + p.arguments = Array(args.dropFirst()) + if let cwd = cwd { p.currentDirectoryURL = URL(fileURLWithPath: cwd) } + let pipe = Pipe(); p.standardOutput = pipe; p.standardError = pipe + try p.run(); p.waitUntilExit() + if p.terminationStatus != 0 { + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let out = String(data: data, encoding: .utf8) ?? "" + throw CMakeError.failed(out) + } + } + + private func which(_ prog: String) -> String? { + let fm = FileManager.default + let paths = (ProcessInfo.processInfo.environment["PATH"] ?? "").split(separator: ":").map(String.init) + for p in paths { + let cand = (p as NSString).appendingPathComponent(prog) + if fm.isExecutableFile(atPath: cand) { return cand } + } + return nil + } + + public func configureBuildInstall(sourceDir: String, + workDir: String, + stagingDir: String, + buildType: String, + defines: [String:String] = [:]) throws -> CMakeArtifacts { + guard let cmake = which("cmake") else { throw CMakeError.cmakeNotFound } + try FileManager.default.createDirectory(at: URL(fileURLWithPath: workDir), + withIntermediateDirectories: true, attributes: nil) + try FileManager.default.createDirectory(at: URL(fileURLWithPath: stagingDir), + withIntermediateDirectories: true, attributes: nil) + + var args = [cmake, "-S", sourceDir, "-B", workDir, + "-DCMAKE_BUILD_TYPE=\(buildType)", + "-DCMAKE_INSTALL_PREFIX=\(stagingDir)", + "-DCMAKE_POSITION_INDEPENDENT_CODE=ON", + "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"] + + // Prefer Ninja if present + if which("ninja") != nil { args += ["-G", "Ninja"] } + + for (k,v) in defines { + args += ["-D\(k)=\(v)"] + } + + try run(args) + try run([cmake, "--build", workDir, "--config", buildType, "--parallel"]) + try run([cmake, "--install", workDir, "--config", buildType]) + + // Very small artifact discovery: gather libs from staging/lib* + var libs: [String] = [] + let fm = FileManager.default + let libRoots = ["lib", "lib64", "lib/Release", "lib/Debug"].map { (stagingDir as NSString).appendingPathComponent($0) } + for root in libRoots where fm.fileExists(atPath: root) { + if let items = try? fm.contentsOfDirectory(atPath: root) { + for f in items where f.hasPrefix("lib") || f.hasSuffix(".dylib") || f.hasSuffix(".so") || f.hasSuffix(".a") { + libs.append((root as NSString).appendingPathComponent(f)) + } + } + } + + return CMakeArtifacts(includeDir: (stagingDir as NSString).appendingPathComponent("include"), + libFiles: libs) + } +} diff --git a/Sources/BuildSystemCMake/CMakeIntegration.swift b/Sources/BuildSystemCMake/CMakeIntegration.swift new file mode 100644 index 00000000000..fd328d63984 --- /dev/null +++ b/Sources/BuildSystemCMake/CMakeIntegration.swift @@ -0,0 +1,69 @@ +import Foundation + +public struct CMakeIntegrationResult { + public let includeDir: String + public let libFiles: [String] + public let extraCFlags: [String] +} + +public enum CMakeIntegration { + /// Main entry used by the BuildPlan hook. + public static func buildAndPrepare(targetRoot: String, + buildDir: String, + stagingDir: String, + configuration: String, + topLevelModuleName: String) throws -> CMakeIntegrationResult { + // 1) Load module map policy and CMake defines + let cfg = ModuleMapPolicy.loadConfig(at: targetRoot) + let defines = cfg.defines ?? [:] + + // 2) Build via CMake + let artifacts = try CMakeBuilder().configureBuildInstall( + sourceDir: targetRoot, + workDir: buildDir, + stagingDir: stagingDir, + buildType: configuration, + defines: defines + ) + + // 3) Handle module map policy + let mmCfg = cfg.moduleMap ?? ModuleMapConfig() + let installAt = mmCfg.installAt ?? "include/module.modulemap" + let dest = (stagingDir as NSString).appendingPathComponent(installAt) + var extraCFlags: [String] = [] + + switch mmCfg.mode { + case .auto: + // assume headers staged under include/; set umbrella = include dir + let excludes = mmCfg.excludeHeaders ?? [] + let textualHeaders = mmCfg.textualHeaders ?? [] + try ModuleMapGenerator.generate(umbrellaDir: artifacts.includeDir, + outFile: dest, + topLevelModuleName: topLevelModuleName, + excludes: excludes, + textualHeaders: textualHeaders) + + case .provided: + guard let providedRel = mmCfg.path else { break } + let providedAbs = (targetRoot as NSString).appendingPathComponent(providedRel) + try ModuleMapGenerator.copyProvided(from: providedAbs, + to: dest, + sanityName: mmCfg.sanityCheckModuleName) + + case .overlay: + if let v = mmCfg.overlay?.vfs { + extraCFlags += ["-Xcc", "-vfsoverlay", "-Xcc", v] + } + if let m = mmCfg.overlay?.moduleMapFile { + extraCFlags += ["-Xcc", "-fmodule-map-file=\(m)"] + } + // no file written to staging + + case .none: + // nothing — user is responsible for flags + break + } + + return CMakeIntegrationResult(includeDir: artifacts.includeDir, libFiles: artifacts.libFiles, extraCFlags: extraCFlags) + } +} diff --git a/Sources/BuildSystemCMake/HeaderAnalyzer.swift b/Sources/BuildSystemCMake/HeaderAnalyzer.swift new file mode 100644 index 00000000000..8bc46f112e5 --- /dev/null +++ b/Sources/BuildSystemCMake/HeaderAnalyzer.swift @@ -0,0 +1,114 @@ +import Foundation + +/// Analyzes C/C++ headers to suggest module map configuration +public struct HeaderAnalyzer { + + /// Result of header analysis + public struct Analysis { + public var textualHeaders: [String] = [] + public var excludedHeaders: [String] = [] + public var warnings: [String] = [] + + public var suggestedModuleMap: String { + var lines: [String] = [] + lines.append("module YourModule [system] {") + lines.append(" umbrella \"include\"") + lines.append("") + + if !textualHeaders.isEmpty { + lines.append(" // Textual headers (included via #include)") + for header in textualHeaders { + lines.append(" textual header \"\(header)\"") + } + lines.append("") + } + + if !excludedHeaders.isEmpty { + lines.append(" // Excluded headers (require external dependencies)") + for header in excludedHeaders { + lines.append(" exclude header \"\(header)\"") + } + lines.append("") + } + + lines.append(" export *") + lines.append(" module * { export * }") + lines.append("}") + + return lines.joined(separator: "\n") + } + } + + /// Analyze headers in a directory + public static func analyze(includeDir: String) -> Analysis { + var analysis = Analysis() + let fm = FileManager.default + + guard let enumerator = fm.enumerator(atPath: includeDir) else { + return analysis + } + + for case let file as String in enumerator { + guard file.hasSuffix(".h") || file.hasSuffix(".hpp") else { continue } + + let fullPath = (includeDir as NSString).appendingPathComponent(file) + guard let content = try? String(contentsOfFile: fullPath, encoding: .utf8) else { + continue + } + + // Detect textual header patterns + if file.contains("begin_code") || file.contains("close_code") || + file.contains("_pre.h") || file.contains("_post.h") { + analysis.textualHeaders.append(file) + continue + } + + // Detect headers requiring external dependencies + let externalIncludes = [ + "", + " String { + var config: [String: Any] = [:] + + var moduleMap: [String: Any] = [ + "mode": "auto" + ] + + if !analysis.textualHeaders.isEmpty { + moduleMap["textualHeaders"] = analysis.textualHeaders + } + + if !excludedHeaders.isEmpty { + moduleMap["excludeHeaders"] = analysis.excludedHeaders + } + + config["moduleMap"] = moduleMap + + if let jsonData = try? JSONSerialization.data(withJSONObject: config, options: [.prettyPrinted, .sortedKeys]), + let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString + } + + return "{}" + } +} diff --git a/Sources/BuildSystemCMake/ModuleMapGenerator.swift b/Sources/BuildSystemCMake/ModuleMapGenerator.swift new file mode 100644 index 00000000000..012cf9d58a2 --- /dev/null +++ b/Sources/BuildSystemCMake/ModuleMapGenerator.swift @@ -0,0 +1,45 @@ +import Foundation + +public enum ModuleMapGenerator { + public static func generate(umbrellaDir: String, + outFile: String, + topLevelModuleName: String, + excludes: [String] = [], + textualHeaders: [String] = []) throws { + var lines: [String] = [] + lines.append("module \(topLevelModuleName) [system] {") + lines.append(" umbrella \"\(umbrellaDir)\"") + + // Exclude headers (e.g., main headers, or headers that will be textual) + for ex in excludes { + lines.append(" exclude header \"\(ex)\"") + } + + // Textual headers (included via #include, not compiled into module) + // These are perfect for SDL's begin_code.h/close_code.h pattern + for textual in textualHeaders { + lines.append(" textual header \"\(textual)\"") + } + + lines.append(" export *") + lines.append(" module * { export * }") + lines.append("}") + try FileManager.default.createDirectory(at: URL(fileURLWithPath: (outFile as NSString).deletingLastPathComponent), + withIntermediateDirectories: true, attributes: nil) + try lines.joined(separator: "\n").write(toFile: outFile, atomically: true, encoding: .utf8) + } + + public static func copyProvided(from provided: String, to dest: String, sanityName: String?) throws { + guard FileManager.default.fileExists(atPath: provided) else { + throw ModuleMapError.badProvidedPath(provided) + } + let contents = try String(contentsOfFile: provided) + if let expected = sanityName { + let found = ModuleMapPolicy.parseModuleName(from: contents) + if found != expected { throw ModuleMapError.nameMismatch(expected: expected, found: found) } + } + try FileManager.default.createDirectory(at: URL(fileURLWithPath: (dest as NSString).deletingLastPathComponent), + withIntermediateDirectories: true, attributes: nil) + try contents.write(toFile: dest, atomically: true, encoding: .utf8) + } +} diff --git a/Sources/BuildSystemCMake/ModuleMapPolicy.swift b/Sources/BuildSystemCMake/ModuleMapPolicy.swift new file mode 100644 index 00000000000..fa271298d89 --- /dev/null +++ b/Sources/BuildSystemCMake/ModuleMapPolicy.swift @@ -0,0 +1,97 @@ +// swift-tools-version: 6.0 +import Foundation + +public enum ModuleMapMode: String, Codable { + case auto, provided, overlay, none +} + +public struct ModuleMapOverlay: Codable { + public var vfs: String? + public var moduleMapFile: String? + public init(vfs: String? = nil, moduleMapFile: String? = nil) { + self.vfs = vfs; self.moduleMapFile = moduleMapFile + } +} + +public struct ModuleMapConfig: Codable { + public var mode: ModuleMapMode = .auto + public var path: String? // for provided + public var installAt: String? // default "include/module.modulemap" + public var overlay: ModuleMapOverlay? + public var sanityCheckModuleName: String? + public var textualHeaders: [String]? // headers to include textually (not compiled into module) + public var excludeHeaders: [String]? // headers to exclude from umbrella + + public init(mode: ModuleMapMode = .auto, + path: String? = nil, + installAt: String? = nil, + overlay: ModuleMapOverlay? = nil, + sanityCheckModuleName: String? = nil, + textualHeaders: [String]? = nil, + excludeHeaders: [String]? = nil) { + self.mode = mode + self.path = path + self.installAt = installAt + self.overlay = overlay + self.sanityCheckModuleName = sanityCheckModuleName + self.textualHeaders = textualHeaders + self.excludeHeaders = excludeHeaders + } +} + +public struct SPMCMakeConfig: Codable { + public var defines: [String:String]? = nil + public var moduleMap: ModuleMapConfig? = nil +} + +public enum ModuleMapError: Error, CustomStringConvertible { + case badProvidedPath(String) + case nameMismatch(expected: String, found: String?) + public var description: String { + switch self { + case .badProvidedPath(let p): return "Provided module map not found: \(p)" + case .nameMismatch(let e, let f): return "Module name mismatch. Expected \(e), found \(f ?? "")" + } + } +} + +public enum ModuleMapPolicy { + public static func loadConfig(at targetRoot: String) -> SPMCMakeConfig { + let jsonPath = (targetRoot as NSString).appendingPathComponent(".spm-cmake.json") + if FileManager.default.fileExists(atPath: jsonPath), + let data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)), + let cfg = try? JSONDecoder().decode(SPMCMakeConfig.self, from: data) { + return cfg + } + // Autodetect: if a module.modulemap exists under include/, prefer provided + let includeMM = (targetRoot as NSString).appendingPathComponent("include/module.modulemap") + if FileManager.default.fileExists(atPath: includeMM) { + return SPMCMakeConfig(defines: nil, + moduleMap: ModuleMapConfig(mode: .provided, path: "include/module.modulemap", installAt: "include/module.modulemap")) + } + let rootMM = (targetRoot as NSString).appendingPathComponent("module.modulemap") + if FileManager.default.fileExists(atPath: rootMM) { + return SPMCMakeConfig(defines: nil, + moduleMap: ModuleMapConfig(mode: .provided, path: "module.modulemap", installAt: "include/module.modulemap")) + } + return SPMCMakeConfig() + } + + public static func parseModuleName(from moduleMapText: String) -> String? { + // very light parser: look for "module " possibly followed by attributes + let pattern = #"module\s+([A-Za-z0-9_][A-Za-z0-9_.-]*)"# + guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { + return nil + } + + let range = NSRange(location: 0, length: moduleMapText.utf16.count) + if let match = regex.firstMatch(in: moduleMapText, options: [], range: range), + match.numberOfRanges > 1 { + let captureRange = match.range(at: 1) + if let swiftRange = Range(captureRange, in: moduleMapText) { + return String(moduleMapText[swiftRange]) + } + } + return nil + } +} diff --git a/Sources/Commands/CMakeAnalyze.swift b/Sources/Commands/CMakeAnalyze.swift new file mode 100644 index 00000000000..de1b7a33973 --- /dev/null +++ b/Sources/Commands/CMakeAnalyze.swift @@ -0,0 +1,135 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-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 ArgumentParser +import Basics +import BuildSystemCMake +import Foundation + +struct AnalyzeCMake: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "analyze-cmake", + abstract: "Analyze a CMake library and suggest module map configuration" + ) + + @Argument(help: "Path to the CMake library directory (containing CMakeLists.txt)") + var libraryPath: String + + @Option(help: "Module name to use in suggestions") + var moduleName: String = "YourModule" + + @Flag(help: "Write suggested configuration files") + var write: Bool = false + + func run() throws { + print("🔍 Analyzing CMake library at: \(libraryPath)") + print() + + // Check for CMakeLists.txt + let cmakeListsPath = (libraryPath as NSString).appendingPathComponent("CMakeLists.txt") + guard FileManager.default.fileExists(atPath: cmakeListsPath) else { + print("❌ No CMakeLists.txt found in \(libraryPath)") + throw ExitCode.failure + } + print("✓ Found CMakeLists.txt") + + // Look for include directory + let possibleIncludeDirs = ["include", "inc", "public", "src"] + var includeDir: String? + + for dir in possibleIncludeDirs { + let path = (libraryPath as NSString).appendingPathComponent(dir) + if FileManager.default.fileExists(atPath: path) { + includeDir = path + print("✓ Found include directory: \(dir)/") + break + } + } + + guard let incDir = includeDir else { + print("⚠️ No standard include directory found") + print(" Looked for: \(possibleIncludeDirs.joined(separator: ", "))") + print() + print("💡 You can still configure manually") + return + } + + // Analyze headers + print() + print("📋 Analyzing headers...") + let analysis = HeaderAnalyzer.analyze(includeDir: incDir) + + if !analysis.textualHeaders.isEmpty { + print() + print("📄 Suggested textual headers:") + for header in analysis.textualHeaders { + print(" - \(header)") + } + } + + if !analysis.excludedHeaders.isEmpty { + print() + print("🚫 Suggested excluded headers (require external dependencies):") + for header in analysis.excludedHeaders { + print(" - \(header)") + } + } + + if !analysis.warnings.isEmpty { + print() + print("⚠️ Warnings:") + for warning in analysis.warnings { + print(" - \(warning)") + } + } + + print() + print("📝 Suggested module map:") + print("─" * 60) + let moduleMap = analysis.suggestedModuleMap.replacingOccurrences(of: "YourModule", with: moduleName) + print(moduleMap) + print("─" * 60) + + print() + print("📝 Suggested .spm-cmake.json:") + print("─" * 60) + let config = HeaderAnalyzer.suggestConfig(for: analysis, libraryName: moduleName) + print(config) + print("─" * 60) + + if write { + print() + print("💾 Writing configuration files...") + + let configDir = (libraryPath as NSString).appendingPathComponent("config") + try FileManager.default.createDirectory(atPath: configDir, withIntermediateDirectories: true) + + let moduleMapPath = (configDir as NSString).appendingPathComponent("\(moduleName).modulemap") + try moduleMap.write(toFile: moduleMapPath, atomically: true, encoding: .utf8) + print("✓ Wrote \(moduleMapPath)") + + let jsonPath = (libraryPath as NSString).appendingPathComponent(".spm-cmake.json") + try config.write(toFile: jsonPath, atomically: true, encoding: .utf8) + print("✓ Wrote \(jsonPath)") + + print() + print("✨ Done! Review and customize as needed.") + } else { + print() + print("💡 To write these files, run with --write flag") + } + } +} + +private func * (left: String, right: Int) -> String { + String(repeating: left, count: right) +} diff --git a/Sources/Commands/CMakeDiagnose.swift b/Sources/Commands/CMakeDiagnose.swift new file mode 100644 index 00000000000..3a1efc741fb --- /dev/null +++ b/Sources/Commands/CMakeDiagnose.swift @@ -0,0 +1,141 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2014-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 ArgumentParser +import Basics +import Foundation + +struct DiagnoseCMake: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "diagnose-cmake", + abstract: "Diagnose CMake availability and configuration" + ) + + func run() throws { + print("CMake Diagnostics") + print("=================") + print() + + // Check for cmake + if let cmakePath = which("cmake") { + print("cmake: \(cmakePath)") + if let version = getCMakeVersion(at: cmakePath) { + print(" version: \(version)") + } + } else { + print("cmake: NOT FOUND") + print(" Install CMake from https://cmake.org/download/") + } + + print() + + // Check for ninja + if let ninjaPath = which("ninja") { + print("ninja: \(ninjaPath)") + if let version = getNinjaVersion(at: ninjaPath) { + print(" version: \(version)") + } + } else { + print("ninja: NOT FOUND (optional, but recommended for faster builds)") + print(" Install Ninja from https://ninja-build.org/") + } + + print() + + // Check PATH + if let path = ProcessInfo.processInfo.environment["PATH"] { + print("PATH directories:") + for dir in path.split(separator: ":") { + print(" - \(dir)") + } + } + } + + private func which(_ program: String) -> String? { + guard let path = ProcessInfo.processInfo.environment["PATH"] else { + return nil + } + + let fm = FileManager.default + for dir in path.split(separator: ":") { + let candidate = dir + "/" + program + if fm.isExecutableFile(atPath: String(candidate)) { + return String(candidate) + } + } + return nil + } + + private func getCMakeVersion(at path: String) -> String? { + let process = Process() + process.executableURL = URL(fileURLWithPath: path) + process.arguments = ["--version"] + + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe + + do { + try process.run() + process.waitUntilExit() + + guard process.terminationStatus == 0 else { + return nil + } + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + guard let output = String(data: data, encoding: .utf8) else { + return nil + } + + // Parse first line: "cmake version X.Y.Z" + if let firstLine = output.split(separator: "\n").first { + let parts = firstLine.split(separator: " ") + if parts.count >= 3 && parts[0] == "cmake" && parts[1] == "version" { + return String(parts[2]) + } + } + return nil + } catch { + return nil + } + } + + private func getNinjaVersion(at path: String) -> String? { + let process = Process() + process.executableURL = URL(fileURLWithPath: path) + process.arguments = ["--version"] + + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe + + do { + try process.run() + process.waitUntilExit() + + guard process.terminationStatus == 0 else { + return nil + } + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + guard let output = String(data: data, encoding: .utf8) else { + return nil + } + + // Ninja version output is just the version number + return output.trimmingCharacters(in: .whitespacesAndNewlines) + } catch { + return nil + } + } +} diff --git a/Sources/Commands/PackageCommands/SwiftPackageCommand.swift b/Sources/Commands/PackageCommands/SwiftPackageCommand.swift index da253850404..d556f54f8f2 100644 --- a/Sources/Commands/PackageCommands/SwiftPackageCommand.swift +++ b/Sources/Commands/PackageCommands/SwiftPackageCommand.swift @@ -72,6 +72,9 @@ public struct SwiftPackageCommand: AsyncParsableCommand { CompletionCommand.self, PluginCommand.self, + DiagnoseCMake.self, + AnalyzeCMake.self, + DefaultCommand.self, ] + (ProcessInfo.processInfo.environment["SWIFTPM_ENABLE_SNIPPETS"] == "1" ? [Learn.self] : []), From 49b7853bb516e313f6e80bfb7a07a35d9acdefdb Mon Sep 17 00:00:00 2001 From: Zamderax Date: Sun, 2 Nov 2025 11:26:46 -0800 Subject: [PATCH 2/4] adding experimental support for swift SDKs --- Sources/Build/BuildPlan/BuildPlan.swift | 69 +++++- Sources/BuildSystemCMake/BuildCache.swift | 15 +- Sources/BuildSystemCMake/CMakeBuilder.swift | 15 +- .../BuildSystemCMake/CMakeIntegration.swift | 35 ++- Sources/BuildSystemCMake/HeaderAnalyzer.swift | 2 +- .../BuildSystemCMake/ModuleMapPolicy.swift | 23 +- .../BuildSystemCMake/SwiftTargetInfo.swift | 226 ++++++++++++++++++ 7 files changed, 359 insertions(+), 26 deletions(-) create mode 100644 Sources/BuildSystemCMake/SwiftTargetInfo.swift diff --git a/Sources/Build/BuildPlan/BuildPlan.swift b/Sources/Build/BuildPlan/BuildPlan.swift index d1e4e54a07a..336c74118fe 100644 --- a/Sources/Build/BuildPlan/BuildPlan.swift +++ b/Sources/Build/BuildPlan/BuildPlan.swift @@ -710,6 +710,58 @@ 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 @@ -738,13 +790,26 @@ public class BuildPlan: SPMBuildCore.BuildPlan { try fileSystem.createDirectory(workDir, recursive: true) try fileSystem.createDirectory(stagingDir, recursive: true) - // Build via CMake + // 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 + topLevelModuleName: target.c99name, + triple: triple, + platformDefines: toolchain.defines, + platformEnv: toolchain.env ) // Cache the result diff --git a/Sources/BuildSystemCMake/BuildCache.swift b/Sources/BuildSystemCMake/BuildCache.swift index f03c8162ab1..4407d7dd694 100644 --- a/Sources/BuildSystemCMake/BuildCache.swift +++ b/Sources/BuildSystemCMake/BuildCache.swift @@ -1,5 +1,4 @@ import Foundation -import Crypto /// Manages build caching to avoid unnecessary CMake rebuilds public struct BuildCache { @@ -11,24 +10,24 @@ public struct BuildCache { /// Compute hash of all inputs that affect the build public func computeInputHash(sourceDir: String, config: SPMCMakeConfig) -> String { - var hasher = SHA256() + var hasher = 0 // Hash CMakeLists.txt if let cmakeData = try? Data(contentsOf: URL(fileURLWithPath: (sourceDir as NSString).appendingPathComponent("CMakeLists.txt"))) { - hasher.update(data: cmakeData) + hasher ^= cmakeData.hashValue } // Hash .spm-cmake.json - let configData = (try? JSONEncoder().encode(config)) ?? Data() - hasher.update(data: configData) + if let configData = try? JSONEncoder().encode(config) { + hasher ^= configData.hashValue + } // Hash CMake version if let version = getCMakeVersion() { - hasher.update(data: Data(version.utf8)) + hasher ^= version.hashValue } - let digest = hasher.finalize() - return digest.map { String(format: "%02x", $0) }.joined() + return String(hasher, radix: 16) } /// Check if cached build is still valid diff --git a/Sources/BuildSystemCMake/CMakeBuilder.swift b/Sources/BuildSystemCMake/CMakeBuilder.swift index c59901901ce..2ddc9cba3a3 100644 --- a/Sources/BuildSystemCMake/CMakeBuilder.swift +++ b/Sources/BuildSystemCMake/CMakeBuilder.swift @@ -69,17 +69,18 @@ public enum CMakeError: Error, CustomStringConvertible { } public final class CMakeBuilder { - private func run(_ args: [String], cwd: String? = nil) throws { + private func run(_ args: [String], env: [String: String]? = nil, cwd: String? = nil) throws { let p = Process() p.executableURL = URL(fileURLWithPath: args[0]) p.arguments = Array(args.dropFirst()) if let cwd = cwd { p.currentDirectoryURL = URL(fileURLWithPath: cwd) } + if let env = env { p.environment = env } let pipe = Pipe(); p.standardOutput = pipe; p.standardError = pipe try p.run(); p.waitUntilExit() if p.terminationStatus != 0 { let data = pipe.fileHandleForReading.readDataToEndOfFile() let out = String(data: data, encoding: .utf8) ?? "" - throw CMakeError.failed(out) + throw CMakeError.failed(out, hint: nil) } } @@ -97,7 +98,8 @@ public final class CMakeBuilder { workDir: String, stagingDir: String, buildType: String, - defines: [String:String] = [:]) throws -> CMakeArtifacts { + defines: [String:String] = [:], + env: [String:String]? = nil) throws -> CMakeArtifacts { guard let cmake = which("cmake") else { throw CMakeError.cmakeNotFound } try FileManager.default.createDirectory(at: URL(fileURLWithPath: workDir), withIntermediateDirectories: true, attributes: nil) @@ -117,9 +119,10 @@ public final class CMakeBuilder { args += ["-D\(k)=\(v)"] } - try run(args) - try run([cmake, "--build", workDir, "--config", buildType, "--parallel"]) - try run([cmake, "--install", workDir, "--config", buildType]) + // Run with platform-aware environment (CC, CXX, etc.) + try run(args, env: env) + try run([cmake, "--build", workDir, "--config", buildType, "--parallel"], env: env) + try run([cmake, "--install", workDir, "--config", buildType], env: env) // Very small artifact discovery: gather libs from staging/lib* var libs: [String] = [] diff --git a/Sources/BuildSystemCMake/CMakeIntegration.swift b/Sources/BuildSystemCMake/CMakeIntegration.swift index fd328d63984..9623fd9ffae 100644 --- a/Sources/BuildSystemCMake/CMakeIntegration.swift +++ b/Sources/BuildSystemCMake/CMakeIntegration.swift @@ -12,18 +12,41 @@ public enum CMakeIntegration { buildDir: String, stagingDir: String, configuration: String, - topLevelModuleName: String) throws -> CMakeIntegrationResult { - // 1) Load module map policy and CMake defines - let cfg = ModuleMapPolicy.loadConfig(at: targetRoot) - let defines = cfg.defines ?? [:] + topLevelModuleName: String, + triple: String? = nil, + platformDefines: [String: String] = [:], + platformEnv: [String: String] = [:]) throws -> CMakeIntegrationResult { + // 1) Load module map policy and CMake defines (with per-triple support) + let cfg = ModuleMapPolicy.loadConfig(at: targetRoot, triple: triple) + var defines = cfg.defines ?? [:] - // 2) Build via CMake + // 2) Merge platform defines (from Swift SDK) - user config takes precedence + for (key, value) in platformDefines { + if defines[key] == nil { + defines[key] = value + } + } + + // 3) Merge environment variables (toolchain compilers, etc.) + var env = ProcessInfo.processInfo.environment + for (key, value) in platformEnv { + env[key] = value + } + // User config can override toolchain + if let userEnv = cfg.env { + for (key, value) in userEnv { + env[key] = value + } + } + + // 4) Build via CMake let artifacts = try CMakeBuilder().configureBuildInstall( sourceDir: targetRoot, workDir: buildDir, stagingDir: stagingDir, buildType: configuration, - defines: defines + defines: defines, + env: env ) // 3) Handle module map policy diff --git a/Sources/BuildSystemCMake/HeaderAnalyzer.swift b/Sources/BuildSystemCMake/HeaderAnalyzer.swift index 8bc46f112e5..58d13c59e30 100644 --- a/Sources/BuildSystemCMake/HeaderAnalyzer.swift +++ b/Sources/BuildSystemCMake/HeaderAnalyzer.swift @@ -98,7 +98,7 @@ public struct HeaderAnalyzer { moduleMap["textualHeaders"] = analysis.textualHeaders } - if !excludedHeaders.isEmpty { + if !analysis.excludedHeaders.isEmpty { moduleMap["excludeHeaders"] = analysis.excludedHeaders } diff --git a/Sources/BuildSystemCMake/ModuleMapPolicy.swift b/Sources/BuildSystemCMake/ModuleMapPolicy.swift index fa271298d89..700dd44c3b9 100644 --- a/Sources/BuildSystemCMake/ModuleMapPolicy.swift +++ b/Sources/BuildSystemCMake/ModuleMapPolicy.swift @@ -41,6 +41,7 @@ public struct ModuleMapConfig: Codable { public struct SPMCMakeConfig: Codable { public var defines: [String:String]? = nil + public var env: [String:String]? = nil public var moduleMap: ModuleMapConfig? = nil } @@ -56,22 +57,38 @@ public enum ModuleMapError: Error, CustomStringConvertible { } public enum ModuleMapPolicy { - public static func loadConfig(at targetRoot: String) -> SPMCMakeConfig { + /// Load config with per-triple fallback support + /// 1. Try: .spm-cmake/.json + /// 2. Fallback: .spm-cmake.json + /// 3. Fallback: auto-detect + public static func loadConfig(at targetRoot: String, triple: String? = nil) -> SPMCMakeConfig { + // Try per-triple config first + if let triple = triple { + let tripleJsonPath = (targetRoot as NSString).appendingPathComponent(".spm-cmake/\(triple).json") + if FileManager.default.fileExists(atPath: tripleJsonPath), + let data = try? Data(contentsOf: URL(fileURLWithPath: tripleJsonPath)), + let cfg = try? JSONDecoder().decode(SPMCMakeConfig.self, from: data) { + return cfg + } + } + + // Fallback to generic config let jsonPath = (targetRoot as NSString).appendingPathComponent(".spm-cmake.json") if FileManager.default.fileExists(atPath: jsonPath), let data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)), let cfg = try? JSONDecoder().decode(SPMCMakeConfig.self, from: data) { return cfg } + // Autodetect: if a module.modulemap exists under include/, prefer provided let includeMM = (targetRoot as NSString).appendingPathComponent("include/module.modulemap") if FileManager.default.fileExists(atPath: includeMM) { - return SPMCMakeConfig(defines: nil, + return SPMCMakeConfig(defines: nil, env: nil, moduleMap: ModuleMapConfig(mode: .provided, path: "include/module.modulemap", installAt: "include/module.modulemap")) } let rootMM = (targetRoot as NSString).appendingPathComponent("module.modulemap") if FileManager.default.fileExists(atPath: rootMM) { - return SPMCMakeConfig(defines: nil, + return SPMCMakeConfig(defines: nil, env: nil, moduleMap: ModuleMapConfig(mode: .provided, path: "module.modulemap", installAt: "include/module.modulemap")) } return SPMCMakeConfig() diff --git a/Sources/BuildSystemCMake/SwiftTargetInfo.swift b/Sources/BuildSystemCMake/SwiftTargetInfo.swift new file mode 100644 index 00000000000..a3b7016288f --- /dev/null +++ b/Sources/BuildSystemCMake/SwiftTargetInfo.swift @@ -0,0 +1,226 @@ +// swift-tools-version: 6.0 +import Foundation + +/// Decoded output from `swift -print-target-info` +public struct SwiftTargetInfo: Decodable { + public struct Target: Decodable { + public let triple: String? + public let unversionedTriple: String? + public let moduleTriple: String? + public let swiftRuntimeCompatibilityVersion: String? + public let librariesRequireRPath: Bool? + } + + public struct Paths: Decodable { + public let runtimeLibraryPaths: [String]? + public let runtimeLibraryImportPaths: [String]? + public let runtimeResourcePath: String? + } + + public let target: Target? + public let paths: Paths? + public let compilerVersion: String? +} + +/// Maps Swift target info to CMake platform variables +public struct CMakePlatformMapper { + + public struct PlatformConfig { + public var systemName: String? + public var systemProcessor: String? + public var sysroot: String? + public var osxSysroot: String? + public var osxArchitectures: String? + public var osxDeploymentTarget: String? + public var compilerTarget: String? + public var findRootPath: String? + public var androidNdk: String? + public var androidArchAbi: String? + public var androidApi: String? + public var msvcRuntimeLibrary: String? + + public init() {} + } + + /// Map Swift triple and SDK info to CMake cache variables + public static func mapPlatform(triple: String, sysroot: String?, runtimePaths: [String]?) -> PlatformConfig { + var config = PlatformConfig() + config.compilerTarget = triple + + let lowerTriple = triple.lowercased() + + // Apple platforms + if lowerTriple.contains("-apple-ios") { + config.systemName = "iOS" + if lowerTriple.contains("simulator") { + config.osxSysroot = "iphonesimulator" + } else { + config.osxSysroot = "iphoneos" + } + // Extract architecture + if let arch = extractArchitecture(from: triple) { + config.osxArchitectures = arch + } + + } else if lowerTriple.contains("-apple-tvos") { + config.systemName = "tvOS" + if lowerTriple.contains("simulator") { + config.osxSysroot = "appletvsimulator" + } else { + config.osxSysroot = "appletvos" + } + if let arch = extractArchitecture(from: triple) { + config.osxArchitectures = arch + } + + } else if lowerTriple.contains("-apple-watchos") { + config.systemName = "watchOS" + if lowerTriple.contains("simulator") { + config.osxSysroot = "watchsimulator" + } else { + config.osxSysroot = "watchos" + } + if let arch = extractArchitecture(from: triple) { + config.osxArchitectures = arch + } + + } else if lowerTriple.contains("-apple-visionos") { + config.systemName = "visionOS" + if lowerTriple.contains("simulator") { + config.osxSysroot = "xrsimulator" + } else { + config.osxSysroot = "xros" + } + if let arch = extractArchitecture(from: triple) { + config.osxArchitectures = arch + } + + } else if lowerTriple.contains("-apple-macosx") || lowerTriple.contains("-apple-darwin") { + config.systemName = "Darwin" + if let sysroot = sysroot { + config.osxSysroot = sysroot + } + if let arch = extractArchitecture(from: triple) { + config.osxArchitectures = arch + } + + // Linux variants + } else if lowerTriple.contains("-linux-android") { + config.systemName = "Android" + // Extract Android ABI from triple + if lowerTriple.starts(with: "aarch64") { + config.androidArchAbi = "arm64-v8a" + } else if lowerTriple.starts(with: "armv7") { + config.androidArchAbi = "armeabi-v7a" + } else if lowerTriple.starts(with: "x86_64") { + config.androidArchAbi = "x86_64" + } else if lowerTriple.starts(with: "i686") { + config.androidArchAbi = "x86" + } + + // Look for NDK in environment + if let ndk = ProcessInfo.processInfo.environment["ANDROID_NDK_HOME"] ?? + ProcessInfo.processInfo.environment["ANDROID_NDK"] { + config.androidNdk = ndk + } + + // Default to API level 21 (minimum for 64-bit) + config.androidApi = "21" + + } else if lowerTriple.contains("-unknown-linux") || lowerTriple.contains("-linux-gnu") { + config.systemName = "Linux" + if let sysroot = sysroot { + config.sysroot = sysroot + config.findRootPath = sysroot + } + if let arch = extractArchitecture(from: triple) { + config.systemProcessor = arch + } + + // WASI + } else if lowerTriple.contains("-wasi") { + config.systemName = "WASI" + if let sysroot = sysroot { + config.sysroot = sysroot + } + + // Windows + } else if lowerTriple.contains("-windows") { + config.systemName = "Windows" + if lowerTriple.contains("msvc") { + config.msvcRuntimeLibrary = "MultiThreadedDLL" + } + if let arch = extractArchitecture(from: triple) { + config.systemProcessor = arch + } + } + + return config + } + + /// Convert PlatformConfig to CMake -D flags + public static func toCMakeDefines(_ config: PlatformConfig) -> [String: String] { + var defines: [String: String] = [:] + + if let systemName = config.systemName { + defines["CMAKE_SYSTEM_NAME"] = systemName + } + if let systemProcessor = config.systemProcessor { + defines["CMAKE_SYSTEM_PROCESSOR"] = systemProcessor + } + if let sysroot = config.sysroot { + defines["CMAKE_SYSROOT"] = sysroot + } + if let osxSysroot = config.osxSysroot { + defines["CMAKE_OSX_SYSROOT"] = osxSysroot + } + if let osxArchitectures = config.osxArchitectures { + defines["CMAKE_OSX_ARCHITECTURES"] = osxArchitectures + } + if let osxDeploymentTarget = config.osxDeploymentTarget { + defines["CMAKE_OSX_DEPLOYMENT_TARGET"] = osxDeploymentTarget + } + if let compilerTarget = config.compilerTarget { + defines["CMAKE_C_COMPILER_TARGET"] = compilerTarget + defines["CMAKE_CXX_COMPILER_TARGET"] = compilerTarget + } + if let findRootPath = config.findRootPath { + defines["CMAKE_FIND_ROOT_PATH"] = findRootPath + } + if let androidNdk = config.androidNdk { + defines["CMAKE_ANDROID_NDK"] = androidNdk + } + if let androidArchAbi = config.androidArchAbi { + defines["CMAKE_ANDROID_ARCH_ABI"] = androidArchAbi + } + if let androidApi = config.androidApi { + defines["CMAKE_ANDROID_API"] = androidApi + } + if let msvcRuntimeLibrary = config.msvcRuntimeLibrary { + defines["CMAKE_MSVC_RUNTIME_LIBRARY"] = msvcRuntimeLibrary + } + + return defines + } + + private static func extractArchitecture(from triple: String) -> String? { + let components = triple.split(separator: "-") + guard let arch = components.first else { return nil } + + let archString = String(arch) + + // Normalize architecture names + switch archString.lowercased() { + case "x86_64", "amd64": + return "x86_64" + case "aarch64", "arm64": + return "arm64" + case "armv7", "armv7a", "armv7l": + return "armv7" + case "i386", "i686": + return "i386" + default: + return archString + } + } +} From 8e20c8058ffca188aa32488bcc68299f3590bf5f Mon Sep 17 00:00:00 2001 From: Zamderax Date: Sun, 2 Nov 2025 11:43:55 -0800 Subject: [PATCH 3/4] Remove emojis from CMakeAnalyze command output --- Sources/Commands/CMakeAnalyze.swift | 45 ++++++++++++++--------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/Sources/Commands/CMakeAnalyze.swift b/Sources/Commands/CMakeAnalyze.swift index de1b7a33973..397142cfd5c 100644 --- a/Sources/Commands/CMakeAnalyze.swift +++ b/Sources/Commands/CMakeAnalyze.swift @@ -31,16 +31,16 @@ struct AnalyzeCMake: ParsableCommand { var write: Bool = false func run() throws { - print("🔍 Analyzing CMake library at: \(libraryPath)") + print("Analyzing CMake library at: \(libraryPath)") print() // Check for CMakeLists.txt let cmakeListsPath = (libraryPath as NSString).appendingPathComponent("CMakeLists.txt") guard FileManager.default.fileExists(atPath: cmakeListsPath) else { - print("❌ No CMakeLists.txt found in \(libraryPath)") + print("error: No CMakeLists.txt found in \(libraryPath)") throw ExitCode.failure } - print("✓ Found CMakeLists.txt") + print("Found CMakeLists.txt") // Look for include directory let possibleIncludeDirs = ["include", "inc", "public", "src"] @@ -50,27 +50,27 @@ struct AnalyzeCMake: ParsableCommand { let path = (libraryPath as NSString).appendingPathComponent(dir) if FileManager.default.fileExists(atPath: path) { includeDir = path - print("✓ Found include directory: \(dir)/") + print("Found include directory: \(dir)/") break } } guard let incDir = includeDir else { - print("⚠️ No standard include directory found") + print("warning: No standard include directory found") print(" Looked for: \(possibleIncludeDirs.joined(separator: ", "))") print() - print("💡 You can still configure manually") + print("note: You can still configure manually") return } // Analyze headers print() - print("📋 Analyzing headers...") + print("Analyzing headers...") let analysis = HeaderAnalyzer.analyze(includeDir: incDir) if !analysis.textualHeaders.isEmpty { print() - print("📄 Suggested textual headers:") + print("Suggested textual headers:") for header in analysis.textualHeaders { print(" - \(header)") } @@ -78,7 +78,7 @@ struct AnalyzeCMake: ParsableCommand { if !analysis.excludedHeaders.isEmpty { print() - print("🚫 Suggested excluded headers (require external dependencies):") + print("Suggested excluded headers (require external dependencies):") for header in analysis.excludedHeaders { print(" - \(header)") } @@ -86,50 +86,47 @@ struct AnalyzeCMake: ParsableCommand { if !analysis.warnings.isEmpty { print() - print("⚠️ Warnings:") + print("Warnings:") for warning in analysis.warnings { print(" - \(warning)") } } print() - print("📝 Suggested module map:") - print("─" * 60) + print("Suggested module map:") + print(String(repeating: "-", count: 60)) let moduleMap = analysis.suggestedModuleMap.replacingOccurrences(of: "YourModule", with: moduleName) print(moduleMap) - print("─" * 60) + print(String(repeating: "-", count: 60)) print() - print("📝 Suggested .spm-cmake.json:") - print("─" * 60) + print("Suggested .spm-cmake.json:") + print(String(repeating: "-", count: 60)) let config = HeaderAnalyzer.suggestConfig(for: analysis, libraryName: moduleName) print(config) - print("─" * 60) + print(String(repeating: "-", count: 60)) if write { print() - print("💾 Writing configuration files...") + print("Writing configuration files...") let configDir = (libraryPath as NSString).appendingPathComponent("config") try FileManager.default.createDirectory(atPath: configDir, withIntermediateDirectories: true) let moduleMapPath = (configDir as NSString).appendingPathComponent("\(moduleName).modulemap") try moduleMap.write(toFile: moduleMapPath, atomically: true, encoding: .utf8) - print("✓ Wrote \(moduleMapPath)") + print("Wrote \(moduleMapPath)") let jsonPath = (libraryPath as NSString).appendingPathComponent(".spm-cmake.json") try config.write(toFile: jsonPath, atomically: true, encoding: .utf8) - print("✓ Wrote \(jsonPath)") + print("Wrote \(jsonPath)") print() - print("✨ Done! Review and customize as needed.") + print("Done! Review and customize as needed.") } else { print() - print("💡 To write these files, run with --write flag") + print("note: To write these files, run with --write flag") } } } -private func * (left: String, right: Int) -> String { - String(repeating: left, count: right) -} From b97f8ddf1f5dec13a717e791fd01dcfb71f0706f Mon Sep 17 00:00:00 2001 From: Zamderax Date: Sun, 2 Nov 2025 12:20:39 -0800 Subject: [PATCH 4/4] Added documentation of SDK support --- Documentation/CMakeIntegration.md | 221 ++++++++++++++++++++++++++++-- 1 file changed, 212 insertions(+), 9 deletions(-) diff --git a/Documentation/CMakeIntegration.md b/Documentation/CMakeIntegration.md index 5beb0769744..e38bdfa1cb3 100644 --- a/Documentation/CMakeIntegration.md +++ b/Documentation/CMakeIntegration.md @@ -9,6 +9,9 @@ This guide shows you how to create a Swift Package that wraps a CMake-based C/C+ * [Configuration Reference](#configuration-reference) * [Module Map Modes](#module-map-modes) * [Advanced Topics](#advanced-topics) + * [Cross-Compilation and Swift SDKs](#cross-compilation-and-swift-sdks) + * [Textual Headers](#textual-headers) + * [Build Caching](#build-caching) * [Troubleshooting](#troubleshooting) * [Examples](#examples) @@ -300,6 +303,11 @@ Complete schema: "BUILD_SHARED_LIBS": "OFF", "ENABLE_TESTING": "OFF" }, + "env": { + "CC": "/path/to/clang", + "CXX": "/path/to/clang++", + "AR": "/path/to/llvm-ar" + }, "moduleMap": { "mode": "auto | provided | overlay | none", "path": "config/MyModule.modulemap", @@ -331,6 +339,30 @@ Any CMake cache variables can be set via `defines`: } ``` +#### Environment Variables + +The `env` field allows you to set environment variables for the CMake build process: + +```json +{ + "env": { + "CC": "/usr/local/bin/clang-18", + "CXX": "/usr/local/bin/clang++-18", + "AR": "/usr/local/bin/llvm-ar", + "RANLIB": "/usr/local/bin/llvm-ranlib", + "STRIP": "/usr/local/bin/llvm-strip" + } +} +``` + +This is useful for: +- Specifying custom compiler toolchains +- Setting Android NDK paths +- Overriding default build tools +- Platform-specific tool selection + +Note: User-provided `env` takes precedence over toolchain defaults. + #### Module Map Configuration | Field | Description | @@ -437,23 +469,194 @@ module MyLib [system] { } ``` -### Platform-Specific Configuration +### Cross-Compilation and Swift SDKs + +SwiftPM's CMake integration automatically respects your selected Swift toolchain and SDK. CMake builds align with the Swift compilation target without manual configuration. + +#### Using Swift SDKs + +Swift SDKs allow you to target different platforms (iOS, Android, static Linux, etc.). The CMake integration automatically configures the build for the selected SDK. + +**Install and use a Swift SDK:** + +```bash +# Install an SDK bundle +swift sdk install https://download.swift.org/.../swift-android.artifactbundle.tar.gz + +# List available SDKs +swift sdk list + +# Build with a specific SDK +swift build --swift-sdk android-armv7 +``` + +When building with `--swift-sdk`, the CMake integration automatically sets: +- `CMAKE_SYSTEM_NAME` (Android, iOS, Linux, etc.) +- `CMAKE_ANDROID_NDK`, `CMAKE_ANDROID_ARCH_ABI`, `CMAKE_ANDROID_API` (for Android) +- `CMAKE_OSX_SYSROOT`, `CMAKE_OSX_ARCHITECTURES` (for Apple platforms) +- `CMAKE_SYSROOT`, `CMAKE_FIND_ROOT_PATH` (for cross-compilation) +- `CMAKE_C_COMPILER_TARGET`, `CMAKE_CXX_COMPILER_TARGET` (target triple) + +**Example: Building for Android** + +```bash +swift sdk install swift-6.0-android.artifactbundle.tar.gz +swift build --swift-sdk android-aarch64 +``` + +CMake automatically receives: +``` +CMAKE_SYSTEM_NAME=Android +CMAKE_ANDROID_ARCH_ABI=arm64-v8a +CMAKE_ANDROID_API=21 +CMAKE_C_COMPILER_TARGET=aarch64-unknown-linux-android +``` + +#### Using Custom Toolchains (macOS) + +On macOS, you can select custom Swift toolchains using the `TOOLCHAINS` environment variable. The CMake integration inherits this and uses the toolchain's compilers. + +```bash +# Select a nightly toolchain +export TOOLCHAINS=org.swift.DEVELOPMENT-SNAPSHOT-2025-10-30-a + +# Verify the toolchain +swift --version + +# Build - CMake will use the toolchain's clang/clang++ +swift build +``` + +The selected toolchain's compilers are automatically passed to CMake via `CC` and `CXX` environment variables. + +#### Using Destination and Toolset Files + +For custom cross-compilation setups, use destination or toolset JSON files: + +**Destination file (linux-cross.json):** +```json +{ + "version": 1, + "target": "aarch64-unknown-linux-gnu", + "sysroot": "/path/to/aarch64-sysroot" +} +``` + +**Build with destination:** +```bash +swift build --destination linux-cross.json +``` -Different defines per platform (future feature): +The CMake integration reads the triple and sysroot from the destination file and configures CMake accordingly. +**Toolset file (custom-llvm.json):** ```json { - "platforms": { - "macos": { - "defines": { "USE_METAL": "ON" } - }, - "linux": { - "defines": { "USE_WAYLAND": "ON" } - } + "version": 1, + "compilers": { + "C": "/opt/llvm-18/bin/clang", + "CXX": "/opt/llvm-18/bin/clang++" + } +} +``` + +**Build with toolset:** +```bash +swift build --toolset custom-llvm.json +``` + +#### Per-Triple Configuration + +You can provide platform-specific CMake configuration by creating per-triple JSON files. This is useful when different platforms need different build settings. + +**Directory structure:** +``` +ThirdParty/MyLib/ + .spm-cmake.json # Default configuration + .spm-cmake/ + arm64-apple-ios.json # iOS-specific + aarch64-unknown-linux-gnu.json # Linux ARM64-specific + x86_64-apple-macosx.json # macOS x86_64-specific +``` + +**Example: iOS-specific configuration (.spm-cmake/arm64-apple-ios.json):** +```json +{ + "defines": { + "BUILD_SHARED_LIBS": "OFF", + "USE_METAL": "ON", + "USE_OPENGL": "OFF" + }, + "env": { + "CC": "/path/to/ios-clang", + "CXX": "/path/to/ios-clang++" } } ``` +**Configuration selection order:** +1. `.spm-cmake/.json` (most specific) +2. `.spm-cmake.json` (fallback) +3. Auto-detection (if no config found) + +When you build with `swift build --swift-sdk ios-arm64`, SwiftPM looks for `.spm-cmake/arm64-apple-ios.json` first. + +#### Environment Variable Overrides + +The `env` field in `.spm-cmake.json` allows you to override environment variables for the CMake build: + +```json +{ + "env": { + "CC": "/usr/local/bin/clang-18", + "CXX": "/usr/local/bin/clang++-18", + "AR": "/usr/local/bin/llvm-ar", + "RANLIB": "/usr/local/bin/llvm-ranlib", + "STRIP": "/usr/local/bin/llvm-strip" + } +} +``` + +**Precedence (highest to lowest):** +1. User-provided `env` in `.spm-cmake.json` +2. Toolchain compilers (from `TOOLCHAINS` or `--toolset`) +3. System defaults + +#### Platform-Specific CMake Variables + +The CMake integration automatically sets platform-specific variables based on the target triple: + +**Apple platforms (iOS, tvOS, watchOS, visionOS, macOS):** +- `CMAKE_SYSTEM_NAME`: iOS, tvOS, watchOS, visionOS, or Darwin +- `CMAKE_OSX_SYSROOT`: iphoneos, iphonesimulator, appletvos, etc. +- `CMAKE_OSX_ARCHITECTURES`: arm64, x86_64, etc. +- `CMAKE_INSTALL_RPATH`: @rpath + +**Linux:** +- `CMAKE_SYSTEM_NAME`: Linux +- `CMAKE_SYSROOT`: SDK sysroot path +- `CMAKE_FIND_ROOT_PATH`: SDK sysroot path +- `CMAKE_C_COMPILER_TARGET`: target triple +- `CMAKE_CXX_COMPILER_TARGET`: target triple + +**Android:** +- `CMAKE_SYSTEM_NAME`: Android +- `CMAKE_ANDROID_NDK`: NDK path from environment +- `CMAKE_ANDROID_ARCH_ABI`: arm64-v8a, armeabi-v7a, x86_64, or x86 +- `CMAKE_ANDROID_API`: 21 (default) +- `CMAKE_C_COMPILER_TARGET`: target triple +- `CMAKE_CXX_COMPILER_TARGET`: target triple + +**Windows:** +- `CMAKE_SYSTEM_NAME`: Windows +- `CMAKE_MSVC_RUNTIME_LIBRARY`: MultiThreadedDLL (for MSVC) + +**WebAssembly (WASI):** +- `CMAKE_SYSTEM_NAME`: WASI +- `CMAKE_SYSROOT`: SDK sysroot path + +You can override any of these by adding them to the `defines` field in `.spm-cmake.json`. + ### Build Caching SwiftPM caches CMake builds and only rebuilds when: