Hello World iOS App Tutorial with Bazel

Taha Bebek
4 min readSep 5, 2022

Build better

Rebuild only what is necessary. Get fast, incremental builds with Bazel’s advanced local and distributed caching, optimized dependency analysis, and parallel execution.

Simply scalable

Scale your organization, codebase, and Continuous Integration systems. Bazel handles codebases of any size, whether in multiple repositories or a huge monorepo.

In this tutorial we will create a minimal app without using Xcode. We will add and edit files for the app on Visual Studio Code. We will build, run and test with Bazel.

Download VSCode

Download VSCode and install “Bazel“ extension.

Create the project

Create a folder named “BazelHelloWorld“. Open this folder in Visual Studio Code. To setup a minimal app, create 5 files in the structure below, .gitignore, BazelHelloWorldApp.swift, ContentView.swift, Info.plist and BazelHelloWorldTests.swift.

BazelHelloWorld
│ .gitignore

└───BazelHelloWorld
│ │ BazelHelloWorldApp.swift
│ │ ContentView.swift
│ │ Info.plist
│ │

└───BazelHelloWorldTests
│ BazelHelloWorldTests.swift

.gitignore

/bazel-BazelHelloWorld
/bazel-bin
/bazel-out
/bazel-testlogs
/bazel-bep.json
/bazel-explain.txt
.DS_STORE
xcuserdata/

BazelHelloWorldApp.swift

import SwiftUI@main
struct BazelHelloWorldApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

ContentView.swift

import SwiftUIstruct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}

Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
</dict>
</plist>

BazelHelloWorldTests.swift

import XCTest
@testable import BazelHelloWorld
class BazelHelloWorldTests: XCTestCase { func testExample() throws {
XCTFail()
}
}

Install baselisk and buildifier on your machine.

brew install bazelisk
brew install buildifier

About Bazelisk

Bazelisk is a wrapper for Bazel written in Go. It automatically picks a good version of Bazel given your current working directory, downloads it from the official server (if required) and then transparently passes through all command-line arguments to the real Bazel binary. You can call it just like you would call Bazel.

About Buildifier

Buildifier is a tool for formatting bazel BUILD and .bzl files with a standard convention.

Add .bazelversion file

Bazelisk will build the project with a Bazel version specified in the .bazelversion file. We will check in that file into our version control to ensure reproducibility of our builds.

Add the following line to it (replace with latest release if needed) :

5.1.1

Create the WORKSPACE.bazel file

A workspace is a directory on your filesystem that contains the source files for the software you want to build. Each workspace directory has a text file named WORKSPACE.bazel which may be empty, or may contain references to external dependencies required to build the outputs.

Directories containing a file called WORKSPACE.bazel are considered the root of a workspace.

Add the following lines to the Workspace.BAZEL file (from rules_apple):

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")http_archive(
name = "build_bazel_rules_apple",
sha256 = "4161b2283f80f33b93579627c3bd846169b2d58848b0ffb29b5d4db35263156a",
url = "https://github.com/bazelbuild/rules_apple/releases/download/0.34.0/rules_apple.0.34.0.tar.gz",
)
load(
"@build_bazel_rules_apple//apple:repositories.bzl",
"apple_rules_dependencies",
)
apple_rules_dependencies()load(
"@build_bazel_rules_swift//swift:repositories.bzl",
"swift_rules_dependencies",
)
swift_rules_dependencies()load(
"@build_bazel_rules_swift//swift:extras.bzl",
"swift_rules_extra_dependencies",
)
swift_rules_extra_dependencies()load(
"@build_bazel_apple_support//lib:repositories.bzl",
"apple_support_dependencies",
)
apple_support_dependencies()

“load” is like an import statement. At line 1, it imports http_archive function from bazel_tools library.

At line 3, it uses http_archive to download rules_apple library from Github url and gives a name to this downloaded library as build_bazel_rules_apple It could give it a different name as well. At line 9, it imports apple_rules_dependencies function from the build_bazel_rules_apple library it just loaded. The rest of the code does the same for swift_rules_dependencies, swift_rules_extra_dependencies and apple_support_dependencies.

Rules

A rule defines a series of actions that Bazel performs on inputs to produce a set of outputs, which are referenced in providers returned by the rule’s implementation function.

Create .bazelrc file

Bazel accepts many options. Some options are varied frequently (for example, — subcommands) while others stay the same across several builds (such as — package_path). To avoid specifying these unchanged options for every build (and other commands), you can specify options in a configuration file, called .bazelrc. More info on bazelrc.

Add the following options to .bazelrc file:

build --ios_minimum_os=15.5
build --ios_simulator_device="iPhone 13"
build --ios_simulator_version=15.5
build --xcode_version=13.4.1
build --verbose_failures

Create BUILD.bazel file for each module

A BUILD file contains several different types of instructions for Bazel. The most important type is the build rule, which tells Bazel how to build the desired outputs, such as executable binaries or libraries. Lets create our first module. Under the BazelHelloWorld/BazelHelloWorld directory, create BUILD.bazel file and add the following:

load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application")
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SwiftLibrary",
srcs = glob([
"*.swift",
]),
data = glob([
"*.xcassets/**/*",
"*.lproj/**/*",
]),
module_name = "BazelHelloWorld",
tags = ["manual"],
visibility = ["//visibility:public"],
)

ios_application(
name = "App",
bundle_id = "com.XXX.BazelHelloWorld",
families = [
"iphone",
"ipad",
],
infoplists = [":Info.plist"],
minimum_os_version = "15.5",
visibility = ["//visibility:public"],
deps = [":SwiftLibrary"],
)

Under the BazelHelloWorld/BazelHelloWorldTests directory, create BUILD.bazel file and add the following:

load("@build_bazel_rules_apple//apple:ios.bzl", "ios_unit_test")
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SwiftLibrary",
srcs = glob([
"*.swift",
]),
tags = ["manual"],
deps = [
"//BazelHelloWorld:SwiftLibrary",
],
)
ios_unit_test(
name = "BazelHelloWorldTests",
families = [
"iphone",
"ipad",
],
minimum_os_version = "15.5",
deps = [":SwiftLibrary"],
)

Final Project Structure

This should be the final project structure:

BazelHelloWorld
│ .gitignore
│ .bazelrc
│ .bazelversion
│ WORKSPACE.bazel

└───BazelHelloWorld
│ │ BazelHelloWorldApp.swift
│ │ ContentView.swift
│ │ Info.plist
│ │ BUILD.bazel

└───BazelHelloWorldTests
│ BazelHelloWorldTests.swift
│ BUILD.bazel

Build the project from command line

bazel build //BazelHelloWorld:App

If there is any error, run the following command to see a detailed information:

bazel build //BazelHelloWorld:App --sandbox_debug

Run the app on the simulator

bazel run //BazelHelloWorld:App

Run tests

bazel test //BazelHelloWorldTests:BazelHelloWorldTests

Dependencies

To see a graph of dependencies, run:

bazel query  --notool_deps --noimplicit_deps "deps(//BazelHelloWorld:App)" --output graph

Then, paste the text into GraphViz.

Generate Xcode Project

XCHammer generates Xcode projects from bazel build files. First, pull XCHammer into the WORKSPACE file:

http_archive(
name = "xchammer",
sha256 = "8436fdeb48e29e787594e85eab92de3fe00a84dbf0e0495e4e83df3e68ed7d47",
urls = [ "https://github.com/pinterest/xchammer/releases/download/v3.4.1.0/xchammer.zip" ],
)

Next, create an xcode_project target including targets:

load("@xchammer//:xcodeproject.bzl", "xcode_project")xcode_project(
name = "XcodeProject",
targets = [ "//BazelHelloWorld:App" ],
paths = [ "**" ],
)

--

--