diff --git a/Sources/ScipioKit/BuildOptions.swift b/Sources/ScipioKit/BuildOptions.swift index 89b582a9..87b3cf81 100644 --- a/Sources/ScipioKit/BuildOptions.swift +++ b/Sources/ScipioKit/BuildOptions.swift @@ -9,7 +9,8 @@ struct BuildOptions: Hashable, Codable, Sendable { sdks: Set, extraFlags: ExtraFlags?, extraBuildParameters: ExtraBuildParameters?, - enableLibraryEvolution: Bool + enableLibraryEvolution: Bool, + customFrameworkModuleMapContents: Data? ) { self.buildConfiguration = buildConfiguration self.isDebugSymbolsEmbedded = isDebugSymbolsEmbedded @@ -18,6 +19,7 @@ struct BuildOptions: Hashable, Codable, Sendable { self.extraFlags = extraFlags self.extraBuildParameters = extraBuildParameters self.enableLibraryEvolution = enableLibraryEvolution + self.customFrameworkModuleMapContents = customFrameworkModuleMapContents } let buildConfiguration: BuildConfiguration @@ -27,6 +29,10 @@ struct BuildOptions: Hashable, Codable, Sendable { let extraFlags: ExtraFlags? let extraBuildParameters: ExtraBuildParameters? let enableLibraryEvolution: Bool + /// A custom framework modulemap contents + /// - Note: It have to store the actual file contents rather than its path, + /// because the cache key should change when the file contents change. + let customFrameworkModuleMapContents: Data? } public struct ExtraFlags: Hashable, Codable, Sendable { @@ -48,6 +54,14 @@ public struct ExtraFlags: Hashable, Codable, Sendable { } } +/// A model indicates modulemap generation policy +public enum FrameworkModuleMapGenerationPolicy: Codable, Sendable, Hashable, Equatable { + // Generate modulemap automatically + case autoGenerated + // Pass the custom modulemap for each distributed framework + case custom(URL) +} + public typealias ExtraBuildParameters = [String: String] public enum BuildConfiguration: String, Codable, Sendable { diff --git a/Sources/ScipioKit/Producer/PIF/FrameworkComponentsCollector.swift b/Sources/ScipioKit/Producer/PIF/FrameworkComponentsCollector.swift index f5f2dcc8..c56fa9c4 100644 --- a/Sources/ScipioKit/Producer/PIF/FrameworkComponentsCollector.swift +++ b/Sources/ScipioKit/Producer/PIF/FrameworkComponentsCollector.swift @@ -48,19 +48,13 @@ struct FrameworkComponentsCollector { } func collectComponents(sdk: SDK) throws -> FrameworkComponents { - let modulemapGenerator = ModuleMapGenerator( - descriptionPackage: descriptionPackage, - fileSystem: fileSystem - ) - - // xcbuild automatically generates modulemaps. However, these are not for frameworks. - // Therefore, it's difficult to contain this generated modulemaps to final XCFrameworks. - // So generate modulemap for frameworks manually - let frameworkModuleMapPath = try modulemapGenerator.generate( - resolvedTarget: buildProduct.target, - sdk: sdk, - buildConfiguration: buildOptions.buildConfiguration - ) + let frameworkModuleMapPath: AbsolutePath? + if let customFrameworkModuleMapContents = buildOptions.customFrameworkModuleMapContents { + logger.info("📝 Using custom modulemap for \(buildProduct.target.name)(\(sdk.displayName))") + frameworkModuleMapPath = try copyModuleMapContentsToBuildArtifacts(customFrameworkModuleMapContents) + } else { + frameworkModuleMapPath = try generateFrameworkModuleMap() + } let targetName = buildProduct.target.c99name let generatedFrameworkPath = generatedFrameworkPath() @@ -99,6 +93,31 @@ struct FrameworkComponentsCollector { return components } + /// Copy content data to the build artifacts + private func copyModuleMapContentsToBuildArtifacts(_ data: Data) throws -> ScipioAbsolutePath { + let generatedModuleMapPath = try descriptionPackage.generatedModuleMapPath(of: buildProduct.target, sdk: sdk) + + try fileSystem.writeFileContents(generatedModuleMapPath.spmAbsolutePath, data: data) + return generatedModuleMapPath + } + + private func generateFrameworkModuleMap() throws -> AbsolutePath? { + let modulemapGenerator = FrameworkModuleMapGenerator( + descriptionPackage: descriptionPackage, + fileSystem: fileSystem + ) + + // xcbuild automatically generates modulemaps. However, these are not for frameworks. + // Therefore, it's difficult to contain this generated modulemaps to final XCFrameworks. + // So generate modulemap for frameworks manually + let frameworkModuleMapPath = try modulemapGenerator.generate( + resolvedTarget: buildProduct.target, + sdk: sdk, + buildConfiguration: buildOptions.buildConfiguration + ) + return frameworkModuleMapPath + } + private func generatedFrameworkPath() -> AbsolutePath { descriptionPackage.productsDirectory( buildConfiguration: buildOptions.buildConfiguration, diff --git a/Sources/ScipioKit/Producer/PIF/ModuleMapGenerator.swift b/Sources/ScipioKit/Producer/PIF/ModuleMapGenerator.swift index aec91b45..9c8fc949 100644 --- a/Sources/ScipioKit/Producer/PIF/ModuleMapGenerator.swift +++ b/Sources/ScipioKit/Producer/PIF/ModuleMapGenerator.swift @@ -3,7 +3,8 @@ import TSCBasic import PackageGraph import PackageModel -struct ModuleMapGenerator { +// A generator to generate modulemaps which are distributed in the XCFramework +struct FrameworkModuleMapGenerator { private struct Context { var resolvedTarget: ScipioResolvedModule var sdk: SDK diff --git a/Sources/ScipioKit/Runner.swift b/Sources/ScipioKit/Runner.swift index f7922557..436971c8 100644 --- a/Sources/ScipioKit/Runner.swift +++ b/Sources/ScipioKit/Runner.swift @@ -84,7 +84,7 @@ public struct Runner { throw Error.invalidPackage(packageDirectory) } - let buildOptions = options.buildOptionsContainer.makeBuildOptions(descriptionPackage: descriptionPackage) + let buildOptions = try options.buildOptionsContainer.makeBuildOptions(descriptionPackage: descriptionPackage) guard !buildOptions.sdks.isEmpty else { throw Error.platformNotSpecified } @@ -95,7 +95,7 @@ public struct Runner { try fileSystem.createDirectory(outputDir.absolutePath, recursive: true) - let buildOptionsMatrix = options.buildOptionsContainer.makeBuildOptionsMatrix(descriptionPackage: descriptionPackage) + let buildOptionsMatrix = try options.buildOptionsContainer.makeBuildOptionsMatrix(descriptionPackage: descriptionPackage) let producer = FrameworkProducer( descriptionPackage: descriptionPackage, @@ -130,6 +130,8 @@ extension Runner { public var extraFlags: ExtraFlags? public var extraBuildParameters: [String: String]? public var enableLibraryEvolution: Bool + /// An option indicates use custom modulemaps for distributionb + public var frameworkModuleMapGenerationPolicy: FrameworkModuleMapGenerationPolicy public init( buildConfiguration: BuildConfiguration = .release, @@ -139,7 +141,8 @@ extension Runner { frameworkType: FrameworkType = .dynamic, extraFlags: ExtraFlags? = nil, extraBuildParameters: [String: String]? = nil, - enableLibraryEvolution: Bool = false + enableLibraryEvolution: Bool = false, + frameworkModuleMapGenerationPolicy: FrameworkModuleMapGenerationPolicy = .autoGenerated ) { self.buildConfiguration = buildConfiguration self.platforms = platforms @@ -149,6 +152,7 @@ extension Runner { self.extraFlags = extraFlags self.extraBuildParameters = extraBuildParameters self.enableLibraryEvolution = enableLibraryEvolution + self.frameworkModuleMapGenerationPolicy = frameworkModuleMapGenerationPolicy } } public struct TargetBuildOptions { @@ -160,6 +164,7 @@ extension Runner { public var extraFlags: ExtraFlags? public var extraBuildParameters: [String: String]? public var enableLibraryEvolution: Bool? + public var frameworkModuleMapGenerationPolicy: FrameworkModuleMapGenerationPolicy? public init( buildConfiguration: BuildConfiguration? = nil, @@ -169,7 +174,8 @@ extension Runner { frameworkType: FrameworkType? = nil, extraFlags: ExtraFlags? = nil, extraBuildParameters: [String: String]? = nil, - enableLibraryEvolution: Bool? = nil + enableLibraryEvolution: Bool? = nil, + frameworkModuleMapGenerationPolicy: FrameworkModuleMapGenerationPolicy? = nil ) { self.buildConfiguration = buildConfiguration self.platforms = platforms @@ -179,6 +185,7 @@ extension Runner { self.extraBuildParameters = extraBuildParameters self.extraFlags = extraFlags self.enableLibraryEvolution = enableLibraryEvolution + self.frameworkModuleMapGenerationPolicy = frameworkModuleMapGenerationPolicy } } @@ -268,12 +275,19 @@ extension Runner.Options.Platform { } extension Runner.Options.BuildOptions { - fileprivate func makeBuildOptions(descriptionPackage: DescriptionPackage) -> BuildOptions { + fileprivate func makeBuildOptions(descriptionPackage: DescriptionPackage, fileSystem: any FileSystem = localFileSystem) throws -> BuildOptions { let sdks = detectSDKsToBuild( platforms: platforms, package: descriptionPackage, isSimulatorSupported: isSimulatorSupported ) + let customFrameworkModuleMapContents: Data? = switch frameworkModuleMapGenerationPolicy { + case .autoGenerated: + nil + case .custom(let url): + try fileSystem.readFileContents(url.absolutePath.spmAbsolutePath) + } + return BuildOptions( buildConfiguration: buildConfiguration, isDebugSymbolsEmbedded: isDebugSymbolsEmbedded, @@ -281,7 +295,8 @@ extension Runner.Options.BuildOptions { sdks: Set(sdks), extraFlags: extraFlags, extraBuildParameters: extraBuildParameters, - enableLibraryEvolution: enableLibraryEvolution + enableLibraryEvolution: enableLibraryEvolution, + customFrameworkModuleMapContents: customFrameworkModuleMapContents ) } @@ -324,19 +339,20 @@ extension Runner.Options.BuildOptions { frameworkType: fetch(\.frameworkType, by: \.frameworkType), extraFlags: mergedExtraFlags, extraBuildParameters: mergedExtraBuildParameters, - enableLibraryEvolution: fetch(\.enableLibraryEvolution, by: \.enableLibraryEvolution) + enableLibraryEvolution: fetch(\.enableLibraryEvolution, by: \.enableLibraryEvolution), + frameworkModuleMapGenerationPolicy: fetch(\.frameworkModuleMapGenerationPolicy, by: \.frameworkModuleMapGenerationPolicy) ) } } extension Runner.Options.BuildOptionsContainer { - fileprivate func makeBuildOptions(descriptionPackage: DescriptionPackage) -> BuildOptions { - baseBuildOptions.makeBuildOptions(descriptionPackage: descriptionPackage) + fileprivate func makeBuildOptions(descriptionPackage: DescriptionPackage) throws -> BuildOptions { + try baseBuildOptions.makeBuildOptions(descriptionPackage: descriptionPackage) } - fileprivate func makeBuildOptionsMatrix(descriptionPackage: DescriptionPackage) -> [String: BuildOptions] { - buildOptionsMatrix.mapValues { runnerOptions in - baseBuildOptions.overridden(by: runnerOptions) + fileprivate func makeBuildOptionsMatrix(descriptionPackage: DescriptionPackage) throws -> [String: BuildOptions] { + try buildOptionsMatrix.mapValues { runnerOptions in + try baseBuildOptions.overridden(by: runnerOptions) .makeBuildOptions(descriptionPackage: descriptionPackage) } } diff --git a/Sources/scipio/scipio.docc/build-pipeline.md b/Sources/scipio/scipio.docc/build-pipeline.md index 200c0706..625cdf7e 100644 --- a/Sources/scipio/scipio.docc/build-pipeline.md +++ b/Sources/scipio/scipio.docc/build-pipeline.md @@ -200,6 +200,19 @@ This matrix can override build options of the specific targets to base build opt Of-course, you are also pass `extraFlags` or `extraBuildParameters` per product. +#### Passing custom modulemap + +Scipio attempt to generate modulemap automatically distributed in the XCFramework. However, if you want to custom modulemap, you can pass `frameworkModuleMapGenerationPolicy` to the build options. In general, this option should be used `buildOptionsMatrix`. + +```swift +buildOptionsMatrix: [ + "MyResourceFramework": .init( + frameworkModuleMapGenerationPolicy: .custom(URL(filePath: "path/to/module.modulemap")) + ), +] + +``` + ### Use Custom Cache Storage In CLI version of Scipio, you can only use Project cache or Local disk cache as a cache storage backend. diff --git a/Tests/ScipioKitTests/CacheSystemTests.swift b/Tests/ScipioKitTests/CacheSystemTests.swift index 66eeca6e..dfdcb660 100644 --- a/Tests/ScipioKitTests/CacheSystemTests.swift +++ b/Tests/ScipioKitTests/CacheSystemTests.swift @@ -9,6 +9,12 @@ Target: arm64-apple-darwin21.6.0 Thread model: posix InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin """ + private let customModuleMap = """ + framework module MyTarget { + umbrella header "umbrella.h" + export * + } + """ func testParseClangVersion() async throws { let hook = { arguments in @@ -29,7 +35,9 @@ InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault sdks: [.iOS], extraFlags: .init(swiftFlags: ["-D", "SOME_FLAG"]), extraBuildParameters: ["SWIFT_OPTIMIZATION_LEVEL": "-Osize"], - enableLibraryEvolution: true), + enableLibraryEvolution: true, + customFrameworkModuleMapContents: Data(customModuleMap.utf8) + ), clangVersion: "clang-1400.0.29.102", xcodeVersion: .init(xcodeVersion: "15.4", xcodeBuildVersion: "15F31d") ) @@ -41,6 +49,7 @@ InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault { "buildOptions" : { "buildConfiguration" : "release", + "customFrameworkModuleMapContents" : "ZnJhbWV3b3JrIG1vZHVsZSBNeVRhcmdldCB7CiAgICB1bWJyZWxsYSBoZWFkZXIgInVtYnJlbGxhLmgiCiAgICBleHBvcnQgKgp9", "enableLibraryEvolution" : true, "extraBuildParameters" : { "SWIFT_OPTIMIZATION_LEVEL" : "-Osize" diff --git a/Tests/ScipioKitTests/RunnerTests.swift b/Tests/ScipioKitTests/RunnerTests.swift index 906dbb85..4bf9b4b1 100644 --- a/Tests/ScipioKitTests/RunnerTests.swift +++ b/Tests/ScipioKitTests/RunnerTests.swift @@ -715,6 +715,7 @@ extension BuildOptions { sdks: [.iOS], extraFlags: nil, extraBuildParameters: nil, - enableLibraryEvolution: true + enableLibraryEvolution: true, + customFrameworkModuleMapContents: nil ) }