Removed rash integration again and backported raco call.
This commit is contained in:
@@ -1,64 +1,35 @@
|
|||||||
0.1.8
|
0.3.6
|
||||||
|
- Restore raco as a small pure-Racket helper in the default racket-makefile API.
|
||||||
|
- Remove the Rash adapter and Rash dependencies from racket-makefile.
|
||||||
|
- Move Rash integration to the separate rash-makefile package.
|
||||||
|
|
||||||
Fixed refresh-makefile for ordinary #lang racket makefiles that require main.rkt
|
0.3.4
|
||||||
directly. Target declarations now remember their source makefile, and refresh
|
- Remove raco from the racket-makefile API; raco is now owned by rash-coreutils.
|
||||||
explicitly resets the registered target state before loading the edited file.
|
- Make the project Makefile require rash-coreutils explicitly.
|
||||||
Removed targets therefore disappear after refresh as expected.
|
|
||||||
|
|
||||||
0.1.7
|
0.3.3
|
||||||
|
- Move the raco implementation to rash-coreutils and re-export it for compatibility.
|
||||||
|
- Add rash-coreutils as a dependency.
|
||||||
|
|
||||||
Added refresh-makefile for reloading the current Makefile.rkt from an existing
|
racket-makefile 0.3.0
|
||||||
Racket/DrRacket interaction session. The makefile source path is kept in a
|
|
||||||
simple module-level variable, so expensive unchanged required modules can stay
|
|
||||||
loaded while makefile target definitions are refreshed.
|
|
||||||
|
|
||||||
0.1.6
|
Breaking redesign of the makefile model.
|
||||||
|
|
||||||
Fixed the raco helper test for the Racket package build service: raco help
|
The separate #lang racket-makefile language has been removed. Use #lang racket with
|
||||||
legitimately writes usage text to stderr, while raco test --drdr treats any
|
(require racket-makefile), or #lang rash with (require racket-makefile/rash).
|
||||||
stderr output as a test failure. The test now captures child stdout/stderr.
|
|
||||||
Changed list-dir/files so its regexp matches only the entry name returned by
|
|
||||||
file-name-from-path, instead of the complete path.
|
|
||||||
|
|
||||||
0.1.5
|
Added the (makefile prefix ...) form. Target, phony, and default-target declarations
|
||||||
|
are clauses inside makefile and are no longer valid as standalone declarations.
|
||||||
|
|
||||||
Added raco helper for invoking Racket tools from makefile recipes.
|
Each static target clause defines a real named procedure. For example
|
||||||
The helper prefers the raco executable from the current Racket installation,
|
(makefile wiki (target status ...)) defines makefile-target-wiki-status.
|
||||||
then the directory of the current racket executable, and uses PATH only as a
|
|
||||||
fallback.
|
|
||||||
Added documentation and tests for raco command execution.
|
|
||||||
|
|
||||||
0.1.4
|
Added a global prefix-aware target procedure registry. make resolves a target in the
|
||||||
|
active prefix, performs dependency/timestamp processing, and invokes the registered
|
||||||
|
procedure.
|
||||||
|
|
||||||
Replaced files-for-regexp, list-dir-re, list-dir-re-r, and filter-dirs with
|
The last evaluated makefile form sets current-makefile-prefix. Multiple prefixes can
|
||||||
list-dir/files, list-files, and list-dirs.
|
remain registered at the same time.
|
||||||
All three helpers support optional #:recursive #t traversal.
|
|
||||||
Non-recursive results use complete paths so they can be passed directly to
|
|
||||||
rm-f and rm-rf.
|
|
||||||
Updated tests, examples, README, and Scribble documentation.
|
|
||||||
|
|
||||||
0.1.3
|
racket-makefile/rash continues to keep make as a normal Racket procedure while adding
|
||||||
|
Rash line syntax such as `make all`.
|
||||||
Added recursive and non-recursive regexp directory helpers:
|
|
||||||
files-for-regexp, list-dir-re, list-dir-re-r, and filter-dirs.
|
|
||||||
Added documentation and an example for regexp-based cleanup with rm-f/rm-rf.
|
|
||||||
|
|
||||||
0.1.2
|
|
||||||
|
|
||||||
Makefile loading no longer starts a build automatically.
|
|
||||||
Added explicit make form: (make), (make target), and (make target ...).
|
|
||||||
Command-line use is now: racket -t Makefile.rkt -e "(make target)".
|
|
||||||
DrRacket can load the makefile with Run and execute make forms interactively.
|
|
||||||
Bare identifiers in make are always interpreted as literal target names.
|
|
||||||
Expanded documentation and examples for command-line and DrRacket use.
|
|
||||||
|
|
||||||
0.1.1
|
|
||||||
|
|
||||||
Initial racket-makefile language implementation.
|
|
||||||
|
|
||||||
Targets and dependencies with timestamp based rebuilding.
|
|
||||||
Phony and default targets.
|
|
||||||
Direct external command execution with run.
|
|
||||||
Recipe values $target, $deps and $<.
|
|
||||||
Cleanup helpers rm-f, rm-rf and cleanup.
|
|
||||||
Scribble documentation and example Makefile.
|
|
||||||
|
|||||||
@@ -1,60 +1,70 @@
|
|||||||
#lang racket
|
#lang racket/base
|
||||||
|
|
||||||
(require "main.rkt")
|
(require "main.rkt")
|
||||||
|
|
||||||
(target all
|
(makefile racket-makefile
|
||||||
(displayln "use (make clean) or (make package) or (make commit/push")
|
(default-target all)
|
||||||
)
|
(phony all commit push status pull clean package doc showdoc refresh)
|
||||||
|
|
||||||
(target commit
|
(target all
|
||||||
(let ((st (git 'status)))
|
(displayln "use (make 'clean), (make 'package), or (make 'commit)"))
|
||||||
|
|
||||||
|
(target commit
|
||||||
|
(let ([st (git 'status)])
|
||||||
(displayln st)
|
(displayln st)
|
||||||
(when (> (length st) 0)
|
(when (> (length st) 0)
|
||||||
(git 'add '-A)
|
(git 'add '-A)
|
||||||
(git 'commit)
|
(git 'commit)
|
||||||
(git 'push))))
|
(git 'push))))
|
||||||
|
|
||||||
(target push
|
(target push
|
||||||
(git 'push))
|
(git 'push))
|
||||||
|
|
||||||
(target status
|
(target status
|
||||||
(displayln "git status:")
|
(displayln "git status:")
|
||||||
(for-each (λ (e)
|
(for-each
|
||||||
|
(λ (e)
|
||||||
(displayln
|
(displayln
|
||||||
(format "~a: ~a (remote ~a)" (caddr e) (cadr e) (car e))))
|
(format "~a\t:\t~a (remote ~a)" (caddr e) (cadr e) (car e))))
|
||||||
(git 'status))
|
(git 'status)))
|
||||||
)
|
|
||||||
|
|
||||||
(target pull
|
(target pull
|
||||||
(git 'pull))
|
(git 'pull))
|
||||||
|
|
||||||
(target clean
|
(target clean
|
||||||
(for-each (λ (f) (displayln f) (rm-f f)) (list-files "." #px"([.]bak|~)$" #:recursive #t))
|
|
||||||
(for-each (λ (d) (displayln d) (rm-rf d)) (list-dirs "." #px"(compiled|doc|docs)$" #:recursive #t))
|
|
||||||
(for-each (λ (f) (displayln f) (rm-f f)) (list-files "scrbl" #px"[.](css|js|html)$"))
|
|
||||||
)
|
|
||||||
|
|
||||||
(target package
|
(for-each (λ (f) (displayln f) (rm-f f))
|
||||||
|
(list-files "." #px"([.]bak|~)$" #:recursive #t))
|
||||||
|
(for-each (λ (d) (displayln d) (rm-rf d))
|
||||||
|
(list-dirs "." #px"(compiled|doc|docs)$" #:recursive #t))
|
||||||
|
(for-each (λ (f) (displayln f) (rm-f f))
|
||||||
|
(list-files "scrbl" #px"[.](css|js|html)$")))
|
||||||
|
|
||||||
|
(target version
|
||||||
|
(displayln (format "Next version: ~a" (git 'next-version))))
|
||||||
|
|
||||||
|
(target package
|
||||||
(deps clean)
|
(deps clean)
|
||||||
(zip-package))
|
(zip-package))
|
||||||
|
|
||||||
(target "docs/racket-makefile.html"
|
(target zip
|
||||||
|
(deps package))
|
||||||
|
|
||||||
|
(target "docs/racket-makefile.html"
|
||||||
(deps "scrbl/racket-makefile.scrbl")
|
(deps "scrbl/racket-makefile.scrbl")
|
||||||
(unless (directory-exists? "docs")
|
(unless (directory-exists? "docs")
|
||||||
(make-directory "docs"))
|
(make-directory "docs"))
|
||||||
(raco '(scribble --html +m --dest "docs" scrbl/racket-makefile.scrbl)))
|
(raco '(scribble --html +m --dest "docs" scrbl/racket-makefile.scrbl)))
|
||||||
|
|
||||||
(target doc
|
(target doc
|
||||||
(deps "docs/racket-makefile.html")
|
(deps "docs/racket-makefile.html")
|
||||||
(displayln "Documentation built")
|
(displayln "Documentation built"))
|
||||||
)
|
|
||||||
|
|
||||||
(target showdoc
|
(target showdoc
|
||||||
(deps doc)
|
(deps doc)
|
||||||
(send-url/file "docs/racket-makefile.html"))
|
(send-url/file "docs/racket-makefile.html"))
|
||||||
|
|
||||||
(target refresh
|
(target refresh
|
||||||
(displayln "Refreshing makefile")
|
(displayln "Refreshing makefile")
|
||||||
(refresh-makefile)
|
(refresh-makefile)
|
||||||
(displayln "done.")
|
(displayln "done.")))
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,176 +1,135 @@
|
|||||||
# racket-makefile
|
# racket-makefile
|
||||||
|
|
||||||
`racket-makefile` is a small `#lang` for make-style builds. It keeps ordinary
|
`racket-makefile` provides make-style dependency builds as ordinary Racket functionality.
|
||||||
Racket available and only adds targets, dependencies, timestamp based rebuilds,
|
`racket-makefile` is a Racket library and has no dependency on Rash. Rash integration is provided separately by the `rash-makefile` package.
|
||||||
phony targets, external command execution, Racket tool execution and a few cleanup helpers.
|
|
||||||
|
|
||||||
A makefile only defines its targets. Loading or running the file does not build
|
## Functional makefiles
|
||||||
anything by itself. Builds are started explicitly with `make`.
|
|
||||||
|
|
||||||
```racket
|
```racket
|
||||||
#lang racket-makefile
|
#lang racket
|
||||||
|
|
||||||
(define CC 'cc)
|
(require racket-makefile)
|
||||||
(define CFLAGS '(-Wall -O2))
|
|
||||||
|
|
||||||
(default-target all)
|
(makefile wiki
|
||||||
(phony all clean)
|
(default-target all)
|
||||||
|
(phony all clean status)
|
||||||
|
|
||||||
(target all
|
(target status
|
||||||
(deps "hello"))
|
(displayln "status"))
|
||||||
|
|
||||||
(target "hello"
|
(target clean
|
||||||
(deps "hello.c")
|
(rm-rf "compiled"))
|
||||||
(run `(,CC ,@CFLAGS -o $target $<)))
|
|
||||||
|
|
||||||
(target clean
|
(target all
|
||||||
(rm-f "hello")
|
(deps status)
|
||||||
(rm-rf "compiled")
|
(displayln "all")))
|
||||||
(cleanup "scrbl" '("**.html" "**.js" "**.css")))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
A `makefile` form defines real target procedures. The example above defines, among others:
|
||||||
|
|
||||||
Install a local checkout with:
|
|
||||||
|
|
||||||
```text
|
|
||||||
raco pkg install
|
|
||||||
```
|
|
||||||
|
|
||||||
For a versioned archive, give the package name explicitly:
|
|
||||||
|
|
||||||
```text
|
|
||||||
raco pkg install --name racket-makefile racket-makefile-0.1.8.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running targets
|
|
||||||
|
|
||||||
Load the makefile with `-t` and evaluate a `make` expression with `-e`:
|
|
||||||
|
|
||||||
```text
|
|
||||||
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)"
|
|
||||||
```
|
|
||||||
|
|
||||||
`(make)` uses the target selected with `default-target`. If no default target is
|
|
||||||
specified, the first declared target is used.
|
|
||||||
|
|
||||||
Loading the file alone only registers the targets:
|
|
||||||
|
|
||||||
```text
|
|
||||||
racket -t Makefile.rkt
|
|
||||||
```
|
|
||||||
|
|
||||||
No recipe is executed in that case.
|
|
||||||
|
|
||||||
In DrRacket, open the makefile and press **Run**. This also only loads and
|
|
||||||
registers the targets. Then use the Interactions window:
|
|
||||||
|
|
||||||
```racket
|
```racket
|
||||||
> (make)
|
makefile-target-wiki-status
|
||||||
> (make clean)
|
makefile-target-wiki-clean
|
||||||
> (make all)
|
makefile-target-wiki-all
|
||||||
> (make clean all)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Bare identifiers in `make` are target names, so no quote is needed. For
|
They are ordinary procedures:
|
||||||
example, `(make clean)` selects the target named `clean`.
|
|
||||||
|
|
||||||
After editing the makefile, use `refresh-makefile` in the same Interactions
|
|
||||||
window to reload the makefile without restarting the Racket process:
|
|
||||||
|
|
||||||
```racket
|
```racket
|
||||||
> (refresh-makefile)
|
(procedure? makefile-target-wiki-status)
|
||||||
> (make all)
|
(makefile-target-wiki-status)
|
||||||
```
|
```
|
||||||
|
|
||||||
This works both with `#lang racket-makefile` and with an ordinary `#lang racket`
|
`make` is also an ordinary procedure. Target names are explicit Racket values:
|
||||||
makefile that requires `main.rkt`. It is useful when the makefile requires modules
|
|
||||||
that are expensive to load. Unchanged required modules can remain instantiated
|
|
||||||
while the target definitions from the makefile are registered again.
|
|
||||||
|
|
||||||
## Running Racket tools with `raco`
|
|
||||||
|
|
||||||
Use `raco` to invoke a Racket tool from a target without depending on `%PATH%`
|
|
||||||
or `$PATH`:
|
|
||||||
|
|
||||||
```racket
|
```racket
|
||||||
(phony setup test)
|
(make)
|
||||||
|
(make 'status)
|
||||||
(target setup
|
(make 'clean 'all)
|
||||||
(raco '(setup racket-makefile)))
|
|
||||||
|
|
||||||
(target test
|
|
||||||
(raco '(test -p racket-makefile)))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`racket-makefile` first looks for `raco` in the console binary directory of
|
`make` resolves target names for the active makefile in the global `makefile-targets` registry. It handles dependencies and timestamp checks and invokes the registered target procedure when the target needs to run.
|
||||||
the Racket installation that is currently running the makefile. It then tries
|
|
||||||
the directory containing the current `racket` executable and uses `PATH` only
|
|
||||||
as a final fallback. On Windows this normally finds `raco.exe` next to
|
|
||||||
`racket.exe` even when the Racket directory is not in `%PATH%`.
|
|
||||||
|
|
||||||
The argument rules are the same as for `run`: symbols, strings, paths and
|
## Makefile prefixes
|
||||||
numbers are converted to command-line arguments, nested lists are flattened,
|
|
||||||
and `$target`, `$deps` and `$<` are available inside a recipe.
|
|
||||||
|
|
||||||
## Regexp-based cleanup
|
Every makefile has a prefix:
|
||||||
|
|
||||||
For cleanup rules that need more control than globs, `racket-makefile` also
|
|
||||||
provides small directory/regexp helpers. The results are ordinary path lists,
|
|
||||||
so they can be passed directly to `rm-f` and `rm-rf` with `apply`:
|
|
||||||
|
|
||||||
```racket
|
```racket
|
||||||
#lang racket-makefile
|
(makefile wiki
|
||||||
|
(target status ...))
|
||||||
|
|
||||||
(default-target all)
|
(makefile audio
|
||||||
(phony all clean)
|
(target status ...))
|
||||||
|
|
||||||
(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."))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`list-dir/files` returns matching files and directories. The regexp is matched
|
Targets with the same name can therefore coexist. Evaluating a `makefile` form makes that prefix current. Consequently the last makefile definition evaluated is the one used by an unqualified `make` call:
|
||||||
against the file or directory name itself, not against the complete path.
|
|
||||||
`list-files` and `list-dirs` restrict the result to files or directories. All
|
|
||||||
three scan one directory level by default; use `#:recursive #t` to walk the
|
|
||||||
complete tree. Results are returned as usable paths, so the lists can be fed
|
|
||||||
directly to `rm-f` or `rm-rf` with `apply`.
|
|
||||||
|
|
||||||
## Generated targets
|
|
||||||
|
|
||||||
Because the rest of the language is ordinary Racket, targets can be generated
|
|
||||||
with normal definitions and loops:
|
|
||||||
|
|
||||||
```racket
|
```racket
|
||||||
(define sources '("foo.c" "bar.c" "baz.c"))
|
(current-makefile-prefix)
|
||||||
|
;; 'audio
|
||||||
|
|
||||||
(define objects
|
(make 'status)
|
||||||
(for/list ([src sources])
|
;; invokes makefile-target-audio-status through the make engine
|
||||||
(define obj (path-replace-extension src #".o"))
|
|
||||||
(target obj
|
|
||||||
(deps src)
|
|
||||||
(run `(cc -c $< -o $target)))
|
|
||||||
obj))
|
|
||||||
|
|
||||||
(target all
|
|
||||||
(deps objects))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Dependency expressions may produce lists; they are recursively flattened.
|
`current-makefile-prefix` is a parameter, so another registered makefile can be selected explicitly:
|
||||||
|
|
||||||
The language adds `target`, `deps`, `phony`, `default-target`, `make`, `run`,
|
```racket
|
||||||
`raco`, `rm-f`, `rm-rf`, `cleanup`, `list-dir/files`, `list-files`, `list-dirs`,
|
(current-makefile-prefix 'wiki)
|
||||||
`$target`, `$deps` and `$<`. Everything else is ordinary Racket.
|
(make 'status)
|
||||||
|
```
|
||||||
|
|
||||||
See the installed `racket-makefile` Scribble documentation for the full API.
|
Re-evaluating a makefile with the same prefix replaces the registrations for that prefix.
|
||||||
|
|
||||||
|
## Rash integration
|
||||||
|
|
||||||
|
Rash integration is intentionally kept out of this package. Install and require the separate `rash-makefile` package when `make target` line syntax is desired.
|
||||||
|
|
||||||
|
## Targets and dependencies
|
||||||
|
|
||||||
|
A target is declared only inside `makefile`:
|
||||||
|
|
||||||
|
```racket
|
||||||
|
(makefile example
|
||||||
|
(target "program"
|
||||||
|
(deps "program.c")
|
||||||
|
(run '(cc -o program program.c))))
|
||||||
|
```
|
||||||
|
|
||||||
|
Target and makefile names are literal identifiers, strings, paths, or quoted symbols. Dependencies may be expressions and may return nested lists; the build engine flattens them.
|
||||||
|
|
||||||
|
`phony` and `default-target` are also makefile clauses:
|
||||||
|
|
||||||
|
```racket
|
||||||
|
(makefile example
|
||||||
|
(default-target all)
|
||||||
|
(phony all clean)
|
||||||
|
...)
|
||||||
|
```
|
||||||
|
|
||||||
|
The automatic recipe values remain available inside target procedures:
|
||||||
|
|
||||||
|
```racket
|
||||||
|
$target
|
||||||
|
$deps
|
||||||
|
$<
|
||||||
|
```
|
||||||
|
|
||||||
|
Because a target is a real procedure, calling the generated procedure directly runs its recipe directly. Calling it through `make` adds dependency traversal and timestamp-based rebuilding.
|
||||||
|
|
||||||
|
## Refreshing
|
||||||
|
|
||||||
|
A `makefile` form remembers the source module in which it occurs. `refresh-makefile` reloads that source without restarting the Racket process:
|
||||||
|
|
||||||
|
```racket
|
||||||
|
(refresh-makefile)
|
||||||
|
(make 'all)
|
||||||
|
```
|
||||||
|
|
||||||
|
In DrRacket, pressing **Run** is normally the simpler way to reevaluate a Rash or Racket makefile.
|
||||||
|
|
||||||
|
## Helpers
|
||||||
|
|
||||||
|
The functional helpers provided by `racket-makefile` are `run`, `raco`, `rm-f`, `rm-rf`, `cleanup`, `list-dir/files`, `list-files`, and `list-dirs`. `raco` locates the executable belonging to the active Racket installation before falling back to `PATH`.
|
||||||
|
|
||||||
|
`racket-makefile` also continues to re-export the package APIs it uses for build scripts, including `git-cli`, `package-zipper`, `net/sendurl`, and `racket/string`.
|
||||||
|
|||||||
+15
-22
@@ -1,37 +1,30 @@
|
|||||||
#lang racket-makefile
|
#lang racket
|
||||||
|
|
||||||
;; Load only:
|
(require racket-makefile)
|
||||||
;; racket -t Makefile.rkt
|
|
||||||
;;
|
;; Functional use: press Run in DrRacket and use (make 'all), (make 'clean),
|
||||||
;; Build the default target:
|
;; or call a generated target procedure directly.
|
||||||
;; racket -t Makefile.rkt -e "(make)"
|
|
||||||
;;
|
|
||||||
;; Clean and rebuild:
|
|
||||||
;; racket -t Makefile.rkt -e "(make clean all)"
|
|
||||||
;;
|
|
||||||
;; In DrRacket, press Run and then enter (make), (make clean), etc.
|
|
||||||
|
|
||||||
(define CC 'cc)
|
(define CC 'cc)
|
||||||
(define CFLAGS '(-Wall -O2))
|
(define CFLAGS '(-Wall -O2))
|
||||||
|
|
||||||
(default-target all)
|
(makefile hello
|
||||||
(phony all clean setup test)
|
(default-target all)
|
||||||
|
(phony all clean setup test)
|
||||||
|
|
||||||
(target all
|
(target all
|
||||||
(deps "hello"))
|
(deps "hello"))
|
||||||
|
|
||||||
(target "hello"
|
(target "hello"
|
||||||
(deps "hello.c")
|
(deps "hello.c")
|
||||||
(run `(,CC ,@CFLAGS -o $target $<)))
|
(run `(,CC ,@CFLAGS -o $target $<)))
|
||||||
|
|
||||||
(target clean
|
(target clean
|
||||||
(rm-f "hello")
|
(rm-f "hello")
|
||||||
(rm-rf "compiled")
|
(rm-rf "compiled"))
|
||||||
(cleanup "scrbl" '("**.html" "**.js" "**.css")))
|
|
||||||
|
|
||||||
|
(target setup
|
||||||
(target setup
|
|
||||||
(raco '(setup racket-makefile)))
|
(raco '(setup racket-makefile)))
|
||||||
|
|
||||||
(target test
|
(target test
|
||||||
(raco '(test -p racket-makefile)))
|
(raco '(test -p racket-makefile))))
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
#lang racket-makefile
|
#lang racket
|
||||||
|
|
||||||
(default-target all)
|
(require racket-makefile)
|
||||||
(phony all clean)
|
|
||||||
|
|
||||||
(target all
|
(makefile cleanup-example
|
||||||
(displayln "use (make clean)"))
|
(default-target all)
|
||||||
|
(phony all clean)
|
||||||
|
|
||||||
(target clean
|
(target all
|
||||||
|
(displayln "use (make 'clean)"))
|
||||||
|
|
||||||
|
(target clean
|
||||||
(display "cleaning up...")
|
(display "cleaning up...")
|
||||||
(apply rm-rf
|
(apply rm-rf
|
||||||
(list-dirs "." #px"compiled$" #:recursive #t))
|
(list-dirs "." #px"compiled$" #:recursive #t))
|
||||||
(apply rm-f
|
(apply rm-f
|
||||||
(list-files "." #px"(?i:([.]bak|~)$)" #:recursive #t))
|
(list-files "." #px"(?i:([.]bak|~)$)" #:recursive #t))
|
||||||
(displayln "done."))
|
(displayln "done.")))
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#lang info
|
#lang info
|
||||||
|
|
||||||
(define pkg-authors '(hnmdijkema))
|
(define pkg-authors '(hnmdijkema))
|
||||||
(define version "0.2")
|
(define version "0.3.6")
|
||||||
(define license 'MIT)
|
(define license 'MIT)
|
||||||
(define collection "racket-makefile")
|
(define collection "racket-makefile")
|
||||||
(define pkg-desc
|
(define pkg-desc
|
||||||
"A small Racket language for make-style dependency builds.")
|
"Functional make-style target procedures for Racket.")
|
||||||
|
|
||||||
(define scribblings
|
(define scribblings
|
||||||
'(("scrbl/racket-makefile.scrbl" () (library 0))))
|
'(("scrbl/racket-makefile.scrbl" () (library 0))))
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
#lang s-exp syntax/module-reader
|
|
||||||
|
|
||||||
racket-makefile
|
|
||||||
@@ -1,23 +1,25 @@
|
|||||||
#lang racket/base
|
#lang racket/base
|
||||||
|
|
||||||
(require (except-in racket #%module-begin)
|
(require racket
|
||||||
(only-in racket/base [#%module-begin racket-module-begin])
|
(for-syntax racket/base
|
||||||
(for-syntax racket/base)
|
racket/list
|
||||||
|
racket/string
|
||||||
|
syntax/parse)
|
||||||
"private/commands.rkt"
|
"private/commands.rkt"
|
||||||
"private/engine.rkt"
|
"private/engine.rkt"
|
||||||
git-cli
|
git-cli
|
||||||
package-zipper
|
package-zipper
|
||||||
net/sendurl
|
net/sendurl
|
||||||
racket/string
|
racket/string)
|
||||||
)
|
|
||||||
|
|
||||||
(provide (all-from-out racket)
|
(provide makefile
|
||||||
(rename-out [makefile-module-begin #%module-begin])
|
|
||||||
target
|
target
|
||||||
deps
|
deps
|
||||||
phony
|
phony
|
||||||
default-target
|
default-target
|
||||||
make
|
make
|
||||||
|
current-makefile-prefix
|
||||||
|
makefile-targets
|
||||||
refresh-makefile
|
refresh-makefile
|
||||||
run
|
run
|
||||||
raco
|
raco
|
||||||
@@ -33,148 +35,195 @@
|
|||||||
(all-from-out git-cli)
|
(all-from-out git-cli)
|
||||||
(all-from-out package-zipper)
|
(all-from-out package-zipper)
|
||||||
(all-from-out net/sendurl)
|
(all-from-out net/sendurl)
|
||||||
(all-from-out racket/string)
|
(all-from-out racket/string))
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
;; The makefile module currently loaded in this Racket process. Keeping only
|
|
||||||
;; its source path is enough: target state lives in the engine and is rebuilt
|
|
||||||
;; when the makefile module is evaluated again.
|
|
||||||
(define loaded-makefile #f)
|
(define loaded-makefile #f)
|
||||||
|
|
||||||
(define (remember-makefile! path)
|
(define (remember-makefile! path)
|
||||||
(set! loaded-makefile path)
|
(when path
|
||||||
|
(set! loaded-makefile path))
|
||||||
(void))
|
(void))
|
||||||
|
|
||||||
(define-for-syntax (remember-source-form stx)
|
|
||||||
(define source (syntax-source stx))
|
|
||||||
(if (path? source)
|
|
||||||
#`(remember-makefile! (string->path #,(path->string source)))
|
|
||||||
#'(void)))
|
|
||||||
|
|
||||||
(define (refresh-makefile)
|
(define (refresh-makefile)
|
||||||
(unless loaded-makefile
|
(unless loaded-makefile
|
||||||
(error 'refresh-makefile "no racket-makefile has been loaded"))
|
(error 'refresh-makefile "no makefile definition has been evaluated"))
|
||||||
|
|
||||||
(define source loaded-makefile)
|
(define source loaded-makefile)
|
||||||
(define directory (or (path-only source) (current-directory)))
|
(define directory (or (path-only source) (current-directory)))
|
||||||
;; Load the edited makefile as a fresh module in the same namespace. Its
|
|
||||||
;; required modules therefore reuse their existing module instances, while
|
|
||||||
;; the makefile body itself runs again and re-registers all targets.
|
|
||||||
(define refresh-source
|
(define refresh-source
|
||||||
(make-temporary-file "racket-makefile-refresh~a.rkt" #f directory))
|
(make-temporary-file "racket-makefile-refresh~a.rkt" #f directory))
|
||||||
|
|
||||||
(copy-file source refresh-source #t)
|
(copy-file source refresh-source #t)
|
||||||
;; With #lang racket, main.rkt is only required and our custom module-begin
|
(reset-makefiles!)
|
||||||
;; does not run. Reset explicitly so removed/renamed targets disappear too.
|
|
||||||
(reset-makefile!)
|
|
||||||
(dynamic-wind
|
(dynamic-wind
|
||||||
void
|
void
|
||||||
(lambda ()
|
(λ ()
|
||||||
(dynamic-require refresh-source #f))
|
(dynamic-require refresh-source #f))
|
||||||
(lambda ()
|
(λ ()
|
||||||
;; Loading the temporary copy also runs remember-makefile!. Keep the
|
|
||||||
;; original source as the file to use for the next refresh.
|
|
||||||
(set! loaded-makefile source)
|
(set! loaded-makefile source)
|
||||||
(delete-file refresh-source)))
|
(delete-file refresh-source)))
|
||||||
(void))
|
(void))
|
||||||
|
|
||||||
;; For target declarations we want a bound identifier to remain an ordinary
|
(define (make . names)
|
||||||
;; Racket expression. This makes generated targets such as (target obj ...)
|
(apply make-targets! names))
|
||||||
;; possible inside a for loop. An unbound identifier is a literal target name.
|
|
||||||
(define-for-syntax (literal-name-or-expression stx)
|
|
||||||
(if (and (identifier? stx)
|
|
||||||
(not (identifier-binding stx)))
|
|
||||||
(datum->syntax stx `(quote ,(syntax-e stx)) stx stx)
|
|
||||||
stx))
|
|
||||||
|
|
||||||
;; For interactive make invocations a bare identifier is always a literal
|
|
||||||
;; target name. Thus (make compile) means the target named "compile" even if
|
|
||||||
;; Racket happens to provide a binding named compile.
|
|
||||||
(define-for-syntax (literal-make-name stx)
|
|
||||||
(if (identifier? stx)
|
|
||||||
(datum->syntax stx `(quote ,(syntax-e stx)) stx stx)
|
|
||||||
stx))
|
|
||||||
|
|
||||||
(define-syntax (makefile-module-begin stx)
|
|
||||||
(syntax-case stx ()
|
|
||||||
[(_ form ...)
|
|
||||||
#'(racket-module-begin
|
|
||||||
(reset-makefile!)
|
|
||||||
(remember-makefile!
|
|
||||||
(variable-reference->module-source (#%variable-reference)))
|
|
||||||
form ...
|
|
||||||
;; Re-export make so that `racket -t Makefile.rkt -e "(make all)"`
|
|
||||||
;; imports the make form into the command-line evaluation namespace.
|
|
||||||
(provide make refresh-makefile quote #%top-interaction #%app #%datum #%top))]))
|
|
||||||
|
|
||||||
(define-syntax (deps stx)
|
|
||||||
(raise-syntax-error 'deps "only valid as the dependency clause of target" stx))
|
|
||||||
|
|
||||||
(define-syntax (target stx)
|
|
||||||
(syntax-case stx (deps)
|
|
||||||
[(_ name (deps dependency ...) body ...)
|
|
||||||
(with-syntax ([target-name (literal-name-or-expression #'name)]
|
|
||||||
[(target-dependency ...)
|
|
||||||
(map literal-name-or-expression
|
|
||||||
(syntax->list #'(dependency ...)))])
|
|
||||||
(with-syntax ([remember-source (remember-source-form stx)])
|
|
||||||
#'(begin
|
|
||||||
remember-source
|
|
||||||
(register-target!
|
|
||||||
target-name
|
|
||||||
(list target-dependency ...)
|
|
||||||
(lambda () body ... (void))))))]
|
|
||||||
[(_ name body ...)
|
|
||||||
(with-syntax ([target-name (literal-name-or-expression #'name)]
|
|
||||||
[remember-source (remember-source-form stx)])
|
|
||||||
#'(begin
|
|
||||||
remember-source
|
|
||||||
(register-target!
|
|
||||||
target-name
|
|
||||||
'()
|
|
||||||
(lambda () body ... (void)))))]))
|
|
||||||
|
|
||||||
(define-syntax (phony stx)
|
|
||||||
(syntax-case stx ()
|
|
||||||
[(_ name ...)
|
|
||||||
(with-syntax ([(target-name ...)
|
|
||||||
(map literal-name-or-expression
|
|
||||||
(syntax->list #'(name ...)))]
|
|
||||||
[remember-source (remember-source-form stx)])
|
|
||||||
#'(begin
|
|
||||||
remember-source
|
|
||||||
(mark-phony! target-name ...)))]))
|
|
||||||
|
|
||||||
(define-syntax (default-target stx)
|
|
||||||
(syntax-case stx ()
|
|
||||||
[(_ name)
|
|
||||||
(with-syntax ([target-name (literal-name-or-expression #'name)]
|
|
||||||
[remember-source (remember-source-form stx)])
|
|
||||||
#'(begin
|
|
||||||
remember-source
|
|
||||||
(set-default-target! target-name)))]))
|
|
||||||
|
|
||||||
(define-syntax (make stx)
|
|
||||||
(syntax-case stx ()
|
|
||||||
[(_ name ...)
|
|
||||||
(with-syntax ([(target-name ...)
|
|
||||||
(map literal-make-name
|
|
||||||
(syntax->list #'(name ...)))])
|
|
||||||
#'(make-targets! target-name ...))]))
|
|
||||||
|
|
||||||
(define-syntax $target
|
(define-syntax $target
|
||||||
(syntax-id-rules ()
|
(syntax-id-rules ()
|
||||||
[$target
|
[$target
|
||||||
(or (current-target)
|
(or (current-target)
|
||||||
(error '$target "$target is only available while a target recipe is running"))]))
|
(error '$target "$target is only available while a target procedure is running"))]))
|
||||||
|
|
||||||
(define-syntax $deps
|
(define-syntax $deps
|
||||||
(syntax-id-rules ()
|
(syntax-id-rules ()
|
||||||
[$deps
|
[$deps
|
||||||
(or (current-dependencies)
|
(or (current-dependencies)
|
||||||
(error '$deps "$deps is only available while a target recipe is running"))]))
|
(error '$deps "$deps is only available while a target procedure is running"))]))
|
||||||
|
|
||||||
(define-syntax $<
|
(define-syntax $<
|
||||||
(syntax-id-rules ()
|
(syntax-id-rules ()
|
||||||
[$<
|
[$<
|
||||||
(or (current-first-dependency)
|
(or (current-first-dependency)
|
||||||
(error '$< "$< is only available while a target recipe with dependencies is running"))]))
|
(error '$< "$< is only available while a target procedure with dependencies is running"))]))
|
||||||
|
|
||||||
|
(define-syntax (target stx)
|
||||||
|
(raise-syntax-error 'target "only valid inside makefile" stx))
|
||||||
|
|
||||||
|
(define-syntax (deps stx)
|
||||||
|
(raise-syntax-error 'deps "only valid inside a target clause in makefile" stx))
|
||||||
|
|
||||||
|
(define-syntax (phony stx)
|
||||||
|
(raise-syntax-error 'phony "only valid inside makefile" stx))
|
||||||
|
|
||||||
|
(define-syntax (default-target stx)
|
||||||
|
(raise-syntax-error 'default-target "only valid inside makefile" stx))
|
||||||
|
|
||||||
|
(begin-for-syntax
|
||||||
|
(struct target-spec (name-datum name-expression dependencies body source) #:transparent)
|
||||||
|
|
||||||
|
(define (literal-name-datum stx who)
|
||||||
|
(syntax-parse stx
|
||||||
|
[id:id
|
||||||
|
(syntax-e #'id)]
|
||||||
|
[s:str
|
||||||
|
(syntax-e #'s)]
|
||||||
|
[((~datum quote) value)
|
||||||
|
(define datum (syntax-e #'value))
|
||||||
|
(unless (or (symbol? datum) (string? datum) (path? datum))
|
||||||
|
(raise-syntax-error who "expected a symbol, string, or path target name" stx))
|
||||||
|
datum]
|
||||||
|
[_
|
||||||
|
(raise-syntax-error
|
||||||
|
who
|
||||||
|
"target and makefile names must be literal identifiers, strings, paths, or quoted symbols"
|
||||||
|
stx)]))
|
||||||
|
|
||||||
|
(define (literal-expression stx who)
|
||||||
|
(define datum (literal-name-datum stx who))
|
||||||
|
#`(quote #,datum))
|
||||||
|
|
||||||
|
(define (dependency-expression stx)
|
||||||
|
(if (and (identifier? stx)
|
||||||
|
(not (identifier-binding stx)))
|
||||||
|
#`(quote #,(syntax-e stx))
|
||||||
|
stx))
|
||||||
|
|
||||||
|
(define (procedure-identifier context prefix target)
|
||||||
|
(define name
|
||||||
|
(string->symbol
|
||||||
|
(format "makefile-target-~a-~a" prefix target)))
|
||||||
|
(datum->syntax context name context context))
|
||||||
|
|
||||||
|
(define (parse-target clause)
|
||||||
|
(syntax-parse clause
|
||||||
|
#:datum-literals (target deps)
|
||||||
|
[(target name (deps dependency ...) body ...)
|
||||||
|
(define datum (literal-name-datum #'name 'target))
|
||||||
|
(target-spec datum
|
||||||
|
(literal-expression #'name 'target)
|
||||||
|
(map dependency-expression
|
||||||
|
(syntax->list #'(dependency ...)))
|
||||||
|
(syntax->list #'(body ...))
|
||||||
|
clause)]
|
||||||
|
[(target name body ...)
|
||||||
|
(define datum (literal-name-datum #'name 'target))
|
||||||
|
(target-spec datum
|
||||||
|
(literal-expression #'name 'target)
|
||||||
|
'()
|
||||||
|
(syntax->list #'(body ...))
|
||||||
|
clause)]))
|
||||||
|
)
|
||||||
|
|
||||||
|
(define-syntax (makefile stx)
|
||||||
|
(syntax-parse stx
|
||||||
|
#:datum-literals (target phony default-target)
|
||||||
|
[(_ prefix clause ...)
|
||||||
|
(define prefix-datum (literal-name-datum #'prefix 'makefile))
|
||||||
|
(define prefix-expression #`(quote #,prefix-datum))
|
||||||
|
(define clauses (syntax->list #'(clause ...)))
|
||||||
|
|
||||||
|
(define targets '())
|
||||||
|
(define phony-forms '())
|
||||||
|
(define default-forms '())
|
||||||
|
|
||||||
|
(for ([clause (in-list clauses)])
|
||||||
|
(syntax-parse clause
|
||||||
|
#:datum-literals (target phony default-target)
|
||||||
|
[(target . _)
|
||||||
|
(set! targets (append targets (list (parse-target clause))))]
|
||||||
|
[(phony name ...)
|
||||||
|
(define names
|
||||||
|
(for/list ([name (in-list (syntax->list #'(name ...)))])
|
||||||
|
(literal-expression name 'phony)))
|
||||||
|
(set! phony-forms
|
||||||
|
(append phony-forms
|
||||||
|
(list #`(mark-phony! #,prefix-expression #,@names))))]
|
||||||
|
[(default-target name)
|
||||||
|
(define name-expression (literal-expression #'name 'default-target))
|
||||||
|
(set! default-forms
|
||||||
|
(append default-forms
|
||||||
|
(list #`(set-default-target!
|
||||||
|
#,prefix-expression
|
||||||
|
#,name-expression))))]
|
||||||
|
[_
|
||||||
|
(raise-syntax-error
|
||||||
|
'makefile
|
||||||
|
"expected target, phony, or default-target clause"
|
||||||
|
clause)]))
|
||||||
|
|
||||||
|
(define target-definitions
|
||||||
|
(for/list ([spec (in-list targets)])
|
||||||
|
(define proc-id
|
||||||
|
(procedure-identifier stx prefix-datum (target-spec-name-datum spec)))
|
||||||
|
(define name-expression (target-spec-name-expression spec))
|
||||||
|
(define dependencies (target-spec-dependencies spec))
|
||||||
|
(define body (target-spec-body spec))
|
||||||
|
#`(begin
|
||||||
|
(define (#,proc-id)
|
||||||
|
(call-target-procedure
|
||||||
|
#,prefix-expression
|
||||||
|
#,name-expression
|
||||||
|
(λ ()
|
||||||
|
#,@body
|
||||||
|
(void))))
|
||||||
|
(register-target!
|
||||||
|
#,prefix-expression
|
||||||
|
#,name-expression
|
||||||
|
(list #,@dependencies)
|
||||||
|
#,proc-id))))
|
||||||
|
|
||||||
|
(define source (syntax-source stx))
|
||||||
|
(define remember-source
|
||||||
|
(if (path? source)
|
||||||
|
#`(remember-makefile! (string->path #,(path->string source)))
|
||||||
|
#'(void)))
|
||||||
|
|
||||||
|
#`(begin
|
||||||
|
#,remember-source
|
||||||
|
(begin-makefile! #,prefix-expression)
|
||||||
|
#,@target-definitions
|
||||||
|
#,@phony-forms
|
||||||
|
#,@default-forms
|
||||||
|
;; The last makefile definition evaluated is the active makefile.
|
||||||
|
(current-makefile-prefix #,prefix-expression)
|
||||||
|
(void))]))
|
||||||
|
|||||||
@@ -75,25 +75,19 @@
|
|||||||
(and (file-exists? path) path))))
|
(and (file-exists? path) path))))
|
||||||
|
|
||||||
(define (find-raco-executable)
|
(define (find-raco-executable)
|
||||||
;; Prefer the console executable directory reported by the current Racket
|
|
||||||
;; installation. This does not depend on PATH and therefore also works for
|
|
||||||
;; typical Windows installations under Program Files.
|
|
||||||
(define console-bin
|
(define console-bin
|
||||||
(with-handlers ([exn:fail? (lambda (_) #f)])
|
(with-handlers ([exn:fail? (λ (_) #f)])
|
||||||
(find-console-bin-dir)))
|
(find-console-bin-dir)))
|
||||||
(define from-console-bin
|
(define from-console-bin
|
||||||
(existing-raco-in console-bin))
|
(existing-raco-in console-bin))
|
||||||
|
|
||||||
;; A useful fallback for portable/non-standard installations: raco normally
|
|
||||||
;; lives alongside the currently running racket executable.
|
|
||||||
(define exec-file
|
(define exec-file
|
||||||
(with-handlers ([exn:fail? (lambda (_) #f)])
|
(with-handlers ([exn:fail? (λ (_) #f)])
|
||||||
(find-system-path 'exec-file)))
|
(find-system-path 'exec-file)))
|
||||||
(define from-exec-dir
|
(define from-exec-dir
|
||||||
(and exec-file
|
(and exec-file
|
||||||
(existing-raco-in (path-only exec-file))))
|
(existing-raco-in (path-only exec-file))))
|
||||||
|
|
||||||
;; PATH is intentionally last; it might refer to a different Racket install.
|
|
||||||
(or from-console-bin
|
(or from-console-bin
|
||||||
from-exec-dir
|
from-exec-dir
|
||||||
(find-executable-path (raco-executable-name))
|
(find-executable-path (raco-executable-name))
|
||||||
@@ -103,7 +97,7 @@
|
|||||||
(unless (list? command)
|
(unless (list? command)
|
||||||
(raise-argument-error 'raco "list?" command))
|
(raise-argument-error 'raco "list?" command))
|
||||||
(define arguments
|
(define arguments
|
||||||
(append-map (lambda (item) (command-item->strings 'raco item)) command))
|
(append-map (λ (item) (command-item->strings 'raco item)) command))
|
||||||
(when (null? arguments)
|
(when (null? arguments)
|
||||||
(error 'raco "empty command"))
|
(error 'raco "empty command"))
|
||||||
(define executable (find-raco-executable))
|
(define executable (find-raco-executable))
|
||||||
|
|||||||
+140
-65
@@ -2,26 +2,44 @@
|
|||||||
|
|
||||||
(require racket/list)
|
(require racket/list)
|
||||||
|
|
||||||
(provide register-target!
|
(provide makefile-targets
|
||||||
|
current-makefile-prefix
|
||||||
|
begin-makefile!
|
||||||
|
register-target!
|
||||||
mark-phony!
|
mark-phony!
|
||||||
set-default-target!
|
set-default-target!
|
||||||
reset-makefile!
|
reset-makefiles!
|
||||||
|
call-target-procedure
|
||||||
make-targets!
|
make-targets!
|
||||||
current-target
|
current-target
|
||||||
current-dependencies
|
current-dependencies
|
||||||
current-first-dependency)
|
current-first-dependency)
|
||||||
|
|
||||||
(struct target-definition (name dependencies recipe) #:transparent)
|
;; Target procedures are registered globally per makefile prefix. The value is
|
||||||
|
;; deliberately the real target procedure; `make` ultimately resolves a target
|
||||||
(define targets (make-hash))
|
;; in this hash and calls that procedure.
|
||||||
|
(define makefile-targets (make-hash))
|
||||||
|
(define target-dependencies (make-hash))
|
||||||
(define phony-targets (make-hash))
|
(define phony-targets (make-hash))
|
||||||
(define target-order '())
|
(define target-order (make-hash))
|
||||||
(define default-target-name #f)
|
(define default-targets (make-hash))
|
||||||
|
|
||||||
|
;; The most recently evaluated makefile becomes the active makefile. Setting a
|
||||||
|
;; parameter without parameterize is persistent for the current thread, which
|
||||||
|
;; is exactly what the interactive DrRacket/Rash use needs.
|
||||||
|
(define current-makefile-prefix (make-parameter #f))
|
||||||
|
|
||||||
(define current-target (make-parameter #f))
|
(define current-target (make-parameter #f))
|
||||||
(define current-dependencies (make-parameter #f))
|
(define current-dependencies (make-parameter #f))
|
||||||
(define current-first-dependency (make-parameter #f))
|
(define current-first-dependency (make-parameter #f))
|
||||||
|
|
||||||
|
(define (prefix-key value)
|
||||||
|
(cond
|
||||||
|
[(symbol? value) (symbol->string value)]
|
||||||
|
[(path? value) (path->string value)]
|
||||||
|
[(string? value) value]
|
||||||
|
[else (raise-argument-error 'makefile-prefix "(or/c symbol? path-string?)" value)]))
|
||||||
|
|
||||||
(define (target-key value)
|
(define (target-key value)
|
||||||
(cond
|
(cond
|
||||||
[(symbol? value) (symbol->string value)]
|
[(symbol? value) (symbol->string value)]
|
||||||
@@ -29,79 +47,123 @@
|
|||||||
[(string? value) value]
|
[(string? value) value]
|
||||||
[else (raise-argument-error 'target "(or/c symbol? path-string?)" value)]))
|
[else (raise-argument-error 'target "(or/c symbol? path-string?)" value)]))
|
||||||
|
|
||||||
(define (reset-makefile!)
|
(define (registry-key prefix target)
|
||||||
(hash-clear! targets)
|
(cons (prefix-key prefix) (target-key target)))
|
||||||
(hash-clear! phony-targets)
|
|
||||||
(set! target-order '())
|
|
||||||
(set! default-target-name #f))
|
|
||||||
|
|
||||||
(define (target-keys value)
|
(define (target-keys value)
|
||||||
(if (list? value)
|
(if (list? value)
|
||||||
(append-map target-keys value)
|
(append-map target-keys value)
|
||||||
(list (target-key value))))
|
(list (target-key value))))
|
||||||
|
|
||||||
(define (register-target! name dependencies recipe)
|
(define (clear-prefix! prefix)
|
||||||
(define key (target-key name))
|
(define pkey (prefix-key prefix))
|
||||||
(unless (hash-has-key? targets key)
|
(for ([key (in-list (hash-keys makefile-targets))])
|
||||||
(set! target-order (append target-order (list key))))
|
(when (equal? (car key) pkey)
|
||||||
(hash-set! targets
|
(hash-remove! makefile-targets key)
|
||||||
key
|
(hash-remove! target-dependencies key)
|
||||||
(target-definition key
|
(hash-remove! phony-targets key)))
|
||||||
(append-map target-keys dependencies)
|
(hash-remove! target-order pkey)
|
||||||
recipe))
|
(hash-remove! default-targets pkey)
|
||||||
(void))
|
(void))
|
||||||
|
|
||||||
(define (mark-phony! . names)
|
(define (begin-makefile! prefix)
|
||||||
|
(clear-prefix! prefix)
|
||||||
|
(void))
|
||||||
|
|
||||||
|
(define (reset-makefiles!)
|
||||||
|
(hash-clear! makefile-targets)
|
||||||
|
(hash-clear! target-dependencies)
|
||||||
|
(hash-clear! phony-targets)
|
||||||
|
(hash-clear! target-order)
|
||||||
|
(hash-clear! default-targets)
|
||||||
|
(current-makefile-prefix #f)
|
||||||
|
(void))
|
||||||
|
|
||||||
|
(define (register-target! prefix name dependencies procedure)
|
||||||
|
(define pkey (prefix-key prefix))
|
||||||
|
(define tkey (target-key name))
|
||||||
|
(define key (cons pkey tkey))
|
||||||
|
|
||||||
|
(unless (hash-has-key? makefile-targets key)
|
||||||
|
(hash-set! target-order
|
||||||
|
pkey
|
||||||
|
(append (hash-ref target-order pkey '()) (list tkey))))
|
||||||
|
|
||||||
|
(hash-set! makefile-targets key procedure)
|
||||||
|
(hash-set! target-dependencies key (append-map target-keys dependencies))
|
||||||
|
(void))
|
||||||
|
|
||||||
|
(define (mark-phony! prefix . names)
|
||||||
(for* ([name (in-list names)]
|
(for* ([name (in-list names)]
|
||||||
[key (in-list (target-keys name))])
|
[tkey (in-list (target-keys name))])
|
||||||
(hash-set! phony-targets key #t))
|
(hash-set! phony-targets (cons (prefix-key prefix) tkey) #t))
|
||||||
(void))
|
(void))
|
||||||
|
|
||||||
(define (set-default-target! name)
|
(define (set-default-target! prefix name)
|
||||||
(set! default-target-name (target-key name))
|
(hash-set! default-targets (prefix-key prefix) (target-key name))
|
||||||
(void))
|
(void))
|
||||||
|
|
||||||
(define (phony? name)
|
(define (target-procedure prefix name)
|
||||||
(hash-ref phony-targets name #f))
|
(hash-ref makefile-targets
|
||||||
|
(registry-key prefix name)
|
||||||
|
(λ ()
|
||||||
|
(error 'racket-makefile
|
||||||
|
"unknown target ~a for makefile ~a"
|
||||||
|
name
|
||||||
|
prefix))))
|
||||||
|
|
||||||
|
(define (dependencies-for prefix name)
|
||||||
|
(hash-ref target-dependencies (registry-key prefix name) '()))
|
||||||
|
|
||||||
|
(define (call-target-procedure prefix name body)
|
||||||
|
(define dependencies (dependencies-for prefix name))
|
||||||
|
(parameterize ([current-target (target-key name)]
|
||||||
|
[current-dependencies dependencies]
|
||||||
|
[current-first-dependency
|
||||||
|
(if (null? dependencies) #f (car dependencies))])
|
||||||
|
(body))
|
||||||
|
(void))
|
||||||
|
|
||||||
|
(define (phony? prefix name)
|
||||||
|
(hash-ref phony-targets (registry-key prefix name) #f))
|
||||||
|
|
||||||
(define (path-modify-seconds path)
|
(define (path-modify-seconds path)
|
||||||
(file-or-directory-modify-seconds path #f (lambda () #f)))
|
(file-or-directory-modify-seconds path #f (λ () #f)))
|
||||||
|
|
||||||
(define (dependency-time name built-results)
|
(define (registered-target? prefix name)
|
||||||
|
(hash-has-key? makefile-targets (registry-key prefix name)))
|
||||||
|
|
||||||
|
(define (dependency-time prefix name built-results)
|
||||||
(cond
|
(cond
|
||||||
[(phony? name) +inf.0]
|
[(phony? prefix name) +inf.0]
|
||||||
[(hash-has-key? targets name)
|
[(registered-target? prefix name)
|
||||||
(define result (hash-ref built-results name #f))
|
(define result (hash-ref built-results (target-key name) #f))
|
||||||
(or (path-modify-seconds name)
|
(or (path-modify-seconds name)
|
||||||
(and result +inf.0)
|
(if result +inf.0 #f))]
|
||||||
#f)]
|
|
||||||
[else
|
[else
|
||||||
(or (path-modify-seconds name)
|
(or (path-modify-seconds name)
|
||||||
(error 'racket-makefile "dependency does not exist and has no target: ~a" name))]))
|
(error 'racket-makefile
|
||||||
|
"dependency does not exist and has no target: ~a"
|
||||||
|
name))]))
|
||||||
|
|
||||||
(define (needs-build? definition built-results)
|
(define (needs-build? prefix name dependencies built-results)
|
||||||
(define name (target-definition-name definition))
|
|
||||||
(cond
|
(cond
|
||||||
[(phony? name) #t]
|
[(phony? prefix name) #t]
|
||||||
[else
|
[else
|
||||||
(define target-time (path-modify-seconds name))
|
(define target-time (path-modify-seconds name))
|
||||||
(cond
|
(cond
|
||||||
[(not target-time) #t]
|
[(not target-time) #t]
|
||||||
[else
|
[else
|
||||||
(for/or ([dependency (in-list (target-definition-dependencies definition))])
|
(for/or ([dependency (in-list dependencies)])
|
||||||
(define dep-time (dependency-time dependency built-results))
|
(define dep-time (dependency-time prefix dependency built-results))
|
||||||
(and dep-time (> dep-time target-time)))])]))
|
(and dep-time (> dep-time target-time)))])]))
|
||||||
|
|
||||||
(define (execute-target! definition)
|
(define (execute-target! prefix name)
|
||||||
(define name (target-definition-name definition))
|
(printf "racket-makefile[~a]: ~a\n" prefix (target-key name))
|
||||||
(define dependencies (target-definition-dependencies definition))
|
((target-procedure prefix name))
|
||||||
(printf "racket-makefile: ~a\n" name)
|
(void))
|
||||||
(parameterize ([current-target name]
|
|
||||||
[current-dependencies dependencies]
|
|
||||||
[current-first-dependency (and (pair? dependencies) (car dependencies))])
|
|
||||||
((target-definition-recipe definition))))
|
|
||||||
|
|
||||||
(define (build-target! name built-results visiting)
|
(define (build-target! prefix name built-results visiting)
|
||||||
(define key (target-key name))
|
(define key (target-key name))
|
||||||
(cond
|
(cond
|
||||||
[(hash-has-key? built-results key)
|
[(hash-has-key? built-results key)
|
||||||
@@ -109,40 +171,53 @@
|
|||||||
[(hash-has-key? visiting key)
|
[(hash-has-key? visiting key)
|
||||||
(error 'racket-makefile "dependency cycle involving target: ~a" key)]
|
(error 'racket-makefile "dependency cycle involving target: ~a" key)]
|
||||||
[else
|
[else
|
||||||
(define definition
|
(unless (registered-target? prefix key)
|
||||||
(hash-ref targets key
|
(error 'racket-makefile "unknown target ~a for makefile ~a" key prefix))
|
||||||
(lambda ()
|
|
||||||
(error 'racket-makefile "unknown target: ~a" key))))
|
|
||||||
(hash-set! visiting key #t)
|
(hash-set! visiting key #t)
|
||||||
(for ([dependency (in-list (target-definition-dependencies definition))])
|
(define dependencies (dependencies-for prefix key))
|
||||||
(if (hash-has-key? targets dependency)
|
|
||||||
(build-target! dependency built-results visiting)
|
(for ([dependency (in-list dependencies)])
|
||||||
|
(if (registered-target? prefix dependency)
|
||||||
|
(build-target! prefix dependency built-results visiting)
|
||||||
(unless (path-modify-seconds dependency)
|
(unless (path-modify-seconds dependency)
|
||||||
(error 'racket-makefile
|
(error 'racket-makefile
|
||||||
"dependency does not exist and has no target: ~a"
|
"dependency does not exist and has no target: ~a"
|
||||||
dependency))))
|
dependency))))
|
||||||
(define rebuilt? (needs-build? definition built-results))
|
|
||||||
|
(define rebuilt? (needs-build? prefix key dependencies built-results))
|
||||||
(when rebuilt?
|
(when rebuilt?
|
||||||
(execute-target! definition))
|
(execute-target! prefix key))
|
||||||
|
|
||||||
(hash-remove! visiting key)
|
(hash-remove! visiting key)
|
||||||
(hash-set! built-results key rebuilt?)
|
(hash-set! built-results key rebuilt?)
|
||||||
rebuilt?]))
|
rebuilt?]))
|
||||||
|
|
||||||
(define (default-targets)
|
(define (default-target-list prefix)
|
||||||
|
(define pkey (prefix-key prefix))
|
||||||
(cond
|
(cond
|
||||||
[default-target-name (list default-target-name)]
|
[(hash-has-key? default-targets pkey)
|
||||||
[(pair? target-order) (list (car target-order))]
|
(list (hash-ref default-targets pkey))]
|
||||||
|
[(pair? (hash-ref target-order pkey '()))
|
||||||
|
(list (car (hash-ref target-order pkey)))]
|
||||||
[else '()]))
|
[else '()]))
|
||||||
|
|
||||||
(define (make-targets! . names)
|
(define (make-targets! . names)
|
||||||
|
(define prefix (current-makefile-prefix))
|
||||||
|
(unless prefix
|
||||||
|
(error 'make "no current makefile prefix; evaluate a makefile definition first"))
|
||||||
|
|
||||||
(define selected
|
(define selected
|
||||||
(if (null? names)
|
(if (null? names)
|
||||||
(default-targets)
|
(default-target-list prefix)
|
||||||
(append-map target-keys names)))
|
(append-map target-keys names)))
|
||||||
|
|
||||||
(when (null? selected)
|
(when (null? selected)
|
||||||
(error 'racket-makefile "no target defined"))
|
(error 'make "makefile ~a does not define a target" prefix))
|
||||||
|
|
||||||
(define built-results (make-hash))
|
(define built-results (make-hash))
|
||||||
(define visiting (make-hash))
|
(define visiting (make-hash))
|
||||||
|
|
||||||
(for ([name (in-list selected)])
|
(for ([name (in-list selected)])
|
||||||
(build-target! name built-results visiting))
|
(build-target! prefix name built-results visiting))
|
||||||
(void))
|
(void))
|
||||||
|
|||||||
@@ -1,375 +0,0 @@
|
|||||||
#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)"
|
|
||||||
}
|
|
||||||
+94
-294
@@ -1,376 +1,176 @@
|
|||||||
#lang scribble/manual
|
#lang scribble/manual
|
||||||
|
|
||||||
@(require (for-label racket/base
|
@(require (for-label racket
|
||||||
racket/file
|
racket/file
|
||||||
(only-in racket-makefile
|
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}
|
@title{racket-makefile}
|
||||||
@author{Hans Dijkema}
|
@author["Hans Dijkema / hans@dijkewijk.nl"]
|
||||||
|
|
||||||
@defmodulelang[racket-makefile]
|
@defmodule[racket-makefile]
|
||||||
|
|
||||||
@tt{racket-makefile} is a small make-style language built on Racket. It adds
|
@tt{racket-makefile} provides make-style dependency builds as ordinary Racket functionality. The package has no Rash dependency. Rash integration is provided by the separate @tt{rash-makefile} package.
|
||||||
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
|
@section{A functional makefile}
|
||||||
any recipe automatically. Builds are started explicitly with @racket[make].
|
|
||||||
|
|
||||||
@section{Installation}
|
A makefile is declared with one prefix and a set of target clauses:
|
||||||
|
|
||||||
@tt{racket-makefile} is available through the
|
|
||||||
@hyperlink["https://pkgs.racket-lang.org/"]{Racket Package Catalog}.
|
|
||||||
|
|
||||||
Install it with:
|
|
||||||
|
|
||||||
@verbatim{
|
|
||||||
raco pkg install racket-makefile
|
|
||||||
}
|
|
||||||
|
|
||||||
or using the @tt{DrRacket} package manager.
|
|
||||||
|
|
||||||
After installation a makefile can start with:
|
|
||||||
|
|
||||||
@verbatim{
|
|
||||||
#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:
|
|
||||||
|
|
||||||
@racketblock[
|
@racketblock[
|
||||||
(define CC 'cc)
|
(require racket-makefile)
|
||||||
(define CFLAGS '(-Wall -O2))
|
|
||||||
|
|
||||||
(default-target all)
|
(makefile wiki
|
||||||
(phony all clean)
|
(default-target all)
|
||||||
|
(phony status clean all)
|
||||||
|
|
||||||
(target all
|
(target status
|
||||||
(deps "hello"))
|
(displayln "status"))
|
||||||
|
|
||||||
(target "hello"
|
(target clean
|
||||||
(deps "hello.c")
|
(rm-rf "compiled"))
|
||||||
(run `(,CC ,@CFLAGS -o $target $<)))
|
|
||||||
|
|
||||||
(target clean
|
(target all
|
||||||
(rm-f "hello")
|
(deps status)
|
||||||
(rm-rf "compiled")
|
(displayln "all")))
|
||||||
(cleanup "scrbl" '("**.html" "**.js" "**.css")))
|
|
||||||
]
|
]
|
||||||
|
|
||||||
Loading the file only registers these targets; it does not build @tt{all} or
|
The @racket[makefile] form defines real Racket procedures. The example defines @racket[makefile-target-wiki-status], @racket[makefile-target-wiki-clean], and @racket[makefile-target-wiki-all]. They can be inspected or called like any other procedure.
|
||||||
any other target.
|
|
||||||
|
|
||||||
@section{Executing targets}
|
@racketblock[
|
||||||
|
(procedure? makefile-target-wiki-status)
|
||||||
|
(makefile-target-wiki-status)
|
||||||
|
]
|
||||||
|
|
||||||
@defform[(make name ...)]{
|
Calling a generated target procedure directly executes the recipe directly. Calling the target through @racket[make] adds dependency traversal and timestamp based rebuilding.
|
||||||
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
|
@section{Makefile definitions}
|
||||||
@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
|
@defform[(makefile prefix clause ...)]{
|
||||||
example @racket[(make clean all)]. Dependencies that are shared by multiple
|
Defines one prefixed makefile. The @racket[prefix] is a literal identifier, string, path, or quoted symbol. Each @racket[clause] is a @racket[target], @racket[phony], or @racket[default-target] clause.
|
||||||
selected targets are built only once during that @racket[make] invocation.
|
|
||||||
|
Before registering the new clauses, registrations for the same prefix are removed. After all clauses are registered, @racket[current-makefile-prefix] is set to the prefix. Consequently the last evaluated @racket[makefile] form becomes the active makefile.
|
||||||
|
|
||||||
|
A target named @racket[status] in a makefile with prefix @racket[wiki] defines the procedure @racket[makefile-target-wiki-status].
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ...)]{
|
@defform[(target name (deps dependency ...) body ...)]{
|
||||||
Defines a target. @racket[name] can be a symbol, string, path, or a Racket
|
Defines a named target procedure inside @racket[makefile]. The @racket[name] is a literal identifier, string, path, or quoted symbol. The target procedure executes @racket[body ...].
|
||||||
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,
|
Dependency expressions are evaluated when the makefile is registered. A dependency expression may produce nested lists; the build engine flattens them.
|
||||||
@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.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
A target without a @racket[deps] clause has no dependencies.
|
||||||
|
|
||||||
@defform[(deps dependency ...)]{
|
@defform[(deps dependency ...)]{
|
||||||
Specifies the dependencies of a target. Dependency expressions that produce
|
Specifies target dependencies. The form is valid only as the dependency clause of @racket[target].
|
||||||
lists are automatically spliced into the dependency list. @racket[deps] is only
|
|
||||||
valid directly inside @racket[target].
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@defform[(phony name ...)]{
|
@defform[(phony name ...)]{
|
||||||
Marks targets as phony. A phony target is always executed when requested or
|
Marks the named targets as phony for the surrounding makefile prefix. A phony target is always executed when requested or reached as a dependency.
|
||||||
when reached as a dependency.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@defform[(default-target name)]{
|
@defform[(default-target name)]{
|
||||||
Selects the target used by @racket[(make)] when no target is supplied. If no
|
Selects the target used by @racket[(make)] for the surrounding makefile prefix. If no default is specified, the first declared target is used.
|
||||||
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
|
The declaration forms @racket[target], @racket[deps], @racket[phony], and @racket[default-target] are not standalone declarations in version 0.3.0. They are clauses of @racket[makefile].
|
||||||
dependency is newer than the target. Registered target dependencies are built
|
|
||||||
first. A dependency that is neither a registered target nor an existing file
|
@section{Executing targets}
|
||||||
is an error. Dependency cycles are reported as errors.
|
|
||||||
|
@defproc[(make [name (or/c symbol? path-string?)] ...) void?]{
|
||||||
|
Builds the supplied targets in the makefile selected by @racket[current-makefile-prefix]. @racket[make] is an ordinary procedure, so symbolic target names are quoted explicitly.
|
||||||
|
|
||||||
|
With no arguments, the configured default target is used. If no explicit default exists, the first target of the active makefile is used. Multiple supplied targets are processed in order, and shared dependencies are built once during one @racket[make] call.
|
||||||
|
|
||||||
|
For a target that needs rebuilding, the engine looks up the target procedure in @racket[makefile-targets] and calls it.
|
||||||
|
}
|
||||||
|
|
||||||
|
@defthing[current-makefile-prefix parameter?]{
|
||||||
|
A parameter containing the active makefile prefix, or @racket[#f] before any makefile has been evaluated. Evaluating @racket[makefile] sets the parameter persistently to that makefile's prefix.
|
||||||
|
|
||||||
|
Another registered makefile can be selected explicitly:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(current-makefile-prefix 'wiki)
|
||||||
|
(make 'status)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@defthing[makefile-targets hash?]{
|
||||||
|
The global prefix-aware target registry. Registry values are the generated target procedures themselves. This binding is exposed mainly for inspection and tooling; normal target execution should use @racket[make].
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Rash integration}
|
||||||
|
|
||||||
|
Rash integration is intentionally provided by the separate @tt{rash-makefile} package so that @tt{racket-makefile} remains a pure Racket dependency.
|
||||||
|
|
||||||
@section{Recipe context}
|
@section{Recipe context}
|
||||||
|
|
||||||
Inside a target recipe the following identifiers are available:
|
@defidform[$target]{The current target name while a generated target procedure is running.}
|
||||||
|
|
||||||
@defidform[$target]{The current target name.}
|
@defidform[$deps]{The flattened list of dependencies of the current target.}
|
||||||
|
|
||||||
@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.}
|
||||||
|
|
||||||
@defidform[$<]{The first dependency of the current target. An error is raised
|
The generated procedure establishes this context even when called directly.
|
||||||
when the target has no dependencies.}
|
|
||||||
|
|
||||||
The same names can occur as symbols inside a quoted command passed to
|
@section{Dependency processing}
|
||||||
@racket[run].
|
|
||||||
|
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 in the same prefix nor an existing file is an error. Dependency cycles are reported as errors.
|
||||||
|
|
||||||
|
Multiple makefile prefixes can remain registered simultaneously. Dependency lookup stays within the prefix of the target being built.
|
||||||
|
|
||||||
|
@section{Refreshing definitions}
|
||||||
|
|
||||||
|
@defproc[(refresh-makefile) void?]{
|
||||||
|
Reloads the source module containing the most recently evaluated @racket[makefile] form. Existing makefile registrations are cleared before the source is evaluated again. Modules already instantiated in the current namespace are reused.
|
||||||
|
|
||||||
|
In DrRacket, pressing @bold{Run} is usually the simpler way to reevaluate a Racket or Rash makefile.
|
||||||
|
}
|
||||||
|
|
||||||
@section{Running commands}
|
@section{Running commands}
|
||||||
|
|
||||||
@defproc[(run [command list?]) void?]{
|
@defproc[(run [command list?]) void?]{
|
||||||
Runs an external command directly, without an intermediate shell. The first
|
Runs an external command directly without an intermediate shell. Symbols, strings, paths, numbers, and nested lists are converted to command-line arguments. A non-zero result raises an error.
|
||||||
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
|
The symbols @racket['$target], @racket['$deps], and @racket['$<] are expanded from the current target context when they occur in the command list.
|
||||||
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?]{
|
@defproc[(raco [command list?]) void?]{
|
||||||
Runs a @tt{raco} command using the @tt{raco} executable that belongs to the
|
Runs @tt{raco} from the active Racket installation. The command is supplied as a list, for example @racket[(raco '(setup racket-makefile))]. The helper first looks in the console executable directory of the current Racket installation, then next to the running Racket executable, and only then on @tt{PATH}. A non-zero result raises an error.
|
||||||
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}
|
@section{Cleanup helpers}
|
||||||
|
|
||||||
@defproc[(rm-f [path path-string?] ...) void?]{
|
@defproc[(rm-f [path path-string?] ...) void?]{
|
||||||
Removes files when they exist. Missing files are ignored. Directories are not
|
Removes files when they exist. Missing files are ignored. Directories are rejected.
|
||||||
removed; use @racket[rm-rf] for those.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@defproc[(rm-rf [path path-string?] ...) void?]{
|
@defproc[(rm-rf [path path-string?] ...) void?]{
|
||||||
Removes files or directory trees recursively. Missing paths are ignored.
|
Removes files or directory trees recursively. Missing paths are ignored.
|
||||||
}
|
}
|
||||||
|
|
||||||
@defproc[(cleanup [directory path-string?]
|
@defproc[(cleanup [directory path-string?] [patterns list?]) void?]{
|
||||||
[patterns list?])
|
Removes files below @racket[directory] that match the supplied Racket glob patterns.
|
||||||
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?]
|
@defproc[(list-dir/files [directory path-string?]
|
||||||
[regexp regexp?]
|
[regexp regexp?]
|
||||||
[#:recursive recursive any/c #f])
|
[#:recursive recursive any/c #f])
|
||||||
list?]{
|
list?]{
|
||||||
Returns entries below @racket[directory] whose file or directory name matches
|
Returns matching files and directories. The regular expression is applied to each file or directory name, not to the complete path.
|
||||||
@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?]
|
@defproc[(list-files [directory path-string?]
|
||||||
[regexp regexp?]
|
[regexp regexp?]
|
||||||
[#:recursive recursive any/c #f])
|
[#:recursive recursive any/c #f])
|
||||||
list?]{
|
list?]{
|
||||||
Like @racket[list-dir/files], but keeps only paths for which
|
Like @racket[list-dir/files], but keeps only files.
|
||||||
@racket[file-exists?] is true.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@defproc[(list-dirs [directory path-string?]
|
@defproc[(list-dirs [directory path-string?]
|
||||||
[regexp regexp?]
|
[regexp regexp?]
|
||||||
[#:recursive recursive any/c #f])
|
[#:recursive recursive any/c #f])
|
||||||
list?]{
|
list?]{
|
||||||
Like @racket[list-dir/files], but keeps only paths for which
|
Like @racket[list-dir/files], but keeps only directories.
|
||||||
@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)"
|
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-9
@@ -8,30 +8,34 @@
|
|||||||
|
|
||||||
(dynamic-wind
|
(dynamic-wind
|
||||||
void
|
void
|
||||||
(lambda ()
|
(λ ()
|
||||||
(parameterize ([current-directory tmp]
|
(parameterize ([current-directory tmp]
|
||||||
[current-command-line-arguments #()])
|
[current-command-line-arguments #()])
|
||||||
(call-with-output-file "input.txt"
|
(call-with-output-file "input.txt"
|
||||||
(lambda (out) (display "one" out)))
|
(λ (out) (display "one" out)))
|
||||||
|
|
||||||
(define build-count 0)
|
(define build-count 0)
|
||||||
|
|
||||||
(reset-makefile!)
|
(reset-makefiles!)
|
||||||
(set-default-target! 'all)
|
(begin-makefile! 'demo)
|
||||||
(mark-phony! 'all)
|
(set-default-target! 'demo 'all)
|
||||||
|
(mark-phony! 'demo 'all)
|
||||||
|
|
||||||
(register-target!
|
(register-target!
|
||||||
|
'demo
|
||||||
"result.txt"
|
"result.txt"
|
||||||
'("input.txt")
|
'("input.txt")
|
||||||
(lambda ()
|
(λ ()
|
||||||
(set! build-count (add1 build-count))
|
(set! build-count (add1 build-count))
|
||||||
(copy-file "input.txt" "result.txt" #t)))
|
(copy-file "input.txt" "result.txt" #t)))
|
||||||
|
|
||||||
;; A dependency expression may produce a list. The engine flattens it.
|
(register-target!
|
||||||
(register-target! 'all
|
'demo
|
||||||
|
'all
|
||||||
(list (list "result.txt"))
|
(list (list "result.txt"))
|
||||||
void)
|
void)
|
||||||
|
|
||||||
|
(current-makefile-prefix 'demo)
|
||||||
(make-targets!)
|
(make-targets!)
|
||||||
(check-equal? build-count 1)
|
(check-equal? build-count 1)
|
||||||
(check-true (file-exists? "result.txt"))
|
(check-true (file-exists? "result.txt"))
|
||||||
@@ -45,5 +49,5 @@
|
|||||||
(file-or-directory-modify-seconds "input.txt" (+ result-time 2))
|
(file-or-directory-modify-seconds "input.txt" (+ result-time 2))
|
||||||
(make-targets!)
|
(make-targets!)
|
||||||
(check-equal? build-count 2)))
|
(check-equal? build-count 2)))
|
||||||
(lambda ()
|
(λ ()
|
||||||
(delete-directory/files tmp #:must-exist? #f)))
|
(delete-directory/files tmp #:must-exist? #f)))
|
||||||
|
|||||||
+53
-42
@@ -1,55 +1,66 @@
|
|||||||
#lang racket
|
#lang racket
|
||||||
|
|
||||||
(require rackunit
|
(require rackunit
|
||||||
racket/file
|
racket-makefile
|
||||||
racket/runtime-path
|
(only-in "../private/engine.rkt" reset-makefiles!))
|
||||||
racket/system)
|
|
||||||
|
|
||||||
(define-runtime-path package-root "..")
|
(reset-makefiles!)
|
||||||
|
|
||||||
(define racket-executable
|
(define calls '())
|
||||||
(find-system-path 'exec-file))
|
(define context #f)
|
||||||
|
|
||||||
(define tmp (make-temporary-file "racket-makefile-make~a" 'directory))
|
(makefile first
|
||||||
|
(default-target status)
|
||||||
|
(phony status all context)
|
||||||
|
|
||||||
(define (run-racket . args)
|
(target status
|
||||||
(parameterize ([current-directory tmp])
|
(set! calls (append calls '(first-status))))
|
||||||
(apply system* racket-executable args)))
|
|
||||||
|
|
||||||
(dynamic-wind
|
(target all
|
||||||
void
|
(deps status)
|
||||||
(lambda ()
|
(set! calls (append calls '(first-all))))
|
||||||
(call-with-output-file (build-path tmp "Makefile.rkt")
|
|
||||||
(lambda (out)
|
|
||||||
(displayln "#lang racket-makefile" out)
|
|
||||||
(displayln "(default-target all)" out)
|
|
||||||
(displayln "(phony all clean 'compile)" out)
|
|
||||||
(displayln "(target all (call-with-output-file \"all.out\" (lambda (o) (display \"all\" o)) #:exists 'replace))" out)
|
|
||||||
(displayln "(target clean (rm-f \"all.out\" \"compile.out\"))" out)
|
|
||||||
;; `compile` is deliberately chosen because Racket already binds that
|
|
||||||
;; identifier. `(make compile)` must still mean the target named compile.
|
|
||||||
(displayln "(target 'compile (call-with-output-file \"compile.out\" (lambda (o) (display \"compile\" o)) #:exists 'replace))" out)))
|
|
||||||
|
|
||||||
;; Loading the makefile alone must not run any target.
|
(target context
|
||||||
(check-true (run-racket "-t" "Makefile.rkt"))
|
(deps input.txt other.txt)
|
||||||
(check-false (file-exists? (build-path tmp "all.out")))
|
(set! context (list $target $deps $<))))
|
||||||
|
|
||||||
;; Explicit make invocation from the command line.
|
(check-equal? (current-makefile-prefix) 'first)
|
||||||
(check-true (run-racket "-t" "Makefile.rkt" "-e" "(make all)"))
|
(check-true (procedure? make))
|
||||||
(check-true (file-exists? (build-path tmp "all.out")))
|
(check-true (procedure? makefile-target-first-status))
|
||||||
|
(check-true (procedure? makefile-target-first-all))
|
||||||
|
(check-true (procedure? makefile-target-first-context))
|
||||||
|
|
||||||
;; A bare target identifier is literal, even when Racket binds the same name.
|
;; make resolves the target procedure through the active prefix.
|
||||||
(check-true (run-racket "-t" "Makefile.rkt" "-e" "(make compile)"))
|
(make 'all)
|
||||||
(check-true (file-exists? (build-path tmp "compile.out")))
|
(check-equal? calls '(first-status first-all))
|
||||||
|
|
||||||
;; Multiple targets are executed in the requested order.
|
;; The generated target is a real Racket procedure and can be called directly.
|
||||||
(check-true (run-racket "-t" "Makefile.rkt" "-e" "(make clean all)"))
|
(makefile-target-first-status)
|
||||||
(check-true (file-exists? (build-path tmp "all.out")))
|
(check-equal? calls '(first-status first-all first-status))
|
||||||
(check-false (file-exists? (build-path tmp "compile.out")))
|
|
||||||
|
|
||||||
;; (make) uses the configured default target.
|
;; Direct procedure calls still get the automatic target variables.
|
||||||
(delete-file (build-path tmp "all.out"))
|
(makefile-target-first-context)
|
||||||
(check-true (run-racket "-t" "Makefile.rkt" "-e" "(make)"))
|
(check-equal? context '("context" ("input.txt" "other.txt") "input.txt"))
|
||||||
(check-true (file-exists? (build-path tmp "all.out"))))
|
|
||||||
(lambda ()
|
(makefile second
|
||||||
(delete-directory/files tmp #:must-exist? #f)))
|
(default-target status)
|
||||||
|
(phony status)
|
||||||
|
(target status
|
||||||
|
(set! calls (append calls '(second-status)))))
|
||||||
|
|
||||||
|
;; The last makefile definition becomes active.
|
||||||
|
(check-equal? (current-makefile-prefix) 'second)
|
||||||
|
(make)
|
||||||
|
(check-equal? calls '(first-status first-all first-status second-status))
|
||||||
|
|
||||||
|
;; Both prefixes remain registered and can be selected explicitly.
|
||||||
|
(current-makefile-prefix 'first)
|
||||||
|
(make 'status)
|
||||||
|
(check-equal?
|
||||||
|
calls
|
||||||
|
'(first-status first-all first-status second-status first-status))
|
||||||
|
|
||||||
|
;; The registry stores the generated procedure itself.
|
||||||
|
(check-eq?
|
||||||
|
(hash-ref makefile-targets '("first" . "status"))
|
||||||
|
makefile-target-first-status)
|
||||||
|
|||||||
+29
-91
@@ -2,26 +2,21 @@
|
|||||||
|
|
||||||
(require rackunit
|
(require rackunit
|
||||||
racket/file
|
racket/file
|
||||||
racket/runtime-path
|
|
||||||
racket/system)
|
racket/system)
|
||||||
|
|
||||||
(define racket-executable
|
(define racket-executable (find-system-path 'exec-file))
|
||||||
(find-system-path 'exec-file))
|
|
||||||
|
|
||||||
(define tmp (make-temporary-file "racket-makefile-refresh~a" 'directory))
|
(define tmp (make-temporary-file "racket-makefile-refresh~a" 'directory))
|
||||||
|
|
||||||
(define (write-lines path . lines)
|
(define (write-lines path . lines)
|
||||||
(call-with-output-file path
|
(call-with-output-file path
|
||||||
#:exists 'truncate/replace
|
#:exists 'truncate/replace
|
||||||
(lambda (out)
|
(λ (out)
|
||||||
(for ([line (in-list lines)])
|
(for ([line (in-list lines)])
|
||||||
(displayln line out)))))
|
(displayln line out)))))
|
||||||
|
|
||||||
(dynamic-wind
|
(dynamic-wind
|
||||||
void
|
void
|
||||||
(lambda ()
|
(λ ()
|
||||||
;; This module records how often it is instantiated. A refresh must reuse
|
|
||||||
;; the existing module instance instead of loading it a second time.
|
|
||||||
(write-lines
|
(write-lines
|
||||||
(build-path tmp "slow.rkt")
|
(build-path tmp "slow.rkt")
|
||||||
"#lang racket/base"
|
"#lang racket/base"
|
||||||
@@ -34,99 +29,42 @@
|
|||||||
|
|
||||||
(write-lines
|
(write-lines
|
||||||
(build-path tmp "Makefile.rkt")
|
(build-path tmp "Makefile.rkt")
|
||||||
"#lang racket-makefile"
|
"#lang racket"
|
||||||
"(require \"slow.rkt\")"
|
"(require racket-makefile \"slow.rkt\")"
|
||||||
"(phony change)"
|
"(makefile demo"
|
||||||
"(target change (copy-file \"NewMakefile.rkt\" \"Makefile.rkt\" #t))")
|
" (phony change old)"
|
||||||
|
" (target old (displayln \"old\"))"
|
||||||
|
" (target change (copy-file \"NewMakefile.rkt\" \"Makefile.rkt\" #t)))")
|
||||||
|
|
||||||
(write-lines
|
(write-lines
|
||||||
(build-path tmp "NewMakefile.rkt")
|
(build-path tmp "NewMakefile.rkt")
|
||||||
"#lang racket-makefile"
|
"#lang racket"
|
||||||
"(require \"slow.rkt\")"
|
"(require racket-makefile \"slow.rkt\")"
|
||||||
"(phony changed)"
|
"(makefile demo"
|
||||||
"(target changed"
|
" (phony changed)"
|
||||||
|
" (target changed"
|
||||||
" (call-with-output-file \"result.txt\" #:exists 'truncate/replace"
|
" (call-with-output-file \"result.txt\" #:exists 'truncate/replace"
|
||||||
" (lambda (out) (display slow-value out))))")
|
" (lambda (out) (display slow-value out)))))")
|
||||||
|
|
||||||
(parameterize ([current-directory tmp])
|
|
||||||
(check-true
|
|
||||||
(system* racket-executable
|
|
||||||
"-t" "Makefile.rkt"
|
|
||||||
"-e" "(make change)"
|
|
||||||
"-e" "(refresh-makefile)"
|
|
||||||
"-e" "(make changed)")))
|
|
||||||
|
|
||||||
(check-equal? (file->string (build-path tmp "result.txt")) "loaded")
|
|
||||||
(check-equal? (file->string (build-path tmp "slow-count.txt")) "1")
|
|
||||||
(check-equal?
|
|
||||||
(filter (lambda (path)
|
|
||||||
(regexp-match? #rx"^racket-makefile-refresh.*[.]rkt$"
|
|
||||||
(path->string (file-name-from-path path))))
|
|
||||||
(directory-list tmp #:build? #t))
|
|
||||||
'()))
|
|
||||||
(lambda ()
|
|
||||||
(delete-directory/files tmp #:must-exist? #f)))
|
|
||||||
|
|
||||||
;; A makefile can also use ordinary #lang racket and require main.rkt directly.
|
|
||||||
;; This is the style used by racket-makefile's own development Makefile. The
|
|
||||||
;; declaration macros must remember the source file in that case, because the
|
|
||||||
;; custom racket-makefile #%module-begin is not involved.
|
|
||||||
(define direct-tmp (make-temporary-file "racket-makefile-direct-refresh~a" 'directory))
|
|
||||||
(define-runtime-path package-main "../main.rkt")
|
|
||||||
(define-runtime-path package-commands "../private/commands.rkt")
|
|
||||||
(define-runtime-path package-engine "../private/engine.rkt")
|
|
||||||
|
|
||||||
(dynamic-wind
|
|
||||||
void
|
|
||||||
(lambda ()
|
|
||||||
(make-directory (build-path direct-tmp "private"))
|
|
||||||
(copy-file package-main (build-path direct-tmp "main.rkt"))
|
|
||||||
(copy-file package-commands (build-path direct-tmp "private" "commands.rkt"))
|
|
||||||
(copy-file package-engine (build-path direct-tmp "private" "engine.rkt"))
|
|
||||||
|
|
||||||
(write-lines
|
(write-lines
|
||||||
(build-path direct-tmp "slow.rkt")
|
(build-path tmp "driver.rkt")
|
||||||
"#lang racket/base"
|
|
||||||
"(require racket/file)"
|
|
||||||
"(define count-file \"slow-count.txt\")"
|
|
||||||
"(define count (if (file-exists? count-file) (string->number (file->string count-file)) 0))"
|
|
||||||
"(call-with-output-file count-file #:exists 'truncate/replace (lambda (out) (display (add1 count) out)))"
|
|
||||||
"(provide slow-value)"
|
|
||||||
"(define slow-value \"loaded\")")
|
|
||||||
|
|
||||||
(write-lines
|
|
||||||
(build-path direct-tmp "Makefile.rkt")
|
|
||||||
"#lang racket"
|
"#lang racket"
|
||||||
"(require \"main.rkt\" \"slow.rkt\")"
|
"(require racket-makefile)"
|
||||||
"(phony change old)"
|
|
||||||
"(target old (displayln \"old\"))"
|
|
||||||
"(target change (copy-file \"NewMakefile.rkt\" \"Makefile.rkt\" #t))")
|
|
||||||
|
|
||||||
(write-lines
|
|
||||||
(build-path direct-tmp "NewMakefile.rkt")
|
|
||||||
"#lang racket"
|
|
||||||
"(require \"main.rkt\" \"slow.rkt\")"
|
|
||||||
"(phony changed)"
|
|
||||||
"(target changed"
|
|
||||||
" (call-with-output-file \"result.txt\" #:exists 'truncate/replace"
|
|
||||||
" (lambda (out) (display slow-value out))))")
|
|
||||||
|
|
||||||
(write-lines
|
|
||||||
(build-path direct-tmp "driver.rkt")
|
|
||||||
"#lang racket"
|
|
||||||
"(require \"main.rkt\")"
|
|
||||||
"(dynamic-require \"Makefile.rkt\" #f)"
|
"(dynamic-require \"Makefile.rkt\" #f)"
|
||||||
"(make change)"
|
"(make 'change)"
|
||||||
"(refresh-makefile)"
|
"(refresh-makefile)"
|
||||||
"(make changed)"
|
"(make 'changed)"
|
||||||
"(with-handlers ([exn:fail? (lambda (e) (displayln \"old target removed\"))])"
|
"(define old-removed?"
|
||||||
" (make old)"
|
" (with-handlers ([exn:fail? (lambda (e) #t)])"
|
||||||
|
" (make 'old)"
|
||||||
|
" #f))"
|
||||||
|
"(unless old-removed?"
|
||||||
" (error 'refresh-test \"old target survived refresh\"))")
|
" (error 'refresh-test \"old target survived refresh\"))")
|
||||||
|
|
||||||
(parameterize ([current-directory direct-tmp])
|
(parameterize ([current-directory tmp])
|
||||||
(check-true (system* racket-executable "driver.rkt")))
|
(check-true (system* racket-executable "driver.rkt")))
|
||||||
|
|
||||||
(check-equal? (file->string (build-path direct-tmp "result.txt")) "loaded")
|
(check-equal? (file->string (build-path tmp "result.txt")) "loaded")
|
||||||
(check-equal? (file->string (build-path direct-tmp "slow-count.txt")) "1"))
|
(check-equal? (file->string (build-path tmp "slow-count.txt")) "1"))
|
||||||
(lambda ()
|
(λ ()
|
||||||
(delete-directory/files direct-tmp #:must-exist? #f)))
|
(delete-directory/files tmp #:must-exist? #f)))
|
||||||
|
|||||||
Reference in New Issue
Block a user