Skip to content

Commit

Permalink
Merge pull request #104 from giginet/framework-constructor
Browse files Browse the repository at this point in the history
Introduce new framework generation process
  • Loading branch information
giginet authored Oct 3, 2023
2 parents db57716 + 29dd847 commit ba240dc
Show file tree
Hide file tree
Showing 9 changed files with 446 additions and 237 deletions.
38 changes: 33 additions & 5 deletions Sources/ScipioKit/DescriptionPackage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,45 @@ struct DescriptionPackage {
workspaceDirectory.appending(component: "DerivedData")
}

var productsPath: AbsolutePath {
derivedDataPath.appending(component: "Products")
}

func generatedModuleMapPath(of target: ResolvedTarget, sdk: SDK) throws -> AbsolutePath {
let relativePath = try RelativePath(validating: "GeneratedModuleMaps/\(sdk.settingValue)")
let relativePath = try RelativePath(validating: "ModuleMapsForFramework/\(sdk.settingValue)")
return workspaceDirectory
.appending(relativePath)
.appending(component: target.modulemapName)
}

/// Returns an Products directory path
/// It should be the default setting of `TARGET_BUILD_DIR`
func productsDirectory(buildConfiguration: BuildConfiguration, sdk: SDK) -> AbsolutePath {
let intermediateDirectoryName = productDirectoryName(
buildConfiguration: buildConfiguration,
sdk: sdk
)
return derivedDataPath.appending(components: ["Products", intermediateDirectoryName])
}

/// Returns a directory path which contains assembled frameworks
var assembledFrameworksRootDirectory: AbsolutePath {
workspaceDirectory.appending(component: "AssembledFrameworks")
}

/// Returns a directory path of the assembled frameworks path for the specific Configuration/Platform
func assembledFrameworksDirectory(buildConfiguration: BuildConfiguration, sdk: SDK) -> AbsolutePath {
let intermediateDirName = productDirectoryName(buildConfiguration: buildConfiguration, sdk: sdk)
return assembledFrameworksRootDirectory
.appending(component: intermediateDirName)
}

/// Returns an intermediate directory name in the Products dir.
/// e.g. `Debug` / `Debug-iphoneos`
private func productDirectoryName(buildConfiguration: BuildConfiguration, sdk: SDK) -> String {
if sdk == .macOS {
return buildConfiguration.settingsValue
} else {
return "\(buildConfiguration.settingsValue)-\(sdk.settingValue)"
}
}

// MARK: Initializer

private static func makeWorkspace(toolchain: UserToolchain, packagePath: AbsolutePath) throws -> Workspace {
Expand Down
12 changes: 6 additions & 6 deletions Sources/ScipioKit/Producer/Compiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ extension Compiler {
buildConfiguration: buildConfiguration)
let dumpedDSYMsMaps = try await extractor.dump(dwarfPath: debugSymbol.dwarfPath)
let bcSymbolMapPaths: [AbsolutePath] = dumpedDSYMsMaps.values.compactMap { uuid in
let path = descriptionPackage.buildArtifactsDirectoryPath(buildConfiguration: debugSymbol.buildConfiguration, sdk: debugSymbol.sdk)
let path = descriptionPackage.productsDirectory(
buildConfiguration: debugSymbol.buildConfiguration,
sdk: debugSymbol.sdk
)
.appending(component: "\(uuid.uuidString).bcsymbolmap")
guard fileSystem.exists(path) else { return nil }
return path
Expand All @@ -42,11 +45,8 @@ extension Compiler {
}

extension DescriptionPackage {
fileprivate func buildArtifactsDirectoryPath(buildConfiguration: BuildConfiguration, sdk: SDK) -> AbsolutePath {
productsPath.appending(component: "\(buildConfiguration.settingsValue)-\(sdk.settingValue)")
}

fileprivate func buildDebugSymbolPath(buildConfiguration: BuildConfiguration, sdk: SDK, target: ResolvedTarget) -> AbsolutePath {
buildArtifactsDirectoryPath(buildConfiguration: buildConfiguration, sdk: sdk).appending(component: "\(target.name).framework.dSYM")
productsDirectory(buildConfiguration: buildConfiguration, sdk: sdk)
.appending(component: "\(target.name).framework.dSYM")
}
}
4 changes: 4 additions & 0 deletions Sources/ScipioKit/Producer/FrameworkProducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ struct FrameworkProducer {
if fileSystem.exists(descriptionPackage.derivedDataPath) {
try fileSystem.removeFileTree(descriptionPackage.derivedDataPath)
}

if fileSystem.exists(descriptionPackage.assembledFrameworksRootDirectory) {
try fileSystem.removeFileTree(descriptionPackage.assembledFrameworksRootDirectory)
}
}

private func processAllTargets(buildProducts: [BuildProduct]) async throws {
Expand Down
121 changes: 121 additions & 0 deletions Sources/ScipioKit/Producer/PIF/FrameworkBundleAssembler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import Foundation
import TSCBasic

/// A assembler to generate framework bundle
/// This assembler just relocates framework components into the framework structure
struct FrameworkBundleAssembler {
private let frameworkComponents: FrameworkComponents
private let outputDirectory: AbsolutePath
private let fileSystem: any FileSystem

private var frameworkBundlePath: AbsolutePath {
outputDirectory.appending(component: "\(frameworkComponents.frameworkName).framework")
}

init(frameworkComponents: FrameworkComponents, outputDirectory: AbsolutePath, fileSystem: some FileSystem) {
self.frameworkComponents = frameworkComponents
self.outputDirectory = outputDirectory
self.fileSystem = fileSystem
}

@discardableResult
func assemble() throws -> AbsolutePath {
try fileSystem.createDirectory(frameworkBundlePath, recursive: true)

try copyInfoPlist()

try copyBinary()

try copyHeaders()

try copyModules()

try copyResources()

return frameworkBundlePath
}

private func copyInfoPlist() throws {
let sourcePath = frameworkComponents.infoPlistPath
let destinationPath = frameworkBundlePath.appending(component: "Info.plist")
try fileSystem.copy(from: sourcePath, to: destinationPath)
}

private func copyBinary() throws {
let sourcePath = frameworkComponents.binaryPath
let destinationPath = frameworkBundlePath.appending(component: frameworkComponents.frameworkName)
if fileSystem.isSymlink(sourcePath) {
// Frameworks for macOS have Versions. So their binaries are symlinks
// Follow symlink to copy a original binary
let sourceURL = sourcePath.asURL
try fileSystem.copy(
from: sourceURL.resolvingSymlinksInPath().absolutePath,
to: destinationPath
)
} else {
try fileSystem.copy(
from: frameworkComponents.binaryPath,
to: destinationPath
)
}
}

private func copyHeaders() throws {
let headers = (frameworkComponents.publicHeaderPaths ?? [])
+ (frameworkComponents.bridgingHeaderPath.flatMap { [$0] } ?? [])

guard !headers.isEmpty else {
return
}

let headerDir = frameworkBundlePath.appending(component: "Headers")

try fileSystem.createDirectory(headerDir)

for header in headers {
try fileSystem.copy(
from: header,
to: headerDir.appending(component: header.basename)
)
}
}

private func copyModules() throws {
let modules = [
frameworkComponents.swiftModulesPath,
frameworkComponents.modulemapPath,
]
.compactMap { $0 }

let needToGenerateModules = !modules.isEmpty

guard needToGenerateModules else {
return
}

let modulesDir = frameworkBundlePath.appending(component: "Modules")

try fileSystem.createDirectory(modulesDir)

if let swiftModulesPath = frameworkComponents.swiftModulesPath {
try fileSystem.copy(
from: swiftModulesPath,
to: modulesDir.appending(component: swiftModulesPath.basename)
)
}

if let moduleMapPath = frameworkComponents.modulemapPath {
try fileSystem.copy(
from: moduleMapPath,
to: modulesDir.appending(component: "module.modulemap")
)
}
}

private func copyResources() throws {
if let resourceBundlePath = frameworkComponents.resourceBundlePath {
let destinationPath = frameworkBundlePath.appending(component: resourceBundlePath.basename)
try fileSystem.copy(from: resourceBundlePath, to: destinationPath)
}
}
}
176 changes: 176 additions & 0 deletions Sources/ScipioKit/Producer/PIF/FrameworkComponentsCollector.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import Foundation
import TSCBasic
import PackageModel

/// FileLists to assemble a framework bundle
struct FrameworkComponents {
var frameworkName: String
var binaryPath: AbsolutePath
var infoPlistPath: AbsolutePath
var swiftModulesPath: AbsolutePath?
var publicHeaderPaths: Set<AbsolutePath>?
var bridgingHeaderPath: AbsolutePath?
var modulemapPath: AbsolutePath?
var resourceBundlePath: AbsolutePath?
}

/// A collector to collect framework components from a DerivedData dir
struct FrameworkComponentsCollector {
enum Error: LocalizedError {
case infoPlistNotFound(frameworkBundlePath: AbsolutePath)

var errorDescription: String? {
switch self {
case .infoPlistNotFound(let frameworkBundlePath):
return "Info.plist is not found in \(frameworkBundlePath.pathString)"
}
}
}

private let descriptionPackage: DescriptionPackage
private let buildProduct: BuildProduct
private let sdk: SDK
private let buildOptions: BuildOptions
private let fileSystem: any FileSystem

init(
descriptionPackage: DescriptionPackage,
buildProduct: BuildProduct,
sdk: SDK,
buildOptions: BuildOptions,
fileSystem: any FileSystem
) {
self.descriptionPackage = descriptionPackage
self.buildProduct = buildProduct
self.sdk = sdk
self.buildOptions = buildOptions
self.fileSystem = fileSystem
}

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 targetName = buildProduct.target.c99name
let generatedFrameworkPath = generatedFrameworkPath()

let binaryPath = generatedFrameworkPath.appending(component: targetName)

let swiftModulesPath = try collectSwiftModules(
of: targetName,
in: generatedFrameworkPath
)

let bridgingHeaderPath = try collectBridgingHeader(
of: targetName,
in: generatedFrameworkPath
)

let publicHeaders = try collectPublicHeader()

let resourceBundlePath = try collectResourceBundle(
of: targetName,
in: generatedFrameworkPath
)

let infoPlistPath = try collectInfoPlist(in: generatedFrameworkPath)

let components = FrameworkComponents(
frameworkName: buildProduct.target.name.packageNamed(),
binaryPath: binaryPath,
infoPlistPath: infoPlistPath,
swiftModulesPath: swiftModulesPath,
publicHeaderPaths: publicHeaders,
bridgingHeaderPath: bridgingHeaderPath,
modulemapPath: frameworkModuleMapPath,
resourceBundlePath: resourceBundlePath
)
return components
}

private func generatedFrameworkPath() -> AbsolutePath {
descriptionPackage.productsDirectory(
buildConfiguration: buildOptions.buildConfiguration,
sdk: sdk
)
.appending(component: "\(buildProduct.target.c99name).framework")
}

private func collectInfoPlist(in frameworkBundlePath: AbsolutePath) throws -> AbsolutePath {
let infoPlistLocationCandidates = [
// In a regular framework bundle, Info.plist should be on its root
frameworkBundlePath.appending(component: "Info.plist"),
// In a versioned framework bundle (for macOS), Info.plist should be in Resources
frameworkBundlePath.appending(components: "Resources", "Info.plist"),
]
guard let infoPlistPath = infoPlistLocationCandidates.first(where: fileSystem.exists(_:)) else {
throw Error.infoPlistNotFound(frameworkBundlePath: frameworkBundlePath)
}
return infoPlistPath
}

/// Collects *.swiftmodules* in a generated framework bundle
private func collectSwiftModules(of targetName: String, in frameworkPath: AbsolutePath) throws -> AbsolutePath? {
let swiftModulesPath = frameworkPath.appending(
components: "Modules", "\(targetName).swiftmodule"
)

if fileSystem.exists(swiftModulesPath) {
return swiftModulesPath
}
return nil
}

/// Collects a bridging header in a generated framework bundle
private func collectBridgingHeader(of targetName: String, in frameworkPath: AbsolutePath) throws -> AbsolutePath? {
let generatedBridgingHeader = frameworkPath.appending(
components: "Headers", "\(targetName)-Swift.h"
)

if fileSystem.exists(generatedBridgingHeader) {
return generatedBridgingHeader
}

return nil
}

/// Collects public headers of clangTarget
private func collectPublicHeader() throws -> Set<AbsolutePath>? {
guard let clangTarget = buildProduct.target.underlyingTarget as? ClangTarget else {
return nil
}

let publicHeaders = clangTarget
.headers
.filter { $0.isDescendant(of: clangTarget.includeDir) }
let notSymlinks = publicHeaders.filter { !fileSystem.isSymlink($0) }
let symlinks = publicHeaders.filter { fileSystem.isSymlink($0) }

// Sometimes, public headers include a file and its symlink both.
// This situation raises a duplication error
// So duplicated symlinks have to be omitted
let notDuplicatedSymlinks = symlinks.filter { path in
notSymlinks.allSatisfy { FileManager.default.contentsEqual(atPath: path.pathString, andPath: $0.pathString) }
}
.map { $0.asURL.resolvingSymlinksInPath() }
.map(\.absolutePath)

return Set(notSymlinks + notDuplicatedSymlinks)
}

private func collectResourceBundle(of targetName: String, in frameworkPath: AbsolutePath) throws -> AbsolutePath? {
let bundleFileName = try fileSystem.getDirectoryContents(frameworkPath).first { $0.hasSuffix(".bundle") }
return bundleFileName.flatMap { frameworkPath.appending(component: $0) }
}
}
1 change: 0 additions & 1 deletion Sources/ScipioKit/Producer/PIF/ModuleMapGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ struct ModuleMapGenerator {
self.fileSystem = fileSystem
}

@discardableResult
func generate(resolvedTarget: ResolvedTarget, sdk: SDK, buildConfiguration: BuildConfiguration) throws -> AbsolutePath? {
let context = Context(resolvedTarget: resolvedTarget, sdk: sdk, configuration: buildConfiguration)

Expand Down
Loading

0 comments on commit ba240dc

Please sign in to comment.