Build Typescript Project with Bazel Chapter 2: File Structure
In the last chapter, we introduced the basic concept of Bazel. In this blog, I would like to talk about the file structure of Bazel.
Concept and Terminology
Before we introduce the file structure, we need to understand several key concepts and terminology in Bazel.
- Workspace
- Package
- Target
- Rule
These concepts, and terminology, are composed to Build File
, which Bazel will analyze, and execute.
The basic relationship among these concepts looks like this , we will discuss the details one by one.
Workspace
A "workspace" refers to the directories, which contain
- The source files of the project.
- Symbolic links contain the build output.
And the Bazel definition is in a file named WORKSPACE
, or WORKSPACE.bazel
at the root of the project directory. NOTE, one project can only have one WORKSPACE
definition file.
Here is an example of the WORKSPACE
file.
workspace(
name = "com_thisdot_bazel_demo",
)
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
# Fetch rules_nodejs so we can install our npm dependencies
http_archive(
name = "build_bazel_rules_nodejs",
sha256 = "ad4be2c6f40f5af70c7edf294955f9d9a0222c8e2756109731b25f79ea2ccea0",
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.38.3/rules_nodejs-0.38.3.tar.gz"],
)
load("@build_bazel_rules_nodejs//:defs.bzl", "node_repositories", "yarn_install")
node_repositories()
yarn_install(
name = "npm",
package_json = "//:package.json",
yarn_lock = "//:yarn.lock",
)
# Install all Bazel dependencies of the @npm npm packages
load("@npm//:install_bazel_dependencies.bzl", "install_bazel_dependencies")
install_bazel_dependencies()
# Setup the rules_typescript toolchain
load("@npm_bazel_typescript//:index.bzl", "ts_setup_workspace")
ts_setup_workspace()
In a WORKSPACE
file, we should
- Define the name of the workspace. The name should be unique globally, or at least unique in your organization. You could use the reverse dns name, such as
com_thisdot_bazel_demo
, or the name of the project on GitHub. - Install environment related packages, such as
yarn/npm/bazel
. - Setup toolchains needed to
build/test
the project, such astypescript/karma
.
Once WORKSPACE
is ready, application developers don't really need to touch this file.
Package
- The primary unit of code organization (something like module) in a repository
- Collection of related files and a specification of the dependencies among them
- Directory containing a file named BUILD or BUILD.bazel, residing beneath the top-level directory in the workspace
- A package includes all files in its directory, plus all subdirectories beneath it, except those which, themselves, contain a BUILD file
It is important to know how to split a project into package
. It should be easy for the users to develop/test/share the unit of a package
. If the unit is too big, the package
has to be rebuilt on every package file change. If the unit is too small, it will be very hard to maintain and share. So, this is not an issue of Bazel
. It is a general problem of project management.
In Bazel, every package
will have a BUILD.bazel
file, containing all of the build
/test
/bundle
target definitions.
For example, here is a of the Angular structure. Every directory under packages directory is a package
of code organization, and also the build organization of Bazel.
Let's take a look at the file structure of gulpjs
in Angular, so we can have a better understanding about the difference between Bazel and gulpjs.
gulp.task('build-animations', () => {});;
gulp.task('build-core', () => {});
gulp.task('build-core-schematics', () => {});
In most cases,
- a gulpjs file doesn't have 1:1 relationship to the package directory.
- a gulpjs file can reference any files inside the project.
But for Bazel,
- Each
package
should have their ownBUILD.bazel
file. - The
BUILD.bazel
can only reference the file inside the currentpackage
, and if the current package depends on other packages, we need to reference the Bazel buildtarget
from the other packages instead of the files directly.
Here is a Bazel Package directory structure in Angular repo.
Build File
Before we talk about target
, let's take a look at the content of a BUILD.bazel
file.
package(default_visibility = ["//visibility:private"])
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "lib",
srcs = [":lib.ts"],
visibility = ["//visibility:public"],
)
The language of the BUILD.bazel
file is Starlark
.
Starlark
is a subset ofPython
.- It is a very feature-limited language. A ton of
Python
features, such asclass
,import
,while
,yield
,lambda
,is
,raise
, are not supported. Recursion
is not allowed.- Most of Python's builtin methods are not supported.
So Starlark
is a very very simple language, and only supports very limited Python
syntax.
Target
The BUILD.bazel
file contains build targets. Those target
s are the definitions of the build
, test
, and bundle
work we want to achieve.
The build target can represent:
- Files
- Rules
The target can also depend on other targets
- Circular dependencies are not allowed
- Two targets, generating the same output, will cause a problem
- Target dependency must be declared explicitly.
Let's see the previous sample,
package(default_visibility = ["//visibility:private"])
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "lib",
srcs = [":lib.ts"],
visibility = ["//visibility:public"],
)
Here, ts_library
is a rule imported from @npm_bazel_typescript
workspace, and ts_library(name = "lib")
is a target
. The name is lib
, and this target defines the metadata for compiling the lib.ts
with ts_library
rule.
Label
Every target has a unique name called label
. For example, if the BUILD.bazel
file above is under /lib
directory, then the label of the target is
@com_thisdot_bazel_demo//lib:lib
The label is composed of several parts.
- the name of the workspace:
@com_thisdot_bazel_demo
. - the name of the package:
lib
. - the name of the target:
lib
.
So, the composition is <workspace name>//<package name>:<target name>
.
Most of the times, the name of the workspace can be omitted, so the label above can also be expressed as //lib:lib
.
Additionally, if the name of the target is the same as the package's name, the name of the target can also be omitted. Therefore, the label above can also be expressed as //lib
.
NOTE: The label for the target needs to be unique in the workspace.
Visibility
We can also define the visibility to define whether the rule inside this package can be used by other packages.
package(default_visibility = ["//visibility:private"])
The visibility can be:
private
: the rules can be only used inside the current package.public
: the rules can be used everywhere.//some_package:package_scope
: the rules can only be used in the specified scope under//some_package
. Thepackage_scope
can be:__pkg__
/__subpackages__
/package group
.
And if the rules in one package can be accessed from the other package, we can use load
to import them. For example:
load("@npm_bazel_typescript//:index.bzl", "ts_library")
Here, we import the ts_library
rule from the Bazel typescript package.
Target
- Target can be
Files
orRule
. - Target has
input
andoutput
. Theinput
andoutput
are known at build time. - Target will only be rebuilt when
input
changes.
Let's take a look at Rule
first.
Rule
The rule is just like a function
or macro
. It can accept named parameters
as options. Just like in the previous post, calling a rule will not execute an action. It is just metadata. Bazel will decide what to do.
ts_library(
name = "lib",
srcs = [":lib.ts"],
visibility = ["//visibility:public"],
)
So here, we use the ts_library
rule to define a target, and the name is lib
. The srcs is lib.ts
in the same directory. The visibility is public
, so this target can be accessed from the other packages.
Rule Naming
It is very important to follow the naming convention when you want to create your own rule.
*_binary
: executable programs in a given language (nodejs_binary)*_test
: special _binary rule for testing*_library
: compiled module for a given language (ts_library)
Rule common attributes
Several common attributes exist in almost all rules. For example:
ts_library(
name = "lib",
srcs = [":index.ts"],
tags = ["build-target"],
visibility = ["//visibility:public"],
deps = [
":date",
":user",
],
)
- name: unique name within this package
- srcs: inputs of the target, typically files
- deps: compile-time dependencies
- data: runtime dependencies
- testonly: target which should be executed only when running Bazel test
- visibility: specifies who can make a dependency on the given target
Let's see another example:
http_server(
name = "prodserver",
data = [
"index.html",
":bundle",
"styles.css",
],
)
Here, we use the data
attribute. The data
will only be used at runtime.
It will not be analyzed by Bazel at build time.
So, in this blog, we introduced the basic Bazel structure concepts. In the next blog, we will introduce how to query Bazel targets.