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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
|
* Building Third-party Software
Third-party projects usually ship with their own build description, which often
happens to be not compatible with justbuild. Nevertheless, it is highly
desireable to include external projects via their source code base, instead of
relying on the integration of out-of-band binary distributions. justbuild offers
a flexible approach to provide the required build description via an overlay
layer without the need to touch the original code base.
For the remainder of this section, we expect to have the project files available
resulting from successfully completing the tutorial section on /Building C++
Hello World/. We will demonstrate how to use the open-source project
[[https://github.com/fmtlib/fmt][fmtlib]] as an example for integrating
third-party software to a justbuild project.
** Creating the target overlay layer for fmtlib
Before we construct the overlay layer for fmtlib, we need to determine its file
structure ([[https://github.com/fmtlib/fmt/tree/8.1.1][tag 8.1.1]]). The
relevant header and source files are structured as follows:
#+BEGIN_SRC
fmt
|
+--include
| +--fmt
| +--*.h
|
+--src
+--format.cc
+--os.cc
#+END_SRC
The public headers can be found in ~include/fmt~, while the library's source
files are located in ~src~. For the overlay layer, the ~TARGETS~ files should be
placed in a tree structure that resembles the original code base's structure.
It is also good practice to provide a top-level ~TARGETS~ file, leading to the
following structure for the overlay:
#+BEGIN_SRC
fmt-layer
|
+--TARGETS
+--include
| +--fmt
| +--TARGETS
|
+--src
+--TARGETS
#+END_SRC
Let's create the overlay structure:
#+BEGIN_SRC sh
$ mkdir -p fmt-layer/include/fmt
$ mkdir -p fmt-layer/src
#+END_SRC
The directory ~include/fmt~ contains only header files. As we want all files in
this directory to be included in the ~"hdrs"~ target, we can safely
use the explicit ~TREE~ reference[fn:1], which collects, in a single
artifact (describing a directory) /all/ directory contents
from ~"."~ of the workspace root. Note that the ~TARGETS~ file is only part of
the overlay, and
therefore will not be part of this tree. Furthermore, this tree should be staged
to ~"fmt"~, so that any consumer can include those headers via ~<fmt/...>~. The
resulting header directory target ~"hdrs"~ in ~include/fmt/TARGETS~ should be
described as:
[fn:1] Explicit ~TREE~ references are always a list of length 3, to distinguish
them from target references of length 2 (module and target name). Furthermore,
the second list element is always ~null~ as we only want to allow tree
references from the current module.
#+SRCNAME: fmt-layer/include/fmt/TARGETS
#+BEGIN_SRC js
{ "hdrs":
{ "type": ["@", "rules", "data", "staged"]
, "srcs": [["TREE", null, "."]]
, "stage": ["fmt"]
}
}
#+END_SRC
The actual library target is defined in the directory ~src~. For the public
headers, it refers to the previously created ~"hdrs"~ target via its
fully-qualified target name (~["include/fmt", "hdrs"]~). Source files are the
two local files ~format.cc~, and ~os.cc~. The final target description in
~src/TARGETS~ will look like this:
#+SRCNAME: fmt-layer/src/TARGETS
#+BEGIN_SRC js
{ "fmt":
{ "type": ["@", "rules", "CC", "library"]
, "name": ["fmt"]
, "hdrs": [["include/fmt", "hdrs"]]
, "srcs": ["format.cc", "os.cc"]
}
}
#+END_SRC
Finally, the top-level ~TARGETS~ file can be created. While it is technically
not strictly required, it is considered good practice to /export/ every target
that may be used by another project. Exported targets are subject to high-level
target caching, which allows to skip the analysis and traversal of entire
subgraphs in the action graph. Therefore, we create an export target that
exports the target ~["src", "fmt"]~, with only the variables in the field
~"flexible_config"~ being propagated. The top-level ~TARGETS~ file contains the
following content:
#+SRCNAME: fmt-layer/TARGETS
#+BEGIN_SRC js
{ "fmt":
{ "type": "export"
, "target": ["src", "fmt"]
, "flexible_config": ["CXX", "CXXFLAGS", "ADD_CXXFLAGS", "AR", "ENV"]
}
}
#+END_SRC
After adding the library to the multi-repository configuration (next
step), the list of configuration variables a target, like ~["src",
"fmt"]~, actually depends on can be obtained using the ~--dump-vars~
option of the ~analyse~ subcommand. In this way, an informed decision
can be taken when deciding which variables of the export target to
make tunable for the consumer.
** Adding fmtlib to the Multi-Repository Configuration
Based on the /hello world/ tutorial, we can extend the existing ~repos.json~ by
the layer definition ~"fmt-targets-layer"~ and the repository ~"fmtlib"~, which
is based on the Git repository with its target root being overlayed.
Furthermore, we want to use ~"fmtlib"~ in the repository ~"tutorial"~, and
therefore need to introduce an additional binding ~"format"~ for it:
#+SRCNAME: repos.json
#+BEGIN_SRC js
{ "main": "tutorial"
, "repositories":
{ "rules-cc":
{ "repository":
{ "type": "git"
, "branch": "master"
, "commit": "123d8b03bf2440052626151c14c54abce2726e6f"
, "repository": "https://github.com/just-buildsystem/rules-cc.git"
, "subdir": "rules"
}
, "target_root": "tutorial-defaults"
, "rule_root": "rules-cc"
}
, "tutorial":
{ "repository": {"type": "file", "path": "."}
, "bindings": {"rules": "rules-cc", "format": "fmtlib"}
}
, "tutorial-defaults":
{ "repository": {"type": "file", "path": "./tutorial-defaults"}
}
, "fmt-targets-layer":
{ "repository": {"type": "file", "path": "./fmt-layer"}
}
, "fmtlib":
{ "repository":
{ "type": "git"
, "branch": "8.1.1"
, "commit": "b6f4ceaed0a0a24ccf575fab6c56dd50ccf6f1a9"
, "repository": "https://github.com/fmtlib/fmt.git"
}
, "target_root": "fmt-targets-layer"
, "bindings": {"rules": "rules-cc"}
}
}
}
#+END_SRC
This ~"format"~ binding can you be used to add a new private dependency in
~greet/TARGETS~:
#+SRCNAME: greet/TARGETS
#+BEGIN_SRC js
{ "greet":
{ "type": ["@", "rules", "CC", "library"]
, "name": ["greet"]
, "hdrs": ["greet.hpp"]
, "srcs": ["greet.cpp"]
, "stage": ["greet"]
, "private-deps": [["@", "format", "", "fmt"]]
}
}
#+END_SRC
Consequently, the ~fmtlib~ library can now be used by ~greet/greet.cpp~:
#+SRCNAME: greet/greet.cpp
#+BEGIN_SRC cpp
#include "greet.hpp"
#include <fmt/format.h>
void greet(std::string const& s) {
fmt::print("Hello {}!\n", s);
}
#+END_SRC
Due to changes made to ~repos.json~, building this tutorial requires to rerun
~just-mr~, which will fetch the necessary sources for the external repositories:
#+BEGIN_SRC sh
$ just-mr build helloworld
INFO: Requested target is [["@","tutorial","","helloworld"],{}]
INFO: Analysed target [["@","tutorial","","helloworld"],{}]
INFO: Export targets found: 0 cached, 0 uncached, 1 not eligible for caching
INFO: Discovered 7 actions, 3 trees, 0 blobs
INFO: Building [["@","tutorial","","helloworld"],{}].
INFO: Processed 7 actions, 1 cache hits.
INFO: Artifacts built, logical paths are:
helloworld [0ec4e36cfb5f2c3efa0fff789349a46694a6d303:132736:x]
$
#+END_SRC
Note to build the ~fmt~ target alone, its containing repository ~fmtlib~ must be
specified via the ~--main~ option:
#+BEGIN_SRC sh
$ just-mr --main fmtlib build fmt
INFO: Requested target is [["@","fmtlib","","fmt"],{}]
INFO: Analysed target [["@","fmtlib","","fmt"],{}]
INFO: Export targets found: 0 cached, 0 uncached, 1 not eligible for caching
INFO: Discovered 3 actions, 1 trees, 0 blobs
INFO: Building [["@","fmtlib","","fmt"],{}].
INFO: Processed 3 actions, 3 cache hits.
INFO: Artifacts built, logical paths are:
libfmt.a [513b2ac17c557675fc841f3ebf279003ff5a73ae:240914:f]
(1 runfiles omitted.)
$
#+END_SRC
** Employing high-level target caching
The make use of high-level target caching for exported targets, we need to
ensure that all inputs to an export target are transitively content-fixed. This
is automatically the case for ~"type":"git"~ repositories. However, the ~libfmt~
repository also depends on ~"rules-cc"~, ~"tutorial-defaults"~, and
~"fmt-target-layer"~. As the latter two are ~"type":"file"~ repositories, they
must be put under Git versioning first:
#+BEGIN_SRC sh
$ git init .
$ git add tutorial-defaults fmt-layer
$ git commit -m"fix compile flags and fmt targets layer"
#+END_SRC
Note that ~rules-cc~ already is under Git versioning.
Now, to instruct ~just-mr~ to use the content-fixed, committed source trees of
those ~"type":"file"~ repositories the pragma ~"to_git"~ must be set for them in
~repos.json~:
#+SRCNAME: repos.json
#+BEGIN_SRC js
{ "main": "tutorial"
, "repositories":
{ "rules-cc":
{ "repository":
{ "type": "git"
, "branch": "master"
, "commit": "123d8b03bf2440052626151c14c54abce2726e6f"
, "repository": "https://github.com/just-buildsystem/rules-cc.git"
, "subdir": "rules"
}
, "target_root": "tutorial-defaults"
, "rule_root": "rules-cc"
}
, "tutorial":
{ "repository": {"type": "file", "path": "."}
, "bindings": {"rules": "rules-cc", "format": "fmtlib"}
}
, "tutorial-defaults":
{ "repository":
{ "type": "file"
, "path": "./tutorial-defaults"
, "pragma": {"to_git": true}
}
}
, "fmt-targets-layer":
{ "repository":
{ "type": "file"
, "path": "./fmt-layer"
, "pragma": {"to_git": true}
}
}
, "fmtlib":
{ "repository":
{ "type": "git"
, "branch": "master"
, "commit": "b6f4ceaed0a0a24ccf575fab6c56dd50ccf6f1a9"
, "repository": "https://github.com/fmtlib/fmt.git"
}
, "target_root": "fmt-targets-layer"
, "bindings": {"rules": "rules-cc"}
}
}
}
#+END_SRC
Due to changes in the repository configuration, we need to rebuild and the
benefits of the target cache should be visible on the second build:
#+BEGIN_SRC sh
$ just-mr build helloworld
INFO: Requested target is [["@","tutorial","","helloworld"],{}]
INFO: Analysed target [["@","tutorial","","helloworld"],{}]
INFO: Export targets found: 0 cached, 1 uncached, 0 not eligible for caching
INFO: Discovered 7 actions, 3 trees, 0 blobs
INFO: Building [["@","tutorial","","helloworld"],{}].
INFO: Processed 7 actions, 7 cache hits.
INFO: Artifacts built, logical paths are:
helloworld [0ec4e36cfb5f2c3efa0fff789349a46694a6d303:132736:x]
$
$ just-mr build helloworld
INFO: Requested target is [["@","tutorial","","helloworld"],{}]
INFO: Analysed target [["@","tutorial","","helloworld"],{}]
INFO: Export targets found: 1 cached, 0 uncached, 0 not eligible for caching
INFO: Discovered 4 actions, 2 trees, 0 blobs
INFO: Building [["@","tutorial","","helloworld"],{}].
INFO: Processed 4 actions, 4 cache hits.
INFO: Artifacts built, logical paths are:
helloworld [0ec4e36cfb5f2c3efa0fff789349a46694a6d303:132736:x]
$
#+END_SRC
Note that in the second run the export target ~"fmt"~ was taken from cache and
its 3 actions were eliminated, as their result has been recorded to the
high-level target cache during the first run.
** Combining overlay layers for multiple projects
Projects typically depend on multiple external repositories. Creating an overlay
layer for each external repository might unnecessarily clutter up the repository
configuration and the file structure of your repository. One solution to
mitigate this issue is to combine the ~TARGETS~ files of multiple external
repositories in a single overlay layer. To avoid conflicts, the ~TARGETS~ files
can be assigned different file names per repository. As an example, imagine a
common overlay layer with the files ~TARGETS.fmt~ and ~TARGETS.gsl~ for the
repositories ~"fmtlib"~ and ~"gsl-lite"~, respectively:
#+BEGIN_SRC
common-layer
|
+--TARGETS.fmt
+--TARGETS.gsl
+--include
| +--fmt
| | +--TARGETS.fmt
| +--gsl
| +--TARGETS.gsl
|
+--src
+--TARGETS.fmt
#+END_SRC
Such a common overlay layer can be used as the target root for both repositories
with only one difference: the ~"target_file_name"~ field. By specifying this
field, the dispatch where to find the respective target description for each
repository is implemented. For the given example, the following ~repos.json~
defines the overlay ~"common-targets-layer"~, which is used by ~"fmtlib"~ and
~"gsl-lite"~:
#+SRCNAME: repos.json
#+BEGIN_SRC js
{ "main": "tutorial"
, "repositories":
{ "rules-cc":
{ "repository":
{ "type": "git"
, "branch": "master"
, "commit": "123d8b03bf2440052626151c14c54abce2726e6f"
, "repository": "https://github.com/just-buildsystem/rules-cc.git"
, "subdir": "rules"
}
, "target_root": "tutorial-defaults"
, "rule_root": "rules-cc"
}
, "tutorial":
{ "repository": {"type": "file", "path": "."}
, "bindings": {"rules": "rules-cc", "format": "fmtlib"}
}
, "tutorial-defaults":
{ "repository":
{ "type": "file"
, "path": "./tutorial-defaults"
, "pragma": {"to_git": true}
}
}
, "common-targets-layer":
{ "repository":
{ "type": "file"
, "path": "./common-layer"
, "pragma": {"to_git": true}
}
}
, "fmtlib":
{ "repository":
{ "type": "git"
, "branch": "8.1.1"
, "commit": "b6f4ceaed0a0a24ccf575fab6c56dd50ccf6f1a9"
, "repository": "https://github.com/fmtlib/fmt.git"
}
, "target_root": "common-targets-layer"
, "target_file_name": "TARGETS.fmt"
, "bindings": {"rules": "rules-cc"}
}
, "gsl-lite":
{ "repository":
{ "type": "git"
, "branch": "v0.40.0"
, "commit": "d6c8af99a1d95b3db36f26b4f22dc3bad89952de"
, "repository": "https://github.com/gsl-lite/gsl-lite.git"
}
, "target_root": "common-targets-layer"
, "target_file_name": "TARGETS.gsl"
, "bindings": {"rules": "rules-cc"}
}
}
}
#+END_SRC
** Using pre-built dependencies
While building external dependencies from source brings advantages,
most prominently the flexibility to quickly and seamlessly switch
to a different build configuration (production, debug, instrumented
for performance analysis; cross-compiling for a different target
architecture), there are also legitimate reasons to use pre-built
dependencies. The most prominent one is if your project is packaged
as part of a larger distribution. For that reason, just also has (in
~etc/import.prebuilt~) target files for all its dependencies assuming
they are pre-installed. The reason why target files are used at
all for this situation is twofold.
- On the one hand, having a target allows the remaining targets
to not care about where their dependencies come from, or if it
is a build against pre-installed dependencies or not. Also, the
top-level binary does not have to know the linking requirements
of its transitive dependencies. In other words, information stays
where it belongs to and if one target acquires a new dependency,
the information is automatically propagated to all targets using it.
- Still some information is needed to use a pre-installed library
and, as explained, a target describing the pre-installed library
is the right place to collect this information.
- The public header files of the library. By having this explicit,
we do not accumulate directories in the include search path
and hence also properly detect include conflicts.
- The information on how to link the library itself (i.e.,
basically its base name).
- Any dependencies on other libraries that the library might have.
This information is used to obtain the correct linking order
and complete transitive linking arguments while keeping the
description maintainable, as each target still only declares
its direct dependencies.
The target description for a pre-built version of the format
library that was used as an example in this section is shown next;
with our staging mechanism the logical repository it belongs to is
rooted in the ~fmt~ subdirectory of the ~include~ directory of the
ambient system.
#+SRCNAME: etc/import.prebuilt/TARGETS.fmt
#+BEGIN_SRC js
{ "fmt":
{ "type": ["@", "rules", "CC", "library"]
, "name": ["fmt"]
, "stage": ["fmt"]
, "hdrs": [["TREE", null, "."]]
, "private-ldflags": ["-lfmt"]
}
}
#+END_SRC
|