├── .gnupg ├── secring.gpg ├── pubring.gpg └── trustdb.gpg ├── web ├── .gitignore ├── todo.md ├── sidebar.md ├── docnav.md ├── faq.md ├── template.html ├── documentation.md ├── building.md ├── flags.md ├── fac-vs-scons.md ├── index.md ├── mkdown.py ├── security.md ├── file-format.md ├── signatures.md ├── style.scss └── fac-with-git-hooks.md ├── upload-website.sh ├── src ├── version.rs ├── lib.rs ├── main.rs ├── git.rs └── build │ └── hashstat.rs ├── tests ├── help.sh ├── assertion-fails.c ├── version.sh ├── separate-git-dir.sh ├── exhaustive-and-strict.sh ├── bench-dependentchains.sh ├── invalid-argument.sh ├── no-fac-file.sh ├── empty-env.sh ├── symlink-creation.sh ├── dependency.sh ├── non-default-depending-on-default.sh ├── empty-facfile.sh ├── two-rules-mkdir-same-dir.sh ├── fac-lock-file.sh ├── generate-facfile-without-specifying-output.sh ├── git-dir.sh ├── implicit-output-in-subdirectory.sh ├── vanishing-directory.sh ├── duplicate-implicit-outputs.sh ├── duplicate-rule.sh ├── readdir.sh ├── git-add-gitignore.sh ├── no-rule-for-explicit-build.sh ├── readdir-subdir.sh ├── factum-filename.sh ├── rebuild-when-needed.sh ├── assertion-fails.sh ├── spinner.c ├── mkdir-not-rerun-when-dir-changes.sh ├── broken-latex.sh ├── ignore-readdir.sh ├── ignore-facfile-not-explicit.sh ├── stdout-handled.sh ├── git-add-missing.sh ├── dependency-chain.sh ├── can-be-built-things-first.sh ├── strict-with-dependency-chain.sh ├── fac-with-git-hooks.sh ├── git-implicit-missing.sh ├── changed-input.sh ├── log-output.sh ├── interrupt.sh ├── readdir-handling.sh ├── getting-started.sh ├── dependency-cycle.sh ├── stdout-with-error-and-log.sh ├── slow-dependencies.sh ├── failing-build-fails.sh ├── missing-output-file.sh ├── missing-input.sh ├── failing-build-of-dependency.sh ├── adding-missing-rule.sh ├── irrelevant-explicit-dependency.sh ├── generating-facfiles.sh ├── git-describe.sh ├── count-build-steps.sh ├── cache-in-home-as-symlink.sh ├── non-default-rules.sh ├── run-ghc.sh ├── quiet-on-success.sh ├── implicit-dependency-removed-and-not-needed.sh ├── persist-after-failure.sh ├── explicit-build-arguments.sh ├── generate-tupfile.sh ├── script-generates-two-facfiles.sh ├── clean.sh ├── no-pointless-rebuild.sh ├── former-dependency-removed.sh ├── git-add.sh ├── ignore-readdir-generated-content.sh ├── implicit-output-with-existing-files.sh ├── one-rule-wait-for-others.sh ├── clean-directory.sh ├── command-fails-but-produces-files.sh ├── facfile-from-failed-command.sh ├── clean-after-rule-change.sh ├── newly-missing-output.sh ├── relative-symlink-path.sh ├── generate-ninja.sh ├── noop-directory.sh ├── strict.sh ├── bench-dependentchains-big.sh ├── fail-on-two-rules-one-output.sh ├── generate-makefile.sh ├── rename-repository.sh ├── edited-cache.sh ├── rebuild-with-new-inputs.sh ├── lost-dependencies.sh ├── cache-added.sh ├── cached-output-changing.sh ├── dependency-makefiles.sh ├── readdir-generated-content.sh ├── rule-creates-git-hook.sh ├── cache-handling.sh ├── estimate-time.sh.skip-this-too-slow ├── nice-error-message-on-missing-file.sh ├── outside-git-repsoitory.sh ├── symlink-handling.sh ├── bad-fac-file.sh ├── codegen.sh ├── subtle-rule-generates-varying-stuff.sh ├── iterable_hash_test.c ├── build-tarball.sh ├── clean-with-git-rules.sh ├── exhaustive.sh ├── listset.c └── getting-started.py ├── bench ├── upload-data.sh ├── download-data.py ├── fac-plots.py ├── independent.py ├── cats.py ├── sleepy.py ├── dependentchains.py ├── plot-benchmark.py ├── hierarchy.py └── bench.py ├── configure.fac ├── generate-version-header.py ├── .github └── workflows │ ├── test.yml │ └── release.yml ├── afl.py ├── ci ├── script.sh └── install.sh ├── .gitignore ├── git.py ├── sparse.py ├── Cargo.toml ├── .travis.yml ├── appveyor.yml ├── README.md ├── configure.py └── run-tests.py /.gnupg/secring.gpg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | *.svg 2 | -------------------------------------------------------------------------------- /.gnupg/pubring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droundy/fac/HEAD/.gnupg/pubring.gpg -------------------------------------------------------------------------------- /.gnupg/trustdb.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/droundy/fac/HEAD/.gnupg/trustdb.gpg -------------------------------------------------------------------------------- /web/todo.md: -------------------------------------------------------------------------------- 1 | # To do list 2 | 3 | 3. Port to macos. 4 | 5 | 3. Port to windows. 6 | -------------------------------------------------------------------------------- /upload-website.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | fac 6 | 7 | rsync -Lv web/* science.oregonstate.edu:public_html/fac 8 | -------------------------------------------------------------------------------- /src/version.rs: -------------------------------------------------------------------------------- 1 | /// The version of fac 2 | pub const VERSION : &str = git_version::git_version!(args = ["--always", "--dirty"], cargo_suffix="-cargo"); 3 | -------------------------------------------------------------------------------- /tests/help.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | ${FAC:-../../fac} --help 10 | 11 | exit 0 12 | -------------------------------------------------------------------------------- /bench/upload-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ev 4 | 5 | tar jcvf bench/fac-bench.tar.bz2 bench/data/ 6 | 7 | scp bench/fac-bench.tar.bz2 science.oregonstate.edu:public_html/ 8 | -------------------------------------------------------------------------------- /bench/download-data.py: -------------------------------------------------------------------------------- 1 | import urllib.request, tarfile 2 | 3 | response = urllib.request.urlopen('http://physics.oregonstate.edu/~roundyd/fac-bench.tar.bz2') 4 | tarfile.open(fileobj = response, mode = 'r|*').extractall() 5 | -------------------------------------------------------------------------------- /configure.fac: -------------------------------------------------------------------------------- 1 | | python3 configure.py > .fac 2 | > .fac 3 | 4 | | python3 web/mkdown.py 5 | > web/index.html 6 | 7 | | cd bench && python3 fac-plots.py > .fac 8 | > bench/.fac 9 | c .pyc 10 | 11 | | python3 bench/download-data.py 12 | -------------------------------------------------------------------------------- /web/sidebar.md: -------------------------------------------------------------------------------- 1 | Fac 2 | 3 | * [home](index.html) 4 | * [documentation](documentation.html) 5 | * [benchmarks](benchmarks.html) 6 | * [faq](faq.html) 7 | * [forum](https://groups.google.com/forum/#!forum/fac-users) 8 | 9 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Fac is a build system. 2 | 3 | #![cfg_attr(feature = "strict", deny(warnings))] 4 | #![cfg_attr(feature = "strict", deny(missing_docs))] 5 | 6 | extern crate clap; 7 | 8 | /// A module with just the version in it. 9 | pub mod version; 10 | pub mod git; 11 | 12 | pub mod build; 13 | -------------------------------------------------------------------------------- /web/docnav.md: -------------------------------------------------------------------------------- 1 | * [getting started](getting-started.html) 2 | * [building fac](building.html) 3 | * [facfile format](file-format.html) 4 | * [command line](flags.html) 5 | * [feature comparison](features.html) 6 | * [security](security.html) 7 | * [internal rust docs](doc/fac/index.html) 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/assertion-fails.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char **argv) { 5 | int i; 6 | printf("This is a test."); 7 | for (i=1;i<10000;i++) { 8 | printf("counting %d...\n", i); 9 | fflush(stdout); 10 | assert(i % 999); 11 | } 12 | return 0; 13 | } 14 | -------------------------------------------------------------------------------- /tests/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | rm -rf $0.dir 8 | mkdir $0.dir 9 | cd $0.dir 10 | 11 | ${FAC:-../../fac} --version 12 | 13 | ${FAC:-../../fac} --version > version 14 | 15 | grep 'fac' version 16 | 17 | git describe --dirty 18 | 19 | grep `git describe --dirty` version 20 | 21 | exit 0 22 | -------------------------------------------------------------------------------- /tests/separate-git-dir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | mkdir git 10 | mkdir working 11 | cd working 12 | 13 | cat > top.fac < foo 15 | EOF 16 | 17 | git init --separate-git-dir ../git/repo.git 18 | git add top.fac 19 | 20 | ${FAC:-../../../fac} 21 | 22 | grep 'good stuff' foo 23 | 24 | exit 0 25 | -------------------------------------------------------------------------------- /tests/exhaustive-and-strict.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | rm -rf $0.dir 8 | mkdir $0.dir 9 | cd $0.dir 10 | 11 | cat > top.fac < foo 13 | EOF 14 | 15 | git init 16 | git add top.fac 17 | 18 | if ${FAC:-../../fac} --exhaustive --strict; then 19 | echo we should not be able to be both exhaustive and strict 20 | exit 1 21 | fi 22 | 23 | exit 0 24 | -------------------------------------------------------------------------------- /tests/bench-dependentchains.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | git init 10 | 11 | python3 ../../bench/dependentchains.py 12 | 13 | ls .git 14 | 15 | ${FAC:-../../fac} 16 | 17 | ${FAC:-../../fac} -v > fac.out 18 | cat fac.out 19 | 20 | if grep gcc fac.out; then 21 | echo we rebuilt when we should not have 22 | exit 1 23 | fi 24 | 25 | exit 0 26 | -------------------------------------------------------------------------------- /tests/invalid-argument.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | rm -rf $0.dir 8 | mkdir $0.dir 9 | cd $0.dir 10 | 11 | git init 12 | 13 | if ${FAC:-../../fac} -z; then 14 | echo fac with an invalid argument should have failed. 15 | exit 1 16 | fi 17 | 18 | if ${FAC:-../../fac} --z; then 19 | echo fac with an invalid argument should have failed. 20 | exit 1 21 | fi 22 | 23 | exit 0 24 | -------------------------------------------------------------------------------- /tests/no-fac-file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | git init 10 | 11 | if ${FAC:-../../fac}; then 12 | echo build should fail with no fac file 13 | exit 1 14 | fi 15 | 16 | cat > my.fac < foo 18 | EOF 19 | 20 | if ${FAC:-../../fac}; then 21 | echo build should fail with fac file not git added 22 | exit 1 23 | fi 24 | 25 | exit 0 26 | -------------------------------------------------------------------------------- /generate-version-header.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import subprocess, os 4 | 5 | try: 6 | name = subprocess.check_output(['git', 'describe', '--dirty']) 7 | except: 8 | name = subprocess.check_output(['git', 'rev-parse', 'HEAD']) 9 | 10 | version = name.decode(encoding='UTF-8')[:-1] 11 | with open("version-identifier.h", "w") as f: 12 | f.write('static const char *version_identifier = "%s";\n' % version) 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /tests/empty-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo" > foo.fac 11 | > foo.fac 12 | C ~/.cache 13 | c .pyc 14 | 15 | | cat foo > baz 16 | C ~/.cache 17 | c .pyc 18 | > baz 19 | < foo 20 | 21 | EOF 22 | 23 | echo goodness > input 24 | 25 | git init 26 | git add top.fac input 27 | 28 | env - ${FAC:-../../fac} 29 | 30 | exit 0 31 | -------------------------------------------------------------------------------- /tests/symlink-creation.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | mkdir subdir1 10 | mkdir subdir2 11 | ln -s subdir1 subdir3 12 | 13 | cat > top.fac < foo 18 | 19 | git init 20 | git add top.fac 21 | 22 | ${FAC:-../../fac} 23 | 24 | cat top.fac.tum 25 | 26 | grep '>' top.fac.tum | grep bar 27 | 28 | grep foo bar 29 | 30 | exit 0 31 | -------------------------------------------------------------------------------- /tests/dependency.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | cat > top.fac < foo 10 | > foo 11 | 12 | | echo bar > bar 13 | > bar 14 | 15 | | cat foo bar > foobar 16 | > foobar 17 | < foo 18 | < bar 19 | EOF 20 | 21 | git init 22 | git add top.fac 23 | 24 | ${FAC:-../../fac} 25 | 26 | grep foo foo 27 | grep bar bar 28 | grep foo foobar 29 | grep bar foobar 30 | 31 | exit 0 32 | -------------------------------------------------------------------------------- /tests/non-default-depending-on-default.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo 11 | > foo 12 | 13 | ? cat foo > bar 14 | > bar 15 | < foo 16 | 17 | | cat foo > ugg 18 | > ugg 19 | < foo 20 | EOF 21 | 22 | git init 23 | git add top.fac 24 | 25 | ${FAC:-../../fac} 26 | 27 | grep foo foo 28 | grep foo ugg 29 | 30 | test ! -e bar 31 | 32 | exit 0 33 | -------------------------------------------------------------------------------- /tests/empty-facfile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | rm -rf $0.dir 8 | mkdir $0.dir 9 | cd $0.dir 10 | 11 | cat > empty.fac < foo.fac < foo 17 | EOF 18 | 19 | git init 20 | git add foo.fac empty.fac 21 | 22 | ${FAC:-../../fac} 23 | 24 | ${FAC:-../../fac} -c 25 | 26 | ${FAC:-../../fac} --exhaustive 27 | 28 | exit 0 29 | -------------------------------------------------------------------------------- /tests/two-rules-mkdir-same-dir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo/hello 11 | 12 | | mkdir -p foo && echo goodbye > foo/goodbye 13 | EOF 14 | 15 | git init 16 | git add top.fac 17 | 18 | ${FAC:-../../fac} 19 | 20 | rm -rf foo 21 | 22 | ${FAC:-../../fac} foo/goodbye 23 | 24 | rm -rf foo 25 | 26 | ${FAC:-../../fac} foo/hello 27 | 28 | exit 0 29 | -------------------------------------------------------------------------------- /tests/fac-lock-file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > my.fac < good 11 | EOF 12 | 13 | git init 14 | git add my.fac 15 | 16 | ${FAC:-../../fac} & 17 | 18 | sleep 1 19 | 20 | if ${FAC:-../../fac} &> output; then 21 | echo should have failed due to lock 22 | cat output 23 | exit 1 24 | fi 25 | 26 | cat output 27 | grep git/fac-lock output 28 | 29 | exit 0 30 | -------------------------------------------------------------------------------- /tests/generate-facfile-without-specifying-output.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > my.fac < foo.fac 11 | EOF 12 | 13 | git init 14 | git add my.fac 15 | 16 | ${FAC:-../../fac} 17 | 18 | cat > my.fac < foo.fac 20 | > foo.fac 21 | EOF 22 | 23 | if ${FAC:-../../fac}; then 24 | echo should have crashed parsing foo.fac 25 | exit 1 26 | fi 27 | 28 | 29 | exit 0 30 | -------------------------------------------------------------------------------- /tests/git-dir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | export GIT_DIR=`pwd`/git 10 | export GIT_WORK_TREE=`pwd`/working 11 | 12 | mkdir git 13 | mkdir working 14 | cd working 15 | 16 | cat > top.fac < foo 18 | EOF 19 | 20 | git init 21 | git add top.fac 22 | 23 | git rev-parse --show-toplevel 24 | git rev-parse --git-dir 25 | 26 | ${FAC:-../../../fac} 27 | 28 | grep 'good stuff' foo 29 | 30 | exit 0 31 | -------------------------------------------------------------------------------- /tests/implicit-output-in-subdirectory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | mkdir subdir 10 | 11 | cat > subdir/foo.fac < foo 13 | 14 | | echo bar > bar 15 | 16 | | cat foo bar > baz 17 | < bar 18 | < foo 19 | EOF 20 | 21 | git init 22 | git add subdir/foo.fac 23 | 24 | ${FAC:-../../fac} 25 | 26 | grep foo subdir/foo 27 | grep bar subdir/bar 28 | grep foo subdir/baz 29 | grep bar subdir/baz 30 | 31 | exit 0 32 | -------------------------------------------------------------------------------- /tests/vanishing-directory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | git init 10 | 11 | cat > build.fac < bar 13 | EOF 14 | 15 | git add build.fac 16 | 17 | ${FAC:-../../fac} 18 | 19 | grep bar bar 20 | 21 | cat build.fac.tum 22 | 23 | if grep '> foo-directory' build.fac.tum; then 24 | echo we did not actually create foo-directory 25 | exit 1 26 | fi 27 | 28 | exit 0 29 | -------------------------------------------------------------------------------- /tests/duplicate-implicit-outputs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foobar && echo baz > baz 11 | > baz 12 | 13 | | echo bar > foobar && echo foo > foo 14 | > foo 15 | EOF 16 | 17 | git init 18 | git add top.fac 19 | 20 | if ${FAC:-../../fac} > fac.out 2>&1; then 21 | cat fac.out 22 | echo This should not have passed 23 | exit 1 24 | fi 25 | cat fac.out 26 | grep 'same output' fac.out 27 | 28 | exit 0 29 | -------------------------------------------------------------------------------- /tests/duplicate-rule.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | echo foo > foo 10 | echo bar > bar 11 | 12 | cat > top.fac < foobar 14 | > foobar 15 | 16 | | echo foo > foobar 17 | > foobar 18 | EOF 19 | 20 | git init 21 | git add top.fac 22 | 23 | if ${FAC:-../../fac} > fac.out 2>&1; then 24 | cat fac.out 25 | echo This should not have passed 26 | exit 1 27 | fi 28 | cat fac.out 29 | grep -i 'duplicate rule' fac.out 30 | 31 | exit 0 32 | -------------------------------------------------------------------------------- /tests/readdir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < messages 11 | > messages 12 | EOF 13 | 14 | touch foo.message 15 | 16 | git init 17 | git add top.fac foo.message 18 | 19 | ${FAC:-../../fac} 20 | 21 | ls -lhd . 22 | 23 | sleep 2 24 | 25 | grep foo.message messages 26 | 27 | touch bar.message 28 | ${FAC:-../../fac} 29 | 30 | ls -lhd . 31 | 32 | grep foo.message messages 33 | grep bar.message messages 34 | 35 | exit 0 36 | -------------------------------------------------------------------------------- /tests/git-add-gitignore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | rm -rf $0.dir 8 | mkdir $0.dir 9 | cd $0.dir 10 | 11 | cat > top.fac < .gitignore < input.o 20 | 21 | git init 22 | git add top.fac .gitignore 23 | 24 | if ${FAC:-../../fac} --git-add; then 25 | echo this should have failed due to not adding input.o 26 | exit 1 27 | fi 28 | 29 | git add -f input.o 30 | 31 | ${FAC:-../../fac} 32 | 33 | exit 0 34 | -------------------------------------------------------------------------------- /tests/no-rule-for-explicit-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > my.fac < foo 11 | EOF 12 | 13 | git init 14 | git add my.fac 15 | 16 | if ${FAC:-../../fac} bar; then 17 | echo build should fail no rule to build bar 18 | exit 1 19 | fi 20 | 21 | if ${FAC:-../../fac} foo; then 22 | echo build should fail no rule to build foo 23 | exit 1 24 | fi 25 | 26 | ${FAC:-../../fac} 27 | 28 | ${FAC:-../../fac} foo 29 | 30 | grep foo foo 31 | 32 | exit 0 33 | -------------------------------------------------------------------------------- /tests/readdir-subdir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < contents 11 | EOF 12 | 13 | git init 14 | git add top.fac 15 | 16 | mkdir subdir 17 | touch subdir/hello 18 | git add subdir/hello 19 | 20 | ${FAC:-../../fac} 21 | 22 | cat top.fac.tum 23 | 24 | grep hello contents 25 | 26 | sleep 2 27 | 28 | touch subdir/goodbye 29 | 30 | ${FAC:-../../fac} 31 | 32 | ls -lhd . 33 | 34 | grep hello contents 35 | grep goodbye contents 36 | 37 | exit 0 38 | -------------------------------------------------------------------------------- /tests/factum-filename.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < .fac 13 | 14 | EOF 15 | 16 | cat > configure.sh < foo" > .fac 18 | EOF 19 | 20 | git init 21 | git add top.fac configure.sh 22 | 23 | ${FAC:-../../fac} -vvv 24 | 25 | ls -a 26 | 27 | cat top.fac.tum 28 | 29 | cat .fac.tum 30 | 31 | ${FAC:-../../fac} -c 32 | 33 | ls -a 34 | 35 | test ! -e top.fac.tum 36 | 37 | test ! -e .fac.tum 38 | 39 | exit 0 40 | -------------------------------------------------------------------------------- /tests/rebuild-when-needed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | echo foo > foo 10 | echo bar > bar 11 | 12 | cat > top.fac < bar 14 | > bar 15 | < foo 16 | 17 | | cat input > foo 18 | > foo 19 | 20 | EOF 21 | 22 | echo good > input 23 | 24 | git init 25 | git add top.fac input 26 | 27 | ${FAC:-../../fac} -v 28 | 29 | grep good foo 30 | grep good bar 31 | 32 | echo newer > input 33 | 34 | ${FAC:-../../fac} 35 | 36 | grep newer foo 37 | grep newer bar 38 | 39 | exit 0 40 | -------------------------------------------------------------------------------- /afl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from __future__ import print_function 3 | 4 | import os 5 | 6 | assert not os.system('./fac fac-afl') 7 | 8 | assert not os.system('./fac-afl fac-afl') 9 | 10 | os.system('mkdir -p tests/afl-input') 11 | 12 | with open('tests/afl-input/small.fac', 'w') as f: 13 | f.write('''| echo good 14 | > out1 15 | > out2 16 | < in1 17 | < in2 18 | < in3 19 | C /usr 20 | c .fun 21 | 22 | # comment 23 | 24 | | echo bad 25 | ''') 26 | 27 | os.system('afl-fuzz -i tests/afl-input/ -o tests/afl-output -- ./fac-afl --parse-only @@') 28 | -------------------------------------------------------------------------------- /tests/assertion-fails.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | # This test ensures that we can count builds properly to figure out if 8 | # we have built everything correctly. 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | cat > top.fac < foo 2>&1 16 | > foo 17 | 18 | EOF 19 | 20 | git init 21 | git add top.fac 22 | 23 | if ${FAC:-../../fac} --log-ouput log > fac.out; then 24 | cat fac.out 25 | echo this should have failed 26 | exit 1 27 | fi 28 | cat fac.out 29 | 30 | exit 0 31 | -------------------------------------------------------------------------------- /tests/spinner.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char ** argv) { 5 | clock_t ticks = 1*CLOCKS_PER_SEC; 6 | if (argc == 2) { 7 | double seconds = 1; 8 | sscanf(argv[1], "%lg", &seconds); 9 | ticks = seconds*CLOCKS_PER_SEC; 10 | } 11 | printf("Starting timing for %lg seconds...\n", ticks/(double)CLOCKS_PER_SEC); 12 | double foo = 0; 13 | while (clock() < ticks) { 14 | for (int i=0;i<1000000;i++) { 15 | foo += 1; 16 | } 17 | } 18 | printf("All done spinning! %g\n", foo); 19 | return 0; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /bench/fac-plots.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from __future__ import print_function 3 | 4 | try: 5 | import matplotlib 6 | except: 7 | print('# not generating plots due to lack of matplotlib') 8 | exit(0) 9 | 10 | import cats 11 | import hierarchy 12 | import dependentchains 13 | import sleepy 14 | import independent 15 | 16 | modules = [dependentchains, cats, hierarchy, independent] 17 | 18 | for mod in modules: 19 | for verb in mod.verbs: 20 | print('| python3 plot-benchmark.py %s %s' % (mod.name, verb)) 21 | print('< data') 22 | print('c .pyc') 23 | -------------------------------------------------------------------------------- /tests/mkdir-not-rerun-when-dir-changes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < top.fac < foo.tex < top.fac < messages 11 | > messages 12 | EOF 13 | 14 | touch foo.message 15 | 16 | git init 17 | git add top.fac foo.message 18 | 19 | ${FAC:-../../fac} 20 | 21 | ls -lhd . 22 | 23 | sleep 2 24 | 25 | grep foo.message messages 26 | 27 | touch bar.message 28 | ${FAC:-../../fac} 29 | 30 | ls -lhd . 31 | 32 | grep foo.message messages 33 | if grep bar.message messages; then 34 | echo should not have rerun for '|' rule 35 | exit 1 36 | fi 37 | 38 | exit 0 39 | -------------------------------------------------------------------------------- /tests/ignore-facfile-not-explicit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo" > generated.fac 11 | EOF 12 | 13 | git init 14 | git add top.fac 15 | 16 | ${FAC:-../../fac} 17 | 18 | grep foo generated.fac 19 | 20 | if grep foo foo; then 21 | echo there should be no foo 22 | exit 1 23 | fi 24 | 25 | cat > top.fac < foo" > generated.fac 27 | > generated.fac 28 | EOF 29 | 30 | ${FAC:-../../fac} 31 | 32 | # now the generated rules should be run! 33 | grep foo foo 34 | 35 | exit 0 36 | -------------------------------------------------------------------------------- /tests/stdout-handled.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | # This test ensures that we can count builds properly to figure out if 8 | # we have built everything correctly. 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | mkdir subdir 15 | 16 | cat > top.fac < foo 19 | 20 | EOF 21 | 22 | echo done > message 23 | 24 | git init 25 | git add top.fac message 26 | 27 | if ${FAC:-../../fac} > fac.out; then 28 | cat fac.out 29 | echo this should have failed 30 | exit 1 31 | fi 32 | cat fac.out 33 | 34 | grep done fac.out 35 | 36 | exit 0 37 | -------------------------------------------------------------------------------- /ci/script.sh: -------------------------------------------------------------------------------- 1 | # This script takes care of testing your crate 2 | 3 | set -ex 4 | 5 | main() { 6 | git --version 7 | pwd 8 | git pull --tags 9 | git describe --dirty || true 10 | git rev-parse HEAD 11 | 12 | cargo build --target $TARGET 13 | cargo build --target $TARGET --release 14 | 15 | if [ ! -z $DISABLE_TESTS ]; then 16 | return 17 | fi 18 | 19 | cargo test --target $TARGET 20 | cargo test --target $TARGET --release 21 | 22 | # cross run --target $TARGET 23 | # cross run --target $TARGET --release 24 | } 25 | 26 | if [ -z $TRAVIS_TAG ]; then 27 | main 28 | fi 29 | -------------------------------------------------------------------------------- /tests/git-add-missing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | echo foo > foo 10 | echo bar > bar 11 | 12 | git init 13 | git add foo 14 | 15 | cat > top.fac < foobar 17 | > foobar 18 | < foo 19 | < bar 20 | EOF 21 | git add top.fac 22 | 23 | if ${FAC:-../../fac} > fac.out; then 24 | cat fac.out 25 | echo This should not have passed 26 | exit 1 27 | fi 28 | cat fac.out 29 | 30 | grep 'error: add .*bar.* to git, which is required for .*foobar' fac.out 31 | 32 | rm -f foobar 33 | git add bar 34 | 35 | ${FAC:-../../fac} 36 | 37 | exit 0 38 | -------------------------------------------------------------------------------- /tests/dependency-chain.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | cat > top.fac < foo 10 | > foo 11 | 12 | | cat foo > bar 13 | > bar 14 | < foo 15 | 16 | | cat bar > baz 17 | > baz 18 | < bar 19 | EOF 20 | 21 | git init 22 | git add top.fac 23 | 24 | ${FAC:-../../fac} -v 25 | 26 | grep foo foo 27 | grep foo bar 28 | grep foo baz 29 | 30 | rm foo bar baz 31 | 32 | ${FAC:-../../fac} -v 33 | 34 | grep foo foo 35 | grep foo bar 36 | grep foo baz 37 | 38 | rm foo bar 39 | 40 | ${FAC:-../../fac} -v 41 | 42 | grep foo foo 43 | grep foo bar 44 | grep foo baz 45 | 46 | exit 0 47 | -------------------------------------------------------------------------------- /tests/can-be-built-things-first.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | # This test ensures that we can count builds properly to figure out if 8 | # we have built everything correctly. 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | cat > top.fac < foo 16 | 17 | | echo bar > bar 18 | 19 | | echo zoo > zoo 20 | 21 | | cat foo bar zoo > all 22 | < foo 23 | < bar 24 | < zoo 25 | 26 | EOF 27 | 28 | git init 29 | git add top.fac 30 | 31 | ${FAC:-../../fac} -v 32 | 33 | grep foo foo 34 | grep bar bar 35 | grep zoo zoo 36 | 37 | grep foo all 38 | grep bar all 39 | grep zoo all 40 | 41 | exit 0 42 | -------------------------------------------------------------------------------- /tests/strict-with-dependency-chain.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | # This test shows that if a file is created by a rule that generates a 8 | # facfile, then any rules in that facfile will implicitly depend on 9 | # the file being created. 10 | 11 | rm -rf $0.dir 12 | mkdir $0.dir 13 | cd $0.dir 14 | 15 | cat > top.fac < good 17 | > facfile.fac 18 | EOF 19 | 20 | cat > facfile < foo 10 | echo bar > bar 11 | 12 | git init 13 | git add foo 14 | 15 | cat > top.fac < foobar 17 | > foobar 18 | EOF 19 | 20 | git init 21 | git add top.fac 22 | 23 | if ${FAC:-../../fac} > fac.out; then 24 | cat fac.out 25 | echo This should not have passed 26 | exit 1 27 | fi 28 | cat fac.out 29 | 30 | # check that it prints the right file names 31 | grep 'error: .*bar.* should be in git for .*foobar' fac.out 32 | 33 | rm -f foobar 34 | git add bar 35 | 36 | ${FAC:-../../fac} 37 | 38 | exit 0 39 | -------------------------------------------------------------------------------- /tests/changed-input.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo 11 | > foo 12 | EOF 13 | 14 | cat > script.sh < input 19 | 20 | git init 21 | git add top.fac input script.sh 22 | 23 | ${FAC:-../../fac} 24 | 25 | grep foo foo 26 | 27 | grep input top.fac.tum 28 | 29 | echo bars > input 30 | 31 | ${FAC:-../../fac} 32 | 33 | grep bar foo 34 | 35 | cat > script.sh < input2 41 | 42 | ${FAC:-../../fac} 43 | 44 | grep baz foo 45 | 46 | 47 | exit 0 48 | -------------------------------------------------------------------------------- /tests/log-output.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo 12 | > foo 13 | 14 | EOF 15 | 16 | git init 17 | git add top.fac 18 | 19 | ${FAC:-../../fac} --log-output log 20 | 21 | grep foo foo 22 | 23 | ls -l log 24 | 25 | grep working log/foo 26 | cat > top.fac < test/foo 29 | > test/foo 30 | 31 | EOF 32 | 33 | cat top.fac.tum 34 | 35 | ${FAC:-../../fac} --log-output log 36 | 37 | grep foo test/foo 38 | 39 | ls -l log 40 | 41 | grep nice log/test_foo 42 | cat > top.fac < top.fac < input 20 | 21 | git init 22 | git add top.fac input 23 | 24 | ${FAC:-../../fac} -v & 25 | ID=$! 26 | 27 | sleep 1 28 | 29 | kill -s SIGINT $ID 30 | 31 | sleep 2 32 | echo done killing? 33 | 34 | ls -trlh 35 | 36 | if grep input output; then 37 | echo this should have been deleted when code was interrupted 38 | exit 1 39 | fi 40 | 41 | ${FAC:-../../fac} 42 | 43 | exit 0 44 | -------------------------------------------------------------------------------- /tests/readdir-handling.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo 11 | > foo 12 | EOF 13 | 14 | echo hello > hello 15 | 16 | git init 17 | git add top.fac 18 | 19 | ${FAC:-../../fac} 20 | 21 | cat foo 22 | 23 | grep hello foo 24 | grep top.fac foo 25 | 26 | ${FAC:-../../fac} 27 | 28 | cat foo 29 | 30 | grep hello foo 31 | grep top.fac foo 32 | grep foo foo 33 | 34 | sleep 1 35 | 36 | echo goodbye > goodbye 37 | 38 | ${FAC:-../../fac} -v 39 | 40 | cat foo 41 | 42 | grep hello foo 43 | grep top.fac foo 44 | grep foo foo 45 | grep goodbye foo 46 | grep top.fac.tum foo 47 | 48 | exit 0 49 | -------------------------------------------------------------------------------- /web/faq.md: -------------------------------------------------------------------------------- 1 | # Fac Faq 2 | 3 | $docnav 4 | 5 | ## Why is it called fac? 6 | 7 | Fac 8 | 9 | *Fac* is the Latin imperative form of the verb *facere*, which means 10 | "to do" or "to make." Thus when you type *fac* you are saying "Do 11 | it!" or "Make it!" which is basically what you mean. 12 | 13 | There are a number of English words that derive from *facere*: 14 | 15 | * Factory 16 | * Facile or Facilitate 17 | * Manufacture 18 | * Facsimile ("make similar") 19 | * Fact (which originally meant something more like "feat") 20 | * -ify, the suffix that shows up in "clarify", "ossify" or "liquify" 21 | meaning "to make" or "to become" 22 | -------------------------------------------------------------------------------- /tests/getting-started.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | # This test ensures that the getting-started tutorial actually works 8 | # right. 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | cp ${FAC:-../../fac} fac 15 | 16 | if ! which pdflatex; then 17 | echo there is no latex 18 | exit 137 19 | fi 20 | 21 | if ! test -f /usr/share/dict/words; then 22 | echo there is no /usr/share/dict/words 23 | exit 137 24 | fi 25 | 26 | # Setting the PATH in the following ensures that we call our 27 | # newly-built fac, rather than one that is already installed. 28 | PATH=`pwd`:$PATH python3 ../getting-started.py ../../web/getting-started.md 29 | -------------------------------------------------------------------------------- /tests/dependency-cycle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | # This test ensures that we can count builds properly to figure out if 8 | # we have built everything correctly. 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | cat > top.fac < foo 16 | > foo 17 | < bar 18 | 19 | | cat foo > baz 20 | > baz 21 | < foo 22 | 23 | | cat baz > bar 24 | < baz 25 | > bar 26 | 27 | EOF 28 | 29 | git init 30 | git add top.fac 31 | 32 | if ${FAC:-../../fac} foo; then 33 | echo this should fail as a cycle 34 | exit 1 35 | fi 36 | 37 | if ${FAC:-../../fac}; then 38 | echo this should fail as a cycle 39 | exit 1 40 | fi 41 | 42 | exit 0 43 | -------------------------------------------------------------------------------- /tests/stdout-with-error-and-log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | # This test ensures that we can count builds properly to figure out if 8 | # we have built everything correctly. 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | mkdir subdir 15 | 16 | cat > top.fac < foo 19 | 20 | EOF 21 | 22 | echo done > message 23 | 24 | git init 25 | git add top.fac message 26 | 27 | if ${FAC:-../../fac} --log-output log > fac.out; then 28 | cat fac.out 29 | echo this should have failed 30 | exit 1 31 | fi 32 | cat fac.out 33 | 34 | ls log 35 | 36 | cat log/foo 37 | 38 | grep done fac.out 39 | 40 | exit 0 41 | -------------------------------------------------------------------------------- /tests/slow-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | rm -rf $0.dir 8 | mkdir $0.dir 9 | cd $0.dir 10 | 11 | # This test illustrates a bug in which we don't wait to see if a file 12 | # had been created by a rule prior to concluding that it should have 13 | # been in git. 14 | 15 | cat > top.fac < f1 && sleep 1 17 | | date > f2 && sleep 1 18 | | date > f3 && sleep 1 19 | | date > f4 && sleep 1 20 | | date > f5 && sleep 1 21 | | date > f6 && sleep 1 22 | | ls -l > listing 23 | | date > f7 && sleep 1 24 | | date > f8 && sleep 1 25 | | date > f9 && sleep 1 26 | EOF 27 | 28 | git init 29 | git add top.fac 30 | 31 | ${FAC:-../../fac} -j12 32 | 33 | exit 0 34 | -------------------------------------------------------------------------------- /tests/failing-build-fails.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | echo foo > foo 10 | echo bar > bar 11 | 12 | cat > top.fac < foobar 15 | EOF 16 | 17 | git init 18 | git add top.fac 19 | 20 | if ${FAC:-../../fac} > fac.out; then 21 | cat fac.out 22 | echo This should not have passed 23 | exit 1 24 | fi 25 | cat fac.out 26 | grep 'build failed: f' fac.out 27 | 28 | cat > top.fac < foobar 30 | > foobar 31 | EOF 32 | 33 | if ${FAC:-../../fac} > fac.out; then 34 | cat fac.out 35 | echo good 36 | else 37 | cat fac.out 38 | echo This should have passed 39 | exit 1 40 | fi 41 | 42 | exit 0 43 | -------------------------------------------------------------------------------- /tests/missing-output-file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo 11 | > foo 12 | > bar 13 | 14 | | cat bar > ugly 15 | < bar 16 | > ugly 17 | EOF 18 | 19 | git init 20 | git add top.fac 21 | 22 | if ${FAC:-../../fac} > fac.out 2>&1; then 23 | cat fac.out 24 | echo Bilge was okay. That is not good. 25 | exit 1 26 | else 27 | cat fac.out 28 | echo Bilge failed as it ought. 29 | fi 30 | 31 | if grep 'build failed' fac.out | grep ugly; then 32 | echo we should not have attempted to build ugly in the first place 33 | exit 1 34 | fi 35 | 36 | grep 'build failed' fac.out | grep bar 37 | 38 | exit 0 39 | -------------------------------------------------------------------------------- /tests/missing-input.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo 11 | > foo 12 | 13 | | cat bar > baz 14 | > baz 15 | < bar 16 | EOF 17 | 18 | git init 19 | git add top.fac 20 | 21 | if ${FAC:-../../fac} > fac.out 2>&1; then 22 | cat fac.out 23 | echo Bilge was okay. That is not good. 24 | exit 1 25 | else 26 | cat fac.out 27 | echo Bilge failed as it ought. 28 | fi 29 | 30 | grep 'missing file .*bar' fac.out 31 | grep 'missing file .*bar' fac.out | grep baz 32 | 33 | if grep 'cat: bar' fac.out; then 34 | echo we should not have attempted this build in the first place 35 | exit 1 36 | fi 37 | 38 | exit 0 39 | -------------------------------------------------------------------------------- /tests/failing-build-of-dependency.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo 11 | > foo 12 | 13 | | false 14 | > baz 15 | 16 | | cat baz > bar 17 | < baz 18 | > bar 19 | EOF 20 | 21 | git init 22 | git add top.fac 23 | 24 | if ${FAC:-../../fac} > fac.out 2>&1; then 25 | cat fac.out 26 | echo Bilge was okay. That is not good. 27 | exit 1 28 | else 29 | cat fac.out 30 | echo Bilge failed as it ought. 31 | fi 32 | 33 | if grep 'build failed.*bar' fac.out; then 34 | echo we should not have attempted to build bar in the first place 35 | exit 1 36 | fi 37 | 38 | egrep 'build failed.*(baz|false)' fac.out 39 | 40 | exit 0 41 | -------------------------------------------------------------------------------- /tests/adding-missing-rule.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | # This test ensures that we can count builds properly to figure out if 8 | # we have built everything correctly. 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | mkdir subdir 15 | 16 | cat > top.fac < bar 18 | < foo 19 | 20 | EOF 21 | 22 | git init 23 | git add top.fac 24 | 25 | echo hello > foo 26 | 27 | if ${FAC:-../../fac} > fac.out; then 28 | cat fac.out 29 | echo this should have failed 30 | exit 1 31 | fi 32 | cat fac.out 33 | 34 | grep 'add .*foo.* to git' fac.out 35 | 36 | cat >> top.fac < foo 38 | 39 | EOF 40 | 41 | ${FAC:-../../fac} 42 | 43 | grep hello bar 44 | 45 | exit 0 46 | -------------------------------------------------------------------------------- /tests/irrelevant-explicit-dependency.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | git init 10 | 11 | cat > top.fac < greeting 13 | < fake-input 14 | EOF 15 | 16 | git add top.fac 17 | 18 | if ${FAC:-../../fac}; then 19 | echo should have failed 20 | exit 1 21 | fi 22 | 23 | echo diversion > fake-input 24 | 25 | if ${FAC:-../../fac}; then 26 | echo should have failed 27 | exit 1 28 | fi 29 | 30 | git add fake-input 31 | 32 | ${FAC:-../../fac} 33 | 34 | grep hello greeting 35 | 36 | ${FAC:-../../fac} > fac.out 37 | cat fac.out 38 | 39 | if grep echo fac.out; then 40 | echo we rebuilt when we should not have 41 | exit 1 42 | fi 43 | 44 | exit 0 45 | -------------------------------------------------------------------------------- /tests/generating-facfiles.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | git init 10 | 11 | cat > top.fac < 1.fac 13 | > 1.fac 14 | EOF 15 | git add top.fac 16 | 17 | cat > fac.py < 2.fac 20 | > 2.fac 21 | """ 22 | EOF 23 | git add fac.py 24 | 25 | cat > fac2.py < foo 28 | > foo 29 | """ 30 | EOF 31 | git add fac2.py 32 | 33 | git ls-files 34 | 35 | ${FAC:-../../fac} -v 36 | 37 | grep foo foo 38 | 39 | ${FAC:-../../fac} --clean 40 | 41 | if test -e foo; then 42 | echo file foo should have been deleted 43 | exit 1 44 | fi 45 | 46 | ${FAC:-../../fac} 47 | 48 | grep foo foo 49 | 50 | exit 0 51 | -------------------------------------------------------------------------------- /tests/git-describe.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | rm -rf $0.dir 8 | mkdir $0.dir 9 | cd $0.dir 10 | 11 | # This test will fail if we are unable to identify paths within .git/ 12 | # properly. 13 | 14 | cat > top.fac < version 16 | EOF 17 | 18 | git init 19 | git add top.fac 20 | git commit -am 'first version' 21 | git tag -a 0.0 -m 'The first version' 22 | 23 | ${FAC:-../../fac} 24 | 25 | cat version 26 | grep 0.0 version 27 | 28 | grep git top.fac.tum 29 | 30 | sleep 1 31 | 32 | cat > AUTHOR < top.fac < foobar 18 | > foobar 19 | < subdir/foo 20 | < subdir/bar 21 | 22 | EOF 23 | 24 | cat > subdir/.fac < foo 26 | > foo 27 | 28 | | echo bar > bar 29 | > bar 30 | 31 | EOF 32 | 33 | git init 34 | git add top.fac subdir/.fac 35 | 36 | ${FAC:-../../fac} > fac.out 37 | cat fac.out 38 | 39 | grep foo foobar 40 | grep bar foobar 41 | 42 | grep 1/3 fac.out 43 | grep 2/3 fac.out 44 | grep 3/3 fac.out | grep foobar 45 | 46 | exit 0 47 | -------------------------------------------------------------------------------- /tests/cache-in-home-as-symlink.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | # create a new "home directory" for testing 10 | mkdir -p realhome/.cache 11 | # now make a symlink to that actual "home directory", and make the 12 | # symlink be our $HOME. 13 | ln -s `pwd`/realhome home 14 | export HOME=`pwd`/home 15 | 16 | echo $HOME 17 | 18 | mkdir repo 19 | cd repo 20 | 21 | echo foo > foo 22 | echo bar > bar 23 | 24 | cat > top.fac <> ~/.cache/trash && echo baz > baz 27 | C ~/.cache/ 28 | > baz 29 | EOF 30 | 31 | git init 32 | git add top.fac 33 | 34 | ${FAC:-../../../fac} 35 | 36 | grep baz baz 37 | grep foo ~/.cache/trash 38 | 39 | exit 0 40 | -------------------------------------------------------------------------------- /tests/non-default-rules.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < output 12 | < foo 13 | < bar 14 | < baz 15 | 16 | EOF 17 | 18 | for x in foo bar baz hello stupid bad horrible; do 19 | cat >> top.fac < $x 21 | > $x 22 | 23 | EOF 24 | done 25 | 26 | git init 27 | git add top.fac 28 | 29 | ${FAC:-../../fac} 30 | 31 | grep foo foo 32 | grep bar bar 33 | grep baz baz 34 | 35 | grep foo output 36 | grep bar output 37 | grep baz output 38 | 39 | test ! -e hello 40 | test ! -e stupid 41 | test ! -e bad 42 | test ! -e horrible 43 | 44 | ${FAC:-../../fac} hello 45 | 46 | grep hello hello 47 | test ! -e stupid 48 | test ! -e bad 49 | test ! -e horrible 50 | 51 | exit 0 52 | -------------------------------------------------------------------------------- /tests/run-ghc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | if ! which ghc; then 6 | echo there is no ghc 7 | exit 137 8 | fi 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | cat > Foo.hs < Bar.hs < main.hs < top.fac < Foo.o 38 | 39 | | ghc -c Bar.hs 40 | > Bar.o 41 | 42 | | ghc --make -o main main.hs 43 | > main 44 | < Foo.o 45 | < Bar.o 46 | 47 | EOF 48 | 49 | git init 50 | git add top.fac main.hs Foo.hs Bar.hs 51 | 52 | ${FAC:-../../fac} 53 | 54 | exit 0 55 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Fac is a build system. 2 | 3 | #![cfg_attr(feature = "strict", deny(warnings))] 4 | #![cfg_attr(feature = "strict", deny(missing_docs))] 5 | 6 | extern crate clap; 7 | 8 | mod git; 9 | 10 | pub mod build; 11 | mod version; 12 | 13 | #[cfg(feature="profile")] 14 | use cpuprofiler::PROFILER; 15 | 16 | #[cfg(feature="profile")] 17 | fn main() { 18 | let flags = build::flags::args(); 19 | PROFILER.lock().unwrap().start("/tmp/fac.profile").unwrap(); 20 | let exitcode = build::build(flags); 21 | PROFILER.lock().unwrap().stop().unwrap(); 22 | std::process::exit(exitcode); 23 | } 24 | 25 | #[cfg(not(feature="profile"))] 26 | fn main() { 27 | let flags = build::flags::args(); 28 | let exitcode = build::build(flags); 29 | std::process::exit(exitcode); 30 | } 31 | -------------------------------------------------------------------------------- /tests/quiet-on-success.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo 11 | > foo 12 | EOF 13 | 14 | echo I am loud > message 15 | 16 | git init 17 | git add top.fac message 18 | 19 | ${FAC:-../../fac} > fac.out 2>&1 20 | cat fac.out 21 | 22 | if grep 'I am loud' fac.out; then 23 | echo build was noisy on success 24 | exit 1 25 | fi 26 | 27 | sleep 1 28 | touch message 29 | 30 | ${FAC:-../../fac} > fac.out 2>&1 31 | cat fac.out 32 | 33 | if grep 'I am loud' fac.out; then 34 | echo build was noisy on success 35 | exit 1 36 | fi 37 | 38 | echo >> message 39 | 40 | ${FAC:-../../fac} --show-output > fac.out 2>&1 41 | cat fac.out 42 | 43 | grep 'I am loud' fac.out 44 | 45 | exit 0 46 | -------------------------------------------------------------------------------- /tests/implicit-dependency-removed-and-not-needed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | echo hello > input_file 10 | 11 | mkdir repo 12 | cd repo 13 | 14 | cat > top.fac < foobar 16 | < script.sh 17 | > foobar 18 | EOF 19 | 20 | cat > script.sh < ../new_input_file 34 | 35 | if ${FAC:-../../../fac}; then 36 | echo this should have failed 37 | exit 1 38 | fi 39 | 40 | cat > script.sh < top.fac < foo 12 | > foo 13 | 14 | | cat foo bar baz > foobar 15 | > foobar 16 | 17 | | echo bar > bar 18 | > bar 19 | 20 | EOF 21 | 22 | git init 23 | git add top.fac 24 | 25 | if ${FAC:-../../fac} > fac.out; then 26 | echo This should have failed 27 | cat fac.out 28 | exit 1 29 | fi 30 | cat fac.out 31 | 32 | grep foo foo 33 | grep bar bar 34 | 35 | cat > top.fac < foo 37 | > foo 38 | 39 | | cat foo bar > foobar 40 | > foobar 41 | 42 | | echo bar > bar 43 | > bar 44 | 45 | EOF 46 | 47 | ${FAC:-../../fac} 48 | 49 | grep foo foo 50 | grep bar bar 51 | grep foo foobar 52 | grep bar foobar 53 | 54 | exit 0 55 | -------------------------------------------------------------------------------- /git.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """ 4 | 5 | This is a small script that enables conveniently running git with a 6 | modified gnupg environment. The purpose is to enable users to 7 | conveniently verify that commits and tags have been signed by actual 8 | authorized developers. For more information, read 9 | 10 | http://physics.oregonstate.edu/~roundyd/fac/signatures.html 11 | 12 | """ 13 | 14 | import sys, subprocess, os 15 | 16 | if not os.path.exists('git.py'): 17 | print('error: must call this program from its own directory') 18 | exit(1) 19 | 20 | wd = os.getcwd() 21 | 22 | env = os.environ 23 | env['GNUPGHOME'] = wd+'/.gnupg' 24 | 25 | args = sys.argv[1:] 26 | if args[0] == 'pull': 27 | args = [args[0], '--verify-signatures'] + args[1:] 28 | 29 | exit(subprocess.call(['git'] + args, env=env)) 30 | -------------------------------------------------------------------------------- /tests/explicit-build-arguments.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo 11 | > foo 12 | 13 | | cat bar > ugly 14 | < bar 15 | > ugly 16 | EOF 17 | 18 | echo hello > bar 19 | 20 | git init 21 | git add top.fac bar 22 | 23 | ${FAC:-../../fac} foo 24 | 25 | grep foo foo 26 | 27 | if grep hello ugly; then 28 | echo should not be made 29 | exit 1 30 | fi 31 | 32 | ${FAC:-../../fac} 33 | 34 | grep hello ugly 35 | 36 | echo wrong > foo 37 | echo goodbye > ugly # put wrong value in file 38 | 39 | ${FAC:-../../fac} foo 40 | 41 | grep foo foo 42 | # verify the wrong value is still in the ugly file that we did not 43 | # rebuild 44 | grep goodbye ugly 45 | 46 | ${FAC:-../../fac} 47 | 48 | grep foo foo 49 | grep hello ugly 50 | 51 | exit 0 52 | -------------------------------------------------------------------------------- /tests/generate-tupfile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | if ! which tup; then 6 | echo there is no tup 7 | exit 137 8 | fi 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | cat > build.fac < awesome 16 | > awesome 17 | 18 | | wc input > data 19 | 20 | | cat data awesome > great && echo silliness > foo 21 | < data 22 | < awesome 23 | EOF 24 | 25 | cat > input < top.fac < 1.fac 14 | > 2.fac 15 | EOF 16 | git add top.fac 17 | 18 | cat > fac.sh < 1.fac < foo 23 | EOME 24 | 25 | cat > 2.fac < bar 27 | EOME 28 | EOF 29 | git add fac.sh 30 | 31 | ${FAC:-../../fac} -v 32 | 33 | grep foo foo 34 | 35 | grep bar bar 36 | 37 | cat > fac.sh < 1.fac < foo 42 | EOME 43 | 44 | cat > 2.fac < bar 46 | EOME 47 | 48 | EOF 49 | 50 | cat top.fac.tum 51 | 52 | ${FAC:-../../fac} -v 53 | 54 | grep one foo 55 | grep two bar 56 | 57 | echo it all worked 58 | 59 | exit 0 60 | -------------------------------------------------------------------------------- /tests/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo 11 | > foo 12 | 13 | | cat foo > baz 14 | > baz 15 | < foo 16 | 17 | | cat input > output 18 | < input 19 | > output 20 | EOF 21 | 22 | echo goodness > input 23 | 24 | git init 25 | git add top.fac input 26 | 27 | ${FAC:-../../fac} 28 | 29 | grep foo baz 30 | grep foo foo 31 | grep goodness output 32 | 33 | ${FAC:-../../fac} --clean -v 34 | 35 | if test -e foo; then 36 | echo file foo should have been deleted 37 | exit 1 38 | fi 39 | 40 | if test -e baz; then 41 | echo file foo should have been deleted 42 | exit 1 43 | fi 44 | 45 | grep goodness input 46 | 47 | if test -e output; then 48 | echo file output should have been deleted 49 | exit 1 50 | fi 51 | 52 | git diff 53 | 54 | exit 0 55 | -------------------------------------------------------------------------------- /tests/no-pointless-rebuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | echo foo > foo 10 | echo bar > bar 11 | 12 | cat > top.fac < foo 14 | > foo 15 | EOF 16 | 17 | git init 18 | git add top.fac 19 | 20 | ${FAC:-../../fac} -v > fac.out 21 | cat fac.out 22 | if grep 'Building.* foo' fac.out; then 23 | echo It is dirty, as it should be. 24 | else 25 | echo It should not be clean!!! 26 | exit 1 27 | fi 28 | 29 | cat top.fac.tum 30 | 31 | ${FAC:-../../fac} -v > fac.out 32 | cat fac.out 33 | if grep 'Building.* foo' fac.out; then 34 | echo It is dirty, but should not be!!! 35 | exit 1 36 | else 37 | echo All is well. 38 | fi 39 | if grep '1/1' fac.out; then 40 | echo It is dirty, but should not be!!! 41 | exit 1 42 | else 43 | echo All is well. 44 | fi 45 | 46 | exit 0 47 | -------------------------------------------------------------------------------- /tests/former-dependency-removed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | # This test ensures that we can count builds properly to figure out if 8 | # we have built everything correctly. 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | cat > top.fac < foo 16 | > foo 17 | 18 | EOF 19 | 20 | cat > script.sh < input 30 | 31 | git init 32 | git add top.fac input script.sh 33 | 34 | ${FAC:-../../fac} 35 | 36 | grep hello foo 37 | 38 | sleep 1 39 | echo goodbye > input 40 | 41 | ${FAC:-../../fac} 42 | 43 | grep goodbye foo 44 | 45 | cat > script.sh < top.fac < bar 11 | 12 | | cat bar > baz 13 | < bar 14 | 15 | | cat input > output 16 | EOF 17 | 18 | git init 19 | git add top.fac 20 | 21 | echo foo > foo 22 | echo input > input 23 | 24 | git ls-files 25 | 26 | if git ls-files | grep foo; then 27 | echo foo should not yet be in git 28 | exit 1 29 | fi 30 | if git ls-files | grep input; then 31 | echo foo should not yet be in git 32 | exit 1 33 | fi 34 | 35 | if ${FAC:-../../fac}; then 36 | echo fac should have failed 37 | exit 1 38 | fi 39 | 40 | ${FAC:-../../fac} --git-add -v 41 | 42 | git ls-files 43 | git ls-files | grep foo 44 | git ls-files | grep input 45 | 46 | if git ls-files | grep bar; then 47 | echo bar should not be added 48 | exit 1 49 | fi 50 | 51 | exit 0 52 | -------------------------------------------------------------------------------- /tests/ignore-readdir-generated-content.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo-listing 11 | < foo 12 | 13 | | mkdir -p foo 14 | 15 | | echo hello > foo/hello 16 | < foo 17 | 18 | | echo world > foo/world 19 | < foo 20 | EOF 21 | 22 | git init 23 | git add top.fac 24 | 25 | ${FAC:-../../fac} -vvv 26 | 27 | grep hello foo/hello 28 | grep world foo/world 29 | 30 | # foo-listing may not be correct the first time, since content is generated in foo 31 | cat foo-listing 32 | cp foo-listing old-foo-listing 33 | 34 | sleep 1 35 | 36 | ${FAC:-../../fac} -vvv 37 | 38 | grep hello foo/hello 39 | grep world foo/world 40 | 41 | cat foo-listing 42 | diff foo-listing old-foo-listing 43 | 44 | rm foo-listing 45 | 46 | ${FAC:-../../fac} -vvv 47 | 48 | grep hello foo-listing 49 | grep world foo-listing 50 | 51 | exit 0 52 | -------------------------------------------------------------------------------- /tests/implicit-output-with-existing-files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > foo.fac < foo 11 | > foo 12 | 13 | | echo bar > bar 14 | > bar 15 | 16 | | cat foo bar > baz 17 | < bar 18 | < foo 19 | EOF 20 | 21 | git init 22 | git add foo.fac 23 | 24 | ${FAC:-../../fac} 25 | 26 | grep foo foo 27 | grep bar bar 28 | grep foo baz 29 | grep bar baz 30 | 31 | # This test ensures that we can build even when we have rules that do 32 | # not define their output files, and the files already exist. 33 | 34 | cat > foo.fac < foo 36 | 37 | | echo bar > bar 38 | 39 | | cat foo bar hello > baz 40 | < bar 41 | EOF 42 | 43 | echo hello > hello 44 | git add hello 45 | 46 | ${FAC:-../../fac} 47 | 48 | grep foo foo 49 | grep bar bar 50 | grep foo baz 51 | grep bar baz 52 | grep hello baz 53 | 54 | exit 0 55 | -------------------------------------------------------------------------------- /tests/one-rule-wait-for-others.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | # This test ensures that we can count builds properly to figure out if 8 | # we have built everything correctly. 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | cat > top.fac < foo 16 | > foo 17 | 18 | | echo bar > bar 19 | 20 | | echo zoo > zoo 21 | 22 | | echo aaa > aaa 23 | 24 | | echo bbb > bbb 25 | 26 | | echo ccc > ccc 27 | 28 | | echo ddd > ddd 29 | 30 | | cat foo bar zoo aaa bbb ccc ddd > all 31 | < foo 32 | < bar 33 | < zoo 34 | < aaa 35 | < bbb 36 | < ccc 37 | < ddd 38 | 39 | EOF 40 | 41 | git init 42 | git add top.fac 43 | 44 | ${FAC:-../../fac} -v 45 | 46 | grep foo foo 47 | grep bar bar 48 | grep zoo zoo 49 | 50 | grep foo all 51 | grep bar all 52 | grep zoo all 53 | 54 | grep aaa all 55 | grep bbb all 56 | grep ccc all 57 | grep ddd all 58 | 59 | exit 0 60 | -------------------------------------------------------------------------------- /tests/clean-directory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This test is for issue #9 on github. 4 | 5 | set -ev 6 | 7 | rm -rf $0.dir 8 | mkdir $0.dir 9 | cd $0.dir 10 | 11 | cat > top.fac < foo/bar 15 | < foo 16 | 17 | | mkdir foo/subdir 18 | < foo 19 | 20 | | mkdir foo/subdir/subsub 21 | < foo/subdir 22 | 23 | | echo awesome > foo/subdir/subsub/great 24 | < foo/subdir/subsub 25 | 26 | | mkdir foo/subdir2 27 | < foo 28 | EOF 29 | 30 | git init 31 | git add top.fac 32 | 33 | ${FAC:-../../fac} 34 | 35 | grep welcome foo/bar 36 | grep awesome foo/subdir/subsub/great 37 | 38 | test -d foo 39 | 40 | cat top.fac.tum 41 | 42 | ${FAC:-../../fac} -c 43 | 44 | if grep welcome foo/bar; then 45 | echo foo/bar should have been deleted 46 | exit 1 47 | fi 48 | 49 | if test -d foo; then 50 | echo directory foo should have been deleted 51 | exit 1 52 | fi 53 | 54 | exit 0 55 | -------------------------------------------------------------------------------- /tests/command-fails-but-produces-files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | # This test ensures that we can count builds properly to figure out if 8 | # we have built everything correctly. 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | cat > top.fac < cmd.sh < hello; fi 21 | exit 1 22 | EOF 23 | 24 | git init 25 | git add top.fac cmd.sh 26 | 27 | if ${FAC:-../../fac} --log-ouput log > fac.out; then 28 | cat fac.out 29 | echo this should have failed 30 | exit 1 31 | fi 32 | cat fac.out 33 | 34 | if grep hello hello; then 35 | echo should not have created hello 36 | exit 1 37 | fi 38 | 39 | cat > cmd.sh < hello; fi 41 | exit 0 42 | EOF 43 | 44 | ${FAC:-../../fac} 45 | 46 | grep hello hello 47 | 48 | exit 0 49 | -------------------------------------------------------------------------------- /tests/facfile-from-failed-command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | git init 10 | 11 | cat > top.fac < generated.fac 13 | > generated.fac 14 | EOF 15 | git add top.fac 16 | 17 | cat > fac.py < bad-case 20 | > bad-case 21 | """ 22 | 23 | exit(1) # command fails, so this facfile should not be used 24 | EOF 25 | git add fac.py 26 | 27 | git ls-files 28 | 29 | if ${FAC:-../../fac} -v; then 30 | echo fac should fail, because command fails 31 | exit 1 32 | fi 33 | 34 | if grep bad bad-case; then 35 | echo the bad command should not have run 36 | exit 1 37 | fi 38 | 39 | ${FAC:-../../fac} -v > log 2>&1 || echo it failed as expected 40 | 41 | cat log 42 | 43 | if grep bad log; then 44 | echo nothing "bad" should have been seen 45 | exit 1 46 | fi 47 | 48 | exit 0 49 | -------------------------------------------------------------------------------- /sparse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from __future__ import print_function 4 | import glob, os, sys, platform, subprocess 5 | 6 | myplatform = sys.platform 7 | if myplatform == 'linux2': 8 | myplatform = 'linux' 9 | 10 | if myplatform != 'linux': 11 | print('# cannot use sparse on platform', myplatform) 12 | exit(0) 13 | 14 | try: 15 | ver = subprocess.check_output(['sparse', '--version'], 16 | stderr=subprocess.STDOUT) 17 | except: 18 | print('# there is no sparse present') 19 | exit(0) 20 | 21 | myver = b'v0.5.0-' 22 | if ver[:len(myver)] == myver: 23 | print('# looking good!') 24 | else: 25 | print('# I am not confident with sparse version', ver) 26 | 27 | for f in sorted(glob.glob('*.c')): 28 | print('| sparse -Ibigbro -Wsparse-error %s > %s.sparse' % (f, f[:-2])) 29 | print('> %s.sparse' % (f[:-2])) 30 | print('< version-identifier.h') 31 | -------------------------------------------------------------------------------- /tests/clean-after-rule-change.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo 11 | 12 | | echo bar > baz 13 | EOF 14 | 15 | git init 16 | git add top.fac 17 | 18 | ${FAC:-../../fac} 19 | 20 | ls -lh 21 | 22 | grep foo foo 23 | grep bar baz 24 | 25 | echo Oh no, there was a bug in our rule! 26 | 27 | cat > top.fac < foo 29 | 30 | | echo bar > bar 31 | EOF 32 | 33 | ${FAC:-../../fac} 34 | 35 | ls -lh 36 | 37 | grep foo foo 38 | grep bar bar 39 | 40 | ${FAC:-../../fac} --clean -v 41 | 42 | if test -e foo; then 43 | echo file foo should have been deleted 44 | exit 1 45 | fi 46 | 47 | if test -e bar; then 48 | echo file bar should have been deleted 49 | exit 1 50 | fi 51 | 52 | if test -e baz; then 53 | echo file baz should have been deleted 54 | exit 1 55 | fi 56 | 57 | ls -lh 58 | 59 | exit 0 60 | -------------------------------------------------------------------------------- /tests/newly-missing-output.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > foo.sh < foo 11 | echo bar > bar 12 | EOF 13 | 14 | cat > my.fac < foo 17 | EOF 18 | 19 | git init 20 | git add my.fac foo.sh 21 | 22 | ${FAC:-../../fac} 23 | 24 | grep foo foo 25 | grep bar bar 26 | grep foo my.fac.tum 27 | grep bar my.fac.tum 28 | 29 | cat > foo.sh < foo 31 | EOF 32 | 33 | ${FAC:-../../fac} 34 | 35 | grep good foo 36 | grep bar bar 37 | grep foo my.fac.tum 38 | grep bar my.fac.tum 39 | 40 | rm bar 41 | echo foo.sh should be rerun to try to generate bar 42 | 43 | ${FAC:-../../fac} -v > fac.out 44 | cat fac.out 45 | 46 | grep 'because .*bar.* ' fac.out 47 | 48 | grep good foo 49 | grep foo my.fac.tum 50 | 51 | if grep bar my.fac.tum; then 52 | echo bar is not relevant 53 | exit 1 54 | fi 55 | 56 | exit 0 57 | -------------------------------------------------------------------------------- /tests/relative-symlink-path.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Relative and symlink paths fail on initial build 4 | 5 | set -ev 6 | 7 | rm -rf $0.dir 8 | mkdir $0.dir 9 | cd $0.dir 10 | 11 | cat > top.fac < dir1 14 | 15 | | echo "foo" > dir1/foo 16 | < dir1 17 | > dir1/foo 18 | 19 | # aliased symbolic link to dir1 causes failure 20 | 21 | | ln -s dir1 sym1 22 | < dir1 23 | > sym1 24 | 25 | | cat sym1/foo > foo 26 | < sym1/foo 27 | > foo 28 | EOF 29 | 30 | mkdir dir2 31 | cat > dir2/build.fac < foo 33 | < ../dir1/foo 34 | > foo 35 | EOF 36 | 37 | git init 38 | git add . 39 | 40 | # this will fail with: 41 | # error: add dir2/../dir1/foo to git, which is required for dir2/foo 42 | # error: add sym1/foo to git, which is required for foo 43 | ${FAC:-../../fac} 44 | 45 | # subsequent attempt will work since directories are available 46 | ${FAC:-../../fac} 47 | 48 | exit 0 49 | -------------------------------------------------------------------------------- /tests/generate-ninja.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | if ! which ninja; then 6 | echo there is no ninja 7 | exit 137 8 | fi 9 | 10 | if ! (echo $FAC | grep target) ; then 11 | echo not using rust 12 | exit 137 13 | fi 14 | 15 | rm -rf $0.dir 16 | mkdir $0.dir 17 | cd $0.dir 18 | 19 | cat > build.fac < awesome 21 | > awesome 22 | 23 | | wc input > data 24 | 25 | | cat data awesome > great && echo silliness > foo 26 | < data 27 | < awesome 28 | EOF 29 | 30 | cat > input < top.fac < foo/bar 20 | < foo 21 | 22 | | mkdir foo/subdir 23 | < foo 24 | 25 | | mkdir foo/subdir/subsub 26 | < foo/subdir 27 | 28 | | sleep 1 && echo awesome > foo/subdir/subsub/great 29 | < foo/subdir/subsub 30 | 31 | | sleep 1 && mkdir foo/subdir2 32 | < foo 33 | EOF 34 | 35 | git init 36 | git add top.fac 37 | 38 | ${FAC:-../../fac} 39 | 40 | grep foo top.fac.tum 41 | 42 | cat top.fac.tum 43 | 44 | ${FAC:-../../fac} -v -j 1 45 | 46 | exit 0 47 | -------------------------------------------------------------------------------- /web/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fac: $pagetitle 6 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | $docnav 22 | 23 | 26 | 27 |
28 |
29 |

$title

30 |
31 |
32 | $content 33 |
34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/strict.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | # This test ensures that strict will fail if we do not include in the 8 | # facfile an input that is generated by another rule. 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | cat > top.fac < foo 16 | 17 | | cp foo bar 18 | 19 | | cp foo baz 20 | < foo 21 | 22 | EOF 23 | 24 | git init 25 | git add top.fac 26 | 27 | if ${FAC:-../../fac} --strict &> fac.out; then 28 | cat fac.out 29 | echo this should fail for one reason or another 30 | exit 1 31 | fi 32 | 33 | cat fac.out 34 | 35 | if ${FAC:-../../fac} --strict &> fac.out; then 36 | cat fac.out 37 | echo this should fail due to strictness 38 | exit 1 39 | fi 40 | 41 | cat fac.out 42 | grep 'requires foo' fac.out 43 | 44 | cat > top.fac < foo 46 | 47 | | cp foo bar 48 | < foo 49 | 50 | | cp foo baz 51 | < foo 52 | 53 | EOF 54 | 55 | ${FAC:-../../fac} --strict 56 | 57 | grep foo bar 58 | 59 | exit 0 60 | -------------------------------------------------------------------------------- /tests/bench-dependentchains-big.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | # This test is intended to check for stack overflow bugs. Sadly, that 6 | # makes it pretty slow, since it must necessarily deal with large 7 | # numbers of rules. It could be a bit faster if we didn't actually 8 | # invoke gcc for each rule, but that would be tedious, and would also 9 | # reduce the number of inputs for each rule. 10 | 11 | rm -rf $0.dir 12 | mkdir $0.dir 13 | cd $0.dir 14 | 15 | git init 16 | 17 | python3 ../../bench/dependentchains.py 562 # 1778 18 | 19 | ls .git 20 | 21 | time ${FAC:-../../fac} --dry 22 | 23 | exit 0 24 | 25 | # Uncomment the above to have a more thorough test for more stack overflows. 26 | 27 | time ${FAC:-../../fac} 28 | 29 | ${FAC:-../../fac} -v > fac.out 30 | cat fac.out 31 | 32 | if grep gcc fac.out; then 33 | echo we rebuilt when we should not have 34 | exit 1 35 | fi 36 | 37 | cat modifying-header.sh 38 | sh modifying-header.sh 39 | 40 | time ${FAC:-../../fac} 41 | 42 | 43 | exit 0 44 | -------------------------------------------------------------------------------- /tests/fail-on-two-rules-one-output.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | echo foo > foo 10 | echo bar > bar 11 | 12 | cat > top.fac <> foobar && echo nice > nice 14 | > foobar 15 | 16 | | echo bad >> foobar && echo mean > mean 17 | > foobar 18 | EOF 19 | 20 | git init 21 | git add top.fac 22 | 23 | if ${FAC:-../../fac} &> fac.out; then 24 | cat fac.out 25 | echo This should not have passed 26 | exit 1 27 | fi 28 | cat fac.out 29 | 30 | grep 'two' fac.out 31 | 32 | cat > top.fac <> foobar && echo nice > nice 34 | 35 | | echo bad >> foobar && echo mean > mean 36 | EOF 37 | 38 | if ${FAC:-../../fac} &> fac.out; then 39 | cat fac.out 40 | echo This should not have passed 41 | exit 1 42 | fi 43 | cat fac.out 44 | 45 | grep 'two' fac.out 46 | 47 | cat > top.fac <> foobar && echo nice > nice 49 | c bar 50 | 51 | | echo bad >> foobar && echo mean > mean 52 | c bar 53 | EOF 54 | 55 | ${FAC:-../../fac} 56 | 57 | exit 0 58 | -------------------------------------------------------------------------------- /web/documentation.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | $docnav 4 | 5 | The fac documentation consists of several web pages. You can navigate 6 | the site using the menu links, but this page gives you an additional 7 | overview of what you will learn from each page. 8 | 9 | [getting started](getting-started.html) 10 | : This page will lead you through creating your first fac file and 11 | using fac. 12 | 13 | [fac with git hooks](fac-with-git-hooks.html) 14 | : This is a secondary tutorial which gives an example of how you can 15 | use fac with `--continual` and git hooks to rebuild your code in 16 | response to changes of various sorts. 17 | 18 | [building fac](building.html) 19 | : This page will lead you through downloading and installing fac. 20 | 21 | [feature comparison](features.html) 22 | : This page compares fac with three other build systems in a 23 | feature-by-feature way. 24 | 25 | ## Mailing list 26 | 27 | To get help using fac, or to share your experiences, visit the 28 | [fac-users mailing list](https://groups.google.com/forum/#!forum/fac-users). 29 | -------------------------------------------------------------------------------- /web/building.md: -------------------------------------------------------------------------------- 1 | # Building fac 2 | 3 | $docnav 4 | 5 | Before building fac, you need to [install rust](https://rustup.rs). 6 | You will also need to install python3, which is used for various build 7 | scripts. On a Debian-based 8 | system, you can install all of this with 9 | 10 | apt-get install rust python3 11 | 12 | You can obtain the fac source code using git clone: 13 | 14 | git clone git://github.com/droundy/fac.git 15 | 16 | To build fac (assuming you have just cloned fac, and do not have an 17 | older version of fac) just run 18 | 19 | cargo build --release && cp target/release/fac . 20 | 21 | This should build fac on an x86-64 or 32-bit x86 linux system. You 22 | can then rebuild fac by running if you care to. 23 | 24 | ./fac fac 25 | 26 | This will also (if possible) build the documentation and a few other 27 | peripherals. To use fac, you can copy the fac binary into some 28 | location in your path. 29 | 30 | Building the documentation (by running `./fac`) will require a few 31 | more packages: sass, python-markdown, and help2man. 32 | -------------------------------------------------------------------------------- /tests/generate-makefile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | if ! which make; then 6 | echo there is no make 7 | exit 137 8 | fi 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | cat > build.fac < awesome 16 | > awesome 17 | 18 | | echo foo > foo 19 | 20 | # The following is malformed output 21 | 22 | | echo hello > /tmp/mistake-$$ && echo baz > baz 23 | > /tmp/mistake-$$ 24 | 25 | EOF 26 | 27 | git init 28 | git add build.fac 29 | 30 | if ${FAC:-../../fac} --makefile Makefile; then 31 | echo this should have failed, since it has malformed output 32 | fi 33 | 34 | cat > build.fac < awesome 36 | > awesome 37 | 38 | | echo foo > foo 39 | 40 | | echo hello > /tmp/mistake-$$ && echo baz > baz 41 | 42 | EOF 43 | 44 | ${FAC:-../../fac} --makefile Makefile 45 | 46 | grep awesome awesome 47 | grep foo foo 48 | grep baz baz 49 | 50 | cat Makefile 51 | 52 | ${FAC:-../../fac} -c 53 | 54 | make 55 | 56 | grep awesome awesome 57 | grep foo foo 58 | grep baz baz 59 | 60 | rm -f /tmp/mistake-$$ 61 | 62 | exit 0 63 | -------------------------------------------------------------------------------- /tests/rename-repository.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | mkdir repo 10 | cd repo 11 | 12 | git init 13 | 14 | cat > top.fac < hello 16 | > hello 17 | 18 | | cat foo.sh > new.fac 19 | > new.fac 20 | 21 | EOF 22 | 23 | cat > foo.sh < goodbye 25 | EOF 26 | 27 | git add top.fac foo.sh 28 | 29 | ${FAC:-../../../fac} -v 30 | 31 | grep hello hello 32 | grep goodbye goodbye 33 | 34 | cd .. 35 | mv repo silly-name 36 | cd silly-name 37 | 38 | # I have seen a bug in which renaming a directory (actually copying it 39 | # using cp -a) caused fac to believe there were two rules that would 40 | # produce the same file, because the .fac.tum files had the old 41 | # absolute path name. I have not yet been able to reproduce this bug 42 | # here. :( I'm leaving this test in case I see the bug again, so I 43 | # can easily slot in an improvement here. 44 | 45 | echo foo > hello 46 | 47 | ${FAC:-../../../fac} -v 48 | 49 | grep hello hello 50 | grep goodbye goodbye 51 | 52 | echo it all worked 53 | 54 | exit 0 55 | -------------------------------------------------------------------------------- /tests/edited-cache.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | echo foo > foo 10 | echo bar > bar 11 | 12 | cat > top.fac < baz 14 | > baz 15 | EOF 16 | 17 | echo stupid > foocache 18 | 19 | git init 20 | git add top.fac 21 | git add foocache 22 | 23 | ${FAC:-../../fac} 24 | 25 | sleep 1 26 | echo please say this > foocache 27 | 28 | ${FAC:-../../fac} --show-output > fac.out 29 | cat fac.out 30 | 31 | grep 'please say this' fac.out 32 | 33 | ${FAC:-../../fac} --show-output > fac.out 34 | cat fac.out 35 | 36 | if grep 'please say this' fac.out; then 37 | echo we rebuilt when we should not have done so 38 | exit 1 39 | fi 40 | 41 | cat > top.fac < baz 43 | > baz 44 | c cache 45 | EOF 46 | 47 | echo please do not say this > foocache 48 | 49 | ${FAC:-../../fac} --show-output > fac.out 50 | cat fac.out 51 | 52 | if grep 'please do not say this' fac.out; then 53 | echo this was not treated as a cache 54 | 55 | cat top.fac.tum 56 | 57 | exit 1 58 | fi 59 | 60 | exit 0 61 | -------------------------------------------------------------------------------- /tests/rebuild-with-new-inputs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > my.fac < out || true 11 | 12 | EOF 13 | 14 | echo foo > foo 15 | 16 | git init 17 | git add my.fac foo 18 | 19 | ${FAC:-../../fac} 20 | 21 | grep foo out 22 | if grep bar out; then 23 | echo bar should not be there 24 | exit 1 25 | fi 26 | 27 | echo bar > bar 28 | git add bar 29 | 30 | ${FAC:-../../fac} 31 | 32 | # we should not have just rebuilt it, because fac does not know that 33 | # bar could be an input, since we don't track attempts to open 34 | # nonexistent files 35 | 36 | grep foo out 37 | if grep bar out; then 38 | echo bar should not be there 39 | exit 1 40 | fi 41 | 42 | cat > my.fac < out || true 44 | < bar 45 | EOF 46 | 47 | ${FAC:-../../fac} --parse-only my.fac 48 | 49 | if ${FAC:-../../fac} --parse-only=myfac &> fac.out; then 50 | echo should have failed because myfac does not exist 51 | cat fac.out 52 | exit 1 53 | fi 54 | 55 | cat fac.out 56 | grep 'unable to open file' fac.out 57 | grep myfac fac.out 58 | 59 | ${FAC:-../../fac} 60 | 61 | grep foo out 62 | grep bar out 63 | 64 | exit 0 65 | -------------------------------------------------------------------------------- /tests/lost-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | echo foo > foo 10 | echo bar > bar 11 | 12 | cat > top.fac < foobar 15 | EOF 16 | 17 | cat > script.sh < foobar 19 | EOF 20 | 21 | git init 22 | git add top.fac script.sh foo bar 23 | 24 | ${FAC:-../../fac} 25 | 26 | grep foo foobar 27 | grep bar foobar 28 | 29 | sleep 1 30 | echo baz > bar 31 | ${FAC:-../../fac} 32 | 33 | grep foo foobar 34 | grep baz foobar 35 | 36 | cat > script.sh < foobar 38 | EOF 39 | 40 | ${FAC:-../../fac} 41 | 42 | cat foobar 43 | grep foo foobar 44 | if grep baz foobar; then 45 | echo It does have baz 46 | exit 1 47 | else 48 | echo There is no baz 49 | fi 50 | 51 | ${FAC:-../../fac} > fac.out 52 | cat fac.out 53 | if grep dirty fac.out; then 54 | echo It is dirty, but should not be!!! 55 | exit 1 56 | else 57 | echo All is well. 58 | fi 59 | 60 | sleep 1 61 | touch bar 62 | ${FAC:-../../fac} > fac.out 63 | cat fac.out 64 | if grep dirty fac.out; then 65 | echo It is dirty, but should not be!!! 66 | exit 1 67 | else 68 | echo All is well. 69 | fi 70 | 71 | exit 0 72 | -------------------------------------------------------------------------------- /tests/cache-added.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | echo foo > foo 10 | echo bar > bar 11 | 12 | cat > top.fac < cache && echo baz > baz 14 | 15 | | echo foo > cache && echo bar > bar 16 | EOF 17 | 18 | echo good > .cache-read 19 | 20 | git init 21 | git add top.fac 22 | 23 | if ${FAC:-../../fac}; then 24 | echo should have failed 25 | exit 1 26 | fi 27 | 28 | cat > top.fac < cache && echo baz > baz 30 | c cache 31 | 32 | | echo foo > cache && echo bar > bar 33 | c cache 34 | EOF 35 | 36 | ${FAC:-../../fac}; 37 | 38 | grep baz baz 39 | grep bar bar 40 | 41 | cat > top.fac < cache && echo baz > baz 43 | c cache 44 | 45 | | echo foo > cache && echo bar > bar 46 | c cache 47 | 48 | | cat input > output 49 | EOF 50 | 51 | echo hello > input 52 | 53 | if ${FAC:-../../fac}; then 54 | echo should have failed 55 | exit 1 56 | fi 57 | 58 | cat > top.fac < cache && echo baz > baz 60 | c cache 61 | 62 | | echo foo > cache && echo bar > bar 63 | c cache 64 | 65 | | cat input > output 66 | C input 67 | EOF 68 | 69 | ${FAC:-../../fac}; 70 | 71 | grep baz baz 72 | grep bar bar 73 | grep hello output 74 | 75 | exit 0 76 | -------------------------------------------------------------------------------- /tests/cached-output-changing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | rm -rf $0.dir 8 | mkdir $0.dir 9 | cd $0.dir 10 | 11 | cat > top.fac < generate-stuff.sh < foo 20 | 21 | test -e bar || echo bar > bar 22 | 23 | EOF 24 | 25 | git init 26 | git add top.fac generate-stuff.sh 27 | 28 | echo hello > foo 29 | 30 | if ${FAC:-../../fac}; then 31 | echo This should fail because foo exists 32 | fi 33 | 34 | ls -l 35 | grep hello foo 36 | echo bar should ideally not exist, because command failed 37 | 38 | rm -f foo bar 39 | 40 | ${FAC:-../../fac} 41 | 42 | grep foo foo 43 | grep bar bar 44 | 45 | 46 | cat > generate-stuff.sh < foo 50 | test -e bar || echo bar > bar 51 | test -e baz || echo baz > baz 52 | 53 | EOF 54 | 55 | ${FAC:-../../fac} 56 | 57 | grep foo foo 58 | grep bar bar 59 | grep baz baz 60 | 61 | echo badness > baz 62 | 63 | cat top.fac.tum 64 | 65 | ${FAC:-../../fac} 66 | 67 | grep foo foo 68 | grep bar bar 69 | grep badness baz 70 | 71 | cat top.fac.tum 72 | 73 | rm baz 74 | 75 | ${FAC:-../../fac} 76 | 77 | grep foo foo 78 | grep bar bar 79 | grep baz baz 80 | 81 | exit 0 82 | -------------------------------------------------------------------------------- /tests/dependency-makefiles.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | rm -rf $0.dir 8 | mkdir $0.dir 9 | cd $0.dir 10 | 11 | mkdir subdir 12 | 13 | cat > top.fac < foo 20 | 21 | | file foo && ./foo > message 22 | < foo 23 | > message 24 | EOF 25 | 26 | cat > foo.h < foo.c < 31 | #include "foo.h" 32 | 33 | int main() { 34 | printf(message); 35 | return 0; 36 | } 37 | EOF 38 | 39 | git init 40 | git add top.fac foo.c foo.h 41 | 42 | ${FAC:-../../fac} 43 | 44 | grep hello message 45 | 46 | cat .foo.o.dep 47 | 48 | ${FAC:-../../fac} -c 49 | 50 | test ! -e .foo.o.dep 51 | test ! -e foo.o 52 | test ! -e foo 53 | test ! -e message 54 | 55 | ${FAC:-../../fac} --blind 56 | 57 | cat > "name with\tspace.h" < foo.c < 63 | #include "name with\tspace.h" 64 | 65 | int main() { 66 | printf(message); 67 | return 0; 68 | } 69 | EOF 70 | 71 | ${FAC:-../../fac} 72 | 73 | grep greetings message 74 | 75 | cat .foo.o.dep 76 | 77 | ${FAC:-../../fac} 78 | 79 | exit 0 80 | -------------------------------------------------------------------------------- /tests/readdir-generated-content.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < foo-listing 11 | < foo 12 | 13 | | mkdir -p foo 14 | 15 | | echo hello > foo/hello 16 | < foo 17 | 18 | | echo world > foo/world 19 | < foo 20 | EOF 21 | 22 | git init 23 | git add top.fac 24 | 25 | ${FAC:-../../fac} -vvv 26 | 27 | grep hello foo/hello 28 | grep world foo/world 29 | 30 | # foo-listing may not be correct the first time, since content is generated in foo 31 | cat foo-listing 32 | 33 | sleep 1 34 | 35 | ${FAC:-../../fac} -vvv 36 | 37 | grep hello foo/hello 38 | grep world foo/world 39 | 40 | cat foo-listing 41 | 42 | grep hello foo-listing 43 | grep world foo-listing 44 | 45 | cat >> top.fac < foo/wonderful 47 | EOF 48 | 49 | sleep 1 50 | 51 | ${FAC:-../../fac} -vvv 52 | 53 | # have to run twice because first time we modify the directory. 54 | # Should we make this build in just one rule? 55 | 56 | ${FAC:-../../fac} -vvv 57 | 58 | grep wonderful foo/wonderful 59 | 60 | # The foo-listing should be updated, since we have added a new file to 61 | # the foo directory. 62 | cat foo-listing 63 | 64 | grep wonderful foo-listing 65 | grep hello foo-listing 66 | grep world foo-listing 67 | 68 | exit 0 69 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fac" 3 | version = "0.5.4" 4 | authors = ["daveroundy@gmail.com"] 5 | edition = "2018" 6 | 7 | description = "build tool" 8 | license = "GPL-2.0+" 9 | repository = "https://github.com/droundy/fac" 10 | homepage = "https://physics.oregonstate.edu/~droundy/fac" 11 | documentation = "https://physics.oregonstate.edu/~droundy/fac/documentation.html" 12 | 13 | readme = "README.md" 14 | 15 | [badges] 16 | travis-ci = { repository = "droundy/fac", branch = "master" } 17 | appveyor = { repository = "droundy/fac", branch = "master", service = "github"} 18 | 19 | [lib] 20 | name = "fac" 21 | path = "src/lib.rs" 22 | 23 | [[bin]] 24 | name = "fac" 25 | path = "src/main.rs" 26 | doc = false 27 | 28 | [dependencies] 29 | 30 | libc = "^0.2" 31 | bigbro = "0.5" 32 | clap = "3.0.14" 33 | num_cpus = "1.13.1" 34 | metrohash = "1.0.6" 35 | notify = "4.0.17" 36 | tinyset = "0.4.10" 37 | pathdiff = "0.2.0" 38 | git-version = "0.3.4" 39 | internment = { version = "0.6.0", features = ["tinyset"] } 40 | 41 | termcolor = "1.0.5" 42 | atty = "0.2.0" 43 | ctrlc = "3.2.1" 44 | lazy_static = "1.4.0" 45 | 46 | crude-profiler = "^0.1.7" 47 | 48 | [dev-dependencies] 49 | quickcheck = "1.0.3" 50 | 51 | [features] 52 | # Treat warnings as a build error. 53 | strict = [] 54 | 55 | [profile.release] 56 | debug = true 57 | 58 | -------------------------------------------------------------------------------- /bench/independent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | 5 | name='independent' 6 | 7 | def open_and_gitadd(fname): 8 | f = open(fname, 'w') 9 | assert(not os.system('git add '+fname)) 10 | return f 11 | 12 | def create_bench(n): 13 | sconsf = open_and_gitadd('SConstruct') 14 | facf = open_and_gitadd('top.fac') 15 | open_and_gitadd('Tupfile.ini') 16 | sconsf.write(""" 17 | env = Environment() 18 | """) 19 | for i in range(1,n+1): 20 | cname = 'hello_%d.c' % i 21 | with open_and_gitadd(cname) as f: 22 | f.write(r""" 23 | #include 24 | 25 | int main() { 26 | printf("Hello %d\n"); 27 | return 0; 28 | } 29 | """ % i) 30 | facf.write(""" 31 | | gcc -o hello_{i}.exe hello_{i}.c 32 | > hello_{i}.exe 33 | """.format(i=i)) 34 | sconsf.write(""" 35 | env.Program('hello_%d.exe', 'hello_%d.c') 36 | """ % (i, i)) 37 | 38 | verbs = ['building', 'rebuilding', 'modifying-c', 'doing-nothing'] 39 | 40 | def prepare(): 41 | return {'rebuilding': 'rm -f *.exe', 42 | 'modifying-c': 'echo >> hello_7.c'} 43 | 44 | if __name__ == "__main__": 45 | import sys 46 | size = 10 47 | if len(sys.argv) > 1: 48 | size = int(sys.argv[1]) 49 | create_bench(size) 50 | with open("modifying-c.sh", "w") as f: 51 | f.write(prepare()["modifying-c"]) 52 | -------------------------------------------------------------------------------- /tests/rule-creates-git-hook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > my-commit-hook <&2 14 | 15 | if test $(git diff --cached --name-only --diff-filter=A -z HEAD | 16 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 17 | then 18 | echo "Error: Attempt to add a non-ascii file name." 19 | exit 1 20 | fi 21 | 22 | # If there are whitespace errors, print the offending file names and fail. 23 | echo git diff-index --check --cached HEAD -- 24 | exec git diff-index --check --cached HEAD -- 25 | 26 | EOF 27 | 28 | cat > git-hook.fac <> my-commit-hook 49 | 50 | git commit -am 'add nice hooks' 51 | 52 | cat > newfile < foo 10 | echo bar > bar 11 | 12 | cat > top.fac < foobar && echo baz > baz 14 | > baz 15 | c foobar 16 | 17 | | echo foo > /tmp/junk-$$ && echo baz > bar 18 | > bar 19 | 20 | | echo foo > .cache-me && cat .cache-read > localcache 21 | > localcache 22 | C .cache 23 | 24 | | echo bar > foobar && echo foo > foo 25 | > foo 26 | EOF 27 | 28 | echo good > .cache-read 29 | 30 | git init 31 | git add top.fac 32 | 33 | ${FAC:-../../fac} 34 | 35 | cat top.fac.tum 36 | 37 | if egrep '^. \.cache-me' top.fac.tum; then 38 | echo this file should be ignored 39 | exit 1 40 | fi 41 | if egrep '^. \.cache-read' top.fac.tum; then 42 | echo this file should be ignored 43 | exit 1 44 | fi 45 | grep "foobar" top.fac.tum 46 | 47 | 48 | cat > top.fac < foobar && echo baz > baz 50 | > baz 51 | c foobar 52 | 53 | | echo foo > /tmp/junk-$$ && echo baz > bar 54 | > bar 55 | 56 | | echo foo > .cache-me && cat .cache-read > localcache 57 | > localcache 58 | C .cache 59 | 60 | | echo bar > foobar && echo foo > foo 61 | > foo 62 | c oobar 63 | EOF 64 | 65 | rm foo 66 | 67 | ${FAC:-../../fac} 68 | cat top.fac.tum 69 | 70 | 71 | egrep '^. baz' top.fac.tum 72 | 73 | if egrep '^. foobar' top.fac.tum; then 74 | echo this file should be ignored 75 | exit 1 76 | fi 77 | 78 | rm -f /tmp/junk-$$ 79 | 80 | exit 0 81 | -------------------------------------------------------------------------------- /tests/estimate-time.sh.skip-this-too-slow: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | # This test ensures that we can count builds properly to figure out if 8 | # we have built everything correctly. 9 | 10 | rm -rf $0.dir 11 | mkdir $0.dir 12 | cd $0.dir 13 | 14 | mkdir subdir 15 | 16 | for i in `seq 1 48`; do 17 | cat >> top.fac < file_$i.dat 19 | 20 | EOF 21 | done 22 | 23 | git init 24 | git add top.fac 25 | 26 | ../../fac -j4 > fac.out 27 | perl -i -pe 's/\r/\n/' fac.out 28 | cat fac.out 29 | 30 | grep "Build time remaining: 0:0. / 1:0" fac.out 31 | grep "Build succeeded! 1:0" fac.out 32 | 33 | ../../fac -j4 > fac.out 34 | perl -i -pe 's/\r/\n/' fac.out 35 | cat fac.out 36 | 37 | grep "Build succeeded! 0:0" fac.out 38 | 39 | rm *.dat 40 | 41 | ../../fac -j4 > fac.out 42 | perl -i -pe 's/\r/\n/' fac.out 43 | cat fac.out 44 | 45 | grep "Build time remaining: 1:0. / 1:0" fac.out 46 | grep "Build time remaining: 0:05 / 1:0" fac.out 47 | grep "Build succeeded! 1:0" fac.out 48 | 49 | rm file_1.dat file_2.dat file_3.dat file_4.dat 50 | ../../fac -j4 > fac.out 51 | cat fac.out 52 | 53 | grep 1/4 fac.out 54 | grep "Build time remaining: 0:05 / 0:10" fac.out 55 | grep "Build succeeded! 0:05" fac.out 56 | 57 | rm *.dat 58 | 59 | ../../fac -j8 > fac.out 60 | cat fac.out 61 | 62 | grep "Build time remaining: 0:3. / 0:3" fac.out 63 | grep "Build time remaining: 0:0. / 0:3" fac.out 64 | grep "Build succeeded! 0:3" fac.out 65 | 66 | exit 0 67 | -------------------------------------------------------------------------------- /tests/nice-error-message-on-missing-file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | # 1. Fix error message when a file is needed to build another file that 10 | # is needed for a third one, where there are optional rules 11 | # involved. We currently use pretty_rule, but ought to show the 12 | # actual file dependency that leads to the build. 13 | 14 | cat > my.fac < f2 && cat f1 > g1 && cat f1 > g2 16 | > g1 17 | > f2 18 | > g2 19 | < f1 20 | 21 | ? cat f1 > h2 && cat f1 > j1 && gat f1 > j2 22 | > j1 23 | > h2 24 | > j2 25 | < f1 26 | 27 | | cat f2 > f3 28 | > f3 29 | < f2 30 | EOF 31 | 32 | git init 33 | git add my.fac 34 | 35 | if ${FAC:-../../fac} > out; then 36 | cat out 37 | echo build should fail missing file f1 38 | exit 1 39 | fi 40 | 41 | cat out 42 | 43 | # We don't need g1 or g2, we need f2, so that should be mentioned in 44 | # the error message. 45 | grep f2 out 46 | 47 | if grep g1 out; then 48 | echo we should not talk about g1 49 | exit 1 50 | fi 51 | 52 | if grep g2 out; then 53 | echo we should not talk about g2 54 | exit 1 55 | fi 56 | 57 | echo hello > f1 58 | 59 | if ${FAC:-../../fac} > out; then 60 | echo build should fail file f1 is not in git 61 | exit 1 62 | fi 63 | 64 | cat out 65 | 66 | # We don't need g1 or g2, we need f2, so that should be mentioned in 67 | # the error message. 68 | grep f2 out 69 | 70 | if grep g1 out; then 71 | echo we should not talk about g1 72 | exit 1 73 | fi 74 | 75 | if grep g2 out; then 76 | echo we should not talk about g2 77 | exit 1 78 | fi 79 | 80 | git add f1 81 | 82 | ${FAC:-../../fac} -v 83 | 84 | exit 0 85 | -------------------------------------------------------------------------------- /tests/outside-git-repsoitory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | chmod -R +xr /tmp/`whoami`-$$ || echo no such dir 6 | rm -rf /tmp/`whoami`-$$ 7 | mkdir -p /tmp/`whoami`-$$/$0.dir 8 | cd /tmp/`whoami`-$$/$0.dir 9 | 10 | cat > my.fac < out || true 12 | 13 | | mkdir -p directory && echo hello > directory/hello 14 | EOF 15 | 16 | if ${FAC:-../../fac} &> fac.err; then 17 | cat fac.err 18 | echo should have failed here not git 19 | exit 1 20 | fi 21 | 22 | cat fac.err 23 | grep 'Error identifying git top' fac.err 24 | 25 | git init 26 | 27 | if ${FAC:-../../fac} &> fac.err; then 28 | cat fac.err 29 | echo should have failed here no .fac 30 | exit 1 31 | fi 32 | 33 | cat fac.err 34 | grep 'Please .*add a .fac file' fac.err 35 | 36 | git add my.fac 37 | 38 | ${FAC:-../../fac} 39 | 40 | mkdir subdir 41 | cd subdir 42 | 43 | pwd 44 | chmod a-x .. 45 | 46 | if cd ..; then 47 | echo we have some funky broken filesystem here 48 | else 49 | 50 | if ${FAC:-../../fac} &> fac.err; then 51 | cat fac.err 52 | echo should have failed error identifying 53 | exit 1 54 | fi 55 | 56 | cat fac.err 57 | grep 'git' fac.err 58 | 59 | chmod +x .. 60 | 61 | cd .. 62 | fi 63 | 64 | ${FAC:-../../fac} -c 65 | 66 | if ls directory; then 67 | echo directory should have been deleted 68 | exit 1 69 | fi 70 | 71 | chmod a-r .. 72 | 73 | ${FAC:-../../fac} --dry 74 | 75 | ${FAC:-../../fac} 76 | 77 | chmod a+r .. 78 | ls -l 79 | ls directory 80 | 81 | ${FAC:-../../fac} -c 82 | 83 | if ls directory; then 84 | echo directory should have been deleted 85 | exit 1 86 | fi 87 | echo all good 88 | 89 | exit 0 90 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: [ v*.*.* ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | deploy: 12 | name: Deploy 13 | runs-on: ${{ matrix.job.os }} 14 | strategy: 15 | matrix: 16 | job: 17 | - { os: ubuntu-latest , target: x86_64-unknown-linux-musl , use-cross: true } 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Build target 21 | uses: actions-rs/cargo@v1 22 | with: 23 | use-cross: ${{ matrix.job.use-cross }} 24 | command: build 25 | args: --release --target ${{ matrix.job.target }} 26 | 27 | - name: Strip release binary (linux and macOS) 28 | if: matrix.job.os != 'windows-latest' 29 | run: | 30 | if [ "${{ matrix.job.target }}" = "arm-unknown-linux-gnueabihf" ]; then 31 | sudo apt-get -y install gcc-arm-linux-gnueabihf 32 | arm-linux-gnueabihf-strip "target/${{ matrix.job.target }}/release/fac" 33 | else 34 | strip "target/${{ matrix.job.target }}/release/fac" 35 | fi 36 | 37 | - id: get_version 38 | uses: battila7/get-version-action@v2 39 | - name: Package 40 | shell: bash 41 | run: | 42 | bin="target/${{ matrix.job.target }}/release/fac" 43 | staging="fac-${{ steps.get_version.outputs.version }}-${{ matrix.job.target }}" 44 | 45 | mkdir -p "$staging"/doc 46 | cp COPYING README.md $bin $staging 47 | cp web/*.md "$staging"/doc 48 | tar czvf "$staging.tar.gz" $staging 49 | 50 | - name: Publish 51 | uses: softprops/action-gh-release@v1 52 | with: 53 | files: 'fac*' 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | -------------------------------------------------------------------------------- /tests/symlink-handling.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | mkdir subdir1 10 | mkdir subdir2 11 | ln -s subdir1 subdir3 12 | 13 | cat > top.fac < foobar 15 | 16 | | cat foobar > bazbar 17 | < foobar 18 | 19 | | cat subdir1/input > noggin 20 | 21 | | cat subdir2/foo > foo 22 | < subdir2/foo 23 | 24 | | cat subdir3/foo > bar 25 | < subdir3/foo 26 | 27 | | cat subdir3/input > test 28 | 29 | EOF 30 | 31 | cat > subdir1/.fac < foo 33 | EOF 34 | 35 | cat > subdir2/.fac < foo 37 | < ../input 38 | EOF 39 | 40 | echo hello > actual_input 41 | ln -sf actual_input input 42 | echo goodbye > subdir1/input 43 | echo testing > subdir2/input 44 | 45 | git init 46 | git add top.fac subdir1/.fac subdir2/.fac subdir1/input input actual_input subdir3 subdir2/input 47 | 48 | ${FAC:-../../fac} -v 49 | 50 | cat top.fac.tum 51 | 52 | ls -lhR 53 | 54 | grep goodbye noggin 55 | grep goodbye subdir3/foo 56 | grep hello subdir2/foo 57 | grep goodbye subdir1/foo 58 | grep hello bazbar 59 | grep hello foobar 60 | grep goodbye bar 61 | grep goodbye test 62 | 63 | echo HELLO > new_input 64 | git add new_input 65 | ln -sf new_input input 66 | 67 | ${FAC:-../../fac} -v 68 | 69 | grep goodbye noggin 70 | grep goodbye subdir3/foo 71 | grep HELLO subdir2/foo 72 | grep goodbye subdir1/foo 73 | grep HELLO bazbar 74 | grep HELLO foobar 75 | grep goodbye bar 76 | grep goodbye test 77 | 78 | sleep 2 79 | rm subdir3 80 | ln -s subdir2 subdir3 81 | ls -lh subdir3 82 | grep HELLO subdir3/foo 83 | 84 | ${FAC:-../../fac} -vvv 85 | 86 | grep goodbye noggin 87 | grep HELLO subdir3/foo 88 | grep HELLO subdir2/foo 89 | grep goodbye subdir1/foo 90 | grep HELLO bazbar 91 | grep HELLO foobar 92 | grep HELLO bar 93 | grep testing test 94 | 95 | exit 0 96 | -------------------------------------------------------------------------------- /bench/cats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os, hashlib, time, numpy, sys, datetime 4 | 5 | name = 'cat' 6 | 7 | allowed_chars = 'abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ' 8 | 9 | def hashid(n): 10 | m = hashlib.sha1() 11 | m.update(str(n).encode('utf-8')) 12 | name = '' 13 | for i in m.digest()[:24]: 14 | name += allowed_chars[i % len(allowed_chars)] 15 | return name+('_%d' % n) 16 | 17 | def hash_integer(n): 18 | m = hashlib.sha1() 19 | m.update(str(n)) 20 | name = 0 21 | for i in m.digest()[:8]: 22 | name = name*256 + ord(i) 23 | return name 24 | 25 | def open_and_gitadd(fname): 26 | f = open(fname, 'w') 27 | assert(not os.system('git add '+fname)) 28 | return f 29 | 30 | def create_bench(n): 31 | sconsf = open_and_gitadd('SConstruct') 32 | facf = open_and_gitadd('top.fac') 33 | open_and_gitadd('Tupfile.ini') 34 | sconsf.write(""" 35 | env = Environment() 36 | """) 37 | facf.write(""" 38 | | cat %s.txt > final.txt 39 | > final.txt 40 | < %s.txt 41 | 42 | """ % (hashid(n), hashid(n))) 43 | f = open_and_gitadd('%s.txt' % hashid(0)) 44 | f.write("Hello world\n") 45 | for i in range(1,n+1): 46 | sconsf.write(""" 47 | env.Command('%s.txt', '%s.txt', 'cat $SOURCE > $TARGET') 48 | """ % (hashid(i), hashid(i-1))) 49 | facf.write(""" 50 | | cat %s.txt > %s.txt 51 | < %s.txt 52 | > %s.txt 53 | """ % (hashid(i-1), hashid(i), hashid(i-1), hashid(i))) 54 | 55 | verbs = ['building', 'rebuilding', 'doing-nothing'] 56 | 57 | def prepare(): 58 | return {'rebuilding': 'sleep 1 && echo silly > %s.txt' % hashid(0)} 59 | 60 | if __name__ == "__main__": 61 | import sys 62 | size = 10 63 | if len(sys.argv) > 1: 64 | size = int(sys.argv[1]) 65 | create_bench(size) 66 | with open("rebuilding.sh", "w") as f: 67 | f.write(prepare()["rebuilding"]) 68 | -------------------------------------------------------------------------------- /tests/bad-fac-file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > my.fac < foo 11 | 12 | |xThis is a bug 13 | EOF 14 | 15 | git init 16 | git add my.fac 17 | 18 | if ${FAC:-../../fac} &> fac.err; then 19 | cat fac.err 20 | echo build should fail no rule to build bar 21 | exit 1 22 | fi 23 | cat fac.err 24 | 25 | grep 'error:.my.fac.3. Second' fac.err 26 | 27 | 28 | cat > my.fac < foo 32 | EOF 33 | 34 | if ${FAC:-../../fac} &> fac.err; then 35 | cat fac.err 36 | echo build should fail 37 | exit 1 38 | fi 39 | cat fac.err 40 | 41 | grep 'error: my.fac:1: .C..* line' fac.err 42 | 43 | 44 | cat > my.fac < foo 49 | EOF 50 | 51 | if ${FAC:-../../fac} &> fac.err; then 52 | cat fac.err 53 | echo build should fail 54 | exit 1 55 | fi 56 | cat fac.err 57 | 58 | grep 'error: my.fac:2: .<.* line' fac.err 59 | 60 | 61 | cat > my.fac < cache 64 | 65 | | echo foo > foo 66 | EOF 67 | 68 | if ${FAC:-../../fac} &> fac.err; then 69 | cat fac.err 70 | echo build should fail 71 | exit 1 72 | fi 73 | cat fac.err 74 | 75 | grep 'error: my.fac:2: .>.* line' fac.err 76 | 77 | 78 | cat > my.fac < foo 83 | EOF 84 | 85 | if ${FAC:-../../fac} &> fac.err; then 86 | cat fac.err 87 | echo build should fail 88 | exit 1 89 | fi 90 | cat fac.err 91 | 92 | grep 'error: my.fac:2: .c.* line' fac.err 93 | 94 | chmod a-r my.fac 95 | ls -l my.fac 96 | 97 | if wc my.fac; then 98 | echo we have some funky broken filesystem here 99 | else 100 | 101 | if ${FAC:-../../fac} &> fac.err; then 102 | cat fac.err 103 | echo build should fail 104 | exit 1 105 | fi 106 | echo cat fac.err 107 | cat fac.err 108 | 109 | grep 'error: unable to open file' fac.err 110 | fi 111 | 112 | chmod +r my.fac 113 | 114 | exit 0 115 | -------------------------------------------------------------------------------- /tests/codegen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < main.out 12 | 13 | | mkdir -p lib.out 14 | > lib.out 15 | 16 | | python main.py > main.out/main.fac 17 | < main.out 18 | < main 19 | > main.out/main.fac 20 | 21 | | python lib.py > lib.out/lib.fac 22 | < lib.out 23 | < lib 24 | > lib.out/lib.fac 25 | EOF 26 | 27 | cat > lib.py < %s 42 | ''' % (src, obj, src, obj)) 43 | 44 | print('''| ar rcs lib.a %s 45 | < %s 46 | > lib.a 47 | ''' % (' '.join(objs), '\n< '.join(objs))) 48 | EOF 49 | cat > main.py < %s 64 | ''' % (src, obj, src, obj)) 65 | 66 | print('''| gcc -o main %s ../lib.out/lib.a 67 | < %s 68 | < ../lib.out/lib.a 69 | > main 70 | ''' % (' '.join(objs), '\n< '.join(objs))) 71 | EOF 72 | 73 | mkdir inc 74 | cat > inc/lib.h < lib/foo.c < 81 | int foo = 1; 82 | EOF 83 | 84 | mkdir main 85 | cat > main/main.c < 87 | #include 88 | int main() { 89 | printf("success\n"); 90 | return 0; 91 | } 92 | EOF 93 | 94 | git init 95 | git add . 96 | 97 | ${FAC:-../../fac} 98 | ${FAC:-../../fac} 99 | 100 | cat >> inc/lib.h < lib/bar.c < 106 | int bar = 2; 107 | EOF 108 | git add lib/bar.c 109 | 110 | ${FAC:-../../fac} 111 | ${FAC:-../../fac} 112 | 113 | main.out/main 114 | 115 | exit 0 116 | -------------------------------------------------------------------------------- /web/flags.md: -------------------------------------------------------------------------------- 1 | # Running fac 2 | 3 | $docnav 4 | 5 | To run fac, you simply execute 6 | 7 | fac [options] [filenames] 8 | 9 | with the following options. 10 | 11 | `--version` 12 | : Display the version number of fac. 13 | 14 | `--jobs=N, -jN` 15 | : Specify the number of jobs to run simultaneousy. This defaults to 16 | the number of processors available on your computer. 17 | 18 | `--continual` 19 | : Keep rebuilding whenever the source is modified. 20 | 21 | `--clean, -c` 22 | : Clean up build output. This deletes every file (but not directory) 23 | that is output by the build. 24 | 25 | `--git-add` 26 | : Run `git add -- PATH` on any files that fac determines are needed 27 | for the build. This is naturally a somewhat risky maneauver, but 28 | can be convenient if you know you have created a bunch of new input 29 | files. 30 | 31 | `--strict` 32 | : Make fac insist that the facfile specifies sufficient inputs to the 33 | build to determine a correct build order. 34 | 35 | `--exhaustive` 36 | : Make fac insist that *every* input for every rule is specified. 37 | 38 | `--blind` 39 | : Disable the automatic tracking of dependencies. This is useful for 40 | testing, for benchmarking, and possibly for improving build speed 41 | in cases where `--exhaustive has indicated that we have documented 42 | all dependencies (and hope none will change). 43 | 44 | `--verbose, -v` 45 | : Provide extra debugging output. 46 | 47 | `--show-output, -V` 48 | : Show the output of every command (stdout and stderr), even if that 49 | command succeeds. 50 | 51 | `--log-output LOG_DIRECTORY` 52 | : Save the output of every command (both stdout and stderr) to a 53 | separate file in the directory `LOG_DIRECTORY`, which will be created if 54 | it does not yet exist. 55 | 56 | `--makefile MAKEFILE` 57 | : After building, create a makefile with name MAKEFILE, which can be 58 | used to perform this build if fac is unavailable. 59 | 60 | `--script BUILD.SH` 61 | : After building, create a shell script with name BUILD.SH, which can 62 | be used to perform this build if fac is unavailable. 63 | 64 | `--tupfile TUPFILE` 65 | : After building, create a tupfile, which can be used to perform this 66 | build if fac is unavailable. 67 | -------------------------------------------------------------------------------- /tests/subtle-rule-generates-varying-stuff.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | rm -rf $0.dir 6 | mkdir $0.dir 7 | cd $0.dir 8 | 9 | cat > top.fac < subtle-rule.sh < foo 18 | fi 19 | 20 | EOF 21 | 22 | git init 23 | git add top.fac subtle-rule.sh 24 | 25 | ${FAC:-../../fac} 26 | 27 | grep foo foo 28 | 29 | grep foo top.fac.tum | grep '>' 30 | 31 | cat > subtle-rule.sh < foo 35 | fi 36 | 37 | if ! test -e bar; then 38 | echo bar > bar 39 | fi 40 | 41 | EOF 42 | 43 | ${FAC:-../../fac} -vv 44 | 45 | grep foo top.fac.tum | grep '>' 46 | grep bar top.fac.tum | grep '>' 47 | 48 | grep foo foo 49 | grep bar bar 50 | 51 | # Here is a build rule that sadly is not idempotent: 52 | 53 | cat > subtle-rule.sh < foo 57 | fi 58 | 59 | if ! test -e bar; then 60 | echo bar > bar 61 | fi 62 | 63 | EOF 64 | 65 | ${FAC:-../../fac} -vv 66 | 67 | grep foo top.fac.tum | grep '>' 68 | grep bar top.fac.tum | grep '>' 69 | 70 | grep foo foo 71 | grep bar bar 72 | 73 | rm foo 74 | 75 | ${FAC:-../../fac} 76 | 77 | grep foo top.fac.tum | grep '>' 78 | grep bar top.fac.tum | grep '>' 79 | 80 | grep wrong foo 81 | grep bar bar 82 | 83 | # Now the subtle-rule.sh no longer will generate foo. 84 | 85 | cat > subtle-rule.sh < bar 89 | fi 90 | 91 | EOF 92 | 93 | ${FAC:-../../fac} --show-output 94 | 95 | # Fac continues to believe that the rule will generate foo, so long as 96 | # foo still exists. 97 | 98 | grep foo top.fac.tum | grep '>' 99 | grep bar top.fac.tum | grep '>' 100 | 101 | grep wrong foo 102 | grep bar bar 103 | 104 | rm foo 105 | 106 | ${FAC:-../../fac} --show-output 107 | 108 | # Once foo is deleted and the command does not regenerate it, fac 109 | # should remove it from the output list. 110 | 111 | if grep foo top.fac.tum | grep '>'; then 112 | echo Oh no this is bad 113 | exit 1 114 | fi 115 | grep bar top.fac.tum | grep '>' 116 | 117 | grep bar bar 118 | 119 | exit 0 120 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Based on the "trust" template v0.1.1 2 | # https://github.com/japaric/trust/tree/v0.1.1 3 | 4 | dist: trusty 5 | language: rust 6 | services: docker 7 | sudo: required 8 | 9 | # Rust builds on stable by default, this can be overridden on a case 10 | # by case basis down below. 11 | 12 | env: 13 | global: 14 | - CRATE_NAME=fac 15 | 16 | matrix: 17 | include: 18 | # Linux 19 | - env: TARGET=i686-unknown-linux-gnu 20 | # - env: TARGET=i686-unknown-linux-musl DISABLE_TESTS=1 21 | - env: TARGET=x86_64-unknown-linux-gnu 22 | # - env: TARGET=x86_64-unknown-linux-musl 23 | 24 | # OSX 25 | # - env: TARGET=x86_64-apple-darwin 26 | # os: osx 27 | 28 | # *BSD 29 | # - env: TARGET=i686-unknown-freebsd DISABLE_TESTS=1 30 | # - env: TARGET=x86_64-unknown-freebsd DISABLE_TESTS=1 31 | # - env: TARGET=x86_64-unknown-netbsd DISABLE_TESTS=1 32 | 33 | # Other architectures unsupported by bigbro 34 | # - env: TARGET=aarch64-unknown-linux-gnu 35 | # - env: TARGET=armv7-unknown-linux-gnueabihf 36 | # - env: TARGET=mips-unknown-linux-gnu 37 | # - env: TARGET=mips64-unknown-linux-gnuabi64 38 | # - env: TARGET=mips64el-unknown-linux-gnuabi64 39 | # - env: TARGET=mipsel-unknown-linux-gnu 40 | # - env: TARGET=powerpc-unknown-linux-gnu 41 | # - env: TARGET=powerpc64-unknown-linux-gnu 42 | # - env: TARGET=powerpc64le-unknown-linux-gnu 43 | # - env: TARGET=s390x-unknown-linux-gnu DISABLE_TESTS=1 44 | 45 | # Testing other channels 46 | - env: TARGET=x86_64-unknown-linux-gnu 47 | rust: nightly 48 | # - env: TARGET=x86_64-apple-darwin 49 | # os: osx 50 | # rust: nightly 51 | 52 | before_install: set -e 53 | 54 | install: 55 | # - sh ci/install.sh 56 | - sudo apt-get install -y libc6-dev-i386 57 | - rustup target add $TARGET || true # fails if target is already added? 58 | - source ~/.cargo/env || true 59 | 60 | script: 61 | - bash ci/script.sh 62 | 63 | after_script: set +e 64 | 65 | cache: cargo 66 | before_cache: 67 | # Travis can't cache files that are not readable by "others" 68 | - chmod -R a+r $HOME/.cargo 69 | 70 | branches: 71 | only: 72 | # release tags 73 | - /^v\d+\.\d+\.\d+.*$/ 74 | - master 75 | 76 | notifications: 77 | email: 78 | on_success: never 79 | -------------------------------------------------------------------------------- /tests/iterable_hash_test.c: -------------------------------------------------------------------------------- 1 | #include "../lib/iterablehash.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | struct hash_table a; 9 | 10 | void has(const char *string, int line) { 11 | if (!lookup_in_hash(&a, string)) { 12 | printf("FAIL Has not %s (line %d)!!!\n", string, line); 13 | exit(1); 14 | } 15 | if (lookup_in_hash(&a, string)->key != string) { 16 | printf("FAIL has wrong pointer %s (line %d)!!!\n", string, line); 17 | exit(1); 18 | } 19 | } 20 | 21 | void hasnot(const char *string, int line) { 22 | if (lookup_in_hash(&a, string)) { 23 | printf("FAIL Has %s (line %d)!!!\n", string, line); 24 | exit(1); 25 | } 26 | } 27 | 28 | void add(const char *string) { 29 | printf("adding %s\n", string); 30 | struct hash_entry *e = malloc(sizeof(struct hash_entry)); 31 | e->key = string; 32 | add_to_hash(&a, e); 33 | } 34 | 35 | int main(int argc, char **argv) { 36 | init_hash_table(&a, 5); 37 | 38 | assert(a.num_entries == 0); 39 | 40 | hasnot("foo", __LINE__); 41 | hasnot("foobar", __LINE__); 42 | hasnot("bar", __LINE__); 43 | hasnot("", __LINE__); 44 | 45 | add("foo"); 46 | 47 | assert(a.num_entries == 1); 48 | 49 | has("foo", __LINE__); 50 | hasnot("foobar", __LINE__); 51 | hasnot("bar", __LINE__); 52 | hasnot("", __LINE__); 53 | 54 | add("bar"); 55 | 56 | assert(a.num_entries == 2); 57 | 58 | has("foo", __LINE__); 59 | hasnot("foobar", __LINE__); 60 | has("bar", __LINE__); 61 | hasnot("", __LINE__); 62 | 63 | { 64 | int count_entries = 0; 65 | for (struct hash_entry *e = a.first; e; e = e->next) { 66 | count_entries++; 67 | } 68 | assert(count_entries == a.num_entries); 69 | } 70 | 71 | add(""); 72 | 73 | assert(a.num_entries == 3); 74 | 75 | has("foo", __LINE__); 76 | hasnot("foobar", __LINE__); 77 | has("bar", __LINE__); 78 | has("", __LINE__); 79 | 80 | add("foobar"); 81 | 82 | assert(a.num_entries == 4); 83 | 84 | has("foo", __LINE__); 85 | has("foobar", __LINE__); 86 | has("bar", __LINE__); 87 | has("", __LINE__); 88 | 89 | { 90 | int count_entries = 0; 91 | for (struct hash_entry *e = a.first; e; e = e->next) { 92 | count_entries++; 93 | } 94 | assert(count_entries == a.num_entries); 95 | } 96 | 97 | return 0; 98 | } 99 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Based on the "trust" template v0.1.1 2 | # https://github.com/japaric/trust/tree/v0.1.1 3 | 4 | environment: 5 | global: 6 | RUST_VERSION: stable 7 | 8 | CRATE_NAME: fac 9 | 10 | matrix: 11 | # MinGW 12 | - TARGET: i686-pc-windows-gnu 13 | - TARGET: x86_64-pc-windows-gnu 14 | 15 | # MSVC 16 | - TARGET: i686-pc-windows-msvc 17 | - TARGET: x86_64-pc-windows-msvc 18 | 19 | # Testing other channels 20 | - TARGET: x86_64-pc-windows-gnu 21 | RUST_VERSION: nightly 22 | - TARGET: x86_64-pc-windows-msvc 23 | RUST_VERSION: nightly 24 | 25 | # init: 26 | # - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 27 | 28 | # on_finish: 29 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 30 | 31 | install: 32 | - ps: >- 33 | If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') { 34 | $Env:PATH += ';C:\msys64\mingw64\bin' 35 | } ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') { 36 | $Env:PATH += ';C:\msys64\mingw32\bin' 37 | } 38 | - curl -sSf -o rustup-init.exe https://win.rustup.rs/ 39 | - rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_VERSION% 40 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 41 | - rustc -Vv 42 | - cargo -V 43 | 44 | test_script: 45 | # we don't run the "test phase" when doing deploys 46 | - if [%APPVEYOR_REPO_TAG%]==[false] ( 47 | cargo build --target %TARGET% --features strict && 48 | cargo build --target %TARGET% --release && 49 | cargo test --target %TARGET% --features strict && 50 | cargo test --target %TARGET% --release 51 | ) 52 | 53 | cache: 54 | - C:\Users\appveyor\.cargo\registry 55 | - target 56 | 57 | branches: 58 | only: 59 | # Release tags 60 | - /^v\d+\.\d+\.\d+.*$/ 61 | - master 62 | 63 | notifications: 64 | - provider: Email 65 | on_build_success: false 66 | 67 | # Building is done in the test phase, so we disable Appveyor's build phase. 68 | build: false 69 | 70 | 71 | # environment: 72 | # matrix: 73 | # - TARGET: x86_64-pc-windows-msvc 74 | # ARCH: amd64 75 | 76 | # build_script: 77 | # - SET PATH=C:\python35;%PATH% 78 | # - set PATH=%PATH%;C:\msys64\mingw64\bin 79 | # - set PATH=%PATH%;C:\msys64\mingw32\bin 80 | # - python -u build/windows.py 81 | 82 | # branches: 83 | # only: 84 | # - master 85 | -------------------------------------------------------------------------------- /tests/build-tarball.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ev 4 | 5 | echo $0 6 | 7 | rm -rf $0.dir 8 | mkdir $0.dir 9 | cd $0.dir 10 | 11 | cat > top.fac < baz 13 | 14 | | grep nice foo/dir/bar > foo/nice 15 | 16 | | cp foo/nice foo/nice1 17 | < foo/nice 18 | 19 | | cp baz baz2 20 | < baz 21 | 22 | | cp baz2 baz3 23 | < baz2 24 | EOF 25 | 26 | mkdir foo 27 | 28 | cat > foo/foo.fac < foo/bar 33 | mkdir foo/dir 34 | echo mean > foo/dir/bar 35 | echo nice >> foo/dir/bar 36 | 37 | git init 38 | git add top.fac foo/foo.fac 39 | 40 | ${FAC:-../../fac} --git-add 41 | 42 | grep bar baz 43 | grep nice foo/nice 44 | grep bar baz3 45 | 46 | ${FAC:-../../fac} baz3 foo/nice1 47 | 48 | ${FAC:-../../fac} --tar fun.tar.gz --script build.sh --tupfile Tupfile --makefile Makefile 49 | 50 | tar zxvf fun.tar.gz 51 | 52 | cd fun 53 | sh build.sh 54 | grep bar baz 55 | grep nice foo/nice 56 | 57 | cd .. 58 | rm -rf fun 59 | 60 | if which make; then 61 | tar zxvf fun.tar.gz 62 | 63 | cd fun 64 | make 65 | grep bar baz 66 | grep nice foo/nice 67 | 68 | cd .. 69 | rm -rf fun 70 | fi 71 | 72 | if which tup; then 73 | tar zxvf fun.tar.gz 74 | 75 | cd fun 76 | cat Tupfile 77 | tup init 78 | tup 79 | grep bar foo/bar 80 | grep nice foo/dir/bar 81 | grep bar baz 82 | grep nice foo/nice 83 | 84 | cd .. 85 | rm -rf fun 86 | fi 87 | 88 | # the following is hokey, we wouldn't really do this 89 | touch Tupfile.ini 90 | 91 | ${FAC:-../../fac} -v --include-in-tar Tupfile.ini --tar fun.tar.gz --tupfile Tupfile --dotfile fun.dot 92 | 93 | tar zxvf fun.tar.gz 94 | cd fun 95 | cat Tupfile.ini 96 | 97 | if grep bar baz; then 98 | echo baz should not be in the tarball! 99 | exit 1 100 | fi 101 | if grep nice nice; then 102 | echo nice should not be in the tarball! 103 | exit 1 104 | fi 105 | 106 | if which tup; then 107 | 108 | ls -lh --recursive 109 | cat Tupfile 110 | tup init 111 | tup 112 | grep bar foo/bar 113 | grep nice foo/dir/bar 114 | grep bar baz 115 | grep nice foo/nice 116 | fi 117 | 118 | cd .. 119 | rm -rf fun 120 | 121 | # now just test a few extensions: 122 | ${FAC:-../../fac} --tar fun.tgz 123 | tar ztf fun.tgz 124 | ${FAC:-../../fac} --tar fun.tar.bz2 125 | tar jtf fun.tar.bz2 126 | ${FAC:-../../fac} --tar fun.tar 127 | tar tf fun.tar 128 | 129 | exit 0 130 | -------------------------------------------------------------------------------- /web/fac-vs-scons.md: -------------------------------------------------------------------------------- 1 | # Comparison with SCons 2 | 3 | ## A story of a bug 4 | 5 | I was switching my research code from SCons to fac, and I ran into the 6 | following error: 7 | 8 | 4/92 [0.57s]: python2 figs/w2-comparison.py 9 | Traceback (most recent call last):8 10 | File "figs/w2-comparison.py", line 12, in 11 | import wca_erf 12 | ImportError: No module named wca_erf 13 | 14 | This error surprised me. We had been compiling this code regularly, 15 | and never ran into trouble. I looked in the directory, and there was 16 | no sign of any `wca_erf.py` file. I looked in my student's directory, 17 | and there was a backup file `wca_erf.py~`, and a compiled 18 | `wca_erf.pyc` file, but again no sign of the source code itself. 19 | Finally, I thought to search in our git history: 20 | 21 | $ git log --stat wca_erf.py 22 | commit d4b7bd085eab4aff554802ff63336c6f407af5ef 23 | Author: [redacted] 24 | Date: Wed Dec 3 11:36:06 2014 -0800 25 | 26 | remove unused and wrong code 27 | 28 | papers/.../wca_erf.py | 16 ---------------- 29 | 1 file changed, 16 deletions(-) 30 | 31 | So this module was deleted when a bug was found in it, and we didn't 32 | notice that it was still required in order to run two of our scripts 33 | that generate plots for our paper. For three months, we didn't 34 | notice. How did this happen? 35 | 36 | The problem was that we had enabled scons caching. This is a major 37 | feature of scons, that it can cache the results of builds. This is 38 | very helpful, as a complete build of our code takes close to an hour. 39 | Scons uses checksums to verify whether the inputs are identical to the 40 | previous build, including things you might not think of like 41 | environment variables, so this is supposed to be safe. However, scons 42 | relies on the user (or clever scripts) to determine the dependencies, 43 | and in this case our scripts failed to recognize this import as 44 | introducing a dependency on the source file, with the result that when 45 | the file was deleted, scons continued to believe--so long as we didn't 46 | modify the scripts that import it--that it knew the result of running 47 | those scripts and didn't have to try running them. So the build 48 | continued succeeding, and would have continued doing so until some 49 | poor soul tried to build the code in an account that didn't have this 50 | file cached. 51 | 52 | I have not yet implemented caching for fac, but when I do, it will be 53 | safe from this sort of error, because fac will *know* the *true* 54 | dependencies of each rule. 55 | -------------------------------------------------------------------------------- /bench/sleepy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os, hashlib, time, numpy, sys 4 | 5 | name = 'sleepy' 6 | 7 | allowed_chars = 'abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ' 8 | 9 | def hashid(n): 10 | m = hashlib.sha1() 11 | m.update(str(n).encode('utf-8')) 12 | name = '' 13 | for i in m.digest()[:24]: 14 | name += allowed_chars[i % len(allowed_chars)] 15 | return name+('_%d' % n) 16 | 17 | def hash_integer(n): 18 | m = hashlib.sha1() 19 | m.update(str(n)) 20 | name = 0 21 | for i in m.digest()[:8]: 22 | name = name*256 + ord(i) 23 | return name 24 | 25 | def open_and_gitadd(fname): 26 | f = open(fname, 'w') 27 | assert(not os.system('git add '+fname)) 28 | return f 29 | 30 | def create_bench(n): 31 | longsleep = max((n-9)//3, 0) 32 | shortsleep = 1 33 | if longsleep == 0: 34 | shortsleep = 0 35 | final_indices = range(1,n+1,3) 36 | long_indices = [final_indices[0], 37 | final_indices[(n//8) % len(final_indices)], 38 | final_indices[(n//4) % len(final_indices)]] 39 | finalfiles = [hashid(i)+'.txt' for i in final_indices] 40 | 41 | sconsf = open_and_gitadd('SConstruct') 42 | facf = open_and_gitadd('top.fac') 43 | open_and_gitadd('Tupfile.ini') 44 | sconsf.write(""" 45 | env = Environment() 46 | 47 | env.Command('final.txt', ['%s'], 'sleep 1 && cat $SOURCE > $TARGET') 48 | """ % ("', '".join(finalfiles))) 49 | facf.write(""" 50 | | cat %s > final.txt 51 | > final.txt 52 | < %s 53 | 54 | """ % (' '.join(finalfiles), "\n< ".join(finalfiles))) 55 | for i in final_indices: 56 | sleepiness = shortsleep 57 | if i in long_indices: 58 | sleepiness = longsleep 59 | facf.write(""" 60 | | sleep %d && cat %s.txt > %s.txt 61 | < %s.txt 62 | > %s.txt 63 | | sleep %d && cat %s.txt > %s.txt 64 | < %s.txt 65 | > %s.txt 66 | | sleep %d && echo %s > %s.txt 67 | > %s.txt 68 | """ % (sleepiness, hashid(i+1), hashid(i), hashid(i+1), hashid(i), 69 | sleepiness, hashid(i+2), hashid(i+1), hashid(i+2), hashid(i+1), 70 | sleepiness, hashid(i), hashid(i+2), hashid(i+2))) 71 | sconsf.write(""" 72 | env.Command('%s.txt', '%s.txt', 'sleep %d && cat $SOURCE > $TARGET') 73 | env.Command('%s.txt', '%s.txt', 'sleep %d && cat $SOURCE > $TARGET') 74 | env.Command('%s.txt', [], 'sleep %d && echo %s > $TARGET') 75 | """ % (hashid(i), hashid(i+1), sleepiness, 76 | hashid(i+1), hashid(i+2), sleepiness, 77 | hashid(i+2), sleepiness, hashid(i))) 78 | 79 | verbs = ['building', 'rebuilding'] 80 | 81 | def prepare(): 82 | return {'rebuilding': 'rm -f *.txt'} 83 | -------------------------------------------------------------------------------- /web/index.md: -------------------------------------------------------------------------------- 1 | # Fac build system 2 | 3 | Fac is a general-purpose build system inspired by make that utilizes 4 | ptrace to ensure that all dependences are enumerated and that all 5 | source files are added to a (git) repo. An important feature of fac 6 | is that it automatically handles dependencies, rather than either 7 | complaining about them or giving an incorrect build. Currently, fac 8 | only runs on linux systems, but on those systems it is incredibly easy 9 | to use! 10 | 11 | Fac 12 | 13 | * Automatically tracks build dependencies in a way that is independent 14 | of programming language. You are only required to specify the 15 | minimum of dependencies for each rule (which is often an empty set), 16 | and fac works out the rest for you. If you fail to specify 17 | dependencies, fac should still build successfully if you use it 18 | repeatedly. 19 | 20 | * Parallel building. 21 | 22 | * You are forced to write your configuration in a language of your own 23 | choice. (Or conversely, you are not forced to use a language of 24 | *my* choice, much less a custom-built language that I developed.) 25 | 26 | * Integrates with git, to keep you from forgetting to `git add` a file 27 | that is needed for the build. 28 | 29 | ## How does it work? 30 | 31 | - Fac uses ptrace to track every system call your build command makes. 32 | Thus we can see precisely which files are read, and which files are 33 | modified. 34 | 35 | - Fac has an extremely simple declarative 36 | [file format](documentation.html). There are no variables, no 37 | functions, no macros. Just data. This could be a problem for 38 | larger projects if you were forced to write these files by hand. 39 | But in most large projects you will just write a script to generate 40 | these files. 41 | 42 | - You write your "build" script as a program (in the language of your 43 | choice) that creates a `.fac` file. This script is run (and 44 | re-run) using the same dependency-tracking mechanism that is used to 45 | for an ordinary build. Thus, you can get away with writing a simple 46 | but inefficient script, since it will only seldom be run. (Unlike, 47 | e.g. `scons` which has to rerun your `SConstruct` python file on 48 | every build.) 49 | 50 | - If your build rules depend on the operating system, or the system 51 | environment, your "configure" script is the same program (in the 52 | language of your choice) that creates a `.fac` file. Or perhaps 53 | it provides input to the script that actually creates the `.fac` 54 | file. 55 | 56 | For a lengthier introduction and motivation for fac, see 57 | [Introducing fac](introducing-fac.html). 58 | -------------------------------------------------------------------------------- /bench/dependentchains.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os, hashlib, time, sys, datetime 4 | 5 | name='dependent-chain' 6 | 7 | allowed_chars = 'abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ' 8 | 9 | def hashid(n): 10 | m = hashlib.sha1() 11 | m.update(str(n).encode('utf-8')) 12 | name = '' 13 | for i in m.digest()[:24]: 14 | name += allowed_chars[i % len(allowed_chars)] 15 | return name+('_%d' % n) 16 | 17 | def open_and_gitadd(fname): 18 | f = open(fname, 'w') 19 | assert(not os.system('git add '+fname)) 20 | return f 21 | 22 | def create_bench(n): 23 | sconsf = open_and_gitadd('SConstruct') 24 | facf = open_and_gitadd('top.fac') 25 | open_and_gitadd('Tupfile.ini') 26 | sconsf.write(""" 27 | env = Environment(CPPPATH=['.']) 28 | """) 29 | facf.write(""" 30 | | gcc -o final.exe final.c 31 | > final.exe 32 | < final.c 33 | < %s-generated.h 34 | 35 | """ % hashid(n)) 36 | f = open_and_gitadd('final.c') 37 | f.write(""" 38 | #include "%s-generated.h" 39 | #include 40 | 41 | void main() { 42 | printf("%%s\\n", %s); 43 | } 44 | """ % (hashid(n), hashid(n))) 45 | f.close() 46 | f = open_and_gitadd('%s-generated.h' % hashid(0)) 47 | f.write(""" 48 | const char *%s = "Hello world"; 49 | 50 | """ % hashid(0)) 51 | f.close() 52 | for i in range(1,n+1): 53 | cname = hashid(i) + '.c' 54 | hname = hashid(i) + '-generated.h' 55 | f = open_and_gitadd('%s.c' % hashid(i)) 56 | f.write(""" 57 | #include 58 | 59 | int main() { 60 | printf("const char *%s = \\"Hello world\\";\\n"); 61 | return 0; 62 | } 63 | 64 | """ % hashid(i)) 65 | f.close() 66 | facf.write(""" 67 | # %d 68 | | gcc -o %s.exe %s.c 69 | > %s.exe 70 | < %s-generated.h 71 | 72 | | ./%s.exe > %s-generated.h 73 | > %s-generated.h 74 | < %s.exe 75 | """ % (i, hashid(i), hashid(i), hashid(i), hashid(i-1), 76 | hashid(i), hashid(i), hashid(i), hashid(i))) 77 | sconsf.write(""" 78 | env.Program('%s.exe', '%s.c') 79 | env.Command('%s-generated.h', '%s.exe', './$SOURCE > $TARGET') 80 | """ % (hashid(i), hashid(i), hashid(i), hashid(i))) 81 | 82 | verbs = ['building', 'rebuilding', 'modifying-header', 'modifying-c', 'doing-nothing'] 83 | 84 | def prepare(): 85 | return {'rebuilding': 'rm -f *.exe', 86 | 'modifying-header': 'echo >> %s-generated.h' % hashid(0), 87 | 'modifying-c': 'echo >> final.c'} 88 | 89 | if __name__ == "__main__": 90 | import sys 91 | size = 10 92 | if len(sys.argv) > 1: 93 | size = int(sys.argv[1]) 94 | create_bench(size) 95 | with open("modifying-header.sh", "w") as f: 96 | f.write(prepare()["modifying-header"]) 97 | -------------------------------------------------------------------------------- /web/mkdown.py: -------------------------------------------------------------------------------- 1 | 2 | import re, string, os, glob 3 | import markdown as mmdd 4 | 5 | import xml.etree.ElementTree as ET 6 | 7 | 8 | def mkdown(mdfile): 9 | htfile = mdfile[:-2]+'html' 10 | 11 | f = open(mdfile, 'r') 12 | mkstr = f.read() 13 | f.close() 14 | 15 | f = open('web/sidebar.md', 'r') 16 | sidebar = f.read() 17 | f.close() 18 | 19 | f = open('web/docnav.md', 'r') 20 | docnav = '' 21 | f.close() 22 | 23 | f = open('web/template.html', 'r') 24 | templatestr = f.read() 25 | f.close() 26 | 27 | titlere = re.compile(r"^\s*#\s*([^\n]*)(.*)", re.DOTALL) 28 | title = titlere.findall(mkstr) 29 | if len(title) == 0: 30 | title = "Fac" 31 | else: 32 | mkstr = title[0][1] 33 | title = mmdd.markdown(title[0][0]) 34 | title = title[3:len(title)-4] 35 | if title[:4] == "Fac ": 36 | pagetitle = title[4:] 37 | else: 38 | pagetitle = title 39 | 40 | if '$docnav' in mkstr: 41 | templatestr = templatestr.replace('$docnav', docnav) 42 | mkstr = mkstr.replace('$docnav', '') 43 | else: 44 | templatestr = templatestr.replace('$docnav', '') 45 | 46 | template = string.Template(templatestr) 47 | 48 | f = open(htfile, 'w') 49 | myhtml = template.safe_substitute( 50 | title = title, 51 | pagetitle = pagetitle, 52 | #content = mmdd.markdown(mkstr, extensions=['mathjax']), 53 | #sidebar = mmdd.markdown(sidebar, extensions=['mathjax']))) 54 | content = mmdd.markdown(mkstr, extensions=['def_list']), 55 | sidebar = mmdd.markdown(sidebar, extensions=['def_list'])) 56 | myhtml = myhtml.replace('
  • thelog 21 | 22 | # The git gc below edits the contents of the .git/ directory, and can 23 | # cause fac to believe that these are "output" files. This screws 24 | # everything up when we run fac -c and fac deletes some of git's 25 | # internal database. A solution (maybe not the best) is to have fac 26 | # blacklist out files in .git/ (except maybe the hooks). 27 | 28 | * sleep 2 && git gc && git log --stat > thelogstat 29 | 30 | * git status > thestatus 31 | EOF 32 | 33 | cat > README < .gitignore < junk < top.fac < foo 16 | 17 | | cat foo extra > bar 18 | 19 | | cp foo baz 20 | < foo 21 | EOF 22 | 23 | echo extra > extra 24 | 25 | git init 26 | git add top.fac extra 27 | 28 | if ${FAC:-../../fac} --exhaustive &> fac.out; then 29 | cat fac.out 30 | echo this should fail for one reason or another 31 | exit 1 32 | fi 33 | cat fac.out 34 | 35 | if ${FAC:-../../fac} --exhaustive &> fac.out; then 36 | cat fac.out 37 | echo this should fail due to exhaustive strictness 38 | exit 1 39 | fi 40 | 41 | cat fac.out 42 | grep 'missing dependency' fac.out 43 | grep 'requires' fac.out 44 | 45 | cat > top.fac < foo 47 | 48 | | cat foo extra > bar 49 | < foo 50 | 51 | | cp foo baz 52 | < foo 53 | EOF 54 | 55 | if ${FAC:-../../fac} --exhaustive &> fac.out; then 56 | cat fac.out 57 | echo this should fail due to exhaustive strictness 58 | exit 1 59 | fi 60 | 61 | cat fac.out 62 | grep 'missing dependency' fac.out 63 | grep 'requires extra' fac.out 64 | 65 | cat > top.fac < foo 67 | 68 | | cat foo extra > bar 69 | < extra 70 | 71 | | cp foo baz 72 | < foo 73 | EOF 74 | 75 | if ${FAC:-../../fac} --exhaustive &> fac.out; then 76 | cat fac.out 77 | echo this should fail due to exhaustive strictness 78 | exit 1 79 | fi 80 | 81 | cat fac.out 82 | grep 'missing dependency' fac.out 83 | grep 'requires foo' fac.out 84 | 85 | cat > top.fac < foo 87 | 88 | | cat foo extra > bar 89 | < foo 90 | < extra 91 | 92 | | cp foo baz 93 | < foo 94 | EOF 95 | 96 | if ${FAC:-../../fac} --exhaustive &> fac.out; then 97 | cat fac.out 98 | echo this should fail due to exhaustive strictness 99 | exit 1 100 | fi 101 | 102 | cat fac.out 103 | grep 'missing output' fac.out 104 | grep 'builds foo' fac.out 105 | grep 'failed due to missing outputs' fac.out 106 | 107 | cat > top.fac < foo 109 | > foo 110 | 111 | | cat foo extra > bar 112 | < foo 113 | < extra 114 | 115 | | cp foo baz 116 | < foo 117 | EOF 118 | 119 | ${FAC:-../../fac} --exhaustive 120 | 121 | grep foo bar 122 | 123 | # now that we have exhaustive knowledge, we should be able to run blind! 124 | 125 | rm bar foo baz 126 | 127 | ${FAC:-../../fac} --blind 128 | 129 | grep foo foo 130 | grep foo bar 131 | grep foo baz 132 | 133 | ${FAC:-../../fac} --clean --blind 134 | 135 | ${FAC:-../../fac} --blind 136 | 137 | grep foo foo 138 | grep foo bar 139 | grep foo baz 140 | 141 | exit 0 142 | -------------------------------------------------------------------------------- /tests/listset.c: -------------------------------------------------------------------------------- 1 | #include "../lib/listset.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | static int is_in_listset(const listset *ptr, const char *path) { 8 | while (ptr != NULL) { 9 | if (strcmp(ptr->path, path) == 0) { 10 | return 1; 11 | } 12 | ptr = ptr->next; 13 | } 14 | return 0; 15 | } 16 | 17 | static char *mycopy(const char *str) { 18 | char *out = malloc(strlen(str)+1); 19 | strcpy(out, str); 20 | return out; 21 | } 22 | 23 | struct listset *a = 0; 24 | 25 | void has(const char *string, int line) { 26 | if (!is_in_listset(a, string)) { 27 | printf("FAIL Has not %s (line %d)!!!\n", string, line); 28 | exit(1); 29 | } 30 | } 31 | 32 | void hasnot(const char *string, int line) { 33 | if (is_in_listset(a, string)) { 34 | printf("FAIL Has %s (line %d)!!!\n", string, line); 35 | exit(1); 36 | } 37 | } 38 | 39 | void add(const char *string) { 40 | printf("adding %s\n", string); 41 | insert_to_listset(&a, mycopy(string)); 42 | } 43 | 44 | void remove_str(const char *string) { 45 | printf("removing %s\n", string); 46 | delete_from_listset(&a, string); 47 | } 48 | 49 | int main(int argc, char **argv) { 50 | hasnot("foo", __LINE__); 51 | remove_str("foo"); // shouldn't crash 52 | 53 | hasnot("foo", __LINE__); 54 | hasnot("foobar", __LINE__); 55 | hasnot("bar", __LINE__); 56 | hasnot("", __LINE__); 57 | 58 | add("foo"); 59 | 60 | has("foo", __LINE__); 61 | hasnot("foobar", __LINE__); 62 | hasnot("bar", __LINE__); 63 | hasnot("", __LINE__); 64 | 65 | add("bar"); 66 | 67 | has("foo", __LINE__); 68 | hasnot("foobar", __LINE__); 69 | has("bar", __LINE__); 70 | hasnot("", __LINE__); 71 | 72 | add("bar"); 73 | 74 | has("foo", __LINE__); 75 | hasnot("foobar", __LINE__); 76 | has("bar", __LINE__); 77 | hasnot("", __LINE__); 78 | 79 | add(""); 80 | 81 | has("foo", __LINE__); 82 | hasnot("foobar", __LINE__); 83 | has("bar", __LINE__); 84 | has("", __LINE__); 85 | 86 | add("foobar"); 87 | 88 | has("foo", __LINE__); 89 | has("foobar", __LINE__); 90 | has("bar", __LINE__); 91 | has("", __LINE__); 92 | 93 | remove_str("bar"); 94 | 95 | has("foo", __LINE__); 96 | has("foobar", __LINE__); 97 | hasnot("bar", __LINE__); 98 | has("", __LINE__); 99 | 100 | remove_str("foobar"); 101 | 102 | has("foo", __LINE__); 103 | hasnot("foobar", __LINE__); 104 | hasnot("bar", __LINE__); 105 | has("", __LINE__); 106 | 107 | add("bar"); 108 | 109 | has("foo", __LINE__); 110 | hasnot("foobar", __LINE__); 111 | has("bar", __LINE__); 112 | has("", __LINE__); 113 | 114 | remove_str(""); 115 | 116 | has("foo", __LINE__); 117 | hasnot("foobar", __LINE__); 118 | has("bar", __LINE__); 119 | hasnot("", __LINE__); 120 | 121 | remove_str("foo"); 122 | 123 | hasnot("foo", __LINE__); 124 | hasnot("foobar", __LINE__); 125 | has("bar", __LINE__); 126 | hasnot("", __LINE__); 127 | 128 | remove_str("bar"); 129 | 130 | hasnot("foo", __LINE__); 131 | hasnot("foobar", __LINE__); 132 | hasnot("bar", __LINE__); 133 | hasnot("", __LINE__); 134 | 135 | return 0; 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fac 2 | 3 | 4 | 5 | 6 | [![Build Status](https://travis-ci.org/droundy/fac.svg?branch=master)](https://travis-ci.org/droundy/fac) 7 | [![Build status](https://ci.appveyor.com/api/projects/status/opg6nds3m9ahkqvj?svg=true)](https://ci.appveyor.com/project/droundy/fac) 8 | 9 | Fac is a general-purpose build system inspired by make that utilizes 10 | ptrace to ensure that all dependences are enumerated and that all 11 | source files are added to a (git) repo. An important feature of fac 12 | is that it automatically handles dependencies, rather than either 13 | complaining about them or giving an incorrect build. Currently, fac 14 | only runs on linux systems, but on those systems it is incredibly easy 15 | to use! 16 | 17 | * Fac automatically tracks build dependencies in a way that is 18 | independent of programming language. You are only required to 19 | specify the minimum of dependencies for each rule, and fac works out 20 | the rest for you. If you fail to specify dependencies, fac should 21 | still build successfully after enough tries, provided your build 22 | rules fail when dependencies are missing (rather than simply 23 | producing wrong output). Once fac has successfully built your 24 | project, it knows the dependencies of each command, and subsequent 25 | builds in that source tree will be the same as if you had specified 26 | all dependencies and all output. 27 | 28 | * Fac supports parallel builds. 29 | 30 | * You are forced to write your configuration in a language of your own 31 | choice. (Or conversely, you are not forced to use a language of 32 | *my* choice, much less a custom-built language that I developed.) 33 | 34 | * Integrates with git, to keep you from forgetting to `git add` a file 35 | that is needed for the build. 36 | 37 | To find out more about fac, including benchmarks and complete 38 | documentation, please visit the fac web page at: 39 | 40 | http://physics.oregonstate.edu/~roundyd/fac 41 | 42 | ## Build and install 43 | 44 | The easy way to get fac is to first install rust on your computer, 45 | and then to type 46 | 47 | cargo install fac 48 | 49 | ## Build and install from a git clone 50 | 51 | To build fac (and its documentation) just run 52 | 53 | cargo build 54 | 55 | This should build fac on an x86-64 linux system that has rust 56 | installed. You can then build an 57 | optimized version by running 58 | 59 | target/debug/fac fac 60 | 61 | To use fac, you can copy the fac binary into some location in your 62 | path. 63 | 64 | ### Build dependencies and details 65 | 66 | You need to have [rust installed](https://rustup.rs). In addition, 67 | building fac with fac itself requires both `python2` and `python3` (something to fix), and 68 | building the fac documentation (which is the default build target) 69 | requires `sass` and `python-markdown`. 70 | 71 | For more detail on building fac, see the 72 | [web page on building fac](http://physics.oregonstate.edu/~roundyd/fac/building.html), 73 | which is also in the fac repository as `web/building.md`. 74 | 75 | ## License 76 | 77 | Fac is free software, and is licensed under the GNU General Public 78 | License, version 2 or later. 79 | -------------------------------------------------------------------------------- /web/security.md: -------------------------------------------------------------------------------- 1 | # Fac security 2 | 3 | $docnav 4 | 5 | Fac 6 | 7 | This document will define the security model for fac. Although this 8 | may seem excessive given the context, I believe every piece of 9 | software should have a security model describing what it should and 10 | should not do, and what an adversary should be able to control. 11 | 12 | Fac's security model is pretty simple, since it is controlled by a 13 | repository and typically has no interaction with the network. 14 | Nevertheless there is value in making this explicit. 15 | 16 | ## Data flow 17 | 18 | A threat model starts with a data flow diagram, indicating where data 19 | moves in the application. For fac this is very simple, and I will not 20 | bother creating a visual model. Information comes into fac through 21 | the on-disk representation of a git repository. This information 22 | determines what fac does, which will necesarily involve running 23 | arbitrary commands as requested by the configuration stored in the 24 | repository. This will in turn produce files on disk. 25 | 26 | ## Threats 27 | 28 | ### Other users 29 | 30 | *A user without write permission in the repository or the `$PATH` (or 31 | a superuser-like capability) should be unable to affect the commands 32 | run by fac.* This does not mean that any commands run by fac must be 33 | secure, which would be impossible, since fac can run arbitrary 34 | insecure programs. But said user should not be able to cause fac to 35 | run the "wrong" command. 36 | 37 | This could happen either through a tempfile vulnerability, or through 38 | allowing other users to modify our processes. Fac should be safe from 39 | this kind of error due to not using tempfiles, except (in some 40 | circumstances) in the repository directory, which is not writeable by 41 | these attackers. 42 | 43 | ### Privilege escalation 44 | 45 | *A user running fac in their own repository should be unable to 46 | accomplish any action that they could not accomplish without the use 47 | of fac.* This means that if fac were to be implemented as an SUID 48 | program, it would need to be secure. Since it is not so implemented, 49 | I don't see how it could lead to privilege escalation. 50 | 51 | On MacOS, it is possible fac will need to be SUID root, in which case 52 | this could become a significant risk. 53 | 54 | ### Network access 55 | 56 | *Fac should not make or receive any network connections, unless such 57 | connections are explicitly created by a configured rule.* If you 58 | create a rule such as 59 | 60 | | git clone https://github.com/droundy/fac 61 | 62 | then of course fac will access the network, but absent any such rule, 63 | fac should not be accessing the network. No phoning home, for 64 | instance. This should essentially come for free, since we didn't 65 | program this in. 66 | 67 | ## Conclusion 68 | 69 | Because fac is designed to run arbitrary code, there is relatively 70 | little we need worry about. e.g. execution of arbitrary code due to a 71 | malformed input file is a non-issue, since a well-formed input file 72 | also enables the execution of arbitrary code. The only two real 73 | questions are privilege escalation and interference by other users 74 | (most likely due to tempfile vulnerabilities). We believe these risks 75 | are adequately addressed by the current implementation of fac. 76 | -------------------------------------------------------------------------------- /configure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from __future__ import print_function 4 | import string, os, sys, platform, subprocess 5 | 6 | myplatform = sys.platform 7 | if myplatform == 'linux2': 8 | myplatform = 'linux' 9 | 10 | def is_in_path(program): 11 | """ Does the program exist in the PATH? """ 12 | def is_exe(fpath): 13 | return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 14 | fpath, fname = os.path.split(program) 15 | if fpath: 16 | return is_exe(program) 17 | else: 18 | for path in os.environ["PATH"].split(os.pathsep): 19 | path = path.strip('"') 20 | exe_file = os.path.join(path, program) 21 | if is_exe(exe_file): 22 | return True 23 | return False 24 | 25 | def can_run(cmd): 26 | print('# trying', cmd, file=sys.stdout) 27 | try: 28 | subprocess.check_output(cmd, shell=True) 29 | return True 30 | except Exception as e: 31 | print("# error: ", e) 32 | return False 33 | 34 | have_sass = can_run('sass -h') 35 | have_help2man = can_run('help2man --help') 36 | 37 | if have_help2man: 38 | print(''' 39 | | help2man --no-info ./fac > fac.1 40 | < fac''') 41 | else: 42 | print("# no help2man, so we won't build the man page") 43 | 44 | if have_sass: 45 | print(''' 46 | | sass -I. web/style.scss web/style.css 47 | > web/style.css 48 | C .sass-cache 49 | ''') 50 | else: 51 | print("# no sass, so we won't build style.css") 52 | 53 | def cargo_cmd(cmd, inps, outs): 54 | print('\n| {}'.format(cmd)) 55 | for i in inps: 56 | print("< {}".format(i)) 57 | for o in outs: 58 | print("> {}".format(o)) 59 | if 'cargo-test-output.log' not in outs: 60 | print('c .log') 61 | print('''c ~ 62 | c # 63 | C .nfs 64 | c ~ 65 | c .fac 66 | c .tum 67 | c .pyc 68 | c .o 69 | c fac.exe 70 | c __pycache__ 71 | c .gcda 72 | c .gcno 73 | c .gcov 74 | c src/version.rs 75 | c Cargo.lock 76 | c fac 77 | c fac-afl 78 | c -pak 79 | c .deb 80 | c .1 81 | C doc-pak 82 | C bench 83 | C tests 84 | C bugs 85 | C web 86 | C target 87 | C bigbro 88 | ''') 89 | 90 | if is_in_path('cargo'): 91 | cargo_cmd("cargo build --features strict && mv target/debug/fac debug-fac", [], 92 | ["debug-fac"]) 93 | # cargo_cmd("cargo test --features strict > cargo-test-output.log", 94 | # [], ['cargo-test-output.log']) 95 | cargo_cmd("cargo doc --no-deps && cp -a target/doc web/", [], 96 | ["web/doc/fac/index.html"]) 97 | cargo_cmd("cargo build --release && mv target/release/fac fac", [], 98 | ["fac"]) 99 | print(""" 100 | # make copies of the executables, so that if cargo fails we will still 101 | # have an old version of the executable, since cargo deletes output on 102 | # failure. 103 | | cp debug-fac backup-debug-fac 104 | < debug-fac 105 | > backup-debug-fac 106 | 107 | | cp fac backup-fac 108 | < fac 109 | > backup-fac""") 110 | else: 111 | print('# no cargo, so cannot build using rust') 112 | 113 | try: 114 | targets = subprocess.check_output('rustup show', shell=True) 115 | if b'x86_64-pc-windows-gnu' in targets: 116 | cargo_cmd("cargo build --features strict --target x86_64-pc-windows-gnu", [], []) 117 | except: 118 | print('# no rust for windows') 119 | -------------------------------------------------------------------------------- /tests/getting-started.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys, os, re, subprocess 4 | 5 | if sys.version_info < (3,5): 6 | print('Please run this script with python 3.5 or newer:', sys.version) 7 | exit(137) 8 | 9 | runre = re.compile(r'\[run\]: # \((.+)\)') 10 | shellre = re.compile(r'^ \$ (.+)') 11 | filere = re.compile(r'##### (.+)') 12 | verbre = re.compile(r'^ (.*)') 13 | time_remaining_re = re.compile(r'^Build time remaining: ') 14 | 15 | with open(sys.argv[1]) as f: 16 | for line in f: 17 | isfile = filere.findall(line) 18 | isshell = shellre.findall(line) 19 | if len(isfile) > 0: 20 | with open(isfile[0], 'w') as newf: 21 | for line in f: 22 | isverb = verbre.findall(line) 23 | if len(isverb) == 0: 24 | break 25 | newf.write(isverb[0]) 26 | newf.write('\n') 27 | print(isfile[0], ':', isverb[0]) 28 | elif len(isshell) > 0: 29 | print('shell :', isshell[0]) 30 | tocheck = True 31 | if isshell[0][-len('# fails'):] == '# fails': 32 | tocheck = False 33 | print('SHOULD FAIL!') 34 | isshell[0] = isshell[0][:-len('# fails')] 35 | ret = subprocess.run(isshell, shell=True, 36 | stderr=subprocess.STDOUT, 37 | check=tocheck, 38 | stdout=subprocess.PIPE) 39 | if not tocheck and ret.returncode == 0: 40 | print("DID NOT FAIL!!!") 41 | exit(1) 42 | print('output:', ret.stdout) 43 | output = ret.stdout 44 | for outline in output.decode('utf-8').split('\n'): 45 | # The time_remaining_re bit is needed to skip the 46 | # "Build time remaining:" lines that get printed every 47 | # once in a while. These are irregular, which is why 48 | # we need to do this. 49 | if len(outline)>0 and not time_remaining_re.match(outline): 50 | print('output:', outline) 51 | expectedline = f.readline() 52 | if len(verbre.findall(expectedline)) == 0: 53 | print('unexpected output from:', isshell[0]) 54 | print('output is', outline) 55 | exit(1) 56 | if expectedline in [' ...', ' ...\n']: 57 | print('I expected random output.') 58 | break 59 | expected = verbre.findall(expectedline)[0] 60 | expected = expected.replace('.', r'\.') 61 | expected = expected.replace('*', r'\*') 62 | expected = expected.replace(r'\.\.\.', '.*') 63 | expected = expected.replace('[', r'\[') 64 | expected = expected.replace(']', r'\]') 65 | expected = expected.replace('(', r'\(') 66 | expected = expected.replace(')', r'\)') 67 | if not re.compile(expected).match(outline): 68 | print('I expected:', expected) 69 | print('but instead I got:', outline) 70 | exit(1) 71 | else: 72 | print('input', line.strip()) 73 | -------------------------------------------------------------------------------- /bench/plot-benchmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from __future__ import print_function 3 | 4 | import os, sys 5 | 6 | import matplotlib 7 | matplotlib.use('Agg') 8 | import matplotlib.pyplot as plt 9 | import numpy as np 10 | 11 | import cats 12 | import hierarchy 13 | import dependentchains 14 | import sleepy 15 | import independent 16 | 17 | #matplotlib.rc('font', size='16.0') 18 | 19 | datadir = os.getcwd()+'/data/' 20 | modules = [dependentchains, cats, hierarchy, sleepy, independent] 21 | 22 | allcolors = ['r','b','g','k','c','y','m', 'r'] 23 | allpatterns = ['o-', 's:', '*-.', 'x--', '.-', '<-', '>-', 'v-'] 24 | 25 | tool_patterns = {} 26 | fslabels, fshandles = [], [] 27 | toollabels, toolhandles = [], [] 28 | 29 | mod = None 30 | for m in modules: 31 | if m.name == sys.argv[1]: 32 | mod = m 33 | if mod is None: 34 | print("Invalid mod: ", sys.argv[1]) 35 | exit(1) 36 | 37 | verb = sys.argv[2] 38 | if verb not in mod.verbs: 39 | print("Invalid verb: ", sys.argv[2]) 40 | exit(1) 41 | 42 | dates = os.listdir(datadir) 43 | dates.sort() 44 | date = dates[-1] 45 | # os.chdir(datadir+date+'/'+mod.name) 46 | print('date', date, mod.name) 47 | 48 | plt.figure(figsize=(6,4.3)) 49 | plt.title('%s %s on %s' % (verb, mod.name, date)) 50 | have_handled = {} 51 | 52 | num_fs = len(os.listdir(datadir+date+'/'+mod.name+'/fac -j4')) 53 | 54 | tools = os.listdir(datadir+date+'/'+mod.name) 55 | tools.sort() 56 | for tool in tools: 57 | if not tool in tool_patterns: 58 | tool_patterns[tool] = allpatterns[0] 59 | allpatterns = allpatterns[1:] 60 | if num_fs == 1: 61 | mycolor = allcolors[0] 62 | allcolors = allcolors[1:] 63 | toollabels.append(tool) 64 | toolhandles.append(plt.Line2D((0,1),(0,0), marker=tool_patterns[tool][0], 65 | linestyle=tool_patterns[tool][1:], color='k')) 66 | for fs in os.listdir(datadir+date+'/'+mod.name+'/'+tool): 67 | if num_fs > 1 and not fs in fs_colors: 68 | mycolor = allcolors[0] 69 | allcolors = allcolors[1:] 70 | fslabels.append(fs) 71 | fshandles.append(plt.Line2D((0,1),(0,0), color=fs_colors[fs], linewidth=3)) 72 | data = np.loadtxt(datadir+date+'/'+mod.name+'/'+tool+'/'+fs+'/'+verb+'.txt') 73 | # The folowing few lines handles the case where we 74 | # have run the benchmark a few times, and have 75 | # redundant data. We sort it, and then replace all 76 | # the points with a given N with the average of all 77 | # the measurements (from that date). 78 | if len(data.shape) == 2: 79 | for n in data[:,0]: 80 | ind = data[:,0] == n 81 | data[ind,1] = np.mean(data[ind,1]) 82 | data = np.sort(np.vstack({tuple(row) for row in data}), axis=0) # remove duplicate lines 83 | if num_fs > 1: 84 | mylabel = '%s on %s' % (tool, fs) 85 | else: 86 | mylabel = tool 87 | plt.loglog(data[:,0], data[:,1]/data[:,0], 88 | tool_patterns[tool], 89 | color=mycolor, 90 | label=mylabel) 91 | plt.gca().grid(True) 92 | plt.xlabel('$N$') 93 | plt.ylabel('$t/N$ (s)') 94 | if num_fs > 1: 95 | plt.legend(fshandles+toolhandles, fslabels+toollabels, loc='best', frameon=False) 96 | else: 97 | plt.legend(loc='best', frameon=False) 98 | 99 | plt.tight_layout() 100 | # plt.savefig('../web/%s-%s.pdf' % (mod.name, verb)) 101 | plt.savefig('../web/%s-%s.svg' % (mod.name, verb), dpi=60) 102 | # plt.savefig('../web/%s-%s.png' % (mod.name, verb), dpi=100) 103 | -------------------------------------------------------------------------------- /src/git.rs: -------------------------------------------------------------------------------- 1 | //! Support interaction with git 2 | 3 | use std; 4 | 5 | /// Go to the top level of the git repository (typically the one 6 | /// containing a `.git` directory). 7 | pub fn go_to_top() -> std::path::PathBuf { 8 | let mut output = std::process::Command::new("git") 9 | .args(&["rev-parse", "--show-toplevel"]) 10 | .output() 11 | .expect("Error calling git rev-parse --show-toplevel"); 12 | // #ifdef _WIN32 13 | // if (strlen(buf) > 2 && buf[0] == '/' && buf[2] == '/') { 14 | // // this is a workaround for a broken git included in msys2 15 | // // which returns paths like /c/Users/username... 16 | // buf[0] = buf[1]; 17 | // buf[1] = ':'; 18 | // } 19 | // #endif 20 | 21 | if !output.status.success() { 22 | println!("Error identifying git top."); 23 | let newlen = output.stderr.len()-1; 24 | output.stderr.truncate(newlen); 25 | println!(" {}", bytes_to_path(&output.stderr).display()); 26 | std::process::exit(1); 27 | } 28 | let newlen = output.stdout.len()-1; 29 | output.stdout.truncate(newlen); 30 | let p = bytes_to_path(&output.stdout); 31 | std::env::set_current_dir(&p).unwrap(); 32 | p 33 | } 34 | 35 | /// Location of the .git directory 36 | pub fn git_dir() -> std::path::PathBuf { 37 | let mut output = std::process::Command::new("git") 38 | .args(&["rev-parse", "--git-dir"]) 39 | .output() 40 | .expect("Error calling git rev-parse --git-dir"); 41 | // #ifdef _WIN32 42 | // if (strlen(buf) > 2 && buf[0] == '/' && buf[2] == '/') { 43 | // // this is a workaround for a broken git included in msys2 44 | // // which returns paths like /c/Users/username... 45 | // buf[0] = buf[1]; 46 | // buf[1] = ':'; 47 | // } 48 | // #endif 49 | 50 | if !output.status.success() { 51 | println!("Error identifying git-dir."); 52 | let newlen = output.stderr.len()-1; 53 | output.stderr.truncate(newlen); 54 | println!(" {}", bytes_to_path(&output.stderr).display()); 55 | std::process::exit(1); 56 | } 57 | let newlen = output.stdout.len()-1; 58 | output.stdout.truncate(newlen); 59 | bytes_to_path(&output.stdout) 60 | } 61 | 62 | /// Find out what files are in git. 63 | pub fn ls_files() -> std::collections::HashSet { 64 | let output = std::process::Command::new("git") 65 | .arg("ls-files") 66 | .arg("-z") 67 | .output() 68 | .expect("Error calling git ls-files"); 69 | let mut fs = std::collections::HashSet::new(); 70 | for s in output.stdout.split(|c| *c == b'\0') { 71 | if s.len() > 0 { 72 | fs.insert(bytes_to_path(s)); 73 | } 74 | } 75 | fs 76 | } 77 | 78 | /// git add a file or more 79 | pub fn add(p: &std::path::Path) -> std::io::Result<()> { 80 | let s = std::process::Command::new("git") 81 | .arg("add") 82 | .arg(p) 83 | .output()?; 84 | if s.status.success() { 85 | Ok(()) 86 | } else { 87 | Err(std::io::Error::new(std::io::ErrorKind::Other, 88 | String::from_utf8_lossy(&s.stderr).into_owned())) 89 | } 90 | } 91 | 92 | #[test] 93 | fn ls_files_works() { 94 | let x = ls_files(); 95 | assert!(x.contains(&bytes_to_path(b".gitignore"))); 96 | } 97 | 98 | #[cfg(unix)] 99 | use std::os::unix::ffi::OsStrExt; 100 | 101 | #[cfg(unix)] 102 | fn bytes_to_path(b: &[u8]) -> std::path::PathBuf { 103 | std::path::PathBuf::from(std::path::Path::new(std::ffi::OsStr::from_bytes(b))) 104 | } 105 | 106 | #[cfg(not(unix))] 107 | fn bytes_to_path(b: &[u8]) -> std::path::PathBuf { 108 | std::path::PathBuf::from(std::path::Path::new(std::str::from_utf8(b).unwrap())) 109 | } 110 | 111 | -------------------------------------------------------------------------------- /web/file-format.md: -------------------------------------------------------------------------------- 1 | # Fac file format 2 | 3 | $docnav 4 | 5 | To configure this build system, you create one or more files ending 6 | with `.fac`. These files specify the rules to build your project, 7 | and must be added to your git repository. For most moderately complex 8 | projects, you will have just one `.fac` file in git, which will 9 | itself specify specify rules needed to create one or more additional 10 | `.fac` files, which will contain the rules for doing the actual 11 | build. Each `.fac` file consists of: 12 | 13 | 1. Comments beginning with `"# "` (a pound sign followed by a space). 14 | 15 | 2. Rules beginning with `"| "` (a pipe character followed by a 16 | space). The remainder of the line is the actual command to perform 17 | the build. Following this line (possibly separated by blank lines 18 | and comments) are one or more of the following directives. The 19 | order of these directives has no effect, so long as the follow the 20 | `"| "` line to which they apply. 21 | 22 | 2. Rules beginning with `"* "` (an asterisk followed by a space). 23 | This rule is identical to a pipe rule, but will rerun the command 24 | if there are changes to the contents of any directories the rule 25 | calls `readdir(3)` on (or technically `getdents(2)`). You need to 26 | use this rule if you use a wildcard or glob and what the command to 27 | be rerun if new files are added. This behavior is no longer the 28 | default (i.e. the pipe behavior) because it causes many rebuilds 29 | when `python3` is run, since it always reads the current directory 30 | when the first module is imported. 31 | 32 | 2. Optional rules beginning with `"? "` are identical to rules 33 | beginning with a pipe, with the sole difference being that optional 34 | rules are only built if they are needed to build another target, or 35 | if they are explicitly requested, e.g. by invoking `fac 36 | optional-output-filename`. Optional rules enable you to specify a 37 | large number of rules and only have what is needed built. Optional 38 | rules should always specify at least one output file, or they will 39 | never be built. 40 |

    41 | Optional rules can be helpful for rules (often autogenerated) that 42 | might be slow or require dependencies that not all users will 43 | have. They can be convenient for scripts that define a whole slew 44 | of rules in a facfile, and which don't want to determine which of 45 | these are needed for the actual build. Or for something like 46 | documentation, which only some users will want to actually build. 47 |

    48 | 49 | 3. Output specifications beginning with `"> "` followed by the name of 50 | the file that is output. There is no escaping performed, and only 51 | newlines and null characters are disallowed in the file name. 52 | There is little need to specify the output for rules, since fac 53 | can determine this automatically. The only reason to specify 54 | output is so that on the very first build a user can request that 55 | we build only the specified rule. 56 | 57 | 4. Input specifications beginning with `"< "` followed by the name of 58 | the file that is required. You only need specify inputs when they 59 | are generated files. Even then, you need only specify the inputs 60 | if you wish to have the build succeed on the first attempt. 61 | 62 | 5. Dependency output specifications beginning with `"M "` followed by 63 | the name of the deps file that is generated by the command. This 64 | is designed for compilers which have the ability to track 65 | dependencies and output a `Makefile` fragment documenting those 66 | dependencies. This is implemented for two reasons. Firstly, by 67 | relying on the compiler for dependency tracking, fac can run more 68 | quickly than if it tracks file accesses itself. Secondly, it makes 69 | fac more useful on platforms (currently Windows and Mac OS) where 70 | it is unable to track dependencies. *This feature is only 71 | available in the version of fac implemented in rust.* 72 | 73 | 6. There are two ways to specify "cache" files. A cache file is a 74 | file that may be either read or written by a given command, but 75 | doesn't affect the output and not itself an output. One nice 76 | example is the ".pyc" files sometimes generated when you run a 77 | python program. One python command may produce a .pyc file, and 78 | another may read it (if they both use the same module), but that 79 | does not mean that there is a dependency between those two 80 | commands. You can specify a cache suffix (such as `.pyc`) using a 81 | `"c "` line, or you can specify a cache prefix (such as 82 | `~/.ccache`) using a capitalized `"C "` line. The latter can be 83 | helpful if you find that your software is being rebuilt needlessly 84 | due to some cache file being modified. 85 | -------------------------------------------------------------------------------- /bench/hierarchy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os, hashlib, time, numpy, sys, datetime 4 | 5 | name = 'hierarchy' 6 | 7 | def make_name(i): 8 | path = '.' 9 | digits = '%d' % i 10 | for d in digits: 11 | path = path+'/number-'+d 12 | return path[2:]+('_%d' % i) 13 | 14 | allowed_chars = 'abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ' 15 | 16 | def hashid(n): 17 | m = hashlib.sha1() 18 | m.update(str(n).encode('utf-8')) 19 | name = '' 20 | for i in m.digest()[:24]: 21 | name += allowed_chars[i % len(allowed_chars)] 22 | return name+('_%d' % n) 23 | 24 | def hash_integer(n): 25 | m = hashlib.sha1() 26 | m.update(str(n).encode('utf-8')) 27 | name = 0 28 | for i in m.digest()[:8]: 29 | name = name*256 + i 30 | return abs(name) 31 | 32 | def open_and_gitadd(fname): 33 | f = open(fname, 'w') 34 | assert(not os.system('git add '+fname)) 35 | return f 36 | 37 | def create_bench(N): 38 | sconsf = open_and_gitadd('SConstruct') 39 | facf = open_and_gitadd('top.fac') 40 | open_and_gitadd('Tupfile.ini') 41 | sconsf.write(""" 42 | env = Environment(CPPPATH=['.']) 43 | """) 44 | for i in range(N): 45 | if i > 9: 46 | os.makedirs(os.path.dirname(make_name(i)), exist_ok=True) 47 | cname = make_name(i) + '.c' 48 | hname = make_name(i) + '.h' 49 | includes = '' 50 | funcs = '' 51 | for xx in [i+ii for ii in range(10)]: 52 | nhere = hash_integer(xx) % N 53 | includes += '#include "%s.h"\n' % make_name(nhere) 54 | funcs += ' %s();\n' % hashid(nhere) 55 | facf.write(""" 56 | # %d 57 | | gcc -I. -O2 -c -o %s.o %s.c 58 | > %s.o 59 | """ % (i, make_name(i), make_name(i), make_name(i))) 60 | sconsf.write(""" 61 | env.Object('%s.c') 62 | """ % make_name(i)) 63 | f = open_and_gitadd(make_name(i)+'.c') 64 | f.write('\n') 65 | f.write("""/* c file %s */ 66 | 67 | %s#include "%s" 68 | 69 | #include 70 | #include 71 | #include 72 | #include 73 | #include 74 | #include 75 | #include 76 | 77 | void delete_from_%s(list%s **list, const char *path) { 78 | while (*list != NULL) { 79 | if (strcmp((*list)->path, path) == 0) { 80 | list%s *to_be_deleted = *list; 81 | *list = (*list)->next; 82 | free(to_be_deleted->path); 83 | free(to_be_deleted); 84 | return; 85 | } 86 | list = &((*list)->next); 87 | } 88 | } 89 | 90 | int is_in_%s(const list%s *ptr, const char *path) { 91 | while (ptr != NULL) { 92 | if (strcmp(ptr->path, path) == 0) { 93 | return 1; 94 | } 95 | ptr = ptr->next; 96 | } 97 | return 0; 98 | } 99 | 100 | void insert_to_%s(list%s **list, const char *path) { 101 | delete_from_%s(list, path); 102 | list%s *newhead = (list%s *)malloc(sizeof(list%s)); 103 | newhead->next = *list; 104 | newhead->path = malloc(strlen(path)+1); 105 | strcpy(newhead->path, path); 106 | 107 | *list = newhead; 108 | } 109 | 110 | void free_%s(list%s *list) { 111 | while (list != NULL) { 112 | list%s *d = list; 113 | list = list->next; 114 | free(d->path); 115 | free(d); 116 | } 117 | } 118 | 119 | static void stupid_this(char *s) { 120 | while (*s) { 121 | while (*s < 'a') { 122 | *s += 13; 123 | } 124 | while (*s > 'z') { 125 | *s -= 13; 126 | } 127 | printf("%%c", *s); 128 | s++; 129 | } 130 | printf("\\n"); 131 | } 132 | 133 | void %s() { 134 | stupid_this("Hello world %s!\\n"); 135 | %s} 136 | """ % (cname, includes, hname, hashid(i), hashid(i), 137 | hashid(i), hashid(i), hashid(i), hashid(i), hashid(i), 138 | hashid(i), hashid(i), hashid(i), hashid(i), hashid(i), 139 | hashid(i), hashid(i), hashid(i), hashid(i), 140 | funcs)) 141 | f.close() 142 | f = open_and_gitadd(make_name(i)+'.h') 143 | f.write("""/* header %s */ 144 | #ifndef %s_H 145 | #define %s_H 146 | 147 | typedef struct list%s { 148 | char *path; 149 | struct list%s *next; 150 | } list%s; 151 | 152 | void insert_to_%s(list%s **list, const char *path); 153 | void delete_from_%s(list%s **list, const char *path); 154 | int is_in_%s(const list%s *list, const char *path); 155 | 156 | void free_%s(list%s *list); 157 | 158 | void %s(); 159 | 160 | #endif 161 | """ % (hname, 162 | hashid(i), hashid(i), hashid(i), hashid(i), hashid(i), 163 | hashid(i), hashid(i), hashid(i), hashid(i), hashid(i), 164 | hashid(i), hashid(i), hashid(i), hashid(i))) 165 | f.close() 166 | return N 167 | 168 | verbs = ['building', 'touching-all', 'modifying-header', 'modifying-c', 'doing-nothing'] 169 | 170 | def prepare(): 171 | return {'touching-all': r'sleep 1 && find . -name "*.c" -exec touch \{\} \;', 172 | 'modifying-header': 'echo >> number-0.h', 173 | 'modifying-c': 'echo >> number-0.c'} 174 | -------------------------------------------------------------------------------- /bench/bench.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os, hashlib, time, numpy, sys, datetime, subprocess 4 | 5 | import cats 6 | import hierarchy 7 | import dependentchains 8 | import sleepy 9 | import independent 10 | 11 | minute = 60 12 | hour = 60*minute 13 | day = 24*hour 14 | time_limit = 6*day 15 | 16 | tools = [cmd+' -j4' for cmd in ['make', 'fac', 'fac --blind', 'tup', 'scons']] # + ['sh build.sh'] 17 | 18 | if os.system('rust-fac --version') == 0: 19 | tools.append('rust-fac -j4') 20 | tools.append('rust-fac --blind -j4') 21 | tools.append('ninja -j4') 22 | tools = sorted(tools) 23 | 24 | # The variable "date" actually contains the date and short hash of the 25 | # commit information. This could lead to confusion and incorrectness 26 | # if we run benchmarking without actually committing, but if used 27 | # properly it should enable us to more accurately track which version 28 | # of the code corresponded to which benchmarking data. 29 | 30 | # Also note that this will fail if we checkout an old version of the 31 | # code. 32 | 33 | date = subprocess.check_output(['git', 'log', '--pretty=%ci', '-n', '1'], stderr=subprocess.STDOUT).decode('utf-8') 34 | date = date[:10]+'_' 35 | date += subprocess.check_output(['git', 'log', '--pretty=%h', '-n', '1'], stderr=subprocess.STDOUT).decode('utf-8')[:-1] 36 | 37 | datadir = os.getcwd()+'/bench/data/' 38 | os.makedirs(datadir, exist_ok=True) 39 | modules = [dependentchains, hierarchy, cats, independent] 40 | 41 | rootdirnames = ['tmp', 'home' , 'vartmp'] 42 | rootdirs = {'home': os.getcwd()+'/bench/temp', 43 | 'tmp': '/tmp/benchmarking', 44 | 'vartmp': '/var/tmp/benchmarking'} 45 | 46 | def find_mount_point(path): 47 | path = os.path.abspath(path) 48 | while not os.path.ismount(path): 49 | path = os.path.dirname(path) 50 | return path 51 | def identify_filesystem(path): 52 | path = find_mount_point(path) 53 | with open('/proc/mounts') as f: 54 | x = f.read() 55 | x = x[10:] # to skip "rootfs / rootfs" which I do not understand. 56 | x = x[x.find(' '+path+' ')+len(path)+2:] 57 | return x[:x.find(' ')] 58 | 59 | tmprootdirnames = rootdirnames 60 | rootdirnames = [] 61 | filesystems = {} 62 | for r in tmprootdirnames: 63 | fs = identify_filesystem(rootdirs[r]) 64 | if fs in filesystems.values(): 65 | print(fs,'is already covered') 66 | else: 67 | filesystems[r] = fs 68 | rootdirnames.append(r) 69 | print(filesystems) 70 | 71 | def time_command(mod, builder): 72 | the_time = {} 73 | 74 | cmd = '%s' % builder 75 | cmd = '%s > output 2>&1' % builder 76 | for verb in mod.verbs: 77 | if verb in mod.prepare(): 78 | assert(not os.system(mod.prepare()[verb])) 79 | start = time.time() 80 | if not os.system(cmd): 81 | stop = time.time() 82 | print('%s %s took %g seconds.' % (verb, builder, stop - start)) 83 | the_time[verb] = stop - start 84 | else: 85 | stop = time.time() 86 | print('%s %s failed in %g seconds.' % (verb, builder, stop - start)) 87 | return the_time 88 | 89 | Ns = [] 90 | Nfloat = 10.0 91 | if len(sys.argv) > 1: 92 | Nfloat = float(sys.argv[1]) 93 | start_benchmarking = time.time() 94 | while time.time() < start_benchmarking + time_limit: 95 | N = int(Nfloat) 96 | Ns.append(N) 97 | Nfloat *= 1.7782795 98 | for mod in modules: 99 | if mod.name == 'sleepy' and N not in [56, 100, 177]: 100 | continue 101 | print('\nWorking on %s with N = %d' % (mod.name, N)) 102 | for rootdirname in rootdirnames: 103 | rootdir = rootdirs[rootdirname] 104 | print('\n### Using rootdir', rootdirname) 105 | os.makedirs(rootdir, exist_ok=True) 106 | os.chdir(rootdir) 107 | # here we create the source directory 108 | os.system('rm -rf '+mod.name+'-%d'%N) 109 | os.makedirs(mod.name+'-%d'%N) 110 | os.chdir(mod.name+'-%d'%N) 111 | assert(not os.system('git init')) 112 | mod.create_bench(N) 113 | assert(not os.system('fac --makefile Makefile --script build.sh --tupfile Tupfile > /dev/null && fac -c > /dev/null')) 114 | if 'rust-fac -j4' in tools: 115 | assert(not os.system('rust-fac --ninja build.ninja > /dev/null && rust-fac -c > /dev/null')) 116 | os.chdir('..') 117 | for tool in tools: 118 | print('') 119 | # we do each tool's build in a fresh copy of the source 120 | os.system('rm -rf working && cp -a %s-%d working' % (mod.name, N)) 121 | os.chdir('working') 122 | times = time_command(mod, tool) 123 | os.chdir('..') 124 | datadirname = datadir+'/%s/%s/%s/%s/' % (date, mod.name, tool, filesystems[rootdirname]) 125 | os.makedirs(datadirname, exist_ok=True) 126 | for verb in times.keys(): 127 | with open(datadirname+verb+'.txt', 'a') as f: 128 | f.write('%d\t%g\n' % (N, times[verb])) 129 | -------------------------------------------------------------------------------- /web/signatures.md: -------------------------------------------------------------------------------- 1 | # Signatures 2 | 3 | I have begun looking into using gpg signatures to verify fac commits 4 | and tags using git. There doesn't seem to be a consensus approach on 5 | how to use git for this purpose, so I am sort of figuring things out. 6 | This document and this whole business is currently a work in 7 | progress. 8 | 9 | This document has a quick "how to" section, and then a discussion of 10 | the issues, and my reasoning for this approach. 11 | 12 | ## The quick how-to 13 | 14 | You clone the fac repository as usual. If you are an expert, you can 15 | clone a tagged version and then verify that the tag was signed by my 16 | gpg key, which has the following fingerprint: 17 | 18 | 284D 4AF3 EAF7 F01C 449C 24F5 9E5E E4A9 339E 788B 19 | 20 | If you aren't an expert, you assume that you got a safe clone. If you 21 | are an expert, you also be asking me why you should trust that the bad 22 | guys haven't modified the above fingerprint. Either way, you just 23 | have to hope for the best with regard to github's security (or find a 24 | friend who knows me). 25 | 26 | Once you have a good clone of the repository, you can run your git 27 | commands with the wrapper script `git.py` 28 | 29 | ./git.py pull 30 | 31 | This will inform gpg to use a fac-specific keyring when it checks 32 | signatures, and will tell git to refuse the pull if the commit is not 33 | signed by someone authorized to commit to the repository. You need a 34 | pretty recent version of git to make pull verify the signatures. If 35 | you have an older version of git, you can use: 36 | 37 | git pull 38 | ./git.py log --show-signature 39 | 40 | and check manually that the log message indicates that the commit was 41 | signed. 42 | 43 | If you get a warning about permissions, you can fix this with 44 | 45 | chmod go-rwx .gnupg 46 | 47 | ## Rationalization 48 | 49 | The concept of the pgp web of trust is broken, and also (even if it 50 | worked) useless for determining trust with regard to software. The 51 | challenge of determining whether you trust me is pretty significant. 52 | When you choose to compile my software, you are showing a great deal 53 | of trust that I am not going to try to compromise your machine, and 54 | also that I have not let someone else compromise *my* machine, who 55 | might have the desire to compromise *your* machine. Such trust is not 56 | transitive. And really the web of trust only involves trust that the 57 | person's identity matches what their key says. Just because my name 58 | is David Roundy doesn't mean that you should let me install software 59 | on your computer---which is the choice you are making when you choose 60 | to compile and install fac. 61 | 62 | The combination of git with gpg enables you to verify just one thing: 63 | that given commits or tags were created by someone who has access to 64 | a certain secret key. They don't even do a very good job at that, 65 | since by default signatures are checked against every key in your 66 | public keychain, and conventional gnupg use involves not worrying 67 | about the "danger" of fetching a public key, since "trust" is distinct 68 | from having a copy of the key. So what do you gain by my use of gnupg 69 | to sign commits? You gain the knowledge that the same person (or 70 | someone with access to my computer and passphrase) made the changes as 71 | the guy who made the previous release. 72 | 73 | Since this is realistically all you're likely to be able to discern 74 | from my gnupg signatures, I have not uploaded my key to the 75 | keyservers. That would enable you to easily fetch it, but would also 76 | encourage you to easily fetch any number of attackers' keys, and stick 77 | them all together in the same spot. Instead, I'm putting a public 78 | keyring (with my public key) into the repository itself, along with a 79 | handy script to run git using just this keyring. Thus any signatures 80 | by other keys will be rejected. 81 | 82 | You wonder how to trust that this keyring hasn't been compromised? 83 | Sorry, there's no solution to that, other than an out-of-band avenue 84 | (such as possibly this webpage, see above). If fac becomes famous, I 85 | may publish abroad my gnupg fingerprint, so you can fetch it from mail 86 | archives, etc, and hope that the NSA hasn't compromised them *all*. 87 | If you know me personally (or want to meet me) my office is in 401B 88 | Weniger Hall on Oregon State University's campus in Corvallis, Oregon. 89 | You can visit with me and say "hi" and see if you've got the right 90 | keyring. You could try calling me on the phone, and then maybe you 91 | could decide if my voice sounds trustworthy. If you are able 92 | determine trustworthiness over the phone, I suggest you find a job in 93 | which you can make use of this super power. Or just get an alter ego 94 | and a superhero costume. 95 | 96 | You might wonder how you can tell if the keyring is compromised some 97 | time later. Eventually, I'd like to make my script able to verify on 98 | pull that any commit that modifies the keyring must be signed by a key 99 | in the previously existing keyring. Sadly, that sounds challenging. 100 | For now, I'm waiting for the next Debian release, which should have a 101 | git with the `--verify-signatures` option which enables it to refuse 102 | commits that have not been signed. 103 | 104 | -------------------------------------------------------------------------------- /web/style.scss: -------------------------------------------------------------------------------- 1 | 2 | @import "web/normalize.scss"; 3 | 4 | *, *:after, *:before { 5 | -webkit-box-sizing: border-box; 6 | -moz-box-sizing: border-box; 7 | box-sizing: border-box; 8 | } 9 | 10 | $menuback: #fff; 11 | $menuhoverback: #ddf; 12 | $menucurrent: #06d; 13 | $menufore: #333; 14 | $menuitem: #eee; 15 | $menushadow: #ddd; 16 | 17 | $codebackground: #ddf; 18 | 19 | html { /* make scrollbar always visible */ 20 | overflow: -moz-scrollbars-vertical; 21 | overflow-y: scroll; 22 | } 23 | 24 | body { /* just define the font once! */ 25 | font:normal 1em Helvetica, Verdana, Arial, sans-serif; 26 | } 27 | 28 | @mixin border-radius($radius) { 29 | -webkit-border-radius: $radius; 30 | -moz-border-radius: $radius; 31 | -ms-border-radius: $radius; 32 | border-radius: $radius; 33 | } 34 | 35 | nav { 36 | background: $menuback; 37 | height: 2.2em; 38 | max-width: 45em; 39 | margin-left: auto; 40 | margin-right: auto; 41 | padding-left: .5em; 42 | padding-right: .5em; 43 | 44 | ul { 45 | margin:0; 46 | margin-top: 0.2em; 47 | padding:0;list-style-type:none;display:block; 48 | font-weight:bold; 49 | line-height:165%; 50 | width:100%; 51 | } 52 | 53 | ul li{margin:0;padding:0;border-top:.15em solid $menuback; 54 | margin-right: 0.3em; 55 | display:block; 56 | position:relative;float:left;height:2em;} 57 | 58 | ul li a{ 59 | display:block;text-decoration:none;color:$menufore; 60 | background:$menuitem;padding:0 0 0 1.25em;width:100%; 61 | height:2em;padding-right:0.5em;padding-left:0.5em; 62 | border-right:.1em solid $menushadow; 63 | border-left:.2em solid $menuback; 64 | border-bottom:.15em solid $menushadow; 65 | } 66 | 67 | ul li a:hover{ 68 | background:$menuhoverback;} 69 | 70 | ul li a.current, 71 | ul li a.current:hover{ 72 | color:$menucurrent; 73 | } 74 | } 75 | 76 | .docnav { 77 | height: auto; 78 | width: 12em; 79 | float: right; 80 | ul li{ 81 | width: 100%; 82 | } 83 | } 84 | 85 | main { 86 | margin-left: auto; 87 | margin-right: auto; 88 | padding-left: .5em; 89 | padding-right: .5em; 90 | max-width: 45em; 91 | text-align: justify; 92 | h2 { 93 | clear: both; 94 | } 95 | h3:nth-of-type(even) { 96 | clear: left; 97 | } 98 | h3:nth-of-type(odd) { 99 | clear: right; 100 | } 101 | pre { 102 | margin-left: 1em; 103 | margin-right: 1em; 104 | padding: .5em; 105 | background: $codebackground; 106 | } 107 | 108 | img { 109 | max-width: 20em; 110 | } 111 | p:nth-of-type(even) { 112 | img { 113 | /* margin-right: -10em; */ 114 | float: right; 115 | } 116 | } 117 | p:nth-of-type(odd) { 118 | img { 119 | /* margin-left: -10em; */ 120 | float: left; 121 | } 122 | } 123 | } 124 | 125 | @media screen and (min-width: 80em) { 126 | nav { 127 | max-width: 40em; 128 | } 129 | main { 130 | max-width: 40em; 131 | p:nth-of-type(even) { 132 | img { 133 | margin-right: -20em; 134 | margin-top: -2em; /* This means pictures can overlap by a bit. :( */ 135 | } 136 | } 137 | p:nth-of-type(odd) { 138 | img { 139 | margin-left: -20em; 140 | margin-top: -2em; /* This means pictures can overlap by a bit. :( */ 141 | } 142 | } 143 | } 144 | } 145 | 146 | @media screen and (min-width: 100em) { 147 | nav { 148 | margin-left: auto; 149 | margin-right: auto; 150 | max-width: auto; 151 | } 152 | main { 153 | -webkit-column-count: 2; /* Chrome, Safari, Opera */ 154 | -moz-column-count: 2; /* Firefox */ 155 | column-count: 2; 156 | 157 | header { 158 | -webkit-column-span: all; /* Chrome, Safari, Opera */ 159 | column-span: all; 160 | } 161 | 162 | -webkit-column-gap: 5em; /* Chrome, Safari, Opera */ 163 | -moz-column-gap: 5em; /* Firefox */ 164 | column-gap: 4em; 165 | margin-left: 4em; 166 | margin-right: 4em; 167 | max-width: none; 168 | 169 | .indivisible { 170 | /* -webkit-column-break-inside: avoid; */ 171 | /* break-inside: avoid-column; */ 172 | display: inline-block; 173 | } 174 | 175 | p:nth-of-type(even) { 176 | img { 177 | margin-right: 0; 178 | margin-top: 0; 179 | } 180 | } 181 | p:nth-of-type(odd) { 182 | img { 183 | margin-left: 0; 184 | margin-top: 0; /* This means pictures can overlap by a bit. :( */ 185 | } 186 | } 187 | } 188 | } 189 | 190 | @media screen and (min-width: 160em) { 191 | main { 192 | -webkit-column-count: 2; /* Chrome, Safari, Opera */ 193 | -moz-column-count: 2; /* Firefox */ 194 | column-count: 2; 195 | -webkit-column-gap: 0em; /* Chrome, Safari, Opera */ 196 | -moz-column-gap: 0em; /* Firefox */ 197 | column-gap: 0em; 198 | margin-left: 2em; 199 | margin-right: 2em; 200 | header { 201 | padding-left: 21em; 202 | padding-right: 21em; 203 | } 204 | .indivisible { 205 | padding-left: 21em; 206 | padding-right: 21em; 207 | } 208 | p:nth-of-type(even) { 209 | img { 210 | margin-right: -20em; 211 | margin-top: -2em; /* This means pictures can overlap by a bit. :( */ 212 | } 213 | } 214 | p:nth-of-type(odd) { 215 | img { 216 | margin-left: -20em; 217 | margin-top: -2em; /* This means pictures can overlap by a bit. :( */ 218 | } 219 | } 220 | } 221 | } 222 | 223 | @media screen and (min-width: 240em) { 224 | main { 225 | -webkit-column-count: 3; /* Chrome, Safari, Opera */ 226 | -moz-column-count: 3; /* Firefox */ 227 | column-count: 3; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /web/fac-with-git-hooks.md: -------------------------------------------------------------------------------- 1 | # Triggering fac with git hooks 2 | 3 | $docnav 4 | 5 | This tutorial shares how to use fac with both `fac --continual` and 6 | git hooks to have your project automatically built and tested in a 7 | very flexible way without the trouble of ever explicitly calling fac. 8 | 9 | ## Source code 10 | 11 | As an example, let's pick a simple C program, but we will add 12 | infrastructure towards making it more complicated. 13 | 14 | ##### hello.c 15 | #include 16 | 17 | int main() { 18 | printf("Hello dang world!\n"); 19 | } 20 | 21 | We will use a very simple lint program that ensures that inappropriate 22 | words do not work their way into our source code. 23 | 24 | ##### check-for-curses.py 25 | import sys 26 | retval = 0 27 | with open(sys.argv[1]) as f: 28 | contents = f.read() 29 | for profanity in ['dang', 'shucks']: 30 | if contents.find(profanity) >= 0: 31 | print('Watch your language: "{}" in file'.format(profanity), sys.argv[1]) 32 | retval += 1 33 | if retval == 0: 34 | print('File', sys.argv[1], 'contains no inappropriate language!') 35 | else: 36 | print('File', sys.argv[1], 'has', retval, 'problems!') 37 | exit(retval) 38 | 39 | We will create a git repository and add these files to it. 40 | 41 | $ git init 42 | Initialized empty Git repository in ... 43 | $ git config user.email "testing@example.com" 44 | $ git config user.name "Tester" 45 | $ git add hello.c check-for-curses.py 46 | 47 | Now let's get started with figuring out how to build this. Although 48 | we have only one C file, we anticipate creating a large program, so we 49 | don't want to list all the C files manually. Instead we will write a 50 | program that generates a facfile called `.fac` that explains how to 51 | compile everything. For a real program, I would put in more 52 | intelligence here to check what compilers are available, and what 53 | flags are appropriate for said compilers, but let's keep this simple. 54 | 55 | ##### configure.py 56 | import glob, os 57 | facfile = open('.fac', 'w') 58 | objects = [] 59 | curses = [] 60 | for c in glob.glob('*.c'): 61 | objects.append(c[:-1]+'o') 62 | curses.append(c[:-1]+'curse') 63 | facfile.write('| gcc -c %s\n' % c) 64 | facfile.write('? python3 check-for-curses.py %s > %s\n> %s\n' 65 | % (c, curses[-1], curses[-1])) 66 | facfile.write('| gcc -o hello ' + ' '.join(objects) + '\n> hello\n') 67 | for o in objects: 68 | facfile.write('< %s\n' % o) 69 | facfile.write('? cat %s > curses.log\n> curses.log\n' % ' '.join(curses)) 70 | for o in curses: 71 | facfile.write('< %s\n' % o) 72 | 73 | We create a file `configure.fac` which just tells fac to run 74 | `configure.py` to find out what to do. 75 | 76 | ##### configure.fac 77 | * python3 configure.py 78 | > .fac 79 | 80 | We use a `*` rule here because configure uses globs ("%.c") and 81 | therefore may need to be rerun if directory contents change. 82 | 83 | You might wonder, why all the trickiness with the 84 | `check-for-curses.py` rules? The optional aspect `?` is because I 85 | don't expect everyone compiling the program to have the tools needed 86 | for checking the source code (e.g. sparse). So I don't want them to 87 | have to type `fac hello` to get a successful build. The `cat` of many 88 | (in our case one) `*.curse` files into `curses.log` is so we can have a 89 | single fac target that checks the status of any source files that need 90 | to be checked (but no more). I'm assuming this may be a static 91 | checker that is slow, or even a little fuzzer. 92 | 93 | Now we can just build the executable without running the lint by calling 94 | `fac` with no arguments. 95 | 96 | $ git add configure.fac configure.py 97 | $ git commit -am 'first version' 98 | ... 99 | $ fac 100 | 1/... [...s]: python3 configure.py 101 | 2/3 [...s]: gcc -c hello.c 102 | 3/3 [...s]: gcc -o hello hello.o 103 | ... 104 | Build succeeded! ... 105 | 106 | Or we can just run the lint by calling `fac curses.log`. 107 | 108 | $ # I am first running it manually to show you the output 109 | $ python3 check-for-curses.py hello.c # fails 110 | Watch your language: "dang" in file hello.c 111 | File hello.c has 1 problems! 112 | $ fac curses.log # fails 113 | ... build failed: python3 check-for-curses.py hello.c > hello.curse 114 | Build failed 2/2 failures ... 115 | 116 | This fails and shows us that we ought to have specified a return type 117 | for `main`. It also possibly reruns `python3 configure.py`. This is 118 | because the `glob.glob('*.c')` reads the repository directory, and 119 | since the linting modified the repository directory it is possible 120 | that the output of configuring would have changed. 121 | 122 | ## Using `fac --continual` 123 | 124 | Suppose we really like linting while we are editing. In this case, we 125 | might want to execute in a convenient terminal: 126 | 127 | $ fac --continual curses.log 128 | 129 | This will wait for any changes to be made, and then re-run our lint. 130 | Thus you can edit away and see if you are introducing new problems, or 131 | have fixed existing warnings. So let's make an edit: 132 | 133 | ##### hello.c 134 | #include 135 | 136 | int main() { 137 | printf("Hello world!\n"); 138 | } 139 | 140 | You should now see in your shell that sparse passes. Yay! 141 | 142 | ## Using git hooks 143 | 144 | Now if you are very lazy (or perhaps want to ensure a test suite is 145 | already run), you can tell git to run fac for you when you do a commit 146 | (or add). 147 | 148 | ##### .git/hooks/pre-commit 149 | #!/bin/sh 150 | set -ev 151 | fac curses.log 152 | 153 | Now when you make a commit, fac will automagically build your code, 154 | and if the build fails then the commit will be reversed. (Note that 155 | this is a bit sloppy: the build may not be what is staged for commit. 156 | That would take more work.) 157 | 158 | $ chmod +x .git/hooks/pre-commit 159 | $ git commit -am 'fix profanity problem in main' 160 | fac curses.log 161 | 1/2 [...s]: python3 check-for-curses.py hello.c > hello.curse 162 | 2/2 [...s]: cat hello.curse > curses.log 163 | Build succeeded! ... 164 | ... 165 | 166 | The tricky bit here is that you are now running two instances of fac 167 | in the same directory at the same time. This is okay, since fac takes 168 | a lock in the repository before doing any building, so the two should 169 | not interfere with each other. 170 | 171 | To make clear that the test works, we can now introduce a new error: 172 | 173 | ##### hello.c 174 | #include 175 | 176 | int main() { 177 | printf("Hello world shucks!\n"); 178 | } 179 | 180 | $ git commit -am 'break the build' 181 | fac curses.log 182 | 1/1 [...s]: python3 configure.py 183 | !2/3! [...s]: build failed: python3 check-for-curses.py hello.c > hello.curse 184 | Build failed ... 185 | ... 186 | 187 | That concludes this little tutorial. Hopefully this will help you to 188 | see one way that you can use fac with both `--continual` and git 189 | hooks, as well as optional rules, to customize your build. 190 | 191 | -------------------------------------------------------------------------------- /run-tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from __future__ import print_function 3 | 4 | import glob, os, subprocess, platform, sys 5 | 6 | import build 7 | build.elapsed_time() 8 | 9 | # the following is needed for some tests to run under vagrant, since 10 | # git complains about the default email setting. 11 | if 'GIT_AUTHOR_EMAIL' not in os.environ: 12 | os.putenv("GIT_AUTHOR_EMAIL", 'Tester ') 13 | os.putenv("GIT_AUTHOR_NAME", 'Tester') 14 | os.putenv("GIT_COMMITTER_EMAIL", 'Tester ') 15 | os.putenv("GIT_COMMITTER_NAME", 'Tester') 16 | 17 | def system(cmd): 18 | # print("running:", cmd) 19 | x = subprocess.call(cmd, shell=True) 20 | # assert(x == 0) 21 | return x 22 | 23 | # this ensures that when fac calls git it already has index prepared: 24 | system('git status') 25 | 26 | try: 27 | version = subprocess.check_output(['git', 'describe', '--dirty']) 28 | except: 29 | version = subprocess.check_output(['git', 'rev-parse', 'HEAD']) 30 | version = version[:-1] 31 | tarname = 'fac-%s.tar.gz' % version 32 | 33 | system('./fac -c') 34 | system('cargo build') 35 | if system('target/debug/fac debug-fac fac'): 36 | print('Build with fac failed!') 37 | exit(1) 38 | print(build.took('building fac using rust')) 39 | 40 | 41 | def shorten_name(n): 42 | if '/' in n: 43 | n = n.split('/')[1] 44 | if '.sh' in n: 45 | n = n[:-3] 46 | return n 47 | def write_script_name(n, num=0, tot=0): 48 | n = shorten_name(n) 49 | if tot > 0: 50 | n += ' (%d/%d)' % (num+1, tot) 51 | extralen = 8 # len(' (%d/%d)' % (tot,tot)) 52 | sys.stdout.write(n+':') 53 | sys.stdout.flush() 54 | sys.stdout.write(' '*(biggestname+extralen+3-len(n))) 55 | 56 | biggestname = max([len(shorten_name(f)) 57 | for f in glob.glob('tests/*.sh') + glob.glob('tests/*.test')]) 58 | 59 | os.environ['FAC'] = os.getcwd()+"/fac" 60 | 61 | numpassed = 0 62 | numfailed = 0 63 | numskipped = 0 64 | expectedfailures = 0 65 | unexpectedpasses = 0 66 | 67 | 68 | failures = [] 69 | 70 | sh_tests = sorted(glob.glob('tests/*.sh')) 71 | num_sh = len(sh_tests); 72 | 73 | for i in range(num_sh): 74 | sh = sh_tests[i] 75 | with open(sh) as f: 76 | expect_failure = 'expect rust failure' in f.read() 77 | write_script_name(sh, i, num_sh) 78 | cmdline = 'bash %s > %s.log 2>&1' % (sh, sh) 79 | exitval = system(cmdline) 80 | if expect_failure: 81 | if exitval: 82 | print(build.green('fail'), build.took()) 83 | expectedfailures += 1 84 | else: 85 | print(build.red('pass'), sh, build.took()) 86 | if '-v' in sys.argv: 87 | os.system('cat %s.log' % sh) 88 | unexpectedpasses += 1 89 | elif exitval == 137: 90 | print(build.blue('SKIP'), build.took()) 91 | if '-v' in sys.argv: 92 | os.system('cat %s.log' % sh) 93 | numskipped += 1 94 | elif exitval: 95 | print(build.FAIL, build.took()) 96 | if '-v' in sys.argv: 97 | os.system('cat %s.log' % sh) 98 | numfailed += 1 99 | failures.append(shorten_name(sh)) 100 | else: 101 | print(build.PASS, build.took()) 102 | numpassed += 1 103 | for sh in sorted(glob.glob('tests/*.test')): 104 | if 'assertion-fails' in sh: 105 | continue 106 | write_script_name(sh) 107 | if system('%s > %s.log 2>&1' % (sh, sh)): 108 | print(build.FAIL, build.took()) 109 | if '-v' in sys.argv: 110 | os.system('cat %s.log' % sh) 111 | numfailed += 1 112 | failures.append(shorten_name(sh)) 113 | else: 114 | print(build.PASS, build.took()) 115 | numpassed += 1 116 | 117 | for sh in sorted(glob.glob('bugs/*.sh')): 118 | write_script_name(sh) 119 | if system('bash %s > %s.log 2>&1' % (sh, sh)): 120 | print(build.green('fail'), build.took()) 121 | expectedfailures += 1 122 | else: 123 | print(build.red('pass'), sh, build.took()) 124 | if '-v' in sys.argv: 125 | os.system('cat %s.log' % sh) 126 | unexpectedpasses += 1 127 | for sh in sorted(glob.glob('bugs/*.test')): 128 | write_script_name(sh) 129 | if system('bash %s > %s.log 2>&1' % (sh, sh)): 130 | print(build.green('fail'), build.took()) 131 | expectedfailures += 1 132 | else: 133 | print(build.red('pass'), build.took()) 134 | if '-v' in sys.argv: 135 | os.system('cat %s.log' % sh) 136 | unexpectedpasses += 1 137 | 138 | def pluralize(num, noun): 139 | if num == 1: 140 | return str(num)+' '+noun 141 | else: 142 | if (noun[-1] == 's'): 143 | return str(num)+' '+noun+'es' 144 | return str(num)+' '+noun+'s' 145 | 146 | os.environ['FAC'] = os.getcwd()+"/debug-fac" 147 | 148 | debug_numpassed = 0 149 | debug_numfailed = 0 150 | debug_numskipped = 0 151 | 152 | debug_numexpectedfailed = 0 153 | debug_numunexpectedpassed = 0 154 | 155 | for i in range(num_sh): 156 | sh = sh_tests[i] 157 | with open(sh) as f: 158 | expect_failure = 'expect rust failure' in f.read() 159 | write_script_name(sh, i, num_sh) 160 | cmdline = 'bash %s > %s.debug.log 2>&1' % (sh, sh) 161 | exitval = system(cmdline) 162 | if expect_failure: 163 | if exitval == 137: 164 | print(build.blue('SKIP'), "(debug)", build.took()) 165 | if '-v' in sys.argv: 166 | os.system('cat %s.debug.log' % sh) 167 | debug_numskipped += 1 168 | elif exitval: 169 | print(build.warn('fail'), "(debug)", build.took()) 170 | if '-v' in sys.argv: 171 | os.system('cat %s.debug.log' % sh) 172 | debug_numexpectedfailed += 1 173 | else: 174 | print(build.red('pass'), "(debug)", build.took()) 175 | debug_numunexpectedpassed += 1 176 | else: 177 | if exitval == 137: 178 | print(build.blue('SKIP'), "(debug)", build.took()) 179 | if '-v' in sys.argv: 180 | os.system('cat %s.debug.log' % sh) 181 | debug_numskipped += 1 182 | elif exitval: 183 | print(build.FAIL, "(debug)", build.took()) 184 | if '-v' in sys.argv: 185 | os.system('cat %s.debug.log' % sh) 186 | debug_numfailed += 1 187 | else: 188 | print(build.PASS, "(debug)", build.took()) 189 | debug_numpassed += 1 190 | 191 | print() 192 | if debug_numfailed: 193 | print(build.red('Debug failed ' + str(debug_numfailed)+'/'+str(debug_numfailed+debug_numpassed))) 194 | else: 195 | print('All', pluralize(debug_numpassed, 'test'), 'passed using debug!') 196 | 197 | if debug_numunexpectedpassed: 198 | print(build.red('Debug unexpectedly passed ' + str(debug_numunexpectedpassed) 199 | +'/'+str(debug_numunexpectedpassed+debug_numexpectedfailed) 200 | +' expected failures')) 201 | else: 202 | print('All', pluralize(debug_numexpectedfailed, 'test'), 'exected failures failed using debug!') 203 | 204 | if numfailed: 205 | for f in failures: 206 | print(build.red(' FAILED '+f)) 207 | print(build.red('Failed ' + str(numfailed)+'/'+str(numfailed+numpassed))) 208 | else: 209 | print('All', pluralize(numpassed, 'test'), 'passed!') 210 | 211 | if expectedfailures: 212 | print(pluralize(expectedfailures, 'expected failure')) 213 | if numskipped: 214 | print(pluralize(numskipped, 'test'), 'skipped') 215 | 216 | if unexpectedpasses: 217 | print(build.red(pluralize(unexpectedpasses, 'unexpected pass'))) 218 | 219 | if numfailed or debug_numfailed: 220 | exit(1) 221 | -------------------------------------------------------------------------------- /src/build/hashstat.rs: -------------------------------------------------------------------------------- 1 | //! A file structure for holding hash and "stat" information about 2 | //! files. This is what fac uses to determine if a file has changed. 3 | 4 | #[cfg(test)] 5 | extern crate quickcheck; 6 | 7 | use std; 8 | use std::io::{Read}; 9 | use std::hash::{Hasher}; 10 | use metrohash::MetroHash64; 11 | 12 | #[cfg(unix)] 13 | use std::os::unix::fs::{MetadataExt}; 14 | 15 | use crate::build::{FileKind}; 16 | 17 | /// The stat information about a file 18 | #[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Hash, Debug)] 19 | pub struct HashStat { 20 | /// modification time 21 | pub time: i64, 22 | /// in nanoseconds 23 | pub time_ns: i32, 24 | /// The file size. We truncate more significant bits because we 25 | /// only care about size changing, and odds of a change by 4G are 26 | /// slim. 27 | pub size: u32, 28 | /// hash 29 | pub hash: u64, 30 | /// kind of file 31 | pub kind: Option, 32 | } 33 | 34 | #[cfg(test)] 35 | impl quickcheck::Arbitrary for HashStat { 36 | fn arbitrary(g: &mut quickcheck::Gen) -> HashStat { 37 | HashStat { 38 | time: i64::arbitrary(g), 39 | time_ns: i32::arbitrary(g), 40 | size: u32::arbitrary(g), 41 | hash: u64::arbitrary(g), 42 | kind: None, 43 | } 44 | } 45 | } 46 | 47 | #[cfg(test)] 48 | quickcheck::quickcheck! { 49 | fn prop_encode_decode(hs: HashStat) -> bool { 50 | println!("original {:?}", hs); 51 | println!("encoded {:?}", hs.encode()); 52 | println!("decoded {:?}", HashStat::decode(&hs.encode())); 53 | hs == HashStat::decode(&hs.encode()) 54 | } 55 | } 56 | 57 | fn encode(i: u64) -> [u8; 16] { 58 | let mut out = [0;16]; 59 | for x in 0..16 { 60 | let hexit = ((i >> (x*4)) & 15) as u8; 61 | if hexit < 10 { 62 | out[x] = b'0' + hexit; 63 | } else { 64 | out[x] = b'a' + (hexit-10); 65 | } 66 | } 67 | out 68 | } 69 | 70 | // fn decode(i: &[u8; 16]) -> u64 { 71 | fn decode(i: &[u8]) -> u64 { 72 | let mut out = 0; 73 | for x in 0..16 { 74 | let hexit = if i[x] < b'a' { 75 | i[x] - b'0' 76 | } else { 77 | 10 + i[x] - b'a' 78 | }; 79 | out += (hexit as u64) << (x*4); 80 | } 81 | out 82 | } 83 | 84 | impl HashStat { 85 | /// encode as bytes 86 | pub fn encode(&self) -> Vec { 87 | let mut v = Vec::from(&encode(self.size as u64 + ((self.time_ns as u64) << 32))[..]); 88 | v.extend(&encode(self.time as u64)[..]); 89 | v.extend(&encode(self.hash)[..]); 90 | v 91 | } 92 | /// decode from bytes 93 | pub fn decode(h: &[u8]) -> HashStat { 94 | if h.len() != 3*16 { 95 | return HashStat { 96 | size: 0, 97 | time: 0, 98 | time_ns: 0, 99 | hash: 0, 100 | kind: None, 101 | }; 102 | } 103 | let size_time = decode(h); 104 | HashStat { 105 | size: size_time as u32, 106 | time: decode(&h[16..]) as i64, 107 | time_ns: (size_time >> 32) as i32, 108 | hash: decode(&h[32..]), 109 | kind: None, 110 | } 111 | } 112 | } 113 | 114 | fn kind_of(m: &std::fs::Metadata) -> Option { 115 | if m.file_type().is_symlink() { 116 | Some(FileKind::Symlink) 117 | } else if m.file_type().is_dir() { 118 | Some(FileKind::Dir) 119 | } else if m.file_type().is_file() { 120 | Some(FileKind::File) 121 | } else { 122 | None 123 | } 124 | } 125 | 126 | /// stat a file 127 | #[cfg(not(unix))] 128 | pub fn stat(f: &std::path::Path) -> std::io::Result { 129 | let s = std::fs::symlink_metadata(f)?; 130 | Ok(HashStat { 131 | time: 0, 132 | time_ns: 0, 133 | size: s.len() as u32, 134 | hash: 0, 135 | kind: kind_of(&s), 136 | }) 137 | } 138 | /// stat a file 139 | #[cfg(unix)] 140 | pub fn stat(f: &std::path::Path) -> std::io::Result { 141 | let s = std::fs::symlink_metadata(f)?; 142 | Ok(HashStat { 143 | time: s.mtime(), 144 | time_ns: (s.mtime_nsec()/10) as i32, 145 | size: s.len() as u32, 146 | hash: 0, 147 | kind: kind_of(&s), 148 | }) 149 | } 150 | 151 | /// hash and stat a file 152 | pub fn hashstat(f: &std::path::Path) -> std::io::Result { 153 | let mut hs = stat(f)?; 154 | hs.hash(f)?; 155 | Ok(hs) 156 | } 157 | 158 | impl HashStat { 159 | /// A HashStat that does not have any information. 160 | pub fn empty() -> HashStat { 161 | HashStat { 162 | time: 0, 163 | time_ns: 0, 164 | size: 0, 165 | hash: 0, 166 | kind: None, 167 | } 168 | } 169 | /// is the hash known? 170 | pub fn unfinished(&self) -> bool { 171 | self.hash == 0 || self.size == 0 || self.time == 0 172 | } 173 | /// look up any bits of the hashstat that we do not yet know. 174 | pub fn finish(&mut self, f: &std::path::Path) -> std::io::Result<()> { 175 | if self.size == 0 && self.time == 0 { 176 | match hashstat(f) { 177 | Ok(h) => { 178 | *self = h; 179 | }, 180 | Err(_) => { 181 | // nothing to do 182 | } 183 | }; 184 | } else if self.hash == 0 { 185 | self.hash(f)?; 186 | } 187 | Ok(()) 188 | } 189 | /// try running sat 190 | pub fn stat(&mut self, f: &std::path::Path) -> std::io::Result<()> { 191 | if self.time == 0 && self.size == 0 { 192 | *self = stat(f)?; 193 | } 194 | Ok(()) 195 | } 196 | /// see if it matches 197 | pub fn matches(&mut self, f: &std::path::Path, other: &HashStat) -> bool { 198 | self.stat(f).ok(); 199 | if self.size != other.size { 200 | return false; 201 | } 202 | if self.time == other.time && self.time_ns == other.time_ns { 203 | return true; 204 | } 205 | if self.hash == 0 { 206 | self.finish(f).unwrap(); 207 | } 208 | self.hash == other.hash 209 | } 210 | /// see if it we know matches without doing any disk IO 211 | pub fn cheap_matches(&mut self, other: &HashStat) -> bool { 212 | if self.size == 0 { 213 | return false; 214 | } 215 | self.size == other.size 216 | && self.time == other.time 217 | && self.time_ns == other.time_ns 218 | } 219 | /// hash a file 220 | fn hash(&mut self, f: &std::path::Path) -> std::io::Result<()> { 221 | let mut h = MetroHash64::new(); 222 | match self.kind { 223 | Some(FileKind::File) => { 224 | let mut file = std::fs::File::open(f)?; 225 | let mut contents = Vec::new(); 226 | file.read_to_end(&mut contents)?; 227 | h.write(&contents); 228 | }, 229 | Some(FileKind::Dir) => { 230 | let mut entries = Vec::new(); 231 | for entry in std::fs::read_dir(f)? { 232 | entries.push(entry?.file_name()); 233 | } 234 | entries.sort(); 235 | for s in entries { 236 | h.write(osstr_to_bytes(&s)); 237 | } 238 | }, 239 | Some(FileKind::Symlink) => { 240 | h.write(osstr_to_bytes(std::fs::read_link(f)?.as_os_str())); 241 | }, 242 | None => (), 243 | } 244 | self.hash = h.finish(); 245 | Ok(()) 246 | } 247 | 248 | } 249 | 250 | use std::ffi::{OsStr}; 251 | #[cfg(unix)] 252 | use std::os::unix::ffi::{OsStrExt}; 253 | /// Convert OsStr to bytes 254 | #[cfg(unix)] 255 | pub fn osstr_to_bytes(b: &OsStr) -> &[u8] { 256 | OsStr::as_bytes(b) 257 | } 258 | 259 | /// Convert OsStr to bytes 260 | #[cfg(not(unix))] 261 | pub fn osstr_to_bytes(b: &OsStr) -> &[u8] { 262 | b.to_str().unwrap().as_bytes() 263 | } 264 | --------------------------------------------------------------------------------