Skip to content

Commit 0521855

Browse files
committed
Swift: split Xcode autobuild
1 parent f3ed54e commit 0521855

File tree

9 files changed

+484
-431
lines changed

9 files changed

+484
-431
lines changed

swift/xcode-autobuilder/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ swift_cc_binary(
44
name = "xcode-autobuilder",
55
srcs = glob([
66
"*.cpp",
7+
"*.h",
78
]),
89
visibility = ["//swift:__pkg__"],
910
linkopts = [
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#include "XcodeBuildRunner.h"
2+
3+
#include <vector>
4+
#include <iostream>
5+
#include <spawn.h>
6+
7+
static int waitpid_status(pid_t child) {
8+
int status;
9+
while (waitpid(child, &status, 0) == -1) {
10+
if (errno != EINTR) break;
11+
}
12+
return status;
13+
}
14+
15+
extern char** environ;
16+
17+
static bool exec(const std::vector<std::string>& argv) {
18+
const char** c_argv = (const char**)calloc(argv.size() + 1, sizeof(char*));
19+
for (size_t i = 0; i < argv.size(); i++) {
20+
c_argv[i] = argv[i].c_str();
21+
}
22+
c_argv[argv.size()] = nullptr;
23+
24+
pid_t pid = 0;
25+
if (posix_spawn(&pid, argv.front().c_str(), nullptr, nullptr, (char* const*)c_argv, environ) !=
26+
0) {
27+
std::cerr << "[xcode autobuilder] posix_spawn failed: " << strerror(errno) << "\n";
28+
free(c_argv);
29+
return false;
30+
}
31+
free(c_argv);
32+
int status = waitpid_status(pid);
33+
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
34+
return false;
35+
}
36+
return true;
37+
}
38+
39+
void buildTarget(Target& target, bool dryRun) {
40+
std::vector<std::string> argv({"/usr/bin/xcodebuild", "build"});
41+
if (!target.workspace.empty()) {
42+
argv.push_back("-workspace");
43+
argv.push_back(target.workspace);
44+
argv.push_back("-scheme");
45+
} else {
46+
argv.push_back("-project");
47+
argv.push_back(target.project);
48+
argv.push_back("-target");
49+
}
50+
argv.push_back(target.name);
51+
argv.push_back("CODE_SIGNING_REQUIRED=NO");
52+
argv.push_back("CODE_SIGNING_ALLOWED=NO");
53+
54+
if (dryRun) {
55+
std::string s;
56+
for (auto& arg : argv) {
57+
s += arg + " ";
58+
}
59+
std::cout << s << "\n";
60+
} else {
61+
if (!exec(argv)) {
62+
std::cerr << "Build failed\n";
63+
exit(1);
64+
}
65+
}
66+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#pragma once
2+
3+
#include "XcodeTarget.h"
4+
5+
void buildTarget(Target& target, bool dryRun);
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
#include "XcodeProjectParser.h"
2+
#include "XcodeWorkspaceParser.h"
3+
4+
#include <iostream>
5+
#include <filesystem>
6+
#include <unordered_map>
7+
#include <unordered_set>
8+
#include <fstream>
9+
#include <CoreFoundation/CoreFoundation.h>
10+
11+
namespace fs = std::filesystem;
12+
13+
struct TargetData {
14+
std::string workspace;
15+
std::string project;
16+
std::string type;
17+
};
18+
19+
struct CFKeyValues {
20+
static CFKeyValues fromDictionary(CFDictionaryRef dict) {
21+
auto size = CFDictionaryGetCount(dict);
22+
CFKeyValues ret(size);
23+
CFDictionaryGetKeysAndValues(dict, ret.keys.data(), ret.values.data());
24+
return ret;
25+
}
26+
explicit CFKeyValues(size_t size) : size(size), keys(size), values(size) {}
27+
size_t size;
28+
std::vector<const void*> keys;
29+
std::vector<const void*> values;
30+
};
31+
32+
static std::string stringValue(CFDictionaryRef dict, CFStringRef key) {
33+
auto cfValue = (CFStringRef)CFDictionaryGetValue(dict, key);
34+
if (cfValue) {
35+
auto length = CFStringGetLength(cfValue);
36+
std::string s(length, '\0');
37+
if (CFStringGetCString(cfValue, s.data(), length + 1, kCFStringEncodingUTF8)) {
38+
return s;
39+
}
40+
}
41+
return {};
42+
}
43+
44+
typedef std::unordered_map<std::string, CFDictionaryRef> Targets;
45+
typedef std::unordered_map<std::string, std::vector<std::string>> Dependencies;
46+
typedef std::unordered_map<std::string, std::vector<std::pair<std::string, CFDictionaryRef>>>
47+
BuildFiles;
48+
49+
static size_t totalFilesCount(const std::string& target,
50+
const Dependencies& dependencies,
51+
const BuildFiles& buildFiles) {
52+
size_t sum = buildFiles.at(target).size();
53+
for (auto& dep : dependencies.at(target)) {
54+
sum += totalFilesCount(dep, dependencies, buildFiles);
55+
}
56+
return sum;
57+
}
58+
59+
static bool objectIsTarget(CFDictionaryRef object) {
60+
auto isa = (CFStringRef)CFDictionaryGetValue(object, CFSTR("isa"));
61+
if (isa) {
62+
for (auto target :
63+
{CFSTR("PBXAggregateTarget"), CFSTR("PBXNativeTarget"), CFSTR("PBXLegacyTarget")}) {
64+
if (CFStringCompare(isa, target, 0) == kCFCompareEqualTo) {
65+
return true;
66+
}
67+
}
68+
}
69+
return false;
70+
}
71+
72+
static void mapTargetsToSourceFiles(CFDictionaryRef objects,
73+
std::unordered_map<std::string, size_t>& fileCounts) {
74+
Targets targets;
75+
Dependencies dependencies;
76+
BuildFiles buildFiles;
77+
78+
auto kv = CFKeyValues::fromDictionary(objects);
79+
for (size_t i = 0; i < kv.size; i++) {
80+
auto object = (CFDictionaryRef)kv.values[i];
81+
if (objectIsTarget(object)) {
82+
auto name = stringValue(object, CFSTR("name"));
83+
dependencies[name] = {};
84+
buildFiles[name] = {};
85+
targets.emplace(name, object);
86+
}
87+
}
88+
89+
for (auto& [targetName, targetObject] : targets) {
90+
auto deps = (CFArrayRef)CFDictionaryGetValue(targetObject, CFSTR("dependencies"));
91+
auto size = CFArrayGetCount(deps);
92+
for (CFIndex i = 0; i < size; i++) {
93+
auto dependencyID = (CFStringRef)CFArrayGetValueAtIndex(deps, i);
94+
auto dependency = (CFDictionaryRef)CFDictionaryGetValue(objects, dependencyID);
95+
auto targetID = (CFStringRef)CFDictionaryGetValue(dependency, CFSTR("target"));
96+
if (!targetID) {
97+
// Skipping non-targets (e.g., productRef)
98+
continue;
99+
}
100+
auto targetDependency = (CFDictionaryRef)CFDictionaryGetValue(objects, targetID);
101+
auto dependencyName = stringValue(targetDependency, CFSTR("name"));
102+
if (!dependencyName.empty()) {
103+
dependencies[targetName].push_back(dependencyName);
104+
}
105+
}
106+
}
107+
108+
for (auto& [targetName, targetObject] : targets) {
109+
auto buildPhases = (CFArrayRef)CFDictionaryGetValue(targetObject, CFSTR("buildPhases"));
110+
auto buildPhaseCount = CFArrayGetCount(buildPhases);
111+
for (CFIndex buildPhaseIndex = 0; buildPhaseIndex < buildPhaseCount; buildPhaseIndex++) {
112+
auto buildPhaseID = (CFStringRef)CFArrayGetValueAtIndex(buildPhases, buildPhaseIndex);
113+
auto buildPhase = (CFDictionaryRef)CFDictionaryGetValue(objects, buildPhaseID);
114+
auto fileRefs = (CFArrayRef)CFDictionaryGetValue(buildPhase, CFSTR("files"));
115+
if (!fileRefs) {
116+
continue;
117+
}
118+
auto fileRefsCount = CFArrayGetCount(fileRefs);
119+
for (CFIndex fileRefIndex = 0; fileRefIndex < fileRefsCount; fileRefIndex++) {
120+
auto fileRefID = (CFStringRef)CFArrayGetValueAtIndex(fileRefs, fileRefIndex);
121+
auto fileRef = (CFDictionaryRef)CFDictionaryGetValue(objects, fileRefID);
122+
auto fileID = (CFStringRef)CFDictionaryGetValue(fileRef, CFSTR("fileRef"));
123+
if (!fileID) {
124+
// FileRef is not a reference to a file (e.g., PBXBuildFile)
125+
continue;
126+
}
127+
auto file = (CFDictionaryRef)CFDictionaryGetValue(objects, fileID);
128+
if (!file) {
129+
// Sometimes the references file belongs to another project, which is not present for
130+
// various reasons
131+
continue;
132+
}
133+
auto isa = stringValue(file, CFSTR("isa"));
134+
if (isa != "PBXFileReference") {
135+
// Skipping anything that is not a 'file', e.g. PBXVariantGroup
136+
continue;
137+
}
138+
auto fileType = stringValue(file, CFSTR("lastKnownFileType"));
139+
auto path = stringValue(file, CFSTR("path"));
140+
if (fileType == "sourcecode.swift" && !path.empty()) {
141+
buildFiles[targetName].emplace_back(path, file);
142+
}
143+
}
144+
}
145+
}
146+
147+
for (auto& [targetName, _] : targets) {
148+
fileCounts[targetName] = totalFilesCount(targetName, dependencies, buildFiles);
149+
}
150+
}
151+
152+
static CFDictionaryRef xcodeProjectObjects(const std::string& xcodeProject) {
153+
auto allocator = CFAllocatorGetDefault();
154+
auto pbxproj = fs::path(xcodeProject) / "project.pbxproj";
155+
if (!fs::exists(pbxproj)) {
156+
return CFDictionaryCreate(allocator, nullptr, nullptr, 0, nullptr, nullptr);
157+
}
158+
std::ifstream ifs(pbxproj, std::ios::in);
159+
std::string content((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
160+
auto data = CFDataCreate(allocator, (UInt8*)content.data(), content.size());
161+
CFErrorRef error = nullptr;
162+
auto plist = CFPropertyListCreateWithData(allocator, data, 0, nullptr, &error);
163+
if (error) {
164+
auto description = CFCopyDescription(error);
165+
std::cerr << "[xcode autobuilder] Cannot read Xcode project: "
166+
<< CFStringGetCStringPtr(description, kCFStringEncodingUTF8) << ": " << pbxproj
167+
<< "\n";
168+
CFRelease(description);
169+
return CFDictionaryCreate(allocator, nullptr, nullptr, 0, nullptr, nullptr);
170+
}
171+
172+
return (CFDictionaryRef)CFDictionaryGetValue((CFDictionaryRef)plist, CFSTR("objects"));
173+
}
174+
175+
// Maps each target to the number of Swift source files it contains transitively
176+
static std::unordered_map<std::string, size_t> mapTargetsToSourceFiles(
177+
const std::unordered_map<std::string, std::vector<std::string>>& workspaces) {
178+
std::unordered_map<std::string, size_t> fileCounts;
179+
for (auto& [workspace, projects] : workspaces) {
180+
// All targets/dependencies should be resolved in the context of the same workspace
181+
// As different projects in the same workspace may reference each other for dependencies
182+
auto allocator = CFAllocatorGetDefault();
183+
auto allObjects = CFDictionaryCreateMutable(allocator, 0, nullptr, nullptr);
184+
for (auto& project : projects) {
185+
CFDictionaryRef objects = xcodeProjectObjects(project);
186+
auto kv = CFKeyValues::fromDictionary(objects);
187+
for (size_t i = 0; i < kv.size; i++) {
188+
CFDictionaryAddValue(allObjects, kv.keys[i], kv.values[i]);
189+
}
190+
}
191+
mapTargetsToSourceFiles(allObjects, fileCounts);
192+
}
193+
return fileCounts;
194+
}
195+
196+
static std::vector<std::pair<std::string, std::string>> readTargets(const std::string& project) {
197+
auto objects = xcodeProjectObjects(project);
198+
std::vector<std::pair<std::string, std::string>> targets;
199+
auto kv = CFKeyValues::fromDictionary(objects);
200+
for (size_t i = 0; i < kv.size; i++) {
201+
auto object = (CFDictionaryRef)kv.values[i];
202+
if (objectIsTarget(object)) {
203+
auto name = stringValue(object, CFSTR("name"));
204+
auto type = stringValue(object, CFSTR("productType"));
205+
targets.emplace_back(name, type.empty() ? "<unknown_target_type>" : type);
206+
}
207+
}
208+
return targets;
209+
}
210+
211+
static std::unordered_map<std::string, TargetData> mapTargetsToWorkspace(
212+
const std::unordered_map<std::string, std::vector<std::string>>& workspaces) {
213+
std::unordered_map<std::string, TargetData> targetMapping;
214+
for (auto& [workspace, projects] : workspaces) {
215+
for (auto& project : projects) {
216+
auto targets = readTargets(project);
217+
for (auto& [target, type] : targets) {
218+
targetMapping[target] = TargetData{workspace, project, type};
219+
}
220+
}
221+
}
222+
return targetMapping;
223+
}
224+
225+
static std::vector<fs::path> collectFiles(const std::string& workingDir) {
226+
fs::path workDir(workingDir);
227+
std::vector<fs::path> files;
228+
auto iterator = fs::recursive_directory_iterator(workDir);
229+
auto end = fs::recursive_directory_iterator();
230+
for (; iterator != end; iterator++) {
231+
auto filename = iterator->path().filename();
232+
if (filename == "DerivedData" || filename == ".git" || filename == "build") {
233+
// Skip these folders
234+
iterator.disable_recursion_pending();
235+
continue;
236+
}
237+
auto dirEntry = *iterator;
238+
if (!dirEntry.is_directory()) {
239+
continue;
240+
}
241+
if (dirEntry.path().extension() != fs::path(".xcodeproj") &&
242+
dirEntry.path().extension() != fs::path(".xcworkspace")) {
243+
continue;
244+
}
245+
files.push_back(dirEntry.path());
246+
}
247+
return files;
248+
}
249+
250+
static std::unordered_map<std::string, std::vector<std::string>> collectWorkspaces(
251+
const std::string& workingDir) {
252+
// Here we are collecting list of all workspaces and Xcode projects corresponding to them
253+
// Projects without workspaces go into the same "empty-workspace" bucket
254+
std::unordered_map<std::string, std::vector<std::string>> workspaces;
255+
std::unordered_set<std::string> projectsBelongingToWorkspace;
256+
std::vector<fs::path> files = collectFiles(workingDir);
257+
for (auto& path : files) {
258+
if (path.extension() == ".xcworkspace") {
259+
auto projects = readProjectsFromWorkspace(path.string());
260+
for (auto& project : projects) {
261+
projectsBelongingToWorkspace.insert(project.string());
262+
workspaces[path.string()].push_back(project.string());
263+
}
264+
}
265+
}
266+
// Collect all projects not belonging to any workspace into a separate empty bucket
267+
for (auto& path : files) {
268+
if (path.extension() == ".xcodeproj") {
269+
if (projectsBelongingToWorkspace.count(path.string())) {
270+
continue;
271+
}
272+
workspaces[std::string()].push_back(path.string());
273+
}
274+
}
275+
return workspaces;
276+
}
277+
278+
std::vector<Target> collectTargets(const std::string& workingDir) {
279+
// Getting a list of workspaces and the project that belong to them
280+
auto workspaces = collectWorkspaces(workingDir);
281+
if (workspaces.empty()) {
282+
std::cerr << "[xcode autobuilder] Xcode project or workspace not found\n";
283+
exit(1);
284+
}
285+
286+
// Mapping each target to the workspace/project it belongs to
287+
auto targetMapping = mapTargetsToWorkspace(workspaces);
288+
289+
// Mapping each target to the number of source files it contains
290+
auto targetFilesMapping = mapTargetsToSourceFiles(workspaces);
291+
292+
std::vector<Target> targets;
293+
294+
for (auto& [targetName, data] : targetMapping) {
295+
targets.push_back(Target{data.workspace, data.project, targetName, data.type,
296+
targetFilesMapping[targetName]});
297+
}
298+
return targets;
299+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#pragma once
2+
3+
#include "XcodeTarget.h"
4+
#include <vector>
5+
#include <string>
6+
7+
std::vector<Target> collectTargets(const std::string& workingDir);

0 commit comments

Comments
 (0)