Skip to content

eobrain/bajel

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bajel : a Simple Build System

logo

Bajel is an easy way of executing commands for building any kind of target files.

It will only build a target file if is out of date relative to its dependencies.

You specify the build targets, their dependencies, and the build commands in a build file which has a very simple conceptual structure:

  • A set of targets, each of which has
    • a set of dependencies (deps)
    • a command to execute (exec)
  • Declarations of variables that you can use in the exec

You can write the build file in your favorite format:

  • TOML if you like things clean and readable
  • YAML if that's your thing
  • JSON if you want just the basics
  • JavaScript if you want more control
  • Markdown if you like literate programming

The article Bajel, A Simple, Flexible Build and Scripting Tool for NPM describes the motivation for creating Bajel.

Installation

First check prerequisites by making sure that npm -v shows you have a working Node.js installation. If not install Node.js (or if you want the flexibilty of switching between multiple versions of Node.js install NVM)

Choose one of these three options

  1. Install as a global utility

    npm install -g bajel
  2. Install as a global utility as root (if above gives permission denied error).

    sudo npm install -g bajel
  3. Install in the current NPM project (if you only want to use Bajel with Node).

    npm install --save-dev bajel

    Note in this case you will have to replace all the bajel something example commands below with npx bajel something.

Usage

The command

bajel

will build the default target in build file in the current directory.

The default target is the first target in the file that is not a file pattern (with a % wildcard).

bajel foo

Will build target foo.

bajel -n

Will do a dry run, printing out the commands that it would execute, but not actually executing them.

The build file must be one of the following names:

  • BUILD.toml
  • BUILD.yaml or BUILD.yml
  • BUILD.json
  • BUILD.cjs (JavaScript, as a classic Node-JS module)
  • BUILD.mjs (JavaScript, as a new-style ES6 module -- Node version 13.2.0 or later)
  • BUILD.md

All these different languages are alternate syntaxes of specifying the same underlying build file structure, as shown by the following examples (based on a makefile example), each of which specify the same thing.

Examples

BUILD.toml

CC = "gcc"
CFLAGS = "-I."
OBJ = ["hellomake.o", "hellofunc.o"]

["%.o"]
deps = ["%.c", "hellomake.h"]
exec = "$(CC) -c -o $@ $< $(CFLAGS)"

[hellomake]
deps = ["hellomake.o", "hellofunc.o"]
exec = "$(CC) -o $@ $+ $(CFLAGS)"

[clean]
exec = "rm -f hellomake $(OBJ)"

BUILD.yaml

CC: gcc
CFLAGS: -I.
OBJ:
- hellomake.o
- hellofunc.o

"%.o":
  deps:
    - "%.c"
    - hellomake.h
  exec: $(CC) -c -o $@ $< $(CFLAGS)

hellomake:
  deps:
    - hellomake.o
    - hellofunc.o
  exec: $(CC) -o $@ $+ $(CFLAGS)

clean:
  exec: rm -f hellomake $(OBJ)

BUILD.json

{
  "CC": "gcc",
  "CFLAGS": "-I.",
  "OBJ": [
    "hellomake.o",
    "hellofunc.o"
  ],
  "%.o": {
    "deps": [
      "%.c",
      "hellomake.h"
    ],
    "exec": "$(CC) -c -o $@ $< $(CFLAGS)"
  },
  "hellomake": {
    "deps": [
      "hellomake.o",
      "hellofunc.o"
    ],
    "exec": "$(CC) -o $@ $+ $(CFLAGS)"
  },
  "clean": {
    "exec": "rm -f hellomake $(OBJ)"
  }
}

BUILD.cjs

(This JavaScript module format supported with all versions of Node.)

const CC = 'gcc'
const CFLAGS = '-I.'
const DEPS = ['hellomake.h']
const OBJ = ['hellomake.o', 'hellofunc.o']

module.exports = {

  '%.o': {
    deps: ['%.c', ...DEPS],
    exec: `${CC} -c -o $@ $< ${CFLAGS}`
  },

  hellomake: {
    deps: OBJ,
    exec: `${CC} -o $@ $+ ${CFLAGS}`
  },

  clean: {
    exec: `rm -f hellomake ${OBJ.join(' ')}`
  }

}

BUILD.mjs

(This JavaScript module format is only supported if your version of Node is 13.2.0 or later.)

const CC = 'gcc'
const CFLAGS = '-I.'
const DEPS = ['hellomake.h']
const OBJ = ['hellomake.o', 'hellofunc.o']

export default {

  '%.o': {
    deps: ['%.c', ...DEPS],
    exec: `${CC} -c -o $@ $< ${CFLAGS}`
  },

  hellomake: {
    deps: OBJ,
    exec: `${CC} -o $@ $+ ${CFLAGS}`
  },

  clean: {
    exec: `rm -f hellomake ${OBJ.join(' ')}`
  }

}

BUILD.md

# Markdown version of build file

## %.o

Deps: `%.c` `hellomake.h`

```sh
gcc -c -o $@ $< -I.
```

## hellomake

Deps: `hellomake.o` `hellofunc.o`

```sh
gcc -o $@ $+ -I.
```

## clean

```sh
rm -f hellomake hellomake.o hellofunc.o
```

Real world examples

  • Diagmap's BUILD.toml is a fairly straightforward build file in TOML format. The only slightly tricky aspect is that it take advantage of the % wild card to create empty .ok files to indicate that the corresponding .js file has successfully passed the StandardJS linter.
  • Maxichrome's BUILD.toml is another straightforward TOML build file.
  • The BUILD.cjs for a blog uses JavaScript variables, but it is otherwise fairly simple.
  • Mergi's BUILD.mjs is a fairly complex build file which uses the power of JavaScript to remove duplication by refactoring out common elements. For example it extensively uses the ... spread operator to insert sub arrays into the deps arrays and to insert new dynamically generated targets into the main dictionary.

Advanced feature: JavaScript functions as actions

As an alternative to using the exec property to specify a shell command to execute, you can use the call property to specify a JavaScript function to be executed.

The function takes a deps parameter containing the values returned by the dependency functions.

The example below shows how this can be used to create something that works like a spreadsheet.

export default {

  result: {
    deps: ['B4'],
    exec: 'cat $0'
  },

  A1: {
    call: deps => 10
  },
  A2: {
    call: deps => 12
  },
  A3: {
    call: deps => 14
  },
  A4: {
    deps: ['A1', 'A2', 'A3'],
    call: ({ A1, A2, A3 }) => A1 + A2 + A3
  },

  'B%': {
    deps: ['A%'],
    call: deps => deps[0] * 100
  }
}

Build File Structure

A build file is an object with the following structure:

  • variable: variableValue
  • variable: variableValue
  • ...
  • target:
    • deps: dependencies
    • exec: shellCommand
    • call: JavaScript function
  • target:
    • deps: dependencies
    • exec: shellCommand
    • call: JavaScript function

The "call" property can only appear in the JavaScript syntax.

There can be any number of variables and targets.

A variableValue is either a string or an array.

A target can be

  1. a file, that may or may not already exist
  2. or a file pattern with a % representing a wildcard.
  3. otherwise just a label

The dependencies is an array of strings (or string that is a variable reference to an array variable), where each element of the array can be either:

  1. Another target in the build file
  2. An existing file or file pattern

The shellCommand is a string that gets executed by the Linux-style shell, but only if the target does not exist as a file, or if the target is older than all the deps.

The shellCommand may have some special patterns:

  • % expands to whatever matched the % in the target
  • $@ is replaced by the target (after % expansion)
  • $< is replaced by the first deps field (after % expansion)
  • $+ is replaced by the all the deps fields (after % expansion) separated by spaces
  • $(variable) is replaced by the corresponding variableValue. (And the variableValue may contain $(variable) patterns for other variables.)
  • $i (where i is an integer) is replaced by the path to a tmp file containing the return value of the call function the ith dep (zero-based).

(If you are familiar with makefiles you will note that the semantics are the same, though much simplified.)

Notes

The idea of using markdown came from maid.

Legal

Copyright (c) 2020 Eamonn O'Brien-Strain All rights reserved. This program and the accompanying materials are made available under the terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at http://www.eclipse.org/legal/epl-v10.html

This is a purely personal project, not a project of my employer.