2024/02/15: Properly extending PATH

On the build system I'm working on, most users, as well as the tutorial, assume that "the standard tools" (like cp, sh) are somehow magically found. For many build systems this is true, as PATH is somehow inherited from the environment. justbuild, however, executes every action in the specified environment—and in most cases, this is empty. There is a different reason why it works nevertheless, at least on most sytems: this assumption on the presence of those tools is typically made inside a shell invocation, and on most systems sh starts with a "sensible default" for PATH. Try


env -i sh -c 'echo $PATH'

on various systems. On most, you'll find a path good enough to find those standard tools; on nix, however, this is not the case and, in fact, the path you really want is not "standard" in any way, but more something like /home/aehlig/.nix-profile/bin.

There is also another mechanism that influences how actions are executed: the launcher; in order to make sure that argv[0] is interpreted in the specified environment (in particular, searched from the specified path), the command actually executed is the specifed one prefixed by the launcher, which defaults to ["env", "--"]. Again, depending on the system, there are certain paths env searches anyway, if PATH is unset.

Of course, the launcher is configurable, including for fetch actions. And this is also the way to provide the desired default path. In the most simple case, you set (in your .just-mrrc) the launcher to ["env", "PATH=/home/aehlig/.nixprofile/bin"] or similar (depending on what your path for the "local environment" is). While this is technically incorrect (the PATH set by the action is unconditionally ignored), it works surprisingly well—as already mentioned, the environment is empty in most cases anyway.

Still, I decided, things should be done correctly. That is, the value of PATH, if provided, is taken and only extended by the specified default. After all, it's just a default and the path set by the action should be searched in first. The result is the following simple C++20 program (which can freely be used under the Apache-2.0 license); its first argument is the value PATH should be extended with, the rest is the argument vector. Hence it follows the scheme required for a launcher. After computing the extended value of PATH, it execs to /usr/bin/env as the actual launcher—after all, the original argv[0] should be interpreted in the then available path.


#include <algorithm>
#include <cstdlib>
#include <string>
#include <unistd.h>
#include <vector>

int main(int argc, char* argv[]) {
  constexpr int kForkFailed = 65;
  constexpr int kPrerequisiteError = 97;
  auto kEnv = std::string{"/usr/bin/env"};

  if (argc < 3) {
    return kPrerequisiteError;
  }

  // compute PATH
  auto path = std::string{argv[1]};
  auto const* env_path = std::getenv("PATH");
  if (env_path && (*env_path != '\0')) {
    path = std::string{env_path} + std::string{":"} + path;
  }
  auto path_arg = std::string{"PATH="} + path;

  // create new argument vector
  std::vector<char*> nargv{};
  nargv.reserve(argc+3);
  nargv.push_back(kEnv.data());
  nargv.push_back(path_arg.data());
  for (int i=2; i < argc; i++) {
    nargv.push_back(argv[i]);
  }
  nargv.push_back(nullptr);

  // exec
  (void) execvp(nargv[0], static_cast<char* const*>(nargv.data()));
  return kForkFailed;
}
download