diff --git a/.gitignore b/.gitignore index 0735c26..08946b0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ \#*.rkt# \#*.rkt#*# *.kate-swp +*.bak # Compiled racket bytecode compiled/ @@ -18,6 +19,7 @@ compiled/ # Dependency tracking files *.dep -/*.bak -/gui/*.bak /doc +/docs + + diff --git a/info.rkt b/info.rkt index b01b503..be97bef 100644 --- a/info.rkt +++ b/info.rkt @@ -11,7 +11,7 @@ '(("scrbl/racket-makefile.scrbl" () (library 0)))) (define deps - '("base")) + '("base" "git-cli" "package-zipper")) (define build-deps '("racket-doc" diff --git a/main.rkt b/main.rkt index a95b879..ab5fdf6 100644 --- a/main.rkt +++ b/main.rkt @@ -5,7 +5,8 @@ (for-syntax racket/base) "private/commands.rkt" "private/engine.rkt" - ;git + git-cli + package-zipper ) (provide (all-from-out racket) @@ -27,8 +28,9 @@ $target $deps $< + (all-from-out git-cli) + (all-from-out package-zipper) ) - ;(all-from-out git)) ;; The makefile module currently loaded in this Racket process. Keeping only diff --git a/scrbl/.gitignore b/scrbl/.gitignore new file mode 100644 index 0000000..003ab3f --- /dev/null +++ b/scrbl/.gitignore @@ -0,0 +1,25 @@ +# ---> Racket +# gitignore template for the Racket language +# website: http://www.racket-lang.org/ + +# DrRacket autosave files +*.rkt~ +*.html~ +*.css~ +*.rkt.bak +\#*.rkt# +\#*.rkt#*# +*.kate-swp +*.bak + +# Compiled racket bytecode +compiled/ +*.zo + +# Dependency tracking files +*.dep + +/doc +/docs + + diff --git a/scrbl/racket-makefile.bak b/scrbl/racket-makefile.bak new file mode 100644 index 0000000..58d08dd --- /dev/null +++ b/scrbl/racket-makefile.bak @@ -0,0 +1,375 @@ +#lang scribble/manual + +@(require (for-label racket/base + racket/file + (only-in racket-makefile + target deps phony default-target make refresh-makefile + run raco rm-f rm-rf cleanup + list-dir/files list-files list-dirs + $target $deps $<))) + +@title{racket-makefile} +@author{Hans Dijkema} + +@defmodulelang[racket-makefile] + +@tt{racket-makefile} is a small make-style language built on Racket. It adds +targets, dependencies, timestamp based rebuilding, phony targets, command execution, Racket tool execution, and a few cleanup helpers. The rest of the language is ordinary +Racket. + +A makefile only defines targets. Loading or running the module does not execute +any recipe automatically. Builds are started explicitly with @racket[make]. + +@section{Installation} + +Install the package from a local checkout with: + +@verbatim{ +raco pkg install +} + +For a versioned archive, specify the package name explicitly: + +@verbatim{ +raco pkg install --name racket-makefile racket-makefile-0.1.8.zip +} + +After installation a makefile can start with: + +@verbatim{ +#lang racket-makefile +} + +@section{A first makefile} + +For example: + +@racketblock[ +(define CC 'cc) +(define CFLAGS '(-Wall -O2)) + +(default-target all) +(phony all clean) + +(target all + (deps "hello")) + +(target "hello" + (deps "hello.c") + (run `(,CC ,@CFLAGS -o $target $<))) + +(target clean + (rm-f "hello") + (rm-rf "compiled") + (cleanup "scrbl" '("**.html" "**.js" "**.css"))) +] + +Loading the file only registers these targets; it does not build @tt{all} or +any other target. + +@section{Executing targets} + +@defform[(make name ...)]{ +Builds the supplied targets. A bare identifier is always interpreted as a +literal target name, so @racket[(make clean)] selects the target named +@racket['clean] and does not require quoting. + +With no arguments, @racket[(make)] builds the target selected by +@racket[default-target]. If no default target has been specified, the first +declared target is used. + +Multiple targets are allowed and are processed in the supplied order, for +example @racket[(make clean all)]. Dependencies that are shared by multiple +selected targets are built only once during that @racket[make] invocation. +} + +From the command line, load the makefile with @tt{-t} and evaluate a +@racket[make] form with @tt{-e}: + +@verbatim{ +racket -t Makefile.rkt -e "(make)" +racket -t Makefile.rkt -e "(make all)" +racket -t Makefile.rkt -e "(make clean)" +racket -t Makefile.rkt -e "(make clean all)" +} + +Loading a makefile without @tt{-e} executes no target: + +@verbatim{ +racket -t Makefile.rkt +} + +In DrRacket, open the makefile and press @bold{Run}. This loads and registers +the targets. Then execute targets in the Interactions window: + +@verbatim{ +> (make) +> (make clean) +> (make all) +> (make clean all) +} + +After editing the makefile, reload its target definitions in the same Racket +process with: + +@verbatim{ +> (refresh-makefile) +> (make all) +} + +@defproc[(refresh-makefile) void?]{ +Reloads the most recently loaded @tt{racket-makefile} source file and registers +its targets again without restarting the Racket process. This works for both +@tt{#lang racket-makefile} and ordinary @tt{#lang racket} makefiles that require +@tt{main.rkt}. Modules required by the makefile that are already instantiated +are reused. This is useful for makefiles that require expensive libraries. +} + +@section{Targets and dependencies} + +@defform[(target name (deps dependency ...) body ...)]{ +Defines a target. @racket[name] can be a symbol, string, path, or a Racket +expression producing one of those values. Each @racket[dependency] can also +produce a list; dependency lists are recursively flattened. + +An unbound identifier is treated as a literal symbol. Consequently, +@racket[(target clean ...)] defines the target @racket['clean], while a bound +identifier can be used to generate targets from ordinary Racket code. If a +literal target name happens to be bound by Racket, quote it explicitly, for +example @racket[(target 'compile ...)]. + +The body is not evaluated when the target is declared. It is saved as the +target recipe and evaluated only when the target must be rebuilt. +} + +@defform[(deps dependency ...)]{ +Specifies the dependencies of a target. Dependency expressions that produce +lists are automatically spliced into the dependency list. @racket[deps] is only +valid directly inside @racket[target]. +} + +@defform[(phony name ...)]{ +Marks targets as phony. A phony target is always executed when requested or +when reached as a dependency. +} + +@defform[(default-target name)]{ +Selects the target used by @racket[(make)] when no target is supplied. If no +default target is specified, the first declared target is used. +} + +A non-phony target is rebuilt when its output does not exist or when a +dependency is newer than the target. Registered target dependencies are built +first. A dependency that is neither a registered target nor an existing file +is an error. Dependency cycles are reported as errors. + +@section{Recipe context} + +Inside a target recipe the following identifiers are available: + +@defidform[$target]{The current target name.} + +@defidform[$deps]{A list containing all dependencies of the current target.} + +@defidform[$<]{The first dependency of the current target. An error is raised +when the target has no dependencies.} + +The same names can occur as symbols inside a quoted command passed to +@racket[run]. + +@section{Running commands} + +@defproc[(run [command list?]) void?]{ +Runs an external command directly, without an intermediate shell. The first +item is the executable and the remaining items are arguments. Symbols are +converted to strings. Nested lists are flattened, which makes Racket lists of +flags convenient to use. + +The symbols @racket['$target], @racket['$deps], and @racket['$<] are expanded +from the current recipe. @racket['$deps] is spliced into the command. + +A non-zero command result raises an error. +} + +For example: + +@racketblock[ +(target "hello" + (deps "hello.c") + (run '(cc -Wall -O2 -o $target $<))) +] + +Ordinary Racket values can be inserted with quasiquote: + +@racketblock[ +(define CC 'cc) +(define CFLAGS '(-Wall -O2)) + +(target "hello" + (deps "hello.c") + (run `(,CC ,@CFLAGS -o $target $<))) +] + +@section{Running Racket tools} + +@defproc[(raco [command list?]) void?]{ +Runs a @tt{raco} command using the @tt{raco} executable that belongs to the +Racket installation currently running the makefile. The helper first checks +the console binary directory reported by the current Racket installation, then +the directory containing the current @tt{racket} executable, and only then +falls back to @tt{PATH}. This makes the helper useful on Windows installations +where @tt{raco.exe} is installed next to @tt{racket.exe} but is not on +@tt{%PATH%}. + +The command syntax is the same as for @racket[run], except that the executable +is supplied automatically. Symbols, strings, paths and numbers are converted +to command-line arguments, nested lists are flattened, and the recipe values +@racket['$target], @racket['$deps], and @racket['$<] are supported. A non-zero +result raises an error. +} + +For example: + +@racketblock[ +(phony setup test) + +(target setup + (raco '(setup racket-makefile))) + +(target test + (raco '(test -p racket-makefile))) +] + +@section{Cleanup helpers} + +@defproc[(rm-f [path path-string?] ...) void?]{ +Removes files when they exist. Missing files are ignored. Directories are not +removed; use @racket[rm-rf] for those. +} + +@defproc[(rm-rf [path path-string?] ...) void?]{ +Removes files or directory trees recursively. Missing paths are ignored. +} + +@defproc[(cleanup [directory path-string?] + [patterns list?]) + void?]{ +Removes files below @racket[directory] that match the supplied Racket glob +patterns. Directories themselves are left in place. A pattern containing +@tt{**} searches recursively. +} + +For example: + +@racketblock[ +(target clean + (rm-rf "compiled") + (cleanup "scrbl" + '("**.html" + "**.js" + "**.css"))) +] + +@section{Regexp directory helpers} + +@defproc[(list-dir/files [directory path-string?] + [regexp regexp?] + [#:recursive recursive any/c #f]) + list?]{ +Returns entries below @racket[directory] whose file or directory name matches +@racket[regexp]. The regexp is applied to @racket[(file-name-from-path path)], +not to the complete path. By default only the direct contents of +@racket[directory] are inspected. With @racket[#:recursive #t], the complete +tree is walked using @racket[in-directory], and both files and directories can +be returned. + +For a non-recursive listing, complete paths are built from @racket[directory], +so the result can be passed directly to file operations such as +@racket[rm-f] and @racket[rm-rf]. +} + +@defproc[(list-files [directory path-string?] + [regexp regexp?] + [#:recursive recursive any/c #f]) + list?]{ +Like @racket[list-dir/files], but keeps only paths for which +@racket[file-exists?] is true. +} + +@defproc[(list-dirs [directory path-string?] + [regexp regexp?] + [#:recursive recursive any/c #f]) + list?]{ +Like @racket[list-dir/files], but keeps only paths for which +@racket[directory-exists?] is true. +} + +These helpers make regexp-based cleanup concise. For example: + +@racketblock[ +(default-target all) +(phony all clean) + +(target all + (displayln "use (make clean)")) + +(target clean + (display "cleaning up...") + (apply rm-rf + (list-dirs "." #px"compiled$" #:recursive #t)) + (apply rm-f + (list-files "." #px"(?i:([.]bak|~)$)" #:recursive #t)) + (displayln "done.")) +] + +The second regular expression is case-insensitive and matches names ending in +@tt{.bak} or @tt{~}. + +@section{Using ordinary Racket} + +No separate make programming language is introduced. Definitions, functions, +loops, conditionals, modules, and Racket libraries remain available. For +example, targets can be generated in a loop: + +@racketblock[ +(define sources '("foo.c" "bar.c" "baz.c")) + +(define objects + (for/list ([src sources]) + (define obj (path-replace-extension src #".o")) + (target obj + (deps src) + (run `(cc -c $< -o $target))) + obj)) + +(target all + (deps objects)) +] + +The expression @racket[objects] evaluates to a list of dependencies. Dependency +lists are recursively flattened by @tt{racket-makefile}. + +@section{Package example} + +The optional @hyperlink["https://docs.racket-lang.org/package-zipper/index.html"]{@tt{package-zipper}} +package combines naturally with @tt{racket-makefile}: + +@racketblock[ +(require package-zipper) + +(default-target package) +(phony clean package) + +(target clean + (rm-rf "compiled") + (cleanup "scrbl" '("**.html" "**.js" "**.css"))) + +(target package + (zip-package)) +] + +The package target can then be executed explicitly, for example: + +@verbatim{ +racket -t Makefile.rkt -e "(make package)" +} diff --git a/scrbl/racket-makefile.scrbl b/scrbl/racket-makefile.scrbl index 58d08dd..0e9e6e4 100644 --- a/scrbl/racket-makefile.scrbl +++ b/scrbl/racket-makefile.scrbl @@ -22,17 +22,16 @@ any recipe automatically. Builds are started explicitly with @racket[make]. @section{Installation} -Install the package from a local checkout with: +@tt{racket-makefile} is available through the +@hyperlink["https://pkgs.racket-lang.org/"]{Racket Package Catalog}. + +Install it with: @verbatim{ -raco pkg install +raco pkg install racket-makefile } -For a versioned archive, specify the package name explicitly: - -@verbatim{ -raco pkg install --name racket-makefile racket-makefile-0.1.8.zip -} +or using the @tt{DrRacket} package manager. After installation a makefile can start with: @@ -40,6 +39,8 @@ After installation a makefile can start with: #lang racket-makefile } +If you want to see some example of a @tt{Makefile.rkt}}, you can look in the module code of @tt{racket-makefile}. + @section{A first makefile} For example: