I’ve been programming on Unix and Linux machines for many decades now, and one universal I’ve leaned on is the default availability of make, which has been labor-saving swiss-army knife for setting up my build and development flows, or for organizing any random commands that I execute repeatedly in a directory. It’s a great memory aid, reminding me when I come back later of how to work with the files in that directory.
Here are some examples of Makefiles I created a decade ago:
- When I was doing Palm webOS development I wrote a Makefile that has an eclectic collection of commands that comprised my hacked-together Palm build system.
- When I was doing Java web-server development, I wrote a blog post explaining how I used a Makefile to integrate the cool continuous-integration system of the day (Hudson) with the cool Java web framework of the day (Play Framework).
The things I like about make is that:
- It has very little boilerplate.
- It captures the commands, just as you type them in the command line.
- It captures the dependencies between the commands.
- The dependencies are simply the files generated by one command that are used by another command.
- The files can be file patterns, so you can express how one type of file is generated from another in general terms.
- It uses the timestamps of input and output files of the commands to determine if a command is out of date and needs to be run.
- It has variables to factor out common strings.
- It is ubiquitous and installed by default everywhere.
But I have been becoming increasingly dissatisfied with make. It was fine-tuned for the development environment of its day (compiling C on UNIX), and though it is remarkably versatile it still has lots of vestiges of special build-in support for that development flow. And over the decades it has accumulated a lot of complexity in how patterns and variables work.
However, I never found any replacement system that has all the positive features of make. The nearest is Bazel which is a multi-repo public version of Google’s internal monorepo build tool (which replaced an earlier make-driven build system in Google).
So I built my own tool, Bajel.
These days, most of my side projects use the npm ecosystem, so I built Bajel there. If you have npm installed, you can execute Bajel simply by doing
Bajel expects that the current directory contains a build file which in its most straightforward form is a JSON file. However Bajel also supports build files in other syntaxes, including YAML or TOML syntax, which make for cleaner files.
Here is an example
build.yaml file, which is probably the closest of the available syntaxes to a classic Makefile:
TEST: ava test/contract_test.js serve: deps: - dist/index.cjs exec: python -m SimpleHTTPServer 8888 test: deps: - test_default - test_contract_production - test_contract_development - test_contract_no_env test_default: deps: - dist/index.cjs exec: ava test_contract_production: exec: NODE_ENV=production $(TEST) test_contract_development: exec: NODE_ENV=development $(TEST) test_contract_no_env: exec: NODE_ENV= $(TEST) "perf.csv": deps: - src/node/perf.js", "src/common/optimizer.js"] exec: node $< "dist/index.cjs": deps: - rollup.config.js - src/node/index.js - src/common/index.js - src/common/random.js - src/common/color.js - src/common/optimizer.js - src/common/contract.js - src/common/random.js exec: rollup --config $< publish: deps: - dist/index.cjs exec: npm publish clean: exec: rm -rf dist
However YAML as a syntax has fallen out of favor in some quarters, so you might prefer the following, which is semantically identical, but in the nice clean TOML format.
TEST="ava test/contract_test.js" [serve] deps = ["dist/index.cjs"] exec = "python -m SimpleHTTPServer 8888" [test] deps = [ "test_default", "test_contract_production", "test_contract_development", "test_contract_no_env", ] [test_default] deps = ["dist/index.cjs"] exec = "ava" [test_contract_production] exec = "NODE_ENV=production $(TEST)" [test_contract_development] exec = "NODE_ENV=development $(TEST)" [test_contract_no_env] exec = "NODE_ENV= $(TEST)" ["perf.csv"] deps = ["src/node/perf.js", "src/common/optimizer.js"] exec = "node $<" ["dist/index.cjs"] deps = [ "rollup.config.js", "src/node/index.js", "src/common/index.js", "src/common/random.js", "src/common/color.js", "src/common/optimizer.js", "src/common/contract.js", "src/common/random.js", ] exec = "rollup --config $<" [publish] deps = ["dist/index.cjs"] exec = "npm publish" [clean] exec = "rm -rf dist"
Some features to note in the above:
TEST="ava test/contract_test.js"is setting a variable which is referred to later in the file as
$(TEST). In TOML syntax all the variables have to be defined at the top of the file before anything else.
[serve]is the first target, and is the one whose action is executed by default when you do
npx bajel. You can execute any other target by adding it as an argument to the command line, for example
npx bajel test.
deps = ["dist/index.cjs"]says that
servehas one dependency, the target
dist/index.cjs. If that file does not exist or is older than any of its dependencies, then the action for
dist/index.cjsis executed before
exec = "python -m SimpleHTTPServer 8888"specifies the shell command that executes for
serve. This is executed just as if you had typed
python -m SimpleHTTPServer 8888on the command line.
exec = "rollup --config $<"contains
$<which is replaced by the first dependency, in this case
rollup.config.js. Also possible are
$+which is replaced by all the dependencies (blank-separated), and
$@which is replaced by the target.
Bajel implements all the features of make that I value (including
% pattern patching not shown in this example), but keeps things as simple as possible.
For details, see the README on Github.