Last active
December 15, 2019 22:02
-
-
Save nkallen/90054ac97d0e0798b778ac73a84d8d3f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Created by Krzysztof Zablocki on 06/01/2017. | |
// Copyright (c) 2017 Pixle. All rights reserved. | |
// | |
import Foundation | |
extension Array { | |
func parallelFlatMap<T>(transform: (Element) throws -> [T]) throws -> [T] { | |
return try parallelMap(transform).flatMap { $0 } | |
} | |
/// We have to roll our own solution because concurrentPerform will use slowPath if no NSApplication is available | |
func parallelMap<T>(_ transform: (Element) throws -> T, progress: ((Int) -> Void)? = nil) throws -> [T] { | |
let count = self.count | |
let maxConcurrentJobs = ProcessInfo.processInfo.activeProcessorCount | |
guard count > 1 && maxConcurrentJobs > 1 else { | |
// skip GCD overhead if we'd only run one at a time anyway | |
return try map(transform) | |
} | |
var result = [(Int, [T])]() | |
result.reserveCapacity(count) | |
let group = DispatchGroup() | |
let uuid = NSUUID().uuidString | |
let jobCount = Int(ceil(Double(count) / Double(maxConcurrentJobs))) | |
let queueLabelPrefix = "io.pixle.Sourcery.map.\(uuid)" | |
let resultAccumulatorQueue = DispatchQueue(label: "\(queueLabelPrefix).resultAccumulator") | |
var mapError: Error? | |
withoutActuallyEscaping(transform) { escapingtransform in | |
for jobIndex in stride(from: 0, to: count, by: jobCount) { | |
let queue = DispatchQueue(label: "\(queueLabelPrefix).\(jobIndex / jobCount)") | |
queue.async(group: group) { | |
let jobElements = self[jobIndex..<Swift.min(count, jobIndex + jobCount)] | |
do { | |
let jobIndexAndResults = try (jobIndex, jobElements.map(escapingtransform)) | |
resultAccumulatorQueue.sync { | |
result.append(jobIndexAndResults) | |
} | |
} catch { | |
resultAccumulatorQueue.sync { | |
mapError = error | |
} | |
} | |
} | |
} | |
group.wait() | |
} | |
if let mapError = mapError { | |
throw mapError | |
} | |
return result.sorted { $0.0 < $1.0 }.flatMap { $0.1 } | |
} | |
} | |
extension Path { | |
public var isMetalSourceFile: Bool { | |
return !self.isDirectory && self.extension == "metal" | |
} | |
public var isMetalLibFile: Bool { | |
return !self.isDirectory && self.extension == "metallib" | |
} | |
} | |
// | |
// Path+Extensions.swift | |
// Sourcery | |
// | |
// Created by Krunoslav Zaher on 1/6/17. | |
// Copyright © 2017 Pixle. All rights reserved. | |
// | |
public typealias Path = PathKit.Path | |
extension Path { | |
/// - returns: The `.cachesDirectory` search path in the user domain, as a `Path`. | |
public static var defaultBaseCachePath: Path { | |
let paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) as [String] | |
let path = paths[0] | |
return Path(path) | |
} | |
/// - parameter _basePath: The value of the `--cachePath` command line parameter, if any. | |
/// - note: This function does not consider the `--disableCache` command line parameter. | |
/// It is considered programmer error to call this function when `--disableCache` is specified. | |
public static func cachesDir(sourcePath: Path, basePath: Path? = nil, createIfMissing: Bool = true) -> Path { | |
let basePath = basePath ?? defaultBaseCachePath | |
let path = basePath + "MetalSmith" + sourcePath.lastComponent | |
if !path.exists && createIfMissing { | |
// swiftlint:disable:next force_try | |
try! FileManager.default.createDirectory(at: path.url, withIntermediateDirectories: true, attributes: nil) | |
} | |
return path | |
} | |
public var isTemplateFile: Bool { | |
return self.extension == "stencil" || | |
self.extension == "swifttemplate" || | |
self.extension == "ejs" | |
} | |
public var isSwiftSourceFile: Bool { | |
return !self.isDirectory && self.extension == "swift" | |
} | |
public func hasExtension(as string: String) -> Bool { | |
let extensionString = ".\(string)." | |
return self.string.contains(extensionString) | |
} | |
public init(_ string: String, relativeTo relativePath: Path) { | |
var path = Path(string) | |
if !path.isAbsolute { | |
path = (relativePath + path).absolute() | |
} | |
self.init(path.string) | |
} | |
public var allPaths: [Path] { | |
if isDirectory { | |
return (try? recursiveChildren()) ?? [] | |
} else { | |
return [self] | |
} | |
} | |
func attributes() throws -> [FileAttributeKey : Any] { | |
return try FileManager.default.attributesOfItem(atPath: self.string) | |
} | |
subscript(attribute: FileAttributeKey) -> Any? { | |
do { | |
let attrs = try attributes() | |
return attrs[attribute] | |
} catch { | |
return nil | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// FolderWatcher.swift | |
// Sourcery | |
// | |
// Created by Krzysztof Zabłocki on 24/12/2016. | |
// Copyright © 2016 Pixle. All rights reserved. | |
// | |
import Foundation | |
#if os(OSX) | |
public enum FolderWatcher { | |
public struct Event { | |
public let path: String | |
public let flag: Flag | |
public struct Flag: OptionSet { | |
public let rawValue: FSEventStreamEventFlags | |
public init(rawValue: FSEventStreamEventFlags) { | |
self.rawValue = rawValue | |
} | |
init(_ value: Int) { | |
self.rawValue = FSEventStreamEventFlags(value) | |
} | |
public static let isDirectory = Flag(kFSEventStreamEventFlagItemIsDir) | |
public static let isFile = Flag(kFSEventStreamEventFlagItemIsFile) | |
public static let created = Flag(kFSEventStreamEventFlagItemCreated) | |
public static let modified = Flag(kFSEventStreamEventFlagItemModified) | |
public static let removed = Flag(kFSEventStreamEventFlagItemRemoved) | |
public static let renamed = Flag(kFSEventStreamEventFlagItemRenamed) | |
public static let isHardlink = Flag(kFSEventStreamEventFlagItemIsHardlink) | |
public static let isLastHardlink = Flag(kFSEventStreamEventFlagItemIsLastHardlink) | |
public static let isSymlink = Flag(kFSEventStreamEventFlagItemIsSymlink) | |
public static let changeOwner = Flag(kFSEventStreamEventFlagItemChangeOwner) | |
public static let finderInfoModified = Flag(kFSEventStreamEventFlagItemFinderInfoMod) | |
public static let inodeMetaModified = Flag(kFSEventStreamEventFlagItemInodeMetaMod) | |
public static let xattrsModified = Flag(kFSEventStreamEventFlagItemXattrMod) | |
public var description: String { | |
var names: [String] = [] | |
if self.contains(.isDirectory) { names.append("isDir") } | |
if self.contains(.isFile) { names.append("isFile") } | |
if self.contains(.created) { names.append("created") } | |
if self.contains(.modified) { names.append("modified") } | |
if self.contains(.removed) { names.append("removed") } | |
if self.contains(.renamed) { names.append("renamed") } | |
if self.contains(.isHardlink) { names.append("isHardlink") } | |
if self.contains(.isLastHardlink) { names.append("isLastHardlink") } | |
if self.contains(.isSymlink) { names.append("isSymlink") } | |
if self.contains(.changeOwner) { names.append("changeOwner") } | |
if self.contains(.finderInfoModified) { names.append("finderInfoModified") } | |
if self.contains(.inodeMetaModified) { names.append("inodeMetaModified") } | |
if self.contains(.xattrsModified) { names.append("xattrsModified") } | |
return names.joined(separator: ", ") | |
} | |
} | |
} | |
public class Local { | |
private let path: String | |
private var stream: FSEventStreamRef! | |
private let closure: (_ events: [Event]) -> Void | |
/// Creates folder watcher. | |
/// | |
/// - Parameters: | |
/// - path: Path to observe | |
/// - latency: Latency to use | |
/// - closure: Callback closure | |
public init(path: String, latency: TimeInterval = 1/60, closure: @escaping (_ events: [Event]) -> Void) { | |
self.path = path | |
self.closure = closure | |
func handler(_ stream: ConstFSEventStreamRef, clientCallbackInfo: UnsafeMutableRawPointer?, numEvents: Int, eventPaths: UnsafeMutableRawPointer, eventFlags: UnsafePointer<FSEventStreamEventFlags>, eventIDs: UnsafePointer<FSEventStreamEventId>) { | |
let eventStream = unsafeBitCast(clientCallbackInfo, to: Local.self) | |
let paths = unsafeBitCast(eventPaths, to: NSArray.self) | |
let events = (0..<numEvents).compactMap { idx in | |
return (paths[idx] as? String).flatMap { Event(path: $0, flag: Event.Flag(rawValue: eventFlags[idx])) } | |
} | |
eventStream.closure(events) | |
} | |
var context = FSEventStreamContext() | |
context.info = unsafeBitCast(self, to: UnsafeMutableRawPointer.self) | |
let flags = UInt32(kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagFileEvents) | |
stream = FSEventStreamCreate(nil, handler, &context, [path] as CFArray, FSEventStreamEventId(kFSEventStreamEventIdSinceNow), latency, flags) | |
FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) | |
FSEventStreamStart(stream) | |
} | |
deinit { | |
FSEventStreamStop(stream) | |
FSEventStreamInvalidate(stream) | |
FSEventStreamRelease(stream) | |
} | |
} | |
} | |
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class MetalEnvironment: ObservableObject { | |
@Published public var device: MTLDevice? | |
@Published public var library: MTLLibrary? | |
@Published public var commandQueue: MTLCommandQueue? | |
var watcher: [FolderWatcher.Local]? = nil | |
func watch(_ paths: Paths) -> Self { | |
let source = paths.filter { $0.isMetalSourceFile } | |
let compiler = MetalCompiler() | |
self.watcher = source.process() { result in | |
if let sources = try? result.get(), | |
let metallib = try? compiler.compileAndArchive(sources) { | |
self.library = try? self.device?.makeLibrary(filepath: metallib.string) | |
} | |
} | |
return self | |
} | |
} | |
// Create it like: | |
MetalEnvironment().watch(Paths(include: ["/Users/nickkallen/Documents/SwiftMetal"], exclude: [])) | |
// For example, this is what I do | |
struct ContentView: View { | |
@EnvironmentObject var environment: MetalEnvironment | |
var body: some View { | |
let result = environment.device?.makeTexture(width: 480, height: 640, usage: [.shaderRead, .shaderWrite]) | |
return Group { | |
CommandBuffer() { | |
Colors(time: 0, result: result) | |
.dispatch(width: 480, height: 640) | |
} | |
Texture(result) | |
} | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static let environment = MetalEnvironment() | |
.watch(Paths(include: ["/Users/XXX/Documents/YYY"], exclude: [])) | |
static var previews: some View { | |
return ContentView() | |
.environmentObject(environment) | |
.previewLayout(.sizeThatFits) | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import Logging | |
import PathKit | |
typealias CompilationResult = [Path] | |
let log = Logger(label: "com.nk.MetalSmith") | |
public class MetalCompiler { | |
let cachesPath: Path | |
#if os(iOS) || os(watchOS) || os(tvOS) | |
let sdk = "iphoneos" | |
#elseif os(OSX) | |
let sdk = "macosx" | |
#endif | |
public init(cachesPath: Path = Path.cachesDir(sourcePath: Path("MetalSmith"))) { | |
self.cachesPath = cachesPath | |
} | |
public func compileAndArchive(_ sources: [Path]) throws -> Path? { | |
var previousUpdate = 0 | |
var accumulator = 0 | |
let step = sources.count / 10 // every 10% | |
let airs = try sources.parallelMap({ try compile($0) }) { _ in | |
if accumulator > previousUpdate + step { | |
previousUpdate = accumulator | |
let percentage = accumulator * 100 / sources.count | |
log.info("Scanning sources... \(percentage)% (\(sources.count) files)") | |
} | |
accumulator += 1 | |
} | |
return try archive(airs) | |
} | |
public func compile(_ in: Path) throws -> Path { | |
let out = cachesPath + Path("\(`in`.lastComponentWithoutExtension).air") | |
guard out.exists else { | |
log.info("Initial compilation of \(`in`.string).") | |
_ = shell("xcrun -sdk \(sdk) metal -c \(`in`.string) -o \(out.string)") | |
return out | |
} | |
if let inDate = `in`[.modificationDate] as? Date, | |
let outDate = out[.modificationDate] as? Date, | |
outDate < inDate { | |
log.info("Re-compiling \(`in`.string).") | |
_ = shell("xcrun -sdk \(sdk) metal -c \(`in`.string) -o \(out.string)") | |
} else { | |
log.info("Compiled version of \(`in`.string) is already up-to-date; skipping compilation.") | |
} | |
return out | |
} | |
private var defeatCache = 0 // device.makeLibrary() has an internal cache, so we cannot re-use filenames | |
public func archive(_ files: [Path]) throws -> Path? { | |
guard !files.isEmpty else { return nil } | |
let start = CFAbsoluteTimeGetCurrent() | |
if defeatCache > 0 { | |
let old = cachesPath + Path("default\(defeatCache-1).metallib") | |
if old.exists { | |
try old.delete() | |
} | |
} | |
let out = cachesPath + Path("default\(defeatCache).metallib") | |
_ = shell("xcrun -sdk \(sdk) metal \(files.map({ $0.string }).joined(separator: " ")) -o \(out.string)") | |
log.info("Archive: \(CFAbsoluteTimeGetCurrent() - start)") | |
defeatCache += 1 | |
return out | |
} | |
private func topPaths(from paths: [Path]) -> [Path] { | |
var top: [(Path, [Path])] = [] | |
paths.forEach { path in | |
// See if its already contained by the topDirectories | |
guard top.first(where: { (_, children) -> Bool in | |
return children.contains(path) | |
}) == nil else { return } | |
if path.isDirectory { | |
top.append((path, (try? path.recursiveChildren()) ?? [])) | |
} else { | |
let dir = path.parent() | |
let children = (try? dir.recursiveChildren()) ?? [] | |
if children.contains(path) { | |
top.append((dir, children)) | |
} else { | |
top.append((path, [])) | |
} | |
} | |
} | |
return top.map { $0.0 } | |
} | |
} | |
fileprivate func shell(_ command: String) -> String { | |
#if os(OSX) | |
let task = Process() | |
task.launchPath = "/bin/bash" | |
task.arguments = ["-c", command] | |
let pipe = Pipe() | |
task.standardOutput = pipe | |
task.launch() | |
let data = pipe.fileHandleForReading.readDataToEndOfFile() | |
let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String | |
return output | |
#else | |
return "Not sure how to shell out in a Catalyst app" | |
#endif | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
public struct Paths { | |
public typealias Filter = ((Path) -> Bool) | |
public let include: [Path] | |
public let exclude: [Path] | |
public let allPaths: [Path] | |
private var filter: Filter? = nil | |
public var isEmpty: Bool { | |
return allPaths.isEmpty | |
} | |
public init(include: [Path], exclude: [Path] = []) { | |
self.include = include | |
self.exclude = exclude | |
let include = self.include.flatMap { $0.allPaths } | |
let exclude = self.exclude.flatMap { $0.allPaths } | |
self.allPaths = Array(Set(include).subtracting(Set(exclude))).sorted() | |
} | |
public func filter(_ filter: @escaping Filter) -> Self { | |
var result = self | |
result.filter = filter | |
return result | |
} | |
} | |
#if os(OSX) | |
public extension Paths { | |
typealias ProcessCallback = (Result<[Path], Error>) -> Void | |
typealias ProcessOnceCallBack = ([Path]) throws -> Void | |
typealias WatchCallBack = ([FolderWatcher.Event]) -> Void | |
func process(_ callback: @escaping ProcessCallback) -> [FolderWatcher.Local] { | |
func processAndHandleErrors() { | |
do { | |
try processOnce() { | |
callback(.success($0)) | |
} | |
} catch { | |
callback(.failure(error)) | |
} | |
} | |
processAndHandleErrors() | |
return watch() { _ in | |
processAndHandleErrors() | |
} | |
} | |
func watch(_ onEvent: @escaping WatchCallBack) -> [FolderWatcher.Local] { | |
let sourceWatchers = topPaths(from: allPaths).map { watchPath -> FolderWatcher.Local in | |
return FolderWatcher.Local(path: watchPath.string) { events in | |
var eventPaths = events | |
.filter { $0.flag.contains(.isFile) } | |
if let filter = self.filter { | |
eventPaths = eventPaths.filter { filter(Path($0.path)) } | |
} | |
if !eventPaths.isEmpty { | |
onEvent(eventPaths) | |
} | |
} | |
} | |
return sourceWatchers | |
} | |
func processOnce(_ callback: ProcessOnceCallBack) throws { | |
let startScan = CFAbsoluteTimeGetCurrent() | |
log.info("Scanning sources...") | |
var allResults: [Path] = [] | |
let excludeSet = Set(exclude | |
.map { $0.isDirectory ? try? $0.recursiveChildren() : [$0] } | |
.compactMap({ $0 }).flatMap({ $0 })) | |
for from in include { | |
let fileList = from.isDirectory ? try from.recursiveChildren() : [from] | |
var sources = fileList | |
.filter { $0.exists } | |
.filter { | |
return !excludeSet.contains($0) | |
} | |
if let filter = self.filter { | |
sources = sources.filter { filter($0) } | |
} | |
allResults.append(contentsOf: sources) | |
} | |
log.info("Process all files: \(CFAbsoluteTimeGetCurrent() - startScan). \(allResults.count) files found.") | |
if !allResults.isEmpty { | |
try callback(allResults) | |
} | |
} | |
private func topPaths(from paths: [Path]) -> [Path] { | |
var top: [(Path, [Path])] = [] | |
paths.forEach { path in | |
// See if its already contained by the topDirectories | |
guard top.first(where: { (_, children) -> Bool in | |
return children.contains(path) | |
}) == nil else { return } | |
if path.isDirectory { | |
top.append((path, (try? path.recursiveChildren()) ?? [])) | |
} else { | |
let dir = path.parent() | |
let children = (try? dir.recursiveChildren()) ?? [] | |
if children.contains(path) { | |
top.append((dir, children)) | |
} else { | |
top.append((path, [])) | |
} | |
} | |
} | |
return top.map { $0.0 } | |
} | |
} | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment