├── .envrc ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .vscode ├── copyright.code-snippets ├── extensions.json └── tasks.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── all.do ├── check.do ├── clean.do ├── debug.do ├── default.zip.do ├── flake.lock ├── flake.nix ├── minimal ├── default.zz.do ├── do ├── fakedir │ └── .empty └── test.do ├── redo-zombiezen.nix ├── redo ├── py.do ├── sh.do └── whichpython.do ├── release-name.sh ├── release.do ├── rust-toolchain ├── src ├── bin │ └── redo │ │ ├── always.rs │ │ ├── ifchange.rs │ │ ├── ifcreate.rs │ │ ├── log.rs │ │ ├── main.rs │ │ ├── ood.rs │ │ ├── sources.rs │ │ ├── stamp.rs │ │ ├── targets.rs │ │ ├── unlocked.rs │ │ └── whichdo.rs ├── builder.rs ├── cycles.rs ├── deps.rs ├── env.rs ├── error.rs ├── exits.rs ├── helpers.rs ├── jobserver.rs ├── lib.rs ├── logs.rs ├── paths.rs └── state.rs ├── t ├── .gitignore ├── 000-set-minus-e │ ├── .gitignore │ ├── all.do │ ├── clean.do │ └── fatal.do ├── 010-jobserver │ ├── .gitignore │ ├── all.do │ ├── clean.do │ ├── default.spin.do │ ├── default.x.do │ ├── first.do │ ├── parallel.do │ ├── parallel2.do │ ├── paralleltest.do │ ├── second.do │ └── serialtest.do ├── 100-args │ ├── .gitignore │ ├── all.do │ ├── clean.do │ ├── default.args.do │ ├── noargs │ │ ├── all.do │ │ └── run.do │ ├── passfail.do │ ├── passfailtest.do │ └── test2.args.do ├── 101-atime │ ├── .gitignore │ ├── all.do │ ├── atime.do │ ├── atime2.do │ ├── clean.do │ └── tick.py ├── 102-empty │ ├── .gitignore │ ├── all.do │ ├── blank.do │ ├── clean.do │ ├── silencetest.do │ └── touchtest.do ├── 103-unicode │ ├── all.do │ ├── clean.do │ └── unicode.do ├── 104-space │ ├── all.do │ ├── clean.do │ └── space dir │ │ ├── .gitignore │ │ ├── clean.do │ │ ├── space 2.do │ │ ├── space file.do │ │ └── test.do ├── 105-sympath │ ├── .gitignore │ ├── all.do │ ├── clean.do │ └── default.dyn.do ├── 110-compile │ ├── .gitignore │ ├── CC.do │ ├── LD.do │ ├── all.do │ ├── bellow.do │ ├── clean.do │ ├── hello.c │ ├── hello.do │ ├── hello.o.do │ ├── yellow.do │ └── yellow.o.do ├── 111-example │ ├── .gitignore │ ├── CC.do │ ├── Makefile │ ├── all.do │ ├── clean.do │ ├── config.sh │ ├── default.o.do │ ├── hello.do │ ├── main.c │ ├── mystr.c │ └── mystr.h ├── 120-defaults-flat │ ├── .gitignore │ ├── all.do │ ├── c.c.c.b.b.a │ ├── c.do │ ├── clean.do │ ├── default.b.do │ ├── default.c.c.do │ ├── default.c.do │ └── default.do ├── 121-defaults-nested │ ├── .gitignore │ ├── a │ │ ├── b │ │ │ ├── default.y.z.do │ │ │ └── file.x.y.z.do │ │ ├── d │ │ │ └── default.do │ │ ├── default.x.y.z.do │ │ └── default.z.do │ ├── all.do │ ├── clean.do │ ├── default.do │ └── test.do ├── 122-defaults-parent │ ├── .gitignore │ ├── all.do │ ├── clean.do │ ├── inner │ │ └── default.do │ ├── inner2 │ │ └── default.do │ └── x │ │ └── file ├── 130-mode │ ├── .gitignore │ ├── all.do │ ├── clean.do │ └── mode1.do ├── 140-shuffle │ ├── .gitignore │ ├── all.do │ ├── clean.do │ └── default.x.do ├── 141-keep-going │ ├── .gitignore │ ├── all.do │ ├── clean.do │ ├── default.fail.do │ └── default.ok.do ├── 200-shell │ ├── .gitignore │ ├── all.do │ ├── clean.do │ ├── default.vartest.do │ ├── nonshelltest.do │ ├── shelltest.do │ └── vartest.do ├── 201-fail │ ├── .gitignore │ ├── all.do │ ├── clean.do │ ├── fail.do │ └── maybe-fail.do ├── 202-del │ ├── .gitignore │ ├── all.do │ ├── clean.do │ ├── default.spam1.do │ ├── default.spam2.do │ ├── deltest.do │ ├── deltest2.do │ ├── deltest3.do │ └── deltest4.do ├── 203-make │ ├── .gitignore │ ├── Makefile │ ├── all.do │ ├── clean.do │ ├── default.out.do │ ├── whichmake.do │ ├── wipe-redo.sh │ ├── x.do │ └── y.do ├── 204-makeflags │ ├── all.do │ ├── clean.do │ ├── closefds.py │ └── noflags.do ├── 205-readonly │ ├── .gitignore │ ├── all.do │ └── clean.do ├── 220-ifcreate │ ├── .gitignore │ ├── all.do │ ├── clean.do │ ├── ifcreate1.do │ └── ifcreate2.do ├── 250-makedir │ ├── .gitignore │ ├── all.do │ ├── autosubdir │ │ ├── all.do │ │ ├── clean.do │ │ └── default.txt.do │ ├── clean.do │ ├── dirtest │ │ ├── .gitignore │ │ ├── all.do │ │ ├── clean.do │ │ ├── dir1 │ │ │ ├── go.do │ │ │ └── stinky.do │ │ └── t1.do │ ├── makedir.do │ └── makedir2.do ├── 260-whichdo │ ├── all.do │ ├── default.y.z.do │ ├── defaults.do │ ├── exists.do │ ├── fakesub │ │ └── .empty │ ├── fakesub2 │ │ ├── default.do │ │ └── snork.do │ └── nonexists.do ├── 350-deps │ ├── .gitignore │ ├── all.do │ ├── basic │ │ ├── .gitignore │ │ ├── 1.in │ │ ├── 2.in │ │ ├── clean.do │ │ ├── default.out.do │ │ └── test.do │ ├── broken.do │ ├── clean.do │ ├── doublestatic.do │ ├── genfile1.do │ ├── gentest.do │ ├── ifchange-fail.do │ ├── override │ │ ├── .gitignore │ │ ├── a.do │ │ ├── all.do │ │ ├── b.do │ │ └── clean.do │ ├── overwrite.do │ ├── overwrite1.do │ ├── overwrite2.do │ ├── overwrite3.do │ ├── static.in │ ├── static1.do │ ├── static2.do │ ├── t1a.do │ ├── t1dep.do │ ├── t2.do │ ├── test1.do │ └── test2.do ├── 351-deps-forget │ ├── .gitignore │ ├── all.do │ ├── bork.do │ ├── clean.do │ └── sub.do ├── 355-deps-cyclic │ ├── a.do │ ├── all.do │ ├── b.do │ └── clean.do ├── 360-symlinks │ ├── .gitignore │ ├── a.do │ ├── all.do │ ├── b.do │ └── clean.do ├── 370-logs │ ├── .gitignore │ ├── a │ │ └── b │ │ │ └── xlog.do │ ├── all.do │ ├── clean.do │ ├── x.do │ └── y.do ├── 550-chdir │ ├── .gitignore │ ├── all.do │ ├── chdir1.do │ ├── chdir2.do │ ├── chdir3.do │ └── clean.do ├── 640-always │ ├── .gitignore │ ├── all.do │ ├── always1.do │ └── clean.do ├── 950-curse │ ├── .gitignore │ ├── all.do │ ├── check-1.sh │ ├── check-2.sh │ ├── clean.do │ ├── countall.do │ ├── default.n0.do │ ├── default.n1.do │ ├── default.n2.do │ └── seq ├── all.do ├── clean.do ├── dotparams.od ├── flush-cache.do ├── flush-cache.in ├── nothing.od ├── s60-stamp │ ├── .gitignore │ ├── a.do │ ├── ab.do │ ├── abc.do │ ├── all.do │ ├── b.do │ ├── bob.do │ ├── c │ ├── clean.do │ ├── stamptest.do │ ├── stampy.do │ ├── usestamp.do │ └── usestamp2.do ├── shelltest.od ├── skip-if-minimal-do.sh ├── sleep └── stress.do ├── test.do └── tests └── integration.rs /.envrc: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | use flake 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: zombiezen 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - '**' 6 | jobs: 7 | test: 8 | name: Test 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [ubuntu-18.04, macos-latest] 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v2 16 | - name: Set up Rust toolchain 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | profile: minimal 20 | - name: Run tests 21 | run: minimal/do test 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | binaries: 7 | name: Binaries 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-18.04, macos-latest] 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v2 15 | - name: Set up Rust toolchain 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | - name: Build 20 | id: build 21 | run: | 22 | minimal/do release 23 | echo "::set-output name=file::$( ./release-name.sh )" 24 | - name: Upload to GitHub 25 | uses: softprops/action-gh-release@v1 26 | with: 27 | files: ${{ steps.build.outputs.file }} 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /redo[-_]*.zip 2 | 3 | # Rust 4 | /target 5 | **/*.rs.bk 6 | 7 | # Redo directory 8 | .redo/ 9 | 10 | # Integration tests 11 | /redo/py 12 | /redo/sh 13 | /redo/whichpython 14 | *.tmp 15 | 16 | # Minimal do script files 17 | .do_built 18 | .do_built.dir/ 19 | *.did 20 | /minimal/y 21 | 22 | # Nix 23 | result 24 | result-* 25 | -------------------------------------------------------------------------------- /.vscode/copyright.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Copyright": { 3 | "prefix": "copyright", 4 | "body": [ 5 | "$LINE_COMMENT Copyright $CURRENT_YEAR Ross Light", 6 | "$LINE_COMMENT", 7 | "$LINE_COMMENT Licensed under the Apache License, Version 2.0 (the \"License\");", 8 | "$LINE_COMMENT you may not use this file except in compliance with the License.", 9 | "$LINE_COMMENT You may obtain a copy of the License at", 10 | "$LINE_COMMENT", 11 | "$LINE_COMMENT https://www.apache.org/licenses/LICENSE-2.0", 12 | "$LINE_COMMENT", 13 | "$LINE_COMMENT Unless required by applicable law or agreed to in writing, software", 14 | "$LINE_COMMENT distributed under the License is distributed on an \"AS IS\" BASIS,", 15 | "$LINE_COMMENT WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.", 16 | "$LINE_COMMENT See the License for the specific language governing permissions and", 17 | "$LINE_COMMENT limitations under the License.", 18 | "$LINE_COMMENT", 19 | "$LINE_COMMENT SPDX-License-Identifier: Apache-2.0" 20 | ], 21 | "description": "Apache license header" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "matklad.rust-analyzer", 6 | "zombiezen.redo" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "redo", 8 | "target": "debug", 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | }, 13 | "problemMatcher": "$rustc", 14 | "label": "redo: debug" 15 | }, 16 | { 17 | "type": "redo", 18 | "target": "test", 19 | "group": { 20 | "kind": "test", 21 | "isDefault": true 22 | }, 23 | "problemMatcher": "$rustc", 24 | "label": "redo: test" 25 | }, 26 | { 27 | "type": "redo", 28 | "target": "check", 29 | "group": "build", 30 | "problemMatcher": "$rustc", 31 | "label": "redo: check" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # zombiezen/redo Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog][], 6 | and this project adheres to [Semantic Versioning][]. 7 | 8 | [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/ 9 | [Semantic Versioning]: https://semver.org/spec/v2.0.0.html 10 | [Unreleased]: https://github.com/zombiezen/redo-rs/compare/v0.2.1...HEAD 11 | 12 | ## [0.2.1][] - 2021-12-04 13 | 14 | Version 0.2.1 fixes several reliability issues with redo. 15 | 16 | [0.2.1]: https://github.com/zombiezen/redo-rs/releases/tag/v0.2.1 17 | 18 | ### Fixed 19 | 20 | - `redo` and `redo-ifchange` no longer send logs to stderr after they have exited, 21 | nor do they leave zombie `redo-log` processes. 22 | - Fixed various path manipulation issues. 23 | 24 | ## [0.2.0][] - 2021-11-19 25 | 26 | Version 0.2 contains various stability fixes. 27 | Most of the improvements are to the codebase's maintainability, 28 | but there are some user-visible fixes. 29 | 30 | [0.2.0]: https://github.com/zombiezen/redo-rs/releases/tag/v0.2.0 31 | 32 | ### Added 33 | 34 | - `redo --version` displays the program's version. 35 | - Added [Code of Conduct](https://github.com/zombiezen/redo-rs/blob/main/CODE_OF_CONDUCT.md). 36 | 37 | ### Changed 38 | 39 | - Backtraces are no longer shown when `RUST_BACKTRACE=1` is set. 40 | 41 | ### Fixed 42 | 43 | - Reduced likelihood of "database locked" errors 44 | by acquiring write lock at start of most transactions. 45 | - Fix improperly formatted error messages at exit 46 | ([#10](https://github.com/zombiezen/redo-rs/issues/10)). 47 | 48 | ## [0.1.0] - 2021-10-31 49 | 50 | Version 0.1 was the first release. 51 | It passes the entire apenwarr/redo test suite. 52 | 53 | [0.1.0]: https://github.com/zombiezen/redo-rs/releases/tag/v0.1.0 54 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to Ross Light at ross@zombiezen.com. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redo" 3 | version = "0.2.2-alpha1" 4 | authors = ["Ross Light "] 5 | edition = "2018" 6 | repository = "https://github.com/zombiezen/redo-rs" 7 | license = "Apache-2.0" 8 | description = "Port of apenwarr's redo to Rust" 9 | 10 | [features] 11 | debug-jobserver = [] 12 | 13 | [dependencies] 14 | anyhow = "1.0" 15 | clap = { version = "~2.27.0", default-features = false } 16 | common-path = "1.0.0" 17 | futures = "0.3.17" 18 | lazy_static = "1.4.0" 19 | libc = "0.2.67" 20 | libsqlite3-sys = "0.25.1" 21 | nix = "0.25.0" 22 | ouroboros = "0.15.*" 23 | rand = "0.8.4" 24 | rusqlite = { version = "0.28.0", features = ["bundled"] } 25 | sha-1 = "0.10.*" 26 | tempfile = "3.2.0" 27 | term_size = "0.3.2" 28 | zombiezen-const-cstr = "1.0" 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zombiezen/redo 2 | 3 | This is a Rust port of [Avery Pennarun's redo][], which in turn is an 4 | implementation of [Daniel J. Bernstein's redo build system][]. It includes 5 | parallel builds, improved logging, extensive automated tests, and helpful 6 | debugging features. 7 | 8 | [Avery Pennarun's redo]: https://github.com/apenwarr/redo 9 | [Daniel J. Bernstein's redo build system]: http://cr.yp.to/redo.html 10 | 11 | ## Installation 12 | 13 | 1. Download the binary for your platform from the [latest release][]. 14 | 2. Unzip the archive. 15 | 3. Move the contents of the `bin/` directory into a directory in your `PATH`. 16 | 17 | If you use Visual Studio Code, you may be interested in installing the [redo extension][]. 18 | 19 | [latest release]: https://github.com/zombiezen/redo-rs/releases/latest 20 | [redo extension]: https://marketplace.visualstudio.com/items?itemName=zombiezen.redo 21 | 22 | ## Getting Started 23 | 24 | _Borrowed from https://redo.readthedocs.io/en/latest/cookbook/hello/_ 25 | 26 | Create a source file: 27 | 28 | ```shell 29 | cat > hello.c < 31 | 32 | int main() { 33 | printf("Hello, world!\n"); 34 | return 0; 35 | } 36 | EOF 37 | ``` 38 | 39 | Create a .do file to tell redo how to compile it: 40 | 41 | ```shell 42 | cat > hello.do < all.do && 73 | redo && 74 | ./hello 75 | ``` 76 | 77 | ## Further Reading 78 | 79 | The extensive [documentation for Avery Pennarun's redo](https://redo.readthedocs.io/en/latest/) 80 | is relevant for this tool as well, since zombiezen/redo is a straight port. 81 | -------------------------------------------------------------------------------- /all.do: -------------------------------------------------------------------------------- 1 | # shellcheck shell=sh 2 | redo-ifchange debug test 3 | -------------------------------------------------------------------------------- /check.do: -------------------------------------------------------------------------------- 1 | # shellcheck shell=sh 2 | exec >&2 3 | redo-ifchange \ 4 | Cargo.lock \ 5 | Cargo.toml \ 6 | src/*.rs \ 7 | src/bin/redo/*.rs 8 | cargo check 9 | -------------------------------------------------------------------------------- /clean.do: -------------------------------------------------------------------------------- 1 | # shellcheck shell=sh 2 | exec >&2 3 | redo-always 4 | cargo clean 5 | [ -z "$DO_BUILT" ] && rm -rf .do_built .do_built.dir 6 | find . -name 'redo-*.zip' -maxdepth 1 -exec rm -f {} \; 7 | find . -name '*.tmp' -exec rm -f {} \; 8 | find . -name '*.did' -exec rm -f {} \; 9 | -------------------------------------------------------------------------------- /debug.do: -------------------------------------------------------------------------------- 1 | # shellcheck shell=sh 2 | exec >&2 3 | redo-ifchange \ 4 | Cargo.lock \ 5 | Cargo.toml \ 6 | src/*.rs \ 7 | src/bin/redo/*.rs 8 | cargo build 9 | -------------------------------------------------------------------------------- /default.zip.do: -------------------------------------------------------------------------------- 1 | # shellcheck shell=sh 2 | # shellcheck disable=SC2164 3 | # 4 | # Copyright 2021 Ross Light 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | exec >&2 21 | 22 | case "$1" in 23 | redo_*) ;; 24 | *) echo "$0: don't know how to build '$1'" >&2; exit 99 ;; 25 | esac 26 | 27 | redo-ifchange \ 28 | CHANGELOG.md \ 29 | Cargo.lock \ 30 | Cargo.toml \ 31 | LICENSE \ 32 | README.md \ 33 | src/*.rs \ 34 | src/bin/redo/*.rs 35 | cargo build --release 36 | 37 | mytmpdir="$(mktemp -d 2>/dev/null || mktemp -d -t 'redo')" 38 | trap 'rm -rf "$mytmpdir"' EXIT 39 | 40 | root="$mytmpdir/$2" 41 | mkdir "$root" "$root/bin" 42 | cp target/release/redo "$root/bin/redo" 43 | chmod +x "$root/bin/redo" 44 | for name in \ 45 | redo-always \ 46 | redo-ifchange \ 47 | redo-ifcreate \ 48 | redo-log \ 49 | redo-ood \ 50 | redo-sources \ 51 | redo-stamp \ 52 | redo-targets \ 53 | redo-unlocked \ 54 | redo-whichdo; do 55 | ln -s redo "$root/bin/$name" 56 | done 57 | cp CHANGELOG.md LICENSE README.md "$root/" 58 | out="$(pwd)/$3" 59 | cd "$mytmpdir" 60 | zip --recurse-paths --symlinks "$out" "$(basename "$root")" 61 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1676283394, 6 | "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "id": "flake-utils", 14 | "type": "indirect" 15 | } 16 | }, 17 | "nixpkgs": { 18 | "locked": { 19 | "lastModified": 1640269308, 20 | "narHash": "sha256-vBVwv3+kPrxbNyfo48cB5cc5/4tq5zlJGas/qw8XNBE=", 21 | "owner": "NixOS", 22 | "repo": "nixpkgs", 23 | "rev": "0c408a087b4751c887e463e3848512c12017be25", 24 | "type": "github" 25 | }, 26 | "original": { 27 | "id": "nixpkgs", 28 | "type": "indirect" 29 | } 30 | }, 31 | "root": { 32 | "inputs": { 33 | "flake-utils": "flake-utils", 34 | "nixpkgs": "nixpkgs" 35 | } 36 | } 37 | }, 38 | "root": "root", 39 | "version": 7 40 | } 41 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Ross Light 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | 17 | { 18 | description = "Port of apenwarr's redo to Rust"; 19 | 20 | outputs = { self, nixpkgs, flake-utils }: 21 | let 22 | supportedSystems = [ 23 | flake-utils.lib.system.x86_64-linux 24 | flake-utils.lib.system.x86_64-darwin 25 | ]; 26 | in 27 | flake-utils.lib.eachSystem supportedSystems (system: 28 | let 29 | pkgs = import nixpkgs { inherit system; }; 30 | in { 31 | packages.default = pkgs.callPackage ./redo-zombiezen.nix {}; 32 | 33 | packages.rustLibSrc = pkgs.rust.packages.stable.rustPlatform.rustLibSrc; 34 | 35 | apps.default = { 36 | type = "app"; 37 | program = "${self.packages.${system}.default}/bin/redo"; 38 | }; 39 | 40 | devShells.default = pkgs.mkShell { 41 | packages = [ 42 | pkgs.bash 43 | pkgs.python310 44 | pkgs.rust-analyzer 45 | pkgs.rustfmt 46 | pkgs.rust.packages.stable.rustPlatform.rustLibSrc 47 | ]; 48 | inputsFrom = [self.packages.${system}.default]; 49 | }; 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /minimal/default.zz.do: -------------------------------------------------------------------------------- 1 | : 2 | -------------------------------------------------------------------------------- /minimal/do: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # 3 | # A minimal alternative to djb redo that doesn't support incremental builds. 4 | # For the full version, visit http://github.com/apenwarr/redo 5 | # 6 | # The author disclaims copyright to this source file and hereby places it in 7 | # the public domain. (2010 12 14; updated 2019 02 24) 8 | # 9 | USAGE=" 10 | usage: do [-d] [-x] [-v] [-c] 11 | -d print extra debug messages (mostly about dependency checks) 12 | -v run .do files with 'set -v' 13 | -x run .do files with 'set -x' 14 | -c clean up all old targets before starting 15 | 16 | Note: do is an implementation of redo that does *not* check dependencies. 17 | It will never rebuild a target it has already built, unless you use -c. 18 | " 19 | 20 | # CDPATH apparently causes unexpected 'cd' output on some platforms. 21 | unset CDPATH 22 | 23 | # By default, no output coloring. 24 | green="" 25 | bold="" 26 | plain="" 27 | 28 | if [ -n "$TERM" -a "$TERM" != "dumb" ] && tty <&2 >/dev/null 2>&1; then 29 | green="$(printf '\033[32m')" 30 | bold="$(printf '\033[1m')" 31 | plain="$(printf '\033[m')" 32 | fi 33 | 34 | # The 'seq' command is not available on all platforms. 35 | _seq() { 36 | local x=0 max="$1" 37 | while [ "$x" -lt "$max" ]; do 38 | x=$((x + 1)) 39 | echo "$x" 40 | done 41 | } 42 | 43 | # Split $1 into a dir part ($_dirsplit_dir) and base filename ($_dirsplit_base) 44 | _dirsplit() { 45 | _dirsplit_base=${1##*/} 46 | _dirsplit_dir=${1%$_dirsplit_base} 47 | } 48 | 49 | # Like /usr/bin/dirname, but avoids a fork and uses _dirsplit semantics. 50 | qdirname() ( 51 | _dirsplit "$1" 52 | dir=${_dirsplit_dir%/} 53 | echo "${dir:-.}" 54 | ) 55 | 56 | _dirsplit "$0" 57 | REDO=$(cd "$(pwd -P)" && 58 | cd "${_dirsplit_dir:-.}" && 59 | echo "$PWD/$_dirsplit_base") 60 | export REDO 61 | _cmd=$_dirsplit_base 62 | 63 | DO_TOP= 64 | if [ -z "$DO_BUILT" ]; then 65 | export _do_opt_debug= 66 | export _do_opt_exec= 67 | export _do_opt_verbose= 68 | export _do_opt_clean= 69 | fi 70 | while getopts 'dxvcj:h?' _opt; do 71 | case $_opt in 72 | d) _do_opt_debug=1 ;; 73 | x) _do_opt_exec=x ;; 74 | v) _do_opt_verbose=v ;; 75 | c) _do_opt_clean=1 ;; 76 | j) ;; # silently ignore, for compat with real redo 77 | \?|h|*) printf "%s" "$USAGE" >&2 78 | exit 99 79 | ;; 80 | esac 81 | done 82 | shift "$((OPTIND - 1))" 83 | _debug() { 84 | [ -z "$_do_opt_debug" ] || echo "$@" >&2 85 | } 86 | 87 | if [ -z "$DO_BUILT" -a "$_cmd" != "redo-whichdo" ]; then 88 | DO_TOP=1 89 | if [ "$#" -eq 0 ] && [ "$_cmd" = "do" -o "$_cmd" = "redo" ]; then 90 | set all # only toplevel redo has a default target 91 | fi 92 | export DO_STARTDIR="$(pwd -P)" 93 | # If starting /bin/pwd != $PWD, this will fix it. 94 | # That can happen when $PWD contains symlinks that the shell is 95 | # trying helpfully (but unsuccessfully) to hide from the user. 96 | cd "$DO_STARTDIR" || exit 99 97 | export DO_BUILT="$PWD/.do_built" 98 | if [ -z "$_do_opt_clean" -a -e "$DO_BUILT" ]; then 99 | echo "do: Incremental mode. Use -c for clean rebuild." >&2 100 | fi 101 | : >>"$DO_BUILT" 102 | sort -u "$DO_BUILT" >"$DO_BUILT.new" 103 | while read f; do 104 | [ -n "$_do_opt_clean" ] && printf "%s\0%s.did\0" "$f" "$f" 105 | printf "%s.did.tmp\0" "$f" 106 | done <"$DO_BUILT.new" | 107 | xargs -0 rm -f 2>/dev/null 108 | mv "$DO_BUILT.new" "$DO_BUILT" 109 | export DO_PATH="$DO_BUILT.dir" 110 | export PATH="$DO_PATH:$PATH" 111 | rm -rf "$DO_PATH" 112 | mkdir "$DO_PATH" 113 | for d in redo redo-ifchange redo-whichdo; do 114 | ln -s "$REDO" "$DO_PATH/$d" 115 | done 116 | for d in redo-ifcreate redo-stamp redo-always redo-ood \ 117 | redo-targets redo-sources; do 118 | echo "#!/bin/sh" >"$DO_PATH/$d" 119 | chmod a+rx "$DO_PATH/$d" 120 | done 121 | fi 122 | 123 | 124 | # Chop the "file" part off a /path/to/file pathname. 125 | # Note that if the filename already ends in a /, we just remove the slash. 126 | _updir() 127 | { 128 | local v="${1%/*}" 129 | [ "$v" != "$1" ] && echo "$v" 130 | # else "empty" which means we went past the root 131 | } 132 | 133 | 134 | # Returns true if $1 starts with $2. 135 | _startswith() 136 | { 137 | [ "${1#"$2"}" != "$1" ] 138 | } 139 | 140 | 141 | # Returns true if $1 ends with $2. 142 | _endswith() 143 | { 144 | [ "${1%"$2"}" != "$1" ] 145 | } 146 | 147 | 148 | # Prints $1 if it's absolute, or $2/$1 if $1 is not absolute. 149 | _abspath() 150 | { 151 | local here="$2" there="$1" 152 | if _startswith "$1" "/"; then 153 | echo "$1" 154 | else 155 | echo "$2/$1" 156 | fi 157 | } 158 | 159 | 160 | # Prints $1 as a path relative to $PWD (not starting with /). 161 | # If it already doesn't start with a /, doesn't change the string. 162 | _relpath() 163 | { 164 | local here="$2" there="$1" out= hadslash= 165 | #echo "RP start '$there' hs='$hadslash'" >&2 166 | _startswith "$there" "/" || { echo "$there" && return; } 167 | [ "$there" != "/" ] && _endswith "$there" "/" && hadslash=/ 168 | here=${here%/}/ 169 | while [ -n "$here" ]; do 170 | #echo "RP out='$out' here='$here' there='$there'" >&2 171 | [ "${here%/}" = "${there%/}" ] && there= && break; 172 | [ "${there#$here}" != "$there" ] && break 173 | out=../$out 174 | _dirsplit "${here%/}" 175 | here=$_dirsplit_dir 176 | done 177 | there=${there#$here} 178 | if [ -n "$there" ]; then 179 | echo "$out${there%/}$hadslash" 180 | else 181 | echo "${out%/}$hadslash" 182 | fi 183 | } 184 | 185 | 186 | # Prints a "normalized relative" path, with ".." resolved where possible. 187 | # For example, a/b/../c will be reduced to just a/c. 188 | _normpath() 189 | ( 190 | local path="$1" relto="$2" out= isabs= 191 | #echo "NP start '$path'" >&2 192 | if _startswith "$path" "/"; then 193 | isabs=1 194 | else 195 | path="${relto%/}/$path" 196 | fi 197 | set -f 198 | IFS=/ 199 | for d in ${path%/}; do 200 | #echo "NP out='$out' d='$d'" >&2 201 | if [ "$d" = ".." ]; then 202 | out=$(_updir "${out%/}")/ 203 | else 204 | out=$out$d/ 205 | fi 206 | done 207 | #echo "NP out='$out' (done)" >&2 208 | out=${out%/} 209 | if [ -n "$isabs" ]; then 210 | echo "${out:-/}" 211 | else 212 | _relpath "${out:-/}" "$relto" 213 | fi 214 | ) 215 | 216 | 217 | # Prints a "real" path, with all symlinks resolved where possible. 218 | _realpath() 219 | { 220 | local path="$1" relto="$2" isabs= rest= 221 | if _startswith "$path" "/"; then 222 | isabs=1 223 | else 224 | path="${relto%/}/$path" 225 | fi 226 | ( 227 | for d in $(_seq 100); do 228 | #echo "Trying: $PWD--$path" >&2 229 | if cd -P "$path" 2>/dev/null; then 230 | # success 231 | pwd=$(pwd -P) 232 | #echo " chdir ok: $pwd--$rest" >&2 233 | np=$(_normpath "${pwd%/}/$rest" "$relto") 234 | if [ -n "$isabs" ]; then 235 | echo "$np" 236 | else 237 | _relpath "$np" "$relto" 238 | fi 239 | break 240 | fi 241 | _dirsplit "${path%/}" 242 | path=$_dirsplit_dir 243 | rest="$_dirsplit_base/$rest" 244 | done 245 | ) 246 | } 247 | 248 | 249 | # List the possible names for default*.do files in dir $1 matching the target 250 | # pattern in $2. We stop searching when we find the first one that exists. 251 | _find_dofiles_pwd() 252 | { 253 | local dodir="$1" dofile="$2" 254 | _startswith "$dofile" "default." || dofile=${dofile#*.} 255 | while :; do 256 | dofile=default.${dofile#default.*.} 257 | echo "$dodir$dofile" 258 | [ -e "$dodir$dofile" ] && return 0 259 | [ "$dofile" = default.do ] && break 260 | done 261 | return 1 262 | } 263 | 264 | 265 | # List the possible names for default*.do files in $PWD matching the target 266 | # pattern in $1. We stop searching when we find the first name that works. 267 | # If there are no matches in $PWD, we'll search in .., and so on, to the root. 268 | _find_dofiles() 269 | { 270 | local target="$1" dodir= dofile= newdir= 271 | _debug "find_dofile: '$PWD' '$target'" 272 | dofile="$target.do" 273 | echo "$dofile" 274 | [ -e "$dofile" ] && return 0 275 | 276 | # Try default.*.do files, walking up the tree 277 | _dirsplit "$dofile" 278 | dodir=$_dirsplit_dir 279 | dofile=$_dirsplit_base 280 | [ -n "$dodir" ] && dodir=${dodir%/}/ 281 | [ -e "$dodir$dofile" ] && return 0 282 | for i in $(_seq 100); do 283 | [ -n "$dodir" ] && dodir=${dodir%/}/ 284 | #echo "_find_dofiles: '$dodir' '$dofile'" >&2 285 | _find_dofiles_pwd "$dodir" "$dofile" && return 0 286 | newdir=$(_realpath "${dodir}.." "$PWD") 287 | [ "$newdir" = "$dodir" ] && break 288 | dodir=$newdir 289 | done 290 | return 1 291 | } 292 | 293 | 294 | # Print the last .do file returned by _find_dofiles. 295 | # If that file exists, returns 0, else 1. 296 | _find_dofile() 297 | { 298 | local files="$(_find_dofiles "$1")" 299 | rv=$? 300 | #echo "files='$files'" >&2 301 | [ "$rv" -ne 0 ] && return $rv 302 | echo "$files" | { 303 | while read -r linex; do line=$linex; done 304 | printf "%s\n" "$line" 305 | } 306 | } 307 | 308 | 309 | # Actually run the given $dofile with the arguments in $@. 310 | # Note: you should always run this in a subshell. 311 | _run_dofile() 312 | { 313 | export DO_DEPTH="$DO_DEPTH " 314 | export REDO_TARGET="$PWD/$target" 315 | local line1 316 | set -e 317 | read line1 <"$PWD/$dofile" || true 318 | cmd=${line1#"#!/"} 319 | if [ "$cmd" != "$line1" ]; then 320 | set -$_do_opt_verbose$_do_opt_exec 321 | exec /$cmd "$PWD/$dofile" "$@" 322 | else 323 | set -$_do_opt_verbose$_do_opt_exec 324 | # If $dofile is empty, "." might not change $? at 325 | # all, so we clear it first with ":". 326 | :; . "$PWD/$dofile" 327 | fi 328 | } 329 | 330 | 331 | # Find and run the right .do file, starting in dir $1, for target $2, 332 | # providing a temporary output file as $3. Renames the temp file to $2 when 333 | # done. 334 | _do() 335 | { 336 | local dir="$1" target="$1$2" tmp="$1$2.redo.tmp" tdir= 337 | local dopath= dodir= dofile= ext= 338 | if [ "$_cmd" = "redo" ] || 339 | ( [ ! -e "$target" -o -d "$target" ] && 340 | [ ! -e "$target.did" ] ); then 341 | printf '%sdo %s%s%s%s\n' \ 342 | "$green" "$DO_DEPTH" "$bold" "$target" "$plain" >&2 343 | dopath=$(_find_dofile "$target") 344 | if [ ! -e "$dopath" ]; then 345 | echo "do: $target: no .do file ($PWD)" >&2 346 | return 1 347 | fi 348 | _dirsplit "$dopath" 349 | dodir=$_dirsplit_dir dofile=$_dirsplit_base 350 | if _startswith "$dofile" "default."; then 351 | ext=${dofile#default} 352 | ext=${ext%.do} 353 | else 354 | ext= 355 | fi 356 | target=$PWD/$target 357 | tmp=$PWD/$tmp 358 | cd "$dodir" || return 99 359 | target=$(_relpath "$target" "$PWD") || return 98 360 | tmp=$(_relpath "$tmp" "$PWD") || return 97 361 | base=${target%$ext} 362 | tdir=$(qdirname "$target") 363 | [ ! -e "$DO_BUILT" ] || [ ! -w "$tdir/." ] || 364 | : >>"$target.did.tmp" 365 | # $qtmp is a temporary file used to capture stdout. 366 | # Since it might be accidentally deleted as a .do file 367 | # does its work, we create it, then open two fds to it, 368 | # then immediately delete the name. We use one fd to 369 | # redirect to stdout, and the other to read from after, 370 | # because there's no way to fseek(fd, 0) in sh. 371 | qtmp=$DO_PATH/do.$$.tmp 372 | ( 373 | rm -f "$qtmp" 374 | ( _run_dofile "$target" "$base" "$tmp" >&3 3>&- 4<&- ) 375 | rv=$? 376 | if [ $rv != 0 ]; then 377 | printf "do: %s%s\n" "$DO_DEPTH" \ 378 | "$target: got exit code $rv" >&2 379 | rm -f "$tmp.tmp" "$tmp.tmp2" "$target.did" 380 | return $rv 381 | fi 382 | echo "$PWD/$target" >>"$DO_BUILT" 383 | if [ ! -e "$tmp" ]; then 384 | # if $3 wasn't created, copy from stdout file 385 | cat <&4 >$tmp 386 | # if that's zero length too, forget it 387 | [ -s "$tmp" ] || rm -f "$tmp" 388 | fi 389 | ) 3>$qtmp 4<$qtmp # can't use "|| return" here... 390 | # ...because "|| return" would mess up "set -e" inside the () 391 | # on some shells. Running commands in "||" context, even 392 | # deep inside, will stop "set -e" from functioning. 393 | rv=$? 394 | [ "$rv" = 0 ] || return "$rv" 395 | mv "$tmp" "$target" 2>/dev/null 396 | [ -e "$target.did.tmp" ] && 397 | mv "$target.did.tmp" "$target.did" || 398 | : >>"$target.did" 399 | else 400 | _debug "do $DO_DEPTH$target exists." >&2 401 | fi 402 | } 403 | 404 | 405 | # Implementation of the "redo" command. 406 | _redo() 407 | { 408 | local i startdir="$PWD" dir base 409 | set +e 410 | for i in "$@"; do 411 | i=$(_abspath "$i" "$startdir") 412 | ( 413 | cd "$DO_STARTDIR" || return 99 414 | i=$(_realpath "$(_relpath "$i" "$PWD")" "$PWD") 415 | _dirsplit "$i" 416 | dir=$_dirsplit_dir base=$_dirsplit_base 417 | _do "$dir" "$base" 418 | ) 419 | [ "$?" = 0 ] || return 1 420 | done 421 | } 422 | 423 | 424 | # Implementation of the "redo-whichdo" command. 425 | _whichdo() 426 | { 427 | _find_dofiles "$1" 428 | } 429 | 430 | 431 | case $_cmd in 432 | do|redo|redo-ifchange) _redo "$@" ;; 433 | redo-whichdo) _whichdo "$1" ;; 434 | do.test) ;; 435 | *) printf "do: '%s': unexpected redo command" "$_cmd" >&2; exit 99 ;; 436 | esac 437 | [ "$?" = 0 ] || exit 1 438 | 439 | if [ -n "$DO_TOP" ]; then 440 | if [ -n "$_do_opt_clean" ]; then 441 | echo "do: Removing stamp files..." >&2 442 | [ ! -e "$DO_BUILT" ] || 443 | while read f; do printf "%s.did\0" "$f"; done <"$DO_BUILT" | 444 | xargs -0 rm -f 2>/dev/null 445 | fi 446 | fi 447 | -------------------------------------------------------------------------------- /minimal/fakedir/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zombiezen/redo-rs/177c67efe28f0008dd0a301ef907f1e80f58d913/minimal/fakedir/.empty -------------------------------------------------------------------------------- /minimal/test.do: -------------------------------------------------------------------------------- 1 | exec >&2 2 | ./do.test 3 | -------------------------------------------------------------------------------- /redo-zombiezen.nix: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Ross Light 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # SPDX-License-Identifier: Apache-2.0 16 | 17 | { lib, nix-gitignore, rustPlatform }: 18 | 19 | rustPlatform.buildRustPackage rec { 20 | pname = "redo-zombiezen"; 21 | version = "0.2.2-alpha1"; 22 | 23 | src = let 24 | root = ./.; 25 | patterns = nix-gitignore.withGitignoreFile extraIgnores root; 26 | extraIgnores = ["/t/" "/redo/" "*.nix"]; 27 | in builtins.path { 28 | name = "redo-zombiezen"; 29 | path = root; 30 | filter = nix-gitignore.gitignoreFilterPure (_: _: true) patterns root; 31 | }; 32 | cargoLock = { 33 | lockFile = ./Cargo.lock; 34 | }; 35 | cargoTestFlags = ["--lib" "--bins" "--examples"]; 36 | 37 | postInstall = '' 38 | for name in \ 39 | redo-always \ 40 | redo-ifchange \ 41 | redo-ifcreate \ 42 | redo-log \ 43 | redo-ood \ 44 | redo-sources \ 45 | redo-stamp \ 46 | redo-targets \ 47 | redo-unlocked \ 48 | redo-whichdo; do 49 | ln -s redo "$out/bin/$name" 50 | done 51 | ''; 52 | 53 | meta = with lib; { 54 | description = "Port of apenwarr's redo to Rust"; 55 | homepage = "https://github.com/zombiezen/redo-rs"; 56 | license = licenses.asl20; 57 | mainProgram = "redo"; 58 | platforms = platforms.linux ++ platforms.darwin; 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /redo/py.do: -------------------------------------------------------------------------------- 1 | redo-ifchange whichpython 2 | read py $3 <<-EOF 4 | #!/bin/sh 5 | exec $py "\$@" 6 | EOF 7 | chmod a+x $3 8 | -------------------------------------------------------------------------------- /redo/sh.do: -------------------------------------------------------------------------------- 1 | exec >&2 2 | redo-ifchange ../t/shelltest.od 3 | 4 | rm -rf $1.new 5 | mkdir $1.new 6 | 7 | GOOD= 8 | WARN= 9 | 10 | # Note: list low-functionality, maximally POSIX-like shells before more 11 | # powerful ones. We want weaker shells to take precedence, as long as they 12 | # pass the tests, because weaker shells are more likely to point out when you 13 | # use some non-portable feature. 14 | for sh in dash /usr/xpg4/bin/sh ash posh \ 15 | lksh mksh ksh ksh88 ksh93 pdksh \ 16 | zsh bash busybox /bin/sh; do 17 | printf " %-22s" "$sh..." 18 | FOUND=`which $sh 2>/dev/null` || { echo "missing"; continue; } 19 | 20 | # It's important for the file to actually be named 'sh'. Some 21 | # shells (like bash and zsh) only go into POSIX-compatible mode if 22 | # they have that name. If they're not in POSIX-compatible mode, 23 | # they'll fail the test. 24 | rm -f $1.new/sh 25 | ln -s $FOUND $1.new/sh 26 | SH=$PWD/$1.new/sh 27 | 28 | set +e 29 | ( cd ../t && "$SH" shelltest.od ) >shelltest.tmp 2>&1 30 | RV=$? 31 | set -e 32 | 33 | msgs= 34 | crash= 35 | while read line; do 36 | #echo "line: '$line'" >&2 37 | stripw=${line#warning: } 38 | stripf=${line#failed: } 39 | strips=${line#skip: } 40 | crash=$line 41 | [ "$line" = "$stripw" ] || msgs="$msgs W$stripw" 42 | [ "$line" = "$stripf" ] || msgs="$msgs F$stripf" 43 | [ "$line" = "$strips" ] || msgs="$msgs s$strips" 44 | done &2 2 | for py in intentionally-missing python python3 python2 python2.7; do 3 | echo "Trying: $py" 4 | cmd=$(command -v "$py" || true) 5 | out=$($cmd -c 'print("success")' 2>/dev/null) || true 6 | if [ "$out" = "success" ]; then 7 | echo $cmd >$3 8 | exit 0 9 | fi 10 | done 11 | exit 10 12 | -------------------------------------------------------------------------------- /release-name.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2021 Ross Light 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | set -euo pipefail 20 | 21 | os="$(uname -s | tr '[:upper:]' '[:lower:]')" 22 | arch="$(uname -m | tr '_' '-')" 23 | version="$(cargo metadata --format-version=1 | \ 24 | jq -r '.packages[] | select(.name == "redo") | .version')" 25 | echo "redo_v${version}_${os}_${arch}.zip" 26 | -------------------------------------------------------------------------------- /release.do: -------------------------------------------------------------------------------- 1 | # shellcheck shell=sh 2 | # 3 | # Copyright 2021 Ross Light 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | exec >&2 20 | redo-ifchange release-name.sh 21 | redo-ifchange "$( ./release-name.sh )" 22 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | stable 2 | -------------------------------------------------------------------------------- /src/bin/redo/always.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // Copyright 2010-2018 Avery Pennarun and contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // SPDX-License-Identifier: Apache-2.0 17 | 18 | use anyhow::Error; 19 | use rusqlite::TransactionBehavior; 20 | use std::io; 21 | use std::path::PathBuf; 22 | 23 | use redo::logs::LogBuilder; 24 | use redo::{self, DepMode, Env, ProcessState, ProcessTransaction, Stamp}; 25 | 26 | pub(crate) fn run() -> Result<(), Error> { 27 | let env = Env::inherit()?; 28 | LogBuilder::from(&env).setup(io::stderr()); 29 | 30 | let mut me = PathBuf::new(); 31 | me.push(env.startdir()); 32 | me.push(env.pwd()); 33 | me.push(env.target()); 34 | let mut ps = ProcessState::init(env)?; 35 | let mut ptx = ProcessTransaction::new(&mut ps, TransactionBehavior::Immediate)?; 36 | let mut f = redo::File::from_name(&mut ptx, &me, true)?; 37 | f.add_dep(&mut ptx, DepMode::Modified, redo::always_filename())?; 38 | let mut always = redo::File::from_name(&mut ptx, redo::always_filename(), true)?; 39 | always.set_stamp(Stamp::MISSING); 40 | always.set_changed(ptx.state().env()); 41 | always.save(&mut ptx)?; 42 | ptx.commit()?; 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /src/bin/redo/ifchange.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // Copyright 2010-2018 Avery Pennarun and contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // SPDX-License-Identifier: Apache-2.0 17 | 18 | use anyhow::Error; 19 | use rusqlite::TransactionBehavior; 20 | use std::io; 21 | use std::path::PathBuf; 22 | 23 | use redo::builder::{self, StdinLogReader, StdinLogReaderBuilder}; 24 | use redo::logs::LogBuilder; 25 | use redo::{ 26 | self, log_debug2, log_err, DepMode, Dirtiness, Env, JobServer, ProcessState, 27 | ProcessTransaction, RedoError, RedoPath, RedoPathBuf, EXIT_TARGET_FAILED, 28 | }; 29 | 30 | pub(crate) fn run() -> (Result<(), Error>, Option) { 31 | use std::convert::TryFrom; 32 | 33 | let mut targets = { 34 | let mut targets = Vec::::new(); 35 | for arg in std::env::args_os().skip(1) { 36 | targets.push(match RedoPathBuf::try_from(arg) { 37 | Ok(p) => p, 38 | Err(e) => return (Err(e.into()), None), 39 | }); 40 | } 41 | targets 42 | }; 43 | let env = match Env::init(targets.as_slice()) { 44 | Ok(env) => env, 45 | Err(e) => return (Err(e.into()), None), 46 | }; 47 | let mut ps = match ProcessState::init(env) { 48 | Ok(ps) => ps, 49 | Err(e) => return (Err(e.into()), None), 50 | }; 51 | if ps.is_toplevel() && targets.is_empty() { 52 | targets.push(unsafe { RedoPathBuf::from_string_unchecked("all".into()) }); 53 | } 54 | let mut stdin_log_reader: Option = None; 55 | if ps.is_toplevel() && ps.env().log().unwrap_or(true) { 56 | if let Err(e) = builder::close_stdin() { 57 | return (Err(e.into()), None); 58 | } 59 | stdin_log_reader = Some(match StdinLogReaderBuilder::default().start(ps.env()) { 60 | Ok(r) => r, 61 | Err(e) => return (Err(e.into()), None), 62 | }); 63 | } else { 64 | LogBuilder::from(ps.env()).setup(io::stderr()); 65 | } 66 | 67 | let result = || -> Result<(), Error> { 68 | let mut server; 69 | { 70 | let mut ptx = ProcessTransaction::new(&mut ps, TransactionBehavior::Immediate)?; 71 | let f = if !ptx.state().env().target().as_os_str().is_empty() 72 | && !ptx.state().env().is_unlocked() 73 | { 74 | let mut me = PathBuf::new(); 75 | me.push(ptx.state().env().startdir()); 76 | me.push(ptx.state().env().pwd()); 77 | me.push(ptx.state().env().target()); 78 | let f = redo::File::from_name(&mut ptx, &me, true)?; 79 | log_debug2!( 80 | "TARGET: {:?} {:?} {:?}\n", 81 | ptx.state().env().startdir(), 82 | ptx.state().env().pwd(), 83 | ptx.state().env().target() 84 | ); 85 | Some(f) 86 | } else { 87 | log_debug2!("redo-ifchange: not adding depends.\n"); 88 | None 89 | }; 90 | server = JobServer::setup(0)?; 91 | if let Some(mut f) = f { 92 | for t in targets.iter() { 93 | f.add_dep(&mut ptx, DepMode::Modified, t)?; 94 | } 95 | f.save(&mut ptx)?; 96 | ptx.commit()?; 97 | } 98 | } 99 | 100 | let build_result = server.block_on(builder::run( 101 | &mut ps, 102 | &server.handle(), 103 | &targets, 104 | should_build, 105 | )); 106 | // TODO(someday): In the original, there's a state.rollback call. 107 | // Unclear what this is trying to do. 108 | assert!(ps.is_flushed()); 109 | let return_tokens_result = server.force_return_tokens(); 110 | if let Err(e) = &return_tokens_result { 111 | log_err!("unexpected error: {}", e); 112 | } 113 | build_result 114 | .map_err(|e| e.into()) 115 | .and(return_tokens_result.map_err(Into::into)) 116 | }(); 117 | (result, stdin_log_reader) 118 | } 119 | 120 | fn should_build( 121 | ptx: &mut ProcessTransaction, 122 | t: &RedoPath, 123 | ) -> Result<(bool, Dirtiness), RedoError> { 124 | let mut f = redo::File::from_name(ptx, t, true)?; 125 | if f.is_failed(ptx.state().env()) { 126 | return Err(RedoError::immediate_exit( 127 | EXIT_TARGET_FAILED, 128 | format!("target {} failed", t), 129 | )); 130 | } 131 | let dirty = match redo::is_dirty(ptx, &mut f, &mut Default::default())? { 132 | Dirtiness::NeedTargets(t) if t.len() == 1 && t[0].id() == f.id() => Dirtiness::Dirty, 133 | d => d, 134 | }; 135 | Ok((f.is_generated(), dirty)) 136 | } 137 | -------------------------------------------------------------------------------- /src/bin/redo/ifcreate.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // Copyright 2010-2018 Avery Pennarun and contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // SPDX-License-Identifier: Apache-2.0 17 | 18 | use anyhow::{anyhow, Error}; 19 | use rusqlite::TransactionBehavior; 20 | use std::io; 21 | use std::path::{Path, PathBuf}; 22 | use std::process; 23 | 24 | use redo::logs::LogBuilder; 25 | use redo::{self, log_err, DepMode, Env, ProcessState, ProcessTransaction, EXIT_INVALID_TARGET}; 26 | 27 | /// Build the current target if these targets are created. 28 | pub(crate) fn run() -> Result<(), Error> { 29 | let env = Env::inherit()?; 30 | LogBuilder::from(&env).setup(io::stderr()); 31 | 32 | let mut me = PathBuf::new(); 33 | me.push(env.startdir()); 34 | me.push(env.pwd()); 35 | me.push(env.target()); 36 | let mut ps = ProcessState::init(env)?; 37 | let mut ptx = ProcessTransaction::new(&mut ps, TransactionBehavior::Immediate)?; 38 | let mut f = redo::File::from_name(&mut ptx, &me, true)?; 39 | for t in std::env::args_os().skip(1) { 40 | if t.is_empty() { 41 | log_err!("cannot build the empty target (\"\").\n"); 42 | process::exit(EXIT_INVALID_TARGET); 43 | } 44 | if Path::new(&t).exists() { 45 | return Err(anyhow!("{:?} already exists", t)); 46 | } 47 | f.add_dep( 48 | &mut ptx, 49 | DepMode::Created, 50 | t.to_str() 51 | .ok_or_else(|| anyhow!("cannot use {:?} as target name", &t))?, 52 | )?; 53 | } 54 | ptx.commit()?; 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /src/bin/redo/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // Copyright 2010-2018 Avery Pennarun and contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // SPDX-License-Identifier: Apache-2.0 17 | 18 | mod always; 19 | mod ifchange; 20 | mod ifcreate; 21 | mod log; 22 | mod ood; 23 | mod sources; 24 | mod stamp; 25 | mod targets; 26 | mod unlocked; 27 | mod whichdo; 28 | 29 | use anyhow::{anyhow, Error}; 30 | use clap::{crate_version, App, AppSettings, Arg}; 31 | use rusqlite::TransactionBehavior; 32 | use std::convert::Infallible; 33 | use std::env; 34 | use std::ffi::OsString; 35 | use std::io; 36 | use std::path::{Path, PathBuf}; 37 | 38 | use redo::builder::{self, StdinLogReader, StdinLogReaderBuilder}; 39 | use redo::logs::LogBuilder; 40 | use redo::{ 41 | self, log_err, log_warn, Dirtiness, Env, JobServer, OptionalBool, ProcessState, 42 | ProcessTransaction, RedoErrorKind, RedoPath, ENV_COLOR, ENV_DEBUG, ENV_DEBUG_LOCKS, 43 | ENV_DEBUG_PIDS, ENV_KEEP_GOING, ENV_LOG, ENV_PRETTY, ENV_SHUFFLE, ENV_VERBOSE, ENV_XTRACE, 44 | EXIT_SUCCESS, 45 | }; 46 | 47 | fn main() { 48 | let exit_code = { 49 | let name = env::args_os() 50 | .nth(0) 51 | .and_then(|a0| { 52 | PathBuf::from(a0) 53 | .file_name() 54 | .map(|base| base.to_os_string()) 55 | }) 56 | .unwrap_or("redo".into()); 57 | let mut _stdin_log_reader: Option = None; // dropped right before exiting 58 | let result = match name.to_str() { 59 | Some("redo-always") => always::run(), 60 | Some("redo-ifchange") => { 61 | let result = ifchange::run(); 62 | _stdin_log_reader = result.1; 63 | result.0 64 | } 65 | Some("redo-ifcreate") => ifcreate::run(), 66 | Some("redo-log") => log::run(), 67 | Some("redo-ood") => ood::run(), 68 | Some("redo-sources") => sources::run(), 69 | Some("redo-stamp") => stamp::run(), 70 | Some("redo-targets") => targets::run(), 71 | Some("redo-unlocked") => unlocked::run(), 72 | Some("redo-whichdo") => whichdo::run(), 73 | _ => { 74 | let result = run_redo(); 75 | _stdin_log_reader = result.1; 76 | result.0 77 | } 78 | }; 79 | match result { 80 | Ok(_) => EXIT_SUCCESS, 81 | Err(e) => { 82 | let msg = { 83 | use std::fmt::Write; 84 | 85 | let mut s = String::new(); 86 | for e in e.chain() { 87 | if !s.is_empty() { 88 | write!(s, ": ").unwrap(); 89 | } 90 | write!(s, "{}", e).unwrap(); 91 | } 92 | s 93 | }; 94 | log_err!("{}", msg); 95 | RedoErrorKind::of(&e).exit_code() 96 | } 97 | } 98 | }; 99 | std::process::exit(exit_code); 100 | } 101 | 102 | /// Return the common list of `redo-log` flags. 103 | pub(crate) fn log_flags() -> Vec> { 104 | vec![ 105 | Arg::from_usage("--no-details").help("only show 'redo' recursion trace, not build output"), 106 | Arg::from_usage("--details") 107 | .hidden(true) 108 | .overrides_with("no-details"), 109 | Arg::from_usage("--no-status") 110 | .help("don't display build summary line at the bottom of the screen"), 111 | Arg::from_usage("--status") 112 | .hidden(true) 113 | .overrides_with("no-status"), 114 | Arg::from_usage("--no-pretty") 115 | .help("don't pretty-print logs, show raw @@REDO output instead"), 116 | Arg::from_usage("--pretty") 117 | .hidden(true) 118 | .overrides_with("no-pretty"), 119 | Arg::from_usage("--no-color") 120 | .help("disable ANSI color; --color to force enable (default: auto)"), 121 | Arg::from_usage("--color") 122 | .hidden(true) 123 | .overrides_with("no-color"), 124 | Arg::from_usage("--debug-locks 'print messages about file locking (useful for debugging)'"), 125 | Arg::from_usage("--no-debug-locks") 126 | .hidden(true) 127 | .overrides_with("debug-locks"), 128 | Arg::from_usage( 129 | "--debug-pids 'print process ids as part of log messages (useful for debugging)'", 130 | ), 131 | Arg::from_usage("--no-debug-pids") 132 | .hidden(true) 133 | .overrides_with("debug-pids"), 134 | ] 135 | } 136 | 137 | fn run_redo() -> (Result<(), Error>, Option) { 138 | let matches = App::new("redo") 139 | .about("Build the listed targets whether they need it or not.") 140 | .version(crate_version!()) 141 | .setting(AppSettings::DeriveDisplayOrder) 142 | .setting(AppSettings::UnifiedHelpMessage) 143 | .arg(Arg::from_usage( 144 | "-j, --jobs [N] 'maximum number of jobs to build at once'", 145 | )) 146 | .arg(Arg::from_usage( 147 | "-d, --debug... 'print dependency checks as they happen'", 148 | )) 149 | .arg(Arg::from_usage( 150 | "-v, --verbose... 'print commands as they are read from .do files (variables intact)'", 151 | )) 152 | .arg(Arg::from_usage( 153 | "-x, --xtrace... 'print commands as they are executed (variables expanded)'", 154 | )) 155 | .arg(Arg::from_usage( 156 | "-k, --keep-going 'keep going as long as possible even if some targets fail'", 157 | )) 158 | .arg(Arg::from_usage( 159 | "--shuffle 'randomize the build order to find dependency bugs'", 160 | )) 161 | .arg( 162 | Arg::from_usage("--no-log") 163 | .help("don't capture error output, just let it flow straight to stderr"), 164 | ) 165 | .arg( 166 | Arg::from_usage("--log") 167 | .hidden(true) 168 | .overrides_with("no-log"), 169 | ) 170 | .args(&log_flags()) 171 | .arg(Arg::from_usage("[target]...")) 172 | .get_matches(); 173 | { 174 | let n = matches.occurrences_of("debug"); 175 | if n > 0 { 176 | std::env::set_var(ENV_DEBUG, n.to_string()); 177 | } 178 | } 179 | { 180 | let n = matches.occurrences_of("verbose"); 181 | if n > 0 { 182 | std::env::set_var(ENV_VERBOSE, n.to_string()); 183 | } 184 | } 185 | { 186 | let n = matches.occurrences_of("xtrace"); 187 | if n > 0 { 188 | std::env::set_var(ENV_XTRACE, n.to_string()); 189 | } 190 | } 191 | if auto_bool_arg(&matches, "keep-going").unwrap_or(false) { 192 | std::env::set_var(ENV_KEEP_GOING, "1"); 193 | } 194 | if auto_bool_arg(&matches, "shuffle").unwrap_or(false) { 195 | std::env::set_var(ENV_SHUFFLE, "1"); 196 | } 197 | if auto_bool_arg(&matches, "debug-locks").unwrap_or(false) { 198 | std::env::set_var(ENV_DEBUG_LOCKS, "1"); 199 | } 200 | if auto_bool_arg(&matches, "debug-pids").unwrap_or(false) { 201 | std::env::set_var(ENV_DEBUG_PIDS, "1"); 202 | } 203 | fn set_defint(name: &str, val: OptionalBool) { 204 | std::env::set_var( 205 | name, 206 | std::env::var_os(name).unwrap_or_else(|| { 207 | OsString::from(match val { 208 | OptionalBool::Off => "0", 209 | OptionalBool::Auto => "1", 210 | OptionalBool::On => "2", 211 | }) 212 | }), 213 | ); 214 | } 215 | set_defint(ENV_LOG, auto_bool_arg(&matches, "log")); 216 | set_defint(ENV_PRETTY, auto_bool_arg(&matches, "pretty")); 217 | set_defint(ENV_COLOR, auto_bool_arg(&matches, "color")); 218 | let mut targets = { 219 | let mut targets = Vec::<&RedoPath>::new(); 220 | for arg in matches.values_of("target").unwrap_or_default() { 221 | targets.push(match RedoPath::from_str(arg) { 222 | Ok(p) => p, 223 | Err(e) => return (Err(e.into()), None), 224 | }); 225 | } 226 | targets 227 | }; 228 | 229 | let env = match Env::init(targets.as_slice()) { 230 | Ok(env) => env, 231 | Err(e) => return (Err(e.into()), None), 232 | }; 233 | let mut ps = match ProcessState::init(env) { 234 | Ok(ps) => ps, 235 | Err(e) => return (Err(e.into()), None), 236 | }; 237 | if ps.is_toplevel() && targets.is_empty() { 238 | targets.push(unsafe { RedoPath::from_str_unchecked("all") }); 239 | } 240 | let mut j = str::parse::(matches.value_of("jobs").unwrap_or("0")).unwrap_or(0); 241 | if ps.is_toplevel() && (ps.env().log().unwrap_or(true) || j > 1) { 242 | if let Err(e) = builder::close_stdin() { 243 | return (Err(e.into()), None); 244 | } 245 | } 246 | let mut stdin_log_reader: Option = None; 247 | if ps.is_toplevel() && ps.env().log().unwrap_or(true) { 248 | stdin_log_reader = Some( 249 | match StdinLogReaderBuilder::from(ps.env()) 250 | .set_status(auto_bool_arg(&matches, "status").unwrap_or(true)) 251 | .set_details(auto_bool_arg(&matches, "details").unwrap_or(true)) 252 | .set_debug_locks(auto_bool_arg(&matches, "debug-locks").unwrap_or(false)) 253 | .set_debug_pids(auto_bool_arg(&matches, "debug-pids").unwrap_or(false)) 254 | .start(ps.env()) 255 | { 256 | Ok(r) => r, 257 | Err(e) => return (Err(e.into()), None), 258 | }, 259 | ); 260 | } else { 261 | LogBuilder::from(ps.env()).setup(io::stderr()); 262 | } 263 | if (ps.is_toplevel() || j > 1) && ps.env().locks_broken() { 264 | log_warn!("detected broken fcntl locks; parallelism disabled.\n"); 265 | log_warn!(" ...details: https://github.com/Microsoft/WSL/issues/1927\n"); 266 | if j > 1 { 267 | j = 1; 268 | } 269 | } 270 | 271 | let result = || -> Result<(), Error> { 272 | { 273 | let mut ptx = ProcessTransaction::new(&mut ps, TransactionBehavior::Immediate)?; 274 | for t in &targets { 275 | if Path::new(t).exists() { 276 | let f = redo::File::from_name(&mut ptx, t, true)?; 277 | if !f.is_generated() { 278 | log_warn!( 279 | "{}: exists and not marked as generated; not redoing.\n", 280 | f.nice_name(ptx.state().env())? 281 | ); 282 | } 283 | } 284 | } 285 | } 286 | 287 | if j < 0 || j > 1000 { 288 | return Err(anyhow!("invalid --jobs value: {}", j)); 289 | } 290 | let mut server = JobServer::setup(j)?; 291 | assert!(ps.is_flushed()); 292 | let build_result = server.block_on(builder::run( 293 | &mut ps, 294 | &server.handle(), 295 | &targets, 296 | |_, _| -> Result<(bool, Dirtiness), Infallible> { Ok((true, Dirtiness::Dirty)) }, 297 | )); 298 | assert!(ps.is_flushed()); 299 | let return_tokens_result = server.force_return_tokens(); 300 | if let Err(e) = &return_tokens_result { 301 | log_err!("unexpected error: {}", e); 302 | } 303 | build_result 304 | .map_err(|e| e.into()) 305 | .and(return_tokens_result.map_err(Into::into)) 306 | }(); 307 | (result, stdin_log_reader) 308 | } 309 | 310 | /// Converts an argument pair match of `name` and `"no-" + name` into a tri-state. 311 | pub(crate) fn auto_bool_arg>(matches: &clap::ArgMatches, name: S) -> OptionalBool { 312 | let name = name.as_ref(); 313 | const NEGATIVE_PREFIX: &str = "no-"; 314 | let negative_name = { 315 | let mut negative_name = String::with_capacity(name.len() + NEGATIVE_PREFIX.len()); 316 | negative_name.push_str(NEGATIVE_PREFIX); 317 | negative_name.push_str(name); 318 | negative_name 319 | }; 320 | if matches.is_present(name) { 321 | OptionalBool::On 322 | } else if matches.is_present(negative_name) { 323 | OptionalBool::Off 324 | } else { 325 | OptionalBool::Auto 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/bin/redo/ood.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // Copyright 2010-2018 Avery Pennarun and contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // SPDX-License-Identifier: Apache-2.0 17 | 18 | //! List out-of-date targets (ood) targets. 19 | 20 | use anyhow::{anyhow, Error}; 21 | use rusqlite::TransactionBehavior; 22 | use std::cell::RefCell; 23 | use std::collections::HashSet; 24 | use std::env; 25 | use std::io; 26 | 27 | use redo::logs::LogBuilder; 28 | use redo::{ 29 | self, DirtyCallbacksBuilder, Env, File, Files, ProcessState, ProcessTransaction, RedoPath, 30 | }; 31 | 32 | pub(crate) fn run() -> Result<(), Error> { 33 | if env::args_os().len() != 1 { 34 | return Err(anyhow!("no arguments expected.")); 35 | } 36 | 37 | let targets: &[&RedoPath] = &[]; 38 | let env = Env::init(targets)?; 39 | LogBuilder::from(&env).setup(io::stderr()); 40 | 41 | let mut ps = ProcessState::init(env)?; 42 | let env2 = ps.env().clone(); 43 | let mut ptx = ProcessTransaction::new(&mut ps, TransactionBehavior::Deferred)?; 44 | let cache: RefCell> = RefCell::new(HashSet::new()); 45 | let mut cb = DirtyCallbacksBuilder::new() 46 | .is_checked(|f, _| cache.borrow().contains(&f.id())) 47 | .set_checked(|f, _| { 48 | cache.borrow_mut().insert(f.id()); 49 | Ok(()) 50 | }) 51 | .log_override(|_| {}) 52 | .build(); 53 | let mut targets: Vec = Vec::new(); 54 | for resf in Files::list(&mut ptx) { 55 | let f = resf?; 56 | if f.is_target(&env2)? { 57 | targets.push(f); 58 | } 59 | } 60 | let cwd = env::current_dir()?; 61 | for mut f in targets { 62 | if !redo::is_dirty(&mut ptx, &mut f, &mut cb)?.is_clean() { 63 | let p = redo::relpath(env2.base().join(f.name()), &cwd)?; 64 | println!( 65 | "{}", 66 | p.as_os_str() 67 | .to_str() 68 | .ok_or(anyhow!("could not get filename as UTF-8"))? 69 | ); 70 | } 71 | } 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /src/bin/redo/sources.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // Copyright 2010-2018 Avery Pennarun and contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // SPDX-License-Identifier: Apache-2.0 17 | 18 | //! List the known source (not target) files. 19 | 20 | use anyhow::{anyhow, Error}; 21 | use rusqlite::TransactionBehavior; 22 | use std::env; 23 | use std::io; 24 | 25 | use redo::logs::LogBuilder; 26 | use redo::{self, Env, Files, ProcessState, ProcessTransaction, RedoPath}; 27 | 28 | pub(crate) fn run() -> Result<(), Error> { 29 | if env::args_os().len() != 1 { 30 | return Err(anyhow!("no arguments expected.")); 31 | } 32 | 33 | let targets: &[&RedoPath] = &[]; 34 | let env = Env::init(targets)?; 35 | LogBuilder::from(&env).setup(io::stderr()); 36 | 37 | let cwd = env::current_dir()?; 38 | let mut ps = ProcessState::init(env)?; 39 | let env2 = ps.env().clone(); 40 | let mut ptx = ProcessTransaction::new(&mut ps, TransactionBehavior::Deferred)?; 41 | for resf in Files::list(&mut ptx) { 42 | let f = resf?; 43 | if f.is_source(&env2)? { 44 | let p = redo::relpath(env2.base().join(f.name()), &cwd)?; 45 | println!( 46 | "{}", 47 | p.as_os_str() 48 | .to_str() 49 | .ok_or(anyhow!("could not get filename as UTF-8"))? 50 | ); 51 | } 52 | } 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /src/bin/redo/stamp.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // Copyright 2010-2018 Avery Pennarun and contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // SPDX-License-Identifier: Apache-2.0 17 | 18 | use anyhow::{anyhow, Error}; 19 | use nix::unistd; 20 | use rusqlite::TransactionBehavior; 21 | use sha1::Sha1; 22 | use std::env; 23 | use std::io; 24 | use std::path::PathBuf; 25 | 26 | use redo::logs::LogBuilder; 27 | use redo::{self, log_debug2, Env, File, ProcessState, ProcessTransaction}; 28 | 29 | pub(crate) fn run() -> Result<(), Error> { 30 | use sha1::Digest; 31 | 32 | if env::args_os().len() != 1 { 33 | return Err(anyhow!("no arguments expected.")); 34 | } 35 | if unistd::isatty(0).unwrap_or(false) { 36 | return Err(anyhow!("you must provide the data to stamp on stdin")); 37 | } 38 | let env = Env::inherit()?; 39 | LogBuilder::from(&env).setup(io::stderr()); 40 | 41 | let mut sh = Sha1::new(); 42 | io::copy(&mut io::stdin(), &mut sh)?; 43 | let csum = format!("{:x}", sh.finalize()); 44 | 45 | if env.target().as_os_str().is_empty() { 46 | return Ok(()); 47 | } 48 | 49 | let mut me = PathBuf::new(); 50 | me.push(env.startdir()); 51 | me.push(env.pwd()); 52 | me.push(env.target()); 53 | let mut ps = ProcessState::init(env)?; 54 | let mut ptx = ProcessTransaction::new(&mut ps, TransactionBehavior::Immediate)?; 55 | let mut f = File::from_name(&mut ptx, &me, true)?; 56 | let changed = csum != f.checksum(); 57 | log_debug2!("{}: old = {}", f.name(), f.checksum()); 58 | log_debug2!( 59 | "{}: sum = {} ({})", 60 | f.name(), 61 | csum, 62 | if changed { "changed" } else { "unchanged" } 63 | ); 64 | f.set_generated(); 65 | if changed { 66 | f.set_changed(ptx.state().env()); // update_stamp might skip this if mtime is identical 67 | f.set_checksum(csum); 68 | } else { 69 | // unchanged 70 | f.set_checked(ptx.state().env()); 71 | } 72 | f.save(&mut ptx)?; 73 | ptx.commit()?; 74 | Ok(()) 75 | } 76 | -------------------------------------------------------------------------------- /src/bin/redo/targets.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // Copyright 2010-2018 Avery Pennarun and contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // SPDX-License-Identifier: Apache-2.0 17 | 18 | //! List the known targets (not sources). 19 | 20 | use anyhow::{anyhow, Error}; 21 | use rusqlite::TransactionBehavior; 22 | use std::env; 23 | use std::io; 24 | 25 | use redo::logs::LogBuilder; 26 | use redo::{self, Env, Files, ProcessState, ProcessTransaction, RedoPath}; 27 | 28 | pub(crate) fn run() -> Result<(), Error> { 29 | if env::args_os().len() != 1 { 30 | return Err(anyhow!("no arguments expected.")); 31 | } 32 | 33 | let targets: &[&RedoPath] = &[]; 34 | let env = Env::init(targets)?; 35 | LogBuilder::from(&env).setup(io::stderr()); 36 | 37 | let cwd = env::current_dir()?; 38 | let mut ps = ProcessState::init(env)?; 39 | let env2 = ps.env().clone(); 40 | let mut ptx = ProcessTransaction::new(&mut ps, TransactionBehavior::Deferred)?; 41 | for resf in Files::list(&mut ptx) { 42 | let f = resf?; 43 | if f.is_target(&env2)? { 44 | let p = redo::relpath(env2.base().join(f.name()), &cwd)?; 45 | println!( 46 | "{}", 47 | p.as_os_str() 48 | .to_str() 49 | .ok_or(anyhow!("could not get filename as UTF-8"))? 50 | ); 51 | } 52 | } 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /src/bin/redo/unlocked.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // Copyright 2010-2018 Avery Pennarun and contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // SPDX-License-Identifier: Apache-2.0 17 | 18 | //! Internal tool for building dependencies. 19 | 20 | use anyhow::{anyhow, Error}; 21 | use std::env; 22 | use std::ffi::OsString; 23 | use std::io; 24 | use std::process::{self, Command}; 25 | 26 | use redo::logs::LogBuilder; 27 | use redo::{self, Env, ENV_NO_OOB, ENV_UNLOCKED, EXIT_FAILURE}; 28 | 29 | pub(crate) fn run() -> Result<(), Error> { 30 | let mut args = env::args_os(); 31 | if args.len() < 3 { 32 | return Err(anyhow!("at least 2 arguments expected.")); 33 | } 34 | let env = Env::inherit()?; 35 | LogBuilder::from(&env).setup(io::stderr()); 36 | 37 | let target = args.nth(1).unwrap(); 38 | let deps: Vec = args.collect(); 39 | assert!(deps.iter().all(|d| d != &target)); 40 | 41 | // Build the known dependencies of our primary target. This *does* require 42 | // grabbing locks. 43 | let status = Command::new("redo-ifchange") 44 | .args(deps.iter().cloned()) 45 | .env(ENV_NO_OOB, "1") 46 | .spawn()? 47 | .wait()?; 48 | if !status.success() { 49 | process::exit(status.code().unwrap_or(EXIT_FAILURE)); 50 | } 51 | 52 | // We know our caller already owns the lock on target, so we don't have to 53 | // acquire another one; tell redo-ifchange about that. Also, we keep 54 | // REDO_NO_OOB set, because we don't want to do OOB now either. 55 | // (Actually it's most important for the primary target, since it's the one 56 | // who initiated the OOB in the first place.) 57 | let status = Command::new("redo-ifchange") 58 | .args(deps.iter().cloned()) 59 | .env(ENV_NO_OOB, "1") 60 | .env(ENV_UNLOCKED, "1") 61 | .spawn()? 62 | .wait()?; 63 | if !status.success() { 64 | process::exit(status.code().unwrap_or(EXIT_FAILURE)); 65 | } 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /src/bin/redo/whichdo.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // Copyright 2010-2018 Avery Pennarun and contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // SPDX-License-Identifier: Apache-2.0 17 | 18 | //! List the set of .do files considered to build a target. 19 | 20 | use anyhow::{anyhow, Error}; 21 | use std::env; 22 | use std::io; 23 | use std::path::Path; 24 | use std::process; 25 | 26 | use redo::logs::LogBuilder; 27 | use redo::{self, log_err, Env, EXIT_INVALID_TARGET}; 28 | 29 | pub(crate) fn run() -> Result<(), Error> { 30 | if env::args_os().len() != 2 { 31 | return Err(anyhow!("exactly one argument expected.")); 32 | } 33 | 34 | let env = Env::init_no_state()?; 35 | LogBuilder::from(&env).setup(io::stderr()); 36 | 37 | let want = env::args_os().nth(1).unwrap(); 38 | if want.is_empty() { 39 | log_err!("cannot build the empty target (\"\").\n"); 40 | process::exit(EXIT_INVALID_TARGET); 41 | } 42 | let cwd = env::current_dir()?; 43 | let want = redo::abs_path(&cwd, Path::new(&want)); 44 | for df in redo::possible_do_files(want) { 45 | let do_path = df.do_dir().join(df.do_file()); 46 | let relpath = redo::relpath(&do_path, &cwd)?; 47 | let relpath_str = relpath.as_os_str().to_str().unwrap(); 48 | assert!(!relpath_str.contains('\n')); 49 | println!("{}", relpath_str); 50 | if do_path.exists() { 51 | return Ok(()); 52 | } 53 | } 54 | 55 | Err(anyhow!( 56 | "no appropriate dofile found for {}", 57 | env::args().nth(1).unwrap() 58 | )) 59 | } 60 | -------------------------------------------------------------------------------- /src/cycles.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // Copyright 2010-2018 Avery Pennarun and contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // SPDX-License-Identifier: Apache-2.0 17 | 18 | //! Code for detecting and aborting on cyclic dependency loops. 19 | 20 | use std::borrow::Cow; 21 | use std::collections::HashSet; 22 | use std::env; 23 | 24 | use super::error::{RedoError, RedoErrorKind}; 25 | 26 | const ENV_CYCLES: &str = "REDO_CYCLES"; 27 | 28 | /// Get the set of held cycle items. 29 | fn get() -> HashSet { 30 | use std::iter::FromIterator; 31 | 32 | env::var(ENV_CYCLES) 33 | .map(|v| HashSet::from_iter(v.split(':').map(|s| s.to_string()))) 34 | .unwrap_or_default() 35 | } 36 | 37 | /// Add a lock to the list of held cycle items. 38 | pub(crate) fn add<'a, S: Into>>(fid: S) { 39 | use std::iter::FromIterator; 40 | 41 | let fid = fid.into(); 42 | let mut items = get(); 43 | if !items.contains(&fid as &str) { 44 | items.insert(fid.into_owned()); 45 | let items = Vec::from_iter(items); 46 | env::set_var(ENV_CYCLES, items.join(":")); 47 | } 48 | } 49 | 50 | pub(crate) fn check>(fid: S) -> Result<(), RedoError> { 51 | if get().contains(fid.as_ref()) { 52 | // Lock already held by parent: cyclic dependency 53 | Err(RedoErrorKind::CyclicDependency.into()) 54 | } else { 55 | Ok(()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/deps.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // Copyright 2010-2018 Avery Pennarun and contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // https://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // SPDX-License-Identifier: Apache-2.0 17 | 18 | //! Code for checking redo target dependencies. 19 | 20 | use std::cmp; 21 | use std::collections::HashSet; 22 | use std::ops::{Deref, DerefMut}; 23 | 24 | use super::env::Env; 25 | use super::error::{RedoError, RedoErrorKind}; 26 | use super::helpers::RedoPath; 27 | use super::state::{self, DepMode, File, ProcessTransaction, Stamp}; 28 | 29 | /// Determine if the given `File` needs to be built. 30 | pub fn is_dirty( 31 | ptx: &mut ProcessTransaction, 32 | f: &mut File, 33 | cb: &mut DirtyCallbacks, 34 | ) -> Result { 35 | let runid = ptx 36 | .state() 37 | .env() 38 | .runid 39 | .ok_or_else(|| RedoError::new("RUNID not set"))?; 40 | private_is_dirty( 41 | ptx, 42 | MutOrOwned::MutBorrowed(f), 43 | "", 44 | runid, 45 | &HashSet::new(), 46 | cb, 47 | ) 48 | } 49 | 50 | /// Determine if the given `File` needs to be built. 51 | /// 52 | /// `depth` is a string of whitespace representing the recursion depth. 53 | /// `max_changed` is initially the current runid: 54 | /// if a target is newer than this, 55 | /// anything that depends on it is considered outdated. 56 | /// `already_checked` is the list of dependencies already checked in this recursive cycle 57 | /// to avoid infinite loops. 58 | fn private_is_dirty( 59 | ptx: &mut ProcessTransaction, 60 | mut f: MutOrOwned, 61 | depth: &str, 62 | max_changed: i64, 63 | already_checked: &HashSet, 64 | cb: &mut DirtyCallbacks, 65 | ) -> Result { 66 | if already_checked.contains(&f.id()) { 67 | return Err(RedoErrorKind::CyclicDependency.into()); 68 | } 69 | let already_checked = { 70 | let mut already_checked = already_checked.clone(); 71 | already_checked.insert(f.id()); 72 | already_checked 73 | }; 74 | 75 | if ptx.state().env().debug >= 1 { 76 | log_debug!( 77 | "{}?{} {:?},{:?}\n", 78 | depth, 79 | f.nice_name(ptx.state().env())?, 80 | f.is_generated(), 81 | f.is_override 82 | ); 83 | } 84 | 85 | if f.failed_runid.is_some() { 86 | log_debug!("{}-- DIRTY (failed last time)\n", depth); 87 | return Ok(Dirtiness::Dirty); 88 | } 89 | match f.changed_runid { 90 | None => { 91 | log_debug!("{}-- DIRTY (never built)\n", depth); 92 | return Ok(Dirtiness::Dirty); 93 | } 94 | Some(changed_runid) if changed_runid > max_changed => { 95 | log_debug!( 96 | "{}-- DIRTY (built {} > {}; {})\n", 97 | depth, 98 | changed_runid, 99 | max_changed, 100 | ptx.state().env().runid.unwrap_or(-1) 101 | ); 102 | return Ok(Dirtiness::Dirty); // has been built more recently than parent 103 | } 104 | _ => {} 105 | } 106 | if (cb.is_checked)(&f, ptx.state().env()) { 107 | if ptx.state().env().debug >= 1 { 108 | log_debug!("{}-- CLEAN (checked)\n", depth); 109 | } 110 | return Ok(Dirtiness::Clean); // has already been checked during this session 111 | } 112 | match f.stamp.as_ref() { 113 | None => { 114 | log_debug!("{}-- DIRTY (no stamp)\n", depth); 115 | return Ok(Dirtiness::Dirty); 116 | } 117 | Some(oldstamp) => { 118 | let newstamp = f.read_stamp(ptx.state().env())?; 119 | if oldstamp != &newstamp { 120 | if newstamp == Stamp::MISSING { 121 | log_debug!("{}-- DIRTY (missing)\n", depth); 122 | if f.is_generated() { 123 | // previously was stamped and generated, but suddenly missing. 124 | // We can safely forget that it is/was a target; if someone 125 | // does redo-ifchange on it and it doesn't exist, we'll mark 126 | // it a target again, but if someone creates it by hand, 127 | // it'll be a source. This should reduce false alarms when 128 | // files change from targets to sources as a project evolves. 129 | log_debug!("{} converted target -> source {:?}", depth, f.id()); 130 | f.is_generated = false; 131 | f.failed_runid = Some(0); 132 | f.save(ptx)?; 133 | f.refresh(ptx)?; 134 | debug_assert!(!f.is_generated()); 135 | } 136 | } else { 137 | log_debug!("{}-- DIRTY (mtime)\n", depth); 138 | } 139 | return Ok(if !f.checksum().is_empty() { 140 | Dirtiness::NeedTargets(vec![f.into_owned()]) 141 | } else { 142 | Dirtiness::Dirty 143 | }); 144 | } 145 | } 146 | } 147 | 148 | let mut must_build: Vec = Vec::new(); 149 | for (mode, f2) in f.deps(ptx)? { 150 | let mut dirty = Dirtiness::Clean; 151 | match mode { 152 | DepMode::Created => { 153 | if ptx.state().env().base().join(f2.name()).exists() { 154 | log_debug!("{}-- DIRTY (created)\n", depth); 155 | dirty = Dirtiness::Dirty; 156 | } 157 | } 158 | DepMode::Modified => { 159 | let sub = { 160 | let mut depth = depth.to_string(); 161 | depth.push_str(" "); 162 | private_is_dirty( 163 | ptx, 164 | MutOrOwned::Owned(f2), 165 | &depth, 166 | cmp::max( 167 | f.changed_runid 168 | .expect("changed_runid missing on modified file"), 169 | f.checked_runid.unwrap_or(0), 170 | ), 171 | &already_checked, 172 | cb, 173 | )? 174 | }; 175 | if !sub.is_clean() { 176 | log_debug!("{}-- DIRTY (sub)\n", depth); 177 | dirty = sub; 178 | } 179 | } 180 | } 181 | if f.checksum().is_empty() { 182 | // f is a "normal" target: dirty f2 means f is instantly dirty 183 | match dirty { 184 | Dirtiness::Dirty => { 185 | // f2 is definitely dirty, so f definitely needs to 186 | // redo. 187 | return Ok(Dirtiness::Dirty); 188 | } 189 | Dirtiness::NeedTargets(targets) => { 190 | // our child f2 might be dirty, but it's not sure yet. It's 191 | // given us a list of targets we have to redo in order to 192 | // be sure. 193 | must_build.extend(targets); 194 | } 195 | _ => {} 196 | } 197 | } else { 198 | // f is "checksummable": dirty f2 means f needs to redo, 199 | // but f might turn out to be clean after that (ie. our parent 200 | // might not be dirty). 201 | match dirty { 202 | Dirtiness::Dirty => { 203 | // f2 is definitely dirty, so f definitely needs to 204 | // redo. However, after that, f might turn out to be 205 | // unchanged. 206 | return Ok(Dirtiness::NeedTargets(vec![f.into_owned()])); 207 | } 208 | Dirtiness::NeedTargets(targets) => { 209 | // our child f2 might be dirty, but it's not sure yet. It's 210 | // given us a list of targets we have to redo in order to 211 | // be sure. 212 | must_build.extend(targets); 213 | } 214 | _ => {} 215 | } 216 | } 217 | } 218 | if !must_build.is_empty() { 219 | // f is *maybe* dirty because at least one of its children is maybe 220 | // dirty. must_build has accumulated a list of "topmost" uncertain 221 | // objects in the tree. If we build all those, we can then 222 | // redo-ifchange f and it won't have any uncertainty next time. 223 | return Ok(Dirtiness::NeedTargets(must_build)); 224 | } 225 | log_debug!("{}-- CLEAN\n", depth); 226 | 227 | // if we get here, it's because the target is clean 228 | if f.is_override { 229 | (cb.log_override)(f.name()); 230 | } 231 | (cb.set_checked)(&mut f, ptx)?; 232 | Ok(Dirtiness::Clean) 233 | } 234 | 235 | /// Result of a call to [`is_dirty`]. 236 | #[derive(Clone, Debug)] 237 | pub enum Dirtiness { 238 | Clean, 239 | Dirty, 240 | NeedTargets(Vec), 241 | } 242 | 243 | impl Dirtiness { 244 | /// Reports whether the dirtiness value is [`Dirtiness::Clean`]. 245 | pub fn is_clean(&self) -> bool { 246 | match self { 247 | Dirtiness::Clean => true, 248 | _ => false, 249 | } 250 | } 251 | 252 | /// Reports whether the dirtiness value is [`Dirtiness::Dirty`]. 253 | pub fn is_dirty(&self) -> bool { 254 | match self { 255 | Dirtiness::Dirty => true, 256 | _ => false, 257 | } 258 | } 259 | } 260 | 261 | impl Default for Dirtiness { 262 | fn default() -> Self { 263 | Dirtiness::Clean 264 | } 265 | } 266 | 267 | /// Hooks for the [`is_dirty`] function. 268 | pub struct DirtyCallbacks<'a> { 269 | is_checked: Box bool + 'a>, 270 | set_checked: 271 | Box) -> Result<(), RedoError> + 'a>, 272 | log_override: Box, 273 | } 274 | 275 | impl<'a> Default for DirtyCallbacks<'a> { 276 | /// Default hooks read from and write to the state. 277 | fn default() -> DirtyCallbacks<'a> { 278 | DirtyCallbacks { 279 | is_checked: Box::new(File::is_checked), 280 | set_checked: Box::new(File::set_checked_save), 281 | log_override: Box::new(state::warn_override), 282 | } 283 | } 284 | } 285 | 286 | impl<'a> From> for DirtyCallbacks<'a> { 287 | fn from(b: DirtyCallbacksBuilder<'a>) -> DirtyCallbacks<'a> { 288 | b.build() 289 | } 290 | } 291 | 292 | /// Builder for [`DirtyCallbacks`]. 293 | #[derive(Default)] 294 | pub struct DirtyCallbacksBuilder<'a> { 295 | callbacks: DirtyCallbacks<'a>, 296 | } 297 | 298 | impl<'a> DirtyCallbacksBuilder<'a> { 299 | #[inline] 300 | pub fn new() -> DirtyCallbacksBuilder<'a> { 301 | DirtyCallbacksBuilder::default() 302 | } 303 | 304 | /// Sets the `is_checked` callback. 305 | #[inline] 306 | pub fn is_checked bool + 'a>(mut self, f: F) -> Self { 307 | self.callbacks.is_checked = Box::new(f); 308 | self 309 | } 310 | 311 | /// Sets the `set_checked` callback. 312 | #[inline] 313 | pub fn set_checked< 314 | F: FnMut(&mut File, &mut ProcessTransaction<'_>) -> Result<(), RedoError> + 'a, 315 | >( 316 | mut self, 317 | f: F, 318 | ) -> Self { 319 | self.callbacks.set_checked = Box::new(f); 320 | self 321 | } 322 | 323 | /// Sets the function called when a target is clean but the user modified it. 324 | #[inline] 325 | pub fn log_override(mut self, f: F) -> Self { 326 | self.callbacks.log_override = Box::new(f); 327 | self 328 | } 329 | 330 | #[inline] 331 | pub fn build(self) -> DirtyCallbacks<'a> { 332 | self.callbacks 333 | } 334 | } 335 | 336 | /// A smart pointer to owned or mutably borrowed data, 337 | /// similar to [`std::borrow::Cow`]. 338 | #[derive(Debug)] 339 | enum MutOrOwned<'a, B: 'a> { 340 | MutBorrowed(&'a mut B), 341 | Owned(B), 342 | } 343 | 344 | impl MutOrOwned<'_, B> { 345 | /// Extracts the owned data. 346 | /// 347 | /// Clones the data if it is not already owned. 348 | fn into_owned(self) -> B { 349 | match self { 350 | MutOrOwned::MutBorrowed(b) => b.clone(), 351 | MutOrOwned::Owned(o) => o, 352 | } 353 | } 354 | } 355 | 356 | impl<'a, B> Deref for MutOrOwned<'a, B> { 357 | type Target = B; 358 | 359 | fn deref(&self) -> &Self::Target { 360 | match self { 361 | MutOrOwned::MutBorrowed(b) => b, 362 | MutOrOwned::Owned(ref o) => o, 363 | } 364 | } 365 | } 366 | impl<'a, B> DerefMut for MutOrOwned<'a, B> { 367 | fn deref_mut(&mut self) -> &mut Self::Target { 368 | match self { 369 | MutOrOwned::MutBorrowed(b) => b, 370 | MutOrOwned::Owned(ref mut o) => o, 371 | } 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | use std::borrow::Cow; 18 | use std::error::Error; 19 | use std::ffi::OsString; 20 | use std::fmt::{self, Display, Formatter}; 21 | 22 | use super::exits::*; 23 | use super::helpers::RedoPathBuf; 24 | 25 | /// The primary error type for the `redo` crate. 26 | #[derive(Debug)] 27 | pub struct RedoError { 28 | kind: RedoErrorKind, 29 | msg: Cow<'static, str>, 30 | cause: Option>, 31 | } 32 | 33 | impl RedoError { 34 | /// Returns a generic error with the given message. 35 | #[inline] 36 | pub fn new(msg: S) -> RedoError 37 | where 38 | S: Into>, 39 | { 40 | RedoError { 41 | kind: RedoErrorKind::default(), 42 | msg: msg.into(), 43 | cause: None, 44 | } 45 | } 46 | 47 | /// Returns an error with a given exit code. 48 | #[inline] 49 | pub fn immediate_exit(code: i32, msg: S) -> RedoError 50 | where 51 | S: Into>, 52 | { 53 | RedoError { 54 | kind: RedoErrorKind::ImmediateExit(code), 55 | msg: msg.into(), 56 | cause: None, 57 | } 58 | } 59 | 60 | /// Returns a generic error that contains another error as its message. 61 | /// The error is not presented on the source chain. 62 | #[inline] 63 | pub(crate) fn opaque_error(e: E) -> RedoError { 64 | RedoError { 65 | kind: RedoErrorKind::default(), 66 | msg: Cow::Owned(e.to_string()), 67 | cause: None, 68 | } 69 | } 70 | 71 | /// Returns a `RedoError` that wraps an underlying error. 72 | #[inline] 73 | pub(crate) fn wrap(cause: E, msg: S) -> RedoError 74 | where 75 | E: Error + Send + Sync + 'static, 76 | S: Into>, 77 | { 78 | RedoError { 79 | kind: RedoErrorKind::default(), 80 | msg: msg.into(), 81 | cause: Some(Box::new(cause)), 82 | } 83 | } 84 | 85 | #[inline] 86 | pub fn kind(&self) -> &RedoErrorKind { 87 | &self.kind 88 | } 89 | 90 | #[inline] 91 | pub(crate) fn with_kind(self, kind: RedoErrorKind) -> RedoError { 92 | RedoError { kind, ..self } 93 | } 94 | } 95 | 96 | impl Display for RedoError { 97 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 98 | Display::fmt(&self.msg, f) 99 | } 100 | } 101 | 102 | impl Error for RedoError { 103 | fn source(&self) -> Option<&(dyn Error + 'static)> { 104 | self.cause.as_ref().map(|cause| &**cause as &dyn Error) 105 | } 106 | } 107 | 108 | impl From for RedoError { 109 | fn from(kind: RedoErrorKind) -> RedoError { 110 | RedoError { 111 | msg: Cow::Owned(kind.to_string()), 112 | kind, 113 | cause: None, 114 | } 115 | } 116 | } 117 | 118 | /// Classification of a [`RedoError`]. 119 | #[derive(Clone, Eq, PartialEq, Debug)] 120 | #[non_exhaustive] 121 | pub enum RedoErrorKind { 122 | Generic, 123 | FailedInAnotherThread { target: RedoPathBuf }, 124 | InvalidTarget(OsString), 125 | CyclicDependency, 126 | FileNotFound, 127 | ImmediateExit(i32), 128 | } 129 | 130 | impl RedoErrorKind { 131 | /// Returns the first non-[`RedoErrorKind::Generic`] error kind in the error chain or 132 | /// [`RedoErrorKind::Generic`] 133 | pub fn of<'a, E: AsRef>(e: &'a E) -> &'a RedoErrorKind { 134 | let mut next: Option<&(dyn Error + 'static)> = Some(e.as_ref()); 135 | while let Some(e) = next { 136 | next = e.source(); 137 | let kind = match e.downcast_ref::() { 138 | Some(e) => e.kind(), 139 | None => continue, 140 | }; 141 | if kind != &RedoErrorKind::Generic { 142 | return kind; 143 | } 144 | } 145 | &RedoErrorKind::Generic 146 | } 147 | 148 | /// Returns the exit code for the error kind. 149 | pub fn exit_code(&self) -> i32 { 150 | match self { 151 | &RedoErrorKind::FailedInAnotherThread { .. } => EXIT_FAILED_IN_ANOTHER_THREAD, 152 | &RedoErrorKind::InvalidTarget(_) => EXIT_INVALID_TARGET, 153 | &RedoErrorKind::CyclicDependency => EXIT_CYCLIC_DEPENDENCY, 154 | &RedoErrorKind::ImmediateExit(code) => code, 155 | _ => EXIT_FAILURE, 156 | } 157 | } 158 | } 159 | 160 | impl Default for RedoErrorKind { 161 | #[inline] 162 | fn default() -> RedoErrorKind { 163 | RedoErrorKind::Generic 164 | } 165 | } 166 | 167 | impl Display for RedoErrorKind { 168 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 169 | match self { 170 | RedoErrorKind::Generic => f.write_str("error"), 171 | RedoErrorKind::FailedInAnotherThread { target } => { 172 | write!(f, "{:?}: failed in another thread", target) 173 | } 174 | RedoErrorKind::InvalidTarget(target) => write!(f, "invalid target {:?}", target), 175 | RedoErrorKind::CyclicDependency => f.write_str("cyclic dependency detected"), 176 | RedoErrorKind::FileNotFound => f.write_str("file not found"), 177 | RedoErrorKind::ImmediateExit(code) => write!(f, "exit code {}", code), 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/exits.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | /// Success exit code. 18 | pub const EXIT_SUCCESS: i32 = 0; 19 | 20 | /// Generic failure exit code. 21 | pub const EXIT_FAILURE: i32 = 1; 22 | 23 | /// Target failed in a concurrent run. 24 | pub const EXIT_FAILED_IN_ANOTHER_THREAD: i32 = 2; 25 | 26 | /// Attempt to get information about a target not in the redo database. 27 | pub const EXIT_UNKNOWN_TARGET: i32 = 24; 28 | 29 | /// Attempt to build a target that has already failed during the same run. 30 | pub const EXIT_TARGET_FAILED: i32 = 32; 31 | 32 | /// Failed to start internal helper subprocess. 33 | pub(crate) const EXIT_HELPER_FAILURE: i32 = 99; 34 | 35 | /// `MAKEFLAGS` specified invalid file descriptors. 36 | pub(crate) const EXIT_INVALID_JOBSERVER: i32 = 200; 37 | 38 | /// Internal error while spawning a job. 39 | pub(crate) const EXIT_JOB_FAILURE: i32 = 201; 40 | 41 | /// Invalid name for a target. 42 | pub const EXIT_INVALID_TARGET: i32 = 204; 43 | 44 | /// `$1` updated directly. 45 | pub const EXIT_TARGET_DIRECTLY_MODIFIED: i32 = 206; 46 | 47 | /// Standard out written to and `$3` created. 48 | pub const EXIT_MULTIPLE_OUTPUTS: i32 = 207; 49 | 50 | /// Cyclic dependency detected. 51 | pub const EXIT_CYCLIC_DEPENDENCY: i32 = 208; 52 | 53 | /// `BuildJob` internal error. 54 | pub const EXIT_BUILD_JOB_ERROR: i32 = 209; 55 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | /// Log an error. 18 | /// 19 | /// # Example 20 | /// 21 | /// ```no_run 22 | /// # use redo::log_err; 23 | /// # fn main() { 24 | /// log_err!("{} has failed", "everything"); 25 | /// # } 26 | /// ``` 27 | #[macro_export] 28 | macro_rules! log_err { 29 | ($($arg:tt)*) => {{ 30 | let s = format!($($arg)*); 31 | $crate::logs::meta("error", s.trim_end(), None); 32 | }} 33 | } 34 | 35 | /// Log a warning. 36 | /// 37 | /// # Example 38 | /// 39 | /// ```no_run 40 | /// # use redo::log_warn; 41 | /// # fn main() { 42 | /// log_warn!("{} has failed", "something non-critical"); 43 | /// # } 44 | /// ``` 45 | #[macro_export] 46 | macro_rules! log_warn { 47 | ($($arg:tt)*) => {{ 48 | let s = format!($($arg)*); 49 | $crate::logs::meta("warning", s.trim_end(), None); 50 | }} 51 | } 52 | 53 | /// Log a debug message. 54 | /// 55 | /// # Example 56 | /// 57 | /// ```no_run 58 | /// # use redo::log_debug; 59 | /// # fn main() { 60 | /// log_debug!("some details"); 61 | /// # } 62 | /// ``` 63 | #[macro_export] 64 | macro_rules! log_debug { 65 | ($($arg:tt)*) => {{ 66 | if $crate::logs::debug_level() >= 1 { 67 | let s = format!($($arg)*); 68 | $crate::logs::meta("debug", s.trim_end(), None); 69 | } 70 | }} 71 | } 72 | 73 | /// Log a verbose debug message. 74 | /// 75 | /// # Example 76 | /// 77 | /// ```no_run 78 | /// # use redo::log_debug2; 79 | /// # fn main() { 80 | /// log_debug2!("some verbose details"); 81 | /// # } 82 | /// ``` 83 | #[macro_export] 84 | macro_rules! log_debug2 { 85 | ($($arg:tt)*) => {{ 86 | if $crate::logs::debug_level() >= 2 { 87 | let s = format!($($arg)*); 88 | $crate::logs::meta("debug", s.trim_end(), None); 89 | } 90 | }} 91 | } 92 | 93 | /// Log an extra verbose debug message. 94 | /// 95 | /// # Example 96 | /// 97 | /// ```no_run 98 | /// # use redo::log_debug3; 99 | /// # fn main() { 100 | /// log_debug3!("extra verbose details"); 101 | /// # } 102 | /// ``` 103 | #[macro_export] 104 | macro_rules! log_debug3 { 105 | ($($arg:tt)*) => {{ 106 | if $crate::logs::debug_level() >= 3 { 107 | let s = format!($($arg)*); 108 | $crate::logs::meta("debug", s.trim_end(), None); 109 | } 110 | }} 111 | } 112 | 113 | pub mod builder; 114 | mod cycles; 115 | mod deps; 116 | mod env; 117 | mod error; 118 | mod exits; 119 | mod helpers; 120 | mod jobserver; 121 | pub mod logs; 122 | mod paths; 123 | mod state; 124 | 125 | pub use deps::{is_dirty, Dirtiness, DirtyCallbacks, DirtyCallbacksBuilder}; 126 | pub use env::*; 127 | pub use error::{RedoError, RedoErrorKind}; 128 | pub use exits::*; 129 | pub use helpers::{abs_path, normpath, RedoPath, RedoPathBuf}; 130 | pub use jobserver::*; 131 | pub use paths::{possible_do_files, DoFile, PossibleDoFiles}; 132 | pub use state::{ 133 | always_filename, logname, relpath, DepMode, File, Files, Lock, LockType, ProcessState, 134 | ProcessTransaction, Stamp, LOG_LOCK_MAGIC, 135 | }; 136 | -------------------------------------------------------------------------------- /t/.gitignore: -------------------------------------------------------------------------------- 1 | /broken 2 | /shellfile 3 | /shellfail 4 | /shelltest.warned 5 | /shelltest.failed 6 | /shlink 7 | /stress.log 8 | /symlink path 9 | /flush-cache 10 | -------------------------------------------------------------------------------- /t/000-set-minus-e/.gitignore: -------------------------------------------------------------------------------- 1 | log 2 | -------------------------------------------------------------------------------- /t/000-set-minus-e/all.do: -------------------------------------------------------------------------------- 1 | redo-ifchange ../../redo/sh 2 | rm -f log 3 | redo fatal >/dev/null 2>&1 || true 4 | 5 | [ "$(cat log)" = "ok" ] || exit 5 6 | -------------------------------------------------------------------------------- /t/000-set-minus-e/clean.do: -------------------------------------------------------------------------------- 1 | rm -f log *~ .*~ 2 | -------------------------------------------------------------------------------- /t/000-set-minus-e/fatal.do: -------------------------------------------------------------------------------- 1 | rm -f log 2 | echo ok >>log 3 | this-should-cause-a-fatal-error 4 | echo fail >>log # this line should never run 5 | -------------------------------------------------------------------------------- /t/010-jobserver/.gitignore: -------------------------------------------------------------------------------- 1 | *.end 2 | *.start 3 | *.sub 4 | *.spin 5 | *.log 6 | *.x 7 | -------------------------------------------------------------------------------- /t/010-jobserver/all.do: -------------------------------------------------------------------------------- 1 | # We put the -j options at this toplevel to detect an earlier bug 2 | # where the sub-jobserver wasn't inherited by sub-sub-processes, which 3 | # accidentally reverted to the parent jobserver instead. 4 | 5 | redo -j1 serialtest 6 | 7 | # Capture log output to parallel.log to hide the (intentional since we're 8 | # testing it) scary warning from redo about overriding the jobserver. 9 | if [ -n "$REDO_LOCKS_BROKEN" ]; then 10 | echo "Locks are broken on this OS; skipping parallel tests." >&2 11 | exit 0 12 | fi 13 | 14 | echo 'parallel test...' >&2 15 | if ! redo -j10 paralleltest 2>parallel.log; then 16 | cat parallel.log >&2 17 | exit 99 18 | fi 19 | -------------------------------------------------------------------------------- /t/010-jobserver/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *~ .*~ *.log *.sub *.spin *.x *.start *.end \ 2 | first second parallel parallel2 3 | -------------------------------------------------------------------------------- /t/010-jobserver/default.spin.do: -------------------------------------------------------------------------------- 1 | for d in 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19; do 2 | redo $2.$d.x 3 | done 4 | echo hello 5 | -------------------------------------------------------------------------------- /t/010-jobserver/default.x.do: -------------------------------------------------------------------------------- 1 | echo world 2 | -------------------------------------------------------------------------------- /t/010-jobserver/first.do: -------------------------------------------------------------------------------- 1 | # in case we're (erroneously) running in parallel, give second.do some 2 | # time to start but not finish. 3 | echo 'first sleep' >&2 4 | sleep 1 5 | 6 | # Because of --shuffle, we can't be sure if first or second ran first, but 7 | # because all.do uses -j1, we *should* expect that if second ran first, it 8 | # at least ran to completion before we ran at all. 9 | if [ -e second.start ]; then 10 | echo 'first: second already started before we did...' >&2 11 | [ -e second.end ] || exit 21 12 | echo 'first: ...and it finished as it should.' >&2 13 | # no sense continuing the test; can't test anything if second already 14 | # ran. 15 | exit 0 16 | fi 17 | echo 'first: second has not started yet, good.' >&2 18 | 19 | echo 'first spin' >&2 20 | redo 1.a.spin 21 | [ -e 1.a.spin ] || exit 11 22 | echo 'first spin complete' >&2 23 | 24 | ! [ -e second.start ] || exit 22 25 | -------------------------------------------------------------------------------- /t/010-jobserver/parallel.do: -------------------------------------------------------------------------------- 1 | # We should be running in parallel with a jobserver shared with second.do. 2 | # Give second.do some time to start but not finish. 3 | sleep 1 4 | 5 | [ -e parallel2.start ] || exit 31 6 | ! [ -e parallel2.end ] || exit 32 7 | -------------------------------------------------------------------------------- /t/010-jobserver/parallel2.do: -------------------------------------------------------------------------------- 1 | rm -f $1.start $1.end 2 | : >$1.start 3 | sleep 2 4 | : >$1.end 5 | -------------------------------------------------------------------------------- /t/010-jobserver/paralleltest.do: -------------------------------------------------------------------------------- 1 | # Test that -j2 really gives us parallel builds with their own tokens. 2 | # (It's hard to test for sure that we have our own tokens, but if we're 3 | # sharing with other tests, we can't be sure that parallel2 will run while 4 | # parallel is running, and the race condition will make this test at least 5 | # be flakey instead of pass, which means there's a bug.) 6 | rm -f *.sub *.spin *.x parallel *.start *.end 7 | redo parallel parallel2 8 | -------------------------------------------------------------------------------- /t/010-jobserver/second.do: -------------------------------------------------------------------------------- 1 | rm -f $1.start $1.end 2 | echo 'second start' >&2 3 | : >$1.start 4 | redo 2.x 5 | echo 'second sleep' >&2 6 | redo-ifchange first # wait until 'first' finishes, if it's running 7 | echo 'second end' >&2 8 | : >$1.end 9 | -------------------------------------------------------------------------------- /t/010-jobserver/serialtest.do: -------------------------------------------------------------------------------- 1 | # Test that -j1 really serializes all sub-redo processes. 2 | rm -f *.sub *.spin *.x first second *.start *.end 3 | redo first second 4 | -------------------------------------------------------------------------------- /t/100-args/.gitignore: -------------------------------------------------------------------------------- 1 | passfail 2 | -------------------------------------------------------------------------------- /t/100-args/all.do: -------------------------------------------------------------------------------- 1 | redo test.args test2.args passfailtest noargs/run 2 | . ../skip-if-minimal-do.sh 3 | -------------------------------------------------------------------------------- /t/100-args/clean.do: -------------------------------------------------------------------------------- 1 | rm -f passfail *~ .*~ */*~ */.*~ noargs/all 2 | -------------------------------------------------------------------------------- /t/100-args/default.args.do: -------------------------------------------------------------------------------- 1 | [ "$1" = "test.args" ] 2 | [ "$2" = "test" ] 3 | [ "$3" != "test.args" ] 4 | -------------------------------------------------------------------------------- /t/100-args/noargs/all.do: -------------------------------------------------------------------------------- 1 | echo RAN >$3 2 | -------------------------------------------------------------------------------- /t/100-args/noargs/run.do: -------------------------------------------------------------------------------- 1 | rm -f all 2 | redo-ifchange # should not default to 'all' since not running from top level 3 | [ ! -e all ] || exit 11 4 | -------------------------------------------------------------------------------- /t/100-args/passfail.do: -------------------------------------------------------------------------------- 1 | echo $$ 2 | if [ -e pleasefail ]; then 3 | exit 1 4 | else 5 | exit 0 6 | fi 7 | -------------------------------------------------------------------------------- /t/100-args/passfailtest.do: -------------------------------------------------------------------------------- 1 | . ../skip-if-minimal-do.sh 2 | 3 | rm -f pleasefail 4 | redo passfail 5 | [ -e passfail ] || exit 42 6 | PF1=$(cat passfail) 7 | touch pleasefail 8 | redo passfail 2>/dev/null && exit 43 9 | [ -e passfail ] || exit 44 10 | PF2=$(cat passfail) 11 | [ "$PF1" = "$PF2" ] || exit 45 12 | rm -f pleasefail 13 | redo passfail || exit 46 14 | PF3=$(cat passfail) 15 | [ "$PF1" != "$PF3" ] || exit 47 16 | -------------------------------------------------------------------------------- /t/100-args/test2.args.do: -------------------------------------------------------------------------------- 1 | [ "$1" = "test2.args" ] 2 | [ "$2" = "test2.args" ] 3 | [ "$3" != "test2.args" ] 4 | -------------------------------------------------------------------------------- /t/101-atime/.gitignore: -------------------------------------------------------------------------------- 1 | atime2 2 | -------------------------------------------------------------------------------- /t/101-atime/all.do: -------------------------------------------------------------------------------- 1 | redo atime 2 | -------------------------------------------------------------------------------- /t/101-atime/atime.do: -------------------------------------------------------------------------------- 1 | redo atime2 2 | -------------------------------------------------------------------------------- /t/101-atime/atime2.do: -------------------------------------------------------------------------------- 1 | # make sure redo doesn't think merely *reading* the old file counts as 2 | # modifying it in-place. 3 | cat $1 >/dev/null 2>/dev/null || true 4 | ../../redo/py tick.py 5 | cat $1 >/dev/null 2>/dev/null || true 6 | echo hello 7 | -------------------------------------------------------------------------------- /t/101-atime/clean.do: -------------------------------------------------------------------------------- 1 | rm -f atime2 *~ .*~ 2 | -------------------------------------------------------------------------------- /t/101-atime/tick.py: -------------------------------------------------------------------------------- 1 | import time 2 | t2 = int(time.time()) + 1.0 3 | while 1: 4 | t = time.time() 5 | if t >= t2: break 6 | time.sleep(t2 - t + 0.01) 7 | -------------------------------------------------------------------------------- /t/102-empty/.gitignore: -------------------------------------------------------------------------------- 1 | touch1 2 | silence 3 | silence.do 4 | -------------------------------------------------------------------------------- /t/102-empty/all.do: -------------------------------------------------------------------------------- 1 | redo silencetest touchtest blank 2 | -------------------------------------------------------------------------------- /t/102-empty/blank.do: -------------------------------------------------------------------------------- 1 | redo-ifchange 2 | redo-ifcreate 3 | redo 4 | -------------------------------------------------------------------------------- /t/102-empty/clean.do: -------------------------------------------------------------------------------- 1 | rm -f touch1 touch1-ran *~ .*~ silence.do 2 | -------------------------------------------------------------------------------- /t/102-empty/silencetest.do: -------------------------------------------------------------------------------- 1 | # This may have been leftover from a previous run, when switching 2 | # between "real" redo and minimal/do, so clean it up. 3 | rm -f silence 4 | 5 | echo 'echo hello' >silence.do 6 | redo silence 7 | [ -e silence ] || exit 55 8 | echo 'true' >silence.do 9 | redo silence 10 | . ../skip-if-minimal-do.sh 11 | [ ! -e silence ] || exit 66 12 | rm -f silence.do 13 | -------------------------------------------------------------------------------- /t/102-empty/touchtest.do: -------------------------------------------------------------------------------- 1 | # This may have been leftover from a previous run, when switching 2 | # between "real" redo and minimal/do, so clean it up. 3 | rm -f touch1 4 | 5 | # simply create touch1 6 | echo 'echo hello' >touch1.do 7 | redo touch1 8 | [ -e touch1 ] || exit 55 9 | [ "$(cat touch1)" = "hello" ] || exit 56 10 | 11 | # ensure that 'redo touch1' always re-runs touch1.do even if we have 12 | # already built touch1 in this session, and even if touch1 already exists. 13 | echo 'echo hello2' >touch1.do 14 | redo touch1 15 | [ "$(cat touch1)" = "hello2" ] || exit 57 16 | 17 | # ensure that touch1 is rebuilt even if it got deleted after the last redo 18 | # inside the same session. Also ensure that we can produce a zero-byte 19 | # output file explicitly. 20 | rm -f touch1 21 | echo 'touch $3' >touch1.do 22 | redo touch1 23 | [ -e touch1 ] || exit 66 24 | [ -z "$(cat touch1)" ] || exit 67 25 | 26 | # Also test that zero bytes of output does not create the file at all, as 27 | # opposed to creating a zero-byte file. 28 | rm -f touch1 29 | echo 'touch touch1-ran' >touch1.do 30 | redo touch1 31 | [ -e touch1 ] && exit 75 32 | [ -e touch1-ran ] || exit 76 33 | rm -f touch1-ran 34 | 35 | # Make sure that redo-ifchange *won't* rebuild touch1 if we have already 36 | # built it, even if building it did not produce an output file. 37 | redo-ifchange touch1 38 | [ -e touch1 ] && exit 77 39 | [ -e touch1-ran ] && exit 78 40 | 41 | rm -f touch1.do 42 | -------------------------------------------------------------------------------- /t/103-unicode/all.do: -------------------------------------------------------------------------------- 1 | redo unicode 2 | -------------------------------------------------------------------------------- /t/103-unicode/clean.do: -------------------------------------------------------------------------------- 1 | rm -rf *.tmp 2 | -------------------------------------------------------------------------------- /t/103-unicode/unicode.do: -------------------------------------------------------------------------------- 1 | # Test that redo can handle a script whose path contains non-ASCII characters. 2 | # Note: the test directory is intentionally *not* a normalized unicode 3 | # string, ie. filesystems like macOS will convert it to a different string 4 | # at creation time. This tests weird normalization edge cases. 5 | # 6 | # Unfortunately, on macOS with APFS, it may helpfully normalize the path at 7 | # *create* time, but not on future *open* attempts. Thus, we let the shell 8 | # figure out what directory name actually got created, then pass that to redo. 9 | # Hence the weird wildcard expansion loop. 10 | rm -rf test-uni*.tmp 11 | mkdir "test-uniçøðë.tmp" 12 | for p in test-uni*.tmp; do 13 | : >$p/test1.do 14 | redo "$p/test1" 15 | done 16 | 17 | -------------------------------------------------------------------------------- /t/104-space/all.do: -------------------------------------------------------------------------------- 1 | redo "space dir/test" 2 | -------------------------------------------------------------------------------- /t/104-space/clean.do: -------------------------------------------------------------------------------- 1 | redo "space dir/clean" 2 | -------------------------------------------------------------------------------- /t/104-space/space dir/.gitignore: -------------------------------------------------------------------------------- 1 | /space file 2 | /space 2 3 | -------------------------------------------------------------------------------- /t/104-space/space dir/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *~ .*~ "space 2" 2 | -------------------------------------------------------------------------------- /t/104-space/space dir/space 2.do: -------------------------------------------------------------------------------- 1 | echo $$ 2 | -------------------------------------------------------------------------------- /t/104-space/space dir/space file.do: -------------------------------------------------------------------------------- 1 | redo-ifchange "space 2" 2 | -------------------------------------------------------------------------------- /t/104-space/space dir/test.do: -------------------------------------------------------------------------------- 1 | redo "space file" 2 | F1="$(cat "space 2")" 3 | redo "../space dir/space file" 4 | F2="$(cat "space 2")" 5 | [ "$F1" = "$F2" ] || exit 2 6 | [ -n "$F1" ] || exit 3 7 | -------------------------------------------------------------------------------- /t/105-sympath/.gitignore: -------------------------------------------------------------------------------- 1 | /*.dyn 2 | /src 3 | /x 4 | /y 5 | 6 | -------------------------------------------------------------------------------- /t/105-sympath/all.do: -------------------------------------------------------------------------------- 1 | redo-ifchange ../flush-cache 2 | rm -f src 3 | : >src 4 | 5 | for iter in 10 20; do 6 | rm -rf y 7 | rm -f x *.dyn static 8 | mkdir y 9 | : >y/static 10 | ln -s . y/x 11 | ../flush-cache 12 | 13 | ( 14 | cd y/x/x/x/x/x 15 | IFS=$(printf '\n') 16 | redo-ifchange static x/x/x/static $PWD/static \ 17 | $(/bin/pwd)/static /etc/passwd 18 | redo-ifchange $PWD/../static 2>/dev/null && exit 35 19 | redo-ifchange 1.dyn x/x/x/2.dyn $PWD/3.dyn \ 20 | $PWD/../4.dyn $(/bin/pwd)/5.dyn 21 | ) 22 | [ -e y/1.dyn ] || exit $((iter + 1)) 23 | [ -e y/2.dyn ] || exit $((iter + 2)) 24 | [ -e y/3.dyn ] || exit $((iter + 3)) 25 | [ -e 4.dyn ] || exit $((iter + 4)) 26 | [ -e y/5.dyn ] || exit $((iter + 5)) 27 | 28 | # Second iteration won't work in minimal/do since it only ever 29 | # builds things once. 30 | . ../skip-if-minimal-do.sh 31 | done 32 | -------------------------------------------------------------------------------- /t/105-sympath/clean.do: -------------------------------------------------------------------------------- 1 | rm -rf y 2 | rm -f src x *.dyn *~ .*~ 3 | -------------------------------------------------------------------------------- /t/105-sympath/default.dyn.do: -------------------------------------------------------------------------------- 1 | redo-ifchange src 2 | echo dynamic >$3 3 | -------------------------------------------------------------------------------- /t/110-compile/.gitignore: -------------------------------------------------------------------------------- 1 | CC 2 | LD 3 | [yb]ellow 4 | hello 5 | *.o 6 | -------------------------------------------------------------------------------- /t/110-compile/CC.do: -------------------------------------------------------------------------------- 1 | exec >$3 2 | cat <<-EOF 3 | cc -Wall -o /dev/fd/1 -c "\$1" 4 | EOF 5 | chmod a+x $3 6 | -------------------------------------------------------------------------------- /t/110-compile/LD.do: -------------------------------------------------------------------------------- 1 | exec >$3 2 | cat <<-EOF 3 | OUT="\$1" 4 | shift 5 | cc -Wall -o "\$OUT" "\$@" 6 | EOF 7 | chmod a+x $3 8 | -------------------------------------------------------------------------------- /t/110-compile/all.do: -------------------------------------------------------------------------------- 1 | if type cc >/dev/null 2>&1; then 2 | redo-ifchange hello yellow bellow 3 | else 4 | echo "$0: No C compiler installed; skipping this test." >&2 5 | redo-ifcreate /usr/bin/cc 6 | fi 7 | -------------------------------------------------------------------------------- /t/110-compile/bellow.do: -------------------------------------------------------------------------------- 1 | redo-ifchange LD yellow.o 2 | ./LD "$3" yellow.o 3 | ../sleep 2 4 | -------------------------------------------------------------------------------- /t/110-compile/clean.do: -------------------------------------------------------------------------------- 1 | rm -f hello [by]ellow *.o CC LD *~ .*~ 2 | 3 | 4 | -------------------------------------------------------------------------------- /t/110-compile/hello.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() 4 | { 5 | printf("hello, world!\n"); 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /t/110-compile/hello.do: -------------------------------------------------------------------------------- 1 | redo-ifchange LD hello.o 2 | ../sleep 1 3 | ./LD "$3" hello.o 4 | -------------------------------------------------------------------------------- /t/110-compile/hello.o.do: -------------------------------------------------------------------------------- 1 | # This test is meant to confirm some basic redo functionality 2 | # related to static files not in the build tree. But if your 3 | # system doesn't happen to have stdio.h in the usual location, 4 | # let's not explode just for that. 5 | stdio=/usr/include/stdio.h 6 | [ -e "$stdio" ] || stdio= 7 | 8 | redo-ifchange CC hello.c $stdio 9 | redo-ifcreate stdio.h 10 | ../sleep 3 11 | ./CC hello.c 12 | -------------------------------------------------------------------------------- /t/110-compile/yellow.do: -------------------------------------------------------------------------------- 1 | redo-ifchange LD yellow.o 2 | ../sleep 1.5 3 | ./LD "$3" yellow.o 4 | -------------------------------------------------------------------------------- /t/110-compile/yellow.o.do: -------------------------------------------------------------------------------- 1 | redo-ifchange CC hello.c 2 | ../sleep 2 3 | cc -o $3 -c hello.c 4 | -------------------------------------------------------------------------------- /t/111-example/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | CC 3 | hello 4 | -------------------------------------------------------------------------------- /t/111-example/CC.do: -------------------------------------------------------------------------------- 1 | redo-ifchange config.sh 2 | . ./config.sh 3 | exec >$3 4 | cat <<-EOF 5 | redo-ifchange \$2.c 6 | cc $CFLAGS -MD -MF \$3.deps -o \$3 -c \$2.c 7 | read DEPS <\$3.deps 8 | rm -f \$3.deps 9 | redo-ifchange \${DEPS#*:} 10 | EOF 11 | chmod +x $3 12 | -------------------------------------------------------------------------------- /t/111-example/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | %: FORCE 4 | +redo $@ 5 | 6 | .PHONY: FORCE 7 | -------------------------------------------------------------------------------- /t/111-example/all.do: -------------------------------------------------------------------------------- 1 | if type cc >/dev/null 2>&1; then 2 | redo-ifchange hello 3 | else 4 | echo "$0: No C compiler installed; skipping this test." >&2 5 | redo-ifcreate /usr/bin/cc 6 | fi 7 | -------------------------------------------------------------------------------- /t/111-example/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *.tmp *~ *.o hello CC 2 | -------------------------------------------------------------------------------- /t/111-example/config.sh: -------------------------------------------------------------------------------- 1 | CFLAGS="-Wall" 2 | -------------------------------------------------------------------------------- /t/111-example/default.o.do: -------------------------------------------------------------------------------- 1 | redo-ifchange CC 2 | . ./CC "$@" 3 | -------------------------------------------------------------------------------- /t/111-example/hello.do: -------------------------------------------------------------------------------- 1 | DEPS="main.o 2 | mystr.o" 3 | redo-ifchange $DEPS 4 | cc -o $3 $DEPS 5 | -------------------------------------------------------------------------------- /t/111-example/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "mystr.h" 3 | 4 | int main() 5 | { 6 | printf("%s\n", mystr); 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /t/111-example/mystr.c: -------------------------------------------------------------------------------- 1 | 2 | #include "mystr.h" 3 | 4 | char *mystr = "Hello, world!"; 5 | -------------------------------------------------------------------------------- /t/111-example/mystr.h: -------------------------------------------------------------------------------- 1 | #ifndef __MYSTR_H 2 | #define __MYSTR_H 3 | 4 | extern char *mystr; 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /t/120-defaults-flat/.gitignore: -------------------------------------------------------------------------------- 1 | c 2 | c.c 3 | c.c.c 4 | c.c.c.b 5 | c.c.c.b.b 6 | d 7 | -------------------------------------------------------------------------------- /t/120-defaults-flat/all.do: -------------------------------------------------------------------------------- 1 | redo-ifchange c d 2 | -------------------------------------------------------------------------------- /t/120-defaults-flat/c.c.c.b.b.a: -------------------------------------------------------------------------------- 1 | chicken 2 | -------------------------------------------------------------------------------- /t/120-defaults-flat/c.do: -------------------------------------------------------------------------------- 1 | redo-ifchange $1.c 2 | echo c.do 3 | cat $1.c 4 | -------------------------------------------------------------------------------- /t/120-defaults-flat/clean.do: -------------------------------------------------------------------------------- 1 | rm -f c c.c c.c.c c.c.c.b c.c.c.b.b d \ 2 | *~ .*~ 3 | -------------------------------------------------------------------------------- /t/120-defaults-flat/default.b.do: -------------------------------------------------------------------------------- 1 | if [ -e "$1.a" -o -e "default${1#$2}.a" ]; then 2 | redo-ifchange "$1.a" 3 | echo a-to-b 4 | cat "$1.a" 5 | else 6 | redo-ifchange "$1.b" 7 | echo b-to-b 8 | cat "$1.b" 9 | fi 10 | ../sleep 1.1 11 | -------------------------------------------------------------------------------- /t/120-defaults-flat/default.c.c.do: -------------------------------------------------------------------------------- 1 | redo-ifchange $1.b 2 | echo b-to-cc 3 | cat $1.b 4 | ../sleep 1.2 5 | -------------------------------------------------------------------------------- /t/120-defaults-flat/default.c.do: -------------------------------------------------------------------------------- 1 | redo-ifchange $1.c 2 | echo c-to-c 3 | cat $1.c 4 | ../sleep 1.3 5 | -------------------------------------------------------------------------------- /t/120-defaults-flat/default.do: -------------------------------------------------------------------------------- 1 | redo-ifchange c 2 | echo default-rule 3 | cat c 4 | ../sleep 1.4 5 | -------------------------------------------------------------------------------- /t/121-defaults-nested/.gitignore: -------------------------------------------------------------------------------- 1 | /a/b/file 2 | /a/b/file.x.y.z 3 | /a/b/file.y.z 4 | /a/b/file.z 5 | /a/d/file 6 | /a/d/file.x.y.z 7 | /a/d/file.y.z 8 | /a/d/file.z 9 | /a/file 10 | /a/file.x.y.z 11 | /a/file.y.z 12 | /a/file.z 13 | /file.x.y.z 14 | /file.z 15 | /file 16 | -------------------------------------------------------------------------------- /t/121-defaults-nested/a/b/default.y.z.do: -------------------------------------------------------------------------------- 1 | echo default.y.z $2 ${1#$2} 2 | -------------------------------------------------------------------------------- /t/121-defaults-nested/a/b/file.x.y.z.do: -------------------------------------------------------------------------------- 1 | echo file $2 ${1#$2} 2 | -------------------------------------------------------------------------------- /t/121-defaults-nested/a/d/default.do: -------------------------------------------------------------------------------- 1 | echo default $2 ${1#$2} 2 | -------------------------------------------------------------------------------- /t/121-defaults-nested/a/default.x.y.z.do: -------------------------------------------------------------------------------- 1 | echo default.x.y.z $2 ${1#$2} 2 | 3 | -------------------------------------------------------------------------------- /t/121-defaults-nested/a/default.z.do: -------------------------------------------------------------------------------- 1 | echo default.z $2 ${1#$2} 2 | -------------------------------------------------------------------------------- /t/121-defaults-nested/all.do: -------------------------------------------------------------------------------- 1 | redo test 2 | -------------------------------------------------------------------------------- /t/121-defaults-nested/clean.do: -------------------------------------------------------------------------------- 1 | exec >&2 2 | find . -name '*~' -exec rm -f {} \; 3 | rm -f a/b/file a/b/file.x.y.z a/b/file.y.z a/b/file.z \ 4 | a/d/file a/d/file.x.y.z a/d/file.y.z a/d/file.z \ 5 | a/file a/file.x.y.z a/file.y.z a/file.z \ 6 | file.x.y.z file.z file 7 | -------------------------------------------------------------------------------- /t/121-defaults-nested/default.do: -------------------------------------------------------------------------------- 1 | echo root $2 ${1#$2} "$(dirname $3)" 2 | -------------------------------------------------------------------------------- /t/121-defaults-nested/test.do: -------------------------------------------------------------------------------- 1 | exec >&2 2 | redo-ifchange \ 3 | file.x.y.z file.z file \ 4 | a/b/file.x.y.z a/b/file.y.z a/b/file.z a/b/file \ 5 | a/d/file.x.y.z a/d/file.y.z a/d/file.z a/d/file 6 | 7 | (cd a/b && redo-ifchange ../file.x.y.z ../file.y.z ../file.z ../file) 8 | 9 | check() 10 | { 11 | if [ "$(cat $1)" != "$2" ]; then 12 | echo "$1 should contain '$2'" 13 | echo " ...got '$(cat $1)'" 14 | exit 44 15 | fi 16 | } 17 | 18 | check file.x.y.z "root file.x.y.z ." 19 | check file.z "root file.z ." 20 | check file "root file ." 21 | 22 | check a/file.x.y.z "default.x.y.z file .x.y.z" 23 | check a/file.y.z "default.z file.y .z" 24 | check a/file.z "default.z file .z" 25 | check a/file "root a/file a" 26 | 27 | check a/b/file.x.y.z "file file.x.y.z" 28 | check a/b/file.y.z "default.y.z file .y.z" 29 | check a/b/file.z "default.z b/file .z" 30 | check a/b/file "root a/b/file a/b" 31 | 32 | check a/d/file.x.y.z "default file.x.y.z" 33 | check a/d/file.y.z "default file.y.z" 34 | check a/d/file.z "default file.z" 35 | check a/d/file "default file" 36 | 37 | -------------------------------------------------------------------------------- /t/122-defaults-parent/.gitignore: -------------------------------------------------------------------------------- 1 | /*.log 2 | -------------------------------------------------------------------------------- /t/122-defaults-parent/all.do: -------------------------------------------------------------------------------- 1 | rm -f x/shouldfail 2 | 3 | log=$PWD/$1.log 4 | 5 | expect_fail() { 6 | local rv=$1 7 | shift 8 | if ("$@") >>$log 2>&1; then 9 | cat "$log" >&2 10 | echo "unexpected success:" "$@" >&2 11 | return $rv 12 | else 13 | return 0 14 | fi 15 | } 16 | 17 | # These should all fail because there is no matching .do file. 18 | # In previous versions of redo, it would accidentally try to use 19 | # $PWD/default.do even for ../path/file, which is incorrect. That 20 | # could cause it to return success accidentally. 21 | 22 | rm -f "$log" 23 | cd inner 24 | expect_fail 11 redo ../x/shouldfail 25 | expect_fail 12 redo-ifchange ../x/shouldfail 26 | 27 | rm -f "$log" 28 | cd ../inner2 29 | expect_fail 21 redo ../x/shouldfail2 30 | expect_fail 22 redo-ifchange ../x/shouldfail2 31 | 32 | exit 0 33 | -------------------------------------------------------------------------------- /t/122-defaults-parent/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *~ .*~ */*~ */.*~ *.tmp */*.tmp x/shouldfail *.log */*.log 2 | 3 | -------------------------------------------------------------------------------- /t/122-defaults-parent/inner/default.do: -------------------------------------------------------------------------------- 1 | echo "inner/default.do: PWD=$PWD '$1' '$2' '$3'" >&2 2 | echo SUSPICIOUS 3 | -------------------------------------------------------------------------------- /t/122-defaults-parent/inner2/default.do: -------------------------------------------------------------------------------- 1 | echo "inner/default.do: PWD=$PWD '$1' '$2' '$3'" >&2 2 | # output file is left empty 3 | -------------------------------------------------------------------------------- /t/122-defaults-parent/x/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zombiezen/redo-rs/177c67efe28f0008dd0a301ef907f1e80f58d913/t/122-defaults-parent/x/file -------------------------------------------------------------------------------- /t/130-mode/.gitignore: -------------------------------------------------------------------------------- 1 | mode1 -------------------------------------------------------------------------------- /t/130-mode/all.do: -------------------------------------------------------------------------------- 1 | umask 0022 2 | redo mode1 3 | MODE=$(../../redo/py -c \ 4 | 'import os; print(oct(os.stat("mode1").st_mode & 0o7777))') 5 | [ "$MODE" = "0644" -o "$MODE" = "0o644" ] || exit 78 6 | -------------------------------------------------------------------------------- /t/130-mode/clean.do: -------------------------------------------------------------------------------- 1 | rm -f mode1 *~ .*~ 2 | -------------------------------------------------------------------------------- /t/130-mode/mode1.do: -------------------------------------------------------------------------------- 1 | echo hello 2 | -------------------------------------------------------------------------------- /t/140-shuffle/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | -------------------------------------------------------------------------------- /t/140-shuffle/all.do: -------------------------------------------------------------------------------- 1 | exec >&2 2 | . ../skip-if-minimal-do.sh 3 | 4 | # Need to repeat this several times, on the off chance that shuffling the 5 | # input happens to give us the same output (probability 1/factorial(9)) 6 | x=0 7 | while [ "$x" -lt 10 ]; do 8 | x=$(($x + 1)) 9 | rm -f out.log 10 | redo --shuffle 1.x 2.x 3.x 4.x 5.x 6.x 7.x 8.x 9.x 11 | sort out.log >sort.log 12 | if ! diff -q out.log sort.log >/dev/null; then 13 | exit 0 14 | fi 15 | echo "retry: #$x" 16 | done 17 | 18 | # still not shuffled? 19 | exit 22 20 | -------------------------------------------------------------------------------- /t/140-shuffle/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *.log *~ .*~ 2 | -------------------------------------------------------------------------------- /t/140-shuffle/default.x.do: -------------------------------------------------------------------------------- 1 | echo $1 >>out.log 2 | -------------------------------------------------------------------------------- /t/141-keep-going/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | -------------------------------------------------------------------------------- /t/141-keep-going/all.do: -------------------------------------------------------------------------------- 1 | exec >&2 2 | . ../skip-if-minimal-do.sh 3 | 4 | rm -f out.log sort.log err.log 5 | redo --keep-going 1.ok 2.fail 3.fail 4.ok 5.ok 6.fail 7.ok >err.log 2>&1 && 6 | exit 11 # expect it to return nonzero due to failures 7 | sort out.log >sort.log 8 | 9 | expect="1 10 | 2 fail 11 | 3 fail 12 | 4 13 | 5 14 | 6 fail 15 | 7" 16 | 17 | [ "$(cat sort.log)" = "$expect" ] || exit 22 18 | -------------------------------------------------------------------------------- /t/141-keep-going/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *.log *~ .*~ 2 | -------------------------------------------------------------------------------- /t/141-keep-going/default.fail.do: -------------------------------------------------------------------------------- 1 | echo $2 fail >>out.log 2 | exit 1 3 | -------------------------------------------------------------------------------- /t/141-keep-going/default.ok.do: -------------------------------------------------------------------------------- 1 | echo $2 >>out.log 2 | -------------------------------------------------------------------------------- /t/200-shell/.gitignore: -------------------------------------------------------------------------------- 1 | /nonshelltest 2 | /chicken.vartest 3 | -------------------------------------------------------------------------------- /t/200-shell/all.do: -------------------------------------------------------------------------------- 1 | redo nonshelltest shelltest vartest 2 | -------------------------------------------------------------------------------- /t/200-shell/clean.do: -------------------------------------------------------------------------------- 1 | rm -f broken nonshelltest shellfile chicken.vartest *~ .*~ 2 | -------------------------------------------------------------------------------- /t/200-shell/default.vartest.do: -------------------------------------------------------------------------------- 1 | : ${PREFIX=not defined} 2 | echo "$PREFIX" 3 | -------------------------------------------------------------------------------- /t/200-shell/nonshelltest.do: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | $a="perly"; 3 | print "hello $a world\n"; 4 | -------------------------------------------------------------------------------- /t/200-shell/shelltest.do: -------------------------------------------------------------------------------- 1 | set +e 2 | export SHELLTEST_QUIET=1 3 | cd .. 4 | ( . ./shelltest.od ) 5 | RV=$? 6 | case $RV in 7 | 40) exit 0 ;; 8 | 42) exit 0 ;; 9 | *) exit 1 ;; 10 | esac 11 | -------------------------------------------------------------------------------- /t/200-shell/vartest.do: -------------------------------------------------------------------------------- 1 | PREFIX=/a/b/c/d/e redo chicken.vartest 2 | read x /dev/null 2>&1 || exit 32 # expected to fail 3 | ! redo-ifchange this-doesnt-exist >/dev/null 2>&1 || exit 33 # expected to fail 4 | redo-ifcreate this-doesnt-exist >/dev/null 2>&1 || exit 34 # expected to pass 5 | 6 | 7 | rm -f fail 8 | ! redo-ifchange fail >/dev/null 2>&1 || exit 44 # expected to fail 9 | 10 | touch fail 11 | ../flush-cache 12 | # since we created this file by hand, fail.do won't run, so it won't fail. 13 | redo-ifchange fail >/dev/null 2>&1 || exit 55 # expected to pass 14 | 15 | # Make sure we don't leave this lying around for future runs, or redo 16 | # might mark it as "manually modified" (since we did!) 17 | rm -f fail 18 | 19 | rm -f maybe-fail 20 | : >want-fail 21 | ! redo-ifchange maybe-fail >/dev/null 2>&1 || exit 66 22 | rm -f want-fail 23 | ../flush-cache 24 | redo-ifchange maybe-fail || exit 67 # failed last time, must retry 25 | : >want-fail 26 | ../flush-cache 27 | redo-ifchange maybe-fail || exit 68 # passed last time, no dep, no redo 28 | rm -f want-fail 29 | -------------------------------------------------------------------------------- /t/201-fail/clean.do: -------------------------------------------------------------------------------- 1 | rm -f fail maybe-fail want-fail *~ .*~ 2 | -------------------------------------------------------------------------------- /t/201-fail/fail.do: -------------------------------------------------------------------------------- 1 | false 2 | -------------------------------------------------------------------------------- /t/201-fail/maybe-fail.do: -------------------------------------------------------------------------------- 1 | [ -e 'want-fail' ] && exit 1 2 | echo x 3 | -------------------------------------------------------------------------------- /t/202-del/.gitignore: -------------------------------------------------------------------------------- 1 | deltest2 2 | /destruct 3 | /x 4 | -------------------------------------------------------------------------------- /t/202-del/all.do: -------------------------------------------------------------------------------- 1 | redo deltest deltest2 deltest3 deltest4 2 | -------------------------------------------------------------------------------- /t/202-del/clean.do: -------------------------------------------------------------------------------- 1 | rm -rf destruct x 2 | rm -f deltest2 *~ .*~ 3 | -------------------------------------------------------------------------------- /t/202-del/default.spam1.do: -------------------------------------------------------------------------------- 1 | rm -rf x 2 | mkdir x 3 | echo 'stdout' 4 | -------------------------------------------------------------------------------- /t/202-del/default.spam2.do: -------------------------------------------------------------------------------- 1 | rm -rf x 2 | mkdir x 3 | echo 'redir' >$3 4 | 5 | -------------------------------------------------------------------------------- /t/202-del/deltest.do: -------------------------------------------------------------------------------- 1 | # remove an empty output file 2 | rm -f $3 3 | -------------------------------------------------------------------------------- /t/202-del/deltest2.do: -------------------------------------------------------------------------------- 1 | # delete a non-empty output file 2 | echo hello 3 | rm -f $3 4 | -------------------------------------------------------------------------------- /t/202-del/deltest3.do: -------------------------------------------------------------------------------- 1 | rm -rf destruct 2 | mkdir destruct 3 | cd destruct 4 | cat >destruct1.do <<-EOF 5 | rm -f *.tmp 6 | echo 'redir' >\$3 7 | EOF 8 | cat >destruct2.do <<-EOF 9 | rm -f *.tmp 10 | echo 'stdout' 11 | EOF 12 | 13 | # deleting unused stdout file is a warning at most 14 | redo destruct1 2>destruct1.log || exit 11 15 | [ "$(cat destruct1)" = "redir" ] || exit 12 16 | 17 | # deleting *used* stdout file may be a fatal mistake, 18 | # but we won't enforce that, since some redo variants 19 | # might be more accepting or use different tmp file 20 | # algorithms. So either the file should be correct, 21 | # or it should be missing. 22 | redo destruct2 2>destruct2.log || : 23 | if [ -e "destruct2" ]; then 24 | [ "$(cat destruct2)" = "stdout" ] || exit 22 25 | fi 26 | -------------------------------------------------------------------------------- /t/202-del/deltest4.do: -------------------------------------------------------------------------------- 1 | rm -rf x 2 | redo x/a.spam2 3 | [ "$(cat x/a.spam2)" = "redir" ] || exit 11 4 | redo x/a.spam2 5 | [ "$(cat x/a.spam2)" = "redir" ] || exit 12 6 | redo x/b.spam2 7 | [ "$(cat x/b.spam2)" = "redir" ] || exit 13 8 | 9 | rm -rf x 10 | redo x/a.spam1 11 | [ "$(cat x/a.spam1)" = "stdout" ] || exit 21 12 | redo x/a.spam1 13 | [ "$(cat x/a.spam1)" = "stdout" ] || exit 22 14 | redo x/b.spam1 15 | [ "$(cat x/b.spam1)" = "stdout" ] || exit 23 16 | -------------------------------------------------------------------------------- /t/203-make/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.out 3 | whichmake 4 | -------------------------------------------------------------------------------- /t/203-make/Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | 3 | x: 4 | +redo x1.out x2.out 5 | 6 | y: 7 | +redo x 8 | +redo y1.out y2.out 9 | -------------------------------------------------------------------------------- /t/203-make/all.do: -------------------------------------------------------------------------------- 1 | exec >&2 2 | redo-ifchange whichmake 3 | 4 | run() { 5 | rm -f *.out 6 | ./whichmake y 7 | 8 | rm -f *.out 9 | ./whichmake -j10 y 10 | 11 | rm -f *.out 12 | redo y 13 | 14 | rm -f *.out 15 | # Capture output to y.log because we know this intentionally generates 16 | # a scary-looking redo warning (overriding the jobserver). 17 | if ! redo -j10 y 2>y.log; then 18 | cat y.log 19 | exit 99 20 | fi 21 | } 22 | 23 | run 24 | . ./wipe-redo.sh 25 | run 26 | -------------------------------------------------------------------------------- /t/203-make/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *~ .*~ *.out whichmake *.log 2 | -------------------------------------------------------------------------------- /t/203-make/default.out.do: -------------------------------------------------------------------------------- 1 | echo $1 >$3 2 | -------------------------------------------------------------------------------- /t/203-make/whichmake.do: -------------------------------------------------------------------------------- 1 | if type gmake >/dev/null 2>/dev/null; then 2 | make=gmake 3 | elif type make >/dev/null 2>/dev/null; then 4 | make=make 5 | else 6 | # No make installed? That's okay, this test 7 | # isn't *that* important. 8 | make=: 9 | fi 10 | 11 | cat >$3 <<-EOF 12 | #!/bin/sh 13 | $make "\$@" 14 | EOF 15 | chmod a+x $3 16 | 17 | -------------------------------------------------------------------------------- /t/203-make/wipe-redo.sh: -------------------------------------------------------------------------------- 1 | vars=$( 2 | env | { 3 | IFS="=" 4 | while read key value; do 5 | if [ "$key" != "${key#REDO}" ] || 6 | [ "$key" != "${key#MAKE}" ]; then 7 | echo "$key" 8 | fi 9 | done 10 | } 11 | ) 12 | echo "Wiping vars:" $vars >&2 13 | unset $vars 14 | -------------------------------------------------------------------------------- /t/203-make/x.do: -------------------------------------------------------------------------------- 1 | ./whichmake x >&2 2 | -------------------------------------------------------------------------------- /t/203-make/y.do: -------------------------------------------------------------------------------- 1 | ./whichmake y >&2 2 | -------------------------------------------------------------------------------- /t/204-makeflags/all.do: -------------------------------------------------------------------------------- 1 | # Make sure we can survive if a process closes all file descriptors, 2 | # including any jobserver file descriptors, as long as they also 3 | # unset MAKEFLAGS. 4 | redo-ifchange ../../redo/py 5 | 6 | # If we leave MAKEFLAGS set, then it's fair game to complain that the 7 | # advertised file descriptors are gone, because GNU make also complains. 8 | # (Although they only warn while we abort. They can't abort so that 9 | # they don't break backward compat, but we have no such constraint, because 10 | # redo has always failed for that case.) 11 | # 12 | # On the other hand, we shouldn't have to unset REDO_CHEATFDS, both for 13 | # backward compatibility, and because REDO_CHEATFDS is undocumented. 14 | # redo should recover silently from that problem. 15 | unset MAKEFLAGS 16 | ../../redo/py ./closefds.py redo noflags 17 | -------------------------------------------------------------------------------- /t/204-makeflags/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *~ .*~ 2 | -------------------------------------------------------------------------------- /t/204-makeflags/closefds.py: -------------------------------------------------------------------------------- 1 | import subprocess, sys, os 2 | 3 | # subprocess.call(close_fds=True) is unfortunately not a good idea, 4 | # because some versions (Debian's python version?) try to close inordinately 5 | # many file descriptors, like 0..1000000, which takes a very long time. 6 | # 7 | # We happen to know that redo doesn't need such huge fd values, so we'll 8 | # just cheat and use a smaller range. 9 | os.closerange(3, 1024) 10 | rv = subprocess.call(sys.argv[1:]) 11 | sys.exit(rv) 12 | 13 | -------------------------------------------------------------------------------- /t/204-makeflags/noflags.do: -------------------------------------------------------------------------------- 1 | true 2 | -------------------------------------------------------------------------------- /t/205-readonly/.gitignore: -------------------------------------------------------------------------------- 1 | /rodir 2 | -------------------------------------------------------------------------------- /t/205-readonly/all.do: -------------------------------------------------------------------------------- 1 | [ -e rodir ] && chmod u+w rodir 2 | [ -e rodir/rwdir ] && chmod u+w rodir/rwdir 3 | rm -rf rodir 4 | mkdir rodir rodir/rwdir 5 | 6 | cd rodir 7 | cat >default.ro1.do <<-EOF 8 | chmod u+w "\$(dirname "\$1")" 9 | echo 'redir' >\$3 10 | EOF 11 | cat >default.ro2.do <<-EOF 12 | chmod u+w "\$(dirname "\$1")" 13 | echo 'stdout' 14 | EOF 15 | 16 | # Check that: 17 | # - redo works when the .do file is in a read-only directory. 18 | # - redo works when the target is in a read-only directory that becomes 19 | # writable only *after* launching the .do script. (For example, the .do 20 | # might mount a new read-write filesystem in an otherwise read-only 21 | # tree.) 22 | chmod a-w . rwdir 23 | redo rwdir/a.ro1 24 | chmod a-w . rwdir 25 | redo rwdir/a.ro2 26 | -------------------------------------------------------------------------------- /t/205-readonly/clean.do: -------------------------------------------------------------------------------- 1 | [ -e rodir ] && chmod u+w rodir 2 | [ -e rodir/rwdir ] && chmod u+w rodir/rwdir 3 | rm -rf rodir 4 | rm -f *~ .*~ 5 | -------------------------------------------------------------------------------- /t/220-ifcreate/.gitignore: -------------------------------------------------------------------------------- 1 | ifcreate?.log 2 | -------------------------------------------------------------------------------- /t/220-ifcreate/all.do: -------------------------------------------------------------------------------- 1 | rm -f exists ifcreate[12] ifcreate[12].log ifcreate[12].dep 2 | . ../skip-if-minimal-do.sh 3 | touch exists 4 | redo-ifcreate exists 2>/dev/null && exit 91 5 | rm exists 6 | redo-ifcreate exists || exit 92 7 | 8 | for d in 1 2; do 9 | redo ifcreate$d 10 | [ "$(wc -l >ifcreate1.log 7 | -------------------------------------------------------------------------------- /t/220-ifcreate/ifcreate2.do: -------------------------------------------------------------------------------- 1 | cd .. 2 | if [ -e 220-ifcreate/ifcreate2.dep ]; then 3 | redo-ifchange 220-ifcreate/ifcreate2.dep 4 | else 5 | redo-ifcreate 220-ifcreate/ifcreate2.dep 6 | fi 7 | echo $$ >>220-ifcreate/ifcreate2.log 8 | -------------------------------------------------------------------------------- /t/250-makedir/.gitignore: -------------------------------------------------------------------------------- 1 | makedir.log 2 | makedir 3 | -------------------------------------------------------------------------------- /t/250-makedir/all.do: -------------------------------------------------------------------------------- 1 | redo makedir2 dirtest/all autosubdir/all 2 | -------------------------------------------------------------------------------- /t/250-makedir/autosubdir/all.do: -------------------------------------------------------------------------------- 1 | rm -rf sub.tmp sub2.tmp sub3.tmp 2 | 3 | redo-ifchange sub.tmp/test.txt 4 | [ -e sub.tmp/test.txt ] || exit 96 5 | 6 | redo-ifchange sub2.tmp/a/b/c/test.txt 7 | [ -e sub2.tmp/a/b/c/test.txt ] || exit 97 8 | 9 | mkdir -p sub3.tmp/a 10 | redo-ifchange sub3.tmp/a/b/c/test.txt 11 | [ -e sub2.tmp/a/b/c/test.txt ] || exit 98 12 | -------------------------------------------------------------------------------- /t/250-makedir/autosubdir/clean.do: -------------------------------------------------------------------------------- 1 | rm -rf *.tmp 2 | rm -f *~ .*~ 3 | -------------------------------------------------------------------------------- /t/250-makedir/autosubdir/default.txt.do: -------------------------------------------------------------------------------- 1 | mkdir -p $(dirname $1) 2 | echo "hello" >$3 3 | -------------------------------------------------------------------------------- /t/250-makedir/clean.do: -------------------------------------------------------------------------------- 1 | redo dirtest/clean autosubdir/clean 2 | rm -rf makedir 3 | rm -f *~ .*~ makedir.log 4 | 5 | -------------------------------------------------------------------------------- /t/250-makedir/dirtest/.gitignore: -------------------------------------------------------------------------------- 1 | log 2 | dir1/stinky 3 | -------------------------------------------------------------------------------- /t/250-makedir/dirtest/all.do: -------------------------------------------------------------------------------- 1 | rm -f log dir1/log dir1/stinky 2 | touch t1.do 3 | ../../flush-cache 4 | redo t1 5 | touch t1.do 6 | ../../flush-cache 7 | redo t1 8 | ../../flush-cache 9 | redo-ifchange t1 10 | C1="$(wc -l t1, c1=$C1, c2=$C2" >&2 15 | exit 55 16 | fi 17 | -------------------------------------------------------------------------------- /t/250-makedir/dirtest/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *~ .*~ dir1/*~ dir1/.*~ dir1/stinky dir1/log log 2 | -------------------------------------------------------------------------------- /t/250-makedir/dirtest/dir1/go.do: -------------------------------------------------------------------------------- 1 | redo-ifchange stinky 2 | echo $$ >>log 3 | -------------------------------------------------------------------------------- /t/250-makedir/dirtest/dir1/stinky.do: -------------------------------------------------------------------------------- 1 | echo "I'm stinky" 2 | -------------------------------------------------------------------------------- /t/250-makedir/dirtest/t1.do: -------------------------------------------------------------------------------- 1 | redo-ifchange dir1/go 2 | echo $$ >>log 3 | -------------------------------------------------------------------------------- /t/250-makedir/makedir.do: -------------------------------------------------------------------------------- 1 | rm -rf "$1" 2 | mkdir $1 3 | echo $$ >>makedir.log 4 | -------------------------------------------------------------------------------- /t/250-makedir/makedir2.do: -------------------------------------------------------------------------------- 1 | rm -f makedir.log 2 | redo makedir 3 | touch makedir/outfile 4 | ../flush-cache 5 | redo-ifchange makedir 6 | COUNT=$(wc -l &2 2 | 3 | a=$(cd fakesub2 && redo-whichdo d/snork) 4 | # if sh doesn't abort after the above, then it found a .do file as expected 5 | 6 | b=$(cat <&2 2 | 3 | a=$(cd fakesub && redo-whichdo ../a/b/x.y.z) 4 | # if sh doesn't abort after the above, then it found a .do file as expected 5 | 6 | # Note: we expect redo-whichdo to return paths relative to $PWD at the time 7 | # it's run, which in this case is fakesub. 8 | # Likely bugs would be to return paths relative to the start dir, the .redo 9 | # dir, the current target dir, the requested target dir, etc. 10 | b=$(cat <&2 2 | 3 | # Testing the search path for non-existent do files is a little tricky. 4 | # We can't be sure where our current directory is, so we don't know how 5 | # far up the stack redo will need to search. 6 | # 7 | # To dodge the problem, let's "cd /" first so that we're testing a target 8 | # relative to a known location (the root directory). 9 | 10 | if [ -e '/default.do' -o \ 11 | -e '/default.z.do' -o \ 12 | -e '/default.y.z.do' ]; then 13 | echo "Weird: /default.*.do exists; can't run this test." 14 | exit 99 15 | fi 16 | 17 | # redo-whichdo *should* fail here, so don't abort the script for that. 18 | set +e 19 | a=$(cd / && redo-whichdo __nonexist/a/x.y.z) 20 | rv=$? 21 | set -e 22 | 23 | if [ "$rv" -eq 0 ]; then 24 | echo "redo-whichdo should return nonzero for a missing .do file." 25 | exit 10 26 | fi 27 | 28 | b=$(cat <>$2.log 4 | -------------------------------------------------------------------------------- /t/350-deps/basic/test.do: -------------------------------------------------------------------------------- 1 | rm -f *.out *.log 2 | 3 | ../../flush-cache 4 | redo-ifchange 1.out 2.out 5 | [ "$(cat 1.log | wc -l)" -eq 1 ] || exit 55 6 | [ "$(cat 2.log | wc -l)" -eq 1 ] || exit 56 7 | ../../flush-cache 8 | touch 1.in 9 | redo-ifchange 1.out 2.out 10 | [ "$(cat 2.log | wc -l)" -eq 1 ] || exit 58 11 | . ../../skip-if-minimal-do.sh 12 | [ "$(cat 1.log | wc -l)" -eq 2 ] || exit 57 13 | 14 | -------------------------------------------------------------------------------- /t/350-deps/broken.do: -------------------------------------------------------------------------------- 1 | false 2 | -------------------------------------------------------------------------------- /t/350-deps/clean.do: -------------------------------------------------------------------------------- 1 | redo basic/clean override/clean 2 | rm -f *~ .*~ *.count t1a overwrite overwrite[123] \ 3 | genfile2 genfile.log static.log 4 | -------------------------------------------------------------------------------- /t/350-deps/doublestatic.do: -------------------------------------------------------------------------------- 1 | rm -f static.log 2 | 3 | redo static1 static2 4 | 5 | touch static.in 6 | ../flush-cache 7 | redo-ifchange static1 static2 8 | 9 | COUNT=$(wc -l >genfile.log 3 | -------------------------------------------------------------------------------- /t/350-deps/gentest.do: -------------------------------------------------------------------------------- 1 | rm -f genfile2 genfile2.do genfile.log 2 | 3 | echo echo hello >genfile2.do 4 | ../flush-cache 5 | redo genfile1 6 | 7 | # this will cause a rebuild: 8 | # genfile1 depends on genfile2 depends on genfile2.do 9 | rm -f genfile2.do 10 | ../flush-cache 11 | redo-ifchange genfile1 12 | 13 | # but genfile2.do was gone last time, so genfile2 no longer depends on it. 14 | # thus, it can be considered up-to-date. Prior versions of redo had a bug 15 | # where the dependency on genfile2.do was never dropped. 16 | ../flush-cache 17 | redo-ifchange genfile1 18 | 19 | COUNT=$(wc -l /dev/null; then 2 | echo "expected broken.do to fail, but it didn't" >&2 3 | exit 44 4 | fi 5 | -------------------------------------------------------------------------------- /t/350-deps/override/.gitignore: -------------------------------------------------------------------------------- 1 | a 2 | b 3 | *.log 4 | stamp 5 | -------------------------------------------------------------------------------- /t/350-deps/override/a.do: -------------------------------------------------------------------------------- 1 | echo hello-a >$3 2 | -------------------------------------------------------------------------------- /t/350-deps/override/all.do: -------------------------------------------------------------------------------- 1 | exec >&2 2 | rm -f a b *.log stamp 3 | 4 | echo 1 >stamp 5 | redo b 6 | [ "$(cat b)" = "hello-a-1-b" ] || exit 11 7 | 8 | ../../flush-cache 9 | echo 2 >stamp 10 | redo-ifchange b 11 | [ "$(cat b)" = "hello-a-1-b" ] || exit 21 # a unchanged; b not redone 12 | 13 | . ../../skip-if-minimal-do.sh 14 | 15 | # Unfortunately the test below depends on the specific wording of the 16 | # "override" warning message, of the form: 17 | # redo: a - you modified it; skipping 18 | # That's because this is specifically a test that the warning message 19 | # gets generated. I added that test because (of course) when we didn't 20 | # test it, the warning message accidentally got broken. Oops. If you 21 | # rephrase the message, you'll have to also change the test. 22 | 23 | ../../flush-cache 24 | echo 3 >stamp 25 | echo over-a >a 26 | redo-ifchange b >$1.log 2>&1 27 | [ "$(cat b)" = "over-a-3-b" ] || exit 31 # a overwritten, b redone 28 | grep "a - " "$1.log" >/dev/null || exit 32 # expected a warning msg 29 | 30 | ../../flush-cache 31 | echo 4 >stamp 32 | redo-ifchange b >$1.log 2>&1 33 | [ "$(cat b)" = "over-a-3-b" ] || exit 41 # a not changed, b not redone 34 | grep "a - " "$1.log" >/dev/null || exit 42 # still expect a warning msg 35 | -------------------------------------------------------------------------------- /t/350-deps/override/b.do: -------------------------------------------------------------------------------- 1 | redo-ifchange a 2 | printf '%s-%s-b\n' "$(cat a)" "$(cat stamp)" >$3 3 | -------------------------------------------------------------------------------- /t/350-deps/override/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *~ .*~ stamp *.log a b 2 | -------------------------------------------------------------------------------- /t/350-deps/overwrite.do: -------------------------------------------------------------------------------- 1 | . ../skip-if-minimal-do.sh 2 | redo overwrite1 2>&1 && exit 55 3 | redo overwrite2 2>&1 && exit 56 4 | redo overwrite3 2>&1 && exit 57 5 | exit 0 6 | -------------------------------------------------------------------------------- /t/350-deps/overwrite1.do: -------------------------------------------------------------------------------- 1 | # this shouldn't be allowed; we're supposed to write to $3, not $1 2 | echo >$1 3 | -------------------------------------------------------------------------------- /t/350-deps/overwrite2.do: -------------------------------------------------------------------------------- 1 | # this shouldn't be allowed; stdout is connected to $3 already, so if we 2 | # replace it *and* write to stdout, we're probably confused. 3 | echo hello world 4 | rm -f $3 5 | echo goodbye world >$3 6 | -------------------------------------------------------------------------------- /t/350-deps/overwrite3.do: -------------------------------------------------------------------------------- 1 | # we don't delete $3 here, we just truncate and overwrite it. But redo 2 | # can detect this by checking the current file position of our stdout when 3 | # we exit, and making sure it equals either 0 or the file size. 4 | # 5 | # If it doesn't, then we accidentally wrote to *both* stdout and a separate 6 | # file, and we should get warned about it. 7 | echo hello world 8 | echo goodbye world >$3 9 | -------------------------------------------------------------------------------- /t/350-deps/static.in: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zombiezen/redo-rs/177c67efe28f0008dd0a301ef907f1e80f58d913/t/350-deps/static.in -------------------------------------------------------------------------------- /t/350-deps/static1.do: -------------------------------------------------------------------------------- 1 | redo-ifchange static.in 2 | echo $$ >>static.log 3 | -------------------------------------------------------------------------------- /t/350-deps/static2.do: -------------------------------------------------------------------------------- 1 | redo-ifchange static.in 2 | echo $$ >>static.log 3 | -------------------------------------------------------------------------------- /t/350-deps/t1a.do: -------------------------------------------------------------------------------- 1 | redo-ifchange t1dep 2 | echo $$ 3 | -------------------------------------------------------------------------------- /t/350-deps/t1dep.do: -------------------------------------------------------------------------------- 1 | # nothing to do 2 | -------------------------------------------------------------------------------- /t/350-deps/t2.do: -------------------------------------------------------------------------------- 1 | echo $$ >>t2.count -------------------------------------------------------------------------------- /t/350-deps/test1.do: -------------------------------------------------------------------------------- 1 | # This may have been leftover from a previous run, when switching 2 | # between "real" redo and minimal/do, so clean it up. 3 | rm -f t1a 4 | 5 | # force-rebuild t1dep 6 | redo t1dep 7 | 8 | if [ -e t1a ]; then 9 | BEFORE="$(cat t1a)" 10 | else 11 | BEFORE= 12 | fi 13 | ../flush-cache 14 | redo-ifchange t1a # it definitely had to rebuild because t1dep changed 15 | AFTER="$(cat t1a)" 16 | if [ "$BEFORE" = "$AFTER" ]; then 17 | echo "t1a was not rebuilt!" >&2 18 | exit 43 19 | fi 20 | -------------------------------------------------------------------------------- /t/350-deps/test2.do: -------------------------------------------------------------------------------- 1 | rm -f t2.count 2 | redo t2 3 | redo t2 4 | OUT=$(cat t2.count | wc -l) 5 | . ../skip-if-minimal-do.sh 6 | if [ "$OUT" -ne 2 ]; then 7 | echo "t2: expected 2" 8 | exit 43 9 | fi 10 | -------------------------------------------------------------------------------- /t/351-deps-forget/.gitignore: -------------------------------------------------------------------------------- 1 | bork 2 | bork.log 3 | sub 4 | sub.log 5 | sub.warn 6 | silent.out 7 | want 8 | -------------------------------------------------------------------------------- /t/351-deps-forget/all.do: -------------------------------------------------------------------------------- 1 | exec >&2 2 | rm -f want silent.out bork bork.log sub sub.log sub.warn 3 | 4 | # Run a command without displaying its output. 5 | # We are intentionally generating some redo errors, and 6 | # we don't want the log to look scary. 7 | # In case we need the output to debug a failed test, 8 | # we leave the most recent command output in silent.out. 9 | silent() { 10 | "$@" >silent.out 2>&1 11 | } 12 | 13 | # like "grep -q", but portable. 14 | qgrep() { 15 | grep "$@" >/dev/null 16 | } 17 | 18 | # Returns true if bork is marked as a redo-source. 19 | is_source() { 20 | redo-sources | qgrep '^bork$' 21 | } 22 | 23 | # Returns true if bork is marked as a redo-target. 24 | is_target() { 25 | redo-targets | qgrep '^bork$' 26 | } 27 | 28 | # Returns true if bork is marked as an out-of-date redo-target. 29 | is_ood() { 30 | redo-ood | qgrep '^bork$' 31 | } 32 | 33 | . ../skip-if-minimal-do.sh 34 | 35 | 36 | # The table for our table-driven test. 37 | # Column meanings are: 38 | # pre: the state of the 'bork' file at test start 39 | # src = 'bork' is a redo-source 40 | # nil = 'bork' is a redo-target that produced nil (ie. a virtual target) 41 | # add = 'bork' is a redo-target that produced a file 42 | # update: the override to perform after 'pre' 43 | # nop = do nothing 44 | # del = delete 'bork', if it exists 45 | # add = create/override a new 'bork' 46 | # post: the behaviour requested from bork.do after 'pre' and 'update' finish 47 | # err = bork.do exits with an error 48 | # nil = bork.do produces nil (ie. a virtual target) 49 | # add = bork.do produces a file 50 | # subran: 'ran' if sub.do is expected to pass, else 'skip' 51 | # ran: 'ran' if bork.do is expected to run at all, else 'skip' 52 | # warn: 'warn' if 'redo bork' is expected to warn about overrides, else 'no' 53 | # src/targ/ood: 1 if bork should show up in source/target/ood output, else 0 54 | truth=" 55 | # File was initially a source file 56 | src nop err skip skip no 1 0 0 57 | src nop nil skip skip no 1 0 0 58 | src nop add skip skip no 1 0 0 59 | 60 | src del err skip ran no 0 0 0 # content deleted 61 | src del nil ran ran no 0 1 0 62 | src del add ran ran no 0 1 0 63 | 64 | src add err ran skip no 1 0 0 # source updated 65 | src add nil ran skip no 1 0 0 66 | src add add ran skip no 1 0 0 67 | 68 | # File was initially a target that produced nil 69 | nil nop err skip ran no 0 0 0 # content forgotten 70 | nil nop nil ran ran no 0 1 0 71 | nil nop add ran ran no 0 1 0 72 | 73 | nil del err skip ran no 0 0 0 # content nonexistent 74 | nil del nil ran ran no 0 1 0 75 | nil del add ran ran no 0 1 0 76 | 77 | nil add err ran skip warn 1 0 0 # content overridden 78 | nil add nil ran skip warn 1 0 0 79 | nil add add ran skip warn 1 0 0 80 | 81 | # File was initially a target that produced output 82 | add nop err skip ran no 0 1 1 # update failed 83 | add nop nil ran ran no 0 1 0 84 | add nop add ran ran no 0 1 0 85 | 86 | add del err skip ran no 0 0 0 # content nonexistent 87 | add del nil ran ran no 0 1 0 88 | add del add ran ran no 0 1 0 89 | 90 | add add err ran skip warn 1 0 0 # content overridden 91 | add add nil ran skip warn 1 0 0 92 | add add add ran skip warn 1 0 0 93 | " 94 | 95 | echo "$truth" | 96 | while read pre update post subran ran warn src targ ood XX; do 97 | [ "$pre" != "" -a "$pre" != "#" ] || continue 98 | 99 | # add some helpful vertical whitespace between rows when 100 | # using 'redo -x' 101 | : 102 | : 103 | : 104 | : 105 | echo "test: $pre $update $post" 106 | rm -f bork 107 | 108 | # Step 1 does the requested 'pre' operation. 109 | : STEP 1 110 | ../flush-cache 111 | case $pre in 112 | src) 113 | # This is a little convoluted because we need to convince 114 | # redo to forget 'bork' may have previously been known as a 115 | # target. To make it work, we have to let redo see the file 116 | # at least once as "should be existing, but doesn't." That 117 | # will mark is as no longer a target. Then we can create the 118 | # file from outside redo. 119 | rm -f bork 120 | echo err >want 121 | # Now redo will ack the nonexistent file, but *not* create 122 | # it, because bork.do will exit with an error. 123 | silent redo-ifchange bork || true 124 | # Make sure redo is really sure the file is not a target 125 | ! is_target || exit 13 126 | # Manually create the source file and ensure redo knows it's 127 | # a source, and hasn't magically turned back into a target. 128 | echo src >>bork 129 | is_source || exit 11 130 | ! is_target || exit 12 131 | ;; 132 | nil) 133 | echo nil >want 134 | redo bork 135 | ! is_source || exit 11 136 | is_target || exit 12 137 | ;; 138 | add) 139 | echo add >want 140 | redo bork 141 | ! is_source || exit 11 142 | is_target || exit 12 143 | ;; 144 | *) exit 90 ;; 145 | esac 146 | 147 | # Step 2 does the requested 'update' operation. 148 | : STEP 2 149 | skip= 150 | case $update in 151 | nop) ;; 152 | del) rm -f bork; skip=1 ;; 153 | add) echo override >>bork; skip=1 ;; 154 | *) exit 91 ;; 155 | esac 156 | 157 | ../flush-cache 158 | if [ -z "$skip" ]; then 159 | silent redo sub 160 | fi 161 | 162 | # Step 3 does the requested 'post' operation. 163 | : STEP 3 164 | ../flush-cache 165 | : >bork.log 166 | : >sub.log 167 | echo "$post" >want 168 | redo-ifchange sub >sub.warn 2>&1 || true 169 | 170 | read blog >$1.log 2 | redo-always 3 | 4 | read want >$3; exit 0 ;; 10 | *) exit 80 ;; 11 | esac 12 | -------------------------------------------------------------------------------- /t/351-deps-forget/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *~ .*~ want bork bork.log sub sub.log sub.warn silent.out 2 | -------------------------------------------------------------------------------- /t/351-deps-forget/sub.do: -------------------------------------------------------------------------------- 1 | redo-ifchange bork 2 | echo sub 3 | echo sub >&2 4 | printf y >>$1.log 5 | -------------------------------------------------------------------------------- /t/355-deps-cyclic/a.do: -------------------------------------------------------------------------------- 1 | redo-ifchange b 2 | -------------------------------------------------------------------------------- /t/355-deps-cyclic/all.do: -------------------------------------------------------------------------------- 1 | # minimal/do doesn't need to "support" cyclic dependencies, because 2 | # they're always a bug in the .do scripts :) 3 | . ../skip-if-minimal-do.sh 4 | 5 | ! redo a >/dev/null 2>&1 || exit 204 6 | -------------------------------------------------------------------------------- /t/355-deps-cyclic/b.do: -------------------------------------------------------------------------------- 1 | redo-ifchange a 2 | -------------------------------------------------------------------------------- /t/355-deps-cyclic/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *~ a b 2 | -------------------------------------------------------------------------------- /t/360-symlinks/.gitignore: -------------------------------------------------------------------------------- 1 | *.ran 2 | *.extra 3 | *.final 4 | a 5 | b 6 | *.[123] 7 | dir 8 | -------------------------------------------------------------------------------- /t/360-symlinks/a.do: -------------------------------------------------------------------------------- 1 | printf x >>a.ran 2 | rm -f dir/$2.1 $2.2 $2.3 $2.final 3 | echo foo >$2.final 4 | ln -s $2.final $2.3 5 | ln -s "$PWD/$2.3" $2.2 6 | ln -s ../$2.2 dir/$2.1 7 | ln -s dir/$2.1 $3 8 | -------------------------------------------------------------------------------- /t/360-symlinks/all.do: -------------------------------------------------------------------------------- 1 | rm -f a a.ran a.final b b.ran *.[123] dir/*.[123] 2 | mkdir -p dir 3 | 4 | reads() { 5 | aold=$aval 6 | bold=$bval 7 | read aval >a.final 42 | ../flush-cache 43 | redo-ifchange b 44 | reads 45 | [ "$aold" != "$aval" ] || exit 14 46 | [ "$bold" != "$bval" ] || exit 114 47 | 48 | # We should also notice if a.final is removed. 49 | # Now a is a "dangling" symlink. 50 | rm -f a.final 51 | ../flush-cache 52 | redo-ifchange b 53 | reads 54 | [ "$aold" != "$aval" ] || exit 15 55 | [ "$bold" != "$bval" ] || exit 115 56 | 57 | # If the symlink becomes no-longer-dangling, that should be dirty too. 58 | echo "splash" >a.final 59 | ../flush-cache 60 | redo-ifchange b 61 | reads 62 | [ "$aold" != "$aval" ] || exit 16 63 | [ "$bold" != "$bval" ] || exit 116 64 | 65 | # We ought to know the difference between a, the symlink, and its target. 66 | # If a is replaced with a.final directly, that's a change. 67 | rm -f a 68 | mv a.final a 69 | ../flush-cache 70 | redo-ifchange b >/dev/null 2>&1 # hide "you changed it" message 71 | reads 72 | [ "$aold" = "$aval" ] || exit 17 # manual override prevented rebuild 73 | [ "$bold" != "$bval" ] || exit 117 74 | -------------------------------------------------------------------------------- /t/360-symlinks/b.do: -------------------------------------------------------------------------------- 1 | printf x >>b.ran 2 | redo-ifchange a 3 | cat a >$3 || : 4 | -------------------------------------------------------------------------------- /t/360-symlinks/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *~ .*~ a b *.extra *.final *.ran dir/*.[123] *.[123] 2 | rm -rf dir 3 | -------------------------------------------------------------------------------- /t/370-logs/.gitignore: -------------------------------------------------------------------------------- 1 | pid 2 | x 3 | y 4 | -------------------------------------------------------------------------------- /t/370-logs/a/b/xlog.do: -------------------------------------------------------------------------------- 1 | read pid <../../pid 2 | 3 | # Test that log retrieval works correctly when run from a different base dir. 4 | redo-log -ru ../../x | grep -q "^$pid x stderr" || exit 45 5 | redo-log -ru ../../x | grep -q "^$pid y stderr" || exit 46 6 | -------------------------------------------------------------------------------- /t/370-logs/all.do: -------------------------------------------------------------------------------- 1 | exec >&2 2 | rm -f x pid 3 | pid=$$ 4 | echo "$pid" >pid 5 | xout=$(redo x) 6 | 7 | [ "$(printf "$xout" | wc -l)" -eq 0 ] || exit 2 8 | 9 | if [ -n "$REDO_LOG" ]; then 10 | # redo has redo-log support enabled, so check that it saves logs. 11 | 12 | # recursive log dump should show both x and y stderr. 13 | redo-log -ru x | grep -q "^$pid x stderr" || exit 10 14 | redo-log -ru x | grep -q "^$pid y stderr" || exit 11 15 | 16 | # stdout captured by redo into the files x and y, *not* to log 17 | redo-log -ru x | grep -q "^$pid x stdout" && exit 20 18 | redo-log -ru x | grep -q "^$pid y stdout" && exit 21 19 | [ "$(cat x)" = "$pid x stdout" ] || exit 22 20 | [ "$(cat y)" = "$pid y stdout" ] || exit 23 21 | 22 | # non-recursive log dump of x should *not* include y 23 | redo-log x | grep -q "^$pid y stdout" && exit 30 24 | redo-log x | grep -q "^$pid y stderr" && exit 31 25 | 26 | redo a/b/xlog 27 | (cd a && redo b/xlog) 28 | 29 | # Test retrieval from a different $PWD. 30 | ( 31 | cd a/b || exit 40 32 | redo-log -ru ../../x | grep -q "^$pid x stderr" || exit 41 33 | redo-log -ru ../../x | grep -q "^$pid y stderr" || exit 42 34 | ) || exit 35 | fi 36 | 37 | # whether or not redo-log is available, redirecting stderr should work. 38 | pid=$$-bork 39 | rm -f x pid 40 | echo "$pid" >pid 41 | out=$(redo x 2>&1) 42 | 43 | # x's stderr should obviously go where we sent it 44 | echo "$out" | grep -q "^$pid x stderr" || exit 50 45 | 46 | # This one is actually tricky: with redo-log, x's call to 'redo y' would 47 | # normally implicitly redirect y's stderr to a new log. redo needs to 48 | # detect that we've already redirected it where we want, and not take it 49 | # away. 50 | echo "$out" | grep -q "^$pid y stderr" || exit 51 51 | 52 | exit 0 53 | -------------------------------------------------------------------------------- /t/370-logs/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *~ .*~ all x y pid 2 | -------------------------------------------------------------------------------- /t/370-logs/x.do: -------------------------------------------------------------------------------- 1 | read pid &2 4 | rm -f y 5 | redo y 6 | -------------------------------------------------------------------------------- /t/370-logs/y.do: -------------------------------------------------------------------------------- 1 | read pid &2 4 | -------------------------------------------------------------------------------- /t/550-chdir/.gitignore: -------------------------------------------------------------------------------- 1 | chdir1 2 | -------------------------------------------------------------------------------- /t/550-chdir/all.do: -------------------------------------------------------------------------------- 1 | . ../skip-if-minimal-do.sh 2 | 3 | rm -f chdir1 4 | redo chdir2 5 | redo chdir3 6 | 7 | ../flush-cache 8 | redo-ifchange chdir3 9 | 10 | rm -f chdir1 11 | ../flush-cache 12 | redo-ifchange chdir3 13 | [ -e chdir1 ] || exit 77 14 | 15 | rm -f chdir1 16 | ../flush-cache 17 | redo-ifchange chdir3 18 | [ -e chdir1 ] || exit 78 19 | -------------------------------------------------------------------------------- /t/550-chdir/chdir1.do: -------------------------------------------------------------------------------- 1 | echo hello 2 | -------------------------------------------------------------------------------- /t/550-chdir/chdir2.do: -------------------------------------------------------------------------------- 1 | # make sure redo-ifchange records the dependency correctly if we chdir 2 | cd .. 3 | redo-ifchange 550-chdir/chdir1 4 | -------------------------------------------------------------------------------- /t/550-chdir/chdir3.do: -------------------------------------------------------------------------------- 1 | # make sure redo-ifchange records the dependency correctly if we chdir 2 | cd .. 3 | redo-ifchange 550-chdir/chdir2 4 | -------------------------------------------------------------------------------- /t/550-chdir/clean.do: -------------------------------------------------------------------------------- 1 | rm -f chdir1 *~ .*~ 2 | -------------------------------------------------------------------------------- /t/640-always/.gitignore: -------------------------------------------------------------------------------- 1 | always1 2 | always1.log 3 | -------------------------------------------------------------------------------- /t/640-always/all.do: -------------------------------------------------------------------------------- 1 | rm -f always1 always1.log 2 | 3 | cd .. 4 | redo 640-always/always1 5 | cd 640-always 6 | [ "$(wc -l >always1.log 2 | echo $$ 3 | cd .. 4 | redo-always 5 | -------------------------------------------------------------------------------- /t/640-always/clean.do: -------------------------------------------------------------------------------- 1 | rm -f always1 always1.log *~ .*~ 2 | 3 | -------------------------------------------------------------------------------- /t/950-curse/.gitignore: -------------------------------------------------------------------------------- 1 | *.n1 2 | *.n2 3 | *.count 4 | *.countall 5 | countall 6 | -------------------------------------------------------------------------------- /t/950-curse/all.do: -------------------------------------------------------------------------------- 1 | rm -f *.n[012] countall 2 | 3 | . ./check-1.sh 4 | redo-ifchange 1.n0 2.n0 3.n0 5 | DEPS=$(./seq 10 | sed 's/$/.n1/') 6 | redo-ifchange $DEPS 7 | . ../skip-if-minimal-do.sh 8 | . ./check-2.sh 9 | -------------------------------------------------------------------------------- /t/950-curse/check-1.sh: -------------------------------------------------------------------------------- 1 | rm -f in.countall out.countall *.count 2 | touch in.countall out.countall 3 | echo x >x.count 4 | -------------------------------------------------------------------------------- /t/950-curse/check-2.sh: -------------------------------------------------------------------------------- 1 | COUNT_IN=$(ls *.count | wc -l) 2 | COUNT_OUT=$(cat *.count | wc -l) 3 | if [ "$COUNT_IN" -ne "$COUNT_OUT" ]; then 4 | echo "expected $COUNT_IN individual writes, got $COUNT_OUT" >&2 5 | exit 42 6 | fi 7 | COUNTALL_IN=$(cat in.countall | wc -l) 8 | COUNTALL_OUT=$(cat out.countall | wc -l) 9 | if [ "$COUNTALL_IN" -ne "$COUNTALL_OUT" ]; then 10 | echo "expected $COUNTALL_IN allwrites, got $COUNTALL_OUT" >&2 11 | exit 43 12 | fi 13 | -------------------------------------------------------------------------------- /t/950-curse/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *~ .*~ *.n0 *.n1 *.n2 *.tmp *.count countall *.countall 2 | 3 | 4 | -------------------------------------------------------------------------------- /t/950-curse/countall.do: -------------------------------------------------------------------------------- 1 | echo $1 >>out.countall 2 | echo hello 3 | -------------------------------------------------------------------------------- /t/950-curse/default.n0.do: -------------------------------------------------------------------------------- 1 | DEPS=$(./seq 10 | sed 's/$/.n1/') 2 | redo-ifchange $DEPS 3 | -------------------------------------------------------------------------------- /t/950-curse/default.n1.do: -------------------------------------------------------------------------------- 1 | DEPS=$(./seq 100 | sed 's/$/.n2/') 2 | redo-ifchange $DEPS 3 | echo n1-$2 4 | -------------------------------------------------------------------------------- /t/950-curse/default.n2.do: -------------------------------------------------------------------------------- 1 | echo n2-$2 2 | echo $2 >>$2.count 3 | echo $2 >>in.countall 4 | 5 | # we deliberately use 'redo' here instead of redo-ifchange, because this *heavily* 6 | # stresses redo's locking when building in parallel. We end up with 100 7 | # different targets that all not only depend on this file, but absolutely must 8 | # acquire the lock on this file, build it atomically, and release the lock. 9 | redo countall 10 | -------------------------------------------------------------------------------- /t/950-curse/seq: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | i=0 3 | while [ "$i" -lt "$1" ]; do 4 | i=$(($i + 1)) 5 | echo $i 6 | done 7 | -------------------------------------------------------------------------------- /t/all.do: -------------------------------------------------------------------------------- 1 | redo-ifchange flush-cache 2 | 3 | # tests that "set -e" works (.do scripts always run with -e set by default) 4 | rm -f 000-set-minus-e/log 5 | redo 000-set-minus-e/all 6 | if [ "$(cat 000-set-minus-e/log)" != "ok" ]; then 7 | echo "FATAL! .do file not run with 'set -e' in effect!" >&2 8 | exit 5 9 | fi 10 | 11 | # builds 1xx*/all to test for basic/dangerous functionality. 12 | # We don't want to run more advanced tests if the basics don't work. 13 | /bin/ls 1[0-9][0-9]*/all.do | 14 | sed 's/\.do$//' | 15 | xargs redo 16 | 110-compile/hello >&2 17 | 18 | # builds most of the rest in parallel 19 | /bin/ls [2-9][0-9][0-9]*/all.do | 20 | sed 's/\.do$//' | 21 | xargs redo 22 | 23 | # builds the tests that require non-parallel execution. 24 | # If tests are written carefully, this should only be things that 25 | # are checking for unnecessary extra rebuilds of some targets, which 26 | # might happen after flush-cache. 27 | # FIXME: a better solution might be to make flush-cache less destructive! 28 | /bin/ls [s][0-9][0-9]*/all.do | 29 | sed 's/\.do$//' | { 30 | while read d; do 31 | redo "$d" 32 | done 33 | } 34 | -------------------------------------------------------------------------------- /t/clean.do: -------------------------------------------------------------------------------- 1 | /bin/ls [0-9s][0-9][0-9]*/clean.do | 2 | sed 's/\.do$//' | 3 | xargs redo 4 | 5 | rm -f broken shellfile shellfail shelltest.warned shelltest.failed shlink \ 6 | *~ .*~ stress.log flush-cache 'symlink path' 7 | rm -rf 'space home dir' 8 | -------------------------------------------------------------------------------- /t/dotparams.od: -------------------------------------------------------------------------------- 1 | # call this as ". ./dotparams.od a b" from shelltest.od 2 | [ "$1" = a ] && [ "$2" = b ] && [ "$#" = 2 ] || warn 115 3 | -------------------------------------------------------------------------------- /t/flush-cache.do: -------------------------------------------------------------------------------- 1 | redo-ifchange ../redo/whichpython $1.in 2 | read py <../redo/whichpython 3 | ( 4 | echo "#!$py" 5 | cat $1.in 6 | ) >$3 7 | chmod a+x $3 8 | -------------------------------------------------------------------------------- /t/flush-cache.in: -------------------------------------------------------------------------------- 1 | import sys, os, sqlite3 2 | 3 | if "DO_BUILT" in os.environ: 4 | sys.exit(0) 5 | 6 | sys.stderr.write("Flushing redo cache...\n") 7 | 8 | db_file = os.path.join(os.environ["REDO_BASE"], ".redo/db.sqlite3") 9 | db = sqlite3.connect(db_file, timeout=5000) 10 | 11 | # This is very (overly) tricky. Every time we flush the cache, we run an 12 | # atomic transaction that subtracts 1 from all checked_runid and 13 | # changed_runid values across the entire system. Then when checking 14 | # dependencies, we can see if changed_runid for a given dependency is 15 | # greater than checked_runid for the target, and their *relative* values 16 | # will still be intact! So if a dependency had been built during the 17 | # current run, it will act as if a *previous* run built the dependency but 18 | # the current target was built even earlier. Meanwhile, checked_runid is 19 | # less than REDO_RUNID, so everything will still need to be rechecked. 20 | # 21 | # A second tricky point is that failed_runid is usually null (unless 22 | # building a given target really did fail last time). (null - 1) is still 23 | # null, so this transaction doesn't change failed_runid at all unless it 24 | # really did fail. 25 | # 26 | # Finally, an even more insane problem is that since we decrement these 27 | # values more than once per run, they end up decreasing fairly rapidly. 28 | # But 0 is special! Some code treats failed_runid==0 as if it were null, 29 | # so when we decrement all the way to zero, we get a spurious test failure. 30 | # To avoid this, we initialize the runid to a very large number at database 31 | # creation time. 32 | db.executescript("pragma synchronous = off;" 33 | "update Files set checked_runid=checked_runid-1, " 34 | " changed_runid=changed_runid-1, " 35 | " failed_runid=failed_runid-1;") 36 | db.commit() 37 | -------------------------------------------------------------------------------- /t/nothing.od: -------------------------------------------------------------------------------- 1 | # this shell script contains no commands. 2 | # it's used for testing the return value of '. ./nothing.od' in shelltest.od. 3 | -------------------------------------------------------------------------------- /t/s60-stamp/.gitignore: -------------------------------------------------------------------------------- 1 | /*.log 2 | /usestamp 3 | /stampy 4 | /inp 5 | /bob 6 | /usestamp2 7 | /a 8 | /b 9 | /ab 10 | /doing_ab 11 | /abc 12 | /doing_abc 13 | -------------------------------------------------------------------------------- /t/s60-stamp/a.do: -------------------------------------------------------------------------------- 1 | redo-always 2 | echo a | redo-stamp 3 | echo a > $3 4 | -------------------------------------------------------------------------------- /t/s60-stamp/ab.do: -------------------------------------------------------------------------------- 1 | redo-ifchange a b 2 | echo "doing ab" >&2 3 | echo ab >$3 4 | touch doing_ab 5 | -------------------------------------------------------------------------------- /t/s60-stamp/abc.do: -------------------------------------------------------------------------------- 1 | redo-ifchange ab c 2 | echo "doing abc" >&2 3 | echo abc >$3 4 | touch doing_abc 5 | -------------------------------------------------------------------------------- /t/s60-stamp/all.do: -------------------------------------------------------------------------------- 1 | redo stamptest 2 | -------------------------------------------------------------------------------- /t/s60-stamp/b.do: -------------------------------------------------------------------------------- 1 | redo-always 2 | echo b | redo-stamp 3 | echo b > $3 4 | -------------------------------------------------------------------------------- /t/s60-stamp/bob.do: -------------------------------------------------------------------------------- 1 | echo $$ 2 | -------------------------------------------------------------------------------- /t/s60-stamp/c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zombiezen/redo-rs/177c67efe28f0008dd0a301ef907f1e80f58d913/t/s60-stamp/c -------------------------------------------------------------------------------- /t/s60-stamp/clean.do: -------------------------------------------------------------------------------- 1 | rm -f *.log usestamp usestamp2 stampy inp bob *~ .*~ a b ab abc doing_ab 2 | -------------------------------------------------------------------------------- /t/s60-stamp/stamptest.do: -------------------------------------------------------------------------------- 1 | rm -f bob stampy usestamp usestamp2 stampy.log usestamp.log usestamp2.log 2 | echo one >inp 3 | 4 | ../flush-cache 5 | redo stampy 6 | [ "$(wc -l inp 46 | ../flush-cache 47 | redo stampy 48 | [ "$(wc -l >stampy.log 2 | redo-ifchange inp bob 3 | cat inp 4 | cd .. 5 | redo-stamp >usestamp.log 3 | cat stampy 4 | -------------------------------------------------------------------------------- /t/s60-stamp/usestamp2.do: -------------------------------------------------------------------------------- 1 | redo-ifchange stampy 2 | echo 2 $$ >>usestamp2.log 3 | cat stampy 4 | -------------------------------------------------------------------------------- /t/skip-if-minimal-do.sh: -------------------------------------------------------------------------------- 1 | if [ -n "$DO_BUILT" ]; then 2 | echo "$REDO_TARGET: skipping: not supported in minimal/do." >&2 3 | exit 0 4 | fi 5 | -------------------------------------------------------------------------------- /t/sleep: -------------------------------------------------------------------------------- 1 | PATH=/bin:/usr/bin 2 | if [ -n "$SLEEP" ]; then 3 | exec sleep "$@" 4 | fi 5 | -------------------------------------------------------------------------------- /t/stress.do: -------------------------------------------------------------------------------- 1 | rm -f $1.log 2 | exec >$1.log 3 | 4 | # This test twiddles the same files over and over, and seems to trigger race conditions 5 | # in redo if run repeatedly with a large redo -j. 6 | for d in $(seq 25); do 7 | echo "stress test: cycle $d" >&2 8 | ./flush-cache 2>&1 9 | redo 950-curse/all 2>&1 || { rv=$?; echo "stress test: log is $1.log" >&2; exit $rv; } 10 | done 11 | -------------------------------------------------------------------------------- /test.do: -------------------------------------------------------------------------------- 1 | # shellcheck shell=sh 2 | exec >&2 3 | redo-always 4 | cargo test 5 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Ross Light 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // SPDX-License-Identifier: Apache-2.0 16 | 17 | use std::env; 18 | use std::path::Path; 19 | use std::process::Command; 20 | 21 | #[test] 22 | fn integration_test() { 23 | let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 24 | let redo_path = Path::new(env!("CARGO_BIN_EXE_redo")); 25 | 26 | let status = clear_redo_env(&mut Command::new(redo_path)) 27 | .current_dir(crate_dir) 28 | .arg(Path::new("redo").join("py")) 29 | .arg(Path::new("redo").join("sh")) 30 | .arg(Path::new("redo").join("whichpython")) 31 | .env("RUST_BACKTRACE", "1") 32 | .spawn() 33 | .expect("could not build prereqs") 34 | .wait() 35 | .expect("could not get exit status"); 36 | assert!(status.success(), "prereq build status = {:?}", status); 37 | 38 | let status = clear_redo_env(&mut Command::new(redo_path)) 39 | .current_dir(crate_dir.join("t")) 40 | .env("RUST_BACKTRACE", "1") 41 | .spawn() 42 | .expect("could not start integration test") 43 | .wait() 44 | .expect("could not get exit status"); 45 | assert!(status.success(), "integration test status = {:?}", status); 46 | } 47 | 48 | fn clear_redo_env(cmd: &mut Command) -> &mut Command { 49 | for (k, _) in env::vars_os() { 50 | if k.to_str() 51 | .map(|k| k.starts_with("REDO_") || k == "REDO" || k == "MAKEFLAGS" || k == "DO_BUILT") 52 | .unwrap_or(false) 53 | { 54 | cmd.env_remove(k); 55 | } 56 | } 57 | cmd 58 | } 59 | --------------------------------------------------------------------------------