-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Swift: introduce Xcode autobuilder #10786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some minor comments on the code itself, but one generic concern.
I'm not sold yet on the choice to use objective C++. While most of it looks like C++, the objective C-specific parts look alien enough to a C++ developer (like myself and probably a good part of current and future team members) that this might mean quite a maintenance cost down the line.
So on a general note, I think it might make sense to either:
- try to keep it in C++ finding some off-the-shelf plist deserializer. But I'm really meh on this, as the top result on google is this project that has not seen updates since 9 years or so 🙁
- A bit paradoxically, I think Swift might actually be a better choice, even if it's a language further away from C++, as Swift knowledge will always be part of what is necessary to maintain Swift language support. It could even be a good exercise for us 🙂
swift/xcode-autobuilder/BUILD.bazel
Outdated
swift_cc_binary( | ||
name = "xcode-autobuilder", | ||
srcs = glob([ | ||
"*.c", | ||
]), | ||
visibility = ["//swift:__pkg__"], | ||
deps = [ | ||
":xcode-autobuilder-lib", | ||
], | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are we not using objc_binary
here just to capture the settings made by swift_cc_binary
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't find objc_binary
so went with the simplest available option https://bazel.build/reference/be/objective-c
std::unordered_set<std::string> allowedTargetTypes({Application, Framework}); | ||
targets.erase( | ||
std::remove_if(std::begin(targets), std::end(targets), | ||
[&](Target& t) -> bool { return !allowedTargetTypes.count(t.type); }), | ||
std::end(targets)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd probably do this filtering earlier, as early as already inside mapTargetsToWorkspace
to avoid all the rest of the work on target information collection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still need to get all the targets so that we can calculate the files in the dependencies.
std::unordered_set<std::string> allowedTargetTypes({Application, Framework}); | ||
targets.erase( | ||
std::remove_if(std::begin(targets), std::end(targets), | ||
[&](Target& t) -> bool { return !allowedTargetTypes.count(t.type); }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think using a std::unordered_set
to filter out two static values is a bit overkill 😅 return t.type != Application && t.type != Framework
is ok to read and faster to execute.
Re: ObjC: I'll give Swift a try, but I think it will be even more messy/hard to read than the ObjC version. |
00b643a
to
a7397e4
Compare
a7397e4
to
3040837
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's my first pass, I'd like to get into it again tomorrow 🙂
swift/BUILD.bazel
Outdated
pkg_files( | ||
name = "autobuild", | ||
srcs = ["tools/autobuild.sh"], | ||
attributes = pkg_attributes(mode = "0755"), | ||
prefix = "tools", | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we can probably merge this with the qltest
definition above into something like
pkg_files(
name = "scripts",
srcs = ["tools/qltest.sh", "tools/autobuild.sh"],
attributes = pkg_attributes(mode = "0755"),
prefix = "tools",
)
extern char** environ; | ||
|
||
static bool exec(const std::vector<std::string>& argv) { | ||
const char** c_argv = (const char**)calloc(argv.size() + 1, sizeof(char*)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can avoid using calloc
and free
and still pass the arguments to the underlying C exec
API with
std::vector<const char*> c_argv;
c_argv.reserve(argv.size() + 1);
for (const auto& arg : argv) {
c_argv.push_back(argv.c_str());
}
c_argv.push_back(nullptr);
or alternatively
std::vector<const char*> c_argv{argv.size() + 1, nullptr};
for (size_t i = 0; i < argv.size(); i++) {
c_argv[i] = argv[i].c_str();
}
and then pass c_argv.data()
to posix_spawn
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just tried it and it throws way more "incompatible const * char * const ***char
types" errors than I'm ready to deal with 😄
std::string s; | ||
for (auto& arg : argv) { | ||
s += arg + " "; | ||
} | ||
std::cout << s << "\n"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: it's better to avoid building a string if we can directly stream.
std::string s; | |
for (auto& arg : argv) { | |
s += arg + " "; | |
} | |
std::cout << s << "\n"; | |
for (auto& arg : argv) { | |
std::cout << arg << " "; | |
} | |
std::cout << "\n"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sure I had a reason for using a string, but I cannot recall it anymore 😆
Anyways, applied your suggestion!
}; | ||
|
||
static std::string stringValue(CFDictionaryRef dict, CFStringRef key) { | ||
auto cfValue = (CFStringRef)CFDictionaryGetValue(dict, key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
although it requires to type more characters, it's preferable to use static_cast
over a C-style cast 🙂
there are many, many other cases.
In any case as discussed on zoom, probably all these C-style casts should be extracted to functions performing a type sanity check (and aborting with an error if the type does not match)
static std::string stringValue(CFDictionaryRef dict, CFStringRef key) { | ||
auto cfValue = (CFStringRef)CFDictionaryGetValue(dict, key); | ||
if (cfValue) { | ||
const int bufferSize = 256; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, and probably makes no real difference to the compiler, but you can replace const
with constexpr
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { | ||
return false; | ||
} | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe just
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { | |
return false; | |
} | |
return true; | |
return WIFEXITED(status) && WEXITSTATUS(status) == 0; |
Targets targets; | ||
Dependencies dependencies; | ||
BuildFiles buildFiles; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there are three big loops in this function, would it maybe help readability to extract each of those into a separate function with a descriptive name like collectTargets
, collectDependencies
, collectBuildFiles
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me give it a try!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit interconnected, so splitting it into separate functions only makes navigation harder, so I'm not sure if it's actually better...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fair enough!
} | ||
std::ifstream ifs(pbxproj, std::ios::in); | ||
std::string content((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>())); | ||
auto data = CFDataCreate(allocator, (UInt8*)content.data(), content.size()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use static_cast
instead of C-style cast
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's char *
to unsigned char *
so I guess we can only use reinterpret_cast
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right you are 🙂
"-lxml2", | ||
"-framework CoreFoundation", | ||
], | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
) | |
target_compatible_with = ["@platforms//os:macos"], | |
) |
I'm wondering whether a very crude and quick set of C++ wrappers wouldn't help make the code more readable (and do the type sanity checks. I've jotted down this incomplete and totally untested code: class Value {
void* wrapped;
public:
Value(void* wrapped) : wrapped{wrapped} {}
explicit operator bool() const { return wrapped != nullptr; }
template <typename T>
T as() const {
return {wrapped};
}
};
template <>
CFStringRef Value::as<CFStringRef>() const {
CFStringRef ret = wrapped;
// check actually string else abort
return ret;
}
template <>
std::string Value::as<std::string>() const {
auto cfValue = asCFStringRef();
const int bufferSize = 256;
char buf[bufferSize];
if (CFStringGetCString(cfValue, buf, bufferSize, kCFStringEncodingUTF8)) {
return {buf};
}
return {};
}
class Dictionary {
CFDictionaryRef wrapped;
public:
Dictionary(CFDictionaryRef wrapped) : wrapped{wrapped} {
// check actually a dictionary or abort
}
std::size_t size() const {
return CFDictionaryGetCount(wrapped);
}
Value operator[](CFStringRef key) const {
return {CFDictionaryGetValue(wrapped, key)}
}
std::vector<std::pair<Value, Value>> items() const {
auto size = CFDictionaryGetCount(wrapped);
std::vector<const void *> keys{size};
std::vector<const void *> values{size};
CFDictionaryGetKeysAndValues(dict, keys.data(), values.data());
std::vector<std::pair<Value, Value>> ret;
ret.reserve(size);
for (auto i = 0u; i < size; ++i) {
ret.emplace_back({keys[i]}, {values[i]});
}
return ret;
}
};
class Array {
CFArrayRef wrapped;
public:
Array(CFArrayRef wrapped) : wrapped{wrapped} {
// check actually an array or abort
}
std::size_t size() const {
return CFArrayGetCount(wrapped);
}
Value operator[](std::size_t i) const {
return {CFArrayGetValueAtIndex(wrapped, i)};
}
}; The idea would then to do for example
or auto deps = targetObject[CFSTR("dependencies")].as<Array>();
for (auto i = 0u; i < deps.size(); ++i) {
auto dependencyId = deps[i].as<CFStringRef>();
auto dependency = objects[dependencyId].as<Dictionary>();
auto targetId = dependency[CFSTR("target")].as<CFStringRef>();
...
} Do you think it's worth it? |
I picked a bit simpler and less template-heavy approach, will push in a moment. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you already mentioned, the code is not super-easy to follow, but this is in part because of the intrinsic complexity and the APIs. While I'm sure we could come up with some beautiful C++ API wrapping the core foundation one, I also find it's not worth pursuing that for just this piece of code.
Let's , and if the code will hurt in the future we can iterate on it then 🙂
I just run it in a dry-run mode against a bigger sample:
|
No description provided.