From 3a572e209c501018d6a19f1533146be1c8b41ced Mon Sep 17 00:00:00 2001 From: "Richard W.M. Jones" Date: Fri, 20 Sep 2013 17:12:35 +0100 Subject: [PATCH] Lots of documentation. --- .gitignore | 1 + Makefile.am | 17 +++ configure.ac | 8 +- goaljobs-reference.pod | 362 +++++++++++++++++++++++++++++++++++++++++++++++++ goaljobs.mli | 185 +------------------------ goaljobs.pod | 118 ++++++++++++++++ goaljobs.spec.in | 4 +- 7 files changed, 511 insertions(+), 184 deletions(-) create mode 100644 goaljobs-reference.pod create mode 100644 goaljobs.pod diff --git a/.gitignore b/.gitignore index 42a7a0b..a385798 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.cmxa *.o *.a +*.1 Makefile.in Makefile diff --git a/Makefile.am b/Makefile.am index 11d4fbd..8a81cd3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -25,8 +25,11 @@ EXTRA_DIST = \ goaljobs_config.ml.in \ goaljobs.ml \ goaljobs.mli \ + goaljobs.pod \ + goaljobs-reference.pod \ goaljobs.spec \ goaljobs.spec.in \ + html/.gitignore \ META.in \ NOTES \ pa_goal.ml \ @@ -85,6 +88,20 @@ depend: .depend rpm: dist rpmbuild -ta $(PACKAGE_NAME)-$(PACKAGE_VERSION).tar.gz +if HAVE_POD2MAN + +man_MANS = goaljobs.1 goaljobs-reference.1 + +goaljobs.1: goaljobs.pod + $(POD2MAN) -c "Goaljobs" --release $(PACKAGE)-$(VERSION) $< > $@-t + mv $@-t $@ + +goaljobs-reference.1: goaljobs-reference.pod + $(POD2MAN) -c "Goaljobs" --release $(PACKAGE)-$(VERSION) $< > $@-t + mv $@-t $@ + +endif + if HAVE_OCAMLDOC # HTML library documentation. diff --git a/configure.ac b/configure.ac index cdf9cfb..ebc5cfd 100644 --- a/configure.ac +++ b/configure.ac @@ -99,10 +99,12 @@ AM_CONDITIONAL([HAVE_OCAMLDOC], [test "x$OCAMLDOC" != "xno"]) dnl Check for POD (for manual pages). -AC_CHECK_PROG(PERLDOC,perldoc,perldoc) -if test "x$PERLDOC" = "x"; then - AC_MSG_ERROR([You must install the 'perldoc' program]) +AC_CHECK_PROG(POD2MAN,pod2man,pod2man,no) +if test "x$POD2MAN" = "x"; then + AC_MSG_ERROR([You must install the 'pod2man' program]) fi +AM_CONDITIONAL([HAVE_POD2MAN], + [test "x$POD2MAN" != "xno"]) AC_CONFIG_HEADERS([config.h]) AC_CONFIG_FILES([goaljobs_config.ml diff --git a/goaljobs-reference.pod b/goaljobs-reference.pod new file mode 100644 index 0000000..7912568 --- /dev/null +++ b/goaljobs-reference.pod @@ -0,0 +1,362 @@ +=encoding utf8 + +=head1 NAME + +goaljobs-reference - reference documentation for writing goaljobs scripts + +=head1 SUMMARY + + open Unix + open Printf + open Goaljobs + + let goal name args... = + target (condition); + require (goal1 ...); + require (goal2 ...); + (* code to implement the goal *) + + let goal all () = + require (name args...) + + every 30 minutes ( + fun () -> + require (goal1 ...) + ) + +=head1 DESCRIPTION + +Goaljobs is a flexible build system and business rules manager similar +to make and cron, but much more powerful. You can use it to automate +many complex tasks that have multiple steps (even with manual steps) +that have to be carried out in dependency order. + +For a tutorial-like introduction to goaljobs, see +L + +For examples, see the C directory in the source and +L + +For reference documentation on how to write scripts, see below. + +Note this man page does not cover the whole Goaljobs API. To read +about the Goaljobs API, look for the file C (in the +source code or installed as part of the goaljobs package), or see the +HTML documentation installed as part of the goaljobs package. + +=head1 THE SCRIPT FILE + +The script file should usually start with opening these modules (none +of this are required, it's just useful to have them open): + + open Unix + open Printf + open Goaljobs + +This is followed by goals and/or functions and/or top-level OCaml +statements and/or C statements (a.k.a periodic jobs). + +You can use multiple script files to make up a goaljobs program. You +have to list them in dependency order on the goaljobs command line +(earlier files required by later files), the same way that the OCaml +compiler works. So usually you end up writing: + + goaljobs utils.ml another_library.ml script.ml + +where C requires the functions from the utils/library. +Note that circular dependencies are not possible. + +=head1 GOALS + +Each goal should have the following basic form: + + let goal name args... = + target (condition); + require (goal1 ...); + require (goal2 ...); + (* code to implement the goal *) + +There is no hard-and-fast rule about this. In particular you can put +arbitrary OCaml statements anywhere inside a goal (since a goal is +just a special form of OCaml function), but sticking to this overall +plan is a good idea. + +There should be zero or one target. Multiple target statements should +not be used in a goal. The target should come as early as possible, +and the target condition should be as simple and fast to evaluate as +is practical (see L below). + +There should be zero or any number of C statements. Each +require statement should name a single goal (with optional parameters +for that goal). + +After that should come the code that implements the goal, which might +be, for example, a series of shell commands, but could even be +user-interactive. + +As with ordinary OCaml functions, you can define goals recursively +or with mutual recursion using: + + let rec goal1 args... = + ... + and goal2 args... = + ... + and goal3 args... = + ... + +A goal can also have no arguments: + + let goal all () = + ... + +This defines the common goal called C, which acts the same way as +C, ie. if you run the program without any arguments, it will +run the C goal if one exists. + +=head2 PUBLISHING GOALS + +If a goal is "published" it means it is available to be run directly +from the command line. All no-arg goals are published by default. +You do not need to do anything special for them. For example: + + let goal clean () = sh "rm *~" + +can be used on the command line: + + ./myscript clean + +For goals which take any parameters, you have to define a small code +snippet that converts command line arguments to goal parameters (the +reason has to do with OCaml being strongly typed, and because goal +parameters might not all be strings). + + let goal compile program sources = + target (more_recent [program] sources); + ... + + let () = + publish "compile" ( + fun args -> + let program = List.hd args in + let sources = List.tl args in + require (compiled program sources) + ) + +Then you can use: + + ./myscript compile program main.c utils.c + +=head1 TARGET AND REQUIRE + +The target is promise or contract that you make that the given +condition I be true when the goal has finished running. + +In the first example, the target is that the C (object) exists +and is newer than the C (source). The goal meets that target +by running the C compiler (C) which, if it succeeds, will ensure +that the object file exists and is newer than the source file. + + let goal compiled c_file = + let o_file = change_file_extension "o" c_file in + target (more_recent [o_file] [c_file]); + + sh " + cd $builddir + cc -c %s -o %s + " c_file o_file + +In the second example, the goal requires that several files have been +compiled (C) before it can link the final +program: + + let goal built program source = + target (more_recent [program] [source]); + + require (compiled source); + + let object = change_file_extension "o" source in + sh " + cd $builddir + cc %s -o %s + " object program + +=head1 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 C 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: + + 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 + " repo_url; + + (* 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) + ) + +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. + +=head1 SHELL + +You can call out to the Unix shell using one of the functions +C, C or C. (These +functions are documented in the C file / API +documentation.) + +C runs the command(s). C collects the output of the +command (to stdout only) and returns it as a single string. +C collects the output and returns it as a list of lines. + +C, C, C work like printf. ie. You can substitute +variables using C<%s>, C<%d> and so on. For example: + + sh "rsync foo-%s.tar.gz example.com:/html/" version + +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, C somewhere. If you don't want the temporary +directory creation, use C<~tmpdir:false>. + +The environment variable C<$builddir> is exported to the script. This +is the current directory when the goaljobs program was started. + +Each invocation of C (etc) is a single shell (this is slightly +different from how C works). For example: + + 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 C function as a whole exits with +an error. Write: + + command ||: + +to ignore the result of a command. + +C is used unless you set C to some other +value. Note that the environment variable C is I used. + +=head1 THE MEMORY + +"The Memory" is key/value storage which persists across goaljobs +sessions. It is stored in the file C<$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: +C + +A common pattern is: + +let goal tested version = + let key = "myapp-tested-" ^ version in + target (memory_exists key); + + (* some code to test this version *) + + memory_set key "1" + +Note in that example the value C<1> is arbitrary. You just want to +store I value so that a later call to C will +succeed. + +For information about C APIs see the +C file / API documentation. + +=head1 FILES + +=over 4 + +=item C + +This is the default shell used by C APIs. You can change +the shell by setting the C reference. + +=item C + +The curl program (on the path) is used to check for and download +URLs by APIs such as C. + +=item C<~/.goaljobs-memory> + +Persistent key/value store used when you use the C +APIs. + +=back + +=head1 ENVIRONMENT VARIABLES + +=over 4 + +=item B + +This environment variable is set to the current directory when the +goals program starts, and is available in goals, shell scripts, etc. + +=back + +=head1 SEE ALSO + +L + +=head1 AUTHORS + +Richard W.M. Jones + +=head1 COPYRIGHT + +(C) Copyright 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. diff --git a/goaljobs.mli b/goaljobs.mli index 3e1ca0c..c1dff52 100644 --- a/goaljobs.mli +++ b/goaljobs.mli @@ -16,45 +16,9 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *) -(** {1 Goaljobs library of useful helper functions.} *) +(** {1 Goaljobs library API} *) -(** {2 Target and require} - - These are used to write goals. - - 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, 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. - - {v - let goal compiled c_file = - let o_file = change_file_extension "o" c_file in - target (more_recent [o_file] [c_file]); - - sh "cd $builddir && cc -c %s -o %s" c_file o_file - v} - - In the second example, the goal 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 "cd $builddir && cc %s -o %s" object program - v} - -*) +(** {2 Target and require} *) val target : bool -> unit (** [target] {i condition} defines the target condition that {b will} @@ -123,53 +87,7 @@ val require : (unit -> unit) -> unit 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. *) +(** {2 Periodic jobs} *) (* This is what lets you write '30 minutes' etc: *) type period_t = Seconds | Days | Months | Years @@ -269,49 +187,7 @@ val (//) : string -> string -> string val quote : string -> string (** Quote the string to make it safe to pass directly to the shell. *) -(** {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 - 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: - - {v - sh " - package=foo-%s - tarball=$package.tar.gz - cp $HOME/$tarball . - tar zxf $tarball - cd $package - ./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} - to ignore the result of a command. - -*) +(** {2 Shell} *) val sh : ?tmpdir:bool -> ('a, unit, string, unit) format4 -> 'a (** Run the command(s). @@ -370,34 +246,7 @@ val filter_file_extension : string -> string list -> string 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" - 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. *) +(** {2 Memory (persistent key/value storage)} *) val memory_exists : string -> bool (** [memory_exists key] checks that the named [key] exists in @@ -416,29 +265,7 @@ val memory_get : string -> string option val memory_delete : string -> unit (** Delete the [key]. If the key doesn't exist, has no effect. *) -(** {2 Publishing goals} - - To "publish" a goal means it's available on the command line - for users to use directly. - - Goals that have zero arguments are {b automatically published}. - So for example: - - {v - let goal clean () = sh "rm *~" - v} - - can be used on the command line: - - {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 goal in the file. - - You can also publish goals, especially ones which take a non-zero - number of parameters, by calling {!publish}. -*) +(** {2 Publishing goals} *) val publish : string -> (string list -> unit) -> unit (** Publish the named goal. diff --git a/goaljobs.pod b/goaljobs.pod new file mode 100644 index 0000000..3aef222 --- /dev/null +++ b/goaljobs.pod @@ -0,0 +1,118 @@ +=encoding utf8 + +=head1 NAME + +goaljobs - make and cron replacement and business rules manager + +=head1 SUMMARY + + goaljobs [-o output] [lib.ml...] goals.ml + + ./goals -l + + ./goals all + ./goals + +=head1 DESCRIPTION + +Goaljobs is a flexible build system and business rules manager similar +to make and cron, but much more powerful. You can use it to automate +many complex tasks that have multiple steps (even with manual steps) +that have to be carried out in dependency order. + +=head2 GETTING STARTED + +For a tutorial-like introduction to goaljobs, see +L + +For examples, see the C directory in the source and +L + +For reference documentation on how to write scripts, see the +L man page. + +To read about the Goaljobs API, look for the file C (in +the source code or installed as part of the goaljobs package), or see +the HTML documentation installed as part of the goaljobs package. + +=head2 GOALJOBS WRAPPER SCRIPT + +C is a wrapper script that compiles your goals into a binary +(program) which can then be run. It takes a set of input files +(C<*.ml>) and writes a single output program. The output program has +the same name as the last input file minus C<.ml>, but you can +override this using the I<-o> option. + +Note that the C script is just a simple wrapper around the +OCaml compiler (L). + +=head1 OPTIONS + +=over 4 + +=item B<--help> + +Display short help and exit. + +=item B<-g> + +Enable debugging symbols in the output binary. + +=item B<-I> directory + +=item B<-I> +package + +Pass these options directly to the OCaml compiler. You can use this +to include OCaml modules from another directory. The C<+package> form +includes a package relative to the OCaml library directory +(C). + +=item B<-o> output + +Set the name of the output binary. + +The default is to use the name of the final source file, after +removing the C<.ml> extension. + +=item B<--package> package(s) + +This passes I<-package package(s)> to L. You can use it +to enable OCaml packages, eg: + + goaljobs --package bigarray,libvirt source.ml + +=item B<--pkgdir> dir + +If goaljobs is installed in a non-standard directory, pass the name of +the directory using this option. You can also use this if you want to +run goaljobs without installing it (pass the goaljobs source +directory). + +=back + +=head1 SEE ALSO + +L, +L + +=head1 AUTHORS + +Richard W.M. Jones + +=head1 COPYRIGHT + +(C) Copyright 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. diff --git a/goaljobs.spec.in b/goaljobs.spec.in index 7532cb8..f84975d 100644 --- a/goaljobs.spec.in +++ b/goaljobs.spec.in @@ -15,7 +15,7 @@ BuildRequires: ocaml-findlib-devel BuildRequires: ocaml-camlp4-devel # For building manual pages. -BuildRequires: /usr/bin/perldoc +BuildRequires: perl-podlators # Requires camlp4 and ocamlfind at runtime. Requires: /usr/bin/ocamlc @@ -69,7 +69,7 @@ rm $RPM_BUILD_ROOT%{_libdir}/ocaml/goaljobs/goaljobs.cma %else %{_libdir}/ocaml/goaljobs/goaljobs.cma %endif -#%{_mandir}/man1/*.1* +%{_mandir}/man1/*.1* %changelog -- 1.8.3.1