diff --git a/CHANGELOG b/CHANGELOG index cac6886..215c6df 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +0.1.5 + +Added raco helper for invoking Racket tools from makefile recipes. +The helper prefers the raco executable from the current Racket installation, +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 Replaced files-for-regexp, list-dir-re, list-dir-re-r, and filter-dirs with diff --git a/Makefile.rkt b/Makefile.rkt new file mode 100644 index 0000000..c70c23d --- /dev/null +++ b/Makefile.rkt @@ -0,0 +1,20 @@ +#lang racket + +(require "main.rkt" + racket/string) + +(target all + (displayln "use (make clean) or (make package) or (make commit/push") + ) + +(target commit + (display "commit message:") + (flush-output) + (let ((line (read-line))) + (set! line (string-trim line)) + (run '(git add -A)) + (run (append '(git "commit" -m ) (list line)))) + ) + + + \ No newline at end of file diff --git a/README.md b/README.md index e69f293..4d8d029 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ `racket-makefile` is a small `#lang` for make-style builds. It keeps ordinary 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, Racket tool 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`. @@ -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.4.zip +raco pkg install --name racket-makefile racket-makefile-0.1.5.zip ``` ## Running targets @@ -78,6 +78,31 @@ 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`. +## Running Racket tools with `raco` + +Use `raco` to invoke a Racket tool from a target without depending on `%PATH%` +or `$PATH`: + +```racket +(phony setup test) + +(target setup + (raco '(setup racket-makefile))) + +(target test + (raco '(test -p racket-makefile))) +``` + +`racket-makefile` first looks for `raco` in the console binary directory of +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 +numbers are converted to command-line arguments, nested lists are flattened, +and `$target`, `$deps` and `$<` are available inside a recipe. + ## Regexp-based cleanup For cleanup rules that need more control than globs, `racket-makefile` also @@ -98,7 +123,7 @@ so they can be passed directly to `rm-f` and `rm-rf` with `apply`: (apply rm-rf (list-dirs "." #px"compiled$" #:recursive #t)) (apply rm-f - (list-files "." #px"(?i:(?:[.]bak|~)$)" #:recursive #t)) + (list-files "." #px"(?i:([.]bak|~)$)" #:recursive #t)) (displayln "done.")) ``` @@ -131,7 +156,7 @@ with normal definitions and loops: Dependency expressions may produce lists; they are recursively flattened. The language adds `target`, `deps`, `phony`, `default-target`, `make`, `run`, -`rm-f`, `rm-rf`, `cleanup`, `list-dir/files`, `list-files`, `list-dirs`, +`raco`, `rm-f`, `rm-rf`, `cleanup`, `list-dir/files`, `list-files`, `list-dirs`, `$target`, `$deps` and `$<`. Everything else is ordinary Racket. See the installed `racket-makefile` Scribble documentation for the full API. diff --git a/examples/Makefile.rkt b/examples/Makefile.rkt index 50252e1..dca7f02 100644 --- a/examples/Makefile.rkt +++ b/examples/Makefile.rkt @@ -15,7 +15,7 @@ (define CFLAGS '(-Wall -O2)) (default-target all) -(phony all clean) +(phony all clean setup test) (target all (deps "hello")) @@ -28,3 +28,10 @@ (rm-f "hello") (rm-rf "compiled") (cleanup "scrbl" '("**.html" "**.js" "**.css"))) + + +(target setup + (raco '(setup racket-makefile))) + +(target test + (raco '(test -p racket-makefile))) diff --git a/examples/cleanup-regexp.rkt b/examples/cleanup-regexp.rkt index da11054..8386d50 100644 --- a/examples/cleanup-regexp.rkt +++ b/examples/cleanup-regexp.rkt @@ -11,5 +11,5 @@ (apply rm-rf (list-dirs "." #px"compiled$" #:recursive #t)) (apply rm-f - (list-files "." #px"(?i:(?:[.]bak|~)$)" #:recursive #t)) + (list-files "." #px"(?i:([.]bak|~)$)" #:recursive #t)) (displayln "done.")) diff --git a/info.rkt b/info.rkt index 2df97cb..69c4761 100644 --- a/info.rkt +++ b/info.rkt @@ -1,7 +1,7 @@ #lang info (define pkg-authors '(hnmdijkema)) -(define version "0.1.4") +(define version "0.1.5") (define license 'MIT) (define collection "racket-makefile") (define pkg-desc diff --git a/main.rkt b/main.rkt index 8319f17..5733c89 100644 --- a/main.rkt +++ b/main.rkt @@ -14,6 +14,7 @@ default-target make run + raco rm-f rm-rf cleanup diff --git a/private/commands.rkt b/private/commands.rkt index d946b59..4a02a82 100644 --- a/private/commands.rkt +++ b/private/commands.rkt @@ -3,11 +3,14 @@ (require file/glob racket/file racket/list + racket/path racket/string racket/system + setup/dirs "engine.rkt") (provide run + raco rm-f rm-rf cleanup @@ -21,32 +24,33 @@ (error who "~a is only available while a target recipe is running" description)) value) -(define (command-item->strings value) +(define (command-item->strings who value) (cond [(eq? value '$target) - (list (recipe-value 'run current-target "$target"))] + (list (recipe-value who current-target "$target"))] [(eq? value '$deps) - (recipe-value 'run current-dependencies "$deps")] + (recipe-value who current-dependencies "$deps")] [(eq? value '$<) (define first (current-first-dependency)) (unless first - (error 'run "$< is not available: this target has no dependencies")) + (error who "$< is not available: this target has no dependencies")) (list first)] [(symbol? value) (list (symbol->string value))] [(path? value) (list (path->string value))] [(string? value) (list value)] [(number? value) (list (number->string value))] - [(list? value) (append-map command-item->strings value)] + [(list? value) (append-map (lambda (item) (command-item->strings who item)) value)] [else (raise-argument-error - 'run + who "command item (string, symbol, path, number, or list)" value)])) (define (run command) (unless (list? command) (raise-argument-error 'run "list?" command)) - (define arguments (append-map command-item->strings command)) + (define arguments + (append-map (lambda (item) (command-item->strings 'run item)) command)) (when (null? arguments) (error 'run "empty command")) (define program (car arguments)) @@ -59,6 +63,57 @@ (error 'run "command failed: ~a" program)) (void)) + +(define (raco-executable-name) + (if (eq? (system-type 'os) 'windows) + "raco.exe" + "raco")) + +(define (existing-raco-in directory) + (and directory + (let ([path (build-path directory (raco-executable-name))]) + (and (file-exists? path) path)))) + +(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 + (with-handlers ([exn:fail? (lambda (_) #f)]) + (find-console-bin-dir))) + (define from-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 + (with-handlers ([exn:fail? (lambda (_) #f)]) + (find-system-path 'exec-file))) + (define from-exec-dir + (and 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 + from-exec-dir + (find-executable-path (raco-executable-name)) + (error 'raco "cannot find raco for the current Racket installation"))) + +(define (raco command) + (unless (list? command) + (raise-argument-error 'raco "list?" command)) + (define arguments + (append-map (lambda (item) (command-item->strings 'raco item)) command)) + (when (null? arguments) + (error 'raco "empty command")) + (define executable (find-raco-executable)) + (printf "> ~a ~a\n" + (path->string executable) + (string-join arguments " ")) + (unless (apply system* executable arguments) + (error 'raco "command failed: ~a" (car arguments))) + (void)) + (define (rm-f . paths) (for ([path (in-list paths)]) (case (file-or-directory-type path #f) diff --git a/scrbl/racket-makefile.scrbl b/scrbl/racket-makefile.scrbl index c45e459..7a99640 100644 --- a/scrbl/racket-makefile.scrbl +++ b/scrbl/racket-makefile.scrbl @@ -4,7 +4,7 @@ racket/file (only-in racket-makefile target deps phony default-target make - run rm-f rm-rf cleanup + run raco rm-f rm-rf cleanup list-dir/files list-files list-dirs $target $deps $<))) @@ -14,8 +14,7 @@ @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, and a few cleanup helpers. The rest of the language is ordinary +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 @@ -32,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.4.zip +raco pkg install --name racket-makefile racket-makefile-0.1.5.zip } After installation a makefile can start with: @@ -195,6 +194,36 @@ Ordinary Racket values can be inserted with quasiquote: (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?]{ @@ -271,7 +300,7 @@ These helpers make regexp-based cleanup concise. For example: (apply rm-rf (list-dirs "." #px"compiled$" #:recursive #t)) (apply rm-f - (list-files "." #px"(?i:(?:[.]bak|~)$)" #:recursive #t)) + (list-files "." #px"(?i:([.]bak|~)$)" #:recursive #t)) (displayln "done.")) ] diff --git a/tests/raco.rkt b/tests/raco.rkt new file mode 100644 index 0000000..f32bc16 --- /dev/null +++ b/tests/raco.rkt @@ -0,0 +1,17 @@ +#lang racket + +(require rackunit + (only-in racket-makefile raco)) + +(check-not-exn + (lambda () + (raco '(help)))) + +;; It must not require raco to be on PATH. The current Racket installation +;; should locate its own raco first. +(define env (environment-variables-copy (current-environment-variables))) +(environment-variables-set! env #"PATH" #f) +(parameterize ([current-environment-variables env]) + (check-not-exn + (lambda () + (raco '(help)))))