📜 ⬆️ ⬇️

An example of creating a Makefile for Go applications

In this tutorial, we’ll look at how a Go developer can use a Makefile to develop their own applications.

image

What are Makefiles?


Makefile is an incredibly useful automation tool that you can use to run and build applications not only on Go, but also in most other programming languages.
')
It can often be seen in the root directory of many Go apps on Github and Gitlab. It is widely used as a tool for automating tasks that often accompany developers.

If you use Go to create web services, then the Makefile will help solve the following problems:


Here is a typical directory structure for a project:

.env Makefile main.go bin/ src/ vendor/ 

If we call the make command in this directory, we get the following output:

 $ make Choose a command run in my-web-server: install Install missing dependencies. Runs `go get` internally. start Start in development mode. Auto-starts when code changes. stop Stop development mode. compile Compile the binary. watch Run given command when code changes. eg; make watch run="go test ./..." exec Run given command, wrapped with custom GOPATH. eg; make exec run="go test ./..." clean Clean build files. Runs `go clean` internally. 

Environment variables


The first thing we want from the Makefile is to include the environment variables that we defined for the project. Therefore, the first line will look like this:

 include .env 

Next, we define the project name, Go folders / files, paths to pid ...

 PROJECTNAME=$(shell basename "$(PWD)") # Go . GOBASE=$(shell pwd) GOPATH=$(GOBASE)/vendor:$(GOBASE):/home/azer/code/golang #       . GOBIN=$(GOBASE)/bin GOFILES=$(wildcard *.go) #     ,       . STDERR=/tmp/.$(PROJECTNAME)-stderr.txt # PID-    ,       PID=/tmp/.$(PROJECTNAME)-api-server.pid # Make     Linux.   silent. MAKEFLAGS += --silent 

In the remainder of the Makefile, we will often use the GOPATH variable. All our teams must be associated with the GOPATH of a specific project, otherwise they will not work. This provides a clean isolation of our projects, but at the same time complicates the work. To simplify the task, we can add an exec command that will execute any command with our GOPATH.

 # exec:     GOPATH. : make exec run = " go test ./...” exec: @GOPATH=$(GOPATH) GOBIN=$(GOBIN) $(run) 

However, it is worth remembering that you only need to use exec if you need to do something that cannot be written in the makefile.

Development mode


The development mode should:


That sounds easy. However, the difficulty lies in the fact that we simultaneously run both the service and the file-watcher. Before starting a new process, we must ensure that it stops correctly and that we don’t break the normal behavior of the command line when pressing Control-C or Control-D.

 start: bash -c "trap 'make stop' EXIT; $(MAKE) compile start-server watch run='make compile start-server'" stop: stop-server 

The code described above solves the following tasks:


In the following sections, I will explain these commands in more detail.

Compilation


The compile command does not just call go compile in the background - it clears the error output and prints a simplified version.

This is what the command line output looks like when we made breaking edits:

image

 compile: @-touch $(STDERR) @-rm $(STDERR) @-$(MAKE) -s go-compile 2> $(STDERR) @cat $(STDERR) | sed -e '1s/.*/\nError:\n/' | sed 's/make\[.*/ /' | sed "/^/s/^/ /" 1>&2 

Server start / stop


start-server starts a binary compiled in the background, saving its PID to a temporary file. stop-server reads the PID and kills the process if necessary.

 start-server: @echo " > $(PROJECTNAME) is available at $(ADDR)" @-$(GOBIN)/$(PROJECTNAME) 2>&1 & echo $$! > $(PID) @cat $(PID) | sed "/^/s/^/ \> PID: /" stop-server: @-touch $(PID) @-kill `cat $(PID)` 2> /dev/null || true @-rm $(PID) restart-server: stop-server start-server 

Change monitoring


We need a watcher file to track changes. I tried many, but could not find a suitable one, so I wrote my own file monitoring tool - yolo . Install it using the command:

 $ go get github.com/azer/yolo 

After installation, we can observe changes in the project directory, excluding the vendor and bin folders.

 ## watch:      ,  make watch run="echo 'hey'" watch: @yolo -i . -e vendor -e bin -c $(run) 

We now have a watch command that recursively tracks changes to the project directory, with the exception of the vendor directory. We can just pass any command to run.
For example, start calls make-start-server when the code changes:

 make watch run="make compile start-server" 

We can use it to run tests or check race conditions automatically. Environment variables will be set at runtime, so you don't need to worry about GOPATH:

 make watch run="go test ./..." 

A nice feature of Yolo is its web interface. If you enable it, you can immediately see the output of your command in the web interface. All you need to do is pass the -a option:

 yolo -i . -e vendor -e bin -c "go run foobar.go" -a localhost:9001 

Open localhost: 9001 in a browser and immediately see the result of work:

image

Dependency Installation


When we make changes to the code, we would like the missing dependencies to be loaded before compilation. The install command will do the job for us:

 install: go-get 

We will automate the install call when the file changes before compilation, so the dependencies will be installed automatically. If you want to install the dependency manually, you can run:

 make install get="github.com/foo/bar" 

Internally, this command will be converted to:

 $ GOPATH=~/my-web-server GOBIN=~/my-web-server/bin go get github.com/foo/bar 

How it works? See the next section where we add regular Go commands to implement higher level commands.

Go Teams


Since we want to install GOPATH in the project directory in order to simplify dependency management, which has not yet been officially decided in the Go ecosystem, we need to wrap all Go commands in the Makefile.

 go-compile: go-clean go-get go-build go-build: @echo " > Building binary..." @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go build -o $(GOBIN)/$(PROJECTNAME) $(GOFILES) go-generate: @echo " > Generating dependency files..." @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go generate $(generate) go-get: @echo " > Checking if there is any missing dependencies..." @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go get $(get) go-install: @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go install $(GOFILES) go-clean: @echo " > Cleaning build cache" @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go clean 

Help


Finally, we need the help command to see a list of available commands. We can automatically generate beautifully formatted help output using the sed and column commands:

 help: Makefile @echo " Choose a command run in "$(PROJECTNAME)":" @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' 

The following command scans the Makefile for lines starting with ## and displays them. That way, you can simply comment on specific commands, and comments will be displayed with the help command.

If we add a few comments:

 ## install: Install missing dependencies. Runs `go get` internally. install: go-get ## start: Start in development mode. Auto-starts when code changes. start: ## stop: Stop development mode. stop: stop-server 

We'll get:

 $ make help Choose a command run in my-web-server: install Install missing dependencies. Runs `go get` internally. start Start in development mode. Auto-starts when code changes. stop Stop development mode. 

Final version


 include .env PROJECTNAME=$(shell basename "$(PWD)") # Go related variables. GOBASE=$(shell pwd) GOPATH="$(GOBASE)/vendor:$(GOBASE)" GOBIN=$(GOBASE)/bin GOFILES=$(wildcard *.go) # Redirect error output to a file, so we can show it in development mode. STDERR=/tmp/.$(PROJECTNAME)-stderr.txt # PID file will keep the process id of the server PID=/tmp/.$(PROJECTNAME).pid # Make is verbose in Linux. Make it silent. MAKEFLAGS += --silent ## install: Install missing dependencies. Runs `go get` internally. eg; make install get=github.com/foo/bar install: go-get ## start: Start in development mode. Auto-starts when code changes. start: bash -c "trap 'make stop' EXIT; $(MAKE) compile start-server watch run='make compile start-server'" ## stop: Stop development mode. stop: stop-server start-server: stop-server @echo " > $(PROJECTNAME) is available at $(ADDR)" @-$(GOBIN)/$(PROJECTNAME) 2>&1 & echo $$! > $(PID) @cat $(PID) | sed "/^/s/^/ \> PID: /" stop-server: @-touch $(PID) @-kill `cat $(PID)` 2> /dev/null || true @-rm $(PID) ## watch: Run given command when code changes. eg; make watch run="echo 'hey'" watch: @GOPATH=$(GOPATH) GOBIN=$(GOBIN) yolo -i . -e vendor -e bin -c "$(run)" restart-server: stop-server start-server ## compile: Compile the binary. compile: @-touch $(STDERR) @-rm $(STDERR) @-$(MAKE) -s go-compile 2> $(STDERR) @cat $(STDERR) | sed -e '1s/.*/\nError:\n/' | sed 's/make\[.*/ /' | sed "/^/s/^/ /" 1>&2 ## exec: Run given command, wrapped with custom GOPATH. eg; make exec run="go test ./..." exec: @GOPATH=$(GOPATH) GOBIN=$(GOBIN) $(run) ## clean: Clean build files. Runs `go clean` internally. clean: @(MAKEFILE) go-clean go-compile: go-clean go-get go-build go-build: @echo " > Building binary..." @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go build -o $(GOBIN)/$(PROJECTNAME) $(GOFILES) go-generate: @echo " > Generating dependency files..." @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go generate $(generate) go-get: @echo " > Checking if there is any missing dependencies..." @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go get $(get) go-install: @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go install $(GOFILES) go-clean: @echo " > Cleaning build cache" @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go clean .PHONY: help all: help help: Makefile @echo @echo " Choose a command run in "$(PROJECTNAME)":" @echo @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' @echo 

Source: https://habr.com/ru/post/461467/


All Articles