diff --git a/.editorconfig b/.editorconfig index fe5dc29..8b56c7d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,9 +11,6 @@ trim_trailing_whitespace=true [Containerfile*] indent_style=tab -[Makefile] -indent_style=tab - [*.css] indent_size=2 indent_style=space diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index bac6233..4e46d53 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -27,7 +27,7 @@ jobs: with: go-version-file: go.mod - name: Audit - run: make audit-capabilities + run: go run tasks.go audit-capabilities vulnerabilities: name: Vulnerabilities runs-on: ubuntu-22.04 @@ -39,4 +39,4 @@ jobs: with: go-version-file: go.mod - name: Audit - run: make audit-vulnerabilities + run: go run tasks.go audit-vulnerabilities diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 94cde5a..f7a4e5f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -19,7 +19,7 @@ jobs: with: go-version-file: go.mod - name: Build binary - run: make build + run: go run tasks.go build compliance: name: Compliance runs-on: ubuntu-22.04 @@ -33,7 +33,7 @@ jobs: with: go-version-file: go.mod - name: Check compliance - run: make compliance + run: go run tasks.go compliance container: name: Container runs-on: ubuntu-22.04 @@ -53,7 +53,7 @@ jobs: with: go-version-file: go.mod - name: Build container with ${{ matrix.engine }} - run: make container + run: go run tasks.go container env: CONTAINER_ENGINE: ${{ matrix.engine }} - name: Test run container with ${{ matrix.engine }} @@ -66,8 +66,12 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - name: Install Go + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version-file: go.mod - name: Build - run: make dev-img + run: go run tasks.go dev-img format: name: Format runs-on: ubuntu-22.04 @@ -81,7 +85,7 @@ jobs: with: go-version-file: go.mod - name: Check source code formatting - run: make fmt-check + run: go run tasks.go format-check reproducible: name: Reproducible build runs-on: ubuntu-22.04 @@ -95,7 +99,7 @@ jobs: with: go-version-file: go.mod - name: Check reproducibility - run: make reproducible + run: go run tasks.go reproducible test-unit: name: Unit test runs-on: ubuntu-22.04 @@ -109,7 +113,7 @@ jobs: with: go-version-file: go.mod - name: Run tests - run: make test-randomized + run: go run tasks.go test-randomized test-dogfeed: name: Dogfeed runs-on: ubuntu-22.04 @@ -123,7 +127,7 @@ jobs: with: go-version-file: go.mod - name: Run on this repository - run: make run + run: go run tasks.go dogfeed test-mutation: name: Mutation test runs-on: ubuntu-22.04 @@ -137,7 +141,7 @@ jobs: with: go-version-file: go.mod - name: Run mutation tests - run: make test-mutation + run: go run tasks.go test-mutation vet: name: Vet runs-on: ubuntu-22.04 @@ -151,7 +155,7 @@ jobs: with: go-version-file: go.mod - name: Vet source code - run: make vet + run: go run tasks.go vet web: name: Web runs-on: ubuntu-22.04 @@ -165,6 +169,4 @@ jobs: with: go-version-file: go.mod - name: Build web app - run: | - cd web - make build + run: go run tasks.go web-build diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e2d7d58..497bdcd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -72,7 +72,7 @@ jobs: run: | echo "version=${GITHUB_REF#refs/tags/}" >>"$GITHUB_OUTPUT" - name: Compile - run: make release-compile + run: go run tasks.go build-all - name: Create GitHub release uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 with: diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index b61627f..c8dfa9c 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -22,9 +22,7 @@ jobs: with: go-version-file: go.mod - name: Build - run: | - cd web - make build + run: go run tasks.go web-build - name: Deploy uses: JamesIves/github-pages-deploy-action@ec9c88baef04b842ca6f0a132fd61c762aa6c1b0 # v4.6.0 with: diff --git a/.gitignore b/.gitignore index 9b5525b..42309a2 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,6 @@ cmd/ades/** !cmd/ades/*.go !Containerfile !Containerfile.dev -!Makefile !go.mod !go.sum !schema.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49de0a7..0b55fcf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,34 @@ If you decide to make a contribution, please read the [DCO] and use the followin --- +## Tasks + +This project uses a custom Go-based task runner to run common tasks. To get started run: + +```shell +go run tasks.go +``` + +For example, you can run one task as: + +```shell +go run tasks.go verify +``` + +We recommend configuring the following command alias: + +```shell +alias gask='go run tasks.go' +``` + +Which would allow you to run: + +```shell +gask verify +``` + +--- + ## Adding a Rule To add a rule you need to add some information and logic to the `rules.go` file, and corresponding diff --git a/Containerfile b/Containerfile index 8f4c568..944a287 100644 --- a/Containerfile +++ b/Containerfile @@ -18,9 +18,9 @@ FROM docker.io/golang:1.22.0 AS build WORKDIR /src COPY cmd/ ./cmd/ -COPY Makefile go.mod go.sum *.go ./ +COPY go.mod go.sum *.go ./ -RUN make build +RUN go run tasks.go build # --- diff --git a/Containerfile.dev b/Containerfile.dev index 2465d66..567eb6a 100644 --- a/Containerfile.dev +++ b/Containerfile.dev @@ -20,7 +20,9 @@ FROM docker.io/golang:1.22.0-alpine3.19 RUN apk add --no-cache \ - bash git make + bash git perl-utils zip \ + && \ + echo "alias gask='go run tasks.go'" >~/.bashrc WORKDIR /ades COPY go.mod go.sum ./ diff --git a/Makefile b/Makefile deleted file mode 100644 index 337822a..0000000 --- a/Makefile +++ /dev/null @@ -1,268 +0,0 @@ -# MIT No Attribution -# -# Copyright (c) 2024 Eric Cornelissen -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -CONTAINER_ENGINE?=docker -CONTAINER_TAG?=latest - -.PHONY: default -default: - @printf "Usage: make \n\n" - @printf "Commands:\n" - @awk -F ':(.*)## ' '/^[a-zA-Z0-9%\\\/_.-]+:(.*)##/ { \ - printf " \033[36m%-30s\033[0m %s\n", $$1, $$NF \ - }' $(MAKEFILE_LIST) - -.PHONY: audit audit-capabilities audit-vulnerabilities update-capabilities -audit: audit-capabilities audit-vulnerabilities ## Audit the codebase - -audit-capabilities: ## Audit for capabilities - @echo 'Checking capabilities...' - @go run github.com/google/capslock/cmd/capslock \ - -packages ./... \ - -noisy \ - -output=compare capabilities.json - -audit-vulnerabilities: ## Audit for vulnerabilities - @echo 'Checking vulnerabilities...' - @go run golang.org/x/vuln/cmd/govulncheck ./... - -update-capabilities: - @echo 'Updating capabilities...' - @go run github.com/google/capslock/cmd/capslock \ - -packages ./... \ - -noisy \ - -output json >capabilities.json - -.PHONY: build -build: ## Build the ades binary for the current platform - @echo 'Building...' - @go build ./cmd/ades - -.PHONY: clean -clean: ## Reset the project to a clean state - @echo 'Cleaning...' - @git clean -fx \ - _compiled/ \ - ades* \ - cover.* - -.PHONY: compliance -compliance: ## Check license compliance - @echo 'Checking license compliance...' - @go run github.com/google/go-licenses check \ - --allowed_licenses BSD-3-Clause,GPL-3.0,MIT \ - ./... - -.PHONY: container -container: ## Build the ades container for the current platform - @$(CONTAINER_ENGINE) build \ - --file Containerfile \ - --tag ericornelissen/ades:$(CONTAINER_TAG) \ - . - -.PHONY: coverage -coverage: ## Run all tests and generate a coverage report - @echo 'Testing...' - @go test -coverprofile cover.out ./... - @echo 'Generating coverage report...' - @go tool cover -html cover.out -o cover.html - -.PHONY: dev-env dev-img -dev-env: dev-img ## Run an ephemeral development environment container - @$(CONTAINER_ENGINE) run -it \ - --rm \ - --workdir '/ades' \ - --mount "type=bind,source=$(shell pwd),target=/ades" \ - --name 'ades-dev-env' \ - 'ades-dev-img' - -dev-img: ## Build a development environment container image - @$(CONTAINER_ENGINE) build \ - --file 'Containerfile.dev' \ - --tag 'ades-dev-img' \ - . - -.PHONY: fmt fmt-check -fmt: ## Format the source code - @echo 'Formatting...' - @gofmt -w . - @gofmt -w -r 'interface{} -> any' . - @go mod tidy - @go run golang.org/x/tools/cmd/goimports -w . - -fmt-check: ## Check the source code formatting - @echo 'Checking formatting...' - @test -z "$$(gofmt -l .)" - @test -z "$$(gofmt -l -r 'interface{} -> any' .)" - @test -z "$$(go run golang.org/x/tools/cmd/goimports -l .)" - -.PHONY: release release-compile -release: - @echo 'On main and not dirty?' - @test "$$(git branch --show-current)" = 'main' - @test "$$(git status --porcelain)" = '' - - @echo 'Is main up-to-date?' - @git fetch - @test "$$(git rev-parse HEAD)" = "$$(git rev-parse FETCH_HEAD)" - - @echo 'Preparing for version bump...' - @sed -i cmd/ades/main.go -e "s/versionString := \"v[0-9][0-9][.][0-9][0-9]\"/versionString := \"v$$(date '+%y.%m')\"/" - @sed -i test/flags-info.txtar -e "s/stdout 'v[0-9][0-9][.][0-9][0-9]'/stdout 'v$$(date '+%y.%m')'/" - - @echo 'Committing version bump...' - @git checkout -b version-bump - @git add 'cmd/ades/main.go' 'test/flags-info.txtar' - @git commit --signoff --message 'version bump' - - @echo 'Pushing version-bump branch...' - @git push origin version-bump - - @echo '' - @echo 'Next, you should open a Pull Request to merge the branch version-bump into main and' - @echo 'merge it if all checks succeeds. After merging run:' - @echo '' - @echo ' git checkout main' - @echo ' git pull origin main' - @echo " git tag v$$(date '+%y.%m')" - @echo " git push origin v$$(date '+%y.%m')" - @echo '' - @echo 'After that a release should be created automatically. If not, follow the instructions in' - @echo 'RELEASE.md.' - -release-compile: - @mkdir _compiled/ - - @echo 'Compiling for darwin/amd64...' - @env GOOS=darwin GOARCH=amd64 go build -o 'ades' ./cmd/ades - @tar -czf 'ades_darwin_amd64.tar.gz' 'ades' - @mv 'ades_darwin_amd64.tar.gz' '_compiled/' - - @echo 'Compiling for darwin/arm64...' - @env GOOS=darwin GOARCH=arm64 go build -o 'ades' ./cmd/ades - @tar -czf 'ades_darwin_arm64.tar.gz' 'ades' - @mv 'ades_darwin_arm64.tar.gz' '_compiled/' - - @echo 'Compiling for linux/386...' - @env GOOS=linux GOARCH=386 go build -o 'ades' ./cmd/ades - @tar -czf 'ades_linux_386.tar.gz' 'ades' - @mv 'ades_linux_386.tar.gz' '_compiled/' - - @echo 'Compiling for linux/amd64...' - @env GOOS=linux GOARCH=amd64 go build -o 'ades' ./cmd/ades - @tar -czf 'ades_linux_amd64.tar.gz' 'ades' - @mv 'ades_linux_amd64.tar.gz' '_compiled/' - - @echo 'Compiling for linux/arm...' - @env GOOS=linux GOARCH=arm go build -o 'ades' ./cmd/ades - @tar -czf 'ades_linux_arm.tar.gz' 'ades' - @mv 'ades_linux_arm.tar.gz' '_compiled/' - - @echo 'Compiling for linux/arm64...' - @env GOOS=linux GOARCH=arm64 go build -o 'ades' ./cmd/ades - @tar -czf 'ades_linux_arm64.tar.gz' 'ades' - @mv 'ades_linux_arm64.tar.gz' '_compiled/' - - @echo 'Compiling for windows/386...' - @env GOOS=windows GOARCH=386 go build -o 'ades' ./cmd/ades - @mv 'ades' 'ades.exe' - @zip -9q 'ades_windows_386.zip' 'ades.exe' - @mv 'ades_windows_386.zip' '_compiled/' - - @echo 'Compiling for windows/amd64...' - @env GOOS=windows GOARCH=amd64 go build -o 'ades' ./cmd/ades - @mv 'ades' 'ades.exe' - @zip -9q 'ades_windows_amd64.zip' 'ades.exe' - @mv 'ades_windows_amd64.zip' '_compiled/' - - @echo 'Compiling for windows/arm...' - @env GOOS=windows GOARCH=arm go build -o 'ades' ./cmd/ades - @mv 'ades' 'ades.exe' - @zip -9q 'ades_windows_arm.zip' 'ades.exe' - @mv 'ades_windows_arm.zip' '_compiled/' - - @echo 'Compiling for windows/arm64...' - @env GOOS=windows GOARCH=arm64 go build -o 'ades' ./cmd/ades - @mv 'ades' 'ades.exe' - @zip -9q 'ades_windows_arm64.zip' 'ades.exe' - @mv 'ades_windows_arm64.zip' '_compiled/' - - @echo 'Computing checksums...' - @cd _compiled/ && shasum --algorithm 512 ./* >'checksums-sha512.txt' - -.PHONY: reproducible -reproducible: - @make build - @shasum ades | tee checksums.txt - @make clean build - @shasum --check checksums.txt --strict - -.PHONY: run -run: ## Run the project on itself - @go run ./cmd/ades - -.PHONY: test -test: ## Run all tests - @echo 'Testing...' - @go test ./... - -.PHONY: test-mutation -test-mutation: ## Run mutation tests - @echo 'Mutation testing...' - @go test -tags=mutation - -.PHONY: test-randomized -test-randomized: ## Run tests in a random order - @echo 'Testing (random order)...' - @go test -shuffle=on ./... - -.PHONY: vet -vet: ## Vet the source code - @echo 'Vetting...' - @go vet ./... - @go run 4d63.com/gochecknoinits ./... - @go run github.com/alexkohler/dogsled/cmd/dogsled -set_exit_status ./... - @go run github.com/alexkohler/nakedret/v2/cmd/nakedret -l 0 ./... - @go run github.com/alexkohler/prealloc -set_exit_status ./... - @go run github.com/alexkohler/unimport ./... - @go run github.com/butuzov/ireturn/cmd/ireturn ./... - @go run github.com/catenacyber/perfsprint ./... - @go run github.com/dkorunic/betteralign/cmd/betteralign ./... - @go run github.com/go-critic/go-critic/cmd/gocritic check ./... - @go run github.com/gordonklaus/ineffassign ./... - @go run github.com/jgautheron/goconst/cmd/goconst -numbers -set-exit-status ./... - @go run github.com/kisielk/errcheck ./... - @go run github.com/kunwardeep/paralleltest -i -ignoreloopVar ./... - @go run github.com/mdempsky/unconvert ./... - @go run github.com/nishanths/exhaustive/cmd/exhaustive ./... - @go run github.com/polyfloyd/go-errorlint ./... - @go run github.com/remyoudompheng/go-misc/deadcode . - @go run github.com/remyoudompheng/go-misc/deadcode ./cmd/ades - @go run github.com/remyoudompheng/go-misc/deadcode ./web - @go run github.com/rhysd/actionlint/cmd/actionlint - @go run github.com/tetafro/godot/cmd/godot . - @go run github.com/tomarrell/wrapcheck/v2/cmd/wrapcheck ./... - @go run github.com/ultraware/whitespace/cmd/whitespace ./... - @go run go.uber.org/nilaway/cmd/nilaway ./... - @go run golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow ./... - @go run honnef.co/go/tools/cmd/staticcheck ./... - @go run mvdan.cc/unparam ./... - -.PHONY: verify -verify: build compliance fmt-check test run vet ## Verify project is in a good state diff --git a/RELEASE.md b/RELEASE.md index 279ba96..8b13581 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -6,7 +6,7 @@ To release a new version of the `ades` project follow the description in this fi ## Preferred -Run `make release` and follow the instructions it gives. +Run `go run tasks.go release` and follow the instructions it gives. ## Fallback @@ -75,20 +75,20 @@ as an example): checksums) after running: ```shell - make release-compile + go run tasks.go build-all ``` 1. Publish to [Docker Hub], first with a version tag: ```shell - make container CONTAINER_TAG=v23.12 + env CONTAINER_TAG=v23.12 go run tasks.go container docker push ericornelissen/ades:v23.12 ``` then the `latest` tag: ```shell - make container CONTAINER_TAG=latest + env CONTAINER_TAG=latest go run tasks.go container docker push ericornelissen/ades:latest ``` diff --git a/mutation_test.go b/mutation_test.go index c3ae811..411fc17 100644 --- a/mutation_test.go +++ b/mutation_test.go @@ -33,6 +33,6 @@ import ( func TestMutation(t *testing.T) { ooze.Release( t, - ooze.IgnoreSourceFiles("^web/.*\\.go$"), + ooze.IgnoreSourceFiles(`^(tasks|tools|web/.*)\.go$`), ) } diff --git a/tasks.go b/tasks.go new file mode 100644 index 0000000..6f35c24 --- /dev/null +++ b/tasks.go @@ -0,0 +1,817 @@ +// MIT No Attribution +// +// Copyright (c) 2024 Eric Cornelissen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//go:build tasks + +package main + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "io/fs" + "os" + "os/exec" + "regexp" + "strings" +) + +const ( + DEFAULT_CONTAINER_ENGINE = "docker" + ENV_CONTAINER_ENGINE = "CONTAINER_ENGINE" + ENV_CONTAINER_TAG = "CONTAINER_TAG" +) + +var ( + buildAllDir = "_compiled" + webDir = "web" +) + +var ( + permFile fs.FileMode = 0o664 + permDir fs.FileMode = 0o755 +) + +// Audit the codebase. +func TaskAudit(t *T) error { + return t.Run( + TaskAuditCapabilities, + TaskAuditVulnerabilities, + ) +} + +// Audit for capabilities. +func TaskAuditCapabilities(t *T) error { + t.Log("Checking capabilities...") + return t.Exec(` + go run github.com/google/capslock/cmd/capslock + -packages ./... + -noisy + -output=compare capabilities.json + `) +} + +// Audit for known vulnerabilities. +func TaskAuditVulnerabilities(t *T) error { + t.Log("Checking vulnerabilities...") + return t.Exec(`go run golang.org/x/vuln/cmd/govulncheck .`) +} + +// Build the ades binary for the current platform. +func TaskBuild(t *T) error { + t.Log("Building...") + return t.Exec(`go build ./cmd/ades`) +} + +// Build the ades binary for all supported platforms. +func TaskBuildAll(t *T) error { + type Target struct { + GOOS string + GOARCH string + } + + var ( + osMac = "darwin" + osLinux = "linux" + osWindows = "windows" + arch386 = "386" + archAmd64 = "amd64" + archArm = "arm" + archArm64 = "arm64" + ) + + var targets = []Target{ + {GOOS: osMac, GOARCH: archAmd64}, + {GOOS: osMac, GOARCH: archArm64}, + {GOOS: osLinux, GOARCH: arch386}, + {GOOS: osLinux, GOARCH: archAmd64}, + {GOOS: osLinux, GOARCH: archArm}, + {GOOS: osLinux, GOARCH: archArm64}, + {GOOS: osWindows, GOARCH: arch386}, + {GOOS: osWindows, GOARCH: archAmd64}, + {GOOS: osWindows, GOARCH: archArm}, + {GOOS: osWindows, GOARCH: archArm64}, + } + + t.Log("Building (all platforms)...") + if err := os.RemoveAll(buildAllDir); err != nil { + return err + } + if err := os.Mkdir(buildAllDir, permDir); err != nil { + return err + } + + archives := make([]string, len(targets)) + for i, target := range targets { + fmt.Printf("Compiling for %s/%s...\n", target.GOOS, target.GOARCH) + + executable := "ades" + if target.GOOS == osWindows { + executable = "ades.exe" + } + + archiveCmd := "tar -czf" + if target.GOOS == osWindows { + archiveCmd = "zip -9q" + } + + archiveExt := "tar.gz" + if target.GOOS == osWindows { + archiveExt = "zip" + } + + archiveFile := fmt.Sprintf("ades_%s_%s.%s", target.GOOS, target.GOARCH, archiveExt) + archives[i] = archiveFile + + var ( + compile = fmt.Sprintf( + "env GOOS=%s GOARCH=%s go build -o %s ./cmd/ades", + target.GOOS, + target.GOARCH, + executable, + ) + archive = fmt.Sprintf( + "%s '_compiled/%s' %s", + archiveCmd, + archiveFile, + executable, + ) + ) + + if err := t.Exec(compile, archive); err != nil { + return err + } + } + + t.Log("Computing checksums...") + t.Cd("_compiled") + out, err := t.ExecS(`shasum --algorithm 512 ` + strings.Join(archives, " ")) + if err != nil { + return err + } + + return os.WriteFile("./_compiled/checksums-shas512.txt", []byte(out), permFile) +} + +// Reset the project to a clean state. +func TaskClean(t *T) error { + var ( + items = []string{ + "_compiled/", + "web/node_modules/", + "web/ades.wasm", + "web/COPYING.txt", + "web/wasm_exec.js", + "ades", + "ades.exe", + "cover.html", + "cover.out", + } + clean = "git clean -fx " + strings.Join(items, " ") + ) + + t.Log("Cleaning...") + return t.Exec(clean) +} + +// Check license compliance. +func TaskCompliance(t *T) error { + var ( + licenses = []string{ + "BSD-3-Clause", + "GPL-3.0", + "MIT", + } + licenseCheck = fmt.Sprintf( + "go run github.com/google/go-licenses check --allowed_licenses %s ./...", + strings.Join(licenses, ","), + ) + ) + + t.Log("Checking license compliance...") + return t.Exec(licenseCheck) +} + +// Build the ades container for the current platform. +func TaskContainer(t *T) error { + var ( + engine = t.Env(ENV_CONTAINER_ENGINE, DEFAULT_CONTAINER_ENGINE) + tag = t.Env(ENV_CONTAINER_TAG, "latest") + build = fmt.Sprintf( + "%s build --file Containerfile --tag ericornelissen/ades:%s .", + engine, + tag, + ) + ) + + return t.Exec(build) +} + +// Run all tests and generate a coverage report. +func TaskCoverage(t *T) error { + t.Log("Generating coverage report...") + return t.Exec( + "go test -coverprofile cover.out ./...", + "go tool cover -html cover.out -o cover.html", + ) +} + +// Run an ephemeral development environment container. +func TaskDevEnv(t *T) error { + wd, _ := os.Getwd() + + if err := TaskDevImg(t); err != nil { + return err + } + + var ( + engine = t.Env(ENV_CONTAINER_ENGINE, DEFAULT_CONTAINER_ENGINE) + build = fmt.Sprintf( + "%s run -it --rm --workdir '/ades' --mount 'type=bind,source=%s,target=/ades' --name ades-dev-env ades-dev-img", + engine, + wd, + ) + ) + + return t.Exec(build) +} + +// Build a development environment container image. +func TaskDevImg(t *T) error { + var ( + engine = t.Env(ENV_CONTAINER_ENGINE, DEFAULT_CONTAINER_ENGINE) + build = fmt.Sprintf( + "%s build --file 'Containerfile.dev' --tag ades-dev-img .", + engine, + ) + ) + + return t.Exec(build) +} + +// Run the project on itself. +func TaskDogfeed(t *T) error { + return t.Exec(`go run ./cmd/ades`) +} + +// Format the source code. +func TaskFormat(t *T) error { + t.Log("Formatting...") + return t.Exec( + "gofmt -w .", + "gofmt -w -r 'interface{} -> any' .", + "go mod tidy", + "go run golang.org/x/tools/cmd/goimports -w .", + ) +} + +// Check the source code formatting. +func TaskFormatCheck(t *T) error { + t.Log("Checking formatting...") + + out, err := t.ExecS( + "gofmt -l .", + "gofmt -l -r 'interface{} -> any' .", + "go run golang.org/x/tools/cmd/goimports -l .", + ) + if err != nil { + return err + } else if out != "" { + return errors.New("not formatted") + } + + return nil +} + +// Initiate a new release. +func TaskRelease(t *T) error { + var ( + baseBranch = "main" + bumpBranch = "version-bump" + ) + + t.Log("Checking repository state...") + if out, err := t.ExecS(`git branch --show-current`); err != nil { + return err + } else if out != baseBranch { + return errors.New("not on " + baseBranch) + } + + if out, err := t.ExecS(`git status --porcelain`); err != nil { + return err + } else if out != "" { + return errors.New("workspace is dirty") + } + + if _, err := t.ExecS(`git fetch`); err != nil { + return err + } + + head, err := t.ExecS(`git rev-parse HEAD`) + if err != nil { + return err + } + + fetchHead, err := t.ExecS(`git rev-parse FETCH_HEAD`) + if err != nil { + return err + } else if head != fetchHead { + return errors.New("branch is not up-to-date") + } + + t.Log("Preparing for version bump...") + date, err := t.ExecS(`date '+%y.%m'`) + if err != nil { + return err + } + + err = t.Exec( + `sed -i cmd/ades/main.go -e 's/versionString := "v[0-9][0-9][.][0-9][0-9]"/versionString := "v`+date+`"/'`, + `sed -i test/flags-info.txtar -e "s/stdout 'v[0-9][0-9][.][0-9][0-9]'/stdout 'v`+date+`'/"`, + ) + if err != nil { + return err + } + + t.Log("Committing and pushing version bump...") + err = t.Exec( + `git checkout -b `+bumpBranch, + `git add 'cmd/ades/main.go' 'test/flags-info.txtar'`, + `git commit --signoff --message 'version bump'`, + `git push origin `+bumpBranch, + ) + if err != nil { + return err + } + + t.Log("Next steps...") + fmt.Println("Next, you should open a Pull Request to merge the branch " + bumpBranch + " into") + fmt.Println(baseBranch + " and merge it if all checks succeeds. After merging run:") + fmt.Println() + fmt.Println(" git checkout " + baseBranch) + fmt.Println(" git pull origin " + baseBranch) + fmt.Println(" git tag v" + date) + fmt.Println(" git push origin v" + date) + fmt.Println() + fmt.Println("After that a release should be created automatically. If not, follow the release") + fmt.Println("guidelines in RELEASE.md.") + + return nil +} + +// Check if the build is reproducible. +func TaskReproducible(t *T) error { + var ( + build = "go build ./cmd/ades" + checksum = "shasum --algorithm 512 ades" + ) + + t.Log("Initial build...") + checksum1, err := t.ExecS(build, checksum) + if err != nil { + return err + } + + t.Log("Reproducing build...") + checksum2, err := t.ExecS(build, checksum) + if err != nil { + return err + } + + if checksum1 != checksum2 { + return errors.New("Build did not reproduce") + } + + return nil +} + +// Run all tests. +func TaskTest(t *T) error { + t.Log("Testing...") + return t.Exec(`go test ./...`) +} + +// Run mutation tests. +func TaskTestMutation(t *T) error { + t.Log("Mutation testing...") + return t.Exec(`go test -tags=mutation`) +} + +// Run tests in a random order. +func TaskTestRandomized(t *T) error { + t.Log("Testing (random order)...") + return t.Exec(`go test -shuffle=on ./...`) +} + +// Update the capability snapshot to the project's current capabilities. +func TaskUpdateCapabilities(t *T) error { + t.Log("Updating capabilities...") + stdout, err := t.ExecS(` + go run github.com/google/capslock/cmd/capslock + -packages ./... + -noisy + -output json + `) + if err != nil { + return err + } + + return os.WriteFile("./capabilities.json", []byte(stdout), permFile) +} + +// Verify project is in a good state. +func TaskVerify(t *T) error { + return t.Run( + TaskBuild, + TaskCompliance, + TaskFormatCheck, + TaskTest, + TaskDogfeed, + TaskVet, + ) +} + +// Vet the source code. +func TaskVet(t *T) error { + t.Log("Vetting...") + return t.Exec( + "go vet ./...", + "go run 4d63.com/gochecknoinits ./...", + "go run github.com/alexkohler/dogsled/cmd/dogsled -set_exit_status ./...", + "go run github.com/alexkohler/nakedret/v2/cmd/nakedret -l 0 ./...", + "go run github.com/alexkohler/prealloc -set_exit_status ./...", + "go run github.com/alexkohler/unimport ./...", + "go run github.com/butuzov/ireturn/cmd/ireturn ./...", + "go run github.com/catenacyber/perfsprint ./...", + "go run github.com/dkorunic/betteralign/cmd/betteralign ./...", + "go run github.com/go-critic/go-critic/cmd/gocritic check ./...", + "go run github.com/gordonklaus/ineffassign ./...", + "go run github.com/jgautheron/goconst/cmd/goconst -numbers -set-exit-status -ignore 'web' ./...", + "go run github.com/jgautheron/goconst/cmd/goconst -numbers -set-exit-status ./web/...", + "go run github.com/kisielk/errcheck ./...", + "go run github.com/kunwardeep/paralleltest -i -ignoreloopVar ./...", + "go run github.com/mdempsky/unconvert ./...", + "go run github.com/nishanths/exhaustive/cmd/exhaustive ./...", + "go run github.com/polyfloyd/go-errorlint ./...", + "go run github.com/remyoudompheng/go-misc/deadcode .", + "go run github.com/remyoudompheng/go-misc/deadcode ./cmd/ades", + "go run github.com/remyoudompheng/go-misc/deadcode ./web", + "go run github.com/rhysd/actionlint/cmd/actionlint", + "go run github.com/tetafro/godot/cmd/godot .", + "go run github.com/tomarrell/wrapcheck/v2/cmd/wrapcheck ./...", + "go run github.com/ultraware/whitespace/cmd/whitespace ./...", + "go run go.uber.org/nilaway/cmd/nilaway ./...", + "go run golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow ./...", + "go run honnef.co/go/tools/cmd/staticcheck ./...", + "go run mvdan.cc/unparam ./...", + ) +} + +// Build the ades web application. +func TaskWebBuild(t *T) error { + goroot, err := t.ExecS("go env GOROOT") + if err != nil { + return err + } + + var ( + buildWasm = "env GOOS=js GOARCH=wasm go build -o ades.wasm" + copyLicense = "cp ../COPYING.txt ./COPYING.txt" + copyWasmExec = fmt.Sprintf("cp %s/misc/wasm/wasm_exec.js ./wasm_exec.js", goroot) + ) + + t.Log("Building webapp...") + t.Cd(webDir) + return t.Exec(buildWasm, copyLicense, copyWasmExec) +} + +// Serve the ades web application. +func TaskWebServe(t *T) error { + if err := t.Run(TaskWebBuild); err != nil { + return err + } + + t.Log("Serving locally...") + t.Cd(webDir) + if err := t.Exec("npm install"); err != nil { + return err + } + + return t.Exec("npx http-server . --port 8080") +} + +// ------------------------------------------------------------------------------------------------- + +// T is a type passed to Task functions to perform common tasks. +type T struct { + dir string +} + +// Task is a function that performs a task. +type Task func(t *T) error + +// Cd changes the directory in which the task operates. +func (t *T) Cd(dir string) { + t.dir = dir +} + +// Env returns the value of the environment variable identified by key, or the fallback value. +func (t *T) Env(key, fallback string) string { + if value, present := os.LookupEnv(key); present { + return value + } else { + return fallback + } +} + +// Env returns the value of the environment variable identified by key, or the fallback value. +func (t *T) Run(tasks ...Task) error { + for _, task := range tasks { + var tt T + if err := task(&tt); err != nil { + return err + } + } + + return nil +} + +// Exec executes the commands printing to stdout. +func (t *T) Exec(commands ...string) error { + return t.ExecF(os.Stdout, commands...) +} + +// ExecF executes the commands writing stdout to buf. +func (t *T) ExecF(buf io.Writer, commands ...string) error { + for _, commandStr := range commands { + commandName, args := t.parseCommand(commandStr) + + cmd := exec.Command(commandName, args...) + cmd.Dir = t.dir + cmd.Stdin = os.Stdin + cmd.Stdout = buf + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return err + } + } + + return nil +} + +// ExecS executes the commands returning stdout as a string. +func (t *T) ExecS(commands ...string) (string, error) { + buf := new(bytes.Buffer) + err := t.ExecF(buf, commands...) + return strings.TrimSpace(buf.String()), err +} + +// Log prints the messages as a line in bold. Useful to delineate steps in a task. +func (t *T) Log(msgs ...string) { + fmt.Print("\033[1m") + for _, msg := range msgs { + fmt.Print(msg) + } + fmt.Println("\033[0m") +} + +func (t *T) parseCommand(command string) (string, []string) { + commandExp := regexp.MustCompile(`'((?:\'|[^'])+?)'|"((?:\"|[^"])+?)"|(\S+)`) + matches := commandExp.FindAllStringSubmatch(command, -1) + parsed := make([]string, len(matches)) + for i, match := range matches { + if match[1] != "" { + parsed[i] = match[1] + } else if match[2] != "" { + parsed[i] = match[2] + } else { + parsed[i] = match[3] + } + } + + return parsed[0], parsed[1:] +} + +func main() { + type internalTask struct { + desc string + name string + } + + var ( + taskFnPrefix = "Task" + exprCapital = regexp.MustCompile(`(.)([A-Z])`) + exprHyphenated = regexp.MustCompile(`(^|-)[a-z]`) + ) + + var ( + typeCheckTaskParams = func(params []*ast.Field) bool { + if len(params) != 1 { + return false + } + + paramType, ok := params[0].Type.(*ast.StarExpr) + if !ok { + return false + } + + paramTypeIdent, ok := paramType.X.(*ast.Ident) + if !ok || paramTypeIdent.Name != "T" { + return false + } + + return true + } + typeCheckTaskResults = func(results []*ast.Field) bool { + if len(results) != 1 { + return false + } + + _, ok := results[0].Type.(ast.Expr) + return ok + } + ) + + var ( + parse = func() ([]internalTask, error) { + file, err := parser.ParseFile(token.NewFileSet(), "tasks.go", nil, parser.ParseComments) + if err != nil { + return nil, fmt.Errorf("could not parse file: %s", err) + } + + tasks := make([]internalTask, 0) + for _, decl := range file.Decls { + // Check the declaration type, only functions can be tasks + fn, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + + // Check for the task prefix, which marks a runnable task + fnName := fn.Name.Name + if !strings.HasPrefix(fnName, taskFnPrefix) { + continue + } + + // Check that the function signature is correct + if ok := typeCheckTaskParams(fn.Type.Params.List); !ok { + return nil, fmt.Errorf("wrong signature for %q, should accept '*T'", fnName) + } + if ok := typeCheckTaskResults(fn.Type.Results.List); !ok { + return nil, fmt.Errorf("wrong signature for %q, should return 'error'", fnName) + } + + // Convert the function name to a task name + name := strings.TrimPrefix(fnName, taskFnPrefix) + name = exprCapital.ReplaceAllString(name, "${1}-${2}") + name = strings.ToLower(name) + + // Extract task description as the first line of the doc comment + desc := fn.Doc.Text() + if eol := strings.IndexRune(desc, '\n'); eol != -1 { + desc = desc[0:eol] + } + + tasks = append(tasks, internalTask{desc, name}) + } + + return tasks, nil + } + build = func(tasks []string) ([]byte, error) { + wd, err := os.Getwd() + if err != nil { + return nil, errors.New("could not get the current working directory") + } + + original, err := os.ReadFile("./tasks.go") + if err != nil { + return nil, errors.New("could not read the task file") + } + + var sb strings.Builder + sb.WriteString(`func main() {var t T;`) + for _, taskName := range tasks { + name := exprHyphenated.ReplaceAllStringFunc(taskName, strings.ToUpper) + name = strings.ReplaceAll(name, "-", "") + + sb.WriteString(fmt.Sprintf(`t.Cd("%s");`, wd)) + sb.WriteString(fmt.Sprintf(`if err := Task%s(&t); err != nil {`, name)) + sb.WriteString(`fmt.Fprintln(os.Stderr);`) + sb.WriteString(`exitCode := 1;`) + sb.WriteString(`if exitErr, ok := err.(*exec.ExitError); ok {`) + sb.WriteString(`exitCode = exitErr.ExitCode()`) + sb.WriteString(`} else {`) + sb.WriteString(`fmt.Fprintf(os.Stderr, "Error: %v\n", err)`) + sb.WriteString(`};`) + sb.WriteString(fmt.Sprintf(`fmt.Fprintln(os.Stderr, "Task '%s' failed");`, taskName)) + sb.WriteString(`os.Exit(exitCode)`) + sb.WriteString(`};`) + } + sb.WriteRune('}') + + var ( + exprMain = regexp.MustCompile(`func main\(\) \{\n([^\n]*\n)+\}`) + exprUnusedImport = regexp.MustCompile(` "go/[a-z]*"\n`) + ) + + runner := exprMain.ReplaceAll(original, []byte(sb.String())) + runner = exprUnusedImport.ReplaceAll(runner, []byte{}) + return runner, nil + } + run = func(tasks []string) (int, error) { + runner, err := build(tasks) + if err != nil { + return 2, err + } + + wd, err := os.MkdirTemp(os.TempDir(), "go-task-*") + if err != nil { + return 2, errors.New("could not create a temporary working directory") + } + defer os.RemoveAll(wd) + + workerBin := fmt.Sprintf("%s%ctask-runner", wd, os.PathSeparator) + workerSrc := workerBin + ".go" + os.WriteFile(workerSrc, runner, 0o666) + + cmd := exec.Command("go", "build", "-o", workerBin, workerSrc) + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + if err := cmd.Run(); err != nil { + return 2, fmt.Errorf("could not build the task runner: %v", err) + } + + cmd = exec.Command(workerBin) + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + if err := cmd.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return exitErr.ExitCode(), nil + } else { + return 2, fmt.Errorf("unexpected execution error: %v", err) + } + } + + return 0, nil + } + ) + + tasks, err := parse() + if err != nil { + fmt.Fprintf(os.Stderr, "Syntax error: %s\n", err) + os.Exit(2) + } + + if len(os.Args) < 2 { + fmt.Println("usage:\n go run tasks.go [task2...]") + fmt.Println() + fmt.Println("tasks:") + for _, task := range tasks { + fmt.Printf(" %s\n %s\n", task.name, task.desc) + } + + os.Exit(0) + } + + for _, taskName := range os.Args[1:] { + found := false + for _, task := range tasks { + found = (taskName == task.name) || found + } + + if !found { + fmt.Fprintf(os.Stderr, "Task not found: %q\n", taskName) + os.Exit(2) + } + } + + exitCode, err := run(os.Args[1:]) + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + + os.Exit(exitCode) +} diff --git a/tools.go b/tools.go index a1ceaf8..278f3b2 100644 --- a/tools.go +++ b/tools.go @@ -19,7 +19,7 @@ //go:build tools -package ades +package main import ( _ "4d63.com/gochecknoinits" diff --git a/web/Makefile b/web/Makefile deleted file mode 100644 index 04e9745..0000000 --- a/web/Makefile +++ /dev/null @@ -1,54 +0,0 @@ -# MIT No Attribution -# -# Copyright (c) 2024 Eric Cornelissen -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -.PHONY: default -default: - @printf "Usage: make \n\n" - @printf "Commands:\n" - @awk -F ':(.*)## ' '/^[a-zA-Z0-9%\\\/_.-]+:(.*)##/ { \ - printf " \033[36m%-30s\033[0m %s\n", $$1, $$NF \ - }' $(MAKEFILE_LIST) - -.PHONY: build -build: wasm_exec.js ## Build the webapp - @echo 'Building...' - @GOOS=js GOARCH=wasm go build \ - -o ades.wasm - @cp ../COPYING.txt ./COPYING.txt - -.PHONY: clean -clean: ## Clean the webapp directory - @echo 'Cleaning...' - @git clean -fx \ - node_modules/ \ - *.wasm \ - COPYING.txt \ - wasm_exec.js - -.PHONY: serve -serve: build node_modules ## Serve the webapp locally - @echo 'Serving locally...' - @npx http-server . \ - --port 8080 - -node_modules: .npmrc package*.json - @npm clean-install - -wasm_exec.js: - @cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" ./wasm_exec.js