1"use strict";(self.webpackChunkbuildbuddy_docs_website=self.webpackChunkbuildbuddy_docs_website||[]).push([[4143],{80792(e,n,i){i.r(n),i.d(n,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>t,toc:()=>d});var t=i(91206),s=i(86070),a=i(35676);const o={slug:"how-to-migrate-an-ios-app-to-bazel",title:"How to migrate an iOS app to Bazel",description:"Wondering how to migrate your Apple-based project to Bazel? Whether it's iOS, macOS, watchOS, tvOS, or visionOS, this guide is for you.",authors:"brentley",date:"2024-03-13:08:00:00",image:"/img/ios_bazel.png",tags:["bazel"]},r=void 0,l={authorsImageUrls:[void 0]},d=[{value:"Table of contents",id:"table-of-contents",level:2},{value:"Bootstrapping Bazel",id:"bootstrapping-bazel",level:2},{value:"Bazelisk",id:"bazelisk",level:3},{value:"<code>.bazelversion</code>",id:"bazelversion",level:3},{value:"<code>MODULE.bazel</code>",id:"modulebazel",level:3},{value:"Verifying",id:"verifying",level:3},{value:"Defining targets",id:"defining-targets",level:2},{value:"Rules",id:"rules",level:3},{value:"<code>swift_library</code>",id:"swift_library",level:4},{value:"<code>objc_library</code>",id:"objc_library",level:4},{value:"<code>cc_library</code>",id:"cc_library",level:4},{value:"<code>experimental_mixed_language_library</code>",id:"experimental_mixed_language_library",level:4},{value:"<code>apple_intent_library</code>",id:"apple_intent_library",level:4},{value:"<code>apple_resource_bundle</code>",id:"apple_resource_bundle",level:4},{value:"<code>ios_application</code>",id:"ios_application",level:4},{value:"<code>ios_app_clip</code>",id:"ios_app_clip",level:4},{value:"<code>ios_extension</code>",id:"ios_extension",level:4},{value:"<code>ios_framework</code>",id:"ios_framework",level:4},{value:"<code>ios_unit_test</code>",id:"ios_unit_test",level:4},{value:"<code>ios_ui_test</code>",id:"ios_ui_test",level:4},{value:"<code>local_provisioning_profile</code>",id:"local_provisioning_profile",level:4},{value:"Translating the Xcode project",id:"translating-the-xcode-project",level:3},{value:"Dependencies",id:"dependencies",level:4},{value:"<strong>apple_support</strong>, <strong>rules_swift</strong>, <strong>rules_apple</strong>, and <strong>bazel_skylib</strong>",id:"apple_support-rules_swift-rules_apple-and-bazel_skylib",level:4},{value:"Static libraries",id:"static-libraries",level:4},{value:"Provisioning profiles",id:"provisioning-profiles",level:4},{value:"Swift packages (SwiftPM)",id:"swift-packages-swiftpm",level:4},{value:"App extensions",id:"app-extensions",level:4},{value:"Applications",id:"applications",level:4},{value:"Tests",id:"tests",level:4},{value:"Frameworks",id:"frameworks",level:4},{value:"Codegen",id:"codegen",level:4},{value:"Integrating with Xcode",id:"integrating-with-xcode",level:2},{value:"<code>xcodeproj</code>",id:"xcodeproj",level:3},{value:"Generating the Xcode project",id:"generating-the-xcode-project",level:3},{value:"Ensuring Bazel sees the correct Xcode version",id:"ensuring-bazel-sees-the-correct-xcode-version",level:3},{value:"Generating release archives",id:"generating-release-archives",level:3},{value:"Leveraging remote caching and remote execution",id:"leveraging-remote-caching-and-remote-execution",level:2},{value:"Disk cache",id:"disk-cache",level:3},{value:"Remote cache",id:"remote-cache",level:3},{value:"Debugging cache hits",id:"debugging-cache-hits",level:3},{value:"<strong>rules_xcodeproj</strong> cache warming",id:"rules_xcodeproj-cache-warming",level:3},{value:"Build event service",id:"build-event-service",level:3},{value:"Remote execution",id:"remote-execution",level:3},{value:"Optimizing",id:"optimizing",level:2},{value:"Optimal Bazel settings",id:"optimal-bazel-settings",level:3},{value:"Cacheability",id:"cacheability",level:4},{value:"Local performance",id:"local-performance",level:4},{value:"Remote performance",id:"remote-performance",level:4},{value:"Modularization",id:"modularization",level:3},{value:"Next steps",id:"next-steps",level:2}];function c(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",section:"section",strong:"strong",sup:"sup",ul:"ul",...(0,a.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.p,{children:["Do you have an iOS app,\nor really any Apple-based project,\nthat you want to migrate to Bazel?\nWith this guide I'll show you how to migrate your project,\nusing the ",(0,s.jsx)(n.a,{href:"https://github.com/mastodon/mastodon-ios",children:"Mastodon iOS"})," project as an example."]}),"\n",(0,s.jsxs)(n.p,{children:["We will use iOS based rules in the example migration,\nbut similar rules exist in ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple",children:(0,s.jsx)(n.strong,{children:"rules_apple"})})," for the other Apple platforms,\nincluding\nmacOS,\nwatchOS,\ntvOS,\nand visionOS."]}),"\n",(0,s.jsxs)(n.p,{children:["The completed migration is available in ",(0,s.jsx)(n.a,{href:"https://github.com/brentleyjones/mastodon-ios/tree/bj/migrate-to-bazel",children:"my fork of mastodon-ios"}),".\nYou can follow along with the changes made in the following sections by checking out ",(0,s.jsx)(n.a,{href:"https://github.com/brentleyjones/mastodon-ios/commit/c15c418cfc94f0ac2ccab9a10e7a4f08a2f402c4",children:"this commit"})," first.\nAt the end of some sections there will be a link to a new commit that includes the changes mentioned up to that point."]}),"\n",(0,s.jsxs)(n.p,{children:["If Bazel is completely new to you,\nI recommend reading the official ",(0,s.jsx)(n.a,{href:"https://bazel.build/run/build",children:"quick start guide"}),".\nThat will explain some foundational things that I don't cover in this guide."]}),"\n",(0,s.jsx)(n.h2,{id:"table-of-contents",children:"Table of contents"}),"\n",(0,s.jsx)("nav",{className:"toc",children:(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#bootstrapping-bazel",children:"Bootstrapping Bazel"})}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"#defining-targets",children:"Defining targets"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#rules",children:"Rules"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#translating-the-xcode-project",children:"Translating the Xcode project"})}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#integrating-with-xcode",children:"Integrating with Xcode"})}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"#leveraging-remote-caching-and-remote-execution",children:"Leveraging remote caching and remote execution"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#disk-cache",children:"Disk cache"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#remote-cache",children:"Remote cache"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#debugging-cache-hits",children:"Debugging cache hits"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsxs)(n.a,{href:"#rules_xcodeproj-cache-warming",children:[(0,s.jsx)(n.strong,{children:"rules_xcodeproj"})," cache warming"]})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#build-event-service",children:"Build event service"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#remote-execution",children:"Remote execution"})}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"#optimizing",children:"Optimizing"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#optimal-bazel-settings",children:"Optimal Bazel settings"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#modularization",children:"Modularization"})}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"#next-steps",children:"Next steps"})}),"\n"]})}),"\n",(0,s.jsx)(n.h2,{id:"bootstrapping-bazel",children:"Bootstrapping Bazel"}),"\n",(0,s.jsx)(n.p,{children:"Before we can start to define the structure of our project,\nwe need to get some foundational Bazel setup out of the way."}),"\n",(0,s.jsx)(n.h3,{id:"bazelisk",children:"Bazelisk"}),"\n",(0,s.jsxs)(n.p,{children:["There are two main ways to run Bazel: directly or via ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/bazelisk",children:"Bazelisk"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"For this migration I'm going to use Bazelisk.\nI recommend using Bazelisk for multiple reasons:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"One installed binary supports multiple Bazel versions"}),"\n",(0,s.jsxs)(n.li,{children:["The project's supported Bazel version can be managed with a ",(0,s.jsx)(n.code,{children:".bazelversion"})," file"]}),"\n",(0,s.jsxs)(n.li,{children:["The ",(0,s.jsx)(n.code,{children:"tools/bazel"})," wrapper script is automatically run if it exists"]}),"\n",(0,s.jsxs)(n.li,{children:["The Bazel community has largely settled on ",(0,s.jsx)(n.a,{href:"https://bazel.build/install/bazelisk",children:"using Bazelisk as a best practice"})]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"If you don't already have Bazelisk installed,\nI recommend installing it with Homebrew:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shellsession",children:"$ brew install bazelisk\n"})}),"\n",(0,s.jsxs)(n.p,{children:["This will install Bazelisk as ",(0,s.jsx)(n.code,{children:"bazel"}),".\nBecause of this,\nwhen you see ",(0,s.jsx)(n.code,{children:"bazel"})," in future examples,\nit's actually running Bazelisk\n(which then runs Bazel)."]}),"\n",(0,s.jsx)(n.p,{children:"By the way,\nbecause we are going to be using newer features of Bazel,\nmake sure your installed version of Bazelisk is at least v1.19.0."}),"\n",(0,s.jsx)(n.h3,{id:"bazelversion",children:(0,s.jsx)(n.code,{children:".bazelversion"})}),"\n",(0,s.jsxs)(n.p,{children:["Now that we have Bazelisk installed,\nwe need to tell it which version of Bazel we want it to download and run for us.\
1nWe can do that by creating a ",(0,s.jsx)(n.code,{children:".bazelversion"})," file at the root of the project:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shellsession",children:"$ echo '7.0.2' > .bazelversion\n"})}),"\n",(0,s.jsx)(n.h3,{id:"modulebazel",children:(0,s.jsx)(n.code,{children:"MODULE.bazel"})}),"\n",(0,s.jsxs)(n.p,{children:["Since Bazel can be run in subdirectories of a project,\nit uses the existence of a ",(0,s.jsx)(n.a,{href:"https://bazel.build/external/overview#repository",children:"repository boundary marker file"})," to designate the root of the project\n(which Bazel calls a ",(0,s.jsx)(n.a,{href:"https://bazel.build/external/overview#workspace",children:"workspace"}),").\nWe're going to use a ",(0,s.jsx)(n.code,{children:"MODULE.bazel"})," file for this,\nsince we will eventually have ",(0,s.jsx)(n.a,{href:"https://bazel.build/external/overview#bzlmod",children:"Bazel module dependencies"})," to declare.\nWe can start with an empty file to begin with:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shellsession",children:"$ touch MODULE.bazel\n"})}),"\n",(0,s.jsx)(n.h3,{id:"verifying",children:"Verifying"}),"\n",(0,s.jsxs)(n.p,{children:["At this point we can verify that we have Bazel and Bazelisk configured properly by running ",(0,s.jsx)(n.code,{children:"bazel info release"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shellsession",children:"$ bazel info release\nrelease 7.0.2\n"})}),"\n",(0,s.jsxs)(n.p,{children:["At ",(0,s.jsx)(n.a,{href:"https://github.com/brentleyjones/mastodon-ios/commit/7b55ceeaa71b7b2b4159c525b29dc4166c661896",children:"this commit"})," we have Bazel bootstrapped for the Mastodon iOS project."]}),"\n",(0,s.jsx)(n.h2,{id:"defining-targets",children:"Defining targets"}),"\n",(0,s.jsxs)(n.p,{children:["Now that we have the Bazel project bootstrapped,\nwe can start adding ",(0,s.jsx)(n.a,{href:"https://bazel.build/concepts/build-ref#targets",children:"targets"})," to it."]}),"\n",(0,s.jsxs)(n.p,{children:["Bazel targets are defined by instances of ",(0,s.jsx)(n.a,{href:"https://bazel.build/extending/rules",children:"rules"})," in ",(0,s.jsx)(n.a,{href:"https://bazel.build/concepts/build-ref#packages",children:"packages"}),".\nPackages are defined by ",(0,s.jsx)(n.a,{href:"https://bazel.build/concepts/build-files",children:(0,s.jsx)(n.code,{children:"BUILD"})})," files.\nFirst I'll go over the rules we will use,\nand then I'll show how we use them to define our targets."]}),"\n",(0,s.jsx)(n.h3,{id:"rules",children:"Rules"}),"\n",(0,s.jsx)(n.p,{children:"I'm not going to cover every rule that you could use in an Apple-based project,\nbut I will cover some of the more popular/interesting ones,\neven if they won't all be used in this migration."}),"\n",(0,s.jsx)(n.h4,{id:"swift_library",children:(0,s.jsx)(n.code,{children:"swift_library"})}),"\n",(0,s.jsxs)(n.p,{children:["A ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_swift/blob/1.16.0/doc/rules.md#swift_library",children:(0,s.jsx)(n.code,{children:"swift_library"})})," target defines a single Swift static library."]}),"\n",(0,s.jsxs)(n.p,{children:["When built directly\n(ideally though an ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-ios.md#ios_build_test",children:(0,s.jsx)(n.code,{children:"ios_build_test"})})," to ensure it's in the correct configuration),\n",(0,s.jsx)(n.code,{children:"swift_library"})," produces ",(0,s.jsx)(n.code,{children:".swiftmodule"})," and ",(0,s.jsx)(n.code,{children:".swiftdoc"})," files.\nIf the ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_swift/blob/1.16.0/swift/internal/feature_names.bzl#L244-L246",children:(0,s.jsx)(n.code,{children:"swift.enable_library_evolution"})})," and ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_swift/blob/1.16.0/swift/internal/feature_names.bzl#L252-L254",children:(0,s.jsx)(n.code,{children:"swift.emit_swiftinterface"})})," features are enabled,\nit also produces a ",(0,s.jsx)(n.code,{children:".swiftinterface"}
1)," file.\nWhen depended on by an executable producing target,\nsuch as ",(0,s.jsx)(n.a,{href:"#ios_application",children:(0,s.jsx)(n.code,{children:"ios_application"})}),",\nit also produces a ",(0,s.jsx)(n.code,{children:".a"})," file."]}),"\n",(0,s.jsx)(n.h4,{id:"objc_library",children:(0,s.jsx)(n.code,{children:"objc_library"})}),"\n",(0,s.jsxs)(n.p,{children:["An ",(0,s.jsx)(n.a,{href:"https://bazel.build/reference/be/objective-c#objc_library",children:(0,s.jsx)(n.code,{children:"objc_library"})})," target defines a single Objective-C static library.\nUse it instead of ",(0,s.jsx)(n.a,{href:"#cc_library",children:(0,s.jsx)(n.code,{children:"cc_library"})})," when compiling Objective-C or Objective-C++ code."]}),"\n",(0,s.jsxs)(n.p,{children:["When depended on by an executable producing target,\nsuch as an ",(0,s.jsx)(n.a,{href:"#ios_application",children:(0,s.jsx)(n.code,{children:"ios_application"})})," target,\n",(0,s.jsx)(n.code,{children:"objc_library"})," produces a ",(0,s.jsx)(n.code,{children:".a"})," file."]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["If ",(0,s.jsx)(n.code,{children:"srcs"})," is empty,\nan ",(0,s.jsx)(n.code,{children:"objc_library"})," target acts as a collection of\nheaders,\ndefines,\ninclude paths,\nor linkopts,\nwhich are propagated to dependent targets."]})}),"\n",(0,s.jsx)(n.h4,{id:"cc_library",children:(0,s.jsx)(n.code,{children:"cc_library"})}),"\n",(0,s.jsxs)(n.p,{children:["A ",(0,s.jsx)(n.a,{href:"https://bazel.build/reference/be/c-cpp#cc_library",children:(0,s.jsx)(n.code,{children:"cc_library"})})," target defines a single C or C++ static library.\nUse it when compiling C or C++ code.\nWhile you can use ",(0,s.jsx)(n.code,{children:"objc_library"})," to compile C or C++ code,\nusing ",(0,s.jsx)(n.code,{children:"cc_library"})," is more efficient,\nand is more clear in your intent."]}),"\n",(0,s.jsxs)(n.p,{children:["When depended on by an executable producing target,\nsuch as an ",(0,s.jsx)(n.a,{href:"#ios_application",children:(0,s.jsx)(n.code,{children:"ios_application"})})," target,\n",(0,s.jsx)(n.code,{children:"cc_library"})," produces a ",(0,s.jsx)(n.code,{children:".a"})," file."]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["If ",(0,s.jsx)(n.code,{children:"srcs"})," is empty,\na ",(0,s.jsx)(n.code,{children:"cc_library"})," target acts as a collection of\nheaders,\ndefines,\ninclude paths,\nor linkopts,\nwhich are propagated to dependent targets."]})}),"\n",(0,s.jsx)(n.h4,{id:"experimental_mixed_language_library",children:(0,s.jsx)(n.code,{children:"experimental_mixed_language_library"})}),"\n",(0,s.jsxs)(n.p,{children:["An ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-apple.md#experimental_mixed_language_library",children:(0,s.jsx)(n.code,{children:"experimental_mixed_language_library"})})," target defines an Objective-C and Swift mixed-language static library.\nUse it for compiling mixed-language modules."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"experimental_mixed_language_library"})," is actually a ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/extending/macros",children:"macro"})," that creates\na ",(0,s.jsx)(n.a,{href:"#swift_library",children:(0,s.jsx)(n.code,{children:"swift_library"})}),",\nan ",(0,s.jsx)(n.a,{href:"#objc_library",children:(0,s.jsx)(n.code,{children:"objc_library"})}),",\nand some ",(0,s.jsx)(n.a,{href:"https://clang.llvm.org/docs/Modules.html#module-maps",children:"modulemaps"})," to tie them together."]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsx)(n.p,{children:"Due to poor build performance,\nit is not recommended to have mixed-language modules.\nIt's recommended to only use this macro as a migration stopgap until you are able to demix them."})}),"\n",(0,s.jsx)(n.h4,{id:"apple_intent_library",children:(0,s.jsx)(n.code,{children:"apple_intent_library"})}),"\n",(0,s.jsxs)(n.p,{children:["An ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-resources.md#apple_intent_library",children:(0,s.jsx)(n.code,{children:"apple_intent_library"})})," target generates source files for an ",(0,s.jsx)(n.code,{children:".intentdefinition"})," file.\nUse it if you have ",(0,s.jsx)(n.code,{children:".intentdefinition"})," resources."]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-resources.md#swift_intent_library",children:(0,s.jsx)(n.code,{children:"swift_intent_library"})})," and ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-resources.md#objc_intent_library",children:(0,s.jsx)(n.code,{children:"objc_intent_library"})})," macros wrap ",(0,s.jsx)(n.code,{children:"apple_intent_library"})," with ",(0,s.jsx)(n.a,{href:"#swift_library",children:(0,s.jsx)(n.code,{children:"swift_library"})})," and ",(0,s.jsx)(n.a,{href:"#objc_library",children:(0,s.jsx)(n.code,{children:"objc_library"})})," targets respectively.\nUse them instead of ",(0,s.jsx)(n.code,{children:"apple_intent_library"})," directly if possible."]})}),"\n",(0,s.jsx)(n.h4,{id:"apple_resource_bundle",children:(0,s.jsx)(n.code,{children:"apple_resource_bundle"})}),"\n",(0,s.jsxs)(n.p,{children:["An ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-resources.md#apple_resource_bundle",children:(0,s.jsx)(n.code,{children:"apple_resource_bundle"})})," target generates a resource bundle.\nUse it if you require certain resources to be placed in a named ",(0,s.jsx)(n.code,{children:".bundle"}),",\ninstead of directly placed in your top level bundle\n(e.g. an ",(0,s.jsx)(n.code,{children:".app"}),", ",(0,s.jsx)(n.code,{children:".framework"})," , or ",(0,s.jsx)(n.code,{children:".xctest"}),")."]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"apple_resource_bundle"})," targets need to be listed in ",(0,s.jsx)(n.code,{children:"data"})," attributes,\nnot ",(0,s.jsx)(n.code,{children:"deps"})," attributes."]})}),"\n",(0,s.jsx)(n.h4,{id:"ios_application",children:(0,s.jsx)(n.code,{children:"ios_application"})}),"\n",(0,s.jsxs)(n.p,{children:["An ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-ios.md#ios_application",children:(0,s.jsx)(n.code,{children:"ios_application"})})," target generates an application bundle."]}),"\n",(0,s.jsxs)(n.p,{children:["Unlike Xcode,\nBazel separates compilation and bundling targets.\nThis means that an ",(0,s.jsx)(n.code,{children:"ios_application"})," target\n(which is a bundling target)\ndoesn't list its source files,\nand instead requires that its primary module be a single static library dependency,\nsuch as a ",(0,s.jsx)(n.a,{href:"#swift_library",children:"swift_library"})," or an ",(0,s.jsx)(n.a,{href:"#objc_library",children:"objc_library"}),"\n(which is a compilation target)."]}),"\n",(0,s.jsx)(n.h4,{id:"ios_app_clip",children:(0,s.jsx)(n.code,{children:"ios_app_clip"})}),"\n",(0,s.jsxs)(n.p,{children:["An ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-ios.md#ios_app_clip",children:(0,s.jsx)(n.code,{children:"ios_app_clip"})})," target generates an app clip bundle."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"ios_app_clip"})," is used nearly identically to ",(0,s.jsx)(n.a,{href:"#ios_application",children:(0,s.jsx)(n.code,{children:"ios_application"})}),"\n(since app clips are on-demand applications),\nexcept that it also needs to be listed in a parent ",(0,s.jsx)(n.code,{children:"ios_application"}),"'s ",(0,s.jsx)(n.code,{children:"app_clips"})," attribute."]}),"\n",(0,s.jsx)(n.h4,{id:"ios_extension",children:(0,s.jsx)(n.code,{children:"ios_extension"})}),"\n",(0,s.jsxs)(n.p,{children:["An ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-ios.md#ios_extension",children:(0,s.jsx)(n.code,{children:"ios_extension"})})," target generates an application extension bundle."]}),"\n",(0,s.jsxs)(n.p,{children:["Similar to ",(0,s.jsx)(n.a,{href:"#ios_application",children:(0,s.jsx)(n.code,{children:"ios_application"})}),",\n",(0,s.jsx)(n.code,{children:"ios_extension"})," defines a bundling target,\nwhich means that it doesn't list its source files,\nand instead requires that its primary module be a single static library dependency,\nsuch as a ",(0,s.jsx)(n.a,{href:"#swift_library",children:"swift_library"})," or an ",(0,s.jsx)(n.a,{href:"#objc_library",children:"objc_library"}),"\n(which is a compilation target)."]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["Extensions are listed in the ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-ios.md#ios_application-extensions",children:(0,s.jsx)(n.code,{children:"extensions"})})," attribute of the containing application instead of the ",(0,s.jsx)(n.code,{children:"deps"})," attribute."]})}),"\n",(0,s.jsx)(n.h4,{id:"ios_framework",children:(0,s.jsx)(n.code,{children:"ios_framework"})}),"\n",(0,s.jsxs)(n.p,{children:["An ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-ios.md#ios_framework",children:(0,s.jsx)(n.code,{children:"ios_framework"})})," target causes the ",(0,s.jsx)(n.a,{href:"#swift_library",children:"static library"})," targets it depends on to be linked and bundled into a dynamic framework bundle,\ninstead of the top-level bundle target they would have ultimately been linked into\n(e.g. an ",(0,s.jsx)(n.code,{children:".app"}),", ",(0,s.jsx)(n.code,{children:".framework"})," , or ",(0,s.jsx)(n.code,{children:".xctest"}),").\nThese libraries still need to be depended on by your top-level bundle target.\nSee the ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/frameworks.md",children:"frameworks"})," ",(0,s.jsx)(n.strong,{children:"rules_apple"})," documentation for more information on how to use this rule."]}),"\n",(0,s.jsxs)(n.p,{children:["Since library dependencies have to be listed in ",(0,s.jsx)(n.code,{children:"deps"})," attributes,\nregardless if you use dynamic frameworks or not,\nconditionally setting ",(0,s.jsx)(n.code,{children:"frameworks"})," on your top-level bundle targets can be an easy way to switch between dynamic and static linking.\nThis can enable workflows such as using dynamic frameworks for dev builds,\nwhich has faster incremental linking,\nand using static linking for release builds,\nwhich has faster startup time.",(0,s.jsx)(n.sup,{children:(0,s.jsx)(n.a,{href:"#user-content-fn-mergable_libraries-2fd3bf",id:"user-content-fnref-mergable_libraries-2fd3bf","data-footnote-ref":!0,"aria-describedby":"footnote-label",children:"1"})})]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"ios_framework"})," is not intended to be used for distribution\n(i.e. consumed by Xcode).\nUse ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-ios.md#ios_dynamic_framework",children:(0,s.jsx)(n.code,{children:"ios_dynamic_framework"})})," for that."]})}),"\n",(0,s.jsx)(n.h4,{id:"ios_unit_test",children:(0,s.jsx)(n.code,{children:"ios_unit_test"})}),"\n",(0,s.jsxs)(n.p,{children:["An ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-ios.md#ios_unit_test",children:(0,s.jsx)(n.code,{children:"ios_unit_test"})})," target generates a unit testing bundle."]}),"\n",(0,s.jsxs)(n.p,{children:["Similar to ",(0,s.jsx)(n.a,{href:"#ios_application",children:(0,s.jsx)(n.code,{children:"ios_application"})}),",\n",(0,s.jsx)(n.code,{children:"ios_unit_test"})," defines a bundling target,\nwhich means that it doesn't list its source files,\nand instead requires that its primary module be a single static library dependency,\nsuch as a ",(0,s.jsx)(n.a,{href:"#swift_library",children:"swift_library"})," or an ",(0,s.jsx)(n.a,{href:"#objc_library",children:"objc_library"}),"\n(which is a compilation target)."]}),"\n",(0,s.jsx)(n.h4,{id:"ios_ui_test",children:(0,s.jsx)(n.code,{children:"ios_ui_test"})}),"\n",(0,s.jsxs)(n.p,{children:["An ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-ios.md#ios_ui_test",children:(0,s.jsx)(n.code,{children:"ios_ui_test"})})," target generates a UI testing bundle."]}),"\n",(0,s.jsxs)(n.p,{children:["Similar to ",(0,s.jsx)(n.a,{href:"#ios_application",children:(0,s.jsx)(n.code,{children:"ios_application"})}),",\n",(0,s.jsx)(n.code,{children:"ios_ui_test"})," defines a bundling target,\nwhich means that it doesn't list its source files,\nand instead requires that its primary module be a single static library dependency,\nsuch as a ",(0,s.jsx)(n.a,{href:"#swift_library",children:"swift_library"})," or an ",(0,s.jsx)(n.a,{href:"#objc_library",children:"objc_library"}),"\n(which is a compilation target)."]}),"\n",(0,s.jsx)(n.h4,{id:"local_provisioning_profile",children:(0,s.jsx)(n.code,{children:"local_provisioning_profile"})}),"\n",(0,s.jsxs)(n.p,{children:["A ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-apple.md#local_provisioning_profile",children:(0,s.jsx)(n.code,{children:"local_provisioning_profile"})})," target defines a reference to a provisioning file that exists on the user's machine.\nUse it when your provisioning profile is not checked into the workspace,\nsuch as per-user and/or managed by Xcode profiles."]}),"\n",(0,s.jsx)(n.h3,{id:"translating-the-xcode-project",children:"Translating the Xcode project"}),"\n",(0,s.jsx)(n.p,{children:"When migrating a project that uses Xcode to build,\nwhich the Mastodon iOS project does,\nwe have a blueprint in the form of the Xcode project that we can use to guide us on which Bazel targets we need to create.\nEach Xcode target will map to one or more Bazel targets,\nmostly formulaically.\nAnd since we now know which rules we can use for those Bazel targets,\nlet's get to translating."}),"\n",(0,s.jsx)(n.h4,{id:"dependencies",children:"Dependencies"}),"\n",(0,s.jsx)(n.p,{children:"Before talking about how any given target is translated,\nI wanted to mention how dependencies between targets are handled."}),"\n",(0,s.jsxs)(n.p,{children:["For each target dependency that an Xcode target has,\nthe corresponding Bazel target will have the same dependency listed in one of its dependency attributes.\nFor example,\nif the Xcode target ",(0,s.jsx)(n.code,{children:"A"})," depends on targets ",(0,s.jsx)(n.code,{children:"B"})," and ",(0,s.jsx)(n.code,{children:"C"}),",\nthen the Bazel target ",(0,s.jsx)(n.code,{children:"//some:A"})," will have ",(0,s.jsx)(n.code,{children:"//a/package:B"})," and ",(0,s.jsx)(n.code,{children:"//another/pkg:C"})," included in its dependency attributes."]}),"\n",(0,s.jsxs)(n.p,{children:["If a dependency is on a target that translates to multiple Bazel targets,\ne.g. an ",(0,s.jsx)(n.code,{children:"ios_extension"})," and ",(0,s.jsx)(n.code,{children:"swift_library"}),",\nthen only the top-most target\n(",(0,s.jsx)(n.code,{children:"ios_extension"})," in this example)\nshould be included in one of the Bazel target's depen
1dency attributes."]}),"\n",(0,s.jsxs)(n.p,{children:["If a dependency is on a product defined in a Swift package,\nthen a label of the form ",(0,s.jsx)(n.code,{children:"@swiftpkg_foo//:A"}),",\nwhere ",(0,s.jsx)(n.code,{children:"A"})," is a product in the ",(0,s.jsx)(n.code,{children:"Foo"})," Swift package,\nshould be included in one of the Bazel target's dependency attributes.\nI'll give more details on how Swift packages are handled with Bazel in a ",(0,s.jsx)(n.a,{href:"#swift-packages-swiftpm",children:"later section"}),"."]}),"\n",(0,s.jsxs)(n.h4,{id:"apple_support-rules_swift-rules_apple-and-bazel_skylib",children:[(0,s.jsx)(n.strong,{children:"apple_support"}),", ",(0,s.jsx)(n.strong,{children:"rules_swift"}),", ",(0,s.jsx)(n.strong,{children:"rules_apple"}),", and ",(0,s.jsx)(n.strong,{children:"bazel_skylib"})]}),"\n",(0,s.jsxs)(n.p,{children:["Before using rules from some of the core rulesets we need to add dependencies on\n",(0,s.jsx)(n.strong,{children:"apple_support"}),",\n",(0,s.jsx)(n.strong,{children:"rules_swift"}),",\n",(0,s.jsx)(n.strong,{children:"rules_apple"}),",\nand ",(0,s.jsx)(n.strong,{children:"bazel_skylib"}),".\nWe do that by adding ",(0,s.jsx)(n.code,{children:"bazel_dep"}),"s to our ",(0,s.jsx)(n.code,{children:"MODULE.bazel"})," file:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",metastring:'title="MODULE.bazel"',children:'bazel_dep(name = "apple_support", version = "1.14.0")\nbazel_dep(name = "rules_swift", version = "1.16.0", repo_name = "build_bazel_rules_swift")\nbazel_dep(name = "rules_apple", version = "3.3.0", repo_name = "build_bazel_rules_apple")\nbazel_dep(name = "bazel_skylib", version = "1.5.0")\n'})}),"\n",(0,s.jsx)(n.h4,{id:"static-libraries",children:"Static libraries"}),"\n",(0,s.jsxs)(n.p,{children:["While the Mastodon iOS project doesn't define any static library targets in its Xcode project\n(though it implicitly does through its use of Swift packages,\nwhich are discussed later),\nI wanted to mention that they are translated 1:1 with ",(0,s.jsx)(n.a,{href:"#swift_library",children:"static library"})," rules."]}),"\n",(0,s.jsx)(n.h4,{id:"provisioning-profiles",children:"Provisioning profiles"}),"\n",(0,s.jsxs)(n.p,{children:["If you have your provisioning profile stored in your workspace,\nyou can reference it directly with the ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-ios.md#ios_application-provisioning_profile",children:(0,s.jsx)(n.code,{children:"provisioning_profile"})})," attribute.\nIf your provisioning profile is instead stored in the default Xcode location\n(i.e. ",(0,s.jsx)(n.code,{children:"~/Library/MobileDevice/Provisioning Profiles"}),"),\nyou'll want to use the ",(0,s.jsx)(n.a,{href:"#local_provisioning_profile",children:(0,s.jsx)(n.code,{children:"local_provisioning_profile"})})," rule to reference it.\nFinally,\nif you use Xcode's \"Automatic Code Signing\" feature,\nyou'll want to use the ",(0,s.jsx)(n.a,{href:"https://github.com/MobileNativeFoundation/rules_xcodeproj/blob/1.17.0/docs/bazel.md#xcode_provisioning_profile",children:(0,s.jsx)(n.code,{children:"xcode_provisioning_profile"})})," rule as well."]}),"\n",(0,s.jsx)(n.h4,{id:"swift-packages-swiftpm",children:"Swift packages (SwiftPM)"}),"\n",(0,s.jsxs)(n.p,{children:["An Xcode project can declare dependencies on Swift packages,\nboth remote and local.\nThe easy way to handle those dependencies with Bazel is by using ",(0,s.jsx)(n.a,{href:"https://github.com/cgrindel/rules_swift_package_manager",children:(0,s.jsx)(n.strong,{children:"rules_swift_package_manager"})}),"."]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"rules_swift_package_manager"})," requires all Swift package dependencies to be declared in a ",(0,s.jsx)(n.code,{children:"Package.swift"})," file.\nSo any dependencies declared in an Xcode project need to be added to a new ",(0,s.jsx)(n.code,{children:"Package.swift"}),' file.\nI say "new" since there might already exist ',(0,s.jsx)(n.code,{children:"Package.swift"})," files that declare local packages,\nand if you don't want to migrate those packages to Bazel\n(which we aren't going to for this migration),\nyou'll need to declare those packages as local dependencies in a new ",(0,s.jsx)(n.code,{children:"Package.swift"})," file.",(0,s.jsx)(n.sup,{children:(0,s.jsx)(n.a,{href:"#user-content-fn-local-rspm-2fd3bf",id:"user-content-fnref-local-rspm-2fd3bf","data-footnote-ref":!0,"aria-describedby":"footnote-label",children:"2"})})]}),"\n",(0,s.jsxs)(n.p,{children:["Here is the new ",(0,s.jsx)(n.code,{children:"Package.swift"})," file that includes the dependencies that were referenced in the Xcode project:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-swift",metastring:'title="Package.swift"',children:'// swift-tools-version:5.7\n\nimport PackageDescription\n\nlet package = Package(\n name: "Mastodon-iOS",\n defaultLocalization: "en",\n platforms: [\n .iOS(.v16),\n ],\n dependencies: [\n .package(name: "ArkanaKeys", path: "Dependencies/ArkanaKeys"),\n .package(name: "MastodonSDK", path: "MastodonSDK"),\n .package(\n url: "https://github.com/Bearologics/LightChart.git",\n branch: "master"\n ),\n .package(\n url: "https://github.com/jdg/MBProgressHUD.git",\n from: "1.2.0"\n ),\n .package(\n url: "https://github.com/tid-kijyun/Kanna.git",\n from: "5.2.7"\n ),\n ]\n)\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Currently ",(0,s.jsx)(n.strong,{children:"rules_swift_package_manager"})," requires you to use ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/bazel-gazelle",children:"Gazelle"})," in order to create a ",(0,s.jsx)(n.code,{children:"swift_deps_index.json"})," file.",(0,s.jsx)(n.sup,{children:(0,s.jsx)(n.a,{href:"#user-content-fn-rspm-no-gazelle-2fd3bf",id:"user-content-fnref-rspm-no-gazelle-2fd3bf","data-footnote-ref":!0,"aria-describedby":"footnote-label",children:"3"})}),"\nSo we need to add a ",(0,s.jsx)(n.code,{children:"bazel_dep"})," on ",(0,s.jsx)(n.strong,{children:"gazelle"})," in addition to ",(0,s.jsx)(n.strong,{children:"rules_swift_package_manager"})," in our ",(0,s.jsx)(n.code,{children:"MODULE.bazel"})," file.\nWe also need to add a ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/rules/lib/globals/module#use_repo",children:(0,s.jsx)(n.code,{children:"use_repo"})})," stanza for the Swift packages we directly depend on\n(though if you run the ",(0,s.jsx)(n.code,{children:"//:swift_update_pkgs"})," Gazelle target,\nwhich we will define shortly,\nit will add the required stanza for you):"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",metastring:'title="MODULE.bazel"',children:'bazel_dep(name = "rules_swift_package_manager", version = "0.28.0")\nbazel_dep(name = "gazelle", version = "0.35.0")\n\n# swift_deps START\nswift_deps = use_extension(\n "@rules_swift_package_manager//:extensions.bzl",\n "swift_deps",\n)\nswift_deps.from_file(\n deps_index = "//:swift_deps_index.json",\n)\nuse_repo(\n swift_deps,\n "swiftpkg_arkanakeys",\n "swiftpkg_arkanakeysinterfaces",\n "swiftpkg_kanna",\n "swiftpkg_lightchart",\n "swiftpkg_mastodonsdk",\n "swiftpkg_mbprogresshud",\n)\n# swift_deps END\n'})}),"\n",(0,s.jsxs)(n.p,{children:["And now we can define our Gazelle targets in a root ",(0,s.jsx)(n.code,{children:"BUILD"})," file:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",metastring:'title="BUILD"',children:'load("@gazelle//:def.bzl", "gazelle", "gazelle_binary")\nload(\n "@rules_swift_package_manager//swiftpkg:defs.bzl",\n "swift_update_packages",\n)\n\n# - Gazelle\n\n# Ignore the `.build` folder that is created by running Swift package manager\n# commands. The Swift Gazelle plugin executes some Swift package manager\n# commands to resolve external dependencies. This results in a `.build` file\n# being created.\n# NOTE: Swift package manager is not used to build any of the external packages.\n# The `.build` directory should be ignored. Be sure to configure your source\n# control to ignore it (i.e., add it to your `.gitignore`).\n# gazelle:exclude .build\n\n# This declaration builds a Gazelle binary that incorporates all of the Gazelle\n# plugins for the languages that you use in your workspace. In this example, we\n# are only listing the Gazelle plugin for Swift from\n# rules_swift_package_manager.\ngazelle_binary(\n name = "gazelle_bin",\n languages = [\n "@rules_swift_package_manager//gazelle",\n ],\n)\n\n# This macro defines two targets: `swift_update_pkgs` and\n# `swift_update_pkgs_to_latest`.\n#\n# The `swift_update_pkgs` target should be run whenever the list of external\n# dependencies is updated in the `Package.swift`. Running this target will\n# populate the `swift_deps.bzl` with `swift_package` declarations for all of the\n# direct and transitive Swift packages that your project uses.\n#\n# The `swift_update_pkgs_to_latest` target should be run when you want to\n# update your Swift dependencies to their latest eligible version.\nswift_update_packages(\n name = "swift_update_pkgs",\n gazelle = ":gazelle_bin",\n generate_swift_deps_for_workspace = False,\n patches_yaml = "patches/swiftpkgs.yaml",\n update_bzlmod_stanzas = True,\n)\n\n# This target updates the Bazel build files for your project. Run this target\n# whenever you add or remove source files from your project.\ngazelle(\n name = "update_build_files",\n gazelle = ":gazelle_bin",\n)\n'})}),"\n",(0,s.jsxs)(n.admonition,{type:"note",children:[(0,s.jsxs)(n.p,{children:["Because of how ",(0,s.jsx)(n.strong,{children:"rules_apple"})," handles app icon assets\n(though possibly because of a bug),\nwe need to ",(0,s.jsx)(n.a,{href:"https://github.com/cgrindel/rules_swift_package_manager/blob/v0.28.0/docs/patch_swift_package.md",children:"patch"})," the ",(0,s.jsx)(n.strong,{children:"Tabman"})," package to not include test assets.\nOur patches are defined in the ",(0,s.jsx)(n.code,{children:"patches/swiftpkgs.yaml"})," file."]}),(0,s.jsxs)(n.p,{children:["Also,\nin our ",(0,s.jsx)(n.code,{children:".bazelrc"})," file we added something to work around a ",(0,s.jsx)(n.a,{href:"https://github.com/cgrindel/rules_swift_package_manager/blob/v0.28.0/docs/faq.md#my-project-builds-successfully-with-bazel-build--but
1-it-does-not-build-when-using-rules_xcodeproj-how-can-i-fix-this",children:"current issue"})," with ",(0,s.jsx)(n.strong,{children:"rules_swift_package_manager"})," and sandboxing."]})]}),"\n",(0,s.jsxs)(n.p,{children:["To generate the ",(0,s.jsx)(n.code,{children:"swift_deps_index.json"})," file,\nand/or update the ",(0,s.jsx)(n.code,{children:"MODULE.bazel"})," ",(0,s.jsx)(n.code,{children:"use_repo"})," stanza,\nrun the ",(0,s.jsx)(n.code,{children:"//:swift_update_pkgs"})," target:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shellsession",children:"$ bazel run //:swift_update_pkgs\nINFO: Invocation ID: f2427bde-865b-43b3-bd3c-6a8489387f00\nINFO: Analyzed target //:swift_update_pkgs (148 packages loaded, 11707 targets configured).\nINFO: Found 1 target...\nTarget //:swift_update_pkgs up-to-date:\n bazel-bin/swift_update_pkgs-runner.bash\n bazel-bin/swift_update_pkgs\nINFO: Elapsed time: 1.724s, Critical Path: 0.07s\nINFO: 1 process: 1 internal.\nINFO: Build completed successfully, 1 total action\nINFO: Running command line: bazel-bin/swift_update_pkgs\n"})}),"\n",(0,s.jsxs)(n.p,{children:["At ",(0,s.jsx)(n.a,{href:"https://github.com/brentleyjones/mastodon-ios/commit/9616360e2e7148ee5d6ceb63616d14b3363c2b51",children:"this commit"})," Swift packages have been integrated for the Mastodon iOS project."]}),"\n",(0,s.jsx)(n.h4,{id:"app-extensions",children:"App extensions"}),"\n",(0,s.jsxs)(n.p,{children:["App extensions,\nsuch as the\n",(0,s.jsx)(n.code,{children:"MastodonIntent"}),",\n",(0,s.jsx)(n.code,{children:"NotificationService"}),",\n",(0,s.jsx)(n.code,{children:"ShareActionExtension"}),",\n",(0,s.jsx)(n.code,{children:"OpenInActionExtension"}),",\nand ",(0,s.jsx)(n.code,{children:"WidgetExtension"}),"\ntargets,\nare represented by a combination of an ",(0,s.jsx)(n.a,{href:"#ios_extension",children:(0,s.jsx)(n.code,{children:"*_extension"})})," and ",(0,s.jsx)(n.a,{href:"#swift_library",children:"static library"})," rule.\nAnd the static library target will need to have the ",(0,s.jsx)(n.code,{children:"-application-extension"})," (for Swift) or ",(0,s.jsx)(n.code,{children:"-fapplication-extension"})," (for Objective-C) copt set."]}),"\n",(0,s.jsxs)(n.p,{children:["You will need to set the ",(0,s.jsx)(n.code,{children:"infoplists"})," and ",(0,s.jsx)(n.code,{children:"entitlements"})," attributes to your ",(0,s.jsx)(n.code,{children:"Info.plist"})," and ",(0,s.jsx)(n.code,{children:".entitlements"})," files,\nthe ",(0,s.jsx)(n.code,{children:"bundle_id"})," attribute to the value of the ",(0,s.jsx)(n.code,{children:"PRODUCT_BUNDLE_IDENTIFIER"})," build setting,\nand the ",(0,s.jsx)(n.code,{children:"minimum_os_version"})," attribute to the value of ",(0,s.jsx)(n.code,{children:"IPHONEOS_DEPLOYMENT_TARGET"})," build setting."]}),"\n",(0,s.jsxs)(n.admonition,{type:"note",children:[(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"rules_apple"})," processes ",(0,s.jsx)(n.code,{children:"Info.plist"})," and ",(0,s.jsx)(n.code,{children:".entitlements"})," files in a slightly different manner than Xcode.\nIn Xcode,\nyou have build settings such as\n",(0,s.jsx)(n.code,{children:"PRODUCT_BUNDLE_IDENTIFIER"}),",\n",(0,s.jsx)(n.code,{children:"CURRENT_PROJECT_VERSION"}),",\nand ",(0,s.jsx)(n.code,{children:"PRODUCT_MODULE_NAME"}),".\nAnd when it processes an ",(0,s.jsx)(n.code,{children:"Info.plist"}),",\nit substitutes references to those build settings with their resolved value.\n",(0,s.jsx)(n.strong,{children:"rules_apple"})," doesn't use build settings,\nso it's not able to do the same substitutions."]}),(0,s.jsxs)(n.p,{children:["There are some attributes on bundling rules that ",(0,s.jsx)(n.strong,{children:"rules_apple"})," ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/common_info.md#variable-substitution",children:"will substitute"})," in place of a build setting,\nsuch as ",(0,s.jsx)(n.code,{children:"bundle_id"})," for ",(0,s.jsx)(n.code,{children:"PRODUCT_BUNDLE_IDENTIFIER"}),",\nbut for the most part when using Bazel you'll need to remove these build setting references from ",(0,s.jsx)(n.code,{children:"Info.plist"})," and ",(0,s.jsx)(n.code,{children:".entitlements"})," files,\nor use a custom rule to expand those values for you.\nI decided to use the ",(0,s.jsx)(n.strong,{children:"bazel_skylib"})," ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/bazel-skylib/blob/1.5.0
1/docs/expand_template_doc.md",children:(0,s.jsx)(n.code,{children:"expand_template"})})," rule to allow the build setting references to stay in the ",(0,s.jsx)(n.code,{children:"Info.plist"})," files."]})]}),"\n",(0,s.jsxs)(n.p,{children:["Since the Mastodon iOS project doesn't use resource bundles,\nresources will be referenced directly with the ",(0,s.jsx)(n.code,{children:"data"})," attribute of the static library target for the primary module."]}),"\n",(0,s.jsxs)(n.p,{children:["With that in mind I added ",(0,s.jsx)(n.code,{children:"BUILD"})," files for the app extension targets:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",metastring:'title="WidgetExtension/BUILD"',children:'load("@bazel_skylib//rules:expand_template.bzl", "expand_template")\nload("@build_bazel_rules_apple//apple:ios.bzl", "ios_extension")\nload("@build_bazel_rules_apple//apple:resources.bzl", "apple_intent_library")\nload("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")\n\nios_extension(\n name = "WidgetExtension",\n bundle_id = "org.joinmastodon.app.WidgetExtension",\n entitlements = "WidgetExtension.entitlements",\n families = [\n "ipad",\n "iphone",\n ],\n infoplists = [":InfoPlist"],\n minimum_os_version = "16.0",\n resources = glob(\n [\n "**/*.lproj/**",\n "**/*.js",\n "**/*.xcassets/**",\n ],\n exclude = [".*"],\n ),\n visibility = ["//visibility:public"],\n deps = [":WidgetExtension.library"],\n)\n\nswift_library(\n name = "WidgetExtension.library",\n srcs = glob(["**/*.swift"]) + [":WidgetExtension.intent"],\n module_name = "WidgetExtension",\n deps = [\n "@swiftpkg_lightchart//:LightChart",\n "@swiftpkg_mastodonsdk//:MastodonSDKDynamic",\n ],\n)\n\napple_intent_library(\n name = "WidgetExtension.intent",\n src = "Base.lproj/WidgetExtension.intentdefinition",\n language = "Swift",\n visibility = ["//visibility:public"],\n)\n\nexpand_template(\n name = "InfoPlist",\n out = "Bazel.Info.plist",\n substitutions = {\n "$(CURRENT_PROJECT_VERSION)": "1",\n "$(MARKETING_VERSION)": "2024.3",\n },\n template = "Info.plist",\n)\n'})}),"\n",(0,s.jsxs)(n.admonition,{type:"note",children:[(0,s.jsxs)(n.p,{children:["We are using ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/be/functions#glob",children:(0,s.jsx)(n.code,{children:"glob"})})," to collect our source files.\nThis requires that none of the source files in the workspace are dead/unused,\nwhich can sometimes happen when using an Xcode project since it doesn't have to reference all files in a directory.\nIt also requires that all source files for a module live under that module's directory.\nBest practice is to share code via modules instead of having multiple modules reference the same source file."]}),(0,s.jsxs)(n.p,{children:["We also created and used an ",(0,s.jsx)(n.a,{href:"#apple_intent_library",children:(0,s.jsx)(n.code,{children:"apple_intent_library"})})," target for the ",(0,s.jsx)(n.code,{children:"WidgetExtension.intentdefinition"})," file.\nIt has increased ",(0,s.jsx)(n.a,{href:"https://bazel.build/concepts/visibility",children:"visibility"})," since it's also used by the ",(0,s.jsx)(n.code,{children:"Mastodon"})," application target."]})]}),"\n",(0,s.jsxs)(n.p,{children:["At ",(0,s.jsx)(n.a,{href:"https://github.com/brentleyjones/mastodon-ios/commit/4e615641e1028ee6cd7c7f9eb2a0148fa9cb68fb",children:"this commit"})," app extensions have been translated for the Mastodon iOS project."]}),"\n",(0,s.jsx)(n.h4,{id:"applications",children:"Applications"}),"\n",(0,s.jsxs)(n.p,{children:["Applications,\nsuch as the ",(0,s.jsx)(n.code,{children:"Mastodon"})," target,\nare represented by a combination of an ",(0,s.jsx)(n.a,{href:"#ios_application",children:(0,s.jsx)(n.code,{children:"*_application"})})," and ",(0,s.jsx)(n.a,{href:"#swift_library",children:"static library"})," rule.\nSince applications are bundle targets,\nthey are handled very similar to app extension targets,\nwhich means we need to apply the same translations in regards to ",(0,s.jsx)(n.code,{children:"Info.plist"})," files and build settings."]}),"\n",(0,s.jsxs)(n.p,{children:["With that in mind I added a ",(0,s.jsx)(n.code,{children:"BUILD"})," file for the ",(0,s.jsx)(n.code,{children:"Mastodon"})," target:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",metastring:'title="Mastodon/BUILD"',children:'load("@bazel_skylib//rules:expand_template.bzl", "expand_template")\nload("@build_bazel_rules_apple//apple:ios.bzl", "ios_application")\nload("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")\n\nios_application(\n name = "Mastodon",\n bundle_id = "org.joinmastodon.app",\n entitlements = "Mastodon.entitlements",\n extensions = [\n "//MastodonIntent",\n "//NotificationService",\n "//OpenInActionExtension",\n "//ShareActionExtension",\n "//WidgetExtension",\n ],\n families = [\n "ipad",\n "iphone",\n ],\n infoplists = [":InfoPlist"],\n minimum_os_version = "16.0",\n resources = glob(\n [\n "Resources/**",\n "Supporting Files/**",\n ],\n exclude = [\n ".*",\n "Resources/Preview Assets.xcassets",\n ],\n ),\n visibility = ["//visibility:public"],\n deps = [":Mastodon.library"],\n)\n\nswift_library(\n name = "Mastodon.library",\n srcs = glob(["**/*.swift"]) + [\n "//MastodonIntent:Intents.intent",\n "//WidgetExtension:WidgetExtension.intent",\n ],\n module_name = "Mastodon",\n visibility = ["//MastodonTests:__pkg__"],\n deps = [\n "@swiftpkg_kanna//:Kanna",\n "@swiftpkg_mastodonsdk//:MastodonSDKDynamic",\n "@swiftpkg_mbprogresshud//:MBProgressHUD",\n ],\n)\n\nexpand_template(\n name = "InfoPlist",\n out = "Bazel.Info.plist",\n substitutions = {\n "$(CURRENT_PROJECT_VERSION)": "1",\n "$(MARKETING_VERSION)": "2024.3",\n },\n template = "Info.plist",\n)\n'})}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.a,{href:"https://bazel.build/concepts/visibility",children:"visibility"})," of the ",(0,s.jsx)(n.code,{children:"Mastodon"})," target was increased to allow it to be a test host for tests."]})}),"\n",(0,s.jsxs)(n.p,{children:["At ",(0,s.jsx)(n.a,{href:"https://github.com/brentleyjones/mastodon-ios/commit/dcf1dcd12f92b6d27719eea79c8fc7e2cf63df14",children:"this commit"})," the iOS app has been translated for the Mastodon iOS project."]}),"\n",(0,s.jsx)(n.h4,{id:"tests",children:"Tests"}),"\n",(0,s.jsxs)(n.p,{children:["Tests,\nsuch as the ",(0,s.jsx)(n.code,{children:"MastodonTests"})," and ",(0,s.jsx)(n.code,{children:"MastodonUITests"})," targets,\nare represented by a combination of a ",(0,s.jsx)(n.a,{href:"#ios_unit_test",children:(0,s.jsx)(n.code,{children:"*_unit_test"})}),"/",(0,s.jsx)(n.a,{href:"#ios_ui_test",children:(0,s.jsx)(n.code,{children:"*_ui_test"})})," and ",(0,s.jsx)(n.a,{href:"#swift_library",children:"static library"})," rule.\nSince tests are bundle targets,\nthey are handled very similar to application targets,\nwhich means we need to apply the same translations in regards to ",(0,s.jsx)(n.code,{children:"Info.plist"})," files and build settings."]}),"\n",(0,s.jsxs)(n.p,{children:["If a unit test has a host application,\nor a UI test has a target application,\nthe ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-ios.md#ios_unit_test-test_host",children:(0,s.jsx)(n.code,{children:"test_host"})})," attribute needs to be set to the corresponding ",(0,s.jsx)(n.code,{children:"*_application"})," Bazel target.\nIn the case of unit tests,\nif the ",(0,s.jsx)(n.code,{children:"Allow testing Host Application APIs"})," checkbox is unchecked,\n",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-ios.md#ios_unit_test-test_host_is_bundle_loader",children:(0,s.jsx)(n.code,{children:"test_host_is_bundle_loader"})})," needs to be set to ",(0,s.jsx)(n.code,{children:"False"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["With that in mind I added ",(0,s.jsx)(n.code,{children:"BUILD"})," files for the ",(0,s.jsx)(n.code,{children:"MastodonTests"})," and ",(0,s.jsx)(n.code,{children:"MastodonUITests"})," targets:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",metastring:'title="MastodonTests/BUILD"',children:'load("@build_bazel_rules_apple//apple:ios.bzl", "ios_unit_test")\nload("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")\n\nios_unit_test(\n name = "MastodonTests",\n bundle_id = "org.joinmastodon.MastodonTests",\n minimum_os_version = "16.0",\n test_host = "//Mastodon:Mastodon",\n deps = [":MastodonTests.library"],\n)\n\nswift_library(\n name = "MastodonTests.library",\n srcs = glob(["**/*.swift"]),\n module_name = "MastodonTests",\n testonly = True,\n deps = [\n "@swiftpkg_mastodonsdk//:MastodonSDKDynamic",\n "//Mastodon:Mastodon.library",\n ],\n)\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",metastring:'title="MastodonUITests/BUILD"',children:'load("@build_bazel_rules_apple//apple:ios.bzl", "ios_ui_test")\nload("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")\n\nios_ui_test(\n name = "MastodonUITests",\n bundle_id = "org.joinmastodon.MastodonUITests",\n minimum_os_version = "16.0",\n test_host = "//Mastodon:Mastodon",\n deps = [":MastodonUITests.library"],\n)\n\nswift_library(\n name = "MastodonUITests.library",\n srcs = glob(["**/*.swift"]),\n module_name = "MastodonUITests",\n testonly = True,\n)\n'})}),"\n",(0,s.jsxs)(n.p,{children:["At ",(0,s.jsx)(n.a,{href:"https://github.com/brentleyjones/mastodon-ios/commit/f622f72b704b470650b3101aa7f0e2a5e8b2fdaf",children:"this commit"})," the tests have been translated for the Mastodon iOS project."]}),"\n",(0,s.jsx)(n.h4,{id:"frameworks",children:"Frameworks"}),"\n",(0,s.jsxs)(n.p,{children:["Since we are using ",(0,s.jsx)(n.strong,{children:"rules_swift_package_manager"})," for the ",(0,s.jsx)(n.code,{children:"MastodonSDK"})," target,\nwe don't have any framework targets to define.\nIf we did though,\nwe would use the ",(0,s.jsx)(n.a,{href:"#ios_framework",children:(0,s.jsx)(n.code,{children:"ios_framework"})})," rule to define a bundle similar to our application or app extensions,\nand reference the framework with the ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.3.0/doc/rules-ios.md#ios_application-provisioning_profile",children:(0,s.jsx)(n.code,{children:"frameworks"})})," attribute.\nThis is in addition to listing dependencies in the ",(0,s.jsx)(n.code,{children:"deps"})," attribute for any ",(0,s.jsx)(n.a,{href:"#swift_library",children:"static library"})," targets that happen to be bundled in the framework."]}),"\n",(0,s.jsx)(n.h4,{id:"codegen",children:"Codegen"}),"\n",(0,s.jsxs)(n.p,{children:["Bazel actions can't modify source files in the workspace.\nSo in order to use co
1de generation with Bazel you need to use a ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/be/general#genrule",children:(0,s.jsx)(n.code,{children:"genrule"})})," or a custom rule to generate files,\nwhich you then depend on in your ",(0,s.jsx)(n.code,{children:"srcs"})," attributes."]}),"\n",(0,s.jsxs)(n.p,{children:["The Mastodon iOS project uses ",(0,s.jsx)(n.a,{href:"https://github.com/krzysztofzablocki/Sourcery",children:"Sourcery"})," and ",(0,s.jsx)(n.a,{href:"https://github.com/SwiftGen/SwiftGen",children:"SwiftGen"})," to modify source files in-place,\nwhich it then checks into source control.\nIn order to allow building the project with either xcodebuild or Bazel,\nwe will leave code generation as an external process."]}),"\n",(0,s.jsx)(n.h2,{id:"integrating-with-xcode",children:"Integrating with Xcode"}),"\n",(0,s.jsxs)(n.p,{children:["If we were performing this migration more than a year ago this section would be a lot larger,\nand could probably be its own post.\nThankfully since the start of last year we have a\nfast,\nstable,\nand\nmature\ncommunity backed solution in the form of ",(0,s.jsx)(n.a,{href:"https://github.com/MobileNativeFoundation/rules_xcodeproj",children:(0,s.jsx)(n.strong,{children:"rules_xcodeproj"})}),"."]}),"\n",(0,s.jsx)(n.h3,{id:"xcodeproj",children:(0,s.jsx)(n.code,{children:"xcodeproj"})}),"\n",(0,s.jsxs)(n.p,{children:["To use ",(0,s.jsx)(n.strong,{children:"rules_xcodeproj"}),",\nwe add a ",(0,s.jsx)(n.code,{children:"bazel_dep"})," for it to our ",(0,s.jsx)(n.code,{children:"MODULE.bazel"})," file,\nand define an ",(0,s.jsx)(n.code,{children:"xcodeproj"})," target for the project in the root ",(0,s.jsx)(n.code,{children:"BUILD"})," file:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",metastring:'title="MODULE.bazel"',children:'bazel_dep(name = "rules_xcodeproj", version = "1.16.0")\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-python",metastring:'title="BUILD"',children:'load("@rules_xcodeproj//xcodeproj:defs.bzl", "top_level_target", "xcodeproj")\n\nxcodeproj(\n name = "xcodeproj",\n generation_mode = "incremental",\n project_name = "MastodonBazel",\n top_level_targets = [\n top_level_target(\n "//Mastodon:Mastodon",\n target_environments = ["simulator", "device"],\n ),\n "//MastodonTests:MastodonTests",\n "//MastodonUITests:MastodonUITests",\n ],\n)\n'})}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["Any targets listed in ",(0,s.jsx)(n.code,{children:"xcodeproj.top_level_targets"})," will need to grant visibility to ",(0,s.jsx)(n.strong,{children:"rules_xcodeproj"}),".\nThey can do so by having ",(0,s.jsx)(n.code,{children:'"@rules_xcodeproj//xcodeproj:generated"'})," in their ",(0,s.jsx)(n.a,{href:"https://bazel.build/concepts/visibility",children:(0,s.jsx)(n.code,{children:"visibility"})})," attribute."]})}),"\n",(0,s.jsxs)(n.p,{children:["At ",(0,s.jsx)(n.a,{href:"https://github.com/brentleyjones/mastodon-ios/commit/c0139c3cce3784832a00e42405c690a3db9c8d49",children:"this commit"})," ",(0,s.jsx)(n.strong,{children:"rules_xcodeproj"})," has been integrated into the Mastodon iOS project."]}),"\n",(0,s.jsx)(n.h3,{id:"generating-the-xcode-project",children:"Generating the Xcode project"}),"\n",(0,s.jsx)(n.p,{children:"We can now generate a Bazel integrated Xcode project by running a single command:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shellsession",children:'$ bazel run //:xcodeproj\nINFO: Analyzed target //:xcodeproj (1 packages loaded, 1 target configured).\nINFO: Found 1 target...\nTarget //:xcodeproj up-to-date:\n bazel-bin/xcodeproj-runner.sh\nINFO: Elapsed time: 0.089s, Critical Path: 0.00s\nINFO: 3 processes: 3 internal.\nINFO: Build completed successfully, 3 total actions\nINFO: Running command line: bazel-bin/xcodeproj-runner.sh\n\nGenerating "MastodonBazel.xcodeproj"\nINFO: Analyzed target @@rules_xcodeproj~1.16.0~internal~rules_xcodeproj_generated//generator/xcodeproj:xcodeproj (1 packages loaded, 1 target configured).\nINFO: Found 1 target...\nINFO: Elapsed time: 0.308s, Critical Path: 0.07s\nINFO: 4 processes: 2 internal, 2 local.\nINFO: Build completed successfully, 4 total actions\nINFO: Running command line: /private/var/tmp/_bazel_brentley/b406c5544781724b8a84c3c6fa8dad13/rules_xcodeproj.noindex/build_output_base/execroot/_main/bazel-out/darwin_arm64-dbg/bin/external/rules_xcodeproj~1.16.0~internal~rules_xcodeproj_generated/generator/xcodeproj/xcodeproj-installer.sh --xcodeproj_bazelrc /private/var/tmp/_bazel_brentley/b406c5544781724b8a84c3c6fa8dad13/execroot/_main/bazel-out/darwin_arm64-fastbuild/bin/xcodeproj-runner.sh.runfiles/_main/xcodeproj.bazelrc --extra_flags_bazelrc /private/var/tmp/_bazel_brentley/b406c5544781724b8a84c3c6fa8dad13/execroot/_main/bazel-out/darwin_arm64-fastbuild/bin/xcodeproj-runner.sh.runfiles/_main/xcodeproj-extra-flags.bazelrc --bazel_path /Users/brentley/Library/Caches/bazelisk/downloads/sha256/93772ce53afbe2282d0d727137a19c835eaa6f328964d02024bf3c234993bf7b/bin/bazel --execution_root /private/var/tmp/_bazel_brentley/b406c5544781724b8a84c3c6fa8dad13/execroot/_main\nUpdated project at "MastodonBazel.xcodeproj"\n'})}),"\n",(0,s.jsxs)(n.p,{children:["If you open the generated project\n(i.e. ",(0,s.jsx)(n.code,{children:"xed MastodonBazel.xcodeproj"}),")\nand run the ",(0,s.jsx)(n.code,{children:"Mastodon"})," scheme,\nit should\nbuild,\ninstall to the simulator,\nand launch successfully:"]}),"\n",(0,s.jsx)("figure",{children:(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.img,{src:i(55193).A+"",width:"3342",height:"1614"}),"\n",(0,s.jsxs)("figcaption",{children:["The ",(0,s.jsx)("strong",{children:"rules_xcodeproj"})," generated project open in Xcode and running in the Simulator"]})]})}),"\n",(0,s.jsx)(n.h3,{id:"ensuring-bazel-sees-the-correct-xcode-version",children:"Ensuring Bazel sees the correct Xcode version"}),"\n",(0,s.jsxs)(n.p,{children:["Currently there are additional steps that need to be taken in order for Bazel to correctly detect when Xcode versions change.\nI recommend reading the ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.2.1/doc/common_info.md#xcode
1-version-selection-and-invalidation",children:'"Xcode Version Selection and Invalidation"'})," section of the ",(0,s.jsx)(n.strong,{children:"rules_apple"})," docs for details on what and why.\nIf you only build your project inside of Xcode,\nor with the ",(0,s.jsxs)(n.a,{href:"https://github.com/MobileNativeFoundation/rules_xcodeproj/blob/1.17.0/docs/usage.md#command-line-api",children:[(0,s.jsx)(n.strong,{children:"rules_xcodeproj"})," command-line API"]}),",\nthere is nothing else you need to do to manage your Xcode version with Bazel,\nbecause it ",(0,s.jsx)(n.a,{href:"https://github.com/MobileNativeFoundation/rules_xcodeproj/blob/1.17.0/xcodeproj/internal/templates/runner.sh#L148-L171",children:"applies these steps for you"}),"."]}),"\n",(0,s.jsx)(n.h3,{id:"generating-release-archives",children:"Generating release archives"}),"\n",(0,s.jsxs)(n.p,{children:["Currently the Archive action in Xcode doesn't work when using ",(0,s.jsx)(n.strong,{children:"rules_xcodeproj"}),".\nThis means that you'll need to generate release archives on the command line directly with Bazel,\nand then upload them to the App Store."]}),"\n",(0,s.jsxs)(n.p,{children:["For example, you can generate the ",(0,s.jsx)(n.code,{children:".ipa"})," archive for the Mastodon app by running the following command:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shellsession",children:"$ bazel build //Mastodon --ios_multi_cpus=arm64\n"})}),"\n",(0,s.jsxs)(n.p,{children:["And then you can ",(0,s.jsx)(n.a,{href:"https://developer.apple.com/help/app-store-connect/manage-builds/upload-builds/",children:"manually upload the archive"}),".\nOr you can use the ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/3.2.1/doc/rules-xcarchive.md#xcarchive",children:(0,s.jsx)(n.code,{children:"xcarchive"})})," rule from ",(0,s.jsx)(n.strong,{children:"rules_apple"})," to generate an ",(0,s.jsx)(n.code,{children:".xcarchive"})," bundle,\nand then use Xcode to upload the archive."]}),"\n",(0,s.jsx)(n.h2,{id:"leveraging-remote-caching-and-remote-execution",children:"Leveraging remote caching and remote execution"}),"\n",(0,s.jsxs)(n.p,{children:["One of the main reasons to use Bazel,\ninstead of another build system such as xcodebuild,\nis to leverage its remote caching and execution capabilities.\nFor a detailed overview on both of those capabilities,\nI recommend reading my ",(0,s.jsx)(n.a,{href:"/blog/bazels-remote-caching-and-remote-execution-explained",children:"post on the subject"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"For this post I'll detail the steps I would take for any Bazel migration,\nstarting from no caching and ending with both caching and remote execution."}),"\n",(0,s.jsx)(n.h3,{id:"disk-cache",children:"Disk cache"}),"\n",(0,s.jsxs)(n.p,{children:['Bazel supports a form of "remote cache" that uses the local filesystem instead of a remote server.\nIt\'s called the "disk cache" and is enabled with the ',(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--disk_cache",children:(0,s.jsx)(n.code,{children:"--disk_cache"})})," flag."]}),"\n",(0,s.jsx)(n.p,{children:"There are pros and cons to using the disk cache:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Pros"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Lower latency than a remote cache"}),"\n",(0,s.jsxs)(n.li,{children:["Includes blobs",(0,s.jsx)(n.sup,{children:(0,s.jsx)(n.a,{href:"#user-content-fn-blob-2fd3bf",id:"user-content-fnref-blob-2fd3bf","data-footnote-ref":!0,"aria-describedby":"footnote-label",children:"4"})})," that have been built locally,\nwhich is useful for developers that can't upload to the remote cache"]}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Cons"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Only includes blobs that have been built or downloaded for local builds"}),"\n",(0,s.jsx)(n.li,{children:"Some redundancy with blobs already stored in the output base"}),"\n",(0,s.jsxs)(n.li,{children:["No automatic cleanup",(0,s.jsx)(n.sup,{children:(0,s.jsx)(n.a,{href:"#user-content-fn-disk-cache-max-size-2fd3bf",id:"user-content-fnref-disk-cache-max-size-2fd3bf","data-footnote-ref":!0,"aria-describedby":"footnote-label",children:"5"})})]}),"\n",(0,s.jsx)(n.li,{children:"A history of being buggy"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["With those in mind,\nwe are going to start out by enabling the disk cache for all use cases in the ",(0,s.jsx)(n.a,{href:"https://bazel.build/run/bazelrc",children:(0,s.jsx)(n.code,{children:".bazelrc"})})," file:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",metastring:'title=".bazelrc"',children:"# Cache\n\ncommon --disk_cache=~/bazel_disk_cache\n"})}),"\n",(0,s.jsx)(n.h3,{id:"remote-cache",children:"Remote cache"}),"\n",(0,s.jsxs)(n.p,{children:["Using a remote cache allows for multiple machines to benefit from the work performed by a single machine.\nIt can be enabled with the ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--remote_c
1ache",children:(0,s.jsx)(n.code,{children:"--remote_cache"})})," flag."]}),"\n",(0,s.jsx)(n.p,{children:"The disk cache can be used concurrently with a remote cache,\nbut I only recommend that setup for local development.\nThat's because normally the remote cache will only accept writes from CI,\nso the disk cache can still provide a benefit to developers,\nbut CI will rarely see benefits,\nand usually degradation,\nby also using the disk cache."}),"\n",(0,s.jsxs)(n.p,{children:["We can adjust the ",(0,s.jsx)(n.code,{children:".bazelrc"})," file to\nenable a remote cache,\ndisable uploads for local development,\nand disable the disk cache on CI\n(by using a dedicated ",(0,s.jsx)(n.a,{href:"https://bazel.build/run/bazelrc#config",children:"config"}),"):"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",metastring:'title=".bazelrc"',children:"common --remote_cache=grpcs://remote.buildbuddy.io\ncommon --noremote_upload_local_results\ncommon:upload --disk_cache=\ncommon:upload --remote_upload_local_results\n\n# CI\n\ncommon:ci --config=upload\n"})}),"\n",(0,s.jsxs)(n.p,{children:["We used ",(0,s.jsx)(n.a,{href:"https://www.buildbuddy.io/remote-cache",children:"BuildBuddy's Remote Cache"})," because it's free for personal or open source projects.\nWe also used the ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--remote_upload_local_results",children:(0,s.jsx)(n.code,{children:"--[no]remote_upload_local_results"})})," flag to control uploading to the remote cache,\nbut in your project you probably want to use some sort of authentication and authorization method to limit this instead\n(e.g. ",(0,s.jsx)(n.a,{href:"https://www.buildbuddy.io/docs/guide-auth",children:"BuildBuddy API keys"}),")."]}),"\n",(0,s.jsxs)(n.p,{children:["At ",(0,s.jsx)(n.a,{href:"https://github.com/brentleyjones/mastodon-ios/commit/aeac0c76d4d94e227a8862e4d5f92953fd0bf173",children:"this commit"})," a remote cache has been integrated into the Mastodon iOS project."]}),"\n",(0,s.jsx)(n.h3,{id:"debugging-cache-hits",children:"Debugging cache hits"}),"\n",(0,s.jsxs)(n.p,{children:["There might be times when you get fewer cache hits than you are expecting.\nIf that's the case,\nI recommend reading ",(0,s.jsx)(n.a,{href:"https://bazel.build/remote/cache-remote",children:'"Debugging Remote Cache Hits"'})," in the Bazel docs,\nwhich has information that will aid you in finding the problem."]}),"\n",(0,s.jsxs)(n.h3,{id:"rules_xcodeproj-cache-warming",children:[(0,s.jsx)(n.strong,{children:"rules_xcodeproj"})," cache warming"]}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"rules_xcodeproj"})," most likely builds your targets with a slightly different configuration than when you build them on the command line.\nBecause of this,\n",(0,s.jsx)(n.strong,{children:"rules_xcodeproj"})," offers a ",(0,s.jsx)(n.a,{href:"https://github.com/MobileNativeFoundation/rules_xcodeproj/blob/1.17.0/docs/usage.md#command-line-api",children:"command-line API"})," that allows you to build targets under the same configuration that it uses.\nThis allows us to build targets on CI in a way that will populate the remote cache with blobs that developers will then get cache hits on."]}),"\n",(0,s.jsxs)(n.p,{children:["We can add a new config to the ",(0,s.jsx)(n.code,{children:".bazelrc"})," file to ensure that we don't get CI specific modifications:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",metastring:'title=".bazelrc"',children:"# Cache warming\n\ncommon:warming --config=upload\ncommon:warming --remote_download_minimal\n"})}),"\n",(0,s.jsx)(n.p,{children:"Here is the cache warming command we would run on CI:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shellsession",children:"$ bazel run //:xcodeproj -- --generator_output_groups=all_targets 'build --config=warming'\nINFO: Analyzed target //:xcodeproj (0 packages loaded, 0 targets configured).\nINFO: Found 1 target...\nTarget //:xcodeproj up-to-date:\n bazel-bin/xcodeproj-runner.sh\nINFO: Elapsed time: 0.145s, Critical Path: 0.00s\nINFO: 1 process: 1 internal.\nINFO: Build completed successfully, 1 total action\nINFO: Running command line: bazel-bin/xcodeproj-runner.sh '--generator_output_groups=all_targets' 'build --config=warming'\n\nINFO: Analyzed target @@rules_xcodeproj~override~internal~rules_xcodeproj_generated//generator/xcodeproj:xcodeproj (3 packages loaded, 515 targets configured).\nINFO: Found 1 target...\nINFO: Elapsed time: 4.002s, Critical Path: 3.48s\nINFO: 13 processes: 5 internal, 7 local, 1 worker.\nINFO: Build completed successfully, 13 total actions\n"})}),"\n",(0,s.jsxs)(n.admonition,{type:"note",children:[(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"--generator_output_groups=all_targets"})," will build every target,\nin every ",(0,s.jsx)(n.code,{children:"target_environment"}),",\nthat could be built by the project generated by ",(0,s.jsx)(n.code,{children:"//:xcodeproj"}),"."]}),(0,s.jsxs)(n.p,{children:["If you don't want to build ",(0,s.jsx)(n.em,{children:"every"})," target\n(e.g. you don't want to cache tests or device builds),\nyou can adjust the\n",(0,s.jsx)(n.code,{children:"top_level_targets"}),",\n",(0,s.jsx)(n.code,{children:"focused_targets"}),",\nand/or ",(0,s.jsx)(n.code,{children:"unfocused_targets"})," attributes to reflect the subset of targets that you want to build.\nYou can do this by defining additional ",(0,s.jsx)(n.code,{children:"xcodeproj"})," targets,\nor by adjusting the existing ",(0,s.jsx)(n.code,{children:"//:xcodeproj"})," target before running the cache warming command\n(and then throwing away the modifications after)."]})]}),"\n",(0,s.jsx)(n.h3,{id:"build-event-service",children:"Build event service"}),"\n",(0,s.jsx)(n.p,{children:"Now that we have a remote cache enabled,\nwe should also enable uploading events to a Build Event Service (BES).\nUsing BES gives you greater insight into your builds,\nasynchronously to when the build actually happened\n(w
1hich can be immensely useful for CI builds)."}),"\n",(0,s.jsx)(n.p,{children:"Here are some benefits of using BES:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Easily share build logs"}),"\n",(0,s.jsx)(n.li,{children:"See historical build data, including aggregations and trends"}),"\n",(0,s.jsxs)(n.li,{children:["See details not exposed via the terminal\n(e.g. all command-line flags used without having to use ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--announce_rc",children:(0,s.jsx)(n.code,{children:"--announce_rc"})}),",\nor all environment variables set)"]}),"\n",(0,s.jsxs)(n.li,{children:["View action timing data (same as ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--generate_json_trace_profile",children:(0,s.jsx)(n.code,{children:"--generate_json_trace_profile"})}),")"]}),"\n",(0,s.jsx)(n.li,{children:"Visualize queries"}),"\n",(0,s.jsx)(n.li,{children:"View error and test logs"}),"\n",(0,s.jsx)(n.li,{children:"Download action outputs"}),"\n",(0,s.jsx)(n.li,{children:"View remote cache stats"}),"\n",(0,s.jsxs)(n.li,{children:["View related remote execution data","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"List of actions executed"}),"\n",(0,s.jsx)(n.li,{children:"Individual action details\n(e.g. command-line arguments,\nenvironment variables,\nplatform properties,\ntiming data,\nand downloading inputs and outputs)"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["We can adjust the ",(0,s.jsx)(n.code,{children:".bazelrc"})," file to upload to a BES endpoint:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",metastring:'title=".bazelrc"',children:"common --bes_backend=grpcs://remote.buildbuddy.io\ncommon --bes_results_url=https://app.buildbuddy.io/invocation/\ncommon --bes_upload_mode=nowait_for_upload_complete\n"})}),"\n",(0,s.jsxs)(n.p,{children:["We used ",(0,s.jsx)(n.a,{href:"https://www.buildbuddy.io/ui",children:"BuildBuddy's Build and Test UI"})," because it's free for personal or open source projects."]}),"\n",(0,s.jsxs)(n.p,{children:["Now when we perform a build there will be a ",(0,s.jsx)(n.a,{href:"https://app.buildbuddy.io/invocation/e69835b2-672f-4a4f-bb00-c73ca8850859",children:"link to the build results UI"}),",\nwhich when opened shows detailed information about the build performed:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shellsession",children:"$ bazel build //Mastodon\nINFO: Invocation ID: e69835b2-672f-4a4f-bb00-c73ca8850859\nINFO: Streaming build results to: https://app.buildbuddy.io/invocation/e69835b2-672f-4a4f-bb00-c73ca8850859\nINFO: Analyzed target //Mastodon:Mastodon (0 packages loaded, 0 targets configured).\nINFO: Found 1 target...\nTarget //Mastodon:Mastodon up-to-date:\n bazel-bin/Mastodon/Mastodon.ipa\nINFO: Elapsed time: 0.253s, Critical Path: 0.00s\nINFO: 1 process: 1 internal.\nINFO: Build completed successfully, 1 total action\nINFO:\nINFO: Streaming build results to: https://app.buildbuddy.io/invocation/e69835b2-672f-4a4f-bb00-c73ca8850859\n"})}),"\n",(0,s.jsx)("figure",{children:(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.img,{src:i(79643).A+"",width:"2398",height:"2276"}),"\n",(0,s.jsxs)("figcaption",{children:["BuildBuddy Build Results UI for a build of the ",(0,s.jsx)("code",{children:"//Mastodon"})," target"]})]})}),"\n",(0,s.jsxs)(n.p,{children:["At ",(0,s.jsx)(n.a,{href:"https://github.com/brentleyjones/mastodon-ios/commit/aeac0c76d4d94e227a8862e4d5f92953fd0bf173",children:"this commit"})," a BES endpoint has been integrated into the Mastodon iOS project."]}),"\n",(0,s.jsx)(n.h3,{id:"remote-execution",children:"Remote execution"}),"\n",(0,s.jsx)(n.p,{children:'Using a remote execution service allows Bazel to run actions on an external cluster of executors.\nBecause of the inherint latency involved in that\n(e.g. network transfer and staging of remote input trees),\nthe remote executors need to be a fair bit faster than your local machine,\nor your build needs to be "wide enough",\nfor a remote build to be faster than a local one.'}),"\n",(0,s.jsxs)(n.p,{children:["Enabling remote execution after you have remote caching set up is as simple as setting the ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--remote_executor",children:(0,s.jsx)(n.code,{children:"--remote_executor"})})," flag\nand ensuring that ",(0,s.jsx)(n.code,{children:"remote"})," is the first strategy set in ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--spawn_strategy",children:(0,s.jsx)(n.code,{children:"--spawn_strategy"})}),".\n(which it is by default).\nYou might also need to set some default platform properties with the ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--remote_default_exec_properties",children:(0,s.jsx)(n.code,{children:"--remote_default_exec_properties"})})," flag to ensure that actions are routed to the right executors."]}),"\n",(0,s.jsxs)(n.p,{children:["We can adjust the ",(0,s.jsx)(n.code,{children:".bazelrc"})," file to have a dedicated ",(0,s.jsx)(n.code,{children:"remote"})," config which sets ",(0,s.jsx)(n.code,{children:"--remote_executor"})," and some default platform properties for an Apple silicon Mac:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",metastring:'title=".bazelrc"',children:"# Remote exectuion\n\ncommon:remote --config=upload\ncommon:remote --remote_executor=grpcs://remote.buildbuddy.io\ncommon:remote --remote_default_exec_properties=OSFamily=darwin\ncommon:remote --remote_default_exec_properties=Arch=arm64\n"})}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["BuildBuddy's anonymous and personal tiers don't include Mac remote executors.\n",(0,s.jsx)(n.a,{href:"https://www.buildbuddy.io/contact",children:"Contact us"})," if you want to use Mac remote execution."]})}),"\n",(0,s.jsxs)(n.p,{children:["At ",(0,s.jsx)(n.a,{href:"https://github.com/brentleyjones/mastodon-ios/commit/ea98209d9907f03a89c7d08aef9ea0bfec729621",children:"this commit"})," remote execution has been integrated into the Mastodon iOS project."]}),"\n",(0,s.jsx)(n.h2,{id:"optimizing",children:"Optimizing"}),"\n",(0,s.jsx)(n.p,{children:"Now that everything is building,\nand we have a functioning remote cache,\nit's time to optimize the build further.\nIdeally we wouldn't need to do anything further at this point,\nbut currently Bazel doesn't have the best default settings for all projects.\nAnd even if it did,\nthe structure of the build graph itself has a large impact on build performance."}),"\n",(0,s.jsx)(n.h3,{id:"optimal-bazel-settings",children:"Optimal Bazel settings"}),"\n",(0,s.jsxs)(n.p,{children:["If you build with Xcode,\n",(0,s.jsx)(n.strong,{children:"rules_xcodeproj"})," will apply a lot of ",(0,s.jsx)(n.a,{href:"https://github.com/MobileNativeFoundation/rules_xcodeproj/blob/1.17.0/xcodeproj/internal/templates/xcodeproj.bazelrc",children:"optimal settings for you"}),".\nI'll still list them here,\nalong with some settings it doesn't set for you,\nin case you need to build outside of Xcode."]}
1),"\n",(0,s.jsxs)(n.p,{children:["At ",(0,s.jsx)(n.a,{href:"https://github.com/brentleyjones/mastodon-ios/commit/066830f4f2ee0a4c2f533ee26acfbe546f5b4bb2",children:"this commit"})," we applied all of the flags discussed in the following sections to the Mastodon iOS project."]}),"\n",(0,s.jsx)(n.h4,{id:"cacheability",children:"Cacheability"}),"\n",(0,s.jsxs)(n.p,{children:["The\n",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/apple_support/blob/1.14.0/crosstool/cc_toolchain_config.bzl#L2080-L2089",children:(0,s.jsx)(n.code,{children:"oso_prefix_is_pwd"})}),",\n",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/apple_support/blob/1.14.0/crosstool/cc_toolchain_config.bzl#L1747-L1761",children:(0,s.jsx)(n.code,{children:"relative_ast_path"})}),",\nand ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/apple_support/blob/1.14.0/crosstool/cc_toolchain_config.bzl#L1851-L1873",children:(0,s.jsx)(n.code,{children:"remap_xcode_path"})}),"\n",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--features",children:"features"})," in ",(0,s.jsx)(n.strong,{children:"apple_support"}),",\nand the ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_swift/blob/1.16.0/swift/internal/feature_names.bzl#L229-L238",children:(0,s.jsx)(n.code,{children:"swift.cacheable_swiftmodules"})})," feature in ",(0,s.jsx)(n.strong,{children:"rules_swift"}),",\nremove absolute paths from binaries produced by ",(0,s.jsx)(n.strong,{children:"rules_apple"})," and ",(0,s.jsx)(n.strong,{children:"rules_swift"}),".\nIt's highly recommended that you use these features if using a remote cache,\notherwise you might get a low cache hit rate on anything involving those binaries.\nAs long as you are using a version of ",(0,s.jsx)(n.strong,{children:"apple_support"})," and ",(0,s.jsx)(n.strong,{children:"rules_swift"})," that is at least 1.5.0,\nthese features are enabled by default.\nOtherwise,\nyou can enable them with ",(0,s.jsx)(n.code,{children:"--features=oso_prefix_is_pwd,relative_ast_path,remap_xcode_path,swift.cacheable_swiftmodules"}),"."]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["When using these features you may need to ",(0,s.jsx)(n.a,{href:"https://github.com/MobileNativeFoundation/rules_xcodeproj/blob/1.17.0/tools/generators/swift_debug_settings/src/Generator/WriteSwiftDebugSettings.swift#L121-L155",children:"provide additional information to lldb"})," for debugging to work properly."]})}),"\n",(0,s.jsx)(n.h4,{id:"local-performance",children:"Local performance"}),"\n",(0,s.jsxs)(n.p,{children:["Bazel's implementation of ",(0,s.jsx)(n.a,{href:"https://bazel.build/docs/sandboxing",children:"sandboxing"})," on macOS is slow.",(0,s.jsx)(n.sup,{children:(0,s.jsx)(n.a,{href:"#user-content-fn-slow-macos-sandbox-2fd3bf",id:"user-content-fnref-slow-macos-sandbox-2fd3bf","data-footnote-ref":!0,"aria-describedby":"footnote-label",children:"6"})}),"\nBecause of that I recommend,\nat least for non-release builds,\ndisabling sandboxing.\nThis can be achieved by setting the ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--spawn_strategy",children:(0,s.jsx)(n.code,{children:"--spawn_strategy"})})," flag to ",(0,s.jsx)(n.code,{children:"remote,worker,local"}),",\nand setting the ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--worker_sandboxing",children:(0,s.jsx)(n.code,{children:"--noworker_sandboxing"})})," flag."]}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_swift/blob/1.16.0/swift/internal/feature_names.bzl#L167-L174",children:(0,s.jsx)(n.code,{children:"swift.use_global_module_cache"})})," feature in ",(0,s.jsx)(n.strong,{children:"rules_swift"})," sets the Swift module cache to a fixed location,\nallowing it to be reused by multiple compilations.\nThis can result in up to 10 times faster compilations.\nAs long as you are using a version of ",(0,s.jsx)(n.strong,{children:"rules_swift"})," that is at least 1.5.0,\nthis feature is enabled by default.\nOtherwise,\nyou can enable it with ",(0,s.jsx)(n.code,{children:"--features=swift.use_global_module_cache"}),"."]}
1),"\n",(0,s.jsxs)(n.p,{children:["Bazel calculates digests of all input and output files,\nin order to determine when actions need to be rerun.\nBy default it uses the ",(0,s.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/SHA-2",children:"SHA-256"})," hash function to calculate these digests.\nSince version 6.4.0 Bazel now supports using the ",(0,s.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE3",children:"BLAKE3"})," hash function instead,\nby setting the ",(0,s.jsx)(n.a,{href:"https://bazel.build/reference/command-line-reference#flag--digest_function",children:(0,s.jsx)(n.code,{children:"--digest_function"})})," startup flag to ",(0,s.jsx)(n.code,{children:"blake3"}),".\nFor builds with large binaries,\nwhich iOS apps usually have,\nusing BLAKE3 can result in up to 5 times faster digest calculations.\nAnd since the actions producing these large binaries\n(e.g. linking)\nare normally on the critical path,\nthis can speed up incremental development."]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["If you use remote capabilities,\nsuch as\nremote caching,\nremote execution,\nor a build event service,\nthey also have to supp
1ort the digest function you set with ",(0,s.jsx)(n.code,{children:"--digest_function"}),".\nIn case you were wondering,\nall of BuildBuddy's products\n(i.e. Build and Test UI, Remote Build Cache, and Remote Build Execution)\nsupport BLAKE3 digests \ud83d\ude0a."]})}),"\n",(0,s.jsxs)(n.p,{children:["Speaking of digests,\nBazel keeps an in-memory cache of file digests,\nsince computing the digests is expensive.\nThe default number of entries in this cache is set to 50,000,\nwhich can be too low for some projects.\nI recommend adjusting this up by setting the ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--cache_computed_file_digests",children:(0,s.jsx)(n.code,{children:"--cache_computed_file_digests"})})," flag to ",(0,s.jsx)(n.code,{children:"500000"}),".\nThis number seems to have a minimal impact on the amount of memory that the Bazel server retains."]}),"\n",(0,s.jsx)(n.h4,{id:"remote-performance",children:"Remote performance"}),"\n",(0,s.jsx)(n.p,{children:"There are a few settings I recommend adjusting for optimal\nremote caching,\nremote execution,\nand build event service\nusage."}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--experimental_remote_cache_async",children:(0,s.jsx)(n.code,{children:"--experimental_remote_cache_async"})})," flag allows execution of actions that don't depend on the output of a given action to start before the outputs of that action finish uploading.\nThat's a lot of words to say that uploading of outputs becomes as asynchronous as possible,\nwhile still blocking dependent actions from starting.\nThe ",(0,s.jsx)(n.code,{children:"bazel"})," command will also block at the end of a build while it waits for all uploads to finish."]}),"\n",(0,s.jsxs)(n.p,{children:["Similarly,\nsetting the ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--bes_upload_mode",children:(0,s.jsx)(n.code,{children:"--bes_upload_mode"})})," flag to ",(0,s.jsx)(n.code,{children:"nowait_for_upload_complete"})," will prevent the ",(0,s.jsx)(n.code,{children:"bazel"})," command from blocking at the end of a build while it uploads to a build event service.\nIf another build starts while uploads are still happening in the background,\nthat build will wait to start until the previous uploads finish.\nYou can have previous uploads cancelled rather than delay a build by using a value of ",(0,s.jsx)(n.code,{children:"fully_async"})," instead."]}),"\n",(0,s.jsxs)(n.p,{children:["Speaking of BES related flags,\nif your build event service isn't ",(0,s.jsx)(n.a,{href:"https://github.com/google/resultstoreui",children:"ResultStore"}),"\n(e.g. BuildBuddy),\nyou should set ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--legacy_important_outputs",children:(0,s.jsx)(n.code,{children:"--nolegacy_important_outputs"})})," to reduce the size of the uploaded events."]}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--remote_cache_compression",children:(0,s.jsx)(n.code,{children:"--remote_cache_compression"})})," flag causes Bazel to compress uploads and decompress downloads with the ",(0,s.jsx)(n.a,{href:"https://github.com/facebook/zstd",children:"zstd"})," algorithm.\nThis reduces the average number of bytes transferred by around 70%,\nwhich for some projects has resulted in a 45% faster overall build time."]}),"\n",(0,s.jsxs)(n.p,{children:["Some types of actions we shouldn't remotely cache or build.\nWe can tell Bazel about that by using the ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--modify_execution_info",children:(0,s.jsx)(n.code,{children:"--modify_execution_info"})})," flag.\nThe ",(0,s.jsx)(n.strong,{children:"rules_apple"})," documentation has a ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/blob/master/doc/common_info.md#optimizing-remote-cache-and-build-execution-performance",children:"nice explaination"})," on the suggested value for this flag."]}),"\n",(0,s.jsxs)(n.p,{children:["If using remote execution,\nyou might have to set some additional platform properties for optimal performance.\nIn the case of BuildBudddy's product,\nI recommend setting ",(0,s.jsx)(n.a,{href:"https://www.buildbuddy.io/docs/rbe-platforms#action-isolation-and-hermeticity-properties",children:"these properties"}),":"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"recycle-runner=true"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"preserve-workspace=true"})}),"\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"clean-workspace-inputs=*"})}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Finally,\nwhen using remote caching or remote execution,\nyou'll want to use an increased value for the ",(0,s.jsx)(n.a,{href:"https://bazel.build/versions/7.0.0/reference/command-line-reference#flag--jobs",children:(0,s.jsx)(n.code,{children:"--jobs"})})," flag.\nA higher value will result in more concurrent downloads and/or remote actions being executed.\nYou'll need to experiement to determine the correct value for you,\nsince different projects or host machines can benefit from,\n
1or potentially suffer from",(0,s.jsx)(n.sup,{children:(0,s.jsx)(n.a,{href:"#user-content-fn-jobs-flag-performance-2fd3bf",id:"user-content-fnref-jobs-flag-performance-2fd3bf","data-footnote-ref":!0,"aria-describedby":"footnote-label",children:"7"})}),",\ndifferent values."]}),"\n",(0,s.jsx)(n.h3,{id:"modularization",children:"Modularization"}),"\n",(0,s.jsx)(n.p,{children:"To get the most benefit out of Bazel's incremental compilation support,\nyou need to have your project split into many modules.\nIf your project is only a couple large modules,\nthen when you make changes most of that code will need to be recompiled.\nBut if your project is made up of many small modules,\nthen when you make changes a smaller portion of the code will need to be recompiled."}),"\n",(0,s.jsxs)(n.p,{children:['You can think of modularization\n(the process of splitting large modules into smaller modules)\nas one way to optimize your build graph for Bazel.\nAnother would be adjusting your dependencies such that your build graph is "wide" instead of "deep".\nThis allows for more parallel compilation,\nand when using ',(0,s.jsx)(n.a,{href:"#remote-execution",children:"remote execution"})," the wider the build graph is the better."]}),"\n",(0,s.jsx)(n.h2,{id:"next-steps",children:"Next steps"}),"\n",(0,s.jsx)(n.p,{children:"At this point you should have a well functioning iOS Bazel project.\nCongratulations \ud83c\udf89!"}),"\n",(0,s.jsx)(n.p,{children:"Next steps from here depend on what is the highest priority for you and your team.\nYou could\ncontinue focusing on modularization,\nprofile and optimize build performance,\nor maybe look into custom macros and rules to make your specific workflows more efficient or easier to set up."}),"\n",(0,s.jsxs)(n.p,{children:["In you need any help,\nthe ",(0,s.jsx)(n.code,{children:"#apple"})," and ",(0,s.jsx)(n.code,{children:"#rules_xcodeproj"})," channels in the ",(0,s.jsx)(n.a,{href:"https://slack.bazel.build/",children:"Bazel Slack workspace"})," are a great place to ask questions.\nYou can also email us at ",(0,s.jsx)(n.a,{href:"mailto:[email protected]",children:"[email protected]"})," with any questions, comments, or thoughts."]}),"\n","\n",(0,s.jsxs)(n.section,{"data-footnotes":!0,className:"footnotes",children:[(0,s.jsx)(n.h2,{className:"sr-only",id:"footnote-label",children:"Footnotes"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{id:"user-content-fn-mergable_libraries-2fd3bf",children:["\n",(0,s.jsxs)(n.p,{children:["When ",(0,s.jsx)(n.strong,{children:"rules_apple"})," gains support for ",(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/rules_apple/issues/1988",children:"mergable_libraries"})," this type of workflow will be a lot easier to support. ",(0,s.jsx)(n.a,{href:"#user-content-fnref-mergable_libraries-2fd3bf","data-footnote-backref":"","aria-label":"Back to reference 1",className:"data-footnote-backref",children:"\u21a9"})]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{id:"user-content-fn-local-rspm-2fd3bf",children:["\n",(0,s.jsxs)(n.p,{children:["This is an area that I think can be improved upon in ",(0,s.jsx)(n.strong,{children:"rules_swift_package_manager"}),".\nFor example,\nif you are only using a ",(0,s.jsx)(n.code,{children:"Package.swift"})," file,\nwithout an Xcode project,\nI think it would be nice to be able to use ",(0,s.jsx)(n.strong,{children:"rules_swift_package_manager"})," to build the declared products in it,\nwithout having to migrate those to Bazel manually. ",(0,s.jsx)(n.a,{href:"#user-content-fnref-local-rspm-2fd3bf","data-footnote-backref":"","aria-label":"Back to reference 2",className:"data-footnote-backref",children:"\u21a9"})]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{id:"user-content-fn-rspm-no-gazelle-2fd3bf",children:["\n",(0,s.jsxs)(n.p,{children:["There is ",(0,s.jsx)(n.a,{href:"https://github.com/cgrindel/rules_swift_package_manager/discussions/936",children:"a plan"})," that before ",(0,s.jsx)(n.strong,{children:"rules_swift_package_manager"})," reaches 1.0.0 it will remove the need for the ",(0,s.jsx)(n.code,{children:"swift_deps_index.json"})," file,\nand the need to use Gazelle with it. ",(0,s.jsx)(n.a,{href:"#user-content-fnref-rspm-no-gazelle-2fd3bf","data-footnote-backref":"","aria-label":"Back to reference 3",className:"data-footnote-backref",children:"\u21a9"})]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{id:"user-content-fn-blob-2fd3bf",children:["\n",(0,s.jsxs)(n.p,{children:['A "blob" is an ',(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/remote-apis",children:"REAPI"})," term for artifacts that are stored in a cache. ",(0,s.jsx)(n.a,{href:"#user-content-fnref-blob-2fd3bf","data-footnote-backref":"","aria-label":"Back to reference 4",className:"data-footnote-backref",children:"\u21a9"})]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{id:"user-content-fn-disk-cache-max-size-2fd3bf",children:["\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/bazel/issues/5139",children:"Bazel issue #5139"}),". ",(0,s.jsx)(n.a,{href:"#user-content-fnref-disk-cache-max-size-2fd3bf","data-footnote-backref":"","aria-label":"Back to reference 5",className:"data-footnote-backref",children:"\u21a9"})]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{id:"user-content-fn-slow-macos-sandbox-2fd3bf",children:["\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/bazel/issues/8230",children:"Bazel issue #8320"}),". ",(0,s.jsx)(n.a,{href:"#user-content-fnref-slow-macos-sandbox-2fd3bf","data-footnote-backref":"","aria-label":"Back to reference 6",className:"data-footnote-backref",children:"\u21a9"})]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{id:"user-content-fn-jobs-flag-performance-2fd3bf",children:["\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.a,{href:"https://github.com/bazelbuild/bazel/issues/21594",children:"Bazel issue #21954"}),". ",(0,s.jsx)(n.a,{href:"#user-content-fnref-jobs-flag-performance-2fd3bf","data-footnote-backref":"","aria-label":"Back to reference 7",className:"data-footnote-backref",children:"\u21a9"})]}),"\n"]}),"\n"]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},79643(e,n,i){i.d(n,{A:()=>t});const t=i.p+"assets/images/mastodon-bazel-build-results-ui-ba79e24b22cd1f0300cc98853736ea94.png"},55193(e,n,i){i.d(n,{A:()=>t});const t=i.p+"assets/images/mastodon-bazel-xcode-and-simulator-edbd4c56a1187b32e1b1f83b2288c8a4.png"},35676(e,n,i){i.d(n,{R:()=>o,x:()=>r});var t=i(30758);const s={},a=t.createContext(s);function o(e){const n=t.useContext(a);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function r(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),t.createElement(a.Provider,{value:n},e.children)}},91206(e){e.exports=JSON.parse('{"permalink":"/blog/how-to-migrate-an-ios-app-to-bazel","editUrl":"https://github.com/buildbuddy-io/buildbuddy/edit/master/website/blog/how-to-migrate-an-ios-app-to-bazel.md","source":"@site/blog/how-to-migrate-an-ios-app-to-bazel.md","title":"How to migrate an iOS app to Bazel","description":"Wondering how to migrate your Apple-based project to Bazel? Whether it\'s iOS, macOS, watchOS, tvOS, or visionOS, this guide is for you.","date":"2024-03-13T08:00:00.000Z","tags":[{"inline":true,"label":"bazel","permalink":"/blog/tags/bazel"}],"readingTime":40.58,"hasTruncateMarker":true,"authors":[{"name":"Brentley Jones","title":"Developer Evangelist @ BuildBuddy","url":"https://brentleyjones.com","imageURL":"https://avatars.githubusercontent.com/u/158658?v=4","key":"brentley","page":null}
1],"frontMatter":{"slug":"how-to-migrate-an-ios-app-to-bazel","title":"How to migrate an iOS app to Bazel","description":"Wondering how to migrate your Apple-based project to Bazel? Whether it\'s iOS, macOS, watchOS, tvOS, or visionOS, this guide is for you.","authors":"brentley","date":"2024-03-13:08:00:00","image":"/img/ios_bazel.png","tags":["bazel"]},"unlisted":false,"prevItem":{"title":"Why is my Bazel build so slow?","permalink":"/blog/debugging-slow-bazel-builds"},"nextItem":{"title":"How Bazel 7.0 Makes Your Builds Faster","permalink":"/blog/how-bazel-7-0-makes-your-builds-faster"}}')}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.