diff --git a/.gitignore b/.gitignore index 2664535..0735c26 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ compiled/ /*.bak /gui/*.bak +/doc diff --git a/CHANGELOG b/CHANGELOG index 34eaae6..4d60445 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 Initial racket-makefile language implementation. diff --git a/README.md b/README.md index 8ada02c..3d17551 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ Racket available and only adds targets, dependencies, timestamp based rebuilds, 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 #lang racket-makefile @@ -26,6 +29,8 @@ phony targets, external command execution and a few cleanup helpers. (cleanup "scrbl" '("**.html" "**.js" "**.css"))) ``` +## Installation + Install a local checkout with: ```text @@ -35,20 +40,68 @@ raco pkg install For a versioned archive, give the package name explicitly: ```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 -racket Makefile.rkt -racket Makefile.rkt clean -racket Makefile.rkt clean all +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)" ``` -The language adds `target`, `deps`, `phony`, `default-target`, `run`, `rm-f`, -`rm-rf`, `cleanup`, `$target`, `$deps` and `$<`. Dependency expressions may -also produce lists, which are automatically flattened. Everything else is +`(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 +> (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. See the installed `racket-makefile` Scribble documentation for the full API. diff --git a/examples/Makefile.rkt b/examples/Makefile.rkt index ac2803f..50252e1 100644 --- a/examples/Makefile.rkt +++ b/examples/Makefile.rkt @@ -1,5 +1,16 @@ #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 CFLAGS '(-Wall -O2)) diff --git a/examples/generated-targets.rkt b/examples/generated-targets.rkt new file mode 100644 index 0000000..35c9618 --- /dev/null +++ b/examples/generated-targets.rkt @@ -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)) diff --git a/info.rkt b/info.rkt index ce50ddc..32db99c 100644 --- a/info.rkt +++ b/info.rkt @@ -1,7 +1,7 @@ #lang info (define pkg-authors '(hnmdijkema)) -(define version "0.1.1") +(define version "0.1.2") (define license 'MIT) (define collection "racket-makefile") (define pkg-desc diff --git a/main.rkt b/main.rkt index 96f7716..add7177 100644 --- a/main.rkt +++ b/main.rkt @@ -12,6 +12,7 @@ deps phony default-target + make run rm-f rm-rf @@ -20,20 +21,32 @@ $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) (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!) form ... - (module+ main - (run-selected-target!)))])) + ;; 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))])) (define-syntax (deps 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)]) #'(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 (syntax-id-rules () [$target diff --git a/private/engine.rkt b/private/engine.rkt index 7e0cb6f..2eb1781 100644 --- a/private/engine.rkt +++ b/private/engine.rkt @@ -6,7 +6,7 @@ mark-phony! set-default-target! reset-makefile! - run-selected-target! + make-targets! current-target current-dependencies current-first-dependency) @@ -128,20 +128,21 @@ (hash-set! built-results key rebuilt?) rebuilt?])) -(define (selected-targets) - (define args (vector->list (current-command-line-arguments))) +(define (default-targets) (cond - [(pair? args) args] [default-target-name (list default-target-name)] [(pair? target-order) (list (car target-order))] [else '()])) -(define (run-selected-target!) - (define names (selected-targets)) - (when (null? names) +(define (make-targets! . names) + (define selected + (if (null? names) + (default-targets) + (append-map target-keys names))) + (when (null? selected) (error 'racket-makefile "no target defined")) (define built-results (make-hash)) (define visiting (make-hash)) - (for ([name (in-list names)]) + (for ([name (in-list selected)]) (build-target! name built-results visiting)) (void)) diff --git a/scrbl/racket-makefile.scrbl b/scrbl/racket-makefile.scrbl index eca773c..664a06b 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 + target deps phony default-target make run rm-f rm-rf cleanup $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 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: @@ -28,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.1.zip +raco pkg install --name racket-makefile racket-makefile-0.1.2.zip } After installation a makefile can start with: @@ -37,16 +40,74 @@ After installation a makefile can start with: #lang racket-makefile } -Run a makefile with Racket: +@section{A first makefile} -@verbatim{ -racket Makefile.rkt -racket Makefile.rkt clean -racket Makefile.rkt clean package +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. } -With no command-line target, @racket[default-target] is used. If no default is -specified, the first declared target is used. +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) +} @section{Targets and dependencies} @@ -77,7 +138,8 @@ when reached as a dependency. } @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 @@ -183,6 +245,9 @@ example, targets can be generated in a loop: (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}} @@ -201,3 +266,9 @@ package combines naturally with @tt{racket-makefile}: (target package (zip-package)) ] + +The package target can then be executed explicitly, for example: + +@verbatim{ +racket -t Makefile.rkt -e "(make package)" +} diff --git a/tests/engine.rkt b/tests/engine.rkt index 4a259a1..24d9c88 100644 --- a/tests/engine.rkt +++ b/tests/engine.rkt @@ -32,18 +32,18 @@ (list (list "result.txt")) void) - (run-selected-target!) + (make-targets!) (check-equal? build-count 1) (check-true (file-exists? "result.txt")) ;; Nothing changed, so the file target remains up to date. - (run-selected-target!) + (make-targets!) (check-equal? build-count 1) ;; Make the input newer than the output and verify that it rebuilds. (define result-time (file-or-directory-modify-seconds "result.txt")) (file-or-directory-modify-seconds "input.txt" (+ result-time 2)) - (run-selected-target!) + (make-targets!) (check-equal? build-count 2))) (lambda () (delete-directory/files tmp #:must-exist? #f))) diff --git a/tests/make.rkt b/tests/make.rkt new file mode 100644 index 0000000..535e0c5 --- /dev/null +++ b/tests/make.rkt @@ -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)))