Project tasks#

A task is a build or maintenance command the manifest names. nupp task <name> builds what that task says to build and then runs its argv with your arguments appended.

nupp tasks                        # build targets, test, fixpoint, and tasks
nupp tasks docs-serve             # one entry's effective configuration
nupp task docs-serve              # run it
nupp task docs-serve --no-build   # arguments after the name reach the command

Task configuration#

A task is an entry in the manifest's tasks table, keyed by the name nupp task takes:

return {
   include = { "src" },

   tasks = {
      ["docs-serve"] = {
         description = "Build the docs site and serve it",
         argv = { "node", "scripts/docs-serve.mjs" },
      },

      migrate = {
         description = "Apply pending schema migrations",
         build = "app",
         argv = { "luajit", "build/app/migrate.lua" },
         env = { DATABASE_URL = "postgres://localhost/app" },
      },
   },
}
Key Required Means
argv yes The command, as an argv array of strings
description no One line, shown by nupp tasks
build no A build target to build before the command runs
cwd no Working directory, relative to the project root
env no Environment variables, as string to string

Any non-empty string is a name. One that is not a Lua identifier is written as a key, as ["docs-serve"] above. The five keys are the whole set, and a key that is not among them is refused by name before anything runs, with the nearest one when there is a candidate:

nupp: tasks.release has no key "descrption"; did you mean "description"?

Writing a task in Nupp#

A task's argv is any command, so a maintenance tool written in Nupp is a build target plus a task that runs what the target produced. Write the tool:

local channel = os.getenv("RELEASE_CHANNEL") or "dev"
print("stamping " .. channel .. " release")
for index = 1, select("#", ...) do
    print("argument: " .. tostring((select(index, ...))))
end

A bundle target compiles it to one file that luajit can run directly, and the task builds that target before running it:

return {
   include = { "src" },

   build = {
      outDir = "build",
      default = "tools",
      targets = {
         tools = {
            kind = "bundle",
            description = "Build the maintenance tools",
            entries = { "tools.release" },
            output = "build/release.lua",
         },
      },
   },

   tasks = {
      release = {
         description = "Stamp a release archive",
         build = "tools",
         argv = { "luajit", "build/release.lua" },
         env = { RELEASE_CHANNEL = "stable" },
      },
   },
}
nupp task release v1.2.0
stamping stable release
argument: v1.2.0

A modules target works the same way with one more step: it writes build/tools/release.lua beside the rest of the project's modules, so the command needs the output directory on its path, as env = { LUA_PATH = "build/?.lua;;" }. See Build system for what each target kind produces.

Building a target first#

build names one of build.targets, which is built before the command runs. A build that fails stops there: the command does not run and nupp task exits 1. Unlike test, which always builds because there is exactly one test command and it answers for code that has to exist first, a task builds only when it says to. Most tasks name no target at all, and a manifest with no build section can still define and run them.

Naming a target also puts the project's rock tree on LUA_PATH and LUA_CPATH for the command, since a task that built something usually wants to run it, and those rocks live in a tree the project owns rather than a global one. See Testing for the same arrangement under test.

Environment#

env adds to what the command already inherits rather than replacing it. A task that sets CHANNEL still sees HOME, PATH, and everything else the shell that ran nupp task had:

envcheck = {
   argv = { "sh", "-c", "echo home=$HOME channel=$CHANNEL" },
   env = { CHANNEL = "beta" },
},
nupp task envcheck
home=/home/you channel=beta

Keys and values are strings, and a key has to be a plain identifier. Values are quoted for the platform's shell, so spaces and quotes in a value are safe.

Dive deeper

A key is restricted to an identifier because the assignment is written in front of the command rather than handed to a spawn call. The build tool starts a program by giving one string to the system shell, so cd and the shell's own assignment syntax are what an environment and a working directory can be said with, and Windows and POSIX disagree about how to quote both. Callers pass an argv and never build a command line, which is what keeps that disagreement in one module.

Working directory#

cwd runs the command from a directory relative to the project containing nupp.lua. Without it, the command runs from that project root. This is useful for tools that are projects of their own:

format = {
   description = "Format every source language",
   cwd = "tools",
   argv = { "nupp", "run", "tecs/dev/formatmain.nupp", "format" },
},

Here the nested nupp command sees tools/nupp.lua as its manifest. The task's build, when present, still names and builds a target from the outer manifest before the command starts.

Arguments and exit status#

Arguments after the task name belong to the command, so nupp task does not read them. The consequences:

  • -h and --help are answered only before the name. After it they are the command's, and nupp task -- --help names a task called --help.
  • The exit status is the command's own. A task whose command exits 7 makes nupp task exit 7.
  • A name no task matches exits 1, and says to run nupp tasks for the list.

The command runs with the configured cwd, or the project root when cwd is absent, whatever directory you invoked nupp task from.

Listing tasks and targets#

nupp tasks covers everything the project can run, not only the tasks table: each build target, with the default one marked, the default or configured test command, the self-host action nupp fixpoint runs, and each named task.

nupp tasks
release - Stamp a release archive
tools (default) - Build the maintenance tools

nupp tasks <name> prints that entry's effective configuration, filled in with the defaults a build would actually use, and prints only the fields the entry has, so the shape of the output says what kind of entry it is:

nupp tasks release
Name: release
Default: no
Description: Stamp a release archive
Kind: task
Category: task
Command: nupp task release
Build target: tools
Arguments:
  - luajit
  - build/release.lua
Environment:
  - RELEASE_CHANNEL=stable

Both forms take --json, and nupp tasks --schema prints the shape they write. A task named the same as a build target is listed as command:<name>, because the build target holds the plain name; nupp task <name> still runs the task, since it looks only at the tasks table.

Limits#

nupp task runs one argv and returns its exit code, the same as nupp test does. It is not a process supervisor: a task that starts a server runs in the foreground until it is stopped, exactly as running that command directly would.

Tasks do not depend on other tasks. The one ordering a task can express is build, and anything more is the command's own business, which is what a shell or a script written in Nupp is for.