Skip to content

Design Notes

Roman Kuzmin edited this page Dec 7, 2018 · 20 revisions

See also:


General purpose task runner

In spite of the name, Invoke-Build is not necessarily for building something. It offers programming with tasks and their relations. Scripts with tasks are often easier to compose and use than traditional scripts with functions, see Concepts.

Invoke-Build is designed to be a general purpose programming tool in the first place. It is also a build automation tool just because typical build workflows are easily described and automated with tasks.


Why is it a script?

Why is the command Invoke-Build implemented as a script, i.e. not a function of a script module or library?

The main reason which overweights all disadvantages is that a script has its own scope which is referenced using the prefix, e.g. $script:Variable. As a result, tasks may maintain their shared state in a very natural way, as script scope parameters, variables, functions, aliases, and etc.

The script may be called directly, i.e. without loading a module. Auto-loading of modules in PS v3.0+ is not always applicable, e.g. it may be disabled or a module is not in the module path or PS v2.0 is still in use.

There are minor disadvantages. One of them is not the best possible performance on multiple calls. But the cost is comparable with inevitable cost of parsing and running build scripts.

Some internals are exposed to user code. The work around this is use of weird names so that chances of conflicts are small. Also, internals do not pollute the calling scope, they are gone when a build completes.

In PowerShell 5.0 or with PowerShellGet Invoke-Build is available as a module (Install-Module InvokeBuild). The module is still based on scripts, it just provides aliases for invoking them.


Jobs instead of Depends and Action

Unlike other tasks runners, Invoke-Build does not have an explicit parameter for task dependencies. Instead, it uses the parameter Jobs, where jobs are task references and actions in any required number and order. This covers the classic dependency scenarios and more complex: after-tasks, multiple actions.

See Task dependency syntax clarity


Aliases as DSL commands

Invoke-Build provides most of its its DSL commands for scripts as aliases, not functions, because aliases have higher precedence on command name resolution.

Imagine, task is a function and a PowerShell session already has an alias task of Do-Something defined. Then build scripts would not work properly because Do-Something is actually invoked on their task statements.

Thus, Invoke-Build exposes the function Add-BuildTask (Verb-PrefixNoun, as recommended in PowerShell) and its short alias task as the DSL command for scripts. In this case, the existing alias task is not a problem, it is hidden by the alias of Add-BuildTask.


Continue on error

Tasks do not have an option like "continue on error". A task does not know whether or not its failure is fatal for a build. It is a caller of a task decides, i.e. another task or some Invoke-Build command. Start a task reference with "?" in order to allow its failures.

Example. The task Help makes help files. It is referenced by two other tasks:

task Help {<# make help files #>}

task Package ..., Help, {<# make a package #>}

task Test ..., ?Help, {<# invoke tests #>}

The task Package is supposed to fail if Help fails. So Package references Help normally as "Help".

The task Test is not supposed to fail if help files are not created, tests should be invoked anyway. That is why Test uses a safe reference to Help as "?Help".


Removed parameters

Why do some commands copy their parameters to private variables and remove parameter variables after that?

This is done in order to hide the engine presence from a user code as much as possible, as if this code is invoked directly in PowerShell, not by the engine. Not removed parameters of engine commands would be exposed to scripts with some adverse effects.

For example the parameter Result would hide an existing parent scope variable Result which is supposed to be used in a build script.

Or a user task may use its own local variable Result. If by mistake or not it is used uninitialised then the value of not removed parameter Result would be used unintentionally.


Weird internal names

Invoke-Build uses internal functions and variables. Even being declared private some of them are exposed to various code defined in build scripts. In order to minimize chances of conflicts weird internal names starting with * are used.

Invoke-Build tries to minimize the number of variables exposed to user code in order to reduce noise on tasks debugging. Variables are often reused and their names become meaningless, so that just some short names are used.


Source code is difficult

This is true for the engine code, the script Invoke-Build.ps1. The engine is feature complete, mostly bugs free, well documented and covered by tests. The code was optimized for better performance when the time was right.

Users do not have to look at the engine code in order to learn how to use it. The documentation is provided. Help alone is larger than the source code. And the wiki makes the documentation almost exhaustive.

Contributors may have to understand the code. The developer design notes are provided to make this easier, see the top of this page. If something is not clear it will be explained.