Starting tasks
Starting tasks is available through the TaskContext
, and require a task identifier and task factory for the newly started task. The task identifier uniquely identifies a started task instance and allows every other task to retrieve the results for it. The task factory is the implementation of the task that will be newly executed.
In the following example we showcase task usage with the compilation of C++ source files. The user facing task implementation is the following:
@SakerInput(required = true)
public WildcardPath Sources;
@Override
public Void run(TaskContext taskcontext) throws Exception {
NavigableMap<SakerPath, SakerFile> files = taskcontext.getTaskUtilities()
.collectFilesReportAdditionDependency(null,
WildcardFileCollectionStrategy.create(Sources));
for (SakerPath path : files.keySet()) {
taskcontext.reportInputFileDependency(null,
path, CommonTaskContentDescriptors.PRESENT);
taskcontext.startTask(
new WorkerTaskIdentifier(path),
new CompilationWorkerTaskFactory(path),
null);
}
return null;
}
It may be used by specifying the Sources
parameter:
example.compile(Sources: *.cpp)
The implementation will discover the files specified by the wildcard and report the appropriate file addition dependency. The path of the files will be iterated over, and a special content descriptor signaling only their presence will be reported for them as dependency.
A new task that executes the compilation of the file is started for every found file. The build system will schedule the started tasks accordingly, and the compilations for each source file will run.
The task identifier for the compilation worker tasks is a simple data class that holds the path of the file. The implementation of the compilation worker task is the following:
public Void run(TaskContext taskcontext) throws Exception {
SakerFile sourcefile = taskcontext.getTaskUtilities()
.resolveFileAtPath(CompilationWorkerTaskFactory.this.sourcePath);
if (sourcefile == null) {
throw new NoSuchFileException(sourcePath.toString(), null,
"Source file not found.");
}
taskcontext.getTaskUtilities()
.reportInputFileDependency(null, sourcefile);
// actually compile the source file
return null;
}
The worker implementation locates the file at the given source path location, and reports if as an input dependency. The actual compilation of the file should occur at the location of the associated comment.
Build scenarios
Let's examine how the task execution works given the following scenarios.
Simple run
If we have the following files in the working directory: main.cpp
, game.cpp
, resource.txt
, then simply execution the task using the given script:
example.compile(Sources: *.cpp)
Will occur in the following way:
- The
example.compile
task will be invoked by the build system.- It will discover the
main.cpp
andgame.cpp
files to compile, as thos are the ones that match the specified wildcard. - One compilation worker task will be started for each of them.
- The task finishes without a result.
- It will discover the
- The compilation worker tasks for
main.cpp
andgame.cpp
is executed simultaneously.- Both task retrieve the
SakerFile
instance representing the files. - The input dependencies are reported by the worker tasks.
- The worker tasks compile the file, and finish accordingly.
- Both task retrieve the
Incremental running
Following up on the scenario of Simple run. If you run the build, and run it again without changing any files, the build system will detect that no tasks need to be called, and therefore finishing without doing substantial work.
Modifying a file
If you change a compiled source file (e.g. main.cpp
), then the build system will detect that the appropriate compilation worker task needs to be rerun. It is important to note, that the bootstrapping example.compile
task will not be rerun, as it doesn't report a dependency that relies on the contents of the modified source file. This way, the build system determines that the minimal work it needs to do is to call the compilation worker task for main.cpp
.
Adding a file
If you add a new source file that matches the *.cpp
wildcard (e.g. add.cpp
), then the example.compile
task will be rerun. It will detect the addition of the new source file, and start all tasks for every source file again.
The build system will detect that main.cpp
and game.cpp
haven't changed, and the started tasks for them won't be reinvoked. In other words, the build system will do the minimal work for only invoking the compilation worker for the added source file.
Removing a file
When you remove a source file (e.g. game.cpp
), the build system will rerun the example.compile
bootstrapper task. It will only detect the presence of main.cpp
, and start the compilation worker for that.
After the build execution finishes, the build system will detect that the compilation worker task for game.cpp
hasn't been started by the bootstrapper task. It will delete any output files for that task that resides under the build directory, therefore cleaning up any leftover files.
In this scenario, no actual compilation worker task is called, but only the bootstrapper task.
Conclusion
We can see that decoupling various operations into their own task implementation can leverage the incremental support of the build system. By starting subtasks, the build system can optimize the invocations of tasks based on their dependencies, and always doing minimal work when possible.
Appropriately dividing work into tasks can be advantageous as tasks doesn't need to take care of cleaning up after themselves if a corresponding input is removed for an output.