From: Kim Nguyễn Date: Wed, 17 Apr 2013 06:08:10 +0000 (+0200) Subject: Add the remake and configure infrastructure. X-Git-Tag: v0.1~102 X-Git-Url: http://git.nguyen.vg/gitweb/?p=tatoo.git;a=commitdiff_plain;h=8af68de11421dacc2f77a8398dcb48c75a5ff3b1 Add the remake and configure infrastructure. --- diff --git a/Remakefile.in b/Remakefile.in new file mode 100644 index 0000000..74fef80 --- /dev/null +++ b/Remakefile.in @@ -0,0 +1,134 @@ +OCAMLFINDPACKAGES = "ulex,unix,expat,camlp4.macro" +OCAMLFINDSYNTAX = camlp4o +OCAMLFINDPPOPTS = $(addprefix "-ppopt ", @CAMLP4FLAGS@ -I include) +OCAMLFINDINCLUDES = $(addprefix "-I ", src) +OCAMLFINDFLAGS = -syntax $(OCAMLFINDSYNTAX) -package $(OCAMLFINDPACKAGES) \ + $(OCAMLFINDPPOPTS) $(OCAMLFINDINCLUDES) +OCAMLFINDLINKFLAGS = -linkpkg +ODEP=tools/odeps.sh @OCAMLDEP@ +BIN=@PACKAGE_TARNAME@ +EXE=@EXE@ + + + +Remakefile: Remakefile.in config.status + ./config.status Remakefile + +configure config.status: configure.in + autoconf + ./config.status --recheck + +src/%.native: + base=${1%.native} + src=${base}.ml + obj=${base}.cmx + dir=$(dirname $1) + ./remake -v OCAMLDEPNATIVE=-native "$obj" + SRCS=$(ls "$dir"/*.cmx | sed 's/\.cmx/.ml/g') + OBJS=$(@OCAMLDEP@ $OCAMLDEPNATIVE $OCAMLFINDFLAGS -modules $SRCS | tools/osort.sh cmx) + @OCAMLOPT@ -o "$1" @OCAMLFLAGS@ @OCAMLOPTFLAGS@ $OCAMLFINDLINKFLAGS $OCAMLFINDFLAGS $OBJS + +src/%.byte: + base=${1%.byte} + src=${base}.ml + obj=${base}.cmo + dir=$(dirname $1) + ./remake "$obj" + OBJS="" + for i in "$dir"/*.cmi; do + ibase=${i%.cmi} + if test -f "${ibase}.ml" -o -d "${ibase}" -o -f "${ibase}.mly" -o -f "${ibase}.mll"; then + OBJS="$OBJS ${ibase}.cmo" + fi + done + ./remake $OBJS + SRCS=$(ls "$dir"/*.cmo | sed 's/\.cmo/.ml/g') + SORTED_OBJS=$(@OCAMLDEP@ $OCAMLFINDFLAGS -modules $SRCS | tools/osort.sh cmo) + @OCAMLC@ -o "$1" @OCAMLFLAGS@ @OCAMLCFLAGS@ $OCAMLFINDLINKFLAGS $OCAMLFINDFLAGS $SORTED_OBJS + +%.cmx: + target="$1" + base="${target%.cmx}" + src="${base}.ml" + if [ -f "$src" ]; then + ./remake "$src" + DEPS=$( $ODEP -native $OCAMLFINDFLAGS $PACKINCLUDE "$src" ) + echo "$DEPS" | ./remake -v PACKINCLUDE="$PACKINCLUDE" -v OCAMLDEPNATIVE=-native -v OCAMLFORPACK="$OCAMLFORPACK" -r "$target" + @OCAMLOPT@ -o "$target" -c @OCAMLFLAGS@ @OCAMLOPTFLAGS@ $OCAMLFORPACK $OCAMLFINDFLAGS $PACKINCLUDE "$src" + elif [ -d "$base" ]; then + modname=$(basename "$base") + packname=$(echo "$modname" | sed 's/\(.*\)/\u\1/') + OBJS=$(ls "$base"/*.ml | sed 's/\.ml/.cmx/g') + ./remake -v PACKINCLUDE="-I $base" -v OCAMLFORPACK="-for-pack $packname" $OBJS + SORTED_OBJS=$(@OCAMLDEP@ -native $OCAMLFINDFLAGS -I $base -modules "$base"/*.ml | tools/osort.sh cmx) + @OCAMLOPT@ -o "$target" -pack $SORTED_OBJS + elif [ -f "$base".mly ]; then + ./remake "$base".mly + @OCAMLYACC@ "$base".mly + $ODEP $OCAMLDEPNATIVE $OCAMLFINDFLAGS "$src" | \ + ./remake -v PACKINCLUDE="$PACKINCLUDE" -v OCAMLDEPNATIVE=-native -v OCAMLFORPACK="$OCAMLFORPACK" -r "$target" + @OCAMLOPT@ -o "$target" -c @OCAMLFLAGS@ @OCAMLOPTFLAGS@ $OCAMLFORPACK $OCAMLFINDFLAGS $PACKINCLUDE "$src" + fi + + +%.cmo: + target="$1" + base="${target%.cmo}" + src="${base}.ml" + if [ -f "$src" ]; then + ./remake "$src" + DEPS=$( $ODEP $OCAMLFINDFLAGS $PACKINCLUDE "$src" ) + echo "$DEPS" | ./remake -v PACKINCLUDE="$PACKINCLUDE" -r "$target" + @OCAMLC@ -o "$target" -c @OCAMLFLAGS@ @OCAMLCFLAGS@ $OCAMLFINDFLAGS $PACKINCLUDE "$src" + elif [ -d "$base" ]; then + modname=$(basename "$base") + packname=$(echo "$modname" | sed 's/\(.*\)/\u\1/') + OBJS=$(ls "$base"/*.ml | sed 's/\.ml/.cmo/g') + ./remake -v PACKINCLUDE="-I $base" $OBJS + SORTED_OBJS=$(@OCAMLDEP@ $OCAMLFINDFLAGS -I $base -modules "$base"/*.ml | tools/osort.sh cmo) + @OCAMLC@ -o "$target" -pack $SORTED_OBJS + elif [ -f "$base".mly ]; then + ./remake "$base".mly + @OCAMLYACC@ "$base".mly + $ODEP $OCAMLFINDFLAGS $PACKINCLUDE "$src" | ./remake -v PACKINCLUDE="$PACKINCLUDE" -r "$target" + @OCAMLC@ -o "$target" -c @OCAMLFLAGS@ @OCAMLCFLAGS@ $OCAMLFINDFLAGS $PACKINCLUDE "$src" + fi + + + +%.cmi: + target="$1" + base=${target%.cmi} + if test -f "$base".mli; then + ./remake "$base".mli + src=${base}.mli + DEPS=$($ODEP $OCAMLDEPNATIVE $OCAMLFINDFLAGS $PACKINCLUDE "$src") + echo "$DEPS" | ./remake -v PACKINCLUDE="$PACKINCLUDE" -v OCAMLDEPNATIVE="$OCAMLDEPNATIVE" -v OCAMLFORPACK="$OCAMLFORPACK" -r "$target" + if test -z "$OCAMLDEPNATIVE"; then + @OCAMLC@ -o "$target" -c @OCAMLFLAGS@ @OCAMLCFLAGS@ $OCAMLFINDFLAGS $PACKINCLUDE "$src" + else + @OCAMLOPT@ -o "$target" -c @OCAMLFLAGS@ @OCAMLOPTFLAGS@ $OCAMLFINDFLAGS $PACKINCLUDE "$src" + fi + else + if test -z "$OCAMLDEPNATIVE"; then + obj=${base}.cmo + else + obj=${base}.cmx + fi + ./remake -v PACKINCLUDE="$PACKINCLUDE" -v OCAMLDEPNATIVE="$OCAMLDEPNATIVE" -v OCAMLFORPACK="$OCAMLFORPACK" "$obj" + fi + +clean: + find src -name '*.cm*' -o -name '*.o' -o -name '*.byte' -o -name '*.native' -o -name '*.mll' -o -name '*.mly' | while read file; do + case "$file" in + *.mll) + rm -f "${file%.mll}.ml" + ;; + *.mly) + rm -f "${file%.mly}.ml" "${file%.mly}.mli" + ;; + *) + rm -f "$file" + ;; + esac + done diff --git a/configure.in b/configure.in new file mode 100644 index 0000000..fb09327 --- /dev/null +++ b/configure.in @@ -0,0 +1,253 @@ +AC_INIT([TAToo], + [0.0.1], + [Kim Nguyễn ], + [tatoo]) + + +#detect ocamlfind +OCAMLFIND=ocamlfind +AC_ARG_WITH([ocamlfind], + AS_HELP_STRING([--with-ocamlfind=PATH], [location of the ocamlfind binary]), + [OCAMLFIND="$withval"]) +AC_MSG_CHECKING([for ocamlfind ($OCAMLFIND)]) +$OCAMLFIND list >/dev/null 2>&1 +if test $? ; then + AC_MSG_RESULT([found]) + OCAMLC="$OCAMLFIND ocamlc" + OCAMLOPT="$OCAMLFIND ocamlopt" + OCAMLDEP="$OCAMLFIND ocamldep" +else + AC_MSG_RESULT([not found]) + AC_MSG_ERROR([Cannot find ocamlfind.]) +fi + +AC_SUBST([OCAMLFIND]) + +#check ocamlc +AC_MSG_CHECKING([for $OCAMLC]) +$OCAMLC >/dev/null 2>&1 +if test $? ; then + AC_MSG_RESULT([ok]) +else + AC_MSG_RESULT([failed]) + AC_MSG_ERROR(Cannot find ocamlc.) +fi +AC_SUBST([OCAMLC]) + +#check ocamlopt +AC_MSG_CHECKING([for $OCAMLOPT]) +$OCAMLOPT >/dev/null 2>&1 +if test $? ; then + AC_MSG_RESULT([ok]) +else + AC_MSG_RESULT([failed]) + AC_MSG_ERROR(Cannot find ocamlopt.) +fi +AC_SUBST([OCAMLOPT]) + +#check ocamldep +AC_MSG_CHECKING([for $OCAMLDEP]) +$OCAMLDEP >/dev/null 2>&1 +if test $? ; then + AC_MSG_RESULT([ok]) +else + AC_MSG_RESULT([failed]) + AC_MSG_ERROR(Cannot find ocamldep.) +fi +AC_SUBST([OCAMLDEP]) + +#ocaml version +OCAMLVERSION=$($OCAMLC -version) +AC_MSG_CHECKING([ocaml version]) +AC_MSG_RESULT([$OCAMLVERSION]) + +case "$OCAMLVERSION" in + 3.12.*) ;; 4.*) ;; + *) AC_MSG_ERROR([OCaml $OCAMLVERSION is too old]) + ;; +esac + +AC_SUBST(OCAMLVERSION) + +#detect ocamlyacc +OCAMLYACC=ocamlyacc +AC_ARG_WITH([ocamlyacc], + AS_HELP_STRING([--with-ocamlyacc=PATH], [location of the ocamlyacc binary]), + [OCAMLYACC="$withval"]) +AC_MSG_CHECKING([for ocamlyacc ($OCAMLYACC)]) +OCAMLYACC_VERSION=$($OCAMLYACC -version 2>/dev/null || echo foo) +case "$OCAMLYACC_VERSION" in + *$OCAMLVERSION) + AC_MSG_RESULT([ok]) + ;; + foo) + AC_MSG_RESULT([failed]) + AC_MSG_ERROR([Cannot find ocamlyacc]) + ;; + *) + AC_MSG_RESULT([failed]) + AC_MSG_ERROR([Bad ocamlyacc version]) + ;; +esac +AC_SUBST([OCAMLYACC]) + +#detect ocamllex +OCAMLLEX=ocamllex +AC_ARG_WITH([ocamllex], + AS_HELP_STRING([--with-ocamllex=PATH], [location of the ocamllex binary]), + [OCAMLLEX="$withval"]) +AC_MSG_CHECKING([for ocamllex ($OCAMLLEX)]) +OCAMLLEX_VERSION=$($OCAMLLEX -version 2>/dev/null || echo foo) +case "$OCAMLLEX_VERSION" in + *$OCAMLVERSION) + AC_MSG_RESULT([ok]) + ;; + foo) + AC_MSG_RESULT([failed]) + AC_MSG_ERROR([Cannot find ocamllex]) + ;; + *) + AC_MSG_RESULT([failed]) + AC_MSG_ERROR([Bad ocamllex version]) + ;; +esac +AC_SUBST([OCAMLLEX]) + + +#detect camlp4 +CAMLP4=camlp4 +AC_ARG_WITH([camlp4], + AS_HELP_STRING([--with-camlp4=PATH], [location of the camlp4 binary]), + [CAMLP4="$withval"]) +AC_MSG_CHECKING([for camlp4 ($CAMLP4)]) +CAMLP4_VERSION=$($CAMLP4 -version 2>/dev/null || echo foo) +case "$CAMLP4_VERSION" in + *$OCAMLVERSION) + AC_MSG_RESULT([ok]) + ;; + foo) + AC_MSG_RESULT([failed]) + AC_MSG_ERROR([Cannot find camlp4]) + ;; + *) + AC_MSG_RESULT([failed]) + AC_MSG_ERROR([Bad camlp4 version]) + ;; +esac +AC_SUBST([CAMLP4]) + +# platform +AC_MSG_CHECKING([platform]) +OCAML_PLATFORM=$(echo 'print_endline Sys.os_type;;' | ocaml -noprompt | grep '^@<:@A-Z@:>@') +AC_MSG_RESULT([$OCAML_PLATFORM]) +if test "$OCAML_PLATFORM" = "Win32"; then + EXE=.exe +else + EXE= +fi +AC_SUBST(OCAML_PLATFORM) +AC_SUBST(EXE) + +# required libraries +AC_MSG_CHECKING([for ulex]) +$OCAMLFIND query ulex >/dev/null 2>&1 +if test $? ; then + AC_MSG_RESULT([found]) +else + AC_MSG_RESULT([not found]) + AC_MSG_ERROR([Cannot find ulex.]) +fi + +AC_MSG_CHECKING([for expat]) +$OCAMLFIND query ulex >/dev/null 2>&1 +if test $? ; then + AC_MSG_RESULT([found]) +else + AC_MSG_RESULT([not found]) + AC_MSG_ERROR([Cannot find ulex.]) +fi + +#compilation options +#debugging mode +OCAMLFLAGS=$OCAMLFLAGS +CAMLP4FLAGS=$CAMLP4FLAGS +OCAMLCFLAGS=$OCAMLCFLAGS +OCAMLOPTFLAGS=$OCAMLOPTFLAGS + +AC_ARG_ENABLE([debug], + [ --enable-debug build in debug mode], + [DEBUG=$enableval], + [DEBUG=no]) + +if test "x$DEBUG" = "xyes"; then + OCAMLFLAGS="$OCAMLFLAGS -g" + CAMLP4FLAGS="$CAMLP4FLAGS -DDEBUG" +fi + +#profiling +AC_ARG_ENABLE([profile], + [ --enable-profile compile in profiling code], + [PROFILE=$enableval], + [PROFILE=no]) + +if test "x$PROFILE" = "xyes"; then + OCAMLOPTFLAGS="$OCAMLOPTFLAGS -p" + OCAMLC="$OCAMLFIND ocamlcp" +fi + +#tracing code +AC_ARG_ENABLE([trace], + [ --enable-trace add tracing code], + [TRACE=$enableval], + [TRACE=no]) + +if test "x$TRACE" = "xyes"; then + CAMLP4FLAGS="$CAMLP4FLAGS -DHTMLTRACE" +fi + +#inlining + +AC_ARG_ENABLE([inline], + [ --enable-inline set OCaml inlining level (default=100)], + [INLINE=$enableval], + [INLINE=100]) + +#unsafe +AC_ARG_ENABLE([unsafe], + [ --enable-unsafe use unsafe array and string accesses], + [UNSAFE=1], + [UNSAFE=0]) + +if test "x$UNSAFE" = "xyes"; then + CAMLP4FLAGS="$CAMLP4FLAGS -unsafe" +fi + + +AC_PROG_CXX() + + +AC_SUBST([REMAKE], [./remake$EXE]) +if test ! -x "$REMAKE" -o "$REMAKE" -ot remake.cpp; then +AC_MSG_NOTICE([creating $REMAKE]) +case $(uname -s) in +MINGW*) + $CXX -Wall -O2 -o remake.exe remake.cpp -lws2_32 + if test $? != 0; then AC_MSG_FAILURE([failed]); fi + ;; +*) + $CXX -Wall -O2 -o remake remake.cpp + if test $? != 0; then AC_MSG_FAILURE([failed]); fi + ;; +esac +else +AC_MSG_NOTICE([$REMAKE exists, not rebuilding]) +fi + + +AC_SUBST(INLINE) +AC_SUBST(OCAMLFLAGS) +AC_SUBST(OCAMLCFLAGS) +AC_SUBST(OCAMLOPTFLAGS) +AC_SUBST(CAMLP4FLAGS) +AC_CONFIG_FILES(Remakefile) +AC_OUTPUT diff --git a/remake.cpp b/remake.cpp new file mode 100644 index 0000000..5d8dc3d --- /dev/null +++ b/remake.cpp @@ -0,0 +1,2318 @@ +/** +@mainpage Remake, a build system that bridges the gap between make and redo. + +As with make, remake uses a centralized rule file, which is +named Remakefile. It contains rules with a make-like +syntax: + +@verbatim +target1 target2 ... : dependency1 dependency2 ... + shell script + that builds + the targets +@endverbatim + +A target is known to be up-to-date if all its dependencies are. If it +has no known dependencies yet the file already exits, it is assumed to +be up-to-date. Obsolete targets are rebuilt thanks to the shell script +provided by the rule. + +As with redo, remake supports dynamic dependencies in +addition to these static dependencies. Whenever a script executes +remake dependency4 dependency5 ..., these dependencies are +rebuilt if they are obsolete. (So remake acts like +redo-ifchange.) Moreover, these dependencies are stored in file +.remake so that they are remembered in subsequent runs. Note that +dynamic dependencies from previous runs are only used to decide whether a +target is obsolete; they are not automatically rebuilt when they are +obsolete yet a target depends on them. They will only be rebuilt once the +dynamic call to remake is executed. + +In other words, the following two rules have almost the same behavior. + +@verbatim +target1 target2 ... : dependency1 dependency2 ... + shell script + +target1 target2 ... : + remake dependency1 dependency2 ... + shell script +@endverbatim + +(There is a difference if the targets already exist, have never been +built before, and the dependencies are either younger or obsolete, since +the targets will not be rebuilt in the second case.) + +The above usage of dynamic dependencies is hardly useful. Their strength +lies in the fact that they can be computed on the fly: + +@verbatim +%.o : %.c + gcc -MMD -MF $1.d -o $1 -c ${1%.o}.c + remake -r < $1.d + rm $1.d + +%.cmo : %.ml + ocamldep ${1%.cmo}.ml | remake -r $1 + ocamlc -c ${1%.cmo}.ml + +after.xml: before.xml rules.xsl + xsltproc --load-trace -o after.xml rules.xsl before.xml 2> deps + remake $(sed -n -e "\\,//,! s,^.*URL=\"\\([^\"]*\\).*\$,\\1,p" deps) + rm deps +@endverbatim + +Note that the first rule fails if any of the header files included by +a C source file has to be automatically generated. In that case, one +should perform a first call to remake them before calling the +compiler. (Dependencies from several calls to remake are +cumulative, so they will all be remembered the next time.) + +\section sec-usage Usage + +Usage: remake options targets + +Options: + +- -d: Echo script commands. +- -j[N], --jobs=[N]: Allow N jobs at once; infinite jobs + with no argument. +- -k, --keep-going: Keep going when some targets cannot be made. +- -r: Look up targets from the dependencies on standard input. +- -s, --silent, --quiet: Do not echo targets. + +\section sec-syntax Syntax + +Lines starting with a space character or a tabulation are assumed to be rule +scripts. They are only allowed after a rule header. + +Lines starting with # are considered to be comments and are ignored. +They do interrupt rule scripts though. + +Any other line is either a rule header or a variable definition. If such a +line ends with a backslash, the following line break is ignored and the line +extends to the next one. + +Rule headers are a nonempty list of names, followed by a colon, followed by +another list of names, possibly empty. Variable definitions are a single +name followed by equal followed by a list of names, possibly empty. Basically, +the syntax of a rule is as follows: + +@verbatim +targets : prerequisites + shell script +@endverbatim + +List of names are space-separated sequences of names. If a name contains a +space character, it should be put into double quotes. Names can not be any +of the following special characters :$(),=". Again, quotation +should be used. Quotation marks can be escaped by a backslash inside +quoted names. + +\subsection sec-variables Variables + +Variables can be used to factor lists of targets or dependencies. They are +expanded as they are encountered during Remakefile parsing. + +@verbatim +VAR1 = c d +VAR2 = a $(VAR1) b +$(VAR2) e : +@endverbatim + +Variables can be used inside rule scripts; they are available as non-exported +shell variables there. + +\subsection sec-functions Built-in functions + +remake also supports a few built-in functions inspired from make. + +- $(addprefix prefix, list) returns the list obtained + by prepending its first argument to each element of its second argument. +- $(addsuffix suffix, list) returns the list obtained + by appending its first argument to each element of its second argument. + +Note that functions are ignored inside scripts. + +\section sec-semantics Semantics + +\subsection src-obsolete When are targets obsolete? + +A target is obsolete: + +- if there is no file corresponding to the target, or to one of its siblings + in a multi-target rule, +- if any of its dynamic dependencies from a previous run or any of its static + prerequisites is obsolete, +- if the latest file corresponding to its siblings or itself is older than any + of its dynamic dependencies or static prerequisites. + +In all the other cases, it is assumed to be up-to-date (and so are all its +siblings). Note that the last rule above says "latest" and not "earliest". While +it might cause some obsolete targets to go unnoticed in corner cases, it allows +for the following kind of rules: + +@verbatim +config.h stamp-config_h: config.h.in config.status + ./config.status config.h + touch stamp-config_h +@endverbatim + +A config.status file generally does not update header files (here +config.h) if they would not change. As a consequence, if not for the +stamp-config_h file above, a header would always be considered obsolete +once one of its prerequisites is modified. Note that touching config.h +rather than stamp-config_h would defeat the point of not updating it +in the first place, since the program files would need to be rebuilt. + +Once all the static prerequisites of a target have been rebuilt, remake +checks if the target still needs to be built. If it was obsolete only because +its dependencies needed to be rebuilt and none of them changed, the target is +assumed to be up-to-date. + +\subsection sec-rules How are targets (re)built? + +There are two kinds of rules. If any of the targets or prerequisites contains +a % character, the rule is said to be generic. All the +targets of the rule shall then contain a single % character. All the +other rules are said to be specific. + +A rule is said to match a given target: + +- if it is specific and the target appears inside its target list, +- if it is generic and there is a way to replace the % character + from one of its targets so that it matches the given target. + +When remake tries to build a given target, it looks for a specific rule +that matches it. If there is one and its script is nonempty, it uses it to +rebuild the target. + +Otherwise, it looks for a generic rule that match the target. If there are +several matching rules, it chooses the one with the shortest pattern (and if +there are several ones, the earliest one). remake then looks for +specific rules that match each target of the generic rule. All the +prerequisites of these specific rules are added to those of the generic rule. +The script of the generic rule is used to build the target. + +Example: + +@verbatim +t%1 t2%: p1 p%2 + commands building t%1 and t2% + +t2z: p4 + commands building t2z + +ty1: p3 + +# t2x is built by the first rule (which also builds tx1) and its prerequisites are p1, px2 +# t2y is built by the first rule (which also builds ty1) and its prerequisites are p1, py2, p3 +# t2z is built by the second rule and its prerequisite is p4 +@endverbatim + +The set of rules from Remakefile is ill-formed: + +- if any specific rule matching a target of the generic rule has a nonempty script, +- if any target of the generic rule is matched by a generic rule with a shorter pattern. + +\section sec-compilation Compilation + +- On Linux, MacOSX, and BSD: g++ -o remake remake.cpp +- On Windows: g++ -o remake.exe remake.cpp -lws2_32 + +Installing remake is needed only if Remakefile does not +specify the path to the executable for its recursive calls. Thanks to its +single source file, remake can be shipped inside other packages and +built at configuration time. + +\section sec-differences Differences with other build systems + +Differences with make: + +- Dynamic dependencies are supported. +- For rules with multiple targets, the shell script is executed only once + and is assumed to build all the targets. There is no need for + convoluted rules that are robust enough for parallel builds. For generic + rules, this is similar to the behavior of pattern rules from gmake. +- As with redo, only one shell is run when executing a script, + rather than one per script line. Note that the shells are run with + option -e, thus causing them to exit as soon as an error is + encountered. +- The dependencies of generic rules (known as implicit rules in make lingo) + are not used to decide between several of them. remake does not + select one for which it could satisfy the dependencies. +- Variables and built-in functions are expanded as they are encountered + during Remakefile parsing. + +Differences with redo: + +- As with make, it is possible to write the following kind of rules + in remake. +@verbatim +Remakefile: Remakefile.in ./config.status + ./config.status Remakefile +@endverbatim +- If a target is already built the first time remake runs, it still + uses the static prerequisites of rules mentioning it to check whether it + needs to be rebuilt. It does not assume it to be up-to-date. As with + redo though, if its obsolete status would be due to a dynamic + dependency, it will go unnoticed; it should be removed beforehand. +- remake has almost no features: no checksum-based dependencies, no + compatibility with token servers, etc. + +Differences with both make and redo: + +- Multiple targets are supported. +- When executing shell scripts, positional variables $1, + $2, etc, point to the target names of the rule obtained after + substituting %. No other variables are defined. + +\section sec-limitations Limitations + +- When the user or a script calls remake, the current working + directory should be the one containing Remakefile (and thus + .remake too). +- Some cases of ill-formed rules are not caught by remake and can + thus lead to unpredictable behaviors. + +\section sec-links Links + +@see http://cr.yp.to/redo.html for the philosophy of redo and +https://github.com/apenwarr/redo for an implementation and some comprehensive documentation. + +\section sec-licensing Licensing + +@author Guillaume Melquiond +@version 0.5 +@date 2012-2013 +@copyright +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 3 of the License, or +(at your option) any later version. +\n +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. +*/ + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define WINDOWS +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#define MACOSX +#endif + +#ifdef __linux__ +#define LINUX +#endif + +#ifdef WINDOWS +#include +#include +#include +#define pid_t HANDLE +typedef SOCKET socket_t; +#else +#include +#include +#include +typedef int socket_t; +enum { INVALID_SOCKET = -1 }; +#endif + +#if defined(WINDOWS) || defined(MACOSX) +enum { MSG_NOSIGNAL = 0 }; +#endif + +typedef std::list string_list; + +typedef std::set string_set; + +/** + * Reference-counted shared object. + * @note The default constructor delays the creation of the object until it + * is first dereferenced. + */ +template +struct ref_ptr +{ + struct content + { + size_t cnt; + T val; + content(): cnt(1) {} + content(T const &t): cnt(1), val(t) {} + }; + mutable content *ptr; + ref_ptr(): ptr(NULL) {} + ref_ptr(T const &t): ptr(new content(t)) {} + ref_ptr(ref_ptr const &p): ptr(p.ptr) { if (ptr) ++ptr->cnt; } + ~ref_ptr() { if (ptr && --ptr->cnt == 0) delete ptr; } + ref_ptr &operator=(ref_ptr const &p) + { + if (ptr == p.ptr) return *this; + if (ptr && --ptr->cnt == 0) delete ptr; + ptr = p.ptr; + if (ptr) ++ptr->cnt; + return *this; + } + T &operator*() const + { + if (!ptr) ptr = new content; + return ptr->val; + } + T *operator->() const { return &**this; } +}; + +struct dependency_t +{ + string_list targets; + string_set deps; +}; + +typedef std::map > dependency_map; + +typedef std::map variable_map; + +/** + * Build status of a target. + */ +enum status_e +{ + Uptodate, ///< Target is up-to-date. + Todo, ///< Target is missing or obsolete. + Recheck, ///< Target has an obsolete dependency. + Running, ///< Target is being rebuilt. + Remade, ///< Target was successfully rebuilt. + Failed ///< Build failed for target. +}; + +/** + * Build status of a target. + */ +struct status_t +{ + status_e status; ///< Actual status. + time_t last; ///< Last-modified date. +}; + +typedef std::map status_map; + +/** + * A rule loaded from Remakefile. + */ +struct rule_t +{ + string_list targets; ///< Files produced by this rule. + string_list deps; ///< Files used for an implicit call to remake at the start of the script. + std::string script; ///< Shell script for building the targets. +}; + +typedef std::list rule_list; + +typedef std::map > rule_map; + +typedef std::map job_targets_map; + +typedef std::map job_variables_map; + +typedef std::map pid_job_map; + +/** + * Client waiting for a request complete. + * + * There are two kinds of clients: + * - real clients, which are instances of remake created by built scripts, + * - pseudo clients, which are created by the server to build specific targets. + * + * Among pseudo clients, there are two categories: + * - original clients, which are created for the targets passed on the + * command line by the user or for the initial regeneration of the rule file, + * - dependency clients, which are created to handle rules that have + * explicit dependencies and thus to emulate a call to remake. + */ +struct client_t +{ + socket_t socket; ///< Socket used to reply to the client (invalid for pseudo clients). + int job_id; ///< Job for which the built script called remake and spawned the client (negative for original clients). + bool failed; ///< Whether some targets failed in mode -k. + string_list pending; ///< Targets not yet started. + string_set running; ///< Targets being built. + rule_t *delayed; ///< Rule that implicitly created a dependency client, and which script has to be started on request completion. + variable_map variables; ///< Variable that are passed on the command line of a remake invocation. + client_t(): socket(INVALID_SOCKET), job_id(-1), failed(false), delayed(NULL) {} +}; + +typedef std::list client_list; + +/** + * Map from variable names to their content. + */ +static variable_map variables; + +/** + * Precomputed variable assignments for shell usage. + */ +static std::string variable_block; + +/** + * Map from targets to their known dependencies. + */ +static dependency_map dependencies; + +/** + * Map from targets to their build status. + */ +static status_map status; + +/** + * Set of generic rules loaded from Remakefile. + */ +static rule_list generic_rules; + +/** + * Map from targets to specific rules loaded from Remakefile. + */ +static rule_map specific_rules; + +/** + * Map from jobs to targets being built. + */ +static job_targets_map job_targets; + +/** + * Map from jobs to job specific variables + */ +static job_variables_map job_variables; + +/** + * Map from jobs to shell pids. + */ +static pid_job_map job_pids; + +/** + * List of clients waiting for a request to complete. + * New clients are put to front, so that the build process is depth-first. + */ +static client_list clients; + +/** + * Maximum number of parallel jobs (non-positive if unbounded). + * Can be modified by the -j option. + */ +static int max_active_jobs = 1; + +/** + * Whether to keep building targets in case of failure. + * Can be modified by the -k option. + */ +static bool keep_going = false; + +/** + * Number of jobs currently running: + * - it increases when a process is created in #run_script, + * - it decreases when a completion message is received in #finalize_job. + * + * @note There might be some jobs running while #clients is empty. + * Indeed, if a client requested two targets to be rebuilt, if they + * are running concurrently, if one of them fails, the client will + * get a failure notice and might terminate before the other target + * finishes. + */ +static int running_jobs = 0; + +/** + * Number of jobs currently waiting for a build request to finish: + * - it increases when a build request is received in #accept_client + * (since the client is presumably waiting for the reply), + * - it decreases when a reply is sent in #complete_request. + */ +static int waiting_jobs = 0; + +/** + * Global counter used to produce increasing job numbers. + * @see job_targets + */ +static int job_counter = 0; + +/** + * Socket on which the server listens for client request. + */ +static socket_t socket_fd; + +/** + * Whether the request of an original client failed. + */ +static bool build_failure; + +/** + * Name of the server socket in the file system. + */ +static char *socket_name; + +/** + * Name of the first target of the first specific rule, used for default run. + */ +static std::string first_target; + +/** + * Whether a short message should be displayed for each target. + */ +static bool show_targets = true; + +/** + * Whether script commands are echoed. + */ +static bool echo_scripts = false; + +static time_t now = time(NULL); + +static std::string working_dir; + +#ifndef WINDOWS +static volatile sig_atomic_t got_SIGCHLD = 0; + +static void child_sig_handler(int) +{ + got_SIGCHLD = 1; +} +#endif + +struct log +{ + bool active, open; + int depth; + log(): active(false), open(false), depth(0) + { + } + std::ostream &operator()() + { + if (open) std::cerr << std::endl; + assert(depth >= 0); + std::cerr << std::string(depth * 2, ' '); + open = false; + return std::cerr; + } + std::ostream &operator()(bool o) + { + if (o && open) std::cerr << std::endl; + if (!o) --depth; + assert(depth >= 0); + if (o || !open) std::cerr << std::string(depth * 2, ' '); + if (o) ++depth; + open = o; + return std::cerr; + } +}; + +log debug; + +struct log_auto_close +{ + bool still_open; + log_auto_close(): still_open(true) + { + } + ~log_auto_close() + { + if (debug.active && still_open) debug(false) << "done\n"; + } +}; + +#define DEBUG if (debug.active) debug() +#define DEBUG_open log_auto_close auto_close; if (debug.active) debug(true) +#define DEBUG_close if ((auto_close.still_open = false), debug.active) debug(false) + +/** + * Return the original string if it does not contain any special characters, + * a quoted and escaped string otherwise. + */ +static std::string escape_string(std::string const &s) +{ + char const *quoted_char = ",: '"; + char const *escaped_char = "\"\\$!"; + bool need_quotes = false; + size_t len = s.length(), nb = len; + for (size_t i = 0; i < len; ++i) + { + if (strchr(quoted_char, s[i])) need_quotes = true; + if (strchr(escaped_char, s[i])) ++nb; + } + if (nb != len) need_quotes = true; + if (!need_quotes) return s; + std::string t(nb + 2, '\\'); + t[0] = '"'; + for (size_t i = 0, j = 1; i < len; ++i, ++j) + { + if (strchr(escaped_char, s[i])) ++j; + t[j] = s[i]; + } + t[nb + 1] = '"'; + return t; +} + +/** + * Initialize #working_dir. + */ +void init_working_dir() +{ + char buf[1024]; + char *res = getcwd(buf, sizeof(buf)); + if (!res) + { + perror("Failed to get working directory"); + exit(EXIT_FAILURE); + } + working_dir = buf; +} + +/** + * Normalize an absolute path with respect to the working directory. + * Paths outside the working subtree are left unchanged. + */ +static std::string normalize_abs(std::string const &s) +{ + size_t l = working_dir.length(); + if (s.compare(0, l, working_dir)) return s; + size_t ll = s.length(); + if (ll == l) return "."; + if (s[l] != '/') + { + size_t pos = s.rfind('/', l); + assert(pos != std::string::npos); + return s.substr(pos + 1); + } + if (ll == l + 1) return "."; + return s.substr(l + 1); +} + +/** + * Normalize a target name. + */ +static std::string normalize(std::string const &s) +{ +#ifdef WINDOWS + char const *delim = "/\\"; +#else + char delim = '/'; +#endif + size_t prev = 0, len = s.length(); + size_t pos = s.find_first_of(delim); + if (pos == std::string::npos) return s; + bool absolute = pos == 0; + string_list l; + for (;;) + { + if (pos != prev) + { + std::string n = s.substr(prev, pos - prev); + if (n == "..") + { + if (!l.empty()) l.pop_back(); + else if (!absolute) + return normalize(working_dir + '/' + s); + } + else if (n != ".") + l.push_back(n); + } + ++pos; + if (pos >= len) break; + prev = pos; + pos = s.find_first_of(delim, prev); + if (pos == std::string::npos) pos = len; + } + string_list::const_iterator i = l.begin(), i_end = l.end(); + if (i == i_end) return absolute ? "/" : "."; + std::string n; + if (absolute) n.push_back('/'); + n.append(*i); + for (++i; i != i_end; ++i) + { + n.push_back('/'); + n.append(*i); + } + if (absolute) return normalize_abs(n); + return n; +} + +/** + * Normalize the content of a list of targets. + */ +static void normalize_list(string_list &l) +{ + for (string_list::iterator i = l.begin(), + i_end = l.end(); i != i_end; ++i) + { + *i = normalize(*i); + } +} + +/** + * Skip spaces. + */ +static void skip_spaces(std::istream &in) +{ + char c; + while (strchr(" \t", (c = in.get()))) {} + if (in.good()) in.putback(c); +} + +/** + * Skip end of line. + */ +static void skip_eol(std::istream &in) +{ + char c; + while (strchr("\r\n", (c = in.get()))) {} + if (in.good()) in.putback(c); +} + +enum token_e { Word, Eol, Eof, Colon, Equal, Dollar, Rightpar, Comma }; + +/** + * Skip spaces and return the kind of the next token. + */ +static token_e next_token(std::istream &in) +{ + while (true) + { + skip_spaces(in); + char c = in.peek(); + if (!in.good()) return Eof; + switch (c) + { + case ':': return Colon; + case ',': return Comma; + case '=': return Equal; + case '$': return Dollar; + case ')': return Rightpar; + case '\r': + case '\n': + return Eol; + case '\\': + in.ignore(1); + c = in.peek(); + if (c != '\r' && c != '\n') + { + in.putback('\\'); + return Word; + } + skip_eol(in); + break; + default: + return Word; + } + } +} + +/** + * Read a (possibly quoted) word. + */ +static std::string read_word(std::istream &in) +{ + int c = in.get(); + std::string res; + if (!in.good()) return res; + char const *separators = " \t\r\n:$(),=\""; + bool quoted = c == '"'; + if (!quoted) + { + if (strchr(separators, c)) + { + in.putback(c); + return res; + } + res += c; + } + while (true) + { + c = in.get(); + if (!in.good()) return res; + if (quoted) + { + if (c == '\\') + res += in.get(); + else if (c == '"') + return res; + else + res += c; + } + else + { + if (strchr(separators, c)) + { + in.putback(c); + return res; + } + res += c; + } + } +} + +static string_list read_words(std::istream &in); + +/** + * Execute a built-in function @a name and append its result to @a dest. + */ +static void execute_function(std::istream &in, std::string const &name, string_list &dest) +{ + if (false) + { + error: + std::cerr << "Failed to load rules: syntax error" << std::endl; + exit(EXIT_FAILURE); + } + skip_spaces(in); + string_list fix = read_words(in); + if (next_token(in) != Comma) goto error; + in.ignore(1); + string_list names = read_words(in); + if (next_token(in) != Rightpar) goto error; + in.ignore(1); + size_t fixl = fix.size(); + if (name == "addprefix") + { + for (string_list::const_iterator i = names.begin(), + i_end = names.end(); i != i_end; ++i) + { + if (!fixl) + { + dest.push_back(*i); + continue; + } + string_list::const_iterator k = fix.begin(); + for (size_t j = 1; j != fixl; ++j) + { + dest.push_back(*k++); + } + dest.push_back(*k++ + *i); + } + } + else if (name == "addsuffix") + { + for (string_list::const_iterator i = names.begin(), + i_end = names.end(); i != i_end; ++i) + { + if (!fixl) + { + dest.push_back(*i); + continue; + } + string_list::const_iterator k = fix.begin(); + dest.push_back(*i + *k++); + for (size_t j = 1; j != fixl; ++j) + { + dest.push_back(*k++); + } + } + } + else goto error; +} + +/** + * Read a list of words, possibly executing functions. + */ +static string_list read_words(std::istream &in) +{ + if (false) + { + error: + std::cerr << "Failed to load rules: syntax error" << std::endl; + exit(EXIT_FAILURE); + } + string_list res; + while (true) + { + switch (next_token(in)) + { + case Word: + res.push_back(read_word(in)); + break; + case Dollar: + { + in.ignore(1); + if (in.get() != '(') goto error; + std::string name = read_word(in); + if (name.empty()) goto error; + token_e tok = next_token(in); + if (tok == Rightpar) + { + in.ignore(1); + variable_map::const_iterator i = variables.find(name); + if (i != variables.end()) + res.insert(res.end(), i->second.begin(), i->second.end()); + } + else execute_function(in, name, res); + break; + } + default: + return res; + } + } +} + +/** + * Serialize a variable map. + */ +std::string serialize_variables(variable_map &vars, char sep = ' ') +{ + std::ostringstream buf; + for(variable_map::const_iterator i = vars.begin(), + i_end = vars.end(); i != i_end; ++i) + { + buf << i->first << '='; + bool first = true; + std::string val; + for (string_list::const_iterator j = i->second.begin(), + j_end = i->second.end(); j != j_end; ++j) + { + if (first) first = false; + else val += " "; + val += *j; + } + buf << escape_string(val) << sep; + } + std::string s = buf.str(); + return s; +} + +/** + * Load dependencies from @a in. + */ +static void load_dependencies(std::istream &in) +{ + while (!in.eof()) + { + string_list targets = read_words(in); + if (targets.empty()) return; + DEBUG << "reading dependencies of target " << targets.front() << std::endl; + if (in.get() != ':') + { + std::cerr << "Failed to load database" << std::endl; + exit(EXIT_FAILURE); + } + ref_ptr dep; + dep->targets = targets; + string_list d = read_words(in); + dep->deps.insert(d.begin(), d.end()); + for (string_list::const_iterator i = targets.begin(), + i_end = targets.end(); i != i_end; ++i) + { + dependencies[*i] = dep; + } + skip_eol(in); + } +} + +/** + * Load known dependencies from file .remake. + */ +static void load_dependencies() +{ + DEBUG_open << "Loading database... "; + std::ifstream in(".remake"); + if (!in.good()) + { + DEBUG_close << "not found\n"; + return; + } + load_dependencies(in); +} + +/** + * Read a rule starting with target @a first, if nonempty. + * Store into #generic_rules or #specific_rules depending on its genericity. + */ +static void load_rule(std::istream &in, std::string const &first) +{ + DEBUG_open << "Reading rule for target " << first << "... "; + if (false) + { + error: + DEBUG_close << "failed\n"; + std::cerr << "Failed to load rules: syntax error" << std::endl; + exit(EXIT_FAILURE); + } + rule_t rule; + + // Read targets and check genericity. + string_list targets = read_words(in); + if (!first.empty()) targets.push_front(first); + else if (targets.empty()) goto error; + else DEBUG << "actual target: " << targets.front() << std::endl; + bool generic = false; + normalize_list(targets); + for (string_list::const_iterator i = targets.begin(), + i_end = targets.end(); i != i_end; ++i) + { + if (i->empty()) goto error; + if ((i->find('%') != std::string::npos) != generic) + { + if (i == targets.begin()) generic = true; + else goto error; + } + } + std::swap(rule.targets, targets); + skip_spaces(in); + if (in.get() != ':') goto error; + + // Read dependencies. + rule.deps = read_words(in); + normalize_list(rule.deps); + skip_spaces(in); + char c = in.get(); + if (c != '\r' && c != '\n') goto error; + skip_eol(in); + + // Read script. + std::ostringstream buf; + while (true) + { + char c = in.get(); + if (!in.good()) break; + if (c == '\t' || c == ' ') + { + in.get(*buf.rdbuf()); + if (in.fail() && !in.eof()) in.clear(); + } + else if (c == '\r' || c == '\n') + buf << c; + else + { + in.putback(c); + break; + } + } + rule.script = buf.str(); + + // Add generic rules to the correct set. + if (generic) + { + generic_rules.push_back(rule); + return; + } + + // Rules with a nonempty script lump all their targets in the same + // dependency set, while other rules behave as if they had been + // replicated for each of their targets. + if (!rule.script.empty()) + { + ref_ptr dep; + dep->targets = rule.targets; + dep->deps.insert(rule.deps.begin(), rule.deps.end()); + for (string_list::const_iterator i = rule.targets.begin(), + i_end = rule.targets.end(); i != i_end; ++i) + { + ref_ptr &d = dependencies[*i]; + dep->deps.insert(d->deps.begin(), d->deps.end()); + d = dep; + } + } + else + { + for (string_list::const_iterator i = rule.targets.begin(), + i_end = rule.targets.end(); i != i_end; ++i) + { + ref_ptr &dep = dependencies[*i]; + if (dep->targets.empty()) dep->targets.push_back(*i); + dep->deps.insert(rule.deps.begin(), rule.deps.end()); + } + } + + if (first_target.empty()) + first_target = rule.targets.front(); + + ref_ptr r(rule); + for (string_list::const_iterator i = rule.targets.begin(), + i_end = rule.targets.end(); i != i_end; ++i) + { + std::pair j = + specific_rules.insert(std::make_pair(*i, r)); + if (j.second) continue; + std::cerr << "Failed to load rules: " << *i + << " cannot be the target of several rules" << std::endl; + exit(EXIT_FAILURE); + } +} + +/** + * Save all the dependencies in file .remake. + */ +static void save_dependencies() +{ + DEBUG_open << "Saving database... "; + std::ofstream db(".remake"); + while (!dependencies.empty()) + { + ref_ptr dep = dependencies.begin()->second; + for (string_list::const_iterator i = dep->targets.begin(), + i_end = dep->targets.end(); i != i_end; ++i) + { + db << escape_string(*i) << ' '; + dependencies.erase(*i); + } + db << ':'; + for (string_set::const_iterator i = dep->deps.begin(), + i_end = dep->deps.end(); i != i_end; ++i) + { + db << ' ' << escape_string(*i); + } + db << std::endl; + } +} + +/** + * Load rules. + * If some rules have dependencies and non-generic targets, add these + * dependencies to the targets. + */ +static void load_rules() +{ + DEBUG_open << "Loading rules... "; + if (false) + { + error: + std::cerr << "Failed to load rules: syntax error" << std::endl; + exit(EXIT_FAILURE); + } + std::ifstream in("Remakefile"); + if (!in.good()) + { + std::cerr << "Failed to load rules: no Remakefile found" << std::endl; + exit(EXIT_FAILURE); + } + skip_eol(in); + + // Read rules + while (in.good()) + { + char c = in.peek(); + if (c == '#') + { + while (in.get() != '\n') {} + skip_eol(in); + continue; + } + if (c == ' ' || c == '\t') goto error; + token_e tok = next_token(in); + if (tok == Word) + { + std::string name = read_word(in); + if (name.empty()) goto error; + if (next_token(in) == Equal) + { + in.ignore(1); + DEBUG << "Assignment to variable " << name << std::endl; + variables[name] = read_words(in); + skip_eol(in); + } + else load_rule(in, name); + } + else if (tok == Dollar) + load_rule(in, std::string()); + else goto error; + } + + // Generate script for variable assignment + std::ostringstream buf; + for (variable_map::const_iterator i = variables.begin(), + i_end = variables.end(); i != i_end; ++i) + { + std::ostringstream var; + bool first = true; + for (string_list::const_iterator j = i->second.begin(), + j_end = i->second.end(); j != j_end; ++j) + { + if (first) first = false; + else var << ' '; + var << *j; + } + buf << i->first << '=' << escape_string(var.str()) << std::endl; + } + variable_block = buf.str(); +} + +/** + * Substitute a pattern into a list of strings. + */ +static void substitute_pattern(std::string const &pat, string_list const &src, string_list &dst) +{ + for (string_list::const_iterator i = src.begin(), + i_end = src.end(); i != i_end; ++i) + { + size_t pos = i->find('%'); + if (pos == std::string::npos)dst.push_back(*i); + else dst.push_back(i->substr(0, pos) + pat + i->substr(pos + 1)); + } +} + +/** + * Find a generic rule matching @a target: + * - the one leading to shorter matches has priority, + * - among equivalent rules, the earliest one has priority. + */ +static rule_t find_generic_rule(std::string const &target) +{ + size_t tlen = target.length(), plen = tlen + 1; + rule_t rule; + for (rule_list::const_iterator i = generic_rules.begin(), + i_end = generic_rules.end(); i != i_end; ++i) + { + for (string_list::const_iterator j = i->targets.begin(), + j_end = i->targets.end(); j != j_end; ++j) + { + size_t len = j->length(); + if (tlen < len) continue; + if (plen <= tlen - (len - 1)) continue; + size_t pos = j->find('%'); + if (pos == std::string::npos) continue; + size_t len2 = len - (pos + 1); + if (j->compare(0, pos, target, 0, pos) || + j->compare(pos + 1, len2, target, tlen - len2, len2)) + continue; + plen = tlen - (len - 1); + std::string pat = target.substr(pos, plen); + rule = rule_t(); + rule.script = i->script; + substitute_pattern(pat, i->targets, rule.targets); + substitute_pattern(pat, i->deps, rule.deps); + break; + } + } + return rule; +} + +/** + * Find a specific rule matching @a target. Return a generic one otherwise. + * If there is both a specific rule with an empty script and a generic rule, the + * generic one is returned after adding the dependencies of the specific one. + */ +static rule_t find_rule(std::string const &target) +{ + rule_map::const_iterator i = specific_rules.find(target), + i_end = specific_rules.end(); + // If there is a specific rule with a script, return it. + if (i != i_end && !i->second->script.empty()) return *i->second; + rule_t grule = find_generic_rule(target); + // If there is no generic rule, return the specific rule (no script), if any. + if (grule.targets.empty()) + { + if (i != i_end) return *i->second; + return grule; + } + // Optimize the lookup when there is only one target (alread looked up). + if (grule.targets.size() == 1) + { + if (i != i_end) + grule.deps.insert(grule.deps.end(), + i->second->deps.begin(), i->second->deps.end()); + return grule; + } + // Add the dependencies of the specific rules of every target to the + // generic rule. If any of those rules has a nonempty script, error out. + for (string_list::const_iterator j = grule.targets.begin(), + j_end = grule.targets.end(); j != j_end; ++j) + { + i = specific_rules.find(*j); + if (i == i_end) continue; + if (!i->second->script.empty()) return rule_t(); + grule.deps.insert(grule.deps.end(), + i->second->deps.begin(), i->second->deps.end()); + } + return grule; +} + +/** + * Compute and memoize the status of @a target: + * - if the file does not exist, the target is obsolete, + * - if any dependency is obsolete or younger than the file, it is obsolete, + * - otherwise it is up-to-date. + * + * @note With multiple targets, they all share the same status. (If one is + * obsolete, they all are.) For the second rule above, the latest target + * is chosen, not the oldest! + */ +static status_t const &get_status(std::string const &target) +{ + std::pair i = + status.insert(std::make_pair(target, status_t())); + status_t &ts = i.first->second; + if (!i.second) return ts; + DEBUG_open << "Checking status of " << target << "... "; + dependency_map::const_iterator j = dependencies.find(target); + if (j == dependencies.end()) + { + struct stat s; + if (stat(target.c_str(), &s) != 0) + { + DEBUG_close << "missing\n"; + ts.status = Todo; + ts.last = 0; + return ts; + } + DEBUG_close << "up-to-date\n"; + ts.status = Uptodate; + ts.last = s.st_mtime; + return ts; + } + dependency_t const &dep = *j->second; + status_e st = Uptodate; + time_t latest = 0; + for (string_list::const_iterator k = dep.targets.begin(), + k_end = dep.targets.end(); k != k_end; ++k) + { + struct stat s; + if (stat(k->c_str(), &s) != 0) + { + if (st == Uptodate) DEBUG_close << *k << " missing\n"; + s.st_mtime = 0; + st = Todo; + } + status[*k].last = s.st_mtime; + if (s.st_mtime > latest) latest = s.st_mtime; + } + if (st == Todo) goto update; + for (string_set::const_iterator k = dep.deps.begin(), + k_end = dep.deps.end(); k != k_end; ++k) + { + status_t const &ts_ = get_status(*k); + if (latest < ts_.last) + { + DEBUG_close << "older than " << *k << std::endl; + st = Todo; + goto update; + } + if (ts_.status == Uptodate) continue; + if (st == Uptodate) + DEBUG << "obsolete dependency " << *k << std::endl; + st = Recheck; + } + if (st == Uptodate) DEBUG_close << "all siblings up-to-date\n"; + update: + for (string_list::const_iterator k = dep.targets.begin(), + k_end = dep.targets.end(); k != k_end; ++k) + { + status[*k].status = st; + } + return ts; +} + +/** + * Change the status of @a target to #Remade or #Uptodate depending on whether + * its modification time changed. + */ +static void update_status(std::string const &target) +{ + DEBUG_open << "Rechecking status of " << target << "... "; + status_map::iterator i = status.find(target); + assert (i != status.end()); + status_t &ts = i->second; + ts.status = Remade; + if (ts.last >= now) + { + DEBUG_close << "possibly remade\n"; + return; + } + struct stat s; + if (stat(target.c_str(), &s) != 0) + { + DEBUG_close << "missing\n"; + ts.last = 0; + } + else if (s.st_mtime != ts.last) + { + DEBUG_close << "remade\n"; + ts.last = s.st_mtime; + } + else + { + DEBUG_close << "unchanged\n"; + ts.status = Uptodate; + } +} + +/** + * Check if all the prerequisites of @a target ended being up-to-date. + */ +static bool still_need_rebuild(std::string const &target) +{ + DEBUG_open << "Rechecking obsoleteness of " << target << "... "; + status_map::const_iterator i = status.find(target); + assert (i != status.end()); + if (i->second.status != Recheck) return true; + dependency_map::const_iterator j = dependencies.find(target); + assert(j != dependencies.end()); + dependency_t const &dep = *j->second; + for (string_set::const_iterator k = dep.deps.begin(), + k_end = dep.deps.end(); k != k_end; ++k) + { + if (status[*k].status != Uptodate) return true; + } + for (string_list::const_iterator k = dep.targets.begin(), + k_end = dep.targets.end(); k != k_end; ++k) + { + status[*k].status = Uptodate; + } + DEBUG_close << "no longer obsolete\n"; + return false; +} + +/** + * Handle job completion. + */ +static void complete_job(int job_id, bool success) +{ + DEBUG_open << "Completing job " << job_id << "... "; + job_targets_map::iterator i = job_targets.find(job_id); + assert(i != job_targets.end()); + string_list const &targets = i->second; + if (success) + { + for (string_list::const_iterator j = targets.begin(), + j_end = targets.end(); j != j_end; ++j) + { + update_status(*j); + } + } + else + { + DEBUG_close << "failed\n"; + std::cerr << "Failed to build"; + for (string_list::const_iterator j = targets.begin(), + j_end = targets.end(); j != j_end; ++j) + { + status[*j].status = Failed; + std::cerr << ' ' << *j; + remove(j->c_str()); + } + std::cerr << std::endl; + } + job_targets.erase(i); +} + +/** + * Execute the script from @a rule. + */ +static bool run_script(int job_id, rule_t const &rule) +{ + if (show_targets) + { + std::cout << "Building"; + for (string_list::const_iterator i = rule.targets.begin(), + i_end = rule.targets.end(); i != i_end; ++i) + { + std::cout << ' ' << *i; + } + std::cout << std::endl; + } + + ref_ptr dep; + dep->targets = rule.targets; + dep->deps.insert(rule.deps.begin(), rule.deps.end()); + for (string_list::const_iterator i = rule.targets.begin(), + i_end = rule.targets.end(); i != i_end; ++i) + { + dependencies[*i] = dep; + } + + DEBUG_open << "Starting script for job " << job_id << " with variables (" + << (job_id >= 0 ? serialize_variables(job_variables[job_id], ' ') : "") + << ") ... "; +#ifdef WINDOWS + HANDLE pfd[2]; + if (false) + { + error2: + CloseHandle(pfd[0]); + CloseHandle(pfd[1]); + error: + DEBUG_close << "failed\n"; + complete_job(job_id, false); + return false; + } + if (!CreatePipe(&pfd[0], &pfd[1], NULL, 0)) + goto error; + if (!SetHandleInformation(pfd[0], HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)) + goto error2; + STARTUPINFO si; + ZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + si.hStdError = GetStdHandle(STD_ERROR_HANDLE); + si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + si.hStdInput = pfd[0]; + si.dwFlags |= STARTF_USESTDHANDLES; + PROCESS_INFORMATION pi; + ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); + std::ostringstream buf; + buf << job_id; + if (!SetEnvironmentVariable("REMAKE_JOB_ID", buf.str().c_str())) + goto error2; + std::ostringstream argv; + argv << "SH.EXE -e -s"; + if (echo_scripts) argv << " -v"; + for (string_list::const_iterator i = rule.targets.begin(), + i_end = rule.targets.end(); i != i_end; ++i) + { + argv << " \"" << escape_string(*i) << '"'; + } + if (!CreateProcess(NULL, (char *)argv.str().c_str(), NULL, NULL, + true, 0, NULL, NULL, &si, &pi)) + { + goto error2; + } + CloseHandle(pi.hThread); + std::string script = variable_block + + (job_id >= 0 ? serialize_variables(job_variables[job_id], '\n') : "") + + rule.script; + DWORD len = script.length(), wlen; + if (!WriteFile(pfd[1], script.c_str(), len, &wlen, NULL) || wlen < len) + std::cerr << "Unexpected failure while sending script to shell" << std::endl; + CloseHandle(pfd[0]); + CloseHandle(pfd[1]); + ++running_jobs; + job_pids[pi.hProcess] = job_id; + return true; +#else + int pfd[2]; + if (false) + { + error2: + close(pfd[0]); + close(pfd[1]); + error: + DEBUG_close << "failed\n"; + complete_job(job_id, false); + return false; + } + if (pipe(pfd) == -1) + goto error; + if (pid_t pid = fork()) + { + if (pid == -1) goto error2; + std::string script = variable_block + + (job_id >= 0 ? serialize_variables(job_variables[job_id], '\n') : "") + + rule.script; + ssize_t len = script.length(); + if (write(pfd[1], script.c_str(), len) < len) + std::cerr << "Unexpected failure while sending script to shell" << std::endl; + close(pfd[0]); + close(pfd[1]); + ++running_jobs; + job_pids[pid] = job_id; + return true; + } + // Child process starts here. + std::ostringstream buf; + buf << job_id; + if (setenv("REMAKE_JOB_ID", buf.str().c_str(), 1)) + _exit(EXIT_FAILURE); + int num = echo_scripts ? 4 : 3; + char const **argv = new char const *[num + rule.targets.size() + 1]; + argv[0] = "sh"; + argv[1] = "-e"; + argv[2] = "-s"; + if (echo_scripts) argv[3] = "-v"; + for (string_list::const_iterator i = rule.targets.begin(), + i_end = rule.targets.end(); i != i_end; ++i, ++num) + { + argv[num] = i->c_str(); + } + argv[num] = NULL; + if (pfd[0] != 0) + { + dup2(pfd[0], 0); + close(pfd[0]); + } + close(pfd[1]); + execv("/bin/sh", (char **)argv); + _exit(EXIT_FAILURE); +#endif +} + +/** + * Create a job for @a target according to the loaded rules. + * Mark all the targets from the rule as running and reset their dependencies. + * If the rule has dependencies, create a new client to build them just + * before @a current, and change @a current so that it points to it. + */ +static bool start(std::string const &target, client_list::iterator ¤t) +{ + DEBUG_open << "Starting job " << job_counter << " for " << target << "... "; + rule_t rule = find_rule(target); + if (rule.targets.empty()) + { + status[target].status = Failed; + DEBUG_close << "failed\n"; + std::cerr << "No rule for building " << target << std::endl; + return false; + } + for (string_list::const_iterator i = rule.targets.begin(), + i_end = rule.targets.end(); i != i_end; ++i) + { + status[*i].status = Running; + } + int job_id = job_counter++; + job_targets[job_id] = rule.targets; + DEBUG << "Setting variables of job: " << job_id << " (spawn by: " + << current->job_id << ") to " + << serialize_variables(current->variables, ' ') << std::endl; + job_variables[job_id] = current->variables; + if (!rule.deps.empty()) + { + DEBUG << "Current client has job_id: " << job_id + << " and variables " << serialize_variables(current->variables, ' ') + << std::endl; + client_t dep_client = client_t(); + dep_client.variables = current->variables; + current = clients.insert(current, dep_client); + current->job_id = job_id; + current->pending = rule.deps; + current->delayed = new rule_t(rule); + return true; + } + return run_script(job_id, rule); +} + +/** + * Send a reply to a client then remove it. + * If the client was a dependency client, start the actual script. + */ +static void complete_request(client_t &client, bool success) +{ + DEBUG_open << "Completing request from client of job " << client.job_id << "... "; + if (client.delayed) + { + assert(client.socket == INVALID_SOCKET); + if (success) + { + if (still_need_rebuild(client.delayed->targets.front())) + run_script(client.job_id, *client.delayed); + else complete_job(client.job_id, true); + } + else complete_job(client.job_id, false); + delete client.delayed; + } + else if (client.socket != INVALID_SOCKET) + { + char res = success ? 1 : 0; + send(client.socket, &res, 1, 0); + #ifdef WINDOWS + closesocket(client.socket); + #else + close(client.socket); + #endif + --waiting_jobs; + } + + if (client.job_id < 0 && !success) build_failure = true; +} + +/** + * Return whether there are slots for starting new jobs. + */ +static bool has_free_slots() +{ + if (max_active_jobs <= 0) return true; + return running_jobs - waiting_jobs < max_active_jobs; +} + +/** + * Update clients as long as there are free slots: + * - check for running targets that have finished, + * - start as many pending targets as allowed, + * - complete the request if there are neither running nor pending targets + * left or if any of them failed. + */ +static void update_clients() +{ + DEBUG_open << "Updating clients... "; + for (client_list::iterator i = clients.begin(), i_next = i, + i_end = clients.end(); i != i_end && has_free_slots(); i = i_next) + { + ++i_next; + DEBUG_open << "Handling client from job " << i->job_id << "... "; + if (false) + { + failed: + complete_request(*i, false); + clients.erase(i); + DEBUG_close << "failed\n"; + continue; + } + + // Remove running targets that have finished. + for (string_set::iterator j = i->running.begin(), j_next = j, + j_end = i->running.end(); j != j_end; j = j_next) + { + ++j_next; + status_map::const_iterator k = status.find(*j); + assert(k != status.end()); + switch (k->second.status) + { + case Running: + break; + case Failed: + if (!keep_going) goto failed; + i->failed = true; + // no break + case Uptodate: + case Remade: + i->running.erase(j); + break; + case Recheck: + case Todo: + assert(false); + } + } + + // Start pending targets. + while (!i->pending.empty()) + { + std::string target = i->pending.front(); + i->pending.pop_front(); + switch (get_status(target).status) + { + case Running: + i->running.insert(target); + break; + case Failed: + pending_failed: + if (!keep_going) goto failed; + i->failed = true; + // no break + case Uptodate: + case Remade: + break; + case Recheck: + case Todo: + client_list::iterator j = i; + if (!start(target, i)) goto pending_failed; + j->running.insert(target); + if (!has_free_slots()) return; + // Job start might insert a dependency client. + i_next = i; + ++i_next; + break; + } + } + + // Try to complete request. + // (This might start a new job if it was a dependency client.) + if (i->running.empty()) + { + if (i->failed) goto failed; + complete_request(*i, true); + clients.erase(i); + DEBUG_close << "finished\n"; + } + } +} + +/** + * Create a named unix socket that listens for build requests. Also set + * the REMAKE_SOCKET environment variable that will be inherited by all + * the job scripts. + */ +static void create_server() +{ + if (false) + { + error: + perror("Failed to create server"); +#ifndef WINDOWS + error2: +#endif + exit(EXIT_FAILURE); + } + DEBUG_open << "Creating server... "; + +#ifdef WINDOWS + // Prepare a windows socket. + struct sockaddr_in socket_addr; + socket_addr.sin_family = AF_INET; + socket_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + socket_addr.sin_port = 0; + + // Create and listen to the socket. + socket_fd = socket(AF_INET, SOCK_STREAM, 0); + if (socket_fd < 0) goto error; + if (!SetHandleInformation((HANDLE)socket_fd, HANDLE_FLAG_INHERIT, 0)) + goto error; + if (bind(socket_fd, (struct sockaddr *)&socket_addr, sizeof(sockaddr_in))) + goto error; + int len = sizeof(sockaddr_in); + if (getsockname(socket_fd, (struct sockaddr *)&socket_addr, &len)) + goto error; + std::ostringstream buf; + buf << socket_addr.sin_port; + if (!SetEnvironmentVariable("REMAKE_SOCKET", buf.str().c_str())) + goto error; + if (listen(socket_fd, 1000)) goto error; +#else + // Set a handler for SIGCHLD then block the signal (unblocked during select). + sigset_t sigmask; + sigemptyset(&sigmask); + sigaddset(&sigmask, SIGCHLD); + if (sigprocmask(SIG_BLOCK, &sigmask, NULL) == -1) goto error; + struct sigaction sa; + sa.sa_flags = 0; + sa.sa_handler = &child_sig_handler; + sigemptyset(&sa.sa_mask); + if (sigaction(SIGCHLD, &sa, NULL) == -1) goto error; + + // Prepare a named unix socket in temporary directory. + socket_name = tempnam(NULL, "rmk-"); + if (!socket_name) goto error2; + struct sockaddr_un socket_addr; + size_t len = strlen(socket_name); + if (len >= sizeof(socket_addr.sun_path) - 1) goto error2; + socket_addr.sun_family = AF_UNIX; + strcpy(socket_addr.sun_path, socket_name); + len += sizeof(socket_addr.sun_family); + if (setenv("REMAKE_SOCKET", socket_name, 1)) goto error; + + // Create and listen to the socket. +#ifdef LINUX + socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (socket_fd < 0) goto error; +#else + socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (socket_fd < 0) goto error; + if (fcntl(socket_fd, F_SETFD, FD_CLOEXEC) < 0) goto error; +#endif + if (bind(socket_fd, (struct sockaddr *)&socket_addr, len)) + goto error; + if (listen(socket_fd, 1000)) goto error; +#endif +} + +/** + * Accept a connection from a client, get the job it spawned from, + * get the targets, and mark them as dependencies of the job targets. + */ +void accept_client() +{ + DEBUG_open << "Handling client request... "; + + // Accept connection. +#ifdef WINDOWS + socket_t fd = accept(socket_fd, NULL, NULL); + if (fd == INVALID_SOCKET) return; + if (!SetHandleInformation((HANDLE)fd, HANDLE_FLAG_INHERIT, 0)) + { + error2: + std::cerr << "Unexpected failure while setting connection with client" << std::endl; + closesocket(fd); + return; + } + // WSAEventSelect puts sockets into nonblocking mode, so disable it here. + u_long nbio = 0; + if (ioctlsocket(fd, FIONBIO, &nbio)) goto error2; +#elif defined(LINUX) + int fd = accept4(socket_fd, NULL, NULL, SOCK_CLOEXEC); + if (fd < 0) return; +#else + int fd = accept(socket_fd, NULL, NULL); + if (fd < 0) return; + if (fcntl(fd, F_SETFD, FD_CLOEXEC) < 0) return; +#endif + clients.push_front(client_t()); + client_list::iterator proc = clients.begin(); + + if (false) + { + error: + DEBUG_close << "failed\n"; + std::cerr << "Received an ill-formed client message" << std::endl; + #ifdef WINDOWS + closesocket(fd); + #else + close(fd); + #endif + clients.erase(proc); + return; + } + + // Receive message. Stop when encountering two nuls in a row. + std::vector buf; + size_t len = 0; + while (len < sizeof(int) + 2 || buf[len - 1] || buf[len - 2]) + { + buf.resize(len + 1024); + ssize_t l = recv(fd, &buf[0] + len, 1024, 0); + if (l <= 0) goto error; + len += l; + } + + // Parse job that spawned the client. + int job_id; + memcpy(&job_id, &buf[0], sizeof(int)); + proc->socket = fd; + proc->job_id = job_id; + job_targets_map::const_iterator i = job_targets.find(job_id); + if (i == job_targets.end()) goto error; + DEBUG << "receiving request from job " << job_id << std::endl; + + // Parse the targets and mark them as dependencies from the job targets. + dependency_t &dep = *dependencies[job_targets[job_id].front()]; + char const *p = &buf[0] + sizeof(int); + while (true) + { + len = strlen(p); + if (len == 1 && p[0] == 1) + { + //Finished parsing targets. + p += 2; + ++waiting_jobs; + break; + } + std::string target(p, p + len); + DEBUG << "adding dependency " << target << " to job " << job_id << std::endl; + proc->pending.push_back(target); + dep.deps.insert(target); + p += len + 1; + } + + while (true) + { + len = strlen(p); + if (len == 0) return; + std::string line(p, p + len); + std::istringstream in (line); + std::string name = read_word(in); + if (next_token(in) != Equal) { + std::cerr << '\'' << line << "'" << std::endl; + goto error; + } + in.ignore(1); // ignore =. + DEBUG << "adding variable " << line << " to job " << job_id << std::endl; + string_list l = read_words(in); + proc->variables[name] = l; + p += len + 1; + } + + +} + +/** + * Loop until all the jobs have finished. + */ +void server_loop() +{ + while (true) + { + update_clients(); + if (running_jobs == 0) + { + assert(clients.empty()); + break; + } + DEBUG_open << "Handling events... "; + #ifdef WINDOWS + size_t len = job_pids.size() + 1; + HANDLE h[len]; + int num = 0; + for (pid_job_map::const_iterator i = job_pids.begin(), + i_end = job_pids.end(); i != i_end; ++i, ++num) + { + h[num] = i->first; + } + WSAEVENT aev = WSACreateEvent(); + h[num] = aev; + WSAEventSelect(socket_fd, aev, FD_ACCEPT); + DWORD w = WaitForMultipleObjects(len, h, false, INFINITE); + WSAEventSelect(socket_fd, aev, 0); + WSACloseEvent(aev); + if (w < WAIT_OBJECT_0 || WAIT_OBJECT_0 + len <= w) + continue; + if (w == WAIT_OBJECT_0 + len - 1) + { + accept_client(); + continue; + } + pid_t pid = h[w - WAIT_OBJECT_0]; + DWORD s = 0; + bool res = GetExitCodeProcess(pid, &s) && s == 0; + CloseHandle(pid); + pid_job_map::iterator i = job_pids.find(pid); + assert(i != job_pids.end()); + int job_id = i->second; + job_pids.erase(i); + --running_jobs; + complete_job(job_id, res); + #else + sigset_t emptymask; + sigemptyset(&emptymask); + fd_set fdset; + FD_ZERO(&fdset); + FD_SET(socket_fd, &fdset); + int ret = pselect(socket_fd + 1, &fdset, NULL, NULL, NULL, &emptymask); + if (ret > 0 /* && FD_ISSET(socket_fd, &fdset)*/) accept_client(); + if (!got_SIGCHLD) continue; + got_SIGCHLD = 0; + pid_t pid; + int status; + while ((pid = waitpid(-1, &status, WNOHANG)) > 0) + { + bool res = WIFEXITED(status) && WEXITSTATUS(status) == 0; + pid_job_map::iterator i = job_pids.find(pid); + assert(i != job_pids.end()); + int job_id = i->second; + job_pids.erase(i); + --running_jobs; + complete_job(job_id, res); + } + #endif + } +} + +/** + * Load dependencies and rules, listen to client requests, and loop until + * all the requests have completed. + * If Remakefile is obsolete, perform a first run with it only, then reload + * the rules, and perform a second with the original clients. + */ +void server_mode(string_list const &targets) +{ + load_dependencies(); + load_rules(); + create_server(); + if (get_status("Remakefile").status != Uptodate) + { + clients.push_back(client_t()); + clients.back().pending.push_back("Remakefile"); + server_loop(); + if (build_failure) goto early_exit; + variables.clear(); + specific_rules.clear(); + generic_rules.clear(); + first_target.clear(); + load_rules(); + } + clients.push_back(client_t()); + if (!targets.empty()) clients.back().pending = targets; + else if (!first_target.empty()) + clients.back().pending.push_back(first_target); + server_loop(); + early_exit: + close(socket_fd); + remove(socket_name); + save_dependencies(); + exit(build_failure ? EXIT_FAILURE : EXIT_SUCCESS); +} + +/** + * Connect to the server @a socket_name, send a build request for @a targets, + * and exit with the status returned by the server. + */ +void client_mode(char *socket_name, string_list const &targets) +{ + if (false) + { + error: + perror("Failed to send targets to server"); + exit(EXIT_FAILURE); + } + if (targets.empty()) exit(EXIT_SUCCESS); + DEBUG_open << "Connecting to server... "; + + // Connect to server. +#ifdef WINDOWS + struct sockaddr_in socket_addr; + socket_fd = socket(AF_INET, SOCK_STREAM, 0); + if (socket_fd < 0) goto error; + socket_addr.sin_family = AF_INET; + socket_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + socket_addr.sin_port = atoi(socket_name); + if (connect(socket_fd, (struct sockaddr *)&socket_addr, sizeof(sockaddr_in))) + goto error; +#else + struct sockaddr_un socket_addr; + size_t len = strlen(socket_name); + if (len >= sizeof(socket_addr.sun_path) - 1) exit(EXIT_FAILURE); + socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (socket_fd < 0) goto error; + socket_addr.sun_family = AF_UNIX; + strcpy(socket_addr.sun_path, socket_name); + if (connect(socket_fd, (struct sockaddr *)&socket_addr, sizeof(socket_addr.sun_family) + len)) + goto error; +#ifdef MACOSX + int set_option = 1; + if (setsockopt(socket_fd, SOL_SOCKET, SO_NOSIGPIPE, &set_option, sizeof(set_option))) + goto error; +#endif +#endif + + // Send current job id. + char *id = getenv("REMAKE_JOB_ID"); + int job_id = id ? atoi(id) : -1; + if (send(socket_fd, (char *)&job_id, sizeof(job_id), MSG_NOSIGNAL) != sizeof(job_id)) + goto error; + + // Send targets. + for (string_list::const_iterator i = targets.begin(), + i_end = targets.end(); i != i_end; ++i) + { + DEBUG_open << "Sending " << *i << "... "; + ssize_t len = i->length() + 1; + if (send(socket_fd, i->c_str(), len, MSG_NOSIGNAL) != len) + goto error; + } + // send 10 as as separator between targets and variables. + char result = 1; + if (send(socket_fd, &result, 1, MSG_NOSIGNAL) != 1) goto error; + result = 0; + if (send(socket_fd, &result, 1, MSG_NOSIGNAL) != 1) goto error; + // Send variables. + // (maybe split vars in small chunks and send... seems like an overkil) + std::string vars = serialize_variables(variables, 0); + ssize_t sent = vars.size(); + DEBUG << "Sending variables: '" << vars << "' to the server" << std::endl; + if (send(socket_fd, vars.data(), sent, MSG_NOSIGNAL) != sent) + goto error; + + // Send terminating nul and wait for reply. + if (send(socket_fd, &result, 1, MSG_NOSIGNAL) != 1) goto error; + if (recv(socket_fd, &result, 1, 0) != 1) exit(EXIT_FAILURE); + exit(result ? EXIT_SUCCESS : EXIT_FAILURE); +} + +/** + * Display usage and exit with @a exit_status. + */ +void usage(int exit_status) +{ + std::cerr << "Usage: remake [options] [target] ...\n" + "Options\n" + " -d Echo script commands.\n" + " -d -d Print lots of debugging information.\n" + " -h, --help Print this message and exit.\n" + " -j[N], --jobs=[N] Allow N jobs at once; infinite jobs with no arg.\n" + " -k Keep going when some targets cannot be made.\n" + " -r Look up targets from the dependencies on standard input.\n" + " -v V=X, --var V=X Initialize variable V with X" + " -s, --silent, --quiet Do not echo targets.\n"; + exit(exit_status); +} + +/** + * This program behaves in two different ways. + * + * - If the environment contains the REMAKE_SOCKET variable, the client + * connects to this socket and sends to the server its build targets. + * It exits once it receives the server reply. + * + * - Otherwise, it creates a server that waits for build requests. It + * also creates a pseudo-client that requests the targets passed on the + * command line. + */ +int main(int argc, char *argv[]) +{ + init_working_dir(); + + string_list targets; + bool indirect_targets = false; + + // Parse command-line arguments. + for (int i = 1; i < argc; ++i) + { + std::string arg = argv[i]; + if (arg.empty()) usage(EXIT_FAILURE); + if (arg == "-h" || arg == "--help") usage(EXIT_SUCCESS); + if (arg == "-d") + if (echo_scripts) debug.active = true; + else echo_scripts = true; + else if (arg == "-k" || arg =="--keep-going") + keep_going = true; + else if (arg == "-s" || arg == "--silent" || arg == "--quiet") + show_targets = false; + else if (arg == "-r") + indirect_targets = true; + else if (arg.compare(0, 2, "-j") == 0) + max_active_jobs = atoi(arg.c_str() + 2); + else if (arg.compare(0, 7, "--jobs=") == 0) + max_active_jobs = atoi(arg.c_str() + 7); + else if (arg == "-v" || arg == "--var") + { + ++i; + if (i == argc) usage(EXIT_FAILURE); + arg = argv[i]; + std::istringstream in (arg); + std::string name = read_word(in); + if (next_token(in) != Equal) { + std::cerr << "Invalid variable '" << arg << "'" << std::endl; + exit(EXIT_FAILURE); + } + in.ignore(1); + string_list l = read_words(in); + variables[name] = l; + } + else + { + if (arg[0] == '-') usage(1); + targets.push_back(normalize(arg)); + DEBUG << "New target: " << arg << '\n'; + } + } + + if (indirect_targets) + { + load_dependencies(std::cin); + string_list l; + targets.swap(l); + if (l.empty() && !dependencies.empty()) + { + l.push_back(dependencies.begin()->second->targets.front()); + } + for (string_list::const_iterator i = l.begin(), + i_end = l.end(); i != i_end; ++i) + { + dependency_map::const_iterator j = dependencies.find(*i); + if (j == dependencies.end()) continue; + dependency_t const &dep = *j->second; + for (string_set::const_iterator k = dep.deps.begin(), + k_end = dep.deps.end(); k != k_end; ++k) + { + targets.push_back(normalize(*k)); + } + } + dependencies.clear(); + } + +#ifdef WINDOWS + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2,2), &wsaData)) + { + std::cerr << "Unexpected failure while initializing Windows Socket" << std::endl; + return 1; + } +#endif + + // Run as client if REMAKE_SOCKET is present in the environment. + if (char *sn = getenv("REMAKE_SOCKET")) client_mode(sn, targets); + + // Otherwise run as server. + server_mode(targets); +} diff --git a/tools/odeps.sh b/tools/odeps.sh new file mode 100755 index 0000000..2443c21 --- /dev/null +++ b/tools/odeps.sh @@ -0,0 +1,87 @@ +#!/bin/sh +#Save the parameters + +PROG="$0" +CMDLINE="$*" +usage() { + echo "$PROG [ocamldep ... ]" +} + +OPTIONS="" +INCLUDES="" +ARGS="" +NATIVE=0 +while true; do + case $1 in + -ppopt) + OPTIONS="$OPTIONS $1 $2" + shift + ;; + -I) + OPTIONS="$OPTIONS $1 $2" + INCLUDES="$INCLUDES $2" + shift + ;; + -native) + NATIVE=1 + OPTIONS="$OPTIONS $1" + ;; + *.ml*) + ARGS="$ARGS $1" + ;; + *) + OPTIONS="$OPTIONS $1" + ;; + esac + if test "$#" -eq 0; then break; else shift; fi +done + +if test "$NATIVE" -eq 1; then + OEXT="cmx" +else + OEXT="cmo" +fi +IEXT="cmi" + +for src in $ARGS; do + $OPTIONS -modules "$src" | while read file deps; do + if test -z "$file"; then continue; fi + file=${file%:} + if test "$file" != "${file%.mli}"; then + ext="$IEXT" + base="${file%.mli}" + src_ext="mli" + else + ext="$OEXT" + base="${file%.ml}" + src_ext="ml" + fi + dependencies="" + for dep in $deps; do + modbase=$(echo $dep | sed 's/\(.*\)/\l\1/') + for dir in $INCLUDES; do + dir=${dir%/} #remove trailing slash, if any + #Interfaces or bytecode objects depends on other compile interfaces + #if they exists, otherwise they depend on the object + if test \( "$ext" = "cmi" -o "$ext" = "cmo" \) -a -f "$dir"/"$modbase".mli ; then + dependencies="$dependencies $dir/$modbase".cmi + break + elif test -f "$dir"/"$modbase".ml -o -f "$dir"/"$modbase".mly -o -f "$dir"/"$modbase".mll -o -d "$dir"/"$modbase"; then + dependencies="$dependencies $dir/$modbase"."$ext" + break + fi + done + done + #finally add the cmi as dependency of the cmo/cmx, + #if the .mli exists + if test "$ext" != "cmi" -a -f "$base".mli ; then + dependencies=" ${base}.cmi${dependencies}" + fi + #output a phony dependency between the cmi and the cmx/cmo if there is no .mli + if test "$ext" != "cmi" -a ! -f "$base".mli; then + echo "${base}.cmi: ${base}.${ext}" + fi + echo "${base}.${ext}:${dependencies}" + done + +done diff --git a/tools/osort.sh b/tools/osort.sh new file mode 100755 index 0000000..b967ee3 --- /dev/null +++ b/tools/osort.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +EXT="$1" +if [ -z "$EXT" ] +then + EXT="cmx" +fi + +TARGET="$2" + +DEPS=$( +while read target deps +do + target="${target%.ml:}.$EXT" + dir=$(dirname "$target") + echo -n "$target":" " + for d in $deps + do + base=$(echo "$d" | sed 's/\(.*\)/\l\1/g' ) + if [ -f "$dir"/"$base".ml -o -d "$dir"/"$base" ] + then + echo -n "$dir"/"$base"".$EXT " + fi + done + echo +done) + +SORTED="" +while true +do + ROOT=$(echo "$DEPS" | grep '^[^ ]*.'$EXT': *$' | cut -f 1 -d: ) + NDEPS=$(echo "$DEPS" | grep -v '^[^ ]*.'$EXT': *$') + if [ "$DEPS" = "$NDEPS" ] + then + SORTED="$SORTED $(echo $DEPS | cut -f 2 -d :)" + SORTED="$SORTED $(echo $DEPS | cut -f 1 -d :)" + break + fi + SORTED="$SORTED $ROOT" + for r in $ROOT + do + DEPS=$(echo "$DEPS" | sed "s|$r:\?||g") + done +done +for i in $SORTED +do +echo -n "$i " +done +echo