Last active
March 19, 2020 02:46
-
-
Save danielpetroianu/11f8c3223c3baf09e551d6998edc8f88 to your computer and use it in GitHub Desktop.
Create framework Public/Private header lists
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
#!/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)") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
andmodule.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 setmodule.modulemap
parent directory as value forSWIFT_INCLUDE_PATHS
build settingAlso make sure the headers in
PublicHeaders.h
are marked as 'Public' in your project and the headers frommodule.modulemap
as 'Project'inspired by http://nsomar.com/project-and-private-headers-in-a-swift-and-objective-c-framework/