This commit is contained in:
Michael Gran 2021-02-10 06:28:32 -08:00
parent 8aa578a241
commit 6af9c017ff
6 changed files with 728 additions and 394 deletions

@ -47,19 +47,31 @@ is named `makefile.scm`; however, you may choose any name.
This boilerplate loads the library functions and it parses the
command-line arguments. The command-line arguments are the following,
makefile.scm [-efiknprs] [var=value...] [target_name...]
-e makevars from the environment override makefiles
in the script.
-f rebuild even if the prerequisite timestamps are
earlier than the target timestamps
-i ignore error codes return from system commands
-k continue to update other targets that don't depend
on a target that has reported a non-ignored error
-n write commands to be executed, but, do not execute
them
-p write debug output
-r do not use any built-in rules
-s use a terse output
makefile.scm [-hvqVeEbknB] [var=value...] [target_name...]
-h, --help
displays help
-v, --version
displays the version number of this script
-q, --quiet
use a terse output format
-V, --verbose
use a verbose output format
-e, --environment
environment variables are converted to makevars
-E, --elevate-environment
environment variables are converted to makevars
and will override makevars set in the script
-b, --builtins
adds some default makevars and suffix rules
--ignore-errors
keep building even if a command fails
-k, --continue-on-error
keep building some targets even if a command fails
-n, --no-execute
print rules, but only execute rules marked as
'always execute'
-B, --boring
use ASCII-only output and no colors
[var=value...]
set the value of makevars
@ -67,6 +79,11 @@ command-line arguments. The command-line arguments are the following,
Set one or more targets to be executed. If no target
is specified, the first target found will be executed.
Note that in POSIX `make`, it, by default, adds in environment
variables and built-in rules. With this library, these require
command-line arguments to be enabled to pick up environment variables
and built-in rules. This is to make this tool more appropriate for
generating *reproducible builds*.
## Environment Variables
@ -125,7 +142,8 @@ list. There are 3 components
If the COMMAND recipe is a string, it will be passed to the `system`
procedure for execution by the shell. If any call to system returns a
non-zero return value, processing will end. (This behavior is modified
by the '-i' and '-k' command-line arguments.)
by the `--ignore-errors` and `--continue-on-error` command-line
arguments.)
If the COMMAND recipe is a procedure, it will be executed. If it
returns `#f` or a non-zero integer, failure is assumed. If the
@ -134,14 +152,14 @@ COMMAND recipe returns a string, the resulting string is passed to
If the COMMAND recipe is a pair, and the CAR of the pair is one of
`'ignore-error`, `'silent`, or `'always-execute`, it will have the
extra effect of ignoring errors, not printing the command line,
or always executing even when the `-n` option is enabled. The CDR
must be a string or procedure as above.
extra effect of ignoring errors, not printing the command line, or
always executing even when the `--no-execution` option is enabled.
The CDR must be a string or procedure as above.
There are a set of helper functions and variables that can be used to
construct recipes.
compose element ...
string-compose element ...
~ element ...
ignore-error-compose element ...
~- element ...
@ -150,21 +168,28 @@ construct recipes.
always-execute-compose element ...
~+ element ...
`compose` (aka `~`) takes as arguments one or more elements, each of which
is a string or a procedure of zero arguments that returns a string. It
executes any procedure arguments and concatenates the resulting
strings, appending spaces in between them.
`string-compose` (aka `~`) takes as arguments one or more elements. It
converts the elements to strings and concatenates the strings,
appending spaces between them. The conversion to strings happens as if
by `display`.
`ignore-error-compose` (aka `~-`) is like compose but returns a pair
with the first argument of `'ignore-error`. When passed as a recipe,
it causes the recipe not to end execution, even if an error is
For elements that are procedures, they are executed and their result
is used instead.
It is returned as a pair, where the `car` is the symbol `'default`.
That symbol is interpreted by the builder.
`ignore-error-compose` (aka `~-`) is like string-compose but returns a
pair with the first argument of `'ignore-error`. When passed as a
recipe, it causes the recipe not to end execution, even if an error is
signaled.
`silent-compose` (aka `~@`) is like compose, but, it does not print
the resulting string to the output port.
`silent-compose` (aka `~@`) is like string-compose, but, it does not
print the resulting string to the output port, except in verbose mode.
`always-execute-compose` (aka `~+`) is like compose, but, it forces the line
to always be executed, even if the `-n` option was chosen.
`always-execute-compose` (aka `~+`) is like compose, but, it forces
the line to always be executed, even if the `--no-execution` option
was chosen.
target-name
$@
@ -237,7 +262,7 @@ above.
suffix-rule source-suffix target-suffix [commands...]
-> source-suffix target-suffix [commands ...]
`suffix-rule` (aka `->`) adds a suffix rule to the suffix rule
`suffix-rule` (aka `->` or `→`) adds a suffix rule to the suffix rule
list. There are 3 components
- SOURCE-SUFFIX is a string that names the filename suffix of the file

@ -1,10 +0,0 @@
(define-module (potato ecma48)
#:export (underline
reset))
(define (underline)
(display (string #\escape #\[ #\4 #\m)))
(define (reset)
(display (string #\escape #\[ #\0 #\m)))

@ -3,10 +3,10 @@
#:use-module (ice-9 format)
#:use-module (potato exceptions)
#:use-module (potato builtins)
#:use-module (potato ecma48)
#:use-module (potato text)
#:export (initialize-makevars
%makevars
%environment-overrides?
%elevate-environment?
lazy-assign ?=
assign :=
reference $
@ -24,11 +24,12 @@
;; The lower priority level always win, unless the '-e' flag was set
;; If the '-e' flag is set level 1 doesn't override level 3 and 4.
(define %ascii? #f)
(define %makevars #f)
(define %environment-overrides? #f)
(define %debug? #t)
(define %elevate-environment? #f)
(define %verbose? #t)
(define (debug spec . args)
(when %debug?
(when %verbose?
(apply format (append (list #t spec) args))))
@ -53,7 +54,7 @@ later equals signs."
(define (override? old-priority new-priority)
"The logic of whether which makemacro priority levels can override
others."
(if %environment-overrides?
(if %elevate-environment?
(if (and (or (= old-priority) (= old-priority 3) (= old-priority 4))
(= new-priority 1))
#f
@ -129,9 +130,9 @@ the value of MAKEFLAGS or SHELL."
(define (dump-makevars)
"Write out a list of the current makevars."
(when (not (zero? (hash-count (const #t) %makevars)))
(underline)
(display (underline))
(display "Makevars")
(reset)
(display (default))
(newline)
(let ((keyvals
(sort
@ -140,56 +141,64 @@ the value of MAKEFLAGS or SHELL."
(string<? (car a) (car b))))))
(for-each
(lambda (keyval)
(let* ((key (car keyval))
(val (cdr keyval))
(part1 (if (zero? (string-length (car val)))
(string-copy key)
(string-append key " → " (car val))))
(part1-trunc
(if (> (string-length part1) 50)
(string-append (substring part1 0 50) "…")
part1))
(part2 (make-string (- 54 (string-length part1-trunc))
#\space))
(priority (cdr val))
(part3 (list-ref '("unknown"
"script"
"command line"
"MAKEFLAGS"
"environment"
"built-in")
priority)))
(string-map!
(lambda (c)
(if (char<? c #\space)
;; Replace control codes with control pictures
(integer->char (+ #x2400 (char->integer c)))
c))
part1)
(display " ")
(display part1-trunc)
(display part2)
(display part3)
(newline)))
keyvals))))
(let ((key (car keyval))
(val (cdr keyval)))
(let ((keyval-string
(if (zero? (string-length (car val)))
(string-copy key)
(string-append key " " (right-arrow) " " (car val)))))
;; Replace any control characters in VAL, like newline or tab
(set! keyval-string
(string-fold
(lambda (c str)
(string-append str
(if (char<? c #\space)
(C0 c)
(string c))))
""
keyval-string))
;; Truncate
(if (> (string-length keyval-string) 60)
(if %ascii?
(set! keyval-string
(string-append (substring keyval-string 0 57) "..."))
(set! keyval-string
(string-append (substring keyval-string 0 59) "…"))))
(let* ((space (make-string (- 64 (string-length keyval-string))
#\space))
(priority (cdr val))
(source-string (list-ref '("unknown"
"script"
"command line"
"MAKEFLAGS"
"environment"
"built-in")
priority)))
(display " ")
(display keyval-string)
(display space)
(display source-string)
(newline)))))
keyvals))))
(define (initialize-makevars keyvals
elevate-env-vars?
ignore-environment?
no-builtins?
debug?)
(set! %environment-overrides? elevate-env-vars?)
environment?
elevate-environment?
builtins?
verbose?
ascii?)
(set! %elevate-environment? elevate-environment?)
(set! %makevars (make-hash-table))
(set! %debug? debug?)
(unless no-builtins?
(set! %verbose? verbose?)
(set! %ascii? ascii?)
(when builtins?
(makevars-add-builtins))
(makevars-add-environment)
(makevars-add-makeflags)
(when environment?
(makevars-add-environment)
(makevars-add-makeflags))
(makevars-add-keyvals keyvals)
(when %debug?
(dump-makevars))
)
(when %verbose?
(dump-makevars)))
;; API
(define* (lazy-assign key #:optional (val ""))
@ -213,7 +222,7 @@ referenced.
(unless (string? KEY)
(bad-proc-output "lazy-assign" key))
(makevars-set KEY VAL)
(when (and %debug? (string? VAL))
(when (and %verbose? (string? VAL))
(format #t "~A=~A~%" KEY VAL))))
(define-syntax ?=
@ -244,7 +253,7 @@ string to use as the key in the hash table entry.
(unless (string? VAL)
(bad-proc-output "assign" VAL))
(makevars-set KEY VAL)
(when %debug?
(when %verbose?
(format #t "~A=~A~%" KEY VAL))))
(define-syntax :=
@ -287,7 +296,7 @@ space-separated token in the looked-up value."
(unless (string? val)
(bad-proc-output "reference" val))
(hash-set! %makevars key (cons val priority))
(when %debug?
(when %verbose?
(format #t "~A=~A~%" key val)))
(when (procedure? transformer)
(set! val (string-append-with-spaces
@ -327,7 +336,7 @@ that string."
(if (promise? val)
(lambda ()
(let ((VAL (force val)))
;; FIXME: put debug print here?
;; FIXME: put verbose print here?
VAL))
;; else
(lambda ()

@ -19,118 +19,23 @@
target-basename $*
prerequisites $^
build
string-compose ~
string-compose ~
silent-compose ~@
always-execute-compose ~+
ignore-error-compose ~-
))
(define-record-type <target-rule>
(make-target-rule name prerequisites recipes priority)
target-rule?
;; A filename, for real targets, or just a name for phony targets
(name target-rule-get-name target-rule-set-name!)
;; A list of filenames and/or phony targets that have target rules
(prerequisites target-rule-get-prerequisites
target-rule-set-prerequisites!)
;; A list of strings or procedures
(recipes target-rule-get-recipes
target-rule-set-recipes!)
;; 1 = script-defined. 2 = built-in
(priority target-rule-get-priority
target-rule-set-priority!))
(define-record-type <suffix-rule>
(make-suffix-rule source-suffix target-suffix recipes priority)
suffix-rule?
;; A string, usually like ".c". Or a string->string proc.
(source-suffix suffix-rule-get-source
suffix-rule-set-source)
;; A string, usually like ".o". Or a string->bool proc.
(target-suffix suffix-rule-get-target
suffix-rule-set-suffix!)
;; A list of strings or procedures
(recipes suffix-rule-get-recipes
suffix-rule-set-recipes!)
;; 1 = script-defined. 2 = built-in
(priority suffix-rule-get-priority
suffix-rule-set-priority!))
(define-record-type <node>
(make-node name parent status)
node?
;; For a real target, this is a filename. For a phony target it is
;; just a label.
(name node-get-name node-set-name!)
;; A <node> which is the parent of this node, or #f.
(parent note-get-parent note-set-parent!)
;; If 'name' is a regular file, mtime holds its last modification
;; time in nanoseconds since the epoch. If 'name' does not exist,
;; _mtime is #f.
(mtime node-get-mtime node-set-mtime!)
;; One of 'success, 'failure, or 'undetermined
(status node-get-status node-set-status!)
;; A list of rules to evaluate to try to c
(rules node-get-rules node-set-rules!)
(children node-get-children node-set-children!)
;; Determines how many children must pass for the parent
;; to be evaluated. Either 'or or 'and.
(logic node-get-logic node-set-logic!)
)
(define %node-cur #f)
(define target-name
(lambda ()
(if %node-cur
(node-get-name %node-cur)
#f)))
(define $@ target-name)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPER FUNCTIONS
(define (basename str)
"Strip off the '.ext' part of a filename string."
(let ((idx (string-index-right str #\.)))
(if idx
(substring str 0 idx)
str)))
(define target-basename
(lambda ()
(if %node-cur
(basename (node-get-name %node-cur)))))
(define $* target-basename)
(define primary-prerequisite
(lambda ()
(if %node-cur
(let ((prereq (node-get-children %node-cur)))
(if (null? prereq)
""
(car prereq))))))
(define $< primary-prerequisite)
(define (first-target-rule-name)
(if (null? %target-rules)
#f
;; else
(target-rule-get-name (car %target-rules))))
;; target-name $@
;; target-basename $*
;; newer-prerequisites $?
;; prerequisites $^
;; primary-prerequisite $<
;; There are two priorities of rules
;; 1. rules defined in the script have higher priority than built-in rules
;; 2. later-defined rules have higher priority earlier defined-rules
;; 3. a target rule will have higher priority than a suffix rule for the same
;; target
(define %target-rules '())
(define %suffix-rules '())
(define %debug? #f)
(define (string-compose . args)
(define (base-compose . args)
"Returns a lambda that appends args together as a string,
adding intermediate spaces. If an arg is a procedure,
it is evaluated."
@ -150,7 +55,7 @@ it is evaluated."
(set! effective-arg (arg))
#;(unless (string? effective-arg)
(bad-proc-output "~" arg))
(bad-proc-output "~" arg))
)
((string? arg)
(set! effective-arg arg))
@ -171,38 +76,25 @@ it is evaluated."
(if need-a-space? " " "")
effective-arg)))))))))
(define (string-compose . args)
(cons 'default (apply base-compose args)))
(define ~ string-compose)
(define (initialize-rules no-builtins? debug?)
(set! %target-rules '())
(set! %suffix-rules '())
(set! %debug? debug?)
(unless no-builtins?
;; Set up initial suffix rules
(suffix-rule ".c" ".o"
(string-compose
(reference "CC")
(reference "CFLAGS")
"-c"
primary-prerequisite)))
)
(define (ignore-error-compose . args)
(cons 'ignore-error (apply base-compose args)))
(define* (target-rule name #:optional (prerequisites '()) #:rest recipes)
;; FIXME: Typecheck
(let ((rule (make-target-rule name prerequisites recipes 1)))
;; Add to %target-rules
(set! %target-rules (cons rule %target-rules))))
(define ~- ignore-error-compose)
;; Alias
(define : target-rule)
(define (silent-compose . args)
(cons 'silent (apply base-compose args)))
(define (suffix-rule source target . recipes)
;; FIXME: Typecheck
(let ((rule (make-suffix-rule source target recipes 1)))
(set! %suffix-rules (cons rule %suffix-rules))))
(define ~@ silent-compose)
;; Alias
(define -> suffix-rule)
(define (always-execute-compose . args)
(cons 'always-execute (apply base-compose args)))
(define ~@ always-execute-compose)
(define (regular-file? filename)
(let ((st (stat filename #f)))
@ -213,85 +105,149 @@ it is evaluated."
(+ (* 1000000000 (stat:mtime st))
(stat:mtimensec st))))
(define (create-node name parent)
"Constructs a tree of nodes, with name as the root node."
(let ((node (make-node name parent 'untested)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TARGET STRUCT AND METHODS
;; FIXME: here check that this name differs from all ancenstor's
;; names
(define-record-type <target-rule>
(make-target-rule name prerequisites recipes priority)
target-rule?
;; A filename, for real targets, or just a name for phony targets
(name target-rule-get-name target-rule-set-name!)
;; A list of filenames and/or phony targets that have target rules
(prerequisites target-rule-get-prerequisites
target-rule-set-prerequisites!)
;; A list of strings or procedures
(recipes target-rule-get-recipes
target-rule-set-recipes!)
;; 1 = script-defined. 2 = built-in
(priority target-rule-get-priority
target-rule-set-priority!))
;; Try to the file's modification time.
(when (file-exists? name)
(when (not (regular-file? name))
(not-a-regular-file "create-node" name))
(when (not (access? name R_OK))
(no-read-access-to-file "create-node" name))
(node-set-mtime! node (compute-mtime name)))
;; Search for matching target rule.
(when (not (null? %target-rules))
(let loop ((rule (car %target-rules))
(rest (cdr %target-rules)))
;; List of all target rules in order of importance
(define %target-rules '())
;; N.B: here we assume target rule names and
;; predicates are exclusively strings.
(if (string=? name (target-rule-get-name rule))
(begin
;; OK we have a matching rule
(node-set-rules! node (list rule))
(node-set-logic! node 'and)
;; For target-rules, the prerequisites comes from the
;; rule itself.
(define* (target-rule name #:optional (prerequisites '()) #:rest recipes)
"Register a new target rule"
;; FIXME: Typecheck
(let ((rule (make-target-rule name prerequisites recipes 1)))
;; Add to %target-rules
(set! %target-rules (cons rule %target-rules))))
;; Oooh, recursion!
(node-set-children! node
(map (lambda (prereq)
(create-node prereq node))
(target-rule-get-prerequisites rule))))
;; else
(if (not (null? rest))
(loop (car rest) (cdr rest))
;; else, no matching rule found
(node-set-rules! node '())))))
;; Alias
(define : target-rule)
#|
;; If no rule found so far, search for suffix rules.
(when (null? (node-get-rules node))
(for-each
(lambda (rule)
(let ((targ (suffix-rule-get-target rule)))
(when (or
;; string suffix
(and (string? targ)
(string-suffix? targ name))
;; procedure suffix
(and (procedure? targ)
(targ name)))
;; For suffix rules, there will be exactly one child per
;; rule and the name of the child is constructed from a
;; suffix and the parent's name.
(node-set-rules! node (cons rule (node-get-rules node)))
(node-set-logic! node 'or)
(let* ((src (suffix-rule-get-source rule))
(prereq
(if (string? src)
(string-append
(string-drop-right name (string-length src))
src)
;; else, src is a conversion func.
(src name))))
;; Note the recursion here.
(node-set-children! node
(cons (create-node prereq node)
(node-get-children node)))))))
%suffix-rules))
(define (first-target-rule-name)
(if (null? %target-rules)
#f
;; else
(target-rule-get-name (car %target-rules))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SUFFIX STRUCT AND METHODS
(define-record-type <suffix-rule>
(make-suffix-rule source-suffix target-suffix recipes priority)
suffix-rule?
;; A string, usually like ".c". Or a string->string proc.
(source-suffix suffix-rule-get-source
suffix-rule-set-source)
;; A string, usually like ".o". Or a string->bool proc.
(target-suffix suffix-rule-get-target
suffix-rule-set-suffix!)
;; A list of strings or procedures
(recipes suffix-rule-get-recipes
suffix-rule-set-recipes!)
;; 1 = script-defined. 2 = built-in
(priority suffix-rule-get-priority
suffix-rule-set-priority!))
;; The list of all registered suffix rules in order of importance
(define %suffix-rules '())
(define (suffix-rule source target . recipes)
"Register a suffix rule"
;; FIXME: Typecheck
(let ((rule (make-suffix-rule source target recipes 1)))
(set! %suffix-rules (cons rule %suffix-rules))))
;; Alias
(define -> suffix-rule)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; NODE STRUCT AND METHODS
(define-record-type <node>
(make-node name parent status)
node?
;; For a real target, this is a filename. For a phony target it is
;; just a label.
(name node-get-name node-set-name!)
;; A <node> which is the parent of this node, or #f.
(parent note-get-parent note-set-parent!)
;; If 'name' is a regular file, mtime holds its last modification
;; time in nanoseconds since the epoch. If 'name' does not exist,
;; _mtime is #f.
(mtime node-get-mtime node-set-mtime!)
;; One of 'pass, 'fail, or 'undetermined
(status node-get-status node-set-status!)
;; A list of rules to evaluate to try to c
(rules node-get-rules node-set-rules!)
(children node-get-children node-set-children!)
;; Determines how many children must pass for the parent
;; to be evaluated. Either 'or or 'and.
(logic node-get-logic node-set-logic!)
)
;;;;;;;;;
;; Automatic variables
;; target-name $@
;; target-basename $*
;; newer-prerequisites $?
;; prerequisites $^
;; primary-prerequisite $<
;; This is set in the builder to make automatic variables work.
(define %node-cur #f)
(define target-name
(lambda ()
(if %node-cur
(node-get-name %node-cur)
#f)))
(define $@ target-name)
(define target-basename
(lambda ()
(if %node-cur
(basename (node-get-name %node-cur)))))
(define $* target-basename)
(define primary-prerequisite
(lambda ()
(if %node-cur
(let ((prereq (node-get-children %node-cur)))
(if (null? prereq)
""
(car prereq))))))
(define $< primary-prerequisite)
(define newer-prerequisites
(lambda ()
(error "FIXME")))
(define $? newer-prerequisites)
(define prerequisites
(lambda ()
(error "FIXME")))
(define $^ prerequisites)
;; First matching rule has highest priority
(node-set-rules! node (reverse (node-get-rules node)))
(node-set-children! node (reverse (node-get-children node)))
|#
;; And node is ready to go
node))
(define (undetermined? node)
(eq? (node-get-status node) 'undetermined))
@ -320,61 +276,288 @@ it is evaluated."
(else
#f)))
(define (build root)
"Give a tree of <node>, this executes the recipes therein."
(let ((tree (create-node root #f)))
(let ((node root))
(while #t
(if (undetermined? node)
(if (children-complete? node)
(if (children-passed? node)
(if (has-recipe? node)
(run-recipe! node)
;; else, no recipe exists
(run-default-recipe! node))
;; else, children have failed
(set-fail! node))
;; else, children aren't complete
(set! node (get-next-child node)))
;; else, this node is determined
(if (and abort-on-error (failed? node))
(break)
;; else not failed
(if (has-parent? node)
(set! node (get-parent node))
;; else, there is no parent to this node
(break))))))))
(define (has-recipe? node)
(not (null? (node-get-recipe node))))
(define (set-fail! node)
(node-set-status! node 'fail))
(define (set-pass! node)
(node-set-status! node 'fail))
(define (get-next-child node)
"Return the first child node that is not yet PASS or FAIL"
(let ((children (node-get-children node)))
(any (lambda (child)
(if (eqv? (node-get-status child) 'undetermined)
child
#f))
children)))
(define (failed? node)
(eqv? (node-get-status node) 'fail))
(define (has-parent? node)
(if (node-get-parent node)
#t
#f))
(define (up-to-date? node)
"Checks if node is up to date:
- it has an mtime
- all its children have mtimes
- its mtime is older than the mtime of its children"
(let ((children (map node-get-mtime (node-get-children node)))
(parent (node-get-mtime node)))
(if (every (lambda (child)
(and (integer? parent)
(integer? child)
(>= parent child)))
children)
#t
#f)))
(define (get-parent node)
(node-get-parent node))
#|
(define (run-recipe! node quiet verbose)
"Runs the recipes associated with this node, one by one. Recipes
are either strings, procedures that return strings, or generic
procedures. If a failure condition happens, mark the node as having
failed."
(let ((recipes (node-get-recipes node)))
(when (null? recipes)
(error "no recipe"))
(let loop ((opt/recipe (car recipes))
(rest (cdr recipes)))
(let ((opt ((car recipe/opt))
(recipe (cdr recipe)))
;; Recipes are either
;; - strings to pass to system
;; - procedures that return a string which is passed
;; to system
;; - procedures (that don't return a string) that are executed
;; that pass unless they return #f
; OPT is one of 'default, 'ignore, 'silent
(cond
((string=? recipe)
(unless (or quiet (eq? opt 'silent))
(format #t "[SYSTEM] ~A~$" recipe)
(let ((retval (system recipe)))
(unless (or quiet (eq? opt 'silent))
(format #t " --> ~S~%" (status:exit-val retval)))
(when (and (not (eqv? opt 'ignore))
(or (eqv? #f (status:exit-val retval))
(not (zero? (status:exit-val retval)))))
(node-set-status! node 'fail))))
((procedure? recipe)
(unless (or quiet (eq? opt 'silent))
(if (procedure-name recipe)
(format #t "[PROC] ~A~%" (procedure-name recipe))
(format #t "[PROC] ~%")))
(let ((retval (recipe)))
(unless (or quiet (eq? opt 'silent))
(format #t " --> ~S~%" (status:exit-val retval2)))
(cond
((eqv? retval #f)
(unless (eqv? opt 'ignore)
(node-set-status node 'fail)))
((string=? retval)
(unless (or quiet (eq? opt 'silent))
(format #t "[SYSTEM] ~A~$" retval)
(let ((retval2 (system retval)))
(unless (or quiet (eq? opt 'silent))
(format #t " --> ~S~%" (status:exit-val retval2)))
(when (and (not (eqv? opt 'ignore))
(or (eqv? #f (status:exit-val retval))
(not (zero? (status:exit-val retval)))))
(node-set-status! node 'fail))))))))
(cond
((eqv? (node-get-status node) 'fail)
;; quit
)
((null? rest)
(node-set-status! node) 'pass)
(else
((loop (car rest)
(cdr rest))))))))))
(when (eq? 'pass (node-get-status node))
(let ((name (node-get-name node)))
(when (and (file-exists? name)
(regular-file? name))
(node-set-mtime! node (compute-mtime name))))))
(define (run-default-recipe! node)
"The default recipe passes if the file exists"
(let ((name (node-get-name node)))
(if (and (file-exists? name)
(regular-file? name))
(begin
(node-set-status! node 'pass)
(node-set-mtime! node (compute-mtime name)))
;; else
(node-set-status! node 'fail))))
;; Start at root
;; If cur is UNDETERMINED, find a leaf that is UNDETERMINED.
;; Descent to first leaf.
;; If self's mtime is earlier than parent's mtime, mark self as PASS.
;; Elif self has rules, run rules and mark self PASS/FAIL.
;; Else self has no mtime or rules, so mark self FAIL.
;; Go to parent.
;; IF PASS or FAIL, go to parent
;; IF UNDETERMINED do these...
;; Are we done with the children?
;; If AND rule and one child is FAIL, stop
;; If OR rule and one child is PASS, stop
;; If no children left, stop
;; Else keep going
;; Did the children pass?
;; IF AND rule and all children are PASS, true
;; IF OR rule an one child is PASS, true
;; Otherwise, false
;; If the children FAIL, cur is FAIL
;; If the children PASS, run rules and mark self PASS/FAIL
;; Go to parent
;; 3 failures
;; - If anything fails, stop immediately
;; - If anything fails, searching through tree
;; - Ignore errors
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; LET'S GO!
(define %debug? #f)
(define (initialize-rules no-builtins? debug?)
(set! %target-rules '())
(set! %suffix-rules '())
(set! %debug? debug?)
(unless no-builtins?
;; Set up initial suffix rules
(suffix-rule ".c" ".o"
(string-compose
(reference "CC")
(reference "CFLAGS")
"-c"
primary-prerequisite)))
)
(define (create-node name parent)
"Constructs a tree of nodes, with name as the root node."
(let ((node (make-node name parent 'untested)))
;; FIXME: here check that this name differs from all ancenstor's
;; names
;; Try to the file's modification time.
(when (file-exists? name)
(when (not (regular-file? name))
(not-a-regular-file "create-node" name))
(when (not (access? name R_OK))
(no-read-access-to-file "create-node" name))
(node-set-mtime! node (compute-mtime name)))
;; Start at root
;; Search for matching target rule.
(when (not (null? %target-rules))
(let loop ((rule (car %target-rules))
(rest (cdr %target-rules)))
;; If cur is UNDETERMINED, find a leaf that is UNDETERMINED.
;; Descent to first leaf.
;; If self's mtime is earlier than parent's mtime, mark self as PASS.
;; Elif self has rules, run rules and mark self PASS/FAIL.
;; Else self has no mtime or rules, so mark self FAIL.
;; Go to parent.
;; N.B: here we assume target rule names and
;; predicates are exclusively strings.
(if (string=? name (target-rule-get-name rule))
(begin
;; OK we have a matching rule
(node-set-rules! node (list rule))
(node-set-logic! node 'and)
;; For target-rules, the prerequisites comes from the
;; rule itself.
;; IF PASS or FAIL, go to parent
;; Oooh, recursion!
(node-set-children! node
(map (lambda (prereq)
(create-node prereq node))
(target-rule-get-prerequisites rule))))
;; else
(if (not (null? rest))
(loop (car rest) (cdr rest))
;; else, no matching rule found
(node-set-rules! node '())))))
;; IF UNDETERMINED do these...
#|
;; If no rule found so far, search for suffix rules.
(when (null? (node-get-rules node))
(for-each
(lambda (rule)
(let ((targ (suffix-rule-get-target rule)))
(when (or
;; string suffix
(and (string? targ)
(string-suffix? targ name))
;; procedure suffix
(and (procedure? targ)
(targ name)))
;; For suffix rules, there will be exactly one child per
;; rule and the name of the child is constructed from a
;; suffix and the parent's name.
(node-set-rules! node (cons rule (node-get-rules node)))
(node-set-logic! node 'or)
(let* ((src (suffix-rule-get-source rule))
(prereq
(if (string? src)
(string-append
(string-drop-right name (string-length src))
src)
;; else, src is a conversion func.
(src name))))
;; Note the recursion here.
(node-set-children! node
(cons (create-node prereq node)
(node-get-children node)))))))
%suffix-rules))
;; Are we done with the children?
;; If AND rule and one child is FAIL, stop
;; If OR rule and one child is PASS, stop
;; If no children left, stop
;; Else keep going
;; First matching rule has highest priority
(node-set-rules! node (reverse (node-get-rules node)))
(node-set-children! node (reverse (node-get-children node)))
|#
;; And node is ready to go
node))
;; Did the children pass?
;; IF AND rule and all children are PASS, true
;; IF OR rule an one child is PASS, true
;; Otherwise, false
;; If the children FAIL, cur is FAIL
;; If the children PASS, run rules and mark self PASS/FAIL
;; Go to parent
;; 3 failures
;; - If anything fails, stop immediately
;; - If anything fails, searching through tree
;; - Ignore errors
(define (build root)
"Give a tree of <node>, this executes the recipes therein."
(let ((tree (create-node root #f)))
(let ((node root))
(while #t
(if (undetermined? node)
(if (children-complete? node)
(if (children-passed? node)
(if (up-to-date? node)
(set-pass! node)
;; else, not up to date
(if (has-recipe? node)
(run-recipe! node)
;; else, no recipe exists
(run-default-recipe! node)))
;; else, children have failed
(set-fail! node))
;; else, children aren't complete
(set! node (get-next-child node)))
;; else, this node is determined
(if (and abort-on-error (failed? node))
(break)
;; else not failed
(if (has-parent? node)
(set! node (get-parent node))
;; else, there is no parent to this node
(break))))))))
|#

101
potato/text.scm Normal file

@ -0,0 +1,101 @@
(define-module (potato text)
#:export (underline
default
right-arrow
ellipses
C0
red
initialize-text))
(define %fancy #t)
(define (initialize-text ascii)
(set! %fancy (not ascii)))
(define (default)
(if %fancy
(string #\escape #\[ #\0 #\m)
""))
(define (bold)
(if %fancy
(string #\escape #\[ #\1 #\m)
""))
(define (underline)
(if %fancy
(string #\escape #\[ #\4 #\m)
""))
(define (red)
(if %fancy
(string #\escape #\[ #\3 #\1 #\m)
""))
(define (green)
(if %fancy
(string #\escape #\[ #\3 #\2 #\m)
""))
(define (blue)
(if %fancy
(string #\escape #\[ #\3 #\4 #\m)
""))
(define (important)
(if %fancy
"⚠" ; U+26A0 WARNING SIGN
"!!!"))
(define (stop)
(if %fancy
"🛑" ; U+26A0 WARNING SIGN
"XXX"))
(define (right-arrow)
(if %fancy
"→" "->"))
(define (ellipses)
(if %fancy "…" "..."))
(define (QED)
(if %fancy "∎" "QED")) ; U+220E END OF PROOF
(define (C0 c)
(if %fancy
;; Replace control codes with control pictures
(string (integer->char (+ #x2400 (char->integer c))))
(list-ref '("<NUL>" "<SOH>" "<STX>" "<ETX>" "<EOT>" "<ENQ>"
"<ACK>" "<BEL>" "<BS>" "<HT>" "<LF>"
"<VT>" "<FF>" "<CR>" "<SO>" "<SI>"
"<DLE>" "<DC1>" "<DC2>" "<DC3>" "<DC4>"
"<NAK>" "<SYN>" "<ETB>" "<CAN>" "<EM>"
"<SUB>" "<ESC>" "<FS>" "<GS>" "<RS>"
"<US>")
(char->integer c))))
(define (lquo)
(if %fancy (string #\“) (string #\")))
(define (rquo)
(if %fancy (string #\”) (string #\")))
(define (BOL)
"go to beginning of line"
(if %fancy (string #\escape #\[ #\G) "\n"))
#|
in quiet mode it is just
target -> parent (when building)
target -> parent (on pass)
target -> parent (on fail but continue)
🛑 target -> parent (on stop)
(on successful completion)
in normal mode it is
? target -> parent
recipe truncated to 70 cols, using C0 control pics
etc
then
target -> parent (on pass)
|#

@ -7,6 +7,7 @@
#:use-module (potato exceptions)
#:use-module (potato makevars)
#:use-module (potato rules)
#:use-module (potato text)
#:export (initialize
execute
)
@ -74,29 +75,39 @@
(apply format (append (list #t spec) args))))
(define option-spec
'((help (single-char #\h) (value #f))
(version (single-char #\v) (value #f))
(quiet (single-char #\q) (value #f))
(verbose (single-char #\V) (value #f))
(ignore-environment (value #f))
(no-builtins (single-char #\r) (value #f))
(elevate-environment (single-char #\e) (value #f))
(ignore-errors (single-char #\i) (value #f))
'((help (single-char #\h) (value #f))
(version (single-char #\v) (value #f))
(quiet (single-char #\q) (value #f))
(verbose (single-char #\V) (value #f))
(environment (single-char #\e) (value #f))
(elevate-environment (single-char #\E) (value #f))
(builtins (single-char #\b) (value #f))
(ignore-errors (value #f))
(continue-on-error (single-char #\k) (value #f))
;;(dump-macros (single-char #\p) (value #f))
;;(no-builtins (single-char #\r) (value #f))
;;(silent (single-char #\s) (value #f))
(no-execution (single-char #\n) (value #f))
(ascii (single-char #\A) (value #f))
))
(define (display-help-and-exit argv0)
(format #t "~A [-hvVr] [KEY=VALUE ...] [targets ...]~%" argv0)
(format #t " -h, --help print help and exit~%")
(format #t " -v, --version print version and exit~%")
(format #t " -q, --quiet print minimal output~%")
(format #t " -V, --verbose print maximum output~%")
(format #t " --ignore-environment~%")
(format #t " ignore environment variables~%")
(format #t " -r, --no-builtins no default or built-in rules~%")
(format #t "~A [-hvqVeEbn] [KEY=VALUE ...] [targets ...]~%" argv0)
(format #t " -h, --help print help and exit~%")
(format #t " -v, --version print version and exit~%")
(format #t " -q, --quiet print minimal output~%")
(format #t " -V, --verbose print maximum output~%")
(format #t " -e, --environment use environment variables~%")
(format #t " -E, --elevate-environment~%")
(format #t " use environment variables and let~%")
(format #t " them override script variables~%")
(format #t " -b, --builtins~%")
(format #t " include some common variables and suffix rules~%")
(format #t " --ignore-errors~%")
(format #t " keep building even if commands fail~%")
(format #t " -k, --continue-on-error~%")
(format #t " keep building even if commands fail~%")
(format #t " -n, --no-execution~%")
(format #t " only execute rules marked as 'always execute'~%")
(format #t " -a, --ascii~%")
(format #t " ASCII only output and no colors~%")
(exit 0))
(define (display-version-and-exit argv0)
@ -130,36 +141,38 @@ return them in a list."
lst))
(define* (initialize #:optional
(arguments '())
(ignore-environment 'unknown))
"Set up the options, built-in rules, and built-in makevars. If
IGNORE_ENVIRONMENT is #t or #f, don't parse environment variables,
despite the setting of any ignore-environment flag."
(arguments '()))
"Set up the options, rules, and makevars. If ARGUMENTS
is not set, it will use options, makevars, and targets as
specified by the command line. If it is set, it is
expected to be a list of strings that are command-line
arguments."
(when (null? arguments)
(set! arguments (program-arguments)))
;; We start of with the --help and --version command-line arguments.
(let ((options (getopt-long arguments option-spec))
(%opt-builtins #f)
(%opt-environment #f)
(%opt-elevate-environment #f)
(%opt-no-builtins #f)
(%opt-ignore-environment #f))
(%opt-no-errors #f)
(%opt-continue-on-error #f)
(%opt-no-execution #f)
(%opt-ascii #f))
(when (option-ref options 'help #f)
(display-help-and-exit (car arguments)))
(when (option-ref options 'version #f)
(display-version-and-exit (car arguments)))
;; Then, we do --ignore-environment, because we need to know that
;; Then, we do --environment, because we need to know that
;; before we start parsing MAKEFLAGS
(if (eqv? ignore-environment 'unknown)
(set! %opt-ignore-environment
(option-ref options 'ignore-environment #f))
;; else
(set! %opt-ignore-environment ignore-environment))
(set! %opt-environment
(option-ref options 'environment #f))
;; Parse MAKEFLAGS before the command-line, because we want
;; command-line options to override MAKEFLAGS options.
(unless %opt-ignore-environment
(when %opt-environment
(let ((mf (getenv "MAKEFLAGS")))
(when mf
(let ((tokens (string-tokenize mf)))
@ -169,8 +182,16 @@ despite the setting of any ignore-environment flag."
(when (member "verbose" tokens)
(set! %opt-verbose #t)
(set! %opt-quiet #f))
(when (member "no-builtins" tokens)
(set! %opt-no-builtins #t))))))
(when (member "builtins" tokens)
(set! %opt-builtins #t))
(when (member "ascii" tokens)
(set! %opt-ascii #t))
(when (member "ignore-errors" tokens)
(set! %opt-ignore-errors #t))
(when (member "continue-on-error" tokens)
(set! %opt-continue-on-error #t))
(when (member "no-execution" tokens)
(set! %opt-no-execution #t))))))
;; Now the bulk of the command-line options.
(when (option-ref options 'quiet #f)
@ -179,25 +200,30 @@ despite the setting of any ignore-environment flag."
(when (option-ref options 'verbose #f)
(set! %opt-verbose #t)
(set! %opt-quiet #f))
(set! %opt-no-builtins
(option-ref options 'no-builtins #f))
(set! %opt-builtins
(option-ref options 'builtins #f))
(set! %opt-elevate-environment
(option-ref options 'elevate-environment #f))
(set! %opt-ignore-errors
(option-ref options 'ignore-errors #f))
(set! %opt-continue-on-error
(option-ref options 'continue-on-error #f))
(set! %opt-no-execution
(option-ref options 'no-execution #f))
(set! %opt-ascii
(option-ref options 'ascii #f))
;; Now that all the options are set, we can set up
;; the build environment.
(let ((extra (option-ref options '() '())))
(initialize-text %opt-ascii)
(initialize-makevars (parse-macros extra)
%opt-environment
%opt-elevate-environment
%opt-ignore-environment
%opt-no-builtins
%opt-verbose)
(initialize-rules %opt-no-builtins
%opt-builtins
%opt-verbose
%opt-ascii)
#;(initialize-rules %opt-no-builtins
%opt-verbose)
;; The remaining command-line words are the build targets that