├── .gitignore ├── @all.do ├── @build.do ├── @clean.do ├── @install.do ├── AUTHORS ├── LICENSE ├── LINKS ├── NOTES ├── README.md ├── REDO_DIR.md ├── TODO ├── all ├── build ├── clean ├── common.go ├── config.go ├── db.go ├── db_test.go ├── dependent.go ├── doc.go ├── doc ├── default.1.do ├── default.html.do ├── default.md.do ├── ifchange.md ├── ifchange.txt ├── ifcreate.md ├── ifcreate.txt ├── init.md ├── init.txt ├── redo-ifchange.1 ├── redo-ifchange.html ├── redo-ifcreate.1 ├── redo-ifcreate.html ├── redo-init.1 ├── redo-init.html ├── redo.1 ├── redo.html ├── redo.md └── redo.txt ├── dofile.go ├── doinfo.go ├── encoding.go ├── error.go ├── event.go ├── examples └── hello-world-0 │ ├── @clean.do │ ├── @hello.do │ ├── README.md │ ├── README.txt │ ├── default.html.do │ ├── default.md.do │ ├── github.css │ ├── hello.c │ └── hello.do ├── file.go ├── filedb.go ├── init.go ├── install ├── metadata.go ├── mustrebuild.go ├── nulldb.go ├── op.go ├── options.go ├── output.go ├── prerequisite.go ├── redo_test.go ├── redux ├── ifx.go ├── init.go ├── install-man-test.sh ├── install.go ├── main.go └── redo.go ├── relpath.go ├── stat.go ├── stat_windows.go ├── test.sh ├── tree_test.go ├── utils.go └── z_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .redo 3 | tags 4 | bin/* 5 | redux/redux 6 | -------------------------------------------------------------------------------- /@all.do: -------------------------------------------------------------------------------- 1 | b=$(basename $0) 2 | b=${b#@} 3 | b=${b%.do} 4 | exec $(dirname $0)/$b 5 | -------------------------------------------------------------------------------- /@build.do: -------------------------------------------------------------------------------- 1 | b=$(basename $0) 2 | b=${b#@} 3 | b=${b%.do} 4 | exec $(dirname $0)/$b 5 | -------------------------------------------------------------------------------- /@clean.do: -------------------------------------------------------------------------------- 1 | b=$(basename $0) 2 | b=${b#@} 3 | b=${b%.do} 4 | exec $(dirname $0)/$b 5 | -------------------------------------------------------------------------------- /@install.do: -------------------------------------------------------------------------------- 1 | b=$(basename $0) 2 | b=${b#@} 3 | b=${b%.do} 4 | exec $(dirname $0)/$b 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Gyepi Sam (http://gyepi.com) 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Gyepi Sam All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /LINKS: -------------------------------------------------------------------------------- 1 | redo 2 | redo-ifchange 3 | redo-ifcreate 4 | redo-init 5 | -------------------------------------------------------------------------------- /NOTES: -------------------------------------------------------------------------------- 1 | Working notes which will slowly get out of date... 2 | 3 | procedure outdate-dependents(dependency-type, target) 4 | for each dependent of type dependency-type on target 5 | mark dependent as out of date 6 | 7 | procedure add-file-record(file do-file timestamp hash) 8 | # note: do-file is nil when file is source, not target 9 | add record 10 | 11 | procedure update-file-record(file do-file timestamp hash) 12 | update record 13 | 14 | procedure delete-file-record(file) 15 | delete file record 16 | [call procedure outdate-dependents 'ifcreate' file] 17 | delete dependency records to file 18 | delete dependency records from file 19 | 20 | 21 | procedure run-do-file(target do-file) 22 | run do-file [1] 23 | # check for multiple outputs... 24 | 25 | created = record.hash == nil 26 | if result.hash != record.hash 27 | save new file 28 | update target record(target do-file exit-status content-hash timestamp) 29 | if created 30 | [call procedure outdate-dependents 'ifcreate' file] 31 | 32 | [call procedure outdate-dependents 'ifchange' file] 33 | 34 | procedure file-timestamp-changed (record file) 35 | file.timestamp != record.timestamp 36 | 37 | procedure file-hash-changed(record file) 38 | file.hash != record.hash 39 | 40 | procedure file-changed?(record file) 41 | file-timestamp-changed?(record file) or file-hash-changed(record file) 42 | 43 | 44 | procedure redo-target(target, do-file, do-files-not-found) 45 | [call procedure redo-ifchange(target, do-file)] 46 | for each file in do-files-not-found 47 | [call procedure redo-ifcreate(target, file)] 48 | [call procedure run-do-file target do-file] 49 | 50 | procedure is-doable?(record) 51 | record.do_file != nil 52 | 53 | procedure redo(target file) 54 | do-file, do-files-not-found = find-do-file(target) 55 | record, record-found? = find-file-record(target) 56 | 57 | if target file exists 58 | if record-found? 59 | if do-file 60 | [call procedure redo-target target do-file do-files-not-found] 61 | else if is-doable?(record) 62 | error "Missing do file for target" (note: 3) 63 | else if file-changed?(record target) #target is a source file 64 | [call procedure update-file-record target] 65 | [call procedure outdate-dependents 'ifchange' target] 66 | else 67 | if do-file 68 | [call procedure redo-target target do-file do-files-not-found] 69 | else #target is source 70 | [call procedure add-file-record target] 71 | [call procedure outdate-dependents 'ifcreate' target] 72 | 73 | else # target file does not exist 74 | if record-found? 75 | # existed at one point but was deleted 76 | [call procedure outdate-dependents 'ifcreate' file] 77 | if do-file 78 | [call procedure redo-target target do-file do-files-not-found] 79 | else if is-doable?(record) 80 | error "cannot find do file for target" 81 | else # target is a deleted source file 82 | [call procedure outdate-dependents 'ifchange' target] 83 | [call procedure forget-file target] 84 | error "source file does not exist" 85 | else 86 | if do-file 87 | [call procedure redo-target target do-file do-files-not-found] 88 | else 89 | error "cannot redo unknown file: target file" 90 | 91 | Notes: 92 | 93 | [1] In the special case where a class of targets are generated by a default.do file, 94 | but some files are 'source' files and not generated, it is necessary for the default.do 95 | file to recognize the special case and 'generate' the file with cat. 96 | 97 | [2] If a more specific do file is created for a file, 98 | the current redo-ifcreate rule (for the less specific file) would be invoked once. 99 | but the rules should then be changed to 'redo-ifchange do file' for the more specific 100 | This can be accomplished by deleting the old set and generating a new set. 101 | 102 | [3] This implies a need for the command '`redo-forget` target' to remove a target's record from the 103 | database. 104 | 105 | [4] In this implementation, the case where a more specific do file is added and the less specific 106 | one modified will cause both scripts to mark the target as out of date. 107 | 108 | 109 | # redo-static 110 | 111 | Static or source files represent leaf nodes in the dependency graph. 112 | They do not have dependencies on other files. However, since they are 113 | edited, they do change and must be tracked in order to trigger dependency 114 | actions. When 115 | 116 | procedure redo-static(target) 117 | [call procedure update-file-record record target] 118 | [call procedure outdate-dependents "if-change" target] 119 | 120 | 121 | procedure out-of-date?(target) 122 | 123 | if not file-exists? target 124 | return true 125 | 126 | record, record-found? = find-file-record(target) 127 | if not record-found? 128 | return true 129 | 130 | if record.outdated? 131 | return true 132 | 133 | if file-changed?(record, target) 134 | return true 135 | 136 | if any? (dependencies file) changed? 137 | return true 138 | 139 | 140 | procedure redo-ifchange(target) 141 | 142 | ensure-dependency-record((get-env "REDO-TARGET") "ifchange" target) 143 | 144 | if out-of-date?(target) 145 | do-file, do-files-not-found = find-do-file(target) 146 | if do-file 147 | [call procedure run-do-file target do-file] 148 | else 149 | record = find-file-record(target) 150 | if is-doable?(record) 151 | error "Missing do file for target" 152 | else if file-changed(record, target) 153 | [call procedure redo-static target] 154 | 155 | 156 | Notes: 157 | [1] This means that unchanging 'source' files will always be considered to be out of date 158 | only to be left unchanged by the `redo` algorithm. While this seems wasteful, it does serve 159 | the purpose of allowing the redo algorithm to detect when a source file is changed to a generated 160 | one. 161 | 162 | 163 | # redo-forget 164 | 165 | The `redo-forget` command is used to clear database records for targets that have been deleted. 166 | It is invoked as `redo-forget TARGET...` 167 | 168 | This has not been written yet. 169 | 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | redux is top down software build tool similar to make but simpler and more reliable. 4 | 5 | It implements and extends the redo concept somewhat sparsely described by [DJ Bernstein](http://cr.yp.to/redo.html). 6 | 7 | I implemented a minimal set of redo tools some years ago and used them enough to become convinced 8 | that the idea was worthwhile. However, they needed to be better, sharper, and faster before they could 9 | replace Make in my toolbox, thus, this tool. 10 | 11 | # Installation 12 | 13 | redux is written in Go and requires the Go compiler to be installed. 14 | Go can be fetched from the [Go site](http://www.golang.com) or your favorite distribution channel. 15 | 16 | Assuming Go is installed, the command: 17 | 18 | $ go get github.com/gyepisam/redux/redux 19 | 20 | will fetch, build and install redux into a $GOBIN directory. 21 | To complete the installation, run the command: 22 | 23 | $ redux install 24 | 25 | which installs the associated links to the binary and the man pages in a related directory. 26 | By default, the links are created in the same directory as the executable, but this can be 27 | changed with the environment variable: $BINDIR. 28 | 29 | The redux man page documentation can be installed separately with the command 30 | 31 | $ redux install man 32 | 33 | There are several options for where to install the manual pages. 34 | Please see 'redux help install' for details. 35 | 36 | redux supports the following commands: 37 | 38 | * init -- Creates or reinitializes one or more redo root directories. 39 | * ifchange -- Creates dependency on targets and ensure that targets are up to date. 40 | * ifcreate -- Creates dependency on non-existence of targets. 41 | * redo -- Builds files atomically. 42 | * install -- Installs links and manual pages 43 | 44 | The `install links` command creates links for each of these commands so they can be invoked as: 45 | 46 | * [redo-init](/doc/redo-init.html) 47 | * [redo-ifchange](/doc/redo-ifchange.html) 48 | * [redo-ifcreate](/doc/redo-ifcreate.html) 49 | * [redo](/doc/redo.html) 50 | 51 | 52 | # Overview 53 | 54 | A redo based system must have a directory named .redo at the top level project 55 | directory. 56 | 57 | It is created with the `redo-init` command, whose effects are idempotent. 58 | 59 | The database contains file status and metadata. Its format is not specified, 60 | but it must be capable of supporting multiple readers and multiple writers 61 | (though not necessarily writing to the same sections). Redux implements a file based 62 | database and a null database. The current file based database is probably about the 63 | slowest. Fortunately, new databases can be easily plugged in. 64 | 65 | In the redo system, there are three kinds of files. 66 | 67 | * `do` files are scripts that build targets. In addition, they establish 68 | file dependencies by calling `redo-ifcreate` and `redo-ifchange`. `do` files 69 | are invoked by the `redo` commands directly and indirectly by `redo-ifchange`. 70 | 71 | * `target` files are generated or otherwise composed, likely from other files, by `do` scripts 72 | 73 | * `static` or `source` files are manually created/edited. They are tracked 74 | for changes, but are not generated. As such, they have no do files. 75 | 76 | There are two kinds of dependencies 77 | 78 | * ifchange denotes a relationship where a change in one file marks the other as out of date. 79 | A ifchange B implies that a change in B invalidates A. Change includes creation, modification or deletion. 80 | 81 | * ifcreate denotes a dependency on the creation or deletion of a file. 82 | A ifcreate B implies that when B comes into existence or ceases to exist, A will be outdated. 83 | 84 | Please see the individual command documentation for further details. 85 | 86 | # Credits 87 | 88 | redux is written by Gyepi Sam . 89 | 90 | The redo concept is DJ Bernstein's and is described [here](http://cr.yp.to/redo.html). 91 | 92 | I am interested in any and all feedback on this software. 93 | The following people have made requests, provided feedback, suggestions, ideas and bug reports, all of which 94 | are welcome. My thanks to them all. Keep them coming! 95 | 96 | Szabolcs Szasz 97 | Mateusz Czapliński 98 | dontdieych 99 | Ivan Andrus 100 | -------------------------------------------------------------------------------- /REDO_DIR.md: -------------------------------------------------------------------------------- 1 | * The .redo/data directory 2 | 3 | The .redo directory marks the project root directory and holds configuration and data files. 4 | 5 | .redo/data is a directory 6 | 7 | entries are directories whose names are sha1 checksums of file paths relative to the .redo directory. 8 | 9 | Each entry could have up to four contents 10 | 11 | METADATA is a JSON file containing the following data: 12 | 13 | path 14 | sha1sum 15 | 16 | REBUILD is an empty file created to denote that the target is out of date. 17 | 18 | satisfies and requires are directories which denote the target's relationships. 19 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | TODO List 2 | 3 | - Todo items begin with a dash 4 | * Starred items are done. 5 | 6 | -* A starred Todo item is completely done. 7 | 8 | -* bug -- detect and break loops in dependency chain. 9 | -* refactor -- rather than create $3 file and check for changes, 10 | a simplier method is to not create file at all and check for existence. 11 | -* bug -- task flag detection is broken 12 | 13 | - Add redo-stamp for compatibility with other implementations. 14 | 15 | - Add command to list database entries. Either for a single file or all files. 16 | 17 | - Add mechanism to run jobs in parallel 18 | Useful for 'redo' or 'ifchange' with multiple targets. 19 | 20 | - Expand documentation on ifcreate: 21 | 'redo-ifcreate a` creates a dependency on the nonexistent file `a`. 22 | If `a` exists redo-ifcreate complains and exits with an error status. 23 | 24 | - optimization -- rewrite redoTarget with sets: 25 | 26 | old = existing prerequisite keys 27 | new = new prerequisites keys 28 | 29 | deletes = old - new 30 | writes = new - old 31 | # updates 32 | for each file in old && new 33 | if !file.IsCurrent() { 34 | add file to deletes 35 | add file to writes 36 | 37 | delete(deletes) 38 | put(writes) 39 | 40 | In most cases, this will eliminate a number of redundant operations. 41 | 42 | 43 | - Increased verbosity should 44 | * Show do file selected 45 | Show file stats for generated output 46 | 47 | 48 | - Tests 49 | Add docker based test with external suites 50 | * check arguments 51 | * writing to stdout vs file 52 | * cross project dependencies 53 | * Devise more special case tests 54 | 55 | - Use different database? 56 | 57 | Doubtlessly, an embedded SQL or key value database would be faster. 58 | Need to ensure serializable access. 59 | 60 | -* Add debug flag 61 | 62 | -* Change verbosity > 2 behaviour to use debug 63 | 64 | -* Combine into single executable with commands. 65 | 66 | redux init 67 | redux ifchange 68 | redux ifcreate 69 | redux redo 70 | 71 | With links to redux for each command. 72 | 73 | -* Rebuild binary for tests 74 | 75 | -* Add support for multiple levels of extensions. 76 | See https://github.com/gyepisam/redux/issues/1 77 | 78 | 79 | - Consider slightly different file storage 80 | every object gets a SHA1 81 | 82 | prerequisite (source SHA, dest SHA, data, type {static, auto, standard}) 83 | dependency (source SHA, dest SHA) 84 | metadata (source SHA, data) 85 | 86 | - Add requirements.txt file for hacking on redux 87 | 88 | - Add config file. redux init can create a default config 89 | 90 | -* Add apenwarr configuration value and make it fully compatible with apenwarr redo when enabled. 91 | $2 arg depends on do script. Basically, the missing prefixes, if any, from the do script are 92 | appended to the $2 arg. 93 | 94 | - Generate PDF docs 95 | 96 | -------------------------------------------------------------------------------- /all: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This do script can be directly invoked, for bootstrapping with 3 | # $ sh @all.do 4 | # $ redo @all 5 | 6 | set -e 7 | 8 | sh $(dirname $0)/build 9 | 10 | # create docs 11 | xargs -L1 -i bin/redux redo $PWD/doc/{}.1 $PWD/doc/{}.html < LINKS 12 | -------------------------------------------------------------------------------- /build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # The go compiler does its own dependency tracking and is so fast that 4 | # it is ironically simpler to always rebuild. 5 | go build -o bin/redux github.com/gyepisam/redux/redux 6 | bin/redux install links 7 | 8 | -------------------------------------------------------------------------------- /clean: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -f $(sed 's}^}bin/}' < LINKS) $(find $(dirname $0) -path ./t -prune -o -type f -name '*~') 3 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | // TASK_PREFIX is a marker for scripts that don't produce content. 14 | TASK_PREFIX = `@` 15 | 16 | // REDO_DIR names the hidden directory used for data and configuration. 17 | REDO_DIR = ".redo" 18 | 19 | // REDO_DIR_ENV_NAME names the environment variable for the REDO_DIR hidden directory. 20 | REDO_DIR_ENV_NAME = "REDO_DIR" 21 | 22 | // KEY_SEPARATOR is used to join the parts of the database key. 23 | KEY_SEPARATOR = "/" 24 | 25 | // AUTO marks system generated event records. 26 | AUTO = "auto" 27 | 28 | // Directory creation permission mode 29 | DIR_PERM = 0755 30 | 31 | // Extension for do scripts 32 | DO_EXT = "do" 33 | 34 | // Basename for default script 35 | DO_BASENAME = "default" 36 | 37 | // Extension separator 38 | EXT_SEP = "." 39 | 40 | DEFAULT_TARGET = "all" 41 | 42 | DEFAULT_DO = DEFAULT_TARGET + EXT_SEP + DO_EXT 43 | ) 44 | 45 | // Dependency Relations 46 | type Relation string 47 | 48 | const ( 49 | SATISFIES Relation = "satisfies" 50 | REQUIRES Relation = "requires" 51 | ) 52 | 53 | // makeKey returns a database key consisting of provided arguments, joined with KEY_SEPARATOR 54 | // and prefixed with the PathHash. 55 | func (f *File) makeKey(subkeys ...interface{}) (val string) { 56 | 57 | keys := make([]string, len(subkeys)+1) 58 | 59 | keys[0] = string(f.PathHash) 60 | 61 | for i, value := range subkeys { 62 | keys[i+1] = fmt.Sprintf("%s", value) 63 | } 64 | 65 | return strings.Join(keys, KEY_SEPARATOR) 66 | } 67 | 68 | func (f *File) metadataKey() string { 69 | return f.makeKey("METADATA") 70 | } 71 | 72 | func (f *File) mustRebuildKey() string { 73 | return f.makeKey("REBUILD") 74 | } 75 | 76 | func RecordRelation(dependent *File, target *File, event Event, m *Metadata) error { 77 | if err := dependent.PutPrerequisite(event, target.PathHash, target.AsPrerequisite(dependent.RootDir, m)); err != nil { 78 | return err 79 | } 80 | 81 | if err := target.PutDependency(event, dependent.PathHash, dependent.AsDependent(target.RootDir)); err != nil { 82 | return err 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | // Configuration container 8 | type Config struct { 9 | DBType string 10 | } 11 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | type Record struct { 8 | Key string 9 | Value []byte 10 | } 11 | 12 | // DB allows allows for multiple implementations of the redo database. 13 | type DB interface { 14 | IsNull() bool 15 | 16 | // Put stores value under key 17 | Put(key string, value []byte) error 18 | 19 | // Get returns the value stored under key and a boolean indicating 20 | // whether the returned value was actually found in the database. 21 | Get(key string) ([]byte, bool, error) 22 | 23 | // Delete removes the value stored under key. 24 | // If key does not exist, the operation is a noop. 25 | Delete(key string) error 26 | 27 | // GetKeys returns a list of keys which have the specified key as a prefix. 28 | GetKeys(prefix string) ([]string, error) 29 | 30 | // GetValues returns a list of values whose keys matching the specified key prefix. 31 | GetValues(prefix string) ([][]byte, error) 32 | 33 | // GetRecords returns a list of records (keys and data) matchign the specified key prefix. 34 | GetRecords(prefix string) ([]Record, error) 35 | 36 | Close() error 37 | } 38 | 39 | func WithDB(arg string, f func(DB) error) error { 40 | db, err := FileDbOpen(arg) 41 | if err != nil { 42 | return err 43 | } 44 | defer db.Close() 45 | return f(db) 46 | } 47 | 48 | // Delete removes a target's database records 49 | func (f *File) DeleteRecords() error { 50 | // procedure delete-file-record(file) 51 | if err := f.NotifyDependents(IFCREATE); err != nil { 52 | return err 53 | } 54 | 55 | if err := f.DeleteAllDependencies(); err != nil { 56 | return err 57 | } 58 | 59 | if err := f.DeleteAllPrerequisites(); err != nil { 60 | return err 61 | } 62 | 63 | if err := f.DeleteMetadata(); err != nil { 64 | return err 65 | } 66 | 67 | if err := f.DeleteMustRebuild(); err != nil { 68 | return err 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (f *File) Delete(key string) error { 75 | err := f.db.Delete(key) 76 | f.Debug("@Delete: %s -> %s\n", key, err) 77 | return err 78 | } 79 | -------------------------------------------------------------------------------- /db_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | import ( 8 | "crypto/rand" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "os" 13 | "reflect" 14 | "testing" 15 | ) 16 | 17 | func initRoot() (root string, fn func(), err error) { 18 | 19 | root, err = ioutil.TempDir("", "redo-db-root-") 20 | fn = func() { 21 | os.RemoveAll(root) 22 | } 23 | 24 | if err != nil { 25 | return 26 | } 27 | 28 | err = InitDir(root) 29 | return 30 | } 31 | 32 | func makeDBFunc(root string) func(func(DB) error) error { 33 | return func(f func(DB) error) error { 34 | return WithDB(root, f) 35 | } 36 | } 37 | 38 | func randBytes(b []byte) (err error) { 39 | _, err = io.ReadFull(rand.Reader, b) 40 | return 41 | } 42 | 43 | func TestDBOpen(t *testing.T) { 44 | 45 | root, fn, err := initRoot() 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | defer fn() 50 | 51 | f := func(db DB) error { 52 | return nil 53 | } 54 | 55 | if err := WithDB(root, f); err != nil { 56 | t.Fatal(err) 57 | } 58 | } 59 | 60 | func TestDBNullKey(t *testing.T) { 61 | root, fn, err := initRoot() 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | defer fn() 66 | 67 | err = WithDB(root, func(db DB) error { 68 | return db.Put("", []byte{0}) 69 | }) 70 | 71 | if err != NullKeyErr { 72 | t.Fatal(err) 73 | } 74 | } 75 | 76 | func TestDBAction(t *testing.T) { 77 | root, fn, err := initRoot() 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | defer fn() 82 | 83 | type KeyValue struct { 84 | key string 85 | value []byte 86 | } 87 | 88 | sizes := []int{0, 1, 2, 3, 5, 7, 10, 100, 1001, 10002, 100003} 89 | data := make([]KeyValue, len(sizes)) 90 | var prefixKeys []string 91 | var prefixValues [][]byte 92 | 93 | prefix := "random/byte/0" 94 | 95 | for i, size := range sizes { 96 | 97 | data[i].key = fmt.Sprintf("random/byte/%02d", size) 98 | 99 | data[i].value = make([]byte, size) 100 | if err := randBytes(data[i].value); err != nil { 101 | t.Fatal(err) 102 | } 103 | if size < 10 { 104 | prefixKeys = append(prefixKeys, data[i].key) 105 | prefixValues = append(prefixValues, data[i].value) 106 | } 107 | } 108 | 109 | for _, rec := range data { 110 | //Put 111 | err := WithDB(root, func(db DB) error { 112 | return db.Put(rec.key, rec.value) 113 | }) 114 | 115 | if err != nil { 116 | t.Fatalf("Error: %s in db.Put(%q,...)", err, rec.key) 117 | } 118 | 119 | // Get 120 | var value []byte 121 | err = WithDB(root, func(db DB) error { 122 | var found bool 123 | var err error 124 | value, found, err = db.Get(rec.key) 125 | if err == nil && !found { 126 | err = fmt.Errorf("cannot find key: %s", rec.key) 127 | } 128 | return err 129 | }) 130 | if err != nil { 131 | t.Fatal(err) 132 | } 133 | 134 | if !reflect.DeepEqual(rec.value, value) { 135 | t.Fatalf("Get: input and output bytes differ: %v != %v", rec.value, value) 136 | } 137 | 138 | t.Logf("%s/%s\n", root, rec.key) 139 | } 140 | 141 | //GetKeys 142 | var keys []string 143 | err = WithDB(root, func(db DB) error { 144 | var err error 145 | keys, err = db.GetKeys(prefix) 146 | return err 147 | }) 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | 152 | if !reflect.DeepEqual(prefixKeys, keys) { 153 | t.Fatalf("GetKeys: input and output prefixed keys differ: %v != %v", prefixKeys, keys) 154 | } 155 | 156 | //GetValues 157 | var values [][]byte 158 | err = WithDB(root, func(db DB) error { 159 | var err error 160 | values, err = db.GetValues(prefix) 161 | return err 162 | }) 163 | 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | 168 | if !reflect.DeepEqual(prefixValues, values) { 169 | t.Fatalf("GetValues: input and output prefixed values differ: %v != %v", prefixValues, values) 170 | } 171 | 172 | //Delete 173 | for _, rec := range data { 174 | err = WithDB(root, func(db DB) error { 175 | return db.Delete(rec.key) 176 | }) 177 | 178 | if err != nil { 179 | t.Fatal(err) 180 | } 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /dependent.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | // Dependent is the inverse of Prerequisite 8 | type Dependent struct { 9 | Path string 10 | } 11 | 12 | func (d Dependent) File(dir string) (*File, error) { 13 | f, err := NewFile(dir, d.Path) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return f, nil 18 | } 19 | 20 | func (p Prerequisite) File(dir string) (*File, error) { 21 | f, err := NewFile(dir, p.Path) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return f, nil 26 | } 27 | 28 | func (f *File) DependentFiles(prefix string) ([]*File, error) { 29 | 30 | data, err := f.db.GetValues(prefix) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | files := make([]*File, len(data)) 36 | 37 | for i, b := range data { 38 | if dep, err := decodeDependent(b); err != nil { 39 | return nil, err 40 | } else if item, err := dep.File(f.RootDir); err != nil { 41 | return nil, err 42 | } else { 43 | files[i] = item 44 | } 45 | } 46 | 47 | return files, nil 48 | } 49 | 50 | func (f *File) AllDependents() ([]*File, error) { 51 | return f.DependentFiles(f.makeKey(SATISFIES)) 52 | } 53 | 54 | func (f *File) EventDependents(event Event) ([]*File, error) { 55 | return f.DependentFiles(f.makeKey(SATISFIES, event)) 56 | } 57 | 58 | func (f *File) DeleteAllDependencies() (err error) { 59 | keys, err := f.db.GetKeys(f.makeKey(SATISFIES)) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | for _, key := range keys { 65 | if err := f.Delete(key); err != nil { 66 | return err 67 | } 68 | } 69 | return nil 70 | } 71 | 72 | func (f *File) DeleteDependency(event Event, hash Hash) error { 73 | return f.Delete(f.makeKey(SATISFIES, event, hash)) 74 | } 75 | 76 | func (f *File) PutDependency(event Event, hash Hash, dep Dependent) error { 77 | return f.Put(f.makeKey(SATISFIES, event, hash), dep) 78 | } 79 | 80 | // NotifyDependents flags dependents as out of date because target has been created, modified, or deleted. 81 | func (f *File) NotifyDependents(event Event) (err error) { 82 | 83 | dependents, err := f.EventDependents(event) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | for _, dependent := range dependents { 89 | if err := dependent.PutMustRebuild(); err != nil { 90 | return err 91 | } 92 | f.Debug("@Notify %s -> %s\n", event, dependent.Path) 93 | } 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | redux is a library for redux, a top down software build tool 4 | which implements and extends the redo concept somewhat sparsely 5 | described by [DJ Bernstein](http://cr.yp.to/redo.html). 6 | 7 | To install it, run 8 | 9 | $ go get github.com/gyepisam/redux/redux 10 | 11 | The command 12 | 13 | $ redux help 14 | 15 | provides built-in help, which is augmented by the html, man 16 | pages and the README file. 17 | */ 18 | package redux 19 | -------------------------------------------------------------------------------- /doc/default.1.do: -------------------------------------------------------------------------------- 1 | 2 | source=${2##redo-}.md 3 | 4 | redo-ifchange $source 5 | pandoc -s -t man $source 6 | -------------------------------------------------------------------------------- /doc/default.html.do: -------------------------------------------------------------------------------- 1 | 2 | source=${2##redo-}.md 3 | 4 | redo-ifchange $source 5 | pandoc -s -t html $source 6 | -------------------------------------------------------------------------------- /doc/default.md.do: -------------------------------------------------------------------------------- 1 | # combine various bits to create a single documentation file 2 | 3 | redo-ifchange $2.txt 4 | 5 | if test $2 = "redo" ; then 6 | name=$2 7 | else 8 | name="redo-"$2 9 | fi 10 | 11 | cat < 17 | 18 | EOS 19 | 20 | ../bin/redux documentation $2 | sed 's/^\( \+-.\+\)/\1\n/' 21 | 22 | echo 23 | 24 | cat $2.txt 25 | -------------------------------------------------------------------------------- /doc/ifchange.md: -------------------------------------------------------------------------------- 1 | % redo-ifchange(1) Redux User Manual 2 | % Gyepi Sam 3 | % October 03, 2018 4 | 5 | 6 | 7 | 8 | # NAME 9 | 10 | ifchange - Creates dependency on targets and ensure that targets are up to date. 11 | 12 | # SYNOPSIS 13 | 14 | redux ifchange [TARGET...] 15 | 16 | # OPTIONS 17 | 18 | -? Show help 19 | 20 | -h Show help 21 | 22 | -help 23 | 24 | Show help 25 | 26 | 27 | # NOTES 28 | 29 | 30 | The ifchange command creates a dependency on the target files and ensures that 31 | the target files are up to date, calling the redo command, if necessary. 32 | 33 | The current file will be invalidated if a target is rebuilt. 34 | 35 | 36 | # DESCRIPTION 37 | 38 | This command can be invoked as `redux ifchange` or, through a symlink, as `redo-ifchange`. 39 | 40 | The `redo-ifchange` command is used in a `.do` script. 41 | 42 | When the `.do` script for a target A contains the line `redo-ifchange B`, 43 | this means that the target A depends on B and A should be rebuilt if B changes. 44 | 45 | This command should be placed in a `.do` script and not run directly. 46 | 47 | # DETAILS 48 | 49 | Conceptually, redo-ifchange performs three tasks. 50 | 51 | First, it creates a prerequisite record between A and B so A can track changes to B. 52 | Second, it creates a dependency record between B and A so a change in B immediately invalidates A. 53 | Finally, if B is out of date, redo-ifchange ensures that B is made up to date. 54 | 55 | B is considered out of date if it does not exist, is not in the database, is flagged as out of date, 56 | has been modified or any of its dependents are out of date. Obviously this process may recurse. 57 | -------------------------------------------------------------------------------- /doc/ifchange.txt: -------------------------------------------------------------------------------- 1 | # DESCRIPTION 2 | 3 | This command can be invoked as `redux ifchange` or, through the installed symlink, as `redo-ifchange`. 4 | 5 | The `redo-ifchange` command is used in a `.do` script. 6 | 7 | When the `.do` script for a target A contains the line `redo-ifchange B`, 8 | this means that the target A depends on B and A should be rebuilt if B changes. 9 | 10 | This command should be placed in a `.do` script and not run directly. 11 | 12 | # DETAILS 13 | 14 | Conceptually, redo-ifchange performs three tasks. 15 | 16 | First, it creates a prerequisite record between A and B so A can track changes to B. 17 | Second, it creates a dependency record between B and A so a change in B immediately invalidates A. 18 | Finally, if B is out of date, redo-ifchange ensures that B is made up to date. 19 | 20 | B is considered out of date if it does not exist, is not in the database, is flagged as out of date, 21 | has been modified or any of its dependents are out of date. Obviously this process may recurse. 22 | -------------------------------------------------------------------------------- /doc/ifcreate.md: -------------------------------------------------------------------------------- 1 | % redo-ifcreate(1) Redux User Manual 2 | % Gyepi Sam 3 | % October 03, 2018 4 | 5 | 6 | 7 | 8 | # NAME 9 | 10 | ifcreate - Creates dependency on non-existence of targets. 11 | 12 | # SYNOPSIS 13 | 14 | redux ifcreate [TARGET...] 15 | 16 | # OPTIONS 17 | 18 | -? Show help 19 | 20 | -h Show help 21 | 22 | -help 23 | 24 | Show help 25 | 26 | 27 | # NOTES 28 | 29 | 30 | The ifcreate command creates a dependency on the non-existence of the target files. 31 | The current file will be invalidated if the target comes into existence. 32 | If the target exists, the command returns an error. 33 | 34 | 35 | # DESCRIPTION 36 | 37 | This command can be invoked as `redux ifcreate` or, through a symlink, as `redo-ifcreate`. 38 | 39 | The `ifcreate` command marks the current target as out of date when a 40 | non-existent prerequisite file comes into existence or is deleted. 41 | 42 | This command should be placed in a do file and not run directly. 43 | -------------------------------------------------------------------------------- /doc/ifcreate.txt: -------------------------------------------------------------------------------- 1 | # DESCRIPTION 2 | 3 | This command can be invoked as `redux ifcreate` or, through an installed symlink, as `redo-ifcreate`. 4 | 5 | The `ifcreate` command marks the current target as out of date when a 6 | non-existent prerequisite file comes into existence or is deleted. 7 | 8 | This command should be placed in a do file and not run directly. 9 | -------------------------------------------------------------------------------- /doc/init.md: -------------------------------------------------------------------------------- 1 | % redo-init(1) Redux User Manual 2 | % Gyepi Sam 3 | % October 03, 2018 4 | 5 | 6 | 7 | 8 | # NAME 9 | 10 | init - Creates or reinitializes one or more redo root directories. 11 | 12 | # SYNOPSIS 13 | 14 | redux init [OPTIONS] [DIRECTORY ...] 15 | 16 | # OPTIONS 17 | 18 | -? Show help 19 | 20 | -h Show help 21 | 22 | -help 23 | 24 | Show help 25 | 26 | 27 | # NOTES 28 | 29 | 30 | If one or more DIRECTORY arguments are specified, the command initializes each one. 31 | If no arguments are provided, but an environment variable named REDO_DIR exists, it is initialized. 32 | If neither arguments nor an environment variable is provided, the current directory is initialized. 33 | 34 | 35 | #DESCRIPTION 36 | 37 | This command can be invoked as `redux init` or, through a symlink, as `redo-init`. 38 | 39 | The init command creates and initializes a .redo directory, 40 | which holds the redo configuration and database files 41 | and also demarcates the root of the redo enabled project. 42 | 43 | The redo-init command must be invoked before any other redo commands can be used 44 | in the project. 45 | 46 | The command is idempotent and can be safely invoked multiple times in the same directory. 47 | 48 | #EXAMPLES 49 | 50 | redo-init DIRECTORY 51 | ~ The target directory is specified as an argument. 52 | 53 | env REDO_DIR=DIRECTORY redo-init 54 | ~ The target directory is provided by the REDO_DIR environment value. 55 | 56 | redo-init 57 | ~ The target directory not provided at all, so the current directory is used. 58 | -------------------------------------------------------------------------------- /doc/init.txt: -------------------------------------------------------------------------------- 1 | #DESCRIPTION 2 | 3 | This command can be invoked as `redux init` or, through a symlink, as `redo-init`. 4 | 5 | The init command creates and initializes a .redo directory, 6 | which holds the redo configuration and database files 7 | and also demarcates the root of a redo enabled project. 8 | 9 | The redo-init command must be invoked before any other redo commands can be used 10 | in the project directory or sub-directories. 11 | 12 | The command is idempotent and can be safely invoked multiple times in the same directory. 13 | 14 | #EXAMPLES 15 | 16 | redo-init DIRECTORY 17 | ~ The target directory is specified as an argument. 18 | 19 | env REDO_DIR=DIRECTORY redo-init 20 | ~ The target directory is provided by the REDO_DIR environment value. 21 | 22 | redo-init 23 | ~ The target directory not provided, so the current directory is used. 24 | -------------------------------------------------------------------------------- /doc/redo-ifchange.1: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 1.16.0.2 2 | .\" 3 | .TH "redo\-ifchange" "1" "October 03, 2018" "Redux User Manual" "" 4 | .hy 5 | .SH NAME 6 | .PP 7 | ifchange \- Creates dependency on targets and ensure that targets are up 8 | to date. 9 | .SH SYNOPSIS 10 | .PP 11 | redux ifchange [TARGET...] 12 | .SH OPTIONS 13 | .PP 14 | \-? 15 | Show help 16 | .PP 17 | \-h Show help 18 | .PP 19 | \-help 20 | .IP 21 | .nf 22 | \f[C] 23 | \ \ \ \ Show\ help 24 | \f[] 25 | .fi 26 | .SH NOTES 27 | .PP 28 | The ifchange command creates a dependency on the target files and 29 | ensures that the target files are up to date, calling the redo command, 30 | if necessary. 31 | .PP 32 | The current file will be invalidated if a target is rebuilt. 33 | .SH DESCRIPTION 34 | .PP 35 | This command can be invoked as \f[C]redux\ ifchange\f[] or, through a 36 | symlink, as \f[C]redo\-ifchange\f[]. 37 | .PP 38 | The \f[C]redo\-ifchange\f[] command is used in a \f[C]\&.do\f[] script. 39 | .PP 40 | When the \f[C]\&.do\f[] script for a target A contains the line 41 | \f[C]redo\-ifchange\ B\f[], this means that the target A depends on B 42 | and A should be rebuilt if B changes. 43 | .PP 44 | This command should be placed in a \f[C]\&.do\f[] script and not run 45 | directly. 46 | .SH DETAILS 47 | .PP 48 | Conceptually, redo\-ifchange performs three tasks. 49 | .PP 50 | First, it creates a prerequisite record between A and B so A can track 51 | changes to B. 52 | Second, it creates a dependency record between B and A so a change in B 53 | immediately invalidates A. 54 | Finally, if B is out of date, redo\-ifchange ensures that B is made up 55 | to date. 56 | .PP 57 | B is considered out of date if it does not exist, is not in the 58 | database, is flagged as out of date, has been modified or any of its 59 | dependents are out of date. 60 | Obviously this process may recurse. 61 | .SH AUTHORS 62 | Gyepi Sam. 63 | -------------------------------------------------------------------------------- /doc/redo-ifchange.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | redo-ifchange(1) Redux User Manual 10 | 11 | 12 | 13 | 18 | 19 |

NAME

20 |

ifchange - Creates dependency on targets and ensure that targets are up to date.

21 |

SYNOPSIS

22 |

redux ifchange [TARGET...]

23 |

OPTIONS

24 |

-? Show help

25 |

-h Show help

26 |

-help

27 |
    Show help
28 |

NOTES

29 |

The ifchange command creates a dependency on the target files and ensures that the target files are up to date, calling the redo command, if necessary.

30 |

The current file will be invalidated if a target is rebuilt.

31 |

DESCRIPTION

32 |

This command can be invoked as redux ifchange or, through a symlink, as redo-ifchange.

33 |

The redo-ifchange command is used in a .do script.

34 |

When the .do script for a target A contains the line redo-ifchange B, this means that the target A depends on B and A should be rebuilt if B changes.

35 |

This command should be placed in a .do script and not run directly.

36 |

DETAILS

37 |

Conceptually, redo-ifchange performs three tasks.

38 |

First, it creates a prerequisite record between A and B so A can track changes to B. Second, it creates a dependency record between B and A so a change in B immediately invalidates A. Finally, if B is out of date, redo-ifchange ensures that B is made up to date.

39 |

B is considered out of date if it does not exist, is not in the database, is flagged as out of date, has been modified or any of its dependents are out of date. Obviously this process may recurse.

40 | 41 | 42 | -------------------------------------------------------------------------------- /doc/redo-ifcreate.1: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 1.16.0.2 2 | .\" 3 | .TH "redo\-ifcreate" "1" "October 03, 2018" "Redux User Manual" "" 4 | .hy 5 | .SH NAME 6 | .PP 7 | ifcreate \- Creates dependency on non\-existence of targets. 8 | .SH SYNOPSIS 9 | .PP 10 | redux ifcreate [TARGET...] 11 | .SH OPTIONS 12 | .PP 13 | \-? 14 | Show help 15 | .PP 16 | \-h Show help 17 | .PP 18 | \-help 19 | .IP 20 | .nf 21 | \f[C] 22 | \ \ \ \ Show\ help 23 | \f[] 24 | .fi 25 | .SH NOTES 26 | .PP 27 | The ifcreate command creates a dependency on the non\-existence of the 28 | target files. 29 | The current file will be invalidated if the target comes into existence. 30 | If the target exists, the command returns an error. 31 | .SH DESCRIPTION 32 | .PP 33 | This command can be invoked as \f[C]redux\ ifcreate\f[] or, through a 34 | symlink, as \f[C]redo\-ifcreate\f[]. 35 | .PP 36 | The \f[C]ifcreate\f[] command marks the current target as out of date 37 | when a non\-existent prerequisite file comes into existence or is 38 | deleted. 39 | .PP 40 | This command should be placed in a do file and not run directly. 41 | .SH AUTHORS 42 | Gyepi Sam. 43 | -------------------------------------------------------------------------------- /doc/redo-ifcreate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | redo-ifcreate(1) Redux User Manual 10 | 11 | 12 | 13 | 18 | 19 |

NAME

20 |

ifcreate - Creates dependency on non-existence of targets.

21 |

SYNOPSIS

22 |

redux ifcreate [TARGET...]

23 |

OPTIONS

24 |

-? Show help

25 |

-h Show help

26 |

-help

27 |
    Show help
28 |

NOTES

29 |

The ifcreate command creates a dependency on the non-existence of the target files. The current file will be invalidated if the target comes into existence. If the target exists, the command returns an error.

30 |

DESCRIPTION

31 |

This command can be invoked as redux ifcreate or, through a symlink, as redo-ifcreate.

32 |

The ifcreate command marks the current target as out of date when a non-existent prerequisite file comes into existence or is deleted.

33 |

This command should be placed in a do file and not run directly.

34 | 35 | 36 | -------------------------------------------------------------------------------- /doc/redo-init.1: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 1.16.0.2 2 | .\" 3 | .TH "redo\-init" "1" "October 03, 2018" "Redux User Manual" "" 4 | .hy 5 | .SH NAME 6 | .PP 7 | init \- Creates or reinitializes one or more redo root directories. 8 | .SH SYNOPSIS 9 | .PP 10 | redux init [OPTIONS] [DIRECTORY ...] 11 | .SH OPTIONS 12 | .PP 13 | \-? 14 | Show help 15 | .PP 16 | \-h Show help 17 | .PP 18 | \-help 19 | .IP 20 | .nf 21 | \f[C] 22 | \ \ \ \ Show\ help 23 | \f[] 24 | .fi 25 | .SH NOTES 26 | .PP 27 | If one or more DIRECTORY arguments are specified, the command 28 | initializes each one. 29 | If no arguments are provided, but an environment variable named REDO_DIR 30 | exists, it is initialized. 31 | If neither arguments nor an environment variable is provided, the 32 | current directory is initialized. 33 | .SH DESCRIPTION 34 | .PP 35 | This command can be invoked as \f[C]redux\ init\f[] or, through a 36 | symlink, as \f[C]redo\-init\f[]. 37 | .PP 38 | The init command creates and initializes a .redo directory, which holds 39 | the redo configuration and database files and also demarcates the root 40 | of the redo enabled project. 41 | .PP 42 | The redo\-init command must be invoked before any other redo commands 43 | can be used in the project. 44 | .PP 45 | The command is idempotent and can be safely invoked multiple times in 46 | the same directory. 47 | .SH EXAMPLES 48 | .TP 49 | .B redo\-init DIRECTORY 50 | The target directory is specified as an argument. 51 | .RS 52 | .RE 53 | .TP 54 | .B env REDO_DIR=DIRECTORY redo\-init 55 | The target directory is provided by the REDO_DIR environment value. 56 | .RS 57 | .RE 58 | .TP 59 | .B redo\-init 60 | The target directory not provided at all, so the current directory is 61 | used. 62 | .RS 63 | .RE 64 | .SH AUTHORS 65 | Gyepi Sam. 66 | -------------------------------------------------------------------------------- /doc/redo-init.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | redo-init(1) Redux User Manual 10 | 11 | 12 | 13 | 18 | 19 |

NAME

20 |

init - Creates or reinitializes one or more redo root directories.

21 |

SYNOPSIS

22 |

redux init [OPTIONS] [DIRECTORY ...]

23 |

OPTIONS

24 |

-? Show help

25 |

-h Show help

26 |

-help

27 |
    Show help
28 |

NOTES

29 |

If one or more DIRECTORY arguments are specified, the command initializes each one. If no arguments are provided, but an environment variable named REDO_DIR exists, it is initialized. If neither arguments nor an environment variable is provided, the current directory is initialized.

30 |

DESCRIPTION

31 |

This command can be invoked as redux init or, through a symlink, as redo-init.

32 |

The init command creates and initializes a .redo directory, which holds the redo configuration and database files and also demarcates the root of the redo enabled project.

33 |

The redo-init command must be invoked before any other redo commands can be used in the project.

34 |

The command is idempotent and can be safely invoked multiple times in the same directory.

35 |

EXAMPLES

36 |
37 |
redo-init DIRECTORY
38 |
The target directory is specified as an argument. 39 |
40 |
env REDO_DIR=DIRECTORY redo-init
41 |
The target directory is provided by the REDO_DIR environment value. 42 |
43 |
redo-init
44 |
The target directory not provided at all, so the current directory is used. 45 |
46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /doc/redo.1: -------------------------------------------------------------------------------- 1 | .\" Automatically generated by Pandoc 1.16.0.2 2 | .\" 3 | .TH "redo" "1" "October 03, 2018" "Redux User Manual" "" 4 | .hy 5 | .SH NAME 6 | .PP 7 | redo \- Builds files atomically. 8 | .SH SYNOPSIS 9 | .PP 10 | redux redo [OPTION]... 11 | [TARGET]... 12 | .SH OPTIONS 13 | .PP 14 | \-? 15 | Show help 16 | .PP 17 | \-d Alias for debug 18 | .PP 19 | \-debug 20 | .IP 21 | .nf 22 | \f[C] 23 | \ \ \ \ Print\ debugging\ output. 24 | \f[] 25 | .fi 26 | .PP 27 | \-h Show help 28 | .PP 29 | \-help 30 | .IP 31 | .nf 32 | \f[C] 33 | \ \ \ \ Show\ help 34 | \f[] 35 | .fi 36 | .PP 37 | \-sh string 38 | .IP 39 | .nf 40 | \f[C] 41 | \ \ \ \ Extra\ arguments\ for\ /bin/sh. 42 | \f[] 43 | .fi 44 | .PP 45 | \-task 46 | .IP 47 | .nf 48 | \f[C] 49 | \ \ \ \ Run\ .do\ script\ for\ side\ effects\ and\ ignore\ output. 50 | \f[] 51 | .fi 52 | .PP 53 | \-v Alias for verbose 54 | .PP 55 | \-verbose 56 | .IP 57 | .nf 58 | \f[C] 59 | \ \ \ \ Be\ verbose.\ Repeat\ for\ intensity. 60 | \f[] 61 | .fi 62 | .SH NOTES 63 | .PP 64 | The redo command builds files atomically by running a do script 65 | asssociated with the target. 66 | .PP 67 | redo normally requires one or more target arguments. 68 | If no target arguments are provided, redo runs the default target \@all 69 | in the current directory if its do script \@all.do exists. 70 | .SH DESCRIPTION 71 | .PP 72 | This command can be invoked as \f[C]redux\ redo\f[] or, through a link, 73 | as \f[C]redo\f[]. 74 | .PP 75 | A redo target is produced by an sh script whose name has a \[aq].do\[aq] 76 | file extension and, as such, is called a do script. 77 | .PP 78 | For a given target named \f[C]target\f[], the corresponding do file may 79 | be named, in order of decreasing specificity, \f[C]target.do\f[] or 80 | \f[C]default.do\f[]. 81 | .PP 82 | For a given target named \f[C]target.ext\f[], the corresponding do file 83 | may be named, in order of decreasing specificity, 84 | \f[C]target.ext.do\f[], \f[C]default.ext.do\f[] or, finally, 85 | \f[C]default.do\f[]. 86 | .PP 87 | For targets with multiple extensions, the corresponding do files may be 88 | named, in order of decreasing specificity, \f[C]target.ext.do\f[] where 89 | \f[C]ext\f[] is the full extension, followed by default do files with 90 | shorter and shorter suffixes of the extension, finally ending in 91 | \f[C]default.do\f[]. 92 | For example, the target \f[C]file.x.y.z\f[] results in a search for the 93 | following do scripts: 94 | .PP 95 | file.x.y.z.do default.x.y.z.do default.y.z.do default.z.do default.do 96 | .PP 97 | Jumping ahead slightly, note that in the case of multiple extensions, 98 | the $2 argument to the do script contains all but one extension. 99 | In the example above, $2 would be \f[C]file.x.y\f[] for all do files. 100 | Doing otherwise causes the $2 argument to depend on the do file used. 101 | .PP 102 | Redo searches for each of these script files, in order of specificity, 103 | starting in the target\[aq]s directory and moving into parent 104 | directories. 105 | The search stops when a script is found or when the project root 106 | directory has been unsuccessfully searched. 107 | .PP 108 | In the latter case, if the target file exists on disk, it is taken to be 109 | a source file, not generated by script and its metadata is stored in the 110 | database. 111 | The file will be subsequently watched for changes. 112 | .PP 113 | In the former case, where the script is found, it is assumed to be an sh 114 | script and executed with three arguments: 115 | .PP 116 | $1 = path to target, relative to do script directory $2 = same as $1, 117 | with a single file extension, if any, removed $3 = temporary file name 118 | .PP 119 | The script is executed by /bin/sh with the current working directory 120 | (cwd) set to its directory and with stdout opened to a temporary file 121 | (which is unnamed and different from $3). 122 | It is normally expected to produce output on stdout or write to the file 123 | specified by its $3 parameter. 124 | It is an error for a script to write to both outputs. 125 | .PP 126 | If the script completes successfully, redo chooses the correct output, 127 | renames the temporary file to the target file and updates its database 128 | with the new file\[aq]s metadata record. 129 | Since only one of the two temporary files can have content, redo has no 130 | trouble selecting the correct one. 131 | Conversely, if neither file has content, then either is a valid 132 | candidate. 133 | .PP 134 | In the do file, which is an sh script, the line 135 | .IP 136 | .nf 137 | \f[C] 138 | redo\-ifchange\ A\ B\ C 139 | \f[] 140 | .fi 141 | .PP 142 | specifies the files A, B, and C as prerequisites for the target file. 143 | .PP 144 | Similarly, a call to 145 | .IP 146 | .nf 147 | \f[C] 148 | redo\-ifcreate\ A 149 | \f[] 150 | .fi 151 | .PP 152 | specifies that the target should be rebuilt when the non\-existent file 153 | A appears or is deleted. 154 | .PP 155 | As a special case, a do file whose name is prefixed with \[aq]\@\[aq] is 156 | run for side effect. 157 | redux does not create a temporary file when running such a file and uses 158 | \[aq]/dev/stdout\[aq] as the output file name so its output is visible 159 | but is otherwise not saved. 160 | While the $3 parameter is provided for consistency, it is an error for a 161 | task script to write to it since its output is discarded. 162 | .PP 163 | A call to redo without an argument will search for a file named 164 | \f[C]\@all.do\f[] in the current directory. 165 | .PP 166 | A \[aq]\@\[aq] prefixed task is analogous to a \f[C]\&.PHONY\f[] target 167 | in make. 168 | Any do file can also be run as a task by invoking \[aq]redo\[aq] with 169 | the \[aq]\-task\[aq] flag. 170 | .SH ENVIRONMENT VARIABLES 171 | .PP 172 | The \-verbose variable can be set with the environment variable 173 | \f[C]REDO_VERBOSE\f[]. 174 | The value of the variable is not relevant, but its length corresponds to 175 | the intensity of verbosity. 176 | For example, \f[C]REDO_VERBOSE=xx\f[] is comparable to invoking redo 177 | with the arguments \[aq]\-verbose \-verbose\[aq]. 178 | .PP 179 | The \-sh variable can be set with the environment variable 180 | \f[C]REDO_SHELL_ARGS\f[]. 181 | This can be used to pass the \[aq]\-v\[aq] or \[aq]\-x\[aq] options, 182 | among others, to the shell (/bin/sh). 183 | redo prepends a \[aq]\-\[aq] to the variable if necessary, so 184 | \[aq]\-xv\[aq] could also be specified as \[aq]xv\[aq] 185 | .PP 186 | The \-debug option can be set with the environment variable 187 | \f[C]REDO_DEBUG\f[]. 188 | The value is not relevant, merely its presence. 189 | \f[C]REDO_DEBUG=true\f[] works fine. 190 | .PP 191 | The \f[C]REDO_TMP_DIR\f[] environment variable, which does not have a 192 | corresponding flag, can be set to control where redo creates temporary 193 | output files. 194 | The specified directory must exist and be writable to the redo process. 195 | This may be useful if /tmp is mounted on a fast device such as a ram 196 | disk or solid state drive (SSD). 197 | .SH AUTHORS 198 | Gyepi Sam. 199 | -------------------------------------------------------------------------------- /doc/redo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | redo(1) Redux User Manual 10 | 11 | 12 | 13 | 18 | 19 |

NAME

20 |

redo - Builds files atomically.

21 |

SYNOPSIS

22 |

redux redo [OPTION]... [TARGET]...

23 |

OPTIONS

24 |

-? Show help

25 |

-d Alias for debug

26 |

-debug

27 |
    Print debugging output.
28 |

-h Show help

29 |

-help

30 |
    Show help
31 |

-sh string

32 |
    Extra arguments for /bin/sh.
33 |

-task

34 |
    Run .do script for side effects and ignore output.
35 |

-v Alias for verbose

36 |

-verbose

37 |
    Be verbose. Repeat for intensity.
38 |

NOTES

39 |

The redo command builds files atomically by running a do script asssociated with the target.

40 |

redo normally requires one or more target arguments. If no target arguments are provided, redo runs the default target @all in the current directory if its do script @all.do exists.

41 |

DESCRIPTION

42 |

This command can be invoked as redux redo or, through a link, as redo.

43 |

A redo target is produced by an sh script whose name has a '.do' file extension and, as such, is called a do script.

44 |

For a given target named target, the corresponding do file may be named, in order of decreasing specificity, target.do or default.do.

45 |

For a given target named target.ext, the corresponding do file may be named, in order of decreasing specificity, target.ext.do, default.ext.do or, finally, default.do.

46 |

For targets with multiple extensions, the corresponding do files may be named, in order of decreasing specificity, target.ext.do where ext is the full extension, followed by default do files with shorter and shorter suffixes of the extension, finally ending in default.do. For example, the target file.x.y.z results in a search for the following do scripts:

47 |

file.x.y.z.do default.x.y.z.do default.y.z.do default.z.do default.do

48 |

Jumping ahead slightly, note that in the case of multiple extensions, the $2 argument to the do script contains all but one extension. In the example above, $2 would be file.x.y for all do files. Doing otherwise causes the $2 argument to depend on the do file used.

49 |

Redo searches for each of these script files, in order of specificity, starting in the target's directory and moving into parent directories. The search stops when a script is found or when the project root directory has been unsuccessfully searched.

50 |

In the latter case, if the target file exists on disk, it is taken to be a source file, not generated by script and its metadata is stored in the database. The file will be subsequently watched for changes.

51 |

In the former case, where the script is found, it is assumed to be an sh script and executed with three arguments:

52 |

$1 = path to target, relative to do script directory $2 = same as $1, with a single file extension, if any, removed $3 = temporary file name

53 |

The script is executed by /bin/sh with the current working directory (cwd) set to its directory and with stdout opened to a temporary file (which is unnamed and different from $3). It is normally expected to produce output on stdout or write to the file specified by its $3 parameter. It is an error for a script to write to both outputs.

54 |

If the script completes successfully, redo chooses the correct output, renames the temporary file to the target file and updates its database with the new file's metadata record. Since only one of the two temporary files can have content, redo has no trouble selecting the correct one. Conversely, if neither file has content, then either is a valid candidate.

55 |

In the do file, which is an sh script, the line

56 |
redo-ifchange A B C
57 |

specifies the files A, B, and C as prerequisites for the target file.

58 |

Similarly, a call to

59 |
redo-ifcreate A
60 |

specifies that the target should be rebuilt when the non-existent file A appears or is deleted.

61 |

As a special case, a do file whose name is prefixed with '@' is run for side effect. redux does not create a temporary file when running such a file and uses '/dev/stdout' as the output file name so its output is visible but is otherwise not saved. While the $3 parameter is provided for consistency, it is an error for a task script to write to it since its output is discarded.

62 |

A call to redo without an argument will search for a file named @all.do in the current directory.

63 |

A '@' prefixed task is analogous to a .PHONY target in make. Any do file can also be run as a task by invoking 'redo' with the '-task' flag.

64 |

ENVIRONMENT VARIABLES

65 |

The -verbose variable can be set with the environment variable REDO_VERBOSE. The value of the variable is not relevant, but its length corresponds to the intensity of verbosity. For example, REDO_VERBOSE=xx is comparable to invoking redo with the arguments '-verbose -verbose'.

66 |

The -sh variable can be set with the environment variable REDO_SHELL_ARGS. This can be used to pass the '-v' or '-x' options, among others, to the shell (/bin/sh). redo prepends a '-' to the variable if necessary, so '-xv' could also be specified as 'xv'

67 |

The -debug option can be set with the environment variable REDO_DEBUG. The value is not relevant, merely its presence. REDO_DEBUG=true works fine.

68 |

The REDO_TMP_DIR environment variable, which does not have a corresponding flag, can be set to control where redo creates temporary output files. The specified directory must exist and be writable to the redo process. This may be useful if /tmp is mounted on a fast device such as a ram disk or solid state drive (SSD).

69 | 70 | 71 | -------------------------------------------------------------------------------- /doc/redo.md: -------------------------------------------------------------------------------- 1 | % redo(1) Redux User Manual 2 | % Gyepi Sam 3 | % October 03, 2018 4 | 5 | 6 | 7 | 8 | # NAME 9 | 10 | redo - Builds files atomically. 11 | 12 | # SYNOPSIS 13 | 14 | redux redo [OPTION]... [TARGET]... 15 | 16 | # OPTIONS 17 | 18 | -? Show help 19 | 20 | -d Alias for debug 21 | 22 | -debug 23 | 24 | Print debugging output. 25 | -h Show help 26 | 27 | -help 28 | 29 | Show help 30 | -sh string 31 | 32 | Extra arguments for /bin/sh. 33 | -task 34 | 35 | Run .do script for side effects and ignore output. 36 | -v Alias for verbose 37 | 38 | -verbose 39 | 40 | Be verbose. Repeat for intensity. 41 | 42 | 43 | # NOTES 44 | 45 | 46 | The redo command builds files atomically by running a do script asssociated with the target. 47 | 48 | redo normally requires one or more target arguments. 49 | If no target arguments are provided, redo runs the default target @all in the current directory 50 | if its do script @all.do exists. 51 | 52 | 53 | # DESCRIPTION 54 | 55 | This command can be invoked as `redux redo` or, through a link, as `redo`. 56 | 57 | A redo target is produced by an sh script whose name has a '.do' file extension 58 | and, as such, is called a do script. 59 | 60 | For a given target named `target`, the corresponding do file may be named, 61 | in order of decreasing specificity, `target.do` or `default.do`. 62 | 63 | For a given target named `target.ext`, the corresponding do file may be named, 64 | in order of decreasing specificity, `target.ext.do`, `default.ext.do` or, finally, `default.do`. 65 | 66 | For targets with multiple extensions, the corresponding do files may be named, 67 | in order of decreasing specificity, `target.ext.do` where `ext` is the full extension, 68 | followed by default do files with shorter and shorter suffixes of the extension, finally 69 | ending in `default.do`. For example, the target `file.x.y.z` results in a search for 70 | the following do scripts: 71 | 72 | file.x.y.z.do 73 | default.x.y.z.do 74 | default.y.z.do 75 | default.z.do 76 | default.do 77 | 78 | Jumping ahead slightly, note that in the case of multiple extensions, the $2 argument 79 | to the do script contains all but one extension. In the example above, $2 would be `file.x.y` for all do 80 | files. Doing otherwise causes the $2 argument to depend on the do file used. 81 | 82 | Redo searches for each of these script files, in order of specificity, starting in the target's directory 83 | and moving into parent directories. The search stops when a script is found or when the project 84 | root directory has been unsuccessfully searched. 85 | 86 | In the latter case, if the target file exists on disk, it is taken to be a source file, 87 | not generated by script and its metadata is stored in the database. 88 | The file will be subsequently watched for changes. 89 | 90 | In the former case, where the script is found, it is assumed to be an sh script and executed with three arguments: 91 | 92 | $1 = path to target, relative to do script directory 93 | $2 = same as $1, with a single file extension, if any, removed 94 | $3 = temporary file name 95 | 96 | The script is executed by /bin/sh with the current working directory (cwd) set to its directory 97 | and with stdout opened to a temporary file (which is unnamed and different from $3). 98 | It is normally expected to produce output on stdout or write to the file specified by its $3 parameter. 99 | It is an error for a script to write to both outputs. 100 | 101 | If the script completes successfully, redo chooses the correct output, renames the temporary file 102 | to the target file and updates its database with the new file's metadata record. 103 | Since only one of the two temporary files can have content, redo has no trouble selecting the correct one. 104 | Conversely, if neither file has content, then either is a valid candidate. 105 | 106 | In the do file, which is an sh script, the line 107 | 108 | redo-ifchange A B C 109 | 110 | specifies the files A, B, and C as prerequisites for the target file. 111 | 112 | Similarly, a call to 113 | 114 | redo-ifcreate A 115 | 116 | specifies that the target should be rebuilt when the non-existent file A appears or is deleted. 117 | 118 | As a special case, a do file whose name is prefixed with '@' is run for 119 | side effect. redux does not create a temporary file when running such 120 | a file and uses '/dev/stdout' as the output file name so its 121 | output is visible but is otherwise not saved. 122 | While the $3 parameter is provided for consistency, it is an error for a task script 123 | to write to it since its output is discarded. 124 | 125 | A call to redo without an argument will search for a file named `@all.do` 126 | in the current directory. 127 | 128 | A '@' prefixed task is analogous to a `.PHONY` target in make. 129 | Any do file can also be run as a task by invoking 'redo' with the '-task' flag. 130 | 131 | # ENVIRONMENT VARIABLES 132 | 133 | The -verbose variable can be set with the environment variable `REDO_VERBOSE`. 134 | The value of the variable is not relevant, but its length corresponds to the 135 | intensity of verbosity. For example, `REDO_VERBOSE=xx` is comparable 136 | to invoking redo with the arguments '-verbose -verbose'. 137 | 138 | The -sh variable can be set with the environment variable `REDO_SHELL_ARGS`. 139 | This can be used to pass the '-v' or '-x' options, among others, to the shell (/bin/sh). 140 | redo prepends a '-' to the variable if necessary, so '-xv' could also be specified as 'xv' 141 | 142 | The -debug option can be set with the environment variable `REDO_DEBUG`. 143 | The value is not relevant, merely its presence. `REDO_DEBUG=true` works fine. 144 | 145 | The `REDO_TMP_DIR` environment variable, which does not have a corresponding flag, can be set 146 | to control where redo creates temporary output files. The specified directory must exist and be writable 147 | to the redo process. This may be useful if /tmp is mounted on a fast device such as a ram disk 148 | or solid state drive (SSD). 149 | -------------------------------------------------------------------------------- /doc/redo.txt: -------------------------------------------------------------------------------- 1 | # DESCRIPTION 2 | 3 | This command can be invoked as `redux redo` or, through an installed symlink, as `redo`. 4 | 5 | A redo target is produced by an sh script whose name has a '.do' file extension 6 | and, as such, is called a do script. 7 | 8 | For a given target named `target`, the corresponding do file may be named, 9 | in order of decreasing specificity, `target.do` or `default.do`. 10 | 11 | For a given target named `target.ext`, the corresponding do file may be named, 12 | in order of decreasing specificity, `target.ext.do`, `default.ext.do` or, finally, `default.do`. 13 | 14 | For targets with multiple extensions, the corresponding do files may be named, 15 | in order of decreasing specificity, `target.ext.do` where `ext` is the full extension, 16 | followed by default do files with shorter and shorter suffixes of the extension, finally 17 | ending in `default.do`. For example, the target `file.x.y.z` results in a search for 18 | the following do scripts: 19 | 20 | file.x.y.z.do 21 | default.x.y.z.do 22 | default.y.z.do 23 | default.z.do 24 | default.do 25 | 26 | Redo searches for each of these script files, in order of specificity, starting in the target's directory 27 | and moving into parent directories. The search stops when a script is found or when the project 28 | root directory has been unsuccessfully searched. 29 | 30 | If the do script is not found but the target file exists on disk, it is 31 | considered a source file not generated by script and its metadata is 32 | stored in the database. The file will be subsequently watched for changes. 33 | 34 | In the case where the script is found, it is assumed to be an sh script and executed with three arguments: 35 | 36 | $1 = path to target, relative to do script directory 37 | $2 = target basename, also relative to do script directory 38 | $3 = temporary output file name 39 | 40 | For compatability with existing do scripts, the value of `$2` depends 41 | on which do script is used as well as on file extensions. 42 | 43 | 1. If a target uses a specific do script, `$2` is the same as the target 44 | 2. If a target uses a default script, `$2` then more specific scripts generate 45 | shorter values for `$2`. 46 | 47 | This chart shows how target and do filenames interact to produce $2. 48 | 49 | Target Do Arg2 50 | blenny blenny.do blenny 51 | blenny default.do blenny 52 | 53 | blenny.a blenny.a.do blenny.a 54 | blenny.a default.a.do blenny 55 | blenny.a default.do blenny.a 56 | 57 | blenny.a.b blenny.a.b.do blenny.a.b 58 | blenny.a.b default.a.b.do blenny 59 | blenny.a.b default.b.do blenny.a 60 | blenny.a.b default.do blenny.a.b 61 | 62 | etc... 63 | 64 | The script is executed by /bin/sh with the current working directory (cwd) set to its directory 65 | and with stdout opened to a temporary file (which is unnamed and different from $3). 66 | It is normally expected to produce output on stdout or write to the file specified by its $3 parameter. 67 | It is an error for a script to write to both outputs. 68 | 69 | If the script completes successfully, redo chooses the correct output, renames the temporary file 70 | to the target file and updates its database with the new file's metadata record. 71 | Since only one of the two temporary files can have content, redo has no trouble selecting the correct one. 72 | Conversely, if neither file has content, then either is a valid candidate. 73 | 74 | In the do file, which is an sh script, the line 75 | 76 | redo-ifchange A B C 77 | 78 | specifies the files A, B, and C as prerequisites for the target file. 79 | 80 | Similarly, a call to 81 | 82 | redo-ifcreate A 83 | 84 | specifies that the target should be rebuilt when the non-existent file A appears or is deleted. 85 | 86 | As a special case, a do file whose name is prefixed with '@' is run for 87 | side effect. redux does not create a temporary file when running such 88 | a file and uses '/dev/stdout' as the output file name so its 89 | output is visible but is otherwise not saved. 90 | While the $3 parameter is provided for consistency, it is an error for a task script 91 | to write to it since its output is discarded. 92 | 93 | A call to redo without an argument will search for a file named `@all.do` 94 | in the current directory. 95 | 96 | A '@' prefixed task is analogous to a `.PHONY` target in make. 97 | Any do file can also be run as a task by invoking 'redo' with the '-task' flag. 98 | 99 | # ENVIRONMENT VARIABLES 100 | 101 | The -verbose variable can be set with the environment variable `REDO_VERBOSE`. 102 | The value of the variable is not relevant, but its length corresponds to the 103 | intensity of verbosity. For example, `REDO_VERBOSE=xx` is comparable 104 | to invoking redo with the arguments '-verbose -verbose'. 105 | 106 | The -sh variable can be set with the environment variable `REDO_SHELL_ARGS`. 107 | This can be used to pass the '-v' or '-x' options, among others, to the shell (/bin/sh). 108 | redo prepends a '-' to the variable if necessary, so '-xv' could also be specified as 'xv' 109 | 110 | The -debug option can be set with the environment variable `REDO_DEBUG`. 111 | The value is not relevant, merely its presence. `REDO_DEBUG=true` works fine. 112 | 113 | The `REDO_TMP_DIR` environment variable, which does not have a corresponding flag, can be set 114 | to control where redo creates temporary output files. The specified directory must exist and be writable 115 | to the redo process. This may be useful if /tmp is mounted on a fast device such as a ram disk 116 | or solid state drive (SSD). 117 | -------------------------------------------------------------------------------- /dofile.go: -------------------------------------------------------------------------------- 1 | package redux 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gyepisam/fileutils" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | /* 14 | findDofile searches for the most specific .do file for the target and returns a DoInfo structure. 15 | The structure's Missing field contains paths to more specific .do files, if any, that were not found. 16 | If a file is found the structure's Name and Arg2 fields are also set appropriately. 17 | */ 18 | func (f *File) findDoFile() (*DoInfo, error) { 19 | 20 | relPath := &RelPath{} 21 | var missing []string 22 | 23 | dir := f.Dir 24 | candidates := f.DoInfoCandidates() 25 | 26 | TOP: 27 | for { 28 | 29 | for _, do := range candidates { 30 | path := filepath.Join(dir, do.Name) 31 | exists, err := fileutils.FileExists(path) 32 | f.Debug("%s %t %v\n", path, exists, err) 33 | if err != nil { 34 | return nil, err 35 | } else if exists { 36 | do.Dir = dir 37 | do.RelDir = relPath.Join() 38 | do.Missing = missing 39 | return do, nil 40 | } 41 | 42 | missing = append(missing, path) 43 | } 44 | 45 | if dir == f.RootDir { 46 | break TOP 47 | } 48 | relPath.Add(filepath.Base(dir)) 49 | dir = filepath.Dir(dir) 50 | } 51 | 52 | return &DoInfo{Missing: missing}, nil 53 | } 54 | 55 | const shell = "/bin/sh" 56 | 57 | // RunDoFile executes the do file script, records the metadata for the resulting output, then 58 | // saves the resulting output to the target file, if applicable. 59 | // The execution is equivalent to: 60 | // sh target.ext.do target.ext target outfn > out0 61 | // A well behaved .do file writes to stdout (out0) or to the $3 file (outfn), but not both. 62 | func (target *File) RunDoFile(doInfo *DoInfo) (err error) { 63 | 64 | // out0 is an open file connected to subprocess stdout 65 | // However, a task subprocess, meaning it is run for side effects, 66 | // emits output to stdout. 67 | var out0 *Output 68 | 69 | if target.IsTask() { 70 | out0 = NewOutput(os.Stdout) 71 | } else { 72 | out0, err = target.NewOutput() 73 | if err != nil { 74 | return 75 | } 76 | defer out0.Cleanup() 77 | } 78 | 79 | // outfn is the arg3 filename argument to the do script. 80 | var outfn *Output 81 | 82 | outfn, err = target.NewOutput() 83 | if err != nil { 84 | return 85 | } 86 | defer outfn.Cleanup() 87 | 88 | // Arg3 file should not exist prior to script execution 89 | // so its subsequent existence can be significant. 90 | if err = outfn.SetupArg3(); err != nil { 91 | return 92 | } 93 | 94 | if err = target.runCmd(out0.File, outfn.Name(), doInfo); err != nil { 95 | return 96 | } 97 | 98 | file, err := os.Open(outfn.Name()) 99 | if err != nil { 100 | if os.IsNotExist(err) { 101 | if target.IsTask() { 102 | return nil 103 | } 104 | } else { 105 | return 106 | } 107 | } 108 | 109 | if target.IsTask() { 110 | // Task files should not write to the temp file. 111 | return target.Errorf("Task do file %s unexpectedly wrote to $3", target.DoFile) 112 | } 113 | 114 | if err = out0.Close(); err != nil { 115 | return 116 | } 117 | 118 | outputs := make([]*Output, 0) 119 | 120 | finfo, err := os.Stat(out0.Name()) 121 | if err != nil { 122 | return 123 | } 124 | if finfo.Size() > 0 { 125 | outputs = append(outputs, out0) 126 | } 127 | 128 | if file != nil { 129 | outfn.File = file // for consistency 130 | if err = outfn.Close(); err != nil { 131 | return 132 | } 133 | outputs = append(outputs, outfn) 134 | } 135 | 136 | if n := len(outputs); n == 0 { 137 | return target.Errorf("Do file %s generated no output or file activity", target.DoFile) 138 | } else if n == 2 { 139 | return target.Errorf("Do file %s wrote to stdout and to file $3", target.DoFile) 140 | } 141 | 142 | out := outputs[0] 143 | err = os.Rename(out.Name(), target.Fullpath()) 144 | if err != nil && strings.Index(err.Error(), "cross-device") > -1 { 145 | 146 | // The rename failed due to a cross-device error because the output file 147 | // tmp dir is on a different device from the target file. 148 | // Copy the tmp file across the device to the target directory and try again. 149 | var path string 150 | path, err = out.Copy(target.Dir) 151 | if err != nil { 152 | return 153 | } 154 | 155 | err = os.Rename(path, target.Fullpath()) 156 | if err != nil { 157 | _ = os.Remove(path) 158 | } 159 | } 160 | 161 | return 162 | } 163 | 164 | func (target *File) runCmd(out0 *os.File, outfn string, doInfo *DoInfo) error { 165 | 166 | args := []string{"-e"} 167 | 168 | if ShellArgs != "" { 169 | if ShellArgs[0] != '-' { 170 | ShellArgs = "-" + ShellArgs 171 | } 172 | args = append(args, ShellArgs) 173 | } 174 | 175 | pending := os.Getenv("REDO_PENDING") 176 | pendingID := ";" + string(target.FullPathHash) 177 | target.Debug("Current: [%s]. Pending: [%s].\n", pendingID, pending) 178 | 179 | if strings.Contains(pending, pendingID) { 180 | return fmt.Errorf("Loop detected on pending target: %s", target.Target) 181 | } 182 | 183 | pending += pendingID 184 | 185 | relTarget := doInfo.RelPath(target.Name) 186 | args = append(args, doInfo.Name, relTarget, doInfo.RelPath(doInfo.Arg2), outfn) 187 | 188 | target.Debug("@sh %s $3\n", strings.Join(args[0:len(args)-1], " ")) 189 | 190 | cmd := exec.Command(shell, args...) 191 | cmd.Dir = doInfo.Dir 192 | cmd.Stdout = out0 193 | cmd.Stderr = os.Stderr 194 | 195 | depth := 0 196 | if i64, err := strconv.ParseInt(os.Getenv("REDO_DEPTH"), 10, 32); err == nil { 197 | depth = int(i64) 198 | } 199 | 200 | parent := os.Getenv("REDO_PARENT") 201 | 202 | // Add environment variables, replacing existing entries if necessary. 203 | cmdEnv := os.Environ() 204 | env := map[string]string{ 205 | "REDO_PARENT": relTarget, 206 | "REDO_DEPTH": strconv.Itoa(depth + 1), 207 | "REDO_PENDING": pending, 208 | } 209 | 210 | // Update environment values if they exist and append when they dont. 211 | TOP: 212 | for key, value := range env { 213 | prefix := key + "=" 214 | for i, entry := range cmdEnv { 215 | if strings.HasPrefix(entry, prefix) { 216 | cmdEnv[i] = prefix + value 217 | continue TOP 218 | } 219 | } 220 | cmdEnv = append(cmdEnv, prefix+value) 221 | } 222 | 223 | cmd.Env = cmdEnv 224 | 225 | if Verbose() { 226 | prefix := strings.Repeat(" ", depth) 227 | if parent != "" { 228 | prefix += parent + " => " 229 | } 230 | target.Log("%s%s (%s)\n", prefix, target.Rel(target.Fullpath()), target.Rel(doInfo.Path())) 231 | } 232 | 233 | err := cmd.Run() 234 | if err == nil { 235 | return nil 236 | } 237 | 238 | if Verbose() { 239 | return target.Errorf("%s %s: %s", shell, strings.Join(args, " "), err) 240 | } 241 | 242 | return target.Errorf("%s", err) 243 | } 244 | -------------------------------------------------------------------------------- /doinfo.go: -------------------------------------------------------------------------------- 1 | package redux 2 | 3 | import ( 4 | "path/filepath" 5 | ) 6 | 7 | // A DoInfo represents an active do file. 8 | type DoInfo struct { 9 | Dir string 10 | Name string 11 | Arg2 string //do file arg2. Depends on target and do file names. 12 | RelDir string //relative directory to target from do script. 13 | Missing []string //more specific do scripts that were not found. 14 | } 15 | 16 | func (do *DoInfo) Path() string { 17 | return filepath.Join(do.Dir, do.Name) 18 | } 19 | 20 | func (do *DoInfo) RelPath(path string) string { 21 | return filepath.Join(do.RelDir, path) 22 | } 23 | -------------------------------------------------------------------------------- /encoding.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | import ( 8 | "encoding/json" 9 | "path/filepath" 10 | ) 11 | 12 | func decodePrerequisite(b []byte) (Prerequisite, error) { 13 | var p Prerequisite 14 | return p, json.Unmarshal(b, &p) 15 | } 16 | 17 | func decodeDependent(b []byte) (Dependent, error) { 18 | var d Dependent 19 | return d, json.Unmarshal(b, &d) 20 | } 21 | 22 | func (f *File) AsDependent(dir string) Dependent { 23 | relpath, err := filepath.Rel(dir, f.Fullpath()) 24 | if err != nil { 25 | panic(err) 26 | } 27 | return Dependent{Path: relpath} 28 | } 29 | 30 | func (f *File) AsPrerequisite(dir string, m *Metadata) Prerequisite { 31 | relpath, err := filepath.Rel(dir, f.Fullpath()) 32 | if err != nil { 33 | panic(err) 34 | } 35 | return Prerequisite{Path: relpath, Metadata: m} 36 | } 37 | 38 | // Get returns a database record decoded into the specified type. 39 | func (f *File) Get(key string, obj interface{}) (bool, error) { 40 | data, found, err := f.db.Get(key) 41 | defer f.Debug("@Get %s -> %t ...\n", key, found) 42 | if err == nil && found { 43 | err = json.Unmarshal(data, &obj) 44 | } 45 | return found, err 46 | } 47 | 48 | // Put stores a database record. 49 | func (f *File) Put(key string, obj interface{}) (err error) { 50 | defer f.Debug("@Put %s -> %s\n", key, err) 51 | b, err := json.Marshal(obj) 52 | if err != nil { 53 | return err 54 | } 55 | return f.db.Put(key, b) 56 | } 57 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | // Errorf formats errors for the current file. 13 | func (f *File) Errorf(format string, args ...interface{}) error { 14 | return errors.New(fmt.Sprintf("%s: ", f.Target) + fmt.Sprintf(format, args...)) 15 | } 16 | 17 | // ErrUninitialized denotes an uninitialized directory. 18 | func (f *File) ErrUninitialized() error { 19 | return f.Errorf("cannot find redo root directory") 20 | } 21 | 22 | // ErrNotFound is used when the current file is expected to exists and does not. 23 | func (f *File) ErrNotFound(m string) error { 24 | return f.Errorf("file not found at %s", m) 25 | } 26 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | /* 8 | * An Event denotes a state change upon which dependencies are based. 9 | * The line "redo-ifchange B" in the file A.do creates a prerequisite 10 | * from A to B based on the ifchange event. 11 | */ 12 | 13 | // Event names a change or create event 14 | type Event string 15 | 16 | // Events of note 17 | const ( 18 | IFCREATE Event = "ifcreate" 19 | IFCHANGE Event = "ifchange" 20 | 21 | AUTO_IFCREATE Event = AUTO + KEY_SEPARATOR + "ifcreate" 22 | AUTO_IFCHANGE Event = AUTO + KEY_SEPARATOR + "ifchange" 23 | ) 24 | 25 | // String representation. 26 | func (event Event) String() string { 27 | return string(event) 28 | } 29 | -------------------------------------------------------------------------------- /examples/hello-world-0/@clean.do: -------------------------------------------------------------------------------- 1 | rm -f hello *~ -------------------------------------------------------------------------------- /examples/hello-world-0/@hello.do: -------------------------------------------------------------------------------- 1 | bin=${1#@} 2 | redo-ifchange $bin 3 | ./$bin -------------------------------------------------------------------------------- /examples/hello-world-0/README.md: -------------------------------------------------------------------------------- 1 | 2 | # A hello world example for redux 3 | 4 | Redux is a top down software build tool. The best way to explain it is to build something with it. 5 | In this example, we are going to build a `hello world` program in C. We'll assume that you have read the 6 | documentation for redux and know some C, but the example should be useful in any case. If you are reading 7 | this from a source directory, you should have all the accompanying files and can play with them. If you are 8 | not, the source files are available [here](https://github.com/gyepisam/blob/master/doc/examples/hello-world-0) 9 | 10 | 11 | Our basic C hello world program, which consists of a couple of headers and a function, looks like this: 12 | 13 | /* This program prints a greeting */ 14 | 15 | #include 16 | #include 17 | 18 | int main(int argc, char *argv[]) { 19 | fprintf(stdout, "hello world\n"); 20 | exit(0); 21 | } 22 | 23 | The usual command to build this program is 24 | 25 | $ cc -o hello hello.c 26 | 27 | Which you normally type at the command line. 28 | 29 | However, redux is a software build tool so we'll automate the process. First, before we can use redux in a project, 30 | we have to tell redux that we are doing so. The command 31 | 32 | $ redo-init 33 | 34 | Initializes the project directory by creating a '.redo' directory in it. This command only needs 35 | to be run once. It does no harm if you run it multiple times, and redux will remind you if you 36 | forget to run it. 37 | 38 | Now that we've initialized, we can continue with the automation. 39 | In redux, the script to create a target `x` is called `x.do` so we'll create the file `hello.do`, 40 | which contains almost the same command as before. 41 | 42 | redo-ifchange $2.c 43 | cc -o $3 $2.c 44 | 45 | It is worth explaining why this looks different. 46 | First, we've added the line `redo-ifchange $2.c`, which says that if hello.c changes, 47 | hello needs to be rebuilt. In this case we are asking redo-ifchange to note that hello.c 48 | is a prerequisite for hello, which is what we want. 49 | 50 | redux executes the redo script with three arguments. 51 | 52 | * Argument $1 contains the name of the `target` which was given to redo. 53 | 54 | If you say `redo xyz.txt`, the $1 argument is xyz.txt, assuming, of course that there is a build script for it. 55 | 56 | * Argument $2 is the same as $1 but without the extension. 57 | In our example, it would be 'xyz'. 58 | 59 | * The $3 argument contains the name of a temporary file that redux creates to hold the output of of the script. 60 | So, in our script here, the '$2' argument contains the name of the target, without its extension, 61 | which is 'hello'. To this, we append a '.c' to create the name 'hello.c' and we are back at our source file. 62 | 63 | So, knowing all that, we can see that the second line invokes the c compiler on hello.c ($2.c) and sends the output to the 64 | temporary file $3. If the command completes successfuly, redo will move the temporary output to the correct name. 65 | In this case, we have to specify an output path to the compiler because we don't normally spew compiler output to stdout. 66 | We could, but we don't so we send it to a file instead. However, many other programs happily send their output to stdout, 67 | and in that instance, they do not need an output specification when used in a do script. redux will redirect their output 68 | to the correct location. While on the topic, it is important to note that a script cannot write to stdout *and* to the $3 file. 69 | That is an error and would make redux complain. This restriction exists so redux can determine which output represents the 70 | target. 71 | 72 | Also, it is important to remember that do scripts *are* shell scripts. In fact, they are run with /bin/sh. 73 | As such, you can use any shell construct you find necessary to get the job done. We could just as easily have written 74 | 75 | redo-ifchange hello.c 76 | cc -o $3 hello.c 77 | 78 | However, redo scripts are reusable, and using variable names makes it possible 79 | to do so. BTW, the argument the $3 argument is necessary because redux guarantees that the target 80 | file is never left in an incomplete state and uses temporary files to implement the guarantee. 81 | However, this means that the do script must either write to stdout *or* to $3. Yes, that was worth repeating. 82 | 83 | Now, if you type 84 | 85 | $ redo hello 86 | 87 | redux will run the do script and create the file for you. If you had, instead, used the command 88 | 89 | $ redo -v hello 90 | 91 | redux would have emitted some output in the process. In general, redux is quiet and unobtrusive. 92 | It will do its job quietly and only speak up when asked or when something goes wrong. If you don't 93 | see any error messages, it means there were none. 94 | 95 | Now, you can run the hello program to see the expected output. 96 | 97 | $ ./hello 98 | 99 | We can automate even this step! In redux, there are two kinds of do scripts. 100 | Regular do scripts are expected to generate output. Task scripts are not, and 101 | are, instead, run for their side effects. 102 | 103 | So we can create a script to run hello world program for us. 104 | A task script is usually named with an '@' prefix, so our file is named '@hello.do' and contains: 105 | 106 | bin=${1#@} 107 | redo-ifchange $bin 108 | ./$bin 109 | 110 | This file is worth explaining. First, the line 111 | 112 | bin=${1#@} 113 | 114 | is an sh construct that says, set the variable bin to the value of argument $1, but without the '@' prefix. 115 | The '#' is part of a small family of shell substitution commands, all of which are worth knowing. 116 | 117 | The prefix exists in the argument because our do file is called @hello.do and will be invoked with the command 118 | 'redo @hello' at which point, redo will pass the target, @hello, as argument $1. 119 | 120 | We assign the value to bin because it is used twice. On general principle, it makes sense to save the result 121 | of a computation and refer to it rather than repeating the computation. In the first reference, we add a dependency 122 | on the target and in the second, we run it. Now, if we say 123 | 124 | $ redo @hello 125 | 126 | We see the expected output again. Note the chain of dependencies: the target @hello depends on hello, 127 | which itself depends on hello.c. The chain is built incrementally, but redux is able to trace them. 128 | If we delete the 'hello' file or edit the hello.c file, we can actually see redux following the dependencies 129 | all the way through. With the make program, you could touch a file to mark it as changed. However, redux uses the file 130 | content and not the modification time as a change indicator so that wouldn't work. 131 | 132 | Obviously, we can do that with a simple rm command. However, this presents an opportunity to create the @clean target. 133 | This target's purpose is to cleanup. If you know make, this would be equivalent to the 'clean' dependency in make. 134 | The file contains the usual clean incantations; we want to delete the file we build, along with any editor flotsam. 135 | 136 | rm -f hello *~ 137 | 138 | Now, we can say 139 | 140 | $ redo @clean 141 | 142 | Next, we say 143 | 144 | $ redo -v @hello 145 | 146 | and see that redux first built the hello file before running the @hello target to produce the greeting. BTW, the name @hello.do 147 | is not special, we could just as well have named it @greeting.do. However, the name hello.do *is* special because it produces 148 | the hello file. Because task scripts are not expected to produce output, they can be named anything. Regular scripts, however, 149 | must be named after their output file. 150 | 151 | To recap, in this example, we created a simple hello world project that uses redux in order to show the basic steps of process 152 | and used redo-init, redo-ifchange and redo. The final command, redo-ifcreate, is not as common and can be left off until later. 153 | The only other useful item to know about are default do files. However, that is covered in the redo documentation and I really 154 | want you to read it, so I'll point you [there](https://github.com/gyepisam/redux/blob/master/doc/redo.md) 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /examples/hello-world-0/README.txt: -------------------------------------------------------------------------------- 1 | # A hello world example for redux 2 | 3 | Redux is a top down software build tool. The best way to explain it is to build something with it. 4 | In this example, we are going to build a `hello world` program in C. We'll assume that you have read the 5 | documentation for redux and know some C, but the example should be useful in any case. If you are reading 6 | this from a source directory, you should have all the accompanying files and can play with them. If you are 7 | not, the source files are available [here](https://github.com/gyepisam/blob/master/doc/examples/hello-world-0) 8 | 9 | 10 | Our basic C hello world program, which consists of a couple of headers and a function, looks like this: 11 | 12 | .pull hello.c,code 13 | 14 | The usual command to build this program is 15 | 16 | $ cc -o hello hello.c 17 | 18 | Which you normally type at the command line. 19 | 20 | However, redux is a software build tool so we'll automate the process. First, before we can use redux in a project, 21 | we have to tell redux that we are doing so. The command 22 | 23 | $ redo-init 24 | 25 | Initializes the project directory by creating a '.redo' directory in it. This command only needs 26 | to be run once. It does no harm if you run it multiple times, and redux will remind you if you 27 | forget to run it. 28 | 29 | Now that we've initialized, we can continue with the automation. 30 | In redux, the script to create a target `x` is called `x.do` so we'll create the file `hello.do`, 31 | which contains almost the same command as before. 32 | 33 | .pull hello.do,code 34 | 35 | It is worth explaining why this looks different. 36 | First, we've added the line `redo-ifchange $2.c`, which says that if hello.c changes, 37 | hello needs to be rebuilt. In this case we are asking redo-ifchange to note that hello.c 38 | is a prerequisite for hello, which is what we want. 39 | 40 | redux executes the redo script with three arguments. 41 | 42 | * Argument $1 contains the name of the `target` which was given to redo. 43 | 44 | If you say `redo xyz.txt`, the $1 argument is xyz.txt, assuming, of course that there is a build script for it. 45 | 46 | * Argument $2 is the same as $1 but without the extension. 47 | In our example, it would be 'xyz'. 48 | 49 | * The $3 argument contains the name of a temporary file that redux creates to hold the output of of the script. 50 | So, in our script here, the '$2' argument contains the name of the target, without its extension, 51 | which is 'hello'. To this, we append a '.c' to create the name 'hello.c' and we are back at our source file. 52 | 53 | So, knowing all that, we can see that the second line invokes the c compiler on hello.c ($2.c) and sends the output to the 54 | temporary file $3. If the command completes successfuly, redo will move the temporary output to the correct name. 55 | In this case, we have to specify an output path to the compiler because we don't normally spew compiler output to stdout. 56 | We could, but we don't so we send it to a file instead. However, many other programs happily send their output to stdout, 57 | and in that instance, they do not need an output specification when used in a do script. redux will redirect their output 58 | to the correct location. While on the topic, it is important to note that a script cannot write to stdout *and* to the $3 file. 59 | That is an error and would make redux complain. This restriction exists so redux can determine which output represents the 60 | target. 61 | 62 | Also, it is important to remember that do scripts *are* shell scripts. In fact, they are run with /bin/sh. 63 | As such, you can use any shell construct you find necessary to get the job done. We could just as easily have written 64 | 65 | redo-ifchange hello.c 66 | cc -o $3 hello.c 67 | 68 | However, redo scripts are reusable, and using variable names makes it possible 69 | to do so. BTW, the argument the $3 argument is necessary because redux guarantees that the target 70 | file is never left in an incomplete state and uses temporary files to implement the guarantee. 71 | However, this means that the do script must either write to stdout *or* to $3. Yes, that was worth repeating. 72 | 73 | Now, if you type 74 | 75 | $ redo hello 76 | 77 | redux will run the do script and create the file for you. If you had, instead, used the command 78 | 79 | $ redo -v hello 80 | 81 | redux would have emitted some output in the process. In general, redux is quiet and unobtrusive. 82 | It will do its job quietly and only speak up when asked or when something goes wrong. If you don't 83 | see any error messages, it means there were none. 84 | 85 | Now, you can run the hello program to see the expected output. 86 | 87 | $ ./hello 88 | 89 | We can automate even this step! In redux, there are two kinds of do scripts. 90 | Regular do scripts are expected to generate output. Task scripts are not, and 91 | are, instead, run for their side effects. 92 | 93 | So we can create a script to run hello world program for us. 94 | A task script is usually named with an '@' prefix, so our file is named '@hello.do' and contains: 95 | 96 | .pull @hello.do,code 97 | 98 | This file is worth explaining. First, the line 99 | 100 | bin=${1#@} 101 | 102 | is an sh construct that says, set the variable bin to the value of argument $1, but without the '@' prefix. 103 | The '#' is part of a small family of shell substitution commands, all of which are worth knowing. 104 | 105 | The prefix exists in the argument because our do file is called @hello.do and will be invoked with the command 106 | 'redo @hello' at which point, redo will pass the target, @hello, as argument $1. 107 | 108 | We assign the value to bin because it is used twice. On general principle, it makes sense to save the result 109 | of a computation and refer to it rather than repeating the computation. In the first reference, we add a dependency 110 | on the target and in the second, we run it. Now, if we say 111 | 112 | $ redo @hello 113 | 114 | We see the expected output again. Note the chain of dependencies: the target @hello depends on hello, 115 | which itself depends on hello.c. The chain is built incrementally, but redux is able to trace them. 116 | If we delete the 'hello' file or edit the hello.c file, we can actually see redux following the dependencies 117 | all the way through. With the make program, you could touch a file to mark it as changed. However, redux uses the file 118 | content and not the modification time as a change indicator so that wouldn't work. 119 | 120 | Obviously, we can do that with a simple rm command. However, this presents an opportunity to create the @clean target. 121 | This target's purpose is to cleanup. If you know make, this would be equivalent to the 'clean' dependency in make. 122 | The file contains the usual clean incantations; we want to delete the file we build, along with any editor flotsam. 123 | 124 | .pull @clean.do,code 125 | 126 | Now, we can say 127 | 128 | $ redo @clean 129 | 130 | Next, we say 131 | 132 | $ redo -v @hello 133 | 134 | and see that redux first built the hello file before running the @hello target to produce the greeting. BTW, the name @hello.do 135 | is not special, we could just as well have named it @greeting.do. However, the name hello.do *is* special because it produces 136 | the hello file. Because task scripts are not expected to produce output, they can be named anything. Regular scripts, however, 137 | must be named after their output file. 138 | 139 | To recap, in this example, we created a simple hello world project that uses redux in order to show the basic steps of process 140 | and used redo-init, redo-ifchange and redo. The final command, redo-ifcreate, is not as common and can be left off until later. 141 | The only other useful item to know about are default do files. However, that is covered in the redo documentation and I really 142 | want you to read it, so I'll point you [there](https://github.com/gyepisam/redux/blob/master/doc/redo.md) 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /examples/hello-world-0/default.html.do: -------------------------------------------------------------------------------- 1 | # build html from markdown 2 | redo-ifchange $2.md 3 | blackfriday-tool -css github.css $2.md $3 4 | -------------------------------------------------------------------------------- /examples/hello-world-0/default.md.do: -------------------------------------------------------------------------------- 1 | src=$2.txt 2 | redo-ifchange $src 3 | gitdown $src $3 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/hello-world-0/github.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family:Georgia, Palatino, 'Palatino Linotype', Times, 'Times New Roman', serif; 3 | font-size: 14px; 4 | line-height: 1.6; 5 | padding-top: 10px; 6 | padding-bottom: 10px; 7 | background-color: white; 8 | padding: 30px; 9 | width: 85%; 10 | } 11 | 12 | body > *:first-child { 13 | margin-top: 0 !important; } 14 | body > *:last-child { 15 | margin-bottom: 0 !important; } 16 | 17 | a { 18 | color: #4183C4; } 19 | a.absent { 20 | color: #cc0000; } 21 | a.anchor { 22 | display: block; 23 | padding-left: 30px; 24 | margin-left: -30px; 25 | cursor: pointer; 26 | position: absolute; 27 | top: 0; 28 | left: 0; 29 | bottom: 0; } 30 | 31 | h1, h2, h3, h4, h5, h6 { 32 | margin: 20px 0 10px; 33 | padding: 0; 34 | font-weight: bold; 35 | -webkit-font-smoothing: antialiased; 36 | cursor: text; 37 | position: relative; } 38 | 39 | h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor { 40 | background: url("../../images/modules/styleguide/para.png") no-repeat 10px center; 41 | text-decoration: none; } 42 | 43 | h1 tt, h1 code { 44 | font-size: inherit; } 45 | 46 | h2 tt, h2 code { 47 | font-size: inherit; } 48 | 49 | h3 tt, h3 code { 50 | font-size: inherit; } 51 | 52 | h4 tt, h4 code { 53 | font-size: inherit; } 54 | 55 | h5 tt, h5 code { 56 | font-size: inherit; } 57 | 58 | h6 tt, h6 code { 59 | font-size: inherit; } 60 | 61 | h1 { 62 | font-size: 28px; 63 | color: black; } 64 | 65 | h2 { 66 | font-size: 24px; 67 | border-bottom: 1px solid #cccccc; 68 | color: black; } 69 | 70 | h3 { 71 | font-size: 18px; } 72 | 73 | h4 { 74 | font-size: 16px; } 75 | 76 | h5 { 77 | font-size: 14px; } 78 | 79 | h6 { 80 | color: #777777; 81 | font-size: 14px; } 82 | 83 | p, blockquote, ul, ol, dl, li, table, pre { 84 | margin: 15px 0; } 85 | 86 | hr { 87 | background: transparent url("../../images/modules/pulls/dirty-shade.png") repeat-x 0 0; 88 | border: 0 none; 89 | color: #cccccc; 90 | height: 4px; 91 | padding: 0; } 92 | 93 | body > h2:first-child { 94 | margin-top: 0; 95 | padding-top: 0; } 96 | body > h1:first-child { 97 | margin-top: 0; 98 | padding-top: 0; } 99 | body > h1:first-child + h2 { 100 | margin-top: 0; 101 | padding-top: 0; } 102 | body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child { 103 | margin-top: 0; 104 | padding-top: 0; } 105 | 106 | a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 { 107 | margin-top: 0; 108 | padding-top: 0; } 109 | 110 | h1 p, h2 p, h3 p, h4 p, h5 p, h6 p { 111 | margin-top: 0; } 112 | 113 | li p.first { 114 | display: inline-block; } 115 | 116 | ul, ol { 117 | padding-left: 30px; } 118 | 119 | ul :first-child, ol :first-child { 120 | margin-top: 0; } 121 | 122 | ul :last-child, ol :last-child { 123 | margin-bottom: 0; } 124 | 125 | dl { 126 | padding: 0; } 127 | dl dt { 128 | font-size: 14px; 129 | font-weight: bold; 130 | font-style: italic; 131 | padding: 0; 132 | margin: 15px 0 5px; } 133 | dl dt:first-child { 134 | padding: 0; } 135 | dl dt > :first-child { 136 | margin-top: 0; } 137 | dl dt > :last-child { 138 | margin-bottom: 0; } 139 | dl dd { 140 | margin: 0 0 15px; 141 | padding: 0 15px; } 142 | dl dd > :first-child { 143 | margin-top: 0; } 144 | dl dd > :last-child { 145 | margin-bottom: 0; } 146 | 147 | blockquote { 148 | border-left: 4px solid #dddddd; 149 | padding: 0 15px; 150 | color: #777777; } 151 | blockquote > :first-child { 152 | margin-top: 0; } 153 | blockquote > :last-child { 154 | margin-bottom: 0; } 155 | 156 | table { 157 | padding: 0; } 158 | table tr { 159 | border-top: 1px solid #cccccc; 160 | background-color: white; 161 | margin: 0; 162 | padding: 0; } 163 | table tr:nth-child(2n) { 164 | background-color: #f8f8f8; } 165 | table tr th { 166 | font-weight: bold; 167 | border: 1px solid #cccccc; 168 | text-align: left; 169 | margin: 0; 170 | padding: 6px 13px; } 171 | table tr td { 172 | border: 1px solid #cccccc; 173 | text-align: left; 174 | margin: 0; 175 | padding: 6px 13px; } 176 | table tr th :first-child, table tr td :first-child { 177 | margin-top: 0; } 178 | table tr th :last-child, table tr td :last-child { 179 | margin-bottom: 0; } 180 | 181 | img { 182 | max-width: 100%; } 183 | 184 | span.frame { 185 | display: block; 186 | overflow: hidden; } 187 | span.frame > span { 188 | border: 1px solid #dddddd; 189 | display: block; 190 | float: left; 191 | overflow: hidden; 192 | margin: 13px 0 0; 193 | padding: 7px; 194 | width: auto; } 195 | span.frame span img { 196 | display: block; 197 | float: left; } 198 | span.frame span span { 199 | clear: both; 200 | color: #333333; 201 | display: block; 202 | padding: 5px 0 0; } 203 | span.align-center { 204 | display: block; 205 | overflow: hidden; 206 | clear: both; } 207 | span.align-center > span { 208 | display: block; 209 | overflow: hidden; 210 | margin: 13px auto 0; 211 | text-align: center; } 212 | span.align-center span img { 213 | margin: 0 auto; 214 | text-align: center; } 215 | span.align-right { 216 | display: block; 217 | overflow: hidden; 218 | clear: both; } 219 | span.align-right > span { 220 | display: block; 221 | overflow: hidden; 222 | margin: 13px 0 0; 223 | text-align: right; } 224 | span.align-right span img { 225 | margin: 0; 226 | text-align: right; } 227 | span.float-left { 228 | display: block; 229 | margin-right: 13px; 230 | overflow: hidden; 231 | float: left; } 232 | span.float-left span { 233 | margin: 13px 0 0; } 234 | span.float-right { 235 | display: block; 236 | margin-left: 13px; 237 | overflow: hidden; 238 | float: right; } 239 | span.float-right > span { 240 | display: block; 241 | overflow: hidden; 242 | margin: 13px auto 0; 243 | text-align: right; } 244 | 245 | code, tt { 246 | margin: 0 2px; 247 | padding: 0 5px; 248 | white-space: nowrap; 249 | border: 1px solid #eaeaea; 250 | background-color: #f8f8f8; 251 | border-radius: 3px; } 252 | 253 | pre code { 254 | margin: 0; 255 | padding: 0; 256 | white-space: pre; 257 | border: none; 258 | background: transparent; } 259 | 260 | .highlight pre { 261 | background-color: #f8f8f8; 262 | border: 1px solid #cccccc; 263 | font-size: 13px; 264 | line-height: 19px; 265 | overflow: auto; 266 | padding: 6px 10px; 267 | border-radius: 3px; } 268 | 269 | pre { 270 | background-color: #f8f8f8; 271 | border: 1px solid #cccccc; 272 | font-size: 13px; 273 | line-height: 19px; 274 | overflow: auto; 275 | padding: 6px 10px; 276 | border-radius: 3px; } 277 | pre code, pre tt { 278 | background-color: transparent; 279 | border: none; } 280 | 281 | pre, code, kbd, samp { font-family: monospace, monospace; _font-family: 'courier new', monospace; } -------------------------------------------------------------------------------- /examples/hello-world-0/hello.c: -------------------------------------------------------------------------------- 1 | /* This program prints a greeting */ 2 | 3 | #include 4 | #include 5 | 6 | int main(int argc, char *argv[]) { 7 | fprintf(stdout, "hello world\n"); 8 | exit(0); 9 | } 10 | -------------------------------------------------------------------------------- /examples/hello-world-0/hello.do: -------------------------------------------------------------------------------- 1 | redo-ifchange $2.c 2 | cc -o $3 $2.c -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "io/ioutil" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | 15 | "github.com/gyepisam/fileutils" 16 | ) 17 | 18 | // File represents a source or target file.. 19 | type File struct { 20 | Target string // file name argument to redo, redo-ifchange, redo-ifcreate, etc 21 | 22 | RootDir string // contains .redo directory 23 | Path string // Relative to RootDir 24 | 25 | Dir string 26 | Name string 27 | Ext string // File extension. Could be empty. Includes preceding dot. 28 | 29 | PathHash Hash // SHA1 hash of Path. Used as database key. 30 | FullPathHash Hash //SHA1 hash of RootDir/Path. Used for loop detection. 31 | DoFile string // Do script used to generate target output. 32 | 33 | Config Config 34 | db DB 35 | isTask bool // If true, target is a task and run for side effects 36 | } 37 | 38 | // IsTask denotes when the current target is a task script, either 39 | // explicitly (-task argument to redo) or 40 | // implicitly (name begins with @ or name is default task). 41 | func (f *File) IsTask() bool { 42 | return f.isTask 43 | } 44 | 45 | 46 | func (f *File) SetTaskFlag(isTask bool) { 47 | if isTask { 48 | f.isTask = isTask 49 | } else { 50 | base := filepath.Base(f.Name) 51 | f.isTask = strings.HasPrefix(base, TASK_PREFIX) || DEFAULT_TARGET == base 52 | } 53 | } 54 | 55 | func splitpath(path string) (string, string) { 56 | return filepath.Dir(path), filepath.Base(path) 57 | } 58 | 59 | // NewFile creates and returns a File instance for the given path. 60 | // If the path is not fully qualified, it is made relative to dir. 61 | // The newly created instance is initialized with the database specified by 62 | // the configuration file found in its root directory or the default database. 63 | // If a file does not have a root directory, it is initialized with a NullDb 64 | // and HasNullDb will return true. 65 | func NewFile(dir, path string) (f *File, err error) { 66 | 67 | if path == "" { 68 | return nil, errors.New("target path cannot be empty") 69 | } 70 | 71 | var targetPath string 72 | 73 | if filepath.IsAbs(path) { 74 | targetPath = path 75 | } else { 76 | targetPath = filepath.Clean(filepath.Join(dir, path)) 77 | } 78 | 79 | if isdir, err := fileutils.IsDir(targetPath); err != nil { 80 | return nil, err 81 | } else if isdir { 82 | return nil, fmt.Errorf("target %s is a directory", targetPath) 83 | } 84 | 85 | f = new(File) 86 | 87 | f.Target = path 88 | 89 | rootDir, filename := splitpath(targetPath) 90 | relPath := &RelPath{} 91 | relPath.Add(filename) 92 | 93 | hasRoot := false 94 | 95 | for { 96 | exists, err := fileutils.DirExists(filepath.Join(rootDir, REDO_DIR)) 97 | if err != nil { 98 | return nil, err 99 | } 100 | if exists { 101 | hasRoot = true 102 | break 103 | } 104 | if rootDir == "/" || rootDir == "." { 105 | break 106 | } 107 | rootDir, filename = splitpath(rootDir) 108 | relPath.Add(filename) 109 | } 110 | 111 | f.RootDir = rootDir 112 | 113 | f.Path = relPath.Join() 114 | 115 | f.PathHash = MakeHash(f.Path) 116 | f.FullPathHash = MakeHash(filepath.Join(f.RootDir, f.Path)) 117 | 118 | f.Debug("@Hash %s: %s -> %s\n", f.RootDir, f.Path, f.PathHash) 119 | 120 | f.Dir, f.Name = splitpath(f.Fullpath()) 121 | f.Ext = filepath.Ext(f.Name) 122 | 123 | if hasRoot { 124 | // TODO(gsam): Read config file in rootDir to determine DB type, if any. 125 | // Default to FileDB if not specified. 126 | // f.Config = Config{DBType: "file"} 127 | f.db, err = FileDbOpen(f.RootDir) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | err := os.Mkdir(f.tempDir(), 0755) 133 | if err != nil && !os.IsExist(err) { 134 | return nil, err 135 | } 136 | 137 | } else { 138 | f.Config = Config{DBType: "null"} 139 | f.db, err = NullDbOpen("") 140 | if err != nil { 141 | return nil, err 142 | } 143 | f.Debug("@NullDb\n") 144 | } 145 | 146 | return 147 | } 148 | 149 | // HasNullDb specifies whether the File receiver uses a NullDb. 150 | func (f *File) HasNullDb() bool { 151 | return f.db.IsNull() 152 | } 153 | 154 | // Fullpath returns the fully qualified path to the target file. 155 | func (f *File) Fullpath() string { 156 | return filepath.Join(f.RootDir, f.Path) 157 | } 158 | 159 | // Rel makes path relative to f.RootDir. 160 | func (f *File) Rel(path string) string { 161 | relpath, err := filepath.Rel(f.RootDir, path) 162 | if err != nil { 163 | panic(err) 164 | } 165 | return filepath.Clean(relpath) 166 | } 167 | 168 | // Abs returns a cleaned up fullpath by joining f.RootDir to path. 169 | func (f *File) Abs(path string) string { 170 | if filepath.IsAbs(path) { 171 | return path 172 | } 173 | return filepath.Clean(filepath.Join(f.RootDir, path)) 174 | } 175 | 176 | // Exist verifies that the file exists on disk. 177 | func (f *File) Exists() (bool, error) { 178 | return fileutils.FileExists(f.Fullpath()) 179 | } 180 | 181 | // HasDoFile returns true if the receiver has been assigned a .do script. 182 | func (f *File) HasDoFile() bool { 183 | return len(f.DoFile) > 0 184 | } 185 | 186 | // IsCurrent returns a boolean denoting whether the target is up to date. 187 | 188 | // A target is up to date if the following conditions hold: 189 | // The file exists 190 | // The file has not been flagged to be rebuilt 191 | // The file has not changed since creation. That is; the file has a metadata record 192 | // and that record matches the actual file metadata. 193 | // All the file's prerequisites are also current. 194 | 195 | func (f *File) IsCurrent() (bool, error) { 196 | return f.isCurrent() 197 | } 198 | 199 | func (f *File) isCurrent() (bool, error) { 200 | 201 | reason := func(msg string) (bool, error) { 202 | f.Debug("@Outdated because %s\n", msg) 203 | return false, nil 204 | } 205 | 206 | if f.MustRebuild() { 207 | return reason("REBUILD") 208 | } 209 | 210 | storedMeta, found, err := f.GetMetadata() 211 | if err != nil { 212 | return false, err 213 | } else if !found { 214 | return reason("no record metadata") 215 | } 216 | 217 | fileMeta, err := f.NewMetadata() 218 | if err != nil { 219 | return false, err 220 | } else if fileMeta == nil { 221 | return reason("no file metadata") 222 | } 223 | 224 | if !storedMeta.Equal(fileMeta) { 225 | return reason("record metadata != file metadata") 226 | } 227 | 228 | // redo-ifcreate dependencies 229 | created, err := f.PrerequisiteFiles(IFCREATE, AUTO_IFCREATE) 230 | if err != nil { 231 | return false, err 232 | } 233 | 234 | for _, prerequisite := range created { 235 | if exists, err := prerequisite.Exists(); err != nil { 236 | return false, err 237 | } else if exists { 238 | return reason("ifcreate dependency target exists") 239 | } 240 | } 241 | 242 | // redo-ifchange dependencies 243 | changed, err := f.Prerequisites(IFCHANGE, AUTO_IFCHANGE) 244 | if err != nil { 245 | return false, err 246 | } 247 | 248 | for _, prerequisite := range changed { 249 | if isCurrent, err := prerequisite.IsCurrent(f.RootDir); err != nil || !isCurrent { 250 | return isCurrent, err 251 | } 252 | } 253 | 254 | return true, nil 255 | } 256 | 257 | // NewMetadata computes and returns the file metadata. 258 | func (f *File) NewMetadata() (m *Metadata, err error) { 259 | 260 | m, err = NewMetadata(f.Fullpath(), f.Path) 261 | if m == nil || err == nil { 262 | return 263 | } 264 | 265 | if len(f.DoFile) > 0 { 266 | if path, err := filepath.Rel(f.RootDir, f.DoFile); err != nil { 267 | m.DoFile = f.DoFile 268 | } else { 269 | m.DoFile = path 270 | } 271 | } 272 | 273 | return 274 | } 275 | 276 | // ContentHash returns a cryptographic hash of the file contents. 277 | func (f *File) ContentHash() (Hash, error) { 278 | return ContentHash(f.Fullpath()) 279 | } 280 | 281 | // Log prints out messages to stderr when the verbosity is greater than N. 282 | func (f *File) Log(format string, args ...interface{}) { 283 | fmt.Fprintf(os.Stderr, format, args...) 284 | } 285 | 286 | // Debug prints out messages to stderr when the debug flag is enabled. 287 | func (f *File) Debug(format string, args ...interface{}) { 288 | if Debug { 289 | for i, value := range args { 290 | if value == nil { 291 | args[i] = "" 292 | } 293 | } 294 | fmt.Fprintf(os.Stderr, "%s %s: ", os.Args[0], f.Target) 295 | fmt.Fprintf(os.Stderr, format, args...) 296 | } 297 | } 298 | 299 | func (f *File) GenerateNotifications(oldMeta, newMeta *Metadata) error { 300 | 301 | if oldMeta == nil { 302 | if err := f.NotifyDependents(IFCREATE); err != nil { 303 | return err 304 | } 305 | } 306 | 307 | if !newMeta.Equal(oldMeta) { 308 | if err := f.NotifyDependents(IFCHANGE); err != nil { 309 | return err 310 | } 311 | } 312 | 313 | return nil 314 | } 315 | 316 | // RedoDir returns the path to the .redo directory. 317 | func (f *File) RedoDir() string { 318 | return filepath.Join(f.RootDir, REDO_DIR) 319 | } 320 | 321 | func (f *File) tempDir() string { 322 | if s := os.Getenv("REDO_TMP_DIR"); len(s) > 0 { 323 | return s 324 | } 325 | return filepath.Join(f.RedoDir(), "tmp") 326 | } 327 | 328 | func (f *File) tempFile() (*os.File, error) { 329 | return ioutil.TempFile(f.tempDir(), strings.Replace(f.Name, ".", "-", -1)+"-redo-tmp-") 330 | } 331 | 332 | // DoInfoCandidates generates a list of possible do filenames for this file 333 | func (f *File) DoInfoCandidates() []*DoInfo { 334 | 335 | const sep = `.` 336 | parts := strings.Split(f.Name, sep) 337 | n := len(parts) 338 | if n == 0 { 339 | return nil 340 | } 341 | 342 | // +1 for default.do 343 | out := make([]*DoInfo, n+1) 344 | 345 | add := func(index int, basename string, exts, arg2 []string) { 346 | a := make([]string, len(exts)+2) 347 | a[0] = basename 348 | copy(a[1:], exts) 349 | a[len(a)-1] = DO_EXT 350 | out[index] = &DoInfo{Name: strings.Join(a, sep), Arg2: strings.Join(arg2, sep)} 351 | } 352 | 353 | add(0, parts[0], parts[1:n], parts[0:n]) // target.(ext\.)*do 354 | 355 | for i := 1; i < len(out); i++ { 356 | add(i, DO_BASENAME, parts[i:n], parts[0:i]) 357 | } 358 | 359 | return out 360 | } 361 | 362 | // NewOutput returns an initialized Output 363 | func (f *File) NewOutput() (*Output, error) { 364 | tmp, err := f.tempFile() 365 | if err != nil { 366 | return nil, err 367 | } 368 | return NewOutput(tmp), nil 369 | } 370 | -------------------------------------------------------------------------------- /filedb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "io/ioutil" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | 15 | "github.com/gyepisam/fileutils" 16 | ) 17 | 18 | const ( 19 | // Where is data kept? 20 | DATA_DIR = "data" 21 | ) 22 | 23 | // FileDb is a file based DB for storing Redo relationships and metadata. 24 | type FileDb struct { 25 | DataDir string 26 | } 27 | 28 | var NullKeyErr = errors.New("Key cannot be empty.") 29 | var NullPrefixErr = errors.New("Prefix cannot be empty.") 30 | 31 | // Open requires a project root argument 32 | func FileDbOpen(rootdir string) (DB, error) { 33 | 34 | redodir := filepath.Join(rootdir, REDO_DIR) 35 | 36 | if exists, err := fileutils.DirExists(redodir); err != nil { 37 | return nil, err 38 | } else if !exists { 39 | return nil, fmt.Errorf("FileDb redo directory [%s] not found. Forget to call redo-init?", redodir) 40 | } 41 | 42 | datadir := filepath.Join(redodir, DATA_DIR) 43 | if err := os.Mkdir(datadir, DIR_PERM); err != nil && !os.IsExist(err) { 44 | return nil, fmt.Errorf("FileDb cannot make data directory [%s]. %s", datadir, err) 45 | } 46 | 47 | return &FileDb{DataDir: datadir}, nil 48 | } 49 | 50 | func (db *FileDb) IsNull() bool { return false } 51 | 52 | func (db *FileDb) makePath(key string) string { 53 | return filepath.Join(db.DataDir, key) 54 | } 55 | 56 | // Close is a noop for this DB type 57 | func (db *FileDb) Close() error { 58 | return nil 59 | } 60 | 61 | func (db *FileDb) Put(key string, value []byte) error { 62 | if len(key) == 0 { 63 | return NullKeyErr 64 | } 65 | return fileutils.AtomicWrite(db.makePath(key), func(tmpFile *os.File) error { 66 | _, err := tmpFile.Write(value) 67 | return err 68 | }) 69 | } 70 | 71 | func (db *FileDb) Get(key string) ([]byte, bool, error) { 72 | if len(key) == 0 { 73 | return nil, false, NullKeyErr 74 | } 75 | 76 | b, err := ioutil.ReadFile(db.makePath(key)) 77 | 78 | var found bool 79 | if err == nil { 80 | found = true 81 | } else if os.IsNotExist(err) { 82 | found = false //explicit is better than implicit 83 | err = nil 84 | } 85 | return b, found, err 86 | } 87 | 88 | func (db *FileDb) Delete(key string) error { 89 | if len(key) == 0 { 90 | return NullKeyErr 91 | } 92 | 93 | err := os.Remove(db.makePath(key)) 94 | if err != nil && os.IsNotExist(err) { 95 | return nil 96 | } 97 | return err 98 | } 99 | 100 | func (db *FileDb) GetRecords(prefix string) ([]Record, error) { 101 | 102 | if len(prefix) == 0 { 103 | return nil, NullPrefixErr 104 | } 105 | 106 | var out []Record 107 | rootLen := len(db.DataDir) + 1 108 | 109 | walker := func(path string, info os.FileInfo, err error) error { 110 | if err != nil { 111 | return err 112 | } 113 | key := path[rootLen:] 114 | // Go 1.0.x compatible syntax for info.Mode().IsRegular() 115 | if isRegular := info.Mode()&os.ModeType == 0; isRegular && strings.HasPrefix(key, prefix) { 116 | if b, err := ioutil.ReadFile(path); err != nil { 117 | return err 118 | } else { 119 | out = append(out, Record{Key: key, Value: b}) 120 | } 121 | } 122 | 123 | return nil 124 | } 125 | 126 | return out, filepath.Walk(db.DataDir+"/", walker) 127 | } 128 | 129 | // GetKeys returns an array of keys that are prefixes of the specified key. 130 | func (db *FileDb) GetKeys(prefix string) ([]string, error) { 131 | 132 | records, err := db.GetRecords(prefix) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | out := make([]string, len(records)) 138 | for i, rec := range records { 139 | out[i] = rec.Key 140 | } 141 | return out, nil 142 | } 143 | 144 | // GetValues returns an array of data values for keys with the specified prefix. 145 | func (db *FileDb) GetValues(prefix string) ([][]byte, error) { 146 | records, err := db.GetRecords(prefix) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | out := make([][]byte, len(records)) 152 | for i, rec := range records { 153 | out[i] = make([]byte, len(rec.Value)) 154 | copy(out[i], rec.Value) 155 | } 156 | return out, nil 157 | } 158 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package redux 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | // InitDir creates a redo directory in the specified project root directory. 9 | func InitDir(dirname string) error { 10 | 11 | if len(dirname) == 0 { 12 | wd, err := os.Getwd() 13 | if err != nil { 14 | return err 15 | } 16 | dirname = wd 17 | } else if c := dirname[0]; c != '.' && c != '/' { 18 | wd, err := os.Getwd() 19 | if err != nil { 20 | return err 21 | } 22 | dirname = filepath.Join(wd, dirname) 23 | } 24 | 25 | return os.MkdirAll(filepath.Join(dirname, REDO_DIR), DIR_PERM) 26 | } 27 | -------------------------------------------------------------------------------- /install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | target=github.com/gyepisam/redux/redux 4 | go install $target 5 | bin=$(go list -f '{{.Target}}' $target) 6 | if test -x $bin ; then 7 | $bin install links 8 | else 9 | echo "missing bin: $bin" 10 | exit 1 11 | fi 12 | -------------------------------------------------------------------------------- /metadata.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | import "os" 8 | 9 | // File Metadata. 10 | type Metadata struct { 11 | Path string //not used for comparison 12 | ContentHash Hash 13 | DoFile string 14 | } 15 | 16 | // Equal compares metadata instances for equality. 17 | func (m *Metadata) Equal(other *Metadata) bool { 18 | return other != nil && m.ContentHash == other.ContentHash 19 | } 20 | 21 | // IsCreated compares m to other to determine m represents a newly created file. 22 | func (m Metadata) IsCreated(other Metadata) bool { 23 | return len(other.ContentHash) == 0 && len(m.ContentHash) > 0 24 | } 25 | 26 | // NewMetadata returns a metadata instance for the given path. 27 | // If the file is not found, nil is returned. 28 | func NewMetadata(path string, storedPath string) (*Metadata, error) { 29 | 30 | hash, err := ContentHash(path) 31 | if os.IsNotExist(err) { 32 | return nil, nil 33 | } 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return &Metadata{Path: storedPath, ContentHash: hash}, nil 39 | } 40 | 41 | //HasDoFile returns true if the metadata has a non-empty DoField field. 42 | func (m Metadata) HasDoFile() bool { 43 | return len(m.DoFile) > 0 44 | } 45 | 46 | // PutMetadata stores the file's metadata in the database. 47 | func (f *File) PutMetadata(m *Metadata) error { 48 | if m != nil { 49 | return f.Put(f.metadataKey(), *m) 50 | } 51 | 52 | m, err := NewMetadata(f.Fullpath(), f.Path) 53 | if err != nil { 54 | return err 55 | } 56 | if m == nil { 57 | return f.ErrNotFound("PutMetadata") 58 | } 59 | 60 | return f.Put(f.metadataKey(), m) 61 | } 62 | 63 | // GetMetadata returns a record as a metadata structure 64 | // found denotes whether the record was found. 65 | func (f *File) GetMetadata() (Metadata, bool, error) { 66 | m := Metadata{} 67 | found, err := f.Get(f.metadataKey(), &m) 68 | return m, found, err 69 | } 70 | 71 | // DeleteMetadata removes the metadata record, if any, from the database. 72 | func (f *File) DeleteMetadata() error { 73 | return f.Delete(f.metadataKey()) 74 | } 75 | -------------------------------------------------------------------------------- /mustrebuild.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | // MustRebuild returns a boolean denoting whether the target must be rebuilt. 8 | func (f *File) MustRebuild() bool { 9 | var x string 10 | found, err := f.Get(f.mustRebuildKey(), &x) 11 | if err != nil { 12 | panic(err) 13 | } 14 | return found 15 | } 16 | 17 | // PutMustRebuild sets the eponymous database record value. 18 | func (f *File) PutMustRebuild() error { 19 | return f.Put(f.mustRebuildKey(), nil) 20 | } 21 | 22 | // DeleteMustRebuild removed the database record. 23 | func (f *File) DeleteMustRebuild() error { 24 | return f.Delete(f.mustRebuildKey()) 25 | } 26 | -------------------------------------------------------------------------------- /nulldb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | // NullDb is a blackhole database, used for source files outside the redo project directory structure.. 8 | // It never fails, all writes disappear, and reads return nothing. 9 | type NullDb struct { 10 | } 11 | 12 | // Open requires a project root argument 13 | func NullDbOpen(ignored string) (DB, error) { 14 | return &NullDb{}, nil 15 | } 16 | 17 | func (db *NullDb) IsNull() bool { return true } 18 | 19 | // Put stores value under key 20 | func (db *NullDb) Put(key string, value []byte) error { 21 | return nil 22 | } 23 | 24 | // Get returns the value stored under key and a boolean indicating 25 | // whether the returned value was actually found in the database. 26 | func (db *NullDb) Get(key string) ([]byte, bool, error) { 27 | return []byte{}, false, nil 28 | } 29 | 30 | // Delete removes the value stored under key. 31 | // If key does not exist, the operation is a noop. 32 | func (db *NullDb) Delete(key string) error { 33 | return nil 34 | } 35 | 36 | // GetKeys returns a list of keys which have the specified key as a prefix. 37 | func (db *NullDb) GetKeys(prefix string) ([]string, error) { 38 | return []string{}, nil 39 | } 40 | 41 | // GetValues returns a list of values whose keys matching the specified key prefix. 42 | func (db *NullDb) GetValues(prefix string) ([][]byte, error) { 43 | return [][]byte{}, nil 44 | } 45 | 46 | // GetRecords returns a list of records (keys and data) matchign the specified key prefix. 47 | func (db *NullDb) GetRecords(prefix string) ([]Record, error) { 48 | return []Record{}, nil 49 | } 50 | 51 | func (db *NullDb) Close() error { 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /op.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | // Redo finds and executes the .do file for the given target. 12 | func (target *File) Redo() error { 13 | 14 | doInfo, err := target.findDoFile() 15 | if err != nil { 16 | return err 17 | } 18 | 19 | if doInfo != nil { 20 | target.DoFile = doInfo.Path() 21 | } 22 | 23 | cachedMeta, recordFound, err := target.GetMetadata() 24 | if err != nil { 25 | return err 26 | } 27 | 28 | targetMeta, err := target.NewMetadata() 29 | if err != nil { 30 | return err 31 | } 32 | targetExists := targetMeta != nil 33 | 34 | if targetExists { 35 | if recordFound { 36 | if target.HasDoFile() { 37 | return target.redoTarget(doInfo, targetMeta) 38 | } else if cachedMeta.HasDoFile() { 39 | return target.Errorf("Missing .do file") 40 | } else if !targetMeta.Equal(&cachedMeta) { 41 | return target.redoStatic(IFCHANGE, targetMeta) 42 | } 43 | } else { 44 | if target.HasDoFile() { 45 | return target.redoTarget(doInfo, targetMeta) 46 | } else { 47 | return target.redoStatic(IFCREATE, targetMeta) 48 | } 49 | } 50 | } else { 51 | if recordFound { 52 | // target existed at one point but was deleted... 53 | if target.HasDoFile() { 54 | return target.redoTarget(doInfo, targetMeta) 55 | } else if cachedMeta.HasDoFile() { 56 | return target.Errorf("Missing .do file") 57 | } else { 58 | // target is a deleted source file. Clean up and fail. 59 | if err = target.NotifyDependents(IFCHANGE); err != nil { 60 | return err 61 | } else if err = target.DeleteMetadata(); err != nil { 62 | return err 63 | } 64 | return fmt.Errorf("Source file %s does not exist", target.Target) 65 | } 66 | } else { 67 | if target.HasDoFile() { 68 | return target.redoTarget(doInfo, targetMeta) 69 | } else { 70 | return target.Errorf("Target [%s] does not have a do file", target.Target) 71 | } 72 | } 73 | } 74 | 75 | return nil 76 | } 77 | 78 | // redoTarget records a target's .do file dependencies, runs the target's do file and notifies dependents. 79 | func (f *File) redoTarget(doInfo *DoInfo, oldMeta *Metadata) error { 80 | 81 | // can't build without a database... 82 | if f.HasNullDb() { 83 | return f.ErrUninitialized() 84 | } 85 | 86 | if doInfo == nil { 87 | panic("nil DoInfo") 88 | } 89 | 90 | // Prerequisites will be recreated... 91 | // Ideally, this could be done within a transaction to allow for rollback 92 | // in the event of failure. 93 | if err := f.DeleteAutoPrerequisites(); err != nil { 94 | return err 95 | } 96 | 97 | for _, path := range doInfo.Missing { 98 | relpath := f.Rel(path) 99 | err := f.PutPrerequisite(AUTO_IFCREATE, MakeHash(relpath), Prerequisite{Path: relpath}) 100 | if err != nil { 101 | return err 102 | } 103 | } 104 | 105 | doFile, err := NewFile(doInfo.Dir, doInfo.Name) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | // metadata needs to be stored twice and is relatively expensive to acquire. 111 | doMeta, err := doFile.NewMetadata() 112 | 113 | if err != nil { 114 | return err 115 | } else if doMeta == nil { 116 | return doFile.ErrNotFound("redoTarget: doFile.NewMetadata") 117 | } else if err := doFile.PutMetadata(doMeta); err != nil { 118 | return err 119 | } 120 | 121 | relpath := f.Rel(doInfo.Path()) 122 | if err := f.PutPrerequisite(AUTO_IFCHANGE, MakeHash(relpath), Prerequisite{relpath, doMeta}); err != nil { 123 | return err 124 | } 125 | 126 | if err := f.RunDoFile(doInfo); err != nil { 127 | return err 128 | } 129 | 130 | // A task script does not produce output and has no dependencies... 131 | if f.IsTask() { 132 | return nil 133 | } 134 | 135 | newMeta, err := f.NewMetadata() 136 | if err != nil { 137 | return err 138 | } else if newMeta == nil { 139 | return f.ErrNotFound("redoTarget: f.NewMetadata") 140 | } 141 | 142 | if err := f.PutMetadata(newMeta); err != nil { 143 | return err 144 | } 145 | 146 | if err := f.DeleteMustRebuild(); err != nil { 147 | return err 148 | } 149 | 150 | // Notify dependents if a content change has occurred. 151 | return f.GenerateNotifications(oldMeta, newMeta) 152 | } 153 | 154 | // redoStatic tracks changes and dependencies for static files, which are edited manually and do not have a do script. 155 | func (f *File) redoStatic(event Event, oldMeta *Metadata) error { 156 | 157 | // A file that exists outside this (or any) redo project directory 158 | // and has no database in which to store metadata or dependencies is assigned a NullDb. 159 | // Such a file is still useful it can serve as a prerequisite for files inside a redo project directory. 160 | // However, it cannot store metadata or notify dependents of changes. 161 | if f.HasNullDb() { 162 | return nil 163 | } 164 | 165 | newMeta, err := f.NewMetadata() 166 | if err != nil { 167 | return err 168 | } else if newMeta == nil { 169 | return f.ErrNotFound("redoStatic") 170 | } 171 | 172 | if err := f.PutMetadata(newMeta); err != nil { 173 | return err 174 | } 175 | 176 | //TODO (gsam): store prerequisites on missing do files... 177 | 178 | return f.GenerateNotifications(oldMeta, newMeta) 179 | } 180 | 181 | // RedoIfChange runs redo on the target if it is out of date or its current state 182 | // disagrees with its dependent's version of its state. 183 | func (target *File) RedoIfChange(dependent *File) error { 184 | 185 | recordRelation := func(m *Metadata) error { 186 | return RecordRelation(dependent, target, IFCHANGE, m) 187 | } 188 | 189 | targetMeta, err := target.NewMetadata() 190 | if err != nil { 191 | return err 192 | } 193 | 194 | // No metadata means the target has not been seen before. 195 | // Redo will sort that out. 196 | if targetMeta == nil { 197 | goto REDO 198 | } 199 | 200 | if isCurrent, err := target.IsCurrent(); err != nil { 201 | return err 202 | } else if !isCurrent { 203 | goto REDO 204 | } else { 205 | 206 | // Compare dependent's version of the target's state to its current state. 207 | // Target is self consistent, but may have changed since the prerequisite record was created. 208 | prereq, found, err := dependent.GetPrerequisite(IFCHANGE, target.PathHash) 209 | if err != nil { 210 | return err 211 | } else if !found { 212 | // There is no record of the dependency so this is the first time through. 213 | // Since the target is up to date, use its metadata for the dependency. 214 | return recordRelation(targetMeta) 215 | } 216 | 217 | if prereq.Equal(targetMeta) { 218 | // target is up to date and its current state agrees with dependent's version. 219 | // Nothing to do here. 220 | return nil 221 | } 222 | } 223 | 224 | REDO: 225 | err = target.Redo() 226 | if err != nil { 227 | return err 228 | } 229 | 230 | targetMeta, err = target.NewMetadata() 231 | if err != nil { 232 | return err 233 | } 234 | if targetMeta == nil { 235 | return fmt.Errorf("Cannot find recently created target: %s", target.Target) 236 | } 237 | 238 | return recordRelation(targetMeta) 239 | } 240 | 241 | /* RedoIfCreate records a dependency record on a file that does not yet exist */ 242 | func (target *File) RedoIfCreate(dependent *File) error { 243 | if exists, err := target.Exists(); err != nil { 244 | return err 245 | } else if exists { 246 | return fmt.Errorf("%s. File exists", dependent.Target) 247 | } 248 | 249 | //In case it existed before 250 | target.DeleteMetadata() 251 | 252 | return RecordRelation(dependent, target, IFCREATE, nil) 253 | } 254 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | import ( 8 | "os" 9 | ) 10 | 11 | // Options default to env values, to be overriden by main() if necessary. 12 | var ( 13 | Verbosity = len(os.Getenv("REDO_VERBOSE")) 14 | Debug = len(os.Getenv("REDO_DEBUG")) > 0 15 | ShellArgs = os.Getenv("REDO_SHELL_ARGS") 16 | ) 17 | 18 | func Verbose() bool { return Verbosity > 0 } 19 | -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | package redux 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | ) 8 | 9 | // An Output is the output of a .do scripts, either through stdout or $3 (Arg3) 10 | // If the .do script invocation is equivalent to the sh command, 11 | // 12 | // sh target.ext.do target.ext target tmp0 > tmp1 13 | // 14 | // tmp0 and tmp1 would be outputs. 15 | type Output struct { 16 | *os.File 17 | IsArg3 bool 18 | } 19 | 20 | func NewOutput(file *os.File) *Output { 21 | return &Output{File: file} 22 | } 23 | 24 | func (out *Output) SetupArg3() error { 25 | if err := out.Close(); err != nil { 26 | return err 27 | } 28 | 29 | if err := os.Remove(out.Name()); err != nil { 30 | return err 31 | } 32 | 33 | out.IsArg3 = true 34 | 35 | return nil 36 | } 37 | 38 | func (out *Output) Copy(destDir string) (destPath string, err error) { 39 | src, err := os.Open(out.Name()) 40 | if err != nil { 41 | return 42 | } 43 | 44 | dst, err := ioutil.TempFile(destDir, "-redux-output-") 45 | if err != nil { 46 | return 47 | } 48 | 49 | destPath = dst.Name() 50 | 51 | defer func() { 52 | src.Close() 53 | dst.Close() 54 | 55 | if err != nil { 56 | os.Remove(dst.Name()) 57 | } 58 | }() 59 | 60 | _, err = io.Copy(dst, src) 61 | if err != nil { 62 | return 63 | } 64 | 65 | if out.IsArg3 { 66 | err = out.copyAttribs(src, dst) 67 | } 68 | 69 | return 70 | } 71 | 72 | func (out *Output) copyAttribs(src, dst *os.File) (err error) { 73 | 74 | srcInfo, err := src.Stat() 75 | if err != nil { 76 | return 77 | } 78 | 79 | dstInfo, err := dst.Stat() 80 | if err != nil { 81 | return 82 | } 83 | 84 | if perm := srcInfo.Mode() & os.ModePerm; perm != (dstInfo.Mode() & os.ModePerm) { 85 | err = dst.Chmod(perm) 86 | if err != nil { 87 | return 88 | } 89 | } 90 | 91 | // Fixup file ownership as necessary. 92 | // These operations are not portable, but should always succeed where they are supported. 93 | srcUid, srcGid, err := statUidGid(srcInfo) 94 | if err != nil { 95 | return 96 | } 97 | 98 | dstUid, dstGid, err := statUidGid(dstInfo) 99 | if err != nil { 100 | return 101 | } 102 | 103 | if dstUid != srcUid || dstGid != srcGid { 104 | err = dst.Chown(int(srcUid), int(srcGid)) 105 | } 106 | 107 | return 108 | } 109 | 110 | func (out *Output) Cleanup() { 111 | _ = out.Close() // ignore error 112 | _ = os.Remove(out.Name()) //ignore error 113 | } 114 | -------------------------------------------------------------------------------- /prerequisite.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | // Prerequisite from a source to a target. 8 | type Prerequisite struct { 9 | Path string // path back to target of prerequisite. 10 | *Metadata // target's metadata upon record creation. 11 | } 12 | 13 | // PutPrerequisite stores the given prerequisite using a key based on the event and hash. 14 | func (f *File) PutPrerequisite(event Event, hash Hash, prereq Prerequisite) error { 15 | return f.Put(f.makeKey(REQUIRES, event, hash), prereq) 16 | } 17 | 18 | // GetPrerequisite returns the prerequisite for the event and hash. 19 | // If the record does not exist, found is false and err is nil. 20 | func (f *File) GetPrerequisite(event Event, hash Hash) (prereq Prerequisite, found bool, err error) { 21 | found, err = f.Get(f.makeKey(REQUIRES, event, hash), &prereq) 22 | return 23 | } 24 | 25 | type record struct { 26 | key string 27 | *Prerequisite 28 | } 29 | 30 | func prefixed(f *File, prefix string) ([]*record, error) { 31 | 32 | rows, err := f.db.GetRecords(prefix) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | out := make([]*record, len(rows)) 38 | 39 | for i, row := range rows { 40 | if decoded, err := decodePrerequisite(row.Value); err != nil { 41 | return nil, err 42 | } else { 43 | out[i] = &record{row.Key, &decoded} 44 | } 45 | } 46 | 47 | return out, nil 48 | } 49 | 50 | func (f *File) eventRecords(events ...Event) ([]*record, error) { 51 | 52 | if len(events) == 0 { 53 | return prefixed(f, f.makeKey(REQUIRES)) 54 | } 55 | 56 | var records []*record 57 | for _, event := range events { 58 | eventRecords, err := prefixed(f, f.makeKey(REQUIRES, event)) 59 | if err != nil { 60 | return nil, err 61 | } 62 | records = append(records, eventRecords...) 63 | } 64 | return records, nil 65 | } 66 | 67 | // Prerequisites returns a slice of prerequisites for the file. 68 | func (f *File) Prerequisites(events ...Event) (out []*Prerequisite, err error) { 69 | 70 | records, err := f.eventRecords(events...) 71 | if err != nil { 72 | return 73 | } 74 | 75 | out = make([]*Prerequisite, len(records)) 76 | 77 | for i, rec := range records { 78 | out[i] = rec.Prerequisite 79 | } 80 | 81 | return 82 | } 83 | 84 | // PrerequisiteFiles returns a slice of *File objects for the file's prerequisites for the list of events. 85 | func (f *File) PrerequisiteFiles(events ...Event) ([]*File, error) { 86 | records, err := f.eventRecords(events...) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | out := make([]*File, len(records)) 92 | 93 | for i, rec := range records { 94 | if file, err := rec.File(f.RootDir); err != nil { 95 | return nil, err 96 | } else { 97 | out[i] = file 98 | } 99 | } 100 | 101 | return out, nil 102 | } 103 | 104 | // DeletePrerequisite removes a single prerequisite. 105 | func (f *File) DeletePrerequisite(event Event, hash Hash) error { 106 | return f.Delete(f.makeKey(REQUIRES, event, hash)) 107 | } 108 | 109 | type visitor func(*record) error 110 | 111 | func visit(f *File, prefix string, fn visitor) error { 112 | records, err := prefixed(f, prefix) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | for _, rec := range records { 118 | if err := fn(rec); err != nil { 119 | return err 120 | } 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func destroy(f *File, prefix string) error { 127 | return visit(f, prefix, func(rec *record) error { 128 | return f.Delete(rec.key) 129 | }) 130 | } 131 | 132 | // DeleteAutoPrerequisites removes all of the file's system generated prerequisites. 133 | func (f *File) DeleteAutoPrerequisites() error { 134 | return destroy(f, f.makeKey(REQUIRES, AUTO)) 135 | } 136 | 137 | // DeleteAllPrerequisites removed all of the file's prerequisites. 138 | func (f *File) DeleteAllPrerequisites() error { 139 | return destroy(f, f.makeKey(REQUIRES)) 140 | } 141 | 142 | func (p *Prerequisite) IsCurrent(rootDir string) (isCurrent bool, err error) { 143 | f, err := p.File(rootDir) 144 | if err != nil { 145 | return 146 | } 147 | 148 | m, err := f.NewMetadata() 149 | if err != nil { 150 | return 151 | } 152 | 153 | isCurrent = p.Equal(m) 154 | if !isCurrent { 155 | return 156 | } 157 | 158 | return f.IsCurrent() 159 | } 160 | -------------------------------------------------------------------------------- /redo_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "io/ioutil" 12 | "os" 13 | "os/exec" 14 | "path/filepath" 15 | "regexp" 16 | "sort" 17 | "strings" 18 | "testing" 19 | ) 20 | 21 | type Dir struct { 22 | root string 23 | path string 24 | t *testing.T 25 | } 26 | 27 | func newDir(t *testing.T) (Dir, error) { 28 | s, err := ioutil.TempDir("", "redo-test-") 29 | return Dir{s, s, t}, err 30 | } 31 | 32 | func newDirAt(t *testing.T, dir string) (Dir, error) { 33 | d, err := newDir(t) 34 | if err != nil { 35 | return d, err 36 | } 37 | 38 | if len(dir) == 0 { 39 | return d, errors.New("newDir requires non-empty dir arg") 40 | } 41 | 42 | s := filepath.Join(d.path, dir) 43 | if err := os.MkdirAll(s, 0755); err != nil { 44 | return d, err 45 | } 46 | 47 | d.path = s 48 | 49 | return d, nil 50 | } 51 | 52 | func (dir Dir) newDirAt(s string) (Dir, error) { 53 | s = filepath.Join(dir.path, s) 54 | if err := os.MkdirAll(s, 0755); err != nil { 55 | return dir, err 56 | } 57 | return Dir{s, s, dir.t}, nil 58 | } 59 | 60 | func (dir Dir) Init() error { 61 | state := run(dir.t, exec.Command("redo-init", dir.path)) 62 | if state.Err != nil { 63 | return state 64 | } 65 | return nil 66 | } 67 | 68 | func (dir Dir) Cleanup() { 69 | os.RemoveAll(dir.root) 70 | } 71 | 72 | func (dir Dir) Append(values ...string) string { 73 | s := make([]string, len(values)+1) 74 | s[0] = dir.path 75 | copy(s[1:], values) 76 | return filepath.Join(s...) 77 | } 78 | 79 | func (dir Dir) WriteFile(filename, content string) error { 80 | return ioutil.WriteFile(dir.Append(filename), []byte(content), 0655) 81 | } 82 | 83 | // A Script encapsulates an input, output, and the do file that generates one from the other. 84 | type Script struct { 85 | Name string // Names the test and, implicitly, the do file 86 | In string // Input data for creating the output 87 | Out string // Output data 88 | Command string // Do script contents 89 | OutDir string // Output file location 90 | DoFileName string // defaults to Name, but can be changed. 91 | } 92 | 93 | func echoScript(name, text string) *Script { 94 | return &Script{ 95 | Name: name, 96 | In: text, 97 | Out: text, 98 | Command: fmt.Sprintf("echo -n '%s'", quote(text)), 99 | } 100 | } 101 | 102 | type TestCases map[string]*Script 103 | 104 | var Scripts = make(TestCases) 105 | 106 | func (c TestCases) Add(name string) *Script { 107 | if len(name) == 0 { 108 | panic("TestCase missing name") 109 | } 110 | s := &Script{Name: name} 111 | c[s.Name] = s 112 | return s 113 | } 114 | 115 | func (c TestCases) Get(v string) Script { 116 | if s, ok := c[v]; ok { 117 | return *s 118 | } 119 | panic("Unknown test case: " + v) 120 | } 121 | 122 | func init() { 123 | var s *Script 124 | 125 | s = Scripts.Add("simple") 126 | s.In = "slippers,pumps,mules,sneakers,bowling shoes" 127 | words := strings.Split(s.In, ",") 128 | sort.Strings(words) 129 | s.Out = strings.Join(words, ",") 130 | s.Command = fmt.Sprintf("echo -n '%s'", quote(s.Out)) 131 | 132 | s = Scripts.Add("allcaps") 133 | s.In = ` 134 | When to the sessions of sweet silent thought, 135 | I summon up remembrance of things past, 136 | I sigh the lack of many a thing I sought, 137 | And with old woes new wail my dear time's waste: 138 | ` 139 | s.Out = strings.ToUpper(s.In) 140 | s.Command = fmt.Sprintf("echo -n '%s' | tr a-z A-Z", quote(s.In)) 141 | 142 | s = Scripts.Add("fmt.txt") 143 | s.In = `I returned, and saw under the sun, that the race is not to the 144 | swift, nor the battle to the strong, neither yet bread to the wise, 145 | nor yet riches to men of understanding, nor yet favour to men of 146 | skill; but time and chance happeneth to them all.` 147 | s.Out = `I returned, and saw under the sun, that the race is not to the 148 | swift, nor 149 | swift, the 150 | swift, battle 151 | swift, to 152 | swift, the 153 | swift, strong, 154 | swift, neither 155 | swift, yet 156 | swift, bread 157 | swift, to 158 | swift, the 159 | swift, wise, 160 | nor yet riches to men of understanding, nor yet favour to men of 161 | skill; but time and chance happeneth to them all. 162 | ` 163 | s.Command = fmt.Sprintf("fmt --width 10 --prefix swift, < $3 199 | ` 200 | s = Scripts.Add("must-act") 201 | s.Command = ` 202 | exit 0 203 | ` 204 | } 205 | 206 | func quote(s string) string { 207 | return strings.Replace(s, "'", "'\\''", -1) 208 | } 209 | 210 | func (s Script) Write(dir string) error { 211 | return ioutil.WriteFile(filepath.Join(dir, s.GetDoFileName()), []byte(s.Command), os.ModePerm) 212 | } 213 | 214 | func (s Script) GetDoFileName() string { 215 | if len(s.DoFileName) > 0 { 216 | return s.DoFileName 217 | } 218 | return s.Name + ".do" 219 | } 220 | 221 | func (s Script) OutputFileName() string { 222 | return filepath.Join(s.OutDir, s.Name) 223 | } 224 | 225 | func (s Script) CheckOutput(t *testing.T, projectDir string) { 226 | CheckFileContent(t, filepath.Join(projectDir, s.OutputFileName()), s.Out) 227 | } 228 | 229 | func CheckFileContentf(t *testing.T, filepath, want, format string, args ...interface{}) { 230 | b, err := ioutil.ReadFile(filepath) 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | got := string(b) 235 | 236 | if want != got { 237 | output := fmt.Sprintf("Mismatched content for %s.\nWANT:\n[%s]\nGOT:\n[%s]", filepath, want, got) 238 | if len(format) > 0 { 239 | output += fmt.Sprintf(format, args...) 240 | } 241 | t.Error(output) 242 | } 243 | } 244 | 245 | func CheckFileContent(t *testing.T, filepath, want string) { 246 | CheckFileContentf(t, filepath, want, "") 247 | } 248 | 249 | func (s Script) Checks(t *testing.T, dir Dir) { 250 | s.CheckOutput(t, dir.path) 251 | checkMetadata(t, dir.Append(s.GetDoFileName())) 252 | checkMetadata(t, dir.Append(s.OutputFileName())) 253 | checkPrerequisites(t, dir.Append(s.OutputFileName()), s.GetDoFileName()) 254 | } 255 | 256 | type Result struct { 257 | Command string 258 | Stdout string 259 | Stderr string 260 | Err error 261 | } 262 | 263 | func (r Result) String() string { 264 | out := make([]string, 3) 265 | 266 | out = append(out, "Result: {\n") 267 | out = append(out, fmt.Sprintf("Command: %s\n", r.Command)) 268 | 269 | if len(r.Stdout) > 0 { 270 | out = append(out, fmt.Sprintf("Stdout:\n%s\n", r.Stdout)) 271 | } 272 | 273 | if len(r.Stderr) > 0 { 274 | out = append(out, fmt.Sprintf("Stderr:\n%s\n", r.Stderr)) 275 | } 276 | 277 | if r.Err != nil { 278 | out = append(out, fmt.Sprintf("Error:\n%s\n", r.Err)) 279 | } 280 | 281 | out = append(out, "}") 282 | 283 | return strings.Join(out, "") 284 | } 285 | 286 | func (r Result) Error() string { 287 | return r.String() 288 | } 289 | 290 | func run(t *testing.T, cmd *exec.Cmd) Result { 291 | 292 | so, se := new(bytes.Buffer), new(bytes.Buffer) 293 | cmd.Stdout, cmd.Stderr = so, se 294 | 295 | result := Result{Command: fmt.Sprintf("%s %s", cmd.Path, strings.Join(cmd.Args, " "))} 296 | 297 | result.Err = cmd.Run() 298 | 299 | result.Stdout, result.Stderr = so.String(), se.String() 300 | 301 | if testing.Verbose() { 302 | t.Log(result.String()) 303 | } 304 | return result 305 | } 306 | 307 | func (dir Dir) Command(target Script, scripts ...Script) *exec.Cmd { 308 | 309 | t := dir.t 310 | 311 | err := dir.Init() 312 | if err != nil { 313 | t.Fatal(err) 314 | } 315 | 316 | err = target.Write(dir.path) 317 | if err != nil { 318 | t.Fatal(err) 319 | } 320 | 321 | for _, script := range scripts { 322 | err := script.Write(dir.path) 323 | if err != nil { 324 | t.Fatal(err) 325 | } 326 | } 327 | 328 | cmdArgs := []string{} 329 | if testing.Verbose() { 330 | cmdArgs = append(cmdArgs, "-verbose") 331 | } 332 | cmdArgs = append(cmdArgs, target.Name) 333 | binary := "redo" 334 | if s := os.Getenv("REDO_BIN"); len(s) > 0 { 335 | binary = s 336 | } 337 | cmd := exec.Command(binary, cmdArgs...) 338 | 339 | cmd.Dir = dir.path 340 | 341 | return cmd 342 | } 343 | 344 | func (dir Dir) Run(target Script, scripts ...Script) Result { 345 | return run(dir.t, dir.Command(target, scripts...)) 346 | } 347 | 348 | func checkFileMetadata(t *testing.T, path string, m0 *Metadata) { 349 | f, err := NewFile("", path) 350 | if err != nil { 351 | t.Fatal(err) 352 | } 353 | 354 | m1, err := f.NewMetadata() 355 | if err != nil { 356 | t.Fatal(err) 357 | } else if m1 == nil { 358 | t.Fatalf("Missing file (for metadata): " + path) 359 | } 360 | 361 | if !m1.Equal(m0) { 362 | t.Errorf("mismatched record and file metadata for %s", path) 363 | } 364 | } 365 | 366 | func checkMetadata(t *testing.T, path string) { 367 | f, err := NewFile("", path) 368 | if err != nil { 369 | t.Fatal(err) 370 | } 371 | 372 | m0, found, err := f.GetMetadata() 373 | if err != nil { 374 | t.Fatal(err) 375 | } else if !found { 376 | t.Fatalf("Missing record metadata for: " + path) 377 | } 378 | 379 | checkFileMetadata(t, path, &m0) 380 | } 381 | 382 | func checkPrerequisites(t *testing.T, source string, prerequisites ...string) { 383 | f, err := NewFile("", source) 384 | if err != nil { 385 | t.Fatal(err) 386 | } 387 | 388 | list, err := f.Prerequisites() 389 | if err != nil { 390 | t.Fatal(err) 391 | } 392 | 393 | paths := make(map[string]bool) 394 | 395 | for _, p := range list { 396 | paths[p.Path] = true 397 | } 398 | 399 | for _, path := range prerequisites { 400 | if _, ok := paths[path]; !ok { 401 | t.Errorf("File %s is missing prerequisite %s. Looked in %v", source, path, paths) 402 | } 403 | } 404 | } 405 | 406 | // File paths arguments should resolve correctly 407 | func TestRedoArgs(t *testing.T) { 408 | 409 | testCases := []struct { 410 | Name string 411 | Dir string 412 | Path string 413 | }{ 414 | {"pwd_file", "$root", "$target"}, 415 | {"pwd_relative_file", "$root", "./$target"}, 416 | {"pwd_relative_path", "$dir", "$base/$target"}, 417 | {"relative_path", "$dir", "./$base/$target"}, 418 | {"indir_fullpath", "$root", "$root/$target"}, 419 | {"outdir_fullpath", "/tmp", "$root/$target"}, 420 | } 421 | 422 | s := Scripts.Get("allcaps") 423 | 424 | for _, v := range testCases { 425 | 426 | dir, err := newDirAt(t, v.Name) 427 | if err != nil { 428 | t.Fatal(err) 429 | } 430 | defer dir.Cleanup() 431 | 432 | expandfn := func(name string) string { 433 | switch name { 434 | case "root": 435 | return dir.path 436 | case "target": 437 | return s.Name 438 | case "dir": 439 | return filepath.Dir(dir.path) 440 | case "base": 441 | return filepath.Base(dir.path) 442 | } 443 | panic("Unknown expansion variable: " + name) 444 | } 445 | 446 | cmd := dir.Command(s) 447 | cmd.Dir = os.Expand(v.Dir, expandfn) 448 | 449 | // replace the target name arg with test case. 450 | for i, arg := range cmd.Args[1:] { 451 | if arg == s.Name { 452 | cmd.Args[i+1] = os.Expand(v.Path, expandfn) 453 | break 454 | } 455 | } 456 | 457 | t.Logf("cd %s && %s\n", cmd.Dir, strings.Join(cmd.Args, " ")) 458 | 459 | result := run(t, cmd) 460 | if result.Err != nil { 461 | t.Errorf("%s: %s\n", v.Name, result) 462 | continue 463 | } 464 | 465 | s.Checks(t, dir) 466 | } 467 | } 468 | 469 | func CheckMatch(t *testing.T, pattern string, data string) { 470 | matched, err := regexp.Match(pattern, []byte(data)) 471 | if err != nil { 472 | t.Fatalf("%s while matching pattern: %s", err, pattern) 473 | } 474 | if !matched { 475 | t.Errorf("Expected pattern [%s] to match message: [%s]", pattern, data) 476 | } 477 | } 478 | 479 | // Stuff that should fail.. 480 | func TestFailures(t *testing.T) { 481 | tests := []struct { 482 | script Script 483 | pattern string 484 | }{ 485 | // Simple failure 486 | {Scripts.Get("default-fail"), "exit status 1"}, 487 | // A well behaved .do script does not write to both outputs. 488 | {Scripts.Get("multiple-writes"), "Error.+wrote to stdout and to file"}, 489 | {Scripts.Get("must-act"), "no output or file activity"}, 490 | } 491 | 492 | for _, test := range tests { 493 | dir, err := newDir(t) 494 | if err != nil { 495 | t.Fatal(err) 496 | } 497 | defer dir.Cleanup() 498 | 499 | result := dir.Run(test.script) 500 | 501 | if result.Err != nil { 502 | CheckMatch(t, test.pattern, result.Stderr) 503 | } else { 504 | t.Errorf("Expected script %s to fail", test.script.Name) 505 | } 506 | } 507 | } 508 | 509 | //build scripts in higher level directories should work. 510 | func TestBuildScriptLevel(t *testing.T) { 511 | 512 | for _, testScript := range []Script{Scripts.Get("fmt.txt")} { 513 | for _, subdir := range []string{"", "1", "1/2", "1/2/3"} { 514 | for _, doFile := range []string{"default.do", "default.txt.do", testScript.GetDoFileName()} { 515 | dir, err := newDir(t) 516 | if err != nil { 517 | t.Fatal(err) 518 | } 519 | defer dir.Cleanup() 520 | 521 | s := testScript 522 | s.DoFileName = doFile 523 | s.OutDir = subdir 524 | cmd := dir.Command(s) 525 | if len(subdir) > 0 { 526 | cmd.Dir = dir.Append(subdir) 527 | err := os.MkdirAll(cmd.Dir, 0777) 528 | if err != nil { 529 | t.Fatal(err) 530 | } 531 | } 532 | 533 | result := run(t, cmd) 534 | if result.Err != nil { 535 | t.Errorf("%s %s %s: %s\n", dir.path, subdir, doFile, result) 536 | continue 537 | } 538 | 539 | s.Checks(t, dir) 540 | 541 | } 542 | } 543 | } 544 | } 545 | 546 | // This test for redo choosing more specific build scripts over less specific ones. 547 | // In this case, the less specific build scripts fail. 548 | func TestScriptSelectionOrder(t *testing.T) { 549 | 550 | type passfail struct { 551 | Pass Script 552 | Fail Script 553 | } 554 | 555 | cases := []passfail{ 556 | // choose target.ext.do over default.ext.do 557 | {Scripts.Get("fmt.txt"), Scripts.Get("default-txt-fail")}, 558 | 559 | // choose target.ext.do over default.do 560 | {Scripts.Get("fmt.txt"), Scripts.Get("default-fail")}, 561 | 562 | // choose target.do over default.do 563 | {Scripts.Get("allcaps"), Scripts.Get("default-fail")}, 564 | 565 | // choose default.ext.do over default.do 566 | {Scripts.Get("uses-default.txt"), Scripts.Get("default-fail")}, 567 | } 568 | 569 | ext := []string{"x", "a", "b", "c", "d"} 570 | 571 | p := Scripts.Get("allcaps") 572 | p.Name = strings.Join(ext, ".") 573 | 574 | for i := 0; i < len(ext); i++ { 575 | // choose file specific script over any default script. 576 | f := Scripts.Get("default-fail") 577 | 578 | f.Name = strings.Join(append([]string{"default"}, ext[i+1:]...), ".") 579 | 580 | cases = append(cases, passfail{p, f}) 581 | 582 | // chose more specific default script over less specific one. 583 | d := p // copy of successful script 584 | d.DoFileName = f.Name + ".do" // writing to a default do script 585 | 586 | for j := i + 1; j < len(ext); j++ { 587 | f.Name = strings.Join(append([]string{"default"}, ext[j:]...), ".") 588 | cases = append(cases, passfail{d, f}) 589 | } 590 | } 591 | 592 | for _, pair := range cases { 593 | dir, err := newDir(t) 594 | if err != nil { 595 | t.Fatal(err) 596 | } 597 | defer dir.Cleanup() 598 | 599 | result := dir.Run(pair.Pass, pair.Fail) 600 | if result.Err != nil { 601 | t.Errorf("%s: %s\n", dir.path, result) 602 | continue 603 | } 604 | 605 | pair.Pass.Checks(t, dir) 606 | // No need to check for failing script. 607 | // If it ran, Pass would fail! 608 | 609 | } 610 | } 611 | 612 | // Setup scripts and invoke the first one, which should produce expected output. 613 | func SimpleTree(t *testing.T, scripts ...Script) { 614 | if len(scripts) == 0 { 615 | panic("SimpleTree requires at least one script argument") 616 | } 617 | 618 | dir, err := newDir(t) 619 | if err != nil { 620 | t.Fatal(err) 621 | } 622 | defer dir.Cleanup() 623 | first := scripts[0] 624 | var rest []Script 625 | if len(scripts) > 1 { 626 | rest = scripts[1:] 627 | } 628 | 629 | if result := dir.Run(first, rest...); result.Err != nil { 630 | t.Fatal(result) 631 | } 632 | first.Checks(t, dir) 633 | } 634 | 635 | // Basic Dependency -- redo-ifchange 636 | func TestBasicDependency(t *testing.T) { 637 | dir, err := newDir(t) 638 | if err != nil { 639 | t.Fatal(err) 640 | } 641 | defer dir.Cleanup() 642 | 643 | sorted := Scripts.Get("sorted-list") 644 | list := Scripts.Get("list") 645 | 646 | for i := 0; i < 2; i++ { 647 | result := dir.Run(sorted, list) 648 | if result.Err != nil { 649 | t.Errorf("%s: %s\n", dir.path, result) 650 | break 651 | } 652 | 653 | sorted.Checks(t, dir) 654 | list.Checks(t, dir) 655 | 656 | // force source rebuilding 657 | if err := dir.WriteFile(list.OutputFileName(), "Break checksum and timestamp!"); err != nil { 658 | t.Fatal(err) 659 | } 660 | } 661 | } 662 | 663 | // Shared source prerequisite change should trigger ifchange event in all dependents. 664 | // See: https://github.com/gyepisam/redux/issues/6 665 | func TestSharedPrerequisiteChange(t *testing.T) { 666 | dir, err := newDir(t) 667 | if err != nil { 668 | t.Fatal(err) 669 | } 670 | defer dir.Cleanup() 671 | 672 | files := []struct { 673 | name string 674 | content string 675 | }{ 676 | {"shared", ""}, 677 | {"one.x", "one"}, 678 | {"two.x", "two"}, 679 | {"default.y.do", `s="${1%%.y}.x" && redo-ifchange shared $s && cat shared $s | tr a-z A-Z` + "\n"}, 680 | } 681 | 682 | for i, word := range []string{"shared", "boom"} { 683 | // Write most files only once 684 | // Write the first file twice, each time with different content. 685 | files[0].content = word 686 | for _, f := range files { 687 | if err := dir.WriteFile(f.name, f.content); err != nil { 688 | t.Fatal(err) 689 | } 690 | if i == 1 { 691 | break 692 | } 693 | } 694 | 695 | result := dir.Run(Script{Name: "@all", Command: "redo-ifchange one.y two.y"}) 696 | 697 | if result.Err != nil { 698 | t.Fatalf("%s: %s\n", dir.path, result) 699 | continue 700 | } 701 | 702 | // Each turn should produce a different output since the "shared" file changes each time 703 | // and all dependents "one.y" and "two.y" should update accordingly. 704 | for _, name := range []string{"one", "two"} { 705 | CheckFileContent(t, dir.Append(name+".y"), strings.ToUpper(word+name)) 706 | } 707 | } 708 | } 709 | 710 | // Loop detection 711 | func TestDetectLoop(t *testing.T) { 712 | 713 | dir, err := newDir(t) 714 | if err != nil { 715 | t.Fatal(err) 716 | } 717 | defer dir.Cleanup() 718 | 719 | m := func(name, cmd string) Script { 720 | return Script{Name: name, Command: cmd} 721 | } 722 | 723 | result := dir.Run(m(`@all`, `redo-ifchange tick`), 724 | m(`tick`, `redo-ifchange tock; date +%s`), 725 | m(`tock`, `redo-ifchange tick; date +%s`)) 726 | 727 | if result.Err != nil { 728 | CheckMatch(t, "detected on pending target", result.Stderr) 729 | } else { 730 | t.Error("Expected script TestDetectLoop to fail") 731 | } 732 | } 733 | 734 | // The second argument to a do script depends 735 | // on the extensions of the target and do files. 736 | func TestArg2(t *testing.T) { 737 | tests := []struct { 738 | target string 739 | do string 740 | arg2 string 741 | }{ 742 | {`blenny`, `blenny.do`, `blenny`}, 743 | {`blenny`, `default.do`, `blenny`}, 744 | {`blenny.a`, `blenny.a.do`, `blenny.a`}, 745 | {`blenny.a`, `default.a.do`, `blenny`}, 746 | {`blenny.a`, `default.do`, `blenny.a`}, 747 | {`blenny.a.b`, `blenny.a.b.do`, `blenny.a.b`}, 748 | {`blenny.a.b`, `default.a.b.do`, `blenny`}, 749 | {`blenny.a.b`, `default.b.do`, `blenny.a`}, 750 | {`blenny.a.b`, `default.do`, `blenny.a.b`}, 751 | {`blenny.a.b.c`, `blenny.a.b.c.do`, `blenny.a.b.c`}, 752 | {`blenny.a.b.c`, `default.a.b.c.do`, `blenny`}, 753 | {`blenny.a.b.c`, `default.b.c.do`, `blenny.a`}, 754 | {`blenny.a.b.c`, `default.c.do`, `blenny.a.b`}, 755 | {`blenny.a.b.c`, `default.do`, `blenny.a.b.c`}, 756 | } 757 | 758 | for _, r := range tests { 759 | dir, err := newDir(t) 760 | if err != nil { 761 | t.Fatal(err) 762 | } 763 | 764 | entry := Script{Name: `@all`, Command: `redo-ifchange ` + r.target} 765 | subject := Script{Name: r.target, DoFileName: r.do, Command: `echo -n "arg1=$1;arg2=$2"`} 766 | 767 | result := dir.Run(entry, subject) 768 | if result.Err != nil { 769 | t.Fatalf("%v", result) 770 | } 771 | 772 | want := fmt.Sprintf("arg1=%s;arg2=%s", r.target, r.arg2) 773 | CheckFileContentf(t, dir.Append(r.target), want, "\n\nCreated by: %s", r.do) 774 | 775 | dir.Cleanup() 776 | } 777 | } 778 | -------------------------------------------------------------------------------- /redux/ifx.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | 11 | "github.com/gyepisam/redux" 12 | ) 13 | 14 | var cmdIfCreate = &Command{ 15 | Run: runIfCreate, 16 | UsageLine: "redux ifcreate [TARGET...]", 17 | LinkName: "redo-ifcreate", 18 | Short: "Creates dependency on non-existence of targets.", 19 | Long: ` 20 | The ifcreate command creates a dependency on the non-existence of the target files. 21 | The current file will be invalidated if the target comes into existence. 22 | If the target exists, the command returns an error. 23 | `, 24 | } 25 | 26 | func runIfCreate(args []string) error { 27 | return redoIfX(args, func(file *redux.File, dependent *redux.File) error { 28 | return file.RedoIfCreate(dependent) 29 | }) 30 | } 31 | 32 | var cmdIfChange = &Command{ 33 | Run: runIfChange, 34 | UsageLine: "redux ifchange [TARGET...]", 35 | LinkName: "redo-ifchange", 36 | Short: "Creates dependency on targets and ensure that targets are up to date.", 37 | Long: ` 38 | The ifchange command creates a dependency on the target files and ensures that 39 | the target files are up to date, calling the redo command, if necessary. 40 | 41 | The current file will be invalidated if a target is rebuilt. 42 | `, 43 | } 44 | 45 | func runIfChange(args []string) error { 46 | return redoIfX(args, func(file *redux.File, dependent *redux.File) error { 47 | return file.RedoIfChange(dependent) 48 | }) 49 | } 50 | 51 | func redoIfX(args []string, fn func(*redux.File, *redux.File) error) error { 52 | 53 | dependentPath := os.Getenv("REDO_PARENT") 54 | if len(dependentPath) == 0 { 55 | return fmt.Errorf("Missing env variable REDO_PARENT. This program should be run inside a redo script") 56 | } 57 | 58 | wd, err := os.Getwd() 59 | if err != nil { 60 | return err 61 | } 62 | 63 | // The action is triggered by dependent. 64 | dependent, err := redux.NewFile(wd, dependentPath) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | for _, path := range args { 70 | if file, err := redux.NewFile(wd, path); err != nil { 71 | return err 72 | } else if err := fn(file, dependent); err != nil { 73 | return err 74 | } 75 | } 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /redux/init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/gyepisam/redux" 8 | ) 9 | 10 | var cmdInit = &Command{ 11 | Run: runInit, 12 | LinkName: "redo-init", 13 | UsageLine: "redux init [OPTIONS] [DIRECTORY ...]", 14 | Short: "Creates or reinitializes one or more redo root directories.", 15 | } 16 | 17 | func init() { 18 | text := ` 19 | If one or more DIRECTORY arguments are specified, the command initializes each one. 20 | If no arguments are provided, but an environment variable named %s exists, it is initialized. 21 | If neither arguments nor an environment variable is provided, the current directory is initialized. 22 | ` 23 | cmdInit.Long = fmt.Sprintf(text, redux.REDO_DIR_ENV_NAME) 24 | } 25 | 26 | func runInit(args []string) error { 27 | if len(args) == 0 { 28 | if value := os.Getenv(redux.REDO_DIR_ENV_NAME); value != "" { 29 | args = append(args, value) 30 | } 31 | } 32 | 33 | if len(args) == 0 { 34 | args = append(args, ".") 35 | } 36 | 37 | for _, dir := range args { 38 | if err := redux.InitDir(dir); err != nil { 39 | return fmt.Errorf("cannot initialize directory: %s", err) 40 | } 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /redux/install-man-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Test redux installation. 4 | 5 | set -e 6 | scriptdir=$(realpath $(dirname $0)) 7 | 8 | workdir=$(mktemp --tmpdir --directory) 9 | 10 | # avoid breaking inside the source dir 11 | cd $workdir 12 | 13 | trap "rm -rf $workdir" 0 14 | 15 | expected=$(mktemp --tmpdir=$workdir redux-man-XXXXXX) 16 | 17 | cat - > $expected < $t 38 | cmp --verbose $expected $t 39 | } 40 | 41 | dir=$(mktemp --directory --tmpdir=$workdir redux-install-XXXXXX) 42 | 43 | # 0 use --mandir flag value if it exists 44 | run $dir/manA "$bin install --mandir $dir/manA man" 45 | 46 | # 1 use MANDIR if it is set 47 | run $dir/manB "env MANDIR=$dir/manB $bin install man" 48 | 49 | # 2 use first non-empty path in MANPATH if it is set. 50 | # This is equivalent to: $(echo $MANPATH | cut -f1 -d:) except that we skip empty fields. 51 | # There are a few variations on MANPATH 52 | run $dir/manC "env MANPATH=:$dir/manC $bin install man" 53 | run $dir/manD "env MANPATH=$dir/manD: $bin install man" 54 | run $dir/manE "env MANPATH=$dir/manE::does-not-exist $bin install man" 55 | 56 | # 3 use $(dirname redux)/../man if it is writable 57 | 58 | # does not exist yet, but will be created 59 | run $bindir/../man "$bin install man" 60 | 61 | # exists but empty 62 | rm -rf $bindir/../man/* 63 | run $bindir/../man "$bin install man" 64 | 65 | # 4 use first non-empty path in `manpath` if there's one. 66 | # This is equivalent to: $(manpath 2>/dev/null | cut -f1 -d:) except that we skip empty fields. 67 | 68 | # create a directory for manpath to return 69 | alt_man_dir=$(mktemp --directory --tmpdir=$workdir redux-test-4-XXXXXX) 70 | echo MANPATH_MAP $bindir $alt_man_dir > ~/.manpath 71 | 72 | # make $bindir/../man a file so redux can't use it and, instead, calls manpath 73 | rm -rf $bindir/../man 74 | touch $bindir/../man 75 | 76 | # manpath checks ~/.manpath entries against entries in $PATH, so we add it, temporarily. 77 | OLDPATH=$PATH 78 | PATH=$bindir:$PATH 79 | run $alt_man_dir "$bin install man" 80 | PATH=$OLDPATH 81 | 82 | # 5 use '/usr/local/man' 83 | # $bindir/../man is still unusable, 84 | # now mock up a failing manpath 85 | cat > $bindir/manpath <&1) || : 96 | echo "$message" | egrep -q "^Error:.+error.+permission denied" 97 | if test "$?" != "0" ; then 98 | echo "Expected error message: $message" > /dev/stderr 99 | fi 100 | 101 | # cleanup 102 | rm -f ~/.manpath 103 | -------------------------------------------------------------------------------- /redux/install.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "go/build" 8 | "io/ioutil" 9 | "os" 10 | "os/exec" 11 | "path" 12 | "path/filepath" 13 | "strings" 14 | ) 15 | 16 | const docPkg = "github.com/gyepisam/redux" 17 | 18 | var cmdInstall = &Command{ 19 | UsageLine: "redux install [OPTIONS] [component, ...]", 20 | Short: "Installs one or more components", 21 | Long: `The install command is an admin command used for post installation. 22 | The command 23 | 24 | redux install links -- installs links to main binary 25 | redux install [--mandir MANDIR] man -- installs man pages 26 | redux install -- installs all components 27 | 28 | links are installed in $BINDIR, if specified, or the same directory as the executable. 29 | 30 | man pages are installed in the --mandir directory, $MANDIR, the first directory in $MANPATH, 31 | the first directory in manpath's output, or /usr/local/man. 32 | `, 33 | } 34 | 35 | var dryRun bool 36 | var verbose bool 37 | var manDir string 38 | 39 | func init() { 40 | // break loop 41 | cmdInstall.Run = runInstall 42 | 43 | flg := flag.NewFlagSet("install", flag.ContinueOnError) 44 | flg.BoolVar(&dryRun, "n", false, "Dry run. Show actions without running them.") 45 | flg.BoolVar(&verbose, "v", false, "Be verbose. Show actions while running them.") 46 | flg.StringVar(&manDir, "mandir", "", "man page installation directory.") 47 | cmdInstall.Flag = flg 48 | } 49 | 50 | type runner struct { 51 | Name string 52 | Func func() error 53 | } 54 | 55 | var runners = []runner{ 56 | {Name: "links", Func: installLinks}, 57 | {Name: "man", Func: installMan}, 58 | } 59 | 60 | func runInstall(args []string) error { 61 | 62 | var todo []runner 63 | 64 | if len(args) == 0 { 65 | todo = runners 66 | } else { 67 | TOP: 68 | for _, arg := range args { 69 | for _, r := range runners { 70 | if arg == r.Name { 71 | todo = append(todo, r) 72 | continue TOP 73 | } 74 | } 75 | return fmt.Errorf("unknown install target: %s", arg) 76 | } 77 | } 78 | 79 | for _, r := range todo { 80 | err := r.Func() 81 | if err != nil { 82 | return fmt.Errorf("error installing %s. %s", r.Name, err) 83 | } 84 | } 85 | 86 | return nil 87 | } 88 | 89 | // return fullpath to executable file. 90 | func absExePath() (name string, err error) { 91 | name = os.Args[0] 92 | 93 | if name[0] == '.' { 94 | name, err = filepath.Abs(name) 95 | if err == nil { 96 | name = filepath.Clean(name) 97 | } 98 | } else { 99 | name, err = exec.LookPath(filepath.Clean(name)) 100 | } 101 | return 102 | } 103 | 104 | func installLinks() (err error) { 105 | // link target must be absolute, no matter how it was invoked 106 | oldname, err := absExePath() 107 | if err != nil { 108 | return 109 | } 110 | 111 | var dirname string 112 | if s := os.Getenv("BINDIR"); s != "" { 113 | dirname = s 114 | } else { 115 | dirname = filepath.Dir(oldname) 116 | } 117 | 118 | for _, cmd := range commands { 119 | if cmd.LinkName == "" { 120 | continue 121 | } 122 | 123 | newname := filepath.Join(dirname, cmd.LinkName) 124 | if oldname == newname { 125 | continue 126 | } 127 | 128 | if dryRun || verbose { 129 | fmt.Fprintf(os.Stderr, "ln %s %s\n", oldname, newname) 130 | if dryRun { 131 | continue 132 | } 133 | } 134 | 135 | err = os.MkdirAll(filepath.Dir(newname), 0755) 136 | if err != nil { 137 | break 138 | } 139 | 140 | err = os.Remove(newname) 141 | if err != nil && !os.IsNotExist(err) { 142 | break 143 | } 144 | 145 | err = os.Link(oldname, newname) 146 | if err != nil { 147 | break 148 | } 149 | } 150 | 151 | return 152 | } 153 | 154 | func findPackageDir(pkgName string) (string, error) { 155 | pkg, err := build.Import(pkgName, "", build.FindOnly) 156 | if err != nil { 157 | return "", err 158 | } 159 | if pkg.Dir == "" { 160 | return "", fmt.Errorf("no directory for package: %s", pkgName) 161 | } 162 | return pkg.Dir, nil 163 | } 164 | 165 | /* 166 | 167 | 0 use --mandir flag value if it exists 168 | 1 use MANDIR if it is set 169 | 2 use first non-empty path in MANPATH if it is set. 170 | This is equivalent to: $(echo $MANPATH | cut -f1 -d:) except that we skip empty fields. 171 | 3 use $(dirname redux)/../man if it is writable 172 | 4 use first non-empty path in `manpath` if there's one. 173 | This is equivalent to: $(manpath 2>/dev/null | cut -f1 -d:) except that we skip empty fields. 174 | 5 use '/usr/local/man' 175 | 176 | See https://github.com/gyepisam/redux/issues/4 for details. 177 | 178 | */ 179 | func findManDir() (string, error) { 180 | 181 | if manDir != "" { 182 | return manDir, nil 183 | } 184 | 185 | if s := os.Getenv("MANDIR"); s != "" { 186 | return s, nil 187 | } 188 | 189 | if s := os.Getenv("MANPATH"); s != "" { 190 | // MANPATH values can have a colon at the start, at the end, two in the middle or none at all. 191 | // Find and return the first non-empty path in the list. 192 | paths := strings.Split(s, ":") 193 | for _, path := range paths { 194 | if len(path) > 0 { 195 | return path, nil 196 | } 197 | } 198 | } 199 | 200 | // The go/.../man directory may not exist and if it does, may not be writable. 201 | // Test both before commiting to using it. 202 | path, err := func() (dirname string, err error) { 203 | path, err := absExePath() 204 | if err != nil { 205 | return 206 | } 207 | 208 | dir := filepath.Clean(filepath.Join(filepath.Dir(path), "..", "man")) 209 | 210 | // This is more of a club than a scalpel, but is a more effective and safer alternative 211 | // to access(2) which isn't available in Go anyway. 212 | err = os.MkdirAll(dir, 0755) 213 | if err != nil { 214 | return 215 | } 216 | 217 | // See if we can write to it 218 | tmpDir, err := ioutil.TempDir(dir, "redux-install-man-test-") 219 | if err != nil { 220 | return 221 | } 222 | 223 | os.Remove(tmpDir) 224 | 225 | return dir, nil 226 | }() 227 | 228 | if err == nil { 229 | return path, nil 230 | } 231 | 232 | cmd := exec.Command("manpath") 233 | b, err := cmd.Output() 234 | // Ignore error; either it doesn't exist or is somehow broken. 235 | if err == nil { 236 | // Find and return the first non-empty path in the list 237 | paths := bytes.Split(b, []byte{':'}) 238 | for _, path := range paths { 239 | if len(path) > 0 { 240 | return string(path), nil 241 | } 242 | } 243 | } 244 | 245 | return "/usr/local/man", nil 246 | } 247 | 248 | func copyFile(dst, src string, perm os.FileMode) error { 249 | b, err := ioutil.ReadFile(src) 250 | if err != nil { 251 | return err 252 | } 253 | return ioutil.WriteFile(dst, b, perm) 254 | } 255 | 256 | func installMan() (err error) { 257 | pkgDir, err := findPackageDir(docPkg) 258 | if err != nil { 259 | return 260 | } 261 | 262 | if err != nil { 263 | return 264 | } 265 | 266 | manDir, err := findManDir() 267 | if err != nil { 268 | return 269 | } 270 | 271 | for _, section := range strings.Split("1", "") { 272 | srcFiles, err := filepath.Glob(path.Join(pkgDir, "doc", "*."+section)) 273 | if err != nil { 274 | return err 275 | } 276 | 277 | for _, src := range srcFiles { 278 | srcInfo, err := os.Stat(src) 279 | if err != nil { 280 | return err 281 | } 282 | 283 | dstDir := path.Join(manDir, "man"+section) 284 | dst := path.Join(dstDir, srcInfo.Name()) 285 | if dryRun || verbose { 286 | fmt.Fprintf(os.Stderr, "cp %s %s\n", src, dst) 287 | if dryRun { 288 | continue 289 | } 290 | } 291 | 292 | err = os.MkdirAll(dstDir, 0755) 293 | if err != nil { 294 | return err 295 | } 296 | 297 | err = copyFile(dst, src, srcInfo.Mode()&os.ModePerm) 298 | if err != nil { 299 | return err 300 | } 301 | } 302 | } 303 | 304 | return nil 305 | } 306 | -------------------------------------------------------------------------------- /redux/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "text/template" 12 | ) 13 | 14 | // Command represents a redux command such as redo, ifchange, etc. 15 | type Command struct { 16 | 17 | // Run runs the command. 18 | Run func(args []string) error 19 | 20 | // LinkName is the the name used to link to the executable so it can be called as such. 21 | LinkName string 22 | 23 | // UsageLine shows the usage for the command. 24 | UsageLine string 25 | 26 | // Short is a short, single line, description. 27 | Short string 28 | 29 | // Long is a long description. 30 | Long string 31 | 32 | // Flag is a list of flags that the command handles. 33 | Flag *flag.FlagSet 34 | 35 | // Denotes whether the help flag has been invoked 36 | Help bool 37 | } 38 | 39 | // Name returns the name of the command, which is the second word in UsageLine. 40 | func (cmd *Command) Name() string { 41 | s := strings.SplitN(cmd.UsageLine, " ", 3) 42 | if len(s) < 2 { 43 | return cmd.UsageLine 44 | } 45 | return s[1] 46 | } 47 | 48 | var commands = []*Command{ 49 | cmdInit, 50 | cmdIfChange, 51 | cmdIfCreate, 52 | cmdRedo, 53 | cmdInstall, 54 | } 55 | 56 | func cmdByName(name string) *Command { 57 | for _, cmd := range commands { 58 | if name == cmd.Name() { 59 | return cmd 60 | } 61 | } 62 | return nil 63 | } 64 | 65 | func cmdByLinkName(linkName string) *Command { 66 | for _, cmd := range commands { 67 | if linkName == cmd.LinkName { 68 | return cmd 69 | } 70 | } 71 | return nil 72 | } 73 | 74 | var wantHelp bool 75 | 76 | func initFlags() { 77 | 78 | helpFlags := []string{"help", "h", "?"} 79 | helpUsage := "Show help" 80 | 81 | for _, name := range helpFlags { 82 | flag.BoolVar(&wantHelp, name, false, helpUsage) 83 | } 84 | 85 | for _, cmd := range commands { 86 | name := cmd.Name() 87 | if cmd.Flag == nil { 88 | cmd.Flag = flag.NewFlagSet(name, flag.ContinueOnError) 89 | } 90 | cmd.Flag.Usage = func() { 91 | printHelp(os.Stderr, name) 92 | } 93 | 94 | if f := cmd.Flag.Lookup("help"); f == nil { 95 | for _, name := range helpFlags { 96 | cmd.Flag.BoolVar(&cmd.Help, name, false, helpUsage) 97 | } 98 | } 99 | } 100 | } 101 | 102 | func main() { 103 | 104 | initFlags() 105 | 106 | // Called by link? 107 | cmd := cmdByLinkName(filepath.Base(os.Args[0])) 108 | if cmd != nil { 109 | runCommand(cmd, os.Args[1:]) 110 | return 111 | } 112 | 113 | flag.Parse() 114 | if wantHelp { 115 | printHelpAll(os.Stderr) 116 | return 117 | } 118 | 119 | args := flag.Args() 120 | if len(args) < 1 { 121 | printHelpAll(os.Stderr) 122 | os.Exit(2) 123 | return 124 | } 125 | 126 | cmdName := args[0] 127 | 128 | if cmdName == "help" || cmdName == "documentation" { 129 | 130 | if len(args) < 2 { 131 | printHelpAll(os.Stdout) 132 | return 133 | } 134 | 135 | cmd := cmdByName(args[1]) 136 | if cmd == nil { 137 | printUnknown(os.Stderr, args[1]) 138 | os.Exit(2) 139 | } else { 140 | cmd.printDoc(os.Stdout, cmdName) 141 | } 142 | return 143 | } 144 | 145 | cmd = cmdByName(cmdName) 146 | if cmd == nil { 147 | printUnknown(os.Stderr, cmdName) 148 | os.Exit(2) 149 | return 150 | } 151 | 152 | runCommand(cmd, args[1:]) 153 | return 154 | } 155 | 156 | func runCommand(cmd *Command, args []string) { 157 | err := cmd.Flag.Parse(args) 158 | if err != nil || cmd.Help { 159 | printHelp(os.Stderr, cmd.Name()) 160 | os.Exit(0) 161 | return 162 | } 163 | 164 | err = cmd.Run(cmd.Flag.Args()) 165 | if err != nil { 166 | fatalErr(err) 167 | return 168 | } 169 | os.Exit(0) 170 | } 171 | 172 | var templates = map[string]string{ 173 | "overview": `redux is an implementation of the redo top down build tools. 174 | 175 | Usage: redux command [options] [arguments] 176 | 177 | Commands: 178 | {{range .}} 179 | {{.Name | printf "%11s"}} -- {{.Short}} 180 | {{end}} 181 | 182 | See 'redux help [command]' for details about each command. 183 | `, 184 | 185 | "help": `{{.Name}} - {{.Short}} 186 | 187 | Usage: {{.UsageLine}} 188 | 189 | Options 190 | 191 | {{.Options}} 192 | 193 | {{.Long}} 194 | `, 195 | "documentation": ` 196 | # NAME 197 | 198 | {{.Name}} - {{.Short}} 199 | 200 | # SYNOPSIS 201 | 202 | {{.UsageLine}} 203 | 204 | # OPTIONS 205 | 206 | {{.Options}} 207 | 208 | # NOTES 209 | 210 | {{.Long}} 211 | `, 212 | } 213 | 214 | func (cmd *Command) printDoc(out io.Writer, docType string) { 215 | text, ok := templates[docType] 216 | if !ok { 217 | panic("unknown docType: " + docType) 218 | } 219 | 220 | tmpl, err := template.New(docType).Parse(text) 221 | if err != nil { 222 | panic(err) 223 | } 224 | 225 | if docType == "overview" { 226 | err = tmpl.Execute(out, commands) 227 | if err != nil { 228 | panic(err) 229 | } 230 | return 231 | } 232 | 233 | var buf bytes.Buffer 234 | cmd.Flag.SetOutput(&buf) 235 | cmd.Flag.PrintDefaults() 236 | 237 | data := map[string]string{ 238 | "Name": cmd.Name(), 239 | "UsageLine": cmd.UsageLine, 240 | "Short": cmd.Short, 241 | "Long": cmd.Long, 242 | "Options": buf.String(), 243 | } 244 | 245 | err = tmpl.Execute(out, data) 246 | if err != nil { 247 | panic(err) 248 | } 249 | } 250 | 251 | func printHelpAll(out io.Writer) { 252 | cmd := &Command{} 253 | cmd.printDoc(out, "overview") 254 | } 255 | 256 | func printUnknown(out io.Writer, name string) { 257 | fmt.Fprintf(out, "%s: unknown command %s. See %s --help\n", os.Args[0], name, os.Args[0]) 258 | } 259 | 260 | func printHelp(out io.Writer, args ...string) { 261 | 262 | if len(args) == 0 { 263 | printHelpAll(out) 264 | return 265 | } 266 | 267 | cmdName := args[0] 268 | 269 | cmd := cmdByName(cmdName) 270 | if cmd == nil { 271 | printUnknown(out, cmdName) 272 | return 273 | } 274 | 275 | cmd.printDoc(out, "help") 276 | } 277 | 278 | func fatal(format string, args ...interface{}) { 279 | fmt.Fprintf(os.Stderr, "Error: %s: ", os.Args[0]) 280 | fmt.Fprintf(os.Stderr, format+"\n", args...) 281 | os.Exit(1) 282 | } 283 | 284 | func fatalErr(err error) { 285 | fatal("%s", err) 286 | } 287 | -------------------------------------------------------------------------------- /redux/redo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/gyepisam/fileutils" 10 | "github.com/gyepisam/multiflag" 11 | "github.com/gyepisam/redux" 12 | ) 13 | 14 | var cmdRedo = &Command{ 15 | UsageLine: "redux redo [OPTION]... [TARGET]...", 16 | Short: "Builds files atomically.", 17 | LinkName: "redo", 18 | } 19 | 20 | func init() { 21 | // break loop 22 | cmdRedo.Run = runRedo 23 | 24 | text := ` 25 | The redo command builds files atomically by running a do script associated with the target. 26 | 27 | redo normally requires one or more target arguments. 28 | If no target argument is provided, redo runs the default do script %s if it exists in the current 29 | directory. 30 | 31 | For compatibility, if %s does not exist, but %s exists, it is used instead. 32 | ` 33 | it := redux.TASK_PREFIX + redux.DEFAULT_DO 34 | cmdRedo.Long = fmt.Sprintf(text, it, it, redux.DEFAULT_DO) 35 | } 36 | 37 | var ( 38 | verbosity *multiflag.Value 39 | debug *multiflag.Value 40 | isTask bool 41 | shArgs string 42 | ignored bool // like /dev/null for variables 43 | ) 44 | 45 | func init() { 46 | flg := flag.NewFlagSet("redo", flag.ContinueOnError) 47 | 48 | verbosity = multiflag.BoolSet(flg, "verbose", "false", "Be verbose. Repeat for intensity.", "v") 49 | 50 | debug = multiflag.BoolSet(flg, "debug", "false", "Print debugging output.", "d") 51 | 52 | flg.BoolVar(&isTask, "task", false, "Run .do script for side effects and ignore output.") 53 | 54 | flg.StringVar(&shArgs, "sh", "", "Extra arguments for /bin/sh.") 55 | 56 | flg.BoolVar(&ignored, "old-args", false, "Ignored apenwarr redo compatibility flag") 57 | 58 | cmdRedo.Flag = flg 59 | } 60 | 61 | func runRedo(targets []string) error { 62 | 63 | // set options from environment if not provided. 64 | if verbosity.NArg() == 0 { 65 | for i := len(os.Getenv("REDO_VERBOSE")); i > 0; i-- { 66 | verbosity.Set("true") 67 | } 68 | } 69 | 70 | if debug.NArg() == 0 { 71 | if len(os.Getenv("REDO_DEBUG")) > 0 { 72 | debug.Set("true") 73 | } 74 | } 75 | 76 | if s := shArgs; s != "" { 77 | os.Setenv("REDO_SHELL_ARGS", s) 78 | redux.ShellArgs = s 79 | } 80 | 81 | // if shell args are set, ensure that at least minimal verbosity is also set. 82 | if redux.ShellArgs != "" && (verbosity.NArg() == 0) { 83 | verbosity.Set("true") 84 | } 85 | 86 | // Set explicit options to avoid clobbering environment inherited options. 87 | if n := verbosity.NArg(); n > 0 { 88 | os.Setenv("REDO_VERBOSE", strings.Repeat("x", n)) 89 | redux.Verbosity = n 90 | } 91 | 92 | if n := debug.NArg(); n > 0 { 93 | os.Setenv("REDO_DEBUG", "true") 94 | redux.Debug = true 95 | } 96 | 97 | // If no argument is specified, use default target if its .do file exists. 98 | // Otherwise, print usage and exit. 99 | if len(targets) == 0 { 100 | for _, prefix := range []string{redux.TASK_PREFIX, ""} { 101 | doFile := prefix + redux.DEFAULT_DO 102 | if found, err := fileutils.FileExists(doFile); err != nil { 103 | return err 104 | } else if found { 105 | targets = append(targets, prefix+redux.DEFAULT_TARGET) 106 | break 107 | } 108 | } 109 | 110 | if len(targets) == 0 { 111 | cmdRedo.Flag.Usage() 112 | os.Exit(1) 113 | return nil 114 | } 115 | } 116 | 117 | wd, err := os.Getwd() 118 | if err != nil { 119 | return err 120 | } 121 | 122 | // It *is* slower to reinitialize for each target, but doing 123 | // so guarantees that a single redo call with multiple targets that 124 | // potentially have differing roots will work correctly. 125 | for _, path := range targets { 126 | file, err := redux.NewFile(wd, path) 127 | if err != nil { 128 | return err 129 | } 130 | file.SetTaskFlag(isTask) 131 | if err := file.Redo(); err != nil { 132 | return err 133 | } 134 | } 135 | 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /relpath.go: -------------------------------------------------------------------------------- 1 | package redux 2 | 3 | import "path/filepath" 4 | 5 | // RelPath is a structure to simplify paths from 6 | // directory upwards traversal 7 | type RelPath struct { 8 | entries []string 9 | } 10 | 11 | // Add an entry 12 | func (r *RelPath) Add(s string) { 13 | r.entries = append(r.entries, s) 14 | } 15 | 16 | // Reverse and Join 17 | func (r *RelPath) Join() string { 18 | s := make([]string, len(r.entries)) 19 | for i, j := 0, len(s)-1; i < len(s); i, j = i+1, j-1 { 20 | s[i] = r.entries[j] 21 | } 22 | return filepath.Join(s...) 23 | } 24 | -------------------------------------------------------------------------------- /stat.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package redux 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | "syscall" 9 | ) 10 | 11 | func statUidGid(finfo os.FileInfo) (uint32, uint32, error) { 12 | sys := finfo.Sys() 13 | if sys == nil { 14 | return 0, 0, errors.New("finfo.Sys() is unsupported") 15 | } 16 | stat := sys.(*syscall.Stat_t) 17 | return stat.Uid, stat.Gid, nil 18 | } 19 | -------------------------------------------------------------------------------- /stat_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package redux 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | ) 9 | 10 | func statUidGid(finfo os.FileInfo) (uint32, uint32, error) { 11 | return 0, 0, errors.New("finfo.Sys() is unsupported") 12 | } 13 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | d=$(dirname $0) 5 | 6 | sh $d/build 7 | $d/bin/redux install links 8 | go test $@ 9 | 10 | sh redux/install-man-test.sh 11 | -------------------------------------------------------------------------------- /tree_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | import ( 8 | "math/rand" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | // Create and test a maximally linked dependency graph with no cycles. 14 | // Since most nodes have multiple dependencies and prerequisites, a node 15 | // will be invoked as a redo-ifchange prerequisite multiple times. 16 | // This test verifies that despite the multiple invocations, each node is built just once. 17 | // The test also varies the dependency orderings to ensure that the result does not depend 18 | // in some particularly auspicious order. 19 | func TestDeepTree(t *testing.T) { 20 | const N = 10 // creates a tree with N nodes and N * (N - 1) / 2 dependencies 21 | const out = "1" 22 | 23 | tree := make([]Script, N) 24 | head := make([]string, N) //part of script prior to body 25 | tail := make([]string, N) //part of script after the body 26 | 27 | // A script that counts its invocations. 28 | // Multiple invocations will produce incorrect output (the number of extra invocations). 29 | // Each script's output will be included in its dependencies' output. 30 | body := ` 31 | value=1 32 | if test -e $1 ; then 33 | value=$(expr $(cat $1) + 1) 34 | fi 35 | printf "%d" $value 36 | ` 37 | for i := 0; i < N; i++ { 38 | name := string('A' + i) 39 | tree[i] = Script{Name: name, Out: out} 40 | head[i] = "redo-ifchange " + name 41 | tail[i] = "cat " + name 42 | } 43 | 44 | for _, order := range []string{"forward", "reverse", "shuffle"} { 45 | for k := 0; k < N; k++ { 46 | tmp := head[k+1 : N] //each node depends on all succeeding nodes. 47 | head0 := make([]string, len(tmp)) 48 | copy(head0, tmp) 49 | 50 | tmp = tail[k+1 : N] 51 | tail0 := make([]string, len(tmp)) 52 | copy(tail0, tmp) 53 | 54 | switch order { 55 | case "forward": 56 | //do nothing 57 | case "reverse": 58 | for i, j := 0, len(head0)-1; i < j; i, j = i+1, j-1 { 59 | head0[i], head0[j] = head0[j], head0[i] 60 | tail0[i], tail0[j] = tail0[j], tail0[i] 61 | } 62 | case "shuffle": 63 | //fischer yates shuffle 64 | for i := len(head0) - 1; i > 0; i-- { 65 | j := rand.Intn(i + 1) 66 | head0[i], head0[j] = head0[j], head0[i] 67 | tail0[i], tail0[j] = tail0[j], tail0[i] 68 | } 69 | default: 70 | panic("unknown order: " + order) 71 | } 72 | 73 | tree[k].Command = strings.Join(head0, "\n") + body + strings.Join(tail0, "\n") 74 | } 75 | 76 | // Each new node N+1 becomes a prerequisite for each of the previous N nodes. 77 | // Since each node, includes its prerequisite's output, the N+1'st node increases 78 | // the output by N. The total output size is 2^(N-1). 79 | // One could also use a counting argument: 1 node produces 1, 2 -> 2, 3 -> 4, 4 -> 8, etc. 80 | tree[0].Out = strings.Repeat(out, 1< $3 127 | chmod +x $3 128 | ` 129 | SimpleTree(t, s0, s1) 130 | } 131 | 132 | // Redo target directories may be created by the do script. 133 | func TestNoneExistentDir(t *testing.T) { 134 | s0 := Script{Name: "A", Out: "AB"} 135 | s0.Command = ` 136 | path=$(dirname $1)/X/Y/Z/B 137 | redo-ifchange $path 138 | echo -n A 139 | cat $path 140 | ` 141 | s1 := Script{Name: "B"} 142 | s1.Command = ` 143 | #Produce output for each invocation 144 | if test -e $1 ; then 145 | cat $1 146 | fi 147 | mkdir -p $(dirname $1) 148 | echo -n B 149 | ` 150 | SimpleTree(t, s0, s1) 151 | } 152 | 153 | // Uid and Gid should be preserved across device copy operations. 154 | // At least on unix systems 155 | // Going to cheat here and assume that a successful Gid change 156 | // implies a potentially successful Uid change. 157 | func TestUidGid(t *testing.T) { 158 | s0 := Script{Name: "A", Out: "GOT GID"} 159 | s0.Command = ` 160 | redo-ifchange B 161 | wantgroup=$(groups | awk '{print $2}') 162 | gotgroup=$(stat --printf="%G" B) 163 | if test "$wantgroup" = "$gotgroup" ; then 164 | cat B 165 | fi 166 | ` 167 | s1 := Script{Name: "B"} 168 | s1.Command = ` 169 | echo -n "GOT GID" > $3 170 | grp=$(groups | awk '{print $2}') 171 | chown :$grp $3 172 | ` 173 | SimpleTree(t, s0, s1) 174 | } 175 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gyepi Sam. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package redux 6 | 7 | import ( 8 | "crypto/sha1" 9 | "encoding/hex" 10 | "fmt" 11 | "io/ioutil" 12 | ) 13 | 14 | type Hash string 15 | 16 | func MakeHash(content interface{}) Hash { 17 | 18 | hash := sha1.New() 19 | 20 | switch c := content.(type) { 21 | case []byte: 22 | hash.Write(c) 23 | case string: 24 | hash.Write([]byte(c)) 25 | default: 26 | panic(fmt.Errorf("Unhandled argument: %+v", content)) 27 | } 28 | 29 | return Hash(hex.EncodeToString(hash.Sum(nil))) 30 | } 31 | 32 | func ContentHash(path string) (hash Hash, err error) { 33 | b, err := ioutil.ReadFile(path) 34 | if err == nil { 35 | hash = MakeHash(b) 36 | } 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /z_test.go: -------------------------------------------------------------------------------- 1 | package redux 2 | 3 | // This file exists to setup the binary for tests and to cleanup. 4 | // Please don't create any test files that sort lower than z 5 | // and don't add more tests to this file. 6 | 7 | import ( 8 | "io/ioutil" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | var binDir string 17 | 18 | // build the binary, just for testing. 19 | // 1. Ensures that we are using current code 20 | // 2. Not inadvertenly running a version installed somewhere else in the path. 21 | func init() { 22 | var err error 23 | 24 | binDir, err = ioutil.TempDir("", "redux-test-bin-") 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | bin := filepath.Join(binDir, "/redux") 30 | 31 | err = exec.Command("go", "build", "-o", bin, "github.com/gyepisam/redux/redux").Run() 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | err = exec.Command(bin, "install", "links").Run() 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | //redux should be first in path. 42 | path := []string{binDir} 43 | 44 | // add all other path entries, except .../go/bin entries. 45 | for _, slot := range strings.Split(os.Getenv("PATH"), ":") { 46 | if len(slot) > 0 && strings.Index(slot, "/go/bin") == -1 { 47 | path = append(path, slot) 48 | } 49 | } 50 | 51 | err = os.Setenv("PATH", strings.Join(path, ":")) 52 | if err != nil { 53 | panic(err) 54 | } 55 | } 56 | 57 | // This is not a test and must always be the last function in the file! 58 | func TestAtExit(t *testing.T) { 59 | os.RemoveAll(binDir) 60 | } 61 | --------------------------------------------------------------------------------