Made it possible to use (refresh-makefile) in order to refresh an edited makefile on the fly.

This commit is contained in:
2026-08-11 17:06:57 +02:00
parent 051cce6e33
commit d8f2710507
7 changed files with 250 additions and 18 deletions
+14
View File
@@ -1,3 +1,17 @@
0.1.8
Fixed refresh-makefile for ordinary #lang racket makefiles that require main.rkt
directly. Target declarations now remember their source makefile, and refresh
explicitly resets the registered target state before loading the edited file.
Removed targets therefore disappear after refresh as expected.
0.1.7
Added refresh-makefile for reloading the current Makefile.rkt from an existing
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 0.1.6
Fixed the raco helper test for the Racket package build service: raco help Fixed the raco helper test for the Racket package build service: raco help
+1
View File
@@ -22,6 +22,7 @@
(dgit 'push)) (dgit 'push))
(target status (target status
(displayln "git status:")
(void (dgit 'status)) (void (dgit 'status))
) )
+14 -1
View File
@@ -40,7 +40,7 @@ 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.6.zip raco pkg install --name racket-makefile racket-makefile-0.1.8.zip
``` ```
## Running targets ## Running targets
@@ -78,6 +78,19 @@ registers the targets. Then use the Interactions window:
Bare identifiers in `make` are target names, so no quote is needed. For Bare identifiers in `make` are target names, so no quote is needed. For
example, `(make clean)` selects the target named `clean`. 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
> (refresh-makefile)
> (make all)
```
This works both with `#lang racket-makefile` and with an ordinary `#lang racket`
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` ## Running Racket tools with `raco`
Use `raco` to invoke a Racket tool from a target without depending on `%PATH%` Use `raco` to invoke a Racket tool from a target without depending on `%PATH%`
+1 -1
View File
@@ -1,7 +1,7 @@
#lang info #lang info
(define pkg-authors '(hnmdijkema)) (define pkg-authors '(hnmdijkema))
(define version "0.1.6") (define version "0.1.8")
(define license 'MIT) (define license 'MIT)
(define collection "racket-makefile") (define collection "racket-makefile")
(define pkg-desc (define pkg-desc
+66 -10
View File
@@ -15,6 +15,7 @@
phony phony
default-target default-target
make make
refresh-makefile
run run
raco raco
rm-f rm-f
@@ -29,6 +30,47 @@
) )
;(all-from-out git)) ;(all-from-out git))
;; 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 (remember-makefile! path)
(set! loaded-makefile path)
(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)
(unless loaded-makefile
(error 'refresh-makefile "no racket-makefile has been loaded"))
(define source loaded-makefile)
(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
(make-temporary-file "racket-makefile-refresh~a.rkt" #f directory))
(copy-file source refresh-source #t)
;; With #lang racket, main.rkt is only required and our custom module-begin
;; does not run. Reset explicitly so removed/renamed targets disappear too.
(reset-makefile!)
(dynamic-wind
void
(lambda ()
(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)
(delete-file refresh-source)))
(void))
;; For target declarations we want a bound identifier to remain an ordinary ;; For target declarations we want a bound identifier to remain an ordinary
;; Racket expression. This makes generated targets such as (target obj ...) ;; Racket expression. This makes generated targets such as (target obj ...)
;; possible inside a for loop. An unbound identifier is a literal target name. ;; possible inside a for loop. An unbound identifier is a literal target name.
@@ -51,10 +93,12 @@
[(_ form ...) [(_ form ...)
#'(racket-module-begin #'(racket-module-begin
(reset-makefile!) (reset-makefile!)
(remember-makefile!
(variable-reference->module-source (#%variable-reference)))
form ... form ...
;; Re-export make so that `racket -t Makefile.rkt -e "(make all)"` ;; Re-export make so that `racket -t Makefile.rkt -e "(make all)"`
;; imports the make form into the command-line evaluation namespace. ;; imports the make form into the command-line evaluation namespace.
(provide make quote #%top-interaction #%app #%datum #%top))])) (provide make refresh-makefile 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))
@@ -66,30 +110,42 @@
[(target-dependency ...) [(target-dependency ...)
(map literal-name-or-expression (map literal-name-or-expression
(syntax->list #'(dependency ...)))]) (syntax->list #'(dependency ...)))])
#'(register-target! (with-syntax ([remember-source (remember-source-form stx)])
#'(begin
remember-source
(register-target!
target-name target-name
(list target-dependency ...) (list target-dependency ...)
(lambda () body ... (void))))] (lambda () body ... (void))))))]
[(_ name body ...) [(_ name body ...)
(with-syntax ([target-name (literal-name-or-expression #'name)]) (with-syntax ([target-name (literal-name-or-expression #'name)]
#'(register-target! [remember-source (remember-source-form stx)])
#'(begin
remember-source
(register-target!
target-name target-name
'() '()
(lambda () body ... (void))))])) (lambda () body ... (void)))))]))
(define-syntax (phony stx) (define-syntax (phony stx)
(syntax-case stx () (syntax-case stx ()
[(_ name ...) [(_ name ...)
(with-syntax ([(target-name ...) (with-syntax ([(target-name ...)
(map literal-name-or-expression (map literal-name-or-expression
(syntax->list #'(name ...)))]) (syntax->list #'(name ...)))]
#'(mark-phony! target-name ...))])) [remember-source (remember-source-form stx)])
#'(begin
remember-source
(mark-phony! target-name ...)))]))
(define-syntax (default-target stx) (define-syntax (default-target stx)
(syntax-case stx () (syntax-case stx ()
[(_ name) [(_ name)
(with-syntax ([target-name (literal-name-or-expression #'name)]) (with-syntax ([target-name (literal-name-or-expression #'name)]
#'(set-default-target! target-name))])) [remember-source (remember-source-form stx)])
#'(begin
remember-source
(set-default-target! target-name)))]))
(define-syntax (make stx) (define-syntax (make stx)
(syntax-case stx () (syntax-case stx ()
+18 -2
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 make target deps phony default-target make refresh-makefile
run raco rm-f rm-rf cleanup run raco rm-f rm-rf cleanup
list-dir/files list-files list-dirs list-dir/files list-files list-dirs
$target $deps $<))) $target $deps $<)))
@@ -31,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.6.zip raco pkg install --name racket-makefile racket-makefile-0.1.8.zip
} }
After installation a makefile can start with: After installation a makefile can start with:
@@ -109,6 +109,22 @@ the targets. Then execute targets in the Interactions window:
> (make clean 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} @section{Targets and dependencies}
@defform[(target name (deps dependency ...) body ...)]{ @defform[(target name (deps dependency ...) body ...)]{
+132
View File
@@ -0,0 +1,132 @@
#lang racket
(require rackunit
racket/file
racket/runtime-path
racket/system)
(define racket-executable
(find-system-path 'exec-file))
(define tmp (make-temporary-file "racket-makefile-refresh~a" 'directory))
(define (write-lines path . lines)
(call-with-output-file path
#:exists 'truncate/replace
(lambda (out)
(for ([line (in-list lines)])
(displayln line out)))))
(dynamic-wind
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
(build-path tmp "slow.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 tmp "Makefile.rkt")
"#lang racket-makefile"
"(require \"slow.rkt\")"
"(phony change)"
"(target change (copy-file \"NewMakefile.rkt\" \"Makefile.rkt\" #t))")
(write-lines
(build-path tmp "NewMakefile.rkt")
"#lang racket-makefile"
"(require \"slow.rkt\")"
"(phony changed)"
"(target changed"
" (call-with-output-file \"result.txt\" #:exists 'truncate/replace"
" (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
(build-path direct-tmp "slow.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"
"(require \"main.rkt\" \"slow.rkt\")"
"(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)"
"(make change)"
"(refresh-makefile)"
"(make changed)"
"(with-handlers ([exn:fail? (lambda (e) (displayln \"old target removed\"))])"
" (make old)"
" (error 'refresh-test \"old target survived refresh\"))")
(parameterize ([current-directory direct-tmp])
(check-true (system* racket-executable "driver.rkt")))
(check-equal? (file->string (build-path direct-tmp "result.txt")) "loaded")
(check-equal? (file->string (build-path direct-tmp "slow-count.txt")) "1"))
(lambda ()
(delete-directory/files direct-tmp #:must-exist? #f)))