target()
The target()
function defines a target in the build system.
Parameters
-
name: string
A unique identifier for the target. Responsible for invoking, distinguishing and caching the target. If the target is an artifact, it should be a file path (string).
-
deps?: Dependencies
Dependencies of this target. If a dependency is an artifact, it should be a file path (string).
-
virtual?: boolean
If the target is virtual (i.e. no artifact, analogous to PHONY in Makefile).
-
doc?: string
The documentation of the target.
-
rebuild?: 'always' | 'never' | 'onChanged'
The rebuild mode of the target.
always
: always rebuild the target.never
: never rebuild the target.onChanged
: rebuild the target if any of its dependencies has been changed. When the dependencies are not specified, the target will always be rebuilt.onChanged
will not track the changes of directory contents.
-
logError?: boolean
Whether to log errors when building the target fails (default to true).
-
build: (arg: { deps: InferTargets, target: string }) => void | Promise<void>
The build logic of the target.
Reference source code.
interface TargetParams
{
/**
* A unique identifier for the target.
*
* Responsible for invoking, distinguishing and caching the target.
*/
name: string,
/**
* Dependencies of the target.
*
* If a dependency is an artifact, it should be a file path (string).
*/
deps?: Deps,
/**
* If the target is virtual (i.e. no artifact, analogous to PHONY in Makefile).
*/
virtual?: boolean,
/**
* The documentation of the target.
*/
doc?: string,
/**
* The build logic of the target.
*/
build: (arg: { deps: InferencedDeps, target: string }) => void | Promise<void>,
/**
*
* The rebuild mode of the target.
*
* - 'always': always rebuild the target.
*
* - 'never': never rebuild the target.
*
* - 'onChanged': rebuild the target if any of its dependencies has been changed.
*
* !!! PS1: when the dependencies are not specified, the target will always be rebuilt.
*
* !!! PS2: 'onChanged' will not track the changes of directory contents
*/
rebuild?: 'always' | 'never' | 'onChanged',
/**
*
* Whether to log errors when building the target fails (default to true).
*/
logError?: boolean
}
Retrieving the Computed Target Name
In practice, the name
parameter of the build
function may be a computed one. Hence, we might reference the target name in the build logic using the target
parameter.
NM.target(
{
name: 'output.txt',
async build( { target } )
{
console.log(target) // 'output.txt'
}
})
Type-safe Retrieval of Dependencies
The deps
parameter of target()
have multiple forms that ease the retrieval of dependencies in the build logic using a type-safe manner.
A dependency can be the following cases:
string
- the return of
target()
(type:Target
) - an array of the above types or an async generator of the above types.
- a record with the above types as values.
The source code defines the following types including BuildDependency
:
export type BuildDependency =
| string
| Target
export type BuildDependencySugar =
| AsyncGenerator<BuildDependency, any, any>
| BuildDependency[]
| BuildDependency
// the type of `deps` in `target()`
export type BuildDependencies =
| BuildDependencySugar
| { [key: string]: BuildDependencySugar }
When retrieving dependencies in the build logic, the deps
parameter gives you an "inferenced" result of your dependencies:
const list_targets: NM.Target[] = [NM.target({ name: 'b.txt', ... })]
async function* async_targets()
{
yield 'c.txt'
for await (...) yield ...
}
NM.target(
{
name: 'build',
virtual: true, // like phony in Makefile
deps: { a: 'a.txt', b: list_targets, c: async_targets },
async build({ deps })
{
console.log(deps.a) // 'a.txt'
console.log(deps.b) // [ 'b.txt' ]
console.log(deps.c) // [ 'c.txt', ... ]
}
})
The mapping of dependencies to the deps
parameter is as follows:
Dependencies | Inferenced Dependencies |
---|---|
BuildDependency |
string |
BuildDependency[] |
string[] |
AsyncGenerator<BuildDependency> |
string[] |
{ a: BuildDependency } |
{ a: string } |
{ a: BuildDependency[] } |
{ a: string[] } |
{ a: AsyncGenerator<BuildDependency> } |
{ a: string[] } |
You might be refer to the Dependency definition.
Grouping Dependencies
You can group dependencies using Record
-style composition, which helps your organize your dependencies.
import * as NM from 'https://github.com/thautwarm/nomake/raw/v0.1.5/mod.ts'
function glob(options: { root: string, suffix: string, excludeParts?: string[] })
{
const { root, suffix, excludeParts } = options
return new NM.Path(root).fwalk(
(p) => p.endsWith(suffix) && !excludeParts?.some(part => p.parts.includes(part)),
{ includeDir: false, recursive: true }
)
}
NM.target(
name: './linux-x64/app',
deps:
{
cppSources: glob({ root: 'src', suffix: '.cpp', excludeParts: ['generated'] }),
libraries: ['lib1.so', 'lib2.so'],
async build( { target, deps: { cppSources, libraries } } )
{
// you might do something like:
// gcc $cppSources -l:$libraries -o $target
}
})
Virtual Targets
A virtual target is a target that does not produce an artifact. It is analogous to the PHONY
target in Makefile.
import * as NM from 'https://github.com/thautwarm/nomake/raw/v0.1.5/mod.ts'
NM.target(
{
name: 'clean',
virtual: true,
async build()
{
await new NM.Path('build').rm(
{
// no worries as all the parameters have
// 1. **intellisense support**
// 2. **exhaustive type checking**
// 3. **documentations**
onError: 'ignore',
recursive: true
}
)
}
})
Caching & Rebuilding
Similar to Makefile, NoMake provides a mechanism to automatically rebuild targets based on the changes in their dependencies.
However, flexibility are left to the user to specify the rebuild mode of the target.
The rebuild
parameter of target()
specifies the rebuild mode of the target. It can be one of the following:
always
: always rebuild the target.never
: never rebuild the target.onChanged
: rebuild the target if any of its dependencies has been changed.
Note
The default behavior when rebuild
is not specified:
- When no
deps
are specified,rebuild
is set toalways
- When any
deps
are specified,rebuild
is set toonChanged
The following code demonstrates the default behavior of the rebuild
parameter:
import * as NM from 'https://github.com/thautwarm/nomake/raw/v0.1.5/mod.ts'
NM.target(
{
name: 'clean',
virtual: true,
async build()
{
// this build logic will be always executed
}
})
NM.target(
{
name: 'output.txt',
deps: ['input.txt'],
async build({ deps, target })
{
// this build logic will be executed only when 'input.txt' is changed
}
})
Adding Documentations
The doc
parameter of target()
specifies the documentation of the target.
So far, only virtual targets have been documented. The documentation of the target can be accessed via the help
option.
import * as NM from 'https://github.com/thautwarm/nomake/raw/v0.1.5/mod.ts'
NM.option(
'legacy',
{
callback: ({ value }) => { /* do stuff with value */ },
doc: 'Enable legacy mode.'
})
// see parseOptions() for more details
NM.parseOptions()
NM.target(
{
name: 'build',
virtual: true,
doc: 'This target generates the output file.',
async build()
{
// build logic
}
})
// trigger the build process
await NM.makefile()
You can trigger the documentations by running the following command:
sh> deno run -A build.ts help
Targets:
[build] This target generates the output file.
Options:
[-Dlegacy=value] Enable legacy mode.