Skip to content

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

Merged
merged 12 commits into from
Oct 19, 2022
Merged

Conversation

AlexDenisov
Copy link
Contributor

No description provided.

Copy link
Contributor

@redsun82 redsun82 left a 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 🙂

Comment on lines 8 to 13
swift_cc_binary(
name = "xcode-autobuilder",
srcs = glob([
"*.c",
]),
visibility = ["//swift:__pkg__"],
deps = [
":xcode-autobuilder-lib",
],
)
Copy link
Contributor

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?

Copy link
Contributor Author

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

Comment on lines 369 to 373
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));
Copy link
Contributor

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.

Copy link
Contributor Author

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); }),
Copy link
Contributor

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.

@AlexDenisov
Copy link
Contributor Author

Re: ObjC: I'll give Swift a try, but I think it will be even more messy/hard to read than the ObjC version.

@AlexDenisov AlexDenisov force-pushed the alexdenisov/xcode-autobuilder branch from 00b643a to a7397e4 Compare October 14, 2022 10:45
@AlexDenisov AlexDenisov force-pushed the alexdenisov/xcode-autobuilder branch from a7397e4 to 3040837 Compare October 14, 2022 10:45
Copy link
Contributor

@redsun82 redsun82 left a 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 🙂

Comment on lines 33 to 39
pkg_files(
name = "autobuild",
srcs = ["tools/autobuild.sh"],
attributes = pkg_attributes(mode = "0755"),
prefix = "tools",
)

Copy link
Contributor

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*));
Copy link
Contributor

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.

Copy link
Contributor Author

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 😄

Comment on lines 55 to 59
std::string s;
for (auto& arg : argv) {
s += arg + " ";
}
std::cout << s << "\n";
Copy link
Contributor

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.

Suggested change
std::string s;
for (auto& arg : argv) {
s += arg + " ";
}
std::cout << s << "\n";
for (auto& arg : argv) {
std::cout << arg << " ";
}
std::cout << "\n";

Copy link
Contributor Author

@AlexDenisov AlexDenisov Oct 18, 2022

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);
Copy link
Contributor

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 🙂

https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es49-if-you-must-use-a-cast-use-a-named-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;
Copy link
Contributor

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

Comment on lines +33 to +36
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
return false;
}
return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe just

Suggested change
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
return false;
}
return true;
return WIFEXITED(status) && WEXITSTATUS(status) == 0;

Targets targets;
Dependencies dependencies;
BuildFiles buildFiles;

Copy link
Contributor

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?

Copy link
Contributor Author

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!

Copy link
Contributor Author

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...

Copy link
Contributor

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());
Copy link
Contributor

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

Copy link
Contributor Author

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?

Copy link
Contributor

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",
],
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
)
target_compatible_with = ["@platforms//os:macos"],
)

@AlexDenisov AlexDenisov marked this pull request as ready for review October 18, 2022 07:04
@AlexDenisov AlexDenisov requested a review from a team as a code owner October 18, 2022 07:04
@redsun82
Copy link
Contributor

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

if (auto isa = object[CFSTR("isa")].as<CFStringRef>()) {
  ...
}

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?

@AlexDenisov
Copy link
Contributor Author

AlexDenisov commented Oct 18, 2022

Do you think it's worth it?

I picked a bit simpler and less template-heavy approach, will push in a moment.

Copy link
Contributor

@redsun82 redsun82 left a 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 :shipit: , and if the code will hurt in the future we can iterate on it then 🙂

@AlexDenisov
Copy link
Contributor Author

I just run it in a dry-run mode against a bigger sample:

Total: 2595
Crashed: 2
Errored: 341
Succeeded: 2252

Errored here means either there was no Xcode project/workspace found or there were no suitable targets (e.g., ObjC only project, etc.).
I will take a look at the crashes, but so far the result is pretty solid, so I'm confident we can merge it.

@AlexDenisov AlexDenisov merged commit 5c8fa84 into main Oct 19, 2022
@AlexDenisov AlexDenisov deleted the alexdenisov/xcode-autobuilder branch October 19, 2022 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants