<rss xmlns:source="http://source.scripting.com/" version="2.0">
  <channel>
    <title>Adin Ćebić</title>
    <link>https://adincebic.com/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Sun, 13 Sep 2026 18:04:29 +0200</lastBuildDate>
    <item>
      <title>Exploring map_directory in Bazel 9</title>
      <link>https://adincebic.com/2026/09/13/exploring-mapdirectory-in-bazel.html</link>
      <pubDate>Sun, 13 Sep 2026 18:04:29 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/09/13/exploring-mapdirectory-in-bazel.html</guid>
      <description>&lt;p&gt;In the world of Bazel, we prefer to be explicit as much as possible and register actions ahead of time. In fact, this is not just a preference but a strongly enforced rule that enables all the good stuff Bazel gives us.&lt;/p&gt;
&lt;p&gt;However, there is a world where a bit of dynamism is useful—or, in some cases, unavoidable.&lt;/p&gt;
&lt;p&gt;Today I am exploring the little-known &lt;code&gt;map_directory&lt;/code&gt; API that was introduced in Bazel 9.&lt;/p&gt;
&lt;h2 id=&#34;registering-actions-dynamically&#34;&gt;Registering actions dynamically&lt;/h2&gt;
&lt;p&gt;Say we write a rule that generates a couple of files and then want to register a separate Bazel action for each of those files.&lt;/p&gt;
&lt;p&gt;The Bazel API did not facilitate this use case before version 9. Sure, we could have resorted to all sorts of tricks and hacks, but there was no nice native way to register actions based on the contents of a generated directory.&lt;/p&gt;
&lt;p&gt;This is where &lt;code&gt;map_directory&lt;/code&gt; comes in. It allows part of action registration to be deferred until execution time, when the contents of an input directory are known.&lt;/p&gt;
&lt;h2 id=&#34;map_directory-in-action&#34;&gt;map_directory in action&lt;/h2&gt;
&lt;p&gt;The example below demonstrates generating a couple of files and then copying them with separate actions:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bzl&#34; data-lang=&#34;bzl&#34;&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_copy_files&lt;/span&gt;(template_ctx, &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;, input_directories, output_directories, tools, &lt;span style=&#34;color:#f92672&#34;&gt;**&lt;/span&gt;_kwargs):
    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; src &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; input_directories[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;src&amp;#34;&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;children:
        out &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; template_ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;declare_file(src&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;tree_relative_path, directory &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; output_directories[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;out&amp;#34;&lt;/span&gt;])
        &lt;span style=&#34;color:#75715e&#34;&gt;# Here we register one action per file; Bazel executes the command later.&lt;/span&gt;
        &lt;span style=&#34;color:#75715e&#34;&gt;# Each action depends only on its input file and the copy tool.&lt;/span&gt;
        template_ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;run(
            executable &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tools[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;copy&amp;#34;&lt;/span&gt;],
            inputs &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [src],
            outputs &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [out],
            arguments &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [src&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;path, out&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;path],
        )

&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_demo_impl&lt;/span&gt;(ctx):
    src &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;actions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;declare_directory(ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;label&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;_input&amp;#34;&lt;/span&gt;)
    out &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;actions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;declare_directory(ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;label&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;_output&amp;#34;&lt;/span&gt;)

    ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;actions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;run_shell(
        outputs &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [src],
        arguments &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [src&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;path],
        command &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;mkdir -p &amp;#34;$1/nested&amp;#34;
&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;echo hello &amp;gt; &amp;#34;$1/a.txt&amp;#34;
&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;echo world &amp;gt; &amp;#34;$1/nested/b.txt&amp;#34;
&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;,
    )

    copy &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;actions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;declare_file(ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;label&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;_copy.sh&amp;#34;&lt;/span&gt;)
    ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;actions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;write(copy, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;#!/bin/sh&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;exec /bin/cp &amp;#34;$@&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&lt;/span&gt;, is_executable &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)

    ctx&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;actions&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;map_directory(
        input_directories &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;src&amp;#34;&lt;/span&gt;: src},
        output_directories &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;out&amp;#34;&lt;/span&gt;: out},
        tools &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;copy&amp;#34;&lt;/span&gt;: copy},
        mnemonic &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;CopyFile&amp;#34;&lt;/span&gt;,
        implementation &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; _copy_files,
    )

    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; [DefaultInfo(files &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; depset([out]))]

map_directory_demo &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; rule(implementation &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; _demo_impl)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Creating a target from this rule and building it produces an unremarkable result, but it demonstrates what is now possible with this API at our disposal.&lt;/p&gt;
&lt;p&gt;The important part is that &lt;code&gt;_copy_files&lt;/code&gt; does not run during the regular analysis phase. It runs later, once Bazel knows the contents of the &lt;code&gt;src&lt;/code&gt; tree artifact. At that point, it can inspect its children and register an individual copy action for each file.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;While still greatly limited, &lt;code&gt;map_directory&lt;/code&gt; allows us to introduce some controlled dynamism into our Bazel builds and makes certain things possible that previously required considerably more creativity.&lt;/p&gt;
&lt;p&gt;I am pretty sure there are far more interesting examples of this feature being used, so feel free to look around on Github.&lt;/p&gt;
&lt;p&gt;Finally, as always, I suggest consulting the official &lt;a href=&#34;https://bazel.build/rules/lib/builtins/actions#map_directory&#34;&gt;docs&lt;/a&gt;, at least as a reference, as well as the &lt;a href=&#34;https://github.com/bazelbuild/bazel/discussions/28346&#34;&gt;GitHub discussion&lt;/a&gt; around dynamic dependencies.&lt;/p&gt;
</description>
      <source:markdown>In the world of Bazel, we prefer to be explicit as much as possible and register actions ahead of time. In fact, this is not just a preference but a strongly enforced rule that enables all the good stuff Bazel gives us.

However, there is a world where a bit of dynamism is useful—or, in some cases, unavoidable.

Today I am exploring the little-known `map_directory` API that was introduced in Bazel 9.

## Registering actions dynamically

Say we write a rule that generates a couple of files and then want to register a separate Bazel action for each of those files.

The Bazel API did not facilitate this use case before version 9. Sure, we could have resorted to all sorts of tricks and hacks, but there was no nice native way to register actions based on the contents of a generated directory.

This is where `map_directory` comes in. It allows part of action registration to be deferred until execution time, when the contents of an input directory are known.

## map_directory in action

The example below demonstrates generating a couple of files and then copying them with separate actions:

```bzl
def _copy_files(template_ctx, *, input_directories, output_directories, tools, **_kwargs):
    for src in input_directories[&#34;src&#34;].children:
        out = template_ctx.declare_file(src.tree_relative_path, directory = output_directories[&#34;out&#34;])
        # Here we register one action per file; Bazel executes the command later.
        # Each action depends only on its input file and the copy tool.
        template_ctx.run(
            executable = tools[&#34;copy&#34;],
            inputs = [src],
            outputs = [out],
            arguments = [src.path, out.path],
        )

def _demo_impl(ctx):
    src = ctx.actions.declare_directory(ctx.label.name + &#34;_input&#34;)
    out = ctx.actions.declare_directory(ctx.label.name + &#34;_output&#34;)

    ctx.actions.run_shell(
        outputs = [src],
        arguments = [src.path],
        command = &#34;&#34;&#34;
mkdir -p &#34;$1/nested&#34;
echo hello &gt; &#34;$1/a.txt&#34;
echo world &gt; &#34;$1/nested/b.txt&#34;
&#34;&#34;&#34;,
    )

    copy = ctx.actions.declare_file(ctx.label.name + &#34;_copy.sh&#34;)
    ctx.actions.write(copy, &#39;#!/bin/sh\nexec /bin/cp &#34;$@&#34;\n&#39;, is_executable = True)

    ctx.actions.map_directory(
        input_directories = {&#34;src&#34;: src},
        output_directories = {&#34;out&#34;: out},
        tools = {&#34;copy&#34;: copy},
        mnemonic = &#34;CopyFile&#34;,
        implementation = _copy_files,
    )

    return [DefaultInfo(files = depset([out]))]

map_directory_demo = rule(implementation = _demo_impl)
```

Creating a target from this rule and building it produces an unremarkable result, but it demonstrates what is now possible with this API at our disposal.

The important part is that `_copy_files` does not run during the regular analysis phase. It runs later, once Bazel knows the contents of the `src` tree artifact. At that point, it can inspect its children and register an individual copy action for each file.

## Conclusion

While still greatly limited, `map_directory` allows us to introduce some controlled dynamism into our Bazel builds and makes certain things possible that previously required considerably more creativity.

I am pretty sure there are far more interesting examples of this feature being used, so feel free to look around on Github.

Finally, as always, I suggest consulting the official [docs](https://bazel.build/rules/lib/builtins/actions#map_directory), at least as a reference, as well as the [GitHub discussion](https://github.com/bazelbuild/bazel/discussions/28346) around dynamic dependencies.
</source:markdown>
    </item>
    
    <item>
      <title>Bazel, APFS Clones, and Disk Space</title>
      <link>https://adincebic.com/2026/09/06/bazel-apfs-clones-and-disk.html</link>
      <pubDate>Sun, 06 Sep 2026 17:22:12 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/09/06/bazel-apfs-clones-and-disk.html</guid>
      <description>&lt;p&gt;I was recently writing about the &lt;a href=&#34;https://adincebic.com/2026/08/30/how-to-reduce-bazel-disk.html&#34;&gt;problem of disk space usage&lt;/a&gt; when building with Bazel due to its many caches. That led me to pay more attention to the work being done in this area, and I discovered that Bazel 9.3.0 is expected to make better use of copy-on-write cloning on macOS &lt;a href=&#34;https://github.com/bazelbuild/bazel/pull/30776&#34;&gt;#30776&lt;/a&gt; to avoid unnecessarily duplicating files between the disk cache and the output base.&lt;/p&gt;
&lt;p&gt;On APFS, this essentially means copy-on-write: the cloned files initially share the same underlying data blocks, so they don&amp;rsquo;t immediately take up twice the physical disk space. If either copy is modified, the filesystem only needs to allocate storage for the changed blocks.&lt;/p&gt;
&lt;h2 id=&#34;using-apfs-clones-from-swift&#34;&gt;Using APFS clones from Swift&lt;/h2&gt;
&lt;p&gt;Naturally, I got interested in learning how this feature works, and it turns out to be pretty simple. There is a low-level API function, &lt;code&gt;clonefile(...)&lt;/code&gt;, which does exactly what you would expect: it creates a copy-on-write clone of a file.&lt;/p&gt;
&lt;p&gt;If I were to wrap that function in Swift, here is roughly how I would do it. In production code, I would also provide better error handling:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;clone&lt;/span&gt;(from source: String, to destination: String) -&amp;gt; Bool {
    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; source = source.cString(using: .utf8)
    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; destination = destination.cString(using: .utf8)
    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; result = clonefile(source, destination, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;)
    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; result == &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I recommend taking a look at its &lt;a href=&#34;https://keith.github.io/xcode-man-pages/clonefile.2.html&#34;&gt;man page&lt;/a&gt; to learn more about it.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I wanted to share this because I feel like APFS cloning is not talked about enough. It&amp;rsquo;s a simple API backed by a pretty powerful filesystem feature, and I hope you find it handy someday.&lt;/p&gt;
</description>
      <source:markdown>I was recently writing about the [problem of disk space usage](https://adincebic.com/2026/08/30/how-to-reduce-bazel-disk.html) when building with Bazel due to its many caches. That led me to pay more attention to the work being done in this area, and I discovered that Bazel 9.3.0 is expected to make better use of copy-on-write cloning on macOS [#30776](https://github.com/bazelbuild/bazel/pull/30776) to avoid unnecessarily duplicating files between the disk cache and the output base.

On APFS, this essentially means copy-on-write: the cloned files initially share the same underlying data blocks, so they don&#39;t immediately take up twice the physical disk space. If either copy is modified, the filesystem only needs to allocate storage for the changed blocks.

## Using APFS clones from Swift

Naturally, I got interested in learning how this feature works, and it turns out to be pretty simple. There is a low-level API function, `clonefile(...)`, which does exactly what you would expect: it creates a copy-on-write clone of a file.

If I were to wrap that function in Swift, here is roughly how I would do it. In production code, I would also provide better error handling:

```swift
func clone(from source: String, to destination: String) -&gt; Bool {
    let source = source.cString(using: .utf8)
    let destination = destination.cString(using: .utf8)
    let result = clonefile(source, destination, 0)
    return result == 0
}
```

I recommend taking a look at its [man page](https://keith.github.io/xcode-man-pages/clonefile.2.html) to learn more about it.

## Conclusion

I wanted to share this because I feel like APFS cloning is not talked about enough. It&#39;s a simple API backed by a pretty powerful filesystem feature, and I hope you find it handy someday.
</source:markdown>
    </item>
    
    <item>
      <title>How to Reduce Bazel Disk Space Usage Across Git Worktrees</title>
      <link>https://adincebic.com/2026/08/30/how-to-reduce-bazel-disk.html</link>
      <pubDate>Sun, 30 Aug 2026 18:59:22 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/08/30/how-to-reduce-bazel-disk.html</guid>
      <description>&lt;p&gt;Ever since the advent of coding agents, a lot of engineers have started utilizing Git worktrees and, by extension, multiple Bazel output bases. We all know the story: Bazel caches tend to take up a lot of disk space when working with large projects. Manual deletion and garbage collection can only take you so far.&lt;/p&gt;
&lt;h2 id=&#34;enter-bb-clientd&#34;&gt;Enter bb-clientd&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/buildbarn/bb-clientd&#34;&gt;bb-clientd&lt;/a&gt; is a daemon that runs on your machine and can act as a local remote cache and proxy for Bazel.&lt;/p&gt;
&lt;p&gt;More importantly, it also implements Bazel&amp;rsquo;s &lt;a href=&#34;https://blog.bazel.build/2024/07/23/remote-output-service.html&#34;&gt;Output Service protocol&lt;/a&gt;, introduced in Bazel 7.2. This allows &lt;code&gt;bb_clientd&lt;/code&gt; to manage Bazel&amp;rsquo;s output tree through a virtual filesystem—FUSE on Linux and NFSv4 on macOS—and lazily materialize files when they are actually accessed.&lt;/p&gt;
&lt;p&gt;Combined with its content-addressed local cache, this means that multiple Bazel output bases can reuse the same cached content instead of each storing their own copies of identical files. This is particularly useful when working with multiple Git worktrees.&lt;/p&gt;
&lt;p&gt;Because &lt;a href=&#34;https://github.com/buildbarn/bb-clientd&#34;&gt;bb-clientd&lt;/a&gt; already has a nice README, there is no point in me explaining the setup in detail. But here is an example &lt;code&gt;.bazelrc&lt;/code&gt; configuration that I keep in my global &lt;code&gt;.bazelrc&lt;/code&gt; so I can enable it when needed:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-bazelrc&#34; data-lang=&#34;bazelrc&#34;&gt;common:bb_clientd --disk_cache=
common:bb_clientd --remote_cache=unix:///Users/&amp;lt;user&amp;gt;/Library/Caches/bb_clientd/grpc
common:bb_clientd --remote_instance_name=local/projects
common:bb_clientd --remote_upload_local_results
common:bb_clientd --experimental_remote_output_service=unix:///Users/&amp;lt;user&amp;gt;/Library/Caches/bb_clientd/grpc
common:bb_clientd --experimental_remote_output_service_output_path_prefix=/Users/&amp;lt;user&amp;gt;/bb_clientd/outputs
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This lets me use just the local cache with:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;bazel build --config&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;bb_clientd //...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you&amp;rsquo;re using &lt;code&gt;rules_swift&lt;/code&gt;, make sure to check out the &lt;code&gt;swift.module_map_home_is_cwd&lt;/code&gt; feature if you&amp;rsquo;re not using the Xcode toolchain. It makes module-map generation and compilation assume that header paths are relative to the workspace root, which can be important when using a virtualized output tree.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This is one of those things that I don&amp;rsquo;t see discussed enough in the Bazel community, yet it can save gigabytes of disk space—especially if you&amp;rsquo;re regularly working across multiple worktrees.&lt;/p&gt;
</description>
      <source:markdown>Ever since the advent of coding agents, a lot of engineers have started utilizing Git worktrees and, by extension, multiple Bazel output bases. We all know the story: Bazel caches tend to take up a lot of disk space when working with large projects. Manual deletion and garbage collection can only take you so far.

## Enter bb-clientd

[bb-clientd](https://github.com/buildbarn/bb-clientd) is a daemon that runs on your machine and can act as a local remote cache and proxy for Bazel.

More importantly, it also implements Bazel&#39;s [Output Service protocol](https://blog.bazel.build/2024/07/23/remote-output-service.html), introduced in Bazel 7.2. This allows `bb_clientd` to manage Bazel&#39;s output tree through a virtual filesystem—FUSE on Linux and NFSv4 on macOS—and lazily materialize files when they are actually accessed.

Combined with its content-addressed local cache, this means that multiple Bazel output bases can reuse the same cached content instead of each storing their own copies of identical files. This is particularly useful when working with multiple Git worktrees.

Because [bb-clientd](https://github.com/buildbarn/bb-clientd) already has a nice README, there is no point in me explaining the setup in detail. But here is an example `.bazelrc` configuration that I keep in my global `.bazelrc` so I can enable it when needed:

```bazelrc
common:bb_clientd --disk_cache=
common:bb_clientd --remote_cache=unix:///Users/&lt;user&gt;/Library/Caches/bb_clientd/grpc
common:bb_clientd --remote_instance_name=local/projects
common:bb_clientd --remote_upload_local_results
common:bb_clientd --experimental_remote_output_service=unix:///Users/&lt;user&gt;/Library/Caches/bb_clientd/grpc
common:bb_clientd --experimental_remote_output_service_output_path_prefix=/Users/&lt;user&gt;/bb_clientd/outputs
```

This lets me use just the local cache with:

```sh
bazel build --config=bb_clientd //...
```

**NOTE:** If you&#39;re using `rules_swift`, make sure to check out the `swift.module_map_home_is_cwd` feature if you&#39;re not using the Xcode toolchain. It makes module-map generation and compilation assume that header paths are relative to the workspace root, which can be important when using a virtualized output tree.

## Conclusion

This is one of those things that I don&#39;t see discussed enough in the Bazel community, yet it can save gigabytes of disk space—especially if you&#39;re regularly working across multiple worktrees.
</source:markdown>
    </item>
    
    <item>
      <title>Preventing Transitive Swift Imports with Bazel</title>
      <link>https://adincebic.com/2026/08/23/preventing-transitive-swift-imports-with.html</link>
      <pubDate>Sun, 23 Aug 2026 21:27:01 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/08/23/preventing-transitive-swift-imports-with.html</guid>
      <description>&lt;p&gt;Swift&amp;rsquo;s permissive nature when it comes to module dependencies has always annoyed me and made my life a bit harder than I expected. Of course, I&amp;rsquo;m talking about transitive module imports.&lt;/p&gt;
&lt;p&gt;You know the situation: there is a &lt;code&gt;ModuleA&lt;/code&gt; which is a dependency of &lt;code&gt;ModuleB&lt;/code&gt;, and if you declare &lt;code&gt;ModuleB&lt;/code&gt; as a dependency of &lt;code&gt;ModuleC&lt;/code&gt;, &lt;code&gt;ModuleC&lt;/code&gt; can import &lt;code&gt;ModuleA&lt;/code&gt; even though it never declared &lt;code&gt;ModuleA&lt;/code&gt; as its own direct dependency.&lt;/p&gt;
&lt;p&gt;There are a lot of reasons why you might not want that, from making dependencies harder to visualize to enforcing a certain module structure, and so on.&lt;/p&gt;
&lt;p&gt;Unfortunately, Swift itself does not provide a mechanism to enforce direct module dependencies—or at least I don&amp;rsquo;t know of one.&lt;/p&gt;
&lt;h2 id=&#34;layering-check-via-rules_swift&#34;&gt;Layering check via rules_swift&lt;/h2&gt;
&lt;p&gt;One of the reasons I like working with Bazel is the flexibility it provides. One example of that is a feature in &lt;a href=&#34;https://github.com/bazelbuild/rules_swift&#34;&gt;rules_swift&lt;/a&gt; called &lt;strong&gt;layering check&lt;/strong&gt;, which prevents importing modules that aren&amp;rsquo;t declared as direct dependencies.&lt;/p&gt;
&lt;p&gt;It was introduced a couple of months ago in &lt;a href=&#34;https://github.com/bazelbuild/rules_swift/pull/1780&#34;&gt;#1780&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So, for example:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;swift_library(
    name = &amp;quot;Bottom&amp;quot;,
    srcs = [&amp;quot;Bottom.swift&amp;quot;],
    module_name = &amp;quot;Bottom&amp;quot;,
)

swift_library(
    name = &amp;quot;Middle&amp;quot;,
    srcs = [&amp;quot;Middle.swift&amp;quot;],
    module_name = &amp;quot;Middle&amp;quot;,
    deps = [&amp;quot;:Bottom&amp;quot;],
)

swift_library(
    name = &amp;quot;Top&amp;quot;,
    srcs = [&amp;quot;Top.swift&amp;quot;],
    module_name = &amp;quot;Top&amp;quot;,
    deps = [&amp;quot;:Middle&amp;quot;],
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now let&amp;rsquo;s say &lt;code&gt;Top.swift&lt;/code&gt; tries to do this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Middle&lt;/span&gt;
&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Bottom&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Even though &lt;code&gt;Bottom&lt;/code&gt; is available transitively through &lt;code&gt;Middle&lt;/code&gt;, &lt;code&gt;Top&lt;/code&gt; never declared it as a direct dependency.&lt;/p&gt;
&lt;p&gt;With the Swift layering check enabled, building it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;bazel build //:Top --features&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;swift.layering_check_swift
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;should fail with an error resembling:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;Layering violation in //path/to/package:Top
  The following modules were imported, but they are not direct dependencies:

      Bottom

  Please add the correct &amp;#39;deps&amp;#39; ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And that&amp;rsquo;s exactly what I want.&lt;/p&gt;
&lt;p&gt;To fix it, simply declare &lt;code&gt;Bottom&lt;/code&gt; as a direct Bazel dependency:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;swift_library(
    name = &amp;quot;Top&amp;quot;,
    srcs = [&amp;quot;Top.swift&amp;quot;],
    module_name = &amp;quot;Top&amp;quot;,
    deps = [
        &amp;quot;:Bottom&amp;quot;,
        &amp;quot;:Middle&amp;quot;,
    ],
)
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This is one of those features I wish more people knew about and used. It gives you much tighter control over your dependency graph and makes it harder for accidental dependencies to get imported.&lt;/p&gt;
</description>
      <source:markdown>Swift&#39;s permissive nature when it comes to module dependencies has always annoyed me and made my life a bit harder than I expected. Of course, I&#39;m talking about transitive module imports.

You know the situation: there is a `ModuleA` which is a dependency of `ModuleB`, and if you declare `ModuleB` as a dependency of `ModuleC`, `ModuleC` can import `ModuleA` even though it never declared `ModuleA` as its own direct dependency.

There are a lot of reasons why you might not want that, from making dependencies harder to visualize to enforcing a certain module structure, and so on.

Unfortunately, Swift itself does not provide a mechanism to enforce direct module dependencies—or at least I don&#39;t know of one.

## Layering check via rules_swift

One of the reasons I like working with Bazel is the flexibility it provides. One example of that is a feature in [rules_swift](https://github.com/bazelbuild/rules_swift) called **layering check**, which prevents importing modules that aren&#39;t declared as direct dependencies.

It was introduced a couple of months ago in [#1780](https://github.com/bazelbuild/rules_swift/pull/1780).

So, for example:

```starlark
swift_library(
    name = &#34;Bottom&#34;,
    srcs = [&#34;Bottom.swift&#34;],
    module_name = &#34;Bottom&#34;,
)

swift_library(
    name = &#34;Middle&#34;,
    srcs = [&#34;Middle.swift&#34;],
    module_name = &#34;Middle&#34;,
    deps = [&#34;:Bottom&#34;],
)

swift_library(
    name = &#34;Top&#34;,
    srcs = [&#34;Top.swift&#34;],
    module_name = &#34;Top&#34;,
    deps = [&#34;:Middle&#34;],
)
```

Now let&#39;s say `Top.swift` tries to do this:

```swift
import Middle
import Bottom
```

Even though `Bottom` is available transitively through `Middle`, `Top` never declared it as a direct dependency.

With the Swift layering check enabled, building it:

```bash
bazel build //:Top --features=swift.layering_check_swift
```

should fail with an error resembling:

```text
Layering violation in //path/to/package:Top
  The following modules were imported, but they are not direct dependencies:

      Bottom

  Please add the correct &#39;deps&#39; ...
```

And that&#39;s exactly what I want.

To fix it, simply declare `Bottom` as a direct Bazel dependency:

```starlark
swift_library(
    name = &#34;Top&#34;,
    srcs = [&#34;Top.swift&#34;],
    module_name = &#34;Top&#34;,
    deps = [
        &#34;:Bottom&#34;,
        &#34;:Middle&#34;,
    ],
)
```

## Conclusion

This is one of those features I wish more people knew about and used. It gives you much tighter control over your dependency graph and makes it harder for accidental dependencies to get imported.
</source:markdown>
    </item>
    
    <item>
      <title>Profiling Starlark Memory Usage in Bazel</title>
      <link>https://adincebic.com/2026/08/16/profiling-starlark-memory-usage-in.html</link>
      <pubDate>Sun, 16 Aug 2026 18:15:22 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/08/16/profiling-starlark-memory-usage-in.html</guid>
      <description>&lt;p&gt;When using Bazel within a large monorepo, there comes a time when memory starts becoming a problem, and it is important to be aware of the available tools that can help with diagnosis. Fortunately, Bazel offers Starlark memory profiling, which we can utilize to see how much memory is being swallowed by the analysis phase.&lt;/p&gt;
&lt;h2 id=&#34;setting-up&#34;&gt;Setting up&lt;/h2&gt;
&lt;p&gt;Surprisingly, it is a little tedious to set this up, but in the end, it is not that difficult either.&lt;/p&gt;
&lt;p&gt;First, we need to download the allocation instrumenter JAR from &lt;a href=&#34;https://repo1.maven.org/maven2/com/google/code/java-allocation-instrumenter/java-allocation-instrumenter/3.3.4&#34;&gt;Maven Central&lt;/a&gt;. Once that&amp;rsquo;s on disk, we can start Bazel in memory tracking mode like so:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;bazel --host_jvm_args&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;-javaagent:/path/to/java-allocation-instrumenter-3.3.4.jar &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;      --host_jvm_args&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;-DRULE_MEMORY_TRACKER&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;      build --nobuild //path/to/target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here, we start Bazel with memory tracking enabled and pass &lt;code&gt;--nobuild&lt;/code&gt; so that only analysis is performed, which is what we want to measure.&lt;/p&gt;
&lt;p&gt;After analysis completes, make sure not to shut down the Bazel server because you&amp;rsquo;ll lose the gathered data.&lt;/p&gt;
&lt;p&gt;Now, to produce a memory profile, we&amp;rsquo;ll use Bazel&amp;rsquo;s &lt;code&gt;dump&lt;/code&gt; command:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;bazel --host_jvm_args&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;-javaagent:/path/to/java-allocation-instrumenter-3.3.4.jar &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;      --host_jvm_args&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;-DRULE_MEMORY_TRACKER&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;      dump --skylark_memory&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;memory_profile.gz
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notice how we repeat the startup flags:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;--host_jvm_args=-javaagent:/path/to/java-allocation-instrumenter-3.3.4.jar
--host_jvm_args=-DRULE_MEMORY_TRACKER=1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Failing to do so will result in a Bazel server restart, and you&amp;rsquo;ll have to repeat the whole process from the beginning.&lt;/p&gt;
&lt;h2 id=&#34;dealing-with-gathered-data&#34;&gt;Dealing with gathered data&lt;/h2&gt;
&lt;p&gt;Now that you have &lt;code&gt;memory_profile.gz&lt;/code&gt;, you need to make sense of it. The Bazel docs recommend using &lt;a href=&#34;https://github.com/google/pprof&#34;&gt;pprof&lt;/a&gt; to inspect the profile.&lt;/p&gt;
&lt;p&gt;There doesn&amp;rsquo;t appear to be a &lt;code&gt;pprof&lt;/code&gt; formula in the main Homebrew formula index, but installing it is straightforward if you already have Go:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;go install github.com/google/pprof@latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The binary is installed into &lt;code&gt;$GOPATH/bin&lt;/code&gt;, which is &lt;code&gt;$HOME/go/bin&lt;/code&gt; by default.&lt;/p&gt;
&lt;p&gt;Once installed, a useful starting point is a flame graph:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;pprof -flame memory_profile.gz
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or a text report annotated with source lines:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;pprof -text -lines memory_profile.gz
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Most Bazel users won&amp;rsquo;t ever need to gather a memory profile, but I believe it is good to be aware that this exists so you can reach for it when needed. Rule authors should be especially aware of it, as it is not that difficult to make a coding mistake in a rule that results in huge memory usage.&lt;/p&gt;
&lt;p&gt;You can find more information about optimizing performance in the &lt;a href=&#34;https://bazel.build/versions/9.0.0/rules/performance&#34;&gt;official Bazel docs&lt;/a&gt;.&lt;/p&gt;
</description>
      <source:markdown>When using Bazel within a large monorepo, there comes a time when memory starts becoming a problem, and it is important to be aware of the available tools that can help with diagnosis. Fortunately, Bazel offers Starlark memory profiling, which we can utilize to see how much memory is being swallowed by the analysis phase.

## Setting up

Surprisingly, it is a little tedious to set this up, but in the end, it is not that difficult either.

First, we need to download the allocation instrumenter JAR from [Maven Central](https://repo1.maven.org/maven2/com/google/code/java-allocation-instrumenter/java-allocation-instrumenter/3.3.4). Once that&#39;s on disk, we can start Bazel in memory tracking mode like so:

```shell
bazel --host_jvm_args=-javaagent:/path/to/java-allocation-instrumenter-3.3.4.jar \
      --host_jvm_args=-DRULE_MEMORY_TRACKER=1 \
      build --nobuild //path/to/target
```

Here, we start Bazel with memory tracking enabled and pass `--nobuild` so that only analysis is performed, which is what we want to measure.

After analysis completes, make sure not to shut down the Bazel server because you&#39;ll lose the gathered data.

Now, to produce a memory profile, we&#39;ll use Bazel&#39;s `dump` command:

```shell
bazel --host_jvm_args=-javaagent:/path/to/java-allocation-instrumenter-3.3.4.jar \
      --host_jvm_args=-DRULE_MEMORY_TRACKER=1 \
      dump --skylark_memory=memory_profile.gz
```

Notice how we repeat the startup flags:

```text
--host_jvm_args=-javaagent:/path/to/java-allocation-instrumenter-3.3.4.jar
--host_jvm_args=-DRULE_MEMORY_TRACKER=1
```

Failing to do so will result in a Bazel server restart, and you&#39;ll have to repeat the whole process from the beginning.

## Dealing with gathered data

Now that you have `memory_profile.gz`, you need to make sense of it. The Bazel docs recommend using [pprof](https://github.com/google/pprof) to inspect the profile.

There doesn&#39;t appear to be a `pprof` formula in the main Homebrew formula index, but installing it is straightforward if you already have Go:

```shell
go install github.com/google/pprof@latest
```

The binary is installed into `$GOPATH/bin`, which is `$HOME/go/bin` by default.

Once installed, a useful starting point is a flame graph:

```shell
pprof -flame memory_profile.gz
```

or a text report annotated with source lines:

```shell
pprof -text -lines memory_profile.gz
```

## Conclusion

Most Bazel users won&#39;t ever need to gather a memory profile, but I believe it is good to be aware that this exists so you can reach for it when needed. Rule authors should be especially aware of it, as it is not that difficult to make a coding mistake in a rule that results in huge memory usage.

You can find more information about optimizing performance in the [official Bazel docs](https://bazel.build/versions/9.0.0/rules/performance).
</source:markdown>
    </item>
    
    <item>
      <title>Testing Bazel Remote Build Execution Locally with actiond</title>
      <link>https://adincebic.com/2026/08/09/testing-bazel-remote-build-execution.html</link>
      <pubDate>Sun, 09 Aug 2026 18:50:43 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/08/09/testing-bazel-remote-build-execution.html</guid>
      <description>&lt;p&gt;When configuring remote build execution, it is very important to test changes locally so you don&amp;rsquo;t waste time on CI. A lot of the time, people don&amp;rsquo;t even have access to an RBE environment. Yes, &lt;a href=&#34;https://www.buildbuddy.io&#34;&gt;BuildBuddy&lt;/a&gt; offers their service for free for open-source projects, but I feel like having access to an RBE environment locally is invaluable.&lt;/p&gt;
&lt;p&gt;I only recently started utilizing RBE more seriously, and while working through that setup I accidentally discovered &lt;a href=&#34;https://github.com/hermeticbuild/actiond&#34;&gt;actiond&lt;/a&gt;. I really wish I had known about it earlier. Having a remote executor that you can run locally makes experimenting with RBE, debugging issues, and checking whether your targets actually execute remotely much easier.&lt;/p&gt;
&lt;h2 id=&#34;actiond-from-hermeticbuild&#34;&gt;actiond from hermeticbuild&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/hermeticbuild/actiond&#34;&gt;actiond&lt;/a&gt; is a full-fledged remote build executor that you can spin up easily simply by downloading it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;curl -L &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;  https://github.com/hermeticbuild/actiond/releases/latest/download/darwin-actiond_macos_arm64 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;  -o darwin-actiond_macos_arm64
curl -L &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;  https://github.com/hermeticbuild/actiond/releases/latest/download/SHA256.txt &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;  -o SHA256.txt
grep &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39; darwin-actiond_macos_arm64$&amp;#39;&lt;/span&gt; SHA256.txt | shasum -a &lt;span style=&#34;color:#ae81ff&#34;&gt;256&lt;/span&gt; -c -
chmod +x darwin-actiond_macos_arm64
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And then just run it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;./darwin-actiond_macos_arm64 serve-vm &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;  --listen&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;127.0.0.1:8980 &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;  --root&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$HOME&lt;span style=&#34;color:#e6db74&#34;&gt;/Library/Caches/actiond/vm&amp;#34;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; I assume you&amp;rsquo;re running on macOS.&lt;/p&gt;
&lt;h2 id=&#34;pointing-bazel-at-it&#34;&gt;Pointing Bazel at it&lt;/h2&gt;
&lt;p&gt;It is a matter of setting a few flags, which is best done in &lt;code&gt;.bazelrc&lt;/code&gt;, as I can&amp;rsquo;t imagine anyone manually passing them every time:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-bazelrc&#34; data-lang=&#34;bazelrc&#34;&gt;build:local_rbe --remote_executor=grpc://127.0.0.1:8980
build:local_rbe --remote_cache=grpc://127.0.0.1:8980
build:local_rbe --platforms=//:linux_x86_64
build:local_rbe --extra_execution_platforms=//:linux_x86_64
build:local_rbe --spawn_strategy=remote
build:local_rbe --genrule_strategy=remote
build:local_rbe --remote_local_fallback=false
build:local_rbe --remote_upload_local_results=false
build:local_rbe --noremote_cache_compression
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then build with &lt;code&gt;bazel build --config=local_rbe //...&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This is an incredibly easy way to test whether all your targets build remotely, either when preparing to use a real RBE service or as a rule author wanting to ensure that your rules are fully hermetic and can execute remotely.&lt;/p&gt;
&lt;p&gt;I wish I had found &lt;code&gt;actiond&lt;/code&gt; earlier, because it removes a lot of the friction from experimenting with RBE locally and makes it much easier to catch remote-execution problems before they reach CI.&lt;/p&gt;
</description>
      <source:markdown>When configuring remote build execution, it is very important to test changes locally so you don&#39;t waste time on CI. A lot of the time, people don&#39;t even have access to an RBE environment. Yes, [BuildBuddy](https://www.buildbuddy.io) offers their service for free for open-source projects, but I feel like having access to an RBE environment locally is invaluable.

I only recently started utilizing RBE more seriously, and while working through that setup I accidentally discovered [actiond](https://github.com/hermeticbuild/actiond). I really wish I had known about it earlier. Having a remote executor that you can run locally makes experimenting with RBE, debugging issues, and checking whether your targets actually execute remotely much easier.

## actiond from hermeticbuild

[actiond](https://github.com/hermeticbuild/actiond) is a full-fledged remote build executor that you can spin up easily simply by downloading it:

```bash
curl -L \
  https://github.com/hermeticbuild/actiond/releases/latest/download/darwin-actiond_macos_arm64 \
  -o darwin-actiond_macos_arm64
curl -L \
  https://github.com/hermeticbuild/actiond/releases/latest/download/SHA256.txt \
  -o SHA256.txt
grep &#39; darwin-actiond_macos_arm64$&#39; SHA256.txt | shasum -a 256 -c -
chmod +x darwin-actiond_macos_arm64
```

And then just run it:

```bash
./darwin-actiond_macos_arm64 serve-vm \
  --listen=127.0.0.1:8980 \
  --root=&#34;$HOME/Library/Caches/actiond/vm&#34;
```

**NOTE:** I assume you&#39;re running on macOS.

## Pointing Bazel at it

It is a matter of setting a few flags, which is best done in `.bazelrc`, as I can&#39;t imagine anyone manually passing them every time:

```bazelrc
build:local_rbe --remote_executor=grpc://127.0.0.1:8980
build:local_rbe --remote_cache=grpc://127.0.0.1:8980
build:local_rbe --platforms=//:linux_x86_64
build:local_rbe --extra_execution_platforms=//:linux_x86_64
build:local_rbe --spawn_strategy=remote
build:local_rbe --genrule_strategy=remote
build:local_rbe --remote_local_fallback=false
build:local_rbe --remote_upload_local_results=false
build:local_rbe --noremote_cache_compression
```

Then build with `bazel build --config=local_rbe //...`.

## Conclusion

This is an incredibly easy way to test whether all your targets build remotely, either when preparing to use a real RBE service or as a rule author wanting to ensure that your rules are fully hermetic and can execute remotely.

I wish I had found `actiond` earlier, because it removes a lot of the friction from experimenting with RBE locally and makes it much easier to catch remote-execution problems before they reach CI.
</source:markdown>
    </item>
    
    <item>
      <title>Less Boilerplate for Bazel Transitions</title>
      <link>https://adincebic.com/2026/08/02/less-boilerplate-for-bazel-transitions.html</link>
      <pubDate>Sun, 02 Aug 2026 18:41:58 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/08/02/less-boilerplate-for-bazel-transitions.html</guid>
      <description>&lt;p&gt;I’ve written about Bazel transitions multiple times: first when demonstrating &lt;a href=&#34;https://adincebic.com/2026/02/22/applying-bazel-transitions-to-thirdparty.html&#34;&gt;rule extensions&lt;/a&gt;, and then when explaining &lt;a href=&#34;https://adincebic.com/2026/03/22/bazel-split-transitions.html&#34;&gt;split transitions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now I want to showcase a community-built way to apply transitions more easily.&lt;/p&gt;
&lt;h2 id=&#34;enter-with_cfgbzl&#34;&gt;Enter with_cfg.bzl&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/fmeum/with_cfg.bzl&#34;&gt;with_cfg.bzl&lt;/a&gt; is a convenient way to apply Bazel transitions. It was created by well-known community member &lt;a href=&#34;https://github.com/fmeum&#34;&gt;Fabian&lt;/a&gt;, and I can’t recommend it enough.&lt;/p&gt;
&lt;h2 id=&#34;applying-a-transition-to-a-plain-swift_library&#34;&gt;Applying a transition to a plain swift_library&lt;/h2&gt;
&lt;p&gt;Say we want to ensure that our &lt;code&gt;swift_library&lt;/code&gt; is always built with &lt;code&gt;--compilation_mode=opt&lt;/code&gt;. We can achieve that quite easily with the following &lt;code&gt;.bzl&lt;/code&gt; file:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;load(&amp;quot;@rules_swift//swift:swift_library.bzl&amp;quot;, &amp;quot;swift_library&amp;quot;)
load(&amp;quot;@with_cfg.bzl&amp;quot;, &amp;quot;with_cfg&amp;quot;)

opt_swift_library, _opt_swift_library_internal = (
    with_cfg(swift_library)
        .set(&amp;quot;compilation_mode&amp;quot;, &amp;quot;opt&amp;quot;)
        .build()
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That’s it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; You can ignore &lt;code&gt;_opt_swift_library_internal&lt;/code&gt;. It needs to be assigned to a global variable because of Bazel’s restrictions on rule definitions, but you aren’t supposed to use it directly. Instead, use the generated &lt;code&gt;opt_swift_library&lt;/code&gt; macro.&lt;/p&gt;
&lt;p&gt;Finally, load &lt;code&gt;opt_swift_library&lt;/code&gt; from the &lt;code&gt;.bzl&lt;/code&gt; file containing the code above and use it just like a regular &lt;code&gt;swift_library&lt;/code&gt;. The target and all its transitive dependencies will be built with the &lt;code&gt;opt&lt;/code&gt; compilation mode.&lt;/p&gt;
&lt;h2 id=&#34;prior-art&#34;&gt;Prior art&lt;/h2&gt;
&lt;p&gt;I won’t go into detail about what this would look like without &lt;a href=&#34;https://github.com/fmeum/with_cfg.bzl&#34;&gt;with_cfg.bzl&lt;/a&gt;, as that’s already covered in my earlier articles.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Unfortunately, I discovered this way too late, but I’m glad it exists. It makes transitions much less tedious to write and reason about.&lt;/p&gt;
</description>
      <source:markdown>I’ve written about Bazel transitions multiple times: first when demonstrating [rule extensions](https://adincebic.com/2026/02/22/applying-bazel-transitions-to-thirdparty.html), and then when explaining [split transitions](https://adincebic.com/2026/03/22/bazel-split-transitions.html).

Now I want to showcase a community-built way to apply transitions more easily.

## Enter with_cfg.bzl

[with_cfg.bzl](https://github.com/fmeum/with_cfg.bzl) is a convenient way to apply Bazel transitions. It was created by well-known community member [Fabian](https://github.com/fmeum), and I can’t recommend it enough.

## Applying a transition to a plain swift_library

Say we want to ensure that our `swift_library` is always built with `--compilation_mode=opt`. We can achieve that quite easily with the following `.bzl` file:

```starlark
load(&#34;@rules_swift//swift:swift_library.bzl&#34;, &#34;swift_library&#34;)
load(&#34;@with_cfg.bzl&#34;, &#34;with_cfg&#34;)

opt_swift_library, _opt_swift_library_internal = (
    with_cfg(swift_library)
        .set(&#34;compilation_mode&#34;, &#34;opt&#34;)
        .build()
)
```

That’s it.

**NOTE:** You can ignore `_opt_swift_library_internal`. It needs to be assigned to a global variable because of Bazel’s restrictions on rule definitions, but you aren’t supposed to use it directly. Instead, use the generated `opt_swift_library` macro.

Finally, load `opt_swift_library` from the `.bzl` file containing the code above and use it just like a regular `swift_library`. The target and all its transitive dependencies will be built with the `opt` compilation mode.

## Prior art

I won’t go into detail about what this would look like without [with_cfg.bzl](https://github.com/fmeum/with_cfg.bzl), as that’s already covered in my earlier articles.

## Conclusion

Unfortunately, I discovered this way too late, but I’m glad it exists. It makes transitions much less tedious to write and reason about.
</source:markdown>
    </item>
    
    <item>
      <title>Pruning Unused Action Inputs in Bazel</title>
      <link>https://adincebic.com/2026/07/26/pruning-unused-action-inputs-in.html</link>
      <pubDate>Sun, 26 Jul 2026 17:53:04 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/07/26/pruning-unused-action-inputs-in.html</guid>
      <description>&lt;p&gt;Bazel has grown significantly since its initial open-source release, and it is hard to keep up with every release. One useful feature that may have slipped past you is &lt;code&gt;unused_inputs_list&lt;/code&gt;, a parameter of &lt;code&gt;ctx.actions.run(...)&lt;/code&gt; that lets an action report which of its declared inputs it did not actually use.&lt;/p&gt;
&lt;p&gt;After the action executes successfully, Bazel can prune those files from the action&amp;rsquo;s effective input set. On subsequent incremental builds, changing only one of the reported unused files will not cause the action to run again.&lt;/p&gt;
&lt;p&gt;This is &lt;strong&gt;post-execution dependency pruning&lt;/strong&gt;, not pre-execution input discovery. The action still starts with all of its declared inputs. It reports unused inputs only after it has run. If the action later needs to execute again, Bazel restores the original input set first, because a previously unused input may be needed during the new execution.&lt;/p&gt;
&lt;p&gt;A natural use case would be a compiler-like rule that begins with a conservative list of possible headers or source files, but can determine which ones it actually read.&lt;/p&gt;
&lt;h2 id=&#34;a-simple-example&#34;&gt;A simple example&lt;/h2&gt;
&lt;p&gt;To demonstrate the &lt;code&gt;unused_inputs_list&lt;/code&gt; parameter on &lt;code&gt;ctx.actions.run(...)&lt;/code&gt;, I came up with a deliberately contrived example that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Receives &lt;code&gt;choice.txt&lt;/code&gt; through a &lt;code&gt;selector&lt;/code&gt; attribute.&lt;/li&gt;
&lt;li&gt;Reads the contents of &lt;code&gt;choice.txt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Selects one file from &lt;code&gt;srcs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Copies the selected file to the output.&lt;/li&gt;
&lt;li&gt;Reports every unselected source file as unused.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It is an extremely unrealistic rule, but I believe it demonstrates the idea clearly.&lt;/p&gt;
&lt;h3 id=&#34;the-select_one-rule&#34;&gt;The select_one rule&lt;/h3&gt;
&lt;p&gt;Given the following &lt;code&gt;select_one&lt;/code&gt; rule:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;def _select_one_impl(ctx):
    output = ctx.actions.declare_file(ctx.label.name + &amp;quot;.out&amp;quot;)
    unused_inputs = ctx.actions.declare_file(
        ctx.label.name + &amp;quot;.unused_inputs&amp;quot;,
    )

    args = ctx.actions.args()
    args.add(ctx.file.selector)
    args.add(output)
    args.add(unused_inputs)
    args.add_all(ctx.files.srcs)

    ctx.actions.run(
        executable = ctx.executable._tool,
        arguments = [args],
        inputs = [ctx.file.selector] + ctx.files.srcs,
        outputs = [
            output,
            unused_inputs,
        ],
        unused_inputs_list = unused_inputs,
        mnemonic = &amp;quot;SelectOne&amp;quot;,
    )

    return [
        DefaultInfo(files = depset([output])),
    ]

select_one = rule(
    implementation = _select_one_impl,
    attrs = {
        &amp;quot;selector&amp;quot;: attr.label(
            mandatory = True,
            allow_single_file = True,
        ),
        &amp;quot;srcs&amp;quot;: attr.label_list(
            mandatory = True,
            allow_files = True,
        ),
        &amp;quot;_tool&amp;quot;: attr.label(
            default = Label(&amp;quot;//:selector_tool&amp;quot;),
            executable = True,
            cfg = &amp;quot;exec&amp;quot;,
        ),
    },
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;unused_inputs&lt;/code&gt; file is still an ordinary declared output: it must appear in the action&amp;rsquo;s &lt;code&gt;outputs&lt;/code&gt; list, and the tool must create it.&lt;/p&gt;
&lt;p&gt;Passing that same &lt;code&gt;File&lt;/code&gt; through &lt;code&gt;unused_inputs_list&lt;/code&gt; gives the output additional meaning. It tells Bazel to read the file after execution and interpret its contents as a list of inputs that can be pruned.&lt;/p&gt;
&lt;h3 id=&#34;the-tool-itself&#34;&gt;The tool itself&lt;/h3&gt;
&lt;p&gt;Here is the small Swift program that reads the selector, copies the selected file, and reports the remaining candidates as unused:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Foundation&lt;/span&gt;

&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;() &lt;span style=&#34;color:#66d9ef&#34;&gt;throws&lt;/span&gt; {
    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; arguments = Array(CommandLine.arguments.dropFirst())
    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; selectorPath = arguments[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]
    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; outputPath = arguments[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; unusedInputsListPath = arguments[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;]
    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; candidates = Array(arguments.dropFirst(&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;))

    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; selectedName = &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; String(contentsOfFile: selectorPath, encoding: .utf8)
        .trimmingCharacters(&lt;span style=&#34;color:#66d9ef&#34;&gt;in&lt;/span&gt;: .whitespacesAndNewlines)
    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; selectedPath = candidates.first(&lt;span style=&#34;color:#66d9ef&#34;&gt;where&lt;/span&gt;: {
        URL(fileURLWithPath: $0).lastPathComponent == selectedName
    })&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;

    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; selectedContents = &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; Data(
        contentsOf: URL(fileURLWithPath: selectedPath)
    )
    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; selectedContents.write(
        to: URL(fileURLWithPath: outputPath)
    )

    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; unusedPaths = candidates.filter { $0 &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; selectedPath }
    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; unusedContents =
        unusedPaths.isEmpty
        ? &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
        : unusedPaths.joined(separator: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;

    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; Data(unusedContents.utf8).write(
        to: URL(fileURLWithPath: unusedInputsListPath)
    )
}

&lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; run()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The unused-input file contains one input path per line. These need to be the action&amp;rsquo;s execution paths, not merely arbitrary workspace-relative names. Bazel reads each line and matches it against the mapped execution paths of the action&amp;rsquo;s inputs.&lt;/p&gt;
&lt;p&gt;The example avoids having to reconstruct those paths by writing the exact candidate strings that Bazel passed to the tool.&lt;/p&gt;
&lt;p&gt;The selected file must not appear in the unused-input list because its contents were copied to the output. The selector must not appear there either because changing &lt;code&gt;choice.txt&lt;/code&gt; may change which source file is selected.&lt;/p&gt;
&lt;h3 id=&#34;putting-it-all-together&#34;&gt;Putting it all together&lt;/h3&gt;
&lt;p&gt;Here is the final &lt;code&gt;BUILD.bazel&lt;/code&gt; file:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;load(&amp;quot;@rules_swift//swift:swift_binary.bzl&amp;quot;, &amp;quot;swift_binary&amp;quot;)
load(&amp;quot;:select_one.bzl&amp;quot;, &amp;quot;select_one&amp;quot;)

swift_binary(
    name = &amp;quot;selector_tool&amp;quot;,
    srcs = [&amp;quot;main.swift&amp;quot;],
)

select_one(
    name = &amp;quot;demo&amp;quot;,
    selector = &amp;quot;choice.txt&amp;quot;,
    srcs = [
        &amp;quot;a.txt&amp;quot;,
        &amp;quot;b.txt&amp;quot;,
    ],
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Suppose the files contain:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;# choice.txt
a.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;# a.txt
Contents of A
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;# b.txt
Contents of B
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On the first build, the action receives all three declared inputs:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;bazel build //:demo
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The tool selects &lt;code&gt;a.txt&lt;/code&gt;, writes its contents to &lt;code&gt;demo.out&lt;/code&gt;, and writes the path of &lt;code&gt;b.txt&lt;/code&gt; to &lt;code&gt;demo.unused_inputs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;At that point, Bazel knows that &lt;code&gt;b.txt&lt;/code&gt; did not contribute to this execution. Modifying only &lt;code&gt;b.txt&lt;/code&gt; will therefore not cause the &lt;code&gt;SelectOne&lt;/code&gt; action to execute again during a subsequent incremental build:&lt;/p&gt;
&lt;p&gt;Changing &lt;code&gt;a.txt&lt;/code&gt;, on the other hand, must rerun the action because &lt;code&gt;a.txt&lt;/code&gt; produced the output.&lt;/p&gt;
&lt;p&gt;Changing &lt;code&gt;choice.txt&lt;/code&gt; must also rerun it. When that happens, Bazel restores the complete original input set before executing the action. The tool can then select &lt;code&gt;b.txt&lt;/code&gt;, even though &lt;code&gt;b.txt&lt;/code&gt; was reported as unused during the previous execution.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I have not seen many rulesets use &lt;code&gt;unused_inputs_list&lt;/code&gt; directly, but it is worth knowing that it exists. It is most useful for actions that must declare a conservative set of possible inputs but can reliably determine which subset mattered after execution.&lt;/p&gt;
&lt;p&gt;I enjoy exploring the more obscure corners of Bazel from time to time, since I tend to find features I did not know I needed.&lt;/p&gt;
</description>
      <source:markdown>Bazel has grown significantly since its initial open-source release, and it is hard to keep up with every release. One useful feature that may have slipped past you is `unused_inputs_list`, a parameter of `ctx.actions.run(...)` that lets an action report which of its declared inputs it did not actually use.

After the action executes successfully, Bazel can prune those files from the action&#39;s effective input set. On subsequent incremental builds, changing only one of the reported unused files will not cause the action to run again.

This is **post-execution dependency pruning**, not pre-execution input discovery. The action still starts with all of its declared inputs. It reports unused inputs only after it has run. If the action later needs to execute again, Bazel restores the original input set first, because a previously unused input may be needed during the new execution.

A natural use case would be a compiler-like rule that begins with a conservative list of possible headers or source files, but can determine which ones it actually read.

## A simple example

To demonstrate the `unused_inputs_list` parameter on `ctx.actions.run(...)`, I came up with a deliberately contrived example that:

1. Receives `choice.txt` through a `selector` attribute.
2. Reads the contents of `choice.txt`.
3. Selects one file from `srcs`.
4. Copies the selected file to the output.
5. Reports every unselected source file as unused.

It is an extremely unrealistic rule, but I believe it demonstrates the idea clearly.

### The select_one rule

Given the following `select_one` rule:

```starlark
def _select_one_impl(ctx):
    output = ctx.actions.declare_file(ctx.label.name + &#34;.out&#34;)
    unused_inputs = ctx.actions.declare_file(
        ctx.label.name + &#34;.unused_inputs&#34;,
    )

    args = ctx.actions.args()
    args.add(ctx.file.selector)
    args.add(output)
    args.add(unused_inputs)
    args.add_all(ctx.files.srcs)

    ctx.actions.run(
        executable = ctx.executable._tool,
        arguments = [args],
        inputs = [ctx.file.selector] + ctx.files.srcs,
        outputs = [
            output,
            unused_inputs,
        ],
        unused_inputs_list = unused_inputs,
        mnemonic = &#34;SelectOne&#34;,
    )

    return [
        DefaultInfo(files = depset([output])),
    ]

select_one = rule(
    implementation = _select_one_impl,
    attrs = {
        &#34;selector&#34;: attr.label(
            mandatory = True,
            allow_single_file = True,
        ),
        &#34;srcs&#34;: attr.label_list(
            mandatory = True,
            allow_files = True,
        ),
        &#34;_tool&#34;: attr.label(
            default = Label(&#34;//:selector_tool&#34;),
            executable = True,
            cfg = &#34;exec&#34;,
        ),
    },
)
```

The `unused_inputs` file is still an ordinary declared output: it must appear in the action&#39;s `outputs` list, and the tool must create it.

Passing that same `File` through `unused_inputs_list` gives the output additional meaning. It tells Bazel to read the file after execution and interpret its contents as a list of inputs that can be pruned.

### The tool itself

Here is the small Swift program that reads the selector, copies the selected file, and reports the remaining candidates as unused:

```swift
import Foundation

func run() throws {
    let arguments = Array(CommandLine.arguments.dropFirst())
    let selectorPath = arguments[0]
    let outputPath = arguments[1]
    let unusedInputsListPath = arguments[2]
    let candidates = Array(arguments.dropFirst(3))

    let selectedName = try String(contentsOfFile: selectorPath, encoding: .utf8)
        .trimmingCharacters(in: .whitespacesAndNewlines)
    let selectedPath = candidates.first(where: {
        URL(fileURLWithPath: $0).lastPathComponent == selectedName
    })!

    let selectedContents = try Data(
        contentsOf: URL(fileURLWithPath: selectedPath)
    )
    try selectedContents.write(
        to: URL(fileURLWithPath: outputPath)
    )

    let unusedPaths = candidates.filter { $0 != selectedPath }
    let unusedContents =
        unusedPaths.isEmpty
        ? &#34;&#34;
        : unusedPaths.joined(separator: &#34;\n&#34;) + &#34;\n&#34;

    try Data(unusedContents.utf8).write(
        to: URL(fileURLWithPath: unusedInputsListPath)
    )
}

try run()
```

The unused-input file contains one input path per line. These need to be the action&#39;s execution paths, not merely arbitrary workspace-relative names. Bazel reads each line and matches it against the mapped execution paths of the action&#39;s inputs.

The example avoids having to reconstruct those paths by writing the exact candidate strings that Bazel passed to the tool.

The selected file must not appear in the unused-input list because its contents were copied to the output. The selector must not appear there either because changing `choice.txt` may change which source file is selected.

### Putting it all together

Here is the final `BUILD.bazel` file:

```starlark
load(&#34;@rules_swift//swift:swift_binary.bzl&#34;, &#34;swift_binary&#34;)
load(&#34;:select_one.bzl&#34;, &#34;select_one&#34;)

swift_binary(
    name = &#34;selector_tool&#34;,
    srcs = [&#34;main.swift&#34;],
)

select_one(
    name = &#34;demo&#34;,
    selector = &#34;choice.txt&#34;,
    srcs = [
        &#34;a.txt&#34;,
        &#34;b.txt&#34;,
    ],
)
```

Suppose the files contain:

```text
# choice.txt
a.txt
```

```text
# a.txt
Contents of A
```

```text
# b.txt
Contents of B
```

On the first build, the action receives all three declared inputs:

```bash
bazel build //:demo
```

The tool selects `a.txt`, writes its contents to `demo.out`, and writes the path of `b.txt` to `demo.unused_inputs`.

At that point, Bazel knows that `b.txt` did not contribute to this execution. Modifying only `b.txt` will therefore not cause the `SelectOne` action to execute again during a subsequent incremental build:

Changing `a.txt`, on the other hand, must rerun the action because `a.txt` produced the output.

Changing `choice.txt` must also rerun it. When that happens, Bazel restores the complete original input set before executing the action. The tool can then select `b.txt`, even though `b.txt` was reported as unused during the previous execution.

## Conclusion

I have not seen many rulesets use `unused_inputs_list` directly, but it is worth knowing that it exists. It is most useful for actions that must declare a conservative set of possible inputs but can reliably determine which subset mattered after execution.

I enjoy exploring the more obscure corners of Bazel from time to time, since I tend to find features I did not know I needed.
</source:markdown>
    </item>
    
    <item>
      <title>A Note on Bazel’s config.exec()</title>
      <link>https://adincebic.com/2026/07/19/a-note-on-bazels-configexec.html</link>
      <pubDate>Sun, 19 Jul 2026 17:22:27 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/07/19/a-note-on-bazels-configexec.html</guid>
      <description>&lt;p&gt;When writing a rule that executes in the exec configuration, we need to communicate that to Bazel so it doesn’t attempt to build it for the target configuration:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;generator = rule(
    implementation = _generator_impl,
    ...,
    cfg = &amp;quot;exec&amp;quot;,
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;While there is nothing wrong with &lt;code&gt;cfg = &amp;quot;exec&amp;quot;&lt;/code&gt;, my opinion is that we should use the newer transition object API.&lt;/p&gt;
&lt;p&gt;This means that instead of:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;cfg = &amp;quot;exec&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;we use:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;cfg = config.exec()
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;why&#34;&gt;Why?&lt;/h2&gt;
&lt;p&gt;Apart from legitimate use cases such as &lt;a href=&#34;https://github.com/bazelbuild/proposals/blob/main/designs/2024-04-16-transition-composition.md&#34;&gt;transition composition&lt;/a&gt; and &lt;a href=&#34;https://bazel.build/rules/lib/toplevel/config#exec&#34;&gt;passing an exec group&lt;/a&gt;, I think using the object form is better for readability and discoverability.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;quot;exec&amp;quot;&lt;/code&gt; is a special string. You need to already know what it means and where it is supported.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;config.exec()&lt;/code&gt;, on the other hand, looks like an API. It is easier to discover, easier to search for, and makes it clearer that we are applying an execution transition.&lt;/p&gt;
&lt;p&gt;Passing the &lt;code&gt;&amp;quot;exec&amp;quot;&lt;/code&gt; string is not deprecated, and I am not suggesting that it is incorrect.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This is simply my take on &lt;code&gt;config.exec()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Migrating an entire codebase from &lt;code&gt;cfg = &amp;quot;exec&amp;quot;&lt;/code&gt; to &lt;code&gt;cfg = config.exec()&lt;/code&gt; will not result in any build-time improvement. It will, however, make the API usage slightly more explicit and leave room for features that the string form cannot express.&lt;/p&gt;
</description>
      <source:markdown>When writing a rule that executes in the exec configuration, we need to communicate that to Bazel so it doesn’t attempt to build it for the target configuration:

```starlark
generator = rule(
    implementation = _generator_impl,
    ...,
    cfg = &#34;exec&#34;,
)
```

While there is nothing wrong with `cfg = &#34;exec&#34;`, my opinion is that we should use the newer transition object API.

This means that instead of:

```starlark
cfg = &#34;exec&#34;
```

we use:

```starlark
cfg = config.exec()
```

## Why?

Apart from legitimate use cases such as [transition composition](https://github.com/bazelbuild/proposals/blob/main/designs/2024-04-16-transition-composition.md) and [passing an exec group](https://bazel.build/rules/lib/toplevel/config#exec), I think using the object form is better for readability and discoverability.

`&#34;exec&#34;` is a special string. You need to already know what it means and where it is supported.

`config.exec()`, on the other hand, looks like an API. It is easier to discover, easier to search for, and makes it clearer that we are applying an execution transition.

Passing the `&#34;exec&#34;` string is not deprecated, and I am not suggesting that it is incorrect.

## Conclusion

This is simply my take on `config.exec()`.

Migrating an entire codebase from `cfg = &#34;exec&#34;` to `cfg = config.exec()` will not result in any build-time improvement. It will, however, make the API usage slightly more explicit and leave room for features that the string form cannot express.
</source:markdown>
    </item>
    
    <item>
      <title>Making Bazel Module Extensions Work Together with override_repo</title>
      <link>https://adincebic.com/2026/07/12/making-bazel-module-extensions-work.html</link>
      <pubDate>Sun, 12 Jul 2026 14:59:39 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/07/12/making-bazel-module-extensions-work.html</guid>
      <description>&lt;p&gt;Recently, I have noticed more rulesets adopting Bzlmod-specific features. With Bazel 6 no longer supported and Bzlmod adoption continuing across the ecosystem, rulesets can increasingly rely on newer module APIs.&lt;/p&gt;
&lt;p&gt;One feature that caught my attention while I was setting up a hermetic Android toolchain is the ability to override a repository generated by a module extension.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;override_repo&lt;/code&gt; requires Bazel 7.4.0 or newer and can only be used by the root module.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;overriding-a-repository&#34;&gt;Overriding a repository&lt;/h2&gt;
&lt;p&gt;There are several reasons to override a repository generated by a module extension.&lt;/p&gt;
&lt;p&gt;It can simplify migrations by allowing existing call sites to keep using the same repository name. It can also avoid introducing multiple similarly named repositories. Most importantly, it gives module authors a way to provide a smoother developer experience when integrating with other rulesets.&lt;/p&gt;
&lt;p&gt;A good example is &lt;a href=&#34;https://github.com/keith/hermetic_android_toolchains&#34;&gt;Keith’s hermetic Android toolchain&lt;/a&gt;. Setting it up requires only a small amount of configuration:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;bazel_dep(
    name = &amp;quot;hermetic_android_toolchains&amp;quot;,
    version = &amp;quot;0.3.0&amp;quot;,
)
bazel_dep(
    name = &amp;quot;rules_android&amp;quot;,
    version = &amp;quot;0.7.3&amp;quot;,
)

android = use_extension(
    &amp;quot;@hermetic_android_toolchains//:extensions.bzl&amp;quot;,
    &amp;quot;android&amp;quot;,
)
android.sdk(
    build_tools_version = &amp;quot;37.0.0&amp;quot;,
    version = &amp;quot;37.0&amp;quot;,
)
use_repo(android, &amp;quot;androidsdk&amp;quot;)

# Make @rules_android&#39;s @androidsdk labels resolve to the hermetic SDK.
rules_android_sdk = use_extension(
    &amp;quot;@rules_android//rules/android_sdk_repository:rule.bzl&amp;quot;,
    &amp;quot;android_sdk_repository_extension&amp;quot;,
)
override_repo(rules_android_sdk, &amp;quot;androidsdk&amp;quot;)

register_toolchains(&amp;quot;@androidsdk//:all&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The important line is:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;override_repo(rules_android_sdk, &amp;quot;androidsdk&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;rules_android_sdk&lt;/code&gt; extension normally generates its own repository named &lt;code&gt;androidsdk&lt;/code&gt;. The positional form of &lt;code&gt;override_repo&lt;/code&gt; tells Bazel to replace it with the repository of the same name that is already visible to the root module—in this case, the &lt;code&gt;androidsdk&lt;/code&gt; repository generated by &lt;code&gt;hermetic_android_toolchains&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As a result, references to &lt;code&gt;@androidsdk&lt;/code&gt; from the &lt;code&gt;rules_android&lt;/code&gt; extension resolve to the hermetically downloaded SDK rather than a separately configured Android SDK repository.&lt;/p&gt;
&lt;p&gt;The keyword form can be used when the two repositories have different names:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;override_repo(
    some_extension,
    generated_repo_name = &amp;quot;replacement_repo_name&amp;quot;,
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here, &lt;code&gt;generated_repo_name&lt;/code&gt; is the repository produced by the extension, while &lt;code&gt;replacement_repo_name&lt;/code&gt; is a repository visible to the root module.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;That is all there is to it. &lt;code&gt;override_repo&lt;/code&gt; is a small Bzlmod feature, but it can make integrations and migrations considerably cleaner. It is worth keeping in mind whenever two module extensions need to agree on the repository behind a well-known name.&lt;/p&gt;
</description>
      <source:markdown>Recently, I have noticed more rulesets adopting Bzlmod-specific features. With Bazel 6 no longer supported and Bzlmod adoption continuing across the ecosystem, rulesets can increasingly rely on newer module APIs.

One feature that caught my attention while I was setting up a hermetic Android toolchain is the ability to override a repository generated by a module extension.

&gt; `override_repo` requires Bazel 7.4.0 or newer and can only be used by the root module.

## Overriding a repository

There are several reasons to override a repository generated by a module extension.

It can simplify migrations by allowing existing call sites to keep using the same repository name. It can also avoid introducing multiple similarly named repositories. Most importantly, it gives module authors a way to provide a smoother developer experience when integrating with other rulesets.

A good example is [Keith’s hermetic Android toolchain](https://github.com/keith/hermetic_android_toolchains). Setting it up requires only a small amount of configuration:

```starlark
bazel_dep(
    name = &#34;hermetic_android_toolchains&#34;,
    version = &#34;0.3.0&#34;,
)
bazel_dep(
    name = &#34;rules_android&#34;,
    version = &#34;0.7.3&#34;,
)

android = use_extension(
    &#34;@hermetic_android_toolchains//:extensions.bzl&#34;,
    &#34;android&#34;,
)
android.sdk(
    build_tools_version = &#34;37.0.0&#34;,
    version = &#34;37.0&#34;,
)
use_repo(android, &#34;androidsdk&#34;)

# Make @rules_android&#39;s @androidsdk labels resolve to the hermetic SDK.
rules_android_sdk = use_extension(
    &#34;@rules_android//rules/android_sdk_repository:rule.bzl&#34;,
    &#34;android_sdk_repository_extension&#34;,
)
override_repo(rules_android_sdk, &#34;androidsdk&#34;)

register_toolchains(&#34;@androidsdk//:all&#34;)
```

The important line is:

```starlark
override_repo(rules_android_sdk, &#34;androidsdk&#34;)
```

The `rules_android_sdk` extension normally generates its own repository named `androidsdk`. The positional form of `override_repo` tells Bazel to replace it with the repository of the same name that is already visible to the root module—in this case, the `androidsdk` repository generated by `hermetic_android_toolchains`.

As a result, references to `@androidsdk` from the `rules_android` extension resolve to the hermetically downloaded SDK rather than a separately configured Android SDK repository.

The keyword form can be used when the two repositories have different names:

```starlark
override_repo(
    some_extension,
    generated_repo_name = &#34;replacement_repo_name&#34;,
)
```

Here, `generated_repo_name` is the repository produced by the extension, while `replacement_repo_name` is a repository visible to the root module.

## Conclusion

That is all there is to it. `override_repo` is a small Bzlmod feature, but it can make integrations and migrations considerably cleaner. It is worth keeping in mind whenever two module extensions need to agree on the repository behind a well-known name.
</source:markdown>
    </item>
    
    <item>
      <title>Stamping iOS Builds with Bazel</title>
      <link>https://adincebic.com/2026/07/05/stamping-ios-builds-with-bazel.html</link>
      <pubDate>Sun, 05 Jul 2026 19:05:10 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/07/05/stamping-ios-builds-with-bazel.html</guid>
      <description>&lt;p&gt;Stamping is the act of embedding build metadata into the product that we ship to customers. It can help with issue diagnosis, analytics, and so on. Conveniently, Bazel offers us a first-class solution, and it is very easy to take advantage of it in the context of iOS apps.&lt;/p&gt;
&lt;h2 id=&#34;workspace-status-script&#34;&gt;Workspace status script&lt;/h2&gt;
&lt;p&gt;The first step in enabling stamping is to create a workspace status script. For example, we can create a script that emits the current Git commit hash:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
set -eu -o pipefail

echo &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;STABLE_GIT_COMMIT &lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;git rev-parse HEAD&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we need to tell Bazel to execute the script:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;--workspace_status_command&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;./tools/workspace_status.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;reading-the-value-at-build-time&#34;&gt;Reading the value at build time&lt;/h2&gt;
&lt;p&gt;For iOS apps, or really any Apple platform app, it is usually best to embed this data in a plist file so we can read it at runtime. First, we use a &lt;code&gt;genrule&lt;/code&gt; to read the workspace status data and materialize a plist file:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;genrule(
    name = &amp;quot;commit_plist&amp;quot;,
    outs = [&amp;quot;Commit.plist&amp;quot;],
    cmd = &amp;quot;&amp;quot;&amp;quot;
commit=&amp;quot;$$(sed -n &#39;s/^STABLE_GIT_COMMIT //p&#39; bazel-out/stable-status.txt)&amp;quot;
plutil -convert xml1 -o &amp;quot;$@&amp;quot; - &amp;lt;&amp;lt;EOF
{
    &amp;quot;GIT_COMMIT&amp;quot;: &amp;quot;$${commit}&amp;quot;
}
EOF
&amp;quot;&amp;quot;&amp;quot;,
    stamp = True,
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;From there, it is just a matter of adding this target to the &lt;code&gt;infoplists&lt;/code&gt; attribute of any Apple platform application target, like &lt;code&gt;ios_application&lt;/code&gt;. Because &lt;code&gt;rules_apple&lt;/code&gt; performs plist merging, this value will end up in the final &lt;code&gt;Info.plist&lt;/code&gt; file that we ship in the app bundle.&lt;/p&gt;
&lt;h2 id=&#34;a-word-on-the-stamp-attribute&#34;&gt;A word on the &lt;code&gt;stamp&lt;/code&gt; attribute&lt;/h2&gt;
&lt;p&gt;Notice the &lt;code&gt;stamp = True&lt;/code&gt; attribute on the &lt;code&gt;genrule&lt;/code&gt;? That is what allows the &lt;code&gt;genrule&lt;/code&gt; action to access &lt;code&gt;bazel-out/stable-status.txt&lt;/code&gt; and &lt;code&gt;bazel-out/volatile-status.txt&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Without it, the action should not rely on those files being present. In this example, the important part is stamping the &lt;code&gt;genrule&lt;/code&gt; that materializes the plist.&lt;/p&gt;
&lt;p&gt;This is separate from the &lt;code&gt;stamp&lt;/code&gt; attribute you may see on Apple rules like &lt;code&gt;ios_application&lt;/code&gt;, where stamping controls whether build information is encoded into the binary. For this plist-based approach, we do not need to rely on link stamping at the application target level.&lt;/p&gt;
&lt;h2 id=&#34;reading-the-value-at-runtime&#34;&gt;Reading the value at runtime&lt;/h2&gt;
&lt;p&gt;Because the value ends up in &lt;code&gt;Info.plist&lt;/code&gt;, we can read it at runtime through the &lt;code&gt;Bundle&lt;/code&gt; / &lt;code&gt;NSBundle&lt;/code&gt; API.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;There you have it: an easy way to stamp iOS builds. I hope it helps you discover and fix bugs in production more easily.&lt;/p&gt;
</description>
      <source:markdown>Stamping is the act of embedding build metadata into the product that we ship to customers. It can help with issue diagnosis, analytics, and so on. Conveniently, Bazel offers us a first-class solution, and it is very easy to take advantage of it in the context of iOS apps.

## Workspace status script

The first step in enabling stamping is to create a workspace status script. For example, we can create a script that emits the current Git commit hash:

```bash
#!/usr/bin/env bash

set -eu -o pipefail

echo &#34;STABLE_GIT_COMMIT $(git rev-parse HEAD)&#34;
```

Now we need to tell Bazel to execute the script:

```bash
--workspace_status_command=./tools/workspace_status.sh
```

## Reading the value at build time

For iOS apps, or really any Apple platform app, it is usually best to embed this data in a plist file so we can read it at runtime. First, we use a `genrule` to read the workspace status data and materialize a plist file:

```starlark
genrule(
    name = &#34;commit_plist&#34;,
    outs = [&#34;Commit.plist&#34;],
    cmd = &#34;&#34;&#34;
commit=&#34;$$(sed -n &#39;s/^STABLE_GIT_COMMIT //p&#39; bazel-out/stable-status.txt)&#34;
plutil -convert xml1 -o &#34;$@&#34; - &lt;&lt;EOF
{
    &#34;GIT_COMMIT&#34;: &#34;$${commit}&#34;
}
EOF
&#34;&#34;&#34;,
    stamp = True,
)
```

From there, it is just a matter of adding this target to the `infoplists` attribute of any Apple platform application target, like `ios_application`. Because `rules_apple` performs plist merging, this value will end up in the final `Info.plist` file that we ship in the app bundle.

## A word on the `stamp` attribute

Notice the `stamp = True` attribute on the `genrule`? That is what allows the `genrule` action to access `bazel-out/stable-status.txt` and `bazel-out/volatile-status.txt`.

Without it, the action should not rely on those files being present. In this example, the important part is stamping the `genrule` that materializes the plist.

This is separate from the `stamp` attribute you may see on Apple rules like `ios_application`, where stamping controls whether build information is encoded into the binary. For this plist-based approach, we do not need to rely on link stamping at the application target level.

## Reading the value at runtime

Because the value ends up in `Info.plist`, we can read it at runtime through the `Bundle` / `NSBundle` API.

## Conclusion

There you have it: an easy way to stamp iOS builds. I hope it helps you discover and fix bugs in production more easily.
</source:markdown>
    </item>
    
    <item>
      <title>Making Developer Tools Available Through Bazel</title>
      <link>https://adincebic.com/2026/06/28/making-developer-tools-available-through.html</link>
      <pubDate>Sun, 28 Jun 2026 18:51:25 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/06/28/making-developer-tools-available-through.html</guid>
      <description>&lt;p&gt;Traditionally, when setting up a developer machine, instructions include something like &amp;ldquo;install the following tools using Homebrew&amp;rdquo;. What if we could always have tools available without asking developers to install anything but Bazel?&lt;/p&gt;
&lt;p&gt;This is easily achievable with Bazel since it gives us a way to download and execute binaries. Before diving into the implementation, let&amp;rsquo;s first explore the downsides of asking developers to install tools on their own.&lt;/p&gt;
&lt;h2 id=&#34;problems-with-homebrew-for-developer-tools&#34;&gt;Problems with Homebrew for developer tools&lt;/h2&gt;
&lt;h3 id=&#34;brew-install-&#34;&gt;brew install &amp;hellip;&lt;/h3&gt;
&lt;p&gt;When developing on macOS, the &amp;ldquo;default&amp;rdquo; package manager is Homebrew, so we install tools like linters and formatters using it. However, it is not great for versioning in this use case. By default, we usually end up installing whatever version Homebrew currently resolves, unless we specifically do extra work to avoid that.&lt;/p&gt;
&lt;p&gt;This is the first problem: we can&amp;rsquo;t expect people to ensure that they have exactly the same version of a tool as everybody else, especially if the organization is large.&lt;/p&gt;
&lt;h3 id=&#34;distributing-tools&#34;&gt;Distributing tools&lt;/h3&gt;
&lt;p&gt;Just like we can&amp;rsquo;t easily enforce versions, we also can&amp;rsquo;t easily enforce tool replacement. Say that we built an internal linter and we want everybody to use it. What is the distribution mechanism? Perhaps an internal Brew formula? It works, but it is tedious to set up and maintain.&lt;/p&gt;
&lt;h2 id=&#34;using-bazel-and-rules_multitool&#34;&gt;Using Bazel and rules_multitool&lt;/h2&gt;
&lt;p&gt;There have been a couple of &lt;a href=&#34;https://blog.aspect.build/run-tools-installed-by-bazel&#34;&gt;community posts&lt;/a&gt; about &lt;a href=&#34;https://registry.bazel.build/modules/rules_multitool&#34;&gt;rules_multitool&lt;/a&gt;, and I want to give my take on it.&lt;/p&gt;
&lt;h3 id=&#34;how-it-works&#34;&gt;How it works&lt;/h3&gt;
&lt;p&gt;This ruleset provides a convenient way to tell Bazel to download a binary and expose it as a target under the &lt;code&gt;@multitool//tools/{TOOL_NAME}&lt;/code&gt; label. It takes in &lt;code&gt;multitool.lock.json&lt;/code&gt; files and uses information from the lockfile to invoke Bazel repository rules, download the specified binary, and expose it as a runnable tool.&lt;/p&gt;
&lt;h3 id=&#34;setting-it-up&#34;&gt;Setting it up&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Declare the dependency in &lt;code&gt;MODULE.bazel&lt;/code&gt; as &lt;a href=&#34;https://registry.bazel.build/modules/rules_multitool&#34;&gt;described here&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Call its module extension from &lt;code&gt;MODULE.bazel&lt;/code&gt;:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;multitool = use_extension(&amp;quot;@rules_multitool//multitool:extension.bzl&amp;quot;, &amp;quot;multitool&amp;quot;)
multitool.hub(lockfile = &amp;quot;//tools:multitool.lock.json&amp;quot;)
use_repo(multitool, &amp;quot;multitool&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Of course, you can put your &lt;code&gt;multitool.lock.json&lt;/code&gt; wherever you like. I tend to keep it under the &lt;code&gt;tools/&lt;/code&gt; package.&lt;/p&gt;
&lt;ol start=&#34;3&#34;&gt;
&lt;li&gt;Add &lt;code&gt;multitool.lock.json&lt;/code&gt; and give it some tools to work with, e.g.:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;{
    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;$schema&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://raw.githubusercontent.com/theoremlp/rules_multitool/main/lockfile.schema.json&amp;#34;&lt;/span&gt;,
    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;bb&amp;#34;&lt;/span&gt;: {
        &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;binaries&amp;#34;&lt;/span&gt;: [
            {
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;kind&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;file&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://github.com/buildbuddy-io/bazel/releases/download/5.0.350/bazel-5.0.350-linux-x86_64&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;sha256&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;d14e6a240dc5e8bc3ebb625ff7c139ba8e380f1440f9e2f60e9c1d7850d012c9&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;os&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;linux&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;cpu&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;x86_64&amp;#34;&lt;/span&gt;
            },
            {
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;kind&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;file&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://github.com/buildbuddy-io/bazel/releases/download/5.0.350/bazel-5.0.350-darwin-arm64&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;sha256&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;f16cc54449eb62ee65ac7ec3b45d7bce7922225a3c004925cbb25982faa8a9cd&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;os&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;macos&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;cpu&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;arm64&amp;#34;&lt;/span&gt;
            }
        ]
    },
    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;yq&amp;#34;&lt;/span&gt;: {
        &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;binaries&amp;#34;&lt;/span&gt;: [
            {
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;kind&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;file&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://github.com/mikefarah/yq/releases/download/v4.52.4/yq_linux_arm64&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;sha256&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;4c2cc022a129be5cc1187959bb4b09bebc7fb543c5837b93001c68f97ce39a5d&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;os&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;linux&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;cpu&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;arm64&amp;#34;&lt;/span&gt;
            },
            {
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;kind&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;file&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://github.com/mikefarah/yq/releases/download/v4.52.4/yq_linux_amd64&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;sha256&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;0c4d965ea944b64b8fddaf7f27779ee3034e5693263786506ccd1c120f184e8c&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;os&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;linux&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;cpu&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;x86_64&amp;#34;&lt;/span&gt;
            },
            {
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;kind&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;file&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://github.com/mikefarah/yq/releases/download/v4.52.4/yq_darwin_arm64&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;sha256&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;6bfa43a439936644d63c70308832390c8838290d064970eaada216219c218a13&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;os&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;macos&amp;#34;&lt;/span&gt;,
                &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;cpu&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;arm64&amp;#34;&lt;/span&gt;
            }
        ]
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is it. You can now execute &lt;code&gt;bazel run @multitool//tools/yq&lt;/code&gt; or &lt;code&gt;bazel run @multitool//tools/bb&lt;/code&gt;, and Bazel will download and execute them just fine.&lt;/p&gt;
&lt;h3 id=&#34;making-it-more-convenient&#34;&gt;Making it more convenient&lt;/h3&gt;
&lt;p&gt;Typing out the label from above will quickly get tedious. What if we could instead run the &lt;code&gt;yq&lt;/code&gt; tool as easily as &lt;code&gt;./tools/yq&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;To achieve that, we could do the following trick:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a script at &lt;code&gt;tools/_run_tool.sh&lt;/code&gt; with the following content:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
target&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;@multitool//tools/&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;$(&lt;/span&gt;basename &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$0&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;

bazel run &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;    --run_in_cwd &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;    --noshow_progress &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;    --show_result&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;    --ui_event_filters&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;-info &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$target&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt; -- &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$@&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start=&#34;2&#34;&gt;
&lt;li&gt;Create a symlink for every tool with its name, e.g. a &lt;code&gt;yq&lt;/code&gt; symlink that links to &lt;code&gt;tools/_run_tool.sh&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That will expand &lt;code&gt;$(basename &amp;quot;$0&amp;quot;)&lt;/code&gt; in &lt;code&gt;tools/_run_tool.sh&lt;/code&gt; to the name of the symlink and allow you to execute &lt;code&gt;./tools/yq&lt;/code&gt;. Or, if you put the symlink at the root, it gets even more convenient: &lt;code&gt;./yq&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I think the trick with symlinks is quite powerful, and you don&amp;rsquo;t really have to use &lt;a href=&#34;https://registry.bazel.build/modules/rules_multitool&#34;&gt;rules_multitool&lt;/a&gt; if you don&amp;rsquo;t want to. The same idea can work with any Bazel target that exposes a runnable tool.&lt;/p&gt;
&lt;p&gt;Overall, I like this setup very much because it requires almost nothing from developers. They can just use the tools provided to them while not even thinking about versions.&lt;/p&gt;
</description>
      <source:markdown>Traditionally, when setting up a developer machine, instructions include something like &#34;install the following tools using Homebrew&#34;. What if we could always have tools available without asking developers to install anything but Bazel?

This is easily achievable with Bazel since it gives us a way to download and execute binaries. Before diving into the implementation, let&#39;s first explore the downsides of asking developers to install tools on their own.

## Problems with Homebrew for developer tools

### brew install ...

When developing on macOS, the &#34;default&#34; package manager is Homebrew, so we install tools like linters and formatters using it. However, it is not great for versioning in this use case. By default, we usually end up installing whatever version Homebrew currently resolves, unless we specifically do extra work to avoid that.

This is the first problem: we can&#39;t expect people to ensure that they have exactly the same version of a tool as everybody else, especially if the organization is large.

### Distributing tools

Just like we can&#39;t easily enforce versions, we also can&#39;t easily enforce tool replacement. Say that we built an internal linter and we want everybody to use it. What is the distribution mechanism? Perhaps an internal Brew formula? It works, but it is tedious to set up and maintain.

## Using Bazel and rules_multitool

There have been a couple of [community posts](https://blog.aspect.build/run-tools-installed-by-bazel) about [rules_multitool](https://registry.bazel.build/modules/rules_multitool), and I want to give my take on it.

### How it works

This ruleset provides a convenient way to tell Bazel to download a binary and expose it as a target under the `@multitool//tools/{TOOL_NAME}` label. It takes in `multitool.lock.json` files and uses information from the lockfile to invoke Bazel repository rules, download the specified binary, and expose it as a runnable tool.

### Setting it up

1. Declare the dependency in `MODULE.bazel` as [described here](https://registry.bazel.build/modules/rules_multitool).

2. Call its module extension from `MODULE.bazel`:

```starlark
multitool = use_extension(&#34;@rules_multitool//multitool:extension.bzl&#34;, &#34;multitool&#34;)
multitool.hub(lockfile = &#34;//tools:multitool.lock.json&#34;)
use_repo(multitool, &#34;multitool&#34;)
```

Of course, you can put your `multitool.lock.json` wherever you like. I tend to keep it under the `tools/` package.

3. Add `multitool.lock.json` and give it some tools to work with, e.g.:

```json
{
    &#34;$schema&#34;: &#34;https://raw.githubusercontent.com/theoremlp/rules_multitool/main/lockfile.schema.json&#34;,
    &#34;bb&#34;: {
        &#34;binaries&#34;: [
            {
                &#34;kind&#34;: &#34;file&#34;,
                &#34;url&#34;: &#34;https://github.com/buildbuddy-io/bazel/releases/download/5.0.350/bazel-5.0.350-linux-x86_64&#34;,
                &#34;sha256&#34;: &#34;d14e6a240dc5e8bc3ebb625ff7c139ba8e380f1440f9e2f60e9c1d7850d012c9&#34;,
                &#34;os&#34;: &#34;linux&#34;,
                &#34;cpu&#34;: &#34;x86_64&#34;
            },
            {
                &#34;kind&#34;: &#34;file&#34;,
                &#34;url&#34;: &#34;https://github.com/buildbuddy-io/bazel/releases/download/5.0.350/bazel-5.0.350-darwin-arm64&#34;,
                &#34;sha256&#34;: &#34;f16cc54449eb62ee65ac7ec3b45d7bce7922225a3c004925cbb25982faa8a9cd&#34;,
                &#34;os&#34;: &#34;macos&#34;,
                &#34;cpu&#34;: &#34;arm64&#34;
            }
        ]
    },
    &#34;yq&#34;: {
        &#34;binaries&#34;: [
            {
                &#34;kind&#34;: &#34;file&#34;,
                &#34;url&#34;: &#34;https://github.com/mikefarah/yq/releases/download/v4.52.4/yq_linux_arm64&#34;,
                &#34;sha256&#34;: &#34;4c2cc022a129be5cc1187959bb4b09bebc7fb543c5837b93001c68f97ce39a5d&#34;,
                &#34;os&#34;: &#34;linux&#34;,
                &#34;cpu&#34;: &#34;arm64&#34;
            },
            {
                &#34;kind&#34;: &#34;file&#34;,
                &#34;url&#34;: &#34;https://github.com/mikefarah/yq/releases/download/v4.52.4/yq_linux_amd64&#34;,
                &#34;sha256&#34;: &#34;0c4d965ea944b64b8fddaf7f27779ee3034e5693263786506ccd1c120f184e8c&#34;,
                &#34;os&#34;: &#34;linux&#34;,
                &#34;cpu&#34;: &#34;x86_64&#34;
            },
            {
                &#34;kind&#34;: &#34;file&#34;,
                &#34;url&#34;: &#34;https://github.com/mikefarah/yq/releases/download/v4.52.4/yq_darwin_arm64&#34;,
                &#34;sha256&#34;: &#34;6bfa43a439936644d63c70308832390c8838290d064970eaada216219c218a13&#34;,
                &#34;os&#34;: &#34;macos&#34;,
                &#34;cpu&#34;: &#34;arm64&#34;
            }
        ]
    }
}
```

This is it. You can now execute `bazel run @multitool//tools/yq` or `bazel run @multitool//tools/bb`, and Bazel will download and execute them just fine.

### Making it more convenient

Typing out the label from above will quickly get tedious. What if we could instead run the `yq` tool as easily as `./tools/yq`?

To achieve that, we could do the following trick:

1. Create a script at `tools/_run_tool.sh` with the following content:

```bash
#!/usr/bin/env bash

target=&#34;@multitool//tools/$(basename &#34;$0&#34;)&#34;

bazel run \
    --run_in_cwd \
    --noshow_progress \
    --show_result=0 \
    --ui_event_filters=-info \
    &#34;$target&#34; -- &#34;$@&#34;
```

2. Create a symlink for every tool with its name, e.g. a `yq` symlink that links to `tools/_run_tool.sh`.

That will expand `$(basename &#34;$0&#34;)` in `tools/_run_tool.sh` to the name of the symlink and allow you to execute `./tools/yq`. Or, if you put the symlink at the root, it gets even more convenient: `./yq`.

## Conclusion

I think the trick with symlinks is quite powerful, and you don&#39;t really have to use [rules_multitool](https://registry.bazel.build/modules/rules_multitool) if you don&#39;t want to. The same idea can work with any Bazel target that exposes a runnable tool.

Overall, I like this setup very much because it requires almost nothing from developers. They can just use the tools provided to them while not even thinking about versions.
</source:markdown>
    </item>
    
    <item>
      <title>Avoiding .DS_Store Cache Misses in Bazel</title>
      <link>https://adincebic.com/2026/06/21/avoiding-dsstore-cache-misses-in.html</link>
      <pubDate>Sun, 21 Jun 2026 18:42:25 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/06/21/avoiding-dsstore-cache-misses-in.html</guid>
      <description>&lt;p&gt;It is well known that macOS Finder &lt;code&gt;.DS_Store&lt;/code&gt; files should never be checked in to a repo, or leave the single machine for that matter.&lt;/p&gt;
&lt;p&gt;Fairly recently, I noticed that a lot of my iOS resource processing actions were missing the cache for seemingly no reason. That is, until I looked at the Bazel action inputs. There, I noticed that every action that missed the cache had an extra input. Of course, it was the &lt;code&gt;.DS_Store&lt;/code&gt; file.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The problem&lt;/h2&gt;
&lt;p&gt;The problem popped up because of the act of balancing developer convenience and build correctness. Given the following glob pattern:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;resources = glob([&amp;quot;Assets.xcassets/**&amp;quot;]),
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;we allow engineers to freely add or remove files in an iOS asset catalog without needing to constantly modify the list in the &lt;code&gt;BUILD.bazel&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;This, of course, means that &lt;code&gt;.DS_Store&lt;/code&gt; files can get picked up if the engineer ever opened a Finder window at the given path. One might say that &lt;code&gt;rules_apple&lt;/code&gt; should take care of this. However, that&amp;rsquo;s easier said than done, since asset catalogs can host many different resource types. Plus, Apple might extend the list of accepted resources at any time, which would require a &lt;code&gt;rules_apple&lt;/code&gt; release just to add a file extension to some list.&lt;/p&gt;
&lt;h2 id=&#34;the-solution&#34;&gt;The solution&lt;/h2&gt;
&lt;p&gt;The solution to this problem is quite simple: just make a macro for the &lt;code&gt;glob()&lt;/code&gt; function:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;def safe_glob(include, **kwargs):
    exclude_pattern = kwargs.pop(&amp;quot;exclude&amp;quot;, []) + [&amp;quot;**/.DS_Store&amp;quot;]
    return native.glob(
        include = include,
        exclude = exclude_pattern,
        **kwargs
    )
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now just load this symbol and use it instead of plain &lt;code&gt;glob(...)&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;A neat trick to get around the fact that neither &lt;code&gt;.bazelignore&lt;/code&gt; nor &lt;code&gt;REPO.bazel&lt;/code&gt; solve this problem. I bet similar annoying files exist on Linux as well as Windows.&lt;/p&gt;
</description>
      <source:markdown>It is well known that macOS Finder `.DS_Store` files should never be checked in to a repo, or leave the single machine for that matter.

Fairly recently, I noticed that a lot of my iOS resource processing actions were missing the cache for seemingly no reason. That is, until I looked at the Bazel action inputs. There, I noticed that every action that missed the cache had an extra input. Of course, it was the `.DS_Store` file.

## The problem

The problem popped up because of the act of balancing developer convenience and build correctness. Given the following glob pattern:

```starlark
resources = glob([&#34;Assets.xcassets/**&#34;]),
```

we allow engineers to freely add or remove files in an iOS asset catalog without needing to constantly modify the list in the `BUILD.bazel` file.

This, of course, means that `.DS_Store` files can get picked up if the engineer ever opened a Finder window at the given path. One might say that `rules_apple` should take care of this. However, that&#39;s easier said than done, since asset catalogs can host many different resource types. Plus, Apple might extend the list of accepted resources at any time, which would require a `rules_apple` release just to add a file extension to some list.

## The solution

The solution to this problem is quite simple: just make a macro for the `glob()` function:

```starlark
def safe_glob(include, **kwargs):
    exclude_pattern = kwargs.pop(&#34;exclude&#34;, []) + [&#34;**/.DS_Store&#34;]
    return native.glob(
        include = include,
        exclude = exclude_pattern,
        **kwargs
    )
```

Now just load this symbol and use it instead of plain `glob(...)`.

## Conclusion

A neat trick to get around the fact that neither `.bazelignore` nor `REPO.bazel` solve this problem. I bet similar annoying files exist on Linux as well as Windows.
</source:markdown>
    </item>
    
    <item>
      <title>External Repo File Checks In Bazel 9</title>
      <link>https://adincebic.com/2026/06/14/external-repo-file-checks-in.html</link>
      <pubDate>Sun, 14 Jun 2026 16:42:34 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/06/14/external-repo-file-checks-in.html</guid>
      <description>&lt;p&gt;In a quest to speed up Bazel builds we tend to pick every available low-hanging fruit once somebody discovers it. One of those used to be telling Bazel not to check external repos for file changes, since that can take a while in a dependency-heavy repo.&lt;/p&gt;
&lt;h2 id=&#34;prior-art&#34;&gt;Prior Art&lt;/h2&gt;
&lt;p&gt;Historically we used &lt;code&gt;--noexperimental_check_external_repository_files&lt;/code&gt; to skip checks for files in external repositories. That flag still exists in Bazel (&lt;a href=&#34;https://github.com/bazelbuild/bazel/blob/1af61b21df99edc2fc66939cdf14449c2661f873/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptions.java#L322-L331&#34;&gt;source&lt;/a&gt;), and &lt;code&gt;bazelrc-preset.bzl&lt;/code&gt; still sets it (&lt;a href=&#34;https://github.com/bazel-contrib/bazelrc-preset.bzl/blob/main/flags.bzl#L86-L91&#34;&gt;source&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Bazel 9 gained the repo contents cache via &lt;code&gt;--repo_contents_cache&lt;/code&gt;. Cacheable external repos can now be served out of that cache.&lt;/p&gt;
&lt;p&gt;That matters because Bazel does not treat repo-contents-cache-backed files as the old &lt;code&gt;EXTERNAL_REPO&lt;/code&gt; case. In the source they are tracked as &lt;code&gt;EXTERNAL_OTHER&lt;/code&gt; instead. Bazel 9 also added &lt;code&gt;--experimental_check_external_other_files&lt;/code&gt; to control checks for those paths..&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;If you have repo contents cache enabled and your goal is the old &amp;ldquo;don&amp;rsquo;t spend time stat&amp;rsquo;ing external repos on no-op builds&amp;rdquo; behavior, you likely want both:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--noexperimental_check_external_repository_files&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--experimental_check_external_other_files=false&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The old flag still matters for repos that are not served out of the repo contents cache. The new flag matters for cache-backed repos.&lt;/p&gt;
&lt;p&gt;If repo contents cache is disabled, &lt;code&gt;--experimental_check_external_other_files=false&lt;/code&gt; can still help with those broader &lt;code&gt;EXTERNAL_OTHER&lt;/code&gt; checks, but it does not replace the old external-repository flag.&lt;/p&gt;
</description>
      <source:markdown>In a quest to speed up Bazel builds we tend to pick every available low-hanging fruit once somebody discovers it. One of those used to be telling Bazel not to check external repos for file changes, since that can take a while in a dependency-heavy repo.

## Prior Art

Historically we used `--noexperimental_check_external_repository_files` to skip checks for files in external repositories. That flag still exists in Bazel ([source](https://github.com/bazelbuild/bazel/blob/1af61b21df99edc2fc66939cdf14449c2661f873/src/main/java/com/google/devtools/build/lib/bazel/repository/RepositoryOptions.java#L322-L331)), and `bazelrc-preset.bzl` still sets it ([source](https://github.com/bazel-contrib/bazelrc-preset.bzl/blob/main/flags.bzl#L86-L91)).

Bazel 9 gained the repo contents cache via `--repo_contents_cache`. Cacheable external repos can now be served out of that cache.

That matters because Bazel does not treat repo-contents-cache-backed files as the old `EXTERNAL_REPO` case. In the source they are tracked as `EXTERNAL_OTHER` instead. Bazel 9 also added `--experimental_check_external_other_files` to control checks for those paths..

## Conclusion

If you have repo contents cache enabled and your goal is the old &#34;don&#39;t spend time stat&#39;ing external repos on no-op builds&#34; behavior, you likely want both:

- `--noexperimental_check_external_repository_files`
- `--experimental_check_external_other_files=false`

The old flag still matters for repos that are not served out of the repo contents cache. The new flag matters for cache-backed repos.

If repo contents cache is disabled, `--experimental_check_external_other_files=false` can still help with those broader `EXTERNAL_OTHER` checks, but it does not replace the old external-repository flag.
</source:markdown>
    </item>
    
    <item>
      <title>Cleaning up old Bazel patterns</title>
      <link>https://adincebic.com/2026/06/07/cleaning-up-old-bazel-patterns.html</link>
      <pubDate>Sun, 07 Jun 2026 21:41:28 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/06/07/cleaning-up-old-bazel-patterns.html</guid>
      <description>&lt;p&gt;From time to time, it is worth cleaning up old Bazel stuff in your repositories. This is especially useful before a major Bazel upgrade, because it reduces the amount of migration noise you need to deal with. Most of these cleanups are not difficult, but they make the codebase a little easier to deal with.&lt;/p&gt;
&lt;p&gt;The suggestions below are relevant if you are on Bazel 8.1.0 or newer.&lt;/p&gt;
&lt;h2 id=&#34;sets&#34;&gt;Sets&lt;/h2&gt;
&lt;p&gt;Starting with Bazel 8.1, Starlark has native support for sets, which removes the need to use &lt;code&gt;sets&lt;/code&gt; from &lt;code&gt;bazel_skylib&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So instead of:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;sets.make([1, 2, 3])
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;you can write:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;set([1, 2, 3])
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Native sets support the usual set algebra, such as union, intersection, difference, and symmetric difference, so this should cover most use cases where you previously reached for &lt;code&gt;bazel_skylib&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;remove-function_transition_allowlist-when-creating-transitions&#34;&gt;Remove function_transition_allowlist when creating transitions&lt;/h2&gt;
&lt;p&gt;The conventional wisdom used to be that you needed to create a private &lt;code&gt;_allowlist_function_transition&lt;/code&gt; attribute for Starlark transitions to work.&lt;/p&gt;
&lt;p&gt;That is no longer necessary in modern Bazel versions, so you can remove:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;&amp;quot;_allowlist_function_transition&amp;quot;: attr.label(
    default = &amp;quot;@bazel_tools//tools/allowlists/function_transition_allowlist&amp;quot;,
),
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;repo_name-is-usually-no-longer-worth-keeping&#34;&gt;repo_name is usually no longer worth keeping&lt;/h2&gt;
&lt;p&gt;Historically, many repositories used reverse-DNS-style names for external dependencies because that was the common WORKSPACE convention. With Bzlmod, the module name is usually the better default.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;bazel_dep(name = &amp;quot;rules_swift&amp;quot;, version = &amp;quot;3.6.1&amp;quot;, repo_name = &amp;quot;build_bazel_rules_swift&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;can become:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;bazel_dep(name = &amp;quot;rules_swift&amp;quot;, version = &amp;quot;3.6.1&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This makes labels and load statements shorter and easier to write by hand.&lt;/p&gt;
&lt;p&gt;Just make sure you update any remaining references to the old apparent repository name, such as &lt;code&gt;@build_bazel_rules_swift&lt;/code&gt;, before removing &lt;code&gt;repo_name&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;start-using-repobazel&#34;&gt;Start using &lt;code&gt;REPO.bazel&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;I already wrote about this in my article about &lt;a href=&#34;https://adincebic.com/2026/04/19/a-better-way-to-ignore.html&#34;&gt;dropping .bazelignore&lt;/a&gt; and in &lt;a href=&#34;https://adincebic.com/2026/05/24/suppressing-warnings-in-external-swift.html&#34;&gt;suppressing warnings in external Swift repositories&lt;/a&gt;, so I encourage reading those if you want more details.&lt;/p&gt;
&lt;p&gt;The short version is that &lt;code&gt;REPO.bazel&lt;/code&gt; gives you a better place to express repository-wide behavior. It marks a repository boundary and lets you set repository-level attributes in a way that fits better with modern Bazel.&lt;/p&gt;
&lt;h2 id=&#34;compatibility_level-is-a-no-op&#34;&gt;&lt;code&gt;compatibility_level&lt;/code&gt; is a no-op&lt;/h2&gt;
&lt;p&gt;If you are a rules author, do not spend time tuning &lt;code&gt;compatibility_level&lt;/code&gt;. Starting with Bazel 8.6.0 and 9.1.0, both &lt;code&gt;compatibility_level&lt;/code&gt; and &lt;code&gt;max_compatibility_level&lt;/code&gt; are no-ops.&lt;/p&gt;
&lt;p&gt;This makes me extremely happy because this thing often created more pain for users than it solved. If you introduce a breaking change, it is better to provide clear error messages and an actionable migration path instead of relying on Bazel module version selection to protect users.&lt;/p&gt;
&lt;h2 id=&#34;a-word-about-flags&#34;&gt;A word about flags&lt;/h2&gt;
&lt;p&gt;In every major Bazel version, there are flags that get removed, become no-ops, or get flipped.&lt;/p&gt;
&lt;p&gt;I do not recommend tracking all of that manually. There is a good chance you will miss something, or sometimes get it wrong. The better approach is to use &lt;a href=&#34;https://registry.bazel.build/modules/bazelrc-preset.bzl&#34;&gt;bazelrc-preset.bzl&lt;/a&gt;, which applies version-appropriate flags for the Bazel version you are using.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;There is probably more cleanup work that I am missing. However, these are low-hanging improvements that are usually easy to apply and easy to review.&lt;/p&gt;
</description>
      <source:markdown>From time to time, it is worth cleaning up old Bazel stuff in your repositories. This is especially useful before a major Bazel upgrade, because it reduces the amount of migration noise you need to deal with. Most of these cleanups are not difficult, but they make the codebase a little easier to deal with.

The suggestions below are relevant if you are on Bazel 8.1.0 or newer.

## Sets

Starting with Bazel 8.1, Starlark has native support for sets, which removes the need to use `sets` from `bazel_skylib`.

So instead of:

```starlark
sets.make([1, 2, 3])
```

you can write:

```starlark
set([1, 2, 3])
```

Native sets support the usual set algebra, such as union, intersection, difference, and symmetric difference, so this should cover most use cases where you previously reached for `bazel_skylib`.

## Remove function_transition_allowlist when creating transitions

The conventional wisdom used to be that you needed to create a private `_allowlist_function_transition` attribute for Starlark transitions to work.

That is no longer necessary in modern Bazel versions, so you can remove:

```starlark
&#34;_allowlist_function_transition&#34;: attr.label(
    default = &#34;@bazel_tools//tools/allowlists/function_transition_allowlist&#34;,
),
```

## repo_name is usually no longer worth keeping

Historically, many repositories used reverse-DNS-style names for external dependencies because that was the common WORKSPACE convention. With Bzlmod, the module name is usually the better default.

For example:

```starlark
bazel_dep(name = &#34;rules_swift&#34;, version = &#34;3.6.1&#34;, repo_name = &#34;build_bazel_rules_swift&#34;)
```

can become:

```starlark
bazel_dep(name = &#34;rules_swift&#34;, version = &#34;3.6.1&#34;)
```

This makes labels and load statements shorter and easier to write by hand.

Just make sure you update any remaining references to the old apparent repository name, such as `@build_bazel_rules_swift`, before removing `repo_name`.

## Start using `REPO.bazel`

I already wrote about this in my article about [dropping .bazelignore](https://adincebic.com/2026/04/19/a-better-way-to-ignore.html) and in [suppressing warnings in external Swift repositories](https://adincebic.com/2026/05/24/suppressing-warnings-in-external-swift.html), so I encourage reading those if you want more details.

The short version is that `REPO.bazel` gives you a better place to express repository-wide behavior. It marks a repository boundary and lets you set repository-level attributes in a way that fits better with modern Bazel.

## `compatibility_level` is a no-op

If you are a rules author, do not spend time tuning `compatibility_level`. Starting with Bazel 8.6.0 and 9.1.0, both `compatibility_level` and `max_compatibility_level` are no-ops.

This makes me extremely happy because this thing often created more pain for users than it solved. If you introduce a breaking change, it is better to provide clear error messages and an actionable migration path instead of relying on Bazel module version selection to protect users.

## A word about flags

In every major Bazel version, there are flags that get removed, become no-ops, or get flipped.

I do not recommend tracking all of that manually. There is a good chance you will miss something, or sometimes get it wrong. The better approach is to use [bazelrc-preset.bzl](https://registry.bazel.build/modules/bazelrc-preset.bzl), which applies version-appropriate flags for the Bazel version you are using.

## Conclusion

There is probably more cleanup work that I am missing. However, these are low-hanging improvements that are usually easy to apply and easy to review.
</source:markdown>
    </item>
    
    <item>
      <title>Running Multiple Bazel Targets in a Single Invocation</title>
      <link>https://adincebic.com/2026/05/31/running-multiple-bazel-targets-in.html</link>
      <pubDate>Sun, 31 May 2026 17:44:36 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/05/31/running-multiple-bazel-targets-in.html</guid>
      <description>&lt;p&gt;There are many instances where it would be really convenient to run multiple targets at once. By default, Bazel will not execute all targets even if you pass multiple ones:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;bazel run //:lint //:format
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this case, only one of them would be executed.&lt;/p&gt;
&lt;h2 id=&#34;enter-rules_multirun&#34;&gt;Enter rules_multirun&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/keith/rules_multirun&#34;&gt;rules_multirun&lt;/a&gt; is a set of rules that helps with running multiple targets either sequentially or in parallel. It is developed and maintained by &lt;a href=&#34;https://github.com/keith/&#34;&gt;Keith Smiley&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;running-multiple-targets&#34;&gt;Running multiple targets&lt;/h2&gt;
&lt;p&gt;It is extremely easy to get started. First, load the &lt;code&gt;multirun&lt;/code&gt; rule and use it like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bzl&#34; data-lang=&#34;bzl&#34;&gt;load(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;@rules_multirun//:defs.bzl&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;multirun&amp;#34;&lt;/span&gt;)

multirun(
    name &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;xcodeproj&amp;#34;&lt;/span&gt;,
    testonly &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;,
    commands &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;//apps/app1:xcodeproj&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;//apps/app2:xcodeproj&amp;#34;&lt;/span&gt;],
    jobs &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here, I used the &lt;code&gt;multirun&lt;/code&gt; rule to create a single runnable target that generates Xcode projects for two of my apps:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;bazel run //:xcodeproj
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;execution-modes&#34;&gt;Execution modes&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;jobs&lt;/code&gt; attribute specifies whether targets should run sequentially or in parallel. The default value is &lt;code&gt;1&lt;/code&gt;, which means that targets will run one after the other. In the example above, I explicitly set it to &lt;code&gt;0&lt;/code&gt; to make sure both Xcode projects are generated in parallel.&lt;/p&gt;
&lt;p&gt;This is something that needs to be decided on a case-by-case basis, since parallel execution might not be a good fit for tools that modify files.&lt;/p&gt;
&lt;h2 id=&#34;why-testonly&#34;&gt;Why testonly&lt;/h2&gt;
&lt;p&gt;This is typically not required, but given the specifics of &lt;code&gt;rules_xcodeproj&lt;/code&gt; and my project setup, I need to pass it in this scenario.&lt;/p&gt;
&lt;p&gt;That is because &lt;code&gt;xcodeproj&lt;/code&gt; passes &lt;code&gt;testonly = True&lt;/code&gt; as soon as you add test targets, and it does that to satisfy Bazel’s restriction that non-test targets cannot depend on test-only targets.&lt;/p&gt;
&lt;p&gt;So, like I said, this is typically not needed, but you might run into it, so I figured it was worth explaining.&lt;/p&gt;
&lt;h2 id=&#34;other-rules-from-rules_multirun&#34;&gt;Other rules from rules_multirun&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/keith/rules_multirun&#34;&gt;rules_multirun&lt;/a&gt; is a set of rules, not just the &lt;code&gt;multirun&lt;/code&gt; rule. There is a rule for configuring individual targets and commands, rules for executing targets with transitions, and more.&lt;/p&gt;
&lt;p&gt;It is best to consult the &lt;a href=&#34;https://github.com/keith/rules_multirun/tree/main/doc&#34;&gt;rules_multirun docs on GitHub&lt;/a&gt; for the full list of available options.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This is a ruleset that I find extremely convenient in my daily work, and I tend to strive to optimize that last mile of developer experience whenever I can.&lt;/p&gt;
&lt;p&gt;One thing I intentionally changed: your intro said Bazel executes only the “first” target, but then the example said only &lt;code&gt;format&lt;/code&gt; runs. I made it “only one of them” to avoid the contradiction.&lt;/p&gt;
</description>
      <source:markdown>There are many instances where it would be really convenient to run multiple targets at once. By default, Bazel will not execute all targets even if you pass multiple ones:

```bash
bazel run //:lint //:format
```

In this case, only one of them would be executed.

## Enter rules_multirun

[rules_multirun](https://github.com/keith/rules_multirun) is a set of rules that helps with running multiple targets either sequentially or in parallel. It is developed and maintained by [Keith Smiley](https://github.com/keith/).

## Running multiple targets

It is extremely easy to get started. First, load the `multirun` rule and use it like this:

```bzl
load(&#34;@rules_multirun//:defs.bzl&#34;, &#34;multirun&#34;)

multirun(
    name = &#34;xcodeproj&#34;,
    testonly = True,
    commands = [&#34;//apps/app1:xcodeproj&#34;, &#34;//apps/app2:xcodeproj&#34;],
    jobs = 0,
)
```

Here, I used the `multirun` rule to create a single runnable target that generates Xcode projects for two of my apps:

```bash
bazel run //:xcodeproj
```

## Execution modes

The `jobs` attribute specifies whether targets should run sequentially or in parallel. The default value is `1`, which means that targets will run one after the other. In the example above, I explicitly set it to `0` to make sure both Xcode projects are generated in parallel.

This is something that needs to be decided on a case-by-case basis, since parallel execution might not be a good fit for tools that modify files.

## Why testonly

This is typically not required, but given the specifics of `rules_xcodeproj` and my project setup, I need to pass it in this scenario.

That is because `xcodeproj` passes `testonly = True` as soon as you add test targets, and it does that to satisfy Bazel’s restriction that non-test targets cannot depend on test-only targets.

So, like I said, this is typically not needed, but you might run into it, so I figured it was worth explaining.

## Other rules from rules_multirun

[rules_multirun](https://github.com/keith/rules_multirun) is a set of rules, not just the `multirun` rule. There is a rule for configuring individual targets and commands, rules for executing targets with transitions, and more.

It is best to consult the [rules_multirun docs on GitHub](https://github.com/keith/rules_multirun/tree/main/doc) for the full list of available options.

## Conclusion

This is a ruleset that I find extremely convenient in my daily work, and I tend to strive to optimize that last mile of developer experience whenever I can.

One thing I intentionally changed: your intro said Bazel executes only the “first” target, but then the example said only `format` runs. I made it “only one of them” to avoid the contradiction.
</source:markdown>
    </item>
    
    <item>
      <title>Suppressing Warnings in External Swift Dependencies with Bazel</title>
      <link>https://adincebic.com/2026/05/24/suppressing-warnings-in-external-swift.html</link>
      <pubDate>Sun, 24 May 2026 17:35:00 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/05/24/suppressing-warnings-in-external-swift.html</guid>
      <description>&lt;p&gt;It’s very common to want to apply some Bazel feature only to your first-party repo while omitting external dependencies.&lt;/p&gt;
&lt;p&gt;A common case in the Swift world is suppressing warnings for external dependencies brought in by &lt;code&gt;rules_swift_package_manager&lt;/code&gt;, since we usually can’t do much about third-party code. There are countless other examples too, like treating warnings as errors for our own code while avoiding that for third-party deps.&lt;/p&gt;
&lt;h2 id=&#34;repobazel-to-the-rescue&#34;&gt;REPO.bazel to the rescue&lt;/h2&gt;
&lt;p&gt;I wrote about &lt;code&gt;REPO.bazel&lt;/code&gt; in an &lt;a href=&#34;https://adincebic.com/2026/04/19/a-better-way-to-ignore.html&#34;&gt;earlier article&lt;/a&gt;, where I explained how to replace &lt;code&gt;.bazelignore&lt;/code&gt; with glob semantics.&lt;/p&gt;
&lt;p&gt;For the use cases described in the intro of this article, &lt;code&gt;REPO.bazel&lt;/code&gt; is extremely useful. It lets us apply Bazel features, which I’ve also &lt;a href=&#34;https://adincebic.com/2026/02/08/using-features-in-bazel-rules.html&#34;&gt;written about before&lt;/a&gt;, only to our own repo.&lt;/p&gt;
&lt;h2 id=&#34;suppressing-warnings-in-external-swift-libraries&#34;&gt;Suppressing warnings in external Swift libraries&lt;/h2&gt;
&lt;p&gt;To achieve this, we need to do two things.&lt;/p&gt;
&lt;p&gt;First, suppress warnings globally in &lt;code&gt;.bazelrc&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-.bazelrc&#34; data-lang=&#34;.bazelrc&#34;&gt;# Suppress Swift warnings
common                --features=swift.suppress_warnings
common                --host_features=swift.suppress_warnings

# Suppress clang warnings
common                --features=suppress_warnings
common                --host_features=suppress_warnings
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, disable those features for our first-party repo using the &lt;code&gt;repo(...)&lt;/code&gt; function in &lt;code&gt;REPO.bazel&lt;/code&gt;. The &lt;code&gt;repo(...)&lt;/code&gt; function accepts the same arguments as &lt;code&gt;package(...)&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;repo(
    features = [
        &amp;quot;-swift.suppress_warnings&amp;quot;,
        &amp;quot;-suppress_warnings&amp;quot;,
    ],
)
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;And that’s really it. A neat trick to have in your toolbox.&lt;/p&gt;
&lt;p&gt;I also want to make it clear that I wasn’t aware of this trick until a fellow Apple rules maintainer, &lt;a href=&#34;https://github.com/aaronsky&#34;&gt;Aaron Sky&lt;/a&gt;, shared it in the Bazel Slack workspace.&lt;/p&gt;
</description>
      <source:markdown>It’s very common to want to apply some Bazel feature only to your first-party repo while omitting external dependencies.

A common case in the Swift world is suppressing warnings for external dependencies brought in by `rules_swift_package_manager`, since we usually can’t do much about third-party code. There are countless other examples too, like treating warnings as errors for our own code while avoiding that for third-party deps.

## REPO.bazel to the rescue

I wrote about `REPO.bazel` in an [earlier article](https://adincebic.com/2026/04/19/a-better-way-to-ignore.html), where I explained how to replace `.bazelignore` with glob semantics.

For the use cases described in the intro of this article, `REPO.bazel` is extremely useful. It lets us apply Bazel features, which I’ve also [written about before](https://adincebic.com/2026/02/08/using-features-in-bazel-rules.html), only to our own repo.

## Suppressing warnings in external Swift libraries

To achieve this, we need to do two things.

First, suppress warnings globally in `.bazelrc`:

```.bazelrc
# Suppress Swift warnings
common                --features=swift.suppress_warnings
common                --host_features=swift.suppress_warnings

# Suppress clang warnings
common                --features=suppress_warnings
common                --host_features=suppress_warnings
```

Then, disable those features for our first-party repo using the `repo(...)` function in `REPO.bazel`. The `repo(...)` function accepts the same arguments as `package(...)`:

```starlark
repo(
    features = [
        &#34;-swift.suppress_warnings&#34;,
        &#34;-suppress_warnings&#34;,
    ],
)
```

## Conclusion

And that’s really it. A neat trick to have in your toolbox.

I also want to make it clear that I wasn’t aware of this trick until a fellow Apple rules maintainer, [Aaron Sky](https://github.com/aaronsky), shared it in the Bazel Slack workspace.
</source:markdown>
    </item>
    
    <item>
      <title>Hot Reloading a Bazel-Based iOS App with InjectionNext</title>
      <link>https://adincebic.com/2026/05/17/hot-reloading-a-bazelbased-ios.html</link>
      <pubDate>Sun, 17 May 2026 21:25:55 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/05/17/hot-reloading-a-bazelbased-ios.html</guid>
      <description>&lt;p&gt;When working on a medium to large iOS app, it can be daunting to constantly rebuild and manually go through app screens just to test your changes. Yes, Xcode previews exist, but in my experience, they can be slow on larger projects. They also require real code in the preview setup, which can be tricky to get right if you use dependency injection, since the code that registers all the dependencies probably will not run, often leading to crashes.&lt;/p&gt;
&lt;h2 id=&#34;enter-injectionnext&#34;&gt;Enter InjectionNext&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/johnno1962/InjectionNext/releases/download/2.0.1RC0/InjectionNext.zip&#34;&gt;InjectionNext&lt;/a&gt; is an app that uses the &lt;code&gt;-interposable&lt;/code&gt; linker feature to dynamically swap classes so that changes are reflected without rebuilding the app.&lt;/p&gt;
&lt;p&gt;To set it up in a Bazel-based iOS project, I recommend the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Integrate the &lt;a href=&#34;https://github.com/johnno1962/InjectionNext&#34;&gt;InjectionNext Swift package&lt;/a&gt; using &lt;code&gt;rules_swift_package_manager&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Make sure to set the &lt;code&gt;-interposable&lt;/code&gt; linker flag in debug mode only on your &lt;code&gt;ios_application&lt;/code&gt; target:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;linkopts = select({
    # InjectionNext hot reload needs debug app symbols to stay
    # interposable so injected dylibs can rebind calls to replacement
    # implementations instead of always hitting the original image.
    &amp;quot;//:debug&amp;quot;: [&amp;quot;-interposable&amp;quot;],
    &amp;quot;//conditions:default&amp;quot;: [],
}),
&lt;/code&gt;&lt;/pre&gt;&lt;ol start=&#34;3&#34;&gt;
&lt;li&gt;Similar to the linker flag, make sure that &lt;a href=&#34;https://github.com/johnno1962/InjectionNext&#34;&gt;InjectionNext&lt;/a&gt; is linked to your app only in debug mode on the &lt;code&gt;ios_application&lt;/code&gt; target:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;deps = [&amp;quot;:App.library&amp;quot;] + select({
    &amp;quot;//:debug&amp;quot;: [&amp;quot;@swiftpkg_injectionnext//:InjectionNext&amp;quot;],
    &amp;quot;//conditions:default&amp;quot;: [],
}),
&lt;/code&gt;&lt;/pre&gt;&lt;ol start=&#34;4&#34;&gt;
&lt;li&gt;Finally, you need the &lt;a href=&#34;https://github.com/johnno1962/InjectionNext/releases/download/2.0.1RC0/InjectionNext.zip&#34;&gt;InjectionNext macOS app&lt;/a&gt;. Launch it, then click the option to launch Xcode from there.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Please use the version I linked above or newer, because this is the version where &lt;a href=&#34;https://github.com/johnno1962/InjectionLite/pull/25&#34;&gt;my patch&lt;/a&gt; to make it work with &lt;code&gt;rules_xcodeproj&lt;/code&gt; landed.&lt;/p&gt;
&lt;h2 id=&#34;further-source-changes-needed&#34;&gt;Further Source Changes Needed&lt;/h2&gt;
&lt;p&gt;Unfortunately, this is still not enough. To make everything work, you need to add &lt;code&gt;@objc func injected()&lt;/code&gt; to every UIKit view or view controller where you call functions like &lt;code&gt;setNeedsLayout()&lt;/code&gt; and &lt;code&gt;layoutIfNeeded()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Of course, doing that manually for every view is tedious. The solution is either to integrate &lt;a href=&#34;https://github.com/krzysztofzablocki/Inject&#34;&gt;Inject&lt;/a&gt;, a Swift package that does this for you and also handles SwiftUI, or to write your own Swift macro, such as &lt;code&gt;@HotReloadable&lt;/code&gt;, which you apply to the relevant types and which generates this code for you in debug mode.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I know this can seem like a lot of work, but I firmly believe that it quickly starts paying dividends as soon as you start iterating on your app, since it saves so much time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; To make the integration with &lt;code&gt;rules_swift_package_manager&lt;/code&gt; work, you need a release that includes &lt;a href=&#34;https://github.com/cgrindel/rules_swift_package_manager/pull/2294#event-25625680708&#34;&gt;my fix&lt;/a&gt; for collecting &lt;code&gt;.s&lt;/code&gt; files.&lt;/p&gt;
</description>
      <source:markdown>When working on a medium to large iOS app, it can be daunting to constantly rebuild and manually go through app screens just to test your changes. Yes, Xcode previews exist, but in my experience, they can be slow on larger projects. They also require real code in the preview setup, which can be tricky to get right if you use dependency injection, since the code that registers all the dependencies probably will not run, often leading to crashes.

## Enter InjectionNext

[InjectionNext](https://github.com/johnno1962/InjectionNext/releases/download/2.0.1RC0/InjectionNext.zip) is an app that uses the `-interposable` linker feature to dynamically swap classes so that changes are reflected without rebuilding the app.

To set it up in a Bazel-based iOS project, I recommend the following:

1. Integrate the [InjectionNext Swift package](https://github.com/johnno1962/InjectionNext) using `rules_swift_package_manager`.

2. Make sure to set the `-interposable` linker flag in debug mode only on your `ios_application` target:

```starlark
linkopts = select({
    # InjectionNext hot reload needs debug app symbols to stay
    # interposable so injected dylibs can rebind calls to replacement
    # implementations instead of always hitting the original image.
    &#34;//:debug&#34;: [&#34;-interposable&#34;],
    &#34;//conditions:default&#34;: [],
}),
```

3. Similar to the linker flag, make sure that [InjectionNext](https://github.com/johnno1962/InjectionNext) is linked to your app only in debug mode on the `ios_application` target:

```starlark
deps = [&#34;:App.library&#34;] + select({
    &#34;//:debug&#34;: [&#34;@swiftpkg_injectionnext//:InjectionNext&#34;],
    &#34;//conditions:default&#34;: [],
}),
```

4. Finally, you need the [InjectionNext macOS app](https://github.com/johnno1962/InjectionNext/releases/download/2.0.1RC0/InjectionNext.zip). Launch it, then click the option to launch Xcode from there.

**NOTE:** Please use the version I linked above or newer, because this is the version where [my patch](https://github.com/johnno1962/InjectionLite/pull/25) to make it work with `rules_xcodeproj` landed.

## Further Source Changes Needed

Unfortunately, this is still not enough. To make everything work, you need to add `@objc func injected()` to every UIKit view or view controller where you call functions like `setNeedsLayout()` and `layoutIfNeeded()`.

Of course, doing that manually for every view is tedious. The solution is either to integrate [Inject](https://github.com/krzysztofzablocki/Inject), a Swift package that does this for you and also handles SwiftUI, or to write your own Swift macro, such as `@HotReloadable`, which you apply to the relevant types and which generates this code for you in debug mode.

## Conclusion

I know this can seem like a lot of work, but I firmly believe that it quickly starts paying dividends as soon as you start iterating on your app, since it saves so much time.

**NOTE:** To make the integration with `rules_swift_package_manager` work, you need a release that includes [my fix](https://github.com/cgrindel/rules_swift_package_manager/pull/2294#event-25625680708) for collecting `.s` files.
</source:markdown>
    </item>
    
    <item>
      <title>A Practical Introduction to Bazel Persistent Workers</title>
      <link>https://adincebic.com/2026/05/10/a-practical-introduction-to-bazel.html</link>
      <pubDate>Sun, 10 May 2026 18:45:12 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/05/10/a-practical-introduction-to-bazel.html</guid>
      <description>&lt;p&gt;Typically, Bazel rules execute actions that usually correspond to tool processes on the host OS. Sometimes this behavior can incur startup costs, like bootstrapping a JVM or initializing a compiler. To work around that, Bazel has the concept of &lt;a href=&#34;https://bazel.build/remote/creating&#34;&gt;persistent workers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A persistent worker is essentially a long-lived process that accepts work requests and responds with work responses. Imagine a process that keeps a compiler alive and dispatches sources to compile without paying the startup cost every time.&lt;/p&gt;
&lt;h2 id=&#34;creating-a-rule-that-leverages-workers&#34;&gt;Creating a rule that leverages workers&lt;/h2&gt;
&lt;p&gt;Because this is a fairly advanced concept in Bazel, and usually only rule authors deal with it, I tried to come up with a simple example that demonstrates it.&lt;/p&gt;
&lt;h3 id=&#34;an-uppercase-rule&#34;&gt;An uppercase rule&lt;/h3&gt;
&lt;p&gt;We will write a rule that simply uppercases the text in a given file. To begin, we need to meet a few requirements. The first one is adding dependencies in our &lt;code&gt;MODULE.bazel&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;bazel_dep(name = &amp;quot;swift_argument_parser&amp;quot;, version = &amp;quot;1.7.1&amp;quot;)
bazel_dep(name = &amp;quot;rules_swift&amp;quot;, version = &amp;quot;3.6.1&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These will come into play a bit later.&lt;/p&gt;
&lt;h3 id=&#34;creating-a-rule&#34;&gt;Creating a rule&lt;/h3&gt;
&lt;p&gt;Like I said, this is a simple rule, but the code may look a bit scary at first. Create &lt;code&gt;uppercase.bzl&lt;/code&gt; at the root of the directory:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;def _uppercase_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name + &amp;quot;.out&amp;quot;)
    args_file = ctx.actions.declare_file(ctx.label.name + &amp;quot;.worker_args&amp;quot;)

    # These are the per-action arguments. Bazel will send these to the
    # persistent worker inside each WorkRequest.
    ctx.actions.write(
        output = args_file,
        content = &amp;quot;\n&amp;quot;.join([
            &amp;quot;--input=&amp;quot; + ctx.file.src.path,
            &amp;quot;--output=&amp;quot; + out.path,
        ]),
    )

    ctx.actions.run(
        executable = ctx.executable._worker,
        inputs = [
            ctx.file.src,
            args_file,
        ],
        outputs = [out],
        arguments = [
            # For worker actions, the last argument is special:
            # it must be an @flagfile containing the per-request args.
            &amp;quot;@&amp;quot; + args_file.path,
        ],
        mnemonic = &amp;quot;UppercaseWorker&amp;quot;,
        execution_requirements = {
            &amp;quot;supports-workers&amp;quot;: &amp;quot;1&amp;quot;,
            &amp;quot;requires-worker-protocol&amp;quot;: &amp;quot;json&amp;quot;,
        },
    )

    return [DefaultInfo(files = depset([out]))]


uppercase = rule(
    implementation = _uppercase_impl,
    attrs = {
        &amp;quot;src&amp;quot;: attr.label(
            allow_single_file = True,
            mandatory = True,
        ),
        &amp;quot;_worker&amp;quot;: attr.label(
            default = &amp;quot;//tools:worker&amp;quot;,
            executable = True,
            cfg = &amp;quot;exec&amp;quot;,
        ),
    },
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The important part here is the &lt;code&gt;arguments&lt;/code&gt; list. For worker actions, Bazel treats the last argument specially when it is an &lt;code&gt;@flagfile&lt;/code&gt;. The contents of that file become the per-request arguments inside the &lt;code&gt;WorkRequest&lt;/code&gt;. Any arguments before that are considered startup arguments for the worker process.&lt;/p&gt;
&lt;p&gt;Now that the rule is in place, we need to create the actual worker binary. Because Swift is my language of choice, we will write it using Swift, but you can implement it in any language.&lt;/p&gt;
&lt;h3 id=&#34;the-swift-worker&#34;&gt;The Swift worker&lt;/h3&gt;
&lt;p&gt;Typically, I would split this out into multiple Swift files, but for the sake of simplicity, I will shove everything into one Swift file called &lt;code&gt;worker.swift&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Foundation&lt;/span&gt;
&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ArgumentParser&lt;/span&gt;

&lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;WorkRequest&lt;/span&gt;: Decodable {
    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; arguments: [String]?
    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; requestId: Int?

    &lt;span style=&#34;color:#75715e&#34;&gt;// Bazel may send other fields such as inputs, verbosity, etc.&lt;/span&gt;
    &lt;span style=&#34;color:#75715e&#34;&gt;// JSONDecoder ignores unknown fields by default, which is what we want.&lt;/span&gt;
}

&lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;WorkResponse&lt;/span&gt;: Encodable {
    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; requestId: Int
    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; exitCode: Int
    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; output: String
}

&lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;UppercaseArgs&lt;/span&gt;: ParsableArguments {
    @Option(name: .long)
    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; input: String

    @Option(name: .long)
    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; output: String
}

&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;expandArgs&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;_&lt;/span&gt; args: [String]) &lt;span style=&#34;color:#66d9ef&#34;&gt;throws&lt;/span&gt; -&amp;gt; [String] {
    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; expanded: [String] = []

    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; arg &lt;span style=&#34;color:#66d9ef&#34;&gt;in&lt;/span&gt; args {
        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; arg.hasPrefix(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;@&amp;#34;&lt;/span&gt;) {
            &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; path = String(arg.dropFirst())
            &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; contents = &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; String(contentsOfFile: path, encoding: .utf8)

            &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; line &lt;span style=&#34;color:#66d9ef&#34;&gt;in&lt;/span&gt; contents.split(separator: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;) {
                &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; trimmed = line.trimmingCharacters(&lt;span style=&#34;color:#66d9ef&#34;&gt;in&lt;/span&gt;: .whitespacesAndNewlines)

                &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;trimmed.isEmpty {
                    expanded.append(trimmed)
                }
            }
        } &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; {
            expanded.append(arg)
        }
    }

    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; expanded
}

&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;runOne&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;_&lt;/span&gt; rawArgs: [String]) &lt;span style=&#34;color:#66d9ef&#34;&gt;throws&lt;/span&gt; {
    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; args = &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; UppercaseArgs.parse(expandArgs(rawArgs))

    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; inputText = &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; String(contentsOfFile: args.input, encoding: .utf8)

    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; inputText.uppercased().write(
        toFile: args.output,
        atomically: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;,
        encoding: .utf8
    )
}

&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;writeResponse&lt;/span&gt;(requestId: Int, exitCode: Int = &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, output: String = &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;) {
    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; response = WorkResponse(
        requestId: requestId,
        exitCode: exitCode,
        output: output
    )

    &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt; {
        &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; data = &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; JSONEncoder().encode(response)

        FileHandle.standardOutput.write(data)
        FileHandle.standardOutput.write(Data(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;.utf8))
    } &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; {
        &lt;span style=&#34;color:#75715e&#34;&gt;// Important: do not print normal logs to stdout.&lt;/span&gt;
        &lt;span style=&#34;color:#75715e&#34;&gt;// In worker mode, stdout is reserved for WorkResponse JSON.&lt;/span&gt;
        FileHandle.standardError.write(
            Data(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;failed to encode WorkResponse: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;\(&lt;/span&gt;error&lt;span style=&#34;color:#e6db74&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;.utf8)
        )
        exit(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;)
    }
}

&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;persistentLoop&lt;/span&gt;() {
    &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; decoder = JSONDecoder()

    &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; line = readLine() {
        &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt; {
            &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; request = &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; decoder.decode(
                WorkRequest.&lt;span style=&#34;color:#66d9ef&#34;&gt;self&lt;/span&gt;,
                from: Data(line.utf8)
            )

            &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; requestId = request.requestId ?? &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;
            &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; arguments = request.arguments ?? []

            &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt; {
                &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; runOne(arguments)
                writeResponse(requestId: requestId)
            } &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; {
                writeResponse(
                    requestId: requestId,
                    exitCode: &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;,
                    output: String(describing: error)
                )
            }
        } &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; {
            writeResponse(
                requestId: &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,
                exitCode: &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;,
                output: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;failed to decode WorkRequest: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;\(&lt;/span&gt;error&lt;span style=&#34;color:#e6db74&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
            )
        }
    }
}

@main
&lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Worker&lt;/span&gt; {
    &lt;span style=&#34;color:#66d9ef&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;() {
        &lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; startupArgs = Array(CommandLine.arguments.dropFirst())

        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; startupArgs.contains(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--persistent_worker&amp;#34;&lt;/span&gt;) {
            persistentLoop()
        } &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; {
            &lt;span style=&#34;color:#75715e&#34;&gt;// Non-worker fallback path. This lets the same executable still work&lt;/span&gt;
            &lt;span style=&#34;color:#75715e&#34;&gt;// when Bazel uses local execution instead of worker execution.&lt;/span&gt;
            &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt; {
                &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; runOne(startupArgs)
            } &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; {
                FileHandle.standardError.write(Data(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;\(&lt;/span&gt;error&lt;span style=&#34;color:#e6db74&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;.utf8))
                exit(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;)
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A persistent worker has a small protocol contract with Bazel: it should accept the &lt;code&gt;--persistent_worker&lt;/code&gt; flag, read &lt;code&gt;WorkRequest&lt;/code&gt;s from stdin, and write &lt;code&gt;WorkResponse&lt;/code&gt;s to stdout. If the same binary is run without &lt;code&gt;--persistent_worker&lt;/code&gt;, it should behave like a normal one-shot tool. This fallback path is useful because Bazel may still run the action without the worker strategy.&lt;/p&gt;
&lt;p&gt;One small but important detail: in worker mode, stdout belongs to the worker protocol. If you need to log something, write it to stderr instead.&lt;/p&gt;
&lt;p&gt;I will not get into every detail here. I do expect the reader to be familiar with Swift and the general concept of Bazel workers.&lt;/p&gt;
&lt;p&gt;We are missing the actual Bazel target for the worker at &lt;code&gt;tools/BUILD.bazel&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;load(&amp;quot;@rules_swift//swift:swift_binary.bzl&amp;quot;, &amp;quot;swift_binary&amp;quot;)

swift_binary(
    name = &amp;quot;worker&amp;quot;,
    srcs = [&amp;quot;worker.swift&amp;quot;],
    visibility = [&amp;quot;//visibility:public&amp;quot;],
    deps = [&amp;quot;@swift_argument_parser//:ArgumentParser&amp;quot;],
)
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;trying-out-the-rule&#34;&gt;Trying out the rule&lt;/h3&gt;
&lt;p&gt;At the root, it is time to create a &lt;code&gt;BUILD.bazel&lt;/code&gt;, load our rule, and build it:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;load(&amp;quot;//:uppercase.bzl&amp;quot;, &amp;quot;uppercase&amp;quot;)

uppercase(
    name = &amp;quot;hello&amp;quot;,
    src = &amp;quot;hello.txt&amp;quot;,
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;hello.txt&lt;/code&gt; is just a text file that I created to demonstrate the rule.&lt;/p&gt;
&lt;h3 id=&#34;building-and-verifying&#34;&gt;Building and verifying&lt;/h3&gt;
&lt;p&gt;To try out our new rule, execute:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;bazel build :hello --spawn_strategy&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;worker,sandboxed --worker_verbose
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We set &lt;code&gt;--spawn_strategy=worker,sandboxed&lt;/code&gt; to make sure that our rule runs using the worker strategy and falls back to the standard &lt;code&gt;sandboxed&lt;/code&gt; strategy. The fallback is important because there are actions that run because of &lt;code&gt;rules_swift&lt;/code&gt; that do not necessarily use workers.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--worker_verbose&lt;/code&gt; is here just to make it easier to see that our worker is being used.&lt;/p&gt;
&lt;p&gt;The output should look something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;INFO: Analyzed target //:hello &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;102&lt;/span&gt; packages loaded, &lt;span style=&#34;color:#ae81ff&#34;&gt;649&lt;/span&gt; targets configured, &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt; aspect applications&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt;.
INFO: Created new non-sandboxed singleplex SwiftCompile worker &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;id 5, key hash -1813863811&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt;, logging to /Users/adincebic/Library/Caches/bazel/_bazel_adincebic/19f2a862cd16d28bfab74de8ca294508/bazel-workers/worker-5-SwiftCompile.log
INFO: Created new non-sandboxed singleplex UppercaseWorker worker &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;id 6, key hash -755134554&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt;, logging to /Users/adincebic/Library/Caches/bazel/_bazel_adincebic/19f2a862cd16d28bfab74de8ca294508/bazel-workers/worker-6-UppercaseWorker.log
INFO: Found &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; target...
Target //:hello up-to-date:
  bazel-bin/hello.out
INFO: Elapsed time: 19.851s, Critical Path: 19.03s
INFO: &lt;span style=&#34;color:#ae81ff&#34;&gt;60&lt;/span&gt; processes: &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt; internal, &lt;span style=&#34;color:#ae81ff&#34;&gt;26&lt;/span&gt; darwin-sandbox, &lt;span style=&#34;color:#ae81ff&#34;&gt;4&lt;/span&gt; worker.
INFO: Build completed successfully, &lt;span style=&#34;color:#ae81ff&#34;&gt;60&lt;/span&gt; total actions
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To verify the result, inspect &lt;code&gt;bazel-bin/hello.out&lt;/code&gt;. It should contain the uppercase version of &lt;code&gt;hello.txt&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And that’s it.&lt;/p&gt;
&lt;h2 id=&#34;a-few-notes&#34;&gt;A few notes&lt;/h2&gt;
&lt;p&gt;This is the simplest example I could come up with, and it comes with a few caveats:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My worker implementation does not implement cancellation.&lt;/li&gt;
&lt;li&gt;This is a singleplex worker, meaning Bazel sends it one request at a time.&lt;/li&gt;
&lt;li&gt;The parsing logic could be more robust.&lt;/li&gt;
&lt;li&gt;The worker ignores fields like &lt;code&gt;inputs&lt;/code&gt; and &lt;code&gt;verbosity&lt;/code&gt; from &lt;code&gt;WorkRequest&lt;/code&gt;, which is fine for this example but probably not what you would do in a production worker.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This is one of those advanced Bazel concepts that you do not run into often, even if you write your own rules, purely because it is not always needed. But if you ever need persistent workers, I hope this gets you started.&lt;/p&gt;
</description>
      <source:markdown>Typically, Bazel rules execute actions that usually correspond to tool processes on the host OS. Sometimes this behavior can incur startup costs, like bootstrapping a JVM or initializing a compiler. To work around that, Bazel has the concept of [persistent workers](https://bazel.build/remote/creating).

A persistent worker is essentially a long-lived process that accepts work requests and responds with work responses. Imagine a process that keeps a compiler alive and dispatches sources to compile without paying the startup cost every time.

## Creating a rule that leverages workers

Because this is a fairly advanced concept in Bazel, and usually only rule authors deal with it, I tried to come up with a simple example that demonstrates it.

### An uppercase rule

We will write a rule that simply uppercases the text in a given file. To begin, we need to meet a few requirements. The first one is adding dependencies in our `MODULE.bazel`:

```starlark
bazel_dep(name = &#34;swift_argument_parser&#34;, version = &#34;1.7.1&#34;)
bazel_dep(name = &#34;rules_swift&#34;, version = &#34;3.6.1&#34;)
```

These will come into play a bit later.

### Creating a rule

Like I said, this is a simple rule, but the code may look a bit scary at first. Create `uppercase.bzl` at the root of the directory:

```starlark
def _uppercase_impl(ctx):
    out = ctx.actions.declare_file(ctx.label.name + &#34;.out&#34;)
    args_file = ctx.actions.declare_file(ctx.label.name + &#34;.worker_args&#34;)

    # These are the per-action arguments. Bazel will send these to the
    # persistent worker inside each WorkRequest.
    ctx.actions.write(
        output = args_file,
        content = &#34;\n&#34;.join([
            &#34;--input=&#34; + ctx.file.src.path,
            &#34;--output=&#34; + out.path,
        ]),
    )

    ctx.actions.run(
        executable = ctx.executable._worker,
        inputs = [
            ctx.file.src,
            args_file,
        ],
        outputs = [out],
        arguments = [
            # For worker actions, the last argument is special:
            # it must be an @flagfile containing the per-request args.
            &#34;@&#34; + args_file.path,
        ],
        mnemonic = &#34;UppercaseWorker&#34;,
        execution_requirements = {
            &#34;supports-workers&#34;: &#34;1&#34;,
            &#34;requires-worker-protocol&#34;: &#34;json&#34;,
        },
    )

    return [DefaultInfo(files = depset([out]))]


uppercase = rule(
    implementation = _uppercase_impl,
    attrs = {
        &#34;src&#34;: attr.label(
            allow_single_file = True,
            mandatory = True,
        ),
        &#34;_worker&#34;: attr.label(
            default = &#34;//tools:worker&#34;,
            executable = True,
            cfg = &#34;exec&#34;,
        ),
    },
)
```

The important part here is the `arguments` list. For worker actions, Bazel treats the last argument specially when it is an `@flagfile`. The contents of that file become the per-request arguments inside the `WorkRequest`. Any arguments before that are considered startup arguments for the worker process.

Now that the rule is in place, we need to create the actual worker binary. Because Swift is my language of choice, we will write it using Swift, but you can implement it in any language.

### The Swift worker

Typically, I would split this out into multiple Swift files, but for the sake of simplicity, I will shove everything into one Swift file called `worker.swift`:

```swift
import Foundation
import ArgumentParser

struct WorkRequest: Decodable {
    var arguments: [String]?
    var requestId: Int?

    // Bazel may send other fields such as inputs, verbosity, etc.
    // JSONDecoder ignores unknown fields by default, which is what we want.
}

struct WorkResponse: Encodable {
    var requestId: Int
    var exitCode: Int
    var output: String
}

struct UppercaseArgs: ParsableArguments {
    @Option(name: .long)
    var input: String

    @Option(name: .long)
    var output: String
}

func expandArgs(_ args: [String]) throws -&gt; [String] {
    var expanded: [String] = []

    for arg in args {
        if arg.hasPrefix(&#34;@&#34;) {
            let path = String(arg.dropFirst())
            let contents = try String(contentsOfFile: path, encoding: .utf8)

            for line in contents.split(separator: &#34;\n&#34;) {
                let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)

                if !trimmed.isEmpty {
                    expanded.append(trimmed)
                }
            }
        } else {
            expanded.append(arg)
        }
    }

    return expanded
}

func runOne(_ rawArgs: [String]) throws {
    let args = try UppercaseArgs.parse(expandArgs(rawArgs))

    let inputText = try String(contentsOfFile: args.input, encoding: .utf8)

    try inputText.uppercased().write(
        toFile: args.output,
        atomically: true,
        encoding: .utf8
    )
}

func writeResponse(requestId: Int, exitCode: Int = 0, output: String = &#34;&#34;) {
    let response = WorkResponse(
        requestId: requestId,
        exitCode: exitCode,
        output: output
    )

    do {
        let data = try JSONEncoder().encode(response)

        FileHandle.standardOutput.write(data)
        FileHandle.standardOutput.write(Data(&#34;\n&#34;.utf8))
    } catch {
        // Important: do not print normal logs to stdout.
        // In worker mode, stdout is reserved for WorkResponse JSON.
        FileHandle.standardError.write(
            Data(&#34;failed to encode WorkResponse: \(error)\n&#34;.utf8)
        )
        exit(1)
    }
}

func persistentLoop() {
    let decoder = JSONDecoder()

    while let line = readLine() {
        do {
            let request = try decoder.decode(
                WorkRequest.self,
                from: Data(line.utf8)
            )

            let requestId = request.requestId ?? 0
            let arguments = request.arguments ?? []

            do {
                try runOne(arguments)
                writeResponse(requestId: requestId)
            } catch {
                writeResponse(
                    requestId: requestId,
                    exitCode: 1,
                    output: String(describing: error)
                )
            }
        } catch {
            writeResponse(
                requestId: 0,
                exitCode: 1,
                output: &#34;failed to decode WorkRequest: \(error)&#34;
            )
        }
    }
}

@main
struct Worker {
    static func main() {
        let startupArgs = Array(CommandLine.arguments.dropFirst())

        if startupArgs.contains(&#34;--persistent_worker&#34;) {
            persistentLoop()
        } else {
            // Non-worker fallback path. This lets the same executable still work
            // when Bazel uses local execution instead of worker execution.
            do {
                try runOne(startupArgs)
            } catch {
                FileHandle.standardError.write(Data(&#34;\(error)\n&#34;.utf8))
                exit(1)
            }
        }
    }
}
```

A persistent worker has a small protocol contract with Bazel: it should accept the `--persistent_worker` flag, read `WorkRequest`s from stdin, and write `WorkResponse`s to stdout. If the same binary is run without `--persistent_worker`, it should behave like a normal one-shot tool. This fallback path is useful because Bazel may still run the action without the worker strategy.

One small but important detail: in worker mode, stdout belongs to the worker protocol. If you need to log something, write it to stderr instead.

I will not get into every detail here. I do expect the reader to be familiar with Swift and the general concept of Bazel workers.

We are missing the actual Bazel target for the worker at `tools/BUILD.bazel`:

```starlark
load(&#34;@rules_swift//swift:swift_binary.bzl&#34;, &#34;swift_binary&#34;)

swift_binary(
    name = &#34;worker&#34;,
    srcs = [&#34;worker.swift&#34;],
    visibility = [&#34;//visibility:public&#34;],
    deps = [&#34;@swift_argument_parser//:ArgumentParser&#34;],
)
```

### Trying out the rule

At the root, it is time to create a `BUILD.bazel`, load our rule, and build it:

```starlark
load(&#34;//:uppercase.bzl&#34;, &#34;uppercase&#34;)

uppercase(
    name = &#34;hello&#34;,
    src = &#34;hello.txt&#34;,
)
```

`hello.txt` is just a text file that I created to demonstrate the rule.

### Building and verifying

To try out our new rule, execute:

```bash
bazel build :hello --spawn_strategy=worker,sandboxed --worker_verbose
```

We set `--spawn_strategy=worker,sandboxed` to make sure that our rule runs using the worker strategy and falls back to the standard `sandboxed` strategy. The fallback is important because there are actions that run because of `rules_swift` that do not necessarily use workers.

`--worker_verbose` is here just to make it easier to see that our worker is being used.

The output should look something like this:

```bash
INFO: Analyzed target //:hello (102 packages loaded, 649 targets configured, 2 aspect applications).
INFO: Created new non-sandboxed singleplex SwiftCompile worker (id 5, key hash -1813863811), logging to /Users/adincebic/Library/Caches/bazel/_bazel_adincebic/19f2a862cd16d28bfab74de8ca294508/bazel-workers/worker-5-SwiftCompile.log
INFO: Created new non-sandboxed singleplex UppercaseWorker worker (id 6, key hash -755134554), logging to /Users/adincebic/Library/Caches/bazel/_bazel_adincebic/19f2a862cd16d28bfab74de8ca294508/bazel-workers/worker-6-UppercaseWorker.log
INFO: Found 1 target...
Target //:hello up-to-date:
  bazel-bin/hello.out
INFO: Elapsed time: 19.851s, Critical Path: 19.03s
INFO: 60 processes: 30 internal, 26 darwin-sandbox, 4 worker.
INFO: Build completed successfully, 60 total actions
```

To verify the result, inspect `bazel-bin/hello.out`. It should contain the uppercase version of `hello.txt`.

And that’s it.

## A few notes

This is the simplest example I could come up with, and it comes with a few caveats:

* My worker implementation does not implement cancellation.
* This is a singleplex worker, meaning Bazel sends it one request at a time.
* The parsing logic could be more robust.
* The worker ignores fields like `inputs` and `verbosity` from `WorkRequest`, which is fine for this example but probably not what you would do in a production worker.

## Conclusion

This is one of those advanced Bazel concepts that you do not run into often, even if you write your own rules, purely because it is not always needed. But if you ever need persistent workers, I hope this gets you started.
</source:markdown>
    </item>
    
    <item>
      <title>Why My Xcode Extension Kept Asking for File Permissions</title>
      <link>https://adincebic.com/2026/05/03/why-my-xcode-extension-kept.html</link>
      <pubDate>Sun, 03 May 2026 21:41:59 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/05/03/why-my-xcode-extension-kept.html</guid>
      <description>&lt;p&gt;Recently, I worked on developing an Xcode source editor extension that needed to run some of our internal code formatters. These formatters are driven by configuration files that define how the tools should be executed. Because Xcode extensions must be sandboxed, they can’t directly access arbitrary file locations, including these configuration files.&lt;/p&gt;
&lt;p&gt;To work around this, we used a container app to prompt users to select the location of the configuration files. We then created security-scoped bookmarks and passed them to the extension process. As expected, the standard way to share data between processes—such as an app and its extension—is by using Apple’s App Groups capability.&lt;/p&gt;
&lt;p&gt;After setting this up, I noticed that the extension kept prompting the user to grant access to the shared files, even though both the app and extension were part of the same app group. This was unexpected—intuitively, accessing files within your own shared container shouldn’t trigger permission prompts.&lt;/p&gt;
&lt;h2 id=&#34;the-mistake&#34;&gt;The mistake&lt;/h2&gt;
&lt;p&gt;Coming from an iOS background, I defined the app group ID like this:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;&amp;lt;key&amp;gt;com.apple.security.application-groups&amp;lt;/key&amp;gt;
&amp;lt;array&amp;gt;
	&amp;lt;string&amp;gt;group.example.app&amp;lt;/string&amp;gt;
&amp;lt;/array&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After running both the app and the extension and inspecting &lt;code&gt;~/Library/Group Containers/&lt;/code&gt;, it was clear that the shared container had been created. However, what I missed is that on macOS, App Group identifiers must be prefixed with the Team ID (for example, &lt;code&gt;TEAMID.group.example.app&lt;/code&gt;). This allows the system to correctly associate the app group with your developer account and properly link the app and its extension.&lt;/p&gt;
&lt;p&gt;Without this prefix, the container may still appear to exist, but entitlement validation and access behavior can be inconsistent—leading to issues like repeated permission prompts.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This turned out to be one of those frustrating issues where the root cause isn’t immediately obvious, even after checking open-source projects and documentation. To be fair, Apple does document this requirement—but it’s easy to overlook, especially since iOS does not require this detail and doesn’t expose the same behavior as clearly.&lt;/p&gt;
</description>
      <source:markdown>Recently, I worked on developing an Xcode source editor extension that needed to run some of our internal code formatters. These formatters are driven by configuration files that define how the tools should be executed. Because Xcode extensions must be sandboxed, they can’t directly access arbitrary file locations, including these configuration files.

To work around this, we used a container app to prompt users to select the location of the configuration files. We then created security-scoped bookmarks and passed them to the extension process. As expected, the standard way to share data between processes—such as an app and its extension—is by using Apple’s App Groups capability.

After setting this up, I noticed that the extension kept prompting the user to grant access to the shared files, even though both the app and extension were part of the same app group. This was unexpected—intuitively, accessing files within your own shared container shouldn’t trigger permission prompts.

## The mistake

Coming from an iOS background, I defined the app group ID like this:

```
&lt;key&gt;com.apple.security.application-groups&lt;/key&gt;
&lt;array&gt;
	&lt;string&gt;group.example.app&lt;/string&gt;
&lt;/array&gt;
```

After running both the app and the extension and inspecting `~/Library/Group Containers/`, it was clear that the shared container had been created. However, what I missed is that on macOS, App Group identifiers must be prefixed with the Team ID (for example, `TEAMID.group.example.app`). This allows the system to correctly associate the app group with your developer account and properly link the app and its extension.

Without this prefix, the container may still appear to exist, but entitlement validation and access behavior can be inconsistent—leading to issues like repeated permission prompts.

## Conclusion

This turned out to be one of those frustrating issues where the root cause isn’t immediately obvious, even after checking open-source projects and documentation. To be fair, Apple does document this requirement—but it’s easy to overlook, especially since iOS does not require this detail and doesn’t expose the same behavior as clearly.
</source:markdown>
    </item>
    
    <item>
      <title>Centralizing Dependency Fetching in Bazel with the Remote Asset API</title>
      <link>https://adincebic.com/2026/04/26/centralizing-dependency-fetching-in-bazel.html</link>
      <pubDate>Sun, 26 Apr 2026 17:40:57 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/04/26/centralizing-dependency-fetching-in-bazel.html</guid>
      <description>&lt;p&gt;It has become increasingly common for major providers to experience outages—from Git servers being unavailable to failures when downloading external dependencies.&lt;/p&gt;
&lt;p&gt;There are several ways to work around this, such as internal mirrors, vendoring dependencies, and similar approaches. While effective, these solutions can feel somewhat heavy-handed.&lt;/p&gt;
&lt;h2 id=&#34;bazel-remote-asset-api&#34;&gt;Bazel Remote Asset API&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&#34;https://github.com/bazelbuild/remote-apis/blob/main/build/bazel/remote/asset/v1/remote_asset.proto&#34;&gt;Bazel Remote Asset API&lt;/a&gt; provides a mechanism for managing external dependencies in a centralized way.&lt;/p&gt;
&lt;p&gt;More precisely, it maps &lt;strong&gt;external resource identifiers (such as URLs or Git repositories)&lt;/strong&gt; to &lt;strong&gt;content stored in a content-addressable storage (CAS)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In practice, this allows a server to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fetch external resources (e.g. tarballs, Git repos)&lt;/li&gt;
&lt;li&gt;Store them in CAS&lt;/li&gt;
&lt;li&gt;Serve them to clients by digest&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When used via Bazel’s remote downloader, this effectively acts as a &lt;strong&gt;download proxy/cache&lt;/strong&gt;: instead of every developer machine and CI runner downloading dependencies independently, requests go through a central service that can fetch and cache them once.&lt;/p&gt;
&lt;h2 id=&#34;how-to-use&#34;&gt;How to Use&lt;/h2&gt;
&lt;p&gt;Getting started is straightforward: pass
&lt;code&gt;--experimental_remote_downloader=SERVER_ADDRESS&lt;/code&gt;
either on the command line or in your &lt;code&gt;.bazelrc&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This configures Bazel to route external downloads through a Remote Asset API–compatible service.&lt;/p&gt;
&lt;p&gt;Before using it, ensure your remote cache/server supports the API. Many commercial solutions do, and the popular open-source &lt;a href=&#34;https://github.com/buchgr/bazel-remote&#34;&gt;bazel-remote&lt;/a&gt; supports (a subset of) it as well—though support is still marked experimental.&lt;/p&gt;
&lt;h2 id=&#34;a-note-on-the-experimental-flag&#34;&gt;A Note on the Experimental Flag&lt;/h2&gt;
&lt;p&gt;Although the flag is prefixed with &lt;code&gt;experimental&lt;/code&gt;, the feature has been available for some time and is widely used in practice. There is some good info on the &lt;a href=&#34;https://bazelbuild.slack.com/archives/CA31HN1T3/p1777051154383449&#34;&gt;Bazel Slack&lt;/a&gt; about it.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Combined with Bazel’s repository cache, the Remote Asset API provides a nice way to improve reliability when fetching external repos. It reduces reliance on third-party availability while avoiding the operational overhead of fully vendoring or mirroring all dependencies.&lt;/p&gt;
</description>
      <source:markdown>It has become increasingly common for major providers to experience outages—from Git servers being unavailable to failures when downloading external dependencies.

There are several ways to work around this, such as internal mirrors, vendoring dependencies, and similar approaches. While effective, these solutions can feel somewhat heavy-handed.

## Bazel Remote Asset API

The [Bazel Remote Asset API](https://github.com/bazelbuild/remote-apis/blob/main/build/bazel/remote/asset/v1/remote_asset.proto) provides a mechanism for managing external dependencies in a centralized way.

More precisely, it maps **external resource identifiers (such as URLs or Git repositories)** to **content stored in a content-addressable storage (CAS)**.

In practice, this allows a server to:

* Fetch external resources (e.g. tarballs, Git repos)
* Store them in CAS
* Serve them to clients by digest

When used via Bazel’s remote downloader, this effectively acts as a **download proxy/cache**: instead of every developer machine and CI runner downloading dependencies independently, requests go through a central service that can fetch and cache them once.

## How to Use

Getting started is straightforward: pass
`--experimental_remote_downloader=SERVER_ADDRESS`
either on the command line or in your `.bazelrc`.

This configures Bazel to route external downloads through a Remote Asset API–compatible service.

Before using it, ensure your remote cache/server supports the API. Many commercial solutions do, and the popular open-source [bazel-remote](https://github.com/buchgr/bazel-remote) supports (a subset of) it as well—though support is still marked experimental.

## A Note on the Experimental Flag

Although the flag is prefixed with `experimental`, the feature has been available for some time and is widely used in practice. There is some good info on the [Bazel Slack](https://bazelbuild.slack.com/archives/CA31HN1T3/p1777051154383449) about it.

## Conclusion

Combined with Bazel’s repository cache, the Remote Asset API provides a nice way to improve reliability when fetching external repos. It reduces reliance on third-party availability while avoiding the operational overhead of fully vendoring or mirroring all dependencies.
</source:markdown>
    </item>
    
    <item>
      <title>A Better Way to Ignore Files in Bazel with repo.bazel</title>
      <link>https://adincebic.com/2026/04/19/a-better-way-to-ignore.html</link>
      <pubDate>Sun, 19 Apr 2026 22:22:10 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/04/19/a-better-way-to-ignore.html</guid>
      <description>&lt;p&gt;In the Bazel world, we don’t always want it to track all the files in our repository. A typical example is ignoring the &lt;code&gt;.git&lt;/code&gt; directory, as it can grow quite large over time. Additionally, some IDE integrations like &lt;code&gt;rules_xcodeproj&lt;/code&gt; don’t work particularly well when it is present.&lt;/p&gt;
&lt;p&gt;Traditionally, to instruct Bazel to ignore directories and files, we used the &lt;code&gt;.bazelignore&lt;/code&gt; file, which requires explicitly listing paths to ignore. This works, but it has an important limitation: &lt;code&gt;.bazelignore&lt;/code&gt; does not support glob patterns. As a result, we often need to update the file whenever new directories should be ignored—and it’s easy to forget to do so.&lt;/p&gt;
&lt;h2 id=&#34;introducing-repobazel&#34;&gt;Introducing &lt;code&gt;repo.bazel&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;repo.bazel&lt;/code&gt; is a simple configuration file that allows us to achieve similar behavior, but with support for glob patterns. It is a relatively recent addition to Bazel, introduced around the same time as bzlmod.&lt;/p&gt;
&lt;p&gt;An example &lt;code&gt;repo.bazel&lt;/code&gt; file looks like this:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;ignore_directories([
    # Ignore all .build directories produced by Swift Package Manager
    &amp;quot;**/.build&amp;quot;,
    # Ignore Node modules directories
    &amp;quot;**/node_modules&amp;quot;,
])
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And that’s it.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This approach builds on the same idea as &lt;code&gt;.bazelignore&lt;/code&gt;, but adds a few quality-of-life improvements—most notably, support for glob patterns.&lt;/p&gt;
&lt;p&gt;For more information, see the &lt;a href=&#34;https://bazel.build/rules/lib/globals/repo&#34;&gt;official Bazel documentation&lt;/a&gt;.&lt;/p&gt;
</description>
      <source:markdown>In the Bazel world, we don’t always want it to track all the files in our repository. A typical example is ignoring the `.git` directory, as it can grow quite large over time. Additionally, some IDE integrations like `rules_xcodeproj` don’t work particularly well when it is present.

Traditionally, to instruct Bazel to ignore directories and files, we used the `.bazelignore` file, which requires explicitly listing paths to ignore. This works, but it has an important limitation: `.bazelignore` does not support glob patterns. As a result, we often need to update the file whenever new directories should be ignored—and it’s easy to forget to do so.

## Introducing `repo.bazel`

`repo.bazel` is a simple configuration file that allows us to achieve similar behavior, but with support for glob patterns. It is a relatively recent addition to Bazel, introduced around the same time as bzlmod.

An example `repo.bazel` file looks like this:

```starlark
ignore_directories([
    # Ignore all .build directories produced by Swift Package Manager
    &#34;**/.build&#34;,
    # Ignore Node modules directories
    &#34;**/node_modules&#34;,
])
```

And that’s it.

## Conclusion

This approach builds on the same idea as `.bazelignore`, but adds a few quality-of-life improvements—most notably, support for glob patterns.

For more information, see the [official Bazel documentation](https://bazel.build/rules/lib/globals/repo).
</source:markdown>
    </item>
    
    <item>
      <title>Reconfiguring bazel downloader</title>
      <link>https://adincebic.com/2026/04/12/reconfiguring-bazel-downloader.html</link>
      <pubDate>Sun, 12 Apr 2026 16:00:36 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/04/12/reconfiguring-bazel-downloader.html</guid>
      <description>&lt;p&gt;There are many security as well as practical reasons why one might need to reconfigure Bazel&amp;rsquo;s downloading behavior. One concrete case that I ran into fairly recently was Google rate-limiting our CI for an unknown reason. To work around that, I needed to redirect the downloader to a mirror. There are many ways to achieve that, like patching individual rules (tedious), using an internal registry (doesn&amp;rsquo;t solve everything), etc.&lt;/p&gt;
&lt;h2 id=&#34;bazel-downloader-config&#34;&gt;Bazel downloader config&lt;/h2&gt;
&lt;p&gt;Bazel offers a way to configure its downloader in a very simple manner. Unfortunately, it is not very well documented, but there are various resources online as well as the actual &lt;a href=&#34;https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriterConfig.java#L66&#34;&gt;Bazel source&lt;/a&gt;, which explains it quite nicely. To enable it, we simply pass &lt;code&gt;--downloader_config=&amp;lt;path_to_file&amp;gt;&lt;/code&gt; either on the command line or in &lt;code&gt;.bazelrc&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;file-structure-and-syntax&#34;&gt;File structure and syntax&lt;/h2&gt;
&lt;p&gt;The structure is easy to understand because it allows only a small set of directives:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;allow host.name&lt;/code&gt; to allow a specific domain&lt;/li&gt;
&lt;li&gt;&lt;code&gt;block host.name&lt;/code&gt; to block a certain domain (also supports &lt;code&gt;block *&lt;/code&gt; to block everything except what is explicitly allowed)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rewrite pattern replacement&lt;/code&gt; to rewrite URLs using regex&lt;/li&gt;
&lt;li&gt;&lt;code&gt;all_blocked_message message&lt;/code&gt; — a message shown if all candidate URLs end up blocked&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;rewrite-directive&#34;&gt;Rewrite directive&lt;/h2&gt;
&lt;p&gt;Because all other directives are fairly self-explanatory, I will focus only on &lt;code&gt;rewrite&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As an example, if we want to ensure that all GitHub downloads are redirected to an internal Artifactory, we could write a file like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-cfg&#34; data-lang=&#34;cfg&#34;&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;rewrite github.com/(.*) internal.artifactory.example.com/$1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Of course, it is possible to define more sophisticated rewrite patterns, e.g.:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-cfg&#34; data-lang=&#34;cfg&#34;&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;rewrite android.googlesource.com/platform/dalvik/\+archive/([0-9a-f]+)\.tar\.gz mirror.bazel.build/android.googlesource.com/platform/dalvik/+archive/$1.tar.gz&lt;/span&gt;
&lt;span style=&#34;color:#a6e22e&#34;&gt;rewrite android.googlesource.com/platform/dalvik/\+archive/([0-9a-f]+)\.tar\.gz android.googlesource.com/platform/dalvik/+archive/$1.tar.gz&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This rewrites requests for &lt;code&gt;android.googlesource.com&lt;/code&gt; to &lt;code&gt;mirror.bazel.build&lt;/code&gt; for this specific &lt;code&gt;dalvik&lt;/code&gt; archive. The second rewrite directive ensures that Bazel falls back to the original URL if the mirror is unavailable.&lt;/p&gt;
&lt;h2 id=&#34;evaluation-order&#34;&gt;Evaluation order&lt;/h2&gt;
&lt;p&gt;Bazel applies the directives in the following order, regardless of their position in the file:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;rewrite&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;allow&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;block&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;comments&#34;&gt;Comments&lt;/h2&gt;
&lt;p&gt;It is possible to add comments using &lt;code&gt;#&lt;/code&gt; at the beginning of a line. Keep in mind that inline comments are not supported.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Typically, this is not needed very often, but it is good to keep the option in the back of your mind so you can reach for it when needed.&lt;/p&gt;
</description>
      <source:markdown>There are many security as well as practical reasons why one might need to reconfigure Bazel&#39;s downloading behavior. One concrete case that I ran into fairly recently was Google rate-limiting our CI for an unknown reason. To work around that, I needed to redirect the downloader to a mirror. There are many ways to achieve that, like patching individual rules (tedious), using an internal registry (doesn&#39;t solve everything), etc.

## Bazel downloader config

Bazel offers a way to configure its downloader in a very simple manner. Unfortunately, it is not very well documented, but there are various resources online as well as the actual [Bazel source](https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/UrlRewriterConfig.java#L66), which explains it quite nicely. To enable it, we simply pass `--downloader_config=&lt;path_to_file&gt;` either on the command line or in `.bazelrc`.

## File structure and syntax

The structure is easy to understand because it allows only a small set of directives:

* `allow host.name` to allow a specific domain
* `block host.name` to block a certain domain (also supports `block *` to block everything except what is explicitly allowed)
* `rewrite pattern replacement` to rewrite URLs using regex
* `all_blocked_message message` — a message shown if all candidate URLs end up blocked

## Rewrite directive

Because all other directives are fairly self-explanatory, I will focus only on `rewrite`.

As an example, if we want to ensure that all GitHub downloads are redirected to an internal Artifactory, we could write a file like this:

```cfg
rewrite github.com/(.*) internal.artifactory.example.com/$1
```

Of course, it is possible to define more sophisticated rewrite patterns, e.g.:

```cfg
rewrite android.googlesource.com/platform/dalvik/\+archive/([0-9a-f]+)\.tar\.gz mirror.bazel.build/android.googlesource.com/platform/dalvik/+archive/$1.tar.gz
rewrite android.googlesource.com/platform/dalvik/\+archive/([0-9a-f]+)\.tar\.gz android.googlesource.com/platform/dalvik/+archive/$1.tar.gz
```

This rewrites requests for `android.googlesource.com` to `mirror.bazel.build` for this specific `dalvik` archive. The second rewrite directive ensures that Bazel falls back to the original URL if the mirror is unavailable.

## Evaluation order

Bazel applies the directives in the following order, regardless of their position in the file:

1. `rewrite`
2. `allow`
3. `block`

## Comments

It is possible to add comments using `#` at the beginning of a line. Keep in mind that inline comments are not supported.

## Conclusion

Typically, this is not needed very often, but it is good to keep the option in the back of your mind so you can reach for it when needed.
</source:markdown>
    </item>
    
    <item>
      <title>Bazel Output Groups: Producing Outputs on Demand</title>
      <link>https://adincebic.com/2026/04/05/bazel-output-groups-producing-outputs.html</link>
      <pubDate>Sun, 05 Apr 2026 20:39:47 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/04/05/bazel-output-groups-producing-outputs.html</guid>
      <description>&lt;p&gt;Typically, when writing a Bazel rule, we produce outputs using the &lt;code&gt;DefaultInfo&lt;/code&gt; provider. However, there are cases where we want to produce additional outputs only on demand.&lt;/p&gt;
&lt;h2 id=&#34;enter-output-groups&#34;&gt;Enter output groups&lt;/h2&gt;
&lt;p&gt;Simply put, output groups are a way to tell Bazel to produce different sets of outputs instead of—or in addition to—the default outputs. For example, we might want to generate debug symbols, but we don’t need them unless explicitly requested.&lt;/p&gt;
&lt;h2 id=&#34;smallest-possible-example&#34;&gt;Smallest possible example&lt;/h2&gt;
&lt;p&gt;Here is a minimal rule that demonstrates the use of output groups:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;def _impl(ctx):
    out1 = ctx.actions.declare_file(&amp;quot;main.txt&amp;quot;)
    out2 = ctx.actions.declare_file(&amp;quot;debug.txt&amp;quot;)

    ctx.actions.write(out1, &amp;quot;main output&amp;quot;)
    ctx.actions.write(out2, &amp;quot;debug output&amp;quot;)

    return [
        DefaultInfo(files = depset([out1])),
        OutputGroupInfo(
            debug = depset([out2]),
        ),
    ]

my_rule = rule(
    implementation = _impl,
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Notice how easy it is to use output groups. &lt;code&gt;OutputGroupInfo&lt;/code&gt; is just another provider—a key-value mapping where, in this case, &lt;code&gt;debug&lt;/code&gt; is the key (the output group name), and &lt;code&gt;out2&lt;/code&gt; is the value wrapped in a &lt;code&gt;depset&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;requesting-the-debug-output&#34;&gt;Requesting the debug output&lt;/h2&gt;
&lt;p&gt;If we instantiate this rule in a BUILD file:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-starlark&#34; data-lang=&#34;starlark&#34;&gt;my_rule(
    name = &amp;quot;groups&amp;quot;,
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We can build it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;bazel build :groups
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This produces:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;INFO: Analyzed target //:groups &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt; packages loaded, &lt;span style=&#34;color:#ae81ff&#34;&gt;7&lt;/span&gt; targets configured&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt;.
INFO: Found &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; target...
Target //:groups up-to-date:
  bazel-bin/main.txt
INFO: Elapsed time: 0.119s, Critical Path: 0.00s
INFO: &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt; processes: &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt; internal.
INFO: Build completed successfully, &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt; total actions
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The important part here is &lt;code&gt;bazel-bin/main.txt&lt;/code&gt;. This happens because we did not tell Bazel to include outputs from the &lt;code&gt;debug&lt;/code&gt; output group.&lt;/p&gt;
&lt;p&gt;To do that, we use the &lt;code&gt;--output_groups&lt;/code&gt; flag and specify the group name (in this case, &lt;code&gt;debug&lt;/code&gt;):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;bazel build :groups --output_groups&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;debug
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;INFO: Analyzed target //:groups &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; packages loaded, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; targets configured&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt;.
INFO: Found &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; target...
Target //:groups up-to-date:
  bazel-bin/debug.txt
INFO: Elapsed time: 0.065s, Critical Path: 0.00s
INFO: &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt; processes: &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt; internal.
INFO: Build completed successfully, &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt; total actions
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the debug file is produced.&lt;/p&gt;
&lt;p&gt;An important detail: &lt;code&gt;debug.txt&lt;/code&gt; is produced &lt;em&gt;instead of&lt;/em&gt; &lt;code&gt;main.txt&lt;/code&gt;, not in addition to it. To request both the default outputs and an output group at the same time, use the &lt;code&gt;+&lt;/code&gt; prefix:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;bazel build :groups --output_groups&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;+debug
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This produces both files:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;INFO: Analyzed target //:groups &lt;span style=&#34;color:#f92672&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt; packages loaded, &lt;span style=&#34;color:#ae81ff&#34;&gt;7&lt;/span&gt; targets configured&lt;span style=&#34;color:#f92672&#34;&gt;)&lt;/span&gt;.
INFO: Found &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; target...
Target //:groups up-to-date:
  bazel-bin/debug.txt
  bazel-bin/main.txt
INFO: Elapsed time: 0.108s, Critical Path: 0.00s
INFO: &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt; processes: &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt; internal.
INFO: Build completed successfully, &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt; total actions
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Output groups are simple to use, both when defining rules and when consuming them. They’re a small feature, but an extremely useful one.&lt;/p&gt;
</description>
      <source:markdown>Typically, when writing a Bazel rule, we produce outputs using the `DefaultInfo` provider. However, there are cases where we want to produce additional outputs only on demand.

## Enter output groups

Simply put, output groups are a way to tell Bazel to produce different sets of outputs instead of—or in addition to—the default outputs. For example, we might want to generate debug symbols, but we don’t need them unless explicitly requested.

## Smallest possible example

Here is a minimal rule that demonstrates the use of output groups:

```starlark
def _impl(ctx):
    out1 = ctx.actions.declare_file(&#34;main.txt&#34;)
    out2 = ctx.actions.declare_file(&#34;debug.txt&#34;)

    ctx.actions.write(out1, &#34;main output&#34;)
    ctx.actions.write(out2, &#34;debug output&#34;)

    return [
        DefaultInfo(files = depset([out1])),
        OutputGroupInfo(
            debug = depset([out2]),
        ),
    ]

my_rule = rule(
    implementation = _impl,
)
```

Notice how easy it is to use output groups. `OutputGroupInfo` is just another provider—a key-value mapping where, in this case, `debug` is the key (the output group name), and `out2` is the value wrapped in a `depset`.

## Requesting the debug output

If we instantiate this rule in a BUILD file:

```starlark
my_rule(
    name = &#34;groups&#34;,
)
```

We can build it:

```bash
bazel build :groups
```

This produces:

```bash
INFO: Analyzed target //:groups (5 packages loaded, 7 targets configured).
INFO: Found 1 target...
Target //:groups up-to-date:
  bazel-bin/main.txt
INFO: Elapsed time: 0.119s, Critical Path: 0.00s
INFO: 2 processes: 2 internal.
INFO: Build completed successfully, 2 total actions
```

The important part here is `bazel-bin/main.txt`. This happens because we did not tell Bazel to include outputs from the `debug` output group.

To do that, we use the `--output_groups` flag and specify the group name (in this case, `debug`):

```bash
bazel build :groups --output_groups=debug
```

Output:

```bash
INFO: Analyzed target //:groups (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:groups up-to-date:
  bazel-bin/debug.txt
INFO: Elapsed time: 0.065s, Critical Path: 0.00s
INFO: 2 processes: 2 internal.
INFO: Build completed successfully, 2 total actions
```

Now the debug file is produced.

An important detail: `debug.txt` is produced *instead of* `main.txt`, not in addition to it. To request both the default outputs and an output group at the same time, use the `+` prefix:

```bash
bazel build :groups --output_groups=+debug
```

This produces both files:

```bash
INFO: Analyzed target //:groups (5 packages loaded, 7 targets configured).
INFO: Found 1 target...
Target //:groups up-to-date:
  bazel-bin/debug.txt
  bazel-bin/main.txt
INFO: Elapsed time: 0.108s, Critical Path: 0.00s
INFO: 3 processes: 3 internal.
INFO: Build completed successfully, 3 total actions
```

## Conclusion

Output groups are simple to use, both when defining rules and when consuming them. They’re a small feature, but an extremely useful one.
</source:markdown>
    </item>
    
    <item>
      <title>What Bazel Really Runs (and How to See It)</title>
      <link>https://adincebic.com/2026/03/29/what-bazel-really-runs-and.html</link>
      <pubDate>Sun, 29 Mar 2026 18:16:03 +0200</pubDate>
      
      <guid>http://adincebic.micro.blog/2026/03/29/what-bazel-really-runs-and.html</guid>
      <description>&lt;p&gt;There comes a time when working with Bazel when we want to understand the command-line flags used to build our code. For example, you might want to see what flags are being passed to &lt;code&gt;swiftc&lt;/code&gt;. Up until Bazel 9, we would typically rely on &lt;code&gt;--subcommands&lt;/code&gt;, but it could get quite verbose.&lt;/p&gt;
&lt;h2 id=&#34;action-graph-query&#34;&gt;Action graph query&lt;/h2&gt;
&lt;p&gt;In addition to the standard &lt;code&gt;bazel query&lt;/code&gt; command, there are also &lt;code&gt;bazel cquery&lt;/code&gt; (configurable query) and &lt;code&gt;bazel aquery&lt;/code&gt; (action graph query). Each of these helps us explore different parts of the build graph. Since we’re interested in inspecting command-line flags, &lt;code&gt;aquery&lt;/code&gt; is the right tool—it exposes all declared actions, including the exact commands being executed.&lt;/p&gt;
&lt;p&gt;For a project like &lt;a href=&#34;https://github.com/mattrobmattrob/bazel-ios-swiftui-template&#34;&gt;this iOS template&lt;/a&gt;, we can explore how Swift code is compiled by running:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;bazel aquery //app:app.library --output&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;commands
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which produces output like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;bazel-out/darwin_arm64-opt-exec/bin/external/rules_swift+/tools/worker/worker swiftc -target arm64-apple-macos12.6 -sdk __BAZEL_XCODE_SDKROOT__ -file-prefix-map &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;__BAZEL_XCODE_DEVELOPER_DIR__=/PLACEHOLDER_DEVELOPER_DIR&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;-Xwrapped-swift=-bazel-target-label=@@//app:app.library&amp;#39;&lt;/span&gt; -emit-object -output-file-map bazel-out/darwin_arm64-fastbuild/bin/app/app.library.output_file_map.json -Xfrontend -no-clang-module-breadcrumbs -emit-module-path bazel-out/darwin_arm64-fastbuild/bin/app/app.swiftmodule &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;-enforce-exclusivity=checked&amp;#39;&lt;/span&gt; -emit-const-values-path bazel-out/darwin_arm64-fastbuild/bin/app/app.library_objs/source/ContentView.swift.swiftconstvalues -Xfrontend -const-gather-protocols-file -Xfrontend external/rules_swift+/swift/toolchains/config/const_protocols_to_gather.json -DDEBUG -Onone -Xfrontend -internalize-at-link -Xfrontend -no-serialize-debugging-options -enable-testing -disable-sandbox -gline-tables-only &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;-Xwrapped-swift=-file-prefix-pwd-is-dot&amp;#39;&lt;/span&gt; -file-prefix-map &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;__BAZEL_XCODE_DEVELOPER_DIR__=/PLACEHOLDER_DEVELOPER_DIR&amp;#39;&lt;/span&gt; -file-compilation-dir . -module-cache-path bazel-out/darwin_arm64-fastbuild/bin/_swift_module_cache -Ibazel-out/darwin_arm64-fastbuild/bin/modules/Models -Ibazel-out/darwin_arm64-fastbuild/bin/modules/API &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;-Xwrapped-swift=-macro-expansion-dir=bazel-out/darwin_arm64-fastbuild/bin/app/app.library.macro-expansions&amp;#39;&lt;/span&gt; -Xcc -iquote. -Xcc -iquotebazel-out/darwin_arm64-fastbuild/bin -Xfrontend -color-diagnostics -enable-batch-mode -module-name app -index-store-path bazel-out/darwin_arm64-fastbuild/bin/app/app.library.indexstore -index-ignore-system-modules &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;-Xwrapped-swift=-global-index-store-import-path=bazel-out/_global_index_store&amp;#39;&lt;/span&gt; -enable-bare-slash-regex -Xfrontend -disable-clang-spi -enable-experimental-feature AccessLevelOnImport -parse-as-library -static -Xcc -O0 -Xcc &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;-DDEBUG=1&amp;#39;&lt;/span&gt; -Xfrontend &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;-checked-async-objc-bridging=on&amp;#39;&lt;/span&gt; app/source/ContentView.swift app/source/MainApp.swift
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At first glance, this output looks overwhelming. But if you break it down, it’s simply Bazel invoking tools with the appropriate flags.&lt;/p&gt;
&lt;h2 id=&#34;doing-something-useful&#34;&gt;Doing something useful&lt;/h2&gt;
&lt;p&gt;While this output can help us understand what is being executed and how, it becomes much more powerful when used comparatively.&lt;/p&gt;
&lt;p&gt;One practical approach is to diff this output across ruleset versions or Bazel releases. For example:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;bazel aquery //app:app.library --output&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;commands &amp;gt; commands.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can generate one file per version and use standard diffing tools to spot regressions or better understand what changed between versions.&lt;/p&gt;
&lt;h2 id=&#34;making-it-executable&#34;&gt;Making it executable&lt;/h2&gt;
&lt;p&gt;In a &lt;a href=&#34;https://www.youtube.com/watch?v=QJUTeD43QlE&#34;&gt;Bazel 9 video by aspect.build&lt;/a&gt;, Alex Eagle shared an interesting idea: turning &lt;code&gt;aquery&lt;/code&gt; output into an executable shell script.&lt;/p&gt;
&lt;p&gt;That idea is what got me intrigued. While the output isn’t directly executable, it seems feasible to get there by replacing placeholder variables, adjusting formatting, and fiddling with cwd. With a bit of effort, this could become a powerful debugging tool.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This is a small quality-of-life improvement in Bazel 9, but it unlocks a very practical debugging technique.&lt;/p&gt;
</description>
      <source:markdown>There comes a time when working with Bazel when we want to understand the command-line flags used to build our code. For example, you might want to see what flags are being passed to `swiftc`. Up until Bazel 9, we would typically rely on `--subcommands`, but it could get quite verbose.

## Action graph query

In addition to the standard `bazel query` command, there are also `bazel cquery` (configurable query) and `bazel aquery` (action graph query). Each of these helps us explore different parts of the build graph. Since we’re interested in inspecting command-line flags, `aquery` is the right tool—it exposes all declared actions, including the exact commands being executed.

For a project like [this iOS template](https://github.com/mattrobmattrob/bazel-ios-swiftui-template), we can explore how Swift code is compiled by running:

```bash
bazel aquery //app:app.library --output=commands
```

Which produces output like:

```bash
bazel-out/darwin_arm64-opt-exec/bin/external/rules_swift+/tools/worker/worker swiftc -target arm64-apple-macos12.6 -sdk __BAZEL_XCODE_SDKROOT__ -file-prefix-map &#39;__BAZEL_XCODE_DEVELOPER_DIR__=/PLACEHOLDER_DEVELOPER_DIR&#39; &#39;-Xwrapped-swift=-bazel-target-label=@@//app:app.library&#39; -emit-object -output-file-map bazel-out/darwin_arm64-fastbuild/bin/app/app.library.output_file_map.json -Xfrontend -no-clang-module-breadcrumbs -emit-module-path bazel-out/darwin_arm64-fastbuild/bin/app/app.swiftmodule &#39;-enforce-exclusivity=checked&#39; -emit-const-values-path bazel-out/darwin_arm64-fastbuild/bin/app/app.library_objs/source/ContentView.swift.swiftconstvalues -Xfrontend -const-gather-protocols-file -Xfrontend external/rules_swift+/swift/toolchains/config/const_protocols_to_gather.json -DDEBUG -Onone -Xfrontend -internalize-at-link -Xfrontend -no-serialize-debugging-options -enable-testing -disable-sandbox -gline-tables-only &#39;-Xwrapped-swift=-file-prefix-pwd-is-dot&#39; -file-prefix-map &#39;__BAZEL_XCODE_DEVELOPER_DIR__=/PLACEHOLDER_DEVELOPER_DIR&#39; -file-compilation-dir . -module-cache-path bazel-out/darwin_arm64-fastbuild/bin/_swift_module_cache -Ibazel-out/darwin_arm64-fastbuild/bin/modules/Models -Ibazel-out/darwin_arm64-fastbuild/bin/modules/API &#39;-Xwrapped-swift=-macro-expansion-dir=bazel-out/darwin_arm64-fastbuild/bin/app/app.library.macro-expansions&#39; -Xcc -iquote. -Xcc -iquotebazel-out/darwin_arm64-fastbuild/bin -Xfrontend -color-diagnostics -enable-batch-mode -module-name app -index-store-path bazel-out/darwin_arm64-fastbuild/bin/app/app.library.indexstore -index-ignore-system-modules &#39;-Xwrapped-swift=-global-index-store-import-path=bazel-out/_global_index_store&#39; -enable-bare-slash-regex -Xfrontend -disable-clang-spi -enable-experimental-feature AccessLevelOnImport -parse-as-library -static -Xcc -O0 -Xcc &#39;-DDEBUG=1&#39; -Xfrontend &#39;-checked-async-objc-bridging=on&#39; app/source/ContentView.swift app/source/MainApp.swift
...
```

At first glance, this output looks overwhelming. But if you break it down, it’s simply Bazel invoking tools with the appropriate flags.

## Doing something useful

While this output can help us understand what is being executed and how, it becomes much more powerful when used comparatively.

One practical approach is to diff this output across ruleset versions or Bazel releases. For example:

```bash
bazel aquery //app:app.library --output=commands &gt; commands.txt
```

You can generate one file per version and use standard diffing tools to spot regressions or better understand what changed between versions.

## Making it executable

In a [Bazel 9 video by aspect.build](https://www.youtube.com/watch?v=QJUTeD43QlE), Alex Eagle shared an interesting idea: turning `aquery` output into an executable shell script.

That idea is what got me intrigued. While the output isn’t directly executable, it seems feasible to get there by replacing placeholder variables, adjusting formatting, and fiddling with cwd. With a bit of effort, this could become a powerful debugging tool.

## Conclusion

This is a small quality-of-life improvement in Bazel 9, but it unlocks a very practical debugging technique.
</source:markdown>
    </item>
    
  </channel>
</rss>
