Avoid Re-Running NPM/YARN in CMake targets

Introduction

Despite all the webpacks and gulp and whatever build/package system you have in your npm/yarn project, chances are you have cmake that orchestrates your build. In my case, our organization requires project to be built/package by cmake, mainly to simplify our build system. Effectively reducing it to run:

  1. git clone <project url>
  2. cmake …
  3. make test
  4. make package
  5. Upload generated rpm(s) / deb(s).

This simplicity effectively reduced the dev-op person to just one. We, the developers, write these cmake targets for them.

Naive Way

when I was first working at my current job, I was assigned to their very new (and only) javascript/node product. I’m also their first javascript/node developer, so folks who created the cmake file did the bare minimum to get a build system going.

add_custom_target(installDeps
  COMMAND yarn install --frozen-lockfile
  WORKSPACE ${CMAKE_CURRENT_SOURCE_DIR}
)

There’s a reason why this section is called Naive Way, this is really inefficient. In the case of our build steps above containing make testand,make package it’s gonna be called at least three times. make test needs to pull its dependencies and make package will do a make build and run cpack (also does a make build) to build our rpm/deb files.

In addition to this, make build is run twice, once at make package and once at cpack. We need a way to tell if our package have finished building or not, much like how this things would behave if we are building C/C++ project. So how do you actually avoid these rebuilds?

Why Targets Re-Run In CMake

So how do you exactly avoid re-running target in cmake? In the end, cmake just “transpiles” to make files. So the question should be, how do you avoid re-running targets in make?

A basic make structure would look like the following:

<target>: <dependencies>
    <command to execute>

To compile a hello world Cprogram, it would look like the following:

hello_world.o: hello_world.c
        cc -c hello_world.c

Once, it run once and object file target hello_world.o is generated, it doesn’t rerun until:

  • target is deleted. In this case, hello_world.o is deleted.
  • dependencies are modified. In this case, just hello_world.c

So why is our installDeps target above kept being rebuilt? What does it’s makefile looks like? Well, it would look something like this:

installDeps:
  yarn install --frozen-lockfile

That’s it! It will always re-run since:

  • installDeps file is never generated, and
  • there is no dependencies to watch for change.

Now that we know the culprit, let’s find ourselves the solution.

Avoid Re-Running Targets in CMake

To avoid re-running our targets, I proposed the following solution:

Always use files as target

But, yarn install --frozen-lockfile generates a node_modules directory, not a file. How do we generate a file from the yarn install --frozen-lockfile? My solution is to generate a checksum of the generated node_modules directory. So by adding a “postinstall” target in our package.json,

"postinstall": "tar c node_modules | md5sum > node_modules.md5"

This will generate a file with the md5 hash of the node_modules directory after the yarn install --frozen-lockfile. Now in our makefile (don’t worry we will convert it back to cmake soon):

node_modules.md5:
  yarn install --frozen-lockfile

installDeps: node_modules.md5

This guarantees that it will never re-run installDep target. There is a new problem now though, we want make installDeps to run again when package.json/yarn.lock is modified (for best practice purposes, I’ll use yarn.lock). So we modify it to be:

node_modules.md5: yarn.lock
  yarn install --frozen-lockfile

installDeps: node_modules.md5

That’s it! This will re-run if package.json/yarn.lock changes or node_modules.md5 is not generated and won’t re-run beyond that. So how does these look in cmake?

add_custom_command(
  OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/node_modules.md5
  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/yarn.lock
  COMMAND yarn install --frozen-lockfile
  WORKSPACE ${CMAKE_CURRENT_SOURCE_DIR}
)

add_custom_target(installDeps
  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/node_modules.md5
)

This pattern can be used on other things. Maybe you have a yarn build in your make build and you don’t want to call yarn build unnecessarily. Similar pattern is used.

Conclusion

We recently released a version and just doing housekeeping things like speeding up the build. I was able to improve our build time from 45 minutes to 13 minutes, and most of the increase is gained from the patterns described above. I hope you reap some sick time by avoiding this redundant builds, not to mention, save your employer some electric bills.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.