]> git.saurik.com Git - apple/dyld.git/blobdiff - dyld3/dyld_app_cache_util.cpp
dyld-832.7.1.tar.gz
[apple/dyld.git] / dyld3 / dyld_app_cache_util.cpp
diff --git a/dyld3/dyld_app_cache_util.cpp b/dyld3/dyld_app_cache_util.cpp
new file mode 100644 (file)
index 0000000..0fe8495
--- /dev/null
@@ -0,0 +1,1801 @@
+/*
+ * Copyright (c) 2017 Apple Inc. All rights reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ *
+ * @APPLE_LICENSE_HEADER_END@
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <list>
+#include <map>
+#include <vector>
+
+#include <variant>
+
+#include <CoreFoundation/CFBundle.h>
+#include <CoreFoundation/CFPropertyList.h>
+#include <CoreFoundation/CFStream.h>
+
+#include "kernel_collection_builder.h"
+#include "ClosureFileSystemPhysical.h"
+#include "FileUtils.h"
+#include "JSONWriter.h"
+#include "MachOAppCache.h"
+
+using namespace dyld3::json;
+
+using dyld3::closure::FileSystemPhysical;
+using dyld3::closure::LoadedFileInfo;
+using dyld3::GradedArchs;
+using dyld3::MachOAnalyzer;
+using dyld3::MachOAppCache;
+using dyld3::Platform;
+using dyld3::json::Node;
+
+__attribute__((noreturn))
+static void exit_usage(const char* missingOption = nullptr) {
+    if ( missingOption != nullptr ) {
+        fprintf(stderr, "Missing option '%s'\n", missingOption);
+        fprintf(stderr, "\n");
+    }
+
+    fprintf(stderr, "Usage: dyld_app_cache_util [-layout] [-entrypoint] [-fixups] [-symbols] [-kmod] [-uuid] [-fips] -arch arch -platform platform -app-cache app-cache-path\n");
+    fprintf(stderr, "  -layout          print the layout of an existing app cache\n");
+    fprintf(stderr, "  -entrypoint      print the entrypoint of an existing app cache\n");
+    fprintf(stderr, "  -fixups          print the fixups of an existing app cache\n");
+    fprintf(stderr, "  -symbols         print the symbols of an existing app cache\n");
+    fprintf(stderr, "  -kmod            print the kmod_info of an existing app cache\n");
+    fprintf(stderr, "  -uuid            print the UUID of an existing app cache\n");
+    fprintf(stderr, "  -fips            print the FIPS section of an existing app cache\n");
+    fprintf(stderr, "\n");
+
+    fprintf(stderr, "Usage: dyld_app_cache_util -validate file-path -arch arch -platform platform\n");
+    fprintf(stderr, "  -validate        the path to check is valid for inserting in to an app cache\n");
+    fprintf(stderr, "\n");
+
+    fprintf(stderr, "Usage: dyld_app_cache_util -list-bundles directory-path\n");
+    fprintf(stderr, "  -list-bundles    the directory to index for bundles\n");
+    fprintf(stderr, "\n");
+
+    fprintf(stderr, "Usage: dyld_app_cache_util -create-kernel-collection kernel-collection -kernel kernel-path [-extensions path-to-extensions] [-bundle-id bundle-id]*\n");
+    fprintf(stderr, "  -create-kernel-collection    create a kernel collection and write to the given path\n");
+    fprintf(stderr, "  -kernel                      path to the kernel static executable\n");
+    fprintf(stderr, "  -extensions                  path to the kernel extensions directory\n");
+    fprintf(stderr, "  -bundle-id                   zero or more bundle-ids to link in to the kernel collection\n");
+    fprintf(stderr, "  -sectcreate                  segment name, section name, and payload file path for more data to embed in the kernel\n");
+    fprintf(stderr, "\n");
+
+    fprintf(stderr, "Usage: dyld_app_cache_util -create-pageable-kernel-collection aux-kernel-collection -kernel-collection kernel-collection-path [-extensions path-to-extensions] [-bundle-id bundle-id]*\n");
+    fprintf(stderr, "  -create-pageable-kernel-collection   create a pageable kernel collection and write to the given path\n");
+    fprintf(stderr, "  -kernel-collection                   path to the kernel collection collection\n");
+    fprintf(stderr, "  -extensions                          path to the kernel extensions directory\n");
+    fprintf(stderr, "  -bundle-id                           zero or more bundle-ids to link in to the kernel collection\n");
+    fprintf(stderr, "\n");
+
+    fprintf(stderr, "Usage: dyld_app_cache_util -create-aux-kernel-collection aux-kernel-collection -kernel-collection kernel-collection-path [-extensions path-to-extensions] [-bundle-id bundle-id]*\n");
+    fprintf(stderr, "  -create-aux-kernel-collection    create an aux kernel collection and write to the given path\n");
+    fprintf(stderr, "  -kernel-collection               path to the kernel collection\n");
+    fprintf(stderr, "  -pageable-collection             path to the pageable collection\n");
+    fprintf(stderr, "  -extensions                      path to the kernel extensions directory\n");
+    fprintf(stderr, "  -bundle-id                       zero or more bundle-ids to link in to the kernel collection\n");
+    fprintf(stderr, "\n");
+
+    fprintf(stderr, "Common options:\n");
+    fprintf(stderr, "  -arch xxx        the arch to use to create the app cache\n");
+    fprintf(stderr, "  -platform xxx    the platform to use to create the app cache\n");
+
+    exit(1);
+}
+
+struct DumpOptions {
+    bool printLayout            = false;
+    bool printEntryPoint        = false;
+    bool printFixups            = false;
+    bool printSymbols           = false;
+    bool printUUID              = false;
+    bool printKModInfo          = false;
+    bool printFIPS              = false;
+};
+
+struct ValidateOptions {
+    const char* filePath    = nullptr;
+};
+
+struct ListBundlesOptions {
+    const char* directoryPath    = nullptr;
+};
+
+// The payload of -sectcreate
+struct SectionData {
+    const char* segmentName         = nullptr;
+    const char* sectionName         = nullptr;
+    const char* payloadFilePath     = nullptr;
+};
+
+struct CreateKernelCollectionOptions {
+    const char*                             outputCachePath         = nullptr;
+    const char*                             kernelPath              = nullptr;
+    const char*                             kernelCollectionPath    = nullptr;
+    const char*                             pageableCollectionPath  = nullptr;
+    const char*                             extensionsPath          = nullptr;
+    const char*                             volumeRoot              = "";
+    std::vector<const char*>                bundleIDs;
+    bool                                    verbose                 = false;
+    bool                                    printJSONErrors         = false;
+    CollectionKind                          collectionKind          = unknownKC;
+    StripMode                               stripMode               = unknownStripMode;
+    std::vector<SectionData>                sections;
+    const char*                             prelinkInfoExtraData    = nullptr;
+};
+
+typedef std::variant<std::monostate, DumpOptions, ValidateOptions, ListBundlesOptions, CreateKernelCollectionOptions> OptionsVariants;
+
+struct CommonOptions {
+    const char*                 appCachePath        = nullptr;
+    std::vector<const char*>    archs;
+    const char*                 platform            = nullptr;
+};
+
+CommonOptions gOpts;
+
+template<typename T>
+static T& exitOrGetState(OptionsVariants& options, const char* argv) {
+    if (std::holds_alternative<std::monostate>(options)) {
+        return options.emplace<T>();
+    }
+    if (std::holds_alternative<T>(options))
+        return std::get<T>(options);
+    exit_usage();
+}
+
+static bool parseArgs(int argc, const char* argv[], OptionsVariants& options) {
+    for (int i = 1; i < argc; ++i) {
+        const char* arg = argv[i];
+        if (arg[0] != '-') {
+            fprintf(stderr, "unknown option: %s\n", arg);
+            exit_usage();
+        }
+
+        // Common options
+        if (strcmp(arg, "-app-cache") == 0) {
+            if (gOpts.appCachePath != nullptr)
+                exit_usage();
+            gOpts.appCachePath = argv[++i];
+            continue;
+        }
+        if (strcmp(arg, "-arch") == 0) {
+            gOpts.archs.push_back(argv[++i]);
+            continue;
+        }
+        if (strcmp(arg, "-platform") == 0) {
+            if (gOpts.platform != nullptr)
+                exit_usage();
+            gOpts.platform = argv[++i];
+            continue;
+        }
+
+        // DumpOptions
+        if (strcmp(arg, "-layout") == 0) {
+            exitOrGetState<DumpOptions>(options, arg).printLayout = true;
+            continue;
+        }
+        if (strcmp(arg, "-entrypoint") == 0) {
+            exitOrGetState<DumpOptions>(options, arg).printEntryPoint = true;
+            continue;
+        }
+        if (strcmp(arg, "-fixups") == 0) {
+            exitOrGetState<DumpOptions>(options, arg).printFixups = true;
+            continue;
+        }
+        if (strcmp(arg, "-symbols") == 0) {
+            exitOrGetState<DumpOptions>(options, arg).printSymbols = true;
+            continue;
+        }
+        if (strcmp(arg, "-uuid") == 0) {
+            exitOrGetState<DumpOptions>(options, arg).printUUID = true;
+            continue;
+        }
+        if (strcmp(arg, "-kmod") == 0) {
+            exitOrGetState<DumpOptions>(options, arg).printKModInfo = true;
+            continue;
+        }
+        if (strcmp(arg, "-fips") == 0) {
+            exitOrGetState<DumpOptions>(options, arg).printFIPS = true;
+            continue;
+        }
+
+        // ValidateOptions
+        if (strcmp(arg, "-validate") == 0) {
+            exitOrGetState<ValidateOptions>(options, arg).filePath = argv[++i];
+            continue;
+        }
+
+        // ListBundlesOptions
+        if (strcmp(arg, "-list-bundles") == 0) {
+            exitOrGetState<ListBundlesOptions>(options, arg).directoryPath = argv[++i];
+            continue;
+        }
+
+        // CreateKernelCollectionOptions
+        if (strcmp(arg, "-create-kernel-collection") == 0) {
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).outputCachePath = argv[++i];
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).collectionKind = baseKC;
+            continue;
+        }
+        if (strcmp(arg, "-kernel") == 0) {
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).kernelPath = argv[++i];
+            continue;
+        }
+        if (strcmp(arg, "-create-pageable-kernel-collection") == 0) {
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).outputCachePath = argv[++i];
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).collectionKind = pageableKC;
+            continue;
+        }
+        if (strcmp(arg, "-create-aux-kernel-collection") == 0) {
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).outputCachePath = argv[++i];
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).collectionKind = auxKC;
+            continue;
+        }
+        if (strcmp(arg, "-kernel-collection") == 0) {
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).kernelCollectionPath = argv[++i];
+            continue;
+        }
+        if (strcmp(arg, "-pageable-collection") == 0) {
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).pageableCollectionPath = argv[++i];
+            continue;
+        }
+        if (strcmp(arg, "-extensions") == 0) {
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).extensionsPath = argv[++i];
+            continue;
+        }
+        if (strcmp(arg, "-volume-root") == 0) {
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).volumeRoot = argv[++i];
+            continue;
+        }
+        if (strcmp(arg, "-bundle-id") == 0) {
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).bundleIDs.push_back(argv[++i]);
+            continue;
+        }
+        if (strcmp(arg, "-verbose") == 0) {
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).verbose = true;
+            continue;
+        }
+        if (strcmp(arg, "-json-errors") == 0) {
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).printJSONErrors = true;
+            continue;
+        }
+        if (strcmp(arg, "-strip-all") == 0) {
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).stripMode = stripAll;
+            continue;
+        }
+        if (strcmp(arg, "-strip-all-kexts") == 0) {
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).stripMode = stripAllKexts;
+            continue;
+        }
+        if (strcmp(arg, "-sectcreate") == 0) {
+            const char* segmentName = argv[++i];
+            const char* sectionName = argv[++i];
+            const char* payloadFilePath = argv[++i];
+            SectionData sectData = { segmentName, sectionName, payloadFilePath };
+                exitOrGetState<CreateKernelCollectionOptions>(options, arg).sections.push_back(sectData);
+            continue;
+        }
+        if (strcmp(arg, "-prelink-info-extra") == 0) {
+            const char* payloadFilePath = argv[++i];
+            exitOrGetState<CreateKernelCollectionOptions>(options, arg).prelinkInfoExtraData = payloadFilePath;
+            continue;
+        }
+
+
+        fprintf(stderr, "unknown option: %s\n", arg);
+        exit_usage();
+    }
+
+    return true;
+}
+
+static Platform stringToPlatform(const std::string& str) {
+    if (str == "unknown")
+        return Platform::unknown;
+    if (str == "macOS")
+        return Platform::macOS;
+    if (str == "iOS")
+        return Platform::iOS;
+    if (str == "tvOS")
+        return Platform::tvOS;
+    if (str == "watchOS")
+        return Platform::watchOS;
+    if (str == "bridgeOS")
+        return Platform::bridgeOS;
+    if (str == "iOSMac")
+        return Platform::iOSMac;
+    if (str == "UIKitForMac")
+        return Platform::iOSMac;
+    if (str == "iOS_simulator")
+        return Platform::iOS_simulator;
+    if (str == "tvOS_simulator")
+        return Platform::tvOS_simulator;
+    if (str == "watchOS_simulator")
+        return Platform::watchOS_simulator;
+    return Platform::unknown;
+}
+
+static int dumpAppCache(const DumpOptions& options) {
+    // Verify any required options
+    if (gOpts.archs.size() != 1)
+        exit_usage("-arch");
+    if (gOpts.platform == nullptr)
+        exit_usage("-platform");
+
+    if (gOpts.appCachePath == nullptr)
+        exit_usage();
+
+    FileSystemPhysical fileSystem;
+    if (!fileSystem.fileExists(gOpts.appCachePath)) {
+        fprintf(stderr, "App-cache path does not exist: %s\n", gOpts.appCachePath);
+        return 1;
+    }
+
+    const GradedArchs& archs = GradedArchs::forName(gOpts.archs[0]);
+    Platform platform = Platform::unknown;
+    bool isKernelCollection = false;
+
+    // HACK: Pass a real option for building a kernel app cache
+    if (!strcmp(gOpts.platform, "kernel")) {
+        isKernelCollection = true;
+    } else {
+        platform = stringToPlatform(gOpts.platform);
+        if (platform == Platform::unknown) {
+            fprintf(stderr, "Could not create app cache because: unknown platform '%s'\n", gOpts.platform);
+            return 1;
+        }
+    }
+
+    __block Diagnostics diag;
+    char appCacheRealPath[MAXPATHLEN];
+    LoadedFileInfo loadedFileInfo = MachOAnalyzer::load(diag, fileSystem, gOpts.appCachePath, archs, platform, appCacheRealPath);
+    if (diag.hasError()) {
+        fprintf(stderr, "Could not load app cache because: %s\n", diag.errorMessage().c_str());
+        return 1;
+    }
+
+    MachOAppCache* appCacheMA = (MachOAppCache*)loadedFileInfo.fileContent;
+    if (appCacheMA == nullptr) {
+        fprintf(stderr, "Could not load app cache: %s\n", gOpts.appCachePath);
+        return 1;
+    }
+
+    if (options.printLayout) {
+        __block Node topNode;
+
+        // Add the segments for the app cache
+        __block Node segmentsNode;
+        __block bool hasError = false;
+        appCacheMA->forEachSegment(^(const dyld3::MachOFile::SegmentInfo &info, bool &stop) {
+            Node segmentNode;
+            segmentNode.map["name"] = makeNode(info.segName);
+            segmentNode.map["vmAddr"] = makeNode(hex(info.vmAddr));
+            segmentNode.map["vmSize"] = makeNode(hex(info.vmSize));
+            segmentNode.map["vmEnd"] = makeNode(hex(info.vmAddr + info.vmSize));
+            switch (info.protections) {
+                case VM_PROT_READ:
+                    segmentNode.map["permissions"] = makeNode("r--");
+                    break;
+                case VM_PROT_WRITE:
+                    segmentNode.map["permissions"] = makeNode("-w-");
+                    break;
+                case VM_PROT_EXECUTE:
+                    segmentNode.map["permissions"] = makeNode("--x");
+                    break;
+                case VM_PROT_READ | VM_PROT_WRITE:
+                    segmentNode.map["permissions"] = makeNode("rw-");
+                    break;
+                case VM_PROT_READ | VM_PROT_EXECUTE:
+                    segmentNode.map["permissions"] = makeNode("r-x");
+                    break;
+                case VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE:
+                    segmentNode.map["permissions"] = makeNode("rwx");
+                    break;
+                default:
+                    fprintf(stderr, "Unknown permissions on segment '%s'\n", info.segName);
+                    hasError = true;
+                    stop = true;
+            }
+
+            __block Node sectionsNode;
+            appCacheMA->forEachSection(^(const dyld3::MachOAnalyzer::SectionInfo &sectInfo, bool malformedSectionRange, bool &stop) {
+                if ( strncmp(sectInfo.segInfo.segName, info.segName, 16) != 0 )
+                    return;
+
+                Node sectionNode;
+                sectionNode.map["name"] = makeNode(sectInfo.sectName);
+                sectionNode.map["vmAddr"] = makeNode(hex(sectInfo.sectAddr));
+                sectionNode.map["vmSize"] = makeNode(hex(sectInfo.sectSize));
+                sectionNode.map["vmEnd"] = makeNode(hex(sectInfo.sectAddr + sectInfo.sectSize));
+
+                sectionsNode.array.push_back(sectionNode);
+            });
+
+            if ( !sectionsNode.array.empty() ) {
+                segmentNode.map["sections"] = sectionsNode;
+            }
+
+            segmentsNode.array.push_back(segmentNode);
+        });
+
+        if (hasError)
+            return 1;
+
+        topNode.map["cache-segments"] = segmentsNode;
+
+        // Map from name to relative path
+        __block std::unordered_map<std::string, std::string> relativePaths;
+        appCacheMA->forEachPrelinkInfoLibrary(diag, ^(const char *bundleName, const char* relativePath,
+                                                      const std::vector<const char *> &deps) {
+            if ( relativePath != nullptr )
+                relativePaths[bundleName] = relativePath;
+        });
+
+        __block Node dylibsNode;
+        appCacheMA->forEachDylib(diag, ^(const MachOAnalyzer *ma, const char *name, bool &stop) {
+            __block Node segmentsNode;
+            ma->forEachSegment(^(const dyld3::MachOFile::SegmentInfo &info, bool &stop) {
+                Node segmentNode;
+                segmentNode.map["name"] = makeNode(info.segName);
+                segmentNode.map["vmAddr"] = makeNode(hex(info.vmAddr));
+                segmentNode.map["vmSize"] = makeNode(hex(info.vmSize));
+                segmentNode.map["vmEnd"] = makeNode(hex(info.vmAddr + info.vmSize));
+
+                switch (info.protections) {
+                    case VM_PROT_READ:
+                        segmentNode.map["permissions"] = makeNode("r--");
+                        break;
+                    case VM_PROT_WRITE:
+                        segmentNode.map["permissions"] = makeNode("-w-");
+                        break;
+                    case VM_PROT_EXECUTE:
+                        segmentNode.map["permissions"] = makeNode("--x");
+                        break;
+                    case VM_PROT_READ | VM_PROT_WRITE:
+                        segmentNode.map["permissions"] = makeNode("rw-");
+                        break;
+                    case VM_PROT_READ | VM_PROT_EXECUTE:
+                        segmentNode.map["permissions"] = makeNode("r-x");
+                        break;
+                    default:
+                        fprintf(stderr, "Unknown permissions on segment '%s'\n", info.segName);
+                        hasError = true;
+                        stop = true;
+                }
+
+                __block Node sectionsNode;
+                ma->forEachSection(^(const dyld3::MachOAnalyzer::SectionInfo &sectInfo, bool malformedSectionRange, bool &stop) {
+                    if ( strncmp(sectInfo.segInfo.segName, info.segName, 16) != 0 )
+                        return;
+
+                    Node sectionNode;
+                    sectionNode.map["name"] = makeNode(sectInfo.sectName);
+                    sectionNode.map["vmAddr"] = makeNode(hex(sectInfo.sectAddr));
+                    sectionNode.map["vmSize"] = makeNode(hex(sectInfo.sectSize));
+                    sectionNode.map["vmEnd"] = makeNode(hex(sectInfo.sectAddr + sectInfo.sectSize));
+
+                    sectionsNode.array.push_back(sectionNode);
+                });
+
+                if ( !sectionsNode.array.empty() ) {
+                    segmentNode.map["sections"] = sectionsNode;
+                }
+
+                segmentsNode.array.push_back(segmentNode);
+            });
+
+            Node dylibNode;
+            dylibNode.map["name"] = makeNode(name);
+            dylibNode.map["segments"] = segmentsNode;
+
+            auto relativePathIt = relativePaths.find(name);
+            if ( relativePathIt != relativePaths.end() )
+                dylibNode.map["relativePath"] = makeNode(relativePathIt->second);
+
+            dylibsNode.array.push_back(dylibNode);
+        });
+
+        topNode.map["dylibs"] = dylibsNode;
+
+        printJSON(topNode, 0, std::cout);
+    }
+
+    if (options.printEntryPoint) {
+        __block Node topNode;
+
+        // add entry
+        uint64_t    entryOffset;
+        bool        usesCRT;
+        Node        entryPointNode;
+        if ( appCacheMA->getEntry(entryOffset, usesCRT) ) {
+            entryPointNode.value = hex(appCacheMA->preferredLoadAddress() + entryOffset);
+        }
+
+        topNode.map["entrypoint"] = entryPointNode;
+
+        printJSON(topNode, 0, std::cout);
+    }
+
+    if (options.printFixups) {
+        __block Node topNode;
+
+        __block uint64_t baseAddress = ~0ULL;
+        appCacheMA->forEachSegment(^(const dyld3::MachOAnalyzer::SegmentInfo& info, bool& stop) {
+            baseAddress = std::min(baseAddress, info.vmAddr);
+        });
+        uint64_t cacheBaseAddress = baseAddress;
+        uint64_t textSegVMAddr = appCacheMA->preferredLoadAddress();
+
+        auto getFixupsNode = [cacheBaseAddress, textSegVMAddr](const dyld3::MachOAnalyzer* ma) {
+            __block Node fixupsNode;
+
+            if (!ma->hasChainedFixups()) {
+                return makeNode("none");
+            }
+
+            // Keep track of the fixups seen by chained fixups.  The remainder might be
+            // classic relocs if we are the x86_64 kernel collection
+            __block std::set<uint64_t> seenFixupVMOffsets;
+
+            __block Diagnostics diag;
+            ma->withChainStarts(diag, 0, ^(const dyld_chained_starts_in_image* starts) {
+                ma->forEachFixupInAllChains(diag, starts, false, ^(dyld3::MachOLoaded::ChainedFixupPointerOnDisk* fixupLoc, const dyld_chained_starts_in_segment* segInfo, bool& stop) {
+                    uint64_t vmOffset = (uint8_t*)fixupLoc - (uint8_t*)ma;
+                    seenFixupVMOffsets.insert(vmOffset);
+
+                    // Correct for __DATA being before __TEXT, in which case the offset
+                    // is from __DATA, not a mach header offset
+                    vmOffset += (textSegVMAddr - cacheBaseAddress);
+
+                    fixupsNode.map[hex(vmOffset)] = makeNode("fixup");
+                    switch (segInfo->pointer_format) {
+                        case DYLD_CHAINED_PTR_64_KERNEL_CACHE:
+                        case DYLD_CHAINED_PTR_X86_64_KERNEL_CACHE: {
+                            uint64_t targetVMOffset = fixupLoc->kernel64.target;
+                            uint64_t targetVMAddr = targetVMOffset + cacheBaseAddress;
+                            std::string level = "kc(" + decimal(fixupLoc->kernel64.cacheLevel) + ")";
+                            std::string fixup = level + " + " + hex(targetVMAddr);
+                            if (fixupLoc->kernel64.isAuth) {
+                                fixup += " auth(";
+                                fixup += fixupLoc->kernel64.keyName();
+                                fixup += " ";
+                                fixup += fixupLoc->kernel64.addrDiv ? "addr" : "!addr";
+                                fixup += " ";
+                                fixup += decimal(fixupLoc->kernel64.diversity);
+                                fixup += ")";
+                            }
+                            fixupsNode.map[hex(vmOffset)] = makeNode(fixup);
+                            break;
+                        }
+                        default:
+                            diag.error("unknown pointer type %d", segInfo->pointer_format);
+                            break;
+                    }
+                 });
+            });
+            diag.assertNoError();
+
+            ma->forEachRebase(diag, ^(const char *opcodeName, const dyld3::MachOAnalyzer::LinkEditInfo &leInfo,
+                                      const dyld3::MachOAnalyzer::SegmentInfo *segments,
+                                      bool segIndexSet, uint32_t pointerSize, uint8_t segmentIndex,
+                                      uint64_t segmentOffset, dyld3::MachOAnalyzer::Rebase kind, bool &stop) {
+                uint64_t rebaseVmAddr  = segments[segmentIndex].vmAddr + segmentOffset;
+                uint64_t runtimeOffset = rebaseVmAddr - textSegVMAddr;
+                const uint8_t* fixupLoc = (const uint8_t*)ma + runtimeOffset;
+
+                // Correct for __DATA being before __TEXT, in which case the offset
+                // is from __DATA, not a mach header offset
+                runtimeOffset += (textSegVMAddr - cacheBaseAddress);
+
+                std::string fixup = "kc(0) + ";
+                switch ( kind ) {
+                    case dyld3::MachOAnalyzer::Rebase::unknown:
+                        fixup += " : unhandled";
+                        break;
+                    case dyld3::MachOAnalyzer::Rebase::pointer32: {
+                        uint32_t value = *(uint32_t*)(fixupLoc);
+                        fixup += hex(value) + " : pointer32";
+                        break;
+                    }
+                    case dyld3::MachOAnalyzer::Rebase::pointer64: {
+                        uint64_t value = *(uint64_t*)(fixupLoc);
+                        fixup += hex(value) + " : pointer64";
+                        break;
+                    }
+                    case dyld3::MachOAnalyzer::Rebase::textPCrel32:
+                        fixup += " : pcrel32";
+                        break;
+                    case dyld3::MachOAnalyzer::Rebase::textAbsolute32:
+                        fixup += " : absolute32";
+                        break;
+                }
+                fixupsNode.map[hex(runtimeOffset)] = makeNode(fixup);
+            });
+            diag.assertNoError();
+
+            return fixupsNode;
+        };
+
+        topNode.map["fixups"] = getFixupsNode(appCacheMA);
+
+        __block Node dylibsNode;
+        appCacheMA->forEachDylib(diag, ^(const MachOAnalyzer *ma, const char *name, bool &stop) {
+            Node dylibNode;
+            dylibNode.map["name"] = makeNode(name);
+            dylibNode.map["fixups"] = getFixupsNode(ma);
+
+            dylibsNode.array.push_back(dylibNode);
+        });
+
+        topNode.map["dylibs"] = dylibsNode;
+
+        printJSON(topNode, 0, std::cout);
+    }
+
+    if (options.printSymbols) {
+        __block Node topNode;
+
+        __block Node dylibsNode;
+        appCacheMA->forEachDylib(diag, ^(const MachOAnalyzer *ma, const char *name, bool &stop) {
+            __block Node globalSymbolsNode;
+            ma->forEachGlobalSymbol(diag, ^(const char *symbolName, uint64_t n_value, uint8_t n_type, uint8_t n_sect, uint16_t n_desc, bool &stop) {
+                Node symbolNode;
+                symbolNode.map["name"] = makeNode(symbolName);
+                symbolNode.map["vmAddr"] = makeNode(hex(n_value));
+
+                globalSymbolsNode.array.push_back(symbolNode);
+            });
+
+            __block Node localSymbolsNode;
+            ma->forEachLocalSymbol(diag, ^(const char *symbolName, uint64_t n_value, uint8_t n_type, uint8_t n_sect, uint16_t n_desc, bool &stop) {
+                Node symbolNode;
+                symbolNode.map["name"] = makeNode(symbolName);
+                symbolNode.map["vmAddr"] = makeNode(hex(n_value));
+
+                localSymbolsNode.array.push_back(symbolNode);
+            });
+
+            if (globalSymbolsNode.array.empty())
+                globalSymbolsNode = makeNode("none");
+            if (localSymbolsNode.array.empty())
+                localSymbolsNode = makeNode("none");
+
+            Node dylibNode;
+            dylibNode.map["name"] = makeNode(name);
+            dylibNode.map["global-symbols"] = globalSymbolsNode;
+            dylibNode.map["local-symbols"] = localSymbolsNode;
+
+            dylibsNode.array.push_back(dylibNode);
+        });
+
+        topNode.map["dylibs"] = dylibsNode;
+
+        printJSON(topNode, 0, std::cout);
+    }
+
+    if (options.printUUID) {
+        __block Node topNode;
+
+        // add uuid
+        Node uuidNode;
+        uuid_t uuid = {};
+        if ( appCacheMA->getUuid(uuid) ) {
+            uuid_string_t uuidString;
+            uuid_unparse_upper(uuid, uuidString);
+            uuidNode.value = uuidString;
+        }
+
+        topNode.map["uuid"] = uuidNode;
+
+        auto getPlistUUID = ^(const char* jsonNodeName, CFStringRef keyName) {
+            uuid_t uuid = {};
+            const uint8_t* prelinkInfoBuffer = nullptr;
+            uint64_t prelinkInfoBufferSize = 0;
+            prelinkInfoBuffer = (const uint8_t*)appCacheMA->findSectionContent("__PRELINK_INFO", "__info", prelinkInfoBufferSize);
+            if ( prelinkInfoBuffer != nullptr ) {
+                CFReadStreamRef readStreamRef = CFReadStreamCreateWithBytesNoCopy(kCFAllocatorDefault, prelinkInfoBuffer, prelinkInfoBufferSize, kCFAllocatorNull);
+                if ( !CFReadStreamOpen(readStreamRef) ) {
+                    fprintf(stderr, "Could not open plist stream\n");
+                    exit(1);
+                }
+                CFErrorRef errorRef = nullptr;
+                CFPropertyListRef plistRef = CFPropertyListCreateWithStream(kCFAllocatorDefault, readStreamRef, prelinkInfoBufferSize, kCFPropertyListImmutable, nullptr, &errorRef);
+                if ( errorRef != nullptr ) {
+                    CFStringRef stringRef = CFErrorCopyFailureReason(errorRef);
+                    fprintf(stderr, "Could not read plist because: %s\n", CFStringGetCStringPtr(stringRef, kCFStringEncodingASCII));
+                    CFRelease(stringRef);
+                    exit(1);
+                }
+                assert(CFGetTypeID(plistRef) == CFDictionaryGetTypeID());
+                CFDataRef uuidDataRef = (CFDataRef)CFDictionaryGetValue((CFDictionaryRef)plistRef, keyName);
+                if ( uuidDataRef != nullptr ) {
+                    CFDataGetBytes(uuidDataRef, CFRangeMake(0, CFDataGetLength(uuidDataRef)), uuid);
+
+                    Node uuidNode;
+                    uuid_string_t uuidString;
+                    uuid_unparse_upper(uuid, uuidString);
+                    uuidNode.value = uuidString;
+                    topNode.map[jsonNodeName] = uuidNode;
+                }
+                CFRelease(plistRef);
+                CFRelease(readStreamRef);
+            }
+        };
+
+        getPlistUUID("prelink-info-uuid", CFSTR("_PrelinkKCID"));
+
+        // If we are an auxKC, then we should also have a reference to the baseKC UUID
+        getPlistUUID("prelink-info-base-uuid", CFSTR("_BootKCID"));
+
+        // If we are an pageableKC, then we should also have a reference to the pageableKC UUID
+        getPlistUUID("prelink-info-pageable-uuid", CFSTR("_PageableKCID"));
+
+        printJSON(topNode, 0, std::cout);
+    }
+
+    if (options.printKModInfo) {
+        __block Node topNode;
+
+        appCacheMA->forEachDylib(diag, ^(const MachOAnalyzer *ma, const char *name, bool &stop) {
+            Node dylibNode;
+            dylibNode.map["name"] = makeNode(name);
+
+            // Check for a global first
+            __block uint64_t kmodInfoVMOffset = 0;
+            __block bool found = false;
+            {
+                dyld3::MachOAnalyzer::FoundSymbol foundInfo;
+                found = ma->findExportedSymbol(diag, "_kmod_info", true, foundInfo, nullptr);
+                if ( found ) {
+                    kmodInfoVMOffset = foundInfo.value;
+                }
+            }
+            // And fall back to a local if we need to
+            if ( !found ) {
+                ma->forEachLocalSymbol(diag, ^(const char* aSymbolName, uint64_t n_value, uint8_t n_type,
+                                               uint8_t n_sect, uint16_t n_desc, bool& stop) {
+                    if ( strcmp(aSymbolName, "_kmod_info") == 0 ) {
+                        kmodInfoVMOffset = n_value - ma->preferredLoadAddress();
+                        found = true;
+                        stop = true;
+                    }
+                });
+            }
+
+            if ( found ) {
+                dyld3::MachOAppCache::KModInfo64_v1* kmodInfo = (dyld3::MachOAppCache::KModInfo64_v1*)((uint8_t*)ma + kmodInfoVMOffset);
+                Node kmodInfoNode;
+                kmodInfoNode.map["info-version"] = makeNode(decimal(kmodInfo->info_version));
+                kmodInfoNode.map["name"] = makeNode((const char*)&kmodInfo->name[0]);
+                kmodInfoNode.map["version"] = makeNode((const char*)&kmodInfo->version[0]);
+                kmodInfoNode.map["address"] = makeNode(hex(kmodInfo->address));
+                kmodInfoNode.map["size"] = makeNode(hex(kmodInfo->size));
+
+                dylibNode.map["kmod_info"] = kmodInfoNode;
+            } else {
+                dylibNode.map["kmod_info"] = makeNode("none");
+            }
+
+            topNode.array.push_back(dylibNode);
+        });
+
+        printJSON(topNode, 0, std::cout);
+    }
+
+    if (options.printFIPS) {
+        __block Node topNode;
+
+        appCacheMA->forEachDylib(diag, ^(const MachOAnalyzer *ma, const char *name, bool &stop) {
+            if ( strcmp(name, "com.apple.kec.corecrypto") != 0 )
+                return;
+
+            uint64_t hashStoreSize;
+            const void* hashStoreLocation = ma->findSectionContent("__TEXT", "__fips_hmacs", hashStoreSize);
+            assert(hashStoreLocation != nullptr);
+
+            const uint8_t* hashBuffer = (const uint8_t*)hashStoreLocation;
+            std::string hashString;
+
+            for (int i = 0; i < hashStoreSize; ++i) {
+                uint8_t byte = hashBuffer[i];
+                uint8_t nibbleL = byte & 0x0F;
+                uint8_t nibbleH = byte >> 4;
+                if ( nibbleH < 10 ) {
+                    hashString += '0' + nibbleH;
+                } else {
+                    hashString += 'a' + (nibbleH-10);
+                }
+                if ( nibbleL < 10 ) {
+                    hashString += '0' + nibbleL;
+                } else {
+                    hashString += 'a' + (nibbleL-10);
+                }
+            }
+
+            stop = true;
+
+            topNode.map["fips"] = makeNode(hashString);
+        });
+
+        printJSON(topNode, 0, std::cout);
+    }
+
+    return 0;
+}
+
+static int validateFile(const ValidateOptions& options) {
+    // Verify any required options
+    if (gOpts.archs.size() != 1)
+        exit_usage("-arch");
+    if (gOpts.platform == nullptr)
+        exit_usage("-platform");
+    if (options.filePath == nullptr)
+        exit_usage();
+
+    const GradedArchs& archs = GradedArchs::forName(gOpts.archs[0]);
+    Platform platform = Platform::unknown;
+
+    // HACK: Pass a real option for building a kernel app cache
+    if (strcmp(gOpts.platform, "kernel")) {
+        fprintf(stderr, "Could not create app cache because: unknown platform '%s'\n", gOpts.platform);
+        return 1;
+    }
+
+    __block Diagnostics diag;
+
+    std::string file = options.filePath;
+    {
+        FileSystemPhysical fileSystem;
+        char fileRealPath[MAXPATHLEN];
+        LoadedFileInfo loadedFileInfo = MachOAnalyzer::load(diag, fileSystem, file.c_str(), archs, platform, fileRealPath);
+        if (diag.hasError()) {
+            fprintf(stderr, "Could not load file '%s' because: %s\n", file.c_str(), diag.errorMessage().c_str());
+            diag.clearError();
+            return 1;
+        }
+
+        MachOAnalyzer* ma = (MachOAnalyzer*)loadedFileInfo.fileContent;
+        if (ma == nullptr) {
+            fprintf(stderr, "Could not load file: %s\n", file.c_str());
+            return 1;
+        }
+
+        auto errorHandler = ^(const char* msg) {
+            diag.warning("File '%s' cannot be placed in kernel collection because: %s", file.c_str(), msg);
+        };
+        if (ma->canBePlacedInKernelCollection(file.c_str(), errorHandler)) {
+            return 0;
+        } else {
+            fileSystem.unloadFile(loadedFileInfo);
+        }
+    }
+
+    {
+        // Since we found no files, print warnings for the ones we tried
+        if (diag.warnings().empty()) {
+            fprintf(stderr, "File '%s' was not valid for app-cache\n", file.c_str());
+        } else {
+            for (const std::string& msg : diag.warnings()) {
+                fprintf(stderr, "  %s\n", msg.c_str());
+            }
+        }
+        return 1;
+    }
+
+    return 0;
+}
+
+static void forEachBundle(const char* bundlesDirectoryPath,
+                          void (^callback)(CFBundleRef bundleRef, const char* bundleName)) {
+    CFStringRef sourcePath = CFStringCreateWithCStringNoCopy(kCFAllocatorDefault, bundlesDirectoryPath,
+                                                             kCFStringEncodingASCII, kCFAllocatorNull);
+    CFURLRef sourceURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, sourcePath,
+                                                       kCFURLPOSIXPathStyle, true);
+    CFArrayRef bundles = CFBundleCreateBundlesFromDirectory(kCFAllocatorDefault, sourceURL, nullptr);
+
+    for (CFIndex i = 0, e = CFArrayGetCount(bundles); i != e; ++i) {
+        CFBundleRef bundleRef = (CFBundleRef)CFArrayGetValueAtIndex(bundles, i);
+        CFStringRef bundleID = CFBundleGetIdentifier(bundleRef);
+        if (!bundleID)
+            continue;
+        const char* bundleName = CFStringGetCStringPtr(bundleID, kCFStringEncodingASCII);
+        callback(bundleRef, bundleName);
+    }
+
+    CFRelease(sourcePath);
+    CFRelease(sourceURL);
+    CFRelease(bundles);
+}
+
+static int listBundles(const ListBundlesOptions& options) {
+    // Verify any required options
+    if (options.directoryPath == nullptr)
+        exit_usage();
+
+    forEachBundle(options.directoryPath, ^(CFBundleRef bundleRef, const char* bundleName){
+        printf("Bundle: %s\n", bundleName);
+    });
+
+    return 0;
+}
+
+static CFDataRef
+createKernelCollectionForArch(const CreateKernelCollectionOptions& options, const char* arch,
+                              Diagnostics& diag) {
+    const GradedArchs& archs = GradedArchs::forName(arch);
+    Platform platform = Platform::unknown;
+
+
+    KernelCollectionBuilder* kcb = nullptr;
+    {
+        CFStringRef archStringRef = CFStringCreateWithCString(kCFAllocatorDefault, arch, kCFStringEncodingASCII);
+        BuildOptions_v1 buildOptions = { 1, options.collectionKind, options.stripMode, archStringRef, options.verbose };
+        kcb = createKernelCollectionBuilder(&buildOptions);
+        CFRelease(archStringRef);
+    }
+
+    FileSystemPhysical fileSystem;
+    LoadedFileInfo kernelCollectionLoadedFileInfo;
+
+    auto loadKernelCollection = ^(const char* kernelCollectionPath, CollectionKind collectionKind) {
+        if (!fileSystem.fileExists(kernelCollectionPath)) {
+            fprintf(stderr, "kernel collection path does not exist: %s\n", options.kernelPath);
+            return false;
+        }
+        LoadedFileInfo info;
+        char realerPath[MAXPATHLEN];
+        bool loadedFile = fileSystem.loadFile(kernelCollectionPath, info, realerPath, ^(const char *format, ...) {
+            va_list list;
+            va_start(list, format);
+            diag.error(format, list);
+            va_end(list);
+        });
+        if ( !loadedFile )
+            return false;
+        CFStringRef pathStringRef = CFStringCreateWithCString(kCFAllocatorDefault, kernelCollectionPath, kCFStringEncodingASCII);
+        CFDataRef dataRef = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, (const uint8_t*)info.fileContent, info.fileContentLen, kCFAllocatorNull);
+        if ( !addCollectionFile(kcb, pathStringRef, dataRef, collectionKind) ) {
+            diag.error("Could not load kernel collection file");
+            return false;
+        }
+        CFRelease(dataRef);
+        CFRelease(pathStringRef);
+        return true;
+    };
+
+    switch (options.collectionKind) {
+        case unknownKC:
+            fprintf(stderr, "Invalid kernel collection kind\n");
+            exit(1);
+        case baseKC: {
+            if (!fileSystem.fileExists(options.kernelPath)) {
+                fprintf(stderr, "Kernel path does not exist: %s\n", options.kernelPath);
+                return {};
+            }
+            LoadedFileInfo info;
+            char realerPath[MAXPATHLEN];
+            bool loadedFile = fileSystem.loadFile(options.kernelPath, info, realerPath, ^(const char *format, ...) {
+                va_list list;
+                va_start(list, format);
+                diag.error(format, list);
+                va_end(list);
+            });
+            if ( !loadedFile )
+                return {};
+            CFStringRef pathStringRef = CFStringCreateWithCString(kCFAllocatorDefault, options.kernelPath, kCFStringEncodingASCII);
+            CFDataRef dataRef = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, (const uint8_t*)info.fileContent, info.fileContentLen, kCFAllocatorNull);
+            if ( !addKernelFile(kcb, pathStringRef, dataRef) ) {
+                uint64_t errorCount = 0;
+                const char* const* errors = getErrors(kcb, &errorCount);
+                for (uint64_t i = 0; i != errorCount; ++i)
+                    diag.error("Could not load kernel file because: '%s'", errors[i]);
+                return {};
+            }
+            CFRelease(dataRef);
+            CFRelease(pathStringRef);
+            break;
+        }
+        case pageableKC:
+            if ( !loadKernelCollection(options.kernelCollectionPath, baseKC) )
+                return {};
+            break;
+        case auxKC:
+            if ( !loadKernelCollection(options.kernelCollectionPath, baseKC) )
+                return {};
+            // Pageable is optional
+            if ( options.pageableCollectionPath != nullptr ) {
+                if ( !loadKernelCollection(options.pageableCollectionPath, pageableKC) )
+                    return {};
+            }
+            break;
+    }
+
+    if ( !options.bundleIDs.empty() ) {
+        struct BundleData {
+            std::string                 executablePath;
+            std::string                 bundlePath;
+            std::vector<std::string>    dependencies;
+            CFDictionaryRef             infoPlist       = nullptr;
+        };
+        __block std::map<std::string, BundleData> foundBundles;
+
+        // Look for bundles in the extensions directory and any PlugIns directories its kext's contain
+        __block std::list<std::pair<std::string, bool>> kextDirectoriesToProcess;
+        kextDirectoriesToProcess.push_back({ options.extensionsPath, true });
+        while ( !kextDirectoriesToProcess.empty() ) {
+            std::string kextDir = kextDirectoriesToProcess.front().first;
+            bool lookForPlugins = kextDirectoriesToProcess.front().second;
+            kextDirectoriesToProcess.pop_front();
+
+            __block bool foundError = false;
+            forEachBundle(kextDir.c_str(), ^(CFBundleRef bundleRef, const char* bundleName) {
+
+                if (foundError)
+                    return;
+
+                // If the directory contains a PlugIns directory, then add it to the list to seach for kexts
+                if (lookForPlugins) {
+                    CFURLRef pluginsRelativeURL = CFBundleCopyBuiltInPlugInsURL(bundleRef);
+                    if ( pluginsRelativeURL != nullptr ) {
+                        CFURLRef pluginsAbsoluteURL = CFURLCopyAbsoluteURL(pluginsRelativeURL);
+                        CFStringRef pluginString = CFURLCopyFileSystemPath(pluginsAbsoluteURL, kCFURLPOSIXPathStyle);
+                        const char* pluginPath = CFStringGetCStringPtr(pluginString, kCFStringEncodingASCII);
+                        kextDirectoriesToProcess.push_back({ pluginPath, false });
+
+                        CFRelease(pluginString);
+                        CFRelease(pluginsAbsoluteURL);
+                        CFRelease(pluginsRelativeURL);
+                    }
+                }
+
+#if 0
+                // For now always load bundles as we don't require every bundle to be listed on the command line
+                // but can instead bring them in on demand.
+
+                // Once we've looked for plugins, if we don't want this bundle then we can skip validating it.
+                if ( foundBundles.count(bundleName) == 0 )
+                    return;
+#endif
+
+                BundleData bundleData;
+                bundleData.infoPlist = CFBundleGetInfoDictionary(bundleRef);
+
+                CFURLRef bundleExecutableRelativeURL = CFBundleCopyExecutableURL(bundleRef);
+
+                // Its ok to be missing an executable.  We'll just skip this bundle
+                if ( bundleExecutableRelativeURL == nullptr ) {
+                    // FIXME: Its possibly not ok to be missing the executable if its actually listed
+                    // as a CFBundleExecutable path in the plist
+                    foundBundles[bundleName] = bundleData;
+                    return;
+                }
+
+                CFURLRef bundleExecutableAbsoluteURL = CFURLCopyAbsoluteURL(bundleExecutableRelativeURL);
+                CFStringRef bundleExecutableString = CFURLCopyFileSystemPath(bundleExecutableAbsoluteURL, kCFURLPOSIXPathStyle);
+                const char* bundleExecutablePath = CFStringGetCStringPtr(bundleExecutableString, kCFStringEncodingASCII);
+
+                // Check for an arch specific dependency list first
+                std::string archBundleLibraries = std::string("OSBundleLibraries") + "_" + arch;
+                CFStringRef archBundleLibrariesStringRef = CFStringCreateWithCStringNoCopy(kCFAllocatorDefault, archBundleLibraries.c_str(),
+                                                                                           kCFStringEncodingASCII, kCFAllocatorNull);
+                CFTypeRef depsRef = CFBundleGetValueForInfoDictionaryKey(bundleRef, archBundleLibrariesStringRef);
+                if ( depsRef == nullptr ) {
+                    // No arch specific deps, so try the defaults
+                    depsRef = CFBundleGetValueForInfoDictionaryKey(bundleRef, CFSTR("OSBundleLibraries"));
+                }
+                if (depsRef != nullptr) {
+                    if (CFGetTypeID(depsRef) != CFDictionaryGetTypeID()) {
+                        fprintf(stderr, "Bad bundle '%s' (\"OSBundleLibraries\" is not a dictionary)\n", bundleName);
+                        foundError = true;
+                        return;
+                    }
+
+                    CFDictionaryRef dictRef = (CFDictionaryRef)depsRef;
+                    CFDictionaryApplyFunction(dictRef, [](const void *key, const void *value, void *context) {
+                        BundleData* bundleData = (BundleData*)context;
+                        CFStringRef keyRef = (CFStringRef)key;
+                        //CFStringRef valueRef = (CFStringRef)value;
+                        bundleData->dependencies.push_back(CFStringGetCStringPtr(keyRef, kCFStringEncodingASCII));
+                    }, &bundleData);
+                }
+
+                // Make sure no-one tries to link the kernel directly.  They must do so via symbol sets
+                if ( !bundleData.dependencies.empty() ) {
+                    for (const std::string& dep : bundleData.dependencies) {
+                        if (dep == "com.apple.kernel") {
+                            fprintf(stderr, "Rejecting bundle '%s' as it is trying to link directly to the kernel\n",
+                                    bundleName);
+                            foundError = true;
+                            return;
+                        }
+                    }
+                }
+
+                bundleData.executablePath = bundleExecutablePath;
+
+                CFURLRef bundleURLRef = CFBundleCopyBundleURL(bundleRef);
+                CFStringRef bundleURLString = CFURLCopyFileSystemPath(bundleURLRef, kCFURLPOSIXPathStyle);
+                const char* bundleURLPath = CFStringGetCStringPtr(bundleURLString, kCFStringEncodingASCII);
+                if (strncmp(bundleURLPath, options.extensionsPath, strlen(options.extensionsPath)) != 0) {
+                    fprintf(stderr, "Bundle path '%s' is not within extensions directory '%s'\n",
+                            bundleURLPath, options.extensionsPath);
+                }
+                // Don't remove the whole extensions prefix, but instead the volume root, if we have one
+                bundleData.bundlePath = bundleURLPath + strlen(options.volumeRoot);
+                foundBundles[bundleName] = bundleData;
+
+                CFRelease(bundleExecutableString);
+                CFRelease(bundleExecutableAbsoluteURL);
+                CFRelease(bundleExecutableRelativeURL);
+            });
+
+            if (foundError)
+                return {};
+        }
+
+        __block std::set<std::string> existingBundles;
+        auto addSymbolSetsBundleIDs = ^(const dyld3::MachOAnalyzer* kernelMA) {
+            assert(kernelMA != nullptr);
+
+            __block std::list<std::string> nonASCIIStrings;
+            auto getString = ^(Diagnostics& diags, CFStringRef symbolNameRef) {
+                const char* symbolName = CFStringGetCStringPtr(symbolNameRef, kCFStringEncodingUTF8);
+                if ( symbolName != nullptr )
+                    return symbolName;
+
+                CFIndex len = CFStringGetMaximumSizeForEncoding(CFStringGetLength(symbolNameRef), kCFStringEncodingUTF8);
+                char buffer[len + 1];
+                if ( !CFStringGetCString(symbolNameRef, buffer, len, kCFStringEncodingUTF8) ) {
+                    diags.error("Could not convert string to ASCII");
+                    return (const char*)nullptr;
+                }
+                buffer[len] = '\0';
+                nonASCIIStrings.push_back(buffer);
+                return nonASCIIStrings.back().c_str();
+            };
+
+            uint64_t symbolSetsSize = 0;
+            const void* symbolSetsContent = kernelMA->findSectionContent("__LINKINFO", "__symbolsets", symbolSetsSize);
+            if ( symbolSetsContent != nullptr ) {
+
+                // A helper to automatically call CFRelease when we go out of scope
+                struct AutoReleaseTypeRef {
+                    AutoReleaseTypeRef() = default;
+                    ~AutoReleaseTypeRef() {
+                        if ( ref != nullptr ) {
+                            CFRelease(ref);
+                        }
+                    }
+                    void setRef(CFTypeRef typeRef) {
+                        assert(ref == nullptr);
+                        ref = typeRef;
+                    }
+
+                    CFTypeRef ref = nullptr;
+                };
+
+                AutoReleaseTypeRef dataRefReleaser;
+                AutoReleaseTypeRef plistRefReleaser;
+
+                CFDataRef dataRef = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, (const uint8_t*)symbolSetsContent, symbolSetsSize, kCFAllocatorNull);
+                if ( dataRef == nullptr ) {
+                    diag.error("Could not create data ref for symbol sets");
+                    return false;
+                }
+                dataRefReleaser.setRef(dataRef);
+
+                CFErrorRef errorRef = nullptr;
+                CFPropertyListRef plistRef = CFPropertyListCreateWithData(kCFAllocatorDefault, dataRef, kCFPropertyListImmutable, nullptr, &errorRef);
+                if (errorRef != nullptr) {
+                    CFStringRef errorString = CFErrorCopyDescription(errorRef);
+                    diag.error("Could not load plist because :%s",
+                               CFStringGetCStringPtr(errorString, kCFStringEncodingASCII));
+                    CFRelease(errorRef);
+                    return false;
+                }
+                if ( plistRef == nullptr ) {
+                    diag.error("Could not create plist ref for symbol sets");
+                    return false;
+                }
+                plistRefReleaser.setRef(plistRef);
+
+                if ( CFGetTypeID(plistRef) != CFDictionaryGetTypeID() ) {
+                    diag.error("Symbol set plist should be a dictionary");
+                    return false;
+                }
+                CFDictionaryRef symbolSetsDictRef = (CFDictionaryRef)plistRef;
+                CFArrayRef symbolSetArrayRef = (CFArrayRef)CFDictionaryGetValue(symbolSetsDictRef, CFSTR("SymbolsSets"));
+                if ( symbolSetArrayRef != nullptr ) {
+                    if ( CFGetTypeID(symbolSetArrayRef) != CFArrayGetTypeID() ) {
+                        diag.error("SymbolsSets value should be an array");
+                        return false;
+                    }
+                    for (CFIndex symbolSetIndex = 0; symbolSetIndex != CFArrayGetCount(symbolSetArrayRef); ++symbolSetIndex) {
+                        CFDictionaryRef symbolSetDictRef = (CFDictionaryRef)CFArrayGetValueAtIndex(symbolSetArrayRef, symbolSetIndex);
+                        if ( CFGetTypeID(symbolSetDictRef) != CFDictionaryGetTypeID() ) {
+                            diag.error("Symbol set element should be a dictionary");
+                            return false;
+                        }
+
+                        // CFBundleIdentifier
+                        CFStringRef bundleIDRef = (CFStringRef)CFDictionaryGetValue(symbolSetDictRef, CFSTR("CFBundleIdentifier"));
+                        if ( (bundleIDRef == nullptr) || (CFGetTypeID(bundleIDRef) != CFStringGetTypeID()) ) {
+                            diag.error("Symbol set bundle ID should be a string");
+                            return false;
+                        }
+
+                        const char* dylibID = getString(diag, bundleIDRef);
+                        if ( dylibID == nullptr )
+                            return false;
+                        existingBundles.insert(dylibID);
+                    }
+                }
+            }
+            return true;
+        };
+
+        auto addExistingBundleIDs = ^(const char* path, bool isBaseKC) {
+            char fileRealPath[MAXPATHLEN];
+            auto kernelCollectionLoadedFileInfo = MachOAnalyzer::load(diag, fileSystem, path, archs, platform, fileRealPath);
+            if ( diag.hasError() ) {
+                fprintf(stderr, "Could not load file '%s' because: %s\n", path, diag.errorMessage().c_str());
+                return false;
+            }
+            const MachOAppCache* appCacheMA = (const MachOAppCache*)kernelCollectionLoadedFileInfo.fileContent;
+            if (appCacheMA == nullptr) {
+                fprintf(stderr, "Could not load file: %s\n", path);
+                return false;
+            }
+            if ( !appCacheMA->isFileSet() ) {
+                fprintf(stderr, "kernel collection is not a cache file: %s\n", path);
+                return false;
+            }
+            __block const dyld3::MachOAnalyzer* kernelMA = nullptr;
+            appCacheMA->forEachDylib(diag, ^(const MachOAnalyzer *ma, const char *name, bool &stop) {
+                if ( ma->isStaticExecutable() ) {
+                    kernelMA = ma;
+                }
+                existingBundles.insert(name);
+            });
+
+            if ( isBaseKC ) {
+                if ( !addSymbolSetsBundleIDs(kernelMA) )
+                    return false;
+            }
+            fileSystem.unloadFile(kernelCollectionLoadedFileInfo);
+            return true;
+        };
+        if ( options.collectionKind == baseKC ) {
+            char fileRealPath[MAXPATHLEN];
+            auto kernelLoadedFileInfo = MachOAnalyzer::load(diag, fileSystem, options.kernelPath, archs, platform, fileRealPath);
+            if ( diag.hasError() ) {
+                fprintf(stderr, "Could not load file '%s' because: %s\n", options.kernelPath, diag.errorMessage().c_str());
+                return {};
+            }
+            const MachOAppCache* kernelMA = (const MachOAppCache*)kernelLoadedFileInfo.fileContent;
+            if (kernelMA == nullptr) {
+                fprintf(stderr, "Could not load file: %s\n", options.kernelPath);
+                return {};
+            }
+            if ( !kernelMA->isStaticExecutable() ) {
+                fprintf(stderr, "kernel is not a static executable: %s\n", options.kernelPath);
+                return {};
+            }
+
+            if ( !addSymbolSetsBundleIDs(kernelMA) )
+                return {};
+            fileSystem.unloadFile(kernelLoadedFileInfo);
+        }
+        if ( (options.collectionKind == auxKC) || (options.collectionKind == pageableKC) ) {
+            // Work out which bundle-ids are already in the base KC
+            if ( !addExistingBundleIDs(options.kernelCollectionPath, true) )
+                return {};
+        }
+        if ( options.pageableCollectionPath != nullptr ) {
+            // Work out which bundle-ids are already in the pageable KC
+            if ( !addExistingBundleIDs(options.pageableCollectionPath, false) )
+                return {};
+        }
+
+        std::set<std::string> processedBundleIDs;
+        std::list<const char*> bundleIDsToLoad;
+        bundleIDsToLoad.insert(bundleIDsToLoad.end(), options.bundleIDs.begin(), options.bundleIDs.end());
+        while (!bundleIDsToLoad.empty()) {
+            std::string bundleID = bundleIDsToLoad.front();
+            bundleIDsToLoad.pop_front();
+
+            std::string stripModeString;
+            if ( const char* colonPos = strchr(bundleID.c_str(), ':') ) {
+                stripModeString = colonPos + 1;
+                bundleID.erase(colonPos - bundleID.data());
+            }
+
+            // If we've seen this one already then skip it
+            if (!processedBundleIDs.insert(bundleID).second)
+                continue;
+
+            // Find the bundle for this ID
+            auto it = foundBundles.find(bundleID);
+            if (it == foundBundles.end()) {
+                fprintf(stderr, "[WARNING]: Could not find bundle with ID '%s'\n", bundleID.c_str());
+                continue;
+            }
+
+            BundleData& bundleData = it->second;
+
+            LoadedFileInfo info;
+
+            // Codeless kexts don't have an executable path, but we still want to put their
+            // plist in the prelink info
+            bool isCodeless = bundleData.executablePath.empty();
+            if ( !isCodeless ) {
+                char realerPath[MAXPATHLEN];
+                bool loadedFile = fileSystem.loadFile(bundleData.executablePath.c_str(), info, realerPath,
+                                                      ^(const char *format, ...) {
+                    va_list list;
+                    va_start(list, format);
+                    diag.error(format, list);
+                    va_end(list);
+                });
+                if ( !loadedFile )
+                    return {};
+            }
+
+            std::vector<const char*> deps;
+            for (const std::string& dependency : bundleData.dependencies)
+                deps.push_back(dependency.c_str());
+
+            CFStringRef         kextPathStringRef   = nullptr;
+            CFDataRef           kextDataRef         = nullptr;
+            if ( !isCodeless) {
+                kextPathStringRef = CFStringCreateWithCString(kCFAllocatorDefault, bundleData.executablePath.c_str(), kCFStringEncodingASCII);
+                kextDataRef = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, (const uint8_t*)info.fileContent, info.fileContentLen, kCFAllocatorNull);
+            }
+
+            CFMutableArrayRef   kextDepsArrayRef = CFArrayCreateMutable(kCFAllocatorDefault, bundleData.dependencies.size(), &kCFTypeArrayCallBacks);
+            for (const std::string& dependency : bundleData.dependencies) {
+                CFStringRef depStringRef = CFStringCreateWithCString(kCFAllocatorDefault, dependency.c_str(), kCFStringEncodingASCII);
+                CFArrayAppendValue(kextDepsArrayRef, depStringRef);
+                CFRelease(depStringRef);
+            }
+            CFStringRef         kextBundleIDStringRef = CFStringCreateWithCString(kCFAllocatorDefault, bundleID.c_str(), kCFStringEncodingASCII);
+            CFStringRef         kextBundlePathStringRef = CFStringCreateWithCString(kCFAllocatorDefault, bundleData.bundlePath.c_str(), kCFStringEncodingASCII);
+
+            BinaryStripMode stripMode = binaryStripNone;
+            if ( !stripModeString.empty() ) {
+                if ( stripModeString == "locals" ) {
+                    stripMode = binaryStripLocals;
+                } else if ( stripModeString == "exports" ) {
+                    stripMode = binaryStripExports;
+                } else if ( stripModeString == "all" ) {
+                    stripMode = binaryStripAll;
+                } else {
+                    diag.error("Unknown strip mode: '%s'", stripModeString.c_str());
+                    return {};
+                }
+            }
+
+            KextFileData_v1 fileData = { 1, kextPathStringRef, kextDataRef,
+                                         kextDepsArrayRef, kextBundleIDStringRef, kextBundlePathStringRef,
+                                         bundleData.infoPlist, stripMode };
+
+            if ( !addKextDataFile(kcb, &fileData) ) {
+                uint64_t errorCount = 0;
+                const char* const* errors = getErrors(kcb, &errorCount);
+                for (uint64_t i = 0; i != errorCount; ++i)
+                    diag.error("Could not load kext file because: '%s'", errors[i]);
+                return {};
+            }
+
+            // Walk the dependencies and add any new ones to the list
+            for (const std::string& dependency : bundleData.dependencies) {
+                if ( existingBundles.find(dependency) == existingBundles.end() )
+                    bundleIDsToLoad.push_back(dependency.c_str());
+            }
+        }
+
+        // Filter dependencies to kext's with binaries
+#if 0
+        const std::map<std::string, BundleData>* foundBundlesPtr = &foundBundles;
+        for (AppCacheBuilder::InputDylib& file : loadedFiles) {
+            file.dylibDeps.erase(std::remove_if(file.dylibDeps.begin(), file.dylibDeps.end(),
+                                                [&](const std::string& depName) {
+                auto it = foundBundlesPtr->find(depName);
+                assert(it != foundBundlesPtr->end());
+                return it->second.executablePath.empty();
+            }),file.dylibDeps.end());
+        }
+#endif
+    }
+
+#if 0
+    for (AppCacheBuilder::InputDylib& file : loadedFiles) {
+        char fileRealPath[MAXPATHLEN];
+        const char* path = file.dylib.loadedFileInfo.path;
+        LoadedFileInfo loadedFileInfo = MachOAnalyzer::load(diag, fileSystem, path, archs, platform, fileRealPath);
+        if ( diag.hasError() ) {
+            fprintf(stderr, "Could not load file '%s' because: %s\n", path, diag.errorMessage().c_str());
+            return {};
+        }
+
+        MachOAnalyzer* ma = (MachOAnalyzer*)loadedFileInfo.fileContent;
+        if (ma == nullptr) {
+            fprintf(stderr, "Could not load file: %s\n", path);
+            return {};
+        }
+
+        auto errorHandler = ^(const char* msg) {
+            diag.error("Binary located at '%s' cannot be placed in kernel collection because: %s", path, msg);
+        };
+        if (ma->canBePlacedInKernelCollection(path, errorHandler)) {
+            DyldSharedCache::MappedMachO mappedFile(path, ma, loadedFileInfo.sliceLen, false, false,
+                                                    loadedFileInfo.sliceOffset, loadedFileInfo.mtime,
+                                                    loadedFileInfo.inode);
+            CacheBuilder::LoadedMachO loadedMachO = { mappedFile, loadedFileInfo, nullptr };
+            file.dylib = loadedMachO;
+        } else {
+            fileSystem.unloadFile(loadedFileInfo);
+        }
+        if ( diag.hasError() ) {
+            fprintf(stderr, "%s\n", diag.errorMessage().c_str());
+            return {};
+        }
+    }
+#endif
+
+#if 0
+    if (loadedFiles.empty()) {
+        fprintf(stderr, "Could not find any valid files to create kernel collection\n");
+
+        // Since we found no files, print warnings for the ones we tried
+        if (!diag.warnings().empty()) {
+            fprintf(stderr, "Failed to use the following files:\n");
+            for (const std::string& msg : diag.warnings()) {
+                fprintf(stderr, "  %s\n", msg.c_str());
+            }
+        }
+        return {};
+    }
+
+    if (options.verbose) {
+        for (const AppCacheBuilder::InputDylib& loadedFile : loadedFiles)
+            fprintf(stderr, "Building cache with file: %s\n", loadedFile.dylib.loadedFileInfo.path);
+    }
+#endif
+
+    for (const SectionData& sectData : options.sections) {
+        CFStringRef segmentName   = CFStringCreateWithCString(kCFAllocatorDefault, sectData.segmentName, kCFStringEncodingASCII);
+        CFStringRef sectionName   = nullptr;
+        if ( sectData.sectionName != nullptr )
+            sectionName = CFStringCreateWithCString(kCFAllocatorDefault, sectData.sectionName, kCFStringEncodingASCII);
+
+        CFDataRef sectionData = nullptr;
+        {
+            struct stat stat_buf;
+            int fd = ::open(sectData.payloadFilePath, O_RDONLY, 0);
+            if (fd == -1) {
+                diag.error("can't open file '%s', errno=%d\n", sectData.payloadFilePath, errno);
+                return {};
+            }
+
+            if (fstat(fd, &stat_buf) == -1) {
+                diag.error("can't stat open file '%s', errno=%d\n", sectData.payloadFilePath, errno);
+                ::close(fd);
+                return {};
+            }
+
+            const void* buffer = mmap(NULL, (size_t)stat_buf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+            if (buffer == MAP_FAILED) {
+                diag.error("mmap() for file at %s failed, errno=%d\n", sectData.payloadFilePath, errno);
+                ::close(fd);
+                return {};
+            }
+            ::close(fd);
+
+            sectionData = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, (const uint8_t*)buffer, stat_buf.st_size, kCFAllocatorNull);
+        }
+
+        if ( !addSegmentData(kcb, segmentName, sectionName, sectionData) ) {
+            uint64_t errorCount = 0;
+            const char* const* errors = getErrors(kcb, &errorCount);
+            for (uint64_t i = 0; i != errorCount; ++i)
+                diag.error("Could not load section data file because: '%s'", errors[i]);
+            return {};
+        }
+    }
+
+    if ( options.prelinkInfoExtraData != nullptr ) {
+        struct stat stat_buf;
+        int fd = ::open(options.prelinkInfoExtraData, O_RDONLY, 0);
+        if (fd == -1) {
+            diag.error("can't open file '%s', errno=%d\n", options.prelinkInfoExtraData, errno);
+            return {};
+        }
+
+        if (fstat(fd, &stat_buf) == -1) {
+            diag.error("can't stat open file '%s', errno=%d\n", options.prelinkInfoExtraData, errno);
+            ::close(fd);
+            return {};
+        }
+
+        const void* buffer = mmap(NULL, (size_t)stat_buf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+        if (buffer == MAP_FAILED) {
+            diag.error("mmap() for file at %s failed, errno=%d\n", options.prelinkInfoExtraData, errno);
+            ::close(fd);
+            return {};
+        }
+        ::close(fd);
+
+        CFDataRef prelinkInfoData = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, (const uint8_t*)buffer, stat_buf.st_size, kCFAllocatorNull);
+
+        CFErrorRef errorRef = nullptr;
+        CFPropertyListRef plistRef = CFPropertyListCreateWithData(kCFAllocatorDefault, prelinkInfoData, kCFPropertyListImmutable, nullptr, &errorRef);
+        if (errorRef != nullptr) {
+            CFStringRef errorString = CFErrorCopyDescription(errorRef);
+            diag.error("Could not load prelink info plist because :%s",
+                       CFStringGetCStringPtr(errorString, kCFStringEncodingASCII));
+            CFRelease(errorRef);
+            return {};
+        }
+        if ( plistRef == nullptr ) {
+            diag.error("Could not create plist ref for prelink info");
+            return {};
+        }
+        if ( CFGetTypeID(plistRef) != CFDictionaryGetTypeID() ) {
+            diag.error("Prelink info plist should be a dictionary");
+            return {};
+        }
+
+        if ( !addPrelinkInfo(kcb, (CFDictionaryRef)plistRef) ) {
+            uint64_t errorCount = 0;
+            const char* const* errors = getErrors(kcb, &errorCount);
+            for (uint64_t i = 0; i != errorCount; ++i)
+                diag.error("Could not prelink data file because: '%s'", errors[i]);
+            return {};
+        }
+    }
+
+    bool success = runKernelCollectionBuilder(kcb);
+    uint64_t errorCount = 0;
+    const char* const* errors = getErrors(kcb, &errorCount);
+    if ( errors != nullptr ) {
+        if ( !options.printJSONErrors ) {
+            for (uint64_t i = 0; i != errorCount; ++i) {
+                fprintf(stderr, "Could not build kernel collection because '%s'\n", errors[i]);
+            }
+        }
+        CFDictionaryRef errorDictRef = getKextErrors(kcb);
+        if ( errorDictRef != nullptr ) {
+            Node rootNode;
+
+            CFDictionaryApplyFunction(errorDictRef, [](const void *key, const void *value, void *context) {
+                Node* rootNode = (Node*)context;
+                CFStringRef keyRef = (CFStringRef)key;
+                CFArrayRef valueRef = (CFArrayRef)value;
+
+                Node bundleNode;
+                bundleNode.map["id"] = Node(CFStringGetCStringPtr(keyRef, kCFStringEncodingASCII));
+
+                Node errorsNode;
+                CFArrayApplyFunction(valueRef, CFRangeMake(0, CFArrayGetCount(valueRef)), [](const void *value, void *context) {
+                    Node* errorsNode = (Node*)context;
+                    CFStringRef valueRef = (CFStringRef)value;
+
+                    errorsNode->array.push_back(Node(CFStringGetCStringPtr(valueRef, kCFStringEncodingASCII)));
+                }, &errorsNode);
+
+                bundleNode.map["errors"] = errorsNode;
+
+                rootNode->array.push_back(bundleNode);
+            }, &rootNode);
+
+            // sort the nodes so that the output is reproducible
+            std::sort(rootNode.array.begin(), rootNode.array.end(),
+                      [](const Node& a, const Node&b) {
+                return a.map.find("id")->second.value < b.map.find("id")->second.value;
+            });
+
+            printJSON(rootNode);
+        } else {
+            Node rootNode;
+            for (uint64_t i = 0; i != errorCount; ++i) {
+                rootNode.array.push_back(Node(errors[i]));
+            }
+            printJSON(rootNode);
+        }
+        return {};
+    }
+
+    if ( !success )
+        return {};
+
+    uint64_t fileResultCount = 0;
+    const auto* fileResults = getCollectionFileResults(kcb, &fileResultCount);
+    if ( fileResults == nullptr ) {
+        fprintf(stderr, "Could not get file results\n");
+        return {};
+    }
+    if ( fileResultCount != 1 ) {
+        fprintf(stderr, "Unsupported file result count: %lld\n", fileResultCount);
+        return {};
+    }
+
+    CFDataRef dataRef = fileResults[0]->data;
+    CFRetain(dataRef);
+
+    destroyKernelCollectionBuilder(kcb);
+
+    return dataRef;
+}
+
+static int createKernelCollection(const CreateKernelCollectionOptions& options) {
+    // Verify any required options
+    if (gOpts.archs.empty()) {
+        exit_usage("-arch");
+    } else {
+        std::set<std::string_view> archs(gOpts.archs.begin(), gOpts.archs.end());
+        if (archs.size() != gOpts.archs.size()) {
+            fprintf(stderr, "Duplicate -arch specified\n");
+            exit(1);
+        }
+    }
+    if (options.outputCachePath == nullptr)
+        exit_usage();
+
+    switch (options.stripMode) {
+        case unknownStripMode:
+        case stripNone:
+            break;
+        case stripAll:
+        case stripAllKexts:
+            if ( options.collectionKind != baseKC ) {
+                fprintf(stderr, "Cannot use -strip-all-kexts with auxKC.  Use strip-all instead\n");
+                exit(1);
+            }
+            break;
+    }
+
+    switch (options.collectionKind) {
+        case unknownKC:
+            fprintf(stderr, "Invalid kernel collection kind\n");
+            exit(1);
+        case baseKC:
+            if (options.kernelPath == nullptr)
+                exit_usage("-kernel");
+            break;
+        case pageableKC:
+        case auxKC:
+            if (options.kernelCollectionPath == nullptr)
+                exit_usage("-kernel-collection");
+            break;
+    }
+
+    if ( !options.bundleIDs.empty() ) {
+        if (options.extensionsPath == nullptr)
+            exit_usage("-extensions");
+    }
+
+    // Volume root should be a prefix of extensions path
+    if ( options.extensionsPath != nullptr ) {
+        if ( strncmp(options.extensionsPath, options.volumeRoot, strlen(options.volumeRoot)) != 0 ) {
+            fprintf(stderr, "Volume root '%s' is not a prefix of extensions path '%s'\n",
+                    options.volumeRoot, options.extensionsPath);
+        }
+    }
+
+    std::vector<CFDataRef> buffers;
+    for (const char* arch : gOpts.archs) {
+        Diagnostics diag;
+        CFDataRef bufferRef = createKernelCollectionForArch(options, arch, diag);
+        if ( diag.hasError() ) {
+            fprintf(stderr, "%s\n", diag.errorMessage().c_str());
+            return 1;
+        }
+        if ( bufferRef == nullptr ) {
+            // If we  want errors then return 0
+            if ( options.printJSONErrors )
+                return 0;
+            return 1;
+        }
+        buffers.push_back(bufferRef);
+    }
+
+    if (buffers.size() == 1) {
+        // Single arch.  Just write the file directly
+        CFDataRef bufferRef = buffers.front();
+        if ( !safeSave(CFDataGetBytePtr(bufferRef), CFDataGetLength(bufferRef), options.outputCachePath) ) {
+            fprintf(stderr, "Could not write app cache\n");
+            return 1;
+        }
+        CFRelease(bufferRef);
+    } else {
+        // Multiple buffers.  Create a FAT file
+        std::vector<uint8_t> fatBuffer;
+
+        // Add the FAT header to the start of the buffer
+        fatBuffer.resize(0x4000, 0);
+        fat_header* header = (fat_header*)&fatBuffer.front();
+        header->magic       = OSSwapHostToBigInt32(FAT_MAGIC);
+        header->nfat_arch   = OSSwapHostToBigInt32((uint32_t)buffers.size());
+
+        for (uint32_t i = 0; i != buffers.size(); ++i) {
+            CFDataRef bufferRef = buffers[i];
+            mach_header* mh = (mach_header*)CFDataGetBytePtr(bufferRef);
+
+            uint32_t offsetInBuffer = (uint32_t)fatBuffer.size();
+
+            fat_arch* archBuffer = (fat_arch*)(&fatBuffer.front() + sizeof(fat_header));
+            archBuffer[i].cputype       = OSSwapHostToBigInt32(mh->cputype);
+            archBuffer[i].cpusubtype    = OSSwapHostToBigInt32(mh->cpusubtype);
+            archBuffer[i].offset        = OSSwapHostToBigInt32(offsetInBuffer);
+            archBuffer[i].size          = OSSwapHostToBigInt32((uint32_t)CFDataGetLength(bufferRef));
+            archBuffer[i].align         = OSSwapHostToBigInt32(14);
+
+            auto align = [](uint64_t addr, uint8_t p2) {
+                uint64_t mask = (1 << p2);
+                return (addr + mask - 1) & (-mask);
+            };
+
+            uint32_t alignedSize = (uint32_t)align((uint32_t)CFDataGetLength(bufferRef), 14);
+            fatBuffer.resize(fatBuffer.size() + alignedSize, 0);
+            memcpy(&fatBuffer.front() + offsetInBuffer, CFDataGetBytePtr(bufferRef), CFDataGetLength(bufferRef));
+        }
+
+        if ( !safeSave(&fatBuffer.front(), fatBuffer.size(), options.outputCachePath) ) {
+            fprintf(stderr, "Could not write app cache\n");
+            return 1;
+        }
+    }
+
+    return 0;
+}
+
+int main(int argc, const char* argv[]) {
+    OptionsVariants options;
+    if (!parseArgs(argc, argv, options))
+        return 1;
+
+    if (std::holds_alternative<DumpOptions>(options)) {
+        return dumpAppCache(std::get<DumpOptions>(options));
+    }
+
+    if (std::holds_alternative<ValidateOptions>(options)) {
+        return validateFile(std::get<ValidateOptions>(options));
+    }
+
+    if (std::holds_alternative<ListBundlesOptions>(options)) {
+        return listBundles(std::get<ListBundlesOptions>(options));
+    }
+
+    if (std::holds_alternative<CreateKernelCollectionOptions>(options)) {
+        return createKernelCollection(std::get<CreateKernelCollectionOptions>(options));
+    }
+
+    assert(std::holds_alternative<std::monostate>(options));
+
+    exit_usage();
+}