Skip to content

Commit ffc3a48

Browse files
authored
Add a working introduction to Bazel. (#17)
* Add a working introduction to Bazel. * Address review feedback. * Address review feedback. * Address feedback.
1 parent ef67318 commit ffc3a48

File tree

1 file changed

+235
-0
lines changed

1 file changed

+235
-0
lines changed

working/bazel/module-structure.md

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
The following is a reference for [`Bazel`](https://bazel.build/) (internally
2+
called `Blaze` at Google) structures modules of Dart code, especially when
3+
compared to external structures (i.e. via `pub`).
4+
5+
This is *not* a proposal, nor a commitment to build out further Bazel support
6+
externally, but rather a reference to help guide discussions around visibility
7+
modifiers and terminology around modular/reusable Dart code.
8+
9+
## Monolothic Repository
10+
11+
In ["Why Google Stores Billions of Lines of Code in a Single Repository"][1]
12+
the author explain the benefits of a _Monolothic Repository_ ("Mono Repo").
13+
Many of the constraints described in this document are due to this decision,
14+
and be expected to be _unchangeable_ - i.e. it is quite outside of the scope
15+
of any language (including Dart) to change these requirements.
16+
17+
[1]: https://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/fulltext
18+
19+
## Typical Structure
20+
21+
In the Google mono-repository, there is a single root directory. Let's call it
22+
`google` - and it is commonly referenced by tools and Bazel with a prefix of
23+
`//`, so `//ninjacat` is logically located at `/google/ninjacat`. This root
24+
directory is called a _workspace_.
25+
26+
Bazel [defines a _package_][2] as so:
27+
28+
> The primary unit of code organization in a workspace is the _package_. A
29+
> package is a collection of related files and a specification of the
30+
> dependencies among them.
31+
>
32+
> A package is defined as a directory containing a file named `BUILD`,
33+
> residing beneath the top-level directory in the workspace. A package includes
34+
> all files in its directory, plus all subdirectories beneath it, except those
35+
> which themselves contain a BUILD file.
36+
37+
[2]: https://docs.bazel.build/versions/master/build-ref.html#packages_targets
38+
39+
As one can see, already _package_ is very different from a [`pub` package][3].
40+
41+
[3]: https://www.dartlang.org/guides/libraries/create-library-packages
42+
43+
We map the concept of `pub` packages with the following rules:
44+
45+
* A `package:<a>.<b>/uri.dart` resolves to `//a/b/lib/uri.dart`.
46+
* Any other `package:<name>/uri.dart` resolves to `//third_party/dart/<name>`,
47+
i.e. without a `.` in the name.
48+
49+
### Inside a Package
50+
51+
Inside a package, such as `//ninjacat/app`, you can expect the following:
52+
53+
```
54+
> ninjacat/
55+
> app/
56+
> lib/
57+
> test/
58+
> BUILD
59+
```
60+
61+
(If the package happens to be a Dart web _entrypoint_, you might also see `web/`
62+
and for VM binaries, you might also see `bin/`.)
63+
64+
However, there is another important concept, [`targets`][4]:
65+
66+
> A package is a container. The elements of a package are called _targets_. Most
67+
> targets are one of two principal kinds, files and rules. Additionally, there
68+
> is another kind of target, package groups, but they are far less numerous.
69+
70+
[4]: https://docs.bazel.build/versions/master/build-ref.html#targets
71+
72+
So, imagine the following, in the `BUILD` file:
73+
74+
```
75+
dart_library(
76+
name = "app",
77+
srcs = ["lib/app.dart"],
78+
deps = [
79+
":flags",
80+
],
81+
)
82+
83+
dart_library(
84+
name = "flags",
85+
srcs = ["lib/flags.dart"],
86+
)
87+
88+
dart_test(
89+
name = "flags_test",
90+
srcs = ["test/flags_test.dart"],
91+
deps = [
92+
":flags",
93+
"//third_party/dart/test",
94+
],
95+
)
96+
```
97+
98+
Here we have _3_ targets:
99+
* `app`, which potentially is code that wraps together application-specific
100+
code before being used later in the `main()` function of something in either
101+
`web/` or `bin/`.
102+
* `flags`, which contains some common code for setting/getting flags.
103+
* `flags_test`, which tests that `flags` is working-as-intended.
104+
105+
This concept already is quite different than a `pub` package, where all of the
106+
files in `lib/` are accessible once you have a dependency on that package. In
107+
fact, a common issue externally is that `pubspec.yaml` (sort of similar to
108+
`BUILD`) is not granular enough, leading to the creation of "micro packages"
109+
that have a single file or capability.
110+
111+
### Common Patterns
112+
113+
Based on the above, teams tend to structure their projects _hierarchically_,
114+
with more specific code living deeper in a sub-package. For example, imagine
115+
the `ninjacat` project after a few more weeks:
116+
117+
```
118+
> ninjacat/
119+
> app/
120+
> views/
121+
> checkout/
122+
> lib/
123+
> test/
124+
> BUILD
125+
> home/
126+
> lib/
127+
> test/
128+
> BUILD
129+
> common/
130+
> widgets/
131+
> login/
132+
> lib/
133+
> test/
134+
> testing/
135+
> lib/
136+
> BUILD
137+
> BUILD
138+
```
139+
140+
Already we have the following "packages":
141+
142+
* `package:ninjacat.app.views.checkout`
143+
* `package:ninjacat.app.views.home`
144+
* `package:ninjacat.common.widgets.login`
145+
* `package:ninjacat.common.widgets.login.testing`
146+
147+
It's common to use the [`visibility`][5] property to setup the concept of
148+
"friend" packages, i.e. packages that are only accessible to _specific_ other
149+
packages. In the following code, we ensure that the `login` "package" is only
150+
accessible to packages under `app/`:
151+
152+
```
153+
# //ninjacat/common/widgets/login/BUILD
154+
155+
dart_library(
156+
name = "login",
157+
srcs = glob(["lib/**.dart"]),
158+
visibility = [
159+
"//ninajacat/app:__subpackages__",
160+
],
161+
)
162+
```
163+
164+
[5]: https://docs.bazel.build/versions/master/be/common-definitions.html#common-attributes
165+
166+
Another common pattern is the concept of `testonly`, and `testing` packages:
167+
168+
```
169+
# //ninjacat/common/widgets/login/BUILD
170+
171+
dart_test(
172+
name = "login_test",
173+
srcs = ["test/login_test.dart"],
174+
deps = [
175+
"//ninjacat/common/widgets/login/testing",
176+
"//third_party/dart/test",
177+
],
178+
)
179+
```
180+
181+
```
182+
# //ninjacat/common/widgets/login/testing
183+
184+
dart_library(
185+
name = "testing",
186+
srcs = glob(["lib/**.dart"]),
187+
testonly = 1,
188+
deps = [
189+
"//third_party/dart/pageloader",
190+
],
191+
)
192+
```
193+
194+
In this case, `package:ninjacat.common.widgets.login.testing` exposes a set of
195+
test-only libraries that can be used, only in _test_ targets. This pattern isn't
196+
replicable externally at all, but is _very_ common internally to avoid bringing
197+
in test utilities and code into production applications.
198+
199+
## Notable Differences
200+
201+
### No cyclical _targets_
202+
203+
Dart, as the language, allows cyclical dependencies between `.dart` files
204+
(libraries). Bazel does _not_ allow cyclical dependencies between packages
205+
(i.e. _targets_). So, the following is illegal in Bazel where it is fine
206+
externally with `pub`:
207+
208+
```
209+
# //ninjacat/common/foo (i.e. package:ninjacat.common.foo/foo.dart)
210+
211+
dart_library(
212+
name = "foo",
213+
srcs = ["lib/foo.dart"],
214+
dependencies = [
215+
"//ninjacat/common/bar",
216+
],
217+
)
218+
```
219+
220+
```
221+
# //ninjacat/common/bar (i.e. package:ninjacat.common.bar/bar.dart)
222+
223+
dart_library(
224+
name = "bar",
225+
srcs = ["lib/bar.dart"],
226+
dependencies = [
227+
"//ninjacat/common/foo",
228+
],
229+
)
230+
```
231+
232+
Unfortunately this is quite common when you take into account the concept of
233+
pub's `dev_dependencies: ...`. If you have a testing only package (
234+
`angular_test`, `build_test`, `flutter_test`), which depends on the main package
235+
but is also used within the main packages tests you have a cyclic dependency.

0 commit comments

Comments
 (0)