Change behavior. (require racket-makefile) and use (make <target>) instead of racket Makefile.rkt <target>

This commit is contained in:
2026-08-08 22:38:13 +02:00
parent f32503f3e7
commit 57707e5fc0
11 changed files with 276 additions and 32 deletions
+1
View File
@@ -20,3 +20,4 @@ compiled/
/*.bak /*.bak
/gui/*.bak /gui/*.bak
/doc
+9
View File
@@ -1,3 +1,12 @@
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 0.1.1
Initial racket-makefile language implementation. Initial racket-makefile language implementation.
+61 -8
View File
@@ -4,6 +4,9 @@
Racket available and only adds targets, dependencies, timestamp based rebuilds, Racket available and only adds targets, dependencies, timestamp based rebuilds,
phony targets, external command execution and a few cleanup helpers. phony targets, external command execution and a few cleanup helpers.
A makefile only defines its targets. Loading or running the file does not build
anything by itself. Builds are started explicitly with `make`.
```racket ```racket
#lang racket-makefile #lang racket-makefile
@@ -26,6 +29,8 @@ phony targets, external command execution and a few cleanup helpers.
(cleanup "scrbl" '("**.html" "**.js" "**.css"))) (cleanup "scrbl" '("**.html" "**.js" "**.css")))
``` ```
## Installation
Install a local checkout with: Install a local checkout with:
```text ```text
@@ -35,20 +40,68 @@ raco pkg install
For a versioned archive, give the package name explicitly: For a versioned archive, give the package name explicitly:
```text ```text
raco pkg install --name racket-makefile racket-makefile-0.1.1.zip raco pkg install --name racket-makefile racket-makefile-0.1.2.zip
``` ```
Run the default target or select one or more targets explicitly: ## Running targets
Load the makefile with `-t` and evaluate a `make` expression with `-e`:
```text ```text
racket Makefile.rkt racket -t Makefile.rkt -e "(make)"
racket Makefile.rkt clean racket -t Makefile.rkt -e "(make all)"
racket Makefile.rkt clean all racket -t Makefile.rkt -e "(make clean)"
racket -t Makefile.rkt -e "(make clean all)"
``` ```
The language adds `target`, `deps`, `phony`, `default-target`, `run`, `rm-f`, `(make)` uses the target selected with `default-target`. If no default target is
`rm-rf`, `cleanup`, `$target`, `$deps` and `$<`. Dependency expressions may specified, the first declared target is used.
also produce lists, which are automatically flattened. Everything else is
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
> (make)
> (make clean)
> (make all)
> (make clean all)
```
Bare identifiers in `make` are target names, so no quote is needed. For
example, `(make clean)` selects the target named `clean`.
## Generated targets
Because the rest of the language is ordinary Racket, targets can be generated
with normal definitions and loops:
```racket
(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))
```
Dependency expressions may produce lists; they are recursively flattened.
The language adds `target`, `deps`, `phony`, `default-target`, `make`, `run`,
`rm-f`, `rm-rf`, `cleanup`, `$target`, `$deps` and `$<`. Everything else is
ordinary Racket. ordinary Racket.
See the installed `racket-makefile` Scribble documentation for the full API. See the installed `racket-makefile` Scribble documentation for the full API.
+11
View File
@@ -1,5 +1,16 @@
#lang racket-makefile #lang racket-makefile
;; Load only:
;; racket -t Makefile.rkt
;;
;; Build the default target:
;; 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))
+22
View File
@@ -0,0 +1,22 @@
#lang racket-makefile
;; Ordinary Racket can generate targets and dependency lists.
(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))
(default-target all)
(phony all clean)
(target all
(deps objects))
(target clean
(apply rm-f objects))
+1 -1
View File
@@ -1,7 +1,7 @@
#lang info #lang info
(define pkg-authors '(hnmdijkema)) (define pkg-authors '(hnmdijkema))
(define version "0.1.1") (define version "0.1.2")
(define license 'MIT) (define license 'MIT)
(define collection "racket-makefile") (define collection "racket-makefile")
(define pkg-desc (define pkg-desc
+23 -2
View File
@@ -12,6 +12,7 @@
deps deps
phony phony
default-target default-target
make
run run
rm-f rm-f
rm-rf rm-rf
@@ -20,20 +21,32 @@
$deps $deps
$<) $<)
;; For target declarations we want a bound identifier to remain an ordinary
;; Racket expression. This makes generated targets such as (target obj ...)
;; possible inside a for loop. An unbound identifier is a literal target name.
(define-for-syntax (literal-name-or-expression stx) (define-for-syntax (literal-name-or-expression stx)
(if (and (identifier? stx) (if (and (identifier? stx)
(not (identifier-binding stx))) (not (identifier-binding stx)))
(datum->syntax stx `(quote ,(syntax-e stx)) stx stx) (datum->syntax stx `(quote ,(syntax-e stx)) 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) (define-syntax (makefile-module-begin stx)
(syntax-case stx () (syntax-case stx ()
[(_ form ...) [(_ form ...)
#'(racket-module-begin #'(racket-module-begin
(reset-makefile!) (reset-makefile!)
form ... form ...
(module+ main ;; Re-export make so that `racket -t Makefile.rkt -e "(make all)"`
(run-selected-target!)))])) ;; imports the make form into the command-line evaluation namespace.
(provide make quote #%top-interaction #%app #%datum #%top))]))
(define-syntax (deps stx) (define-syntax (deps stx)
(raise-syntax-error 'deps "only valid as the dependency clause of target" stx)) (raise-syntax-error 'deps "only valid as the dependency clause of target" stx))
@@ -70,6 +83,14 @@
(with-syntax ([target-name (literal-name-or-expression #'name)]) (with-syntax ([target-name (literal-name-or-expression #'name)])
#'(set-default-target! target-name))])) #'(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
+9 -8
View File
@@ -6,7 +6,7 @@
mark-phony! mark-phony!
set-default-target! set-default-target!
reset-makefile! reset-makefile!
run-selected-target! make-targets!
current-target current-target
current-dependencies current-dependencies
current-first-dependency) current-first-dependency)
@@ -128,20 +128,21 @@
(hash-set! built-results key rebuilt?) (hash-set! built-results key rebuilt?)
rebuilt?])) rebuilt?]))
(define (selected-targets) (define (default-targets)
(define args (vector->list (current-command-line-arguments)))
(cond (cond
[(pair? args) args]
[default-target-name (list default-target-name)] [default-target-name (list default-target-name)]
[(pair? target-order) (list (car target-order))] [(pair? target-order) (list (car target-order))]
[else '()])) [else '()]))
(define (run-selected-target!) (define (make-targets! . names)
(define names (selected-targets)) (define selected
(when (null? names) (if (null? names)
(default-targets)
(append-map target-keys names)))
(when (null? selected)
(error 'racket-makefile "no target defined")) (error 'racket-makefile "no target defined"))
(define built-results (make-hash)) (define built-results (make-hash))
(define visiting (make-hash)) (define visiting (make-hash))
(for ([name (in-list names)]) (for ([name (in-list selected)])
(build-target! name built-results visiting)) (build-target! name built-results visiting))
(void)) (void))
+81 -10
View File
@@ -3,7 +3,7 @@
@(require (for-label racket/base @(require (for-label racket/base
racket/file racket/file
(only-in racket-makefile (only-in racket-makefile
target deps phony default-target target deps phony default-target make
run rm-f rm-rf cleanup run rm-f rm-rf cleanup
$target $deps $<))) $target $deps $<)))
@@ -17,6 +17,9 @@ targets, dependencies, timestamp based rebuilding, phony targets, command
execution, and a few cleanup helpers. The rest of the language is ordinary execution, and a few cleanup helpers. The rest of the language is ordinary
Racket. 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} @section{Installation}
Install the package from a local checkout with: Install the package from a local checkout with:
@@ -28,7 +31,7 @@ raco pkg install
For a versioned archive, specify the package name explicitly: For a versioned archive, specify the package name explicitly:
@verbatim{ @verbatim{
raco pkg install --name racket-makefile racket-makefile-0.1.1.zip raco pkg install --name racket-makefile racket-makefile-0.1.2.zip
} }
After installation a makefile can start with: After installation a makefile can start with:
@@ -37,16 +40,74 @@ After installation a makefile can start with:
#lang racket-makefile #lang racket-makefile
} }
Run a makefile with Racket: @section{A first makefile}
@verbatim{ For example:
racket Makefile.rkt
racket Makefile.rkt clean @racketblock[
racket Makefile.rkt clean package (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.
} }
With no command-line target, @racket[default-target] is used. If no default is From the command line, load the makefile with @tt{-t} and evaluate a
specified, the first declared target is used. @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)
}
@section{Targets and dependencies} @section{Targets and dependencies}
@@ -77,7 +138,8 @@ when reached as a dependency.
} }
@defform[(default-target name)]{ @defform[(default-target name)]{
Selects the target used when no target is supplied on the command line. 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 A non-phony target is rebuilt when its output does not exist or when a
@@ -183,6 +245,9 @@ example, targets can be generated in a loop:
(deps objects)) (deps objects))
] ]
The expression @racket[objects] evaluates to a list of dependencies. Dependency
lists are recursively flattened by @tt{racket-makefile}.
@section{Package example} @section{Package example}
The optional @hyperlink["https://docs.racket-lang.org/package-zipper/index.html"]{@tt{package-zipper}} The optional @hyperlink["https://docs.racket-lang.org/package-zipper/index.html"]{@tt{package-zipper}}
@@ -201,3 +266,9 @@ package combines naturally with @tt{racket-makefile}:
(target package (target package
(zip-package)) (zip-package))
] ]
The package target can then be executed explicitly, for example:
@verbatim{
racket -t Makefile.rkt -e "(make package)"
}
+3 -3
View File
@@ -32,18 +32,18 @@
(list (list "result.txt")) (list (list "result.txt"))
void) void)
(run-selected-target!) (make-targets!)
(check-equal? build-count 1) (check-equal? build-count 1)
(check-true (file-exists? "result.txt")) (check-true (file-exists? "result.txt"))
;; Nothing changed, so the file target remains up to date. ;; Nothing changed, so the file target remains up to date.
(run-selected-target!) (make-targets!)
(check-equal? build-count 1) (check-equal? build-count 1)
;; Make the input newer than the output and verify that it rebuilds. ;; Make the input newer than the output and verify that it rebuilds.
(define result-time (file-or-directory-modify-seconds "result.txt")) (define result-time (file-or-directory-modify-seconds "result.txt"))
(file-or-directory-modify-seconds "input.txt" (+ result-time 2)) (file-or-directory-modify-seconds "input.txt" (+ result-time 2))
(run-selected-target!) (make-targets!)
(check-equal? build-count 2))) (check-equal? build-count 2)))
(lambda () (lambda ()
(delete-directory/files tmp #:must-exist? #f))) (delete-directory/files tmp #:must-exist? #f)))
+55
View File
@@ -0,0 +1,55 @@
#lang racket
(require rackunit
racket/file
racket/runtime-path
racket/system)
(define-runtime-path package-root "..")
(define racket-executable
(find-system-path 'exec-file))
(define tmp (make-temporary-file "racket-makefile-make~a" 'directory))
(define (run-racket . args)
(parameterize ([current-directory tmp])
(apply system* racket-executable args)))
(dynamic-wind
void
(lambda ()
(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.
(check-true (run-racket "-t" "Makefile.rkt"))
(check-false (file-exists? (build-path tmp "all.out")))
;; Explicit make invocation from the command line.
(check-true (run-racket "-t" "Makefile.rkt" "-e" "(make all)"))
(check-true (file-exists? (build-path tmp "all.out")))
;; A bare target identifier is literal, even when Racket binds the same name.
(check-true (run-racket "-t" "Makefile.rkt" "-e" "(make compile)"))
(check-true (file-exists? (build-path tmp "compile.out")))
;; Multiple targets are executed in the requested order.
(check-true (run-racket "-t" "Makefile.rkt" "-e" "(make clean all)"))
(check-true (file-exists? (build-path tmp "all.out")))
(check-false (file-exists? (build-path tmp "compile.out")))
;; (make) uses the configured default target.
(delete-file (build-path tmp "all.out"))
(check-true (run-racket "-t" "Makefile.rkt" "-e" "(make)"))
(check-true (file-exists? (build-path tmp "all.out"))))
(lambda ()
(delete-directory/files tmp #:must-exist? #f)))