Skip to main content

A Makefile for Golang

·6 mins

I created a GNU Makefile to facilitate building my Go projects. It is composed of two files, Makefile and golang.mk, which are available at the top of my Golearn project in Gitlab.

The Makefile has two core targets: all, the default, and tests. The all target builds all source code in a project, and the tests target runs all associated Go tests. There are two other targets, clean and cleanall, to clean command binaries and all binaries (.a libraries included) respectively, that may have been created before.

The Makefile uses default values to capture the way the project is organized. They can be modified as required to accommodate different directory structures.

The Default Project Layout #

The basic structure of a project is illustrated in the following example:

$GOPATH
├── bin
├── pkg
└── src
    └── gitlab.com
        └── pirivan
            ├── project
            │   ├── cmd
            │   │   ├── prog1
            │   │   ├── prog2
            │   │   └── prog3
            │   └── internal
            │       └── pkg
            │           ├── liba
            │           ├── libb
            │           └── libc
            └── other_project

The top three directories underneath $GOPATH, bin, pkg, and src, host the Go workspace binaries, libraries, and source code respectively. In this example the Go project is at gitlab.com/pirivan/project and has three programs called prog{1,2,3} under the cmd directory. Each of these directories has one or more *.go and *_test.go source files.

The project includes three internal libraries lib{a,b,c} in the internal/pkg directory. They are internal, meaning that they are not meant to be used by other projects. Each of these library directories have *.go and *._test.go files as well.

This is how you use the Makefile with a project organized this way:

# cd to the project's directory
$ cd $GOPATH/src/gitlab.com/pirivan/project
# compile and build binaries
$ make
# run tests
$ make tests
# clean the prog{1,2,3} binaries
$ make clean
# or, clean the prog{1,2,3} binaries and the lib{a,b,c} .a files
$ make cleanall

The Makefile creates the project binaries in the $(GOPATH)/bin directory, and the .a library files in $GOPATH/linux_amd64/gitlab.com/pirivan/project/internal/pkg, as usual.

Whenever you modify a source file under the cmd directory, the corresponding command program is rebuilt when you run make again. Similarly, the libraries are rebuilt when one of their source files is modified. More interestingly however, command binaries are automatically rebuilt when one or more of the libraries used by the commands are modified. Therefore, all dependencies are taken care of.

Finally, whenever you modify a Go test program in any directory, the corresponding tests are rerun when you run make tests again.

The Makefile #

The Makefile is split into two files: Makefile, which you may want to customize, and golang.mk, which shouldn’t need to be modified. Here is the Makefile:

# Directory for binary files
# Default is GOBIN := $(GOPATH)/bin

# Directory for source code relative to current directory
# Default is CMDDIR := cmd

# Directory for internal libraries relative to current directory
# Default is INTERNALDIR := internal/pkg

include golang.mk

With the exception of the last line, which includes the golang.mk file, the default Makefile is just comments! This means that you can use it immediately if your project is organized as described above. There is nothing else to say, just copy the Makefile and golang.mk files to the root of your Go project and you can start building and testing your code right away, it couldn’t be easier!

Changing the Commands Directory #

You may want to organize the project inside sub-directories for convenience. For example, client packages go under cmd/client and server pakages go under cmd/server. The project structure would look like this:

$GOPATH
├── bin
├── pkg
└── src
    └── gitlab.com
        └── pirivan
            └── project
                ├── bin
                ├── cmd
                │   ├── client
                │   │   └── cli
                │   └── server
                │       ├── prog1
                │       └── prog2
                └── internal
                    └── pkg
                        ├── liba
                        ├── libb
                        └── libc
            └── other_project

As before, each of the directories client/cli and server/prog{1,2} contains one or more source and test files implementing a Go package.

The Makefile as described above still works, there is no need for you to modify anything. The Go tools recursively traverse the cmd directory scanning all subdirectories and determining what Go packages are there that need to be compiled. But, say that you want to compile the client programs only, maybe because the server directory is in a messy state at the moment. You can do this by modifying the Makefile as follows:

# Directory for binary files
# Default is GOBIN := $(GOPATH)/bin

# Directory for source code relative to current directory
# Default is CMDDIR := cmd
CMDDIR := cmd/client

# Directory for internal libraries relative to current directory
# Default is INTERNALDIR := internal/pkg

include golang.mk

To compile the server programs instead, modify it accordingly with

CMDDIR := cmd/server

With these changes you limit the Makefile to process only one set of packages at a time. However, see below for a way to run the Makefile in a more dynamic way using command-line variables.

You can modify the INTERNALDIR variable also if you need to, but I haven’t found this to be necessary yet.

Changing the Binary Directory #

You may want the change the directory used to host the command binaries. For example, you may want to avoid polluting the main $GOPATH/bin directory with project binaries that you are just testing.

Let’s say that you want to use a local bin directory inside your project to host the binaries. You create this directory as follows:

# cd to the project's directory
$ cd $GOPATH/src/gitlab.com/pirivan/project
# create the local bin directory
$ mkdir bin

Now you modify the Makefile to read like this:

# Directory for binary files
# Default is BINDIR := $(GOPATH)/bin
GOBIN = bin

# Directory for source code relative to current directory
# Default is CMDDIR := cmd

# Directory for internal libraries relative to current directory
# Default is INTERNALDIR := internal/pkg

include golang.mk

When you run the make command again, the project binaries are installed now in $GOPATH/src/gitlab.com/pirivan/project/bin, which you can run as bin/prog{1,2,3}.

Of course you can keep changes to both the CMDDIR and BINDIR definitions together if both need to be changed.

Using Command-Line Variables #

You may override the definitions of the variables from the command line. This avoids you having to modify the Makefile and eventually having to keep track of changes in your version control system.

The following example illustrates how to run the original, unmodified, Makefile for the client programs but still implementing the changes discussed above:

# cd to the project's directory
$ cd $GOPATH/src/gitlab.com/pirivan/project
# compile and build the client binaries
$ make CMDDIR=cmd/client GOBIN=bin
# compile and build the server binaries
$ make CMDDIR=cmd/server GOBIN=bin
# run all tests, no need to use GOBIN
$ make tests
# clean the local bin directory
$ make CMDDIR=cmd/client GOBIN=bin clean

Future Improvements #

Other targets such as format, debug and deploy may come later. Handling of versions and vendor branches are also some possibilities. But I found that the current version of the Makefile simplifies the command line effort substantially when building my projects. I hope you do find it useful too.

Makefile targets #

In summary, the Makefile supports the following targets:


Target Description
Runs the libs and commands targets
libs Compiles libraries from the INTERNALS directory
commands Compiles command binaries from the CMDDIR directory
tests Runs all project tests
clean Removes all binaries created by the commands target
cleanall Removes all binaries created by the commands and libs targets