Last Updated: April 23, 2022
rules_xcodeproj has a few high level design goals:
- Only the
xcodeproj
rule is necessary to create a working project - The project can be further customized by using additional rules_xcodeproj rules, or by returning certain rules_xcodeproj providers from custom rules
- Outputs are as similar to
bazel build
as possible - The project feels as “native” in Xcode as possible
- All definition of a project exists in
BUILD
files - The project can be configured to use either Xcode or Bazel as the build system
It’s also worth mentioning a few non-goals of rules_xcodeproj:
- Providing additional non-
xcodeproj
related build rules - Changing the way a target builds in order to enable Xcode features
If all one does is define an xcodeproj
target, then the resulting project
should build and run. There should be no need to adjust the way your workspace
is built in any way, or define any additional intermediary targets.
Bazel allows for some pretty complicated stuff, and not all of it will automatically translate neatly into Xcode’s world. In those cases the project should still build and run (see how in the build mode section), but the project might not be in an ideal state (e.g. schemes might not be the way you want them, or custom rule targets might not be represented ideally). This can be addressed through project customization.
As mentioned above, the default state of using just the xcodeproj
rule might
result in a project that isn’t “ideal”. While the project should be able to
build and run without doing anything else, rules_xcodeproj will support project
customization through the use of additional rules and providers.
At the bare minimum, the xcodeproj
rule depends on the targets that you want
represented in the project. This will generate a project that allows you to
build, and if applicable run, those targets. If possible, all of the transitive
dependencies will also individually be buildable and runnable. Default schemes
will be created for each of these Xcode targets.
What if you don’t like the way the schemes are created (e.g. too many, with incorrect options, or not enough targets per scheme)? Or what if you don’t want all of the transitive dependencies represented in Xcode? Or what if you want to customize how a target is represented, maybe by adding additional Xcode build settings (e.g. to support XCRemoteCache) or Run Script build phases (e.g. to add IDE only linting)?
These scenarios will be handled by additional rules that xcodeproj
can depend
on to customize your project. The key characteristic of these rules is it gives
the project generator control over how their project is setup. This is in
contrast to the other customization point, providers.
The core of the xcodeproj
rule is the xcodeproj_aspect
aspect, which
traverses the dependency graph of the targets passed to an xcodeproj
instance.
The aspect collects information from providers of other rules (i.e.
CcInfo
,
SwiftInfo
,
and various rules_apple providers), as well as information from rules_xcodeproj
providers that it creates. The rule then uses that information to shape the
project that it generates.
The xcodeproj
rule has to make some assumptions about the data it gets, as the
providers from other rules don’t have the fidelity needed to perfectly recreate
a similar Xcode target. rules_xcodeproj will expose providers and associated
helper functions, to allow rules, including your own custom ones, to control how
the xcodeproj
generates targets. The goal being that a default,
non-customized, project is as natural as possible. Rules that return
rules_xcodeproj providers can choose to expose customization points, similar to
the additional rules mentioned above, but that’s not their primary purpose.
By virtue of the first goal, most
Xcode outputs will match those produced by bazel build
. There are other areas
though, e.g. dealing with hermeticity, debugging, and indexing, where additional
care will need to be taken.
Ideally, you shouldn’t be able to tell that a project was generated with rules_xcodeproj. It should look and feel like any other Xcode project. This should also be the case regardless of which build mode is used.
When that isn’t the case, and there needs to be a deviation (mainly to satisfy other constraints), then the generated project should deviate as little as possible. A different build mode might be required to make the project feel more native.
The building blocks of a rules_xcodeproj project generation is the xcodeproj
rule and associated customization rules. Targets using
these rules are defined in BUILD
files, and generating a project happens by
executing bazel run
on an xcodeproj
target. There is no need to create
additional configuration files, or to run additional commands.
Some setups might require something more dynamic, in particular when using the
focused project customization. For these cases the recommended approach, which
we might supply some optional tools for, is to dynamically generate .bzl
files
with macros that create the required targets and use those in your BUILD
files.
The xcodeproj
rule will allow specifying a build mode that should be used by
the generated project. This will allow the project to build with Xcode instead
of Bazel, if that is desired.
Here are a few reasons one may want to build with Xcode instead of Bazel:
- If a new, possibly beta, version of Xcode is released with a feature that Build with Bazel doesn’t support yet, because one of Bazel, rules_apple, rules_swift, or rules_xcodeproj doesn’t support it
- To work around a bug in Bazel, rules_apple, rules_swift, or rules_xcodeproj
- To compare the Bazel and Xcode build systems or build commands
- As a step when migrating to Bazel
In the “Build with Xcode” mode, the generated project will let Xcode orchestrate the build. Xcode Build Settings, target dependencies, etc. are all set up to create a normal Xcode experience.
To ensure that the resulting build is as similar to a Bazel build as
possible, some things are done differently than a vanilla Xcode project. In
particular, BUILT_PRODUCTS_DIR
is set to a nested folder, mirroring the layout
of bazel-out/
, and various search paths are adjusted to account for this.
There are also aspects of a Bazel build that can’t be neatly translated into an
Xcode concept (though updating rules to supply rules_xcodeproj
providers can help). One that will come up in nearly every project
is code generation. In these situations the project takes on a hybrid approach,
invoking bazel build
for a portion of the build. The degree to which bazel build
needs to handle the build depends on the the rules involved.
Of note, projects can be customized to force more of the build to be hybrid, through the use of focused project rules.
In the “Build with Bazel” mode, the generated project will invoke bazel build
to perform the actual build, as a root or detached dependency, and then stub
out the build actions that Xcode tries to perform. A version of this method is
detailed here
and here.
In addition to the stubbing, targets will have their serialized diagnostics replayed, resulting in fully functional warnings and fix-its, that stick around between builds, but also clear when they are supposed to.
This will be the default build mode by the 1.0 release.
rules_xcodeproj will support one more build mode called “Build with Proxy”. In this mode the generated project will rely on Xcode using an XCBBuildServiceProxy. This takes the Xcode build system entirely out of the equation, allowing Bazel to fully control the build.
Here are some benefits that the proxy provides over “Build with Bazel”:
- No additional
BazelDependencies
target in the build graph - Removal of duplicate warnings/errors
- More stable Indexing
- User created schemes (without defining bazel rules to create them) work as expected
- Fully functional progress bar (though there is hope that we can get it to partially work with “Build with Bazel”)
- Detailed build report
- Less overhead
With all of these benefits, you may be wondering why “Build with Bazel” will be the default build mode instead of “Build with Proxy”. There are two main reasons:
- The proxy relies on a private API that Xcode uses to communicate with XCBBuildService. This can have, and has had, breaking changes between Xcode versions.
- To have Xcode use the proxy, you either need to launch it with an environment
variable (via
launchctl setenv
once per boot,/etc/launchd.conf
permanently, orenv VAR=X open -a Xcode.app
), or slightly modify the Xcode app bundle. If using the global environment variable approach (as opposed toopen -a
or a modified bundle), all Xcode versions that are launched will use the same proxy. While the proxy will be written to only build with Bazel for projects generated with rules_xcodeproj, the proxy might be tied to a specific version of Xcode and not work with other versions.
For some teams the benefits outweigh these inconveniences, and there will always be the “Build with Bazel” fallback mode in case something goes wrong.
rules_xcodeproj won’t provide additional non-xcodeproj
related build rules.
That is to say, the only rules that will be provided are xcodeproj
and rules
that interact directly with xcodeproj
. These rules won’t modify your build
graph.
To put it yet another way, rules_xcodeproj won’t provide rules that make your
bazel build
more like Xcode (which is one reason it wasn’t named
“rules_xcode”). Those sorts of rules will have to come from other rulesets.
This non-goal stems from restrictions that SwiftUI previews have, in particular that they aren’t supported with static libraries. rules_xcodeproj won’t change the product type of your target to enable SwiftUI previews; doing so would get in the way of previous goals. We will provide guidance on how to change your build yourself and integrate that change with rules_xcodeproj, but the rules themselves won’t make those changes for you.