Hello World iOS App Tutorial with Bazel
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 BazelHelloWorldclass 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 = [ "**" ],
)