diff --git a/.bazelrc b/.bazelrc index 0e769e7..fb20231 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,9 +1,12 @@ -build \ ---announce_rc \ ---spawn_strategy=local \ ---strategy=SwiftCompile=worker \ ---verbose_failures \ ---apple_platform_type=macos \ ---compilation_mode=dbg \ ---features=swift.use_global_module_cache \ No newline at end of file +build --announce_rc +build --spawn_strategy=local +build --strategy=SwiftCompile=worker +build --verbose_failures +build --apple_platform_type=macos +build --compilation_mode=dbg +build --features=swift.use_global_module_cache +build --objc_enable_binary_stripping=true +build --features=dead_strip +build --experimental_multi_threaded_digest +build --disk_cache=./bazel-cache \ No newline at end of file diff --git a/BUILD b/BUILD index 10c0dd8..081866a 100644 --- a/BUILD +++ b/BUILD @@ -1,5 +1,5 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") -load("@build_bazel_rules_apple//apple:macos.bzl", "macos_command_line_application", "macos_unit_test") +load("@build_bazel_rules_apple//apple:macos.bzl", "macos_command_line_application") load("@rules_cc//cc:defs.bzl", "objc_library") objc_library( @@ -12,22 +12,23 @@ objc_library( # PodToBUILD is a core library enabling Starlark code generation swift_library( name = "PodToBUILD", - srcs = glob(["Sources/PodToBUILD/*.swift"]), + srcs = glob(["Sources/PodToBUILD/**/*.swift"]), deps = [":ObjcSupport"], copts = ["-swift-version", "5"], + visibility = ["//Tests:__pkg__"] ) # Compiler macos_command_line_application( name = "Compiler", - minimum_os_version = "10.13", + minimum_os_version = "10.11", deps = [":CompilerLib"], ) swift_library( name = "CompilerLib", - srcs = glob(["Sources/Compiler/*.swift"]), - deps = [":PodToBUILD", "@swift-argument-parser//:ArgumentParser"], + srcs = glob(["Sources/Compiler/**/*.swift"]), + deps = [":PodToBUILD", "@bazelpods-swift-argument-parser//:ArgumentParser"], copts = ["-swift-version", "5"], ) @@ -35,42 +36,13 @@ swift_library( macos_command_line_application( name = "Generator", - minimum_os_version = "10.13", + minimum_os_version = "10.11", deps = [":GeneratorLib"], ) swift_library( name = "GeneratorLib", - srcs = glob(["Sources/Generator/*.swift"]), - deps = [":PodToBUILD", "@swift-argument-parser//:ArgumentParser"], + srcs = glob(["Sources/Generator/**/*.swift"]), + deps = [":PodToBUILD", "@bazelpods-swift-argument-parser//:ArgumentParser"], copts = ["-swift-version", "5"], ) - -# This tests RepoToolsCore and Starlark logic -swift_library( - name = "PodToBUILDTestsLib", - srcs = glob(["Tests/PodToBUILDTests/*.swift"]), - deps = ["@podtobuild-SwiftCheck//:SwiftCheck"], - data = glob(["Examples/**/*.podspec.json"]) -) - -macos_unit_test( - name = "PodToBUILDTests", - deps = [":PodToBUILDTestsLib"], - minimum_os_version = "10.13", -) - -swift_library( - name = "BuildTestsLib", - srcs = glob(["Tests/BuildTests/*.swift"]), - deps = ["@podtobuild-SwiftCheck//:SwiftCheck"], - data = glob(["Examples/**/*.podspec.json"]) -) - -# This tests RepoToolsCore and Starlark logic -macos_unit_test( - name = "BuildTests", - deps = [":BuildTestsLib"], - minimum_os_version = "10.13", -) - diff --git a/Sources/Compiler/RootCommand.swift b/Sources/Compiler/RootCommand.swift new file mode 100644 index 0000000..f5c4e74 --- /dev/null +++ b/Sources/Compiler/RootCommand.swift @@ -0,0 +1,57 @@ +// +// MainCommand.swift +// PodToBUILD +// +// Created by Sergey Khliustin on 26.08.2022. +// + +import Foundation +import ArgumentParser +import PodToBUILD +import ObjcSupport + +extension String: LocalizedError { + public var errorDescription: String? { return self } +} + +struct RootCommand: ParsableCommand { + static var configuration = CommandConfiguration(commandName: "Compiler", abstract: "Compiles podspec.json to BUILD file") + @Argument(help: "podspec.json", completion: .file(extensions: ["json"])) + var podspecJson: String + + @Option(name: .long, help: "Sources root") + var src: String? + + @Option(name: .long, parsing: .upToNextOption, help: "Subspecs list") + var subspecs: [String] = [] + + @Option(name: .long, help: "Minimum iOS version if not listed in podspec") + var minIos: String = "13.0" + + func run() throws { + _ = CrashReporter() + let jsonData = try NSData(contentsOfFile: podspecJson, options: []) as Data + let jsonFile = try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) as AnyObject + + guard let jsonPodspec = jsonFile as? JSONDict else { + throw "Error parsing podspec at path \(podspecJson)" + } + + let podSpec = try PodSpec(JSONPodspec: jsonPodspec) + + let podSpecURL = NSURL(fileURLWithPath: podspecJson) + let assumedPodName = podSpecURL.lastPathComponent!.components(separatedBy: ".")[0] + let options = BasicBuildOptions(podName: assumedPodName, + subspecs: subspecs, + iosPlatform: minIos) + + let result = PodBuildFile.with(podSpec: podSpec, buildOptions: options).compile() + print(result) + } + + func absolutePath(_ path: String) -> String { + guard let src = src else { return path } + guard !path.starts(with: "/") else { return path } + return (src as NSString).appendingPathComponent(path) + } +} diff --git a/Sources/Compiler/main.swift b/Sources/Compiler/main.swift new file mode 100644 index 0000000..1e95aaf --- /dev/null +++ b/Sources/Compiler/main.swift @@ -0,0 +1,2 @@ + +RootCommand.main() diff --git a/Sources/Generator/PodConfig.swift b/Sources/Generator/PodConfig.swift new file mode 100644 index 0000000..cfccabb --- /dev/null +++ b/Sources/Generator/PodConfig.swift @@ -0,0 +1,14 @@ +// +// PodConfig.swift +// PodToBUILD +// +// Created by Sergey Khliustin on 25.08.2022. +// + +import Foundation + +struct PodConfig: Decodable { + let name: String + let podspec: String + let development: Bool +} diff --git a/Sources/Generator/PodSpecification.swift b/Sources/Generator/PodSpecification.swift new file mode 100644 index 0000000..64af4eb --- /dev/null +++ b/Sources/Generator/PodSpecification.swift @@ -0,0 +1,47 @@ +// +// PodSpecification.swift +// PodToBUILD +// +// Created by Sergey Khliustin on 25.08.2022. +// + +import Foundation +import PodToBUILD + +struct PodSpecification { + let name: String + let subspecs: [String] + let podspec: String + let development: Bool + + static func resolve(with podConfigsMap: [String: PodConfig]) -> [PodSpecification] { + let podConfigs = Array(podConfigsMap.values) + let (podspecPaths, subspecsByPodName) = + podConfigs.reduce(([String: String](), [String: [String]]())) { partialResult, podConfig in + var podspecPaths = partialResult.0 + var subspecsByPodName = partialResult.1 + + let components = podConfig.name.components(separatedBy: "/") + guard let podName = components.first else { + return partialResult + } + + podspecPaths[podName] = podConfig.podspec + + if components.count == 2, let subspecName = components.last { + var subspecs = subspecsByPodName[podName] ?? [] + subspecs.append(subspecName) + subspecsByPodName[podName] = subspecs + } + return (podspecPaths, subspecsByPodName) + } + return podspecPaths.map({ + PodSpecification(name: $0.key, subspecs: subspecsByPodName[$0.key] ?? [], podspec: $0.value, development: podConfigsMap[$0.key]?.development ?? false) + }) + } + + func toBuildOptions(src: String, ios: String) -> BuildOptions { + let path = (src as NSString).appendingPathComponent("Pods/\(name)") + return BasicBuildOptions(podName: name, subspecs: subspecs, podspecPath: podspec, sourcePath: path, iosPlatform: ios) + } +} diff --git a/Sources/Generator/RootCommand.swift b/Sources/Generator/RootCommand.swift new file mode 100644 index 0000000..13c2ae0 --- /dev/null +++ b/Sources/Generator/RootCommand.swift @@ -0,0 +1,174 @@ +// +// MainCommand.swift +// PodToBUILD +// +// Created by Sergey Khliustin on 26.08.2022. +// + +import Foundation +import ArgumentParser +import PodToBUILD +import ObjcSupport + +extension String: LocalizedError { + public var errorDescription: String? { return self } +} + + +struct RootCommand: ParsableCommand { + static var configuration = CommandConfiguration(commandName: "Generator", abstract: "Generates BUILD files for pods") + @Argument(help: "Pods.json") + var podsJson: String + + @Option(name: .long, help: "Sources root") + var src: String + + @Option(name: .long, help: "Minimum iOS version if not listed in podspec") + var minIos: String = "13.0" + + @Flag(name: .shortAndLong, help: "Concurrent mode for generating files faster") + var concurrent: Bool = false + + @Flag(name: .shortAndLong, help: "Print BUILD files contents to terminal output") + var printOutput: Bool = false + + @Flag(name: .shortAndLong, help: "Debug mode. Files will not be written") + var debug: Bool = false + + @Flag(name: .shortAndLong, help: "Will add podspec.json to the pod directory. Just for debugging purposes.") + var addPodspec: Bool = false + + func run() throws { + _ = CrashReporter() + let data = try NSData(contentsOfFile: absolutePath(podsJson), options: []) + let json = try JSONDecoder().decode([String: PodConfig].self, from: data as Data) + + let specifications = PodSpecification.resolve(with: json).sorted(by: { $0.name < $1.name }) + let compiler: (PodSpecification) throws -> Void = { specification in + print("Generating: \(specification.name)" + + (specification.subspecs.isEmpty ? "" : " \n\tsubspecs: " + specification.subspecs.joined(separator: " "))) + let podSpec: PodSpec + var podSpecJson: JSONDict? + if specification.podspec.hasSuffix(".json") { + let jsonData = try NSData(contentsOfFile: absolutePath(specification.podspec), options: []) as Data + let jsonFile = try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) + guard let jsonPodspec = jsonFile as? JSONDict else { + throw "Error parsing podspec at path \(specification.podspec)" + } + podSpec = try PodSpec(JSONPodspec: jsonPodspec) + podSpecJson = jsonPodspec + } else { + let jsonPodspec = try getJSONPodspec(shell: SystemShellContext(trace: false), + podspecName: specification.name, + path: absolutePath(specification.podspec)) + podSpec = try PodSpec(JSONPodspec: jsonPodspec) + podSpecJson = jsonPodspec + } + + // Consider adding a split here to split out sublibs + let skylarkString = PodBuildFile + .with(podSpec: podSpec, buildOptions: specification.toBuildOptions(src: src, ios: minIos)) + .compile() + + if printOutput { + print(skylarkString) + } + if !debug { + if specification.development && !FileManager.default.fileExists(atPath: absolutePath("Pods/\(specification.name)")) { + try? FileManager.default.createDirectory(atPath: absolutePath("Pods/\(specification.name)"), withIntermediateDirectories: false) + let contents = (try? FileManager.default.contentsOfDirectory(atPath: src)) ?? [] + contents.forEach({ file in + let sourcePath = absolutePath(file) + let symlinkPath = absolutePath("Pods/\(specification.name)/\(file)") + do { + try FileManager.default.createSymbolicLink(atPath: symlinkPath, withDestinationPath: sourcePath) + } catch { + print("Error creating symlink: \(error)") + } + }) + } + let filePath = "Pods/\(specification.name)/BUILD.bazel" + if let data = skylarkString.data(using: .utf8) { + try data.write(to: URL(fileURLWithPath: absolutePath(filePath))) + } else { + throw "Error writing file: \(filePath)" + } + if addPodspec, + let podSpecJson = podSpecJson, + let data = try? JSONSerialization.data(withJSONObject: podSpecJson, options: .prettyPrinted) { + try? data.write(to: URL(fileURLWithPath: absolutePath("Pods/\(specification.name)/\(specification.name).json"))) + } + } + } + + if concurrent { + let dGroup = DispatchGroup() + specifications.forEach({ specification in + dGroup.enter() + DispatchQueue.global().async { + do { + try compiler(specification) + } + catch { + print("Error generating \(specification.name): \(error)") + } + dGroup.leave() + } + }) + dGroup.wait() + } else { + specifications.forEach({ specification in + do { + try compiler(specification) + } + catch { + print("Error generating \(specification.name): \(error)") + } + }) + } + if !debug { + try Data().write(to: URL(fileURLWithPath: absolutePath("Pods/BUILD.bazel"))) + } + } + + func absolutePath(_ path: String) -> String { + guard !path.starts(with: "/") else { return path } + return (src as NSString).appendingPathComponent(path) + } + + func getJSONPodspec(shell: ShellContext, podspecName: String, path: String) throws -> JSONDict { + let jsonData: Data + // Check the path and child paths + let podspecPath = path + let currentDirectoryPath = src + if FileManager.default.fileExists(atPath: "\(podspecPath).json") { + jsonData = shell.command("/bin/cat", arguments: [podspecPath + ".json"]).standardOutputData + } else if FileManager.default.fileExists(atPath: podspecPath) { + // This uses the current environment's cocoapods installation. + let whichPod = shell.shellOut("which pod").standardOutputAsString + if whichPod.isEmpty { + throw "RepoTools requires a cocoapod installation on host" + } + let podBin = whichPod.components(separatedBy: "\n")[0] + let podResult = shell.command(podBin, arguments: ["ipc", "spec", podspecPath]) + guard podResult.terminationStatus == 0 else { + throw """ + PodSpec decoding failed \(podResult.terminationStatus) + stdout: \(podResult.standardOutputAsString) + stderr: \(podResult.standardErrorAsString) + """ + } + jsonData = podResult.standardOutputData + } else { + throw "Missing podspec ( \(podspecPath) ) inside \(currentDirectoryPath)" + } + + guard let JSONFile = try? JSONSerialization.jsonObject(with: jsonData, options: + JSONSerialization.ReadingOptions.allowFragments) as AnyObject, + let JSONPodspec = JSONFile as? JSONDict + else { + throw "Invalid JSON Podspec: (look inside \(currentDirectoryPath))" + } + return JSONPodspec + } +} diff --git a/Sources/Generator/main.swift b/Sources/Generator/main.swift new file mode 100644 index 0000000..e8891f0 --- /dev/null +++ b/Sources/Generator/main.swift @@ -0,0 +1,8 @@ +// +// main.swift +// PodToBUILD +// +// Created by Sergey Khliustin on 26.08.2022. +// + +RootCommand.main() diff --git a/Sources/ObjcSupport/CrashReporter.m b/Sources/ObjcSupport/CrashReporter.m new file mode 100644 index 0000000..1e44dbd --- /dev/null +++ b/Sources/ObjcSupport/CrashReporter.m @@ -0,0 +1,50 @@ +// +// CrashReporter.m +// PodToBUILD +// +// Created by Jerry Marino on 4/25/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +#import "CrashReporter.h" +#import +#import + +@implementation CrashReporter + + +void sigHandler(int signal) +{ + NSLog(@"Received signal: %d", signal); + void* callstack[128]; + int frames = backtrace(callstack, 128); + NSString *crashLogFilePath = @"/tmp/repo_tools_log.txt"; + const char* fileNameCString = [crashLogFilePath cStringUsingEncoding:NSUTF8StringEncoding]; + FILE* crashFile = fopen(fileNameCString, "w"); + short crashLogFileDescriptor = crashFile->_file; + backtrace_symbols_fd(callstack, frames, crashLogFileDescriptor); + exit(signal); +} + +- (id)init +{ + if (self = [super init]) { + // All of the signals + signal(SIGXFSZ, sigHandler); + signal(SIGXCPU, sigHandler); + signal(SIGALRM, sigHandler); + signal(SIGPIPE, sigHandler); + signal(SIGSYS, sigHandler); + signal(SIGSEGV, sigHandler); + signal(SIGBUS, sigHandler); + signal(SIGFPE, sigHandler); + signal(SIGEMT, sigHandler); + signal(SIGABRT, sigHandler); + signal(SIGTRAP, sigHandler); + signal(SIGILL, sigHandler); + signal(SIGQUIT, sigHandler); + } + return self; +} + +@end diff --git a/Sources/ObjcSupport/include/CrashReporter.h b/Sources/ObjcSupport/include/CrashReporter.h new file mode 100644 index 0000000..0993d17 --- /dev/null +++ b/Sources/ObjcSupport/include/CrashReporter.h @@ -0,0 +1,15 @@ +// +// CrashReporter.h +// PodToBUILD +// +// Created by Jerry Marino on 4/25/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +#import +// FIXME: is there a better way to do this? +#import + +@interface CrashReporter : NSObject + +@end diff --git a/Sources/ObjcSupport/include/ExceptionCatcher.h b/Sources/ObjcSupport/include/ExceptionCatcher.h new file mode 100644 index 0000000..5e0ea9c --- /dev/null +++ b/Sources/ObjcSupport/include/ExceptionCatcher.h @@ -0,0 +1,23 @@ +// +// ExceptionCatcher.h +// PodToBUILD +// +// Created by Jerry Marino on 10/27/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +// +// ExceptionCatcher.h +// + +#import + +NS_INLINE NSException * _Nullable tryBlock(void(^_Nonnull tryBlock)(void)) { + @try { + tryBlock(); + } + @catch (NSException *e) { + return e; + } + return nil; +} diff --git a/Sources/PodToBUILD/BuildFile.swift b/Sources/PodToBUILD/BuildFile.swift new file mode 100644 index 0000000..3c5eccb --- /dev/null +++ b/Sources/PodToBUILD/BuildFile.swift @@ -0,0 +1,88 @@ +// +// Pod.swift +// PodToBUILD +// +// Created by Jerry Marino on 4/14/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import Foundation + +public struct PodBuildFile: SkylarkConvertible { + /// Skylark Convertibles excluding prefix nodes. + /// @note Use toSkylark() to generate the actual BUILD file + let skylarkConvertibles: [SkylarkConvertible] + + private let options: BuildOptions + + /// Return the skylark representation of the entire BUILD file + func toSkylark() -> SkylarkNode { + let convertibleNodes: [SkylarkNode] = skylarkConvertibles.compactMap { $0.toSkylark() } + + return .lines([ + makeLoadNodes(forConvertibles: skylarkConvertibles) + ] + [ + ConfigSetting.makeConfigSettingNodes() + ] + convertibleNodes) + } + + public static func with(podSpec: PodSpec, + buildOptions: BuildOptions = + BasicBuildOptions.empty) -> PodBuildFile { + let libs = PodBuildFile.makeConvertables(fromPodspec: podSpec, buildOptions: buildOptions) + return PodBuildFile(skylarkConvertibles: libs, + options: buildOptions) + } + + func makeLoadNodes(forConvertibles skylarkConvertibles: [SkylarkConvertible]) -> SkylarkNode { + return .lines( + Set( + skylarkConvertibles + .compactMap({ $0 as? BazelTarget }) + .map({ $0.loadNode }) + .filter({ !$0.isEmpty }) + ) + .map({ SkylarkNode.skylark($0) }) + ) + } + + static func makeSourceLibs(spec: PodSpec, + subspecs: [PodSpec] = [], + deps: [BazelTarget] = [], + dataDeps: [BazelTarget] = [], + options: BuildOptions) -> [BazelTarget] { + return [ + AppleFramework(spec: spec, + subspecs: subspecs, + deps: Set((deps + dataDeps).map({ $0.name })), + options: options) + ] + } + + static func makeConvertables( + fromPodspec podSpec: PodSpec, + buildOptions: BuildOptions = BasicBuildOptions.empty + ) -> [SkylarkConvertible] { + let subspecs = podSpec.selectedSubspecs(subspecs: buildOptions.subspecs) + + let extraDeps = + AppleFrameworkImport.vendoredFrameworks(withPodspec: podSpec, subspecs: subspecs, options: buildOptions) + + ObjcImport.vendoredLibraries(withPodspec: podSpec, subspecs: subspecs) + + let sourceLibs = makeSourceLibs(spec: podSpec, + subspecs: subspecs, + deps: extraDeps, + options: buildOptions) + + var output: [BazelTarget] = sourceLibs + extraDeps + + output = UserConfigurableTransform.transform(convertibles: output, + options: buildOptions, + podSpec: podSpec) + return output + } + + public func compile() -> String { + return SkylarkCompiler(toSkylark()).run() + } +} diff --git a/Sources/PodToBUILD/BuildOptions.swift b/Sources/PodToBUILD/BuildOptions.swift new file mode 100644 index 0000000..5ff1838 --- /dev/null +++ b/Sources/PodToBUILD/BuildOptions.swift @@ -0,0 +1,70 @@ +// +// BuildOptions.swift +// PodToBUILD +// +// Created by Jerry Marino on 3/25/2020. +// Copyright © 2020 Pinterest Inc. All rights reserved. + +public protocol BuildOptions { + var podName: String { get } + var subspecs: [String] { get } + var podspecPath: String { get } + var sourcePath: String { get } + + var userOptions: [String] { get } + var globalCopts: [String] { get } + + var path: String { get } + var iosPlatform: String { get } + + var podBaseDir: String { get } + var genfileOutputBaseDir: String { get } +} + +public struct BasicBuildOptions: BuildOptions { + public let podName: String + public let subspecs: [String] + public let podspecPath: String + public let sourcePath: String + + public let userOptions: [String] + public let globalCopts: [String] + public let path: String + public let iosPlatform: String + + public init(podName: String = "", + subspecs: [String] = [], + podspecPath: String = "", + sourcePath: String = "", + path: String = ".", + userOptions: [String] = [], + globalCopts: [String] = [], + iosPlatform: String = "13.0" + ) { + self.podName = podName + self.subspecs = subspecs + self.path = path + self.podspecPath = podspecPath + self.sourcePath = sourcePath + self.userOptions = userOptions + self.globalCopts = globalCopts + self.iosPlatform = iosPlatform + } + + public static let empty = BasicBuildOptions(podName: "") + + public var podBaseDir: String { + return "Pods" + } + + public var genfileOutputBaseDir: String { + let basePath = "Pods" + let podName = podName + let parts = path.split(separator: "/") + if path == "." || parts.count < 2 { + return "\(basePath)/\(podName)" + } + + return String(parts[0..<2].joined(separator: "/")) + } +} diff --git a/Sources/PodToBUILD/Core/Magmas.swift b/Sources/PodToBUILD/Core/Magmas.swift new file mode 100644 index 0000000..74353fd --- /dev/null +++ b/Sources/PodToBUILD/Core/Magmas.swift @@ -0,0 +1,232 @@ +// +// Magmas.swift +// PodToBUILD +// +// Created by Brandon Kase on 4/28/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import Foundation + +/// Magmas are closed binary operators +/// Watch http://2017.funswiftconf.com/ (Brandon Kase) & (Brandon Williams) if you're confused + +infix operator<>: AdditionPrecedence +/// Law: x <> (y <> z) = (x <> y) <> z (associativity) +public protocol Semigroup { + static func<>(lhs: Self, rhs: Self) -> Self +} + +/// A wrapper that takes anything and makes it into something +/// that you can combine by ignoring the first thing +struct Last { let v: T; init(_ v: T) { self.v = v } } +extension Last: Semigroup { + static func<>(lhs: Last, rhs: Last) -> Last { + return rhs + } +} +/// A wrapper that takes anything and makes it into something +/// that you can combine by ignoring the second thing +struct First { let v: T; init(_ v: T) { self.v = v } } +extension First: Semigroup { + static func<>(lhs: First, rhs: First) -> First { + return lhs + } +} + +/// A type with some sort of Identity element +public protocol Identity { + static var empty: Self { get } +} + + +/// Monoids are the building blocks for clean, reusable, +/// composable code. +/// +/// Assuming you have an empty and a <> that obeys the laws +/// You can freely lift expressions into functions or lower +/// let bindings into subexpressions. You have ultimate power +/// +/// Law: empty <> x = x <> empty = x (identity) +public protocol Monoid: Semigroup, Identity {} + +public func mfold(_ monoids: [M]) -> M { + return monoids.reduce(M.empty){ $0 <> $1 } +} + +/// This is a hack since Swift doesn't have conditional conformances +infix operator<+>: AdditionPrecedence +public func<+> (lhs: T?, rhs: T?) -> T? { + switch (lhs, rhs) { + case (.none, _): return rhs + case (_, .none): return lhs + case let (.some(x), .some(y)): return .some(x <> y) + default: fatalError("Swift's exhaustivity checker is bad") + } +} +// induce the monoid with optional since swift can't handle +// option monoids +public func sfold(_ semigroups: [S?]) -> S? { + return semigroups.reduce(nil){ $0 <+> $1 } +} + +extension Array: Monoid { + public static func <>(lhs: Array, rhs: Array) -> Array { + return lhs + rhs + } + + public static var empty: Array { return [] } +} + +extension String: Monoid { + public static func <>(lhs: String, rhs: String) -> String { + return lhs + rhs + } + + public static var empty: String = "" +} + +extension Dictionary: Monoid { + /// Override with the stuff on the right + /// [b:1] <> [b:2] => [b:2] + public static func<>(lhs: Dictionary, rhs: Dictish) -> Dictionary where Dictish.Iterator.Element == Dictionary.Element { + return rhs.reduce(lhs) { (acc: Dictionary, kv: (Key, Value)) in + var d = acc + d[kv.0] = kv.1 + return d + } + } + + public static func<+>(lhs: Dictionary, rhs: Dictionary) -> Dictionary where Dictionary.Value == Array { + return rhs.reduce(lhs) { (acc: Dictionary, kv: (Key, Value)) in + var result = acc + if let current = result[kv.0] { + result[kv.0] = current + kv.1 + } else { + result[kv.0] = kv.1 + } + return result + } + + } + + public static var empty: Dictionary { return [:] } +} + +extension Optional: Monoid { + public static func <>(lhs: Optional, rhs: Optional) -> Optional { + switch (lhs, rhs) { + case (.none, _): return rhs + case (.some, .some): return lhs <> rhs + case (.some, _): return lhs + case (_, .some): return rhs + } + } + public static var empty: Optional { return nil } +} + +extension Set: Monoid { + public static var empty: Set { return [] } + + /// Override with the stuff on the right + /// [Fred1] <> [Fred2] => [Fred2] (assuming Fred1 == Fred2) + public static func<>(lhs: Set, rhs: Set) -> Set { + var base = Set() + rhs.forEach { base.insert($0) } + lhs.forEach { base.insert($0) } + return base + } +} + +/// Used when you need a monoid for some constraint but you really didn't care about the value +/// This is just a wrapper for the `()` type in Swift +struct Trivial { } +extension Trivial: Monoid { + static func<>(lhs: Trivial, rhs: Trivial) -> Trivial { + return Trivial() + } + + public static var empty: Trivial { return Trivial() } +} + +/// Law: forall x. x.isEmpty => x is `empty` +public protocol EmptyAwareness: Identity { + var isEmpty: Bool { get } +} + +/// If we have an optional of a monoid there are two empties +/// The empty for monoid or nil +/// normalize uses nil; denormalize uses the empty +extension Optional where Wrapped: Monoid & EmptyAwareness { + public func normalize() -> Optional { + return flatMap { $0.isEmpty ? nil : $0 } + } + + public func denormalize() -> Wrapped { + return self ?? Wrapped.empty + } +} + +extension String: EmptyAwareness {} + +extension Array: EmptyAwareness {} +extension Dictionary: EmptyAwareness {} +extension Optional { + public var isEmptyAwareEmpty: Bool { + return false + } +} + +extension Optional where Wrapped: EmptyAwareness { + public var isEmptyAwareEmpty: Bool { + switch self { + case .none: return true + case .some(let val): return val.isEmpty + } + } +} + + +extension Optional: EmptyAwareness { + public var isEmpty: Bool { + switch self { + case .none: return true + case .some: return self.isEmptyAwareEmpty + } + } +} + +extension Set: EmptyAwareness {} + +/// Lift a value into a public function that ignores it's input +/// Example: I have an (x: Int) +/// I want a (String) -> Int +/// const(x): (String) -> Int +public func const(_ b: @autoclosure @escaping () -> B) -> (A) -> B { + return { _ in b() } +} + +/// Pipe forward just lets you turn a pipeline of transformations +/// `f then g then h` from the illogical `h(g(f(x)))` to `x |> f |> g |> h` +/// Another way to think about it is "calling methods" with free public functions +precedencegroup PipeForward { + associativity: left + lowerThan: TernaryPrecedence + higherThan: AssignmentPrecedence +} +infix operator |>: PipeForward +public func |>(x: T, f: (T) -> U) -> U { + return f(x) +} + +public indirect enum Either { + case left(T) + case right(U) + + public func fold(left: (T) -> R, right: (U) -> R) -> R { + switch self { + case let .left(t): return left(t) + case let .right(u): return right(u) + } + } +} diff --git a/Sources/PodToBUILD/Core/MultiPlatform.swift b/Sources/PodToBUILD/Core/MultiPlatform.swift new file mode 100644 index 0000000..8796a0f --- /dev/null +++ b/Sources/PodToBUILD/Core/MultiPlatform.swift @@ -0,0 +1,416 @@ +// +// MultiPlatform.swift +// PodToBUILD +// +// Created by Brandon Kase on 4/28/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import Foundation + +enum SelectCase: String { + case ios = "iosCase" + case osx = "osxCase" + case watchos = "watchosCase" + case tvos = "tvosCase" + case fallback = "//conditions:default" +} + +typealias AttrSetConstraint = Monoid & SkylarkConvertible & EmptyAwareness + +struct MultiPlatform: Monoid, SkylarkConvertible, EmptyAwareness { + let ios: T? + let osx: T? + let watchos: T? + let tvos: T? + + static var empty: MultiPlatform { return MultiPlatform(ios: nil, osx: nil, watchos: nil, tvos: nil) } + + var isEmpty: Bool { + return (ios == nil || ios.isEmpty) + && (osx == nil || osx.isEmpty) + && (watchos == nil || watchos.isEmpty) + && (tvos == nil || tvos.isEmpty) + } + + // overwrites the value with the one on the right + static func<>(lhs: MultiPlatform, rhs: MultiPlatform) -> MultiPlatform { + return MultiPlatform( + ios: lhs.ios <+> rhs.ios, + osx: lhs.osx <+> rhs.osx, + watchos: lhs.watchos <+> rhs.watchos, + tvos: lhs.tvos <+> rhs.tvos + ) + } + + init(ios: T?, osx: T?, watchos: T?, tvos: T?) { + self.ios = ios.normalize() + self.osx = osx.normalize() + self.watchos = watchos.normalize() + self.tvos = tvos.normalize() + } + + init(ios: T?) { + self.ios = ios.normalize() + self.osx = nil + self.watchos = nil + self.tvos = nil + } + + init(osx: T?) { + self.osx = osx.normalize() + self.ios = nil + self.watchos = nil + self.tvos = nil + } + + init(watchos: T?) { + self.watchos = watchos.normalize() + self.ios = nil + self.osx = nil + self.tvos = nil + } + + init(tvos: T?) { + self.tvos = tvos.normalize() + self.ios = nil + self.osx = nil + self.watchos = nil + } + + init(value: T?) { + self.init(ios: value, osx: value, watchos: value, tvos: value) + } + + func map(_ transform: (T) -> U) -> MultiPlatform { + return MultiPlatform(ios: ios.map(transform), + osx: osx.map(transform), + watchos: watchos.map(transform), + tvos: tvos.map(transform)) + } + + func toSkylark() -> SkylarkNode { + precondition(ios != nil || osx != nil || watchos != nil || tvos != nil, "MultiPlatform empty can't be rendered") + + return .functionCall(name: "select", arguments: [.basic(( + osx.map { [":\(SelectCase.osx.rawValue)": $0] } <+> + watchos.map { [":\(SelectCase.watchos.rawValue)": $0] } <+> + tvos.map { [":\(SelectCase.tvos.rawValue)": $0] } <+> + // TODO: Change to T.empty and move ios up when we support other platforms + [SelectCase.fallback.rawValue: ios ?? T.empty ] ?? [:] + ).toSkylark())]) + } +} + +extension MultiPlatform: Equatable where T: AttrSetConstraint, T: Equatable { + static func == (lhs: MultiPlatform, rhs: MultiPlatform) -> Bool { + return lhs.ios == rhs.ios && + lhs.watchos == rhs.watchos && + lhs.tvos == rhs.tvos && + lhs.osx == rhs.osx + } +} + +struct AttrTuple: AttrSetConstraint { + let first: A? + let second: B? + + init(_ arg1: A?, _ arg2: B?) { + first = arg1 + second = arg2 + } + + static func <> (lhs: AttrTuple, rhs: AttrTuple) -> AttrTuple { + return AttrTuple( + lhs.first <+> rhs.first, + lhs.second <+> rhs.second + ) + } + + static var empty: AttrTuple { + return AttrTuple(nil, nil) + } + + var isEmpty: Bool { + return (first == nil || first.isEmpty) && + (second == nil || second.isEmpty) + } + + func toSkylark() -> SkylarkNode { + fatalError("You tried to toSkylark on a tuple (our domain modelling failed here :( )") + } +} + +struct AttrSet: Monoid, SkylarkConvertible, EmptyAwareness { + let basic: T? + let multi: MultiPlatform + + init(value: T?) { + self.basic = value.normalize() + self.multi = MultiPlatform(value: value) + } + + init(basic: T?) { + self.basic = basic.normalize() + multi = MultiPlatform.empty + } + + init(multi: MultiPlatform) { + basic = nil + self.multi = multi + } + + init(basic: T?, multi: MultiPlatform) { + self.basic = basic.normalize() + self.multi = multi + } + + func partition(predicate: @escaping (T) -> Bool) -> (AttrSet, AttrSet) { + return (self.filter(predicate), self.filter { x in !predicate(x) }) + } + + func map(_ transform: (T) -> U) -> AttrSet { + return AttrSet(basic: basic.map(transform), multi: multi.map(transform)) + } + + func filter(_ predicate: (T) -> Bool) -> AttrSet { + let basicPass = self.basic.map { predicate($0) ? $0 : T.empty } + let multiPass = self.multi.map { predicate($0) ? $0 : T.empty } + return AttrSet(basic: basicPass, multi: multiPass) + } + + func fold(basic: (T?) -> U, multi: (U, MultiPlatform) -> U) -> U { + return multi(basic(self.basic), self.multi) + } + + func trivialize(into accum: U, _ transform: ((inout U, T) -> Void)) -> U { + var mutAccum = accum + self.basic.map { transform(&mutAccum, $0) } + let multi = self.multi + multi.ios.map { transform(&mutAccum, $0) } + multi.tvos.map { transform(&mutAccum, $0) } + multi.osx.map { transform(&mutAccum, $0) } + multi.watchos.map { transform(&mutAccum, $0) } + return mutAccum + } + + func zip(_ other: AttrSet) -> AttrSet> { + return AttrSet>( + basic: AttrTuple(self.basic, other.basic), + multi: MultiPlatform>( + ios: AttrTuple(self.multi.ios, other.multi.ios), + osx: AttrTuple(self.multi.osx, other.multi.osx), + watchos: AttrTuple(self.multi.watchos, other.multi.watchos), + tvos: AttrTuple(self.multi.tvos, other.multi.tvos) + ) + ) + } + + static var empty: AttrSet { return AttrSet(basic: nil, multi: MultiPlatform.empty) } + + var isEmpty: Bool { + return (basic == nil || basic.isEmpty) + && (multi.isEmpty) + } + + static func<>(lhs: AttrSet, rhs: AttrSet) -> AttrSet { + return AttrSet( + basic: lhs.basic <+> rhs.basic, + multi: lhs.multi <> rhs.multi + ) + } + + func toSkylark() -> SkylarkNode { + switch basic { + case .none where multi.isEmpty: return T.empty.toSkylark() + case let .some(b) where multi.isEmpty: return b.toSkylark() + case .none: return multi.toSkylark() + case let .some(b): return b.toSkylark() .+. multi.toSkylark() + } + } +} + +extension AttrSet { + /// Sequences a list of `AttrSet`s to a list of each input's value + func sequence(_ input: [AttrSet]) -> AttrSet<[T]> { + return ([self] + input).reduce(AttrSet<[T]>.empty) { + accum, next -> AttrSet<[T]> in + return accum.zip(next).map { zip in + let first = zip.first ?? [] + guard let second = zip.second else { + return first + } + return first + [second] + } + } + } +} + +extension MultiPlatform where T == Optional { + func denormalize() -> MultiPlatform { + return self.map { $0.denormalize() } + } +} +extension AttrSet where T == Optional { + func denormalize() -> AttrSet { + return self.map { $0.denormalize() } + } +} + +extension AttrSet { + /// This makes all the code operate on a multi platform + func unpackToMulti() -> AttrSet { + if let basic = self.basic { + return AttrSet(multi: MultiPlatform( + ios: basic <+> self.multi.ios, + osx: basic <+> self.multi.osx, + watchos: basic <+> self.multi.watchos, + tvos: basic <+> self.multi.tvos + )) + } + return self + } +} + + +extension AttrSet: Equatable where T: AttrSetConstraint, T: Equatable { + static func == (lhs: AttrSet, rhs: AttrSet) -> Bool { + return lhs.basic == rhs.basic && + lhs.multi == rhs.multi + } +} + +extension AttrSet where T: AttrSetConstraint, T: Equatable { + func flattenToBasicIfPossible() -> AttrSet { + if self.isEmpty { + return self + } + if self.basic == nil && self.multi.ios == self.multi.osx && + self.multi.osx == self.multi.watchos && + self.multi.watchos == self.multi.tvos { + return AttrSet(basic: self.multi.ios) + } + return self + } + + /// For simplicity of the BUILD file, we'll condense if all is the same + func toSkylark() -> SkylarkNode { + let renderable = self.flattenToBasicIfPossible() + switch renderable.basic { + case .none where renderable.multi.isEmpty: return T.empty.toSkylark() + case let .some(b) where renderable.multi.isEmpty: return b.toSkylark() + case .none: return renderable.multi.toSkylark() + case let .some(b): return b.toSkylark() .+. renderable.multi.toSkylark() + } + } +} + +extension Dictionary { + init(tuples: S) where S.Iterator.Element == (Key, Value) { + self = tuples.reduce([:]) { d, t in d <> [t.0:t.1] } + } +} + +// Because we don't have conditional conformance we have to specialize these +extension Optional where Wrapped == Array { + static func == (lhs: Optional, rhs: Optional) -> Bool { + switch (lhs, rhs) { + case (.none, .none): return true + case let (.some(x), .some(y)): return x == y + case (_, _): return false + } + } +} + +extension MultiPlatform where T == [String] { + static func == (lhs: MultiPlatform, rhs: MultiPlatform) -> Bool { + return lhs.ios == rhs.ios && lhs.osx == rhs.osx && lhs.watchos == rhs.watchos && lhs.tvos == rhs.tvos + } + + func sorted(by areInIncreasingOrder: (String, String) throws -> Bool) rethrows +-> MultiPlatform { + return try MultiPlatform( + ios: ios?.sorted(by: areInIncreasingOrder), + osx: osx?.sorted(by: areInIncreasingOrder), + watchos: watchos?.sorted(by: areInIncreasingOrder), + tvos: tvos?.sorted(by: areInIncreasingOrder) + ) + } +} + +extension MultiPlatform where T == Set { + static func == (lhs: MultiPlatform, rhs: MultiPlatform) -> Bool { + return lhs.ios == rhs.ios && lhs.osx == rhs.osx && lhs.watchos == rhs.watchos && lhs.tvos == rhs.tvos + } +} + +extension AttrSet where T == [String] { + static func == (lhs: AttrSet, rhs: AttrSet) -> Bool { + return lhs.basic == rhs.basic && lhs.multi == rhs.multi + } + + func sorted(by areInIncreasingOrder: (String, String) throws -> Bool) rethrows +-> AttrSet { + return try AttrSet( + basic: basic?.sorted(by: areInIncreasingOrder), + multi: multi.sorted(by: areInIncreasingOrder) + ) + } +} + +extension AttrSet where T == Set { + static func == (lhs: AttrSet, rhs: AttrSet) -> Bool { + return lhs.basic == rhs.basic && lhs.multi == rhs.multi + } +} +extension PodSpec { + func attr(_ keyPath: KeyPath) -> AttrSet { + return getAttrSet(spec: self, keyPath: keyPath) + } + + func collectAttribute(with subspecs: [PodSpec] = [], + keyPath: KeyPath) -> AttrSet> { + return (subspecs + [self]) + .reduce(into: AttrSet>.empty) { partialResult, spec in + partialResult = partialResult <> spec.attr(keyPath).unpackToMulti().map({ Set($0) }) + } + } + + func collectAttribute(with subspecs: [PodSpec] = [], + keyPath: KeyPath) -> AttrSet<[String: [String]]> { + return (subspecs + [self]) + .reduce(into: AttrSet<[String: [String]]>.empty) { partialResult, spec in + partialResult = partialResult.zip(spec.attr(keyPath).unpackToMulti()).map({ + ($0.first ?? [:]) <+> ($0.second ?? [:]) + }) + } + } + + func collectAttribute(with subspecs: [PodSpec] = [], + keyPath: KeyPath) -> AttrSet<[String: String]> { + return (subspecs + [self]) + .reduce(into: AttrSet<[String: String]>.empty) { partialResult, spec in + partialResult = partialResult.zip(spec.attr(keyPath).unpackToMulti()).map({ + if let second = $0.second { + return ($0.first ?? [:]) <> (second ?? [:]) + } else { + return $0.first ?? [:] + } + }) + } + } +} + +// for extracting attr sets +// The key reason that we have this code is to: +// - merge the spec.ios.attr spec.attr +func getAttrSet(spec: PodSpec, keyPath: KeyPath) -> AttrSet { + let value = spec[keyPath: keyPath] + return AttrSet(basic: value) <> AttrSet(multi: MultiPlatform( + ios: spec.ios?[keyPath: keyPath], + osx: spec.osx?[keyPath: keyPath], + watchos: spec.watchos?[keyPath: keyPath], + tvos: spec.tvos?[keyPath: keyPath]) + ) +} + diff --git a/Sources/PodToBUILD/GlobNode.swift b/Sources/PodToBUILD/GlobNode.swift new file mode 100644 index 0000000..69a432d --- /dev/null +++ b/Sources/PodToBUILD/GlobNode.swift @@ -0,0 +1,231 @@ +// +// GlobNode.swift +// PodToBUILD +// +// Created by Jerry Marino on 05/21/18. +// Copyright © 2020 Pinterest Inc. All rights reserved. +// + +import Foundation + +public struct GlobNode: SkylarkConvertible { + // Bazel Glob function: glob(include, exclude=[], exclude_directories=1) + public let include: [Either, GlobNode>] + public let exclude: [Either, GlobNode>] + public let excludeDirectories: Bool = true + static let emptyArg: Either, GlobNode> + = Either.left(Set([String]())) + + public init(include: Set = Set(), exclude: Set = Set()) { + self.init(include: [.left(include)], exclude: [.left(exclude)]) + } + + public init(include: Either, GlobNode>, exclude: Either, GlobNode>) { + self.init(include: [include], exclude: [exclude]) + } + + public init(include: [Either, GlobNode>] = [], exclude: [Either, GlobNode>] = []) { + // Upon allocation, form the most simple version of the glob + self.include = include.simplify() + self.exclude = exclude.simplify() + } + + public func toSkylark() -> SkylarkNode { + // An empty glob doesn't need to be rendered + guard isEmpty == false else { + return .empty + } + + let include = self.include + let exclude = self.exclude + let includeArgs: [SkylarkFunctionArgument] = [ + .basic(include.reduce(SkylarkNode.empty) { + $0 .+. $1.toSkylark() + }), + ] + + // If there's no excludes omit the argument + let excludeArgs: [SkylarkFunctionArgument] = exclude.isEmpty ? [] : [ + .named(name: "exclude", value: exclude.reduce(SkylarkNode.empty) { + $0 .+. $1.toSkylark() + }), + ] + + // Omit the default argument for exclude_directories + let dirArgs: [SkylarkFunctionArgument] = self.excludeDirectories ? [] : [ + .named(name: "exclude_directories", + value: .int(self.excludeDirectories ? 1 : 0)), + ] + + return .functionCall(name: "glob", + arguments: includeArgs + excludeArgs + dirArgs) + } +} + +extension Either: Equatable where T == Set, U == GlobNode { + public static func == (lhs: Either, rhs: Either) -> Bool { + if case let .left(lhsL) = lhs, case let .left(rhsL) = rhs { + return lhsL == rhsL + } + if case let .right(lhsR) = lhs, case let .right(rhsR) = rhs { + return lhsR == rhsR + } + if lhs.isEmpty && rhs.isEmpty { + return true + } + return false + } + + public func map(_ transform: (String) -> String) -> Either, GlobNode> { + switch self { + case let .left(setVal): + return .left(Set(setVal.map(transform))) + case let .right(globVal): + return .right(GlobNode( + include: globVal.include.map { + $0.map(transform) + }, exclude: globVal.exclude.map { + $0.map(transform) + } + )) + } + } + + public func compactMapInclude(_ transform: (String) -> String?) -> Either, GlobNode> { + switch self { + case let .left(setVal): + return .left(Set(setVal.compactMap(transform))) + case let .right(globVal): + let inc = globVal.include.compactMap({ + $0.compactMapInclude(transform) + }) + return .right(GlobNode( + include: inc, exclude: globVal.exclude)) + } + } + +} + +extension Array where Iterator.Element == Either, GlobNode> { + var isEmpty: Bool { + return self.reduce(true) { + $0 == false ? $0 : $1.isEmpty + } + } + + public func simplify() -> [Either, GlobNode>] { + // First simplify the elements and then filter the empty elements + return self + .map { $0.simplify() } + .filter { !$0.isEmpty } + } +} + +extension Either where T == Set, U == GlobNode { + var isEmpty: Bool { + switch self { + case let .left(val): + return val.isEmpty + case let .right(val): + return val.isEmpty + } + } + + public func simplify() -> Either, GlobNode> { + // Recursivly simplfies the globs + switch self { + case let .left(val): + // Base case, this is as simple as it gets + return .left(val) + case let .right(val): + let include = val.include.simplify() + let exclude = val.exclude.simplify() + if exclude.isEmpty { + // When there is no excludes we can do the following: + // 1. smash all sets into a single set + // 2. return a set if there are no other globs + // 3. otherwise, return a simplified glob with 1 set and + // remaining globs + var setAccum: Set = Set() + let remainingGlobs = include + .reduce(into: [Either, GlobNode>]()) { + accum, next in + switch next { + case let .left(val): + setAccum = setAccum <> val + case let .right(val): + if !val.isEmpty { + accum.append(next) + } + } + } + + // If there are no remaining globs, simplify to a set + if remainingGlobs.count == 0 { + return .left(setAccum) + } else { + return .right(GlobNode(include: remainingGlobs + [.left(setAccum)])) + } + } else { + return .right(GlobNode(include: include, exclude: exclude)) + } + } + } +} + +extension GlobNode: Equatable { + public static func == (lhs: GlobNode, rhs: GlobNode) -> Bool { + return lhs.include == rhs.include + && lhs.exclude == rhs.exclude + } +} + +extension GlobNode: EmptyAwareness { + public var isEmpty: Bool { + // If the include is the same as the exclude then it's empty + return self.include.isEmpty || self.include == self.exclude + } + + public static var empty: GlobNode { + return GlobNode(include: Set(), exclude: Set()) + } +} + +extension GlobNode: Monoid { + public static func <> (_: GlobNode, _: GlobNode) -> GlobNode { + // Currently, there is no way to implement this reasonablly + fatalError("cannot combine GlobNode ( added for AttrSet )") + } +} + + +extension GlobNode { + /// Evaluates the glob for all the sources on disk + public func sourcesOnDisk() -> Set { + let includedFiles = self.include.reduce(into: Set()) { + accum, next in + switch next { + case .left(let setVal): + setVal.forEach { podGlob(pattern: $0).forEach { accum.insert($0) } } + case .right(let globVal): + globVal.sourcesOnDisk().forEach { accum.insert($0) } + } + } + + let excludedFiles = self.exclude.reduce(into: Set()) { + accum, next in + switch next { + case .left(let setVal): + setVal.forEach { podGlob(pattern: $0).forEach { accum.insert($0) } } + case .right(let globVal): + globVal.sourcesOnDisk().forEach { accum.insert($0) } + } + } + return includedFiles.subtracting(excludedFiles) + } + + func hasSourcesOnDisk() -> Bool { + return sourcesOnDisk().count > 0 + } +} + diff --git a/Sources/PodToBUILD/GlobUtils.swift b/Sources/PodToBUILD/GlobUtils.swift new file mode 100644 index 0000000..7876e89 --- /dev/null +++ b/Sources/PodToBUILD/GlobUtils.swift @@ -0,0 +1,572 @@ +// +// GlobUtils.swift +// PodToBUILD +// +// Created by Jerry Marino on 4/18/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import Foundation +import Darwin + +// ============================================ +// +// Created by Eric Firestone on 3/22/16. +// Copyright © 2016 Square, Inc. All rights reserved. +// Released under the Apache v2 License. +// +// Adapted from https://gist.github.com/efirestone/ce01ae109e08772647eb061b3bb387c3 +// Adapted again from https://github.com/Bouke/Glob/blob/master/Sources/Glob.swift + +public let GlobBehaviorBashV3 = Glob.Behavior( + supportsGlobstar: false, + includesFilesFromRootOfGlobstar: false, + includesDirectoriesInResults: true, + includesFilesInResultsIfTrailingSlash: false +) +public let GlobBehaviorBashV4 = Glob.Behavior( + supportsGlobstar: true, // Matches Bash v4 with "shopt -s globstar" option + includesFilesFromRootOfGlobstar: true, + includesDirectoriesInResults: true, + includesFilesInResultsIfTrailingSlash: false +) +public let GlobBehaviorGradle = Glob.Behavior( + supportsGlobstar: true, + includesFilesFromRootOfGlobstar: true, + includesDirectoriesInResults: false, + includesFilesInResultsIfTrailingSlash: true +) + +/** + Finds files on the file system using pattern matching. + */ +public class Glob: Collection { + + /** + * Different glob implementations have different behaviors, so the behavior of this + * implementation is customizable. + */ + public struct Behavior { + // If true then a globstar ("**") causes matching to be done recursively in subdirectories. + // If false then "**" is treated the same as "*" + let supportsGlobstar: Bool + + // If true the results from the directory where the globstar is declared will be included as well. + // For example, with the pattern "dir/**/*.ext" the fie "dir/file.ext" would be included if this + // property is true, and would be omitted if it's false. + let includesFilesFromRootOfGlobstar: Bool + + // If false then the results will not include directory entries. This does not affect recursion depth. + let includesDirectoriesInResults: Bool + + // If false and the last characters of the pattern are "**/" then only directories are returned in the results. + let includesFilesInResultsIfTrailingSlash: Bool + } + + public static var defaultBehavior = GlobBehaviorBashV4 + + private var isDirectoryCache = [String: Bool]() + + public let behavior: Behavior + var paths = [String]() + public var startIndex: Int { return paths.startIndex } + public var endIndex: Int { return paths.endIndex } + + public init(pattern: String, behavior: Behavior = Glob.defaultBehavior) { + + self.behavior = behavior + + var adjustedPattern = pattern + let hasTrailingGlobstarSlash = pattern.hasSuffix("**/") + var includeFiles = !hasTrailingGlobstarSlash + + if behavior.includesFilesInResultsIfTrailingSlash { + includeFiles = true + if hasTrailingGlobstarSlash { + // Grab the files too. + adjustedPattern += "*" + } + } + let patterns = behavior.supportsGlobstar ? expandGlobstar(pattern: adjustedPattern) : [adjustedPattern] + + for pattern in patterns { + var gt = glob_t() + if executeGlob(pattern: pattern, gt: >) { + populateFiles(gt: gt, includeFiles: includeFiles) + } + + globfree(>) + } + + paths = Array(Set(paths)).sorted { lhs, rhs in + lhs.compare(rhs) != ComparisonResult.orderedDescending + } + + clearCaches() + } + + // MARK: Private + + private var globalFlags = GLOB_TILDE | GLOB_BRACE | GLOB_MARK + + private func executeGlob(pattern: UnsafePointer, gt: UnsafeMutablePointer) -> Bool { + return 0 == glob(pattern, globalFlags, nil, gt) + } + + private func expandGlobstar(pattern: String) -> [String] { + guard pattern.contains("**") else { + return [pattern] + } + + var results = [String]() + var parts = pattern.components(separatedBy: "**") + var firstPart = parts.removeFirst() + var lastPart = parts.joined(separator: "**") + + if firstPart == "" { + firstPart = "." + } + let fileManager = FileManager.default + + var directories: [String] + // If using an empty filepath then no need to look for it + if !firstPart.isEmpty { + do { + directories = try fileManager.subpathsOfDirectory(atPath: firstPart).compactMap { subpath in + let fullPath = NSString(string: firstPart).appendingPathComponent(subpath) + var isDirectory = ObjCBool(false) + if fileManager.fileExists(atPath: fullPath, isDirectory: &isDirectory) && isDirectory.boolValue { + return fullPath + } else { + return nil + } + } + } catch { + directories = [] + fputs("Error parsing file system item: \(error)", __stderrp) + } + } else { + directories = [] + fputs("Error parsing file system item: EMPTY\n", __stderrp) + } + + if behavior.includesFilesFromRootOfGlobstar { + // Check the base directory for the glob star as well. + directories.insert(firstPart, at: 0) + + // Include the globstar root directory ("dir/") in a pattern like "dir/**" or "dir/**/" + if lastPart.isEmpty { + results.append(firstPart) + } + } + + if lastPart.isEmpty { + lastPart = "*" + } + for directory in directories { + let partiallyResolvedPattern = NSString(string: directory).appendingPathComponent(lastPart) + results.append(contentsOf: expandGlobstar(pattern: partiallyResolvedPattern)) + } + + return results + } + + private func isDirectory(path: String) -> Bool { + var isDirectory = isDirectoryCache[path] + if let isDirectory = isDirectory { + return isDirectory + } + + var isDirectoryBool = ObjCBool(false) + isDirectory = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectoryBool) && isDirectoryBool.boolValue + isDirectoryCache[path] = isDirectory! + + return isDirectory! + } + + private func clearCaches() { + isDirectoryCache.removeAll() + } + + private func populateFiles(gt: glob_t, includeFiles: Bool) { + let includeDirectories = behavior.includesDirectoriesInResults + + for i in 0 ..< Int(gt.gl_matchc) { + if let path = String(validatingUTF8: gt.gl_pathv[i]!) { + if !includeFiles || !includeDirectories { + let isDirectory = self.isDirectory(path: path) + if (!includeFiles && !isDirectory) || (!includeDirectories && isDirectory) { + continue + } + } + + paths.append(path) + } + } + } + + // MARK: Subscript Support + + public subscript(i: Int) -> String { + return paths[i] + } + + // MARK: IndexableBase + + public func index(after i: Glob.Index) -> Glob.Index { + return i + 1 + } +} + +// ============================================ + +// Glob +// Return True if the pattern contains the needle +func glob(pattern: String, contains needle: String) -> Bool { + guard let regex = try? NSRegularExpression(withGlobPattern: pattern) else { + return false + } + let match = regex.matches(in: needle) + return match.count != 0 +} + +// Grammar for RubyGlob pieces +enum RubyGlobChunk { + case Wild // * + case DirWild // ** + case DotStarWild // .* + case CharWild // just one character but anything + case Either(Set) // matches any string here + case Str(String) +} +extension RubyGlobChunk: Equatable { + static func ==(lhs: RubyGlobChunk, rhs: RubyGlobChunk) -> Bool { + switch (lhs, rhs) { + case (.Wild, .Wild), (.DirWild, .DirWild), (.CharWild, .CharWild): return true + case let (.Either(s1), .Either(s2)): + return s1 == s2 + case let (.Str(s1), .Str(s2)): + return s1 == s2 + default: + return false + } + } +} + +// Bazel supports only a subset of Ruby's globs +enum BazelGlobChunk { + case Wild + case DirWild + case DotStarWild + case Str(String) +} +extension BazelGlobChunk: Equatable { + static func ==(lhs: BazelGlobChunk, rhs: BazelGlobChunk) -> Bool { + switch (lhs, rhs) { + case (.Wild, .Wild), (.DirWild, .DirWild): return true + case let (.Str(s1), .Str(s2)): return s1 == s2 + default: return false + } + } + + func hasSuffix(_ suffix: String) -> Bool { + switch self { + case .Wild, .DotStarWild, .DirWild: + return false + case let .Str(s): + return s.hasSuffix(suffix) + } + } +} + + +// * is Wild +let star = Parsers.just("*") +let parseWild: Parser = star.map{ _ in RubyGlobChunk.Wild } +// ** is DirWild +let starstar = Parsers.prefix(["*", "*"]) +let parseDirWild: Parser = starstar.map{ _ in RubyGlobChunk.DirWild } + +// .* +let dotStar = Parsers.prefix([".", "*"]) +let parseDotStarWild: Parser = dotStar.map{ _ in RubyGlobChunk.DotStarWild } + +// ? is CharWild +let questionmark = Parsers.just("?") +let parseCharWild: Parser = questionmark.map{ _ in RubyGlobChunk.CharWild } + +// {h, y, z} yields Either(["h", "y", "z"]) +let comma = Parsers.just(",") +// one piece of the {} curly set +let curlyChunk = Parsers.anyChar.butNot("}") // need to exclude "}" to counter greediness + .manyUntil(terminator: comma.forget) +// matches 1, 2, 3 +let curlyChunks = curlyChunk + .rep(separatedBy: (comma.andThen{ Parsers.whitespace.many() }).forget) +// now we wrap with { } +let parseCurlySet: Parser = + curlyChunks + .wrapped(startingWith: "{", endingWith: "}") + .map{ css in RubyGlobChunk.Either(Set(css.map{ String($0) })) } + +// [hm] yields Either(["h", "m"]) +let parseCharacterSet: Parser = + Parsers.anyChar.butNot("]") // a single character (counter greediness by excluding ]) + .manyUntil(terminator: Parsers.just("]").forget) // repeated + .wrapped(startingWith: "[", endingWith: "]") // wrapped up + .map{ cs in RubyGlobChunk.Either(Set(cs.map{ String($0) }))} + +// Either can be {h,m} or [hm] +let parseEither = parseCurlySet.orElse(parseCharacterSet) + +// Special glob chunks are everything except plain strings +// we need to check these first and fallthrough. +// The order matters: We must check DirWild before Wild or +// we will parse incorrectly. +let parseSpecial = + Parser.first([ + parseDirWild, // ** + parseDotStarWild, // ** + parseWild, // * + parseCharWild, // ? + parseEither // [hm] + ]) + +// A non-empty group of repeated characters (until hitting a special sequence of +// tokens) forms a string in the regex. +// NOTE: This is strictly MORE powerful than Ruby since we will parse "abc[" +// but Cocoapods will have valid regexes so this isn't be a problem. +let parseStr: Parser = + Parsers.anyChar.manyUntil(terminator: parseSpecial.forget, atLeast: 1) + .map{ cs in RubyGlobChunk.Str(String(cs)) } + +// One chunk of ruby is either a special chunk or a string +let parseOneRubyChunk: Parser = + Parser.first([ + parseSpecial, + parseStr + ]) + +// A glob is a repeated sequence of ruby chunks +let parseGlob: Parser<[RubyGlobChunk]> = parseOneRubyChunk.many() + +extension Sequence where Iterator.Element == RubyGlobChunk { + // This takes every non-deterministic path and replaces with all cases + // outer list is different regexes + // inner list is one regex + func toBazelChunks() -> [[BazelGlobChunk]] { + // TODO: Use linked list + // The runtime complexity of this transformation is abhorrent + return self.reduce([[]]) { (acc, x) in + let foo: RubyGlobChunk = x + switch foo { + case .Wild: + return acc.map{ cs in cs + [BazelGlobChunk.Wild] } + case .DotStarWild: + return acc.map{ cs in cs + [BazelGlobChunk.DotStarWild] } + case .DirWild: + return acc.map{ cs in cs + [BazelGlobChunk.DirWild] } + case .CharWild: + fatalError("Unsupported chunk (CharWild)") + case let .Either(strs): + return acc.flatMap { cs in + // cs is one different regex + // for every char we want to make a new regex + strs.map{ str in + cs + [BazelGlobChunk.Str(str)] + } + } + case let .Str(str): + return acc.map { cs in cs + [BazelGlobChunk.Str(str)]} + } + } + } +} + +extension Sequence where Iterator.Element == BazelGlobChunk { + // [.Str(a), .Str(b)] means the same as .Str(a + b) + // We can simplify our representation by performing that transformation on our IR + // This means the .Str constructor is a string concat homomorphism + var simplify: [BazelGlobChunk] { + return self.reduce([BazelGlobChunk]()) { (acc, x) in + switch (acc.last, x) { + case let (.some(.Str(s1)), .Str(s2)): + var accCopy = acc + accCopy[acc.count-1] = .Str(s1 + s2) + return accCopy + default: + return acc + [x] + } + } + } + + var bazelString: String { + return self.reduce("") { (acc, x) in + switch x { + case .Wild: + return acc + "*" + case .DotStarWild: + // represent DotStarWild as a file extension. + return acc + case .DirWild: + return acc + "**" + case let .Str(str): + return acc + str + } + } + } +} + +// Pattern ( Bazel Specific ) +// Return all subsets of the pattern for a given file type +// +// Background: +// In PodSpec, there are heterogeneous file globs, for example Some/*.{h, m} +// or Some/**/* +// +// In Bazel, we need to specify specific subsets of the files in various parts +// of the BUILD file. +// +// For example, we can only use .h files in headers and .m files in srcs +// +// Additionally, we need to create multiple insertions for each file type since +// Bazel does not support character group globs +public func pattern(fromPattern pattern: String, includingFileTypes fileTypes: Set) -> [String] { + // if we have a proper ruby pattern, + if let arr = parseGlob.parseFully(Array(pattern)) { + // Remove empty empty chunks + let filtered: [[BazelGlobChunk]] = arr.toBazelChunks() + .map{ $0.simplify } + .filter{ (bazelChunks: [BazelGlobChunk]) -> Bool in + bazelChunks.count != 0 && bazelChunks[0] != BazelGlobChunk.Str("") + } + + // Allow all file types + if fileTypes.count == 0 { + let strs: [String] = filtered + .map{ (glob: [BazelGlobChunk]) -> String in glob.bazelString } + return Array(Set(strs)) + } + // In Bazel, to keep things simple, we want all patterns to end in an extension + // Unfortunately, Cocoapods patterns could end in anything, so we need to fix them all + let suffixFixed: [[BazelGlobChunk]] = filtered + .flatMap{ (bazelGlobChunks: [BazelGlobChunk]) -> [[BazelGlobChunk]] in + let lastChunk = bazelGlobChunks.last! // count != 0 already filtered out + switch lastChunk { + // ending in * needs to map to *.m (except .*) + case .Wild, .DotStarWild: + let lastTwo = Array(bazelGlobChunks.suffix(2)) + // if we end in .* + if (lastTwo.count == 2 && lastTwo[0].hasSuffix(".")) { + return fileTypes.map{ + // strip the last * and replace with the extension + bazelGlobChunks.prefix(upTo: bazelGlobChunks.count-1) + [BazelGlobChunk.Str($0)] + } + } else { + // otherwise we can just append the extension + return fileTypes.map{ bazelGlobChunks + [BazelGlobChunk.Str($0)] } + } + // ending in ** needs to map to **/*.m + case .DirWild: + return fileTypes.map{ bazelGlobChunks + [.Str("/"), .Wild, .Str($0)] } + // ending in a string if that string doesn't have an extension + // needs the full /**/*.m + case let .Str(s): + let allButLast = bazelGlobChunks.count >= 2 ? bazelGlobChunks.prefix(upTo: bazelGlobChunks.count-1) : [] + // assume that alphanumeric suffix is an extension + let regex = try! NSRegularExpression(pattern: "\\.[^/]*$", options: []) + return fileTypes.map{ (fileType: String) -> [BazelGlobChunk] in + return regex.matches(in: s).count > 0 ? + allButLast + [lastChunk] : + allButLast + [lastChunk, .Str("/"), .DirWild, .Str("/"), .Wild, .Str(fileType)] + } + } + } + + // compile the bazel chunks + let strs: [String] = suffixFixed + .map{ (glob: [BazelGlobChunk]) -> String in glob.bazelString } + + return Array(Set(strs)) + // Only include patterns that contain the suffixes we care about + .filter{ (bazelRegex: String) -> Bool in + // if at least one of the filetypes we're checking matches then we're good + (fileTypes.filter { fileType in + bazelRegex.hasSuffix(fileType) + }).count > 0 + } + } else { + return [] + } +} + +public func extractResources(patterns: Set) -> Set { + return Set(extractResources(patterns: Array(patterns))) +} + +public func extractResources(patterns: [String]) -> [String] { + return patterns.flatMap { (p: String) -> [String] in + pattern(fromPattern: p, includingFileTypes: []).map({ + var components = $0.components(separatedBy: "/") + if let last = components.last { + var fixed = last + .replacingOccurrences(of: "xcassets", with: "xcassets/**") +// .replacingOccurrences(of: "lproj", with: "lproj") + if fixed.isEmpty || fixed == "*" { + fixed = "**" + } + components.removeLast() + components.append(fixed) + } + return components.joined(separator: "/") + }) + } +} + +// Glob with the semantics of pod `source_file` globs. +// @note the original PodSpec globs are based on the ruby glob semantics +public func podGlob(pattern: String) -> [String] { + return Glob(pattern: pattern, behavior: GlobBehaviorBashV4).paths +} + +// MARK: - NSRegularExpression + +extension NSRegularExpression { + + // Convert a glob to a regex + class func pattern(withGlobPattern pattern: String) -> String { + var regexStr = "" + + for idx in pattern.indices { + let c = pattern[idx] + if c == "*" { + regexStr.append(".*") + } else if c == "?" { + regexStr.append(".") + } else if c == "[" { + regexStr.append("[") + } else if c == "]" { + regexStr.append("[") + } else if c == "{" { + regexStr.append("[") + } else if c == "}" { + regexStr.append("]") + } else { + regexStr.append(c) + } + } + return regexStr + } + + convenience init(withGlobPattern pattern: String) throws { + let globPattern = NSRegularExpression.pattern(withGlobPattern: pattern) + try self.init(pattern: globPattern) + } + + func matches(in text: String) -> [String] { + let nsString = text as NSString + let results = matches(in: text, range: NSRange(location: 0, length: nsString.length)) + return results.map { nsString.substring(with: $0.range) } + } +} + diff --git a/Sources/PodToBUILD/Parser.swift b/Sources/PodToBUILD/Parser.swift new file mode 100644 index 0000000..8e2a792 --- /dev/null +++ b/Sources/PodToBUILD/Parser.swift @@ -0,0 +1,194 @@ +// +// Parser.swift +// PodToBUILD +// +// Created by Brandon Kase on 9/8/17. +// Copyright © 2017 Pinterest. All rights reserved. +// + +import Foundation + +// A simple Parser combinator library + +/** A Parser is a wrapper around a function: + * `let run: ([Character]) -> (A, [Character])?` + * in English: Given a stream of input, try to parse an A: + * either fail (return nil); + * or succeed and provide the A and the stream that hasn't been consumed + */ +struct Parser { + let run: ([Character]) -> (A, [Character])? + + /// Run the parser; ensure that the result stream is empty or else fail + func parseFully(_ s: [Character]) -> A? { + if let (a, rest) = self.run(s), rest.count == 0 { + return a + } else { + return nil + } + } + + func map(_ f: @escaping (A) -> B) -> Parser { + return Parser{ s in + if let (a, rest) = self.run(s) { + return (f(a), rest) + } else { + return nil + } + } + } + + func flatMap(_ f: @escaping (A) -> Parser) -> Parser { + return Parser { s in + if let (a, rest) = self.run(s) { + return f(a).run(rest) + } else { + return nil + } + } + } + + /// flatMap but ignore your input + func andThen(_ next: @escaping () -> Parser) -> Parser { + return self.flatMap { _ in next() } + } + + /// A Parser that always fails + static func fail() -> Parser { + return Parser { _ in nil } + } + + /// A Parser that always succeeds and returns `x` + static func trivial(_ x: A) -> Parser { + return Parser { s in (x, s) } + } + + /// Forget the data structure you've parsed + var forget: Parser<()> { + return self.map{ _ in () } + } + + /// Try this parser, but if it fails, try `p` before truly failing + func orElse(_ p: Parser) -> Parser { + return Parser { s in + if let (a, rest) = self.run(s) { + return (a, rest) + } else { + return p.run(s) + } + } + } + + /// Try each parser in `ps` in order, take the result of the first that succeeds + static func first(_ ps: S) -> Parser where S.Iterator.Element == Parser { + return ps.reduce(Parser.fail()) { (acc, p) in + return acc.orElse(p) + } + } + + /// Make a parser that parses your data zero or more times + func many() -> Parser<[A]> { + return manyUntil(terminator: Parser<()>.fail()) + } + + /// Make a parser that wraps this parser with two characters + /// Note: Make sure you don't greedily parse the endingWith character in `self` + func wrapped(startingWith: Character, endingWith: Character) -> Parser { + return Parsers.just(startingWith).andThen{ self }.flatMap{ x in + return Parsers.just(endingWith).map{ _ in x } + } + } + + /// Make a parser that looks for data repeated separated by `separatedBy` + /// and return all the data (excluding the separatedBy bit) + /// Note: Make sure you don't greedily parse the `separatedBy` character in `self` + func rep(separatedBy: Parser<()>) -> Parser<[A]> { + return self.flatMap{ a in + return Parser<[A]>.trivial([a]) <> (separatedBy.andThen{ self}).many() + } + } + + /// Make a parser that is like this parser but always fails when encountering `char` + func butNot(_ char: Character) -> Parser { + return Parser { s in + if let (a, rest) = self.run(s), s[0] != char { + return (a, rest) + } else { + return nil + } + } + } + + /// Make a parser that parses many times until hitting a `terminator` and there are + /// `atLeast` successfuly pieces of data parsed. + func manyUntil(terminator: Parser<()>, atLeast: Int = 0) -> Parser<[A]> { + func checkedReturn(_ t: ([A], [Character])) -> ([A], [Character])? { + return t.0.count >= atLeast ? t : nil + } + func loop(_ next: [Character], _ build: [A]) -> ([A], [Character])? { + if let (_, _) = terminator.run(next) { + return checkedReturn((build, next)) + } + + if let (a, rest) = self.run(next) { + return loop(rest, build + [a]) + } else { + return checkedReturn((build, next)) + } + } + + return Parser<[A]> { s in + return loop(s, []) + } + } +} + +/// Combining two parsers when the we're making a semigroup +/// can automatically combine the inner bits in a single parser +/// that tries `lhs` and then `rhs` in sequence. +/// This means Parser is a semigroup homomorphism. +extension Parser where A: Semigroup { + static func <>(lhs: Parser, rhs: Parser) -> Parser { + return lhs.flatMap{ (l: A) -> Parser in + rhs.map{ (r: A) -> A in + l <> r + } + } + } +} + +enum Parsers { + /// As long as the input stream is non-empty this will pass with the first character + static let anyChar: Parser = Parser { cs in + if cs.count > 0 { + return (cs[0], Array(cs[1.. = Parser { cs in + if cs.count > 0, cs[0] == " " || cs[0] == "\t" { + return (cs[0], Array(cs[1.. Parser { + return Parsers.prefix([x]).map{ cs in cs[0] } + } + + /// Make a parser to parse a sequence of characters `prefix` + static func prefix(_ prefix: [Character]) -> Parser<[Character]> { + return Parser<[Character]> { s in + if (prefix.count <= s.count && zip(prefix, s[0..? { get } + var platforms: [String: String]? { get } + var podTargetXcconfig: [String: String]? { get } + var userTargetXcconfig: [String: String]? { get } + var sourceFiles: [String] { get } + var excludeFiles: [String] { get } + var frameworks: [String] { get } + var weakFrameworks: [String] { get } + var subspecs: [PodSpec] { get } + var dependencies: [String] { get } + var compilerFlags: [String] { get } + var source: PodSpecSource? { get } + var libraries: [String] { get } + var resourceBundles: [String: [String]] { get } + var resources: [String] { get } + var vendoredFrameworks: [String] { get } + var vendoredLibraries: [String] { get } + var headerDirectory: String? { get } + var xcconfig: [String: String]? { get } + var moduleName: String? { get } + var requiresArc: Either? { get } + var publicHeaders: [String] { get } + var privateHeaders: [String] { get } + var prefixHeaderContents: String? { get } + var prefixHeaderFile: Either? { get } + var preservePaths: [String] { get } + var defaultSubspecs: [String] { get } +} + +public struct PodSpec: PodSpecRepresentable { + let name: String + let version: String? + let swiftVersions: Set? + let platforms: [String: String]? + let sourceFiles: [String] + let excludeFiles: [String] + let frameworks: [String] + let weakFrameworks: [String] + let subspecs: [PodSpec] + let dependencies: [String] + let compilerFlags: [String] + let source: PodSpecSource? + let license: PodSpecLicense + let libraries: [String] + let defaultSubspecs: [String] + + let headerDirectory: String? + let moduleName: String? + // requiresArc can be a bool + // or it could be a list of pattern + // or it could be omitted (in which case we need to fallback) + let requiresArc: Either? + + let publicHeaders: [String] + let privateHeaders: [String] + + // lib/cocoapods/installer/xcode/pods_project_generator/pod_target_installer.rb:170 + let prefixHeaderFile: Either? + let prefixHeaderContents: String? + + let preservePaths: [String] + + let vendoredFrameworks: [String] + let vendoredLibraries: [String] + + // TODO: Support resource / resources properties as well + let resourceBundles: [String: [String]] + let resources: [String] + + let podTargetXcconfig: [String: String]? + let userTargetXcconfig: [String: String]? + let xcconfig: [String: String]? + + let ios: PodSpecRepresentable? + let osx: PodSpecRepresentable? + let tvos: PodSpecRepresentable? + let watchos: PodSpecRepresentable? + + let prepareCommand = "" + + public init(JSONPodspec: JSONDict) throws { + + let fieldMap: [PodSpecField: Any] = Dictionary(tuples: JSONPodspec.compactMap { k, v in + guard let field = PodSpecField.init(rawValue: k) else { + return nil + } + return .some((field, v)) + }) + + if let name = try? ExtractValue(fromJSON: fieldMap[.name]) as String { + self.name = name + } else { + // This is for "ios", "macos", etc + name = "" + } + version = try ExtractValue(fromJSON: fieldMap[.version]) as String? + frameworks = strings(fromJSON: fieldMap[.frameworks]) + weakFrameworks = strings(fromJSON: fieldMap[.weakFrameworks]) + excludeFiles = strings(fromJSON: fieldMap[.excludeFiles]) + sourceFiles = strings(fromJSON: fieldMap[.sourceFiles]).map({ + $0.hasSuffix("/") ? String($0.dropLast()) : $0 + }) + publicHeaders = strings(fromJSON: fieldMap[.publicHeaders]) + privateHeaders = strings(fromJSON: fieldMap[.privateHeaders]) + + prefixHeaderFile = (fieldMap[.prefixHeaderFile] as? Bool).map{ .left($0) } ?? // try a bool + (fieldMap[.prefixHeaderFile] as? String).map { .right($0) } // try a string + + prefixHeaderContents = fieldMap[.prefixHeaderContents] as? String + + preservePaths = strings(fromJSON: fieldMap[.preservePaths]) + compilerFlags = strings(fromJSON: fieldMap[.compilerFlags]) + libraries = strings(fromJSON: fieldMap[.libraries]) + + defaultSubspecs = strings(fromJSON: fieldMap[.defaultSubspecs]) + + vendoredFrameworks = strings(fromJSON: fieldMap[.vendoredFrameworks]) + vendoredLibraries = strings(fromJSON: fieldMap[.vendoredLibraries]) + + headerDirectory = fieldMap[.headerDirectory] as? String + moduleName = fieldMap[.moduleName] as? String + requiresArc = (fieldMap[.requiresArc] as? Bool).map{ .left($0) } ?? // try a bool + stringsStrict(fromJSON: fieldMap[.requiresArc]).map{ .right($0) } // try a string + + if let podSubspecDependencies = fieldMap[.dependencies] as? JSONDict { + dependencies = Array(podSubspecDependencies.keys) + } else { + dependencies = [] + } + + if let resourceBundleMap = fieldMap[.resourceBundles] as? JSONDict { + resourceBundles = Dictionary(tuples: resourceBundleMap.map { key, val in + (key, strings(fromJSON: val)) + }) + } else { + resourceBundles = [:] + } + + resources = strings(fromJSON: fieldMap[.resources]) + + if let JSONPodSubspecs = fieldMap[.subspecs] as? [JSONDict] { + subspecs = try JSONPodSubspecs.map { try PodSpec(JSONPodspec: $0) } + } else { + subspecs = [] + } + + if let JSONSource = fieldMap[.source] as? JSONDict { + source = PodSpecSource.source(fromDict: JSONSource) + } else { + source = nil + } + + license = PodSpecLicense.license(fromJSON: fieldMap[.license]) + + platforms = try? ExtractValue(fromJSON: fieldMap[.platforms]) + xcconfig = try? ExtractValue(fromJSON: fieldMap[.xcconfig]) + podTargetXcconfig = try? ExtractValue(fromJSON: fieldMap[.podTargetXcconfig]) + userTargetXcconfig = try? ExtractValue(fromJSON: fieldMap[.userTargetXcconfig]) + + ios = (fieldMap[.ios] as? JSONDict).flatMap { try? PodSpec(JSONPodspec: $0) } + osx = (fieldMap[.osx] as? JSONDict).flatMap { try? PodSpec(JSONPodspec: $0) } + tvos = (fieldMap[.tvos] as? JSONDict).flatMap { try? PodSpec(JSONPodspec: $0) } + watchos = (fieldMap[.watchos] as? JSONDict).flatMap { try? PodSpec(JSONPodspec: $0) } + var resultSwiftVersions = Set() + if let swiftVersions = fieldMap[.swiftVersions] as? String { + resultSwiftVersions.insert(swiftVersions) + } else if let swiftVersions = fieldMap[.swiftVersions] as? [String] { + swiftVersions.forEach { + resultSwiftVersions.insert($0) + } + } + if let swiftVersion = fieldMap[.swiftVersion] as? String { + resultSwiftVersions.insert(swiftVersion) + } + self.swiftVersions = !resultSwiftVersions.isEmpty ? resultSwiftVersions : nil + } + + func allSubspecs(_ isSubspec: Bool = false) -> [PodSpec] { + return (isSubspec ? [self] : []) + self.subspecs.reduce([PodSpec]()) { + return $0 + $1.allSubspecs(true) + } + } + + func selectedSubspecs(subspecs: [String]) -> [PodSpec] { + let defaultSubspecs = Set(subspecs.isEmpty ? self.defaultSubspecs : subspecs) + let subspecs = allSubspecs() + guard !defaultSubspecs.isEmpty else { + return subspecs + } + return subspecs.filter { defaultSubspecs.contains($0.name) } + } +} + +struct FallbackSpec { + let specs: [PodSpec] + // Takes the first non empty value + func attr(_ keyPath: KeyPath) -> AttrSet { + for spec in specs { + let value = spec.attr(keyPath) + if !value.isEmpty { + return value + } + } + return AttrSet.empty + } +} + +// The source component of a PodSpec +// @note currently only git is supported +enum PodSpecSource { + case git(url: URL, tag: String?, commit: String?) + case http(url: URL) + + static func source(fromDict dict: JSONDict) -> PodSpecSource { + if let gitURLString: String = try? ExtractValue(fromJSON: dict["git"]) { + guard let gitURL = URL(string: gitURLString) else { + fatalError("Invalid source URL for Git: \(gitURLString)") + } + let tag: String? = try? ExtractValue(fromJSON: dict["tag"]) + let commit: String? = try? ExtractValue(fromJSON: dict["commit"]) + return .git(url: gitURL, tag: tag, commit: commit) + } else if let httpURLString: String = try? ExtractValue(fromJSON: dict["http"]) { + guard let httpURL = URL(string: httpURLString) else { + fatalError("Invalid source URL for HTTP: \(httpURLString)") + } + return .http(url: httpURL) + } else { + fatalError("Unsupported source for PodSpec - \(dict)") + } + } +} + +struct PodSpecLicense { + /// The type of the license. + /// @note it's primarily used for the UI + let type: String? + + /// A license can either be a file or a text license + /// If there is no explict license, the LICENSE(.*) is implicitly + /// used + let text: String? + let file: String? + + static func license(fromJSON value: Any?) -> PodSpecLicense { + if let licenseJSON = value as? JSONDict { + return PodSpecLicense( + type: try? ExtractValue(fromJSON: licenseJSON["type"]), + text: try? ExtractValue(fromJSON: licenseJSON["text"]), + file: try? ExtractValue(fromJSON: licenseJSON["file"]) + ) + } + if let licenseString = value as? String { + return PodSpecLicense(type: licenseString, text: nil, file: nil) + } + return PodSpecLicense(type: nil, text: nil, file: nil) + } +} + +// MARK: - JSON Value Extraction + +public typealias JSONDict = [String: Any] + +enum JSONError: Error { + case unexpectedValueError +} + +func ExtractValue(fromJSON JSON: Any?) throws -> T { + if let value = JSON as? T { + return value + } + throw JSONError.unexpectedValueError +} + +// Pods intermixes arrays and strings all over +// Coerce to a more sane type, since we don't care about the +// original input +func strings(fromJSON JSONValue: Any? = nil) -> [String] { + if let str = JSONValue as? String { + return [str] + } + if let array = JSONValue as? [String] { + return array + } + return [String]() +} + +fileprivate func stringsStrict(fromJSON JSONValue: Any? = nil) -> [String]? { + if let str = JSONValue as? String { + return [str] + } + if let array = JSONValue as? [String] { + return array + } + return nil +} + diff --git a/Sources/PodToBUILD/RuleUtils.swift b/Sources/PodToBUILD/RuleUtils.swift new file mode 100644 index 0000000..cfe9b59 --- /dev/null +++ b/Sources/PodToBUILD/RuleUtils.swift @@ -0,0 +1,161 @@ +// +// RuleUtils.swift +// PodToBUILD +// +// Created by Jerry Marino on 9/20/2018. +// Copyright © 2018 Pinterest Inc. All rights reserved. +// + +public enum BazelSourceLibType { + case objc + case swift + case cpp + + func getLibNameSuffix() -> String { + switch self { + case .objc: + return "_objc" + case .cpp: + return "_cxx" + case .swift: + return "_swift" + } + } +} + +/// Extract files from a source file pattern. +func extractFiles(fromPattern patternSet: AttrSet<[String]>, + includingFileTypes: Set, + usePrefix: Bool = true, + options: BuildOptions) -> +AttrSet<[String]> { + let sourcePrefix = usePrefix ? getSourcePatternPrefix(options: options) : "" + return patternSet.map { + (patterns: [String]) -> [String] in + let result = patterns.flatMap { (p: String) -> [String] in + pattern(fromPattern: sourcePrefix + p, includingFileTypes: + includingFileTypes) + } + return result + } +} + +public func extractFiles(fromPattern patternSet: [String], + includingFileTypes: Set, + usePrefix: Bool = true, + options: BuildOptions) -> [String] { + let sourcePrefix = usePrefix ? getSourcePatternPrefix(options: options) : "" + return patternSet.flatMap { (p: String) -> [String] in + pattern(fromPattern: sourcePrefix + p, includingFileTypes: + includingFileTypes) + } +} + +let ObjcLikeFileTypes = Set([".m", ".c", ".s", ".S"]) +let CppLikeFileTypes = Set([".mm", ".cpp", ".cxx", ".cc"]) +let SwiftLikeFileTypes = Set([".swift"]) +let HeaderFileTypes = Set([".h", ".hpp", ".hxx"]) +let AnyFileTypes = ObjcLikeFileTypes + .union(CppLikeFileTypes) + .union(SwiftLikeFileTypes) + .union(HeaderFileTypes) + +public func getRulePrefix(name: String) -> String { + return "//Pods/\(name)" +} + +public func getPodBaseDir() -> String { + return "Pods" +} + +/// We need to hardcode a copt to the $(GENDIR) for simplicity. +/// Expansion of $(location //target) is not supported in known Xcode generators +public func getGenfileOutputBaseDir(options: BuildOptions) -> String { + let basePath = "Pods" + let podName = options.podName + let parts = options.path.split(separator: "/") + if options.path == "." || parts.count < 2 { + return "\(basePath)/\(podName)" + } + + return String(parts[0..<2].joined(separator: "/")) +} + +public func getNamePrefix(options: BuildOptions) -> String { + if options.path.split(separator: "/").count > 2 { + return options.podName + "_" + } + return "" +} + +public func getSourcePatternPrefix(options: BuildOptions) -> String { + let parts = options.path.split(separator: "/") + if options.path == "." || parts.count < 2 { + return "" + } + let sourcePrefix = String(parts[2.. String { + let results = podDepName.components(separatedBy: "/") + if results.count > 1 && results[0] == podName { + // This is a local subspec reference + let join = results[1 ... results.count - 1].joined(separator: "/") + return ":\(getNamePrefix(options: options) + bazelLabel(fromString: join))" + } else { + if results.count > 1 { + return getRulePrefix(name: results[0]) + } else { + // This is a reference to another pod library + return getRulePrefix(name: + bazelLabel(fromString: results[0])) + } + } +} + +/// Convert a string to a Bazel label conventional string +public func bazelLabel(fromString string: String) -> String { + return string.replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "+", with: "_") +} + +public func replacePodsEnvVars(_ value: String, options: BuildOptions) -> String { + let podDir = options.podBaseDir + let targetDir = options.genfileOutputBaseDir + return value + .replacingOccurrences(of: "$(inherited)", with: "") + .replacingOccurrences(of: "$(PODS_ROOT)", with: podDir) + .replacingOccurrences(of: "${PODS_ROOT}", with: podDir) + .replacingOccurrences(of: "$(PODS_TARGET_SRCROOT)", with: targetDir) + .replacingOccurrences(of: "${PODS_TARGET_SRCROOT}", with: targetDir) +} + +public func xcconfigSettingToList(_ value: String) -> [String] { + return value + .components(separatedBy: "=\"") + .map { + let components = $0.components(separatedBy: "\"") + guard components.count == 2 else { + return $0 + } + let modifiedValue = [ + components.first?.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? "", + components.dropFirst().joined() + ].joined(separator: "\\\"") + return modifiedValue + } + .joined(separator: "=\\\"") + .components(separatedBy: .whitespaces) + .map { $0.removingPercentEncoding ?? "" } + .filter({ $0 != "$(inherited)"}) + .filter({ !$0.isEmpty }) +} diff --git a/Sources/PodToBUILD/Shell/LogicalShellContext.swift b/Sources/PodToBUILD/Shell/LogicalShellContext.swift new file mode 100644 index 0000000..f5daee8 --- /dev/null +++ b/Sources/PodToBUILD/Shell/LogicalShellContext.swift @@ -0,0 +1,120 @@ +// +// LogicalShellContext.swift +// PodToBUILD +// +// Created by Jerry Marino on 5/9/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import Foundation + +/// Encode commands to a string +private func encode(_ command: String, arguments: [String]) -> String { + return "\(command)\(arguments.joined(separator: "_"))" +} + +typealias ShellInvocation = (String, CommandOutput) + +/// Build up a shell invocation. +/// The logical shell simply returns these when someone calls shell.command +/// with a the command and arguments +func MakeShellInvocation(_ command: String, arguments: [String], value standardOutValue: Any = "", exitCode: Int32 = 0) -> ShellInvocation { + var output = LogicalCommandOutput() + output.terminationStatus = exitCode + + // Convert the user's input to a data representation + if let data = standardOutValue as? Data { + output.standardOutputData = data + } else { + if let stringValue = standardOutValue as? String { + // Convert + let stringData = stringValue.data(using: String.Encoding.utf8)! + output.standardErrorData = stringData + } + } + return (encode(command, arguments: arguments), output) +} + +struct LogicalCommandOutput : CommandOutput { + var standardErrorData = Data() + var standardOutputData = Data() + var terminationStatus: Int32 = 0 +} + +/// LogicalShellContext is a Shell context which is virtualized +/// We don't actually interact with the file system, but execute +/// CommandInvocations +class LogicalShellContext : ShellContext { + let commandInvocations : [String: CommandOutput] + private var invokedCommands = [String] () + + init(commandInvocations: [(String, CommandOutput)]) { + var values = [String: CommandOutput]() + commandInvocations.forEach { + values[$0.0] = $0.1 + } + self.commandInvocations = values + } + + func executed(encodedCommand: String) -> Bool { + print(invokedCommands) + return invokedCommands.contains(encodedCommand) + } + + func executed(_ command: String, arguments: [String]) -> Bool { + return invokedCommands.contains(encode(command, arguments: arguments)) + } + + func command(_ launchPath: String, arguments: [String]) -> CommandOutput { + let encoded = encode(launchPath, arguments: arguments) + print("Execute: \(encoded)\n") + invokedCommands.append(encoded) + print("CommandInvocations: \(commandInvocations)\n") + print("InvokedCommands: \(invokedCommands)\n") + return commandInvocations[encoded] ?? LogicalCommandOutput() + } + + func shellOut(_ script: String) -> CommandOutput { + let encoded = "SHELL " + script + print("Execute: \(encoded)\n") + invokedCommands.append(encoded) + print("CommandInvocations: \(commandInvocations)\n") + print("InvokedCommands: \(invokedCommands)\n") + return commandInvocations[encoded] ?? LogicalCommandOutput() + } + + func dir(_ path: String) { + invokedCommands.append("DIR:\(path)") + } + + func symLink(from: String, to: String) { + let encoded = "symLink from:\(from) to:\(to)" + invokedCommands.append(encoded) + } + + func hardLink(from: String, to: String) { + let encoded = "hardLink from:\(from) to:\(to)" + invokedCommands.append(encoded) + } + + func write(value: String, toPath path: URL) { + let encoded = "write value:\(value) toPath:\(path)" + invokedCommands.append(encoded) + } + + static func encodeDownload(url: URL, toFile file: String) -> String { + return "download url:\(url) toFile:\(file)" + } + + func download(url: URL, toFile file: String) -> Bool { + let encoded = LogicalShellContext.encodeDownload(url: url, toFile: file) + invokedCommands.append(encoded) + return true + } + + func tmpdir() -> String { + let encoded = "create tmpdir" + invokedCommands.append(encoded) + return "%TMP%" + } +} diff --git a/Sources/PodToBUILD/Shell/ShellContext.swift b/Sources/PodToBUILD/Shell/ShellContext.swift new file mode 100644 index 0000000..465ffe4 --- /dev/null +++ b/Sources/PodToBUILD/Shell/ShellContext.swift @@ -0,0 +1,345 @@ +// +// ShellContext.swift +// PodToBUILD +// +// Created by Jerry Marino on 5/9/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import Foundation +import ObjcSupport + +/// The output of a ShellContext command +public protocol CommandOutput { + var standardErrorData: Data { get } + var standardOutputData: Data { get } + var terminationStatus: Int32 { get } +} + +public struct CommandBinary { + public static let mkdir = "/bin/mkdir" + public static let ln = "/bin/ln" + public static let pwd = "/bin/pwd" + public static let sh = "/bin/sh" + public static let ditto = "/usr/bin/ditto" + public static let rm = "/bin/rm" +} + +extension CommandOutput { + /// Return Standard Output as a String + public var standardOutputAsString : String { + return String(data: standardOutputData, encoding: String.Encoding.utf8) ?? "" + } + + public var standardErrorAsString : String { + return String(data: standardErrorData, encoding: String.Encoding.utf8) ?? "" + } +} + +/// Multiply seconds by minutes by 4 +let FOUR_HOURS_TIME_CONSTANT = (60.0 * 60.0 * 4.0) + +/// Shell Contex is a context to interact with the users system +/// All interaction with the the system should go through this +/// layer, so that it is auditable, traceable, and testable +public protocol ShellContext { + @discardableResult func command(_ launchPath: String, arguments: [String]) -> CommandOutput + + @discardableResult func shellOut(_ script: String) -> CommandOutput + + func dir(_ path: String) + + func hardLink(from: String, to: String) + + func symLink(from: String, to: String) + + func write(value: String, toPath path: URL) + + func download(url: URL, toFile: String) -> Bool + + func tmpdir() -> String +} + +/// Escape paths +public func escape(_ string: String) -> String { + return string.replacingOccurrences(of: "\\", with: "\\\\", options: .literal, range: nil) +} + +let kTaskDidFinishNotificationName = NSNotification.Name(rawValue: "kTaskDidFinishNotificationName") + +private struct ShellTaskResult : CommandOutput { + let standardErrorData: Data + let standardOutputData: Data + let terminationStatus: Int32 +} + +/// Shell task runs a given command and waits +/// for it to terminate or timeout +class ShellTask : NSObject { + let timeout: CFTimeInterval + let command: String + let path: String? + let printOutput: Bool + let arguments: [String] + private var standardOutputData: Data + private var standardErrorData: Data + + init(command: String, arguments: [String], timeout: CFTimeInterval, cwd: + String? = nil, printOutput: Bool = false) { + self.command = command + self.arguments = arguments + self.timeout = timeout + self.path = cwd + self.printOutput = printOutput + self.standardErrorData = Data() + self.standardOutputData = Data() + } + + /// Create a task with a script and timeout + /// By default, it runs under bash for the current path. + public static func with(script: String, timeout: Double, cwd: String? = nil, + printOutput: Bool = false) -> ShellTask { + let path = ProcessInfo.processInfo.environment["PATH"]! + let script = "PATH=\"\(path)\" /bin/sh -c '\(script)'" + return ShellTask(command: "/bin/sh", arguments: ["-c", script], + timeout: timeout, cwd: cwd, printOutput: printOutput) + } + + override var description: String { + return "ShellTask: " + command + " " + arguments.joined(separator: " ") + } + override var debugDescription : String { + return description + } + + class RunLoopContext { + var process: Process + init (process: Process) { + self.process = process + } + } + + /// Launch a task and get the output + func launch() -> CommandOutput { + // Setup outputs + // FIXME: this causes issues on Catalina + let stream = false + let stdout = Pipe() + let stderr = Pipe() + + // Setup the process. + let process = createProcess(stream: stream, stdout: stdout, stderr: stderr) + + // Start a timer to kill the process, will no-op if it triggers after process ends + DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: { process.terminate() }) + + // Run the process, remove the timer when done. + let exception = tryBlock { + process.launch() + process.waitUntilExit() + } + + // Handle the result + return constructProcessResult( + stream: stream, + stdout: stdout, + stderr: stderr, + process: process, + exception: exception + ) + } + + private func createProcess(stream: Bool, stdout: Pipe, stderr: Pipe) -> Process { + let process = Process() + if stream { + stdout.fileHandleForReading.readabilityHandler = { + handle in + let data = handle.availableData + guard data.count > 0 else { + return + } + if self.printOutput { + FileHandle.standardOutput.write(data) + } + self.standardOutputData.append(data) + } + stderr.fileHandleForReading.readabilityHandler = { + handle in + let data = handle.availableData + guard data.count > 0 else { + return + } + if self.printOutput { + FileHandle.standardError.write(data) + } + self.standardErrorData.append(data) + } + } + var env = ProcessInfo.processInfo.environment + env["LANG"] = "en_US.UTF-8" + process.environment = env + process.standardOutput = stdout + process.standardError = stderr + process.launchPath = command + process.arguments = arguments + if let cwd = path { + if #available(OSX 10.13, *) { + process.currentDirectoryURL = URL(fileURLWithPath: cwd) + } else { + // Fallback on earlier versions + process.currentDirectoryPath = cwd + } + } + return process + } + + private func constructProcessResult( + stream: Bool, + stdout: Pipe, + stderr: Pipe, + process: Process, + exception: Any? + ) -> ShellTaskResult { + if exception != nil { + if !stream { + self.standardErrorData = stderr.fileHandleForReading.readDataToEndOfFile() + if self.printOutput { + FileHandle.standardError.write(self.standardErrorData) + } + } + return ShellTaskResult(standardErrorData: standardErrorData, + standardOutputData: Data(), + terminationStatus: 42) + } + + if !stream { + self.standardErrorData = stderr.fileHandleForReading.readDataToEndOfFile() + self.standardOutputData = stdout.fileHandleForReading.readDataToEndOfFile() + if self.printOutput { + FileHandle.standardError.write(self.standardErrorData) + FileHandle.standardError.write(self.standardOutputData) + } + } + return ShellTaskResult( + standardErrorData: standardErrorData, + standardOutputData: standardOutputData, + terminationStatus: process.terminationStatus + ) + } +} + +/// SystemShellContext is a shell context that mutates the user's system +/// All mutations may be logged +public struct SystemShellContext : ShellContext { + func command(_ launchPath: String, arguments: [String]) -> (String, CommandOutput) { + let data = startShellAndWait(launchPath, arguments: arguments) + let string = String(data: data.standardOutputData, encoding: String.Encoding.utf8) ?? "" + return (string, data) + } + + private let trace: Bool + + public init(trace: Bool = false) { + // Warning this disables buffering for all of the output + setbuf(__stdoutp, nil) + + self.trace = trace + } + + public func command(_ launchPath: String, arguments: [String] = [String]()) -> CommandOutput { + return startShellAndWait(launchPath, arguments: arguments) + } + + @discardableResult public func shellOut(_ script: String) -> CommandOutput { + log("SHELL:\(script)") + let task = ShellTask.with(script: script, + timeout: FOUR_HOURS_TIME_CONSTANT, + printOutput: trace) + let result = task.launch() + let stderrData = result.standardErrorData + let stdoutData = result.standardOutputData + let statusCode = result.terminationStatus + log("PIPE OUTPUT\(script) stderr:\(readData(stderrData)) stdout:\(readData(stdoutData)) code:\(statusCode)") + return result + } + + public func dir(_ path: String) { + let dir = command(CommandBinary.pwd).standardOutputAsString.components(separatedBy: "\n")[0] + let relativedir = escape("\(dir)/\(path)") + log("DIR\(relativedir)") + let status = command(CommandBinary.mkdir, arguments: ["-p", relativedir]).terminationStatus + log("DIR STATUS \(status)") + } + + public func hardLink(from: String, to: String) { + log("LINK FROM \(from) to \(to)") + let status = command("/bin/ln", arguments: ["", escape(from), escape(to)]).terminationStatus + log("LINK STATUS \(status)") + } + + public func symLink(from: String, to: String) { + log("LINK FROM \(from) to \(to)") + do { + try FileManager.default.createSymbolicLink(atPath: to, withDestinationPath: from) + print("LINK SUCCESS") + } catch { + print("LINK ERROR: ", error.localizedDescription) + } + } + + public func write(value: String, toPath path: URL) { + log("WRITE \(value) TO \(path)") + try? value.write(to: path, atomically: false, encoding: String.Encoding.utf8) + } + + public func download(url: URL, toFile file: String) -> Bool { + log("DOWNLOAD \(url) TO \(file)") + guard let fileData = NSData(contentsOf: url) else { + return false + } + let err: AutoreleasingUnsafeMutablePointer? = nil + NSFileCoordinator().coordinate(writingItemAt: url, + options: NSFileCoordinator.WritingOptions.forReplacing, + error: err) { (fileURL) in + FileManager.default.createFile(atPath: file, contents: fileData as Data, attributes: nil) + } + return (err == nil) + } + + public func tmpdir() -> String { + log("CREATE TMPDIR") + // Taken from https://stackoverflow.com/a/46701313/3000133 + let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) + do { + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) + } catch { + fatalError("Can't create temp dir") + } + return url.path + } + + // MARK: - Private + + // Start a shell and wait for the result + // @note we use UTF-8 here as the default language and the current env + private func startShellAndWait(_ launchPath: String, arguments: [String] = + [String]()) -> CommandOutput { + log("COMMAND:\(launchPath) \(arguments)") + let task = ShellTask(command: launchPath, arguments: arguments, + timeout: FOUR_HOURS_TIME_CONSTANT, printOutput: trace) + let result = task.launch() + let statusCode = result.terminationStatus + log("TASK EXITED\(launchPath) \(arguments) code:\(statusCode )") + return result + } + + private func readData(_ data: Data) -> String { + return String(data: data, encoding: String.Encoding.utf8) ?? "" + } + + private func log(_ args: Any...) { + if trace { + print(args.map { "\($0)" }.joined(separator: " ") ) + } + } +} diff --git a/Sources/PodToBUILD/Skylark.swift b/Sources/PodToBUILD/Skylark.swift new file mode 100644 index 0000000..cdb3671 --- /dev/null +++ b/Sources/PodToBUILD/Skylark.swift @@ -0,0 +1,194 @@ +// +// Skylark.swift +// PodToBUILD +// +// Created by Jerry Marino on 4/14/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import Foundation + +public indirect enum SkylarkNode { + /// A integer in Skylark. + case int(Int) + + /// A Boolean in Skylark. + case bool(Bool) + + /// A string in Skylark. + /// @note The string value is enclosed within "" + case string(String) + + /// A multiline string in Skylark. + /// @note The string value enclosed within """ """ + case multiLineString(String) + + /// A list of any Skylark Types + case list([SkylarkNode]) + + /// A function call. + /// Arguments may be either named or basic + case functionCall(name: String, arguments: [SkylarkFunctionArgument]) + + /// Arbitrary skylark code. + /// This code is escaped and compiled directly as specifed in the string. + /// Use this for code that needs to be evaluated. + case skylark(String) + + /// A skylark dict + case dict([String: SkylarkNode]) + + /// An expression with a lhs and a rhs separated by an op + case expr(lhs: SkylarkNode, op: String, rhs: SkylarkNode) + + /// Lines are a bunch of nodes that we will render as separate lines + case lines([SkylarkNode]) + + /// Flatten nested lines to a single array of lines + func canonicalize() -> SkylarkNode { + // at the inner layer we just strip the .lines + func helper(inner: SkylarkNode) -> [SkylarkNode] { + switch inner { + case let .lines(nodes): return nodes + case let other: return [other] + } + } + + // and at the top level we keep the .lines wrapper + switch self { + case let .lines(nodes): return .lines(nodes.flatMap(helper)) + case let other: return other + } + } +} + +extension SkylarkNode: Monoid, EmptyAwareness { + public static var empty: SkylarkNode { return .list([]) } + + // TODO(bkase): Annotate AttrSet with monoidal public struct wrapper to get around this hack + /// WARNING: This doesn't obey the laws :(. + public static func<>(lhs: SkylarkNode, rhs: SkylarkNode) -> SkylarkNode { + return lhs .+. rhs + } + + public var isEmpty: Bool { + switch self { + case let .list(xs): return xs.isEmpty + case let .dict(dict): return dict.isEmpty + case let .string(string): return string.isEmpty + default: return false + } + } +} + +// because it must be done +infix operator .+.: AdditionPrecedence +func .+.(lhs: SkylarkNode, rhs: SkylarkNode) -> SkylarkNode { + switch (lhs, rhs) { + case (.list(let l), .list(let r)): return .list(l + r) + case (_, .list(let v)) where v.isEmpty: return lhs + case (.list(let v), _) where v.isEmpty: return rhs + default: return .expr(lhs: lhs, op: "+", rhs: rhs) + } +} + +infix operator .=.: AdditionPrecedence +func .=.(lhs: SkylarkNode, rhs: SkylarkNode) -> SkylarkNode { + return .expr(lhs: lhs, op: "=", rhs: rhs) +} + +public indirect enum SkylarkFunctionArgument { + case basic(SkylarkNode) + case named(name: String, value: SkylarkNode) +} + + +// MARK: - SkylarkCompiler + +public struct SkylarkCompiler { + let root: SkylarkNode + let indent: Int + private let whitespace: String + + public init(_ lines: [SkylarkNode]) { + self.init(.lines(lines)) + } + + public init(_ root: SkylarkNode, indent: Int = 0) { + self.root = root.canonicalize() + self.indent = indent + whitespace = SkylarkCompiler.white(indent: indent) + } + + public func run() -> String { + return compile(root) + } + + private func compile(_ node: SkylarkNode) -> String { + switch node { + case let .int(value): + return "\(value)" + case let .bool(value): + return value ? "True" : "False" + case let .string(value): + return "\"\(value)\"" + case let .multiLineString(value): + return "\"\"\"\(value)\"\"\"" + case let .functionCall(call, arguments): + let compiler = SkylarkCompiler(node, indent: indent + 2) + return compiler.compile(call: call, arguments: arguments, closeParenWhitespace: whitespace) + case let .skylark(value): + return value + case let .list(value): + guard !value.isEmpty else { return "[]" } + return "[\n" + value.map { node in + "\(SkylarkCompiler.white(indent: indent + 2))\(compile(node))" + }.joined(separator: ",\n") + "\n\(whitespace)]" + case let .expr(lhs, op, rhs): + return compile(lhs) + " \(op) " + compile(rhs) + case let .dict(dict): + guard !dict.isEmpty else { return "{}" } + // Stabilize dict keys here. Other inputs are required to be stable. + let sortedKeys = Array(dict.keys).sorted { $0 < $1 } + let compiler = SkylarkCompiler(node, indent: indent + 2) + return "{\n" + sortedKeys.compactMap { key in + guard let val = dict[key] else { return nil } + return "\(SkylarkCompiler.white(indent: indent + 2))\(compiler.compile(.string(key))): \(compiler.compile(val))" + }.joined(separator: ",\n") + "\n\(whitespace)}" + case let .lines(lines): + return lines.map(compile).joined(separator: "\n") + } + } + + // MARK: - Private + + private func compile(call: String, arguments: [SkylarkFunctionArgument], closeParenWhitespace: String) -> String { + var buildFile = "" + buildFile += "\(call)(\n" + for (idx, argument) in arguments.enumerated() { + let comma = idx == arguments.count - 1 ? "" : "," + switch argument { + case let .named(name, argValue): + buildFile += "\(whitespace)\(name) = \(compile(argValue))\(comma)\n" + case let .basic(argValue): + buildFile += "\(whitespace)\(compile(argValue))\(comma)\n" + } + } + buildFile += "\(closeParenWhitespace))" + return buildFile + } + + private static func white(indent: Int) -> String { + precondition(indent >= 0) + + if indent == 0 { + return "" + } + + var white = "" + for _ in 1 ... indent { + white += " " + } + return white + } +} diff --git a/Sources/PodToBUILD/SkylarkConvertible.swift b/Sources/PodToBUILD/SkylarkConvertible.swift new file mode 100644 index 0000000..074b895 --- /dev/null +++ b/Sources/PodToBUILD/SkylarkConvertible.swift @@ -0,0 +1,96 @@ +// +// SkylarkConvertible.swift +// PodToBUILD +// +// Created by Jerry Marino on 4/19/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import Foundation + +// SkylarkConvertible is a higher level representation of types within Skylark +protocol SkylarkConvertible { + func toSkylark() -> SkylarkNode +} + +extension SkylarkNode: SkylarkConvertible { + func toSkylark() -> SkylarkNode { + return self + } +} + +extension SkylarkNode: ExpressibleByStringLiteral { + public typealias StringLiteralType = String + public init(stringLiteral value: String) { + self = .string(value) + } + public init(unicodeScalarLiteral value: String) { + self.init(stringLiteral: value) + } + public init(extendedGraphemeClusterLiteral value: String) { + self.init(stringLiteral: value) + } +} + +extension SkylarkNode: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self = .int(value) + } +} + +extension Int: SkylarkConvertible { + func toSkylark() -> SkylarkNode { + return .int(self) + } +} + +extension String: SkylarkConvertible { + func toSkylark() -> SkylarkNode { + return .string(self) + } +} + +extension Array: SkylarkConvertible { + func toSkylark() -> SkylarkNode { + return .list(self.map { x in (x as! SkylarkConvertible).toSkylark() }) + } +} + +extension Optional: SkylarkConvertible { + func toSkylark() -> SkylarkNode { + switch self { + case .none: return SkylarkNode.empty + case .some(let x): return (x as! SkylarkConvertible).toSkylark() + } + } +} + +extension Dictionary: SkylarkConvertible { + func toSkylark() -> SkylarkNode { + return .dict([:] <> self.map { kv in + let key = kv.0 as! String + let value = kv.1 as! SkylarkConvertible + return (key, value.toSkylark()) + }) + } +} + +extension Set: SkylarkConvertible { + func toSkylark() -> SkylarkNode { + // HACK: Huge hack, but fixing this for real would require major refactoring + // ASSUMPTION: You're only calling Set.toSkylark on strings!!! + // FIXME in Swift 4 + return self.map{ $0 as! String }.sorted().toSkylark() + } +} + +extension Either: SkylarkConvertible where T: SkylarkConvertible, U: SkylarkConvertible { + func toSkylark() -> SkylarkNode { + switch self { + case .left(let value): + return value.toSkylark() + case .right(let value): + return value.toSkylark() + } + } +} diff --git a/Sources/PodToBUILD/SkylarkConvertibleTransform.swift b/Sources/PodToBUILD/SkylarkConvertibleTransform.swift new file mode 100644 index 0000000..5bedcf0 --- /dev/null +++ b/Sources/PodToBUILD/SkylarkConvertibleTransform.swift @@ -0,0 +1,14 @@ +// +// SkylarkConvertibleTransform.swift +// PodToBUILD +// +// Created by Jerry Marino on 5/2/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import Foundation + +protocol SkylarkConvertibleTransform { + /// Apply a transform to skylark convertibles + static func transform(convertibles: [BazelTarget], options: BuildOptions, podSpec: PodSpec) -> [BazelTarget] +} diff --git a/Sources/PodToBUILD/Targets/AppleBundleImport.swift b/Sources/PodToBUILD/Targets/AppleBundleImport.swift new file mode 100644 index 0000000..e2fd3b5 --- /dev/null +++ b/Sources/PodToBUILD/Targets/AppleBundleImport.swift @@ -0,0 +1,36 @@ +// +// AppleBundleImport.swift +// BazelPods +// +// Created by Sergey Khliustin on 27.08.2022. +// + +import Foundation +// Currently not used +// https://github.com/bazelbuild/rules_apple/blob/master/doc/rules-resources.md#apple_bundle_import +public struct AppleBundleImport: BazelTarget { + public let loadNode = "load('@build_bazel_rules_apple//apple:resources.bzl', 'apple_bundle_import')" + public let name: String + let bundleImports: AttrSet<[String]> + + public var acknowledged: Bool { + return true + } + + public func toSkylark() -> SkylarkNode { + return .functionCall( + name: "apple_bundle_import", + arguments: [ + .named(name: "name", value: bazelLabel(fromString: name).toSkylark()), + .named(name: "bundle_imports", + value: bundleImports.map { GlobNode(include: Set($0)) }.toSkylark() ) + ]) + } + + static func extractBundleName(fromPath path: String) -> String { + return path.components(separatedBy: "/").map { (s: String) in + s.hasSuffix(".bundle") ? s : "" + }.reduce("", +).replacingOccurrences(of: ".bundle", with: "") + } + +} diff --git a/Sources/PodToBUILD/Targets/AppleFramework.swift b/Sources/PodToBUILD/Targets/AppleFramework.swift new file mode 100644 index 0000000..7e8acb6 --- /dev/null +++ b/Sources/PodToBUILD/Targets/AppleFramework.swift @@ -0,0 +1,326 @@ +// +// AppleFramework.swift +// PodToBUILD +// +// Created by Sergey Khliustin on 04.08.2022. +// + +struct AppleFramework: BazelTarget { + let loadNode = "load('@build_bazel_rules_ios//rules:framework.bzl', 'apple_framework')" + let name: String + let version: String + let sourceFiles: AttrSet + let moduleName: AttrSet + let platforms: [String: String]? + let deps: AttrSet<[String]> + + // Resource files + let resources: AttrSet> + // .bundle in resource attribute + let bundles: AttrSet> + // resource_bundles attribute + let resourceBundles: AttrSet<[String: Set]> + + let externalName: String + let objcDefines: AttrSet<[String]> + let swiftDefines: AttrSet<[String]> + let swiftVersion: AttrSet + let xcconfig: [String: SkylarkNode] + + let publicHeaders: AttrSet + let privateHeaders: AttrSet + + let sdkFrameworks: AttrSet> + let weakSdkFrameworks: AttrSet> + let sdkDylibs: AttrSet> + let objcCopts: [String] + let swiftCopts: [String] + let linkOpts: [String] + + init(spec: PodSpec, subspecs: [PodSpec], deps: Set = [], dataDeps: Set = [], options: BuildOptions) { + + let podName = spec.name + let name = podName + + self.name = name + self.version = spec.version ?? "1.0" + var platforms = spec.platforms ?? [:] + if platforms["ios"] == nil { + platforms["ios"] = options.iosPlatform + } + self.platforms = platforms + + self.externalName = spec.name + + let fallbackSpec = FallbackSpec(specs: [spec] + subspecs) + + self.moduleName = Self.resolveModuleName(spec: spec) + + sourceFiles = Self.getFilesNodes(from: spec, subspecs: subspecs, includesKeyPath: \.sourceFiles, excludesKeyPath: \.excludeFiles, fileTypes: AnyFileTypes, options: options) + publicHeaders = Self.getFilesNodes(from: spec, subspecs: subspecs, includesKeyPath: \.publicHeaders, excludesKeyPath: \.privateHeaders, fileTypes: HeaderFileTypes, options: options) + privateHeaders = Self.getFilesNodes(from: spec, subspecs: subspecs, includesKeyPath: \.privateHeaders, fileTypes: HeaderFileTypes, options: options) + sdkDylibs = spec.collectAttribute(with: subspecs, keyPath: \.libraries) + sdkFrameworks = spec.collectAttribute(with: subspecs, keyPath: \.frameworks) + weakSdkFrameworks = spec.collectAttribute(with: subspecs, keyPath: \.weakFrameworks) + let allPodSpecDeps = spec.collectAttribute(with: subspecs, keyPath: \.dependencies) + .map({ + $0.map({ + getDependencyName(options: options, podDepName: $0, podName: podName) + }).filter({ !$0.hasPrefix(":") }) + }) + + let depNames = deps.map { ":\($0)" } + self.deps = AttrSet(basic: depNames) <> allPodSpecDeps + + self.swiftDefines = AttrSet(basic: ["COCOAPODS"]) + self.objcDefines = AttrSet(basic: ["COCOAPODS=1"]) + + let resources = spec.collectAttribute(with: subspecs, keyPath: \.resources).unpackToMulti() + + self.resources = resources.map({ (value: Set) -> Set in + value.filter({ !$0.hasSuffix(".bundle") }) + }).map(extractResources) + + self.bundles = resources.map({ (value: Set) -> Set in + value.filter({ $0.hasSuffix(".bundle") }) + }) + + self.resourceBundles = spec.collectAttribute(with: subspecs, keyPath: \.resourceBundles).map({ value -> [String: Set] in + var result = [String: Set]() + for key in value.keys { + result[key] = Set(extractResources(patterns: value[key]!)) + } + return result + }) + + + self.swiftVersion = Self.resolveSwiftVersion(spec: fallbackSpec) + + let xcconfigParser = XCConfigParser(spec: spec, subspecs: subspecs, options: options) + self.xcconfig = xcconfigParser.xcconfig + + self.objcCopts = xcconfigParser.objcCopts + self.swiftCopts = xcconfigParser.swiftCopts + self.linkOpts = xcconfigParser.linkOpts + } + + func toSkylark() -> SkylarkNode { + let basicSwiftDefines: SkylarkNode = + .functionCall(name: "select", + arguments: [ + .basic([ + "//conditions:default": [ + "DEBUG", + ], + ].toSkylark()) + ] + ) + let basicObjcDefines: SkylarkNode = + .functionCall(name: "select", + arguments: [ + .basic([ + ":release": [ + "POD_CONFIGURATION_RELEASE=1", + ], + "//conditions:default": [ + "POD_CONFIGURATION_DEBUG=1", + "DEBUG=1", + ], + ].toSkylark()) + ]) + + let swiftDefines = self.swiftDefines.toSkylark() .+. basicSwiftDefines + let objcDefines = self.objcDefines.toSkylark() .+. basicObjcDefines + + let deps = deps.unpackToMulti().multi.ios.map { + Set($0).sorted(by: (<)) + } + + // TODO: Make headers conditional + let publicHeaders = (self.publicHeaders.multi.ios ?? .empty) + let privateHeaders = self.privateHeaders.multi.ios ?? .empty + + // TODO: Make sources conditional + let sourceFiles = self.sourceFiles.multi.ios.map({ + GlobNode(include: $0.include) + }) ?? .empty + + let resourceBundles = (self.resourceBundles.multi.ios ?? [:]).mapValues({ + GlobNode(include: $0) + }) + + let moduleName = moduleName.unpackToMulti().multi.ios ?? "" + let bundleId = "org.cocoapods.\(moduleName)" + + let lines: [SkylarkFunctionArgument] = [ + .named(name: "name", value: name.toSkylark()), + .named(name: "module_name", value: moduleName.toSkylark()), + .named(name: "bundle_id", value: bundleId.toSkylark()), + .named(name: "swift_version", value: swiftVersion.toSkylark()), + .named(name: "platforms", value: platforms.toSkylark()), + .named(name: "srcs", value: sourceFiles.toSkylark()), + .named(name: "public_headers", value: publicHeaders.toSkylark()), + .named(name: "private_headers", value: privateHeaders.toSkylark()), + .named(name: "resource_bundles", value: resourceBundles.toSkylark()), + .named(name: "data", value: packData()), + .named(name: "deps", value: deps.toSkylark()), + .named(name: "sdk_frameworks", value: sdkFrameworks.toSkylark()), + .named(name: "weak_sdk_frameworks", value: weakSdkFrameworks.toSkylark()), + .named(name: "sdk_dylibs", value: sdkDylibs.toSkylark()), + .named(name: "swift_defines", value: swiftDefines), + .named(name: "objc_defines", value: objcDefines), + .named(name: "swift_copts", value: swiftCopts.toSkylark()), + .named(name: "objc_copts", value: objcCopts.toSkylark()), + .named(name: "linkopts", value: linkOpts.toSkylark()), + .named(name: "xcconfig", value: xcconfig.toSkylark()), + .named(name: "visibility", value: ["//visibility:public"].toSkylark()) + ] + .filter({ + switch $0 { + case .basic: + return true + case .named(_, let value): + return !value.isEmpty + } + }) + + return .functionCall( + name: "apple_framework", + arguments: lines + ) + } + + private func packData() -> SkylarkNode { + let data: SkylarkNode + let resources = self.resources.multi.ios ?? [] + let bundles = self.bundles.multi.ios ?? [] + let resourcesNode = GlobNode(include: resources).toSkylark() + let bundlesNode = bundles.toSkylark() + + switch (!resources.isEmpty, !bundles.isEmpty) { + case (false, false): + data = .empty + case (true, false): + data = resourcesNode + case (false, true): + data = bundlesNode + case (true, true): + data = SkylarkNode.expr(lhs: resourcesNode, op: "+", rhs: bundlesNode) + } + return data + } + + private static func resolveModuleName(spec: PodSpec) -> AttrSet { + let transformer: (String) -> String = { + $0.replacingOccurrences(of: "-", with: "_") + } + let moduleNameAttr = spec.attr(\.moduleName) + if moduleNameAttr.isEmpty { + return AttrSet(basic: transformer(spec.name)) + } + return spec.attr(\.moduleName).map({ + if let value = $0, !value.isEmpty { + return value + } + return transformer(spec.name) + }) + + } + + private static func getSourcesNodes(spec: PodSpec, deps: [PodSpec] = [], options: BuildOptions) -> AttrSet { + let (implFiles, implExcludes) = Self.getSources(spec: spec, deps: deps, options: options) + + return implFiles.zip(implExcludes).map { + GlobNode(include: .left($0.first ?? Set()), exclude: .left($0.second ?? Set())) + } + } + + private static func getSources(spec: PodSpec, deps: [PodSpec] = [], options: BuildOptions) -> (includes: AttrSet>, excludes: AttrSet>) { + let depsIncludes = AttrSet>(value: .empty) + let depsExcludes = AttrSet>(value: .empty) + + let depsSources = deps.reduce((includes: depsIncludes, excludes: depsExcludes)) { partialResult, spec in + let sources = Self.getSources(spec: spec, options: options) + let includes = partialResult.includes <> sources.includes + let excludes = partialResult.excludes <> sources.excludes + return (includes, excludes) + } + + let allSourceFiles = spec.attr(\.sourceFiles) + let implFiles = extractFiles(fromPattern: allSourceFiles, includingFileTypes: AnyFileTypes, options: options) + .unpackToMulti() + .map { Set($0) } + + let allExcludes = spec.attr(\.excludeFiles) + let implExcludes = extractFiles(fromPattern: allExcludes, includingFileTypes: AnyFileTypes, options: options) + .unpackToMulti() + .map { Set($0) } + return (implFiles <> depsSources.includes, implExcludes <> depsSources.excludes) + } + + private static func getFilesNodes(from spec: PodSpec, + subspecs: [PodSpec] = [], + includesKeyPath: KeyPath, + excludesKeyPath: KeyPath? = nil, + fileTypes: Set, + options: BuildOptions) -> AttrSet { + let (implFiles, implExcludes) = Self.getFiles(from: spec, + subspecs: subspecs, + includesKeyPath: includesKeyPath, + excludesKeyPath: excludesKeyPath, + fileTypes: fileTypes, + options: options) + + return implFiles.zip(implExcludes).map { + GlobNode(include: .left($0.first ?? Set()), exclude: .left($0.second ?? Set())) + } + } + + private static func getFiles(from spec: PodSpec, + subspecs: [PodSpec] = [], + includesKeyPath: KeyPath, + excludesKeyPath: KeyPath? = nil, + fileTypes: Set, + options: BuildOptions) -> (includes: AttrSet>, excludes: AttrSet>) { + let depsIncludes = AttrSet>(value: .empty) + let depsExcludes = AttrSet>(value: .empty) + + let depsSources = subspecs.reduce((includes: depsIncludes, excludes: depsExcludes)) { partialResult, spec in + let sources = Self.getFiles(from: spec, includesKeyPath: includesKeyPath, excludesKeyPath: excludesKeyPath, fileTypes: fileTypes, options: options) + let includes = partialResult.includes <> sources.includes + let excludes = partialResult.excludes <> sources.excludes + return (includes, excludes) + } + + let allFiles = spec.attr(includesKeyPath) + let implFiles = extractFiles(fromPattern: allFiles, includingFileTypes: fileTypes, options: options) + .unpackToMulti() + .map { Set($0) } + + var implExcludes: AttrSet> = AttrSet.empty + + if let excludesKeyPath = excludesKeyPath { + let allExcludes = spec.attr(excludesKeyPath) + implExcludes = extractFiles(fromPattern: allExcludes, includingFileTypes: fileTypes, options: options) + .unpackToMulti() + .map { Set($0) } + } + + return (implFiles <> depsSources.includes, implExcludes <> depsSources.excludes) + } + + private static func resolveSwiftVersion(spec: FallbackSpec) -> AttrSet { + return spec.attr(\.swiftVersions).map { + if let versions = $0?.compactMap({ Double($0) }) { + if versions.contains(where: { $0 >= 5.0 }) { + return "5" + } else if versions.contains(where: { $0 >= 4.2 }) { + return "4.2" + } else if !versions.isEmpty { + return "4" + } + } + return nil + } + } +} diff --git a/Sources/PodToBUILD/Targets/AppleFrameworkImport.swift b/Sources/PodToBUILD/Targets/AppleFrameworkImport.swift new file mode 100644 index 0000000..765991e --- /dev/null +++ b/Sources/PodToBUILD/Targets/AppleFrameworkImport.swift @@ -0,0 +1,103 @@ +// +// AppleFrameworkImport.swift +// BazelPods +// +// Created by Sergey Khliustin on 27.08.2022. +// + +import Foundation + +// https://github.com/bazelbuild/rules_apple/blob/818e795208ae3ca1cf1501205549d46e6bc88d73/doc/rules-general.md#apple_static_framework_import +struct AppleFrameworkImport: BazelTarget { + var loadNode: String { + let rule = appleFrameworkImport(isDynamic: isDynamic, isXCFramework: isXCFramework) + if isXCFramework { + return "load('@build_bazel_rules_apple//apple:apple.bzl', '\(rule)')" + } else { + return "load('@build_bazel_rules_ios//rules:apple_patched.bzl', '\(rule)')" + } + } + let name: String // A unique name for this rule. + let frameworkImport: AttrSet // The list of files under a .framework directory which are provided to Objective-C targets that depend on this target. + let isXCFramework: Bool + let isDynamic: Bool + + init(name: String, isDynamic: Bool, isXCFramework: Bool, frameworkImport: AttrSet) { + self.name = name + self.isDynamic = isDynamic + self.frameworkImport = frameworkImport + self.isXCFramework = isXCFramework + } + + // apple_static_framework_import( + // name = "OCMock", + // framework_imports = [ + // glob(["iOS/OCMock.framework/**"]), + // ], + // visibility = ["visibility:public"] + // ) + func toSkylark() -> SkylarkNode { + let ruleName = appleFrameworkImport(isDynamic: isDynamic, isXCFramework: isXCFramework) + + return SkylarkNode.functionCall( + name: ruleName, + arguments: [SkylarkFunctionArgument]([ + .named(name: "name", value: .string(name)), + .named(name: isXCFramework ? "xcframework_imports": "framework_imports", + value: frameworkImport.map { + GlobNode(include: Set([$0 + "/**"])) + }.toSkylark()), + .named(name: "visibility", value: .list(["//visibility:public"])) + ]) + ) + } + + /// framework import for apple framework import + /// - Parameters: + /// - isDynamic: whether internal framework is dynamic or static + /// - isXCFramework: if it is XCFramework + /// - Returns: apple framework import string such as "apple_static_xcframework_import" + func appleFrameworkImport(isDynamic: Bool, isXCFramework: Bool) -> String { + return "apple_" + (isDynamic ? "dynamic_" : "static_") + (isXCFramework ? "xcframework_" : "framework_") + "import" + } + + static func vendoredFrameworks(withPodspec spec: PodSpec, subspecs: [PodSpec], options: BuildOptions) -> [BazelTarget] { + // TODO: Make frameworks AttrSet + let vendoredFrameworks = spec.collectAttribute(with: subspecs, keyPath: \.vendoredFrameworks) + let frameworks = vendoredFrameworks.map { + $0.compactMap { + var isDynamic: Bool = false + + let frameworkPath = URL(fileURLWithPath: $0, relativeTo: URL(fileURLWithPath: options.sourcePath)) + let frameworkExtension = frameworkPath.pathExtension + let frameworkName = frameworkPath.deletingPathExtension().lastPathComponent + + let isXCFramework = frameworkExtension == "xcframework" + if !isXCFramework { + let executablePath = frameworkPath.appendingPathComponent(frameworkName) + let archs = SystemShellContext().command("/usr/bin/lipo", arguments: ["-archs", executablePath.path]).standardOutputAsString + // TODO: Refactor this + if !archs.contains("x86_64") && frameworkExtension == "framework" { + return nil + } + // TODO: Find proper way + let output = SystemShellContext().command("/usr/bin/file", arguments: [executablePath.path]).standardOutputAsString + isDynamic = output.contains("dynamically") + } else { + let contents = (try? FileManager.default.contentsOfDirectory(at: frameworkPath, includingPropertiesForKeys: nil)) ?? [] + if let slice = contents.first(where: { $0.lastPathComponent.hasPrefix("ios-") && $0.lastPathComponent.contains("x86_64") }) { + let sliceContents = (try? FileManager.default.contentsOfDirectory(at: slice, includingPropertiesForKeys: nil)) ?? [] + if sliceContents.contains(where: { $0.pathExtension == "framework" }) { + isDynamic = true + } + } + } + return AppleFrameworkImport(name: "\(spec.moduleName ?? spec.name)_\(frameworkName)_VendoredFramework", + isDynamic: isDynamic, + isXCFramework: isXCFramework, + frameworkImport: AttrSet(basic: $0)) + } as [AppleFrameworkImport] + } + return (frameworks.basic ?? []) + (frameworks.multi.ios ?? []) + } +} diff --git a/Sources/PodToBUILD/Targets/AppleResourceBundle.swift b/Sources/PodToBUILD/Targets/AppleResourceBundle.swift new file mode 100644 index 0000000..e0e76bf --- /dev/null +++ b/Sources/PodToBUILD/Targets/AppleResourceBundle.swift @@ -0,0 +1,55 @@ +// +// AppleResourceBundle.swift +// BazelPods +// +// Created by Sergey Khliustin on 27.08.2022. +// + +import Foundation + +// Currently not used +// https://github.com/bazelbuild/rules_apple/blob/0.13.0/doc/rules-resources.md#apple_resource_bundle +struct AppleResourceBundle: BazelTarget { + let loadNode = "load('@build_bazel_rules_apple//apple:resources.bzl', 'apple_resource_bundle')" + let name: String + let bundleName: String + let resources: AttrSet> + + func toSkylark() -> SkylarkNode { + let resources = extractResources(patterns: (resources.basic ?? []).union(resources.multi.ios ?? [])) + + return .functionCall( + name: "apple_resource_bundle", + arguments: [ + .named(name: "name", value: name.toSkylark()), + .named(name: "bundle_name", value: bundleName.toSkylark()), + .named(name: "infoplists", value: ["\(name)_InfoPlist"].toSkylark()), + .named(name: "resources", + value: GlobNode(include: resources).toSkylark() ) + ]) + } + + static func bundleResources(withPodSpec spec: PodSpec, subspecs: [PodSpec], options: BuildOptions) -> [BazelTarget] { + // See if the Podspec specifies a prebuilt .bundle file + + let resourceBundles = spec.collectAttribute(with: subspecs, keyPath: \.resourceBundles) + .map({ value -> [String: Set] in + var result = [String: Set]() + for key in value.keys { + result[key] = Set(extractResources(patterns: value[key]!)) + } + return result + }) + .map({ + return $0.map({ + (x: (String, Set)) -> BazelTarget in + let name = "\(spec.moduleName ?? spec.name)_\(x.0)_Bundle" + let bundleName = x.0 + return AppleResourceBundle(name: name, bundleName: bundleName, resources: AttrSet(basic: x.1)) + }) + }) + + return ((resourceBundles.basic ?? []) + (resourceBundles.multi.ios ?? + [])).sorted { $0.name < $1.name } + } +} diff --git a/Sources/PodToBUILD/Targets/Base/BazelTarget.swift b/Sources/PodToBUILD/Targets/Base/BazelTarget.swift new file mode 100644 index 0000000..27af1de --- /dev/null +++ b/Sources/PodToBUILD/Targets/Base/BazelTarget.swift @@ -0,0 +1,13 @@ +// +// BazelTarget.swift +// PodToBUILD +// +// Created by Jerry Marino on 10/17/2018. +// Copyright © 2018 Pinterest Inc. All rights reserved. +// + +/// Law: Names must be valid bazel names; see the spec +protocol BazelTarget: SkylarkConvertible { + var loadNode: String { get } + var name: String { get } +} diff --git a/Sources/PodToBUILD/Targets/Base/GenRule.swift b/Sources/PodToBUILD/Targets/Base/GenRule.swift new file mode 100644 index 0000000..ea89fab --- /dev/null +++ b/Sources/PodToBUILD/Targets/Base/GenRule.swift @@ -0,0 +1,34 @@ +// +// GenRule.swift +// PodToBUILD +// +// Created by Sergey Khliustin on 02.09.2022. +// + +import Foundation + +class GenRule: BazelTarget { + let loadNode = "" + let name: String + let srcs: [String] + let outs: [String] + let cmd: String + + init(name: String, srcs: [String] = [], outs: [String] = [], cmd: String = "") { + self.name = name + self.srcs = srcs + self.outs = outs + self.cmd = cmd + } + + func toSkylark() -> SkylarkNode { + return .functionCall( + name: "genrule", + arguments: [ + .named(name: "name", value: name.toSkylark()), + .named(name: "srcs", value: srcs.toSkylark()), + .named(name: "outs", value: outs.toSkylark()), + .named(name: "cmd", value: .multiLineString(cmd)) + ]) + } +} diff --git a/Sources/PodToBUILD/Targets/ConfigSetting.swift b/Sources/PodToBUILD/Targets/ConfigSetting.swift new file mode 100644 index 0000000..ca9b562 --- /dev/null +++ b/Sources/PodToBUILD/Targets/ConfigSetting.swift @@ -0,0 +1,50 @@ +// +// ConfigSetting.swift +// BazelPods +// +// Created by Sergey Khliustin on 27.08.2022. +// + +import Foundation + +// https://bazel.build/versions/master/docs/be/general.html#config_setting +public struct ConfigSetting: BazelTarget { + public let loadNode = "" + public let name: String + let values: [String: String] + + public func toSkylark() -> SkylarkNode { + return .functionCall( + name: "config_setting", + arguments: [ + .named(name: "name", value: name.toSkylark()), + .named(name: "values", value: values.toSkylark()) + ]) + } + + /// Config Setting Nodes + /// Write Build dependent COPTS. + /// @note We consume this as an expression in ObjCLibrary + static func makeConfigSettingNodes() -> SkylarkNode { + let comment = [ + "# Add a config setting release for compilation mode", + "# Assume that people are using `opt` for release mode", + "# see the bazel user manual for more information", + "# https://docs.bazel.build/versions/master/be/general.html#config_setting", + ].map { SkylarkNode.skylark($0) } + return .lines([.lines(comment), + ConfigSetting( + name: "release", + values: ["compilation_mode": "opt"]).toSkylark(), + ConfigSetting( + name: "osxCase", + values: ["apple_platform_type": "macos"]).toSkylark(), + ConfigSetting( + name: "tvosCase", + values: ["apple_platform_type": "tvos"]).toSkylark(), + ConfigSetting( + name: "watchosCase", + values: ["apple_platform_type": "watchos"]).toSkylark() + ]) + } +} diff --git a/Sources/PodToBUILD/Targets/InfoPlist/InfoPlist.swift b/Sources/PodToBUILD/Targets/InfoPlist/InfoPlist.swift new file mode 100644 index 0000000..8dfa705 --- /dev/null +++ b/Sources/PodToBUILD/Targets/InfoPlist/InfoPlist.swift @@ -0,0 +1,59 @@ +// +// InfoPlist.swift +// BazelPods +// +// Created by Sergey Khliustin on 02.09.2022. +// + +import Foundation + +final class InfoPlist: GenRule { + struct PlistData: Codable { + enum PackageType: String, Codable { + case BNDL + } + enum Platforms: String, Codable { + case iPhoneSimulator + } + + var CFBundleInfoDictionaryVersion = "6.0" + var CFBundleSignature = "????" + var CFBundleVersion = "1" + var NSPrincipalClass: String = "" + let CFBundleIdentifier: String + let CFBundleName: String + let CFBundleShortVersionString: String + let CFBundlePackageType: PackageType + let MinimumOSVersion: String + let CFBundleSupportedPlatforms: [Platforms] + let UIDeviceFamily: [Int] + + } + + init(name: String, data: PlistData) { + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml + var xml: String = "" + do { + let encoded = try encoder.encode(data) + xml = String(data: encoded, encoding: .utf8) ?? "" + } catch { + print("Error encode Info.plist: \(error)") + } + let cmd = "cat < $@\n\(xml)\nEOF" + super.init(name: name, outs: ["\(name).plist"], cmd: cmd) + } + + convenience init(bundle: AppleResourceBundle, spec: PodSpec, options: BuildOptions) { + let data = PlistData( + CFBundleIdentifier: "org.cocoapods.\(bundle.bundleName)", + CFBundleName: bundle.bundleName, + CFBundleShortVersionString: spec.version ?? "1.0", + CFBundlePackageType: .BNDL, + MinimumOSVersion: spec.platforms?["ios"] ?? options.iosPlatform, + CFBundleSupportedPlatforms: [.iPhoneSimulator], + UIDeviceFamily: [1, 2] + ) + self.init(name: bundle.name + "_InfoPlist", data: data) + } +} diff --git a/Sources/PodToBUILD/Targets/ObjcImport.swift b/Sources/PodToBUILD/Targets/ObjcImport.swift new file mode 100644 index 0000000..0851f5c --- /dev/null +++ b/Sources/PodToBUILD/Targets/ObjcImport.swift @@ -0,0 +1,30 @@ +// +// ObjcImport.swift +// BazelPods +// +// Created by Sergey Khliustin on 27.08.2022. +// + +import Foundation + +// https://bazel.build/versions/master/docs/be/objective-c.html#objc_import +struct ObjcImport: BazelTarget { + let loadNode = "" + let name: String // A unique name for this rule. + let archives: AttrSet> // The list of .a files provided to Objective-C targets that depend on this target. + + func toSkylark() -> SkylarkNode { + return SkylarkNode.functionCall( + name: "objc_import", + arguments: [ + .named(name: "name", value: name.toSkylark()), + .named(name: "archives", value: archives.toSkylark()), + ] + ) + } + + static func vendoredLibraries(withPodspec spec: PodSpec, subspecs: [PodSpec]) -> [BazelTarget] { + let libraries = spec.collectAttribute(with: subspecs, keyPath: \.vendoredLibraries) + return libraries.isEmpty ? [] : [ObjcImport(name: "\(spec.moduleName ?? spec.name)_VendoredLibraries", archives: libraries)] + } +} diff --git a/Sources/PodToBUILD/UserConfigurable.swift b/Sources/PodToBUILD/UserConfigurable.swift new file mode 100644 index 0000000..7135260 --- /dev/null +++ b/Sources/PodToBUILD/UserConfigurable.swift @@ -0,0 +1,107 @@ +// +// UserConfigurable.swift +// PodToBUILD +// +// Created by Jerry Marino on 5/2/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import Foundation + +public struct UserConfigurableTargetAttributes { + let keyPathOperators: [String] + + init(keyPathOperators: [String]) { + self.keyPathOperators = keyPathOperators + } + + init (buildOptions: BuildOptions) { + // User options available are keypath operators + keyPathOperators = buildOptions.userOptions + } +} + +/// Support a collection of operators +enum UserConfigurableOpt : String { + /// Add values to a value + /// EX: "Some.copts += -foo, -bar" + case PlusEqual = "+=" +} + +protocol UserConfigurable: BazelTarget { + var name : String { get } + + /// Add a given value to a key + mutating func add(configurableKey: String, value: Any) + + /// Apply options. + /// This shouldn't be implemented in consumers + func apply(keyPathOperators: [String], copts: [String]) -> Self +} + +extension UserConfigurable { + func apply(keyPathOperators: [String], copts: [String]) -> Self { + var copy = self + // First, apply all of the global options + copts.forEach { copy.add(configurableKey: "copts", value: $0) } + // Explicit keyPathOperators override defaults + // Since in LLVM option parsing, the rightmost option wins + // https://clang.llvm.org/doxygen/classclang_1_1tooling_1_1CommonOptionsParser.html + for keyPathOperator in keyPathOperators { + guard let opt = UserConfigurableOpt(rawValue: "+=") else { + print("Invalid operator") + fatalError() + } + + let components = keyPathOperator.components(separatedBy: opt.rawValue) + guard components.count > 1 else { continue } + + let key = components[0].trimmingCharacters(in: .whitespaces) + let values = components[1].components(separatedBy: ",") + for value in values { + let value = value.trimmingCharacters(in: .whitespaces) + copy.add(configurableKey: key, value: value) + } + } + return copy + } +} + +enum UserConfigurableTransform : SkylarkConvertibleTransform { + public static func transform(convertibles: [BazelTarget], options: + BuildOptions, podSpec: PodSpec) -> [BazelTarget] { + let attributes = UserConfigurableTargetAttributes(buildOptions: options) + return UserConfigurableTransform.executeUserOptionsTransform(onConvertibles: convertibles, copts: options.globalCopts, userAttributes: attributes) + } + + public static func executeUserOptionsTransform(onConvertibles convertibles: + [BazelTarget], copts: + [String], userAttributes: + UserConfigurableTargetAttributes) + -> [BazelTarget] { + var operatorByTarget = [String: [String]]() + for keyPath in userAttributes.keyPathOperators { + let components = keyPath.split(separator: ".", maxSplits: 1) + if let target = components.first { + var oprs = (operatorByTarget[String(target)] ?? [String]()) + oprs.append(String(components[1])) + operatorByTarget[String(target)] = oprs + } + } + + let output: [BazelTarget] = convertibles.map { + (inputConvertible: BazelTarget) in + guard let configurable = inputConvertible as? UserConfigurable else { + return inputConvertible + } + + if let operators = operatorByTarget[configurable.name] { + return configurable.apply(keyPathOperators: operators, copts: + copts) + } else { + return configurable.apply(keyPathOperators: [], copts: copts) + } + } + return output + } +} diff --git a/Sources/PodToBUILD/XCConfig/XCConfigParser.swift b/Sources/PodToBUILD/XCConfig/XCConfigParser.swift new file mode 100644 index 0000000..f9605fc --- /dev/null +++ b/Sources/PodToBUILD/XCConfig/XCConfigParser.swift @@ -0,0 +1,70 @@ +// +// XCConfigParser.swift +// PodToBUILD +// +// Created by Sergey Khliustin on 02.09.2022. +// + +import Foundation + +final class XCConfigParser { + private(set) var xcconfig: [String: SkylarkNode] = [:] + private(set) var swiftCopts: [String] = [] + private(set) var objcCopts: [String] = [] + private(set) var linkOpts: [String] = [] + private let transformers: [String: XCConfigSettingTransformer] + private static let defaultTransformers: [XCConfigSettingTransformer] = [ + HeaderSearchPathTransformer(), + ApplicationExtensionAPIOnlyTransformer(), + LinkOptsListTransformer("OTHER_LDFLAGS"), + ObjCOptsListTransformer("OTHER_CFLAGS"), + ObjCOptsListTransformer("OTHER_CPLUSPLUSFLAGS") + ] + + convenience init(spec: PodSpec, subspecs: [PodSpec] = [], options: BuildOptions) { + let resolved = + spec.collectAttribute(with: subspecs, keyPath: \.xcconfig) <> + spec.collectAttribute(with: subspecs, keyPath: \.podTargetXcconfig) <> + spec.collectAttribute(with: subspecs, keyPath: \.userTargetXcconfig) + + self.init(resolved.multi.ios ?? [:], options: options) + } + + init(_ config: [String: String], + options: BuildOptions, + transformers: [XCConfigSettingTransformer] = defaultTransformers) { + + self.transformers = transformers.reduce([String: XCConfigSettingTransformer](), { result, transformer in + var result = result + result[transformer.key] = transformer + return result + }) + + for key in config.keys { + guard !XCSpecs.forceIgnore.contains(key) else { continue } + let node: SkylarkNode? + let value = replacePodsEnvVars(config[key]!, options: options) + switch XCSpecs.allSettingsKeyType[key] { + case .boolean, .string, .enumeration: + node = .string(value) + case .stringList: + node = .list(xcconfigSettingToList(value).map({ $0.toSkylark() })) + case .path: + node = .string(value) + case .pathList: + node = .list(xcconfigSettingToList(value).map({ $0.toSkylark() })) + case .none: + node = nil + } + if let node = node { + xcconfig[key] = node + } else if let transformer = self.transformers[key] { + swiftCopts += (transformer as? SwiftCoptsProvider)?.swiftCopts(value) ?? [] + objcCopts += (transformer as? ObjcCoptsProvider)?.objcCopts(value) ?? [] + linkOpts += (transformer as? LinkOptsProvider)?.linkOpts(value) ?? [] + } else { + print("WARNING: Unhandled xcconfig \(key)") + } + } + } +} diff --git a/Sources/PodToBUILD/XCConfig/XCConfigSettingTransformer.swift b/Sources/PodToBUILD/XCConfig/XCConfigSettingTransformer.swift new file mode 100644 index 0000000..16eb1d4 --- /dev/null +++ b/Sources/PodToBUILD/XCConfig/XCConfigSettingTransformer.swift @@ -0,0 +1,94 @@ +// +// XCConfigSettingTransformer.swift +// BazelPods +// +// Created by Sergey Khliustin on 02.09.2022. +// + +import Foundation + +protocol XCConfigSettingTransformer { + var key: String { get } +} + +protocol SwiftCoptsProvider { + func swiftCopts(_ value: String) -> [String] +} + +protocol ObjcCoptsProvider { + func objcCopts(_ value: String) -> [String] +} + +protocol LinkOptsProvider { + func linkOpts(_ value: String) -> [String] +} + +struct HeaderSearchPathTransformer: XCConfigSettingTransformer, + SwiftCoptsProvider, + ObjcCoptsProvider { + let key = "HEADER_SEARCH_PATHS" + + func swiftCopts(_ value: String) -> [String] { + return xcconfigSettingToList(value) + .reduce([String]()) { partialResult, path in + return partialResult + [ + "-Xcc", "-I\(path.replacingOccurrences(of: "\"", with: ""))" + ] + } + } + + func objcCopts(_ value: String) -> [String] { + return xcconfigSettingToList(value) + .map({ "-I\($0.replacingOccurrences(of: "\"", with: ""))" }) + } +} + +struct ApplicationExtensionAPIOnlyTransformer: XCConfigSettingTransformer, + SwiftCoptsProvider, + ObjcCoptsProvider { + let key = "APPLICATION_EXTENSION_API_ONLY" + + func swiftCopts(_ value: String) -> [String] { + return value.lowercased() == "yes" ? ["-application-extension"] : [] + } + + func objcCopts(_ value: String) -> [String] { + return value.lowercased() == "yes" ? ["-fapplication-extension"] : [] + } +} + +struct LinkOptsListTransformer: XCConfigSettingTransformer, + LinkOptsProvider { + let key: String + init(_ key: String) { + self.key = key + } + + func linkOpts(_ value: String) -> [String] { + return xcconfigSettingToList(value) + } +} + +struct ObjCOptsListTransformer: XCConfigSettingTransformer, + ObjcCoptsProvider { + let key: String + init(_ key: String) { + self.key = key + } + + func objcCopts(_ value: String) -> [String] { + return xcconfigSettingToList(value) + } +} + +struct SwiftOptsListTransformer: XCConfigSettingTransformer, + SwiftCoptsProvider { + let key: String + init(_ key: String) { + self.key = key + } + + func swiftCopts(_ value: String) -> [String] { + return xcconfigSettingToList(value) + } +} diff --git a/Sources/PodToBUILD/XCConfig/XCSpecs.swift b/Sources/PodToBUILD/XCConfig/XCSpecs.swift new file mode 100644 index 0000000..f8b53ee --- /dev/null +++ b/Sources/PodToBUILD/XCConfig/XCSpecs.swift @@ -0,0 +1,404 @@ +// +// XCSpecs.swift +// PodToBUILD +// +// Created by Sergey Khliustin on 02.09.2022. +// + +import Foundation + +// https://github.com/bazel-ios/rules_ios/blob/master/data/xcspecs.bzl +// TODO: Optimize +struct XCSpecs { + enum XType { + case boolean + case string + case stringList + case path + case pathList + case enumeration + } + typealias XElement = (name: String, type: XType) + + static let allSettings = clang + coredata + coredataMapping + swift + static let allSettingsKeyType: [String: XType] = { + return allSettings.reduce(into: [String: XType]()) { result, element in + result[element.name] = element.type + } + }() + + // TODO: Investigate + static let forceIgnore = [ + "GCC_C_LANGUAGE_STANDARD" + ] + + // com.apple.compilers.llvm.clang.1_0 + static let clang: [XElement] = [ + ("CLANG_ADDRESS_SANITIZER", .boolean), + ("CLANG_ADDRESS_SANITIZER_ALLOW_ERROR_RECOVERY", .boolean), + ("CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW", .boolean), + ("CLANG_ADDRESS_SANITIZER_USE_AFTER_SCOPE", .boolean), + ("CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES", .boolean), + ("CLANG_ARC_MIGRATE_DIR", .path), + ("CLANG_ARC_MIGRATE_EMIT_ERROR", .boolean), + ("CLANG_ARC_MIGRATE_PRECHECK", .enumeration), + ("CLANG_ARC_MIGRATE_REPORT_OUTPUT", .path), + ("CLANG_BITCODE_GENERATION_MODE", .enumeration), + ("CLANG_COLOR_DIAGNOSTICS", .boolean), + ("CLANG_COVERAGE_MAPPING", .boolean), + ("CLANG_COVERAGE_MAPPING_LINKER_ARGS", .boolean), + ("CLANG_CXX_LANGUAGE_STANDARD", .enumeration), + ("CLANG_CXX_LIBRARY", .enumeration), + ("CLANG_DEBUG_INFORMATION_LEVEL", .enumeration), + ("CLANG_DEBUG_MODULES", .boolean), + ("CLANG_ENABLE_APP_EXTENSION", .boolean), + ("CLANG_ENABLE_CODE_COVERAGE", .boolean), + ("CLANG_ENABLE_CPP_STATIC_DESTRUCTORS", .boolean), + ("CLANG_ENABLE_MODULES", .boolean), + ("CLANG_ENABLE_MODULE_DEBUGGING", .boolean), + ("CLANG_ENABLE_MODULE_IMPLEMENTATION_OF", .boolean), + ("CLANG_ENABLE_OBJC_ARC", .boolean), + ("CLANG_ENABLE_OBJC_WEAK", .boolean), + ("CLANG_INDEX_STORE_ENABLE", .boolean), + ("CLANG_INDEX_STORE_PATH", .path), + ("CLANG_INSTRUMENT_FOR_OPTIMIZATION_PROFILING", .boolean), + ("CLANG_LINK_OBJC_RUNTIME", .boolean), + ("CLANG_MACRO_BACKTRACE_LIMIT", .string), + ("CLANG_MODULES_AUTOLINK", .boolean), + ("CLANG_MODULES_BUILD_SESSION_FILE", .string), + ("CLANG_MODULES_DISABLE_PRIVATE_WARNING", .boolean), + ("CLANG_MODULES_IGNORE_MACROS", .stringList), + ("CLANG_MODULES_PRUNE_AFTER", .string), + ("CLANG_MODULES_PRUNE_INTERVAL", .string), + ("CLANG_MODULES_VALIDATE_SYSTEM_HEADERS", .boolean), + ("CLANG_MODULE_CACHE_PATH", .string), + ("CLANG_MODULE_LSV", .boolean), + ("CLANG_OBJC_MIGRATE_DIR", .path), + ("CLANG_OPTIMIZATION_PROFILE_FILE", .path), + ("CLANG_RETAIN_COMMENTS_FROM_SYSTEM_HEADERS", .boolean), + ("CLANG_TARGET_TRIPLE_ARCHS", .stringList), + ("CLANG_TARGET_TRIPLE_VARIANTS", .stringList), + ("CLANG_THREAD_SANITIZER", .boolean), + ("CLANG_TOOLCHAIN_FLAGS", .stringList), + ("CLANG_TRIVIAL_AUTO_VAR_INIT", .enumeration), + ("CLANG_UNDEFINED_BEHAVIOR_SANITIZER", .boolean), + ("CLANG_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER", .boolean), + ("CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY", .boolean), + ("CLANG_USE_OPTIMIZATION_PROFILE", .boolean), + ("CLANG_WARN_ASSIGN_ENUM", .boolean), + ("CLANG_WARN_ATOMIC_IMPLICIT_SEQ_CST", .boolean), + ("CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING", .enumeration), + ("CLANG_WARN_BOOL_CONVERSION", .enumeration), + ("CLANG_WARN_COMMA", .enumeration), + ("CLANG_WARN_CONSTANT_CONVERSION", .enumeration), + ("CLANG_WARN_CXX0X_EXTENSIONS", .boolean), + ("CLANG_WARN_DELETE_NON_VIRTUAL_DTOR", .enumeration), + ("CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS", .boolean), + ("CLANG_WARN_DIRECT_OBJC_ISA_USAGE", .enumeration), + ("CLANG_WARN_DOCUMENTATION_COMMENTS", .boolean), + ("CLANG_WARN_EMPTY_BODY", .boolean), + ("CLANG_WARN_ENUM_CONVERSION", .enumeration), + ("CLANG_WARN_FLOAT_CONVERSION", .enumeration), + ("CLANG_WARN_FRAMEWORK_INCLUDE_PRIVATE_FROM_PUBLIC", .enumeration), + ("CLANG_WARN_IMPLICIT_SIGN_CONVERSION", .enumeration), + ("CLANG_WARN_INFINITE_RECURSION", .boolean), + ("CLANG_WARN_INT_CONVERSION", .enumeration), + ("CLANG_WARN_MISSING_NOESCAPE", .enumeration), + ("CLANG_WARN_NON_LITERAL_NULL_CONVERSION", .enumeration), + ("CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION", .boolean), + ("CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE", .boolean), + ("CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES", .boolean), + ("CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF", .boolean), + ("CLANG_WARN_OBJC_INTERFACE_IVARS", .enumeration), + ("CLANG_WARN_OBJC_LITERAL_CONVERSION", .enumeration), + ("CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS", .boolean), + ("CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK", .enumeration), + ("CLANG_WARN_OBJC_ROOT_CLASS", .enumeration), + ("CLANG_WARN_PRAGMA_PACK", .enumeration), + ("CLANG_WARN_PRIVATE_MODULE", .boolean), + ("CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER", .enumeration), + ("CLANG_WARN_RANGE_LOOP_ANALYSIS", .boolean), + ("CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY", .boolean), + ("CLANG_WARN_STRICT_PROTOTYPES", .enumeration), + ("CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION", .enumeration), + ("CLANG_WARN_SUSPICIOUS_MOVE", .boolean), + ("CLANG_WARN_UNGUARDED_AVAILABILITY", .enumeration), + ("CLANG_WARN_UNREACHABLE_CODE", .enumeration), + ("CLANG_WARN_VEXING_PARSE", .enumeration), + ("CLANG_WARN__ARC_BRIDGE_CAST_NONARC", .boolean), + ("CLANG_WARN__DUPLICATE_METHOD_MATCH", .boolean), + ("CLANG_WARN__EXIT_TIME_DESTRUCTORS", .boolean), + ("CLANG_X86_VECTOR_INSTRUCTIONS", .enumeration), + ("CPP_HEADERMAP_FILE", .path), + ("CPP_HEADERMAP_FILE_FOR_ALL_NON_FRAMEWORK_TARGET_HEADERS", .path), + ("CPP_HEADERMAP_FILE_FOR_ALL_TARGET_HEADERS", .path), + ("CPP_HEADERMAP_FILE_FOR_GENERATED_FILES", .path), + ("CPP_HEADERMAP_FILE_FOR_OWN_TARGET_HEADERS", .path), + ("CPP_HEADERMAP_FILE_FOR_PROJECT_FILES", .path), + ("CPP_HEADERMAP_PRODUCT_HEADERS_VFS_FILE", .path), + ("CPP_HEADER_SYMLINKS_DIR", .path), + ("DEFAULT_SSE_LEVEL_3_NO", .string), + ("DEFAULT_SSE_LEVEL_3_SUPPLEMENTAL_NO", .string), + ("DEFAULT_SSE_LEVEL_3_SUPPLEMENTAL_YES", .string), + ("DEFAULT_SSE_LEVEL_3_YES", .string), + ("DEFAULT_SSE_LEVEL_4_1_NO", .string), + ("DEFAULT_SSE_LEVEL_4_1_YES", .string), + ("DEFAULT_SSE_LEVEL_4_2_NO", .string), + ("DEFAULT_SSE_LEVEL_4_2_YES", .string), + ("ENABLE_APPLE_KEXT_CODE_GENERATION", .boolean), + ("ENABLE_NS_ASSERTIONS", .boolean), + ("ENABLE_STRICT_OBJC_MSGSEND", .boolean), + ("GCC_CHAR_IS_UNSIGNED_CHAR", .boolean), + ("GCC_CW_ASM_SYNTAX", .boolean), + ("GCC_C_LANGUAGE_STANDARD", .enumeration), + ("GCC_DEBUG_INFORMATION_FORMAT", .enumeration), + ("GCC_DYNAMIC_NO_PIC", .boolean), + ("GCC_ENABLE_ASM_KEYWORD", .boolean), + ("GCC_ENABLE_BUILTIN_FUNCTIONS", .boolean), + ("GCC_ENABLE_CPP_EXCEPTIONS", .boolean), + ("GCC_ENABLE_CPP_RTTI", .boolean), + ("GCC_ENABLE_EXCEPTIONS", .boolean), + ("GCC_ENABLE_FLOATING_POINT_LIBRARY_CALLS", .boolean), + ("GCC_ENABLE_KERNEL_DEVELOPMENT", .boolean), + ("GCC_ENABLE_OBJC_EXCEPTIONS", .boolean), + ("GCC_ENABLE_PASCAL_STRINGS", .boolean), + ("GCC_ENABLE_SSE3_EXTENSIONS", .boolean), + ("GCC_ENABLE_SSE41_EXTENSIONS", .boolean), + ("GCC_ENABLE_SSE42_EXTENSIONS", .boolean), + ("GCC_ENABLE_SUPPLEMENTAL_SSE3_INSTRUCTIONS", .boolean), + ("GCC_ENABLE_TRIGRAPHS", .boolean), + ("GCC_FAST_MATH", .boolean), + ("GCC_GENERATE_DEBUGGING_SYMBOLS", .boolean), + ("GCC_GENERATE_TEST_COVERAGE_FILES", .boolean), + ("GCC_INCREASE_PRECOMPILED_HEADER_SHARING", .boolean), + ("GCC_INLINES_ARE_PRIVATE_EXTERN", .boolean), + ("GCC_INPUT_FILETYPE", .enumeration), + ("GCC_INSTRUMENT_PROGRAM_FLOW_ARCS", .boolean), + ("GCC_LINK_WITH_DYNAMIC_LIBRARIES", .boolean), + ("GCC_MACOSX_VERSION_MIN", .string), + ("GCC_NO_COMMON_BLOCKS", .boolean), + ("GCC_OBJC_ABI_VERSION", .enumeration), + ("GCC_OBJC_LEGACY_DISPATCH", .boolean), + ("GCC_OPERATION", .enumeration), + ("GCC_OPTIMIZATION_LEVEL", .enumeration), + ("GCC_PFE_FILE_C_DIALECTS", .stringList), + ("GCC_PRECOMPILE_PREFIX_HEADER", .boolean), + ("GCC_PREFIX_HEADER", .string), + ("GCC_PREPROCESSOR_DEFINITIONS", .stringList), + ("GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS", .stringList), + ("GCC_PRODUCT_TYPE_PREPROCESSOR_DEFINITIONS", .stringList), + ("GCC_REUSE_STRINGS", .boolean), + ("GCC_SHORT_ENUMS", .boolean), + ("GCC_STRICT_ALIASING", .boolean), + ("GCC_SYMBOLS_PRIVATE_EXTERN", .boolean), + ("GCC_THREADSAFE_STATICS", .boolean), + ("GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS", .boolean), + ("GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS", .boolean), + ("GCC_TREAT_WARNINGS_AS_ERRORS", .boolean), + ("GCC_UNROLL_LOOPS", .boolean), + ("GCC_USE_GCC3_PFE_SUPPORT", .boolean), + ("GCC_USE_STANDARD_INCLUDE_SEARCHING", .boolean), + ("GCC_WARN_64_TO_32_BIT_CONVERSION", .enumeration), + ("GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS", .boolean), + ("GCC_WARN_ABOUT_INVALID_OFFSETOF_MACRO", .boolean), + ("GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS", .boolean), + ("GCC_WARN_ABOUT_MISSING_NEWLINE", .boolean), + ("GCC_WARN_ABOUT_MISSING_PROTOTYPES", .boolean), + ("GCC_WARN_ABOUT_POINTER_SIGNEDNESS", .boolean), + ("GCC_WARN_ABOUT_RETURN_TYPE", .enumeration), + ("GCC_WARN_ALLOW_INCOMPLETE_PROTOCOL", .boolean), + ("GCC_WARN_CHECK_SWITCH_STATEMENTS", .boolean), + ("GCC_WARN_FOUR_CHARACTER_CONSTANTS", .boolean), + ("GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS", .boolean), + ("GCC_WARN_INHIBIT_ALL_WARNINGS", .boolean), + ("GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED", .boolean), + ("GCC_WARN_MISSING_PARENTHESES", .boolean), + ("GCC_WARN_MULTIPLE_DEFINITION_TYPES_FOR_SELECTOR", .boolean), + ("GCC_WARN_NON_VIRTUAL_DESTRUCTOR", .boolean), + ("GCC_WARN_PEDANTIC", .boolean), + ("GCC_WARN_SHADOW", .boolean), + ("GCC_WARN_SIGN_COMPARE", .boolean), + ("GCC_WARN_STRICT_SELECTOR_MATCH", .boolean), + ("GCC_WARN_TYPECHECK_CALLS_TO_PRINTF", .boolean), + ("GCC_WARN_UNDECLARED_SELECTOR", .boolean), + ("GCC_WARN_UNINITIALIZED_AUTOS", .enumeration), + ("GCC_WARN_UNKNOWN_PRAGMAS", .boolean), + ("GCC_WARN_UNUSED_FUNCTION", .boolean), + ("GCC_WARN_UNUSED_LABEL", .boolean), + ("GCC_WARN_UNUSED_PARAMETER", .boolean), + ("GCC_WARN_UNUSED_VALUE", .boolean), + ("GCC_WARN_UNUSED_VARIABLE", .boolean), + ("HEADERMAP_FILE_FORMAT", .enumeration), + ("LLVM_IMPLICIT_AGGRESSIVE_OPTIMIZATIONS", .boolean), + ("LLVM_LTO", .enumeration), + ("LLVM_OPTIMIZATION_LEVEL_VAL_0", .boolean), + ("LLVM_OPTIMIZATION_LEVEL_VAL_1", .boolean), + ("LLVM_OPTIMIZATION_LEVEL_VAL_2", .boolean), + ("LLVM_OPTIMIZATION_LEVEL_VAL_3", .boolean), + ("LLVM_OPTIMIZATION_LEVEL_VAL_fast", .boolean), + ("LLVM_OPTIMIZATION_LEVEL_VAL_s", .boolean), + ("LLVM_OPTIMIZATION_LEVEL_VAL_z", .boolean), + ("OTHER_CFLAGS", .stringList), + ("OTHER_CPLUSPLUSFLAGS", .stringList), + ("USE_HEADERMAP", .boolean), + ("USE_HEADER_SYMLINKS", .boolean), + ("WARNING_CFLAGS", .stringList), + ("arch", .string), + ("diagnostic_message_length", .string), + ("print_note_include_stack", .boolean) + ] + + // com.apple.compilers.model.coredata + static let coredata: [XElement] = [ + ("DEPLOYMENT_TARGET", .string), + ("MOMC_MODULE", .string), + ("MOMC_NO_DELETE_RULE_WARNINGS", .boolean), + ("MOMC_NO_INVERSE_RELATIONSHIP_WARNINGS", .boolean), + ("MOMC_NO_MAX_PROPERTY_COUNT_WARNINGS", .boolean), + ("MOMC_NO_WARNINGS", .boolean), + ("MOMC_OUTPUT_SUFFIX", .string), + ("MOMC_OUTPUT_SUFFIX__xcdatamodel", .string), + ("MOMC_OUTPUT_SUFFIX__xcdatamodeld", .string), + ("MOMC_SUPPRESS_INVERSE_TRANSIENT_ERROR", .boolean), + ("build_file_compiler_flags", .stringList) + ] + + // com.apple.compilers.model.coredatamapping + static let coredataMapping: [XElement] = [ + ("DEPLOYMENT_TARGET", .string), + ("MAPC_MODULE", .string), + ("MAPC_NO_WARNINGS", .boolean), + ("build_file_compiler_flags", .stringList) + ] + + // com.apple.pbx.linkers.ld + static let linker: [XElement] = [ + ("ALL_OTHER_LDFLAGS", .stringList), + ("ALTERNATE_LINKER", .string), + ("AdditionalCommandLineArguments", .stringList), + ("BUNDLE_LOADER", .path), + ("CLANG_ARC_MIGRATE_DIR", .path), + ("CLANG_ARC_MIGRATE_PRECHECK", .enumeration), + ("DEAD_CODE_STRIPPING", .boolean), + ("EXPORTED_SYMBOLS_FILE", .path), + ("FRAMEWORK_SEARCH_PATHS", .pathList), + ("GENERATE_PROFILING_CODE", .boolean), + ("INIT_ROUTINE", .string), + ("KEEP_PRIVATE_EXTERNS", .boolean), + ("LD_ADDITIONAL_DEPLOYMENT_TARGET_FLAGS", .stringList), + ("LD_BITCODE_GENERATION_MODE", .enumeration), + ("LD_DEBUG_VARIANT", .boolean), + ("LD_DEPENDENCY_INFO_FILE", .path), + ("LD_DEPLOYMENT_TARGET", .string), + ("LD_DONT_RUN_DEDUPLICATION", .boolean), + ("LD_DYLIB_ALLOWABLE_CLIENTS", .stringList), + ("LD_DYLIB_INSTALL_NAME", .path), + ("LD_EXPORT_GLOBAL_SYMBOLS", .boolean), + ("LD_FINAL_OUTPUT_FILE", .path), + ("LD_GENERATE_BITCODE_SYMBOL_MAP", .boolean), + ("LD_GENERATE_MAP_FILE", .boolean), + ("LD_HIDE_BITCODE_SYMBOLS", .boolean), + ("LD_LTO_OBJECT_FILE", .path), + ("LD_MAP_FILE_PATH", .path), + ("LD_NO_PIE", .boolean), + ("LD_OBJC_ABI_VERSION", .enumeration), + ("LD_QUOTE_LINKER_ARGUMENTS_FOR_COMPILER_DRIVER", .boolean), + ("LD_RUNPATH_SEARCH_PATHS", .pathList), + ("LD_TARGET_TRIPLE_ARCHS", .stringList), + ("LD_TARGET_TRIPLE_VARIANTS", .stringList), + ("LD_THREAD_SANITIZER", .boolean), + ("LD_VERIFY_BITCODE", .boolean), + ("LIBRARY_SEARCH_PATHS", .pathList), + ("LINKER_DISPLAYS_MANGLED_NAMES", .boolean), + ("LINK_WITH_STANDARD_LIBRARIES", .boolean), + ("MACH_O_TYPE", .enumeration), + ("ORDER_FILE", .path), + ("OTHER_LDRFLAGS", .stringList), + ("PRESERVE_DEAD_CODE_INITS_AND_TERMS", .boolean), + ("PRODUCT_TYPE_FRAMEWORK_SEARCH_PATHS", .pathList), + ("PRODUCT_TYPE_LIBRARY_SEARCH_PATHS", .pathList), + ("REEXPORTED_FRAMEWORK_NAMES", .stringList), + ("REEXPORTED_LIBRARY_NAMES", .stringList), + ("REEXPORTED_LIBRARY_PATHS", .pathList), + ("SYSTEM_FRAMEWORK_SEARCH_PATHS", .pathList), + ("UNEXPORTED_SYMBOLS_FILE", .path), + ("__INPUT_FILE_LIST_PATH__", .path), + ("arch", .string) + ] + + // com.apple.xcode.tools.ibtool.compiler + static let ibtool: [XElement] = [ + ("IBC_COMPILER_AUTO_ACTIVATE_CUSTOM_FONTS", .boolean), + ("IBC_COMPILER_USE_NIBARCHIVES_FOR_MACOS", .string), + ("IBC_ERRORS", .boolean), + ("IBC_FLATTEN_NIBS", .boolean), + ("IBC_MODULE", .string), + ("IBC_NOTICES", .boolean), + ("IBC_OTHER_FLAGS", .stringList), + ("IBC_OVERRIDING_PLUGINS_AND_FRAMEWORKS_DIR", .path), + ("IBC_PLUGINS", .stringList), + ("IBC_PLUGIN_SEARCH_PATHS", .pathList), + ("IBC_REGIONS_AND_STRINGS_FILES", .stringList), + ("IBC_WARNINGS", .boolean), + ("RESOURCES_PLATFORM_NAME", .string), + ("RESOURCES_TARGETED_DEVICE_FAMILY", .stringList), + ("XIB_COMPILER_INFOPLIST_CONTENT_FILE", .path), + ("build_file_compiler_flags", .stringList) + ] + + // com.apple.xcode.tools.swift.compiler + static let swift: [XElement] = [ + ("CLANG_COVERAGE_MAPPING", .boolean), + ("CLANG_COVERAGE_MAPPING_LINKER_ARGS", .boolean), + ("CLANG_MODULE_CACHE_PATH", .path), + ("FRAMEWORK_SEARCH_PATHS", .pathList), + ("GCC_GENERATE_DEBUGGING_SYMBOLS", .boolean), + ("OTHER_SWIFT_FLAGS", .stringList), + ("SWIFT_ACTIVE_COMPILATION_CONDITIONS", .stringList), + ("SWIFT_ADDRESS_SANITIZER", .boolean), + ("SWIFT_ADDRESS_SANITIZER_ALLOW_ERROR_RECOVERY", .boolean), + ("SWIFT_BITCODE_GENERATION_MODE", .enumeration), + ("SWIFT_COMPILATION_MODE", .enumeration), + ("SWIFT_CROSS_MODULE_OPTIMIZATION", .boolean), + ("SWIFT_DEPLOYMENT_TARGET", .string), + ("SWIFT_DISABLE_SAFETY_CHECKS", .boolean), + ("SWIFT_EMIT_MODULE_INTERFACE", .boolean), + ("SWIFT_ENABLE_APP_EXTENSION", .boolean), + ("SWIFT_ENABLE_BATCH_MODE", .boolean), + ("SWIFT_ENABLE_INCREMENTAL_COMPILATION", .boolean), + ("SWIFT_ENABLE_LIBRARY_EVOLUTION", .boolean), + ("SWIFT_ENABLE_TESTABILITY", .boolean), + ("SWIFT_ENFORCE_EXCLUSIVE_ACCESS", .enumeration), + ("SWIFT_EXEC", .path), + ("SWIFT_INCLUDE_PATHS", .pathList), + ("SWIFT_INDEX_STORE_ENABLE", .boolean), + ("SWIFT_INDEX_STORE_PATH", .path), + ("SWIFT_INSTALL_OBJC_HEADER", .boolean), + ("SWIFT_LIBRARIES_ONLY", .boolean), + ("SWIFT_LIBRARY_PATH", .path), + ("SWIFT_LINK_OBJC_RUNTIME", .boolean), + ("SWIFT_MIGRATE_CODE", .boolean), + ("SWIFT_MODULE_NAME", .string), + ("SWIFT_OBJC_BRIDGING_HEADER", .string), + ("SWIFT_OBJC_INTERFACE_HEADER_NAME", .string), + ("SWIFT_OPTIMIZATION_LEVEL", .enumeration), + ("SWIFT_PRECOMPILE_BRIDGING_HEADER", .boolean), + ("SWIFT_REFLECTION_METADATA_LEVEL", .enumeration), + ("SWIFT_RESOURCE_DIR", .path), + ("SWIFT_RESPONSE_FILE_PATH", .path), + ("SWIFT_SERIALIZE_DEBUGGING_OPTIONS", .boolean), + ("SWIFT_STDLIB", .string), + ("SWIFT_SUPPRESS_WARNINGS", .boolean), + ("SWIFT_TARGET_TRIPLE", .string), + ("SWIFT_TARGET_TRIPLE_VARIANTS", .stringList), + ("SWIFT_THREAD_SANITIZER", .boolean), + ("SWIFT_TOOLCHAIN_FLAGS", .stringList), + ("SWIFT_TREAT_WARNINGS_AS_ERRORS", .boolean), + ("SWIFT_USE_PARALLEL_WHOLE_MODULE_OPTIMIZATION", .boolean), + ("SWIFT_USE_PARALLEL_WMO_TARGETS", .boolean), + ("SWIFT_VERSION", .string), + ("SWIFT_WHOLE_MODULE_OPTIMIZATION", .boolean), + ("__SWIFT_ENFORCE_EXCLUSIVE_ACCESS_DEBUG_ENFORCEMENT_DEBUG", .boolean), + ("__SWIFT_ENFORCE_EXCLUSIVE_ACCESS_DEBUG_ENFORCEMENT_RELEASE", .boolean) + ] +} diff --git a/Tests/BUILD b/Tests/BUILD new file mode 100644 index 0000000..f64f436 --- /dev/null +++ b/Tests/BUILD @@ -0,0 +1,35 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +load("@build_bazel_rules_apple//apple:macos.bzl", "macos_unit_test") +# This tests RepoToolsCore and Starlark logic +swift_library( + name = "PodToBUILDTestsLib", + srcs = glob(["PodToBUILDTests/*.swift"]), + deps = [ + "//:PodToBUILD", + "@bazelpods-SwiftCheck//:SwiftCheck", + ], + data = glob(["Examples/**/*.podspec.json"]) +) + +macos_unit_test( + name = "PodToBUILDTests", + deps = [":PodToBUILDTestsLib"], + minimum_os_version = "10.11", +) + +swift_library( + name = "BuildTestsLib", + srcs = glob(["BuildTests/*.swift"]), + deps = [ + "@bazelpods-SwiftCheck//:SwiftCheck", + "//:PodToBUILD", + ], + data = glob(["Examples/**/*.podspec.json"]) +) + +# This tests RepoToolsCore and Starlark logic +macos_unit_test( + name = "BuildTests", + deps = [":BuildTestsLib"], + minimum_os_version = "10.11", +) \ No newline at end of file diff --git a/Tests/BuildTests/BuildTest.swift b/Tests/BuildTests/BuildTest.swift new file mode 100644 index 0000000..d302204 --- /dev/null +++ b/Tests/BuildTests/BuildTest.swift @@ -0,0 +1,78 @@ +// +// BuildTest.swift +// PodToBUILD +// +// Created by Jerry Marino on 6/18/18. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import XCTest +@testable import PodToBUILD +import Foundation + +class BuildTest: XCTestCase { + let shell = SystemShellContext(trace: true) + + func srcRoot() -> String { + // This path is set by Bazel + guard let testSrcDir = ProcessInfo.processInfo.environment["TEST_SRCDIR"] else{ + fatalError("Missing bazel test base") + } + let componets = testSrcDir.components(separatedBy: "/") + return componets[0 ... componets.count - 5].joined(separator: "/") + } + + func run(_ example: String) { + // Travis will throw errors if the process doesn't output after 10 mins. + let timer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { timer in + print("[INFO] Build tests still running...") + } + + let rootDir = srcRoot() + // Give 3 attempts to do the fetch. This is a workaround for flaky + // networking + let firstFetchResult = (0...3).lazy.compactMap { + i -> CommandOutput? in + sleep(UInt32(i * 3)) + print("Starting fetch task", i, example) + let fetchTask = ShellTask(command: "/bin/bash", arguments: [ + "-c", + "make -C \(rootDir)/Examples/\(example) fetch" + ], timeout: 1200.0, printOutput: true) + let fetchResult = fetchTask.launch() + if fetchResult.terminationStatus == 0 { + return fetchResult + } + return nil + }.first + + guard let fetchResult = firstFetchResult else { + fatalError("Can't setup test root.") + } + XCTAssertEqual(fetchResult.terminationStatus, 0) + let bazelScript = "make -C \(rootDir)/Examples/\(example)" + print("running bazel:", bazelScript) + let buildResult = ShellTask(command: "/bin/bash", arguments: [ + "-c", bazelScript ], timeout: 1200.0, printOutput: true).launch() + timer.invalidate() + XCTAssertEqual(buildResult.terminationStatus, 0, "building \(example)") + } + + func testReact() { + // This test is flaky + // run("React") + } + + func testPINRemoteImage() { + run("PINRemoteImage") + } + + func testTexture() { + run("Texture") + } + + func testBasiciOS() { + run("BasiciOS") + } +} + diff --git a/Tests/PodToBUILDTests/BasicBuildOptionsTest.swift b/Tests/PodToBUILDTests/BasicBuildOptionsTest.swift new file mode 100644 index 0000000..ca3140e --- /dev/null +++ b/Tests/PodToBUILDTests/BasicBuildOptionsTest.swift @@ -0,0 +1,69 @@ +// +// BasicBuildOptionsTest.swift +// PodToBUILD +// +// Created by Jerry Marino on 5/2/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import XCTest +@testable import PodToBUILD + +class BasicBuildOptionsTest: XCTestCase { + func testUserOptions() { + let CLIArgs = ["./path/to/Pod", + "Pod", + "init", + "--user_option", + "Foo.bar = -bang" + ] + let action = SerializedRepoToolsAction.parse(args: CLIArgs) + guard case let .initialize(options) = action else { + XCTFail() + return + } + XCTAssertEqual(options.podName, "Pod") + XCTAssertEqual(options.userOptions[0], "Foo.bar = -bang") + } + + func testMultipleUserOptions() { + let CLIArgs = ["./path/to/Pod", + "Pod", + "init", + "--user_option", + "Foo.bar = -bang", + "--user_option", + "Foo.bash = -crash" + ] + let action = SerializedRepoToolsAction.parse(args: CLIArgs) + guard case let .initialize(options) = action else { + XCTFail() + return + } + XCTAssertEqual(options.podName, "Pod") + XCTAssertEqual(options.userOptions[0], "Foo.bar = -bang") + XCTAssertEqual(options.userOptions[1], "Foo.bash = -crash") + } + + func testFrontendOptions() { + let CLIArgs = ["./path/to/Pod", + "Pod", + "init", + "--generate_module_map", + "true", + "--enable_modules", + "true", + "--header_visibility", + "pod_support", + ] + let action = SerializedRepoToolsAction.parse(args: CLIArgs) + guard case let .initialize(options) = action else { + XCTFail() + return + } + XCTAssertEqual(options.podName, "Pod") + XCTAssertEqual(options.enableModules, true) + XCTAssertEqual(options.generateModuleMap, true) + XCTAssertEqual(options.headerVisibility, "pod_support") + } +} diff --git a/Tests/PodToBUILDTests/BuildFileTests.swift b/Tests/PodToBUILDTests/BuildFileTests.swift new file mode 100644 index 0000000..cdc494f --- /dev/null +++ b/Tests/PodToBUILDTests/BuildFileTests.swift @@ -0,0 +1,280 @@ +// +// BuildFileTests.swift +// PodToBUILD +// +// Created by Jerry Marino on 4/14/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import XCTest +@testable import PodToBUILD + +class BuildFileTests: XCTestCase { + func basicGlob(include: Set) -> AttrSet { + return AttrSet(basic: GlobNode(include: include)) + } + + // MARK: - Transform Tests + func testWildCardSourceDependentSourceExclusion() { + let include: Set = ["Source/*.m"] + + let parentLib = ObjcLibrary(name: "Core", externalName: "Core", + sourceFiles: basicGlob(include: include)) + + let depLib = ObjcLibrary(name: "ChildLib", externalName: "Core", + sourceFiles: basicGlob(include: include), + deps: AttrSet(basic: [":Core"])) + + let libByName = executePruneRedundantCompilationTransform(libs: [parentLib, depLib]) + XCTAssertEqual( + libByName["Core"]!.sourceFiles, + AttrSet(basic: GlobNode( + include: [.left(Set(["Source/*.m"]))], + exclude: [.right(GlobNode(include: [.left(Set(["Source/*.m"]))] + ))]) + )) + } + + func testWildCardDirectoryDependentSourceExclusion() { + let include: Set = ["Source/**/*.m"] + + let parentLib = ObjcLibrary(name: "Core", externalName: "Core", + sourceFiles: basicGlob(include: include)) + let depLib = ObjcLibrary(name: "ChildLib", externalName: "Core", + sourceFiles: basicGlob(include: include), + deps: AttrSet(basic: [":Core"])) + + let libByName = executePruneRedundantCompilationTransform(libs: [parentLib, depLib]) + XCTAssertEqual( + libByName["Core"]!.sourceFiles, + AttrSet(basic: GlobNode( + include: [.left(Set(["Source/**/*.m"]))], + exclude: [.right(GlobNode(include: [.left(Set(["Source/**/*.m"]))] + ))]) + )) + } + + func testWildCardSourceDependentSourceExclusionWithExistingExclusing() { + let parentLib = ObjcLibrary(name: "Core", externalName: "Core", + sourceFiles: AttrSet(basic: GlobNode(include:Set(["Source/*.m"]), + exclude: + Set(["Srce/SomeSource.m"])))) + let childSourceFiles = basicGlob(include: Set(["Source/SomeSource.m"])) + let depLib = ObjcLibrary(name: "ChildLib", externalName: "Core", + sourceFiles: childSourceFiles, + deps: AttrSet(basic: [":Core"])) + let libByName = executePruneRedundantCompilationTransform(libs: [parentLib, depLib]) + XCTAssertEqual( + libByName["ChildLib"]!.sourceFiles, + AttrSet(basic: GlobNode( + include: [.left(Set(["Source/SomeSource.m"]))]) + )) + + XCTAssertEqual( + libByName["Core"]!.sourceFiles, + AttrSet(basic: GlobNode( + include: [.left(Set(["Source/*.m"]))], + exclude: [.left(Set(["Srce/SomeSource.m"])), .right(GlobNode(include: [.left(Set(["Source/SomeSource.m"]))] + ))]) + )) + } + + func testNestedDependentExclusion() { + let parentLib = ObjcLibrary(name: "Core", externalName: "Core", + sourceFiles: basicGlob(include: Set(["Source/*.m"]))) + + let depLib = ObjcLibrary(name: "ChildLib", externalName: "Core", + sourceFiles: AttrSet(basic: GlobNode(include: Set(["Source/Foo/*.m"]))), + deps: AttrSet(basic: [":Core"])) + + let depDepLib = ObjcLibrary(name: "GrandChildLib", externalName: "Core", + sourceFiles: AttrSet(basic: GlobNode(include: Set(["Source/Foo/Bar/*.m"]))), + deps: AttrSet(basic: [":ChildLib"])) + + let libByName = executePruneRedundantCompilationTransform(libs: [parentLib, depLib, depDepLib]) + + let transformed = libByName.values.map { $0.toSkylark() } + print(SkylarkCompiler(.lines(transformed)).run()) + + let childSources = libByName["ChildLib"]?.sourceFiles + XCTAssertEqual( + childSources, + AttrSet(basic: GlobNode( + include: [.left(Set(["Source/Foo/*.m"]))], + exclude: [.right(GlobNode(include: [.left(Set(["Source/Foo/Bar/*.m"]))] + ))])) + + ) + + XCTAssertEqual( + libByName["Core"]?.sourceFiles, + AttrSet(basic: GlobNode( + include: [.left(Set(["Source/*.m"]))], + exclude: [ + .right(GlobNode( + include: [.left(Set(["Source/Foo/*.m"]))], + exclude: [])), .right(GlobNode(include: [.left(Set(["Source/Foo/Bar/*.m"]))], exclude: []))]) + )) + } + + private func executePruneRedundantCompilationTransform(libs: [ObjcLibrary]) -> [String: ººObjcLibrary] { + let opts = BasicBuildOptions(podName: "", + userOptions: [String](), + globalCopts: [String](), + trace: false) + let transformed = RedundantCompiledSourceTransform.transform(convertibles: libs, + options: opts, + podSpec: try! PodSpec(JSONPodspec: JSONDict()) + ) + var libByName = [String: ObjcLibrary]() + transformed.forEach { + let t = ($0 as! ObjcLibrary) + libByName[t.name] = t + } + return libByName + } + + // MARK: - Multiplatform Tests + + func testLibFromPodspec() { + let podspec = examplePodSpecNamed(name: "IGListKit") + let lib = ObjcLibrary(parentSpecs: [], spec: podspec) + + let expectedFrameworks: AttrSet<[String]> = AttrSet(multi: MultiPlatform( + ios: ["UIKit"], + osx: ["Cocoa"], + watchos: nil, + tvos: ["UIKit"])) + XCTAssert(lib.sdkFrameworks == expectedFrameworks) + } + + func testDependOnDefaultSubspecs() { + let podspec = examplePodSpecNamed(name: "IGListKit") + let convs = PodBuildFile.makeConvertables(fromPodspec: podspec) + + XCTAssert( + AttrSet(basic: [":Default"]) == + (convs.compactMap{ $0 as? ObjcLibrary}.first!).deps + ) + } + + func testDependOnSubspecs() { + let podspec = examplePodSpecNamed(name: "PINCache") + let convs = PodBuildFile.makeConvertables(fromPodspec: podspec) + + XCTAssert( + AttrSet(basic: [":Core", ":Arc-exception-safe"]) == + (convs.compactMap{ $0 as? ObjcLibrary}.first!).deps + ) + } + + // MARK: - Swift tests + + func testSwiftExtractionSubspec() { + let podspec = examplePodSpecNamed(name: "ObjcParentWithSwiftSubspecs") + let convs = PodBuildFile.makeConvertables(fromPodspec: podspec) + XCTAssertEqual(convs.compactMap{ $0 as? ObjcLibrary }.count, 3) + // Note that we check for sources on disk to generate this. + XCTAssertEqual(convs.compactMap{ $0 as? SwiftLibrary }.count, 0) + } + + + // MARK: - Source File Extraction Tests + + func testExtractionCurly() { + let podPattern = "Source/Classes/**/*.{h,m}" + let extractedHeaders = extractFiles(fromPattern: AttrSet(basic: [podPattern]), + includingFileTypes: HeaderFileTypes).basic + let extractedSources = extractFiles(fromPattern: AttrSet(basic: [podPattern]), + includingFileTypes: ObjcLikeFileTypes).basic + XCTAssertEqual(extractedHeaders, ["Source/Classes/**/*.h"]) + XCTAssertEqual(extractedSources, ["Source/Classes/**/*.m"]) + } + + func testExtractionWithBarPattern() { + let podPattern = "Source/Classes/**/*.[h,m]" + let extractedHeaders = extractFiles(fromPattern: AttrSet(basic: [podPattern]), + includingFileTypes: HeaderFileTypes).basic + let extractedSources = extractFiles(fromPattern: AttrSet(basic: [podPattern]), + includingFileTypes: ObjcLikeFileTypes).basic + + XCTAssertEqual(extractedHeaders, ["Source/Classes/**/*.h"]) + XCTAssertEqual(extractedSources, ["Source/Classes/**/*.m"]) + } + + func testExtractionMultiplatform() { + let podPattern = "Source/Classes/**/*.[h,m]" + let extractedHeaders = extractFiles(fromPattern: AttrSet(basic: [podPattern]), + includingFileTypes: HeaderFileTypes) + let extractedSources = extractFiles(fromPattern: AttrSet(basic: [podPattern]), + includingFileTypes: ObjcLikeFileTypes) + XCTAssert(extractedHeaders == AttrSet(basic: ["Source/Classes/**/*.h"])) + XCTAssert(extractedSources == AttrSet(basic: ["Source/Classes/**/*.m"])) + } + + func testHeaderIncAutoGlob() { + let podSpec = examplePodSpecNamed(name: "UICollectionViewLeftAlignedLayout") + let library = ObjcLibrary(parentSpecs: [], spec: podSpec) + guard let ios = library.headers.multi.ios else { + XCTFail("Missing iOS headers for lib \(library)") + return + } + XCTAssertEqual( + ios, GlobNode(include: Set([ + "UICollectionViewLeftAlignedLayout/**/*.h", + "UICollectionViewLeftAlignedLayout/**/*.hpp", + "UICollectionViewLeftAlignedLayout/**/*.hxx" + ])) + ) + + } + + // MARK: - JSON Examples + + func testGoogleAPISJSONParsing() { + let podSpec = examplePodSpecNamed(name: "googleapis") + XCTAssertEqual(podSpec.name, "googleapis") + XCTAssertEqual(podSpec.sourceFiles, [String]()) + XCTAssertEqual(podSpec.podTargetXcconfig!, [ + "USER_HEADER_SEARCH_PATHS": "$SRCROOT/..", + "GCC_PREPROCESSOR_DEFINITIONS": "$(inherited) GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ] + ) + } + + func testIGListKitJSONParsing() { + let podSpec = examplePodSpecNamed(name: "IGListKit") + XCTAssertEqual(podSpec.name, "IGListKit") + XCTAssertEqual(podSpec.sourceFiles, [String]()) + XCTAssertEqual(podSpec.podTargetXcconfig!, [ + "CLANG_CXX_LANGUAGE_STANDARD": "c++11", + "CLANG_CXX_LIBRARY": "libc++", + ] + ) + } + + // MARK: - XCConfigs + + func testPreProcesorDefsXCConfigs() { + // We strip off inherited. + let config = [ + "USER_HEADER_SEARCH_PATHS": "$SRCROOT/..", + "GCC_PREPROCESSOR_DEFINITIONS": "$(inherited) GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ] + let compilerFlags = XCConfigTransformer + .defaultTransformer(externalName: "test", sourceType: .objc) + .compilerFlags(forXCConfig: config) + XCTAssertEqual(compilerFlags, ["-DGPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1"]) + } + + func testCXXXCConfigs() { + let config = [ + "CLANG_CXX_LANGUAGE_STANDARD": "c++11", + "CLANG_CXX_LIBRARY": "libc++", + ] + let compilerFlags = XCConfigTransformer + .defaultTransformer(externalName: "test", sourceType: .cpp) + .compilerFlags(forXCConfig: config) + XCTAssertEqual(compilerFlags.sorted(by: (<)), ["-std=c++11", "-stdlib=libc++"]) + } +} diff --git a/Tests/PodToBUILDTests/GlobTests.swift b/Tests/PodToBUILDTests/GlobTests.swift new file mode 100644 index 0000000..36369c0 --- /dev/null +++ b/Tests/PodToBUILDTests/GlobTests.swift @@ -0,0 +1,112 @@ +// +// GlobTests.swift +// PodToBUILD +// +// Created by Jerry Marino on 4/18/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import XCTest +import Foundation +@testable import PodToBUILD + +class GlobTests: XCTestCase { + // These tests fail non-deterministically on CI during our OTA builds + // it has something to do with NSRegularExpression. + // We have never seen a failure locally, so we're going to disable them for now +// func testGarbageGlob() { +// let path = "Garbage/Source/*.{h,m}" +// XCTAssertFalse(glob(pattern: path, contains: "")) +// } +// +// func testIteration() { +// XCTAssertTrue(glob(pattern: "A", contains: "A")) +// XCTAssertTrue(glob(pattern: "A/Some", contains: "A/Some")) +// XCTAssertFalse(glob(pattern: "A/Some/Source", contains: "A/Some/**")) +// XCTAssertTrue(glob(pattern: "A/Some/**", contains: "A/Some/Source")) +// } +// +// func testGlobMatchingNoMatch() { +// let testPattern = "^*.[h]" +// XCTAssertFalse(glob(pattern: testPattern, contains: "/Some/Path/*.m")) +// } +// +// func testGlobMatching() { +// let testPattern = "^*.[h]" +// XCTAssertFalse(glob(pattern: testPattern, contains: "/Some/Path/*.h")) +// } +// +// func testPodRegexConversion() { +// let testPattern = NSRegularExpression.pattern(withGlobPattern: "Source/Classes/**/*.{h,m}") +// XCTAssertEqual(testPattern, "Source/Classes/.*.*/.*.[h,m]") +// } + + func testInnerEitherPattern() { + let testPattern = pattern(fromPattern: "Source/{Classes, Masses}/**/*.{h,m}", includingFileTypes: [".h"]) + XCTAssertEqual(testPattern.sorted(by: (<)), ["Source/Classes/**/*.h", "Source/Masses/**/*.h"]) + } + + func testNaievePatternBuilding() { + let testPattern = pattern(fromPattern: "Source/Classes/**/*.{h,m}", includingFileTypes: [".h"]) + XCTAssertEqual(testPattern, ["Source/Classes/**/*.h"]) + } + + func testFalsePositiveBasic() { + let testPattern = pattern(fromPattern: "Source/Classes/**/*.py", includingFileTypes: [".m"]) + XCTAssertEqual(testPattern, []) + } + + func testEndsWithWild() { + let testPattern = pattern(fromPattern: "Source/Classes/**/*", includingFileTypes: [".m"]) + XCTAssertEqual(testPattern, ["Source/Classes/**/*.m"]) + } + + func testEndsWithDirWild() { + let testPattern = pattern(fromPattern: "Source/Classes/**", includingFileTypes: [".m"]) + XCTAssertEqual(testPattern, ["Source/Classes/**/*.m"]) + } + + func testEndsWithDotWild() { + let testPattern = pattern(fromPattern: "Source/Classes/**/*.*", includingFileTypes: [".m"]) + XCTAssertEqual(testPattern, ["Source/Classes/**/*.m"]) + } + + func testFBSdkCorePattern() { + let testPattern = pattern(fromPattern: "FBSDKCoreKit/FBSDKCoreKit/Internal/AppLink/**/*", includingFileTypes: [".m"]) + XCTAssertEqual(testPattern, ["FBSDKCoreKit/FBSDKCoreKit/Internal/AppLink/**/*.m"]) + } + + func testNaievePatternBuildingSecondPart() { + let testPattern = pattern(fromPattern: "Source/Classes/**/*.{h,m}", includingFileTypes: [".m"]) + XCTAssertEqual(testPattern, ["Source/Classes/**/*.m"]) + } + + func testCurlysInMiddle() { + let testPattern = pattern(fromPattern: "Source/{Classes,Glasses}/**/*.m", includingFileTypes: [".m"]) + XCTAssertEqual(testPattern.sorted(by: (<)), ["Source/Classes/**/*.m", "Source/Glasses/**/*.m"]) + } + + func testNaievePatternBuildingBar() { + let testPattern = pattern(fromPattern: "Source/Classes/**/*.{h,m}", includingFileTypes: [".m"]) + XCTAssertEqual(testPattern, ["Source/Classes/**/*.m"]) + } + + func testNaievePatternBuildingMismatch() { + let testPattern = pattern(fromPattern: "Source/Classes/**/*.{h}", includingFileTypes: [".m"]) + XCTAssertEqual(testPattern, []) + } + + func testBoltsStylePattern() { + let sources = pattern(fromPattern: "Source/Classes/**/*.[hm]", includingFileTypes: [".m"]) + XCTAssertEqual(sources, ["Source/Classes/**/*.m"]) + + let headers = pattern(fromPattern: "Source/Classes/**/*.[hm]", includingFileTypes: [".h"]) + XCTAssertEqual(headers, ["Source/Classes/**/*.h"]) + } + + func testPatternsEndingInAlphanumericCharactersYieldGlob() { + let testPattern = pattern(fromPattern: "Source/Classes", includingFileTypes: [".m"]) + XCTAssertEqual(testPattern, ["Source/Classes/**/*.m"]) + } +} + diff --git a/Tests/PodToBUILDTests/Info.plist b/Tests/PodToBUILDTests/Info.plist new file mode 100644 index 0000000..6c6c23c --- /dev/null +++ b/Tests/PodToBUILDTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Tests/PodToBUILDTests/MagmasTests.swift b/Tests/PodToBUILDTests/MagmasTests.swift new file mode 100644 index 0000000..33a3b6e --- /dev/null +++ b/Tests/PodToBUILDTests/MagmasTests.swift @@ -0,0 +1,82 @@ +// +// MagmasTests.swift +// PodToBUILD +// +// Created by Brandon Kase on 5/1/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import Foundation +import SwiftCheck +import XCTest +@testable import PodToBUILD + +class MagmasTests: XCTestCase { + func testArrayExtensions() { + property("Array semigroup associative") <- forAll { (xs: Array, ys: Array, zs: Array) in + return ((xs <> ys) <> zs) == + (xs <> (ys <> zs)) + } + + property("Array monoid identity") <- forAll { (xs: Array) in + return (xs <> Array.empty == xs) "Right identity" + ^&&^ + (Array.empty <> xs == xs) "Left identity" + } + + property("Array empty awareness sound") <- forAll { (xs: Array) in + return xs.isEmpty ? + xs == Array.empty : + xs != Array.empty + } + } + + func testStringExtensions() { + property("String semigroup associative") <- forAll { (x: String, y: String, z: String) in + return ((x <> y) <> z) == (x <> (y <> z)) + } + + property("String monoid identity") <- forAll { (x: String) in + return (x <> String.empty == x) "Right identity" + ^&&^ + (String.empty <> x == x) "Left identity" + } + + property("String empty awareness sound") <- forAll { (x: String) in + return x.isEmpty ? + x == String.empty : + x != String.empty + } + } + + func testDictionaryExtensions() { + property("Dict semigroup associative") <- forAll { (x: Dictionary, y: Dictionary, z: Dictionary) in + return ((x <> y) <> z) == + (x <> (y <> z)) + } + + property("Dict monoid identity") <- forAll { (x: Dictionary) in + return (x <> Dictionary.empty == x) "Right identity" + ^&&^ + (Dictionary.empty <> x == x) "Left identity" + } + + property("Dict empty awareness sound") <- forAll { (x: Dictionary) in + return x.isEmpty ? + x == Dictionary.empty : + x != Dictionary.empty + } + } + + func testNormalizeOptions() { + property("Never admit an empty after normalizing") <- forAll { (x: Optional) in + return x.normalize() != .some(String.empty) + } + } + + func testOptionalCompositionExtension() { + property("composition of optionals using <>") <- forAll { (x: Optional, y: Optional) in + return x <> Optional.empty == x + } + } +} diff --git a/Tests/PodToBUILDTests/ParserTests.swift b/Tests/PodToBUILDTests/ParserTests.swift new file mode 100644 index 0000000..4487451 --- /dev/null +++ b/Tests/PodToBUILDTests/ParserTests.swift @@ -0,0 +1,120 @@ +// +// ParserTests.swift +// PodToBUILD +// +// Created by Brandon Kase on 9/12/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import Foundation +import SwiftCheck +import XCTest +@testable import PodToBUILD + +struct Average: Monoid { + let count: Int + let sum: Int + + init(value: Int) { + self.count = 1 + self.sum = value + } + init(count: Int, sum: Int) { + self.count = count + self.sum = sum + } + + static var empty: Average { + return Average(count: 0, sum: 0) + } + static func <>(lhs: Average, rhs: Average) -> Average { + return Average(count: lhs.count + rhs.count, sum: lhs.sum + rhs.sum) + } + + var avg: Double { + return count == 0 ? 0 : Double(sum) / Double(count) + } +} + +class ParserTests: XCTestCase { + func testParseOneCharacter() { + property("Parsing a character with anyChar is the identity") <- forAll { (c: Character) in + Parsers.anyChar.parseFully([c]) == c + } + } + + func testFailParserFails() { + property("Using a failure parser never succeeds") <- forAll { (cs: Array) in + Parser<()>.fail().parseFully(cs) == nil + } + } + + func testTrivialParserSucceeds() { + property("Using a trivial parser always succeeds and consumes nothing") <- forAll { (cs: Array) in + if let (_, rest) = Parser<()>.trivial(()).run(cs) { + return rest == cs + } else { + return false + } + } + } + + func testButNot() { + let notBang = Character.arbitrary.suchThat{ $0 != "!" } + + property("AnyChar but not ! matches anything except !") <- forAll(notBang) { (x: Character) in + (Parsers.anyChar.butNot("!").parseFully([x]) != nil) "Matches anything but not !" + ^&&^ + (Parsers.anyChar.butNot("!").parseFully(["!"]) == nil) "Doesn't match !" + } + } + + func testParseMany() { + property("Many matches zero or more times") <- forAll { (cs: Array) in + Parsers.anyChar.many().parseFully(cs) != nil + } + + let dot = Parsers.just(".") + XCTAssertEqual( + // parser anything zero or more times until a dot + (Parsers.anyChar.butNot(".").many() <> + // then consume the dot, but don't return it + dot.map{ _ in [] }) + .parseFully(Array("abcde.")) ?? [], + Array("abcde") + ) + } + + func testParseRep() { + let chunks = Character.arbitrary.suchThat{ $0 != "," }.proliferateNonEmpty.map{ cs in String(cs) } + + property("Repeated strings separated by commas") <- forAll(chunks, chunks, chunks) { (xs: String, ys: String, zs: String) in + let reps = Array([xs, ys, zs].joined(separator: ",")) + let parseComma = Parsers.just(",") + let chunks = Parsers.anyChar.butNot(",").many().rep(separatedBy: parseComma.forget).parseFully(reps) ?? [] + return (chunks.count == 3) && + String(chunks[0]) == xs && + String(chunks[1]) == ys && + String(chunks[2]) == zs + } + } + + func testParseIntoMonoidalStructure() { + let nums: Gen> = Int.arbitrary.resize(20).proliferateNonEmpty.map{ Array($0) } + + property("Average stream of numbers into one number") <- forAll(nums) { (nums: Array) in + let baselineAverage = mfold(nums.map{ Average(value: $0) }).avg + + let inputStream = nums.map{ String($0) }.joined(separator: ",") + + let parseComma = Parsers.just(",") + let parsedAvgs = Parsers.anyChar.butNot(",") + .many() + .map{ cs in Average(value: Int(String(cs))!) } + .rep(separatedBy: parseComma.forget) + .parseFully(Array(inputStream)) ?? [] + let parsedAverage = mfold(parsedAvgs).avg + return baselineAverage == parsedAverage + } + } +} diff --git a/Tests/PodToBUILDTests/ShellTaskTest.swift b/Tests/PodToBUILDTests/ShellTaskTest.swift new file mode 100644 index 0000000..b5ef14e --- /dev/null +++ b/Tests/PodToBUILDTests/ShellTaskTest.swift @@ -0,0 +1,34 @@ +// +// ShellTaskTest.swift +// PodSpecToBUILDTests +// +// Created by Jerry Marino on 9/27/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import XCTest +@testable import PodToBUILD + +class ShellTaskTest: XCTestCase { + + func testCanExecute() { + let task = ShellTask(command: "/usr/bin/whoami", arguments: [], timeout: 1.0) + let resultStr = task.launch().standardOutputAsString + XCTAssertTrue(resultStr.count > 0) + } + + func testBashScript() { + let task = ShellTask.with(script: "echo \"hi\"", timeout: 1) + let resultStr = task.launch().standardOutputAsString + // For now, this is required. There is a new line in this output + XCTAssertEqual(resultStr.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines), "hi") + } + + /// For now, timeouts just fail silently. It's up to the caller to + /// check if they have the correct output. + func testTimeout() { + let task = ShellTask.with(script: "sleep 10", timeout: 1.0) + let status = task.launch().terminationStatus + XCTAssertNotEqual(status, 0) + } +} diff --git a/Tests/PodToBUILDTests/SkylarkTests.swift b/Tests/PodToBUILDTests/SkylarkTests.swift new file mode 100644 index 0000000..5a65ee2 --- /dev/null +++ b/Tests/PodToBUILDTests/SkylarkTests.swift @@ -0,0 +1,54 @@ +// +// PodSpecToBUILDTests.swift +// PodSpecToBUILDTests +// +// Created by Jerry Marino on 4/14/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import XCTest +@testable import PodToBUILD + +class PodSpecToBUILDTests: XCTestCase { + let nameArgument = SkylarkFunctionArgument.named(name: "name", value: "test") + + func testFunctionCall() { + let call = SkylarkNode.functionCall(name: "objc_library", arguments: [nameArgument]) + let compiler = SkylarkCompiler([call]) + let expected = compilerOutput([ + "objc_library(", + " name = \"test\"", + ")", + ]) + print(compiler.run()) + XCTAssertEqual(expected, compiler.run()) + } + + func testCallWithSkylark() { + let sourceFiles = ["a.m", "b.m"] + let globCall = SkylarkNode.functionCall(name: "glob", arguments: [.basic(sourceFiles.toSkylark())]) + let srcsArg = SkylarkFunctionArgument.named(name: "srcs", value: globCall) + let call = SkylarkNode.functionCall(name: "objc_library", arguments: [nameArgument, srcsArg]) + let compiler = SkylarkCompiler([call]) + let expected = compilerOutput([ + "objc_library(", + " name = \"test\",", + " srcs = glob(", + " [", + " \"a.m\",", + " \"b.m\"", + " ]", + " )", + ")", + ]) + + let expectedLines = expected.components(separatedBy: "\n") + for (idx, line) in compiler.run().components(separatedBy: "\n").enumerated() { + XCTAssertEqual(line, expectedLines[idx]) + } + } + + func compilerOutput(_ values: [String]) -> String { + return values.joined(separator: "\n") + } +} diff --git a/Tests/PodToBUILDTests/TestUtils.swift b/Tests/PodToBUILDTests/TestUtils.swift new file mode 100644 index 0000000..07571d0 --- /dev/null +++ b/Tests/PodToBUILDTests/TestUtils.swift @@ -0,0 +1,54 @@ +// +// TestUtils.swift +// PodToBUILD +// +// Created by Jerry Marino on 4/21/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import Foundation +@testable import PodToBUILD + +// Get a JSON Podspec from a file +func podSpecWithFixture(JSONPodspecFilePath: String) -> PodSpec { + guard let jsonData = try? Data(contentsOf: URL(fileURLWithPath: JSONPodspecFilePath)) else { + fatalError("Error: Unable to load podspec at \(JSONPodspecFilePath)") + } + + guard let JSONFile = try? JSONSerialization.jsonObject(with: jsonData, options: + JSONSerialization.ReadingOptions.allowFragments) else { + fatalError("Error: Unable to parse JSON podspec at \(JSONPodspecFilePath)") + } + + + guard let JSONPodspec = JSONFile as? JSONDict else { + fatalError("Error: JSON for podspec is malformed. Expected [String:Any] for podspec at: \(JSONPodspecFilePath)") + } + + + guard let podSpec = try? PodSpec(JSONPodspec: JSONPodspec) else { + fatalError("Error: JSON podspec is invalid. Look for missing fields or incorrect data types: \(JSONPodspecFilePath)") + } + + return podSpec +} + +// Assume the directory structure relative to this file +// ( PodToBUILD/Tests/PodToBUILDTests/#file ) +private func srcRoot() -> String { + // This path is set by Bazel + guard let testSrcDir = ProcessInfo.processInfo.environment["TEST_SRCDIR"] else{ + fatalError("Missing bazel test base") + } + let componets = testSrcDir.components(separatedBy: "/") + return componets[0 ... componets.count - 5].joined(separator: "/") +} + +public func examplePodSpecFilePath(name: String) -> String { + return "\(srcRoot())/Examples/PodSpecs/\(name).podspec.json" +} + +public func examplePodSpecNamed(name: String) -> PodSpec { + let podSpec = podSpecWithFixture(JSONPodspecFilePath: examplePodSpecFilePath(name: name)) + return podSpec +} diff --git a/Tests/PodToBUILDTests/UserConfigurableTests.swift b/Tests/PodToBUILDTests/UserConfigurableTests.swift new file mode 100644 index 0000000..dc661d4 --- /dev/null +++ b/Tests/PodToBUILDTests/UserConfigurableTests.swift @@ -0,0 +1,106 @@ +// +// UserConfigurableTests.swift +// PodToBUILD +// +// Created by Jerry Marino on 5/2/17. +// Copyright © 2017 Pinterest Inc. All rights reserved. +// + +import XCTest +@testable import PodToBUILD + +enum TestTargetConfigurableKeys : String { + case copts +} + +struct TestTarget : BazelTarget, UserConfigurable { + var name = "TestTarget" + var copts = AttrSet(basic: [String]()) + var sdkFrameworks = AttrSet(basic: [String]()) + + func toSkylark() -> SkylarkNode { + return .functionCall( + name: "config_setting", + arguments: [SkylarkFunctionArgument]() + ) + } + + mutating func add(configurableKey: String, value: Any) { + if let key = ObjcLibraryConfigurableKeys(rawValue: configurableKey) { + switch key { + case .copts: + if let value = value as? String { + self.copts = self.copts <> AttrSet(basic: [value]) + } + case .sdkFrameworks: + if let value = value as? String { + self.sdkFrameworks = self.sdkFrameworks <> AttrSet(basic: [value]) + } + default: + fatalError() + } + } + } + +} + +class UserConfigurableTests: XCTestCase { + func testUserOptionTransform() { + var target = TestTarget() + target.copts = AttrSet(basic: ["-initial"]) + let attributes = UserConfigurableTargetAttributes(keyPathOperators: ["TestTarget.copts += -foo, -bar"]) + let output = UserConfigurableTransform.executeUserOptionsTransform(onConvertibles: [target], copts: [String](), userAttributes: attributes) + let outputLib = output[0] as! TestTarget + let outputCopts = outputLib.copts.basic + XCTAssertEqual(outputCopts?[0], "-initial") + XCTAssertEqual(outputCopts?[1], "-foo") + XCTAssertEqual(outputCopts?[2], "-bar") + } + + func testUserOptionTransformGlobalCopts() { + var target = TestTarget() + target.copts = AttrSet(basic: ["-initial"]) + let attributes = UserConfigurableTargetAttributes(keyPathOperators: ["TestTarget.copts += -foo, -bar"]) + let output = UserConfigurableTransform.executeUserOptionsTransform(onConvertibles: [target], copts: ["-boom"], userAttributes: attributes) + let outputLib = output[0] as! TestTarget + let outputCopts = outputLib.copts.basic + XCTAssertEqual(outputCopts?[0], "-initial") + XCTAssertEqual(outputCopts?[1], "-boom") + XCTAssertEqual(outputCopts?[2], "-foo") + XCTAssertEqual(outputCopts?[3], "-bar") + } + + + func testUserOptionTransformSdkFrameworks() { + var target = TestTarget() + target.sdkFrameworks = AttrSet(basic: ["UIKit"]) + let attributes = UserConfigurableTargetAttributes(keyPathOperators: ["TestTarget.sdk_frameworks += CoreGraphics, Foundation"]) + let output = UserConfigurableTransform.executeUserOptionsTransform(onConvertibles: [target], copts: [], userAttributes: attributes) + let outputLib = output[0] as! TestTarget + let outputCopts = outputLib.sdkFrameworks.basic + XCTAssertEqual(outputCopts?[0], "UIKit") + XCTAssertEqual(outputCopts?[1], "CoreGraphics") + XCTAssertEqual(outputCopts?[2], "Foundation") + } + + func testUserOptionPresevesDotExtensions() { + let target = TestTarget() + let attributes = UserConfigurableTargetAttributes(keyPathOperators: ["TestTarget.copts += TargetConditionals.h"]) + let output = UserConfigurableTransform.executeUserOptionsTransform(onConvertibles: [target], copts: [String](), userAttributes: attributes) + let outputLib = output[0] as! TestTarget + let outputCopts = outputLib.copts.basic + XCTAssertEqual(outputCopts?[0], "TargetConditionals.h") + } + + func testUserOptionPresevesSpaces() { + let target = TestTarget() + let attributes = UserConfigurableTargetAttributes(keyPathOperators: ["TestTarget.copts += --include TargetConditionals.h"]) + let output = UserConfigurableTransform.executeUserOptionsTransform(onConvertibles: [target], copts: [String](), userAttributes: attributes) + let outputLib = output[0] as! TestTarget + let outputCopts = outputLib.copts.basic + XCTAssertEqual(outputCopts?[0], "--include TargetConditionals.h") + } +} + + + diff --git a/Tests/PodToBUILDTests/XCTestManifests.swift b/Tests/PodToBUILDTests/XCTestManifests.swift new file mode 100644 index 0000000..033af37 --- /dev/null +++ b/Tests/PodToBUILDTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !os(macOS) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(PodToBUILDTests.allTests), + ] +} +#endif \ No newline at end of file diff --git a/WORKSPACE b/WORKSPACE index 9ca0daa..f40d612 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -12,32 +12,6 @@ http_archive( sha256 = "a2fd565e527f83fb3f9eb07eb9737240e668c9242d3bc318712efa54a7deda97", ) -http_archive( - name = "swift-argument-parser", - url = "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.1.3.tar.gz", - strip_prefix = "swift-argument-parser-1.1.3", - sha256 = "e52c0ac4e17cfad9f13f87a63ddc850805695e17e98bf798cce85144764cdcaa", - build_file_content = """ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "ArgumentParser", - srcs = glob(["Sources/ArgumentParser/**/*.swift"]), - deps = [":ArgumentParserToolInfo"], - copts = ["-swift-version", "5"], - visibility = [ - "//visibility:public" - ] -) - -swift_library( - name = "ArgumentParserToolInfo", - srcs = glob(["Sources/ArgumentParserToolInfo/**/*.swift"]), - copts = ["-swift-version", "5"], -) - """ -) - load( "@build_bazel_rules_apple//apple:repositories.bzl", "apple_rules_dependencies", @@ -59,7 +33,7 @@ load( swift_rules_extra_dependencies() -load("//:repositories.bzl", "bazelpods_dependencies", "podtobuild_dependencies") +load("//:repositories.bzl", "bazelpods_dependencies", "bazelpodstests_dependencies") bazelpods_dependencies() -podtobuild_dependencies() +bazelpodstests_dependencies() diff --git a/repositories.bzl b/repositories.bzl index d18449e..6d5698d 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -8,34 +8,7 @@ load( "http_archive" ) -def bazelpods_dependencies(): - http_archive( - name = "swift-argument-parser", - url = "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.1.3.tar.gz", - strip_prefix = "swift-argument-parser-1.1.3", - sha256 = "e52c0ac4e17cfad9f13f87a63ddc850805695e17e98bf798cce85144764cdcaa", - build_file_content = """ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "ArgumentParser", - srcs = glob(["Sources/ArgumentParser/**/*.swift"]), - deps = [":ArgumentParserToolInfo"], - copts = ["-swift-version", "5"], - visibility = [ - "//visibility:public" - ] -) - -swift_library( - name = "ArgumentParserToolInfo", - srcs = glob(["Sources/ArgumentParserToolInfo/**/*.swift"]), - copts = ["-swift-version", "5"], -) - """ - ) - -NAMESPACE_PREFIX = "podtobuild-" +NAMESPACE_PREFIX = "bazelpods-" def namespaced_name(name): if name.startswith("@"): @@ -59,6 +32,12 @@ def namespaced_git_repository(name, **kwargs): **kwargs ) +def namespaced_http_archive(name, **kwargs): + http_archive( + name = namespaced_name(name), + **kwargs + ) + def namespaced_build_file(libs): return """ package(default_visibility = ["//visibility:public"]) @@ -66,35 +45,6 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_c_module", "swift_library") """ + "\n\n".join(libs) -def namespaced_swift_c_library(name, srcs, hdrs, includes, module_map): - return """ -objc_library( - name = "{name}Lib", - srcs = glob([ - {srcs} - ]), - hdrs = glob([ - {hdrs} - ]), - includes = [ - {includes} - ] -) - -swift_c_module( - name = "{name}", - deps = [":{name}Lib"], - module_name = "{name}", - module_map = "{module_map}", -) -""".format(**dict( - name = name, - srcs = ",\n".join(['"%s"' % x for x in srcs]), - hdrs = ",\n".join(['"%s"' % x for x in hdrs]), - includes = ",\n".join(['"%s"' % x for x in includes]), - module_map = module_map, - )) - def namespaced_swift_library(name, srcs, deps = None, defines = None, copts=[]): deps = [] if deps == None else deps defines = [] if defines == None else defines @@ -114,49 +64,28 @@ swift_library( copts = ",\n".join(['"%s"' % x for x in copts]), )) -def podtobuild_dependencies(): - """Fetches repositories that are dependencies of the podtobuild workspace. - - Users should call this macro in their `WORKSPACE` to ensure that all of the - dependencies of podtobuild are downloaded and that they are isolated from - changes to those dependencies. - """ - - namespaced_new_git_repository( - name = "Yams", - remote = "https://github.com/jpsim/Yams.git", - commit = "39698493e08190d867da98ff49210952b8059e78", - patch_cmds = [ - """ -echo ' -module CYaml { - umbrella header "CYaml.h" - export * -} -' > Sources/CYaml/include/Yams.modulemap -""", - ], +def bazelpods_dependencies(): + namespaced_http_archive( + name = "swift-argument-parser", + url = "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.1.3.tar.gz", + strip_prefix = "swift-argument-parser-1.1.3", + sha256 = "e52c0ac4e17cfad9f13f87a63ddc850805695e17e98bf798cce85144764cdcaa", build_file_content = namespaced_build_file([ - namespaced_swift_c_library( - name = "CYaml", - srcs = [ - "Sources/CYaml/src/*.c", - "Sources/CYaml/src/*.h", - ], - hdrs = [ - "Sources/CYaml/include/*.h", - ], - includes = ["Sources/CYaml/include"], - module_map = "Sources/CYaml/include/Yams.modulemap", - ), namespaced_swift_library( - name = "Yams", - srcs = ["Sources/Yams/*.swift"], - deps = [":CYaml", ":CYamlLib"], - defines = ["SWIFT_PACKAGE"], + name = "ArgumentParser", + srcs = ["Sources/ArgumentParser/**/*.swift"], + deps = [":ArgumentParserToolInfo"], + copts = ["-swift-version", "5"], ), - ]), + namespaced_swift_library( + name = "ArgumentParserToolInfo", + srcs = ["Sources/ArgumentParserToolInfo/**/*.swift"], + copts = ["-swift-version", "5"], + ) + ]) ) + +def bazelpodstests_dependencies(): namespaced_new_git_repository( name = "SwiftCheck", remote = "https://github.com/typelift/SwiftCheck.git",