Skip to main content
  1. Posts/

Build watcher

·4 mins

I thought I’d write a simple build watcher in a couple of hours. Of course, it didn’t take that long.

It took longer.

The code is here: https://github.com/barrylapthorn/build-watch.

It’s the very defintion of yak shaving.

The basic requirements were (and still are):

  • watch a set of source directories
  • watch for changes (add/delete) for a given set of file extensions
  • read a template build file and write it with the new set of source files

It ‘works on my machine’, i.e. linux, with a modern version of gcc and cmake.

Non-requirements #

This doesn’t support Windows or Apple (yet). And it is expected that the source code is in a git repository. It may work with Mercurial, but is untested.

Requirements ad nausem #

Of course, the basic set of requirements were woefully naive.

At its most basic, you could watch a set of directories from the root level, and you’re done.

Use blocking inotify_init and you’re halfway there.

Of course, a git repository, with source code has far more directories than you need to actually watch. And just because you’re not watching it, doesn’t mean someone might not create a new directory, or delete an existing one.

Now the feature creep comes in:

  • You may want to gracefully terminate on a signal, because you opened all those handles using inotify. What’s the point of C++ (stop right there), if not to RRID system resources?
  • You also want to be non-blocking and use epoll.
  • A nice CLI, and some logging.
  • What configuration format do I use?
  • What template language do I choose?
  • It’s 2024, let’s dump the stack if we have a problem.
  • Use cmake. Yeah.

The original requirement, redux #

But back to the original problem. I wanted to add and remove files from my CMakeLists.txt as they were created or deleted on disk. Modification of those files is a no-op as far as the build files are concerned.

We therefore need a template file and we need to read that, and then write it with the updated list.

It’s actually pretty trivial using mustache:


add_executable(build-watch
{{#files}}
    {{relpath}}
{{/files}}
)

And that’s it.

Of course, the next step from supporting CMakeLists.txt is to support… something else. Since I have a slight familiarity with Bazel, BUILD files should be supported.

Of course, Bazel wants the files quoted, and comma delimited, so we have to do a little bit of work with “inverted sections” in mustache:

cc_binary(
    name = "build-watch",
    srcs = [
    {{#files}}
        "{{relpath}}"{{^last}}, {{/last}}
    {{/files}}
    ],
)

The C++ code must populate those mustache data fields:

    for (const auto& file : matchingFiles) {
        const bool isLast = (file == matchingFiles.back());
        data d;
        d.set("relpath", file.string());
        d.set("last", data(isLast ? data::type::bool_true : data::type::bool_false));
        files << d;
    }

Configuration #

Supporting one or more build files in the same folder just requires a bit of configuration.

{
  "files": [
    {
      "src": "CMakeLists.txt.mustache",
      "dest": "CMakeLists.txt",
      "extensions": [".hpp", ".cpp", ".h"]
    },
    {
      "src": "BUILD.mustache",
      "dest": "BUILD",
      "extensions": [".hpp", ".cpp", ".h"]
    }
  ],
  "ignoreFiles": [
    ".gitignore"
  ]
}

Of course we can also add BUILD.py.mustache:

py_binary(
    name = "app",
    srcs = [
    {{#files}}
        "{{relpath}}"{{^last}}, {{/last}}
    {{/files}}      
    ],
    deps = [
        "//projects/python_folder/sample_library:calculator",
        requirement("Flask"),  #or '@python_deps_pypi__flask//:pkg'
    ],
    main = "app.py"
)

With a config:

{
  "files": [
    {
      "src": "CMakeLists.txt.mustache",
      "dest": "CMakeLists.txt",
      "extensions": [".hpp", ".cpp", ".h"]
    },
    {
      "src": "BUILD.mustache",
      "dest": "BUILD",
      "extensions": [".hpp", ".cpp", ".h"]
    },
    {
      "src": "BUILD.py.mustache",
      "dest": "BUILD",
      "extensions": [".py"]
    }
  ],
  "ignoreFiles": [
    ".gitignore"
  ]
}

Enough, let’s run it. #

I like just.

Assuming you’re running a modern-ish linux distro, you probably only need to install just.

Then just dogfood will build the release version and run it with the build-watch repo.

That’s it.

There’s more in the README.

It’s version 1, as it works, runs, and does what I want to do. None of the 0.1-alpha-prelease nonsense.

If you find a bug, try fixing it and sending me a PR.

What doesn’t it do? #

There are a few things it doesn’t do:

  • doesn’t reload if you modify the .gitignore file
  • doesn’t handle nested .gitignore files
  • doesn’t reload if you modify .config/BuildWatch/config.json
  • may not run correctly if not pointed at the root of your repository
  • the tests (or lack thereof) work fine, but need more coverage
  • needs refactoring
  • I’ve probably missed out some other things

Help! #

Lastly, the help screen:

help