Building a Go Mod CLI to generate dependency trees
Tags: go
Published: 2021-06-15 21:49:22


So, picture the scene, you have a Go project and then get a security alert saying a vulnerability has been detected. You look at your go.mod to see if you're using it and it's nowhere to be seen, but then you see it in your go.sum. How do you find where it is coming from?

Sound familiar? Well that's the situation I found myself in last week. So obviously, I tried to find a tool that would do it but couldn't find one, so like any programmer would do, I decided to build one.

The tool can be found [here](https://github.com/kapilpau/go-mod-dependency-tree), with amd64 binaries for Linux, Windows and Mac available in the releases.

## Installation

![installation](https://storage.googleapis.com/markdium-image/b2297d6dfefa5b226a886b6d452c461f)

To install, simply download the appropriate binary for your operating system from the latest release, at the time of writing this, it was [v1.2.0](https://github.com/kapilpau/go-mod-dependency-tree/releases/tag/v1.2.0). Once downloaded, make sure the binary is executable and moved to be in your PATH, with the name `go-tree`.

## How to use it

![usage](https://storage.googleapis.com/markdium-image/c8ff1bb5a6b4dec23822bfa2970433d7)

The tool only works with go mod projects, and requires the environment variable `GOPATH` to be set (by default, it will be `$HOME/go`). You can either run it from within the root directory of your go project (where you go.mod is located), or you can use the `-modulePath` flag to pass in a relative or absolute path to the project you want to scan.

```
$ go-tree -modulePath $GOPATH/src/path/to/module/to/scan
```

You can also use the `-maxDepth` flag to set the maximum recursion level, i.e. how far down the tree to scan. The options are either -1 or an integer above 0, -1 is to indicate no limit and is the default value.

```
$ go-tree -maxDepth -1
```

The final flag you can use is the `-find` flag, which is the whole reason this tool exists. If you specify this flag with the module you would like to find, it will print the full tree for all of the instances of that package in the dependency tree. Note that if you use `-find`, the `-maxDepth` will be ignored.

```
$ go-tree -find github.com/kapilpau/go-mod-dependency-tree
```
None of these flags are required.

## How it works

![working](https://storage.googleapis.com/markdium-image/b68ba130ff3b3f7f1dcd648532948105)

As this is a single function cli tool, the whole code is contained within a [single file](https://github.com/kapilpau/go-mod-dependency-tree/blob/main/dependency-tree.go). The code has two main pathways, both of which work in a similar way.

The first is the straight tree dump, i.e. where you don't specify the `-find` flag. This route recursively searches each dependency's go.mod to find all of the dependencies for that module and prints out the name and version of the dependency. For this, we read in the go.mod file for your project, find all of the modules in the `requires` section and look for the module in the `src` or `pkg` folders, in your `GOPATH`.

If they have exist and have a `go.mod` file, we continue through the chains and look for their dependencies. If we can't find a dependency in either location, or it doesn't have a `go.mod` file, we end that branch there and move on.

The other pathway is for when a user is searching for a specific module in the chain. In this case, it is slightly more complicated as we need to decide what to print out later on. For this, we use a custom tree struct, named `dependencyChain`. This struct has two fields, `module` (the name of the module currently being scanned) and `children` (the dependencies of the current module).

We do a similar recursive search to the one detailed above, however, rather than just simply printing out the values as we find them, we have to perform head recursion, so we can look at the outputs of the later recurssions before deciding what to do. So, if we find the module we're looking for, we end the branch of the tree there, as it would be wasteful to carry on, and we populate the `dependencyChain` object to pass back. Then, when we have the list of `dependencyChain`s for each module, we check the size of the `children` field and if it is not empty, we pass it upwards, otherwise we ignore it. The reason we do this check is because we only want to see the branches that end in the module we're looking for.

Once we have completed this head recursive search, we perform a tail recursive print, to loop through the `children` of each `dependencyChain` and display it as a tree.

If the module you are looking for does not exist in the chain, or it cannot be found (as it may be in a non-go mod enabled project), then a message is printed out at the end to say so.

## Lessons learned

![learned](https://storage.googleapis.com/markdium-image/4e6b1cbf958653686ee4d1351510f53f)

I learned a lot from this project, namely how easy it is to create and build cli binaries in Go, even being able to build for different operating systems and architectures, without having to natively use an instance of them. This is definitely just the first of many more to come.

I got to apply the principles of recursion, that we spent so long learning at uni, to a real-life scenario.

I gained a deeper understanding of how Go stores dependencies, and where to find them when I need them.