1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
|
# Computed Roots
The general approach of writing a build description side-by-side with
the source code works in most cases. There are, however, cases where
the build description depends on the contents of source-like files.
Here we consider a somewhat contrived example that, however, shows
all the various types of derived roots. Let's say we have a very
regular structure of our code base: one top-level directory for
each library and if there are dependencies, then there is a plain
file `deps` listing, one entry per line, the libraries depended
upon. From that structure we want a derived build description that
is not maintained manually.
As an example, say, so far we have the file structure
```
src
+--foo
| +-- foo.hpp
| +-- foo.cpp
|
+--bar
+-- bar.hpp
+-- bar.cpp
+-- deps
```
where `src/bar/deps` contains a single line, saying `foo`.
The first step is to write a generator for a single `TARGETS` file. To clearly
separate the infrastructure files from the sources, we add the generator as
`utils/generate.py`.
```{.python srcname="utils/generate.py"}
#!/usr/bin/env python3
import json
import sys
name = sys.argv[1]
deps = []
if len(sys.argv) > 2:
with open(sys.argv[2]) as f:
deps = f.read().splitlines()
target = {"type": ["@", "rules", "CC", "library"],
"name": [name],
"hdrs": [["GLOB", None, "*.hpp"]],
"srcs": [["GLOB", None, "*.cpp"]],
"stage": [name],
}
if deps:
target["deps"] = [[x, ""] for x in deps]
targets = {"": target}
with open("TARGETS", "w") as f:
json.dump(targets, f)
f.write("\n")
```
A `TARGETS` file has to be created for every directory containing
files (and not just other directories). Additionally, there needs to be
a top-level target staging all those files that is exported. This can
be implemented by another script, say `utils/call-generator-targets.py`.
```{.python srcname="utils/call-generator-targets.py"}
#!/usr/bin/env python3
import json
import sys
import os
targets = {}
stage = {}
for root, dirs, files in os.walk("."):
if files:
target_name = "lib " + root
with_deps = "deps" in files
deps_name = os.path.join(root, "deps")
entry = {"type": "generic",
"outs": ["TARGETS"],
"deps": ([["@", "utils", "", "generate.py"]]
+ ([["", deps_name]] if with_deps else [])),
"cmds": ["./generate.py " + os.path.normpath(root)
+ (" " + deps_name if with_deps else "")]}
targets[target_name] = entry
stage[os.path.normpath(os.path.join(root, "TARGETS"))] = target_name
targets["stage"] = {"type": "install", "files": stage}
targets[""] = {"type": "export", "target": "stage"}
with open(sys.argv[1], "w") as f:
json.dump(targets, f, sort_keys=True)
f.write("\n")
```
Of course, those scripts have to be executable.
```shell
$ chmod 755 utils/*.py
```
With that, we can generate the build description for generating
the target files. We first write a target file `utils/targets.generate`.
```{.json srcname="utils/targets.generate"}
{ "": {"type": "export", "target": "generate"}
, "generate":
{ "type": "generic"
, "cmds": ["cd src && ../call-generator-targets.py ../TARGETS"]
, "outs": ["TARGETS"]
, "deps": [["@", "utils", "", "call-generator-targets.py"], "src"]
}
, "src": {"type": "install", "dirs": [[["TREE", null, "."], "src"]]}
}
```
As we intend to make `utils` a separate logical repository, we also
add a trivial top-level targets file.
```shell
$ echo {} > utils/TARGETS
```
Next we can start a repository description. Here we notice that
the tasks to be performed to generate the target files only depend
on the tree structure of the `src` repository. So, we use the
tree structure as workspace root to avoid unnecessary runs of
`utils/targets.generate`.
```{.json srcname="etc/repos.json"}
{ "repositories":
{ "src":
{"repository": {"type": "file", "path": "src", "pragma": {"to_git": true}}}
, "utils":
{ "repository":
{"type": "file", "path": "utils", "pragma": {"to_git": true}}
}
, "src target tasks description":
{ "repository": {"type": "tree structure", "repo": "src"}
, "target_root": "utils"
, "target_file_name": "targets.generate"
, "bindings": {"utils": "utils"}
}
}
}
```
Of course, the `"to_git"` pragma works best, if we have everything under version
control (which is a good idea in general anyway).
```shell
$ git init
$ git add .
$ git commit -m 'Initial commit'
```
Now the default target of `"src target tasks description"` shows how to
build the target files we want.
```shell
$ just-mr --main 'src target tasks description' build -p
INFO: Performing repositories setup
INFO: Found 3 repositories involved
INFO: Setup finished, exec ["just","build","-C","...","-p"]
INFO: Repository "src target tasks description" depends on 1 top-level computed roots
INFO: Requested target is [["@","src target tasks description","",""],{}]
INFO: Analysed target [["@","src target tasks description","",""],{}]
INFO: Export targets found: 0 cached, 1 uncached, 0 not eligible for caching
INFO: Discovered 1 actions, 0 tree overlays, 0 trees, 0 blobs
INFO: Building [["@","src target tasks description","",""],{}].
INFO: Processed 1 actions, 0 cache hits.
INFO: Artifacts built, logical paths are:
TARGETS [68336b9823a86d0f64a5a79990c6b171d4f6523b:434:f]
{"": {"target": "stage", "type": "export"}, "lib ./bar": {"cmds": ["./generate.py bar ./bar/deps"], "deps": [["@", "utils", "", "generate.py"], ["", "./bar/deps"]], "outs": ["TARGETS"], "type": "generic"}, "lib ./foo": {"cmds": ["./generate.py foo"], "deps": [["@", "utils", "", "generate.py"]], "outs": ["TARGETS"], "type": "generic"}, "stage": {"files": {"bar/TARGETS": "lib ./bar", "foo/TARGETS": "lib ./foo"}, "type": "install"}}
INFO: Backing up artifacts of 1 export targets
```
From that, we can, step by step, define the actual build description.
- The tasks to generate the target files is a computed root of the
`"src target tasks"` using the top-level target `["", ""]`. We
call it `"src target tasks"`.
- This root will be the target root in the repository `src target
build` describing how to generate the actual target files.
- The `"src targets"` are then, again, a computed root.
- Finally, we can define the top-level repository `""`. As we want
to be able to build with uncommitted changes as long as they
do not affect the target description, we use an explicit file
repository instead of referring to the `to_git` repository `"src"`.
- As the top-level targets also depend on our C/C++ rules, we
include those as well and set an appropriate binding for `""`.
Therefore, our final repository description looks as follows.
```{.json srcname="etc/repos.json"}
{ "repositories":
{ "src":
{"repository": {"type": "file", "path": "src", "pragma": {"to_git": true}}}
, "utils":
{ "repository":
{"type": "file", "path": "utils", "pragma": {"to_git": true}}
}
, "src target tasks description":
{ "repository": {"type": "tree structure", "repo": "src"}
, "target_root": "utils"
, "target_file_name": "targets.generate"
, "bindings": {"utils": "utils"}
}
, "src target tasks":
{ "repository":
{ "type": "computed"
, "repo": "src target tasks description"
, "target": ["", ""]
}
}
, "src target build":
{ "repository": "src"
, "target_root": "src target tasks"
, "bindings": {"utils": "utils"}
}
, "src targets":
{ "repository":
{"type": "computed", "repo": "src target build", "target": ["", ""]}
}
, "":
{ "repository": {"type": "file", "path": "src"}
, "target_root": "src targets"
, "bindings": {"rules": "rules"}
}
, "rules":
{ "repository":
{ "type": "git"
, "branch": "master"
, "commit": "7a2fb9f639a61cf7b7d7e45c7c4cea845e7528c6"
, "repository": "https://github.com/just-buildsystem/rules-cc.git"
, "subdir": "rules"
}
}
}
}
```
With that, we can now analyse `["bar", ""]` and see that the dependency we
wrote in `src/bar/deps` is honored. With increased log level we can also see
hints on the computation of the computed roots.
```shell
$ just-mr analyse --log-limit 4 bar ''
INFO: Performing repositories setup
INFO: Found 8 repositories involved
INFO: Setup finished, exec ["just","analyse","-C","...","--log-limit","4","bar",""]
INFO: Repository "" depends on 1 top-level computed roots
PERF: Export target ["@","src target tasks description","",""] taken from cache: [52dd0203238644382280dc9ae79c75d1f7e5adf1:120:f] -> [79a4114597a03d82acfd95a5f95f0d22c6f09ccb:582:f]
PERF: Root [["@","src target tasks description","",""],{}] evaluated to e5dfc10a073e3e101a256bc38fae67ec234afccb, log aa803f1f445bfe20826495e33252ea02c4c1d7e0
PERF: Export target ["@","src target build","",""] registered for caching: [309aac8800c83359aa900b7368b25b03bb343110:120:f]
PERF: Root [["@","src target build","",""],{}] evaluated to db732bc9b76cb485970795dad3de7941567f4caa, log c33eb0167ccd7b5b3b8db51657d0d80885c61f98
INFO: Requested target is [["@","","bar",""],{}]
INFO: Analysed target [["@","","bar",""],{}]
INFO: Result of target [["@","","bar",""],{}]: {
"artifacts": {
"bar/libbar.a": {"data":{"id":"081122c668771bb09ef30b12687c6f131583506714a992595133ab9983366ce7","path":"work/bar/libbar.a"},"type":"ACTION"}
},
"provides": {
"compile-args": [
],
"compile-deps": {
"foo/foo.hpp": {"data":{"path":"foo/foo.hpp","repository":""},"type":"LOCAL"}
},
"debug-hdrs": {
},
"debug-srcs": {
},
"dwarf-pkg": {
},
"link-args": [
"bar/libbar.a",
"foo/libfoo.a"
],
"link-deps": {
"foo/libfoo.a": {"data":{"id":"613a6756639b7fac44a698379581f7ac9113536f95722e4180cae3af45befeb9","path":"work/foo/libfoo.a"},"type":"ACTION"}
},
"lint": [
],
"package": {
"cflags-files": {},
"ldflags-files": {},
"name": "bar"
},
"run-libs": {
},
"run-libs-args": [
]
},
"runfiles": {
"bar/bar.hpp": {"data":{"path":"bar/bar.hpp","repository":""},"type":"LOCAL"}
}
}
```
The quoted logs can be inspected with the `install-cas` subcommand as usual.
To see how target files adapt to source changes, let's
add a new directory `baz` with source and header files,
as well as a `deps` file saying `bar`.
```shell
$ mkdir src/baz
$ echo '#include "bar/bar.hpp" #...' > src/baz/baz.hpp
$ touch src/baz/baz.cpp
$ echo 'bar' > src/baz/deps
```
As this affects the target structure, we commit those changes.
```shell
$ git add . && git commit -m 'New library baz'
```
After that, we can immediately build the new library.
```shell
$ just-mr build --log-limit 4 baz ''
INFO: Performing repositories setup
INFO: Found 8 repositories involved
INFO: Setup finished, exec ["just","build","-C","...","--log-limit","4","baz",""]
INFO: Repository "" depends on 1 top-level computed roots
PERF: Export target ["@","src target tasks description","",""] registered for caching: [02e0545e14758f7fe08a90b56cbfae2e12bdd51e:120:f]
PERF: Root [["@","src target tasks description","",""],{}] evaluated to 43a0b068d6065519061b508a22725c50e68279be, log bf4cc2d0d803bcff78bd7e4e835440467f3a3674
PERF: Export target ["@","src target build","",""] registered for caching: [00c234393fc6986c308c16d6847ed09e79282097:120:f]
PERF: Root [["@","src target build","",""],{}] evaluated to 739a4750d43328ad9c6ff7d9445246a6506368fd, log 51525391c622a398b5190adee07c9de6338d56ba
INFO: Requested target is [["@","","baz",""],{}]
INFO: Analysed target [["@","","baz",""],{}]
INFO: Discovered 6 actions, 0 tree overlays, 3 trees, 0 blobs
INFO: Building [["@","","baz",""],{}].
INFO: Processed 2 actions, 0 cache hits.
INFO: Artifacts built, logical paths are:
baz/libbaz.a [c6eb3219ec0b1017f242889327f9c2f93a316546:1060:f]
(1 runfiles omitted.)
INFO: Target tainted ["test"].
```
Obviously, the tree structure has changed, so `"src target tasks
description"` target gets rebuild. Also, the `"src target build"`
target gets rebuild, but if we inspect the log, we see that 2 out
of 3 actions are taken from cache.
A similar construction is also used in the `justbuild` main `git`
repository for describing the task of formatting all JSON files: the
target root of the logical repository `"format-json"` is computed,
based on the underlying tree structure. Again, the workspace root
for `"format-json"` is the plain file root, so that uncommitted
changes (to committed files) are taken into account.
|