#include "XcodeProjectParser.h" #include "XcodeWorkspaceParser.h" #include #include #include #include #include #include namespace fs = std::filesystem; struct TargetData { std::string workspace; std::string project; std::string type; }; struct CFKeyValues { static CFKeyValues fromDictionary(CFDictionaryRef dict) { auto size = CFDictionaryGetCount(dict); CFKeyValues ret(size); CFDictionaryGetKeysAndValues(dict, ret.keys.data(), ret.values.data()); return ret; } explicit CFKeyValues(size_t size) : size(size), keys(size), values(size) {} size_t size; std::vector keys; std::vector values; }; static std::string stringValue(CFDictionaryRef dict, CFStringRef key) { auto cfValue = (CFStringRef)CFDictionaryGetValue(dict, key); if (cfValue) { auto length = CFStringGetLength(cfValue); std::string s(length, '\0'); if (CFStringGetCString(cfValue, s.data(), length + 1, kCFStringEncodingUTF8)) { return s; } } return {}; } typedef std::unordered_map Targets; typedef std::unordered_map> Dependencies; typedef std::unordered_map>> BuildFiles; static size_t totalFilesCount(const std::string& target, const Dependencies& dependencies, const BuildFiles& buildFiles) { size_t sum = buildFiles.at(target).size(); for (auto& dep : dependencies.at(target)) { sum += totalFilesCount(dep, dependencies, buildFiles); } return sum; } static bool objectIsTarget(CFDictionaryRef object) { auto isa = (CFStringRef)CFDictionaryGetValue(object, CFSTR("isa")); if (isa) { for (auto target : {CFSTR("PBXAggregateTarget"), CFSTR("PBXNativeTarget"), CFSTR("PBXLegacyTarget")}) { if (CFStringCompare(isa, target, 0) == kCFCompareEqualTo) { return true; } } } return false; } static void mapTargetsToSourceFiles(CFDictionaryRef objects, std::unordered_map& fileCounts) { Targets targets; Dependencies dependencies; BuildFiles buildFiles; auto kv = CFKeyValues::fromDictionary(objects); for (size_t i = 0; i < kv.size; i++) { auto object = (CFDictionaryRef)kv.values[i]; if (objectIsTarget(object)) { auto name = stringValue(object, CFSTR("name")); dependencies[name] = {}; buildFiles[name] = {}; targets.emplace(name, object); } } for (auto& [targetName, targetObject] : targets) { auto deps = (CFArrayRef)CFDictionaryGetValue(targetObject, CFSTR("dependencies")); auto size = CFArrayGetCount(deps); for (CFIndex i = 0; i < size; i++) { auto dependencyID = (CFStringRef)CFArrayGetValueAtIndex(deps, i); auto dependency = (CFDictionaryRef)CFDictionaryGetValue(objects, dependencyID); auto targetID = (CFStringRef)CFDictionaryGetValue(dependency, CFSTR("target")); if (!targetID) { // Skipping non-targets (e.g., productRef) continue; } auto targetDependency = (CFDictionaryRef)CFDictionaryGetValue(objects, targetID); auto dependencyName = stringValue(targetDependency, CFSTR("name")); if (!dependencyName.empty()) { dependencies[targetName].push_back(dependencyName); } } } for (auto& [targetName, targetObject] : targets) { auto buildPhases = (CFArrayRef)CFDictionaryGetValue(targetObject, CFSTR("buildPhases")); auto buildPhaseCount = CFArrayGetCount(buildPhases); for (CFIndex buildPhaseIndex = 0; buildPhaseIndex < buildPhaseCount; buildPhaseIndex++) { auto buildPhaseID = (CFStringRef)CFArrayGetValueAtIndex(buildPhases, buildPhaseIndex); auto buildPhase = (CFDictionaryRef)CFDictionaryGetValue(objects, buildPhaseID); auto fileRefs = (CFArrayRef)CFDictionaryGetValue(buildPhase, CFSTR("files")); if (!fileRefs) { continue; } auto fileRefsCount = CFArrayGetCount(fileRefs); for (CFIndex fileRefIndex = 0; fileRefIndex < fileRefsCount; fileRefIndex++) { auto fileRefID = (CFStringRef)CFArrayGetValueAtIndex(fileRefs, fileRefIndex); auto fileRef = (CFDictionaryRef)CFDictionaryGetValue(objects, fileRefID); auto fileID = (CFStringRef)CFDictionaryGetValue(fileRef, CFSTR("fileRef")); if (!fileID) { // FileRef is not a reference to a file (e.g., PBXBuildFile) continue; } auto file = (CFDictionaryRef)CFDictionaryGetValue(objects, fileID); if (!file) { // Sometimes the references file belongs to another project, which is not present for // various reasons continue; } auto isa = stringValue(file, CFSTR("isa")); if (isa != "PBXFileReference") { // Skipping anything that is not a 'file', e.g. PBXVariantGroup continue; } auto fileType = stringValue(file, CFSTR("lastKnownFileType")); auto path = stringValue(file, CFSTR("path")); if (fileType == "sourcecode.swift" && !path.empty()) { buildFiles[targetName].emplace_back(path, file); } } } } for (auto& [targetName, _] : targets) { fileCounts[targetName] = totalFilesCount(targetName, dependencies, buildFiles); } } static CFDictionaryRef xcodeProjectObjects(const std::string& xcodeProject) { auto allocator = CFAllocatorGetDefault(); auto pbxproj = fs::path(xcodeProject) / "project.pbxproj"; if (!fs::exists(pbxproj)) { return CFDictionaryCreate(allocator, nullptr, nullptr, 0, nullptr, nullptr); } std::ifstream ifs(pbxproj, std::ios::in); std::string content((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())); auto data = CFDataCreate(allocator, (UInt8*)content.data(), content.size()); CFErrorRef error = nullptr; auto plist = CFPropertyListCreateWithData(allocator, data, 0, nullptr, &error); if (error) { auto description = CFCopyDescription(error); std::cerr << "[xcode autobuilder] Cannot read Xcode project: " << CFStringGetCStringPtr(description, kCFStringEncodingUTF8) << ": " << pbxproj << "\n"; CFRelease(description); return CFDictionaryCreate(allocator, nullptr, nullptr, 0, nullptr, nullptr); } return (CFDictionaryRef)CFDictionaryGetValue((CFDictionaryRef)plist, CFSTR("objects")); } // Maps each target to the number of Swift source files it contains transitively static std::unordered_map mapTargetsToSourceFiles( const std::unordered_map>& workspaces) { std::unordered_map fileCounts; for (auto& [workspace, projects] : workspaces) { // All targets/dependencies should be resolved in the context of the same workspace // As different projects in the same workspace may reference each other for dependencies auto allocator = CFAllocatorGetDefault(); auto allObjects = CFDictionaryCreateMutable(allocator, 0, nullptr, nullptr); for (auto& project : projects) { CFDictionaryRef objects = xcodeProjectObjects(project); auto kv = CFKeyValues::fromDictionary(objects); for (size_t i = 0; i < kv.size; i++) { CFDictionaryAddValue(allObjects, kv.keys[i], kv.values[i]); } } mapTargetsToSourceFiles(allObjects, fileCounts); } return fileCounts; } static std::vector> readTargets(const std::string& project) { auto objects = xcodeProjectObjects(project); std::vector> targets; auto kv = CFKeyValues::fromDictionary(objects); for (size_t i = 0; i < kv.size; i++) { auto object = (CFDictionaryRef)kv.values[i]; if (objectIsTarget(object)) { auto name = stringValue(object, CFSTR("name")); auto type = stringValue(object, CFSTR("productType")); targets.emplace_back(name, type.empty() ? "" : type); } } return targets; } static std::unordered_map mapTargetsToWorkspace( const std::unordered_map>& workspaces) { std::unordered_map targetMapping; for (auto& [workspace, projects] : workspaces) { for (auto& project : projects) { auto targets = readTargets(project); for (auto& [target, type] : targets) { targetMapping[target] = TargetData{workspace, project, type}; } } } return targetMapping; } static std::vector collectFiles(const std::string& workingDir) { fs::path workDir(workingDir); std::vector files; auto iterator = fs::recursive_directory_iterator(workDir); auto end = fs::recursive_directory_iterator(); for (; iterator != end; iterator++) { auto filename = iterator->path().filename(); if (filename == "DerivedData" || filename == ".git" || filename == "build") { // Skip these folders iterator.disable_recursion_pending(); continue; } auto dirEntry = *iterator; if (!dirEntry.is_directory()) { continue; } if (dirEntry.path().extension() != fs::path(".xcodeproj") && dirEntry.path().extension() != fs::path(".xcworkspace")) { continue; } files.push_back(dirEntry.path()); } return files; } static std::unordered_map> collectWorkspaces( const std::string& workingDir) { // Here we are collecting list of all workspaces and Xcode projects corresponding to them // Projects without workspaces go into the same "empty-workspace" bucket std::unordered_map> workspaces; std::unordered_set projectsBelongingToWorkspace; std::vector files = collectFiles(workingDir); for (auto& path : files) { if (path.extension() == ".xcworkspace") { auto projects = readProjectsFromWorkspace(path.string()); for (auto& project : projects) { projectsBelongingToWorkspace.insert(project.string()); workspaces[path.string()].push_back(project.string()); } } } // Collect all projects not belonging to any workspace into a separate empty bucket for (auto& path : files) { if (path.extension() == ".xcodeproj") { if (projectsBelongingToWorkspace.count(path.string())) { continue; } workspaces[std::string()].push_back(path.string()); } } return workspaces; } std::vector collectTargets(const std::string& workingDir) { // Getting a list of workspaces and the project that belong to them auto workspaces = collectWorkspaces(workingDir); if (workspaces.empty()) { std::cerr << "[xcode autobuilder] Xcode project or workspace not found\n"; exit(1); } // Mapping each target to the workspace/project it belongs to auto targetMapping = mapTargetsToWorkspace(workspaces); // Mapping each target to the number of source files it contains auto targetFilesMapping = mapTargetsToSourceFiles(workspaces); std::vector targets; for (auto& [targetName, data] : targetMapping) { targets.push_back(Target{data.workspace, data.project, targetName, data.type, targetFilesMapping[targetName]}); } return targets; }