Using NimScript for your build system

Welcome to this tutorial on using NimScript for your build system! In this tutorial, we'll explore how to use NimScript to define tasks and automate your build process. NimScript is a powerful and flexible scripting language that is a sub-dialect of the Nim programming language.

Benefits

NimScript is a versatile scripting language that can be used on any platform where Nim can run. With NimScript, you can enjoy the same syntax, style, and ecosystem as compiled Nim. This means that developers won't need to learn anything new or switch contexts. [source: https://nim-lang.github.io/Nim/nims.html#Benefits]

Getting Started

To use NimScript as your build system, you can use the task template. With this, you can write tasks that can be executed with a working Nim compiler. You can write these tasks either in the .nimble file or in a separate .nims file, which can be executed by calling nim taskname filename.nims directly. Running nimble tasks in the package root will display a list of available tasks.

Alright, the first thing you gotta do is figure out how to run the tests you wrote for your project, which are stored in the tests folder. By default, Nimble has a pre-defined test task that compiles and runs all files in the tests directory that begin with 't' in their filename. However, you might want to customize this test task to do something like generating coverage reports for each individual test file.

An issue that commonly arises when running tests is the need to import the associated package. It's important to avoid accidentally using the package that's already installed locally in your ~/.nimble directory. To ensure that everything is resolved correctly, you'll need to modify the path. Simply use the --path:"../src/" argument and place it in a nim.cfg file in the tests/ directory to make it permanent. This should be sufficient to prioritize it over any locally installed package.

You can control the behavior of your script by setting the global mode variable to one of three options: Silent, Verbose, or Whatif. Setting the mode to ScriptMode.Verbose, for instance, will cause the script to output every command that is being executed to your terminal.

Run tests with switch and setCommand

You can use the switch procedure to convert command line switches, and then use the setCommand procedure to specify the Nim command to be executed after the completion of the current Nimscript. Here's an example:

task test, "Run the tests":
  # Lists of available options https://nim-lang.github.io/Nim/nimc.html
  switch("define", "debug")
  switch("run")
  setCommand "c", "tests/all.nim" # runs nim

It's important to note that although the shorthand for the switch template exists with the -- prefix (e.g. --define:release), it can be frustrating to discover that its arguments are passed verbatim using astToStr. This means that stringifying variables, for example, will not work as expected.

A common approach is to use a all.nim file that simply imports all the tests from the test folder. However, as we will see later, there are other ways to accomplish the same thing.

Run tests with exec

Using exec in your NimScript allows for more flexibility in executing programs when running tasks, including the Nim compiler. For instance, you can override the default test task like this:

import std/[os, strutils]

task test, "Run the tests":
  for f in listFiles("tests"):
    if f.startsWith("t") and f.endsWith(".nim"):
      exec("nim c -r --hints:off -w:off " & quoteShell(f))

When calling the nim compiler from your Nimscript using the exec command any switch calls defined in the Nimscript will be ignored.

Bonus: Task that produces coverage reports

Congratulations on making it this far! Here's a task that can generate a coverage report for your tests. However, please note that the report is outputted in C, not Nim, so some experience with reading the Nim compiler's output may be required.

task cov, "Produce coverage reports":
  mkDir "build"
  let name = "all" # The name of the test file
  let target = "myFunc" # Target a specific library function.
  exec "nim c --cc:clang -d:danger -d:useMalloc -t:\"-fprofile-instr-generate -fcoverage-mapping\" -l:\"-fprofile-instr-generate -fcoverage-mapping\" -g -f --out:build/" & name & " tests/" & name & ".nim"
  withDir("build/"):
    exec "LLVM_PROFILE_FILE=\"" & name & ".profraw\" ./" & name
    exec "llvm-profdata merge -sparse " & name & ".profraw -o " & name & ".profdata"
    exec "llvm-cov show -instr-profile=" & name & ".profdata -name=" & target & " ./" & name

One way to learn more about how tasks are implemented in various popular packages is to search nimble.directory. You can find many interesting tasks there, such as generating APK files of Android applications or setting up build environments.

Conclusion

In conclusion, NimScript can be a powerful and flexible build tool that can be used in many different projects. Its ability to run on multiple platforms and its familiar syntax make it a great option for developers who are already using Nim. With NimScript, you can define tasks, switch command-line arguments, and even call the Nim compiler itself. Additionally, you can use NimScript to generate coverage reports for your tests. Overall, if you're looking for a lightweight and customizable build tool, NimScript is definitely worth considering.

Comments

Popular posts from this blog

Naylib Goes Mobile: Porting to Android in Just 3 Days!

An introduction to ECS by example