diff options
Diffstat (limited to 'doc/tutorial/hello-world.md')
-rw-r--r-- | doc/tutorial/hello-world.md | 379 |
1 files changed, 379 insertions, 0 deletions
diff --git a/doc/tutorial/hello-world.md b/doc/tutorial/hello-world.md new file mode 100644 index 00000000..9af68f07 --- /dev/null +++ b/doc/tutorial/hello-world.md @@ -0,0 +1,379 @@ +Building C++ Hello World +======================== + +*justbuild* is a true language-agnostic (there are no more-equal +languages) and multi-repository build system. As a consequence, +high-level concepts (e.g., C++ binaries, C++ libraries, etc.) are not +hardcoded built-ins of the tool, but rather provided via a set of rules. +These rules can be specified as a true dependency to your project like +any other external repository your project might depend on. + +Setting up the Multi-Repository Configuration +--------------------------------------------- + +To build a project with multi-repository dependencies, we first need to +provide a configuration that declares the required repositories. Before +we begin, we need to declare where the root of our workspace is located +by creating an empty file `ROOT`: + +``` sh +$ touch ROOT +``` + +Second, we also need to create the multi-repository configuration +`repos.json` in the workspace root: + +``` {.jsonc srcname="repos.json"} +{ "main": "tutorial" +, "repositories": + { "rules-cc": + { "repository": + { "type": "git" + , "branch": "master" + , "commit": "123d8b03bf2440052626151c14c54abce2726e6f" + , "repository": "https://github.com/just-buildsystem/rules-cc.git" + , "subdir": "rules" + } + } + , "tutorial": + { "repository": {"type": "file", "path": "."} + , "bindings": {"rules": "rules-cc"} + } + } +} +``` + +In that configuration, two repositories are defined: + +1. The `"rules-cc"` repository located in the subdirectory `rules` of + [just-buildsystem/rules-cc:123d8b03bf2440052626151c14c54abce2726e6f](https://github.com/just-buildsystem/rules-cc/tree/123d8b03bf2440052626151c14c54abce2726e6f), + which contains the high-level concepts for building C/C++ binaries + and libraries. + +2. The `"tutorial"` repository located at `.`, which contains the + targets that we want to build. It has a single dependency, which is + the *rules* that are needed to build the target. These rules are + bound via the open name `"rules"` to the just created repository + `"rules-cc"`. In this way, the entities provided by `"rules-cc"` can + be accessed from within the `"tutorial"` repository via the + fully-qualified name `["@", "rules", "<module>", "<name>"]`; + fully-qualified names (for rules, targets to build (like libraries, + binaries), etc) are given by a repository name, a path specifying a + directory within that repository (the "module") where the + specification file is located, and a symbolic name (i.e., an + arbitrary string that is used as key in the specification). + +The final repository configuration contains a single `JSON` object with +the key `"repositories"` referring to an object of repository names as +keys and repository descriptions as values. For convenience, the main +repository to pick is set to `"tutorial"`. + +Description of the helloworld target +------------------------------------ + +For this tutorial, we want to create a target `helloworld` that produces +a binary from the C++ source `main.cpp`. To define such a target, create +a `TARGETS` file with the following content: + +``` {.jsonc srcname="TARGETS"} +{ "helloworld": + { "type": ["@", "rules", "CC", "binary"] + , "name": ["helloworld"] + , "srcs": ["main.cpp"] + } +} +``` + +The `"type"` field refers to the rule `"binary"` from the module `"CC"` +of the `"rules"` repository. This rule additionally requires the string +field `"name"`, which specifies the name of the binary to produce; as +the generic interface of rules is to have fields either take a list of +strings or a list of targets, we have to specify the name as a list +(this rule will simply concatenate all strings given in this field). +Furthermore, at least one input to the binary is required, which can be +specified via the target fields `"srcs"` or `"deps"`. In our case, the +former is used, which contains our single source file (files are +considered targets). + +Now, the last file that is missing is the actual source file `main.cpp`: + +``` {.cpp srcname="main.cpp"} +#include <iostream> + +int main() { + std::cout << "Hello world!\n"; + return 0; +} +``` + +Building the helloworld target +------------------------------ + +To build the `helloworld` target, we need specify it on the `just-mr` +command line: + +``` sh +$ just-mr build helloworld +INFO: Requested target is [["@","tutorial","","helloworld"],{}] +INFO: Analysed target [["@","tutorial","",helloworld"],{}] +INFO: Export targets found: 0 cached, 0 uncached, 0 not eligible for caching +INFO: Discovered 2 actions, 1 trees, 0 blobs +INFO: Building [["@","helloworld","","helloworld"],{}]. +INFO: Processed 2 actions, 0 cache hits. +INFO: Artifacts built, logical paths are: + helloworld [b5cfca8b810adc4686f5cac00258a137c5d4a3ba:17088:x] +$ +``` + +Note that the target is taken from the `tutorial` repository, as it +specified as the main repository in `repos.json`. If targets from other +repositories should be build, the repository to use must be specified +via the `--main` option. + +`just-mr` reads the repository configuration, fetches externals (if +any), generates the actual build configuration, and stores it in its +cache directory (by default under `$HOME/.cache/just`). Afterwards, the +generated configuration is used to call the `just` binary, which +performs the actual build. + +Note that these two programs, `just-mr` and `just`, can also be run +individually. To do so, first run `just-mr` with `setup` and capture the +path to the generated build configuration from stdout by assigning it to +a shell variable (e.g., `CONF`). Afterwards, `just` can be called to +perform the actual build by explicitly specifying the configuration file +via `-C`: + +``` sh +$ CONF=$(just-mr setup tutorial) +$ just build -C $CONF helloworld +``` + +Note that `just-mr` only needs to be run the very first time and only +once again whenever the `repos.json` file is modified. + +By default, the BSD-default compiler front-ends (which are also defined +for most Linux distributions) `cc` and `c++` are used for C and C++ +(variables `"CC"` and `"CXX"`). If you want to temporarily use different +defaults, you can use `-D` to provide a JSON object that sets different +default variables. For instance, to use Clang as C++ compiler for a +single build invocation, you can use the following command to provide an +object that sets `"CXX"` to `"clang++"`: + +``` sh +$ just-mr build helloworld -D'{"CXX":"clang++"}' +INFO: Requested target is [["@","tutorial","","helloworld"],{"CXX":"clang++"}] +INFO: Analysed target [["@","tutorial","","helloworld"],{"CXX":"clang++"}] +INFO: Export targets found: 0 cached, 0 uncached, 0 not eligible for caching +INFO: Discovered 2 actions, 1 trees, 0 blobs +INFO: Building [["@","tutorial","","helloworld"],{"CXX":"clang++"}]. +INFO: Processed 2 actions, 0 cache hits. +INFO: Artifacts built, logical paths are: + helloworld [b8cf7b8579d9dc7172b61660139e2c14521cedae:16944:x] +$ +``` + +Defining project defaults +------------------------- + +To define a custom set of defaults (toolchain and compile flags) for +your project, you need to create a separate file root for providing +required `TARGETS` file, which contains the `"defaults"` target that +should be used by the rules. This file root is then used as the *target +root* for the rules, i.e., the search path for `TARGETS` files. In this +way, the description of the `"defaults"` target is provided in a +separate file root, to keep the rules repository independent of these +definitions. + +We will call the new file root `tutorial-defaults` and need to create a +module directory `CC` in it: + +``` sh +$ mkdir -p ./tutorial-defaults/CC +``` + +In that module, we need to create the file +`tutorial-defaults/CC/TARGETS` that contains the target `"defaults"` and +specifies which toolchain and compile flags to use; it has to specify +the complete toolchain, but can specify a `"base"` toolchain to inherit +from. In our case, we don't use any base, but specify all the required +fields directly. + +``` {.jsonc srcname="tutorial-defaults/CC/TARGETS"} +{ "defaults": + { "type": ["CC", "defaults"] + , "CC": ["cc"] + , "CXX": ["c++"] + , "CFLAGS": ["-O2", "-Wall"] + , "CXXFLAGS": ["-O2", "-Wall"] + , "AR": ["ar"] + , "PATH": ["/bin", "/usr/bin"] + } +} +``` + +To use the project defaults, modify the existing `repos.json` to reflect +the following content: + +``` {.jsonc srcname="repos.json"} +{ "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"} + } + , "tutorial-defaults": + { "repository": {"type": "file", "path": "./tutorial-defaults"} + } + } +} +``` + +Note that the `"defaults"` target uses the rule `["CC", "defaults"]` +without specifying any external repository (e.g., +`["@", "rules", ...]`). This is because `"tutorial-defaults"` is not a +full-fledged repository but merely a file root that is considered local +to the `"rules-cc"` repository. In fact, the `"rules-cc"` repository +cannot refer to any external repository as it does not have any defined +bindings. + +To rebuild the project, we need to rerun `just-mr` (note that due to +configuration changes, rerunning only `just` would not suffice): + +``` sh +$ just-mr build helloworld +INFO: Requested target is [["@","tutorial","","helloworld"],{}] +INFO: Analysed target [["@","tutorial","","helloworld"],{}] +INFO: Export targets found: 0 cached, 0 uncached, 0 not eligible for caching +INFO: Discovered 2 actions, 1 trees, 0 blobs +INFO: Building [["@","tutorial","","helloworld"],{}]. +INFO: Processed 2 actions, 0 cache hits. +INFO: Artifacts built, logical paths are: + helloworld [487dc9e47b978877ed2f7d80b3395ce84b23be92:16992:x] +$ +``` + +Note that the output binary may have changed due to different defaults. + +Modeling target dependencies +---------------------------- + +For demonstration purposes, we will separate the print statements into a +static library `greet`, which will become a dependency to our binary. +Therefore, we create a new subdirectory `greet` with the files +`greet/greet.hpp`: + +``` {.cpp srcname="greet/greet.hpp"} +#include <string> + +void greet(std::string const& s); +``` + +and `greet/greet.cpp`: + +``` {.cpp srcname="greet/greet.cpp"} +#include "greet.hpp" +#include <iostream> + +void greet(std::string const& s) { + std::cout << "Hello " << s << "!\n"; +} +``` + +These files can now be used to create a static library `libgreet.a`. To +do so, we need to create the following target description in +`greet/TARGETS`: + +``` {.jsonc srcname="greet/TARGETS"} +{ "greet": + { "type": ["@", "rules", "CC", "library"] + , "name": ["greet"] + , "hdrs": ["greet.hpp"] + , "srcs": ["greet.cpp"] + , "stage": ["greet"] + } +} +``` + +Similar to `"binary"`, we have to provide a name and source file. +Additionally, a library has public headers defined via `"hdrs"` and an +optional staging directory `"stage"` (default value `"."`). The staging +directory specifies where the consumer of this library can expect to +find the library's artifacts. Note that this does not need to reflect +the location on the file system (i.e., a full-qualified path like +`["com", "example", "utils", "greet"]` could be used to distinguish it +from greeting libraries of other projects). The staging directory does +not only affect the main artifact `libgreet.a` but also it's +*runfiles*, a second set of artifacts, usually those a consumer needs to +make proper use the actual artifact; in the case of a library, the +runfiles are its public headers. Hence, the public header will be staged +to `"greet/greet.hpp"`. With that knowledge, we can now perform the +necessary modifications to `main.cpp`: + +``` {.cpp srcname="main.cpp"} +#include "greet/greet.hpp" + +int main() { + greet("Universe"); + return 0; +} +``` + +The target `"helloworld"` will have a direct dependency to the target +`"greet"` of the module `"greet"` in the top-level `TARGETS` file: + +``` {.jsonc srcname="TARGETS"} +{ "helloworld": + { "type": ["@", "rules", "CC", "binary"] + , "name": ["helloworld"] + , "srcs": ["main.cpp"] + , "private-deps": [["greet", "greet"]] + } +} +``` + +Note that there is no need to explicitly specify `"greet"`'s public +headers here as the appropriate artifacts of dependencies are +automatically added to the inputs of compile and link actions. The new +binary can be built with the same command as before (no need to rerun +`just-mr`): + +``` sh +$ just-mr build helloworld +INFO: Requested target is [["@","tutorial","","helloworld"],{}] +INFO: Analysed target [["@","tutorial","","helloworld"],{}] +INFO: Export targets found: 0 cached, 0 uncached, 0 not eligible for caching +INFO: Discovered 4 actions, 2 trees, 0 blobs +INFO: Building [["@","tutorial","","helloworld"],{}]. +INFO: Processed 4 actions, 0 cache hits. +INFO: Artifacts built, logical paths are: + helloworld [2b81e3177afc382452a2df9f294d3df90a9ccaf0:17664:x] +$ +``` + +To only build the static library target `"greet"` from module `"greet"`, +run the following command: + +``` sh +$ just-mr build greet greet +INFO: Requested target is [["@","tutorial","greet","greet"],{}] +INFO: Analysed target [["@","tutorial","greet","greet"],{}] +INFO: Export targets found: 0 cached, 0 uncached, 0 not eligible for caching +INFO: Discovered 2 actions, 1 trees, 0 blobs +INFO: Building [["@","tutorial","greet","greet"],{}]. +INFO: Processed 2 actions, 2 cache hits. +INFO: Artifacts built, logical paths are: + greet/libgreet.a [83ed406e21f285337b0c9bd5011f56f656bba683:2992:f] + (1 runfiles omitted.) +$ +``` |