Manifest.mm   [plain text]


//
//  Manifest.mm
//  dyld
//
//  Created by Louis Gerbarg on 7/23/15.
//
//

#if BOM_SUPPORT
extern "C" {
#include <Bom/Bom.h>
#include <Metabom/MBTypes.h>
#include <Metabom/MBEntry.h>
#include <Metabom/MBMetabom.h>
#include <Metabom/MBIterator.h>
};
#endif /* BOM_SUPPORT */

#include <algorithm>

#include <Foundation/Foundation.h>
#include <rootless.h>

#include "MachOFileAbstraction.hpp"
#include "FileAbstraction.hpp"
#include "Trie.hpp"
#include "Logging.h"

#include <mach-o/loader.h>
#include <mach-o/fat.h>

#include <array>
#include <vector>

#include "Manifest.h"
#include "dsc_iterator.h"

#include "mega-dylib-utils.h"

namespace {
	//FIXME this should be in a class
	static bool rootless = true;

        static inline NSString* cppToObjStr(const std::string& str) { return [NSString stringWithUTF8String:str.c_str()]; }

#if BOM_SUPPORT
        std::string effectivePath(const std::string source, const std::string target)
        {
            if (target[0] == '/')
                return normalize_absolute_file_path(target);

            std::size_t found = source.find_last_of('/');
            return normalize_absolute_file_path(source.substr(0, found) + "/" + target);
        }

        std::string checkSymlink(const std::string path, const std::pair<std::string, std::string>& symlink, const std::set<std::string>& directories)
        {
            if (directories.count(symlink.second) == 0) {
                if (path == symlink.second)
                    return symlink.first;
            } else {
                auto res = std::mismatch(symlink.second.begin(), symlink.second.end(), path.begin());
                if (res.first == symlink.second.end()) {
                    std::string alias = normalize_absolute_file_path(symlink.first + std::string(res.second, path.end()));
                    return alias;
                }
            }
            return "";
        }
#endif
        }

#if BOM_SUPPORT

        Manifest::Manifest(const std::string& path)
            : Manifest(path, std::set<std::string>())
        {
        }

        Manifest::Manifest(const std::string& path, const std::set<std::string>& overlays)
        {
            NSMutableDictionary* manifestDict = [NSMutableDictionary dictionaryWithContentsOfFile:cppToObjStr(path)];
            std::map<std::string, std::string>           metabomTagMap;
            std::map<std::string, std::set<std::string>> metabomExcludeTagMap;
            std::map<std::string, std::set<std::string>> metabomRestrictedTagMap;
            metabomFile = [manifestDict[@"metabomFile"] UTF8String];

            for (NSString* project in manifestDict[@"projects"]) {
                for (NSString* source in manifestDict[@"projects"][project]) {
                    projects[[project UTF8String]].sources.push_back([source UTF8String]);
                }
            }

            for (NSString* configuration in manifestDict[@"configurations"]) {
                std::string configStr = [configuration UTF8String];
                std::string configTag = [manifestDict[@"configurations"][configuration][@"metabomTag"] UTF8String];
                metabomTagMap[configTag] = configStr;

                if (manifestDict[@"configurations"][configuration][@"metabomExcludeTags"]) {
                    for (NSString* excludeTag in manifestDict[@"configurations"][configuration][@"metabomExcludeTags"]) {
                        metabomExcludeTagMap[configStr].insert([excludeTag UTF8String]);
                        configurations[configStr].metabomExcludeTags.insert([excludeTag UTF8String]);
                    }
                }

                if (manifestDict[@"configurations"][configuration][@"metabomRestrictTags"]) {
                    for (NSString* restrictTag in manifestDict[@"configurations"][configuration][@"metabomRestrictTags"]) {
                        metabomRestrictedTagMap[configStr].insert([restrictTag UTF8String]);
                        configurations[configStr].metabomRestrictTags.insert([restrictTag UTF8String]);
                    }
                }

                configurations[configStr].metabomTag = configTag;
                configurations[configStr].platformName =
                    [manifestDict[@"configurations"][configuration][@"platformName"] UTF8String];
            }

            manifest_version = [manifestDict[@"manifest-version"] unsignedIntValue];
            build = [manifestDict[@"build"] UTF8String];
            if (manifestDict[@"dylibOrderFile"]) {
                dylibOrderFile = [manifestDict[@"dylibOrderFile"] UTF8String];
            }
            if (manifestDict[@"dirtyDataOrderFile"]) {
                dirtyDataOrderFile = [manifestDict[@"dirtyDataOrderFile"] UTF8String];
            }

            auto    metabom = MBMetabomOpen(metabomFile.c_str(), false);
            auto    metabomEnumerator = MBIteratorNewWithPath(metabom, ".", "");
            MBEntry entry;

            std::map<std::string, std::string> symlinks;
            std::set<std::string> directories;

            // FIXME error handling (NULL metabom)

            while ((entry = MBIteratorNext(metabomEnumerator))) {
                auto        fsObject = MBEntryGetFSObject(entry);
                std::string entryPath = BOMFSObjectPathName(fsObject);
                if (entryPath[0] == '.') {
                    entryPath.erase(0, 1);
                }
                auto entryType = BOMFSObjectType(fsObject);

                switch (entryType) {
                case BOMFileType: {
                    MBTag tag;
                    auto  tagCount = MBEntryGetNumberOfProjectTags(entry);

                    if (!BOMFSObjectIsBinaryObject(fsObject))
                        break;

                    if (tagCount == 0) {
                        break;
                    } else if (tagCount == 1) {
                        MBEntryGetProjectTags(entry, &tag);
                    } else {
                        MBTag*                tags = (MBTag*)malloc(sizeof(MBTag) * tagCount);
                        MBEntryGetProjectTags(entry, tags);

                        //Sigh, we can have duplicate entries for the same tag, so build a set to work with
                        std::set<std::string> tagStrs;
                        std::map<std::string, MBTag> tagStrMap;
                        for (auto i = 0; i < tagCount; ++i) {
                            tagStrs.insert(MBMetabomGetProjectForTag(metabom, tags[i]));
                            tagStrMap.insert(std::make_pair(MBMetabomGetProjectForTag(metabom, tags[i]), tags[i]));
                        }

                        if (tagStrs.size() > 1) {
                            std::string projects;
                            for (const auto& tagStr : tagStrs) {
                                if (!projects.empty())
                                    projects += ", ";

                                projects += "'" + tagStr + "'";
                            }
                            warning("Bom entry '%s' is claimed by multiple projects: %s, taking first entry", entryPath.c_str(), projects.c_str());
                        }
                        tag = tagStrMap[*tagStrs.begin()];
                        free(tags);
                    }

                    std::string projectName = MBMetabomGetProjectForTag(metabom, tag);

                    // FIXME we need to actually walk down the searchpaths
                    auto project = projects.find(projectName);
                    if (project == projects.end())
                        break;
                    if (project->second.sources.size() == 0)
                        break;
                    std::string projectPath = project->second.sources[0];
                    std::map<std::string, MachOProxy*> proxies;

                    for (const auto& overlay : overlays) {
                        proxies = MachOProxy::findDylibInfo(overlay + "/" + entryPath);
                        if (proxies.size() > 0)
                            break;
                    }

                    if (proxies.size() == 0) {
                        proxies = MachOProxy::findDylibInfo(projectPath + "/" + entryPath);
                    }

                    for (auto& proxy : proxies) {
                        assert(proxy.second != nullptr);
                        if (proxy.second->isExecutable()) {
                            architectureFiles[proxy.first].executables.insert(std::make_pair(entryPath, File(proxy.second)));
                            continue;
                        }
                        if (!proxy.second->isDylib())
                            continue;
                        assert(proxy.second->installName != "");
                        proxy.second->addAlias(entryPath);
                        architectureFiles[proxy.first].dylibs.insert(
                            std::make_pair(proxy.second->installName, File(proxy.second)));
                        auto   tagCount = MBEntryGetNumberOfPackageTags(entry);
                        MBTag* tags = (MBTag*)malloc(sizeof(MBTag) * tagCount);
                        MBEntryGetPackageTags(entry, tags);
                        std::set<std::string> tagStrs;

                        for (auto i = 0; i < tagCount; ++i) {
                            tagStrs.insert(MBMetabomGetPackageForTag(metabom, tags[i]));
                        }

                        for (const auto& tagStr : tagStrs) {
                            // Does the configuration exist
                            auto configuration = metabomTagMap.find(tagStr);
                            if (configuration == metabomTagMap.end())
                                continue;

                            auto restrictions = metabomRestrictedTagMap.find(configuration->second);
                            if (restrictions != metabomRestrictedTagMap.end() && !is_disjoint(restrictions->second, tagStrs)) {
                                configurations[configuration->second].restrictedInstallnames.insert(proxy.second->installName);
                            }
                            // Is the configuration excluded
                            auto exclusions = metabomExcludeTagMap.find(configuration->second);
                            if (exclusions != metabomExcludeTagMap.end() && !is_disjoint(exclusions->second, tagStrs))
                                continue;

                            if ([manifestDict[@"configurations"][cppToObjStr(configuration->second)][@"architectures"]
                                    containsObject:cppToObjStr(proxy.first)]) {
                                configurations[configuration->second.c_str()].architectures[proxy.first].anchors.push_back(
                                    proxy.second->installName);
                            }
                        }

                        free(tags);
                    }
                } break;
                case BOMSymlinkType: {
                    if (!has_prefix(entryPath, "/usr/lib/") && !has_prefix(entryPath, "/System/Library/"))
                        break;
                    const char* target = BOMFSObjectSymlinkTarget(fsObject);
                    if (target) {
                        symlinks[entryPath] = effectivePath(entryPath, target);
                    }
                } break;
                case BOMDirectoryType: {
                    if (!has_prefix(entryPath, "/usr/lib/") && !has_prefix(entryPath, "/System/Library/"))
                        break;
                    directories.insert(entryPath);
                } break;
                default:
                    break;
                }
	}

        MBIteratorFree(metabomEnumerator);
        MBMetabomFree(metabom);

        dispatch_queue_t symlinkQueue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, NULL);
        dispatch_group_t symlinkGroup = dispatch_group_create();

        for (auto& fileSet : architectureFiles) {
            cacheBuilderDispatchGroupAsync(symlinkGroup, symlinkQueue, [&] {
                for (auto& file : fileSet.second.dylibs) {
                    bool aliasAdded = true;
                    auto proxy = file.second.proxy;

                    while (aliasAdded) {
                        aliasAdded = false;

                        for (auto& symlink : symlinks) {
                            std::set<std::string> newAliases;
                            auto                  alias = checkSymlink(proxy->installName, symlink, directories);
                            if (alias != "") {
                                newAliases.insert(alias);
                            }

                            for (auto& existingAlias : proxy->installNameAliases) {
                                alias = checkSymlink(existingAlias, symlink, directories);
                                if (alias != "") {
                                    newAliases.insert(alias);
                                }
                            }

                            for (auto& alias : newAliases) {
                                if (proxy->addAlias(alias)) {
                                    aliasAdded = true;
                                }
                            }
                        }
                    }
                }
            });
        }
        dispatch_group_wait(symlinkGroup, DISPATCH_TIME_FOREVER);

        for (auto& fileSet : architectureFiles) {
            for (auto& file : fileSet.second.dylibs) {
                auto proxy = file.second.proxy;

                for (const auto& dependency : proxy->dependencies) {
                    auto dependencyProxy = dylibProxy(dependency, fileSet.first);
                    if (dependencyProxy == nullptr)
                        break;

                    dependencyProxy->dependents.insert(proxy->installName);
                }
            }
        }
}
#endif

void Manifest::calculateClosure( bool enforceRootless ) {
    rootless = enforceRootless;

    for ( auto& config : configurations ) {
        for ( auto& arch : config.second.architectures ) {
            calculateClosure( config.first, arch.first );
        }
    }
}

Manifest::File* Manifest::dylibForInstallName( const std::string& installname, const std::string& arch ) {
    auto archIter = architectureFiles.find( arch );
    if ( archIter == architectureFiles.end() ) return nullptr;

    auto& files = archIter->second.dylibs;
    auto dylibIterator = files.find( installname );

    if ( dylibIterator != files.end() ) return &dylibIterator->second;

    for ( auto& candidate : files ) {
        if ( candidate.second.proxy->installNameAliases.count( installname ) > 0 ) {
            dylibIterator = files.find( candidate.first );
            return &dylibIterator->second;
        }
        }
	// Check if we can fallback to an interworkable architecture
	std::string fallbackArchStr = fallbackArchStringForArchString( arch );
	if ( !fallbackArchStr.empty() ) {
		return dylibForInstallName( installname, fallbackArchStr );
	}
	
	return nullptr;
}


MachOProxy* Manifest::dylibProxy( const std::string& installname, const std::string& arch ) {
    auto dylib = dylibForInstallName( installname, arch );

    if ( dylib != nullptr ) {
        assert( dylib->proxy != nullptr );
        return dylib->proxy;
    }

    return nullptr;
}

bool
Manifest::sameContentsAsCacheAtPath(const std::string& configuration, const std::string& architecture, const std::string& path) const {
	__block std::set<std::pair<std::string, std::array<char, 16>>> cacheDylibs;
	std::set<std::pair<std::string, std::array<char, 16>>> manifestDylibs;
	struct stat statbuf;
	if ( ::stat(path.c_str(), &statbuf) == -1 ) {
		// <rdar://problem/25912438> don't warn if there is no existing cache file
		if ( errno != ENOENT )
			warning("stat() failed for dyld shared cache at %s, errno=%d", path.c_str(), errno);
		return false;
	}

	int cache_fd = ::open(path.c_str(), O_RDONLY);
	if ( cache_fd < 0 ) {
		warning("open() failed for shared cache file at %s, errno=%d", path.c_str(), errno);
		return false;
	}

	const void *mappedCache = ::mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, cache_fd, 0);
	if (mappedCache == MAP_FAILED) {
		::close(cache_fd);
			warning("mmap() for shared cache at %s failed, errno=%d", path.c_str(), errno);
			return false;
	}
	::close(cache_fd);

	if (configurations.count(configuration) == 0
		|| configurations.find(configuration)->second.architectures.count(architecture) == 0)
		return false;

	for (auto& dylib : configurations.find(configuration)->second.architectures.find(architecture)->second.results.dylibs) {
		if ( dylib.second.included == true) {
			std::pair<std::string, std::array<char, 16>> dylibPair;
			dylibPair.first = dylib.first;
			bcopy((const void *)&dylib.second.uuid[0], &dylibPair.second[0], sizeof(uuid_t));
			manifestDylibs.insert(dylibPair);
                        auto file = architectureFiles.find(architecture)->second.dylibs.find(dylib.first);
                        if (file != architectureFiles.find(architecture)->second.dylibs.end()) {
                            for ( auto& alias : file->second.proxy->installNameAliases ) {
                                std::pair<std::string, std::array<char, 16>> aliasPair;
                                aliasPair.first = alias;
                                        bcopy((const void *)&dylib.second.uuid[0], &aliasPair.second[0], sizeof(uuid_t));
					manifestDylibs.insert(aliasPair);
				}
			}
		}
	}

	(void)dyld_shared_cache_iterate(mappedCache, (uint32_t)statbuf.st_size,
										 ^(const dyld_shared_cache_dylib_info* dylibInfo, const dyld_shared_cache_segment_info* segInfo){
											 std::pair<std::string, std::array<char, 16>> dylibPair;
											 dylibPair.first = dylibInfo->path;
											 bcopy((const void *)&dylibInfo->uuid[0], &dylibPair.second[0], sizeof(uuid_t));
											cacheDylibs.insert(dylibPair);
										 });

	return (manifestDylibs == cacheDylibs);
}

void Manifest::removeDylib( MachOProxy* proxy, const std::string& reason, const std::string& configuration,
                            const std::string& architecture, std::unordered_set<std::string>& processedInstallnames ) {
    auto configIter = configurations.find( configuration );
    if ( configIter == configurations.end() ) return;
    auto archIter = configIter->second.architectures.find( architecture );
    if ( archIter == configIter->second.architectures.end() ) return;
    auto& archManifest = archIter->second;

    if ( archManifest.results.dylibs.count( proxy->installName ) == 0 ) {
        bcopy( &proxy->uuid[0], &archManifest.results.dylibs[proxy->installName].uuid[0], sizeof( uuid_t ) );
        processedInstallnames.insert( proxy->installName );
    }
    archManifest.results.dylibs[proxy->installName].exclude( reason );

    processedInstallnames.insert( proxy->installName );
    for ( auto& alias : proxy->installNameAliases ) {
        processedInstallnames.insert( alias );
    }

    for ( const auto& dependent : proxy->dependents ) {
        auto dependentProxy = dylibProxy( dependent, architecture );
        auto dependentResultIter = archManifest.results.dylibs.find( dependentProxy->installName );
        if ( dependentProxy &&
             ( dependentResultIter == archManifest.results.dylibs.end() || dependentResultIter->second.included == true ) ) {
            removeDylib( dependentProxy, "Missing dependency: " + proxy->installName, configuration, architecture,
                         processedInstallnames );
        }
    }
}

MachOProxy* Manifest::removeLargestLeafDylib( const std::string& configuration, const std::string& architecture ) {
    std::set<std::string> activeInstallnames;
    std::set<MachOProxy*> leafDylibs;

    auto configIter = configurations.find( configuration );
    if ( configIter == configurations.end() ) terminate( "Internal error" );
    ;
    auto archIter = configIter->second.architectures.find( architecture );
    if ( archIter == configIter->second.architectures.end() ) terminate( "Internal error" );
    ;
    for ( const auto& dylibInfo : archIter->second.results.dylibs ) {
        if ( dylibInfo.second.included ) {
            activeInstallnames.insert( dylibInfo.first );
        }
    }
    for ( const auto& installname : activeInstallnames ) {
        auto dylib = dylibProxy( installname, architecture );
        bool dependents = false;
        for ( const auto& depedent : dylib->dependents ) {
            if ( depedent != dylib->installName && activeInstallnames.count( depedent ) ) {
                dependents = true;
                break;
            }
        }
        if ( !dependents ) {
            leafDylibs.insert( dylib );
        }
    }
    if ( leafDylibs.empty() ) {
        terminate( "No leaf dylibs to evict" );
    }
    MachOProxy* largestLeafDylib = nullptr;
    for ( const auto& dylib : leafDylibs ) {
        if ( largestLeafDylib == nullptr || dylib->fileSize > largestLeafDylib->fileSize ) {
            largestLeafDylib = dylib;
        }
    }
    std::unordered_set<std::string> empty;
    removeDylib( largestLeafDylib, "VM space overflow", configuration, architecture, empty );
    return largestLeafDylib;
}

static void recursiveInvalidate(const std::string& invalidName, std::unordered_map<std::string, std::unordered_set<std::string>>& usesOf, std::unordered_set<std::string>& unusableInstallNames)
{
	if ( unusableInstallNames.count(invalidName) )
		return;
	unusableInstallNames.insert(invalidName);
	for (const std::string& name : usesOf[invalidName] ) {
		recursiveInvalidate(name, usesOf, unusableInstallNames);
	}
}

void Manifest::pruneClosure()
{
	for (auto& config : configurations) {
		for (auto& arch : config.second.architectures) {
			pruneClosure(config.first, arch.first);
		}
    }
}

void Manifest::pruneClosure(const std::string& configuration, const std::string& architecture)
{
	auto configIter = configurations.find(configuration);
	if ( configIter == configurations.end() )
		return;
	auto archIter = configIter->second.architectures.find(architecture);
	if ( archIter == configIter->second.architectures.end() )
		return;
	auto& archManifest = archIter->second;

	// build reverse dependency map and list of excluded dylibs
	std::unordered_map<std::string, std::unordered_set<std::string>> reverseDep;
	std::unordered_set<std::string> unusableStart;
	for (const auto& dylib : archManifest.results.dylibs) {
		const std::string dylibInstallName = dylib.first;
		if ( dylib.second.included ) {
			if ( MachOProxy* proxy = dylibProxy(dylibInstallName, architecture) ) {
				for (const std::string& dependentPath : proxy->dependencies) {
					reverseDep[dependentPath].insert(dylibInstallName);
				}
			}
		}
		else {
			unusableStart.insert(dylibInstallName);
		}
	}

	// mark unusable, all dylibs depending on the initially unusable dylibs
	std::unordered_set<std::string> newUnusable;
	for (const std::string& unusable : unusableStart) {
		recursiveInvalidate(unusable, reverseDep, newUnusable);
	}

	// remove unusable dylibs from manifest
	std::unordered_set<std::string> dummy;
	for (const std::string& unusable : newUnusable) {
		if ( MachOProxy* proxy = dylibProxy(unusable, architecture) )
			removeDylib(proxy, "Missing dependency: " + unusable, configuration, architecture, dummy);
		warning("can't use: %s because dependent dylib cannot be used", unusable.c_str());
	}
}

void Manifest::calculateClosure( const std::string& configuration, const std::string& architecture ) {
    auto& archManifest = configurations[configuration].architectures[architecture];
    auto archFileIter = architectureFiles.find( architecture );
    assert( archFileIter != architectureFiles.end() );
    auto files = archFileIter->second.dylibs;

    std::unordered_set<std::string> newInstallnames;

    for ( auto& anchor : archManifest.anchors ) {
                newInstallnames.insert(anchor.installname);
	}

	std::unordered_set<std::string> processedInstallnames;

	while (!newInstallnames.empty()) {
		std::unordered_set<std::string> installnamesToProcess = newInstallnames;
		newInstallnames.clear();

		for (const std::string& installname : installnamesToProcess) {
			if (processedInstallnames.count(installname) > 0) {
				continue;
			}

            auto proxy = dylibProxy( installname, architecture );

            if ( proxy == nullptr ) {
                // No path
                archManifest.results.dylibs[installname].exclude( "Could not find file for install name" );
                warning("Could not find file for install name (%s)", installname.c_str());
                continue;
            }

            if (configurations[configuration].restrictedInstallnames.count(installname) != 0) {
                removeDylib(proxy, "Dylib '" + installname + "' removed due to explict restriction", configuration, architecture,
                    processedInstallnames);
                continue;
            }
            // Validate we have all are depedencies
            for ( const auto& dependency : proxy->dependencies ) {
                if ( !dylibProxy( dependency, architecture ) ) {
                    removeDylib( proxy, "Missing dependency: " + dependency, configuration, architecture,
                                 processedInstallnames );
                    break;
                }
            }

            // assert(info->installName == installname);
            if ( archManifest.results.dylibs.count( proxy->installName ) == 0 ) {
                bcopy( &proxy->uuid[0], &archManifest.results.dylibs[proxy->installName].uuid[0], sizeof( uuid_t ) );
                processedInstallnames.insert( proxy->installName );

                auto fileIter = files.find( proxy->installName );
                if ( fileIter != files.end() ) {
                    for ( auto& aliasName : fileIter->second.proxy->installNameAliases ) {
                        processedInstallnames.insert( aliasName );
                    }
                }
            }

            for ( const auto& dependency : proxy->dependencies ) {
                if ( processedInstallnames.count( dependency ) == 0 ) {
                    newInstallnames.insert( dependency );
                }
            }
        }
	}
}

void Manifest::write( const std::string& path ) {
    if ( path.empty() ) return;

    NSMutableDictionary* cacheDict = [[NSMutableDictionary alloc] init];
    NSMutableDictionary* projectDict = [[NSMutableDictionary alloc] init];
    NSMutableDictionary* configurationsDict = [[NSMutableDictionary alloc] init];
    NSMutableDictionary* resultsDict = [[NSMutableDictionary alloc] init];

    cacheDict[@"manifest-version"] = @( manifest_version );
    cacheDict[@"build"] = cppToObjStr( build );
    cacheDict[@"dylibOrderFile"] = cppToObjStr( dylibOrderFile );
    cacheDict[@"dirtyDataOrderFile"] = cppToObjStr( dirtyDataOrderFile );
    cacheDict[@"metabomFile"] = cppToObjStr( metabomFile );

    cacheDict[@"projects"] = projectDict;
    cacheDict[@"results"] = resultsDict;
    cacheDict[@"configurations"] = configurationsDict;

    for ( const auto& project : projects ) {
        NSMutableArray* sources = [[NSMutableArray alloc] init];

        for ( const auto& source : project.second.sources ) {
            [sources addObject:cppToObjStr( source )];
        }

        projectDict[cppToObjStr( project.first )] = sources;
    }

    for ( auto& configuration : configurations ) {
        NSMutableArray* archArray = [[NSMutableArray alloc] init];
        for ( auto& arch : configuration.second.architectures ) {
            [archArray addObject:cppToObjStr( arch.first )];
        }

        NSMutableArray* excludeTags = [[NSMutableArray alloc] init];
        for ( const auto& excludeTag : configuration.second.metabomExcludeTags ) {
            [excludeTags addObject:cppToObjStr( excludeTag )];
        }

        configurationsDict[cppToObjStr( configuration.first )] = @{
            @"platformName" : cppToObjStr( configuration.second.platformName ),
            @"metabomTag" : cppToObjStr( configuration.second.metabomTag ),
            @"metabomExcludeTags" : excludeTags,
            @"architectures" : archArray
        };
    }

    for ( auto& configuration : configurations ) {
        NSMutableDictionary* archResultsDict = [[NSMutableDictionary alloc] init];
        for ( auto& arch : configuration.second.architectures ) {
            NSMutableDictionary* dylibsDict = [[NSMutableDictionary alloc] init];
            NSMutableArray* warningsArray = [[NSMutableArray alloc] init];
            NSMutableDictionary* devRegionsDict = [[NSMutableDictionary alloc] init];
            NSMutableDictionary* prodRegionsDict = [[NSMutableDictionary alloc] init];
                        NSString *prodCDHash = cppToObjStr(arch.second.results.productionCache.cdHash);
			NSString *devCDHash = cppToObjStr(arch.second.results.developmentCache.cdHash);

                        for ( auto& dylib : arch.second.results.dylibs ) {
                            NSMutableDictionary* dylibDict = [[NSMutableDictionary alloc] init];
                            if ( dylib.second.included ) {
                                NSMutableDictionary* segments = [[NSMutableDictionary alloc] init];
                                dylibDict[@"included"] = @YES;
                                for ( auto& segment : dylib.second.segments ) {
                                    segments[cppToObjStr( segment.name )] =
                                        @{ @"startAddr" : @( segment.startAddr ),
                                           @"endAddr" : @( segment.endAddr ) };
                                }
                                dylibDict[@"segments"] = segments;
                            } else {
                                dylibDict[@"included"] = @NO;
                                dylibDict[@"exclusionInfo"] = cppToObjStr(dylib.second.exclusionInfo);
                            }
                            dylibsDict[cppToObjStr( dylib.first )] = dylibDict;
                        }

                        for ( auto& region : arch.second.results.developmentCache.regions ) {
                            devRegionsDict[cppToObjStr( region.name )] =
                                @{ @"startAddr" : @( region.startAddr ),
                                   @"endAddr" : @( region.endAddr ) };
                        }

                        for ( auto& region : arch.second.results.productionCache.regions ) {
                            prodRegionsDict[cppToObjStr( region.name )] =
                                @{ @"startAddr" : @( region.startAddr ),
                                   @"endAddr" : @( region.endAddr ) };
                        }

                        for ( auto& warning : arch.second.results.warnings ) {
                            [warningsArray addObject:cppToObjStr( warning )];
                        }

                        BOOL built = arch.second.results.failure.empty();
                        archResultsDict[cppToObjStr( arch.first )] = @{
                            @"dylibs" : dylibsDict,
                            @"built" : @( built ),
                            @"failure" : cppToObjStr( arch.second.results.failure ),
                            @"productionCache" : @{@"cdhash" : prodCDHash, @"regions" : prodRegionsDict},
                            @"developmentCache" : @{@"cdhash" : devCDHash, @"regions" : devRegionsDict},
                            @"warnings" : warningsArray
                        };
        }
        resultsDict[cppToObjStr( configuration.first )] = archResultsDict;
    }

    NSError* error = nil;
        NSData *outData = [NSPropertyListSerialization dataWithPropertyList:cacheDict
																 format:NSPropertyListBinaryFormat_v1_0
																options:0
																  error:&error];
	(void)[outData writeToFile:cppToObjStr(path) atomically:YES];
}