X-Git-Url: http://git.annexia.org/?a=blobdiff_plain;f=goaljobs.mli;h=3e1ca0c0d1aff08e15bb130cac990091c5a14895;hb=e945605c6b1db3490c81a1434824151e8854dff6;hp=8b31abe85ce3ff604052e77c5c0c8e2f86703215;hpb=a0bf80d39e5dbb7daac3c28ab546a2c3ae1312a6;p=goaljobs.git diff --git a/goaljobs.mli b/goaljobs.mli index 8b31abe..3e1ca0c 100644 --- a/goaljobs.mli +++ b/goaljobs.mli @@ -18,16 +18,16 @@ (** {1 Goaljobs library of useful helper functions.} *) -(** {2 Targets and requires} +(** {2 Target and require} These are used to write goals. - Normally you write a goal with one or more [target]s and + Normally you write a goal with zero or one [target] and zero or more [require]s, as the examples below should make clear. - In the first example, there are two targets: that [o_file] (object) - exists, and that it is newer than [c_file] (source). The rule + In the first example, the target is that the [o_file] (object) exists + and is newer than the [c_file] (source). The goal meets that target by running the C compiler ([cc]) which, if it succeeds, will ensure that the object file exists and is newer than the source file. @@ -37,11 +37,11 @@ let o_file = change_file_extension "o" c_file in target (more_recent [o_file] [c_file]); - sh "cc -c %s -o %s" c_file o_file - } + sh "cd $builddir && cc -c %s -o %s" c_file o_file + v} - In the second example, the rule requires that several files - have been compiled ([require (compiled ...)] + In the second example, the goal requires that several files + have been compiled ([require (compiled ...)]) before it can link the final program: {v @@ -51,14 +51,17 @@ require (compiled source); let object = change_file_extension "o" source in - sh "cc %s -o %s" object program - } + sh "cd $builddir && cc %s -o %s" object program + v} *) val target : bool -> unit (** [target] {i condition} defines the target condition that {b will} - be met once the current rule has run. + be met once the current goal has run. + + You can think of the target as a promise or contract that you + make, which is met by running the rest of the goal. Goaljobs is much more flexible than [make]. In [make] only a single type of target is possible. The following are roughly @@ -67,30 +70,33 @@ val target : bool -> unit {v foo.o: foo.c ... + v} + {v let goal compiled () = target (more_recent ["foo.o"] ["foo.c"]); - requires (file_exists "foo.c"); + require (file_exists "foo.c"); ... - } + v} + + Targets in goaljobs can be any arbitrary expression. For + example, it can access network resources or test URLs. - Targets in goaljobs can be any arbitrary expression, and you - can have any number of different targets. + Almost every goal should have one target, which should + accurately state the outcome once the goal has been run. - Almost every rule should have one or more targets, which should - accurately state the outcome once the rule has been run. + It is possible to have no target. This means the goal + always runs (like using "force" in make). - If you have more than one [target]s then it's as if they have - been ORed together ({b not} ANDed which you might expect). - You can make this explicit by using a single target and [&&] - or [||] between the expressions. See also {!target_all} - and {!target_exists}. + You should not have multiple targets in a single goal. They + won't work the way you expect, and future versions of goaljobs + will likely stop you from doing this. - Normally you put the target(s) early on in the rule, before any + Normally you put the target(s) early on in the goal, before any running code and before any [require]s. This is not a hard-and-fast rule and it is not enforced, but doing it will - ensure the rule runs most efficiently since if the target is met - already then the rest of the rule doesn't run. *) + ensure the goal runs most efficiently since if the target is met + already then the rest of the goal doesn't run. *) val target_all : bool list -> unit (** [target_all [t1; t2; ...]] is the same as writing @@ -100,17 +106,98 @@ val target_exists : bool list -> unit (** [target_exists [t1; t2; ...]] is the same as writing [target (t1 || t2 || ...)] *) -val require : unit -> unit - (** [require] {!goal} defines the requirements of this rule, that - is, other goals that have to be met before this rule is able to run. +val require : (unit -> unit) -> unit + (** [require] {i goal} defines the requirements of this goal, that + is, other goals that have to be met before the rest of the + goal is able to run. In terms of [make], [require]s are roughly equivalent to the right hand side after the [:], but in goaljobs the requirements can be much richer than simply "that file must exist". - Some very simple rules don't need any [require]s. Unlike with [make], - the requirements of a rule can be placed anywhere within the - rule, as long as you put them before they are needed. *) + Some very simple goals don't need any [require]s. You can + have as many [require]s as you need in a goal, and you can + use a loop or make them conditional if you want. + + Unlike [make], the requirements of a goal can be + placed anywhere within the goal, as long as you put them + before they are needed. *) + +(** {2 Periodic jobs} + + If you want to have a goal that runs when some outside event + happens you have three choices: Manually run the script (this is + basically what [make] forces you to do). Have some sort of hook + that runs the script (eg. a git hook). Or use a periodic job to + poll for an event or change. + + Periodic jobs run regularly to poll for an outside event or + change. If a script has periodic jobs, then it runs continuously + (or until you kill it). + + An example of a script that checks for new git commits and when + it sees one it will ensure it passes the tests: + + {v + let repo = Sys.getenv "HOME" // "repo" + + let goal git_commit_tested commit = + let key = sprintf "repo-tested-%s" commit in + target (memory_exists key); + + sh " + git clone %s test + cd test + ./configure + make + make check + "; + + (* Record that this commit was tested successfully. *) + memory_set key "1" + + every 30 minutes (fun () -> + let commit = shout "cd %s && git rev-parse HEAD" repo in + (* Require that this commit has been tested. *) + require (git_commit_tested commit) + ) + v} + + Some notes about the above example: Firstly only the current HEAD + commit is required to be tested. This is because older commits + are irrelevant and because if they failed the test before there is + not point retesting them (commits are immutable). Secondly we use + the Memory to remember that we have successfully tested a commit. + This is what stops the program from repeatedly testing the same + commit. *) + +(* This is what lets you write '30 minutes' etc: *) +type period_t = Seconds | Days | Months | Years +val seconds : int * period_t +val sec : int * period_t +val secs : int * period_t +val second : int * period_t +val minutes : int * period_t +val min : int * period_t +val mins : int * period_t +val minute : int * period_t +val hours : int * period_t +val hour : int * period_t +val days : int * period_t +val day : int * period_t +val weeks : int * period_t +val week : int * period_t +val months : int * period_t +val month : int * period_t +val years : int * period_t +val year : int * period_t + +val every : ?name:string -> int -> int * period_t -> (unit -> unit) -> unit + (** [every N (seconds|minutes|hours|days|weeks|months|years) f] + runs the function [f] periodically. + + The optional [~name] parameter can be used to name the job + (for debugging). *) (** {2 File and URL testing} @@ -121,18 +208,25 @@ val file_exists : string -> bool (** Return true if the named file exists. This function also exists as a goal. Writing: - {v require (file_exists "somefile");} + {v require (file_exists "somefile"); v} will die unless ["somefile"] exists. *) +val directory_exists : string -> bool + (** Return true if the named directory exists. + + There is also a goal version of this function. *) + val file_newer_than : string -> string -> bool (** [file_newer_than file_a file_b] returns true if [file_a] is newer than [file_b]. Note that if [file_a] does not exist, it - returns false. If [file_b] does not exist, it is an error. *) + returns false. If [file_b] does not exist, it is an error. + + There is also a goal version of this function. *) val more_recent : string list -> string list -> bool (** [more_recent objects sources] expresses the [make] relationship: - {v object(s) ...: source(s) ...} + {v object(s) ...: source(s) ... v} in a convenient way: @@ -140,7 +234,7 @@ val more_recent : string list -> string list -> bool let goal built objects sources = target (more_recent objects sources); ... code to rebuild ... - } + v} It is roughly equivalent to checking that all the object files exist and are newer than all of the source files. @@ -148,14 +242,32 @@ val more_recent : string list -> string list -> bool Note that both parameters are lists (since in [make] you can have a list of source files and a list of object files). If you don't want a list, pass a single-element list containing the - single the object/source file. *) + single the object/source file. + + There is also a goal version of this function. *) val url_exists : string -> bool (** The URL is tested to see if it exists. - This function also exists as a goal. Writing: - {v require (url_exists "http://example.com");} - will die unless the given URL exists. *) + There is also a goal version of this function. *) + +val file_contains_string : string -> string -> bool + (** [file_contains_string filename str] checks if the named file + contains the given substring [str]. + + There is also a goal version of this function. *) + +val url_contains_string : string -> string -> bool + (** [url_contains_string url str] downloads the URL and checks + whether the content contains the given substring [str]. + + There is also a goal version of this function. *) + +val (//) : string -> string -> string + (** Concatenate two paths. *) + +val quote : string -> string + (** Quote the string to make it safe to pass directly to the shell. *) (** {2 Shell} @@ -168,7 +280,15 @@ val url_exists : string -> bool {v sh "rsync foo-%s.tar.gz example.com:/html/" version - } + v} + + Each shell runs in a new temporary directory. The temporary + directory and all its contents is deleted after the shell exits. + If you want to save any data, [cd] somewhere. If you don't + want the temporary directory creation, use [~tmpdir:false]. + + The environment variable [$builddir] is exported to the script. + This is the current directory when the goaljobs program was started. Each invocation of {!sh} (etc) is a single shell (this is slightly different from how [make] works). For example: @@ -183,40 +303,45 @@ val url_exists : string -> bool ./configure make " version - } + v} The shell error mode is set such that if any single command returns an error then the {!sh} function as a whole exits with an error. Write: - {v command ||: } + {v command ||: v} to ignore the result of a command. - Each shell runs in a new temporary directory. The temporary directory - and all its contents is deleted after the shell exits. If you - want to save any data, [cd] somewhere. For example you could start - the command sequence with: - {v cd $HOME/data/ } *) -val sh : ('a, unit, string, unit) format4 -> 'a - (** Run the command(s). *) +val sh : ?tmpdir:bool -> ('a, unit, string, unit) format4 -> 'a + (** Run the command(s). -val shout : ('a, unit, string, string) format4 -> 'a + The command runs in a newly created temporary directory (which + is deleted after the command exits), {i unless} you use + [~tmpdir:false]. *) + +val shout : ?tmpdir:bool -> ('a, unit, string, string) format4 -> 'a (** Run the command(s). Anything printed on stdout is returned as a string. - The trailing [\n] character, if any, is not returned. *) + The trailing [\n] character, if any, is not returned. + + The command runs in a newly created temporary directory (which + is deleted after the command exits), {i unless} you use + [~tmpdir:false]. *) -val shlines : ('a, unit, string, string list) format4 -> 'a +val shlines : ?tmpdir:bool -> ('a, unit, string, string list) format4 -> 'a (** Run the command(s). Any lines printed to stdout is returned as a list of strings. - Trailing [\n] characters are not returned. *) + Trailing [\n] characters are not returned. + + The command runs in a newly created temporary directory (which + is deleted after the command exits), {i unless} you use + [~tmpdir:false]. *) -(* val shell : string ref (** Set this variable to override the default shell ([/bin/sh]). *) -*) (** {2 String functions} @@ -245,7 +370,7 @@ val filter_file_extension : string -> string list -> string would return [["bar.o"]] (a single element list). *) *) -(** {2 Memory (persistent key/value storage) +(** {2 Memory (persistent key/value storage)} "The Memory" is key/value storage which persists across goaljobs sessions. It is stored in the file [$HOME/.goaljobs-memory] @@ -268,19 +393,18 @@ val filter_file_extension : string -> string list -> string ... some work to test version ... memory_set key "1" - } + v} Note in that example the value ["1"] is arbitrary. You just want to store {i any} value so that a later call to {!memory_exists} - will succeed. -*) + will succeed. *) val memory_exists : string -> bool (** [memory_exists key] checks that the named [key] exists in the Memory. It doesn't matter what value it has. This is also available as a goal, so you can write - [requires (memory_exists key)] *) + [require (memory_exists key)] *) val memory_set : string -> string -> unit (** Set [key] to [value] in the Memory. *) @@ -302,15 +426,15 @@ val memory_delete : string -> unit {v let goal clean () = sh "rm *~" - } + v} can be used on the command line: - {v ./script clean } + {v ./script clean v} The special goal called [all] (if it exists) is run implicitly unless the user specifies another goal. Unlike [make], there is - nothing special about the first rule in the file. + nothing special about the first goal in the file. You can also publish goals, especially ones which take a non-zero number of parameters, by calling {!publish}. @@ -331,11 +455,11 @@ val publish : string -> (string list -> unit) -> unit let sources = List.tl args in require (compiled program sources) ) - } + v} This could be used as follows: - {v ./script compiled program main.c utils.c } + {v ./script compiled program main.c utils.c v} You will notice you have to write a bit of OCaml code to map the string arguments from the command line on to the @@ -354,9 +478,12 @@ val publish : string -> (string list -> unit) -> unit * if the predicate returns false. *) val goal_file_exists : string -> unit +val goal_directory_exists : string -> unit val goal_file_newer_than : string -> string -> unit val goal_more_recent : string list -> string list -> unit val goal_url_exists : string -> unit +val goal_file_contains_string : string -> string -> unit +val goal_url_contains_string : string -> string -> unit val goal_memory_exists : string -> unit (* A single call to this function is added by the 'goaljobs' script.