├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── Bitfile ├── Justfile ├── README.md ├── _examples └── c │ ├── .gitignore │ ├── Bitfile │ ├── a.c │ ├── b.c │ └── b.h ├── bin ├── .enumer-1.5.11.pkg ├── .go-1.24.4.pkg ├── .go-check-sumtype-0.3.1.pkg ├── .golangci-lint-1.64.7.pkg ├── .goreleaser-1.26.2.pkg ├── .just-1.41.0.pkg ├── .svu-1.12.0.pkg ├── .watchexec-2.3.2.pkg ├── README.hermit.md ├── activate-hermit ├── enumer ├── go ├── go-check-sumtype ├── gofmt ├── golangci-lint ├── goreleaser ├── hermit ├── hermit.hcl ├── just ├── svu └── watchexec ├── bit.png ├── cmd └── bit │ └── main.go ├── engine ├── database.go ├── engine.go ├── hasher.go ├── internal │ ├── eventsource │ │ └── eventsource.go │ ├── gitignore.go │ ├── glob.go │ ├── memoise.go │ └── pubsub │ │ ├── pubsub.go │ │ └── pubsub_test.go └── logging │ ├── colours.go │ ├── csi │ ├── csi.go │ └── csi_test.go │ └── logger.go ├── go.mod ├── go.sum ├── parser ├── lexer.go ├── lexer │ ├── continuation │ │ ├── continuation.go │ │ └── continuation_test.go │ └── indenter │ │ ├── indenter.go │ │ └── indenter_test.go ├── override_enumer.go ├── parser.go ├── parser_test.go ├── testdata │ └── Bitfile.full ├── text.go ├── text_test.go └── visit.go ├── renovate.json5 └── scripts └── bit /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [alecthomas] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | name: CI 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | - name: Init Hermit 15 | uses: cashapp/activate-hermit@v1 16 | - name: Test 17 | run: go test ./... 18 | sum-types: 19 | name: Lint 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | - name: Init Hermit 25 | uses: cashapp/activate-hermit@v1 26 | - name: golangci-lint 27 | run: golangci-lint run 28 | lint: 29 | name: Check Exhaustive Type Switch 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout code 33 | uses: actions/checkout@v4 34 | - name: Init Hermit 35 | uses: cashapp/activate-hermit@v1 36 | - name: go-check-sumtype 37 | run: go-check-sumtype ./... 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | - name: Activate Hermit 16 | uses: cashapp/activate-hermit@v1 17 | - name: Release 18 | run: goreleaser release 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | build/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: true 3 | timeout: 5m 4 | 5 | output: 6 | print-issued-lines: false 7 | 8 | linters: 9 | enable-all: true 10 | disable: 11 | - lll 12 | - typecheck # `go build` catches this, and it doesn't currently work with Go 1.11 modules 13 | - goimports # horrendously slow with go modules :( 14 | - dupl # has never been actually useful 15 | - gochecknoglobals 16 | - gochecknoinits 17 | - funlen 18 | - whitespace 19 | - godox 20 | - wsl 21 | - dogsled 22 | - gocognit 23 | - gocyclo 24 | - godot 25 | - nestif 26 | - testpackage 27 | - gci 28 | - gofumpt 29 | - nlreturn 30 | - forbidigo 31 | - cyclop 32 | - paralleltest 33 | - tagliatelle 34 | - gomoddirectives 35 | - err113 36 | - varnamelen 37 | - ireturn 38 | - containedctx 39 | - nilnil 40 | - contextcheck 41 | - nonamedreturns 42 | - exhaustruct 43 | - nosprintfhostport 44 | - nilerr 45 | - goconst 46 | - prealloc 47 | - rowserrcheck # doesn't support generics 48 | - wastedassign # doesn't support generics 49 | - goprintffuncname 50 | - dupword 51 | - errchkjson 52 | - musttag 53 | - gofmt # autofmt 54 | - interfacebloat 55 | - tagalign 56 | - nolintlint 57 | - protogetter 58 | - thelper 59 | - wrapcheck 60 | - perfsprint 61 | - makezero 62 | - mnd 63 | - recvcheck 64 | - exportloopref 65 | - err113 66 | 67 | linters-settings: 68 | exhaustive: 69 | default-signifies-exhaustive: true 70 | govet: 71 | enable: 72 | - "shadow" 73 | dupl: 74 | threshold: 100 75 | goconst: 76 | min-len: 8 77 | min-occurrences: 3 78 | gocyclo: 79 | min-complexity: 20 80 | gocritic: 81 | disabled-checks: 82 | - ifElseChain 83 | errcheck: 84 | check-blank: true 85 | depguard: 86 | rules: 87 | main: 88 | deny: 89 | - pkg: github.com/pkg/errors 90 | desc: "use fmt.Errorf or errors.New" 91 | - pkg: github.com/stretchr/testify 92 | desc: "use github.com/alecthomas/assert/v2" 93 | - pkg: github.com/alecthomas/errors 94 | desc: "use fmt.Errorf or errors.New" 95 | - pkg: braces.dev/errtrace 96 | desc: "use fmt.Errorf or errors.New" 97 | # wrapcheck: 98 | # ignorePackageGlobs: 99 | # - github.com/TBD54566975/ftl/* 100 | 101 | issues: 102 | max-same-issues: 0 103 | max-issues-per-linter: 0 104 | exclude-dirs: 105 | - resources 106 | - old 107 | exclude-use-default: false 108 | exclude: 109 | # Captured by errcheck. 110 | - "^(G104|G204):" 111 | # Very commonly not checked. 112 | - 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*Print(f|ln|)|os\.(Un)?Setenv). is not checked' 113 | # Weird error only seen on Kochiku... 114 | - "internal error: no range for" 115 | - 'exported method `.*\.(MarshalJSON|UnmarshalJSON|URN|Payload|GoString|Close|Provides|Requires|ExcludeFromHash|MarshalText|UnmarshalText|Description|Check|Poll|Severity)` should have comment or be unexported' 116 | - "composite literal uses unkeyed fields" 117 | - 'declaration of "err" shadows declaration' 118 | - "by other packages, and that stutters" 119 | - "Potential file inclusion via variable" 120 | - "at least one file in a package should have a package comment" 121 | - "bad syntax for struct tag pair" 122 | - "should have comment or be unexported" 123 | - "package-comments" 124 | - "parameter testing.TB should have name tb" 125 | - "blank-imports" 126 | - 'should have comment \(or a comment on this block\) or be unexported' 127 | - caseOrder 128 | - unused-parameter 129 | - "^loopclosure:" 130 | - 'shadow: declaration of "ctx" shadows declaration at' 131 | - 'shadow: declaration of "ok" shadows declaration' 132 | - "^dot-imports:" 133 | - "fmt.Errorf can be replaced with errors.New" 134 | - "fmt.Sprintf can be replaced with string concatenation" 135 | - "strings.Title has been deprecated" 136 | - "error returned from external package is unwrapped.*TranslatePGError" 137 | - "struct literal uses unkeyed fields" 138 | - "bad syntax for struct tag key" 139 | - "exported: comment on exported type" 140 | - "Error return value of .* is not checked" 141 | - "G115" 142 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - main: ./cmd/bit 3 | env: 4 | - CGO_ENABLED=0 5 | goos: 6 | - linux 7 | #- windows 8 | - darwin 9 | 10 | archives: 11 | - format: tar.gz 12 | # this name template makes the OS and Arch compatible with the results of uname. 13 | name_template: >- 14 | {{ .ProjectName }}- 15 | {{- .Os }}- 16 | {{- .Arch }} 17 | format_overrides: 18 | - goos: windows 19 | format: zip 20 | checksum: 21 | name_template: 'checksums.txt' 22 | snapshot: 23 | name_template: "{{ incpatch .Version }}-next" 24 | changelog: 25 | sort: asc 26 | filters: 27 | exclude: 28 | - '^docs:' 29 | - '^test:' 30 | 31 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 32 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 33 | -------------------------------------------------------------------------------- /Bitfile: -------------------------------------------------------------------------------- 1 | DEST = ./build 2 | 3 | %{DEST}: 4 | build: mkdir -p %{DEST} 5 | 6 | %{DEST}/bit: **/*.go %{DEST} 7 | build: 8 | go build -v -o %{DEST}/bit ./cmd/bit 9 | 10 | parser/override_enumer.go: parser/parser.go 11 | build: go generate %{IN} 12 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | # Start hot reloading 2 | dev: 3 | watchexec -- go build -o build/bit -v ./cmd/bit 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bit - A simple yet powerful build tool [![CI](https://github.com/alecthomas/bit/actions/workflows/ci.yml/badge.svg)](https://github.com/alecthomas/bit/actions/workflows/ci.yml) 2 | 3 | ![Build IT](bit.png) 4 | 5 | Bit's goal is to be a simple yet powerful local build system. It is inspired 6 | by [Make](https://www.gnu.org/software/make/manual/make.html), with the 7 | following goals: 8 | 9 | - Simple declarative file format. 10 | - Leverage existing Unix knowledge. 11 | - Deterministic, incremental, parallel builds. 12 | - As "type safe" as possible while maintaining usability. 13 | - Ability to build virtual targets (e.g. Docker images, Kubernetes resources, etc.). 14 | - Great error messages. 15 | 16 | Non-goals: 17 | 18 | - No need to learn a new Turing complete build language and 19 | associated libraries (e.g. Bazel, Gradle, etc.). 20 | 21 | Bit is driven by a configuration file called a `Bitfile`. It is described below. 22 | 23 | ## How does Bit improve on Make? 24 | 25 | ### Native support for globs, including against outputs 26 | 27 | Inputs for a target can be globs. Globs are matched against the filesystem, as 28 | well as declared outputs of other targets. 29 | 30 | eg. Given the following build file, `build/tool` will be rebuilt if any `.go` 31 | file changes. In particular, even if the generated file `token_enumer.go` is 32 | deleted it will still be included as a dependency. 33 | 34 | ``` 35 | build/tool: cmd/tool/** **/*.go 36 | build: go build -o build/tool 37 | 38 | token_enumer.go: token.go 39 | build: go generate token.go 40 | ``` 41 | 42 | ### Builtin (but overridable) cleaning 43 | 44 | `bit --clean` can be used to remove all declared outputs of all targets. This 45 | behaviour can be overridden per target, including disabling it. 46 | 47 | eg. 48 | 49 | Override the default clean behaviour: 50 | 51 | ```make 52 | build/tool: cmd/tool/** **/*.go 53 | build: go build -o build/tool 54 | clean: rm -f build/tool 55 | ``` 56 | 57 | Disable the default clean behaviour: 58 | 59 | ```make 60 | build/tool: cmd/tool/** **/*.go 61 | build: go build -o build/tool 62 | -clean 63 | ``` 64 | 65 | Extend the default clean behaviour: 66 | 67 | ```make 68 | build/tool: cmd/tool/** **/*.go 69 | build: go build -o build/tool 70 | +clean: go clean -cache 71 | ``` 72 | 73 | ### Verify outputs 74 | 75 | Bit verifies that the declared outputs of a target are generated by that target 76 | during the build, and will raise an error if any are missing. 77 | 78 | ## Development status 79 | 80 | What's implemented so far: 81 | 82 | - [x] [Variable expansion](#variables) 83 | - [x] [Command substitution](#command-substitution) 84 | - [x] Output verification 85 | - [x] [Dependencies](#dependencies) 86 | - [x] Hashing 87 | - [x] [Concrete targets](#targets) 88 | - [x] [Implicit targets](#implicit-targets) 89 | - [ ] [Virtual targets](#virtual-targets) 90 | - [ ] [Templates](#templates) 91 | - [ ] [Inheritance](#inheritance) 92 | - [x] Directives 93 | - [x] [Build](#create-directive-optional) 94 | - [x] [Inputs](#inputs-directive-optional) 95 | - [x] [Outputs](#outputs-directive-optional) 96 | - [x] [Hash](#hash-directive-optional) 97 | - [x] [Clean](#clean-directive-optional) 98 | 99 | ## Motivation 100 | 101 | While I love the simplicity of `make`, it has some pretty big limitations: 102 | 103 | - If a target fails to build an output, it will still succeed, and the 104 | target will silently continue to be out-of-date. 105 | - Similarly, with variable interpolation, if a variable is undefined it will 106 | silently be interpolated as an empty string. 107 | - Make can't (natively) capture non-filesystem dependencies. For example, if a 108 | target depends on a Docker image, it can't be expressed without intermediate 109 | files being manually created to track this. 110 | 111 | ## Bitfile 112 | 113 | The Bitfile is a declarative file that describes how to build targets. It consists 114 | of targets, templates, and variables, that can be substituted into targets. 115 | 116 | ### Glob expansion 117 | 118 | Glob expansion is supported in inputs, but not outputs. This is because 119 | globs operate on the filesystem and outputs may not be present at time of 120 | expansion. Bit will report an error for globs in outputs. 121 | 122 | ### Variables 123 | 124 | Variables are in the form: 125 | 126 | ``` 127 | [export] var = value 128 | ``` 129 | 130 | Or: 131 | 132 | ``` 133 | var = 134 | value 135 | value 136 | ``` 137 | 138 | If a variable is prefixed with `export` it will be made available to child 139 | processes as environment variables. 140 | 141 | eg. 142 | 143 | ``` 144 | export GRADLE_OPTS = -Dorg.gradle.console=plain 145 | ``` 146 | 147 | They can be set on the command line, at the top level of a `Bitfile`, 148 | or in a target. Variables are interpolated with the syntax `%{var}`. Interpolation 149 | occurs after inheritance and before any other evaluation. 150 | 151 | ### Command substitution 152 | 153 | Command substitution is in the form: 154 | 155 | ``` 156 | %(command)% 157 | ``` 158 | 159 | ### Targets 160 | 161 | Targets are in the form: 162 | 163 | ``` 164 | output1 output2 ...: input1 input2 ... 165 | < other-target # Inherit from a target 166 | < template(arg1=value, arg2=value, ...) # Inherit from a template 167 | cd dir # Change directory 168 | var = value # Set variable 169 | -var # Delete variable 170 | var += value # Append to variable 171 | var ^= value # Prepend to variable 172 | directive: parameters # Replace inherited directive 173 | -directive # Delete inherited directive 174 | +directive: parameters # Append to inherited directive 175 | ^directive: parameters # Prepend to inherited directive 176 | ``` 177 | 178 | eg. 179 | 180 | ``` 181 | kotlin-runtime/ftl-runtime/build/libs/ftl-runtime.jar: \ 182 | kotlin-runtime/ftl-runtime/**/*.{kt,kts} \ 183 | kotlin-runtime/gradle/libs.versions.toml \ 184 | **/*.proto **/buf.* \ 185 | protos/xyz/block/ftl/v1/schema/schema.proto 186 | build: cd kotlin-runtime/ftl-runtime && gradle jar 187 | +clean: cd kotlin-runtime/ftl-runtime && gradle clean 188 | ``` 189 | 190 | ### Implicit targets 191 | 192 | Implicit targets are patterns that generate concrete targets for any matching 193 | files. They take the form: 194 | 195 | ``` 196 | implicit