├── .github └── workflows │ ├── book.yml │ └── rust.yml ├── .gitignore ├── .markdownlint.json ├── .vscode ├── launch.json └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── TODO.md ├── Werkfile ├── book ├── .gitignore ├── book.toml └── src │ ├── SUMMARY.md │ ├── build_config.md │ ├── build_recipes.md │ ├── color.md │ ├── command_line.md │ ├── command_line_completion.md │ ├── command_line_help.txt │ ├── depfile_support.md │ ├── env.md │ ├── examples │ ├── .gitignore │ ├── c.md │ ├── c │ │ ├── Werkfile │ │ └── src │ │ │ ├── foo.c │ │ │ ├── foo.h │ │ │ └── main.c │ ├── cargo.md │ ├── cargo │ │ ├── Cargo.toml │ │ ├── Werkfile │ │ └── src │ │ │ └── main.rs │ ├── complex.md │ ├── complex │ │ ├── Cargo.toml │ │ ├── Werkfile │ │ ├── assets │ │ │ └── logo.png │ │ ├── plugin1 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── plugin2 │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── program │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ └── main.rs │ │ └── shaders │ │ │ ├── shader.frag │ │ │ └── shader.vert │ ├── shaders.md │ └── shaders │ │ ├── Werkfile │ │ ├── shader.frag │ │ └── shader.vert │ ├── features.md │ ├── first_example.md │ ├── getting_started.md │ ├── introduction.md │ ├── language.md │ ├── language │ ├── arrays.md │ ├── builtins.md │ ├── config.md │ ├── expressions.md │ ├── include.md │ ├── path_resolution.md │ ├── patterns.md │ ├── recipe_commands.md │ ├── strings.md │ └── variables.md │ ├── long_running_tasks.md │ ├── outdatedness.md │ ├── paths.md │ ├── task_recipes.md │ ├── watch.md │ ├── werk_cache.md │ ├── why_not_just.md │ ├── why_not_make.md │ ├── why_not_others.md │ └── workspace.md ├── documentation └── README.md ├── examples ├── README.md ├── c │ ├── Werkfile │ ├── foo.c │ ├── foo.h │ └── main.c ├── demo │ └── Werkfile ├── hello │ ├── Werkfile │ └── target │ │ └── .werk-cache ├── include │ ├── Werkfile │ ├── config.werk │ ├── foo.c │ ├── foo.h │ ├── main.c │ └── recipes.werk ├── issue-41 │ └── Werkfile ├── shaders │ ├── Werkfile │ ├── shader.frag │ └── shader.vert └── write │ └── Werkfile ├── tests ├── Cargo.toml ├── cases │ ├── array.werk │ ├── copy.werk │ ├── dedup.werk │ ├── discard.werk │ ├── env.werk │ ├── filter.werk │ ├── flatten.werk │ ├── join.werk │ ├── map.werk │ ├── match_expr.werk │ ├── nested_patterns.werk │ ├── read.werk │ ├── split.werk │ ├── string_interp.werk │ └── write.werk ├── fail │ ├── ambiguous_build_recipe.txt │ ├── ambiguous_build_recipe.werk │ ├── ambiguous_path_resolution.txt │ ├── ambiguous_path_resolution.werk │ ├── capture_group_out_of_bounds.txt │ ├── capture_group_out_of_bounds.werk │ ├── duplicate_config.txt │ ├── duplicate_config.werk │ ├── include_missing.txt │ ├── include_missing.werk │ ├── include_self.txt │ ├── include_self.werk │ ├── include_twice.txt │ ├── include_twice.werk │ ├── include_with_default.txt │ ├── include_with_default.werk │ ├── include_with_error.txt │ ├── include_with_error.werk │ ├── index_out_of_bounds.txt │ └── index_out_of_bounds.werk ├── lib.rs ├── mock_io.rs ├── test_cases.rs ├── test_eval.rs ├── test_expressions.rs ├── test_outdatedness.rs ├── test_path_resolution.rs └── test_pattern_match.rs ├── werk-cli ├── Cargo.toml ├── build.rs ├── complete.rs ├── dry_run.rs ├── main.rs ├── render.rs └── render │ ├── ansi.rs │ ├── ansi │ ├── progress.rs │ └── term_width.rs │ ├── json.rs │ ├── log.rs │ ├── null.rs │ └── stream.rs ├── werk-fs ├── Cargo.toml ├── absolute.rs ├── lib.rs ├── path.rs ├── path │ └── validate.rs ├── sym.rs └── traits.rs ├── werk-parser ├── Cargo.toml ├── ast.rs ├── ast │ ├── expr.rs │ ├── keyword.rs │ ├── string.rs │ └── token.rs ├── error.rs ├── lib.rs ├── parser.rs ├── parser │ └── string.rs ├── pattern.rs └── tests │ ├── fail │ ├── build_ident_name.txt │ ├── build_ident_name.werk │ ├── default_invalid_value.txt │ ├── default_invalid_value.werk │ ├── default_unknown_key.txt │ ├── default_unknown_key.werk │ ├── expr_trailing_pipe.txt │ ├── expr_trailing_pipe.werk │ ├── invalid_escape.txt │ ├── invalid_escape.werk │ ├── let_no_eq.txt │ ├── let_no_eq.werk │ ├── let_no_ident.txt │ ├── let_no_ident.werk │ ├── let_no_value.txt │ ├── let_no_value.werk │ ├── match_no_arrow.txt │ ├── match_no_arrow.werk │ ├── match_unterminated.txt │ ├── match_unterminated.werk │ ├── root_invalid.txt │ ├── root_invalid.werk │ ├── spawn_in_build_recipe.txt │ ├── spawn_in_build_recipe.werk │ ├── spawn_in_build_recipe_run_block.txt │ ├── spawn_in_build_recipe_run_block.werk │ ├── task_string_name.txt │ └── task_string_name.werk │ ├── succeed │ ├── c.json │ ├── c.werk │ ├── config.json │ ├── config.werk │ ├── expr_parens.json │ ├── expr_parens.werk │ ├── let_list.json │ ├── let_list.werk │ ├── let_map.json │ ├── let_map.werk │ ├── let_match.json │ ├── let_match.werk │ ├── let_match_inline.json │ ├── let_match_inline.werk │ ├── let_simple.json │ ├── let_simple.werk │ ├── let_simple_interp.json │ ├── let_simple_interp.werk │ ├── spawn.json │ └── spawn.werk │ └── test_cases.rs ├── werk-runner ├── Cargo.toml ├── cache.rs ├── depfile.rs ├── error.rs ├── eval.rs ├── eval │ ├── string.rs │ └── used.rs ├── io.rs ├── io │ └── child.rs ├── ir.rs ├── lib.rs ├── outdatedness.rs ├── pattern.rs ├── render.rs ├── runner.rs ├── runner │ ├── command.rs │ ├── dep_chain.rs │ └── task.rs ├── scope.rs ├── shell.rs ├── value.rs ├── warning.rs └── workspace.rs ├── werk-util ├── Cargo.toml ├── cancel.rs ├── diagnostic.rs ├── lib.rs ├── os_str.rs ├── semantic_hash.rs ├── span.rs └── symbol.rs ├── werk-vscode ├── .vscode │ └── launch.json ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── language-configuration.json ├── package.json ├── syntaxes │ └── werk.tmLanguage.json └── vsc-extension-quickstart.md └── werk.sublime-syntax /.github/workflows/book.yml: -------------------------------------------------------------------------------- 1 | name: Book 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write # To push a branch 12 | pages: write # To push to a GitHub Pages site 13 | id-token: write # To update the deployment status 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Install latest mdbook 19 | run: | 20 | tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name') 21 | url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz" 22 | mkdir mdbook 23 | curl -sSL $url | tar -xz --directory=./mdbook 24 | echo `pwd`/mdbook >> $GITHUB_PATH 25 | - name: Build Book 26 | run: | 27 | # This assumes your book is in the root of your repository. 28 | # Just add a `cd` here if you need to change to another directory. 29 | cd book 30 | mdbook build 31 | - name: Setup Pages 32 | uses: actions/configure-pages@v4 33 | - name: Upload artifact 34 | uses: actions/upload-pages-artifact@v3 35 | with: 36 | # Upload entire repository 37 | path: 'book/book' 38 | - name: Deploy to GitHub Pages 39 | id: deployment 40 | uses: actions/deploy-pages@v4 41 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | name: Build and test 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: dtolnay/rust-toolchain@stable 24 | - name: Build 25 | run: cargo build 26 | - name: Run tests 27 | run: cargo test 28 | lint: 29 | name: Lint 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: dtolnay/rust-toolchain@stable 34 | - name: cargo clippy 35 | run: cargo clippy --all-features -- -D warnings 36 | fmt: 37 | name: Formatting 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: dtolnay/rust-toolchain@stable 42 | - name: cargo fmt --check 43 | run: cargo fmt -- --check --color=always 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD025": false, 3 | "MD033": false 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "rust-analyzer.check.command": "clippy", 4 | "rewrap.wrappingColumn": 80 5 | } 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "werk-parser", 4 | "werk-cli", 5 | "werk-runner", 6 | "werk-fs", 7 | "tests", 8 | "werk-util", 9 | ] 10 | resolver = "2" 11 | 12 | [workspace.package] 13 | version = "0.1.0" 14 | rust-version = "1.85" 15 | edition = "2024" 16 | license = "MIT OR Apache-2.0" 17 | 18 | [workspace.dependencies] 19 | ahash = "0.8.11" 20 | werk-runner.path = "werk-runner" 21 | werk-parser.path = "werk-parser" 22 | werk-fs.path = "werk-fs" 23 | werk-util.path = "werk-util" 24 | winnow = "0.7.0" 25 | thiserror = "2.0" 26 | tracing = "0.1.40" 27 | indexmap = { version = "2.6.0", features = ["serde"] } 28 | parking_lot = "0.12.3" 29 | regex = "1.11.1" 30 | regex-syntax = "0.8.5" 31 | serde = { version = "1.0.215", features = ["derive"] } 32 | smol = "2.0.2" 33 | toml_edit = "0.22.22" 34 | futures = "0.3.31" 35 | annotate-snippets = "0.11.5" 36 | anstream = "0.6.18" 37 | 38 | [workspace.lints.clippy] 39 | pedantic = { level = "warn", priority = -1 } 40 | missing_panics_doc = "allow" 41 | missing_errors_doc = "allow" 42 | inline_always = "allow" 43 | similar_names = "allow" 44 | 45 | [profile.release] 46 | codegen-units = 1 47 | lto = true 48 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Werk Build System And Command Runner 2 | 3 | `werk` is a simplistic and opinionated command runner, similar to `just`, and 4 | also a simplistic build system, similar to `make`. 5 | 6 | You Betta Werk! 💅 7 | 8 | > [!CAUTION] 9 | > Werk is early alpha software. Use at your own risk. It may eat your 10 | > files, so run `git commit` before trying it out. 11 | 12 | ## Why? 13 | 14 | GNU make is extremely useful, but very hard to use correctly, especially if you 15 | have modern expectations of your build system, and you just want a convenient 16 | way to execute build scripts, create asset packs, or run housekeeping tasks in 17 | your project. 18 | 19 | `just` is also extremely useful, and easy to use, but cannot build files. It can 20 | only run commands, delegating to `make`, `cargo`, or other build systems to 21 | actually produce output. Furthermore, it can be difficult to write 22 | cross-platform `Justfile`s, usually relying on a platform-specific shell 23 | availability. 24 | 25 | For mode details, consult the [the book](https://simonask.github.io/werk). 26 | 27 | ## Installation instructions 28 | 29 | `werk` can currently only be installed from source, which requires that you have 30 | Rust and Cargo installed. 31 | 32 | * Clone this repository. 33 | * Run `cargo install --path werk-cli`. 34 | * Ensure that your `$PATH` contains the path to Cargo binaries. This is usually 35 | the case if you have a working installation of Rust and Cargo. 36 | * Cargo installs binaries in `$CARGO_HOME/bin`. 37 | * On UNIX-like systems, the default install location is `$HOME/.cargo/bin`. 38 | * On Windows, the default install location is `%USERPROFILE%\.cargo\bin`. 39 | 40 | ### Language Support for VS Code 41 | 42 | * Clone this repository. 43 | * Install the extension from `werk-vscode`: 44 | * From the command-line: `code --install-extension ` 45 | * From within VS Code: Run "Developer: Install Extension from Location..." and 46 | point it to the path to the `werk-vscode` directory within this 47 | repository. 48 | 49 | ## Features and limitations 50 | 51 | See [Features and limitations](https://simonask.github.io/werk/features.html). 52 | 53 | ## Project non-goals 54 | 55 | * `werk` will probably never be fastest. 56 | 1. User friendliness is always higher priority. 57 | 2. Reporting "no changes" quickly is specifically not a goal. Use Ninja if 58 | this is important to you. Typically, `werk` is invoked when the user has 59 | actually made changes. 60 | 3. That said, `werk` does try to be reasonably fast, and is implemented in 61 | Rust using best practices. 62 | * `werk` will probably never support all use cases. 63 | 1. It is designed to support the use cases that are important to me, the 64 | author. 65 | 2. The needs of build systems are vast and varied. Use the one that fits 66 | your purposes, or file a feature request if you believe that `werk` would 67 | be greater if it could reasonably support it. 68 | * `werk` will never be a scripting language. It is strictly declarative with 69 | minimal support for logic and expressions, but doesn't have (and won't have) 70 | loop constructs. 71 | 72 | ## Examples 73 | 74 | See [Examples](./examples). 75 | 76 | ## Roadmap 77 | 78 | * [ ] IDE integration to run individual tasks. 79 | * [ ] WASM host support. 80 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - [ ] Consider using pattern syntax for glob patterns instead of standard glob 4 | syntax. 5 | - [ ] Autoclean: Match files in the output directory against available recipes 6 | and delete them if they are older than `.werk-cache`. 7 | 8 | ## Done 9 | 10 | - [x] Autowatcher: Run a recipe and detect which files where checked for 11 | outdatedness, and then watch those files and rebuild when any of them changes. 12 | - [x] Don't use `RUST_LOG` to enable logging, as it interferes with child 13 | processes. ~~Use `WERK_LOG` instead.~~ 14 | - [x] Support generating depfiles in the same command that compiles the file. 15 | The logic should be, if the target has a `depfile` field: Only read the 16 | depfile if the target already exists and isn't outdated anyway due to mtime. 17 | If the depfile is missing/outdated, the target is outdated. Determine how to 18 | build the depfile by checking if a recipe can be found that would build it, 19 | and otherwise assume that the compiler invocation generates the depfile (`-MM 20 | -MF`). If the depfile was not generated by the compiler invocation, issue a 21 | warning after building the recipe (not an error). Recipes that have the 22 | `depfile` field, but never actually generate a depfile (either by an explicit 23 | recipe or implicitly) will always be outdated. 24 | - [x] Take `werk.toml` modification time into account in outdatedness. 25 | - [x] Take command-line `--define`s into account in outdatedness. 26 | -------------------------------------------------------------------------------- /Werkfile: -------------------------------------------------------------------------------- 1 | config mdbook-flags = "--open" 2 | 3 | # Serve the documentation using mdbook and open it in the default browser. 4 | task mdbook { 5 | let book-dir = "book" 6 | spawn "mdbook serve {mdbook-flags*} -d " 7 | } 8 | 9 | # Install werk-cli using a current installation of Werk. 10 | task install { 11 | let cli = "werk-cli" 12 | run "cargo install --locked --path " 13 | } 14 | -------------------------------------------------------------------------------- /book/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Simon Ask Ulsnes"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Werk Book" 7 | 8 | [build] 9 | create-missing = false 10 | 11 | [rust] 12 | edition = "2024" 13 | 14 | [output.html] 15 | site-url = "/werk/" 16 | git-repository-url = "https://github.com/simonask/werk" 17 | edit-url-template = "https://github.com/simonask/werk/edit/main/book/{path}" 18 | -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./introduction.md) 4 | - [Getting started](./getting_started.md) 5 | - [My first Werkfile](./first_example.md) 6 | - [Features and limitations](./features.md) 7 | - [Why not `make`?](./why_not_make.md) 8 | - [Why not `just`?](./why_not_just.md) 9 | - [Why not `$toolname`?](./why_not_others.md) 10 | 11 | # User Guide 12 | 13 | - [Workspace](./workspace.md) 14 | - [Paths](./paths.md) 15 | - [.werk-cache](./werk_cache.md) 16 | - [Task recipes](./task_recipes.md) 17 | - [Long-running tasks](./long_running_tasks.md) 18 | - [Build recipes](./build_recipes.md) 19 | - [When is a target outdated?](./outdatedness.md) 20 | - [Depfile support](./depfile_support.md) 21 | - [Configure your build](./build_config.md) 22 | - [Watch for changes](./watch.md) 23 | - [Color support](./color.md) 24 | 25 | # Reference Guide 26 | 27 | - [Language Reference](./language.md) 28 | - [Variables](./language/variables.md) 29 | - [Expressions](./language/expressions.md) 30 | - [String Interpolation](./language/strings.md) 31 | - [Path resolution](./language/path_resolution.md) 32 | - [Patterns](./language/patterns.md) 33 | - [Recipe commands](./language/recipe_commands.md) 34 | - [Built-in variables](./language/builtins.md) 35 | - [Include](./language/include.md) 36 | - [Arrays](./language/arrays.md) 37 | - [Command-line reference](command_line.md) 38 | - [Completion](command_line_completion.md) 39 | - [Environment variables](env.md) 40 | 41 | ----------------------------- 42 | 43 | - [Example: C program](./examples/c.md) 44 | - [Example: Cargo project](./examples/cargo.md) 45 | - [Example: GLSL shaders](./examples/shaders.md) 46 | - [Example: Cargo + WASM + Assets](./examples/complex.md) 47 | -------------------------------------------------------------------------------- /book/src/build_config.md: -------------------------------------------------------------------------------- 1 | # Build configuration 2 | 3 | ## Configure how werk runs 4 | 5 | The behavior of the `werk` command can be configured in two ways: 6 | 7 | 1. Command-line arguments 8 | 2. `default` statements within the Werkfile 9 | 10 | `default` statements take precedence over built-in defaults. Command-line 11 | arguments take precedence over `default` statements. 12 | 13 | Reference: 14 | 15 | ```werk 16 | # Set the output directory, relative to the workspace root. Default is "target". 17 | default out-dir = "output-directory" 18 | 19 | # Set the default recipe to run when werk is run without arguments. 20 | default target = "recipe-name" 21 | ``` 22 | 23 | ## Customize your tasks and recipes 24 | 25 | Build configuration variables in a Werkfile can be overridden from the 26 | command-line using `-Dkey=value`, where `key` is the name of a `config key = 27 | ...` statement in the global scope, and `value` is a string value. 28 | 29 | `config` statements work exactly like `let` statements, except that it is an 30 | error if multiple identical `config` keys exist in the Werkfile. `let` 31 | statements may shadow `config` statements and vice versa, but `config` 32 | statements cannot shadow other `config` statements. 33 | 34 | When a `config` variable is overridden from the command line, the value is 35 | inserted during evaluation at the point where the `config` statement occurs, and 36 | subsequent statements will see the value passed on the command-line. 37 | 38 | Consider this Werkfile: 39 | 40 | ```werk 41 | config greeting = "Hello" 42 | 43 | task greet { 44 | info "{greeting}, World!" 45 | } 46 | ``` 47 | 48 | Running this normally: 49 | 50 | ```sh 51 | $ werk greet 52 | [info] Hello, World! 53 | [ ok ] greet 54 | ``` 55 | 56 | Override the greeting: 57 | 58 | ```sh 59 | $ werk greet -Dgreeting=Goodbye 60 | [info] Goodbye, World! 61 | [ ok ] greet 62 | ``` 63 | 64 | A typical use case for this is to override the build "profile", i.e., whether to 65 | build in debug or release mode. 66 | 67 | Example using the [`match expression`](./language/operations.md#match) to 68 | validate the argument: 69 | 70 | ```werk 71 | config profile = "debug" 72 | let cflags = profile | match { 73 | "debug" => ["-O0", "-g"] 74 | "release" => ["-O3"] 75 | "%" => error "unknown build profile '{profile}'" 76 | } 77 | ``` 78 | 79 | Running this with default arguments: 80 | 81 | ```sh 82 | $ werk --list 83 | Config variables: 84 | profile = "debug" 85 | ``` 86 | 87 | Overriding the argument: 88 | 89 | ```sh 90 | $ werk --list -Dprofile=release 91 | Config variables: 92 | profile = "release" 93 | cflags = ["-O3"] 94 | ``` 95 | 96 | Overriding the argument with an invalid value: 97 | 98 | ```sh 99 | $ werk --list -Dprofile=wrong 100 | Error: unknown build profile 'wrong' 101 | ``` 102 | -------------------------------------------------------------------------------- /book/src/build_recipes.md: -------------------------------------------------------------------------------- 1 | # Build recipes 2 | 3 | Build recipes tell `werk` how to produce a file. They are the equivalent of Make 4 | rules. 5 | 6 | Build recipes are defined in terms of a [pattern](./language/patterns.md), which 7 | may just be the literal name of a file, but it can also be Make-like patterns 8 | with a substitution stem. 9 | 10 | When a build recipe has one or more `run` statements, the recipe will execute 11 | [recipe commands](./language/recipe_commands.md) when invoked in order to 12 | produce the output file. 13 | 14 | Build recipes may depend on each other. When multiple files depend on the same 15 | recipe, that recipe is only executed exactly once (before any of its dependents 16 | are built). 17 | 18 | When a target is outdated, it and all of its dependents will be rebuilt. See the 19 | [outdatedness](./outdatedness.md) chapter for the detailed rules governing when 20 | targets are rebuilt. 21 | 22 | Build recipes should always place their output in the output directory. This can 23 | be achieved by using [path interpolation](./language/strings.md#paths) 24 | (`"<...>"`) when passing files as arguments to external commands. 25 | 26 | ## Reference 27 | 28 | This example builds an `.o` object file from a `.c` source file. See 29 | [Patterns](./language/patterns.md) for more information about which patterns are 30 | supported. 31 | 32 | ```werk 33 | build "%.o" { 34 | # Define a local variable, here setting the name of the source file. 35 | let source-file = "%.c" 36 | 37 | # Define the dependencies of this recipe. May be a list or a single value. 38 | from source-file 39 | 40 | # Set the depfile for this recipe. 41 | depfile "{source-file:.c=.d}" 42 | 43 | # Disable forwarding the output of executed commands to the console. 44 | # Default is to capture (silence) in build recipes. Note that errors and warnings 45 | # from compilers are always forwarded. 46 | capture true 47 | 48 | # Set an environment variable for all child processes in this recipe. 49 | env "MY_VAR" = "value" 50 | 51 | # Remove an environment variable for all child processes in this recipe. 52 | env-remove "MY_VAR" 53 | 54 | # Run an external program to build the file. 55 | # out is the target file of the recipe, and in is the first dependency. 56 | run "clang -c -o " 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /book/src/color.md: -------------------------------------------------------------------------------- 1 | # Color support 2 | 3 | `werk` automatically detects whether or not it is running in a terminal, and 4 | [respects conventional color support environment variables](./env.md). 5 | 6 | Since `werk` captures the stdout/stderr of child processes, programs executed by 7 | `werk` cannot detect that they are running in a terminal, so the only way to get 8 | them to produce color output is to instruct them via environment variables or 9 | command-line arguments. 10 | 11 | `werk` automatically adjusts the color environment variables for child 12 | processes such that child processes get a consistent idea of whether or not 13 | color output should be enabled. For example, `CLICOLOR=1` and `NO_COLOR=1` will 14 | never both be set for a child process. 15 | 16 | Some programs do not respect these conventional environment variables, and must 17 | manually be passed command-line arguments to enable color output. For example, 18 | `clang` must be run with `-fcolor-diagnostics -fansi-escape-codes` to produce 19 | color output when run through `werk` on all platforms. The [built-in global 20 | variable `COLOR`](./language/builtins.md) can be used to conditionally pass such 21 | arguments to compilers when `werk` itself has color output enabled. 22 | 23 | ## Example 24 | 25 | ```werk 26 | let color-cflags = COLOR | match { 27 | "1" => ["-fcolor-diagnostics", "-fansi-escape-codes"] 28 | "%" => [] 29 | } 30 | let cflags = ["-O0", "-g", color-cflags] 31 | ``` 32 | 33 | ## Progress indicator 34 | 35 | When `werk` detects that it is running in a terminal, and color is not disabled 36 | through environment variables or command-line options, it will print and update 37 | a progress indicator (spinner or progress bar, depending on settings) to the 38 | terminal. 39 | 40 | ## Windows support 41 | 42 | `werk` only supports ANSI colors and automatically attempts to set 43 | `ENABLE_VIRTUAL_TERMINAL_PROCESSING` on Windows 10 and above. Legacy Windows 44 | Console color is _not_ supported, so child processes should also be instructed 45 | to emit ANSI color codes, such as passing `-fansi-escape-codes` to `clang`. 46 | -------------------------------------------------------------------------------- /book/src/command_line.md: -------------------------------------------------------------------------------- 1 | # Command-line reference 2 | 3 | Output of `werk --help`: 4 | 5 | ```plain 6 | {{#include command_line_help.txt}} 7 | ``` 8 | -------------------------------------------------------------------------------- /book/src/command_line_completion.md: -------------------------------------------------------------------------------- 1 | # Shell completions 2 | 3 | Werk supports dynamic shell completion of arguments and tasks. 4 | See below for how to enable them in your shell. 5 | 6 | 7 | 8 | ## Registration 9 | 10 | **Bash** 11 | 12 | ```bash 13 | source <(COMPLETE=bash werk) 14 | ``` 15 | 16 | **Zsh** 17 | 18 | ```zsh 19 | source <(COMPLETE=zsh werk) 20 | ``` 21 | 22 | **Fish** 23 | 24 | ```fish 25 | COMPLETE=fish werk | source 26 | ``` 27 | To enable completions automatically, insert the line into `.config/fish/completions/werk.fish`. [^note] 28 | 29 | 30 | [^note]: Note that the communication between `werk` and the shell is not stable, so you should not write the output of `COMPLETE= werk` directly into the completion file (see clap issue [#3166](https://github.com/clap-rs/clap/issues/3166)) -------------------------------------------------------------------------------- /book/src/command_line_help.txt: -------------------------------------------------------------------------------- 1 | Usage: werk [OPTIONS] [TARGET] 2 | 3 | Arguments: 4 | [TARGET] 5 | The target to build 6 | 7 | Options: 8 | -f, --file 9 | The path to the Werkfile. Defaults to searching for `Werkfile` in the current working directory and its parents 10 | 11 | -l, --list 12 | List the available recipes 13 | 14 | --dry-run 15 | Dry run; do not execute any recipe commands. Note: Shell commands used in global variables are still executed! 16 | 17 | -w, --watch 18 | Build the target, then keep rebuilding it when the workspace changes 19 | 20 | --watch-delay 21 | Number of milliseconds to wait after a filesystem change before rebuilding. Implies `--watch` 22 | 23 | [default: 250] 24 | 25 | -j, --jobs 26 | Number of tasks to execute in parallel. Defaults to the number of CPU cores 27 | 28 | --workspace-dir 29 | Override the workspace directory. Defaults to the directory containing Werkfile 30 | 31 | --output-dir 32 | Use the output directory instead of the default 33 | 34 | -D, --define 35 | Override global variable. This takes the form `name=value` 36 | 37 | -h, --help 38 | Print help (see a summary with '-h') 39 | 40 | -V, --version 41 | Print version 42 | 43 | Output options: 44 | --print-commands 45 | Print recipe commands as they are executed. Implied by `--verbose` 46 | 47 | --print-fresh 48 | Print recipes that were up-to-date. Implied by `--verbose` 49 | 50 | --quiet 51 | Silence informational output from executed commands, only printing to the terminal when a recipe fails 52 | 53 | --loud 54 | Print all informational output from executed commands to the terminal, even for quiet recipes. Implied by `--verbose` 55 | 56 | --explain 57 | For each outdated target, explain why it was outdated. Implied by `--verbose` 58 | 59 | -v, --verbose 60 | Shorthand for `--explain --print-commands --print-fresh --no-capture --loud` 61 | 62 | --color 63 | [default: auto] 64 | 65 | Possible values: 66 | - auto: Probe the current terminal and environment variables for color support 67 | - always: Force color output, even if the command is not running in a terminal 68 | - never: Do not use color output 69 | 70 | --output-format 71 | [default: ansi] 72 | 73 | Possible values: 74 | - ansi: Provide friendly user feedback assuming an ANSI terminal 75 | - log: Emit the progress as log statements (assuming `WERK_LOG` is set to a value) 76 | - json: Report progress as JSON to stdout. This also disables color output 77 | 78 | --log [] 79 | Enable debug logging to stdout. 80 | 81 | This takes a logging directive like `RUST_LOG`. 82 | -------------------------------------------------------------------------------- /book/src/depfile_support.md: -------------------------------------------------------------------------------- 1 | # Depfile support 2 | 3 | `werk` has limited support for depfiles (`.d`) generated by other tools, such as 4 | C/C++ compilers, shader compilers, or Cargo. 5 | 6 | Depfiles contain Make rules which indicate the dependencies of a given source 7 | file discovered by a compiler. `werk` can parse these rules and apply them as 8 | implicit dependencies of its own recipes. 9 | 10 | A dependency being "implied" means that the build logic treats the dependency 11 | normally, but the contents of the depfile are _not_ available to the build 12 | recipe through the `in` variable. 13 | 14 | Depfile support has two "modes", and `werk` automatically detects which one is 15 | in use (per-recipe): 16 | 17 | - Depfile is generated during compilation of the main source file. For example, 18 | `clang` and `gcc` support passing `-MF` as an argument during compilation, 19 | which will generate a depfile as an additional side-effect. 20 | - Depfile is generated as a separate invocation of the compiler, without 21 | producing the actual output of the compilation step (`-MF` without `-o`). 22 | 23 | When a `depfile` statement exists in the body of a build recipe, `werk` applies 24 | the following logic: 25 | 26 | - If there is a build recipe to specifically generate that depfile, that file is 27 | added as an implicit dependency, and its recipe will be run before the current 28 | recipe. 29 | - If there is no build recipe to generate the depfile, `werk` assumes that the 30 | current recipe will implicitly generate the depfile. 31 | - If the depfile exists, it is parsed by `werk`, and dependencies discovered in 32 | the depfile are added as implicit dependencies of the recipe. If the depfile 33 | exists, but could not be parsed, the current recipe fails to build with a hard 34 | error. 35 | - If the depfile does _not_ exist: 36 | - If `werk` believes that it should be implicitly be generated by the command, 37 | consider the current recipe outdated. 38 | - If `werk` believes that another explicit recipe should have generated the 39 | depfile, but it did not, emit a hard error. 40 | - If, after building the current recipe, and the depfile should have been 41 | implicitly generated by the compiler, but it still does not exist, `werk` 42 | emits a warning. 43 | 44 | The parser for the depfile is a very permissive, but limited, parser of Makefile 45 | syntax to support different conventions used by different tools on different 46 | platforms. For example, some tools put prerequisites with paths containing 47 | whitespace in double-quotes, while others escape them with a backslash. Some 48 | tools escape backslashes in Windows paths, while others do not. The depfile 49 | parser cannot be used to determine if a file is valid Makefile syntax, and there 50 | is valid Makefile syntax that will be rejected by the depfile parser. 51 | 52 | The depfile parser has been tested with the following tools: 53 | 54 | - `clang` and `gcc` 55 | - `slangc` 56 | - `glslc` 57 | - `cargo` 58 | -------------------------------------------------------------------------------- /book/src/env.md: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | 3 | All environment variables are forwarded to programs being run by `werk`, unless 4 | a recipe overrides this behavior by adding/removing environment variables. 5 | 6 | For details around color terminal output settings, see [Color terminal 7 | support](./color.md). 8 | 9 | ## Environment variables read by Werk 10 | 11 | - `WERK_LOG`: When set to a value, enables detailed logging. This takes a 12 | logging directive ([`RUST_LOG` 13 | conventions](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html)). 14 | Overridden by the `--log` command-line option. 15 | - `NO_COLOR`: Disable color output. Overridden by `--color=always`. Takes 16 | precedence over any other environment variables. 17 | - `CLICOLOR`: When non-zero, enables color output. Overridden by 18 | `--color=always/never`, `NO_COLOR`, and `CLICOLOR_FORCE`. 19 | - `CLICOLOR_FORCE`: When set, force-enable color output, same as 20 | `--color=always`. Overridden by `NO_COLOR` and `--color=never`. 21 | 22 | ## Environment variables set by Werk 23 | 24 | - `CLICOLOR`, `CLICOLOR_FORCE`, `FORCE_COLOR`: These are set for programs 25 | executed by `werk` recipes when color output is enabled (running in a 26 | terminal, or color is enabled through environment variables, or 27 | `--color=always` is passed). Note that programs running through a [`shell` 28 | expression](./language/expressions.md#shell) never have color enabled. 29 | - `NO_COLOR`: This is set for programs executed by `werk` when color output is 30 | _disabled_ (not running in a terminal, or color is disabled through 31 | environment variables, or `--color=never` is passed), and for all programs 32 | executed through a [`shell` expression](./language/expressions.md#shell) 33 | 34 | ## Modifying the environment in recipes 35 | 36 | Recipes may add or remove environment variables for programs executed by that 37 | recipe. Environment variables may be set or removed for a whole recipe, or 38 | within a flow of [recipe commands](./language/recipe_commands.md). 39 | 40 | Set or override an environment variable: `env "MY_VAR" = "..."` 41 | 42 | Remove an environment variable, so it becomes unavailable to the child process: 43 | `env-remove "MY_VAR"`. 44 | 45 | Setting an environment variable in a recipe does _not_ impact the environment 46 | variables seen by its dependencies or its dependents. Only processes executed by 47 | that specific recipe will see modifications to the environment. 48 | 49 | ## Debugging 50 | 51 | - `_WERK_ARTIFICIAL_DELAY`: Number of milliseconds to wait between executing 52 | recipe commands. This may be used while debugging `werk` itself, especially 53 | rendering to the CLI. 54 | -------------------------------------------------------------------------------- /book/src/examples/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock -------------------------------------------------------------------------------- /book/src/examples/c.md: -------------------------------------------------------------------------------- 1 | # Example: C program 2 | 3 | This example shows a very simple project compiling a C program using `werk`. It 4 | showcases using depfiles generated implicitly by `clang`. 5 | 6 | src/foo.h: 7 | 8 | 9 | ```c 10 | {{#include c/src/foo.h}} 11 | ``` 12 | 13 | src/foo.c: 14 | 15 | 16 | ```c 17 | {{#include c/src/foo.c}} 18 | ``` 19 | 20 | src/main.c: 21 | 22 | ```c 23 | {{#include c/src/main.c}} 24 | ``` 25 | 26 | Werkfile: 27 | 28 | ```werk 29 | {{#include c/Werkfile}} 30 | ``` 31 | -------------------------------------------------------------------------------- /book/src/examples/c/Werkfile: -------------------------------------------------------------------------------- 1 | default target = "build" 2 | 3 | # Path to clang 4 | let cc = which "clang" 5 | 6 | # Path to linker 7 | let ld = cc 8 | 9 | # Build profile (debug or release) 10 | let profile = "debug" 11 | 12 | # Pick cflags based on the build profile 13 | let cflags = profile | match { 14 | "debug" => ["-O0", "-g"] 15 | "release" => ["-O3"] 16 | "%" => "" 17 | } 18 | 19 | # Build rule for object files 20 | build "%.o" { 21 | from "%.c" 22 | depfile "%.c.d" 23 | 24 | let include-path = "src" 25 | let flags = [cflags, "-I"] 26 | 27 | # Generate depfile and object file in the same command 28 | run "{cc} -MMD -MT -MF -c {flags*} -o " 29 | } 30 | 31 | # Build rule for the main executable 32 | build "my-program{EXE_SUFFIX}" { 33 | # Include all .c files in the build 34 | from glob "src/**/*.c" | map "{:.c=.o}" 35 | 36 | run "{ld} -o " 37 | } 38 | 39 | task build { 40 | build "my-program{EXE_SUFFIX}" 41 | info "Build complete!" 42 | } 43 | 44 | task run { 45 | let executable = "my-program{EXE_SUFFIX}" 46 | build executable 47 | run "" 48 | } 49 | -------------------------------------------------------------------------------- /book/src/examples/c/src/foo.c: -------------------------------------------------------------------------------- 1 | #include "foo.h" 2 | int foo() { return 123; } 3 | -------------------------------------------------------------------------------- /book/src/examples/c/src/foo.h: -------------------------------------------------------------------------------- 1 | int foo(); 2 | -------------------------------------------------------------------------------- /book/src/examples/c/src/main.c: -------------------------------------------------------------------------------- 1 | #include "foo.h" 2 | #include 3 | 4 | int main() { 5 | printf("foo() returned: %d\n", foo()); 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /book/src/examples/cargo.md: -------------------------------------------------------------------------------- 1 | # Example: Cargo project 2 | 3 | This example shows a simple project compiling a Cargo project using `werk`. It 4 | showcases depfiles generated by Cargo, and using the same output directory as 5 | Cargo for build artifacts. 6 | 7 | Cargo.toml: 8 | 9 | ```toml 10 | {{#include cargo/Cargo.toml:1:4}} 11 | ``` 12 | 13 | src/main.rs: 14 | 15 | ```rust 16 | {{#include cargo/src/main.rs}} 17 | ``` 18 | 19 | Werkfile: 20 | 21 | ```werk 22 | {{#include cargo/Werkfile}} 23 | ``` -------------------------------------------------------------------------------- /book/src/examples/cargo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-project" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [workspace] 7 | members = [] 8 | -------------------------------------------------------------------------------- /book/src/examples/cargo/Werkfile: -------------------------------------------------------------------------------- 1 | default target = "build" 2 | default out-dir = "target" 3 | 4 | let cargo = which "cargo" 5 | 6 | let profile = "debug" 7 | 8 | let cargo-profile = profile | match { 9 | "debug" => "dev" 10 | "%" => "%" 11 | } 12 | 13 | # This rule matches the output path of Cargo. 14 | build "{profile}/test-project{EXE_SUFFIX}" { 15 | # This file is implicitly generated by Cargo. 16 | depfile "{profile}/test-project.d" 17 | 18 | run "cargo build -p test-project --profile={cargo-profile}" 19 | } 20 | 21 | task build { 22 | build "{profile}/test-project{EXE_SUFFIX}" 23 | } 24 | -------------------------------------------------------------------------------- /book/src/examples/cargo/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello from Rust!"); 3 | } 4 | -------------------------------------------------------------------------------- /book/src/examples/complex.md: -------------------------------------------------------------------------------- 1 | # Example: Cargo + WASM + Assets 2 | 3 | This example shows a complex use case, using parts of the other examples: 4 | 5 | - Build a "main" binary for the host architecture using Cargo. 6 | - Build a WASM plugin targeting `wasm32-wasip2` using Cargo. 7 | - Compile shaders using `glslc`. 8 | - Compress an `.tar.gz` "asset pack" containing compiled WASM modules, compiled 9 | shaders, and PNG images. 10 | 11 | Due to the [outdatedness rules](../outdatedness.md) and depfile integration, 12 | every rule accurately captures the actual dependencies of each step. For 13 | example, changing a `.glsl` file included by one of the shaders will only cause 14 | the relevant shaders to be rebuilt, and will cause `assets.tar.gz` to be 15 | repackaged, but it will not cause WASM modules to be rebuilt. Similarly, due to 16 | the glob patterns, adding a `.png` file to the project will cause 17 | `assets.tar.gz` to be repackaged, but nothing else will be rebuilt. 18 | 19 | Werkfile: 20 | 21 | ```werk 22 | {{#include complex/Werkfile}} 23 | ``` 24 | -------------------------------------------------------------------------------- /book/src/examples/complex/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | members = ["program", "plugin1", "plugin2"] 4 | -------------------------------------------------------------------------------- /book/src/examples/complex/Werkfile: -------------------------------------------------------------------------------- 1 | default target = "build" 2 | 3 | let cargo = which "cargo" 4 | let glslc = which "glslc" 5 | let wasm-tuple = "wasm32-wasip2" 6 | 7 | let profile = "debug" 8 | let wasm-profile = "debug" 9 | 10 | let cargo-profile = profile | match { 11 | "debug" => "dev" 12 | "%" => "%" 13 | } 14 | 15 | let cargo-wasm-profile = wasm-profile | match { 16 | "debug" => "dev" 17 | "%" => "%" 18 | } 19 | 20 | # Rule to build a WASM target with Cargo. 21 | build "{wasm-tuple}/{wasm-profile}/%.wasm" { 22 | # Cargo uses dashes in package names and underscores in build artifacts, so 23 | # use a regex to replace it. 24 | let package-name = "{%:s/_/-/}" 25 | 26 | depfile "{wasm-tuple}/{wasm-profile}/%.d" 27 | run "{cargo} build 28 | --target={wasm-tuple} 29 | --profile={cargo-wasm-profile} 30 | -p {package-name}" 31 | } 32 | 33 | # Rule to build a SPIR-V shader with glslc. 34 | build "%.(frag|vert|comp).spv" { 35 | from "%.{0}" 36 | depfile "%.{0}.d" 37 | run "{glslc} -MD -MF -o " 38 | } 39 | 40 | let wasm-targets = ["plugin1", "plugin2"] 41 | | map "{wasm-tuple}/{wasm-profile}/{}.wasm" 42 | 43 | build "assets.tar.gz" { 44 | from [ 45 | wasm-targets, 46 | glob "assets/**/*.png", 47 | glob "shaders/**/*.\{frag,vert,comp\}" | map "{}.spv" 48 | ] 49 | 50 | run "tar -zcf " 51 | } 52 | 53 | # Rule to build the main program. 54 | build "{profile}/program{EXE_SUFFIX}" { 55 | depfile "{profile}/program.d" 56 | run "cargo build -p program --profile={cargo-profile}" 57 | } 58 | 59 | # Task to build everything. 60 | task build { 61 | build ["{profile}/program{EXE_SUFFIX}", "assets.tar.gz"] 62 | } 63 | 64 | # Task that just runs `cargo clean`. This deletes `target/`, so also removes 65 | # compiled shaders. 66 | task clean { 67 | run "{cargo} clean" 68 | } 69 | -------------------------------------------------------------------------------- /book/src/examples/complex/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonask/werk/e57ce24a87654c9cdd19b4136842d701a06fe3d0/book/src/examples/complex/assets/logo.png -------------------------------------------------------------------------------- /book/src/examples/complex/plugin1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plugin1" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /book/src/examples/complex/plugin1/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub fn mul(left: u64, right: u64) -> u64 { 2 | left * right 3 | } 4 | -------------------------------------------------------------------------------- /book/src/examples/complex/plugin2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plugin2" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /book/src/examples/complex/plugin2/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub fn add(left: u64, right: u64) -> u64 { 2 | left + right 3 | } 4 | -------------------------------------------------------------------------------- /book/src/examples/complex/program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "program" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /book/src/examples/complex/program/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /book/src/examples/complex/shaders/shader.frag: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | void main() {} -------------------------------------------------------------------------------- /book/src/examples/complex/shaders/shader.vert: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | void main() {} 4 | -------------------------------------------------------------------------------- /book/src/examples/shaders.md: -------------------------------------------------------------------------------- 1 | # Example: GLSL shaders 2 | 3 | This example shows how to build SPIR-V shaders using `glslc`. It also showcases 4 | capture groups in pattern matching, where the same build rule is used for all 5 | three types of shaders (fragment, vertex, compute). 6 | 7 | Additionally, this also creates an "asset pack" containing all the shaders, 8 | using `tar`. 9 | 10 | Werkfile: 11 | 12 | ```werk 13 | {{#include shaders/Werkfile}} 14 | ``` 15 | -------------------------------------------------------------------------------- /book/src/examples/shaders/Werkfile: -------------------------------------------------------------------------------- 1 | default target = "build" 2 | 3 | let glslc = which "glslc" 4 | let tar = which "tar" 5 | 6 | build "%.(frag|vert|comp).spv" { 7 | from "%.{0}" 8 | depfile "%.{0}.d" 9 | run "{glslc} -MD -MF -o " 10 | } 11 | 12 | build "shaders.tar.gz" { 13 | # Note: Using "native" glob syntax. 14 | from glob "*.\{frag,vert,comp\}" | map "{}.spv" 15 | run "{tar} -zcf " 16 | } 17 | 18 | task build { 19 | build "shaders.tar.gz" 20 | } 21 | -------------------------------------------------------------------------------- /book/src/examples/shaders/shader.frag: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | void main() {} 4 | -------------------------------------------------------------------------------- /book/src/examples/shaders/shader.vert: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | void main() {} 4 | -------------------------------------------------------------------------------- /book/src/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | - **Cross-platform:** Windows is a first-class citizen - no dependency on a 4 | POSIX-compliant shell (or any shell). Werk files work on all platforms out of 5 | the box. 6 | 7 | - **[Simple language:](./language.md)** Werk files are written in a simple and 8 | human-friendly language that's easy to understand. It is designed to be 9 | written by hand. 10 | 11 | - **[Task recipes:](./task_recipes.md)** Real support for executing project 12 | scrips, similar to `just` recipes or `.PHONY` Make targets. A command recipe 13 | will be run at most once per `werk` invocation. 14 | 15 | - **[Build recipes:](./build_recipes.md)** Files can be built from Make-like 16 | patterns, and rebuilt according to modification time. 17 | 18 | - **[Advanced outdatedness:](./outdatedness.md)** `werk` does more than just 19 | compare file modification timestamps. Metadata is cached between runs to 20 | support additional sources of "outdatedness". 21 | 22 | - **Separate output directory:** All files produces by `werk` are put in a 23 | separate output directory, which is always what you want. This is hard to 24 | achieve with Make. 25 | 26 | - **[Globbing:](./language/operations.md#glob)** Filesystem glob patterns work 27 | out of the box, and can be used reliably in dependencies. `werk` caches a hash 28 | of the glob result between builds, so file deletion is detected as a change. 29 | Globbing is based on the `globset` crate, which comes from `ripgrep`. 30 | 31 | - **Paths can contain spaces:** Make simply cannot deal. 32 | 33 | - **[Depfile support:](./depfile_support.md)** Depfile output from C-like 34 | compilers such as `clang`, `gcc`, `cl`, `glslc`, etc. are specially supported 35 | in build recipes. When a recipe contains a `depfile` dependency, it is 36 | automatically built and included when evaluating the dependencies of that 37 | recipe. 38 | 39 | - **.gitignore support:** The `ignore` crate is used to hide files from `werk`. 40 | 41 | - **Dry-run:** Pass `--dry-run` to diagnose the build process without generating 42 | any output. 43 | 44 | - **Concurrency:** Build recipes and tasks run in parallel when possible. 45 | 46 | - **Autowatch:** Werk can be run in `--watch` mode, which waits for file changes 47 | and automatically rebuilds when any change is detected. 48 | 49 | - **Long-running tasks:** Werk natively supports long-running processes, such as 50 | a development webserver running locally, using the `spawn` statement. This 51 | also works in combination with the `--watch` feature, such that any spawned 52 | processes are automatically restarted when a rebuild is triggered. 53 | 54 | # Limitations 55 | 56 | - **Cross-platform:** Paths and commands must work across all platforms. `werk` 57 | does not require a shell (like `sh` or `bash`), but that also means that 58 | common shell features are not available. The language provides cross-platform 59 | alternatives in most cases. 60 | 61 | - **Declarative:** Very advanced build logic can be difficult to express. If the 62 | limited expression support in `werk` is insufficient (and can't be easily 63 | supported in the model), consider using a more advanced solution like GNU 64 | make, CMake, ninja-build, scons, cargo-script, etc. 65 | 66 | - **Separate output directory:** It is not possible to put output files next to 67 | input files, and files in the output directory are assumed to be generated by 68 | `werk`. 69 | 70 | - **Multiple recipes matching the same pattern:** As opposed to Make, build 71 | recipes are not disambiguated by their inputs. This means that for example it 72 | is not possible to have two recipes that match `%.o`, where one takes `%.c` as 73 | input and the other takes `%.cpp` as input. 74 | - *Workaround 1:* Define separate recipes `%.c.o` and `%.cpp.o` to build the two 75 | kinds of source files with different output file extensions. 76 | - *Workaround 2:* Use capture patterns, so the build recipe pattern is 77 | `%.(c|cpp)`, and use `match` expressions to generate the correct compiler 78 | arguments based on the captured pattern `{0}`, which will evaluate to either 79 | "c" or "cpp". 80 | 81 | - **Multiple outputs from a single recipe:** This is not supported. The typical 82 | example is a code generator that outputs both a header file and a source file, 83 | like `bison`. *Note: This may be explicitly supported in the future.* 84 | - *Workaround:* For the common case of a `bison` parser, define the recipe for 85 | the generated `parser.h` file, and add another recipe for the `parser.c` 86 | file that has no commands, but just depends on `parser.h`. 87 | 88 | - **Detailed parallelism control:** `werk` currently does not support marking 89 | specific recipes as "non-parallel". The only way to control parallelism is by 90 | supplying the `--jobs` command line parameter, which controls the number of 91 | worker threads. 92 | -------------------------------------------------------------------------------- /book/src/first_example.md: -------------------------------------------------------------------------------- 1 | # My first Werkfile 2 | 3 | When running `werk`, it looks for a `Werkfile` in the current directory, and all 4 | its parent directories. 5 | 6 | Create a `Werkfile` in a directory with the following contents: 7 | 8 | ```werk 9 | default target = "hello" 10 | 11 | task hello { 12 | info "Hello, World!" 13 | } 14 | ``` 15 | 16 | Run `werk` in the directory: 17 | 18 | ```sh 19 | $ werk 20 | [info] Hello, World! 21 | [ ok ] hello 22 | ``` 23 | -------------------------------------------------------------------------------- /book/src/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | `werk` is installed from source, and prebuilt binaries are not provided at this 4 | time. That said, `werk` is a self-contained binary that can easily be 5 | transferred between machines. 6 | 7 | ## Installation Dependencies 8 | 9 | - `rustc` >= 1.83.0 10 | 11 | ## Installation steps 12 | 13 | 1. `git clone https://github.com/simonask/werk` 14 | 2. `cd werk` 15 | 3. `cargo install --path werk-cli` 16 | 17 | ## Running 18 | 19 | If Cargo is configured properly on your system, your `$PATH` should already 20 | contain the path to Cargo-installed binaries. 21 | 22 | * Cargo installs binaries in `$CARGO_HOME/bin` 23 | * On UNIX-like systems, the default install location is `$HOME/.cargo/bin` 24 | * On Windows, the default install location is `%USERPROFILE%\.cargo\bin` 25 | 26 | Verify that `werk` is installed correctly by running `werk --help`. 27 | 28 | ## Language support for VS Code 29 | 30 | If you are using Visual Studio Code, there is a simple language extension 31 | providing syntax highlighting in the `werk-vscode` subdirectory. 32 | 33 | * From the command-line: `code --install-extension ` 34 | * From within VS Code: Run "Developer: Install Extension from Location..." and 35 | point it to the path to the `werk-vscode` directory within this 36 | repository. 37 | 38 | ## Other IDEs 39 | 40 | If your IDE supports `.sublime-syntax` definition files (such as Sublime Text), 41 | point your IDE to `werk.sublime-syntax` in the repository's root to add syntax 42 | highlighting support. 43 | -------------------------------------------------------------------------------- /book/src/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 |
4 | Werk is early alpha software. Use at your own risk. It may eat your 5 | files, so run git commit before trying it out. 6 |
7 | 8 | Werk is a command runner and build system. It is intended to replace Make in 9 | projects that need a simple build tool adjacent to a main build system, such as 10 | Cargo or NPM. It can also replace `just` in most cases. 11 | 12 | The motivating use case is an asset building pipeline for a video game, which 13 | must perform a series of expensive steps to produce an asset archive that can be 14 | hot-reloaded by a game engine, but it can build anything, including C/C++ 15 | binaries, or integrate with external build systems, like Cargo. 16 | 17 | Werk is [limited and opinionated](./features.md#limitations). It is not suited 18 | for all use cases, and it can not replace more advanced solutions, like CMake or 19 | scons. However, it _is_ suited to work together with such systems, and can be 20 | used to invoke them in a convenient way. See the [Depfile 21 | support](./depfile_support.md) chapter for more details. 22 | 23 | `werk` tries to be really clever about when to rebuild files. In addition to 24 | file modification times, it also takes things like the path to any commands 25 | invoked in a recipe, any environment variables used in the recipe, or changes in 26 | the results of glob patterns (like `*.txt`) into account when deciding whether 27 | or not to rebuild a given file. See the [Outdatedness](./outdatedness.md) 28 | chapter for more details. 29 | 30 | `werk` also tries to be extremely helpful when diagnosing problems. The 31 | command-line option `--explain` provides detailed information about why a given 32 | target was rebuilt, without _excessive_ information. The command-line option 33 | `--dry-run` allows evaluating the dependency graph without executing any 34 | commands. 35 | 36 | `werk` is religiously portable. It works _natively_ on all major platforms 37 | (Linux, Windows, macOS), without any external dependencies - no `sh` required! 38 | 39 | ## Use cases 40 | 41 | Examples of suitable use cases: 42 | 43 | - Simple build processes for things like shaders, WASM modules, small C 44 | libraries, assets, etc. 45 | - Command runner for "housekeeping" tasks, like running tests, publishing 46 | binaries, or downloading static file dependencies. 47 | - Driving other build systems. 48 | 49 | Examples of less suitable use cases: 50 | 51 | - Building cross-platform C/C++ projects with system dependencies. There is no 52 | built-in way to discover "traditional" dependencies via `pkg-config`, `vcpkg`, 53 | or similar. Use CMake instead. 54 | - Builds requiring detailed concurrency management. Werk assumes that all 55 | recipes that don't have an edge between them in the dependency graph can be 56 | run in parallel, and there is no way to limit parallelism outside of the 57 | `--jobs` parameter. 58 | - Multiple outputs per recipe. Driving things like `bison` with Werk may require 59 | workarounds. 60 | - Recursive workspaces. 61 | -------------------------------------------------------------------------------- /book/src/language/arrays.md: -------------------------------------------------------------------------------- 1 | # Arrays / lists 2 | 3 | Werk has first-class support for lists. Elements of lists can be accessed using 4 | the subscript operator `[index]`, where `index` is either a constant integer or 5 | an expression producing the string representation of an integer. 6 | 7 | When array indices are negative, the result is the element from the end of the 8 | array. For example, `-1` gets the last element of the list, `-2` gets the 9 | next-to-last element, and so on. 10 | 11 | Subscript operators may also appear within string interpolations. 12 | 13 | Example: 14 | 15 | ```werk 16 | let my-list = ["a", "b", "c"] 17 | let a = my-list[0] # "a" 18 | 19 | let my-index = "1" 20 | let b = my-list[my-index] # "b" 21 | 22 | let c = my-list[-1] # "c" 23 | ``` 24 | 25 | ## Array operations 26 | 27 | These operations are specific to arrays, but arrays may also appear in other 28 | operations. See [Expressions](./expressions.md). 29 | 30 | ### `len` 31 | 32 | Get the number of elements in a list (as a string). When passed a string, this 33 | always returns 1. 34 | 35 | Example: 36 | 37 | ```werk 38 | let my-list = ["a", "b", "c"] 39 | let len = my-list | len # "3" 40 | ``` 41 | 42 | ### `first` 43 | 44 | Get the first element of a list, or the empty string if the list is empty. This 45 | is different from `array[0]` in that it does not raise an error when the list is 46 | empty. 47 | 48 | Example: 49 | 50 | ```werk 51 | let my-list = ["a", "b", "c"] 52 | let first = my-list | first # "a" 53 | 54 | let empty = [] | first # "" 55 | ``` 56 | 57 | ### `last` 58 | 59 | Get the last element of a list, or the empty string if the list is empty. This 60 | is different from `array[-1]` in that it does not raise an error when the list 61 | is empty. 62 | 63 | Example: 64 | 65 | ```werk 66 | let my-list = ["a", "b", "c"] 67 | let last = my-list | last # "c" 68 | 69 | let empty = [] | last # "" 70 | ``` 71 | 72 | ### `tail` 73 | 74 | Produce a new list with the first element removed, or an empty list if the list 75 | is empty. 76 | 77 | Example: 78 | 79 | ```werk 80 | let my-list = ["a", "b", "c"] 81 | let tail = my-list | tail # ["b", "c"] 82 | ``` 83 | -------------------------------------------------------------------------------- /book/src/language/builtins.md: -------------------------------------------------------------------------------- 1 | # Built-in variables 2 | 3 | ## Variables 4 | 5 | In build recipes: 6 | 7 | - `in`, or `{^}` in strings: Input files (the result of the `from` statement) 8 | - `out`, or `{@}` in strings: Output files (the path of the actual file being 9 | built by the recipe). 10 | - `depfile`: If the recipe has a `depfile` statement, this is the evaluated 11 | [path](../paths.md) to the depfile. 12 | - `%` or `{%}` in strings: The stem of the matched pattern, if a pattern is in 13 | scope and that pattern contains a `%`. When defining patterns in a scope where 14 | another pattern is already present, the interpolated `{%}` may be used to 15 | unambiguously refer to the stem of the "outer" pattern. 16 | 17 | ## Global constants 18 | 19 | These variables are valid in all scopes. 20 | 21 | - `ROOT`: The abstract or filesystem path to the project root, which is always 22 | `/`. When interpolated as ``, this becomes the filesystem path to the 23 | directory containing the Werkfile. 24 | - `OS`: Lowercase name of the host operating system. 25 | - Windows: `windows` 26 | - macOS: `macos` 27 | - Linux: `linux` 28 | - FreeBSD: `freebsd` 29 | - DragonFly: `dragonfly` 30 | - OpenBSD: `openbsd` 31 | - NetBSD: `netbsd` 32 | - WebAssembly (WASIp2): `wasm-wasi` 33 | - `OS_FAMILY`: Classification of the host operating system. 34 | - Windows: `windows` 35 | - Linux, macOS, and BSDs: `unix` 36 | - WebAssembly (WASIp2): `wasm` 37 | - `ARCH`: Name of the host architecture. 38 | - x86_64 / x64: `x86_64` 39 | - x86: `x86` 40 | - ARM (64-bit): `aarch64` 41 | - ARM (32-bit): `arm` 42 | - WebAssembly: `wasm` 43 | - `ARCH_FAMILY`: Classification of the host architecture. 44 | - x86, x86_64: `x86` 45 | - ARM: `arm` 46 | - WebAssembly: `wasm` 47 | - `EXE_SUFFIX`: 48 | - Windows: `.exe` 49 | - Other: empty 50 | - `DYLIB_PREFIX`: 51 | - Windows: empty 52 | - Linux and macOS: `lib` 53 | - `DYLIB_SUFFIX`: 54 | - Windows: `.dll` 55 | - Linux: `.so` 56 | - macOS: `.dylib` 57 | - `STATICLIB_PREFIX`: 58 | - Windows: empty 59 | - Linux and macOS: `lib` 60 | - `STATICLIB_SUFFIX`: 61 | - Windows: `.lib` 62 | - Linux and macOS: `.a` 63 | - `EMPTY`: Always the empty string 64 | - `COLOR`: When color output is enabled for `werk`, this is set to `"1"`. This 65 | may be used to conditionally pass command-line arguments to compilers that 66 | don't respect the conventional `CLICOLOR` environment variables. 67 | -------------------------------------------------------------------------------- /book/src/language/config.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | See [Configure your build](../build_config.md). 4 | -------------------------------------------------------------------------------- /book/src/language/include.md: -------------------------------------------------------------------------------- 1 | # Include 2 | 3 | As a project grows, the size of a `Werkfile` may become unwieldy, and it may be 4 | desirable to split recipes and variables into separate files. 5 | 6 | Werk supports the `include` statement to evaluate a separate file and include 7 | its variables and recipes in the main `Werkfile`. 8 | 9 | Included files are evaluated as-if they were a part of the file that includes 10 | them. For the purposes of expression evaluation, all included files share the 11 | same global scope. 12 | 13 | However, [`default` statements](../build_config.md#configure-how-werk-runs) may 14 | only appear in the "main" Werkfile, as they impact how Werk runs. 15 | 16 | `include` statements take the form of `include "path/in/workspace.werk"`. The 17 | path may also be an expression, so Werkfiles can selectively include other 18 | sources based on the value of expressions. 19 | 20 | ## Example 21 | 22 | Werkfile: 23 | 24 | ```werk 25 | include "config.werk" 26 | include "recipes.werk" 27 | ``` 28 | 29 | config.werk: 30 | 31 | ```werk 32 | config profile = "debug" 33 | ``` 34 | 35 | recipes.werk: 36 | 37 | ```werk 38 | let cflags = profile | match { 39 | "debug" => ["-O0", "-g"] 40 | "release" => ["-O3"] 41 | "%" => [] 42 | } 43 | 44 | build "%.o" { 45 | # ... 46 | } 47 | ``` 48 | 49 | ## Advanced example 50 | 51 | This example includes a different set of configuration variables based on the 52 | current host platform. 53 | 54 | Werkfile: 55 | 56 | ```werk 57 | config profile = "debug" 58 | 59 | include "config_{OS_FAMILY}.werk" 60 | include "recipes.werk" 61 | ``` 62 | 63 | config_windows.werk: 64 | 65 | ```werk 66 | let cc = which "cl" 67 | let debug_cflags = [] 68 | let release_cflags = ["/O"] 69 | ``` 70 | 71 | config_unix.werk: 72 | 73 | ```werk 74 | let cc = which "clang" 75 | let debug_cflags = ["-O0", "-g"] 76 | let release_cflags = ["-O3"] 77 | ``` 78 | 79 | recipes.werk: 80 | 81 | ```werk 82 | let cflags = profile | match { 83 | "debug" => debug_cflags 84 | "release" => release_cflags 85 | } 86 | 87 | build "%.o" { 88 | from "%.c" 89 | run "{cc} {cflags*} -o " 90 | } 91 | ``` 92 | -------------------------------------------------------------------------------- /book/src/language/path_resolution.md: -------------------------------------------------------------------------------- 1 | # Path resolution 2 | 3 | Werk supports translating [abstract paths](../paths.md) into native OS paths in 4 | string interpolations, using the special `"<...>"` interpolation syntax. 5 | 6 | Normal string interpolations `"{...}"` are always "verbatim" - the interpolation 7 | is performed literally. 8 | 9 | However, string interpolation with `<...>` performs extra logic to obtain a 10 | native OS path whenever it occurs, and this logic is sensitive to the 11 | surroundings of the interpolation, as well as the presence of build recipe 12 | rules. 13 | 14 | **Pathiness:** A string containing a `<...>` interpolation (i.e., containing a 15 | native OS path) cannot be used in another `<...>` interpolation, as this would 16 | create nonsensical OS paths. This is transitive, so a native OS path cannot be 17 | "smuggled" through a normal `{...}` interpolation. However, certain operations 18 | remove the "pathiness". 19 | 20 | Consider the following Werkfile: 21 | 22 | ```werk 23 | # c:\workspace 24 | # target\ 25 | # dir\ 26 | # foo.txt 27 | 28 | default out-dir = "target" 29 | 30 | let input = "foo.txt" 31 | let output = "bar.txt" 32 | let dir = "dir" 33 | 34 | let input-path = "" # c:\workspace\foo.txt 35 | let output-path = "" # c:\workspace\target\bar.txt 36 | let output-filename = "{output-path:filename}" # foo.txt 37 | 38 | let output-path = "" # ERROR: Double path resolution 39 | ``` 40 | 41 | - `""` resolves to `c:\workspace\foo.txt`, because `foo.txt` exists in 42 | the workspace. 43 | - `""` resolves to `c:\workspace\target\bar.txt`, because `bar.txt` 44 | does not exist in the workspace. 45 | - `""` resolves to `c:\workspace\target\foo.txt`, because it is 46 | explicitly requested. 47 | - `""` resolves to `c:\workspace\bar.txt`, because it is 48 | explicitly requested, even though the file does not exist in the workspace. 49 | - `""` resolves to `c:\workspace\dir`, even though it is a directory. 50 | - When an `<...>` interpolation would match a file in the workspace, but also 51 | matches a build recipe, `werk` fails with an error describing the ambiguity. 52 | The path can be disambiguated by using `:out-dir` or `:workspace` to 53 | disambiguate path resolution. 54 | - Since they contain `<...>` interpolations, `input-path` and `output-path` are 55 | marked as "pathy", and those variables cannot be used in further `<...>` 56 | interpolations. 57 | - However, the filename component of a path is _not_ "pathy", so 58 | `output-filename` may be used in other `<...>` interpolations. 59 | -------------------------------------------------------------------------------- /book/src/language/patterns.md: -------------------------------------------------------------------------------- 1 | # Patterns and pattern-matching 2 | 3 | Patterns are strings containing special directives. They behave similarly to 4 | Make patterns. 5 | 6 | Special syntax in pattern strings: 7 | 8 | - `%`: The "pattern stem". This matches any sequence of characters, which will 9 | be available to statements within the pattern's scope as `%` or `{%}`. The 10 | latter (braced) can be used if the stem is subject to [interpolation 11 | operations](./strings.md#interpolation-operations), or when used within 12 | another pattern (without introducing a new stem). 13 | - `(a|b)`: Capture group matching either `a` or `b`. 14 | 15 | Patterns can contain [string interpolations](./strings.md#string-interpolation). 16 | Interpolated string values are not interpreted as patterns, but will be matched 17 | literally. For example, if an interpolated value contains `%`, it will only 18 | match the string "%". 19 | 20 | Example, given the pattern `%.(c|cpp)`: 21 | 22 | - The string `"foo.c"` will match. The stem is `foo`, and capture group 0 is `c`. 23 | - The string `"foo/bar/baz.cpp"` will match. The stem is `foo/bar/baz`, and 24 | capture group 0 is `cpp`. 25 | - The string `"foo.h"` will not match, because none of the variants in the 26 | capture group apply. 27 | - The string `"abc"` will not match, because the period is missing. 28 | 29 | When multiple patterns are participating in pattern matching (such as figuring 30 | out which [build recipe](language.md#build-statement-at-global-scope) to run, or 31 | in a [`match` expression](language.md#match-expression)), the "highest-quality" 32 | match is chosen. Match quality is measured by the length of the stem: A match 33 | producing a shorter stem is considered "better" than a match producing a longer 34 | stem. 35 | 36 | - A pattern without a `%` stem is "more specific" than a pattern that has a 37 | stem. 38 | - A pattern that matches the input with a shorter stem is "more specific" than 39 | a pattern that matches a longer stem. 40 | - Capture groups do not affect the "specificity" of a pattern. 41 | 42 | **Important:** When multiple patterns match with equal quality, the pattern 43 | matching operation is ambiguous. In build recipes, this is a hard error. In 44 | [`match` expressions](./operations.md#match), the first matching pattern will 45 | win. Often, capture groups can be used to disambiguate two patterns by 46 | collapsing them into a single pattern. 47 | 48 | ## Example 49 | 50 | Given the patterns `%.c`, `%/a.c`, `foo/%/a.c`, `foo/bar/a.c`, this is how 51 | matches will be chosen based on various inputs: 52 | 53 | - `"bar/b.c"`: The pattern `%.c` will be chosen, because it does not match the 54 | other patterns. The stem is `"bar/b"`. 55 | - `"foo/a.c"`: The pattern `%/a.c` will be chosen, because it produces the 56 | shortest stem `"a"`. The stem is `foo`. 57 | - `"foo/foo/a.c"`: The pattern `foo/%/a.c` will be chosen over `%.c` and 58 | `%/a.c`, because it produces a shorter stem. The stem is `foo`. 59 | - `"foo/bar/a.c"`: The pattern `foo/bar/a.c` will be chosen over `foo/%/a.c`, 60 | because the pattern is literal exact match without a stem. 61 | 62 | **Conflicts:** It's possible to construct patterns that are different but match 63 | the same input with the same "specificity". For example, both patterns 64 | `foo/%/a.c` and `%/foo/a.c` match the input `"foo/foo/a.c"` equally. When such a 65 | situation occurs, that's a hard error. 66 | -------------------------------------------------------------------------------- /book/src/language/variables.md: -------------------------------------------------------------------------------- 1 | # Variables 2 | 3 | Defining a variable in Werk is a `let`-statement. Values are either strings or 4 | lists (no numbers). Lists may contain other lists. All strings are valid UTF-8. 5 | 6 | Syntax: `let identifier = expression`. The left-hand side of the `=` must be a 7 | valid identifier (Unicode is supported), and the right-hand side is any 8 | expression. All expressions produce a value. 9 | 10 | ```werk 11 | let my-string = "value" 12 | 13 | let my-list = ["a", "b", "c"] 14 | ``` 15 | 16 | All variables are immutable, but variable names may shadow local or global 17 | variables with the same name. 18 | 19 | ```werk 20 | let foo = "a" 21 | let foo = "b" # valid 22 | 23 | let bar = foo # == "b" 24 | ``` 25 | 26 | ## Global variables are public 27 | 28 | Variables defined at the global scope (i.e., outside of any recipe) are public, 29 | and will appear in the output of `werk --list`. They can be overridden by 30 | passing `-Dkey=value` on the command-line. Comments immediately preceding a 31 | global variable will appear in the output as documentation for that variable. 32 | 33 | ```werk 34 | # Set the build profile. 35 | let profile = "debug" 36 | ``` 37 | 38 | ```sh 39 | $ werk --list 40 | Global variables: 41 | profile = "debug" # Set the build profile. 42 | ``` 43 | 44 | ## Local variables 45 | 46 | Variables defined within a recipe are local to that recipe. Recipes cannot 47 | change global variables, but they may shadow global variables in their local 48 | scope by defining a variable with the same name as a global variable. 49 | -------------------------------------------------------------------------------- /book/src/long_running_tasks.md: -------------------------------------------------------------------------------- 1 | # Long-running tasks 2 | 3 | Task recipes can spawn long-running processes controlled by `werk` using [the 4 | `spawn` statement](./language/recipe_commands.md#the-spawn-statement). This is 5 | useful for running a development server or other long-running processes that 6 | need to be restarted when the source files change. 7 | 8 | When a `spawn` statement has executed, `werk` will wait for the process to exit 9 | before exiting itself. When `werk` receives a Ctrl-C signal, it will kill the 10 | child process as well. 11 | 12 | ## Autowatch integration 13 | 14 | When `--watch` is enabled, `werk` will automatically kill and restart any 15 | spawned processes when a rebuild is triggered. 16 | 17 |
18 | Note: Some programs, such as local webservers, implement their 19 | own watching mechanism. Using these in conjunction with `--watch` may not be desirable, 20 | because `werk` will unconditionally restart the process on any change. 21 |
22 | -------------------------------------------------------------------------------- /book/src/outdatedness.md: -------------------------------------------------------------------------------- 1 | # Outdatedness 2 | 3 | `werk` has a richer idea of "outdatedness" or "staleness" than Make and similar 4 | tools, enabling many cases that would traditionally require a full rebuild to be 5 | selectively rebuilt instead. 6 | 7 | This is made possible by placing a [`.werk-cache`](./werk_cache.md) file in the 8 | project's [output directory](./workspace.md#output-directory) that tracks 9 | outdatedness information between builds. 10 | 11 | `werk` tracks outdatedness in extremely high detail. Any variable or expression 12 | used in a build recipe may contribute to its outdatedness. This enables the 13 | `--explain` option to provide very detailed information about why a build recipe 14 | was executed, and it causes `werk` to be very accurate when deciding what to 15 | build. 16 | 17 | Outdatedness is always transitive. If a build recipe is outdated, all of its 18 | dependents are also outdated. If a variable relies on another variable that is 19 | determined to have changed, that variable is also determined to have changed, 20 | and recipes relying on it will be outdated. 21 | 22 | The following factors contribute to outdatedness: 23 | 24 | - **File modification timestamps:** If a build recipe depends on a file that has 25 | a newer modification timestamp than a previously built output file, the file 26 | is considered outdated. 27 | 28 | - **Glob results:** If a `glob` expression produces a new result between runs 29 | (i.e., a file is deleted that previously matched the pattern, or a new file is 30 | added matching the pattern), any recipe relying on the results of that glob 31 | expression will be outdated. 32 | 33 | - **Program paths:** If the path to a program's executable changes between runs 34 | (i.e., the result of a `which` expression changed), any recipe relying on the 35 | results of that expression will be outdated. Note: `werk` does not currently 36 | take file modification timestamps of found programs into account, so updating 37 | your tools may still require a manual rebuild. 38 | 39 | - **Environment variables:** If the value of an environment variable changed 40 | between runs, any recipe relying on the value will be outdated. 41 | 42 | - **Recipes:** If the recipe to build a file changes in a way that would cause 43 | the file to be built in a different way, the file is considered outdated. 44 | Insignificant changes that are ignored in this check are `info` and `warn` 45 | statements and comments. 46 | 47 | - **Global variables:** If the definition of a global variable changes in the 48 | Werkfile, all recipes that use that specific variable will be outdated. For 49 | example, changing the string value of a global variable will cause recipes 50 | relying on that variable to become outdated. 51 | 52 | - **Command-line overrides:** If a `-Dkey=value` option is passed to `werk` to 53 | override a config variable, and it was not overridden with the same value in a 54 | previous run, all recipes depending on that variable will be considered 55 | outdated. 56 | 57 | This means that a build recipe that has no input files can still become 58 | outdated, because its outdatedness is determined by these factors. 59 | 60 | Note that task recipes are always "outdated" (just like `.PHONY` targets), so a 61 | build recipe that depends on a task recipe will always be outdated. 62 | 63 | ## Note about globals and recipes 64 | 65 | The outdatedness of global variables and recipes is determined by their 66 | definition in the Werkfile. This check is performed against the hash of the AST 67 | of those statements - not the source code representation. This means that 68 | modifying comments in or around those statements will not affect outdatedness. 69 | 70 | In general, only changes that can affect the commands that are actually run by a 71 | recipe are included in the outdatedness check, so things like modifying the 72 | informational message of an `info` statement will not cause its surrounding 73 | recipe to become outdated. 74 | -------------------------------------------------------------------------------- /book/src/paths.md: -------------------------------------------------------------------------------- 1 | # Paths 2 | 3 | File and directory paths in Werk are not normal paths as understood by the 4 | operating system. This is because one of the primary goals of Werk is to work on 5 | all platforms, and especially equal treatment of poor, maligned Windows. 6 | 7 | Paths in Werk are always relative to the workspace root or the output directory. 8 | Files outside of the workspace cannot be treated as inputs to or outputs of 9 | build recipes. Werk is designed to only write to the output directory. 10 | 11 | However, invoking external commands often requires passing native OS paths. 12 | Using the [special string interpolation 13 | syntax](./language/strings.md#string-interpolation) `""`, the abstract path 14 | stored in `var` will be converted to a native absolute path within the 15 | workspace. 16 | 17 | [Native path resolution](./language/path_resolution.md) may resolve to either an 18 | input file in the workspace or a generated file in the output directory. This 19 | check is based on existence: If the file is found in the workspace, it resolves 20 | to the file inside the workspace. Otherwise, it is assumed that a build recipe 21 | will generate the file in the output directory, and it resolves to an absolute 22 | path inside the output directory, mirroring the directory structure of the 23 | workspace. 24 | 25 | In general, build recipes should take care to not clobber the workspace and only 26 | generate files with paths that coincide with paths in the workspace. 27 | 28 | Logically, the workspace is an "overlay" of the output directory - it always 29 | takes precedence when a file exists, and the output directory is a "fallback". 30 | 31 | Consider this directory structure: 32 | 33 | ```text 34 | c:\ 35 | workspace\ 36 | main.c 37 | foo.c 38 | output\ 39 | main.o 40 | ``` 41 | 42 | Here, `c:\workspace\main.c` has previously been built and placed at 43 | `c:\workspace\output\main.o`. However, `foo.c` has not yet been built. 44 | 45 | Path resolution will then work like this: 46 | 47 | - `/main.c` will resolve to `c:\workspace\main.c` because it exists in the 48 | workspace. 49 | - `/foo.c` will resolve to `c:\workspace\main.c` because it exists in the 50 | workspace. 51 | - `/main.o` will resolve to `c:\workspace\output\main.o` because it does not 52 | exist in the workspace. 53 | - `/foo.o` will resolve to `c:\workspace\output\foo.o` because it does not exist 54 | in the workspace. 55 | - `/other.c` will resolve to `c:\workspace\output\other.c` because it does not 56 | exist in the workspace. 57 | 58 | ## Virtual path rules 59 | 60 | - The path separator is forward slash. 61 | - The root path `/` refers to the workspace root, never the native filesystem 62 | root. 63 | - Path components must be valid UTF-8. Incomplete surrogate pairs on Windows or 64 | arbitrary byte sequences on Linux/macOS are not supported, and will cause an 65 | error. 66 | - Path components must only contain "printable" Unicode characters, no control 67 | characters or newlines. 68 | - Path components must be valid on all platforms. In particular this means that 69 | the more restrictive rules that apply to paths on Windows also apply to path 70 | components in abstract paths, even when `werk` is running on other operating 71 | systems. See [Windows rules](#windows-rules). 72 | - Path components cannot start or end with whitespace. 73 | - Path components cannot end with a period `.` - the filename extension cannot 74 | be empty. 75 | - Complete paths never end in a path separator. 76 | 77 | ## Illegal characters 78 | 79 | The following characters are illegal in abstract paths paths, and it is a 80 | superset of disallowed paths on Unix-like systems and Windows: 81 | 82 | - Shell operators: `<` and `>` and `|` 83 | - Quotation marks: `"` and `'` 84 | - Slashes: `/` and `\` 85 | - Special punctuation characters: `:` and `?` and `*` 86 | 87 | ## Windows rules 88 | 89 | Some file names are reserved on Windows, and may not occur as file names - even 90 | when they also have a file extension, and even in lowercase, or mixed case! 91 | 92 | To complete the madness: For the special filenames ending in numbers, the digits 93 | `1`, `2`, and `3` are considered equal to their superscript equivalents. For 94 | example, `COM¹` is reserved in addition to `COM1`. 95 | 96 | - `CON` 97 | - `PRN` 98 | - `AUX` 99 | - `COM0`-`COM0` 100 | - `LPT0`-`LPT9` 101 | 102 | Werk considers these filenames invalid on _all_ platforms, even when running on 103 | a non-Windows platform. This is to ensure the portability of Werkfiles. 104 | -------------------------------------------------------------------------------- /book/src/task_recipes.md: -------------------------------------------------------------------------------- 1 | # Task recipes 2 | 3 | Task recipes are "housekeeping tasks" or "workflows" that you may frequently 4 | want to run. They have the same role as `.PHONY` targets (Make) and tasks in 5 | `just`. 6 | 7 | When a task recipe has one or more `run` statements, the recipe will execute 8 | [recipe commands](./language/recipe_commands.md) when invoked. 9 | 10 | Task recipes can depend on each other, and they can depend on build recipes. If 11 | a task recipe participates in any [outdatedness check](./outdatedness.md), it 12 | and all of its dependents is considered outdated. 13 | 14 | A single task is only ever run once during a build (occupying a single node in 15 | the dependency graph). In other words, if multiple recipes are being executed 16 | that depend on the same task recipe, that recipe will be executed exactly once, 17 | before any of the recipes that depend on it. 18 | 19 | ## Reference 20 | 21 | ```werk 22 | task my-task { 23 | # Define a local variable, here indicating a build recipe to run. 24 | let my-program-target = "my-program" 25 | 26 | # Run tasks or build recipes before this task. May be a list or a single name. 27 | build "my-program" 28 | 29 | # Enable forwarding the output of executed commands to the console. 30 | capture false 31 | 32 | # Set an environment variable for all child processes in this recipe. 33 | env "MY_VAR" = "value" 34 | 35 | # Remove an environment variable for all child processes in this recipe. 36 | env-remove "MY_VAR" 37 | 38 | # Run an external program after building this task's dependencies. 39 | run "echo \"Hello!\"" 40 | 41 | # Can also run a block of commands. 42 | run { 43 | "echo \"Hello!\"" 44 | "some-other-command" 45 | info "my-task completed!" 46 | } 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /book/src/watch.md: -------------------------------------------------------------------------------- 1 | # Watch for changes 2 | 3 | `werk` can automatically watch for changes to the [workspace](./workspace.md) 4 | and re-run a task or build recipe when a change occurs. 5 | 6 | In any project, run `werk --watch` to build a target and then wait for 7 | changes to any files in the workspace to trigger a rebuild. 8 | 9 | Only files in the workspace are watched for changes - not changes in the output 10 | directory, or changes to files covered by `.gitignore`. 11 | 12 | `werk` inserts a small delay between detecting a file and actually starting a 13 | rebuild, to avoid "overreacting" when many changes occur, and also because some 14 | filesystem notifications are actually delivered before the change is visible. 15 | This delay can be customized using `--watch-delay=`. 16 | 17 | `--watch` works together with other flags, like `--explain`, to provide detailed 18 | information about the build. 19 | 20 | ## Example 21 | 22 | Using [the C example](./examples/c.md), this will build and re-run the 23 | executable for every change. 24 | 25 | ```shell 26 | $ werk run --watch --explain 27 | [ ok ] /my-program.exe 28 | foo() returned: 123 29 | [ ok ] run 30 | [werk] Watching 4 files for changes, press Ctrl-C to stop 31 | ``` 32 | 33 | Making a change to any of the files involved in the build will then cause a 34 | rebuild. Let's say a change was made to `foo.h`, which is included by other 35 | files: 36 | 37 | ```shell 38 | [0/1] rebuilding `/foo.o` 39 | Cause: `/foo.h` was modified 40 | [0/1] rebuilding `/main.o` 41 | Cause: `/foo.h` was modified 42 | [ ok ] /foo.o 43 | [ ok ] /main.o 44 | [0/1] rebuilding `my-program.exe` 45 | Cause: `/foo.o` was rebuilt 46 | Cause: `/main.o` was rebuilt 47 | [ ok ] /my-program.exe 48 | foo() returned: 123 49 | [ ok ] run 50 | [werk] Watching 4 files for changes, press Ctrl-C to stop 51 | ``` 52 | -------------------------------------------------------------------------------- /book/src/werk_cache.md: -------------------------------------------------------------------------------- 1 | # `.werk-cache` 2 | 3 | This is a special file created by `werk` in the [output 4 | directory](./workspace.md#output-directory). 5 | 6 | It is a TOML document containing metadata used during [outdatedness 7 | checks](./outdatedness.md), including [glob](./language/operations.md#glob) 8 | results, used environment variables ([`env`](./language/operations.md#env)), 9 | used program paths ([`which`](./language/operations.md#which)), the recipe 10 | itself, manual command-line overrides (`-Dkey=value`), and any global variables 11 | used while evaluating the recipe. 12 | 13 | In short, `.werk-cache` is what enables `werk` do perform very detailed 14 | outdatedness checks. 15 | 16 | All values stored in `.werk-cache` are hashed to avoid leaking secrets from the 17 | environment, but the hash is not cryptographically secure. It can't be: since 18 | the hash must be stable between runs, using a random seed would defeat the 19 | purpose. 20 | 21 | `.werk-cache` can be safely deleted by the user, but doing so may cause the next 22 | build to rebuild more than necessary. 23 | -------------------------------------------------------------------------------- /book/src/why_not_just.md: -------------------------------------------------------------------------------- 1 | # Why not just? 2 | 3 | [just](https://just.systems/) is a command runner that is very popular. It fits 4 | the niche where what you need is a collection of "housekeeping" tasks for your 5 | project. 6 | 7 | It's very easy to use, and has a syntax inspired by Make, but it isn't able to 8 | actually build things (tracking dependencies between artifacts), only run 9 | commands. 10 | 11 | It also comes with a shell requirement, making it hard to write portable 12 | Justfiles. 13 | -------------------------------------------------------------------------------- /book/src/why_not_make.md: -------------------------------------------------------------------------------- 1 | # Why not make? 2 | 3 | GNU Make is an incredibly powerful tool that has proven its worth through \~50 4 | years of reliable use. But its age is also showing, and its behavior is often 5 | surprising to people who are used to more modern tools. 6 | 7 | Put simply, it solves many hard problems that I don't have, and it doesn't solve 8 | many of the easy problems I _do_ have. 9 | 10 | The most glaring problem with Make is that it does not work well on Windows. It 11 | can be made to work, but not portably - often projects will have specific 12 | Makefiles for each platform, which must be maintained in isolation. 13 | 14 | ## Key differences from Make 15 | 16 | - Truly cross-platform: `werk` treats Windows as a first-class target platform. 17 | - `werk` has a clear separation between "input" files 18 | ([workspace](./workspace.md)) and "output" files (output directory). `werk` 19 | will never produce files alongside input files. 20 | - Globbing "just works". `werk` tracks the result of glob operations between 21 | runs and detects that a file is outdated if one of its dependencies was 22 | removed since the last run. 23 | - No shell requirement: `werk` does not execute commands in a shell, but 24 | performs its own `$PATH` lookup etc., meaning all `Werkfile`s work natively on 25 | all platforms without needing a POSIX emulation layer. 26 | - No shell (2): This also means that common POSIX shell utilities are not 27 | available. Most operations usually delegated to the shell can be performed 28 | using built-in language features instead, but explicitly invoking a shell is 29 | also an option. 30 | - Build recipes and tasks recipes are separate things: No need for `.PHONY` 31 | targets. 32 | - Language semantics matching modern expectations: `werk` distinguished between 33 | lists and strings, meaning that file names can contain spaces. 34 | - No implicit rules. 35 | - Automatic documentation. `werk --list` prints the list of available recipes 36 | and configuration variables to the command line, along with any preceding 37 | comment. 38 | - Dry-run mode: Running `werk --dry-run` does not run any commands, but still 39 | goes through the process of figuring out which commands would have been run. 40 | - Better debugging: Running `werk --explain` explains why a recipe was run. Also 41 | works with `--dry-run`. 42 | - Better error messages. `werk` tries very hard to be helpful when an error 43 | occurs. 44 | -------------------------------------------------------------------------------- /book/src/why_not_others.md: -------------------------------------------------------------------------------- 1 | # Why not `$toolname`? 2 | 3 | Here's a loose collection of reasons that I prefer `werk` to other similar 4 | tools: 5 | 6 | - `ninja`: Too low-level, not nice to write by hand, very specialized for C/C++. 7 | - `scons`: Very clunky in my opinion, annoying Python runtime dependency. 8 | - `meson`: Hard to use, integrates poorly with other tools. 9 | - `rake`: Ruby does not work on Windows. 10 | - `cargo xtask`: Solves a different problem, running Rust code at build time. 11 | - `cargo script`: Solves a different problem. 12 | - `cmake`: Very hard to use correctly, extremely hard to debug. 13 | - All the Java tools (`gradle`, `maven`, `bazel`): Too specific to Java 14 | projects, clunky, and hard to use. 15 | -------------------------------------------------------------------------------- /book/src/workspace.md: -------------------------------------------------------------------------------- 1 | # Workspace 2 | 3 | The workspace is the directory containing the `Werkfile`, minus any files and 4 | directories mentioned by `.gitignore`. 5 | 6 | When writing [build recipes](./build_recipes.md), the dependencies of a build 7 | recipe may be references to files within the workspace, or they may be 8 | referencing the output of another recipe, which will exist in the output 9 | directory. 10 | 11 | ## Output directory 12 | 13 | The output directory is where files produced by `werk` will be placed. The 14 | default path is `$WORKSPACE/target` (same as Cargo), but this can be overridden 15 | in two ways: 16 | 17 | * From within the Werkfile: `default out-dir = ".."` 18 | * From the command-like: `werk --output-dir=..` 19 | 20 | If `werk` detects that an output directory is included in the workspace (i.e., 21 | it is not covered by `.gitignore`), it will emit a hard error. 22 | -------------------------------------------------------------------------------- /documentation/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | Please refer to [the book](https://simonask.github.io/werk). 4 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Werk Examples 2 | 3 | How to run an example without installing `werk`: 4 | 5 | ```sh 6 | $ cargo run -p werk-cli -- -f examples/c/Werkfile 7 | # Build output here 8 | ``` 9 | -------------------------------------------------------------------------------- /examples/c/Werkfile: -------------------------------------------------------------------------------- 1 | default out-dir = "../../target/examples/c" 2 | default target = "build" 3 | 4 | # Path to the C compiler. 5 | config cc = which "clang" 6 | config ld = cc 7 | # "debug" or "release" 8 | config profile = "debug" 9 | let executable = "{profile}/example{EXE_SUFFIX}" 10 | let cflags = profile | match { 11 | "debug" => ["-g", "-O0", "-fdiagnostics-color=always", "-fcolor-diagnostics", "-fansi-escape-codes"] 12 | "release" => ["-O3", "-fdiagnostics-color=always", "-fcolor-diagnostics", "-fansi-escape-codes"] 13 | "%" => error "Unknown profile '%'; valid options are 'debug' and 'release'" 14 | } 15 | 16 | # Build an object file from a C source file. 17 | build "{profile}/%.o" { 18 | from "%.c" 19 | depfile "/{profile}/%.c.d" 20 | run "{cc} -c {cflags*} -o " 21 | } 22 | 23 | # Build the depfile for an object file. 24 | build "{profile}/%.c.d" { 25 | from "%.c" 26 | run "{cc} -MM -MT -MF " 27 | } 28 | 29 | # Build the executable. 30 | build "{executable}" { 31 | from glob "*.c" | match { 32 | "%.c" => "/{profile}%.o" 33 | } 34 | run "{ld} -o " 35 | } 36 | 37 | # Build the executable (shorthand). 38 | task build { 39 | build executable 40 | info "Build done for profile '{profile}'" 41 | } 42 | 43 | # Build and run the executable. 44 | task run { 45 | build "build" 46 | run "" 47 | } 48 | 49 | task clean { 50 | let object-files = glob "*.c" | map "{profile}{:.c=.o}" 51 | run { 52 | delete object-files 53 | delete executable 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /examples/c/foo.c: -------------------------------------------------------------------------------- 1 | #include "foo.h" 2 | 3 | int foo(int a, int b) 4 | { 5 | return a + b; 6 | } 7 | 8 | #pragma message("hello") 9 | -------------------------------------------------------------------------------- /examples/c/foo.h: -------------------------------------------------------------------------------- 1 | #ifdef __cplusplus 2 | extern "C" 3 | { 4 | #endif 5 | 6 | int foo(int a, int b); 7 | 8 | #ifdef __cplusplus 9 | } // extern "C" 10 | #endif 11 | -------------------------------------------------------------------------------- /examples/c/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "foo.h" 3 | int main() 4 | { 5 | printf("Hello, World! %d\n", foo(1, 2)); 6 | printf("Second line of output\n"); 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /examples/demo/Werkfile: -------------------------------------------------------------------------------- 1 | default out-dir = "../../target/examples/demo" 2 | 3 | let cc = which "clang" 4 | let ld = cc 5 | 6 | build "%.o" { 7 | run "{cc} -c -o -MF -MM -MT " 8 | } 9 | 10 | task build { 11 | build "program{EXE_SUFFIX}" 12 | } 13 | -------------------------------------------------------------------------------- /examples/hello/Werkfile: -------------------------------------------------------------------------------- 1 | default target = "hello" 2 | 3 | task hello { 4 | info "Hello, World!" 5 | } 6 | -------------------------------------------------------------------------------- /examples/hello/target/.werk-cache: -------------------------------------------------------------------------------- 1 | # Generated by werk. It can be safely deleted. 2 | 3 | -------------------------------------------------------------------------------- /examples/include/Werkfile: -------------------------------------------------------------------------------- 1 | default out-dir = "../../target/examples/c" 2 | default target = "build" 3 | 4 | include "config.werk" 5 | include "recipes.werk" 6 | 7 | let executable = "{profile}/example{EXE_SUFFIX}" 8 | 9 | # Build the executable. 10 | build "{executable}" { 11 | from glob "*.c" | match { 12 | "%.c" => "/{profile}%.o" 13 | } 14 | run "{ld} -o " 15 | } 16 | 17 | # Build the executable (shorthand). 18 | task build { 19 | build executable 20 | info "Build done for profile '{profile}'" 21 | } 22 | 23 | # Build and run the executable. 24 | task run { 25 | build "build" 26 | run "" 27 | } 28 | 29 | task clean { 30 | let object-files = glob "*.c" | map "{profile}{:.c=.o}" 31 | run { 32 | delete object-files 33 | delete executable 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /examples/include/config.werk: -------------------------------------------------------------------------------- 1 | # Path to the C compiler. 2 | config cc = which "clang" 3 | config ld = cc 4 | # "debug" or "release" 5 | config profile = "debug" 6 | -------------------------------------------------------------------------------- /examples/include/foo.c: -------------------------------------------------------------------------------- 1 | #include "foo.h" 2 | 3 | int foo(int a, int b) 4 | { 5 | return a + b; 6 | } 7 | 8 | #pragma message("hello") 9 | -------------------------------------------------------------------------------- /examples/include/foo.h: -------------------------------------------------------------------------------- 1 | #ifdef __cplusplus 2 | extern "C" 3 | { 4 | #endif 5 | 6 | int foo(int a, int b); 7 | 8 | #ifdef __cplusplus 9 | } // extern "C" 10 | #endif 11 | -------------------------------------------------------------------------------- /examples/include/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "foo.h" 3 | int main() 4 | { 5 | printf("Hello, World! %d\n", foo(1, 2)); 6 | printf("Second line of output\n"); 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /examples/include/recipes.werk: -------------------------------------------------------------------------------- 1 | let cflags = profile | match { 2 | "debug" => ["-g", "-O0", "-fdiagnostics-color=always", "-fcolor-diagnostics", "-fansi-escape-codes"] 3 | "release" => ["-O3", "-fdiagnostics-color=always", "-fcolor-diagnostics", "-fansi-escape-codes"] 4 | "%" => error "Unknown profile '%'; valid options are 'debug' and 'release'" 5 | } 6 | 7 | # Build an object file from a C source file. 8 | build "{profile}/%.o" { 9 | from "%.c" 10 | depfile "/{profile}/%.c.d" 11 | run "{cc} -c {cflags*} -o " 12 | } 13 | 14 | # Build the depfile for an object file. 15 | build "{profile}/%.c.d" { 16 | from "%.c" 17 | run "{cc} -MM -MT -MF " 18 | } 19 | -------------------------------------------------------------------------------- /examples/issue-41/Werkfile: -------------------------------------------------------------------------------- 1 | default out-dir = "../../target/examples/issue-41" 2 | default target = "build" 3 | 4 | build "foo" { 5 | info "" 6 | } 7 | 8 | build "bar" { 9 | info "" 10 | } 11 | 12 | task build { 13 | build "foo" 14 | build "bar" 15 | } 16 | -------------------------------------------------------------------------------- /examples/shaders/Werkfile: -------------------------------------------------------------------------------- 1 | default out-dir = "../../target/examples/shaders" 2 | default target = "build" 3 | 4 | let tar = which "tar" 5 | 6 | build "%.(frag|vert|comp).spv" { 7 | from "{%}.{0}" 8 | run "glslc -o " 9 | } 10 | 11 | build "assets.tar.gz" { 12 | from glob "*.\{frag,vert,comp\}" | map "{}.spv" 13 | run "{tar} -zcf " 14 | } 15 | 16 | task build { 17 | build "assets.tar.gz" 18 | } 19 | -------------------------------------------------------------------------------- /examples/shaders/shader.frag: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | void main() {} 4 | -------------------------------------------------------------------------------- /examples/shaders/shader.vert: -------------------------------------------------------------------------------- 1 | #version 460 core 2 | 3 | void main() {} 4 | -------------------------------------------------------------------------------- /examples/write/Werkfile: -------------------------------------------------------------------------------- 1 | # This example demonstrates the `write` expression in recipes, where the value # 2 | # of any expression can be written to a file. In this case, a list of strings is 3 | # written as lines. 4 | 5 | default out-dir = "../../target/examples/write" 6 | default target = "lines.txt" 7 | 8 | let list = ["a", "b", "c"] 9 | 10 | build "lines.txt" { 11 | run { 12 | write list | join "\n" to out 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tests" 3 | publish = false 4 | version.workspace = true 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license.workspace = true 8 | 9 | [dependencies] 10 | werk-runner.workspace = true 11 | werk-parser.workspace = true 12 | werk-fs.workspace = true 13 | anyhow = "1" 14 | parking_lot.workspace = true 15 | ahash.workspace = true 16 | tracing.workspace = true 17 | tracing-subscriber = "0.3.18" 18 | smol.workspace = true 19 | smol-macros = "0.1.1" 20 | macro_rules_attribute = "0.2.0" 21 | toml_edit.workspace = true 22 | futures.workspace = true 23 | regex.workspace = true 24 | werk-util.workspace = true 25 | anstream.workspace = true 26 | # Hijacking winnow for the Offset trait 27 | winnow.workspace = true 28 | 29 | [dev-dependencies] 30 | criterion = "0.5.1" 31 | iai = "0.1" 32 | 33 | [lib] 34 | path = "lib.rs" 35 | 36 | [[test]] 37 | name = "test_expressions" 38 | path = "test_expressions.rs" 39 | 40 | [[test]] 41 | name = "test_pattern_match" 42 | path = "test_pattern_match.rs" 43 | 44 | [[test]] 45 | name = "test_outdatedness" 46 | path = "test_outdatedness.rs" 47 | 48 | [[test]] 49 | name = "test_path_resolution" 50 | path = "test_path_resolution.rs" 51 | 52 | [[test]] 53 | name = "test_cases" 54 | path = "test_cases.rs" 55 | 56 | [[test]] 57 | name = "test_eval" 58 | path = "test_eval.rs" 59 | -------------------------------------------------------------------------------- /tests/cases/array.werk: -------------------------------------------------------------------------------- 1 | let array = ["a", "b", "c"] 2 | let a = array[0] | assert-eq "a" 3 | let b = array[1] | assert-eq "b" 4 | let c = array[2] | assert-eq "c" 5 | 6 | let a = "x{array[0]}" | assert-eq "xa" 7 | let b = "x{array[1]}" | assert-eq "xb" 8 | let c = "x{array[2]}" | assert-eq "xc" 9 | 10 | let c = "x{array[-1]}" | assert-eq "xc" 11 | 12 | let len = array | len | assert-eq "3" 13 | let first = array | first | assert-eq "a" 14 | let last = array | last | assert-eq "c" 15 | let tail = array | tail | assert-eq ["b", "c"] 16 | 17 | # Stringly typed index 18 | let index = "0" 19 | let a = array[index] | assert-eq "a" 20 | 21 | let negative_index = "-1" 22 | let c = array[negative_index] | assert-eq "c" 23 | -------------------------------------------------------------------------------- /tests/cases/copy.werk: -------------------------------------------------------------------------------- 1 | default target = "bar" 2 | 3 | build "bar" { 4 | from "foo" 5 | run { 6 | copy "{in}" to "{out}" 7 | } 8 | } 9 | 10 | #!file foo=hello 11 | #!assert-file bar=hello 12 | -------------------------------------------------------------------------------- /tests/cases/dedup.werk: -------------------------------------------------------------------------------- 1 | let a = "a" | dedup | assert-eq "a" 2 | let b = ["b"] | dedup | assert-eq ["b"] 3 | let c = ["c", "c"] | dedup | assert-eq ["c"] 4 | let d = ["d", ["d", ["d"]]] | dedup | assert-eq ["d"] 5 | let abcd = ["a", ["b", "a"], ["c", "d"], "d"] | dedup | assert-eq ["a", "b", "c", "d"] 6 | 7 | let a = "a" | "{:dedup}" | assert-eq "a" 8 | let b = ["b"] | "{,*:dedup}" | assert-eq "b" 9 | let c = ["c", "c"] | "{,*:dedup}" | assert-eq "c" 10 | let d = ["d", ["d", ["d"]]] | "{,*:dedup}" | assert-eq "d" 11 | let abcd = ["a", ["b", "a"], ["c", "d"], "d"] | "{,*:dedup}" | assert-eq "a,b,c,d" 12 | -------------------------------------------------------------------------------- /tests/cases/discard.werk: -------------------------------------------------------------------------------- 1 | let input = ["a.c", "b.cpp"] 2 | let result = input 3 | | discard "%.c" 4 | | assert-eq ["b.cpp"] 5 | -------------------------------------------------------------------------------- /tests/cases/env.werk: -------------------------------------------------------------------------------- 1 | default target = "all" 2 | 3 | let x = env "MY_ENV" | assert-eq "foo" 4 | 5 | task all { 6 | build ["passthrough", "override", "override-in-recipe", "remove", "remove-in-recipe"] 7 | } 8 | 9 | build "passthrough" { 10 | run "write-env MY_ENV " 11 | } 12 | 13 | build "override" { 14 | env "MY_ENV" = "override" 15 | run "write-env MY_ENV " 16 | } 17 | 18 | build "override-in-recipe" { 19 | run { 20 | env "MY_ENV" = "override-in-recipe" 21 | shell "write-env MY_ENV " 22 | } 23 | } 24 | 25 | build "remove" { 26 | env-remove "MY_ENV" 27 | run "write-env MY_ENV " 28 | } 29 | 30 | build "remove-in-recipe" { 31 | run { 32 | env-remove "MY_ENV" 33 | shell "write-env MY_ENV " 34 | } 35 | } 36 | 37 | #!env MY_ENV=foo 38 | #!assert-file passthrough=foo 39 | #!assert-file override=override 40 | #!assert-file override-in-recipe=override-in-recipe 41 | #!assert-file remove= 42 | #!assert-file remove-in-recipe= 43 | -------------------------------------------------------------------------------- /tests/cases/filter.werk: -------------------------------------------------------------------------------- 1 | let input = ["a.c", "b.cpp"] 2 | let result = input 3 | | filter "%.c" 4 | | assert-eq ["a.c"] 5 | let result = input 6 | | filter "%.(c|cpp)" 7 | | assert-eq ["a.c", "b.cpp"] 8 | let result = input 9 | | filter "%.(a|b)" 10 | | assert-eq [] 11 | 12 | # recursive, flattens implicitly 13 | let input = ["a.c", ["b.c", ["c.c", "d.c"]]] 14 | let result = input 15 | | filter "%.c" 16 | | assert-eq ["a.c", "b.c", "c.c", "d.c"] 17 | 18 | let input = ["a.c", "b.cpp"] 19 | let result = input 20 | | filter-match "%.c" => "{%}.o" 21 | | assert-eq ["a.o"] 22 | -------------------------------------------------------------------------------- /tests/cases/flatten.werk: -------------------------------------------------------------------------------- 1 | let input = [] 2 | let result = input | flatten | assert-eq [] 3 | 4 | let input = "a" 5 | let result = input | flatten | assert-eq ["a"] 6 | 7 | let input = ["a", "b"] 8 | let result = input | flatten | assert-eq ["a", "b"] 9 | 10 | let input = ["a", ["b"]] 11 | let result = input | flatten | assert-eq ["a", "b"] 12 | 13 | let input = ["a", ["b", "c"]] 14 | let result = input | flatten | assert-eq ["a", "b", "c"] 15 | 16 | let input = ["a", ["b", ["c", "d"]]] 17 | let result = input | flatten | assert-eq ["a", "b", "c", "d"] 18 | 19 | let input = [[[]]] 20 | let result = input | flatten | assert-eq [] 21 | 22 | let input = [[["a"]]] 23 | let result = input | flatten | assert-eq ["a"] 24 | 25 | let input = [[["a"], "b"]] 26 | let result = input | flatten | assert-eq ["a", "b"] 27 | 28 | let input = [[["a"], ["b"]]] 29 | let result = input | flatten | assert-eq ["a", "b"] 30 | 31 | let input = [[["a"], ["b", "c"]]] 32 | let result = input | flatten | assert-eq ["a", "b", "c"] 33 | 34 | let input = [[["a"], ["b", ["c", "d"]]]] 35 | let result = input | flatten | assert-eq ["a", "b", "c", "d"] 36 | -------------------------------------------------------------------------------- /tests/cases/join.werk: -------------------------------------------------------------------------------- 1 | let input = ["a.c", "b.cpp"] 2 | let result = input 3 | | join "\n" 4 | | assert-eq "a.c\nb.cpp" 5 | 6 | # recursive 7 | let input = ["a.c", ["b.c", ["c.c", "d.c"]]] 8 | let result = input 9 | | join "--" 10 | | assert-eq "a.c--b.c--c.c--d.c" 11 | -------------------------------------------------------------------------------- /tests/cases/map.werk: -------------------------------------------------------------------------------- 1 | # map recursive list 2 | let input = ["a", ["b"]]; 3 | let result = input 4 | | map "hello {}" 5 | | assert-eq ["hello a", ["hello b"]] 6 | 7 | # map string 8 | let input = "a"; 9 | let result = input 10 | | map ("hello {}" | assert-eq "hello a") 11 | | assert-eq "hello a" 12 | -------------------------------------------------------------------------------- /tests/cases/match_expr.werk: -------------------------------------------------------------------------------- 1 | # empty match 2 | let input = "a"; 3 | let result = input 4 | | match {} 5 | | assert-eq "a" 6 | 7 | # empty match 8 | let input = []; 9 | let result = input 10 | | match {} 11 | | assert-eq [] 12 | 13 | # match maps the string 14 | let input = "foo.c" 15 | let result = input 16 | | match { 17 | "%.c" => "{%}.o" 18 | } 19 | | assert-eq "foo.o" 20 | 21 | # mismatch falls back to the input value 22 | let input = "foo.cpp" 23 | let result = input 24 | | match { 25 | "%.c" => "{%}.o" 26 | } 27 | | assert-eq "foo.cpp" 28 | 29 | # explicit fallback 30 | let input = "foo.cpp" 31 | let result = input 32 | | match { 33 | "%.c" => "{%}.o" 34 | "%" => "fallback" 35 | } 36 | | assert-eq "fallback" 37 | 38 | # implicit fallback 39 | let result = "foo.cpp" 40 | | match { 41 | "%" => "{}" 42 | } 43 | | assert-eq "foo.cpp" 44 | 45 | # fallback not hit 46 | let input = "foo.c" 47 | let result = input 48 | | match { 49 | "%.c" => "{%}.o" 50 | "%" => "fallback" 51 | } 52 | | assert-eq "foo.o" 53 | 54 | # recursive preserves structure 55 | let input = ["a.c", ["b.c", ["c.c", "d.c"]]] 56 | | match { 57 | "%.c" => "{%}.o" 58 | } 59 | | assert-eq ["a.o", ["b.o", ["c.o", "d.o"]]] 60 | -------------------------------------------------------------------------------- /tests/cases/nested_patterns.werk: -------------------------------------------------------------------------------- 1 | let foo = "a" 2 | let bar = foo | match { 3 | "%" => "a" | match { 4 | # Get the stem from the outer pattern. 5 | "{%}" => "b" 6 | "%" => error "fail" 7 | } 8 | } | assert-eq "b" 9 | -------------------------------------------------------------------------------- /tests/cases/read.werk: -------------------------------------------------------------------------------- 1 | let file-contents = read "foo" | assert-eq "bar" 2 | 3 | #!file foo=bar 4 | -------------------------------------------------------------------------------- /tests/cases/split.werk: -------------------------------------------------------------------------------- 1 | let input = "a b c d e" 2 | let result = input 3 | | split " " 4 | | assert-eq ["a", "b", "c", "d", "e"] 5 | 6 | 7 | let input = "a\nb\nc\nd\r\ne" 8 | let result = input 9 | | split "\n" 10 | | assert-eq ["a", "b", "c", "d\r", "e"] 11 | let result = input 12 | | lines 13 | | assert-eq ["a", "b", "c", "d", "e"] 14 | -------------------------------------------------------------------------------- /tests/cases/string_interp.werk: -------------------------------------------------------------------------------- 1 | let path = "/foo/bar/baz.c" 2 | let filename = "{path:filename}" | assert-eq "baz.c" 3 | let dirname = "{path:dir}" | assert-eq "/foo/bar" 4 | let ext = "{path:ext}" | assert-eq "c" 5 | let obj_path = "{path:.c=.o}" | assert-eq "/foo/bar/baz.o" 6 | let obj_filename1 = "{path:.c=.o,filename}" | assert-eq "baz.o" 7 | let obj_filename1 = "{path:filename,.c=.o}" | assert-eq "baz.o" 8 | let path_regex = "{path:s/bar/qux/}" | assert-eq "/foo/qux/baz.c" 9 | let path_regex_dir = "{path:s/bar/qux/,dir}" | assert-eq "/foo/qux" 10 | -------------------------------------------------------------------------------- /tests/cases/write.werk: -------------------------------------------------------------------------------- 1 | default target = "foo" 2 | 3 | build "foo" { 4 | run { 5 | write "bar" to "{out}" 6 | } 7 | } 8 | 9 | #!assert-file foo=bar 10 | -------------------------------------------------------------------------------- /tests/fail/ambiguous_build_recipe.txt: -------------------------------------------------------------------------------- 1 | error[R0011]: ambiguous pattern match: /foofoo 2 | --> /INPUT:3:7 3 | | 4 | 3 | build "%foo" { 5 | | ------ note: first pattern here 6 | 4 | info "" 7 | 5 | } 8 | 6 | 9 | 7 | build "foo%" { 10 | | ------ note: second pattern here 11 | | 12 | -------------------------------------------------------------------------------- /tests/fail/ambiguous_build_recipe.werk: -------------------------------------------------------------------------------- 1 | default target = "foofoo" 2 | 3 | build "%foo" { 4 | info "" 5 | } 6 | 7 | build "foo%" { 8 | info "" 9 | } 10 | -------------------------------------------------------------------------------- /tests/fail/ambiguous_path_resolution.txt: -------------------------------------------------------------------------------- 1 | error[E0032]: ambiguous path resolution: /bar exists in the workspace, but also matches a build recipe 2 | --> /INPUT:6:10 3 | | 4 | 5 | build "bar" { 5 | | ----- note: matched this build recipe 6 | 6 | info "" 7 | | ^^^^^^^ ambiguous path resolution: /bar exists in the workspace, but also matches a build recipe 8 | | 9 | = help: use `<...:out-dir>` or `<...:workspace>` to disambiguate between paths in the workspace and the output directory 10 | -------------------------------------------------------------------------------- /tests/fail/ambiguous_path_resolution.werk: -------------------------------------------------------------------------------- 1 | default target = "build" 2 | 3 | # Directories should not participate in the lookup that happens as part 4 | # of build recipe matching. 5 | build "bar" { 6 | info "" 7 | } 8 | build "foo" { 9 | info "" 10 | } 11 | 12 | task build { 13 | build ["foo", "bar"] 14 | } 15 | 16 | #!dir bar 17 | -------------------------------------------------------------------------------- /tests/fail/capture_group_out_of_bounds.txt: -------------------------------------------------------------------------------- 1 | error[E0009]: capture group with index 2 is out of bounds in the current scope 2 | --> /INPUT:2:21 3 | | 4 | 2 | "(a|b)(c|d)" => "{2}" 5 | | ^^^^^ capture group with index 2 is out of bounds in the current scope 6 | | 7 | = help: pattern capture groups are zero-indexed, starting from 0 8 | -------------------------------------------------------------------------------- /tests/fail/capture_group_out_of_bounds.werk: -------------------------------------------------------------------------------- 1 | let a = "ac" | match { 2 | "(a|b)(c|d)" => "{2}" 3 | "%" => error "no match" 4 | } 5 | -------------------------------------------------------------------------------- /tests/fail/duplicate_config.txt: -------------------------------------------------------------------------------- 1 | error[E0033]: duplicate config statement 2 | --> /INPUT:2:1 3 | | 4 | 1 | config foo = "a" 5 | | ---------------- note: previous config statement here 6 | 2 | config foo = "b" 7 | | ^^^^^^^^^^^^^^^^ duplicate config statement 8 | | 9 | -------------------------------------------------------------------------------- /tests/fail/duplicate_config.werk: -------------------------------------------------------------------------------- 1 | config foo = "a" 2 | config foo = "b" 3 | -------------------------------------------------------------------------------- /tests/fail/include_missing.txt: -------------------------------------------------------------------------------- 1 | error[E0027]: error including '/does-not-exist.werk': file not found 2 | --> /INPUT:1:1 3 | | 4 | 1 | include "does-not-exist.werk" 5 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error including '/does-not-exist.werk': file not found 6 | | 7 | -------------------------------------------------------------------------------- /tests/fail/include_missing.werk: -------------------------------------------------------------------------------- 1 | include "does-not-exist.werk" 2 | -------------------------------------------------------------------------------- /tests/fail/include_self.txt: -------------------------------------------------------------------------------- 1 | error[E0035]: same file included twice: /included 2 | --> /included:1:1 3 | | 4 | 1 | include "included" 5 | | ^^^^^^^^^^^^^^^^^^ same file included twice: /included 6 | | 7 | ::: /INPUT:1:1 8 | | 9 | 1 | include "included" 10 | | ------------------ note: already included here 11 | | 12 | -------------------------------------------------------------------------------- /tests/fail/include_self.werk: -------------------------------------------------------------------------------- 1 | include "included" 2 | 3 | #!file included=include "included" 4 | -------------------------------------------------------------------------------- /tests/fail/include_twice.txt: -------------------------------------------------------------------------------- 1 | error[E0035]: same file included twice: /included 2 | --> /INPUT:2:1 3 | | 4 | 1 | include "included" 5 | | ------------------ note: already included here 6 | 2 | include "included" 7 | | ^^^^^^^^^^^^^^^^^^ same file included twice: /included 8 | | 9 | -------------------------------------------------------------------------------- /tests/fail/include_twice.werk: -------------------------------------------------------------------------------- 1 | include "included" 2 | include "included" 3 | 4 | #!file included=let a = "foo" 5 | -------------------------------------------------------------------------------- /tests/fail/include_with_default.txt: -------------------------------------------------------------------------------- 1 | error[E0036]: `default` statements are not allowed in included files 2 | --> /included:1:1 3 | | 4 | 1 | default target="foo" 5 | | ^^^^^^^^^^^^^^^^^^^^ `default` statements are not allowed in included files 6 | | 7 | ::: /INPUT:1:1 8 | | 9 | 1 | include "included" 10 | | ------------------ note: included here 11 | | 12 | = help: move `default` statements to the top-level Werkfile 13 | -------------------------------------------------------------------------------- /tests/fail/include_with_default.werk: -------------------------------------------------------------------------------- 1 | include "included" 2 | 3 | #!file included=default target="foo" 4 | -------------------------------------------------------------------------------- /tests/fail/include_with_error.txt: -------------------------------------------------------------------------------- 1 | error[P1001]: parse error 2 | --> /included:1:1 3 | | 4 | 1 | let a= 5 | | - ^ expected expression 6 | | | 7 | | info: while parsing `let` statement 8 | | 9 | ::: /INPUT:1:1 10 | | 11 | 1 | include "included" 12 | | ------------------ note: included here 13 | | 14 | = help: expressions must start with a value, or an `env`, `glob`, `which`, or `shell` operation 15 | -------------------------------------------------------------------------------- /tests/fail/include_with_error.werk: -------------------------------------------------------------------------------- 1 | include "included" 2 | 3 | #!file included=let a= 4 | -------------------------------------------------------------------------------- /tests/fail/index_out_of_bounds.txt: -------------------------------------------------------------------------------- 1 | error[E0037]: index out of bounds: got 3, and the array length is 3 2 | --> /INPUT:2:11 3 | | 4 | 2 | let d = a[3] 5 | | ^ index out of bounds: got 3, and the array length is 3 6 | | 7 | = help: arrays are indexed from zero, and negative indices refer to elements from the end of the array 8 | -------------------------------------------------------------------------------- /tests/fail/index_out_of_bounds.werk: -------------------------------------------------------------------------------- 1 | let a = ["a", "b", "c"] 2 | let d = a[3] 3 | -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | // This file intentionally left blank. 2 | pub mod mock_io; 3 | -------------------------------------------------------------------------------- /tests/test_cases.rs: -------------------------------------------------------------------------------- 1 | use tests::mock_io::*; 2 | use werk_runner::Runner; 3 | 4 | fn strip_colors(s: &str) -> String { 5 | use std::io::Write as _; 6 | let mut buf = Vec::new(); 7 | let mut stream = anstream::StripStream::new(&mut buf); 8 | stream.write_all(s.as_bytes()).unwrap(); 9 | String::from_utf8(buf).unwrap() 10 | } 11 | 12 | fn fix_newlines(s: &str) -> String { 13 | s.replace('\r', "") 14 | } 15 | 16 | async fn evaluate_check(file: &std::path::Path) -> Result<(), anyhow::Error> { 17 | let source = std::fs::read_to_string(file).unwrap(); 18 | let mut test = Test::new(&source).map_err(|err| anyhow::Error::msg(err.to_string()))?; 19 | 20 | let workspace = test 21 | .create_workspace() 22 | .map_err(|err| anyhow::Error::msg(err.to_string()))?; 23 | 24 | // Invoke the runner if there is a default target. 25 | if let Some(ref default_target) = workspace.default_target { 26 | let runner = Runner::new(workspace); 27 | runner 28 | .build_or_run(default_target) 29 | .await 30 | .map_err(|err| anyhow::Error::msg(err.to_string()))?; 31 | std::mem::drop(runner); 32 | test.run_pragma_tests()?; 33 | } 34 | 35 | Ok(()) 36 | } 37 | 38 | macro_rules! success_case { 39 | ($name:ident) => { 40 | #[macro_rules_attribute::apply(smol_macros::test)] 41 | async fn $name() { 42 | _ = tracing_subscriber::fmt::try_init(); 43 | evaluate_check(std::path::Path::new(concat!( 44 | env!("CARGO_MANIFEST_DIR"), 45 | "/cases/", 46 | stringify!($name), 47 | ".werk" 48 | ))) 49 | .await 50 | .unwrap(); 51 | } 52 | }; 53 | } 54 | 55 | macro_rules! error_case { 56 | ($name:ident) => { 57 | #[macro_rules_attribute::apply(smol_macros::test)] 58 | async fn $name() { 59 | _ = tracing_subscriber::fmt::try_init(); 60 | 61 | let case_path = std::path::Path::new(concat!( 62 | env!("CARGO_MANIFEST_DIR"), 63 | "/fail/", 64 | stringify!($name), 65 | ".werk" 66 | )); 67 | let expected_path = std::path::Path::new(concat!( 68 | env!("CARGO_MANIFEST_DIR"), 69 | "/fail/", 70 | stringify!($name), 71 | ".txt" 72 | )); 73 | let expected_text = std::fs::read_to_string(expected_path).unwrap(); 74 | 75 | let output = match evaluate_check(case_path).await { 76 | Ok(_) => panic!("expected error"), 77 | Err(err) => err.to_string(), 78 | }; 79 | 80 | let output_stripped = fix_newlines(&strip_colors(&output)); 81 | let expected_lf = fix_newlines(&expected_text); 82 | 83 | if output_stripped.trim() != expected_lf.trim() { 84 | eprintln!("Error message mismatch!"); 85 | eprintln!("Got:\n{}", output); 86 | eprintln!("Expected:\n{}", expected_text); 87 | panic!("Error message mismatch"); 88 | } 89 | } 90 | }; 91 | } 92 | 93 | success_case!(map); 94 | success_case!(match_expr); 95 | success_case!(flatten); 96 | success_case!(join); 97 | success_case!(split); 98 | success_case!(discard); 99 | success_case!(filter); 100 | success_case!(write); 101 | success_case!(copy); 102 | success_case!(read); 103 | success_case!(env); 104 | success_case!(string_interp); 105 | success_case!(dedup); 106 | success_case!(nested_patterns); 107 | success_case!(array); 108 | 109 | error_case!(ambiguous_build_recipe); 110 | error_case!(ambiguous_path_resolution); 111 | error_case!(capture_group_out_of_bounds); 112 | error_case!(duplicate_config); 113 | error_case!(include_missing); 114 | error_case!(include_with_error); 115 | error_case!(include_self); 116 | error_case!(include_twice); 117 | error_case!(include_with_default); 118 | error_case!(index_out_of_bounds); 119 | -------------------------------------------------------------------------------- /tests/test_expressions.rs: -------------------------------------------------------------------------------- 1 | use werk_runner::Value; 2 | 3 | use tests::mock_io::*; 4 | use werk_util::Symbol; 5 | 6 | fn evaluate_global(source: &str, global_variable_name_to_check: &str) -> Value { 7 | let mut test = Test::new(source).unwrap(); 8 | let workspace = test.create_workspace().unwrap(); 9 | workspace 10 | .manifest 11 | .global_variables 12 | .get(&Symbol::new(global_variable_name_to_check)) 13 | .ok_or_else(|| anyhow::anyhow!("global variable not found")) 14 | .unwrap() 15 | .value 16 | .clone() 17 | } 18 | 19 | #[test] 20 | fn local_var() { 21 | assert_eq!(evaluate_global("let a = \"a\"; let b = a;", "b"), "a"); 22 | } 23 | 24 | #[test] 25 | fn join() { 26 | assert_eq!( 27 | evaluate_global( 28 | r#"let a = ["a", "b"]; let joined = a | join "\n""#, 29 | "joined" 30 | ), 31 | "a\nb" 32 | ); 33 | 34 | assert_eq!( 35 | evaluate_global( 36 | r#"let a = ["a", ["b", ["c"]]]; let joined = a | join "\n""#, 37 | "joined" 38 | ), 39 | "a\nb\nc" 40 | ); 41 | } 42 | 43 | #[test] 44 | fn match_expr_empty() { 45 | // Empty match is a no-op. 46 | assert_eq!( 47 | evaluate_global( 48 | r#" 49 | let input = "a"; 50 | let result = input | match { } 51 | "#, 52 | "result" 53 | ), 54 | "a" 55 | ); 56 | 57 | assert_eq!( 58 | evaluate_global( 59 | r" 60 | let input = []; 61 | let result = input | match { } 62 | ", 63 | "result" 64 | ), 65 | Value::List(vec![]) 66 | ); 67 | } 68 | 69 | #[test] 70 | fn match_expr_recursive() { 71 | // Empty match is a no-op. 72 | assert_eq!( 73 | evaluate_global( 74 | r#" 75 | let input = ["a", ["b"]]; 76 | let result = input 77 | | match { "a" => "foo"; "b" => "bar" } 78 | | assert-eq ["foo", ["bar"]] 79 | "#, 80 | "result" 81 | ), 82 | Value::List(vec![ 83 | Value::from("foo"), 84 | Value::List(vec![Value::from("bar")]) 85 | ]) 86 | ); 87 | } 88 | 89 | #[test] 90 | fn map_recursive() { 91 | // Map a recursive list. 92 | assert_eq!( 93 | evaluate_global( 94 | r#" 95 | let input = ["a", ["b"]]; 96 | let result = input 97 | | map "hello {}" 98 | | assert-eq ["hello a", ["hello b"]] 99 | "#, 100 | "result" 101 | ), 102 | Value::List(vec![ 103 | Value::from("hello a"), 104 | Value::List(vec![Value::from("hello b")]) 105 | ]) 106 | ); 107 | 108 | // Map a single string 109 | assert_eq!( 110 | evaluate_global( 111 | r#" 112 | let input = "a"; 113 | let result = input 114 | | map "hello {}" 115 | | assert-eq "hello a" 116 | "#, 117 | "result" 118 | ), 119 | Value::from("hello a") 120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /tests/test_pattern_match.rs: -------------------------------------------------------------------------------- 1 | use tests::mock_io::*; 2 | use werk_parser::parser::{Input, pattern_expr_inside_quotes}; 3 | use werk_runner::{Pattern, PatternMatchData, Workspace}; 4 | use werk_util::DiagnosticFileId; 5 | 6 | fn parse_and_compile_pattern(workspace: &Workspace, pattern: &str) -> Pattern { 7 | let expr = pattern_expr_inside_quotes(&mut Input::new(pattern)).unwrap(); 8 | werk_runner::eval::eval_pattern(workspace, &expr, DiagnosticFileId(0)) 9 | .unwrap() 10 | .value 11 | } 12 | 13 | #[test] 14 | fn test_pattern_match() -> anyhow::Result<()> { 15 | let mut test = Test::new("").unwrap(); 16 | let workspace = test.create_workspace().unwrap(); 17 | 18 | let empty = parse_and_compile_pattern(workspace, ""); 19 | let all = parse_and_compile_pattern(workspace, "%"); 20 | let specific = parse_and_compile_pattern(workspace, "foo"); 21 | let c_ext = parse_and_compile_pattern(workspace, "%.c"); 22 | 23 | assert_eq!( 24 | empty.match_whole_string(""), 25 | Some(PatternMatchData::default()) 26 | ); 27 | assert_eq!(empty.match_whole_string("a"), None); 28 | 29 | assert_eq!( 30 | all.match_whole_string(""), 31 | Some(PatternMatchData::new(Some(""), None::<&str>)) 32 | ); 33 | assert_eq!( 34 | all.match_whole_string("Hello, World!"), 35 | Some(PatternMatchData::new(Some("Hello, World!"), None::<&str>)) 36 | ); 37 | 38 | assert_eq!( 39 | specific.match_whole_string("foo"), 40 | Some(PatternMatchData::default()) 41 | ); 42 | assert_eq!(specific.match_whole_string("bar"), None); 43 | 44 | assert_eq!( 45 | c_ext.match_whole_string(".c"), 46 | Some(PatternMatchData::new(Some(""), None::<&str>)) 47 | ); 48 | assert_eq!( 49 | c_ext.match_whole_string("a.c"), 50 | Some(PatternMatchData::new(Some("a"), None::<&str>)) 51 | ); 52 | 53 | Ok(()) 54 | } 55 | 56 | #[test] 57 | fn test_capture_groups() -> anyhow::Result<()> { 58 | let mut test = Test::new("").unwrap(); 59 | let workspace = test.create_workspace().unwrap(); 60 | 61 | let abc = parse_and_compile_pattern(workspace, "(a|b|c)"); 62 | 63 | assert_eq!( 64 | abc.match_whole_string("a"), 65 | Some(PatternMatchData::new(None::<&str>, [String::from("a")])) 66 | ); 67 | assert_eq!( 68 | abc.match_whole_string("b"), 69 | Some(PatternMatchData::new(None::<&str>, [String::from("b")])) 70 | ); 71 | assert_eq!( 72 | abc.match_whole_string("c"), 73 | Some(PatternMatchData::new(None::<&str>, [String::from("c")])) 74 | ); 75 | 76 | let stem_abc = parse_and_compile_pattern(workspace, "%(a|b|c)"); 77 | assert_eq!( 78 | stem_abc.match_whole_string("aaa"), 79 | Some(PatternMatchData::new(Some("aa"), [String::from("a")])) 80 | ); 81 | assert_eq!( 82 | stem_abc.match_whole_string("abc"), 83 | Some(PatternMatchData::new(Some("ab"), [String::from("c")])) 84 | ); 85 | assert_eq!( 86 | stem_abc.match_whole_string("bbc"), 87 | Some(PatternMatchData::new(Some("bb"), [String::from("c")])) 88 | ); 89 | assert_eq!(stem_abc.match_whole_string("bbd"), None); 90 | 91 | let abc_stem = parse_and_compile_pattern(workspace, "(a|b|c)%"); 92 | assert_eq!( 93 | abc_stem.match_whole_string("aaa"), 94 | Some(PatternMatchData::new(Some("aa"), [String::from("a")])) 95 | ); 96 | assert_eq!( 97 | abc_stem.match_whole_string("abc"), 98 | Some(PatternMatchData::new(Some("bc"), [String::from("a")])) 99 | ); 100 | assert_eq!( 101 | abc_stem.match_whole_string("bbc"), 102 | Some(PatternMatchData::new(Some("bc"), [String::from("b")])) 103 | ); 104 | assert_eq!( 105 | abc_stem.match_whole_string("bbd"), 106 | Some(PatternMatchData::new(Some("bd"), [String::from("b")])) 107 | ); 108 | assert_eq!(abc_stem.match_whole_string("dbb"), None,); 109 | 110 | Ok(()) 111 | } 112 | -------------------------------------------------------------------------------- /werk-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "werk-cli" 3 | build = "build.rs" 4 | version.workspace = true 5 | rust-version.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | 9 | [[bin]] 10 | name = "werk" 11 | path = "main.rs" 12 | 13 | [dependencies] 14 | ahash.workspace = true 15 | annotate-snippets = "0.11.5" 16 | anstream.workspace = true 17 | anstyle-query = "1.1.2" 18 | anyhow = "1.0.93" 19 | clap = { version = "4.5.20", features = ["derive", "string"] } 20 | clio = "0.3.5" 21 | indexmap.workspace = true 22 | line-span = "0.1.5" 23 | num_cpus = "1.16.0" 24 | owo-colors = "4.1.0" 25 | parking_lot.workspace = true 26 | shadow-rs = "1.1.1" 27 | smol.workspace = true 28 | thiserror.workspace = true 29 | toml_edit.workspace = true 30 | tracing-subscriber = { version = "0.3.18", features = ["std", "env-filter"] } 31 | tracing.workspace = true 32 | werk-fs.workspace = true 33 | werk-parser.workspace = true 34 | werk-runner.workspace = true 35 | werk-util.workspace = true 36 | anstyle = "1.0.10" 37 | serde.workspace = true 38 | serde_json = "1.0.137" 39 | notify-debouncer-full = "0.5.0" 40 | ctrlc = { version = "3.4.5", features = ["termination"] } 41 | futures.workspace = true 42 | libc = "0.2.169" 43 | clap_complete = { version = "4.5.44", features = ["unstable-dynamic"] } 44 | 45 | [target.'cfg(windows)'.dependencies] 46 | # Needed to get terminal width. 47 | windows-sys = { version = "0.59.0", features = [ 48 | "Win32_System_Console", 49 | "Win32_Foundation", 50 | ] } 51 | 52 | 53 | [build-dependencies] 54 | shadow-rs = "0.38.0" 55 | -------------------------------------------------------------------------------- /werk-cli/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | shadow_rs::ShadowBuilder::builder() 3 | .build_pattern(shadow_rs::BuildPattern::Lazy) 4 | .build() 5 | .unwrap(); 6 | } 7 | -------------------------------------------------------------------------------- /werk-cli/complete.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::{CommandFactory, FromArgMatches}; 4 | use clap_complete::CompletionCandidate; 5 | use werk_fs::Normalize; 6 | use werk_runner::Workspace; 7 | use werk_util::DiagnosticFileId; 8 | 9 | use crate::Args; 10 | use crate::dry_run::DryRun; 11 | use crate::render::ColorOutputKind; 12 | use crate::render::null::NullRender; 13 | use crate::{find_werkfile, get_workspace_dir, get_workspace_settings}; 14 | 15 | fn with_werk(f: impl FnOnce(Workspace) -> Result + 'static) -> T { 16 | let result = (|| -> Result { 17 | let args = std::env::args().skip(2); 18 | let arg_matches = Args::command() 19 | .disable_version_flag(true) 20 | .disable_help_flag(true) 21 | .ignore_errors(true) 22 | .try_get_matches_from(args)?; 23 | let args = Args::from_arg_matches(&arg_matches)?; 24 | 25 | let werkfile = match &args.file { 26 | Some(file) => file.clone().normalize()?, 27 | _ => find_werkfile()?, 28 | }; 29 | 30 | let source_code = std::fs::read_to_string(&werkfile)?; 31 | let ast = werk_parser::parse_werk(&source_code)?; 32 | let config = werk_runner::ir::Defaults::new(&ast, DiagnosticFileId(0))?; 33 | 34 | let io = Arc::new(DryRun::new()); 35 | let renderer = Arc::new(NullRender); 36 | 37 | let workspace_dir = get_workspace_dir(&args, &werkfile)?; 38 | let settings = 39 | get_workspace_settings(&config, &args, &workspace_dir, ColorOutputKind::Never)?; 40 | 41 | let mut workspace = Workspace::new(io, renderer, workspace_dir.into_owned(), &settings)?; 42 | let werkfile_path = workspace.unresolve_path(&werkfile)?; 43 | workspace.add_werkfile_parsed(&werkfile_path, &source_code, ast)?; 44 | 45 | let result = f(workspace)?; 46 | 47 | Ok(result) 48 | })(); 49 | 50 | result.unwrap_or_default() 51 | } 52 | 53 | pub fn targets() -> Vec { 54 | with_werk(|workspace| { 55 | let tasks = workspace 56 | .manifest 57 | .task_recipes 58 | .into_iter() 59 | .map(|(name, recipe)| { 60 | CompletionCandidate::new(name).help(Some(recipe.doc_comment.into())) 61 | }); 62 | let builds = workspace 63 | .manifest 64 | .build_recipes 65 | .into_iter() 66 | .map(|build_recipe| { 67 | CompletionCandidate::new(build_recipe.pattern.to_string()) 68 | .help(Some(build_recipe.doc_comment.into())) 69 | }); 70 | 71 | Ok(tasks.chain(builds).collect()) 72 | }) 73 | } 74 | 75 | pub fn defines() -> Vec { 76 | with_werk(|workspace| { 77 | let defines = workspace 78 | .manifest 79 | .config_variables 80 | .iter() 81 | .map(|(symbol, global)| { 82 | let help = global.value.to_string(); 83 | CompletionCandidate::new(format!("{symbol}=")).help(Some(help.into())) 84 | }); 85 | 86 | Ok(defines.collect()) 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /werk-cli/render.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, sync::Arc}; 2 | 3 | mod ansi; 4 | mod json; 5 | mod log; 6 | pub(crate) mod null; 7 | mod stream; 8 | 9 | pub use ansi::term_width::*; 10 | pub use stream::*; 11 | 12 | use crate::OutputChoice; 13 | 14 | #[derive(Clone, Copy, Debug)] 15 | pub struct OutputSettings { 16 | /// Logging is enabled, so don't try to modify terminal contents in-place. 17 | pub logging_enabled: bool, 18 | pub color: ColorOutputKind, 19 | pub output: OutputChoice, 20 | pub print_recipe_commands: bool, 21 | pub print_fresh: bool, 22 | pub dry_run: bool, 23 | pub quiet: bool, 24 | pub loud: bool, 25 | pub explain: bool, 26 | } 27 | 28 | impl OutputSettings { 29 | pub fn from_args_and_defaults( 30 | args: &crate::Args, 31 | defaults: &werk_runner::ir::Defaults, 32 | color_stderr: ColorOutputKind, 33 | ) -> Self { 34 | let verbose = args.output.verbose | defaults.verbose.unwrap_or(false); 35 | let print_recipe_commands = 36 | verbose | args.output.print_commands | defaults.print_commands.unwrap_or(false); 37 | let print_fresh = verbose | args.output.print_fresh | defaults.print_fresh.unwrap_or(false); 38 | let quiet = !verbose && (args.output.quiet || defaults.quiet.unwrap_or(false)); 39 | let loud = !quiet && (verbose | args.output.loud | defaults.loud.unwrap_or(false)); 40 | let explain = verbose | args.output.explain | defaults.explain.unwrap_or(false); 41 | 42 | Self { 43 | logging_enabled: args.output.log.is_some() || args.list, 44 | color: color_stderr, 45 | output: if args.output.log.is_some() { 46 | OutputChoice::Log 47 | } else { 48 | args.output.output_format 49 | }, 50 | print_recipe_commands, 51 | print_fresh, 52 | dry_run: args.dry_run, 53 | quiet, 54 | loud, 55 | explain, 56 | } 57 | } 58 | } 59 | 60 | pub(crate) struct Bracketed(pub T); 61 | impl Display for Bracketed { 62 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 63 | write!(f, "[{}]", self.0) 64 | } 65 | } 66 | 67 | pub(crate) struct Step(usize, usize); 68 | impl Display for Step { 69 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 70 | write!(f, "{}/{}", self.0, self.1) 71 | } 72 | } 73 | 74 | pub fn make_renderer(settings: OutputSettings) -> Arc { 75 | match settings.output { 76 | OutputChoice::Json => Arc::new(json::JsonWatcher::new()), 77 | OutputChoice::Log => Arc::new(log::LogWatcher::new(settings)), 78 | OutputChoice::Ansi => { 79 | let stderr = AutoStream::new(std::io::stderr(), settings.color); 80 | let must_be_linear = settings.logging_enabled | !stderr.supports_nonlinear_output(); 81 | if must_be_linear { 82 | Arc::new(ansi::TerminalRenderer::::new(settings, stderr)) 83 | } else { 84 | Arc::new(ansi::TerminalRenderer::::new(settings, stderr)) 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /werk-cli/render/ansi/term_width.rs: -------------------------------------------------------------------------------- 1 | use anstream::stream::IsTerminal; 2 | 3 | #[allow(dead_code)] 4 | pub enum TtyWidth { 5 | NoTty, 6 | Guess(usize), 7 | Known(usize), 8 | } 9 | 10 | impl TtyWidth { 11 | pub fn diagnostic_terminal_width(&self) -> Option { 12 | match *self { 13 | TtyWidth::NoTty | TtyWidth::Guess(_) => None, 14 | TtyWidth::Known(width) => Some(width), 15 | } 16 | } 17 | 18 | pub fn progress_max_width(&self) -> Option { 19 | match *self { 20 | TtyWidth::NoTty => None, 21 | TtyWidth::Guess(width) | TtyWidth::Known(width) => Some(width), 22 | } 23 | } 24 | } 25 | 26 | #[inline] 27 | pub fn stderr_width() -> TtyWidth { 28 | if std::io::stderr().is_terminal() { 29 | imp::stderr_width() 30 | } else { 31 | TtyWidth::NoTty 32 | } 33 | } 34 | 35 | #[cfg(unix)] 36 | mod imp { 37 | use super::TtyWidth; 38 | 39 | // Adapted from Cargo's implementation of tty width detection: 40 | // 41 | pub fn stderr_width() -> TtyWidth { 42 | unsafe { 43 | let mut winsize: libc::winsize = std::mem::zeroed(); 44 | // The .into() here is needed for FreeBSD which defines TIOCGWINSZ 45 | // as c_uint but ioctl wants c_ulong. 46 | if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut winsize) < 0 { 47 | return TtyWidth::NoTty; 48 | } 49 | if winsize.ws_col > 0 { 50 | TtyWidth::Known(winsize.ws_col as usize) 51 | } else { 52 | TtyWidth::NoTty 53 | } 54 | } 55 | } 56 | } 57 | 58 | #[cfg(windows)] 59 | mod imp { 60 | use windows_sys::{ 61 | Win32::{ 62 | Foundation::{CloseHandle, GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, 63 | Storage::FileSystem::{CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING}, 64 | System::Console::{ 65 | CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, GetStdHandle, 66 | STD_ERROR_HANDLE, 67 | }, 68 | }, 69 | core::PCSTR, 70 | }; 71 | 72 | use super::TtyWidth; 73 | 74 | // Adapted from Cargo's implementation of tty width detection: 75 | // 76 | pub fn stderr_width() -> TtyWidth { 77 | unsafe { 78 | let stdout = GetStdHandle(STD_ERROR_HANDLE); 79 | let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = std::mem::zeroed(); 80 | if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 { 81 | return TtyWidth::Known((csbi.srWindow.Right - csbi.srWindow.Left) as usize); 82 | } 83 | 84 | // On mintty/msys/cygwin based terminals, the above fails with 85 | // INVALID_HANDLE_VALUE. Use an alternate method which works 86 | // in that case as well. 87 | let h = CreateFileA( 88 | c"CONOUT$".as_ptr() as PCSTR, 89 | GENERIC_READ | GENERIC_WRITE, 90 | FILE_SHARE_READ | FILE_SHARE_WRITE, 91 | std::ptr::null_mut(), 92 | OPEN_EXISTING, 93 | 0, 94 | std::ptr::null_mut(), 95 | ); 96 | if h == INVALID_HANDLE_VALUE { 97 | return TtyWidth::NoTty; 98 | } 99 | 100 | let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = std::mem::zeroed(); 101 | let rc = GetConsoleScreenBufferInfo(h, &mut csbi); 102 | CloseHandle(h); 103 | if rc != 0 { 104 | let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize; 105 | // Unfortunately cygwin/mintty does not set the size of the 106 | // backing console to match the actual window size. This 107 | // always reports a size of 80 or 120 (not sure what 108 | // determines that). Use a conservative max of 60 which should 109 | // work in most circumstances. ConEmu does some magic to 110 | // resize the console correctly, but there's no reasonable way 111 | // to detect which kind of terminal we are running in, or if 112 | // GetConsoleScreenBufferInfo returns accurate information. 113 | return TtyWidth::Guess(std::cmp::min(60, width)); 114 | } 115 | 116 | TtyWidth::NoTty 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /werk-cli/render/log.rs: -------------------------------------------------------------------------------- 1 | use werk_runner::Warning; 2 | 3 | use super::OutputSettings; 4 | 5 | /// Watcher implementation that logs events to the terminal, using `tracing`. 6 | /// 7 | /// Note that logging must be enabled for this to actually do anything. 8 | pub struct LogWatcher { 9 | settings: OutputSettings, 10 | } 11 | 12 | impl LogWatcher { 13 | pub fn new(settings: OutputSettings) -> Self { 14 | Self { settings } 15 | } 16 | } 17 | 18 | impl werk_runner::Render for LogWatcher { 19 | fn will_build( 20 | &self, 21 | task_id: werk_runner::TaskId, 22 | num_steps: usize, 23 | outdatedness: &werk_runner::Outdatedness, 24 | ) { 25 | tracing::info!( 26 | task_id = %task_id, 27 | num_steps = num_steps, 28 | "Will build", 29 | ); 30 | if self.settings.explain { 31 | for reason in &outdatedness.reasons { 32 | tracing::info!(task_id = %task_id, "Reason: {reason}"); 33 | } 34 | } 35 | } 36 | 37 | fn did_build( 38 | &self, 39 | task_id: werk_runner::TaskId, 40 | result: &Result, 41 | ) { 42 | match result { 43 | Ok(status) => { 44 | if let werk_runner::BuildStatus::Complete(task_id, _) = status { 45 | tracing::info!(task_id = %task_id, "Success"); 46 | } 47 | } 48 | Err(err) => { 49 | tracing::error!(task_id = %task_id, "Error: {err}"); 50 | } 51 | } 52 | } 53 | 54 | fn will_execute( 55 | &self, 56 | task_id: werk_runner::TaskId, 57 | command: &werk_runner::ShellCommandLine, 58 | step: usize, 59 | _num_steps: usize, 60 | ) { 61 | if self.settings.print_recipe_commands { 62 | tracing::info!(task_id = %task_id, step = step, "Run: {command}"); 63 | } 64 | } 65 | 66 | fn did_execute( 67 | &self, 68 | task_id: werk_runner::TaskId, 69 | command: &werk_runner::ShellCommandLine, 70 | status: &std::io::Result, 71 | step: usize, 72 | _num_steps: usize, 73 | ) { 74 | match status { 75 | Ok(status) => { 76 | if status.success() { 77 | tracing::info!(task_id = %task_id, step = step, "Success: {command}"); 78 | } else { 79 | tracing::error!(task_id = %task_id, step = step, "Failed: {command}"); 80 | } 81 | } 82 | Err(err) => { 83 | tracing::error!(task_id = %task_id, step = step, "Error: {err}"); 84 | } 85 | } 86 | } 87 | 88 | fn message(&self, task_id: Option, message: &str) { 89 | tracing::info!(task_id = ?task_id, "Message: {message}"); 90 | } 91 | 92 | fn warning(&self, task_id: Option, message: &Warning) { 93 | tracing::warn!(task_id = ?task_id, "Warning: {message}"); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /werk-cli/render/null.rs: -------------------------------------------------------------------------------- 1 | use werk_runner::{BuildStatus, Outdatedness, Render, ShellCommandLine, TaskId}; 2 | 3 | pub struct NullRender; 4 | impl Render for NullRender { 5 | fn will_build(&self, _: TaskId, _: usize, _: &Outdatedness) {} 6 | 7 | fn did_build(&self, _: TaskId, _: &Result) {} 8 | 9 | fn will_execute(&self, _: TaskId, _: &ShellCommandLine, _: usize, _: usize) {} 10 | 11 | fn did_execute( 12 | &self, 13 | _: TaskId, 14 | _: &ShellCommandLine, 15 | _: &Result, 16 | _: usize, 17 | _: usize, 18 | ) { 19 | } 20 | 21 | fn message(&self, _: Option, _: &str) {} 22 | 23 | fn warning(&self, _: Option, _: &werk_runner::Warning) {} 24 | } 25 | -------------------------------------------------------------------------------- /werk-fs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "werk-fs" 3 | version.workspace = true 4 | rust-version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | 8 | [lib] 9 | path = "lib.rs" 10 | 11 | [dependencies] 12 | winnow.workspace = true 13 | thiserror.workspace = true 14 | serde.workspace = true 15 | werk-util.workspace = true 16 | 17 | [lints] 18 | workspace = true 19 | -------------------------------------------------------------------------------- /werk-fs/lib.rs: -------------------------------------------------------------------------------- 1 | mod absolute; 2 | mod path; 3 | mod sym; 4 | mod traits; 5 | 6 | pub use absolute::*; 7 | pub use path::*; 8 | pub use sym::*; 9 | 10 | pub use traits::Normalize; 11 | -------------------------------------------------------------------------------- /werk-fs/sym.rs: -------------------------------------------------------------------------------- 1 | use werk_util::Symbol; 2 | 3 | use crate::{Absolute, Path, PathBuf}; 4 | 5 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 6 | pub struct SymPath(Symbol); 7 | 8 | impl SymPath { 9 | #[must_use] 10 | pub fn new(path: impl AsRef) -> Self { 11 | Self(Symbol::new(path.as_ref().as_str())) 12 | } 13 | 14 | #[inline] 15 | #[must_use] 16 | pub fn as_path(&self) -> &'static Path { 17 | Path::new_unchecked(self.0.as_str()) 18 | } 19 | 20 | #[inline] 21 | #[must_use] 22 | pub fn as_str(&self) -> &'static str { 23 | self.0.as_str() 24 | } 25 | } 26 | 27 | impl AsRef for SymPath { 28 | #[inline] 29 | fn as_ref(&self) -> &Path { 30 | self.as_path() 31 | } 32 | } 33 | 34 | impl std::fmt::Debug for SymPath { 35 | #[inline] 36 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 37 | std::fmt::Debug::fmt(self.as_path(), f) 38 | } 39 | } 40 | 41 | impl std::fmt::Display for SymPath { 42 | #[inline] 43 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 44 | std::fmt::Display::fmt(self.as_path(), f) 45 | } 46 | } 47 | 48 | impl Absolute { 49 | #[must_use] 50 | pub fn symbolicate(path: impl AsRef>) -> Self { 51 | Absolute::new_unchecked(SymPath::new(&**path.as_ref())) 52 | } 53 | 54 | #[inline] 55 | #[must_use] 56 | pub fn as_path(&self) -> &'static Absolute { 57 | Absolute::new_ref_unchecked(self.as_inner().as_path()) 58 | } 59 | } 60 | 61 | impl From<&Absolute> for Absolute { 62 | #[inline] 63 | fn from(path: &Absolute) -> Self { 64 | Self::symbolicate(path) 65 | } 66 | } 67 | 68 | impl From> for Absolute { 69 | #[inline] 70 | fn from(path: Absolute) -> Self { 71 | Self::symbolicate(path) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /werk-parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "werk-parser" 3 | version.workspace = true 4 | rust-version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | 8 | [lib] 9 | path = "lib.rs" 10 | 11 | [[test]] 12 | name = "test_cases" 13 | 14 | [dependencies] 15 | indexmap.workspace = true 16 | thiserror.workspace = true 17 | unicode-ident = "1.0.13" 18 | winnow.workspace = true 19 | regex.workspace = true 20 | annotate-snippets.workspace = true 21 | serde.workspace = true 22 | werk-util.workspace = true 23 | 24 | [dev-dependencies] 25 | anstream.workspace = true 26 | serde_json = "1.0.137" 27 | 28 | [lints] 29 | workspace = true 30 | -------------------------------------------------------------------------------- /werk-parser/ast/token.rs: -------------------------------------------------------------------------------- 1 | use winnow::Parser as _; 2 | 3 | use werk_util::{Offset, Span, Spanned}; 4 | 5 | use crate::{ 6 | Failure, 7 | parser::{Input, PResult, Parse, Parser as _}, 8 | }; 9 | 10 | #[derive(Clone, Copy, Default, PartialEq)] 11 | pub struct Token(pub Offset); 12 | impl Token { 13 | #[inline] 14 | #[must_use] 15 | pub const fn with_span(span: Span) -> Self { 16 | Self(span.start) 17 | } 18 | 19 | #[must_use] 20 | pub const fn ignore() -> Self { 21 | Self(Offset::ignore()) 22 | } 23 | } 24 | 25 | impl std::fmt::Debug for Token { 26 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 27 | write!(f, "'{CHAR}' {:?}", self.0) 28 | } 29 | } 30 | 31 | impl Spanned for Token { 32 | #[inline] 33 | fn span(&self) -> Span { 34 | Span { 35 | start: self.0, 36 | end: Offset(self.0.0 + 1), 37 | } 38 | } 39 | } 40 | 41 | impl Parse for Token { 42 | fn parse(input: &mut Input) -> PResult { 43 | CHAR.or_fail(Failure::ExpectedChar(CHAR)) 44 | .token_span() 45 | .map(Token::with_span) 46 | .parse_next(input) 47 | } 48 | } 49 | 50 | macro_rules! def_token { 51 | ($t:ident, $s:literal) => { 52 | #[doc = concat!("`", $s, "`")] 53 | pub type $t = Token<$s>; 54 | }; 55 | } 56 | 57 | def_token!(Colon, ':'); 58 | def_token!(Eq, '='); 59 | def_token!(Comma, ','); 60 | def_token!(Semicolon, ';'); 61 | def_token!(BraceOpen, '{'); 62 | def_token!(BraceClose, '}'); 63 | def_token!(ParenOpen, '('); 64 | def_token!(ParenClose, ')'); 65 | def_token!(BracketOpen, '['); 66 | def_token!(BracketClose, ']'); 67 | def_token!(LessThan, '<'); 68 | def_token!(GreaterThan, '>'); 69 | def_token!(DoubleQuote, '"'); 70 | def_token!(Percent, '%'); 71 | def_token!(Pipe, '|'); 72 | -------------------------------------------------------------------------------- /werk-parser/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::cast_possible_truncation)] 2 | 3 | pub mod ast; 4 | mod error; 5 | pub mod parser; 6 | mod pattern; 7 | 8 | pub use error::*; 9 | pub use parser::{parse_werk, parse_werk_with_diagnostics}; 10 | pub use pattern::*; 11 | 12 | pub fn extract_doc_comment( 13 | source_map: &S, 14 | file: werk_util::DiagnosticFileId, 15 | whitespace: ast::Whitespace, 16 | ) -> &str { 17 | let range: std::ops::Range = whitespace.0.into(); 18 | source_map 19 | .get_source(file) 20 | .and_then(|source| source.source.get(range)) 21 | .and_then(|s| s.trim().lines().map(str::trim).next()) 22 | .unwrap_or("") 23 | } 24 | 25 | pub fn extract_whitespace( 26 | source_map: &S, 27 | file: werk_util::DiagnosticFileId, 28 | whitespace: ast::Whitespace, 29 | ) -> &str { 30 | let range: std::ops::Range = whitespace.0.into(); 31 | source_map 32 | .get_source(file) 33 | .and_then(|source| source.source.get(range)) 34 | .unwrap_or("") 35 | } 36 | -------------------------------------------------------------------------------- /werk-parser/pattern.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 2 | pub struct Pattern { 3 | fragments: Vec, 4 | } 5 | 6 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 7 | pub enum PatternFragment { 8 | Literal(String), 9 | Placeholder(String), 10 | OneOf(Vec), 11 | } 12 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/build_ident_name.txt: -------------------------------------------------------------------------------- 1 | error[P1001]: parse error 2 | --> INPUT:1:1 3 | | 4 | 1 | build foo {} 5 | | - ^ expected pattern literal 6 | | | 7 | | info: while parsing build recipe 8 | | 9 | = help: `build` must be followed by a pattern literal 10 | = help: use string interpolation to use variables in recipe names 11 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/build_ident_name.werk: -------------------------------------------------------------------------------- 1 | build foo {} 2 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/default_invalid_value.txt: -------------------------------------------------------------------------------- 1 | error[P1001]: parse error 2 | --> INPUT:1:18 3 | | 4 | 1 | default target = hello 5 | | ^ expected string literal 6 | | 7 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/default_invalid_value.werk: -------------------------------------------------------------------------------- 1 | default target = hello 2 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/default_unknown_key.txt: -------------------------------------------------------------------------------- 1 | error[P1001]: parse error 2 | --> INPUT:1:9 3 | | 4 | 1 | default foo = "..." 5 | | ^ expected valid key for `default` statement 6 | | 7 | = help: one of `target`, `out-dir`, `print-commands`, `print-fresh`, `quiet`, `loud`, `explain`, `verbose`, `watch-delay`, `jobs`, or `edition` 8 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/default_unknown_key.werk: -------------------------------------------------------------------------------- 1 | default foo = "..." 2 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/expr_trailing_pipe.txt: -------------------------------------------------------------------------------- 1 | error[P1001]: parse error 2 | --> INPUT:1:1 3 | | 4 | 1 | let foo = bar | 5 | | - ^ expected a chaining expression 6 | | | 7 | | info: while parsing `let` statement 8 | | 9 | = help: one of `join`, `flatten`, `map`, `match`, `env`, `glob`, `which`, `shell`, a string, or a sub-expression in parentheses 10 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/expr_trailing_pipe.werk: -------------------------------------------------------------------------------- 1 | let foo = bar | 2 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/invalid_escape.txt: -------------------------------------------------------------------------------- 1 | error[P1003]: parse error 2 | --> INPUT:1:9 3 | | 4 | 1 | let a = "\p" 5 | | - - ^ invalid escape sequence: 'p' 6 | | | | 7 | | | info: while parsing string literal 8 | | info: while parsing `let` statement 9 | | 10 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/invalid_escape.werk: -------------------------------------------------------------------------------- 1 | let a = "\p" 2 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/let_no_eq.txt: -------------------------------------------------------------------------------- 1 | error[P1005]: parse error 2 | --> INPUT:1:1 3 | | 4 | 1 | let hello 5 | | - ^ expected character = 6 | | | 7 | | info: while parsing `let` statement 8 | | 9 | = help: `let ` must be followed by a `=` 10 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/let_no_eq.werk: -------------------------------------------------------------------------------- 1 | let hello 2 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/let_no_ident.txt: -------------------------------------------------------------------------------- 1 | error[P1001]: parse error 2 | --> INPUT:1:1 3 | | 4 | 1 | let = "hello" 5 | | - ^ expected identifier 6 | | | 7 | | info: while parsing `let` statement 8 | | 9 | = help: `let` must be followed by an identifier 10 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/let_no_ident.werk: -------------------------------------------------------------------------------- 1 | let = "hello" 2 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/let_no_value.txt: -------------------------------------------------------------------------------- 1 | error[P1001]: parse error 2 | --> INPUT:1:1 3 | | 4 | 1 | let foo = 5 | | - ^ expected expression 6 | | | 7 | | info: while parsing `let` statement 8 | | 9 | = help: expressions must start with a value, or an `env`, `glob`, `which`, or `shell` operation 10 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/let_no_value.werk: -------------------------------------------------------------------------------- 1 | let foo = 2 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/match_no_arrow.txt: -------------------------------------------------------------------------------- 1 | error[P1002]: parse error 2 | --> INPUT:1:17 3 | | 4 | 1 | let foo = bar | match { "foo" } 5 | | - - ^ expected keyword `=>` 6 | | | | 7 | | | info: while parsing match 8 | | info: while parsing `let` statement 9 | | 10 | = help: pattern must be followed by `=>` in `match` 11 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/match_no_arrow.werk: -------------------------------------------------------------------------------- 1 | let foo = bar | match { "foo" } 2 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/match_unterminated.txt: -------------------------------------------------------------------------------- 1 | error[P1001]: parse error 2 | --> INPUT:1:17 3 | | 4 | 1 | let foo = bar | match { 5 | | - - ^ expected pattern literal 6 | | | | 7 | | | info: while parsing match 8 | | info: while parsing `let` statement 9 | | 10 | = help: `match` arm must start with a pattern 11 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/match_unterminated.werk: -------------------------------------------------------------------------------- 1 | let foo = bar | match { 2 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/root_invalid.txt: -------------------------------------------------------------------------------- 1 | error[P1001]: parse error 2 | --> INPUT:2:1 3 | | 4 | 2 | foo 5 | | ^ expected statement 6 | | 7 | = help: one of `default`, `let`, `task`, or `build` 8 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/root_invalid.werk: -------------------------------------------------------------------------------- 1 | let valid = "hello" 2 | foo 3 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/spawn_in_build_recipe.txt: -------------------------------------------------------------------------------- 1 | error[P1001]: parse error 2 | --> INPUT:1:1 3 | | 4 | 1 | build "foo" { 5 | | - info: while parsing build recipe 6 | 2 | spawn "bar" 7 | | ^ expected build recipe statement 8 | | 9 | = help: could be one of `let`, `from`, `build`, `depfile`, `run`, or `echo` statement 10 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/spawn_in_build_recipe.werk: -------------------------------------------------------------------------------- 1 | build "foo" { 2 | spawn "bar" 3 | } 4 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/spawn_in_build_recipe_run_block.txt: -------------------------------------------------------------------------------- 1 | error[P2001]: parse error 2 | --> INPUT:2:5 3 | | 4 | 1 | build "foo" { 5 | | - info: while parsing build recipe 6 | 2 | run { 7 | | - info: while parsing run 8 | 3 | spawn "bar" 9 | | ^ unexpected spawn statement 10 | | 11 | = help: `spawn` is only allowed in `task` recipes 12 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/spawn_in_build_recipe_run_block.werk: -------------------------------------------------------------------------------- 1 | build "foo" { 2 | run { 3 | spawn "bar" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/task_string_name.txt: -------------------------------------------------------------------------------- 1 | error[P1001]: parse error 2 | --> INPUT:1:1 3 | | 4 | 1 | task "hello" {} 5 | | - ^ expected identifier 6 | | | 7 | | info: while parsing task recipe 8 | | 9 | = help: `task` must be followed by an identifier 10 | -------------------------------------------------------------------------------- /werk-parser/tests/fail/task_string_name.werk: -------------------------------------------------------------------------------- 1 | task "hello" {} 2 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/c.werk: -------------------------------------------------------------------------------- 1 | default out-dir = "../../target/examples/c" 2 | default target = "build" 3 | 4 | # Path to the C compiler. 5 | let cc = which "clang" 6 | let ld = cc 7 | # "debug" or "release" 8 | let profile = "debug" 9 | let executable = "{profile}/example{EXE_SUFFIX}" 10 | let cflags = profile | match { 11 | "debug" => ["-g", "-O0", "-fdiagnostics-color=always", "-fcolor-diagnostics", "-fansi-escape-codes"] 12 | "release" => ["-O3", "-fdiagnostics-color=always", "-fcolor-diagnostics", "-fansi-escape-codes"] 13 | "%" => error "Unknown profile '{}'; valid options are 'debug' and 'release'" 14 | } 15 | 16 | # Build an object file from a C source file. 17 | build "{profile}/%.o" { 18 | from "{%}.c" 19 | depfile "/{profile}/{%}.c.d" 20 | run "{cc} -c {cflags*} -o " 21 | } 22 | 23 | # Build the depfile for an object file. 24 | build "{profile}/%.c.d" { 25 | from "{%}.c" 26 | run "{cc} -MM -MT -MF " 27 | } 28 | 29 | # Build the executable. 30 | build "{executable}" { 31 | from glob "*.c" | match { 32 | "%.c" => "/{profile}{%}.o" 33 | } 34 | run "{ld} -o " 35 | } 36 | 37 | # Build the executable (shorthand). 38 | task build { 39 | build executable 40 | info "Build done for profile '{profile}'" 41 | } 42 | 43 | # Build and run the executable. 44 | task run { 45 | build "build" 46 | run "" 47 | } 48 | 49 | task clean { 50 | let object-files = glob "*.c" | map "{profile}{:.c=.o}" 51 | run { 52 | delete object-files 53 | delete executable 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Default": { 4 | "OutDir": "../../target/examples/c" 5 | } 6 | }, 7 | { 8 | "Default": { 9 | "Target": "build" 10 | } 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/config.werk: -------------------------------------------------------------------------------- 1 | default out-dir = "../../target/examples/c" 2 | default target = "build" 3 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/expr_parens.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Let": { 4 | "ident": "paths", 5 | "expr": { 6 | "type": "List", 7 | "value": { 8 | "items": [ 9 | { 10 | "expr": { 11 | "type": "StringExpr", 12 | "value": "a" 13 | }, 14 | "ops": [] 15 | }, 16 | { 17 | "expr": { 18 | "type": "StringExpr", 19 | "value": "b" 20 | }, 21 | "ops": [] 22 | } 23 | ] 24 | } 25 | }, 26 | "ops": [ 27 | { 28 | "Map": { 29 | "type": "SubExpr", 30 | "value": { 31 | "expr": { 32 | "type": "Env", 33 | "value": "{}" 34 | }, 35 | "ops": [] 36 | } 37 | } 38 | } 39 | ] 40 | } 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/expr_parens.werk: -------------------------------------------------------------------------------- 1 | let paths = ["a", "b"] | map (env "{}") 2 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/let_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Let": { 4 | "ident": "list", 5 | "expr": { 6 | "type": "List", 7 | "value": { 8 | "items": [ 9 | { 10 | "expr": { 11 | "type": "StringExpr", 12 | "value": "a" 13 | }, 14 | "ops": [] 15 | }, 16 | { 17 | "expr": { 18 | "type": "StringExpr", 19 | "value": "b" 20 | }, 21 | "ops": [] 22 | }, 23 | { 24 | "expr": { 25 | "type": "StringExpr", 26 | "value": "c" 27 | }, 28 | "ops": [] 29 | } 30 | ] 31 | } 32 | }, 33 | "ops": [] 34 | } 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/let_list.werk: -------------------------------------------------------------------------------- 1 | let list = ["a", "b", "c"] 2 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/let_map.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Let": { 4 | "ident": "foo", 5 | "expr": { 6 | "type": "StringExpr", 7 | "value": "bar" 8 | }, 9 | "ops": [ 10 | { 11 | "Map": { 12 | "type": "StringExpr", 13 | "value": "{}" 14 | } 15 | } 16 | ] 17 | } 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/let_map.werk: -------------------------------------------------------------------------------- 1 | let foo = "bar" | map "{}" 2 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/let_match.werk: -------------------------------------------------------------------------------- 1 | let cflags = profile | match { 2 | "debug" => ["-g", "-O0", "-fdiagnostics-color=always", "-fcolor-diagnostics", "-fansi-escape-codes"] 3 | "release" => ["-O3", "-fdiagnostics-color=always", "-fcolor-diagnostics", "-fansi-escape-codes"] 4 | "%" => error "Unknown profile '{}'; valid options are 'debug' and 'release'" 5 | } 6 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/let_match_inline.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Let": { 4 | "ident": "foo", 5 | "expr": { 6 | "type": "Ident", 7 | "value": "bar" 8 | }, 9 | "ops": [ 10 | { 11 | "Match": { 12 | "Single": { 13 | "pattern": "%", 14 | "expr": { 15 | "expr": { 16 | "type": "StringExpr", 17 | "value": "hello" 18 | }, 19 | "ops": [] 20 | } 21 | } 22 | } 23 | } 24 | ] 25 | } 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/let_match_inline.werk: -------------------------------------------------------------------------------- 1 | let foo = bar | match "%" => "hello" 2 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/let_simple.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Let": { 4 | "ident": "cc", 5 | "expr": { 6 | "type": "Which", 7 | "value": "clang" 8 | }, 9 | "ops": [] 10 | } 11 | }, 12 | { 13 | "Let": { 14 | "ident": "ld", 15 | "expr": { 16 | "type": "Ident", 17 | "value": "cc" 18 | }, 19 | "ops": [] 20 | } 21 | }, 22 | { 23 | "Let": { 24 | "ident": "profile", 25 | "expr": { 26 | "type": "StringExpr", 27 | "value": "debug" 28 | }, 29 | "ops": [] 30 | } 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/let_simple.werk: -------------------------------------------------------------------------------- 1 | # Path to the C compiler. 2 | let cc = which "clang" 3 | let ld = cc 4 | # "debug" or "release" 5 | let profile = "debug" 6 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/let_simple_interp.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Let": { 4 | "ident": "executable", 5 | "expr": { 6 | "type": "StringExpr", 7 | "value": "{profile}/example{EXE_SUFFIX}" 8 | }, 9 | "ops": [] 10 | } 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/let_simple_interp.werk: -------------------------------------------------------------------------------- 1 | let executable = "{profile}/example{EXE_SUFFIX}" 2 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/spawn.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Task": { 4 | "name": "server", 5 | "body": [ 6 | { 7 | "Spawn": "my-server" 8 | } 9 | ] 10 | } 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /werk-parser/tests/succeed/spawn.werk: -------------------------------------------------------------------------------- 1 | task server { 2 | spawn "my-server" 3 | } 4 | -------------------------------------------------------------------------------- /werk-parser/tests/test_cases.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write as _; 2 | 3 | use werk_parser::*; 4 | use werk_util::{AsDiagnostic as _, DiagnosticSource}; 5 | 6 | fn strip_colors(s: &str) -> String { 7 | let mut buf = Vec::new(); 8 | let mut stream = anstream::StripStream::new(&mut buf); 9 | stream.write_all(s.as_bytes()).unwrap(); 10 | String::from_utf8(buf).unwrap() 11 | } 12 | 13 | fn fix_newlines(s: &str) -> String { 14 | s.replace('\r', "") 15 | } 16 | 17 | macro_rules! error_case { 18 | ($t:ident) => { 19 | #[test] 20 | fn $t() { 21 | let input_path = concat!( 22 | env!("CARGO_MANIFEST_DIR"), 23 | "/tests/fail/", 24 | stringify!($t), 25 | ".werk" 26 | ); 27 | let expected_path = concat!( 28 | env!("CARGO_MANIFEST_DIR"), 29 | "/tests/fail/", 30 | stringify!($t), 31 | ".txt" 32 | ); 33 | 34 | let input = std::fs::read_to_string(input_path).unwrap(); 35 | let expected = std::fs::read_to_string(expected_path).unwrap(); 36 | let Err(err) = parse_werk(&input) else { 37 | panic!("expected error, got Ok") 38 | }; 39 | 40 | let rendered = err 41 | .with_file(werk_util::DiagnosticFileId::default()) 42 | .into_diagnostic_error(DiagnosticSource::new(std::path::Path::new("INPUT"), &input)) 43 | .to_string(); 44 | 45 | let rendered_stripped = fix_newlines(&strip_colors(&rendered)); 46 | let expected_lf = fix_newlines(&expected); 47 | 48 | if rendered_stripped.trim() != expected_lf.trim() { 49 | eprintln!("Error message mismatch!"); 50 | eprintln!("Got:\n{rendered}\n"); 51 | eprintln!("Expected:\n{expected}"); 52 | panic!("Error message mismatch"); 53 | } 54 | } 55 | }; 56 | } 57 | 58 | macro_rules! success_case { 59 | ($t:ident) => { 60 | #[test] 61 | fn $t() { 62 | let input_path = concat!( 63 | env!("CARGO_MANIFEST_DIR"), 64 | "/tests/succeed/", 65 | stringify!($t), 66 | ".werk" 67 | ); 68 | let expected_path = concat!( 69 | env!("CARGO_MANIFEST_DIR"), 70 | "/tests/succeed/", 71 | stringify!($t), 72 | ".json" 73 | ); 74 | 75 | let input = std::fs::read_to_string(input_path).unwrap(); 76 | let expected_json = std::fs::read_to_string(expected_path).unwrap(); 77 | let input = match parse_werk(&input) { 78 | Ok(input) => input, 79 | Err(err) => { 80 | let rendered = err 81 | .with_file(werk_util::DiagnosticFileId::default()) 82 | .into_diagnostic_error(DiagnosticSource::new( 83 | std::path::Path::new("INPUT"), 84 | &input, 85 | )) 86 | .to_string(); 87 | eprintln!("Error message:\n{}", rendered); 88 | panic!("error parsing input"); 89 | } 90 | }; 91 | 92 | let expected_ast = serde_json::from_str::(&expected_json).unwrap(); 93 | 94 | if input != expected_ast { 95 | let input_json = serde_json::to_string(&input).unwrap(); 96 | eprintln!("AST mismatch!"); 97 | // eprintln!("Expected:\n{:?}", expected_ast); 98 | // eprintln!("Got:\n{:?}\n", input.root); 99 | eprintln!("Got:\n{}\n", input_json); 100 | eprintln!("Expected:\n{}", expected_json); 101 | panic!("AST mismatch"); 102 | } 103 | } 104 | }; 105 | } 106 | 107 | error_case!(let_no_ident); 108 | error_case!(let_no_eq); 109 | error_case!(let_no_value); 110 | error_case!(invalid_escape); 111 | error_case!(root_invalid); 112 | error_case!(expr_trailing_pipe); 113 | error_case!(task_string_name); 114 | error_case!(build_ident_name); 115 | error_case!(match_unterminated); 116 | error_case!(match_no_arrow); 117 | error_case!(default_unknown_key); 118 | error_case!(default_invalid_value); 119 | error_case!(spawn_in_build_recipe); 120 | error_case!(spawn_in_build_recipe_run_block); 121 | 122 | success_case!(c); 123 | success_case!(config); 124 | success_case!(let_simple); 125 | success_case!(let_simple_interp); 126 | success_case!(let_match); 127 | success_case!(let_match_inline); 128 | success_case!(let_map); 129 | success_case!(let_list); 130 | success_case!(expr_parens); 131 | success_case!(spawn); 132 | -------------------------------------------------------------------------------- /werk-runner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "werk-runner" 3 | version.workspace = true 4 | rust-version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | 8 | [lib] 9 | path = "lib.rs" 10 | 11 | [dependencies] 12 | werk-parser.workspace = true 13 | werk-fs.workspace = true 14 | thiserror.workspace = true 15 | tracing.workspace = true 16 | parking_lot.workspace = true 17 | indexmap.workspace = true 18 | futures.workspace = true 19 | which = "7.0.0" 20 | globset = "0.4.15" 21 | walkdir = "2.5.0" 22 | ahash = "0.8.11" 23 | ignore = "0.4.23" 24 | regex.workspace = true 25 | regex-syntax.workspace = true 26 | anyhow = "1.0.93" 27 | serde.workspace = true 28 | toml_edit = { workspace = true, features = ["serde"] } 29 | rustc-stable-hash = "0.1.0" 30 | winnow.workspace = true 31 | smol.workspace = true 32 | pin-project-lite = "0.2.16" 33 | memchr = "2.7.4" 34 | annotate-snippets.workspace = true 35 | werk-util.workspace = true 36 | bitflags = "2.8.0" 37 | 38 | [lints] 39 | workspace = true 40 | -------------------------------------------------------------------------------- /werk-runner/cache.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use werk_fs::Absolute; 4 | use werk_util::Symbol; 5 | 6 | /// The contents of `.werk-cache`. 7 | #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] 8 | pub struct WerkCache { 9 | /// Per-build-target caches. 10 | #[serde(default)] 11 | pub build: BTreeMap, TargetOutdatednessCache>, 12 | } 13 | 14 | /// Per-target cache of used outdatedness information. 15 | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 16 | pub struct TargetOutdatednessCache { 17 | /// Hash of the recipe AST. 18 | pub recipe_hash: Hash128, 19 | /// Hash of used glob patterns. 20 | #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] 21 | pub glob: BTreeMap, 22 | /// Hash of resolved binary paths. 23 | #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] 24 | pub which: BTreeMap, 25 | /// Hash of environment variables. 26 | #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] 27 | pub env: BTreeMap, 28 | /// Hash of the definitions (AST expressions) of global variables used. 29 | #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] 30 | pub global: BTreeMap, 31 | /// Hash of `define` variables. 32 | #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] 33 | pub define: BTreeMap, 34 | } 35 | 36 | impl TargetOutdatednessCache { 37 | #[inline] 38 | pub fn is_recipe_outdated(&self, new_hash: Hash128) -> bool { 39 | self.recipe_hash != new_hash 40 | } 41 | 42 | #[inline] 43 | pub fn is_glob_outdated(&self, glob: Symbol, new_hash: Hash128) -> bool { 44 | self.glob 45 | .get(&glob) 46 | .is_some_and(|old_hash| *old_hash != new_hash) 47 | } 48 | 49 | #[inline] 50 | pub fn is_which_outdated(&self, which: Symbol, new_hash: Hash128) -> bool { 51 | self.which 52 | .get(&which) 53 | .is_some_and(|old_hash| *old_hash != new_hash) 54 | } 55 | 56 | #[inline] 57 | pub fn is_env_outdated(&self, env: Symbol, new_hash: Hash128) -> bool { 58 | self.env 59 | .get(&env) 60 | .is_some_and(|old_hash| *old_hash != new_hash) 61 | } 62 | 63 | #[inline] 64 | pub fn is_define_outdated(&self, define: Symbol, new_hash: Hash128) -> bool { 65 | self.define 66 | .get(&define) 67 | .is_none_or(|old_hash| *old_hash != new_hash) 68 | } 69 | 70 | #[inline] 71 | pub fn is_global_outdated(&self, var: Symbol, new_hash: Hash128) -> bool { 72 | self.global 73 | .get(&var) 74 | .is_none_or(|old_hash| *old_hash != new_hash) 75 | } 76 | } 77 | 78 | impl rustc_stable_hash::FromStableHash for Hash128 { 79 | type Hash = rustc_stable_hash::SipHasher128Hash; 80 | 81 | fn from(hash: Self::Hash) -> Self { 82 | let hi = u128::from(hash.0[0]) << 64; 83 | let lo = u128::from(hash.0[1]); 84 | Hash128(hi | lo) 85 | } 86 | } 87 | 88 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 89 | #[repr(transparent)] 90 | pub struct Hash128(pub u128); 91 | impl From for Hash128 { 92 | #[inline] 93 | fn from(n: u128) -> Self { 94 | Hash128(n) 95 | } 96 | } 97 | 98 | impl serde::Serialize for Hash128 { 99 | fn serialize(&self, serializer: S) -> Result 100 | where 101 | S: serde::Serializer, 102 | { 103 | // Serialize as hex string. Also, TOML doesn't support 64-bit integers. 104 | serializer.serialize_str(&format!("{:016x}", self.0)) 105 | } 106 | } 107 | 108 | impl<'de> serde::Deserialize<'de> for Hash128 { 109 | fn deserialize(deserializer: D) -> Result 110 | where 111 | D: serde::Deserializer<'de>, 112 | { 113 | let s = String::deserialize(deserializer)?; 114 | let n = u128::from_str_radix(&s, 16).map_err(serde::de::Error::custom)?; 115 | Ok(Hash128(n)) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /werk-runner/lib.rs: -------------------------------------------------------------------------------- 1 | mod cache; 2 | pub mod depfile; 3 | mod error; 4 | pub mod eval; 5 | mod io; 6 | pub mod ir; 7 | mod outdatedness; 8 | mod pattern; 9 | mod render; 10 | mod runner; 11 | mod scope; 12 | mod shell; 13 | mod value; 14 | mod warning; 15 | mod workspace; 16 | 17 | pub use error::*; 18 | pub use io::*; 19 | pub use outdatedness::*; 20 | pub use pattern::*; 21 | pub use render::*; 22 | pub use runner::*; 23 | pub use scope::*; 24 | pub use shell::*; 25 | pub use value::*; 26 | pub use warning::*; 27 | pub use workspace::*; 28 | 29 | pub use which::Error as WhichError; 30 | 31 | #[doc(no_inline)] 32 | pub use globset; 33 | -------------------------------------------------------------------------------- /werk-runner/pattern.rs: -------------------------------------------------------------------------------- 1 | use werk_util::DiagnosticSpan; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct Pattern { 5 | /// The source span for the pattern. 6 | pub span: DiagnosticSpan, 7 | /// The canonical string representation of the pattern in Werk syntax. 8 | pub string: String, 9 | pub(crate) matcher: PatternMatcher, 10 | } 11 | 12 | #[derive(Debug, Clone)] 13 | pub(crate) enum PatternMatcher { 14 | Literal(String), 15 | Regex(PatternRegex), 16 | } 17 | 18 | #[derive(Debug, Clone, Copy, Default)] 19 | pub(crate) struct PatternRegexCaptures { 20 | pub stem_capture_index: Option, 21 | pub num_normal_capture_groups: usize, 22 | } 23 | 24 | #[derive(Debug, Clone)] 25 | pub(crate) struct PatternRegex { 26 | /// The regular expression used to match this pattern. 27 | pub regex: regex::Regex, 28 | /// Information about the capture groups in the regex. 29 | pub captures: PatternRegexCaptures, 30 | } 31 | 32 | impl PartialEq for Pattern { 33 | #[inline] 34 | fn eq(&self, other: &Self) -> bool { 35 | match (&self.matcher, &other.matcher) { 36 | (PatternMatcher::Literal(lhs), PatternMatcher::Literal(rhs)) => *lhs == *rhs, 37 | (PatternMatcher::Regex(lhs), PatternMatcher::Regex(rhs)) => { 38 | lhs.regex.as_str() == rhs.regex.as_str() 39 | } 40 | _ => false, 41 | } 42 | } 43 | } 44 | 45 | #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] 46 | pub struct PatternMatchData { 47 | /// The matched stem, if the pattern has a stem. 48 | pub stem: Option>, 49 | /// One entry for each `OneOf` capture group `(a|b|...)` in the pattern. 50 | pub captures: Box<[Box]>, 51 | } 52 | 53 | impl PatternMatchData { 54 | pub fn new( 55 | stem: Option>>, 56 | captures: impl IntoIterator>>, 57 | ) -> Self { 58 | Self { 59 | stem: stem.map(Into::into), 60 | captures: captures.into_iter().map(Into::into).collect(), 61 | } 62 | } 63 | } 64 | 65 | impl Pattern { 66 | #[must_use] 67 | pub fn match_whole_string(&self, string: &str) -> Option { 68 | match self.matcher { 69 | PatternMatcher::Literal(ref needle) => { 70 | if string == needle { 71 | Some(PatternMatchData { 72 | stem: None, 73 | captures: Box::default(), 74 | }) 75 | } else { 76 | None 77 | } 78 | } 79 | PatternMatcher::Regex(ref regex) => { 80 | let m = regex.regex.captures(string)?; 81 | let mut capture_groups = 82 | Vec::with_capacity(regex.captures.num_normal_capture_groups); 83 | let mut stem = None; 84 | 85 | let mut group_matches = m.iter(); 86 | // Skip the implicit whole-string match group. 87 | group_matches.next().unwrap(); 88 | 89 | for (index, group) in group_matches.enumerate() { 90 | let group_str = group.unwrap().as_str(); 91 | if regex.captures.stem_capture_index == Some(index) { 92 | stem = Some(group_str); 93 | } else { 94 | capture_groups.push(group_str); 95 | } 96 | } 97 | 98 | Some(PatternMatchData::new(stem, capture_groups)) 99 | } 100 | } 101 | } 102 | 103 | #[must_use] 104 | pub fn match_whole_path(&self, path: &werk_fs::Path) -> Option { 105 | tracing::trace!("Matching '{path}' against {:?}", self.matcher); 106 | self.match_whole_string(path.as_str()) 107 | } 108 | } 109 | 110 | impl std::fmt::Display for Pattern { 111 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 112 | f.write_str(&self.string) 113 | } 114 | } 115 | 116 | impl PatternMatchData { 117 | /// True if the pattern did not contain a stem. 118 | #[inline] 119 | #[must_use] 120 | pub fn is_verbatim(&self) -> bool { 121 | self.stem.is_none() 122 | } 123 | 124 | #[inline] 125 | #[must_use] 126 | pub fn stem(&self) -> Option<&str> { 127 | self.stem.as_deref() 128 | } 129 | 130 | #[inline] 131 | #[must_use] 132 | pub fn captures(&self) -> &[Box] { 133 | &self.captures 134 | } 135 | 136 | #[inline] 137 | #[must_use] 138 | pub fn capture_group(&self, group: usize) -> Option<&str> { 139 | self.captures.get(group).map(|s| &**s) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /werk-runner/render.rs: -------------------------------------------------------------------------------- 1 | use werk_util::DiagnosticFileId; 2 | 3 | use crate::{BuildStatus, Error, Outdatedness, ShellCommandLine, TaskId, Warning}; 4 | 5 | pub trait Render: Send + Sync { 6 | /// Build task is about to start. 7 | fn will_build(&self, task_id: TaskId, num_steps: usize, outdatedness: &Outdatedness); 8 | 9 | /// Build task finished (all steps have been completed). 10 | fn did_build(&self, task_id: TaskId, result: &Result); 11 | /// Run command is about to be executed. 12 | fn will_execute( 13 | &self, 14 | task_id: TaskId, 15 | command: &ShellCommandLine, 16 | step: usize, 17 | num_steps: usize, 18 | ); 19 | 20 | fn on_child_process_stderr_line( 21 | &self, 22 | task_id: TaskId, 23 | command: &ShellCommandLine, 24 | line_without_eol: &[u8], 25 | quiet: bool, 26 | ) { 27 | _ = (task_id, command, line_without_eol, quiet); 28 | } 29 | 30 | fn on_child_process_stdout_line( 31 | &self, 32 | task_id: TaskId, 33 | command: &ShellCommandLine, 34 | line_without_eol: &[u8], 35 | ) { 36 | _ = (task_id, command, line_without_eol); 37 | } 38 | 39 | /// Run command is finished executing, or failed to start. Note that 40 | /// `result` will be `Ok` even if the command returned an error, allowing 41 | /// access to the command's stdout/stderr. 42 | /// 43 | /// The runner guarantees that if an `Ok(output)` is passed to this 44 | /// function, 45 | fn did_execute( 46 | &self, 47 | task_id: TaskId, 48 | command: &ShellCommandLine, 49 | status: &std::io::Result, 50 | step: usize, 51 | num_steps: usize, 52 | ); 53 | 54 | /// Emit a message from the user, typically from the `info` expression in 55 | /// the manifest. 56 | fn message(&self, task_id: Option, message: &str); 57 | 58 | /// Emit a warning from the user, typically from the `warn` expression in 59 | /// the manifest. 60 | fn warning(&self, task_id: Option, warning: &Warning); 61 | 62 | /// Emit an informational message from the runtime, typically the `werk` 63 | /// binary wants to tell the user about something that happened. 64 | /// 65 | /// For example, this is used by the `--watch` flag to tell the user what's 66 | /// happening. 67 | fn runner_message(&self, message: &str) { 68 | _ = message; 69 | } 70 | 71 | /// Reset the renderer. This is called between iterations in `--watch` to 72 | /// reset the render state between runs. For example, if the renderer is 73 | /// debouncing warnings, this might cause warnings to be emitted again. 74 | /// 75 | /// This should also clear the renderer's map of source files. 76 | fn reset(&self) {} 77 | 78 | /// When the renderer is providing friendly diagnostics to the user, this 79 | /// informs the renderer that a source file was added. The expectation is 80 | /// that the renderer keeps a map of source files that it uses when 81 | /// rendering diagnostics. 82 | /// 83 | /// This is called by `Workspace::new()` as source files are discovered. 84 | fn add_source_file(&self, id: DiagnosticFileId, path: &str, source: &str) { 85 | _ = (id, path, source); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /werk-runner/runner/command.rs: -------------------------------------------------------------------------------- 1 | use werk_fs::Absolute; 2 | use werk_util::DiagnosticSpan; 3 | 4 | use crate::ShellCommandLine; 5 | 6 | #[derive(Debug, Clone, PartialEq)] 7 | pub enum RunCommand { 8 | Shell(ShellCommandLine), 9 | Spawn(ShellCommandLine), 10 | Write(Absolute, Vec), 11 | // We don't know yet if the source file is in the workspace or output 12 | // directory, so we will resolve the path when running it. 13 | Copy(Absolute, Absolute), 14 | Info(DiagnosticSpan, String), 15 | Warn(DiagnosticSpan, String), 16 | // Path is always in the output directory. They don't need to exist. 17 | Delete(DiagnosticSpan, Vec>), 18 | Touch(DiagnosticSpan, Vec>), 19 | SetCapture(bool), 20 | SetEnv(String, String), 21 | RemoveEnv(String), 22 | } 23 | 24 | impl std::fmt::Display for RunCommand { 25 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 | match self { 27 | RunCommand::Shell(shell_command_line) => shell_command_line.fmt(f), 28 | RunCommand::Spawn(shell_command_line) => write!(f, "spawn {shell_command_line}"), 29 | RunCommand::Write(path_buf, vec) => { 30 | write!(f, "write {} ({} bytes)", path_buf.display(), vec.len()) 31 | } 32 | RunCommand::Copy(from, to) => { 33 | write!(f, "copy '{}' to '{}'", from, to.display()) 34 | } 35 | RunCommand::Info(_, message) => { 36 | write!(f, "info \"{}\"", message.escape_default()) 37 | } 38 | RunCommand::Warn(_, message) => { 39 | write!(f, "warn \"{}\"", message.escape_default()) 40 | } 41 | RunCommand::Delete(_, paths) => { 42 | write!(f, "delete ")?; 43 | if paths.len() == 1 { 44 | write!(f, "{}", paths[0].display()) 45 | } else { 46 | write!(f, "[")?; 47 | for (i, p) in paths.iter().enumerate() { 48 | if i != 0 { 49 | write!(f, ", ")?; 50 | } 51 | write!(f, "{}", p.display())?; 52 | } 53 | write!(f, "]") 54 | } 55 | } 56 | RunCommand::Touch(_, paths) => { 57 | write!(f, "touch ")?; 58 | if paths.len() == 1 { 59 | write!(f, "{}", paths[0].display()) 60 | } else { 61 | write!(f, "[")?; 62 | for (i, p) in paths.iter().enumerate() { 63 | if i != 0 { 64 | write!(f, ", ")?; 65 | } 66 | write!(f, "{}", p.display())?; 67 | } 68 | write!(f, "]") 69 | } 70 | } 71 | RunCommand::SetCapture(value) => write!(f, "set_capture = {value}"), 72 | RunCommand::SetEnv(key, value) => write!(f, "env {key} = {value}"), 73 | RunCommand::RemoveEnv(key) => write!(f, "env-remove {key}"), 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /werk-runner/runner/dep_chain.rs: -------------------------------------------------------------------------------- 1 | use super::TaskId; 2 | 3 | #[derive(Debug, Clone, Copy)] 4 | pub struct DepChainEntry<'a> { 5 | parent: DepChain<'a>, 6 | this: TaskId, 7 | } 8 | 9 | #[derive(Debug, Clone, Copy)] 10 | pub enum DepChain<'a> { 11 | Empty, 12 | Owned(&'a OwnedDependencyChain), 13 | Ref(&'a DepChainEntry<'a>), 14 | } 15 | 16 | impl<'a> DepChain<'a> { 17 | fn collect_vec(&self) -> Vec { 18 | match self { 19 | DepChain::Empty => Vec::new(), 20 | DepChain::Owned(owned) => owned.vec.clone(), 21 | DepChain::Ref(parent) => parent.collect_vec(), 22 | } 23 | } 24 | 25 | #[must_use] 26 | pub fn contains(&self, task: TaskId) -> bool { 27 | match self { 28 | DepChain::Empty => false, 29 | DepChain::Owned(owned) => owned.vec.contains(&task), 30 | DepChain::Ref(parent) => parent.contains(task), 31 | } 32 | } 33 | 34 | #[must_use] 35 | pub fn push<'b>(self, task: TaskId) -> DepChainEntry<'b> 36 | where 37 | 'a: 'b, 38 | { 39 | DepChainEntry { 40 | parent: self, 41 | this: task, 42 | } 43 | } 44 | } 45 | 46 | impl DepChainEntry<'_> { 47 | #[must_use] 48 | pub fn collect(&self) -> OwnedDependencyChain { 49 | OwnedDependencyChain { 50 | vec: self.collect_vec(), 51 | } 52 | } 53 | 54 | #[must_use] 55 | pub fn collect_vec(&self) -> Vec { 56 | let mut vec = self.parent.collect_vec(); 57 | vec.push(self.this); 58 | vec 59 | } 60 | 61 | #[must_use] 62 | pub fn contains(&self, task_id: TaskId) -> bool { 63 | if self.this == task_id { 64 | true 65 | } else { 66 | self.parent.contains(task_id) 67 | } 68 | } 69 | } 70 | 71 | #[derive(Debug, Clone, PartialEq, Eq)] 72 | pub struct OwnedDependencyChain { 73 | vec: Vec, 74 | } 75 | 76 | impl OwnedDependencyChain { 77 | #[inline] 78 | #[must_use] 79 | pub fn into_inner(self) -> Vec { 80 | self.vec 81 | } 82 | } 83 | 84 | impl std::fmt::Display for OwnedDependencyChain { 85 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 86 | for (i, task_id) in self.vec.iter().enumerate() { 87 | if i > 0 { 88 | write!(f, " -> ")?; 89 | } 90 | write!(f, "{task_id}")?; 91 | } 92 | Ok(()) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /werk-runner/runner/task.rs: -------------------------------------------------------------------------------- 1 | use futures::channel::oneshot; 2 | use werk_fs::{Absolute, SymPath}; 3 | use werk_util::Symbol; 4 | 5 | use crate::{Error, ir}; 6 | 7 | use super::BuildStatus; 8 | 9 | pub enum TaskStatus { 10 | Built(Result), 11 | Pending(Vec>>), 12 | } 13 | 14 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 15 | pub enum TaskId { 16 | Task(Symbol), 17 | // TODO: When recipes can build multiple files, this needs to change to some 18 | // ID that encapsulates the "recipe instance" rather than the path of a 19 | // single target. 20 | Build(Absolute), 21 | } 22 | 23 | impl TaskId { 24 | pub fn command(s: impl Into) -> Self { 25 | let name = s.into(); 26 | debug_assert!(!name.as_str().starts_with('/')); 27 | TaskId::Task(name) 28 | } 29 | 30 | pub fn build(p: impl AsRef>) -> Self { 31 | TaskId::Build(Absolute::symbolicate(p)) 32 | } 33 | 34 | pub fn try_build

(p: P) -> Result 35 | where 36 | P: TryInto>, 37 | { 38 | let path = p.try_into()?; 39 | Ok(TaskId::build(path)) 40 | } 41 | 42 | #[inline] 43 | #[must_use] 44 | pub fn is_command(&self) -> bool { 45 | matches!(self, TaskId::Task(_)) 46 | } 47 | 48 | #[inline] 49 | #[must_use] 50 | pub fn as_str(&self) -> &'static str { 51 | match self { 52 | TaskId::Task(task) => task.as_str(), 53 | TaskId::Build(build) => build.as_inner().as_str(), 54 | } 55 | } 56 | 57 | #[inline] 58 | #[must_use] 59 | pub fn as_path(&self) -> Option<&Absolute> { 60 | if let TaskId::Build(build) = self { 61 | Some(build.as_path()) 62 | } else { 63 | None 64 | } 65 | } 66 | 67 | #[inline] 68 | #[must_use] 69 | pub fn short_name(&self) -> &'static str { 70 | match self { 71 | TaskId::Task(task) => task.as_str(), 72 | TaskId::Build(path) => { 73 | let Some((_prefix, filename)) = path 74 | .as_inner() 75 | .as_str() 76 | .rsplit_once(werk_fs::Path::SEPARATOR) 77 | else { 78 | // The path is absolute. 79 | unreachable!() 80 | }; 81 | filename 82 | } 83 | } 84 | } 85 | } 86 | 87 | impl std::fmt::Display for TaskId { 88 | #[inline] 89 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 90 | f.write_str(self.as_str()) 91 | } 92 | } 93 | 94 | pub enum TaskSpec<'a> { 95 | Recipe(ir::RecipeMatch<'a>), 96 | CheckExists(Absolute), 97 | /// Check if the file exists, but don't emit an error if it doesn't. This 98 | /// applies to dependencies discovered through depfiles, where the depfile 99 | /// may be outdated (from a previous build). 100 | /// 101 | /// If the file does not exist, the task will be considered outdated. 102 | CheckExistsRelaxed(Absolute), 103 | } 104 | 105 | impl TaskSpec<'_> { 106 | #[must_use] 107 | pub fn to_task_id(&self) -> TaskId { 108 | match self { 109 | TaskSpec::Recipe(ir::RecipeMatch::Build(build_recipe_match)) => { 110 | TaskId::build(build_recipe_match.target_file.clone()) 111 | } 112 | TaskSpec::Recipe(ir::RecipeMatch::Task(task_recipe_match)) => { 113 | TaskId::command(task_recipe_match.name) 114 | } 115 | TaskSpec::CheckExists(path_buf) | TaskSpec::CheckExistsRelaxed(path_buf) => { 116 | TaskId::build(path_buf.clone().into_boxed_path()) 117 | } 118 | } 119 | } 120 | } 121 | 122 | pub enum DepfileSpec<'a> { 123 | /// The depfile is explicitly generated by a recipe. 124 | Recipe(ir::BuildRecipeMatch<'a>), 125 | /// The depfile is implicitly generated by the associated command recipe. 126 | ImplicitlyGenerated(Absolute), 127 | } 128 | -------------------------------------------------------------------------------- /werk-runner/shell.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{BTreeMap, BTreeSet}, 3 | ffi::{OsStr, OsString}, 4 | }; 5 | 6 | use werk_fs::Absolute; 7 | 8 | #[derive(Clone, PartialEq)] 9 | pub struct ShellCommandLine { 10 | /// The name of the program to run. Should be an absolute path, either from 11 | /// a `which` expression or an `` interpolation when running an 12 | /// executable produced by another recipe. 13 | pub program: Absolute, 14 | pub arguments: Vec, 15 | } 16 | 17 | impl std::fmt::Display for ShellCommandLine { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | write!(f, "{}", self.program.display())?; 20 | for arg in &self.arguments { 21 | write!(f, " {}", werk_util::DisplayArg(arg))?; 22 | } 23 | Ok(()) 24 | } 25 | } 26 | 27 | impl std::fmt::Debug for ShellCommandLine { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | write!(f, "{:?}", self.program)?; 30 | for arg in &self.arguments { 31 | write!(f, " {}", werk_util::DisplayArgQuoted(arg))?; 32 | } 33 | Ok(()) 34 | } 35 | } 36 | 37 | #[derive(Default, Debug, Clone, PartialEq)] 38 | pub struct Env { 39 | pub env: BTreeMap, 40 | pub env_remove: BTreeSet, 41 | } 42 | 43 | impl Env { 44 | pub fn merge_from(&mut self, other: &Self) { 45 | for k in &other.env_remove { 46 | self.env_remove(k); 47 | } 48 | for (k, v) in &other.env { 49 | self.env(k, v); 50 | } 51 | } 52 | 53 | pub fn get(&self, key: impl AsRef) -> Option<&OsString> { 54 | self.env.get(key.as_ref()) 55 | } 56 | 57 | pub fn envs(&mut self, envs: I) -> &mut Self 58 | where 59 | I: IntoIterator, 60 | K: AsRef, 61 | V: AsRef, 62 | { 63 | self.env.extend( 64 | envs.into_iter() 65 | .map(|(k, v)| (k.as_ref().to_os_string(), v.as_ref().to_os_string())), 66 | ); 67 | self 68 | } 69 | 70 | /// Set an environment variable in the child process. 71 | pub fn env(&mut self, key: impl AsRef, value: impl AsRef) -> &mut Self { 72 | let key = key.as_ref(); 73 | self.env 74 | .insert(key.to_os_string(), value.as_ref().to_os_string()); 75 | self.env_remove.remove(key); 76 | self 77 | } 78 | 79 | /// Remove an environment variable from the child process, i.e. make sure 80 | /// that it does not inherit it from the parent process. 81 | pub fn env_remove(&mut self, key: impl AsRef) -> &mut Self { 82 | let key = key.as_ref(); 83 | self.env_remove.insert(key.to_os_string()); 84 | self.env.remove(key); 85 | self 86 | } 87 | 88 | /// Set the `CLICOLOR_FORCE` and `FORCE_COLOR` environment variable for this 89 | /// command. Also clears the `NO_COLOR` environment variable. 90 | pub fn set_force_color(&mut self) -> &mut Self { 91 | // Remove `NO_COLOR` if previously set. 92 | self.env.remove(OsStr::new("NO_COLOR")); 93 | 94 | // Prevent the inherited environment from setting `NO_COLOR`. 95 | self.env_remove 96 | .insert(OsStr::new("NO_COLOR").to_os_string()); 97 | 98 | // Remove earlier disablement of `FORCE_COLOR`. 99 | self.env_remove.remove(OsStr::new("FORCE_COLOR")); 100 | 101 | self.env("FORCE_COLOR", "1"); 102 | self.env("CLICOLOR", "1"); 103 | self.env("CLICOLOR_FORCE", "1"); 104 | self 105 | } 106 | 107 | /// Set the `NO_COLOR` environment variable for this command. Also clears 108 | /// the `CLICOLOR_FORCE` and `CLICOLOR` environment variables. 109 | pub fn set_no_color(&mut self) -> &mut Self { 110 | // Remove enablement from this command if previously set. 111 | self.env.remove(OsStr::new("FORCE_COLOR")); 112 | self.env.remove(OsStr::new("CLICOLOR")); 113 | self.env.remove(OsStr::new("CLICOLOR_FORCE")); 114 | 115 | // Prevent the inherited environment from setting `FORCE_COLOR`. 116 | self.env_remove 117 | .insert(OsStr::new("FORCE_COLOR").to_os_string()); 118 | self.env_remove 119 | .insert(OsStr::new("CLICOLOR").to_os_string()); 120 | self.env_remove 121 | .insert(OsStr::new("CLICOLOR_FORCE").to_os_string()); 122 | 123 | // Remove earlier disablement of `NO_COLOR`. 124 | self.env_remove.remove(OsStr::new("NO_COLOR")); 125 | 126 | self.env("NO_COLOR", "1"); 127 | self 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /werk-util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "werk-util" 3 | version.workspace = true 4 | rust-version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | 8 | [lib] 9 | path = "lib.rs" 10 | 11 | [dependencies] 12 | ahash.workspace = true 13 | hashbrown = "0.15.2" 14 | parking_lot.workspace = true 15 | serde.workspace = true 16 | annotate-snippets.workspace = true 17 | indexmap.workspace = true 18 | 19 | [lints] 20 | workspace = true 21 | -------------------------------------------------------------------------------- /werk-util/cancel.rs: -------------------------------------------------------------------------------- 1 | //! Cancellation signal 2 | 3 | use std::{ 4 | pin::Pin, 5 | sync::{Arc, atomic::AtomicBool}, 6 | task::{Context, Poll, Waker}, 7 | }; 8 | 9 | use parking_lot::Mutex; 10 | 11 | pub struct Sender { 12 | state: Arc, 13 | } 14 | 15 | #[derive(Clone)] 16 | pub struct Receiver { 17 | state: Arc, 18 | } 19 | 20 | struct State { 21 | cancelled: AtomicBool, 22 | wakers: Mutex>, 23 | } 24 | 25 | impl Sender { 26 | #[must_use] 27 | #[inline] 28 | pub fn new() -> Self { 29 | Sender { 30 | state: Arc::new(State { 31 | cancelled: AtomicBool::new(false), 32 | wakers: Mutex::new(Vec::new()), 33 | }), 34 | } 35 | } 36 | 37 | #[inline] 38 | #[must_use] 39 | pub fn receiver(&self) -> Receiver { 40 | Receiver { 41 | state: self.state.clone(), 42 | } 43 | } 44 | 45 | #[inline] 46 | pub fn forget(self) { 47 | // Drop the sender, but keep the receiver alive. 48 | std::mem::forget(self); 49 | } 50 | 51 | #[inline] 52 | pub fn cancel(&self) { 53 | if self 54 | .state 55 | .cancelled 56 | .swap(true, std::sync::atomic::Ordering::SeqCst) 57 | { 58 | return; 59 | } 60 | 61 | let mut wakers = self.state.wakers.lock(); 62 | for waker in wakers.drain(..) { 63 | waker.wake(); 64 | } 65 | } 66 | } 67 | 68 | impl Default for Sender { 69 | #[inline] 70 | fn default() -> Self { 71 | Self::new() 72 | } 73 | } 74 | 75 | impl Drop for Sender { 76 | fn drop(&mut self) { 77 | // Cancel the receiver if the sender is dropped. 78 | self.cancel(); 79 | } 80 | } 81 | 82 | impl Receiver { 83 | fn poll_pinned(&self, cx: &mut Context<'_>) -> Poll<()> { 84 | if self 85 | .state 86 | .cancelled 87 | .load(std::sync::atomic::Ordering::SeqCst) 88 | { 89 | return Poll::Ready(()); 90 | } 91 | 92 | let mut wakers = self.state.wakers.lock(); 93 | wakers.push(cx.waker().clone()); 94 | Poll::Pending 95 | } 96 | } 97 | 98 | impl Future for Receiver { 99 | type Output = (); 100 | 101 | #[inline] 102 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 103 | self.poll_pinned(cx) 104 | } 105 | } 106 | 107 | impl Future for &Receiver { 108 | type Output = (); 109 | 110 | #[inline] 111 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 112 | self.poll_pinned(cx) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /werk-util/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cancel; 2 | mod diagnostic; 3 | mod os_str; 4 | mod semantic_hash; 5 | mod span; 6 | mod symbol; 7 | 8 | pub use diagnostic::*; 9 | pub use os_str::*; 10 | pub use semantic_hash::*; 11 | pub use span::*; 12 | pub use symbol::*; 13 | -------------------------------------------------------------------------------- /werk-util/os_str.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::OsStr, 3 | fmt::{Debug, Write}, 4 | }; 5 | 6 | /// Display an command-line argument, in quotes it if it contains whitespace, 7 | /// quotes, or special characters. 8 | pub struct DisplayArg<'a>(pub &'a str); 9 | 10 | impl std::fmt::Display for DisplayArg<'_> { 11 | #[inline] 12 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 13 | if contains_whitespace_or_control(self.0) { 14 | DisplayArgQuoted(self.0).fmt(f) 15 | } else { 16 | write_escape_control::(self.0, f) 17 | } 18 | } 19 | } 20 | 21 | pub struct DisplayArgQuoted<'a>(pub &'a str); 22 | 23 | impl std::fmt::Display for DisplayArgQuoted<'_> { 24 | #[inline] 25 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 | f.write_char('"')?; 27 | write_escape_control::(self.0, f)?; 28 | f.write_char('"') 29 | } 30 | } 31 | 32 | #[inline] 33 | fn contains_whitespace_or_control(s: &str) -> bool { 34 | s.as_bytes() 35 | .iter() 36 | .any(|&b| b.is_ascii_whitespace() || b.is_ascii_control() || b == b'"') 37 | } 38 | 39 | #[inline] 40 | fn write_escape_control( 41 | s: &str, 42 | f: &mut std::fmt::Formatter, 43 | ) -> std::fmt::Result { 44 | for ch in s.chars() { 45 | if ch.is_ascii_control() { 46 | ch.escape_default().fmt(f)?; 47 | } else if ESCAPE_QUOTES && ch == '"' { 48 | f.write_str("\\\"")?; 49 | } else { 50 | f.write_char(ch)?; 51 | } 52 | } 53 | Ok(()) 54 | } 55 | 56 | #[inline] 57 | #[must_use] 58 | pub fn trim_os_str(s: &OsStr) -> &OsStr { 59 | let bytes = s.as_encoded_bytes().trim_ascii(); 60 | unsafe { 61 | // SAFETY: Trimming ASCII whitespace cannot produce an invalid OsStr 62 | OsStr::from_encoded_bytes_unchecked(bytes) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /werk-util/semantic_hash.rs: -------------------------------------------------------------------------------- 1 | use std::hash::Hash as _; 2 | 3 | /// Same as `std::hash::Hash`, except that it only includes semantically 4 | /// relevant information. For example, for AST nodes the spans are excluded. 5 | pub trait SemanticHash { 6 | fn semantic_hash(&self, state: &mut H); 7 | } 8 | 9 | impl SemanticHash for [T] { 10 | fn semantic_hash(&self, state: &mut H) { 11 | // Would use `std::hash::Hasher::write_length_prefix()`, but that's 12 | // unstable. 13 | state.write_usize(self.len()); 14 | 15 | for item in self { 16 | item.semantic_hash(state); 17 | } 18 | } 19 | } 20 | 21 | impl SemanticHash for Option { 22 | fn semantic_hash(&self, state: &mut H) { 23 | std::mem::discriminant(self).hash(state); 24 | if let Some(value) = self { 25 | value.semantic_hash(state); 26 | } 27 | } 28 | } 29 | 30 | impl SemanticHash for Box { 31 | fn semantic_hash(&self, state: &mut H) { 32 | T::semantic_hash(self, state); 33 | } 34 | } 35 | 36 | impl SemanticHash for &T { 37 | fn semantic_hash(&self, state: &mut H) { 38 | T::semantic_hash(self, state); 39 | } 40 | } 41 | 42 | #[macro_export] 43 | macro_rules! hash_is_semantic { 44 | ($t:tt $(<$($lifetime_param:tt),+>)?) => { 45 | impl $(<$($lifetime_param),*>)* $crate::SemanticHash for $t $(<$($lifetime_param),+>)? { 46 | #[inline] 47 | fn semantic_hash(&self, state: &mut H) { 48 | use std::hash::Hash; 49 | self.hash(state); 50 | } 51 | } 52 | }; 53 | } 54 | 55 | pub(crate) use hash_is_semantic; 56 | 57 | hash_is_semantic!(str); 58 | -------------------------------------------------------------------------------- /werk-vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /werk-vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | .gitignore 4 | vsc-extension-quickstart.md 5 | -------------------------------------------------------------------------------- /werk-vscode/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "werk" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /werk-vscode/README.md: -------------------------------------------------------------------------------- 1 | # werk README 2 | 3 | This is the README for your extension "werk". After writing up a brief description, we recommend including the following sections. 4 | 5 | ## Features 6 | 7 | Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. 8 | 9 | For example if there is an image subfolder under your extension project workspace: 10 | 11 | \!\[feature X\]\(images/feature-x.png\) 12 | 13 | > Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. 14 | 15 | ## Requirements 16 | 17 | If you have any requirements or dependencies, add a section describing those and how to install and configure them. 18 | 19 | ## Extension Settings 20 | 21 | Include if your extension adds any VS Code settings through the `contributes.configuration` extension point. 22 | 23 | For example: 24 | 25 | This extension contributes the following settings: 26 | 27 | * `myExtension.enable`: Enable/disable this extension. 28 | * `myExtension.thing`: Set to `blah` to do something. 29 | 30 | ## Known Issues 31 | 32 | Calling out known issues can help limit users opening duplicate issues against your extension. 33 | 34 | ## Release Notes 35 | 36 | Users appreciate release notes as you update your extension. 37 | 38 | ### 1.0.0 39 | 40 | Initial release of ... 41 | 42 | ### 1.0.1 43 | 44 | Fixed issue #. 45 | 46 | ### 1.1.0 47 | 48 | Added features X, Y, and Z. 49 | 50 | --- 51 | 52 | ## Working with Markdown 53 | 54 | You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: 55 | 56 | * Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux). 57 | * Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux). 58 | * Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets. 59 | 60 | ## For more information 61 | 62 | * [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) 63 | * [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) 64 | 65 | **Enjoy!** 66 | -------------------------------------------------------------------------------- /werk-vscode/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your language does not support line comments 4 | "lineComment": "#", 5 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 6 | // "blockComment": [ "/*", "*/" ] 7 | }, 8 | // symbols used as brackets 9 | "brackets": [ 10 | [ 11 | "{", 12 | "}" 13 | ], 14 | [ 15 | "[", 16 | "]" 17 | ], 18 | [ 19 | "(", 20 | ")" 21 | ], 22 | ], 23 | // symbols that are auto closed when typing 24 | "autoClosingPairs": [ 25 | [ 26 | "{", 27 | "}" 28 | ], 29 | [ 30 | "<", 31 | ">" 32 | ], 33 | [ 34 | "[", 35 | "]" 36 | ], 37 | [ 38 | "(", 39 | ")" 40 | ], 41 | [ 42 | "\"", 43 | "\"" 44 | ], 45 | [ 46 | "'", 47 | "'" 48 | ] 49 | ], 50 | // symbols that can be used to surround a selection 51 | "surroundingPairs": [ 52 | [ 53 | "{", 54 | "}" 55 | ], 56 | [ 57 | "<", 58 | ">" 59 | ], 60 | [ 61 | "[", 62 | "]" 63 | ], 64 | [ 65 | "(", 66 | ")" 67 | ], 68 | [ 69 | "\"", 70 | "\"" 71 | ], 72 | [ 73 | "'", 74 | "'" 75 | ] 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /werk-vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "werk", 3 | "displayName": "Werk", 4 | "description": "Werk language and build system support", 5 | "version": "0.0.1", 6 | "engines": { 7 | "vscode": "^1.96.0" 8 | }, 9 | "categories": [ 10 | "Programming Languages" 11 | ], 12 | "contributes": { 13 | "languages": [{ 14 | "id": "werk", 15 | "aliases": ["Werk", "werk"], 16 | "extensions": [".werk","Werkfile","werkfile"], 17 | "configuration": "./language-configuration.json" 18 | }], 19 | "grammars": [{ 20 | "language": "werk", 21 | "scopeName": "source.werk", 22 | "path": "./syntaxes/werk.tmLanguage.json" 23 | }] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /werk-vscode/syntaxes/werk.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "Werk", 4 | "patterns": [ 5 | { 6 | "include": "#keywords" 7 | }, 8 | { 9 | "include": "#operators" 10 | }, 11 | { 12 | "include": "#builtins" 13 | }, 14 | { 15 | "include": "#strings" 16 | }, 17 | { 18 | "include": "#constants" 19 | }, 20 | { 21 | "include": "#comments" 22 | } 23 | ], 24 | "repository": { 25 | "keywords": { 26 | "patterns": [ 27 | { 28 | "name": "keyword.control.werk", 29 | "match": "\\b(default|config|let|build|task|from|to|depfile|run|spawn|include)\\b" 30 | } 31 | ] 32 | }, 33 | "operators": { 34 | "patterns": [ 35 | { 36 | "name": "keyword.operator.werk", 37 | "match": "(\\||=>)" 38 | } 39 | ] 40 | }, 41 | "builtins": { 42 | "patterns": [ 43 | { 44 | "name": "support.function.werk", 45 | "match": "\\b(glob|which|env|shell|which|info|warn|error|write|copy|delete|info|warn|error|flatten|join|split|split-pattern|map|lines|filter-match|filter|discard|match|assert-eq|assert-match)\\b" 46 | } 47 | ] 48 | }, 49 | "strings": { 50 | "name": "string.quoted.double.werk", 51 | "begin": "\"", 52 | "end": "\"", 53 | "patterns": [ 54 | { 55 | "name": "constant.character.escape.werk", 56 | "match": "\\\\." 57 | }, 58 | { 59 | "name": "support.variable.werk", 60 | "match": "\\{.*?\\}" 61 | }, 62 | { 63 | "name": "support.variable.werk", 64 | "match": "\\<.*?\\>" 65 | }, 66 | { 67 | "name": "constant.language.werk", 68 | "match": "%" 69 | } 70 | ] 71 | }, 72 | "constants": { 73 | "patterns": [ 74 | { 75 | "name": "constant.language.werk", 76 | "match": "\\b(true|false)\\b" 77 | } 78 | ] 79 | }, 80 | "comments": { 81 | "patterns": [ 82 | { 83 | "name": "comment.line.werk", 84 | "match": "#.*$" 85 | } 86 | ] 87 | } 88 | }, 89 | "scopeName": "source.werk" 90 | } 91 | -------------------------------------------------------------------------------- /werk-vscode/vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your language support and define the location of the grammar file that has been copied into your extension. 7 | * `syntaxes/werk.tmLanguage.json` - this is the Text mate grammar file that is used for tokenization. 8 | * `language-configuration.json` - this is the language configuration, defining the tokens that are used for comments and brackets. 9 | 10 | ## Get up and running straight away 11 | 12 | * Make sure the language configuration settings in `language-configuration.json` are accurate. 13 | * Press `F5` to open a new window with your extension loaded. 14 | * Create a new file with a file name suffix matching your language. 15 | * Verify that syntax highlighting works and that the language configuration settings are working. 16 | 17 | ## Make changes 18 | 19 | * You can relaunch the extension from the debug toolbar after making changes to the files listed above. 20 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 21 | 22 | ## Add more language features 23 | 24 | * To add features such as IntelliSense, hovers and validators check out the VS Code extenders documentation at https://code.visualstudio.com/docs 25 | 26 | ## Install your extension 27 | 28 | * To start using your extension with Visual Studio Code copy it into the `/.vscode/extensions` folder and restart Code. 29 | * To share your extension with the world, read on https://code.visualstudio.com/docs about publishing an extension. 30 | -------------------------------------------------------------------------------- /werk.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | # http://www.sublimetext.com/docs/syntax.html 4 | name: Werk 5 | scope: source.werk 6 | contexts: 7 | main: 8 | - include: keywords 9 | - include: operators 10 | - include: builtins 11 | - include: strings 12 | - include: constants 13 | - include: comments 14 | builtins: 15 | - match: |- 16 | 17 | \b(glob|which|env|shell|which|info|warn|error|write|copy|delete|info|warn|error|flatten|join|split|split-pattern|map|lines|filter-match|filter|discard|match|assert-eq|assert-match)\b 18 | scope: support.function.werk 19 | comments: 20 | - match: '#.*$' 21 | scope: comment.line.werk 22 | constants: 23 | - match: \b(true|false)\b 24 | scope: constant.language.werk 25 | keywords: 26 | - match: \b(config|let|build|task|from|to|depfile|run)\b 27 | scope: keyword.control.werk 28 | operators: 29 | - match: (\||=>) 30 | scope: keyword.operator.werk 31 | strings: 32 | - match: '"' 33 | push: 34 | - meta_scope: string.quoted.double.werk 35 | - match: '"' 36 | pop: true 37 | - match: \\. 38 | scope: constant.character.escape.werk 39 | - match: '\{.*?\}' 40 | scope: support.variable.werk 41 | - match: \<.*?\> 42 | scope: support.variable.werk 43 | - match: '%' 44 | scope: constant.language.werk 45 | --------------------------------------------------------------------------------