First published version.
authorRichard W.M. Jones <rjones@redhat.com>
Sun, 15 Sep 2013 20:39:12 +0000 (21:39 +0100)
committerRichard W.M. Jones <rjones@redhat.com>
Mon, 16 Sep 2013 13:07:22 +0000 (14:07 +0100)
17 files changed:
.depend [new file with mode: 0644]
.gitignore
Makefile.am
NOTES
common-rules.mk [new file with mode: 0644]
configure.ac
examples/Makefile.am
examples/compile-c/.depend [new file with mode: 0644]
examples/compile-c/Makefile.am [new file with mode: 0644]
examples/compile-c/README [new file with mode: 0644]
examples/compile-c/compile.ml [new file with mode: 0644]
examples/compile-c/main.c [new file with mode: 0644]
examples/compile-c/utils.c [new file with mode: 0644]
goaljobs.ml
goaljobs.mli
m4/.gitignore [new file with mode: 0644]
pa_goal.ml [new file with mode: 0644]

diff --git a/.depend b/.depend
new file mode 100644 (file)
index 0000000..1c95889
--- /dev/null
+++ b/.depend
@@ -0,0 +1,3 @@
+goaljobs.cmi :
+goaljobs.cmo : goaljobs.cmi
+goaljobs.cmx : goaljobs.cmi
index b25c15b..5613406 100644 (file)
@@ -1 +1,29 @@
 *~
 *~
+*.cmi
+*.cmo
+*.cmx
+*.cma
+*.cmxa
+*.o
+*.a
+
+Makefile.in
+Makefile
+
+/aclocal.m4
+/autom4te.cache
+/compile
+/config.guess
+/config.h
+/config.h.in
+/config.log
+/config.status
+/config.sub
+/configure
+/examples/compile-c/compile
+/examples/compile-c/program
+/install-sh
+/libtool
+/ltmain.sh
+/missing
+/stamp-h1
index 11997d7..b995c23 100644 (file)
@@ -15,6 +15,8 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
+include common-rules.mk
+
 ACLOCAL_AMFLAGS = -I m4
 
 EXTRA_DIST = \
 ACLOCAL_AMFLAGS = -I m4
 
 EXTRA_DIST = \
@@ -23,35 +25,28 @@ EXTRA_DIST = \
        README \
        $(sources)
 
        README \
        $(sources)
 
-CLEANFILES = *~
-
-OCAMLCFLAGS = -g -package unix
-OCAMLOPTFLAGS = $(OCAMLCFLAGS)
-
 SUBDIRS = . examples tests
 
 sources = \
 SUBDIRS = . examples tests
 
 sources = \
-       test.ml \
        goaljobs.ml \
        goaljobs.mli
 
        goaljobs.ml \
        goaljobs.mli
 
-noinst_SCRIPTS = test
+noinst_SCRIPTS = goaljobs.cmxa pa_goal.cmo
 
 
-test: goaljobs.cmx
-       $(OCAMLFIND) ocamlopt $(OCAMLOPTFLAGS) $< -o $@
+# Library.
+goaljobs.cmxa: goaljobs.cmx
+       $(OCAMLFIND) ocamlopt -a -o $@ $(OCAMLOPTFLAGS) $(OCAMLOPTPACKAGES) $<
 
 
-# Dependencies.
+# Preprocessor for goaljobs scripts.
+pa_goal.cmo: pa_goal.ml
+       $(OCAMLFIND) ocamlc $(OCAMLCFLAGS) -package camlp4.lib -linkpkg \
+           -pp $(CAMLP4OF) -c $< -o $@
 
 
-%.cmi: %.mli
-       $(OCAMLFIND) ocamlc $(OCAMLCFLAGS) -c $< -o $@
-%.cmo: %.ml
-       $(OCAMLFIND) ocamlc $(OCAMLCFLAGS) -c $< -o $@
-%.cmx: %.ml
-       $(OCAMLFIND) ocamlopt $(OCAMLOPTFLAGS) -c $< -o $@
+# Dependencies.
 
 depend: .depend
 
 
 depend: .depend
 
-.depend: $(wildcard $(abs_srcdir)/*.mli) $(wildcard $(abs_srcdir)/*.ml)
+.depend: $(sources)
        rm -f $@ $@-t
        $(OCAMLFIND) ocamldep -I $(abs_srcdir) $^ | \
          $(SED) 's/ *$$//' | \
        rm -f $@ $@-t
        $(OCAMLFIND) ocamldep -I $(abs_srcdir) $^ | \
          $(SED) 's/ *$$//' | \
diff --git a/NOTES b/NOTES
index 14afc95..18dccbe 100644 (file)
--- a/NOTES
+++ b/NOTES
@@ -77,4 +77,3 @@ Example program:
       git describe --tags --abbrev=0 --match='v*'
     " package in
     require (website_updated version)
       git describe --tags --abbrev=0 --match='v*'
     " package in
     require (website_updated version)
-
diff --git a/common-rules.mk b/common-rules.mk
new file mode 100644 (file)
index 0000000..692c906
--- /dev/null
@@ -0,0 +1,30 @@
+# goaljobs
+# Copyright (C) 2013 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+CLEANFILES = *~ *.cmi *.cmo *.cmx *.cma *.cmxa *.o
+
+OCAMLCFLAGS = -g -warn-error CDEFLMPSUVYZX
+OCAMLCPACKAGES = -package unix -I $(top_builddir)
+OCAMLOPTFLAGS = $(OCAMLCFLAGS)
+OCAMLOPTPACKAGES = $(OCAMLCPACKAGES)
+
+%.cmi: %.mli
+       $(OCAMLFIND) ocamlc $(OCAMLCFLAGS) $(OCAMLCPACKAGES) -c $< -o $@
+%.cmo: %.ml
+       $(OCAMLFIND) ocamlc $(OCAMLCFLAGS) $(OCAMLCPACKAGES) -c $< -o $@
+%.cmx: %.ml
+       $(OCAMLFIND) ocamlopt $(OCAMLOPTFLAGS) $(OCAMLOPTPACKAGES) -c $< -o $@
index 1f3189b..6ca6f32 100644 (file)
@@ -87,5 +87,6 @@ fi
 AC_CONFIG_HEADERS([config.h])
 AC_CONFIG_FILES([Makefile
                  examples/Makefile
 AC_CONFIG_HEADERS([config.h])
 AC_CONFIG_FILES([Makefile
                  examples/Makefile
+                 examples/compile-c/Makefile
                  tests/Makefile])
 AC_OUTPUT
                  tests/Makefile])
 AC_OUTPUT
index 9445514..457c3a2 100644 (file)
@@ -15,3 +15,5 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
+SUBDIRS = \
+       compile-c
diff --git a/examples/compile-c/.depend b/examples/compile-c/.depend
new file mode 100644 (file)
index 0000000..a95aa04
--- /dev/null
@@ -0,0 +1,2 @@
+compile.cmo : ../../goaljobs.cmi
+compile.cmx : ../../goaljobs.cmx
diff --git a/examples/compile-c/Makefile.am b/examples/compile-c/Makefile.am
new file mode 100644 (file)
index 0000000..51a996a
--- /dev/null
@@ -0,0 +1,49 @@
+# goaljobs
+# Copyright (C) 2013 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+include ../../common-rules.mk
+
+EXTRA_DIST = main.c utils.c $(sources)
+
+sources = compile.ml
+
+noinst_SCRIPTS = compile
+
+compile: ../../goaljobs.cmxa compile.cmx
+       $(OCAMLFIND) ocamlopt $(OCAMLOPTFLAGS) \
+           $(OCAMLOPTPACKAGES) -linkpkg $^ -o $@
+
+compile.cmx: compile.ml ../../pa_goal.cmo
+       $(OCAMLFIND) ocamlopt $(OCAMLOPTFLAGS) $(OCAMLOPTPACKAGES) \
+           -pp "$(CAMLP4O) ../../pa_goal.cmo" -c $< -o $@
+
+# Dependencies.
+
+depend: .depend
+
+.depend: $(sources)
+       rm -f $@ $@-t
+       $(OCAMLFIND) ocamldep -I $(abs_srcdir) -I $(top_builddir) $^ | \
+         $(SED) 's/ *$$//' | \
+         $(SED) -e :a -e '/ *\\$$/N; s/ *\\\n */ /; ta' | \
+         $(SED) -e 's,$(abs_srcdir)/,$(builddir)/,g' | \
+         sort > $@-t
+       mv $@-t $@
+
+-include .depend
+
+SUFFIXES = .cmo .cmi .cmx .ml .mli .mll .mly
diff --git a/examples/compile-c/README b/examples/compile-c/README
new file mode 100644 (file)
index 0000000..94f5a18
--- /dev/null
@@ -0,0 +1,11 @@
+Very trivial example of compiling a C program.
+
+It compiled 'main.c' & 'utils.c' into 'program' (these are just
+dummy C files which don't do anything interesting).
+
+To try it out, do:
+
+./compile
+
+You can also try deleting intermediate files (eg. rm main.o)
+and see which files get rebuilt.
diff --git a/examples/compile-c/compile.ml b/examples/compile-c/compile.ml
new file mode 100644 (file)
index 0000000..6cd2e88
--- /dev/null
@@ -0,0 +1,30 @@
+open Goaljobs  (* will be implicit *)
+
+let rec goal all () =
+  require (built "program" ["main.c"; "utils.c"])
+
+(* Goal: build the final program from source files. *)
+and built program sources =
+  target (more_recent [program] sources);
+  List.iter (fun s -> require (compiled s)) sources;
+
+  let objects = List.map (change_file_extension "o") sources in
+  sh "cc %s -o %s" (String.concat " " objects) program
+
+(* Goal: Make sure a C file is compiled (to an object file). *)
+and compiled c_file =
+  let o_file = change_file_extension "o" c_file in
+  target (more_recent [o_file] [c_file]);
+  require (file_exists c_file);
+  sh "cc -c %s -o %s" c_file o_file
+
+(* XXX IMPLICIT *)
+let () =
+  try goal_all ()
+  with
+  | Goal_result (Goal_failed msg) ->
+    prerr_endline ("error: " ^ msg);
+    exit 1
+  | exn ->
+    prerr_endline (Printexc.to_string exn);
+    exit 1
diff --git a/examples/compile-c/main.c b/examples/compile-c/main.c
new file mode 100644 (file)
index 0000000..9b45b5f
--- /dev/null
@@ -0,0 +1,14 @@
+/* This is just a dummy C program to test building. */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+extern void do_utility (void);
+
+int
+main ()
+{
+  printf ("hello, world\n");
+  do_utility ();
+  exit (0);
+}
diff --git a/examples/compile-c/utils.c b/examples/compile-c/utils.c
new file mode 100644 (file)
index 0000000..fcbb298
--- /dev/null
@@ -0,0 +1,9 @@
+/* This is just a dummy C program to test building. */
+
+#include <stdio.h>
+
+void
+do_utility (void)
+{
+  printf ("this is the utils.c function\n");
+}
index 45422d2..282c29e 100644 (file)
@@ -26,9 +26,9 @@ let goal_failed msg = raise (Goal_result (Goal_failed msg))
 
 let target v =
   if v then raise (Goal_result Goal_OK)
 
 let target v =
   if v then raise (Goal_result Goal_OK)
-let require = function
-  | Goal_OK -> ()
-  | r -> raise (Goal_result r)
+let target_all vs = target (List.fold_left (&&) true vs)
+let target_exists vs = target (List.fold_left (||) false vs)
+let require () = ()
 
 let file_exists = Sys.file_exists
 
 
 let file_exists = Sys.file_exists
 
@@ -43,7 +43,7 @@ let file_newer_than f1 f2 =
   in
   let s1 = stat f1 and s2 = stat f2 in
   match s1 with
   in
   let s1 = stat f1 and s2 = stat f2 in
   match s1 with
-  | None -> ()
+  | None -> false
   | Some s1 ->
     match s2 with
     | None ->
   | Some s1 ->
     match s2 with
     | None ->
@@ -52,45 +52,105 @@ let file_newer_than f1 f2 =
     | Some s2 ->
       s1.st_mtime >= s2.st_mtime
 
     | Some s2 ->
       s1.st_mtime >= s2.st_mtime
 
+let more_recent objs srcs =
+  if not (List.for_all file_exists objs) then false
+  else (
+    List.for_all (
+      fun obj -> List.for_all (file_newer_than obj) srcs
+    ) objs
+  )
+
 let url_exists url = goal_failed "url_exists not implemented!"
 
 let sh fs =
   let do_sh cmd =
 let url_exists url = goal_failed "url_exists not implemented!"
 
 let sh fs =
   let do_sh cmd =
-    print_endline cmd;
-    let cmd = "set -e\n\n" ^ cmd in
-    let r = System.command cmd in
+    let cmd = "set -e\nset -x\n\n" ^ cmd in
+    let r = Sys.command cmd in
     if r <> 0 then (
     if r <> 0 then (
-      let msg = sprintf "sh: external command failed with code %d" r in
+      let msg = sprintf "external command failed with code %d" r in
       goal_failed msg
     )
   in
   ksprintf do_sh fs
 
       goal_failed msg
     )
   in
   ksprintf do_sh fs
 
-(*
-val shout : ('a, unit, string) format -> 'a
-val shlines : ('a, unit, string) format -> 'a
+let do_shlines cmd =
+  let cmd = "set -e\nset -x\n\n" ^ cmd in
+  let chan = open_process_in cmd in
+  let lines = ref [] in
+  let rec loop () =
+    let line = input_line chan in
+    lines := line :: !lines;
+    loop ()
+  in
+  (try loop () with End_of_file -> ());
+  let r = close_process_in chan in
+  match r with
+  | WEXITED 0 -> List.rev !lines
+  | WEXITED i ->
+    let msg = sprintf "external command failed with code %d" i in
+    goal_failed msg
+  | WSIGNALED i ->
+    let msg = sprintf "external command was killed by signal %d" i in
+    goal_failed msg
+  | WSTOPPED i ->
+    let msg = sprintf "external command was stopped by signal %d" i in
+    goal_failed msg
+let shlines fs = ksprintf do_shlines fs
+
+let do_shout cmd =
+  let lines = do_shlines cmd in
+  String.concat "\n" lines
+let shout fs = ksprintf do_shout fs
 
 
+(*
 val shell : string ref
 *)
 
 (*
 val replace_substring : string -> string -> string -> string
 val shell : string ref
 *)
 
 (*
 val replace_substring : string -> string -> string -> string
-val change_file_extension : string -> string -> string
+*)
+
+let change_file_extension ext filename =
+  let i =
+    try String.rindex filename '.'
+    with Not_found -> String.length filename in
+  String.sub filename 0 i ^ "." ^ ext
+
+(*
 val filter_file_extension : string -> string list -> string
 *)
 
 val filter_file_extension : string -> string list -> string
 *)
 
+(* XXX The Memory is not actually persistent yet. *)
+let memory = Hashtbl.create 13
+
+let memory_exists = Hashtbl.mem memory
+let memory_set = Hashtbl.replace memory
+let memory_get k = try Some (Hashtbl.find memory k) with Not_found -> None
+let memory_delete = Hashtbl.remove memory
+
 let goal_file_exists filename =
   if not (file_exists filename) then (
 let goal_file_exists filename =
   if not (file_exists filename) then (
-    let msg = sprintf "file_exists: %s: file not found" filename in
+    let msg = sprintf "file '%s' required but not found" filename in
     goal_failed msg
   )
 let goal_file_newer_than f1 f2 =
   if not (file_newer_than f1 f2) then (
     goal_failed msg
   )
 let goal_file_newer_than f1 f2 =
   if not (file_newer_than f1 f2) then (
-    let msg = sprintf "file %s is not newer than %s" f1 f2 in
+    let msg = sprintf "file %s is required to be newer than %s" f1 f2 in
+    goal_failed msg
+  )
+let goal_more_recent objs srcs =
+  if not (more_recent objs srcs) then (
+    let msg = sprintf "object(s) %s are required to be newer than source(s) %s"
+      (String.concat " " objs) (String.concat " " srcs) in
     goal_failed msg
   )
 let goal_url_exists url =
   if not (url_exists url) then (
     goal_failed msg
   )
 let goal_url_exists url =
   if not (url_exists url) then (
-    let msg = sprintf "url_exists: %s: URL does not exist" url in
+    let msg = sprintf "url_exists: URL '%s' required but does not exist" url in
+    goal_failed msg
+  )
+let goal_memory_exists k =
+  if not (memory_exists k) then (
+    let msg = sprintf "memory_exists: key '%s' required but does not exist" k in
     goal_failed msg
   )
     goal_failed msg
   )
index 6839108..752e9b7 100644 (file)
@@ -35,8 +35,7 @@
     {v
     let goal compiled c_file =
         let o_file = change_file_extension "o" c_file in
     {v
     let goal compiled c_file =
         let o_file = change_file_extension "o" c_file in
-        target (file_exists o_file);
-        target (file_newer_than o_file c_file);
+        target (more_recent [o_file] [c_file]);
 
         sh "cc -c %s -o %s" c_file o_file
     }
 
         sh "cc -c %s -o %s" c_file o_file
     }
     before it can link the final program:
 
     {v
     before it can link the final program:
 
     {v
-    let goal built program sources =
-        target (file_exists program);
-        target (file_newer_than program sources);
+    let goal built program source =
+        target (more_recent [program] [source]);
 
 
-        List.iter (fun s -> require (compiled s)) sources;
+        require (compiled source);
 
 
-        let objects = List.map (change_file_extension "o") sources in
-        sh "cc %s -o %s" (String.concat " " objects) program
+        let object = change_file_extension "o" source in
+        sh "cc %s -o %s" object program
     }
 
 *)
 
 val target : bool -> unit
     }
 
 *)
 
 val target : bool -> unit
-  (** [target] {i predicate} defines the target condition that will
+  (** [target] {i condition} defines the target condition that {b will}
       be met once the current rule has run.
 
       Goaljobs is much more flexible than [make].  In [make] only a
       be met once the current rule has run.
 
       Goaljobs is much more flexible than [make].  In [make] only a
@@ -71,8 +69,8 @@ val target : bool -> unit
         ...
 
       let goal compiled () =
         ...
 
       let goal compiled () =
-        target (file_exists "foo.o");
-        target (file_newer_than "foo.o" "foo.c");
+        target (more_recent ["foo.o"] ["foo.c"]);
+        requires (file_exists "foo.c");
         ...
       }
 
         ...
       }
 
@@ -80,7 +78,13 @@ val target : bool -> unit
       can have any number of different targets.
 
       Almost every rule should have one or more targets, which should
       can have any number of different targets.
 
       Almost every rule should have one or more targets, which should
-      accurately state the outcome once the rule has been run
+      accurately state the outcome once the rule has been run.
+
+      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}.
 
       Normally you put the target(s) early on in the rule, before any
       running code and before any [require]s.  This is not a
 
       Normally you put the target(s) early on in the rule, before any
       running code and before any [require]s.  This is not a
@@ -88,6 +92,14 @@ val target : bool -> unit
       ensure the rule runs most efficiently since if the target is met
       already then the rest of the rule doesn't run. *)
 
       ensure the rule runs most efficiently since if the target is met
       already then the rest of the rule doesn't run. *)
 
+val target_all : bool list -> unit
+  (** [target_all [t1; t2; ...]] is the same as writing
+      [target (t1 && t2 && ...)] *)
+
+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
   (** [require] {!goal} defines the requirements of this rule, that
       is, other goals that have to be met before this rule is able to run.
@@ -96,7 +108,7 @@ val require : unit -> unit
       right hand side after the [:], but in goaljobs the requirements
       can be much richer than simply "that file must exist".
 
       right hand side after the [:], but in goaljobs the requirements
       can be much richer than simply "that file must exist".
 
-      Some simple rules don't need any [require]s.  Unlike with [make],
+      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. *)
 
       the requirements of a rule can be placed anywhere within the
       rule, as long as you put them before they are needed. *)
 
@@ -117,6 +129,27 @@ val file_newer_than : string -> string -> bool
       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. *)
 
       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. *)
 
+val more_recent : string list -> string list -> bool
+  (** [more_recent objects sources] expresses the [make] relationship:
+
+      {v object(s) ...: source(s) ...}
+
+      in a convenient way:
+
+      {v
+      let goal built objects sources =
+        target (more_recent objects sources);
+        ... code to rebuild ...
+      }
+
+      It is roughly equivalent to checking that all the object files
+      exist and are newer than all of the source files.
+
+      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. *)
+
 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.
 
@@ -165,19 +198,22 @@ val url_exists : string -> bool
     {v cd $HOME/data/ }
 *)
 
     {v cd $HOME/data/ }
 *)
 
-val sh : ('a, unit, string, unit) format4 -> 'a -> unit
+val sh : ('a, unit, string, unit) format4 -> 'a
   (** Run the command(s). *)
 
   (** Run the command(s). *)
 
-(*
-val shout : ('a, unit, string) format -> 'a
-  (** Run the command(s).  Anything printed on stdout is returned
-      as a single string (the trailing [\n] character, if any,
-      is not returned). *)
+val shout : ('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. *)
 
 
-val shlines : ('a, unit, string) format -> 'a
-  (** Run the command(s).  Any lines printed to stdout are returned
-      as a list of strings.  Trailing [\n] characters not returned. *)
+val shlines : ('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. *)
+
+(*
 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]). *)
 *)
@@ -188,9 +224,11 @@ val shell : string ref
     library (see the module [String]).  For convenience some
     extra functions are provided here. *)
 
     library (see the module [String]).  For convenience some
     extra functions are provided here. *)
 
+(*
 val replace_substring : string -> string -> string -> string
   (** [replace_substring patt repl string] replaces all occurrences
       of [patt] with [repl] in [string]. *)
 val replace_substring : string -> string -> string -> string
   (** [replace_substring patt repl string] replaces all occurrences
       of [patt] with [repl] in [string]. *)
+*)
 
 val change_file_extension : string -> string -> string
   (** [change_file_extension ext filename] changes the file extension
 
 val change_file_extension : string -> string -> string
   (** [change_file_extension ext filename] changes the file extension
@@ -199,11 +237,60 @@ val change_file_extension : string -> string -> string
       If the original filename has no extension, this function
       adds the extension. *)
 
       If the original filename has no extension, this function
       adds the extension. *)
 
+(*
 val filter_file_extension : string -> string list -> string
   (** [filter_file_extension ext filenames] returns only those
       filenames in the list which have the given file extension.
       For example [filter_file_extension "o" ["foo.c"; "bar.o"]]
       would return [["bar.o"]] (a single element list). *)
 val filter_file_extension : string -> string list -> string
   (** [filter_file_extension ext filenames] returns only those
       filenames in the list which have the given file extension.
       For example [filter_file_extension "o" ["foo.c"; "bar.o"]]
       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.
+*)
+
+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)] *)
+
+val memory_set : string -> string -> unit
+  (** Set [key] to [value] in the Memory. *)
+
+val memory_get : string -> string option
+  (** Return the current value of [key] in the Memory.  Returns [None]
+      if the key has never been set or was deleted. *)
+
+val memory_delete : string -> unit
+  (** Delete the [key].  If the key doesn't exist, has no effect. *)
 
 (**/**)
 
 
 (**/**)
 
@@ -215,4 +302,10 @@ val filter_file_extension : string -> string list -> string
  *)
 val goal_file_exists : string -> unit
 val goal_file_newer_than : string -> string -> unit
  *)
 val goal_file_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_url_exists : string -> unit
+val goal_memory_exists : string -> 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
diff --git a/m4/.gitignore b/m4/.gitignore
new file mode 100644 (file)
index 0000000..94e6f26
--- /dev/null
@@ -0,0 +1,5 @@
+/libtool.m4
+/ltoptions.m4
+/ltsugar.m4
+/ltversion.m4
+/lt~obsolete.m4
diff --git a/pa_goal.ml b/pa_goal.ml
new file mode 100644 (file)
index 0000000..06b2ec8
--- /dev/null
@@ -0,0 +1,130 @@
+(* goaljobs
+ * Copyright (C) 2013 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *)
+
+(* For general information about camlp4, see:
+ * http://brion.inria.fr/gallium/index.php/Camlp4
+ *
+ * For information about quotations, see:
+ * http://brion.inria.fr/gallium/index.php/Quotation
+ *)
+
+open Printf
+
+open Camlp4.PreCast
+open Syntax
+open Ast
+
+let locfail _loc msg = Loc.raise _loc (Failure msg)
+
+(* 'expr' is a function expression (RHS of a binding).  It has the
+ * form 'fun a -> fun b -> ...'.  Return the parameters and the body of
+ * the function.
+ *)
+let rec function_parameters = function
+  (* fun patt -> expr *)
+  | ExFun (_loc, McArr (_ploc, param, ExNil _, expr)) ->
+    let params, body = function_parameters expr in
+    ((_ploc, param) :: params), body
+  (* patt when expr -> expr *)
+  | ExFun (_loc, McArr (_, _, _, expr)) ->
+    locfail _loc "not supported: goal function uses when-clause"
+  | body -> [], body
+
+(* Define one or more 'let [rec] goal ... [and ...]' expressions.
+ *
+ * 'r' is Some _ if the rec keyword was defined.  'lets' is the list
+ * of let statements.
+ *)
+let generate_let_goal _loc (r : rec_flag) (lets : binding) =
+  (* lets might be a single binding, or multiple bindings using BiAnd
+   * ('let .. and').  Rewrite each individual goal in the list.
+   *)
+  let rec rewrite = function
+    | BiNil _loc -> BiNil _loc
+
+    (* let goal left = ... and right = ... *)
+    | BiAnd (_loc, left, right) ->
+      BiAnd (_loc, rewrite left, rewrite right)
+
+    (* let goal name = expr *)
+    | BiEq (_loc, PaId (_, (IdLid (_, name))), expr) ->
+      (* Rename the function to goal_<name>. *)
+      let gname = "goal_" ^ name in
+
+      (* Split the function into parameters and body. *)
+      let params, body = function_parameters expr in
+
+      if params = [] then
+        locfail _loc "goal must have some parameters; you probably want to put '()' here";
+
+      (* Put a try-clause around the body. *)
+      let body = <:expr< try $body$ with Goal_result Goal_OK -> () >> in
+
+      (* Recreate the function with parameters. *)
+      let expr =
+        List.fold_right (
+          fun (_ploc, param) rest ->
+            ExFun (_loc, McArr (_ploc, param, ExNil _ploc, rest))
+        ) params body in
+
+      <:binding< $lid:gname$ = $expr$ >>
+    | _ ->
+      locfail _loc "cannot parse 'let goal' expression"
+  in
+  let lets = rewrite lets in
+
+  (* let [rec] ... and ... in () *)
+  Ast.StVal (_loc, r, lets)
+
+(* Rewrite 'require (name args...)' as 'require (goal_name args)'.
+ * 'expr' is a function call.
+ *)
+let generate_require _loc expr =
+  (* Note that 'f a b c' is parsed as '((f a) b) c' so the actually
+   * function name is buried deeply in the tree.  Rewrite the name.
+   *)
+  let rec rewrite = function
+    | ExApp (_loc, ExId (_loc1, IdLid (_loc2, name)), right) ->
+      let gname = "goal_" ^ name in
+      ExApp (_loc, ExId (_loc1, IdLid (_loc2, gname)), right)
+    | ExApp (_loc, (ExApp _ as expr), right) ->
+      ExApp (_loc, rewrite expr, right)
+    | _ ->
+      locfail _loc "require (...) expression must contain a call to a goal"
+  in
+  let expr = rewrite expr in
+  <:expr< Goaljobs.require ($expr$) >>
+
+;;
+
+EXTEND Gram
+  GLOBAL: expr str_item;
+
+  (* Rewrite 'require (name args...)'. *)
+  expr: LEVEL "apply" [
+    [ "require"; e = expr ->
+      generate_require _loc e ]
+  ];
+
+  (* "str_item" is a top level statement in an OCaml program. *)
+  str_item: LEVEL "top" [
+    [ "let"; r = opt_rec; "goal"; ls = binding ->
+    generate_let_goal _loc r ls ]
+  ];
+
+END