In part 1 I showed how a simple make rule could be converted to a special “goal” function and I hinted that we were not limited to just the “file is older than” semantics implicit in make.
So let’s have a look at the goals I wrote to automate the recent OCaml rebuild in Fedora.
Recall from part 1: Targets are a contractual promise that you make in goaljobs. They are a promise that some condition will be true after running the goal. Requirements are conditions that must be true before the goal can start running.
For a Fedora package to achieve the goal of being rebuilt, the target is that the Koji build state of the current release must be “Completed”. The requirements are that every dependency of the package has been rebuilt. So:
let goal rebuilt pkg =
target (koji_build_state (fedora_verrel pkg branch)
(* Require the rebuild to have started: *)
require (rebuild_started pkg);
... some code to wait for the build to finish ...
The above code is not complete (it’s a complex, real-world working example after all).
I split the
rebuilt goal into two separate goals for reasons that will become clear later. But the first goal above says that the package rebuild must have been started off, and we’ll wait for the package build to complete.
Note that once the build is complete, the target promise is true.
rebuild_started is defined like this:
let goal rebuild_started pkg =
(* The dependencies of this package: *)
let deps = List.assoc pkg pkg_deps in
match koji_build_state (fedora_verrel pkg branch) with
| `Building | `Complete -> true
| _ -> false
(* All dependent packages must have been fully rebuilt: *)
List.iter (fun dep -> require (rebuilt dep)) deps;
(* Rebuild the package in Koji. *)
koji_build pkg branch
It’s saying that the target (promise) will be that the Koji package will either be building or may even be complete. And that we first of all require that every build dependency of this package has been completely, successfully rebuilt. If those requirements are met, we tell Koji to start building the package (but in this goal we don’t need to wait for it to complete).
Why did I split the goal into two parts?
The reason is that I want to define a make-like
let goal all () =
List.iter (fun pkg -> require (rebuild_started pkg))
This iterates over all my source packages and starts rebuilding them.
Note it doesn’t wait for each one to be rebuilt … unless they are required as dependencies of another package, in which case the
require (rebuilt dep) will kick in and wait for them before rebuilding the dependent package.
In other words, this code automatically resolves dependencies, waiting where necessary, but otherwise just kicking off builds, which is exactly what I wanted.
Finally a bit about how you use a goaljobs script. Unlike
make you have to compile the script into a binary. To compile the script, use the convenient wrapper
goaljobs (it’s a simple shell script that invokes the OCaml compiler):
This makes a binary called
fedora_ocaml_rebuild which is the program for mass-rebuilding the whole of Fedora’s OCaml subset.
When you run it with no arguments, it searches for a goal called
all and “requires” that goal (just like
You can also run other goals directly. Any goal which is “published” can be run from the command line. All goals that have no parameters — such as
all — are published automatically.
For goals that take parameters, if you want to use them from the command line you have to publish them manually. The reason is that you have to provide a small code snippet to convert the command line parameters to goal parameters, which may involve type conversion or other checks (since OCaml is strongly typed and parameters can be any type, not just strings or filenames).