2 * Copyright (C) 2013 Red Hat Inc.
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 (** {1 Goaljobs library of useful helper functions.} *)
21 (** {2 Targets and requires}
23 These are used to write goals.
25 Normally you write a goal with one or more [target]s and
26 zero or more [require]s, as the examples below should make
29 In the first example, there are two targets: that [o_file] (object)
30 exists, and that it is newer than [c_file] (source). The rule
31 meets that target by running the C compiler ([cc]) which, if it
32 succeeds, will ensure that the object file exists and is newer
36 let goal compiled c_file =
37 let o_file = change_file_extension "o" c_file in
38 target (more_recent [o_file] [c_file]);
40 sh "cd $builddir && cc -c %s -o %s" c_file o_file
43 In the second example, the rule requires that several files
44 have been compiled ([require (compiled ...)]
45 before it can link the final program:
48 let goal built program source =
49 target (more_recent [program] [source]);
51 require (compiled source);
53 let object = change_file_extension "o" source in
54 sh "cd $builddir && cc %s -o %s" object program
59 val target : bool -> unit
60 (** [target] {i condition} defines the target condition that {b will}
61 be met once the current rule has run.
63 Goaljobs is much more flexible than [make]. In [make] only a
64 single type of target is possible. The following are roughly
71 let goal compiled () =
72 target (more_recent ["foo.o"] ["foo.c"]);
73 requires (file_exists "foo.c");
77 Targets in goaljobs can be any arbitrary expression, and you
78 can have any number of different targets.
80 Almost every rule should have one or more targets, which should
81 accurately state the outcome once the rule has been run.
83 If you have more than one [target]s then it's as if they have
84 been ORed together ({b not} ANDed which you might expect).
85 You can make this explicit by using a single target and [&&]
86 or [||] between the expressions. See also {!target_all}
89 Normally you put the target(s) early on in the rule, before any
90 running code and before any [require]s. This is not a
91 hard-and-fast rule and it is not enforced, but doing it will
92 ensure the rule runs most efficiently since if the target is met
93 already then the rest of the rule doesn't run. *)
95 val target_all : bool list -> unit
96 (** [target_all [t1; t2; ...]] is the same as writing
97 [target (t1 && t2 && ...)] *)
99 val target_exists : bool list -> unit
100 (** [target_exists [t1; t2; ...]] is the same as writing
101 [target (t1 || t2 || ...)] *)
103 val require : unit -> unit
104 (** [require] {!goal} defines the requirements of this rule, that
105 is, other goals that have to be met before this rule is able to run.
107 In terms of [make], [require]s are roughly equivalent to the
108 right hand side after the [:], but in goaljobs the requirements
109 can be much richer than simply "that file must exist".
111 Some very simple rules don't need any [require]s. Unlike with [make],
112 the requirements of a rule can be placed anywhere within the
113 rule, as long as you put them before they are needed. *)
115 (** {2 Periodic jobs}
117 If you want to have a rule that runs when some outside event
118 happens you have three choices: Manually run the script (this is
119 basically what [make] forces you to do). Have some sort of hook
120 that runs the script (eg. a git hook). Or use a periodic job to
121 poll for an event or change.
123 Periodic jobs run regularly to poll for an outside event or
124 change. If a script has periodic jobs, then it runs continuously
125 (or until you kill it).
127 An example of a script that checks for new git commits and when
128 it sees one it will ensure it passes the tests:
131 let repo = Sys.getenv "HOME" // "repo"
133 let goal git_commit_tested commit =
134 let key = sprintf "repo-tested-%s" commit in
135 target (memory_exists key);
145 (* Record that this commit was tested successfully. *)
148 every 30 minutes (fun () ->
149 let commit = shout "cd %s && git rev-parse HEAD" repo in
150 (* Require that this commit has been tested. *)
151 require (git_commit_tested commit)
155 Some notes about the above example: Firstly only the current HEAD
156 commit is required to be tested. This is because older commits
157 are irrelevant and because if they failed the test before there is
158 not point retesting them (commits are immutable). Secondly we use
159 the Memory to remember that we have successfully tested a commit.
160 This is what stops the program from repeatedly testing the same
163 (* This is what lets you write '30 minutes' etc: *)
164 type period_t = Seconds | Days | Months | Years
165 val seconds : int * period_t
166 val sec : int * period_t
167 val secs : int * period_t
168 val second : int * period_t
169 val minutes : int * period_t
170 val min : int * period_t
171 val mins : int * period_t
172 val minute : int * period_t
173 val hours : int * period_t
174 val hour : int * period_t
175 val days : int * period_t
176 val day : int * period_t
177 val weeks : int * period_t
178 val week : int * period_t
179 val months : int * period_t
180 val month : int * period_t
181 val years : int * period_t
182 val year : int * period_t
184 val every : ?name:string -> int -> int * period_t -> (unit -> unit) -> unit
185 (** [every N (seconds|minutes|hours|days|weeks|months|years) f]
186 runs the function [f] periodically.
188 The optional [~name] parameter can be used to name the job
191 (** {2 File and URL testing}
193 Various functions to test the existence of files, URLs.
196 val file_exists : string -> bool
197 (** Return true if the named file exists.
199 This function also exists as a goal. Writing:
200 {v require (file_exists "somefile");}
201 will die unless ["somefile"] exists. *)
203 val file_newer_than : string -> string -> bool
204 (** [file_newer_than file_a file_b] returns true if [file_a] is
205 newer than [file_b]. Note that if [file_a] does not exist, it
206 returns false. If [file_b] does not exist, it is an error.
208 There is also a goal version of this function. *)
210 val more_recent : string list -> string list -> bool
211 (** [more_recent objects sources] expresses the [make] relationship:
213 {v object(s) ...: source(s) ...}
218 let goal built objects sources =
219 target (more_recent objects sources);
220 ... code to rebuild ...
223 It is roughly equivalent to checking that all the object files
224 exist and are newer than all of the source files.
226 Note that both parameters are lists (since in [make] you can
227 have a list of source files and a list of object files). If you
228 don't want a list, pass a single-element list containing the
229 single the object/source file.
231 There is also a goal version of this function. *)
233 val url_exists : string -> bool
234 (** The URL is tested to see if it exists.
236 There is also a goal version of this function. *)
238 val file_contains_string : string -> string -> bool
239 (** [file_contains_string filename str] checks if the named file
240 contains the given substring [str].
242 There is also a goal version of this function. *)
244 val url_contains_string : string -> string -> bool
245 (** [url_contains_string url str] downloads the URL and checks
246 whether the content contains the given substring [str].
248 There is also a goal version of this function. *)
250 val (//) : string -> string -> string
251 (** Concatenate two paths. *)
253 val quote : string -> string
254 (** Quote the string to make it safe to pass directly to the shell. *)
258 Call out to the Unix shell. [/bin/sh] is used unless you set
259 {!shell} to some other value. Note that the environment variable
260 [SHELL] is {i not} used.
262 {!sh}, {!shout}, {!shlines} work like [printf]. ie. You can
263 substitute variables using [%s], [%d] and so on. For example:
266 sh "rsync foo-%s.tar.gz example.com:/html/" version
269 Each invocation of {!sh} (etc) is a single shell (this is slightly
270 different from how [make] works). For example:
275 tarball=$package.tar.gz
284 The shell error mode is set such that if any single command
285 returns an error then the {!sh} function as a whole exits with
288 to ignore the result of a command.
290 Each shell runs in a new temporary directory. The temporary
291 directory and all its contents is deleted after the shell exits.
292 If you want to save any data, [cd] somewhere. The environment
293 variable [$builddir] is exported to the script. This is the
294 current directory when the goaljobs program was started.
296 For example you could start the command sequence with
297 [cd $HOME/data/] or [cd $builddir].
300 val sh : ('a, unit, string, unit) format4 -> 'a
301 (** Run the command(s). *)
303 val shout : ('a, unit, string, string) format4 -> 'a
304 (** Run the command(s).
306 Anything printed on stdout is returned as a string.
307 The trailing [\n] character, if any, is not returned. *)
309 val shlines : ('a, unit, string, string list) format4 -> 'a
310 (** Run the command(s).
312 Any lines printed to stdout is returned as a list of strings.
313 Trailing [\n] characters are not returned. *)
315 val shell : string ref
316 (** Set this variable to override the default shell ([/bin/sh]). *)
318 (** {2 String functions}
320 Most string functions are provided by the OCaml standard
321 library (see the module [String]). For convenience some
322 extra functions are provided here. *)
325 val replace_substring : string -> string -> string -> string
326 (** [replace_substring patt repl string] replaces all occurrences
327 of [patt] with [repl] in [string]. *)
330 val change_file_extension : string -> string -> string
331 (** [change_file_extension ext filename] changes the file extension
332 of [filename] to [.ext]. For example
333 [change_file_extension "o" "main.c"] returns ["main.o"].
334 If the original filename has no extension, this function
335 adds the extension. *)
338 val filter_file_extension : string -> string list -> string
339 (** [filter_file_extension ext filenames] returns only those
340 filenames in the list which have the given file extension.
341 For example [filter_file_extension "o" ["foo.c"; "bar.o"]]
342 would return [["bar.o"]] (a single element list). *)
345 (** {2 Memory (persistent key/value storage)
347 "The Memory" is key/value storage which persists across goaljobs
348 sessions. It is stored in the file [$HOME/.goaljobs-memory]
349 (which is a binary file, but you can delete it if you want).
351 The Memory is locked during accesses, so it is safe to read
352 or write it from multiple parallel goaljobs sessions.
354 Keys and values are strings. The keys should be globally
355 unique, so it is suggested you use some application-specific
356 prefix. eg: "myapp-key"
361 let goal tested version =
362 let key = "myapp-tested-" ^ version in
363 target (memory_exists key);
365 ... some work to test version ...
370 Note in that example the value ["1"] is arbitrary. You just
371 want to store {i any} value so that a later call to {!memory_exists}
375 val memory_exists : string -> bool
376 (** [memory_exists key] checks that the named [key] exists in
377 the Memory. It doesn't matter what value it has.
379 This is also available as a goal, so you can write
380 [requires (memory_exists key)] *)
382 val memory_set : string -> string -> unit
383 (** Set [key] to [value] in the Memory. *)
385 val memory_get : string -> string option
386 (** Return the current value of [key] in the Memory. Returns [None]
387 if the key has never been set or was deleted. *)
389 val memory_delete : string -> unit
390 (** Delete the [key]. If the key doesn't exist, has no effect. *)
392 (** {2 Publishing goals}
394 To "publish" a goal means it's available on the command line
395 for users to use directly.
397 Goals that have zero arguments are {b automatically published}.
401 let goal clean () = sh "rm *~"
404 can be used on the command line:
408 The special goal called [all] (if it exists) is run implicitly
409 unless the user specifies another goal. Unlike [make], there is
410 nothing special about the first rule in the file.
412 You can also publish goals, especially ones which take a non-zero
413 number of parameters, by calling {!publish}.
416 val publish : string -> (string list -> unit) -> unit
417 (** Publish the named goal.
419 Use this function as in this example:
422 let goal compiled program sources =
423 ... stuff for building the program from sources ...
425 let () = publish "compiled" (
427 let program = List.hd args in
428 let sources = List.tl args in
429 require (compiled program sources)
433 This could be used as follows:
435 {v ./script compiled program main.c utils.c }
437 You will notice you have to write a bit of OCaml code to
438 map the string arguments from the command line on to the
439 goal arguments. In the example it means taking the first
440 string argument as the program name, and the rest of the
441 string arguments as the source filenames. This is also
442 the place to perform string to int conversion, checks, and
443 so on (remember that OCaml is strongly typed). *)
447 (* Goal versions of some common functions. You are using these
448 * versions when you write something like:
449 * require (file_exists "foo");
450 * They work the same way as the regular function, except they die
451 * if the predicate returns false.
453 val goal_file_exists : string -> unit
454 val goal_file_newer_than : string -> string -> unit
455 val goal_more_recent : string list -> string list -> unit
456 val goal_url_exists : string -> unit
457 val goal_file_contains_string : string -> string -> unit
458 val goal_url_contains_string : string -> string -> unit
459 val goal_memory_exists : string -> unit
461 (* A single call to this function is added by the 'goaljobs' script.
462 * It is responsible for parsing the command line and so on.
464 val init : unit -> unit
466 (* Export this so the macros can catch these exceptions. *)
467 type goal_result_t = Goal_OK | Goal_failed of string
468 exception Goal_result of goal_result_t