docs: Document how to use onrun + memory_set.
[goaljobs.git] / goaljobs.mli
index 752e9b7..43432f1 100644 (file)
  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  *)
 
  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  *)
 
-(** {1 Goaljobs library of useful helper functions.} *)
+(** {1 Goaljobs library API} *)
 
 
-(** {2 Targets and requires}
-
-    These are used to write goals.
-
-    Normally you write a goal with one or more [target]s 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
-    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.
-
-    {v
-    let goal compiled c_file =
-        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
-    }
-
-    In the second example, the rule requires that several files
-    have been compiled ([require (compiled ...)]
-    before it can link the final program:
-
-    {v
-    let goal built program source =
-        target (more_recent [program] [source]);
-
-        require (compiled source);
-
-        let object = change_file_extension "o" source in
-        sh "cc %s -o %s" object program
-    }
-
-*)
+(** {2 Target and require} *)
 
 val target : bool -> unit
   (** [target] {i condition} defines the target condition that {b will}
 
 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
 
       Goaljobs is much more flexible than [make].  In [make] only a
       single type of target is possible.  The following are roughly
@@ -67,30 +34,33 @@ val target : bool -> unit
       {v
       foo.o: foo.c
         ...
       {v
       foo.o: foo.c
         ...
+      v}
 
 
+      {v
       let goal compiled () =
         target (more_recent ["foo.o"] ["foo.c"]);
       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
       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
 
 val target_all : bool list -> unit
   (** [target_all [t1; t2; ...]] is the same as writing
@@ -100,17 +70,52 @@ val target_exists : bool list -> unit
   (** [target_exists [t1; t2; ...]] is the same as writing
       [target (t1 || t2 || ...)] *)
 
   (** [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 : string -> (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".
 
 
       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} *)
+
+(* 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}
 
 
 (** {2 File and URL testing}
 
@@ -121,18 +126,25 @@ val file_exists : string -> bool
   (** Return true if the named file exists.
 
       This function also exists as a goal.  Writing:
   (** 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. *)
 
       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
 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:
 
 
 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:
 
 
       in a convenient way:
 
@@ -140,7 +152,7 @@ val more_recent : string list -> string list -> bool
       let goal built objects sources =
         target (more_recent objects sources);
         ... code to rebuild ...
       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.
 
       It is roughly equivalent to checking that all the object files
       exist and are newer than all of the source files.
@@ -148,75 +160,64 @@ 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
       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.
 
 
 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. *)
-
-(** {2 Shell}
-
-    Call out to the Unix shell.  [/bin/sh] is used unless you set
-    {!shell} to some other value.  Note that the environment variable
-    [SHELL] is {i not} used.
-
-    {!sh}, {!shout}, {!shlines} work like [printf].  ie. You can
-    substitute variables using [%s], [%d] and so on.  For example:
-
-    {v
-      sh "rsync foo-%s.tar.gz example.com:/html/" version
-    }
-
-    Each invocation of {!sh} (etc) is a single shell (this is slightly
-    different from how [make] works).  For example:
-
-    {v
-      sh "
-        package=foo-%s
-        tarball=$package.tar.gz
-        cp $HOME/$tarball .
-        tar zxf $tarball
-        cd $package
-        ./configure
-        make
-     " version
-    }
-
-    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 ||: }
-    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/ }
-*)
+      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 sh : ('a, unit, string, unit) format4 -> 'a
-  (** Run the command(s). *)
+val (//) : string -> string -> string
+  (** Concatenate two paths. *)
 
 
-val shout : ('a, unit, string, string) format4 -> 'a
+val quote : string -> string
+  (** Quote the string to make it safe to pass directly to the shell. *)
+
+(** {2 Shell} *)
+
+val sh : ?tmpdir:bool -> ('a, unit, string, unit) format4 -> 'a
+  (** Run the command(s).
+
+      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.
   (** 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.
   (** 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]). *)
 val shell : string ref
   (** Set this variable to override the default shell ([/bin/sh]). *)
-*)
 
 (** {2 String functions}
 
 
 (** {2 String functions}
 
@@ -245,42 +246,14 @@ val filter_file_extension : string -> string list -> string
       would return [["bar.o"]] (a single element list). *)
 *)
 
       would return [["bar.o"]] (a single element list). *)
 *)
 
-(** {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]
-    (which is a binary file, but you can delete it if you want).
-
-    The Memory is locked during accesses, so it is safe to read
-    or write it from multiple parallel goaljobs sessions.
-
-    Keys and values are strings.  The keys should be globally
-    unique, so it is suggested you use some application-specific
-    prefix.  eg: "myapp-key"
-
-    A common pattern is:
-
-    {v
-    let goal tested version =
-        let key = "myapp-tested-" ^ version in
-        target (memory_exists key);
-
-        ... some work to test version ...
-
-        memory_set key "1"
-    }
-
-    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.
-*)
+(** {2 Memory (persistent key/value storage)} *)
 
 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
 
 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. *)
 
 val memory_set : string -> string -> unit
   (** Set [key] to [value] in the Memory. *)
@@ -292,6 +265,59 @@ val memory_get : string -> string option
 val memory_delete : string -> unit
   (** Delete the [key].  If the key doesn't exist, has no effect. *)
 
 val memory_delete : string -> unit
   (** Delete the [key].  If the key doesn't exist, has no effect. *)
 
+(** {2 Publishing goals} *)
+
+val publish : string -> (string list -> unit) -> unit
+  (** Publish the named goal.
+
+      Use this function as in this example:
+
+      {v
+      let goal compiled program sources =
+        ... stuff for building the program from sources ...
+
+      let () = publish "compiled" (
+        fun args ->
+          let program = List.hd args in
+          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}
+
+      You will notice you have to write a bit of OCaml code to
+      map the string arguments from the command line on to the
+      goal arguments.  In the example it means taking the first
+      string argument as the program name, and the rest of the
+      string arguments as the source filenames.  This is also
+      the place to perform string to int conversion, checks, and
+      so on (remember that OCaml is strongly typed). *)
+
+(** {2 Logging script output} *)
+
+val log_program_output : unit -> string
+  (** [log_program_output] should be called at most once, usually at
+      the top-level of the script.  It creates a temporary file
+      and redirects stdout and stderr into this file (they are still
+      sent to the ordinary output, so it acts like [tee]).  The
+      filename of the temporary file is returned. *)
+
+(** {2 Sending email} *)
+
+val mailto : ?from:string -> subject:string -> ?attach:string list-> string -> unit
+  (** Send email.
+
+      Optional [?from] is the sender email address.
+
+      Required [~subject] is the subject line.
+
+      Optional [?attach] is a list of attachments (filenames).
+
+      The bare argument is the destination email address. *)
+
 (**/**)
 
 (* Goal versions of some common functions.  You are using these
 (**/**)
 
 (* Goal versions of some common functions.  You are using these
@@ -301,11 +327,23 @@ val memory_delete : string -> unit
  * if the predicate returns false.
  *)
 val goal_file_exists : string -> 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_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
 
 val goal_memory_exists : string -> unit
 
+(* A single call to this function is added by the 'goaljobs' script.
+ * It is responsible for parsing the command line and so on.
+ *)
+val init : unit -> unit
+
 (* Export this so the macros can catch these exceptions. *)
 type goal_result_t = Goal_OK | Goal_failed of string
 exception Goal_result of goal_result_t
 (* Export this so the macros can catch these exceptions. *)
 type goal_result_t = Goal_OK | Goal_failed of string
 exception Goal_result of goal_result_t
+
+(* Called to print debug message when we enter or leave a goal. *)
+val _enter_goal : string -> unit
+val _leave_goal : string -> unit