DEV Community

Cover image for Making your life easier with a Makefile
Kristian Ivanov
Kristian Ivanov

Posted on

Making your life easier with a Makefile

Intro

Recently I have been spending more time on one of my side projects. This lead to completely refactoring it and expanding the initial simplistic vanilla JS Chrome Extension to Chrome Extension bootstrapped with CRA, android hybrid version and (since last weekend) a web version.
All of this means playing around with yarn watch | build, cordova run | build, different methods of generating build files - .apk for the Android playstore and .zip for the Chrome webstore, different imports and start ups based on the environment and so on. Updating the app version across multiple files in various folders.
My immediate dev response to all of that was to try and bootstrap all of this as much as possible in any way. First of all this would make it less prone to errors and more consistent, and secondly I am too lazy to be bothered to execute several commands over and over for every build and try to remember all of them.
My education had touched makefiles only in the context of C++ and when talking with friends of mine, it turned out that they had similar experience. Here is a super short description of what a Makefile is, what you can put inside it and how you can use makefiles to make your life easier by bundling sections of logic together to avoid repetition.

Enter Makefile

From Wikipedia:

A makefile is a file (by default named "Makefile") containing a set of directives used by a make build automation tool to generate a target/goal.

The simplest way to look at it is - If you can type/do something from your terminal, you can put it into a makefile. If something is too complicated, you can put it into a script file and execute it from the makefile.

Another way to look at it is - a way to keep a set of shell aliases in your repository so you can re-use them across multiple machines and/or users.

Basic anatomy of a Makefile

Commands:

Commands are defined by a string, followed by a colon. What the command would do are the next lines. They are typically prefixed with a tab. You will see examples of commands bellow.

You can define a default command to be executed. It will be run if you execute "make" without any params:

.DEFAULT_GOAL: help
default: help
Enter fullscreen mode Exit fullscreen mode

The most common definition of the help command inside a make file I've seen and used is the following

help: ## Output available commands
    @echo "Available commands:"
    @echo
    @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
Enter fullscreen mode Exit fullscreen mode

This will read all commands listed in the make file and display them in the terminal. It is useful if you have forgotten what the appropriate syntax is to quickly check it, or if you are just getting familiar with a makefile somebody else had added to list all options without reading the makefile itself. For instance, for me it outputs the following:

help:  Output available commands
extension-clean: remove old build of the extension
extension-build:  build the chrome extension
android-clean: remove old build of the android app
android-build:  build the android app
bump-version:  update the version for the chrome extension and the android app
make android-debug: debug android
make extension-debug: debug chrome extension
make web-clean: remove old build for the web
make web-debug: debug web version
make web-build: build web version
make build: build all platforms
Enter fullscreen mode Exit fullscreen mode

You can bundle commands together. You can notice that I have 4 separate build directives listed above. One for each platform and one to build everything. Here is what the one to build everything looks like:

make build: ##build all platforms
    make extension-build
    make android-build
    make web-build
Enter fullscreen mode Exit fullscreen mode

It will call each of the build commands one by one and do whatever is described inside them, bootstrapping the logic even further.

Variables

You can add variables into your makefile. The most common use case from my perspective is to avoid typing long path names over and over. For instance, I have this in mine

unsigned-app-path = platforms/android/app/build/outputs/apk/release/app-release-unsigned.apk
android-app-folder = android-app
Enter fullscreen mode Exit fullscreen mode

Makefiles can also get variables from the shell when executing them. If you run a makefile directive with

make bump-version version=1.1.1
Enter fullscreen mode Exit fullscreen mode

You will be able to use ${version} as a variable in the bump-version command in your make file. More info on that in the next section.

Combining Makefile with Nodejs

I mentioned in the intro that updating the version to a newer one means editing several files and trying to keep it consistent between all of them. This is inherently error prone, "messy" and requires you to either remember where the files are or their names. Alternatively you can also bootstrap it using something in the lines of:

const fs = require('fs');

const files = [
    {
        path: 'android-app/public/manifest.json',
        versionLine: `"version": "`,
        postLineSeparator: ','
    },
    {
        path: 'android-app/config.xml',
        versionLine: `version="`,
    },
    {
        path: 'android-app/package.json',
        versionLine: `"version": "`,
        postLineSeparator: ','
    },
]

const version = process.argv[2]

if (version) {
    files.forEach(config => {    
        fs.readFile(config.path, 'utf8', function (err, data) {
            if (err) {
                console.log(err)
                reject(err) 
            }

            const separator = '\n'
            const newContent = data.split(separator).map(line => {
                if (line.includes(config.versionLine)) {
                    return `${line.split(config.versionLine)[0]}${config.versionLine}${version}"${config.postLineSeparator ? config.postLineSeparator : ''}`
                }
                return line
            }).join(separator)

            fs.writeFile(config.path, newContent, 'utf8', function (err) {
                if (err) {
                    console.log(err);
                } else {
                    console.log(`updated ${config.path} version`)
                }
            });
        });  
    })
}
Enter fullscreen mode Exit fullscreen mode

Although it is a bit hackish it would take of updating the version across the three listed files. But what if you want to go even further? You can combine it with a Makefile directive into something like that:

node versionBump.js ${version}
    git add $(android-app-folder)/config.xml
    git add $(android-app-folder)/package.json
    git add $(android-app-folder)/public/manifest.json
    git commit -m "version bump to ${version}"
    git push -u origin master
    git tag v${version}
    git push origin v${version}
Enter fullscreen mode Exit fullscreen mode

The code above takes 1 additional argument which is the version code and passes it into the short Nodejs script we saw earlier. Then it commits the three files containing the version and uses the version we passed when executing it for the comment as well. It also generates a git tag with the version code. This takes care of several annoying little steps of preparing for a release by executing only 1 command in the shell.

Combining a Makefile + any scripting language means that the options to automate/bundle a sequence of things that you are doing frequently is limitless.

Outro

What I want to leave you with is that we devs are lazy when it comes to do the same thing over and over and we try to find ways to not have to do it. If you think about it, software development is trying to make things easier for all kinds of people and professions by taking care of the repetitive tasks so people can have more time to focus on what really matters. The things listed in the article, although limited by my own use cases, can help you to do the same in your own indie or company projects.

Top comments (0)