The root of all innovation is laziness. This is especially true for the IT field where we are driven by process automation. A process that is particularly annoying, so it needs to be automated, is deployment. Deployment also includes the critical step of building a software, i.e. compiling and modifying the sources to have as a result a running application. At the beginning, people used a set of scripts to perform the same building process. Once the same set of scripts had to be copied and used again, it was obvious that a common system had to be created.
The software Make has been proven to be a very good solution for the problem. It’s flexible and follows a clear concept, but this flexibility comes at a price. Many of the great software innovations we are building cannot work with Make. We don’t have any extension or package, and an extensible configuration isn’t possible. To avoid these issues, the patterns of generating Makefiles, using external scripts, or having multiple Makefiles are quite common.
We should not have to fall back to an ancient tool-chain, just to have a working build system. We should embrace modern technology and a software stack that we know very well. In this article, I’ll introduce you to Jake. It combines the concept and the advantages of Make with a Node.js environment. This means we can use any module we like and that scripting is not only possible, but encouraged.
Specialized Task Runners vs Generic Build Tools
The idea of using a JavaScript environment for creating a build tool isn’t new. Every front-end developer today knows Grunt or Gulp. And in many scenarios, these tools should still be the primary choice. So the question is: Where should I use what tool?
For web-related tasks, such as minifying JavaScript files, prefixing CSS, or optimizing images, task runners are to be preferred. But even in such cases, Jake could be considered as an option because it’s a superset of the mentioned tools. It’s much less specialized, and there is nothing against using it in that context.
With this in mind, Jake is a better fit either if you want to replace another build tool such as Make or if you have another build process that follows the classic dependency-rule approach, an approach where we have a rule that specifies one to many dependencies. The beauty of a generic build tool is that it can be used in many contexts.
Before we discuss the advantages of Jake in details, it’s worth taking a look at Make and its brilliant concept.
A Look at Make
Every build system needs three things:
- Tools (either software or functions) to do the work
- Rules to specify what kind of work to do
- Dependencies to specify what kind of rules to apply
The work is usually a transformation of a source file into another file. Basically, all the operations in such build system are immutable, which gives us maximum agility and predictability.
Jake
The Node.js ecosystem features many great modules that enhance the user’s terminal experience. This is especially handy for a build tool. Due to legacy (and simple) DOM operations, JavaScript is a very string-focused language. This plays really well together with the Unix command line philosophy. But there is another reason why Jake is better than its competitors: special functions for testing and watching file changes are already integrated.
Jake wraps the rule-dependency approach in a hierarchy called tasks. These tasks can run in parallel and will invoke events that can be used to control the flow despite of concurrency. The tasks can be clustered in groups like rule, file, directory, package, publish, test, and watch. These are more than enough options to create truly useful build processes that are highly flexible and do exactly what we want. Most notably, watch tasks give us the ability to invoke some actions such as running the build process once certain files or directories have changed.
Like other build tools, Jake is using a special kind of file to describe the build process. This file is called Jakefile and use Jakefile.js as its default name. However, a short list of other names, such as Jakefile, can be also used and they are automatically recognized. It’s also possible to use custom file names, but in this case you have to specify the name of the file used explicitly.
A Jakefile is a file that includes required modules, defines all tasks, and sets up some rules. To apply some structure to our tasks we may also use a special construct called namespace. We won’t go into namespaces in this article, but the concept itself may be useful to reduce the potential chaos for larger Jakefiles.
A Sample Jakefile to Compile an Application
Before we start with a sample Jakefile, we must have Jake installed. The installation is straightforward if you use npm as you only need to enter the command:
npm install -g jake
The example I’m going to explain is a little bit long, but it’s close to a real-world code and illustrates several important concepts. We’ll go over all the lines by taking a glimpse at each block. We’ll pretend to compile some C++ application, but the example does not require any knowledge about C++.
The first line of the file is:
var chalk = require('chalk');
Here we are including a Node.js module called “chalk”. chalk is a very useful tool for coloring the terminal output and it should definitely be a part of most Jakefiles.
As already mentioned we can make full use of the Node.js ecosystem. So, in the next section we specify some constants that are important to have more flexibility. If we use JavaScript, we have to use it correctly.
var sourceDirectory = 'src';
var outputDirectory = 'bin';
var objectDirectory = 'obj';
var includeDirectory = 'include';
var applicationName = 'example';
var isAsync = { async: true };
The next lines also define some constants but this time we also allow external arguments to override our own definitions. We don’t want to rewrite the build process just to try out another compiler, or to specify different flags. Using these arguments is possible via the process.env
object as shown below:
var cc = process.env.cc || 'g++';
var cflags = process.env.cflags || '-std=c++11';
var options = process.env.options || '-Wall';
var libs = process.env.libs || '-lm';
var defines = process.env.defines || '';
Now the real deal starts. We use the jake.FileList
constructor function to create a new file list, which includes all files having .cpp as their extension in the directory of all source files. This list is then used to create a similar file list with all object files. These files might not exist at that point, but this is no much of a problem. In fact, we don’t use the file retrieval for specifying the list of object files but some JavaScript mapping from the existing file list represented as an array. The code implementing this description is shown below:
var files = new jake.FileList();
files.include(sourceDirectory + '/*.cpp');
var target = outputDirectory + '/' + applicationName;
var objects = files.toArray().map(function(fileName) {
return fileName
.replace(sourceDirectory, objectDirectory)
.replace('.cpp', '.o');
});
Then, a few handy utilities come into play. We define functions for the output, such as plain information or warnings:
var info = function(sender, message) {
jake.logger.log(['[', chalk.green(sender), '] ', chalk.gray(message)].toMessage());
};
var warn = function(sender, message) {
jake.logger.log(['[', chalk.red(sender), '] ', chalk.gray(message)].toMessage());
};
Once done, we set up a regular expression for consuming all object files. Later, we will use this as a condition for our rule to create an object file from a source file. We also define a function that will be used to convert the proper object file name back to its corresponding source file name:
var condition = new RegExp('/' + objectDirectory + '/.+' + '\\.o$');
var sourceFileName = function(fileName) {
var index = fileName.lastIndexOf('/');
return sourceDirectory + fileName.substr(index).replace('.o', '.cpp');
};
We are already down in the rabbit hole. Now we need to define two functions that serve as the access points for doing some real work:
- Linking existing object files together. They form an executable in the given scenario
- Compiling a source file to an object file
These two functions use the provided callback. The callback will be passed to the jake.exec
function which is responsible for running system commands:
var link = function(target, objs, callback) {
var cmd = [cc, cflags, '-o', target, objs, options, libs].join(' ');
jake.exec(cmd, callback);
};
var compile = function(name, source, callback) {
var cmd = [cc, cflags, '-c', '-I', includeDirectory, '-o',
name, source, options, '-O2', defines].join(' ');
jake.exec(cmd, callback);
};
In the next snippet, two crucial parts of a Jakefile are revealed:
- We set up a transformation rule to create object files from source files. We use the previously defined regular expression and function to get all requested object files with their corresponding source files. Besides, we annotate this to be able to run asynchronously. So, we can run multiple source-to-object file creations in parallel. In the callback we close the rule by calling the built-in
complete
method - We define a file rule that creates a single target from multiple dependencies. Once again, the function is marked as being able to run asynchronously. Using the
jake.mkdirP
method we make sure that the directory for storing the output does exist, otherwise it is created.
With these two kinds of rules we are able to set up some tasks. Tasks are rules that can be accessed from the build tool via the command line.
rule(condition, sourceFileName, isAsync, function() {
jake.mkdirP(objectDirectory);
var name = this.name;
var source = this.source;
compile(name, source, function() {
info(cc, 'Compiled ' + chalk.magenta(source) + ' to ' +
chalk.magenta(name) + '.');
complete();
});
});
file(target, objects, isAsync, function() {
jake.mkdirP(outputDirectory);
link(target, objects, function() {
info(cc, 'Linked ' + chalk.magenta(target) + '.');
complete();
});
});
Finally, we set up three tasks. One to create the documentation, another to compile the application, and a default task that is executed when jake
is invoked on the command line without any argument. The default task has the special name default
and it relies on the other two defined tasks. The documentation task is empty on purpose. It only exists to illustrate the concept of multiple tasks.
desc('Creates the documentation');
task('doc', [], isAsync, function() {
info('doc', 'Finished with nothing');
});
desc('Compiles the application');
task('compile', [target], isAsync, function() {
info('compile', 'Finished with compilation');
});
desc('Compiles the application and creates the documentation');
task('default', ['compile', 'doc'], function() {
info('default', 'Everything done!');
});
Running a special task like compile
is possible by running jake compile
on the terminal. All the defined tasks and their respective descriptions are shown by running the command jake -ls
.
Conclusion
Jake is a powerful build tool that should be installed on every computer equipped with Node.js. We can leverage our existing JavaScript skills to create seamless build scripts in an efficient and lightweight manner. Jake is platform-independent and uses the best features from a long list of possible build tools. Besides, we have access to any Node.js module or other software. This includes specialized task runners that solve the issue of creating front-end build processes.
Frequently Asked Questions (FAQs) about Jake
What is Jake and how does it differ from Make?
Jake is a JavaScript build tool that runs on Node.js. It is similar to Make, a widely used build automation tool, but it has some key differences. Jake is written in JavaScript and runs on Node.js, which means it can be used across different platforms without the need for a separate installation. This makes it more portable and easier to use than Make, which is written in C and requires a Unix-like environment to run. Jake also has a more modern and flexible syntax, which can make it easier to write complex build scripts.
How do I install Jake?
Installing Jake is straightforward. You need to have Node.js and npm (Node Package Manager) installed on your system. Once you have these, you can install Jake globally by running the command npm install -g jake
in your terminal. This will make Jake available for use in any directory on your system.
How do I create a Jakefile?
A Jakefile is a JavaScript file that defines the tasks for Jake to run. You can create a Jakefile by creating a new file named Jakefile.js
in your project directory. Inside this file, you can define tasks using the desc
and task
functions provided by Jake. For example, a simple Jakefile might look like this:desc('This is a simple task.');
task('simple', function () {
console.log('Simple task running.');
});
How do I run tasks with Jake?
To run a task with Jake, you use the jake
command followed by the name of the task. For example, if you have a task named ‘build’ in your Jakefile, you would run it by entering jake build
in your terminal. If you don’t specify a task, Jake will run the ‘default’ task.
Can I use Jake with other JavaScript tools and libraries?
Yes, Jake can be used with any JavaScript tool or library that can be run with Node.js. This includes tools like Babel, Webpack, and ESLint, as well as libraries like React and Vue.js. You can define tasks in your Jakefile to run these tools or libraries as part of your build process.
How do I handle dependencies with Jake?
Jake has built-in support for handling task dependencies. You can specify dependencies when defining a task by including an array of task names as the second argument to the task
function. For example, if you have a task named ‘compile’ that depends on a task named ‘clean’, you would define it like this:task('compile', ['clean'], function () {
// Compilation code here
});
Can I use Jake for continuous integration?
Yes, Jake can be used as part of a continuous integration (CI) process. You can define tasks in your Jakefile to run tests, compile code, and perform other build steps, and then run these tasks automatically whenever changes are pushed to your repository. Many CI services, like Jenkins and Travis CI, can run Jake tasks as part of their build process.
How do I debug a Jakefile?
Debugging a Jakefile is similar to debugging any other JavaScript file. You can use console.log
statements to print out information, or use a debugger like the one built into Node.js or a tool like Visual Studio Code. If you’re having trouble with a specific task, make sure to check the task definition and any dependencies it might have.
Can I use Jake with TypeScript?
Yes, you can use Jake with TypeScript. You can define tasks in your Jakefile to compile TypeScript code, and you can even write your Jakefile in TypeScript if you prefer. To do this, you would need to compile your Jakefile to JavaScript before running it with Jake.
Where can I find more information about Jake?
The official Jake website (https://jakejs.com/) is a great resource for learning more about Jake. It includes a comprehensive guide to the Jake API, as well as examples and tutorials. The Jake project is also hosted on GitHub (https://github.com/jakejs/jake), where you can find the source code, report issues, and contribute to the project.
Florian Rappl is an independent IT consultant working in the areas of client / server programming, High Performance Computing and web development. He is an expert in C/C++, C# and JavaScript. Florian regularly gives talks at conferences or user groups. You can find his blog at florian-rappl.de.