From d8f2710507afcfb1c939f51559eabe3727dd048c Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Tue, 11 Aug 2026 17:06:57 +0200 Subject: [PATCH] Made it possible to use (refresh-makefile) in order to refresh an edited makefile on the fly. --- CHANGELOG | 14 ++++ Makefile.rkt | 1 + README.md | 15 +++- info.rkt | 2 +- main.rkt | 84 +++++++++++++++++++---- scrbl/racket-makefile.scrbl | 20 +++++- tests/refresh.rkt | 132 ++++++++++++++++++++++++++++++++++++ 7 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 tests/refresh.rkt diff --git a/CHANGELOG b/CHANGELOG index e8c3bfd..06e07ff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 Fixed the raco helper test for the Racket package build service: raco help diff --git a/Makefile.rkt b/Makefile.rkt index e6e1dba..39531c9 100644 --- a/Makefile.rkt +++ b/Makefile.rkt @@ -22,6 +22,7 @@ (dgit 'push)) (target status + (displayln "git status:") (void (dgit 'status)) ) diff --git a/README.md b/README.md index e760c34..5888004 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ raco pkg install For a versioned archive, give the package name explicitly: ```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 @@ -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 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` Use `raco` to invoke a Racket tool from a target without depending on `%PATH%` diff --git a/info.rkt b/info.rkt index 7062a65..b01b503 100644 --- a/info.rkt +++ b/info.rkt @@ -1,7 +1,7 @@ #lang info (define pkg-authors '(hnmdijkema)) -(define version "0.1.6") +(define version "0.1.8") (define license 'MIT) (define collection "racket-makefile") (define pkg-desc diff --git a/main.rkt b/main.rkt index 9724d59..a95b879 100644 --- a/main.rkt +++ b/main.rkt @@ -15,6 +15,7 @@ phony default-target make + refresh-makefile run raco rm-f @@ -29,6 +30,47 @@ ) ;(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 ;; Racket expression. This makes generated targets such as (target obj ...) ;; possible inside a for loop. An unbound identifier is a literal target name. @@ -51,10 +93,12 @@ [(_ 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 quote #%top-interaction #%app #%datum #%top))])) + (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)) @@ -66,30 +110,42 @@ [(target-dependency ...) (map literal-name-or-expression (syntax->list #'(dependency ...)))]) - #'(register-target! - target-name - (list target-dependency ...) - (lambda () body ... (void))))] + (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)]) - #'(register-target! - target-name - '() - (lambda () body ... (void))))])) + (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 ...)))]) - #'(mark-phony! target-name ...))])) + (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)]) - #'(set-default-target! target-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 () diff --git a/scrbl/racket-makefile.scrbl b/scrbl/racket-makefile.scrbl index cad35c5..58d08dd 100644 --- a/scrbl/racket-makefile.scrbl +++ b/scrbl/racket-makefile.scrbl @@ -3,7 +3,7 @@ @(require (for-label racket/base racket/file (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 list-dir/files list-files list-dirs $target $deps $<))) @@ -31,7 +31,7 @@ raco pkg install For a versioned archive, specify the package name explicitly: @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: @@ -109,6 +109,22 @@ the targets. Then execute targets in the Interactions window: > (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 ...)]{ diff --git a/tests/refresh.rkt b/tests/refresh.rkt new file mode 100644 index 0000000..2b948ab --- /dev/null +++ b/tests/refresh.rkt @@ -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)))