-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #104 from giginet/framework-constructor
Introduce new framework generation process
- Loading branch information
Showing
9 changed files
with
446 additions
and
237 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
Sources/ScipioKit/Producer/PIF/FrameworkBundleAssembler.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
176
Sources/ScipioKit/Producer/PIF/FrameworkComponentsCollector.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.