Skip to content

Instantly share code, notes, and snippets.

@danielpetroianu
Last active March 19, 2020 02:46
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save danielpetroianu/11f8c3223c3baf09e551d6998edc8f88 to your computer and use it in GitHub Desktop.
Save danielpetroianu/11f8c3223c3baf09e551d6998edc8f88 to your computer and use it in GitHub Desktop.
Create framework Public/Private header lists
#!/usr/bin/env xcrun --sdk macosx swift
import Foundation
fileprivate extension Array {
func separate(where condition: (Element) -> Bool) -> ([Element], [Element]) {
return self.reduce( ([Element](), [Element]()) ) { (result, nextElement) -> ([Element], [Element]) in
if condition(nextElement) {
return (result.0 + [nextElement], result.1)
}
return (result.0, result.1 + [nextElement])
}
}
}
fileprivate extension URL {
func safelyAppendingPathComponent(_ pathComponent: String) -> URL {
if pathComponent.count == 0 {
return self
}
if pathComponent == "/" {
if self.absoluteString.hasSuffix("/") {
return self
}
}
return self.appendingPathComponent(pathComponent)
}
func hasDifferentContentThen(_ file2: URL) -> Bool{
let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = [ "diff", self.path, file2.path ]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output.count != 0
}
}
class FrameworkHelper {
let frameworkLocation: URL
let frameworkName: String
var headersRootPath: String = "Classes" {
didSet {
if (self.headersRootPath.hasPrefix("/")) {
self.headersRootPath = String(self.headersRootPath.dropFirst())
}
}
}
init(forFrameworkNamed frameworkName: String, locatedAt frameworkLocation: URL) {
self.frameworkName = frameworkName
self.frameworkLocation = frameworkLocation
}
func fetch_all_headers(grouped_by groupRules: [String]) -> [String:[String]] {
var result: [String:[String]] = [:]
var headers = fetch_all_headers_paths(from: self.frameworkLocation.appendingPathComponent(self.headersRootPath))
groupRules.map { return try! NSRegularExpression(pattern:$0) }.forEach { (rule) in
let (machedHeaders, remainderHeaders) = headers.separate { (headerPath) -> Bool in
return rule.numberOfMatches(in: headerPath,
options: .reportCompletion,
range: NSMakeRange(0,headerPath.count)) > 0
}
result[rule.pattern] = machedHeaders
headers = remainderHeaders
}
return result
}
func create_header(atPath path: String, named headerName: String, importingHeaderGroups headerGroups:[String:[String]]) throws {
let imports =
headerGroups.compactMap { (groupKey, headersPaths) -> String? in
return """
//
// AUTO GENERATED
//
// \(groupKey)
\(headersPaths
.map { $0.components(separatedBy:"/").last! }
.sorted(by: { $1 > $0 })
.map { "#import <\(self.frameworkName)/\($0)>" }
.joined(separator:"\n"))
"""
}
.joined(separator:"\n")
let temporaryHeaderUrl = URL(fileURLWithPath:"/tmp/")
.safelyAppendingPathComponent(headerName)
let destinationHeaderUrl = self.frameworkLocation
.safelyAppendingPathComponent(path)
.safelyAppendingPathComponent(headerName)
//
try imports.write(to: temporaryHeaderUrl, atomically: true, encoding: .utf8)
// if new files where added update the header
if temporaryHeaderUrl.hasDifferentContentThen(destinationHeaderUrl) {
try imports.write(to: destinationHeaderUrl, atomically: true, encoding: .utf8)
}
}
func create_project_modulemap(atPath path: String, withHeaderGroups headerGroups:[String:[String]]) throws {
let imports = headerGroups.compactMap { (groupKey, headersPaths) -> String? in
return """
// \(groupKey)
\(headersPaths
.sorted(by: { $1 > $0 })
.map { "header \"\($0)\"" }
.joined(separator:"\n"))
"""
}
.joined(separator:"\n")
let projectModulemap = """
//
// AUTO GENERATED
//
module \(self.frameworkName)_Private {
\(imports.replacingOccurrences(of: "\n", with: "\n\t"))
export *
}
"""
let temporaryProjectModulemapUrl = URL(fileURLWithPath:"/tmp/")
.safelyAppendingPathComponent("module.modulemap")
let projectModulemapUrl = self.frameworkLocation
.safelyAppendingPathComponent(path)
.safelyAppendingPathComponent("module.modulemap")
//
try projectModulemap.write(to:temporaryProjectModulemapUrl, atomically: true, encoding: .utf8)
// if new files where added update the module map
if temporaryProjectModulemapUrl.hasDifferentContentThen(projectModulemapUrl) {
try projectModulemap.write(to:projectModulemapUrl, atomically: true, encoding: .utf8)
}
}
// MARK: Private
private func fetch_all_headers_paths(from rootDirectory: URL) -> [String] {
let fm = FileManager.default
var parentRootDirectoryPath = rootDirectory.deletingLastPathComponent().path
parentRootDirectoryPath = (parentRootDirectoryPath.hasSuffix("/") ? parentRootDirectoryPath : parentRootDirectoryPath + "/")
var headersPath: [String] = []
let enumerator = fm.enumerator(at: rootDirectory, includingPropertiesForKeys: [.nameKey, .isDirectoryKey])
while let fileUrl = enumerator?.nextObject() as? URL {
let fileUrlValues = try? fileUrl.resourceValues(forKeys: [.nameKey, .isDirectoryKey])
if fileUrlValues?.isDirectory! == false && fileUrlValues?.name?.hasSuffix(".h") == true {
let fileRelativePath = fileUrl.path.replacingOccurrences(of:parentRootDirectoryPath, with: "")
headersPath.append(fileRelativePath)
}
}
return headersPath
}
}
// MARK:
if (CommandLine.argc < 3) {
let executable_name = (CommandLine.arguments[0] as NSString).lastPathComponent
print("""
Usage:
\(executable_name) <framework_srcroot> <framework_name>
""")
exit(1)
}
let FRAMEWORK_SRCROOT = URL(fileURLWithPath: CommandLine.arguments[1])
let frameworkName = CommandLine.arguments[2]
let helper = FrameworkHelper(forFrameworkNamed: frameworkName, locatedAt: FRAMEWORK_SRCROOT)
// Usage example, I chosed to group specific headers as private and all the rest as public
// update to sute your projects needs
let publicHeadersRule = "^.+$"
var groupedHeaders = helper.fetch_all_headers(grouped_by: [
//TODO add your private/public, however you want to group
"Classes/Private/.*$",
publicHeadersRule
])
let publicHeadersGroup = Dictionary(uniqueKeysWithValues: [groupedHeaders.remove(at: groupedHeaders.index(forKey: publicHeadersRule)!)])
do {
try helper.create_header(atPath:"/", named:"PublicHeaders.h", importingHeaderGroups:publicHeadersGroup)
} catch {
print("\(error)")
}
do {
try helper.create_project_modulemap(atPath:"/", withHeaderGroups:groupedHeaders)
} catch {
print("\(error)")
}
@danielpetroianu
Copy link
Author

danielpetroianu commented May 1, 2018

Usage example:
generate_public_private_modulemap.swift "$PROJECT_DIR" "$PRODUCT_NAME"
to be added as a Run Script Phase in your private framework project

This will generate 2 files, PublicHeaders.h and module.modulemap at the destination specified in the first parameter ($PROJECT_DIR in the above example)
Make sure to import PublicHeaders.h in your umbrela header and set module.modulemap parent directory as value for
SWIFT_INCLUDE_PATHS build setting
Also make sure the headers in PublicHeaders.h are marked as 'Public' in your project and the headers from module.modulemap as 'Project'

inspired by http://nsomar.com/project-and-private-headers-in-a-swift-and-objective-c-framework/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment