dartle 0.25.0 dartle: ^0.25.0 copied to clipboard
A simple build system written in Dart. Tasks can be declared in a regular Dart file, or Dart applications can use this package to create their own build systems.
Dartle #
A simple task runner/build system/build library written in Dart.
Purpose #
The goal with Dartle is to define a (sometimes large) number of tasks where only a few of them are explicitly invoked by a human. This is accomplished by defining task phases and declaring interdependencies between tasks.
Dartle makes sure that every task that needs to run, but no others, actually run when you ask it to run one or more tasks.
Tasks on the same phase run in parallel, on their own isolates, and if a task fails, all running tasks are immediately cancelled.
What can I do with Dartle? #
- use it as a Dart-based task runner or build system (write tasks in Dart).
- build Dart projects using
DartleDart
support. - use it as a file system diff tool. Know when things have changed since last cached.
- create your own build system using Dartle as a library! See
jb
, for example.
Using Dartle #
For example, Dartle's own build (which uses Dartle's own support for Dart) has the following tasks
(as shown by running dartle --show-tasks
):
======== Showing build information only, no tasks will be executed ========
Tasks declared in this build:
==> Setup Phase:
* clean
Deletes the outputs of all other tasks in this build.
* cleanWorkingDirs [up-to-date]
Cleanup working dir before builds. Avoids caching generated files.
==> Build Phase:
* analyzeCode [up-to-date]
Analyzes Dart source code
* build [default] [always-runs]
Runs all enabled tasks.
* checkImports [up-to-date]
Checks dart file imports are allowed
* compileExe
Compiles Dart executables declared in pubspec. Argument may specify the name(s) of the executable(s) to compile.
* format [up-to-date]
Formats all Dart source code.
* generateDartSources [up-to-date]
Generates Dart source files
* runBuildRunner [up-to-date]
Runs the Dart build_runner tool.
* runPubGet [up-to-date]
Runs "pub get" in order to update dependencies.
* test [out-of-date]
Runs Dart tests.
==> TearDown Phase:
No tasks in this phase.
The following tasks were selected to run, in order:
cleanWorkingDirs
runPubGet
generateDartSources
runBuildRunner
checkImports
format
analyzeCode
test
build
Note: Tasks on the same column may run in parallel.
When you invoke, say, dartle analyzeCode
, Dartle will make sure that the
analyseCode
task will run, but also that all its dependencies, generateDartSources
,
format
, checkImports
and runPubGet
will run first as long as their runCondition
requires them to run. If any of these tasks doesn't need to run, it is automatically
skipped.
Dartle has several RunCondition
s to determine when a task is up-to-date or needs to run:
RunOnChanges
- run task if any inputs/outputs changed since last run.RunAtMostEvery
- run task at most every T, where T is a period of time.RunToDelete
- run task if any of its outputs exists.
There are also combiners like AndCondition
and OrCondition
(and you can define your own conditions).
For example, the runPubGet
task runs if pubspec.yaml
changes OR if it has not been run for
one week.
How to use #
Add dartle
to your dev_dependencies
:
dart pub add -d dartle
Write a dartle build file
A basic
dartle.dart
file can be automatically generated by invokingdartle
on a directory wheredartle.dart
does not exist yet.
dartle.dart
import 'package:dartle/dartle.dart';
final helloTask = Task(hello, argsValidator: const ArgsCount.range(min: 0, max: 1));
final byeTask = Task(bye, dependsOn: const {'hello'});
final cleanTask = createCleanTask(tasks: {helloTask, byeTask});
main(List<String> args) async =>
run(args, tasks: {helloTask, byeTask, cleanTask}, defaultTasks: {helloTask});
/// To pass an argument to a task, use a ':' prefix, e.g.:
/// dartle hello :joe
hello(List<String> args) =>
print("Hello ${args.isEmpty ? 'World' : args[0]}!");
/// If no arguments are expected, use `_` as the function parameter.
bye(_) => print("Bye!");
Notice that the dartle.dart
script should be very simple (a basic invocation to Dartle's run
, ideally), so it's clear at a
glance what the tasks are.
Put any logic you may need to write for tasks (and even the task declarations) in source file inside the
dartle-src
directory, as that's where Dartle looks for changes to the build (besides dartle.dart
and pubspec.*
).
Check Dartle's own dartle.dart file for a good example.
Run your build!
In dev mode (while you're setting up your build), use dart
to run the build file directly:
dart dartle.dart <tasks>
Notice that all
dev_dependencies
can be used in your build! And all Dart tools work with it, including the Observatory and debugger, after all this is just plain Dart!
Once you're done with the basics of your build, it is recommended to install Dartle for faster performance.
To install Dartle:
dart pub global activate dartle
Now, you can run your build with the dartle
command:
dartle <tasks>
dartle
automatically re-compiles thedartle.dart
script into an executable if necessary to make builds run so fast they feel instant!
Selecting tasks #
If no task is explicitly invoked, Dartle runs the defaultTasks
defined in the build, or does nothing if none was defined.
To run specific task(s), give them as arguments when invoking dartle
:
dartle hello bye
Output:
2020-02-06 20:53:26.917795 - dartle[main] - INFO - Executing 2 tasks out of a total of 4 tasks: 2 tasks selected, 0 due to dependencies
2020-02-06 20:53:26.918155 - dartle[main] - INFO - Running task 'hello'
Hello World!
2020-02-06 20:53:26.918440 - dartle[main] - INFO - Running task 'bye'
Bye!
✔ Build succeeded in 3 ms
Notice that Dartle will cache resources to make builds run faster. It uses the
.dartle_tool/
directory, in the working directory, to manage the cache. You should not commit the.dartle_tool/
directory into source control.
To provide arguments to a task, provide the argument immediately following the task invocation, prefixing it with :
:
./dartle hello :Joe
Prints:
2020-02-06 20:55:00.502056 - dartle[main] - INFO - Executing 1 task out of a total of 4 tasks: 1 task selected, 0 due to dependencies
2020-02-06 20:55:00.502270 - dartle[main] - INFO - Running task 'hello'
Hello Joe!
✔ Build succeeded in 1 ms
Declaring tasks #
The preferred way to declare a task is by wrapping a top-level function, as shown in the example above.
Basically:
import 'package:dartle/dartle.dart';
final allTasks = {Task(hello)};
main(List<String> args) async => run(args, tasks: allTasks);
hello(_) => print("Hello Dartle!");
This allows the task to run in parallel with other tasks on different Isolate
s (potentially on different CPU cores).
Notice that because the task may run on an
Isolate
, it must not depend on any global state. The function will not see changes made from the mainIsolate
or any other.
If that's not important, a lambda can be used, but in such case the task's name must be provided explicitly (because lambdas have no name):
import 'package:dartle/dartle.dart';
final allTasks = {Task((_) => print("Hello Dartle!"), name: 'hello')};
main(List<String> args) async => run(args, tasks: allTasks);
A Task's function should only take arguments if it declares an ArgsValidator
, as shown in the example:
Task(hello, argsValidator: const ArgsCount.range(min: 0, max: 1))
...
hello(List<String> args) => ...
A Task will not be executed if its argsValidator
is not satisfied (Dartle will fail the build if that happens).
Incremental Tasks #
For a Dartle task to become incremental, it only needs to have an action function that accepts an optional argument containing the ChangeSet
since the last build.
For example, the hello
function from the previous example would need to be declared as shown below to become an incremental task:
hello(List<String> args, [ChangeSet? changeSet]) async {
// TODO inspect changes to know what needs to be done
}
Importantly, the ChangeSet
must be an optional argument, otherwise the function won't match the signature expected by Dartle.
Task dependencies and run conditions #
A Task can depend on other task(s), so that whenever it runs, its dependencies also run (as long as they are not up-to-date).
In the example above, the bye
task depends on the hello
task:
Task(bye, dependsOn: const {'hello'})
This means that whenever bye
runs, hello
runs first.
Notice that tasks that have no dependencies between themselves can run at the same time - either on the same
Isolate
or in separateIsolates
(use the-p
flag to indicate that tasks may run in differentIsolate
s when possible, i.e. when their action is a top-level function and there's no dependencies with the other tasks).
A task may be skipped if it's up-to-date according to its RunCondition
. The example Dart file demonstrates that:
Task(encodeBase64,
description: 'Encodes input.txt in base64, writing to output.txt',
runCondition: RunOnChanges(
inputs: file('input.txt'),
outputs: file('output.txt'),
))
The above task only runs if at least one of these conditions is true:
output.txt
does not yet exist.- either
input.txt
oroutput.txt
changed since last time this task ran. - the
-f
or--force-tasks
flag is used.
If a RunCondition
is not provided, the task is always considered out-of-date.
To force all tasks to run, use the
-z
or--reset-cache
flag.
Help #
For more help, run dartle -h
. Proper documentation is going to be available soon!
Prior Art #
Dartle is inspired by Gradle and, loosely, Make and Apache Ant.