loading...
Cover image for Build your Go App using Gulpjs

Build your Go App using Gulpjs

potcode profile image Potpot Updated on ・5 min read

Brief

One day, an idea came to me: I want to write a toy parser, whatever it is.

Antlr is a great tool of such kind to help you create a rich-featured parser in minutes, but I'm not here to advertise something :) It's such a lovely tool that I soon fall in love with it, mentally for sure.

But I soon got into big trouble because it enforces you to name the filename the same as the grammar name, but the problem is that my FS (file system) is case-insensitive! They provide a tool grun to debug your grammar, but it requires you to compile the grammar to the Java target. That's OK, it only requires an extra line in Makefile, how hard could it be, I thought.

It turns out that I have oversight something, while my FS is case-insensitive, it outputs the Java source files in camelCase without surprise. What could it mean? it means that javac won't be happy to compile them.

Well, I'll write some bash lines in Makefile to transform those filenames before feeding them into javac, sounds doable right? And yeah, it soon becomes cumbersome, and the code is getting hard to understand. Most importantly, it doesn't work :(

Gulp to rescue

I have JavaScript background, I know there are tons of awesome build tools, and Gulp is quite the one, simple and lightweight.

About the task

The task is the basic unit of a Gulp file, you define tasks, either to serialize them in a row or to parallelize them in an asynchronized way, it's on your needs.

Go build

In Makefile, to build a Go binary is just one line code, in Gulp, on the other hand, we are in the JavaScript world or, more precisely, the NodeJS world.

Node has a built-in child_process module, it provides the interface to create the Node process, run some shell commands, etc. That's what I need.

const exec = util.promisify(require("child_process").exec);

const { stderr, stdout } = await exec("go build -o app .");
stderr && console.log(stdout);
stdout && console.error(stderr);
Enter fullscreen mode Exit fullscreen mode

Extract variables

It's a common practice that people define the command name and build flags as variables in Makefile, it's also possible and natural in Gulp:

const GOBIN = "app";
const TMP_DIR = "tmp";
const GO_BUILD = "go build";
const GCFLAGS = "all=-N -l";

// ...
exec(`${GO_BUILD} -v -o ${GOBIN}`)
Enter fullscreen mode Exit fullscreen mode

And there is an already full-featured language server, which supports jump to definition in modern IDE, awesome!

A helper runner

It's cumbersome to write the template code everywhere, it's better being DRY:

function exec_template(cmd, name, ...options) {
  const fn = async function (cb) {
    try {
      const { stderr, stdout } = await exec(cmd, ...options);
      stderr && console.log(stdout);
      stdout && console.error(stderr);
    } catch (error) {
      cb && cb(error);
    }
    cb && cb(null);
  };
  if (name !== undefined) {
    fn.displayName = name;
  }
  return fn;
}
Enter fullscreen mode Exit fullscreen mode

fn.displayName is used to configure the task name, since the exec_template is a high-order function and it returns an anonymous function. To give it a name will make the outputs more clearly.

name goes for fn.displayName

So...Antlr?

Let's get down to the business! The steps are listed below:

  • Empty the tmp directory
  • Generate Java files
  • Transform the Java files to PascalCase
  • Run javac to compile

cleanup

I'll use the del package for the task:

// for generated go parser files
const GRAMMAR_OUT_GLOB = "pkg/parser/**";

const del = require("del");

function clean_tmp() {
  return del([TMP_DIR]);
}

function clean_gen_parser() {
  return del([GRAMMAR_OUT_GLOB]);
}

gulp.task("clean", () =>
  del([
    // debugging resources
    TMP_DIR,
    // go binary
    GOBIN,
    // generated go parser files
    GRAMMAR_OUT_GLOB,
  ])
);
gulp.task("clean:tmp", clean_tmp);
gulp.task("clean:gen", clean_gen_parser);
Enter fullscreen mode Exit fullscreen mode

Done! if you run npx gulp --tasks, it will show in the tree.

Generate

Use the previously created helper runner:

const GRAMMAR = "Arithmetic";

exec_template(
  `antlr -Dlanguage=Java ${GRAMMAR}.g4 -o ${TMP_DIR}`,
  "java target" // annotate task name
)
Enter fullscreen mode Exit fullscreen mode

(It's a part of a complete task, I'll talk about it later).

Transform

I use pascal-case for the purpose:

const { pascalCase: pascal } = require("pascal-case");

function capitalize_java_class() {
  return gulp
    .src("tmp/*.java")
    .pipe(
      rename((p) => {
        p.basename = pascal(p.basename);
      })
    )
    .pipe(gulp.dest(TMP_DIR));
}
Enter fullscreen mode Exit fullscreen mode

It reads all Java files in tmp dir, and transform them to PascalCase.

That's a self-contained task, it's ok to leave it be. (Keep it in mind that it is for debugging, so I put the artifacts in tmp dir).

Javac? javac for sure

Like the way we build go:

exec_template(`javac *.java`, "compile java", {
  cwd: TMP_DIR,
})
Enter fullscreen mode Exit fullscreen mode

I can pass a cwd option, no more cd /xxx && javac ...

All together

gulp.task(
  "antlr:debug",
  gulp.series(
    "clean:tmp", // cleanup first
    exec_template(
      `antlr -Dlanguage=Java ${GRAMMAR}.g4 -o ${TMP_DIR}`,
      "java target"
    ),
    function capitalize_java_class() {
      return gulp
        .src("tmp/*.java")
        .pipe(
          rename((p) => {
            p.basename = pascal(p.basename);
          })
        )
        .pipe(gulp.dest(TMP_DIR));
    },
    exec_template(`javac *.java`, "compile java", {
      cwd: TMP_DIR,
    })
  )
);
Enter fullscreen mode Exit fullscreen mode

gulp.series will make them run in a row, and the whole task is named antlr:debug, a common naming convention for npm scripts.

Antlr for Go

const GRAMMAR_OUT = path.normalize("pkg/parser");

// served as a prerequisite
gulp.task(
  "antlr:go",
  exec_template(
    `antlr -Dlanguage=Go ${GRAMMAR}.g4 -o ${GRAMMAR_OUT}`,
    "generate go parser"
  )
);
Enter fullscreen mode Exit fullscreen mode

Modified Go build

const build = gulp.series(
  "clean:gen",
  "antlr:go", // see above
  exec_template(`${GO_BUILD} -v -o ${GOBIN}`, "build in local env")
);

gulp.task("build", build);
exports.default = build; // make it a default build task
Enter fullscreen mode Exit fullscreen mode

Complete Gulpfile

// Std lib
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const path = require("path");

// util
const { pascalCase: pascal } = require("pascal-case");

// Gulp
const gulp = require("gulp");
const rename = require("gulp-rename");
const del = require("del");

// Go build args
const GOBIN = "app";
const TMP_DIR = "tmp";
const GO_BUILD = "go build";
const GRAMMAR = "Arithmetic";
const GRAMMAR_OUT = path.normalize("pkg/parser");
const GCFLAGS = "all=-N -l";

// globs
const GO_SRC_GLOB = "*.go";
const ANTLR_SRC_GLOB = "*.g4";
const JAVA_SRC_GLOB = `${TMP_DIR}/*.java`;
const JAVA_CLASS_GLOB = `${TMP_DIR}/*.class`;
const GRAMMAR_OUT_GLOB = "pkg/parser/**";

function exec_template(cmd, name, ...options) {
  const fn = async function (cb) {
    try {
      const { stderr, stdout } = await exec(cmd, ...options);
      stderr && console.log(stdout);
      stdout && console.error(stderr);
    } catch (error) {
      cb && cb(error);
    }
    cb && cb(null);
  };
  if (name !== undefined) {
    fn.displayName = name;
  }
  return fn;
}

// clean targets
function clean_tmp() {
  return del([TMP_DIR]);
}

function clean_gen_parser() {
  return del([GRAMMAR_OUT_GLOB]);
}

gulp.task("clean", () =>
  del([
    // debugging resources
    TMP_DIR,
    // app build
    GOBIN,
    // generated go parser files
    GRAMMAR_OUT_GLOB,
  ])
);

gulp.task("clean:tmp", clean_tmp);
gulp.task("clean:gen", clean_gen_parser);

// served as prerequisite
gulp.task(
  "antlr:go",
  exec_template(
    `antlr -Dlanguage=Go ${GRAMMAR}.g4 -o ${GRAMMAR_OUT}`,
    "generate go parser"
  )
);

// build java target, for debugging purpose
gulp.task(
  "antlr:debug",
  gulp.series(
    "clean:tmp",
    exec_template(
      `antlr -Dlanguage=Java ${GRAMMAR}.g4 -o ${TMP_DIR}`,
      "java target"
    ),
    function capitalize_java_class() {
      return gulp
        .src("tmp/*.java")
        .pipe(
          rename((p) => {
            p.basename = pascal(p.basename);
          })
        )
        .pipe(gulp.dest(TMP_DIR));
    },
    exec_template(`javac *.java`, "compile java", {
      cwd: TMP_DIR,
    })
  )
);

// local build
const build = gulp.series(
  "clean:gen",
  "antlr:go",
  exec_template(`${GO_BUILD} -v -o ${GOBIN}`, "build in local env")
);

gulp.task("build", build);

// deployment build
const build_prod = gulp.series(
  "clean",
  "antlr:go",
  exec_template(
    `GOARCH=amd64 GOOS=64 ${GO_BUILD} -gcflags="${GCFLAGS}" -v -o ${GOBIN}`,
    "build in linux"
  )
);

gulp.task("build:prod", build_prod);

exports.default = build;
Enter fullscreen mode Exit fullscreen mode

Summary

While Go is good at building build tools, CI, and Cloud engines, it seems like Go is somewhat helpless when it comes to itself.

Anyway, there are some great tools in the NodeJS world, never getting bored trying new stuff in npm, you may find your own treasures there.

It's my first time posting tech articles here and I'm not a native speaker, thus if there are any expression issues, please let me know.

Happy hacking!

Discussion

pic
Editor guide