> ## Documentation Index
> Fetch the complete documentation index at: https://site.aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Running local tools installed by Bazel

> Manage command-line tools efficiently with Bazel. Streamline workflows, ensure version consistency, and boost development productivity with this approach.

export const BlogPost = ({title, date, authors, tags, image, children}) => {
  const tagList = tags ? tags.split(", ").filter(Boolean) : [];
  const tagSlug = t => t.toLowerCase().replace(/&/g, "").replace(/\+/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
  const formattedDate = date ? new Date(date + "T00:00:00").toLocaleDateString("en-US", {
    year: "numeric",
    month: "long",
    day: "numeric"
  }) : "";
  return <section className="w-full flex justify-center px-4 py-12 md:py-16">
      <div style={{
    maxWidth: "800px",
    width: "100%"
  }}>
        {image && (typeof image === "string" ? <img noZoom src={image} alt={title} className="w-full rounded-xl mb-8" style={{
    maxHeight: "400px",
    objectFit: "cover"
  }} /> : <div className="blog-post-hero-image">{image}</div>)}
        <h1 className="text-3xl md:text-4xl font-bold text-zinc-900 dark:text-white">
          {title}
        </h1>
        <div className="flex flex-wrap items-center gap-3 mt-4 text-sm text-zinc-500 dark:text-zinc-400">
          {authors && <span>{authors}</span>}
          {authors && formattedDate && <span>·</span>}
          {formattedDate && <span>{formattedDate}</span>}
        </div>
        {tagList.length > 0 && <div className="flex flex-wrap gap-2 mt-3">
            {tagList.map(tag => <a key={tag} href={"/blog/tags/" + tagSlug(tag)} className="px-2 py-0.5 rounded-full text-xs bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-blue-100 dark:hover:bg-blue-900/40 hover:text-blue-700 dark:hover:text-blue-300 transition">
                {tag}
              </a>)}
          </div>}
        <hr className="my-8 border-zinc-200 dark:border-zinc-700" />
        <div className="prose dark:prose-invert max-w-none">{children}</div>
      </div>
    </section>;
};

export const MarketingPage = () => <div className="marketing-page-marker" style={{
  display: "none"
}} />;

export const Section = ({children, className = "", gray = false, dark = false, id}) => <section id={id} className={`w-full flex justify-center px-4 py-16 md:py-24 ${gray ? "bg-gray-50 dark:bg-zinc-900" : dark ? "bg-zinc-900 dark:bg-zinc-950" : ""} ${className}`}>
    <div className="w-full" style={{
  maxWidth: "1140px"
}}>
      {children}
    </div>
  </section>;

<MarketingPage />

<BlogPost title="Running local tools installed by Bazel" date="2024-04-05" authors="Alex Eagle" tags="Bazel, Aspect CLI">
  <img noZoom src="https://mintcdn.com/aspectbuild/x1L7Iep716jCyJVo/images/blog/hashnode/hn-166ab5a701.jpeg?fit=max&auto=format&n=x1L7Iep716jCyJVo&q=85&s=892906b71a45590bed045f9e206153cb" alt="" className="blog-post-cover" width="1600" height="1068" data-path="images/blog/hashnode/hn-166ab5a701.jpeg" />

  It's a common pattern that developers in your repo are expected to run some command-line tools as part of interacting with the code. For example, maybe they need to run `terraform plan` when working with Terraform files.

  However, it's a non-stop hassle to get the *right version* of Terraform installed on everyone's machine. There's always someone asking in Slack "hey it seems like there's some unsupported syntax in this code can you help me" and after some debugging you realize they installed the tool three years ago and that version isn't supported by the current code.

  Bazel is great for this! However, the pattern to set it up has always been fiddly. I recently had a client who needed a better answer, so here it is!

  ## Fetching the tools

  This post assumes the tools you need to run are statically-linked as an executable. If you're instead meant to `gem install` or `pip install` or `npm install` the tool, then this pattern is too simple to work.

  I recently contributed to a nice [utility ruleset](https://github.com/theoremlp/rules_multitool) for Bazel that gives you a JSON-format lockfile for the tools you want to fetch on their various platforms. Continuing with the `terraform` example, we could have a `tools/multitool.lock.json` file containing:

  ```json theme={null}
  {
    "$schema": "https://raw.githubusercontent.com/theoremlp/rules_multitool/main/lockfile.schema.json",
    "terraform": {
      "binaries": [
        {
          "kind": "archive",
          "url": "https://releases.hashicorp.com/terraform/1.7.5/terraform_1.7.5_darwin_arm64.zip",
          "sha256": "99c4d4feafb0183af2f7fbe07beeea6f83e5f5a29ae29fee3168b6810e37ff98",
          "os": "macos",
          "cpu": "arm64",
          "file": "terraform"
        },
        {
          "kind": "archive",
          "url": "https://releases.hashicorp.com/terraform/1.7.5/terraform_1.7.5_darwin_amd64.zip",
          "sha256": "0eaf64e28f82e2defd06f7a6f3187d8cea03d5d9fcd2af54f549a6c32d6833f7",
          "os": "macos",
          "cpu": "x86_64",
          "file": "terraform"
        },
        {
          "kind": "archive",
          "url": "https://releases.hashicorp.com/terraform/1.7.5/terraform_1.7.5_linux_amd64.zip",
          "sha256": "3ff056b5e8259003f67fd0f0ed7229499cfb0b41f3ff55cc184088589994f7a5",
          "os": "linux",
          "cpu": "x86_64",
          "file": "terraform"
        },
        {
          "kind": "archive",
          "url": "https://releases.hashicorp.com/terraform/1.7.5/terraform_1.7.5_linux_arm64.zip",
          "sha256": "08631c385667dd28f03b3a3f77cb980393af4a2fcfc2236c148a678ad9150c8c",
          "os": "linux",
          "cpu": "arm64",
          "file": "terraform"
        }
      ]
    }
  }
  ```

  > NB: I'd love to work on some tooling to generate this file for an arbitrary list of released binaries. Please fund us!

  Now we just need to point Bazel at this JSON lockfile to get the tools installed. Paste the `MODULE.bazel` or `WORKSPACE` snippet from the latest release: [https://github.com/theoremlp/rules\_multitool/releases](https://github.com/theoremlp/rules_multitool/releases)

  ## Making it nice for developers

  The desired end-user experience shouldn't involve Bazel and should be as simple as running the tool in your terminal:

  ```bash theme={null}
  $ ./tools/terraform -version
  Terraform v1.7.5
  on linux_amd64
  ```

  First, as always, is to find a workaround for a Bazel footgun: `bazel run` cannot possibly behave correctly because it will change the working directory. (It's designed with the assumption that you'd only want to run programs you wrote yourself...) If the working directory is wrong, then we run `./tools/terraform plan` it can't found the Terraform files: `Error: No configuration files`. Oops! Come visit [https://github.com/bazelbuild/bazel/issues/3325](https://github.com/bazelbuild/bazel/issues/3325) and give it a thumbs up.

  You might see `—-run_under` suggested for this purpose, with a simple value to change the working directory like `—-run_under=”cd $PWD &&”`. However this is broken under Bazel as well, as it discards the analysis cache. Oops! Come visit [https://github.com/bazelbuild/bazel/issues/10782](https://github.com/bazelbuild/bazel/issues/10782) and ask for this to happen only when the value of `run_under` looks like a label.

  To work around these issues, we're forced to use a mix of `bazel build` to fetch the tool into `bazel-out`, `bazel cquery` to know what path it's installed under, and `bazel info` to convert that relative path. Sigh.

  Create a helper script in `tools/_multirun_under_cwd.sh` , make it executable and add this content:

  ```bash theme={null}
  #!/bin/sh
  target="@multitool//tools/$(basename "$0")"
  bazel 2>/dev/null build "$target" && exec $(bazel info execution_root)/$(bazel 2>/dev/null cquery --output=files "$target") "$@"
  ```

  This script assumes that the name of the program being run (`basename "$0"`) is the name we gave in the JSON file earlier `"terraform"`. So the final step is just a symlink:

  `cd tools; ln -s _multitool_run_under_cwd.sh terraform`

  Now the tool behaves the way users expect, running in whatever working directory they're in. Let's try under our `/infrastructure` folder:

  ```bash theme={null}
  /infrastructure$ ../tools/terraform init

  Initializing the backend...
  Initializing modules...
  ```

  Nice! Now you can repeat this for other tools you need to run, just by adding an entry to the JSON file, and the corresponding symlink in the `tools` folder.

  **UPDATE May 2024**: rules\_multitool now has a dedicated `cwd` target: [https://github.com/theoremlp/rules\\\_multitool/pull/29](https://github.com/theoremlp/rules\\_multitool/pull/29) However the pattern above is still useful if you fetched your tools in some other way.
</BlogPost>
