summaryrefslogtreecommitdiff
path: root/doc/tutorial/cross-compiling.md
diff options
context:
space:
mode:
authorKlaus Aehlig <klaus.aehlig@huawei.com>2023-07-17 14:20:33 +0200
committerKlaus Aehlig <klaus.aehlig@huawei.com>2023-07-19 14:08:22 +0200
commitcdd2d5fcf8e4287ebbf45a8cc0a74a15bca4ddad (patch)
tree59799b32bdc5d3cf79e782ca718b811acf81a700 /doc/tutorial/cross-compiling.md
parentc87d2ac032f682f489e23d5950cd3166f3e303a2 (diff)
downloadjustbuild-cdd2d5fcf8e4287ebbf45a8cc0a74a15bca4ddad.tar.gz
Add a tutorial on cross compiling
Diffstat (limited to 'doc/tutorial/cross-compiling.md')
-rw-r--r--doc/tutorial/cross-compiling.md276
1 files changed, 276 insertions, 0 deletions
diff --git a/doc/tutorial/cross-compiling.md b/doc/tutorial/cross-compiling.md
new file mode 100644
index 00000000..88f93021
--- /dev/null
+++ b/doc/tutorial/cross-compiling.md
@@ -0,0 +1,276 @@
+Cros compiling and testing cross-compiled targets
+==================================================
+
+So far, we were always building for the platform on which we were
+building. There are, however, good reasons to build on a different
+one. For example, the other platform could be faster (common theme
+when developing for embedded devices), cheaper, or simply available
+in larger quantities. The standard solution for these kind of
+situations is cross compiling: the binary is completely built on
+one platform, while being intended to run on a different one.
+
+Cross compiling using the CC rules
+----------------------------------
+
+The `C` and `C++` rules that come with the `justbuild` repository
+already have cross-compiling already built into the tool-chain
+definitions for the `"gnu"` and `"clang"` compiler families; as
+different compilers expect different ways to be told about the target
+architecture, cross compiling is not supported for the `"unknown"`
+compiler family.
+
+First, ensure that the required tools and libraries (which also have
+to be available for the target architecture) for cross compiling
+are installed. Depending on the distribution used, this can be
+spread over several packages, often shared by the architecture to
+cross compile to.
+
+Once the required packages are installed, usage is quite forward.
+Let's start a new project.
+``` sh
+$ touch ROOT
+```
+
+We create a file `reposs.template.json` specifying the one local repository.
+``` {.jsonc srcname="repos.template.json"}
+{ "repositories":
+ { "":
+ { "repository": {"type": "file", "path": "."}
+ , "bindings": {"rules": "rules-cc"}
+ }
+ }
+}
+```
+
+The actual `rules-cc` are imported via `just-import-git`.
+``` sh
+$ just-import-git -C repos.template.json -b master --as rules-cc https://github.com/just-buildsystem/rules-cc > repos.json
+```
+
+Let's have `main.cpp` be the canonical Hello-World program.
+``` {.cpp srcname="main.cpp"}
+#include <iostream>
+
+int main() {
+ std::cout << "Hello world!\n";
+ return 0;
+}
+```
+
+Then, our `TARGETS` file describe a simple binary, as usual.
+``` {.jsonc srcname="TARGETS"}
+{ "helloworld":
+ { "type": ["@", "rules", "CC", "binary"]
+ , "name": ["helloworld"]
+ , "srcs": ["main.cpp"]
+ }
+}
+```
+
+As mentioned in the introduction, we need to specify `COMPILER_FAMILY`,
+`OS`, and `ARCH`. So the canonical building for host looks something like
+the following.
+``` sh
+$ just-mr build -D '{"COMPILER_FAMILY": "gnu", "OS": "linux", "ARCH": "x86_64"}'
+INFO: Performing repositories setup
+INFO: Found 21 repositories to set up
+INFO: Setup finished, exec ["just","build","-C","...","-D","{\"COMPILER_FAMILY\": \"gnu\", \"OS\": \"linux\", \"ARCH\": \"x86_64\"}"]
+INFO: Requested target is [["@","","","helloworld"],{"ARCH":"x86_64","COMPILER_FAMILY":"gnu","OS":"linux"}]
+INFO: Analysed target [["@","","","helloworld"],{"ARCH":"x86_64","COMPILER_FAMILY":"gnu","OS":"linux"}]
+INFO: Discovered 2 actions, 1 trees, 0 blobs
+INFO: Building [["@","","","helloworld"],{"ARCH":"x86_64","COMPILER_FAMILY":"gnu","OS":"linux"}].
+INFO: Processed 2 actions, 0 cache hits.
+INFO: Artifacts built, logical paths are:
+ helloworld [0d5754a83c7c787b1c4dd717c8588ecef203fb72:16992:x]
+$
+```
+
+To cross compile, we simply add `TARGET_ARCH`.
+```
+$ just-mr build -D '{"COMPILER_FAMILY": "gnu", "OS": "linux", "ARCH": "x86_64", "TARGET_ARCH": "arm64"}'
+INFO: Performing repositories setup
+INFO: Found 21 repositories to set up
+INFO: Setup finished, exec ["just","build","-C","/home/aehlig/.cache/just/protocol-dependent/generation-0/git-sha1/casf/8c/c5d925c6d230e233f0e89107c8edb22d699063","-D","{\"COMPILER_FAMILY\": \"gnu\", \"OS\": \"linux\", \"ARCH\": \"x86_64\", \"TARGET_ARCH\": \"arm64\"}"]
+INFO: Requested target is [["@","","","helloworld"],{"ARCH":"x86_64","COMPILER_FAMILY":"gnu","OS":"linux","TARGET_ARCH":"arm64"}]
+INFO: Analysed target [["@","","","helloworld"],{"ARCH":"x86_64","COMPILER_FAMILY":"gnu","OS":"linux","TARGET_ARCH":"arm64"}]
+INFO: Discovered 2 actions, 1 trees, 0 blobs
+INFO: Building [["@","","","helloworld"],{"ARCH":"x86_64","COMPILER_FAMILY":"gnu","OS":"linux","TARGET_ARCH":"arm64"}].
+INFO: Processed 2 actions, 0 cache hits.
+INFO: Artifacts built, logical paths are:
+ helloworld [b45459ea3dd36c7531756a4de9aaefd6af30e417:9856:x]
+$
+```
+
+To inspect the different command lines for native and cross compilation,
+we can use `just analyse`.
+``` sh
+$ just-mr analyse -D '{"COMPILER_FAMILY": "gnu", "OS": "linux", "ARCH": "x86_64"}' --dump-actions -
+$ just-mr analyse -D '{"COMPILER_FAMILY": "gnu", "OS": "linux", "ARCH": "x86_64", "TARGET_ARCH": "arm64"}' --dump-actions -
+$ just-mr analyse -D '{"COMPILER_FAMILY": "clang", "OS": "linux", "ARCH": "x86_64"}' --dump-actions -
+$ just-mr analyse -D '{"COMPILER_FAMILY": "clang", "OS": "linux", "ARCH": "x86_64", "TARGET_ARCH": "arm64"}' --dump-actions -
+```
+
+
+Testing in the presence of cross compiling
+------------------------------------------
+
+To understand testing the in the presence of cross compiling, let's
+walk through a simple example. We create a basic test by adding
+a file `test/TARGETS`.
+``` {.jsonc srcname="test/TARGETS"}
+{ "basic":
+ { "type": ["@", "rules", "shell/test", "script"]
+ , "name": ["basic"]
+ , "test": ["basic.sh"]
+ , "deps": [["", "helloworld"]]
+ }
+, "basic.sh":
+ { "type": "file_gen"
+ , "name": "basic.sh"
+ , "data": "./helloworld | grep -i world"
+ }
+}
+```
+
+Now, if we try to run the test by simply specifying the target architecture,
+we find that the binary to be tested is still only built for host.
+``` sh
+$ just-mr analyse --dump-targets - -D '{"COMPILER_FAMILY": "gnu", "OS": "linux", "ARCH": "x86_64", "TARGET_ARCH": "arm64"}' test basic
+INFO: Performing repositories setup
+INFO: Found 21 repositories to set up
+INFO: Setup finished, exec ["just","analyse","-C","...","--dump-targets","-","-D","{\"COMPILER_FAMILY\": \"gnu\", \"OS\": \"linux\", \"ARCH\": \"x86_64\", \"TARGET_ARCH\": \"arm64\"}","test","basic"]
+INFO: Requested target is [["@","","test","basic"],{"ARCH":"x86_64","COMPILER_FAMILY":"gnu","OS":"linux","TARGET_ARCH":"arm64"}]
+INFO: Result of target [["@","","test","basic"],{"ARCH":"x86_64","COMPILER_FAMILY":"gnu","OS":"linux","TARGET_ARCH":"arm64"}]: {
+ "artifacts": {
+ "result": {"data":{"id":"33eb2ebd2ea0d6d335dfc1f948d14d506a19f693","path":"result"},"type":"ACTION"},
+ "stderr": {"data":{"id":"33eb2ebd2ea0d6d335dfc1f948d14d506a19f693","path":"stderr"},"type":"ACTION"},
+ "stdout": {"data":{"id":"33eb2ebd2ea0d6d335dfc1f948d14d506a19f693","path":"stdout"},"type":"ACTION"},
+ "time-start": {"data":{"id":"33eb2ebd2ea0d6d335dfc1f948d14d506a19f693","path":"time-start"},"type":"ACTION"},
+ "time-stop": {"data":{"id":"33eb2ebd2ea0d6d335dfc1f948d14d506a19f693","path":"time-stop"},"type":"ACTION"}
+ },
+ "provides": {
+ },
+ "runfiles": {
+ "basic": {"data":{"id":"c57ed1d00bb6c800ff7ebd1c89519c8481885eda"},"type":"TREE"}
+ }
+ }
+INFO: List of analysed targets:
+{
+ "@": {
+ "": {
+ "": {
+ "helloworld": [{"ADD_CFLAGS":null,"ADD_CXXFLAGS":null,"ADD_LDFLAGS":null,"ARCH":"x86_64","BUILD_POSITION_INDEPENDENT":null,"CC":null,"CFLAGS":null,"COMPILER_FAMILY":"gnu","CXX":null,"CXXFLAGS":null,"DEBUG":null,"ENV":null,"HOST_ARCH":null,"LDFLAGS":null,"OS":"linux","TARGET_ARCH":"x86_64"}]
+ },
+ "test": {
+ "basic": [{"ADD_CFLAGS":null,"ADD_CXXFLAGS":null,"ADD_LDFLAGS":null,"ARCH":"x86_64","ARCH_DISPATCH":null,"BUILD_POSITION_INDEPENDENT":null,"CC":null,"CFLAGS":null,"COMPILER_FAMILY":"gnu","CXX":null,"CXXFLAGS":null,"DEBUG":null,"ENV":null,"HOST_ARCH":null,"LDFLAGS":null,"OS":"linux","RUNS_PER_TEST":null,"TARGET_ARCH":"arm64","TEST_ENV":null,"TIMEOUT_SCALE":null}],
+ "basic.sh": [{}]
+ }
+ },
+ "rules-cc": {
+ "CC": {
+ "defaults": [{"ARCH":"x86_64","COMPILER_FAMILY":"gnu","DEBUG":null,"HOST_ARCH":null,"OS":"linux","TARGET_ARCH":"x86_64"}]
+ }
+ },
+ "rules-cc/just/rules": {
+ "CC": {
+ "defaults": [{"ARCH":"x86_64","COMPILER_FAMILY":"gnu","DEBUG":null,"HOST_ARCH":null,"OS":"linux","TARGET_ARCH":"x86_64"}],
+ "gcc": [{"ARCH":"x86_64","HOST_ARCH":null,"OS":"linux","TARGET_ARCH":"x86_64"}],
+ "toolchain": [{"ARCH":"x86_64","COMPILER_FAMILY":"gnu","HOST_ARCH":null,"OS":"linux","TARGET_ARCH":"x86_64"}]
+ }
+ }
+ }
+}
+INFO: Target tainted ["test"].
+$
+```
+
+The reason is that a test actually has to run the created binary
+and that requires a build enviroment of the target architecture.
+So, if not being told how to obtain such an environemt, they carry
+out the test in the best mannor they can, i.e., by transitioning
+everything to host. So, in order to properly test the cross-compiled
+binary, we need to do two things.
+
+- We need to setup remote execution on the correct architecture,
+ either by buying the apropriate hardware, or by running an emulator.
+- We need to tell `justbuild` on how to reach that endpoint.
+
+To continue the example, let's say we set up an `arm64` machine,
+e.g., a Raspberry Pi, in the local network. On that machine, we can
+simply run a single-node execution service using `just execute`;
+note that the `just` binary used there has to be an `arm64` binary,
+e.g., obtained by cross compiling.
+
+The next step is to tell `justbuild` how to reach that machine;
+as we only want to use it for certain actions we can't simply
+set it as (default) remote-execution endpoint (specified by the
+`-r` option). Instead we crate a file `dispatch.json`.
+
+``` {.jsonc srcname="dispatch.json"}
+[[{"runner": "arm64-worker"}, "10.204.62.67:9999"]]
+```
+This file contains a list of lists pairs (lists of length 2) with the
+first entry a map of remote-execution properties and the second entry
+a remote-execution endpoint. For each action, if the remote-execution
+properties of that action match the first component of a pair the
+second component of the first matching pair is taken as remote-execution
+endpoint instead of the default endpoint specified on the command
+line. Here "matching" means that all values in the domain of the
+map have the same value in the remote-execution properties of the
+action (in particular, `{}` matches everything); so more specific
+dispatches have to be specified earlier in the list. In our case,
+we have a single endpoint in our private network that we should
+use whenever the propterty `"runnner"` has value `"arm64-worker"`.
+The IP/port specification might differ in your setup. The path to
+this file is passed by the `--endpoint-configuration` option.
+
+Next, we have to tell the test rule, that we actually do have a
+runner for the `arm64` architecture. The rule expects this in the
+`ARCH_DISPATCH` configuration variable that you might have seen in
+the list of analysed targets earlier. It is expected to be a map
+from architectures to remote-execution properties ensuring the action
+will be run on that architecture. If the rules finds a non-empty
+entry for the specified target architecture, it assumes a runner is
+available and will run the test action with those properties; for
+the compilation steps, still cross compiling is used. In our case,
+we set `ARCH_DISPATCH` to `{"arm64": {"runner": "arm64-worker"}}`.
+
+Finally, we have to provide the credentials needed for mutual
+authentication with the remote-execution endpoint by setting `--tls-*`
+options appropriately. `just` assumes that the same credentials
+can be used all remote-execution endpoints involved. In our example
+we're building locally (where the build process starts the actions
+itself) which does not require any credentials; nevertheless, it
+still accepts any credentials provided.
+
+``` sh
+$ just-mr build -D '{"COMPILER_FAMILY": "gnu", "OS": "linux", "ARCH": "x86_64", "TARGET_ARCH": "arm64", "ARCH_DISPATCH": {"arm64": {"runner": "arm64-worker"}}}' --endpoint-configuration dispatch.json --tls-ca-cert ca.crt --tls-client-cert client.crt --tls-client-key client.key test basic
+INFO: Performing repositories setup
+INFO: Found 21 repositories to set up
+INFO: Setup finished, exec ["just","build","-C","...","-D","{\"COMPILER_FAMILY\": \"gnu\", \"OS\": \"linux\", \"ARCH\": \"x86_64\", \"TARGET_ARCH\": \"arm64\", \"ARCH_DISPATCH\": {\"arm64\": {\"runner\": \"arm64-worker\"}}}","--endpoint-configuration","dispatch.json","--tls-ca-cert","ca.crt","--tls-client-cert","client.crt","--tls-client-key","client.key","test","basic"]
+INFO: Requested target is [["@","","test","basic"],{"ARCH":"x86_64","ARCH_DISPATCH":{"arm64":{"runner":"arm64-worker"}},"COMPILER_FAMILY":"gnu","OS":"linux","TARGET_ARCH":"arm64"}]
+INFO: Analysed target [["@","","test","basic"],{"ARCH":"x86_64","ARCH_DISPATCH":{"arm64":{"runner":"arm64-worker"}},"COMPILER_FAMILY":"gnu","OS":"linux","TARGET_ARCH":"arm64"}]
+INFO: Target tainted ["test"].
+INFO: Discovered 3 actions, 3 trees, 1 blobs
+INFO: Building [["@","","test","basic"],{"ARCH":"x86_64","ARCH_DISPATCH":{"arm64":{"runner":"arm64-worker"}},"COMPILER_FAMILY":"gnu","OS":"linux","TARGET_ARCH":"arm64"}].
+INFO: Processed 3 actions, 2 cache hits.
+INFO: Artifacts built, logical paths are:
+ result [7ef22e9a431ad0272713b71fdc8794016c8ef12f:5:f]
+ stderr [e69de29bb2d1d6434b8b29ae775ad8c2e48c5391:0:f]
+ stdout [cd0875583aabe89ee197ea133980a9085d08e497:13:f]
+ time-start [298a717bc8ba9b7e681a6d789104a1204ebe75b8:11:f]
+ time-stop [298a717bc8ba9b7e681a6d789104a1204ebe75b8:11:f]
+ (1 runfiles omitted.)
+INFO: Target tainted ["test"].
+$
+```
+
+The resulting command line might look complicated, but the
+authentication-related options, as well as the dispatch-related
+otpions (including setting `ARGUMENTS_DISPATCH` via `-D`) can simply
+be set in the `"just args"` entry of the `.just-mrrc` file.
+
+When inspecting the result, we can use `just install-cas` as usual,
+without any special arguments. Whenever dispatching an action to
+a non-default end point, `just` will take care of syncing back the
+artifacts to the default CAS.