diff --git a/.codacy.yaml b/.codacy.yaml new file mode 100644 index 0000000..38a96a7 --- /dev/null +++ b/.codacy.yaml @@ -0,0 +1,8 @@ +engines: + duplication: + minTokenMatch: 50 + exclude_paths: + - "**/*_test.go" + revive: + exclude_paths: + - "**/mock_*_test.go" diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml new file mode 100644 index 0000000..9fa78aa --- /dev/null +++ b/.github/workflows/go.yaml @@ -0,0 +1,31 @@ +name: Go +on: [push, pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: ^1.21 + + - name: Build and tests + env: + CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} + run: make --file=Makefile.base --trace all + + - name: Send coverage report + uses: shogo82148/actions-goveralls@v1 + with: + path-to-profile: ./build/test-all.cover + + - name: Release and publish + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + env: + GH_TOKEN: ${{ github.token }} + run: |- + make --file=Makefile.base --trace release && \ + make --file=Makefile.base --trace publish diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b03c62 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.project + +build +run + +mock_*_test.go diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..b66bdee --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,12 @@ +[extend] + useDefault = true + +[allowlist] + description = "Allowlist false positives" + regexTarget = "match" + regexes = [ + # Mark CDP build secret_version as false positive since it does not contain a secret. + '''(?i)(?:secret_version:)(?:['|\"|\s]{0,5})([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:['|\"|\n|\r|\s]{0,5}|$)''', + # Mark api-id or api_id or x-api-id as false positive since they are uuids that are used in OAS and stacksets to track apis. + '''(?i)(?:api[_-]id(?:['|\"|\s|:]{0,5}))(?:['|\"|\s]{0,5})([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:['|\"|\n|\r|\s]{0,5}|$)''' + ] diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..fe2c6f6 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,198 @@ +run: + # Timeout for analysis, e.g. 30s, 5m. + # Default: 1m + timeout: 3m + +linters: {} + # Placeholder for dynamically enabled linters. + +issues: + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 (we recommend 10-20) + max-same-issues: 10 + + # Use default exclusions for common false positives as defined in + # https://golangci-lint.run/usage/false-positives/#default-exclusions + # Default: true (we use false to sync behavior with Codacy) + exclude-use-default: false + + # Defining manually exclusions that make sense. + exclude-rules: + # Exclude go:generate directives from line length checking. + - source: "^//\\s*go:generate\\s" + linters: [ lll, revive ] + # Exclude magic number in time setups and bit shifting. + - source: "[0-9]+ ?\\* ?time\\.|(<<|>>) ?[0-9]+|[0-9]+ ?(<<|>>)" + linters: [ gomnd ] + # Exclude certain standards from being applied in test. + - path: "_test\\.go" + linters: [ bodyclose, contextcheck, dupl, funlen, goconst, gosec, noctx, + goerr113, wrapcheck ] + + # Exclude error return value check because of too many false positives. + - text: 'Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked' + linters: [ errcheck ] + # Exclude certain revive standards from being applied in tests. + - path: "_test\\.go" + text: "^(max-public-structs|function-length|cognitive-complexity|unchecked-type-assertion):" + linters: [ revive ] + # Exclude dots in unfinished thoughts. + - source: "(noinspection|TODO)" + linters: [ godot ] + + +# This contains only configs which differ from defaults. For other configs see +# https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml +linters-settings: + cyclop: + # The maximal code complexity to report. + # Default: 10 (we allow up to 20) + max-complexity: 20 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled + # Default: 0.0 (we recommend 10.0 as baseline) + package-average: 10.0 + + gocognit: + # Minimal code complexity to report. + # Default: 30 (we recommend 10-20) + min-complexity: 20 + + lll: + # Max line length, lines longer will be reported. '\t' is counted as 1 + # character by default, and can be changed with the tab-width option. + # Default: 120 (we recommend 80 but compromise at 100) + line-length: 100 + # Tab width in spaces. + # Default: 1 (go uses 4 for visualization) + tab-width: 4 + + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] (but some lll does not need explanation) + allow-no-explanation: [ lll, wrapcheck ] + # Enable to require an explanation of nonzero length after each nolint + # directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being + # suppressed. + # Default: false + require-specific: true + + govet: + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + # Default: [] + disable: + - fieldalignment # too strict + - shadow # too strict to always work around + + gosec: + # To specify a set of rules to explicitly exclude. + # Available rules: https://github.com/securego/gosec#available-rules + # Default: [] (issues are fixed) + excludes: [ G307 ] + + gocritic: + # Settings passed to gocritic. The settings key is the name of a supported + # gocritic checker. The list of supported checkers can be find in + # https://go-critic.github.io/overview. + settings: + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + + tenv: + # The option `all` will run the linter on the whole test files regardless + # of method signatures. Otherwise, only methods that take `*testing.T`, + # `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + + revive: + # Enable all available rules. + # Default: false + enable-all-rules: true + # When set to false, ignores files with "GENERATED" header, similar to golint. + # See https://github.com/mgechev/revive#available-rules for details. + # Default: false + ignore-generated-header: true + # Sets the default severity. + # See https://github.com/mgechev/revive#configuration + # Default: warning + severity: error + + rules: + # No need to enforce a file header. + - name: file-header + disabled: true + # Reports on each file in a package. + - name: package-comments + disabled: true + # Reports on comments not matching the name as first word. + - name: exported + disabled: true + # Ident error flow is buggy and throws false alerts. + - name: indent-error-flow + disabled: true + # No need to exclude import shadowing. + - name: import-shadowing + disabled: true + # Restricted alias naming conflicts with '.'-imports. + - name: import-alias-naming + disabled: true + # Exluding '.'-import makes test package separation unnecessary difficult. + - name: dot-imports + disabled: true + # Fails to exclude nolint directives from reporting. + - name: comment-spacings + disabled: true + # Fails to disable writers that actually cannot return errors. + - name: unhandled-error + disabled: true + # Fails to restrict sufficiently in switches with numeric values. + - name: add-constant + disabled: true + # Rule prevents intentional usage of similar variable names. + - name: flag-parameter + disabled: true + # Rule prevents intentional usage of similar private method names. + - name: confusing-naming + disabled: true + + # Enables a more experienced cyclomatic complexity (we enabled a lot of + # rules to counter-act the complexity trap). + - name: cyclomatic + arguments: [20] + # Enables a more experienced cognitive complexity (we enabled a lot of + # rules to counter-act the complexity trap). + - name: cognitive-complexity + arguments: [20] + # Limit line-length to increase readability. + - name: line-length-limit + arguments: [100] + # We are a bit more relaxed with function length consistent with funlen. + - name: function-length + arguments: [40, 60] + # Limit arguments of functions to the maximum understandable value. + - name: argument-limit + arguments: [6] + # Limit results of functions to the maximum understandable value. + - name: function-result-limit + arguments: [4] + # Raise the limit a bit to allow more complex package models. + - name: max-public-structs + arguments: [8] + # I do not know what I'm doing here... + - name: banned-characters + arguments: ["Ω", "Σ", "σ"] diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..5769f66 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,35 @@ +# Default state for all rules +default: true + +# MD012/no-multiple-blanks - Multiple consecutive blank lines +MD012: + # Consecutive blank lines + maximum: 3 + +# MD013/line-length - Line length +MD013: + # Number of characters + line_length: 80 + # Number of characters for headings + heading_line_length: 80 + # Number of characters for code blocks + code_block_line_length: 80 + # Include code blocks + code_blocks: true + # Include tables + tables: true + # Include headings + headings: true + # Include headings + headers: true + # Strict length checking + strict: true + # Stern length checking + stern: false + +# MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines +MD022: + # Blank lines above heading + lines_above: 2 + # Blank lines below heading + lines_below: 1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..20825cb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Tronje Krop + +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 above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/MANUAL.md b/MANUAL.md new file mode 100644 index 0000000..1942203 --- /dev/null +++ b/MANUAL.md @@ -0,0 +1,424 @@ +# `go-make` Manual + + +## Setup and customization + +The [Makefile](Makefile) is using sensitive defaults that are supposed to work +out-of-the-box for most targets. Please see documentation of the target groups +for more information on setup and customization: + +* [Standard targets](#standard-targets) +* [Test targets](#test-targets) +* [Linter targets](#linter-targets) +* [Build targets](#install-targets) +* [Image targets](#image-targets) +* [Release targets](#run-targets) +* [Install targets](#install-targets) +* [Uninstall targets](#uninstall-targets) +* [Release targets](#release-targets) +* [Update targets](#update-targets) +* [Cleanup targets](#cleanup-targets) +* [Init targets](#init-targets) (usually no need to call) + +**Note:** To see an overview of actual targets, use the shell auto-completion +or run `make targets`. To get a short help on most important targets and target +families run `make help`. + +To customize the behavior of the Makefile there exist multiple extension points +that can be used to setup additional variables, definitions, and targets that +modify the behavior of the [Makefile](Makefile). + +* [Makefile.vars](Makefile.vars) allows to modify the behavior of standard + targets by customizing and defining additional variables (see section + [Modifying variables](#modifying-variables) for more details). +* [Makefile.defs](Makefile.defs) allows to customize the runtime environment + for executing of commands (see Section [Running commands](#running-commands) + for more details). +* [Makefile.targets](Makefile.targets) is an optional extension point that + allows to define arbitrary custom targets. + + +### Modifying variables + +While there exist sensible defaults for all configurations variables, some of +them might need to be adjusted. The following list provides an overview of the +most prominent ones + +```Makefile +# Setup code quality level (default: base). +CODE_QUALITY := plus +# Setup codacy integration (default: enabled [enabled, disabled]). +CODACY := enabled + +# Setup required targets before testing (default: ). +TEST_DEPS := run-db +# Setup required targets before running commands (default: ). +RUN_DEPS := run-db +# Setup required aws services for testing (default: ). +AWS_SERVICES := + +# Setup when to push images (default: pulls [never, pulls, merges]) +IMAGE_PUSH ?= never + +# Setup default test timeout (default: 10s). +TEST_TIMEOUT := 15s + +# Setup custom delivery file (default: delivery.yaml). +FILE_DELIVERY := delivery-template.yaml +# Setup custom local build targets (default: init test lint build). +TARGETS_ALL := init delivery test lint build + +# Custom linters applied to prepare next level (default: ). +LINTERS_CUSTOM := nonamedreturns gochecknoinits tagliatelle +# Linters swithed off to complete next level (default: ). +LINTERS_DISABLED := +``` + +You can easily lookup a list using `grep -r " ?= " Makefile`, however, most +will not be officially supported unless mentioned in the above list. + + +### Running commands + +To `run-*` commands as expected, you need to setup the environment variables +for your designated runtime by defining the custom functions for setting it up +via `run-setup`, `run-vars`, `run-vars-local`, and `run-vars-image` in +[Makefile.defs](Makefile.defs). + +While tests are supposed to run with global defaults and test specific config, +the setup of the `run-*` commands strongly depends on the commands execution +context and its purpose. Still, there are common patterns that can be copied +from other commands and projects. + +To enable postgres database support you must add `run-db` to `TEST_DEPS` and +`RUN_DEPS` variables to [Makefile.vars](Makefile.vars). + +You can also override the default setup via the `DB_HOST`, `DB_PORT`, `DB_NAME`, +`DB_USER`, and `DB_PASSWORD` variables, but this is optional. + +**Note:** when running test against a DB you usually have to extend the default +`TEST_TIMEOUT` of 10s to a less aggressive value. + +To enable AWS localstack you have to add `run-aws` to the default`TEST_DEPS` and +`RUN_DEPS` variables, as well as to add your list of required aws services to +the `AWS_SERVICES` variable. + +```Makefile +# Setup required targets before testing (default: ). +TEST_DEPS := run-aws +# Setup required targets before running commands (default: ). +RUN_DEPS := run-aws +# Setup required aws services for testing (default: ). +AWS_SERVICES := s3 sns +``` + +**Note:** Currently, the [Makefile](Makefile) does not support all command-line +arguments since make swallows arguments starting with `-`. To compensate this +shortcoming the commands need to support setup via command specific environment +variables following the principles of the [Twelf Factor App][12factor]. + +[12factor]: https://12factor.net/ + + +## Standard targets + +The [Makefile](Makefile) supports the following often used standard targets. + +```bash +make all # short cut target to init, test, and build binaries locally +make all-clean # short cut target to clean, init, test, and build binaries +make commit # short cut target to execute pre-commit lint and test steps +``` + +The short cut targets can be customized by setting up the variables `TARGETS_*` +(in upper letters), according to your preferences in `Makefile.vars` or in your ++environment. + +Other less customizable commands are targets to build, install, delete, and +cleanup project resources: + +```bash +make test # short cut to execute default test targets +make lint # short cut to execute default lint targets +make build # creates binary files of commands +make clean # removes all resource created during build +``` + +While these targets allow to execute the most important tasks out-of-the-box, +there exist a high number of specialized (sometimes project specific) commands +that provide more features with quicker response times for building, testing, +releasing, and executing of components. + +**Note:** All targets automatically trigger their preconditions and install the +latest version of the required tools, if some are missing. To enforce the setup +of a new tool, you need to run `make init` explicitly. + +The following targets are helpful to investigate the [Makefile](Makefile): + +```bash +make help # prints a short help about major target (families) +make targets # prints a list of all available targets +make show # shows the effective target implementation +``` + + +### Test targets + +Often it is more efficient or even necessary to execute the fine grained test +targets to complete a task. + +```bash +make test # short cut to execute default test targets +make test-all # executes the complete tests suite +make test-unit # executes only unit tests by setting the short flag +make test-self # executes a self-test of the build scripts +make test-cover # opens the test coverage report in the browser +make test-upload # uploads the test coverage files +make test-clean # cleans up the test files +make test-go # test go versions +``` + +In addition, it is possible to restrict test target execution to packages, +files and test cases as follows: + +* For a single package use `make test-(unit|all) ...`. +* For a single test file `make test[-(unit|all) /_test.go ...`. +* For a single test case `make test[-(unit|all) / ...`. + +The default test target can be customized by defining the `TARGETS_TEST` +variable in `Makefile.vars`. Usually this is not necessary. + + +### Linter targets + +The [Makefile](Makefile) supports different targets that help with linting +according to different quality levels, i.e. `min`,`base` (default), `plus`, +`max`, (and `all`) as well as automatically fixing the issues. + +```bash +make lint # short cut to execute default lint targets +make lint-min # lints the go-code using a minimal config +make lint-base # lints the go-code using a baseline config +make lint-plus # lints the go-code using an advanced config +make lint-max # lints the go-code using an expert config +make lint-all # lints the go-code using an insane all-in config +make lint-codacy # lints the go-code using codacy client side tools +make lint-markdown # lints the documentation using markdownlint +make lint-revive # lints the go-code using the revive standalone linter +make lint-shell # lints the sh-code using shellcheck to find issues +make lint-leaks # lints committed code using gitleaks for leaked secrets +make lint-leaks? # lints un-committed code using gitleaks for leaked secrets +make lint-vuln # lints the go-code using govulncheck to find vulnerabilities +make lint-api # lints the api specifications in '/zalando-apis' +``` + +The default target for `make lint` is determined by the selected `CODE_QUALITY` +level (`min`, `base`, `plus`, and `max`), and the `CODACY` setup (`enabled`, +`disabled`). The default setup is to run the targets `lint-base`, `lint-apis`, +`lint-markdown`, and `lint-codacy`. It can be further customized via changing +the `TARGETS_LINT` in `Makefile.vars` - if necessary. + +The `lint-*` targets for `golangci-lint` allow some command line arguments: + +1. The keyword `fix` to lint with auto fixing enabled (when supported), +2. The keyword `config` to shows the effective linter configuration, +3. The keyword `linters` to display the linters with description, or +4. `,...` comma separated list of linters to enable for a quick checks. + +The default linter config is providing a golden path with different levels +out-of-the-box, i.e. a `min` for legacy code, `base` as standard for active +projects, and `plus` for experts and new projects, and `max` enabling all +but the conflicting disabled linters. Besides, there is an `all` level that +allows to experience the full linting capability. + +Independent of the golden path this setting provides, the lint expert levels +can be customized in three ways. + +1. The default way to customize linters is adding and removing linters for all + levels by setting the `LINTERS_CUSTOM` and `LINTERS_DISABLED` variables + providing a white space separated list of linters. +2. Less comfortable and a bit trickier is the approach to override the linter + config variables `LINTERS_DISCOURAGED`, `LINTERS_DEFAULT`, `LINTERS_MINIMUM`, + `LINTERS_BASELINE`, and `LINTERS_EXPERT`, to change the standards. +3. Last the linter configs can be changed via `.golangci.yaml`, as well as + via `.codacy.yaml`, `.markdownlint.yaml`, and `revive.toml`. + +However, customizing `.golangci.yaml` and other config files is currently not +advised, since the `Makefile` is designed to update and enforce a common +version of all configs on running `update-*` targets. + + +### Build targets + +The build targets can build native as well as linux platform executables using +the default system architecture. + +```bash +make build # builds default executables (native) +make build-native # builds native executables using system architecture +make build-linux # builds linux executable using the default architecture +make build-image # builds container image (alias for image-build) +``` + +The platform and architecture of the created executables can be customized via +`BUILDOS` and `BUILDARCH` environment variables. + + +### Image targets + +Based on the convention that all binaries are installed in a single container +image, the [Makefile](Makefile) supports to create and push the container image +as required for a pipeline. + +```bash +make image # short cut for 'image-build' +make image-build # build a container image after building the commands +make image-push # pushes a container image after building it +``` + +The targets are checking silently whether there is an image at all, and whether +it should be build and pushed according to the pipeline setup. You can control +this behavior by setting `IMAGE_PUSH` to `never` or `test` to disable pushing +(and building) or enable it in addition for pull requests. Any other value will +ensure that images are only pushed for `main`-branch and local builds. + + +### Run targets + +The [Makefile](Makefile) supports targets to startup a common DB and a common +AWS container image as well as to run the commands provided by the repository. + +```bash +make run-db # runs a postgres container image to provide a DBMS +make run-aws # runs a localstack container image to simulate AWS +make run-* # runs the matched command using its before build binary +make run-go-* # runs the matched command using 'go run' +make run-image-* # runs the matched command in the container image +make run-clean # kills and removes all running container images +make run-clean-* # kills and removes the container image of the matched command +``` + +To run commands successfully the environment needs to be setup to run the +commands in its runtim. Please visit [Running commands](#running-commands) for +more details on how to do this. + +**Note:** The DB (postgres) and AWS (localstack) containers can be used to +support any number of parallel applications, if they use different tables, +queues, and buckets. Developers are encouraged to continue with this approach +and only switch application ports and setups manually when necessary. + + +### Update targets + +The [Makefile](Makefile) supports targets for common update tasks for package +versions, for build, test, and linter tools, and for configuration files. + +```bash +make update # short cut for 'update-{go,deps,make}' +make update-all # short cut to execute all update targets +make update-go # updates the go version to the current compiler version +make update-deps # updates the project dependencies to the latest version +make update-make # updates the the build environment to the latest version +make update-tools # updates the project tools to the latest versions +``` + +Many update targets support a version with `?`-suffix to test whether an +update is available instead of executing it directly. In addition, a `major` +command line option can be used to also apply major version upgrades. + + +### Cleanup targets + +The [Makefile](Makefile) is designed to clean up everything it has created by +executing the following targets. + +```bash +make clean # short cut for clean-init, clean-build +make clean-all # cleans up all resources, i.e. also tools installed +make clean-init # cleans up all resources created by init targets +make clean-hooks # cleans up all resources created by init-hooks targets +make clean-build # cleans up all resources created by build targets +make clean-run # cleans up all running container images +make clean-run-* # cleans up matched running container image +``` + + +### Install targets + +The install targets installs the latest build version of a command in the +`${GOPATH}/bin` directory for global command line execution. Usually commands +used by the project are installed automatically. + +```bash +make install # installs all software created by this project +make install-all # installs all software created by this project +make install-* # installs the matched software command or service +``` + +If a command, service, job has not been build before, it is first build. + +**Note:** Please use carefully, if your project uses common command names. + + +### Uninstall targets + +The uninstall targets remove the latest installed command from `${GOPATH}/bin`. +A full uninstall of commands used by the project can also be triggered by +`clean-all`. + +```bash +make uninstall # uninstalls all software created by this project +make uninstall-all # uninstalls all software created or used by this project +make uninstall-* # uninstalls the matched software command or service +``` + +**Note:** Please use carefully, if your project uses common command names. + + +### Release targets + +Finally, the [Makefile](Makefile) supports targets for releasing the provided +packages as library. + +```bash +make bump # bumps version to prepare a new release +make release # creates the release tags in the repository +``` + + +### Init targets + +The [Makefile](Makefile) supports initialization targets that are added as +perquisites for targets that require them. So there is usually no need to call +them manually. + + +```bash +make init # short cut for 'init-tools init-hooks init-packages' +make init-codacy # initializes the tools for running the codacy targets +make init-hooks # initializes github hooks for pre-commit, etc +make init-packages # initializes and downloads packages dependencies +make init-sources # initializes sources by generating mocks, etc +``` + + +## Compatibility + +This [Makefile](Makefile) is making extensive use of GNU tools but is supposed +to be compatible to all recent Linux and MacOS versions. Since MacOS is usually +a couple of years behind in applying the GNU standard tools, we document the +restrictions this creates here. + + +### `sed` in place substitution + +In MacOS we need to add `-e ''` after `sed -i` since else the command +section is not automatically restricted to a single argument. In linux this +restriction is automatically applied to the first argument. + + +### `realpath` not supported + +In MacOS we need to use `readlink -f` instead of `realpath`, since there may +not even be a simplified fallback of this command available. This is not the +preferred command in Linux, but for compatibility would be still acceptable. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1d245ff --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +SHELL := /bin/bash + +GOBIN ?= $(shell go env GOPATH)/bin +GOMAKE ?= github.com/tkrop/go-make@latest +TARGETS := $(shell command -v go-make >/dev/null || \ + go install $(GOMAKE) && go-make targets) + + +# Include custom variables to modify behavior. +ifneq ("$(wildcard Makefile.vars)","") + include Makefile.vars +else + $(warning warning: please customize variables in Makefile.vars) +endif + + +# Include standard targets from go-make providing group targets as well as +# single target targets. The group target is used to delegate the remaining +# request targets, while the single target can be used to define the +# precondition of custom target. +.PHONY: $(TARGETS) $(addprefix target/,$(TARGETS)) +$(TARGETS):; $(GOBIN)/go-make $(MAKEFLAGS) $(MAKECMDGOALS); +$(addprefix target/,$(TARGETS)): target/%: + $(GOBIN)/go-make $(MAKEFLAGS) $*; + + +# Include custom targets to extend scripts. +ifneq ("$(wildcard Makefile.ext)","") + include Makefile.ext +endif diff --git a/Makefile.base b/Makefile.base new file mode 100644 index 0000000..f40d408 --- /dev/null +++ b/Makefile.base @@ -0,0 +1,1320 @@ +# === do not change this Makefile! === +# See MAKEFILE.md for documentation. + +SHELL := /bin/bash + +DIR_RUN := $(CURDIR)/run +DIR_CRED := $(DIR_RUN)/creds +DIR_BUILD := $(CURDIR)/build +DIR_TOOLS := $(DIR_BUILD)/tools +DIR_CONFIG := $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) +FIND := find . ! -path "./run/*" ! -path "./build/*" +FILTER := sed "s|^./||" | grep -E "^[a-z0-9/_-]*\.go$$" + +# Include custom variables to modify behavior. +ifneq ("$(wildcard Makefile.vars)","") + include Makefile.vars +else + $(warning info: please customize variables in Makefile.vars) +endif + +# Setup sensible defaults for configuration variables. +CODE_QUALITY ?= base +TEST_TIMEOUT ?= 10s + +# Function to search default config files +search-default = $(firstword $(wildcard $(1)) $(wildcard $(DIR_CONFIG)/$(1))) + +FILE_MAKE ?= $(call search-default,Makefile) +FILE_VARS ?= $(call search-default,Makefile.vars) +FILE_GOLANGCI ?= $(call search-default,.golangci.yaml) +FILE_MARKDOWN ?= $(call search-default,.markdownlint.yaml) +FILE_GITLEAKS ?= $(call search-default,.gitleaks.toml) +FILE_REVIVE ?= $(call search-default,revive.toml) +FILE_CODACY ?= $(call search-default,.codacy.yaml) + +FILE_CONTAINER ?= Dockerfile +FILE_DELIVERY ?= delivery.yaml +FILE_DELIVERY_REGEX ?= (cdp-runtime\/go-|go-version: \^?)[0-9.]* + +REPOSITORY ?= $(shell git remote get-url origin | \ + sed "s/^https:\/\///; s/^git@//; s/.git$$//; s/:/\//") +GITHOSTNAME ?= $(word 1,$(subst /, ,$(REPOSITORY))) +GITORGNAME ?= $(word 2,$(subst /, ,$(REPOSITORY))) +GITREPONAME ?= $(word 3,$(subst /, ,$(REPOSITORY))) + +TEAM ?= $(shell cat .zappr.yaml | grep "X-Zalando-Team" | \ + sed "s/.*:[[:space:]]*\([a-z-]*\).*/\1/") + + +TOOLS_NPM := $(TOOLS_NPM) \ + markdownlint-cli +TOOLS_GO := $(TOOLS_GO) \ + github.com/golangci/golangci-lint/cmd/golangci-lint \ + github.com/zalando/zally/cli/zally \ + golang.org/x/vuln/cmd/govulncheck \ + github.com/uudashr/gocognit/cmd/gocognit \ + github.com/fzipp/gocyclo/cmd/gocyclo \ + github.com/mgechev/revive@v1.2.3 \ + github.com/securego/gosec/v2/cmd/gosec \ + github.com/tsenart/deadcode \ + github.com/tsenart/vegeta \ + honnef.co/go/tools/cmd/staticcheck \ + github.com/zricethezav/gitleaks/v8 \ + github.com/icholy/gomajor \ + github.com/golang/mock/mockgen \ + github.com/tkrop/go-testing/cmd/mock \ + github.com/tkrop/go-make +TOOLS_SH := $(TOOLS_SH) \ + github.com/anchore/syft \ + github.com/anchore/grype + +# function for correct gol-package handling. +go-pkg = $(shell awk -v mode="$(1)" -v filter="$(3)" -v not="$(4)" ' \ + BEGIN { FS = "[/@]"; RS = "[ \n\r]" } { \ + field = NF; \ + if (version = index($$0, "@")) { field-- } \ + if ($$(field) ~ "v[0-9]+") { field-- } \ + if (!filter || \ + (not && ($$(field) !~ filter)) || \ + (!not && ($$(field) ~ filter))) { \ + if (mode == "cmd") { \ + if (!($$(field) in map)) { \ + print $$(field); map[$$(field)]++ \ + } \ + } else if (mode == "install" || mode == "update") { \ + if (!((package = strip(version)) in map)) { \ + if (version) { print $$0; } else { print $$0 "@latest" } \ + map[package]++ \ + } \ + } else if (mode == "strip") { \ + if (!((package = strip(version)) in map)) { \ + print package; map[package]++ \ + } \ + } \ + } \ + } \ + function strip(version) { \ + if (version) { \ + return substr($$0, 1, index($$0, "@") - 1) \ + } else { return $$0 } \ + }' <<<"$(2)") + + +VERSION ?= snapshot +ifeq ($(wildcard VERSION),VERSION) + BUILD_VERSION := $(cat VERSION) +else + BUILD_VERSION ?= $(VERSION) +endif + +IMAGE_PUSH ?= pulls +IMAGE_VERSION ?= $(VERSION) + +ifeq ($(words $(subst /, ,$(IMAGE_NAME))),3) + IMAGE_HOST ?= $(word 1,$(subst /, ,$(IMAGE_NAME))) + IMAGE_TEAM ?= $(word 2,$(subst /, ,$(IMAGE_NAME))) + IMAGE_ARTIFACT ?= $(word 3,$(subst /, ,$(IMAGE_NAME))) +else + IMAGE_HOST ?= pierone.stups.zalan.do + IMAGE_TEAM ?= $(TEAM) + IMAGE_ARTIFACT ?= $(GITREPONAME) +endif +IMAGE ?= $(IMAGE_HOST)/$(IMAGE_TEAM)/$(IMAGE_ARTIFACT):$(IMAGE_VERSION) + + +DB_HOST ?= 127.0.0.1 +DB_PORT ?= 5432 +DB_NAME ?= db +DB_USER ?= user +DB_PASSWORD ?= pass +DB_VERSION ?= latest +DB_IMAGE ?= postgres:$(DB_VERSION) + +AWS_SERVICES ?= sqs s3 +AWS_VERSION ?= latest +AWS_IMAGE ?= localstack/localstack:$(AWS_VERSION) + +# Setup codacy integration. +CODACY ?= enabled +ifdef CDP_PULL_REQUEST_NUMBER + CODACY_CONTINUE ?= true +else + CODACY_CONTINUE ?= false +endif +CODACY_PROVIDER ?= ghe +CODACY_USER ?= $(GITORGNAME) +CODACY_PROJECT ?= $(GITREPONAME) +CODACY_API_BASE_URL ?= https://codacy.bus.zalan.do +CODACY_CLIENTS ?= aligncheck deadcode +CODACY_BINARIES ?= gosec staticcheck +CODACY_GOSEC_VERSION ?= 0.4.5 +CODACY_STATICCHECK_VERSION ?= 3.0.12 + +# Function for conversion of variables name. +upper = $(shell echo "$(1)" | tr '[:lower:]' '[:upper:]') + + +# Default target list for all and cdp builds. +TARGETS_ALL ?= init test lint build image +TARGETS_INIT ?= init-hooks init-go $(if $(filter $(CODACY),enabled),init-codacy,) +TARGETS_CLEAN ?= clean-build +TARGETS_UPDATE ?= update-go update-deps update-make +TARGETS_COMMIT ?= test-go test-unit lint-leaks? lint-$(CODE_QUALITY) lint-markdown +TARGETS_TEST ?= test-all $(if $(filter $(CODACY),enabled),test-upload,) +TARGETS_LINT ?= lint-leaks? lint-$(CODE_QUALITY) lint-markdown lint-apis \ + $(if $(filter $(CODACY),enabled),lint-codacy,) + +INIT_MAKE ?= $(FILE_MAKE) $(FILE_VARS) +UPDATE_MAKE ?= $(FILE_MAKE) $(FILE_GOLANGCI) $(FILE_MARKDOWN) \ + $(FILE_GITLEAKS) $(FILE_CODACY) $(FILE_REVIVE) + +# Setup go to use desired and consistent go versions. +GOVERSION := $(shell go version | sed -E "s/.*go([0-9]+\.[0-9]+).*/\1/") + +# Export private repositories not to be downloaded. +export GOPRIVATE := github.bus.zalan.do +export GOBIN ?= $(shell go env GOPATH)/bin + + +# General setup of tokens for run-targets (not to be modified) + +# Often used token setup functions. +run-token-create = \ + ztoken > $(DIR_CRED)/token; echo "Bearer" > $(DIR_CRED)/type +run-token-link = \ + test -n "$(1)" && test -n "$(2)" && test -n "$(3)" && ( \ + test -h "$(DIR_CRED)/$(1)-$(2)" || ln -s type "$(DIR_CRED)/$(1)-$(2)" && \ + test -h "$(DIR_CRED)/$(1)-$(3)" || ln -s token "$(DIR_CRED)/$(1)-$(3)" \ + ) || test -n "$(1)" && test -n "$(2)" && test -z "$(3)" && ( \ + test -h "$(DIR_CRED)/$(1)" || ln -s type "$(DIR_CRED)/$(1)" && \ + test -h "$(DIR_CRED)/$(2)" || ln -s token "$(DIR_CRED)/$(2)" \ + ) || test -n "$(1)" && test -z "$(2)" && test -z "$(3)" && ( \ + test -h "$(DIR_CRED)/$(1)" || ln -s token "$(DIR_CRED)/$(1)" \ + ) || true + +# Stub function for general setup in run-targets. +run-setup = true +# Stub function for common variables in run-targets. +run-vars = +# Stub function for local runtime variables in run-targets. +run-vars-local = +# Stub function for container specific runtime variables in run-targets. +run-vars-image = $(call run-vars-docker) +# Stub function to set up aws localstack run-target. +run-aws-setup = true + +ifneq ("$(wildcard Makefile.defs)","") + $(info info: please define custom functions in Makefile.defs) + include Makefile.defs +endif + + +# Setup default environment variables. +SOURCES := $(shell $(FIND) -name "*.go" ! -name "mock_*_test.go" | $(FILTER)) +MODULES := $(shell echo $(SOURCES) | xargs -r dirname | sort -u) +COMMANDS := $(shell $(FIND) -name "main.go" | xargs -r readlink -f | \ + sed -E "s/^.*\/([^/]*)\/main.go$$/\1/" | sort -u) +COMMANDS_PKG := $(shell $(FIND) -name "main.go" | xargs -r readlink -f | \ + sed -E "s/^(.*\/([^/]*))\/main.go$$/\2=\1/; s|$(CURDIR)|.|") +COMMANDS_REGEX := $(shell echo "^$(COMMANDS)$$" | tr ' ' '|' ) +COMMANDS_GO := $(call go-pkg,cmd,$(TOOLS_GO),$(COMMANDS_REGEX),not) +COMMANDS_SH := $(call go-pkg,cmd,$(TOOLS_SH),$(COMMANDS_REGEX),not) + + +# Setup optimized golang mock setup environment. +MOCK_MATCH_DST := ^(.*)/(.*)://go:generate.*-destination=([^ ]*).*$$ +MOCK_MATCH_SRC := ^(.*)/(.*)://go:generate.*-source=([^ ]*).*$$ +MOCK_TARGETS := $(shell grep "//go:generate[[:space:]]*mockgen" $(SOURCES) | \ + sed -E "s|^(.*)|./\1|; s|$(MOCK_MATCH_DST)|\1/\3=\1/\2|" | sort -u) +MOCK_SOURCES := $(shell grep "//go:generate[[:space:]]*mockgen.*-source=" $(SOURCES) | \ + sed -E "s|^(.*)|./\1|; s|$(MOCK_MATCH_SRC)|\1/\3|" | sort -u | \ + xargs -r readlink -f | sed "s|$(PWD)/||g") +MOCKS := $(shell for TARGET in $(MOCK_TARGETS); \ + do echo "$${TARGET%%=*}"; done | sort -u) + +# Prepare phony make targets lists. +TARGETS_PUBLISH := $(addprefix publish-, all $(COMMANDS)) +TARGETS_TEST_INIT := test-clean init-sources init-hooks +TARGETS_INIT_MAKE := $(addprefix update/,$(INIT_MAKE)) +TARGETS_INIT_CODACY := $(addprefix init-, $(CODACY_BINARIES)) +TARGETS_LINT_CODACY_CLIENTS := $(addprefix lint-, $(CODACY_CLIENTS)) +TARGETS_LINT_CODACY_BINARIES := $(addprefix lint-, $(CODACY_BINARIES)) +TARGETS_IMAGE := $(shell $(FIND) -type f -name "$(FILE_CONTAINER)*" | sed 's|^./||') +TARGETS_IMAGE_BUILD := $(addprefix image-build/, $(TARGETS_IMAGE)) +TARGETS_IMAGE_PUSH := $(addprefix image-push/, $(TARGETS_IMAGE)) +TARGETS_BUILD := $(addprefix build-, $(COMMANDS)) +TARGETS_BUILD_LINUX := $(addprefix $(DIR_BUILD)/linux/, $(COMMANDS)) +TARGETS_INSTALL := $(addprefix install-, $(COMMANDS)) +TARGETS_INSTALL_GO := $(addprefix install-, $(COMMANDS_GO)) +TARGETS_INSTALL_SH := $(addprefix install-, $(COMMANDS_SH)) +TARGETS_INSTALL_NPM := $(addprefix install-, $(TOOLS_NPM:-cli=)) +TARGETS_INSTALL_ALL := $(TARGETS_INSTALL) $(TARGETS_INSTALL_GO) $(TARGETS_INSTALL_NPM) +TARGETS_UNINSTALL := $(addprefix uninstall-, $(COMMANDS)) +TARGETS_UNINSTALL_GO := $(addprefix uninstall-, $(COMMANDS_GO)) +TARGETS_UNINSTALL_SH := $(addprefix uninstall-, $(COMMANDS_SH)) +TARGETS_UNINSTALL_NPM := $(addprefix uninstall-, $(TOOLS_NPM:-cli=)) +TARGETS_UNINSTALL_CODACY := $(addprefix uninstall-codacy-, $(CODACY_BINARIES)) +TARGETS_UNINSTALL_ALL := $(TARGETS_UNINSTALL) $(TARGETS_UNINSTALL_GO) \ + $(TARGETS_UNINSTALL_NPM) $(TARGETS_UNINSTALL_CODACY) +TARGETS_RUN := $(addprefix run-, $(COMMANDS)) +TARGETS_RUN_GO := $(addprefix run-go-, $(COMMANDS)) +TARGETS_RUN_IMAGE := $(addprefix run-image-, $(COMMANDS)) +TARGETS_RUN_CLEAN := $(addprefix run-clean-, $(COMMANDS) db aws) +TARGETS_CLEAN_ALL := clean-init $(TARGETS_CLEAN) clean-run $(TARGETS_UNINSTALL_ALL) +TARGETS_CLEAN_RUN := $(addprefix clean-run-, $(COMMANDS) db aws) +TARGETS_UPDATE_GO := $(addprefix update-,$(COMMANDS_GO)) +TARGETS_UPDATE_MAKE := $(addprefix update/,$(UPDATE_MAKE)) +TARGETS_UPDATE_MAKE? := $(addsuffix ?,$(TARGETS_UPDATE_MAKE)) +TARGETS_UPDATE_ALL := update-go update-deps update-tools update-make +TARGETS_UPDATE_ALL? := udate-go? update-deps? update-make? +TARGETS_UPDATE? := $(filter $(addsuffix ?,$(TARGETS_UPDATE)), \ + $(TARGETS_UPDATE_ALL?) $(TARGETS_UPDATE_MAKE?)) + +# Setup phony make targets to always be executed. +.PHONY: all all-clean help list commit push fix bump release publish +.PHONY: init init-all init-go init-hooks init-sources +.PHONY: init-make init-make! $(TARGETS_INIT_MAKE) +.PHONY: init-codacy $(TARGETS_INIT_CODACY) +.PHONY: test test-all test-unit test-bench test-clean test-cover +.PHONY: test-upload test-go +.PHONY: lint lint-min lint-base lint-plus lint-max lint-all lint-config +.PHONY: lint-leaks lint-leaks? lint-vuln lint-gocognit lint-gocyclo +.PHONY: lint-markdown lint-apis lint-codacy lint-revive +.PHONY: $(TARGETS_LINT_CODACY_CLIENTS) $(TARGETS_LINT_CODACY_BINARIES) +.PHONY: build build-native build-linux build-image $(TARGETS_BUILD) +.PHONY: image image-build $(TARGETS_IMAGE_BUILD) +.PHONY: image-push $(TARGETS_IMAGE_PUSH) +.PHONY: install install-all $(TARGETS_INSTALL_ALL) +.PHONY: uninstall uninstall-all $(TARGETS_UNINSTALL_ALL) +.PHONY: run-native run-image run-clean $(TARGETS_RUN) run-db run-aws +.PHONY: $(TARGETS_RUN_GO) $(TARGETS_RUN_IMAGE) $(TARGETS_RUN_CLEAN) +.PHONY: clean clean-all $(TARGETS_CLEAN) +.PHONY: $(TARGETS_CLEAN_ALL) $(TARGETS_CLEAN_RUN) +.PHONY: update update? update-all update-all? +.PHONY: $(TARGETS_UPDATE_ALL) $(TARGETS_UPDATE_ALL?) $(TARGETS_UPDATE_GO) +.PHONY: $(TARGETS_UPDATE_MAKE) $(TARGETS_UPDATE_MAKE?) update-base + +# Setup docker or podman command. +IMAGE_CMD ?= $(shell command -v docker || command -v podman) +ifndef IMAGE_CMD + $(error error: docker/podman command not found) +endif + + +# Helper function to resolve match position of word in text. +findany = $(strip $(foreach word,$(1),$(findstring $(word),$(2)))) +pos-recurs = $(if $(findstring $(1),$(2)),$(call pos-recurs,$(1),\ + $(wordlist 2,$(words $(2)),$(2)),x $(3)),$(3)) +pos = $(words $(call pos-recurs,$(1),$(2))) + +# match commands that support arguments ... +CMDWORDS := targets bump release update test- lint run- +CMDMATCH = $(call findany,$(CMDWORDS),$(MAKECMDGOALS))% + +# If any argument contains "run*", "test*", "lint*", "bump" ... +ifneq ($(CMDMATCH),%) + CMD := $(filter $(CMDMATCH),$(MAKECMDGOALS)) + POS = $(call pos,$(CMD),$(MAKECMDGOALS)) + # ... then use the rest as arguments for "run/test-" ... + CMDARGS := $(wordlist $(shell expr $(POS) + 1),\ + $(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) + # ...and turn them into do-nothing targets. + $(eval $(CMDARGS):;@:) + RUNARGS ?= $(CMDARGS) + $(shell if [ -n "$(RUNARGS)" ]; then \ + echo "info: captured arguments [$(RUNARGS)]" >/dev/stderr; \ + fi) +endif + + +## Standard: default targets to test, lint, and build. + +#@ executes the default targets. +all: $(TARGETS_ALL) +#@ executes the default targets after cleaning up. +all-clean: clean clean-run all +#@ executes the pre-commit check targets. +commit: $(TARGETS_COMMIT) +$(DIR_BUILD) $(DIR_RUN) $(DIR_CRED): + @if [ ! -d "$@" ]; then mkdir -p $@; fi; + + +## Support: targets to support processes and users. + +#@ prints this help. +help: + @cat $(MAKEFILE_LIST) | awk ' \ + BEGIN { \ + printf("\n\033[1mUsage:\033[0m $(MAKE) \033[36m\033[0m\n") \ + } \ + /^## .*/ { \ + if (i = index($$0, ":")) { \ + printf("\n\033[1m%s\033[0m%s\n", \ + substr($$0, 4, i - 4), substr($$0, i)) \ + } else { \ + printf("\n\033[1m%s\033[0m\n", substr($$0, 4)) \ + } next \ + } \ + /^#@ / { \ + if (i = index($$0, ":")) { \ + printf(" \033[36m%-25s\033[0m %s\n", \ + substr($$0, 4, i - 4), substr($$0, i + 2)) \ + } else { line = substr($$0, 4) } next \ + } \ + /^[a-zA-Z_0-9?-]+:/ { \ + if (line) { \ + target = substr($$1, 1, length($$1) - 1); \ + if (i = index(line, " # ")) { \ + target = target " " substr(line, 1, i - 1); \ + line = substr(line, i + 3) \ + } \ + printf(" \033[36m%-25s\033[0m %s\n", target, line); \ + line = "" \ + } \ + }'; + +#@ show the effective makefile. +show: + @make --no-builtin-rules --no-builtin-variables --print-data-base \ + --question --makefile=$(firstword $(MAKEFILE_LIST)) 2>/dev/null || true; +#@ # list all available targets. +targets: + @make --no-builtin-rules --no-builtin-variables --print-data-base \ + --question --makefile=$(firstword $(MAKEFILE_LIST)) 2>/dev/null | \ + if [ "$(RUNARGS)" != "raw" ]; then awk -v RS= -F: ' \ + /(^|\n)# Files(\n|$$)/,/(^|\n)# Finished Make data base/ { \ + if ($$1 !~ "^[#.]") { print $$1 } \ + }' | sort | egrep -v -e '^[^[:alnum:]]'; \ + else cat -; fi || true; + +#@ pushes the current branch to the same branch of the origin. +push: + @git push --set-upstream origin \ + $$(git branch 2> /dev/null | sed -e '/^[^*]/d; s/* \(.*\)/\1/'); +#@ pushes the latest changes adding it to the previous commit of the origin. +fix: + @git commit --amend --no-edit && git push --force; +#@ pushes the latest changes adding it to the previous commit updating the message of the origin. +fix-update: + @git commit --amend && git push --force; + +#@ # update version and prepare release of the software. +bump: + @if [ -z "$(RUNARGS)" ]; then \ + echo "error: missing new version"; exit 1; \ + elif [[ "$(RUNARGS)" =~ "^[+~].*$$" ]]; then \ + VERSION="$$(awk -v op=$(RUNARGS) '{ \ + patsplit($$0, vers, "[0-9]*", seps); len = length(seps); \ + if (op ~ "+.*") { vers[length(op)]++ } else { vers[length(op)]-- } \ + for (i = 1; i < len; i++) { printf("%d%s", vers[i], seps[i]) } \ + }' VERSION)"; \ + elif ! [[ "$(RUNARGS)" =~ ^[0-9]+(\.[0-9]+){0,2}(-.*)?$$ ]]; then \ + echo "error: invalid new version [$(RUNARGS)]"; exit 1; \ + else VERSION="$(RUNARGS)"; fi; echo $${VERSION} >VERSION; \ + echo "Bumped version to $${VERSION} for auto release!"; \ + + +#@ # release a fixed version of the software as library. +release: + @if [ -f VERSION ]; then VERSION="$$(cat VERSION)"; fi; \ + if [[ "$(RUNARGS)" =~ ^[0-9]+(\.[0-9]+){0,2}(-.*)?$$ ]]; then \ + VERSION="$(RUNARGS)"; \ + fi; \ + if [ -n "$${VERSION}" -a -z "$$(git tag -l "v$${VERSION}")" ]; then \ + ( git gh-release "v$${VERSION}" ) || ( \ + git config user.name "$$(git log -n 1 --pretty=format:%an)" && \ + git config user.email "$$(git log -n 1 --pretty=format:%ae)" && \ + git tag --message "tag: new version $${VERSION}" "v$${VERSION}" && \ + git push --follow-tags origin "v$${VERSION}" \ + ) && echo "Added release tag v$${VERSION} to repository!"; \ + fi; \ + +publish: $(TARGETS_PUBLISH) +$(TARGETS_PUBLISH): publish-%: + @if [ "$*" != "all" ]; then \ + if [ -f "cmd/$*/main.go" ]; then CMD="/cmd/$*"; else exit 0; fi; \ + fi; \ + if [ -f VERSION ]; then VERSION="$$(cat VERSION)"; else \ + VERSION="$$(TZ=UTC0 git show --quiet --date='format-local:%Y%m%d%H%M%S' \ + --format="0.0.0-%cd-%H" | cut -c -33)"; \ + fi; \ + TARGET="$(REPOSITORY)$${CMD}@v$${VERSION}"; \ + if [ "$(GITHOSTNAME)" == "github.com" ]; then \ + if [ -z "$${CMD}" ]; then \ + echo "publish: curl $${TARGET}"; \ + curl --silent --show-error --fail --location \ + https://sum.golang.org/lookup/$${TARGET};\ + fi; \ + if [ -f ".$${CMD}/main.go" ]; then \ + echo "publish: go install $${TARGET}"; \ + go install $${TARGET}; \ + fi; \ + fi; + + +#@ install all software created by the project. +install: $(TARGETS_INSTALL) +#@ install all software created and used by the project. +install-all: $(TARGETS_INSTALL_ALL) +#@ install-*: install the matched software command or service. + +# install go tools used by the project. +$(TARGETS_INSTALL_GO): install-%: $(GOBIN)/% +$(addprefix $(GOBIN)/,$(COMMANDS_GO)): $(GOBIN)/%: + go install $(call go-pkg,install,$(TOOLS_GO),^$*$$); +# install go tools providing an install.sh script. +$(TARGETS_INSTALL_SH): install-%: $(GOBIN)/% +$(addprefix $(GOBIN)/,$(COMMANDS_SH)): $(GOBIN)/%: + @if ! command -v $*; then \ + curl --silent --show-error --fail --location \ + https://raw.githubusercontent.com/anchore/$*/main/install.sh | \ + sh -s -- -b $(GOBIN); \ + fi; +# install npm tools used by the project. +$(TARGETS_INSTALL_NPM): install-%: $(NVM_BIN)/% +$(addprefix $(NVM_BIN)/,$(TOOLS_NPM:-cli=)): $(NVM_BIN)/%: + @if command -v npm &> /dev/null && ! command -v $*; then \ + echo "npm install --global ^$*$$"; \ + npm install --global $(filter $*-cli,$(TOOLS_NPM)); \ + fi; + +#@ install software command or service created by the project. +$(TARGETS_INSTALL): install-%: $(GOBIN)/% +$(addprefix $(GOBIN)/,$(COMMANDS)): $(GOBIN)/%: $(DIR_BUILD)/% + cp $< $(GOBIN)/$*; + +#@ uninstall all software created by the project. +uninstall: $(TARGETS_UNINSTALL) +#@ uninstall all software created and used by the projct. +uninstall-all: $(TARGETS_UNINSTALL_ALL) +#@ uninstall-*: uninstall the matched software command or service. + +# uninstall go tools used by the project. +$(TARGETS_UNINSTALL_GO): uninstall-%: + @#PACKAGE=$(call go-pkg,strip,$(TOOLS_GO),^$*$$); \ + rm -rf $(wildcard $(GOBIN)/$* $(GOBIN)/$*.config); +# uninstall npm based tools used by the project. +$(TARGETS_UNINSTALL_NPM): uninstall-%: + @if command -v npm &> /dev/null; then \ + echo "npm uninstall --global $*"; \ + npm uninstall --global $(filter $*-cli,$(TOOLS_NPM)); \ + fi; +# uninstall codacy tools used by the project. +$(TARGETS_UNINSTALL_CODACY): uninstall-codacy-%: + @VERSION="$(CODACY_$(call upper,$*)_VERSION)"; \ + rm -f "$(GOBIN)/codacy-$*-$${VERSION}"; +# uninstall software command or service created by the project. +$(TARGETS_UNINSTALL): uninstall-%:; rm -f $(GOBIN)/$*; + + +## Init: targets to initialize tools and (re-)sources. + +#@ initialize the project to prepare it for building. +init: $(TARGETS_INIT) +#@ initialize the go module and package dependencies. +init-go: go.mod go.sum + go mod download; +go.mod go.sum: + go mod init "$(REPOSITORY)" +#@ initialize the pre-commit hook. +init-hooks: .git/hooks/pre-commit +.git/hooks/pre-commit: + @if [ -n "$$(cat Makefile | grep "^GOMAKE ?=.*")" ]; then \ + echo -ne "#!/bin/sh\n$(MAKE) commit" >$@; chmod 755 $@; \ + fi; \ + +#@ initialize the generated sources. +init-sources: $(MOCKS) +$(MOCKS): go.sum $(MOCK_SOURCES) $(GOBIN)/mockgen $(GOBIN)/mock + go generate "$(shell echo $(MOCK_TARGETS) | \ + sed -E "s|.*$@=([^ ]*).*$$|\1|")"; + +#@ initialize the codacy support - if enabled. +init-codacy: $(TARGETS_INIT_CODACY) +$(TARGETS_INIT_CODACY): init-%: + @VERSION="$(CODACY_$(call upper,$*)_VERSION)"; \ + FILE="$(GOBIN)/codacy-$*-$${VERSION}"; \ + if [ ! -f $${FILE} ]; then \ + BASE="https://github.com/codacy/codacy-$*/releases/download"; \ + echo curl --silent --location --output $${FILE} \ + $${BASE}/$${VERSION}/codacy-$*-$${VERSION}; \ + curl --silent --location --output $${FILE} \ + $${BASE}/$${VERSION}/codacy-$*-$${VERSION}; \ + chmod 700 $${FILE}; \ + fi; \ + +#@ initialize project by copying config makefile from template. +init-make: $(TARGETS_INIT_MAKE) +#@ initialize project by copying Makefile.base from template. +init-make!: update-base + @DIR="$$(pwd)"; cd $(BASEDIR); \ + git show HEAD:Makefile.base > $${DIR}/Makefile 2>/dev/null; \ + ( echo -e "\n\n# Makefile"; \ + git show HEAD:README.md 2>/dev/null | \ + sed -n '/^## Standard/,/^## Terms/p' | sed '1d;$$d'; \ + git show HEAD:MANUAL.md 2>/dev/null | \ + sed -n '/^## Setup and/,//p'; \ + ) >$${DIR}/MAKEFILE.md; + + +## Test: targets to test the source code (and compiler environment). +TEST_ALL := $(DIR_BUILD)/test-all.cover +TEST_UNIT := $(DIR_BUILD)/test-unit.cover +TEST_BENCH := $(DIR_BUILD)/test-bench.cover + +#@ execute default test set. +test: $(TARGETS_TEST) +#@ [/] # execute all tests. +test-all: $(TARGETS_TEST_INIT) $(TEST_ALL) +#@ [/] # execute only unit tests. +test-unit: $(TARGETS_TEST_INIT) $(TEST_UNIT) +#@ [/] # execute benchmarks. +test-bench: $(TARGETS_TEST_INIT) $(TEST_BENCH) + +# remove all coverage files of test and benchmarks. +test-clean: + @if [ -f "$(TEST_ALL)" ]; then rm -vf $(TEST_ALL); fi; \ + if [ -f "$(TEST_UNIT)" ]; then rm -vf $(TEST_UNIT); fi; \ + if [ -f "$(TEST_BENCH)" ]; then rm -vf $(TEST_BENCH); fi; \ + +#@ start the test coverage report. +test-cover: + @FILE=$$(ls -Art "$(TEST_ALL)" "$(TEST_UNIT)" \ + "$(TEST_BENCH)" 2>/dev/null); \ + go tool cover -html="$${FILE}"; \ + +#@ upload the test coverage report to codacy. +test-upload: + @FILE=$$(ls -Art "$(TEST_ALL)" "$(TEST_UNIT)" \ + "$(TEST_BENCH)" 2>/dev/null); \ + COMMIT="$$(git log --max-count=1 --pretty=format:"%H")"; \ + SCRIPT="https://coverage.codacy.com/get.sh"; \ + if [ -n "$(CODACY_PROJECT_TOKEN)" ]; then \ + bash <(curl --silent --location $${SCRIPT}) report \ + --project-token $(CODACY_PROJECT_TOKEN) --commit-uuid $${COMMIT} \ + --codacy-api-base-url $(CODACY_API_BASE_URL) --language go \ + --force-coverage-parser go --coverage-reports "$${FILE}"; \ + elif [ -n "$(CODACY_API_TOKEN)" ]; then \ + bash <(curl --silent --location $${SCRIPT}) report \ + --organization-provider $(CODACY_PROVIDER) \ + --username $(CODACY_USER) --project-name $(CODACY_PROJECT) \ + --api-token $(CODACY_API_TOKEN) --commit-uuid $${COMMIT} \ + --codacy-api-base-url $(CODACY_API_BASE_URL) --language go \ + --force-coverage-parser go --coverage-reports "$${FILE}"; \ + fi; \ + +# Function to test versions. +test-go = \ + for VERSION in $${VERSIONS}; do \ + if [ "$(GOVERSION)" != "$${VERSION}" ]; then \ + echo "$${ERROR}: '$(1)' requires $${VERSION}!"; \ + if [[ "$(GOVERSION)" < "$${VERSION}" ]]; then \ + GOCHANGE="upgrade"; CHANGE="downgrade"; \ + else \ + GOCHANGE="downgrade"; CHANGE="upgrade"; \ + fi; \ + echo -e "\t$${GOCHANGE} your local go version to $${VERSION} or "; \ + echo -e "\trun 'make update-go $${VERSION}' to adjust the project!"; \ + exit -1; \ + fi; \ + done; + +#@ test whether project is using the latest go-version. +test-go: + @ERROR="error: local go version is $(GOVERSION)"; \ + if [ ! -f go.mod ]; then \ + VERSIONS=$$(grep "^go [0-9.]*$$" go.mod | cut -f2 -d' '); \ + $(call test-go,go.mod) \ + fi; \ + if [ -f "$(FILE_DELIVERY)" ]; then \ + VERSIONS="$$(grep -Eo "$(FILE_DELIVERY_REGEX)" \ + "$(FILE_DELIVERY)" | grep -Eo "[0-9.]*" | sort -u)"; \ + $(call test-go,$(FILE_DELIVERY)) \ + fi; \ + +# process test arguments. +testargs = \ + if [ -n "$(RUNARGS)" ]; then ARGS=($(RUNARGS)); \ + if [[ -f "$${ARGS[0]}" && ! -d "$${ARGS[0]}" && \ + "$${ARGS[0]}" == *_test.go ]]; then \ + find $$(dirname $(RUNARGS) | sort -u) \ + -maxdepth 1 -a -name "*.go" -a ! -name "*_test.go" \ + -o -name "common_test.go" -o -name "mock_*_test.go" | \ + sed "s|^|./|"; \ + echo $(addprefix ./,$(RUNARGS)); \ + elif [[ -d "$${ARGS[0]}" && ! -f "$${ARGS[0]}" ]]; then \ + echo $(addprefix ./,$(RUNARGS)); \ + elif [[ ! -f "$${ARGS[0]}" && ! -d "$${ARGS[0]}" ]]; then \ + for ARG in $${ARGS[0]}; do \ + if [ -z "$${PACKAGES}" ]; then PACKAGES="$${ARG%/*}"; \ + else PACKAGES="$${PACKAGES}\n$${ARG%/*}"; fi; \ + if [ -z "$${TEST_CASE}" ]; then TEST_CASES="-run $${ARG\#\#*/}"; \ + else TEST_CASES="$${TEST_CASES} -run $${ARG\#\#*/}"; fi; \ + done; \ + echo -en "$${PACKAGES}" | sort -u | sed "s|^|./|"; \ + echo "$${TEST_CASES}"; \ + else \ + echo "warning: invalid test parameters [$${ARGS[@]}]"; \ + fi; \ + else echo "./..."; fi + +TEST_FLAGS ?= -race -mod=readonly -count=1 +TEST_ARGS ?= $(shell $(testargs)) + +# actuall targets for testing. +$(TEST_ALL): $(SOURCES) init-sources $(TEST_DEPS) $(DIR_BUILD) + go test $(TEST_FLAGS) -timeout $(TEST_TIMEOUT) \ + -cover -coverprofile $@ $(TEST_ARGS); +$(TEST_UNIT): $(SOURCES) init-sources $(DIR_BUILD) + go test $(TEST_FLAGS) -timeout $(TEST_TIMEOUT) \ + -cover -coverprofile $@ -short $(TEST_ARGS); +$(TEST_BENCH): $(SOURCES) init-sources $(DIR_BUILD) + go test $(TEST_FLAGS) -benchtime=8s \ + -cover -coverprofile $@ -short -bench=. $(TEST_ARGS); + +#@ execute a kind of self-test running the main targets. +test-self: clean-all all-clean update-all? update-all + echo "Self test finished successfully!!!"; + +# Variables and definitions for linting of source code. +COMMA := , +SPACE := $(null) # + +# disabled (deprecated): deadcode golint interfacer ifshort maligned musttag +# nosnakecase rowserrcheck scopelint structcheck varcheck wastedassign +# diabled (distructive): nlreturn ireturn nonamedreturns varnamelen exhaustruct +# exhaustivestruct gochecknoglobals gochecknoinits tagliatelle +# disabled (conflicting): godox gci paralleltest +# not listed (unnecessary): forcetypeassert wsl +# requires unknown setup: depguard + +LINTERS_DISCOURAGED ?= \ + deadcode exhaustivestruct exhaustruct gci gochecknoglobals gochecknoinits \ + godox golint ifshort interfacer ireturn maligned musttag nlreturn \ + nonamedreturns nosnakecase paralleltest rowserrcheck scopelint structcheck \ + tagliatelle varcheck varnamelen wastedassign +LINTERS_MINIMUM ?= \ + asasalint asciicheck bidichk bodyclose dogsled dupl dupword durationcheck \ + errchkjson execinquery exportloopref funlen gocognit goconst gocyclo \ + godot gofmt gofumpt goimports gosimple govet importas ineffassign maintidx \ + makezero misspell nestif nilerr prealloc predeclared promlinter reassign \ + sqlclosecheck staticcheck typecheck unconvert unparam unused \ + usestdlibvars whitespace +LINTERS_BASELINE ?= \ + containedctx contextcheck cyclop decorder errname forbidigo ginkgolinter \ + gocheckcompilerdirectives goheader gomodguard goprintffuncname gosec \ + grouper interfacebloat lll loggercheck nakedret nilnil noctx nolintlint \ + nosprintfhostport +LINTERS_EXPERT ?= \ + errcheck errorlint exhaustive gocritic goerr113 gomnd gomoddirectives \ + revive stylecheck tenv testableexamples testpackage thelper tparallel \ + wrapcheck + +LINT_FILTER := $(LINTERS_CUSTOM) $(LINTERS_DISABLED) +LINT_DISABLED ?= $(subst $(SPACE),$(COMMA),$(strip \ + $(filter-out $(LINT_FILTER),$(LINTERS_DISCOURAGED)) $(LINTERS_DISABLED))) +LINT_MINIMUM ?= $(subst $(SPACE),$(COMMA),$(strip \ + $(filter-out $(LINT_FILTER),$(LINTERS_MINIMUM)) $(LINTERS_CUSTOM))) +LINT_BASELINE ?= $(subst $(SPACE),$(COMMA),$(strip $(LINT_MINIMUM) \ + $(filter-out $(LINT_FILTER),$(LINTERS_BASELINE)))) +LINT_EXPERT ?= $(subst $(SPACE),$(COMMA),$(strip $(LINT_BASELINE) \ + $(filter-out $(LINT_FILTER),$(LINTERS_EXPERT)))) + +ifeq ($(shell ls $(FILE_GOLANGCI) 2>/dev/null), $(FILE_GOLANGCI)) + LINT_CONFIG := --config $(FILE_GOLANGCI) +endif + +LINT_MIN := --enable $(LINT_MINIMUM) --disable $(LINT_DISABLED) +LINT_BASE := --enable $(LINT_BASELINE) --disable $(LINT_DISABLED) +LINT_PLUS := --enable $(LINT_EXPERT) --disable $(LINT_DISABLED) +LINT_MAX := --enable-all --disable $(LINT_DISABLED) +LINT_ALL := --enable $(LINT_EXPERT),$(LINT_DISABLED) --disable-all + +LINT_FLAGS ?= --allow-serial-runners --sort-results --color always +LINT_CMD ?= golangci-lint run $(LINT_CONFIG) $(LINT_FLAGS) +LINT_CMD_CONFIG := make --no-print-directory lint-config +ifeq ($(RUNARGS),linters) + LINT_CMD := golangci-lint linters $(LINT_CONFIG) $(LINT_FLAGS) +else ifeq ($(RUNARGS),config) + LINT_CMD := @ + LINT_MIN := LINT_ENABLED=$(LINT_MINIMUM) \ + LINT_DISABLED=$(LINT_DISABLED) $(LINT_CMD_CONFIG) + LINT_BASE := LINT_ENABLED=$(LINT_BASELINE) \ + LINT_DISABLED=$(LINT_DISABLED) $(LINT_CMD_CONFIG) + LINT_PLUS := LINT_ENABLED=$(LINT_EXPERT) \ + LINT_DISABLED=$(LINT_DISABLED) $(LINT_CMD_CONFIG) + LINT_MAX := LINT_DISABLED=$(LINT_DISABLED) $(LINT_CMD_CONFIG) + LINT_ALL := LINT_ENABLED=$(LINT_EXPERT),$(LINT_DISABLED) $(LINT_CMD_CONFIG) +else ifeq ($(RUNARGS),fix) + LINT_CMD := golangci-lint run $(LINT_CONFIG) $(LINT_FLAGS) --fix +else ifneq ($(RUNARGS),) + LINT_CMD := golangci-lint run $(LINT_CONFIG) $(LINT_FLAGS) + LINT_MIN := --disable-all --enable $(RUNARGS) + LINT_BASE := --disable-all --enable $(RUNARGS) + LINT_PLUS := --disable-all --enable $(RUNARGS) + LINT_MAX := --disable-all --enable $(RUNARGS) + LINT_ALL := --disable-all --enable $(RUNARGS) +endif + + +## Lint: targets to lint source code. + +#@ # execute all linters for the custom code quality level. +lint: $(TARGETS_LINT) +#@ # execute golangci linters for minimal code quality level (go-files). +lint-min: init-sources $(GOBIN)/golangci-lint; $(LINT_CMD) $(LINT_MIN) +#@ # execute golangci linters for base code quality level (go-files). +lint-base: init-sources $(GOBIN)/golangci-lint; $(LINT_CMD) $(LINT_BASE) +#@ # execute golangci linters for plus code quality level (go-files). +lint-plus: init-sources $(GOBIN)/golangci-lint; $(LINT_CMD) $(LINT_PLUS) +#@ # execute golangci linters for maximal code quality level (go-files). +lint-max: init-sources $(GOBIN)/golangci-lint; $(LINT_CMD) $(LINT_MAX) +#@ # execute all golangci linters for insane code quality level (go-files). +lint-all: init-sources $(GOBIN)/golangci-lint; $(LINT_CMD) $(LINT_ALL) + +#@ create a golangci linter config using the custom code quality level. +lint-config: + @LINT_START=$$(awk '($$0 ~ "^linters:.*"){print NR-1}' $(FILE_GOLANGCI)); \ + LINT_STOP=$$(awk '(start && $$0 ~ "^[^ ]+:$$"){print NR; start=0} \ + ($$0 ~ "^linters:.*"){start=1}' $(FILE_GOLANGCI)); \ + (sed -ne '1,'$${LINT_START}'p' $(FILE_GOLANGCI); \ + echo -e "linters:\n enable:"; \ + for LINTER in "# List of min-set linters (min)" $(LINTERS_MINIMUM) \ + "# List of base-set linters (base)" $(LINTERS_BASELINE) \ + "# List of plus-set linters (plus)" $(LINTERS_EXPERT); do \ + if [ "$${LINTER:0:1}" == "#" ]; then X=""; else X="- "; fi; \ + echo -e " $${X}$${LINTER}"; \ + done; echo -e "\n disable:"; \ + for LINTER in "# List of to-avoid linters (avoid)" $(LINTERS_DISABLED); do \ + if [ "$${LINTER:0:1}" == "#" ]; then X=""; else X="- "; fi; \ + echo -e " $${X}$${LINTER}"; \ + done; \ + echo; sed -ne $${LINT_STOP}',$$p' $(FILE_GOLANGCI)) | \ + awk -v enabled=$${LINT_ENABLED} -v disabled=$${LINT_DISABLED} ' \ + ((start == 2) && ($$1 == "-")) { \ + enabled = enable[$$2]; disabled = disable[$$2]; \ + if (enabled && disabled) { \ + print $$0 " # (conflicting)" \ + } else if (!enabled && disabled) { \ + print gensub("-","# -", 1) " # (disabled)" \ + } else if (!enabled && !disabled && enone) { \ + print gensub("-","# -", 1) " # (missing)" \ + } else { print $$0 } \ + enable[$$2] = 2; next \ + } \ + ((start == 3) && ($$1 == "-")) { \ + enabled = enable[$$2]; disabled = disable[$$2]; \ + if (enabled && disabled) { \ + print gensub("-","# -", 1) " # (conflicting)" \ + } else if (enabled && !disabled) { \ + print gensub("-","# -", 1) " # (enabled)" \ + } else if (!enabled && !disabled && dnone) { \ + print gensub("-","# -", 1) " # (missing)" \ + } else { print $$0 } \ + disable[$$2] = 2; next \ + } \ + ((start != 0) && ($$0 ~ "^[^ ]+:$$")) { start=0 } \ + ((start >= 2) && ($$0 ~ "^ [^ ]+:$$")) { none=1; \ + if (start == 2) { for (key in enable) { \ + if (key && enable[key] == 1) { \ + if (none) { print " # Additional linters" } \ + print " - " key; none=0 \ + } \ + } } else if (start == 3) { for (key in disable) { \ + if (key && disable[key] == 1) { \ + if (none) { print " # Additional linters" } \ + print " - " key; none=0 \ + } \ + } } \ + } \ + ((start != 0) && ($$0 == " disable:")) { start = 3 }\ + ((start != 0) && ($$0 == " enable:")) { start = 2 } \ + ((start == 0) && ($$0 == "linters:")) { start = 1; \ + enone = split(enabled, array, ","); \ + for (i in array){ enable[array[i]] = 1 } \ + dnone = split(disabled, array, ","); \ + for (i in array){ disable[array[i]] = 1 } \ + } { print $$0 }' \ + +#@ execute all codacy linters activated by the custom setup (all files). +lint-codacy: lint-revive $(TARGETS_LINT_CODACY_CLIENTS) $(TARGETS_LINT_CODACY_BINARIES) +$(TARGETS_LINT_CODACY_CLIENTS): lint-%: init-sources + @LARGS=("--allow-network" "--skip-uncommitted-files-check"); \ + LARGS+=("--codacy-api-base-url" "$(CODACY_API_BASE_URL)"); \ + if [ -n "$(CODACY_PROJECT_TOKEN)" ]; then \ + LARGS+=("--project-token" "$(CODACY_PROJECT_TOKEN)"); \ + LARGS+=("--upload" "--verbose"); \ + elif [ -n "$(CODACY_API_TOKEN)" ]; then \ + LARGS+=("--provider" "$(CODACY_PROVIDER)"); \ + LARGS+=("--username" "$(CODACY_USER)"); \ + LARGS+=("--project" "$(CODACY_PROJECT)"); \ + LARGS+=("--api-token" "$(CODACY_API_TOKEN)"); \ + LARGS+=("--upload" "--verbose"); \ + fi; \ + echo """$(IMAGE_CMD) run --rm=true --env CODACY_CODE="/code" \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + --volume /tmp:/tmp --volume ".":"/code" \ + codacy/codacy-analysis-cli analyze "$${LARGS[@]}" --tool $*"""; \ + $(IMAGE_CMD) run --rm=true --env CODACY_CODE="/code" \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + --volume /tmp:/tmp --volume "$$(pwd):/code" \ + codacy/codacy-analysis-cli analyze "$${LARGS[@]}" --tool $* || \ + $(CODACY_CONTINUE); \ + +LINT_ARGS_GOSEC_LOCAL := -log /dev/null -exclude G307 ./... +LINT_ARGS_GOSEC_UPLOAD := -log /dev/null -exclude G307 -fmt json ./... +LINT_ARGS_STATICCHECK_LOCAL := -tests ./... +LINT_ARGS_STATICCHECK_UPLOAD := -tests -f json ./... + +$(TARGETS_LINT_CODACY_BINARIES): lint-%: init-sources init-% $(GOBIN)/% + @COMMIT=$$(git log --max-count=1 --pretty=format:"%H"); \ + VERSION="$(CODACY_$(call upper,$*)_VERSION)"; \ + LARGS=("--silent" "--location" "--request" "POST" \ + "--header" "Content-type: application/json"); \ + if [ -n "$(CODACY_PROJECT_TOKEN)" ]; then \ + BASE="$(CODACY_API_BASE_URL)/2.0/commit/$${COMMIT}"; \ + LARGS+=("-H" "project-token: $(CODACY_PROJECT_TOKEN)"); \ + elif [ -n "$(CODACY_API_TOKEN)" ]; then \ + SPATH="$(CODACY_PROVIDER)/$(CODACY_USER)/$(CODACY_PROJECT)"; \ + BASE="$(CODACY_API_BASE_URL)/2.0/$${SPATH}/commit/$${COMMIT}"; \ + LARGS+=("-H" "api-token: $(CODACY_API_TOKEN)"); \ + fi; \ + if [ -n "$${BASE}" ]; then \ + echo -e """$(GOBIN)/$* $(LINT_ARGS_$(call upper,$*)_UPLOAD) | \ + $(GOBIN)/codacy-$*-$${VERSION} | \ + curl "$${LARGS[@]}" --data @- "$${BASE}/issuesRemoteResults"; \ + curl "$${LARGS[@]}" "$${BASE}/resultsFinal""""; \ + ( $(GOBIN)/$* $(LINT_ARGS_$(call upper,$*)_UPLOAD) | \ + $(GOBIN)/codacy-$*-$${VERSION} | \ + curl "$${LARGS[@]}" --data @- "$${BASE}/issuesRemoteResults"; echo; \ + curl "$${LARGS[@]}" "$${BASE}/resultsFinal"; echo ) || \ + $(CODACY_CONTINUE); \ + else \ + echo $(GOBIN)/$* $(LINT_ARGS_$(call upper,$*)_LOCAL); \ + $(GOBIN)/$* $(LINT_ARGS_$(call upper,$*)_LOCAL) || $(CODACY_CONTINUE); \ + fi; \ + +#@ execute revive linter (go-files). +lint-revive: init-sources $(GOBIN)/revive + revive -formatter friendly -config=revive.toml $(SOURCES) || $(CODACY_CONTINUE); + +#@ execute markdown linter (md-files). +lint-markdown: init-sources $(NVM_BIN)/markdownlint + @echo markdownlint --config .markdownlint.yaml .; \ + if command -v markdownlint &> /dev/null; then \ + markdownlint --config .markdownlint.yaml .; \ + else $(IMAGE_CMD) run --tty --volume $$(pwd):/src:ro \ + container-registry.zalando.net/library/node-18-alpine:latest \ + /bin/sh -c "npm install --global markdownlint-cli >/dev/null 2>&1 && \ + cd /src && markdownlint --config .markdownlint.yaml ."; \ + fi; \ + +#@ execute shellcheck to find potential shell script issues (sh-files). +lint-shell: init-sources + @mapfile -t FILES < <(find -name "*.sh"); ARGS="--color=always"; \ + if [ -n "$(RUNARGS)" ]; then ARGS=(${ARGS} $(RUNARGS)); fi; \ + if [ -n "$${FILES}" ]; then \ + if command -v shellcheck &> /dev/null; then \ + shellcheck "$${ARGS[@]}" "$${FILES[@]}"; \ + else \ + $(IMAGE_CMD) run --rm --volume="$${PWD}:/mnt" \ + koalaman/shellcheck:stable "$${ARGS[@]}" "$${FILES[@]}"; \ + fi; \ + fi; \ + +#@ execute gitleaks to check committed code for leaking secrets (all files). +lint-leaks: $(GOBIN)/gitleaks + gitleaks detect --no-banner --verbose --source .; +#@ execute gitleaks to check un-committed code changes for leaking secrets (all files). +lint-leaks?: $(GOBIN)/gitleaks + gitleaks protect --no-banner --verbose --source .; +#@ execute govulncheck to find vulnerabilities in dependencies (go-files). +lint-vuln: $(GOBIN)/govulncheck + govulncheck -test ./... +#@ # execute go(cyclo|cognit) linter (go-files). +lint-gocyclo lint-gocognit: lint-%: $(GOBIN)/% + @RUNARGS=($(RUNARGS)); MODS="0"; \ + while [ "$${#RUNARGS[@]}" != "0" ]; do \ + if [ "$${RUNARGS[0]}" == "top" ]; then ARGS+=(-$${RUNARGS[0]}); MODS="1"; \ + elif [ "$${RUNARGS[0]}" == "over" ]; then ARGS+=(-$${RUNARGS[0]}); MODS="1"; \ + elif [ "$${RUNARGS[0]}" == "avg" ]; then ARGS+=(-$${RUNARGS[0]}); MODS="0"; \ + else ARGS+=($${RUNARGS[0]}); MODS=$$((MODS-1)); fi; \ + RUNARGS=("$${RUNARGS[@]:1}"); \ + done; \ + if [[ "$${MODS}" == "0" ]]; then ARGS+=($(MODULES)); fi; \ + $* "$${ARGS[@]}"; + +#@ execute zally api-linter to find API issues (yaml-files). +lint-apis: $(GOBIN)/zally + @LINTER="https://infrastructure-api-linter.zalandoapis.com"; \ + if ! curl --silent $${LINTER}; then \ + echo "warning: API linter not available;"; exit 0; \ + fi; \ + ARGS=("--linter-service" "$${LINTER}"); \ + if command -v ztoken > /dev/null; then ARGS+=("--token" "$$(ztoken)"); fi; \ + if [ -n "$(RUNARGS)" ]; then FILES="$(RUNARGS)"; \ + else FILES="$$(find zalando-apis -name "*.yaml" 2>/dev/null)"; fi; \ + for APISPEC in $${FILES}; do \ + echo "check API: zally \"$${APISPEC}\""; \ + zally "$${ARGS[@]}" lint "$${APISPEC}" || exit 1; \ + done; + + +# Variables for setting up container specific build flags. +BUILD_OS ?= $(shell go env GOOS) +BUILD_ARCH ?= $(shell go env GOARCH) +IMAGE_OS ?= ${shell grep "^FROM [^ ]*$$" $(FILE_CONTAINER) 2>/dev/null | \ + grep -v " as " | sed "s/.*\(alpine\|ubuntu\).*/\1/g"} +ifeq ($(IMAGE_OS),alpine) + BUILD_FLAGS ?= -v -mod=readonly -buildvcs=auto + GOCGO := 0 +else + BUILD_FLAGS ?= -v -mod=readonly -buildvcs=auto -race + GOCGO := 1 +endif + +# Function to select right main package of command. +main-pkg = $(patsubst $(1)=%,%,$(firstword $(filter $(1)=%,$(COMMANDS_PKG)))) + +# Functions and variables to propagate versions to build commands. +LD-PATHS := main $(shell go list ./... | grep "config$$") +ld-flag = $(addprefix -X ,$(addsuffix .$(1),$(LD-PATHS))) +ld-flags = $(call ld-flag,Version=$(BUILD_VERSION)) \ + $(call ld-flag,Revision=$(shell git rev-parse HEAD)) \ + $(call ld-flag,Build=$(shell date --iso-8601=seconds)) \ + $(call ld-flag,Commit=$(shell git log -1 --date=iso8601-strict --format=%cd)) \ + $(call ld-flag,Dirty=$(shell git diff --quiet && echo false || echo true)) \ + $(call ld-flag,Path=$(REPOSITORY)$(patsubst .%,%,$(1))) \ + $(call ld-flag,GitHash=$(shell git rev-parse --short HEAD)) $(LDFLAGS) \ + + +## Build: targets to build native and linux platform executables. + +#@ build default executables (native). +build: build-native +#@ build native platform executables using system architecture. +build-native: $(TARGETS_BUILD) +$(TARGETS_BUILD): build-%: $(DIR_BUILD)/% +$(DIR_BUILD)/%: init-hooks $(SOURCES) + @mkdir -p "$(dir $@)"; + GOOS=$(BUILD_OS) GOARCH=$(BUILD_ARCH) CGO_ENABLED=1 \ + go build -ldflags="$(call ld-flags,$(call main-pkg,$*))" \ + $(BUILD_FLAGS) -o $@ $(call main-pkg,$*)/main.go; + +#@ build linux platform executables using default (system) architecture. +build-linux: $(TARGETS_BUILD_LINUX) +$(DIR_BUILD)/linux/%: $(call main-pkg,%) init-hooks $(SOURCES) + @mkdir -p "$(dir $@)"; + GOOS=$(BUILD_OS) GOARCH=$(BUILD_ARCH) CGO_ENABLED=$(GOCGO) \ + go build -ldflags="$(call ld-flags,$(call main-pkg,$*))" \ + $(BUILD_FLAGS) -o $@ $(call main-pkg,$*)/main.go; + +#@ build container image (alias for image-build). +build-image: image-build + + +## Image: targets to build and push container images. + +#@ build and push container images - if setup. +image: $(if $(filter $(IMAGE_PUSH),never),,image-push) +#@ build container images. +image-build: $(TARGETS_IMAGE_BUILD) + +setup-image-file = \ + FILE="$*"; IMAGE=$(IMAGE); \ + PREFIX="$$(basename "$${FILE%$(FILE_CONTAINER)*}")"; \ + SUFFIX="$$(echo $${FILE\#*$(FILE_CONTAINER)} | sed "s/^[^[:alnum:]]*//")"; \ + INFIX="$${SUFFIX:-$${PREFIX}}"; \ + if [ -n "$${INFIX}" ]; then \ + IMAGE="$${IMAGE/:/-$${INFIX}:}"; \ + fi; + +$(TARGETS_IMAGE_BUILD): image-build/%: build-linux + @$(call setup-image-file $*) \ + if [ "$(IMAGE_PUSH)" == "never" ]; then \ + echo "We never build images, aborting [$${IMAGE}]."; exit 0; \ + fi; \ + REVISION=$$(git rev-parse HEAD 2>/dev/null); \ + BASE=$$(grep "^FROM[[:space:]]" $${FILE} | tail -n1 | cut -d" " -f2); \ + $(IMAGE_CMD) build --tag $${IMAGE} --file=$${FILE} . \ + --label=org.opencontainers.image.created="$$(date --rfc-3339=seconds)" \ + --label=org.opencontainers.image.vendor=zalando/$(TEAM) \ + --label=org.opencontainers.image.source=$(REPOSITORY) \ + --label=org.opencontainers.image.version=$${IMAGE##*:} \ + --label=org.opencontainers.image.revision=$${REVISION} \ + --label=org.opencontainers.image.base.name=$${BASE} \ + --label=org.opencontainers.image.ref.name=$${IMAGE}; \ + +#@ push contianer images - if setup and allowed. +image-push: $(TARGETS_IMAGE_PUSH) +$(TARGETS_IMAGE_PUSH): image-push/%: image-build/% + @$(call setup-image-file $*) \ + if [ "$(IMAGE_PUSH)" == "never" ]; then \ + echo "We never push images, aborting [$${IMAGE}]."; exit 0; \ + elif [ "$(IMAGE_VERSION)" == "snapshot" ]; then \ + echo "We never push snapshot images, aborting [$${IMAGE}]."; exit 0; \ + elif [ -n "$(CDP_PULL_REQUEST_NUMBER)" -a "$(IMAGE_PUSH)" != "pulls" ]; then \ + echo "We never push pull request images, aborting [$${IMAGE}]."; exit 0; \ + fi; \ + $(IMAGE_CMD) push $${IMAGE}; \ + + +## Run: targets for starting services and commands for testing. +HOST := "127.0.0.1" + +#@ start a postgres database instance for testing. +run-db: $(DIR_RUN) + @if [[ ! " $(TEST_DEPS) $(RUN_DEPS) " =~ " run-db " ]]; then exit 0; fi; \ + echo "info: ensure $(DB_IMAGE) running on $(HOST):$(DB_PORT)"; \ + if [ -n "$$($(IMAGE_CMD) ps | grep "$(DB_IMAGE).*$(HOST):$(DB_PORT)")" ]; then \ + echo "info: port allocated, use existing db container!"; exit 0; \ + fi; \ + $(IMAGE_CMD) start ${IMAGE_ARTIFACT}-db 2>/dev/null || ( \ + $(IMAGE_CMD) run --detach --tty \ + --name ${IMAGE_ARTIFACT}-db \ + --publish $(HOST):$(DB_PORT):5432 \ + --env POSTGRES_USER="$(DB_USER)" \ + --env POSTGRES_PASSWORD="$(DB_PASSWORD)" \ + --env POSTGRES_DB="$(DB_NAME)" $(DB_IMAGE) \ + -c 'shared_preload_libraries=pg_stat_statements' \ + -c 'pg_stat_statements.max=10000' \ + -c 'pg_stat_statements.track=all' \ + $(RUNARGS) 2>&1 & \ + until [ "$$($(IMAGE_CMD) inspect --format {{.State.Running}} \ + $(IMAGE_ARTIFACT)-db 2>/dev/null)" == "true" ]; \ + do echo "waiting for db container" >/dev/stderr; sleep 1; done && \ + until $(IMAGE_CMD) exec $(IMAGE_ARTIFACT)-db \ + pg_isready -h localhost -U $(DB_USER) -d $(DB_NAME); \ + do echo "waiting for db service" >/dev/stderr; sleep 1; done) |\ + tee -a $(DIR_RUN)/$(IMAGE_ARTIFACT)-db; \ + +#@ start an AWS localstack instance for testing. +run-aws: $(DIR_RUN) + @if [[ ! " $(TEST_DEPS) $(RUN_DEPS) " =~ " run-aws " ]]; then exit 0; fi; \ + echo "info: ensure $(AWS_IMAGE) is running on $(HOST):4566/4571" && \ + if [ -n "$$($(IMAGE_CMD) ps | \ + grep "$(AWS_IMAGE).*$(HOST):4566.*$(HOST):4571")" ]; then \ + echo "info: ports allocated, use existing aws container!"; \ + $(call run-aws-setup) || exit 1 && exit 0 ; \ + fi; \ + $(IMAGE_CMD) start ${IMAGE_ARTIFACT}-aws 2>/dev/null || ( \ + $(IMAGE_CMD) run --detach --tty --name ${IMAGE_ARTIFACT}-aws \ + --publish $(HOST):4566:4566 --publish $(HOST):4571:4571 \ + --env SERVICES="$(AWS_SERVICES)" $(AWS_IMAGE) $(RUNARGS) 2>&1 && \ + until [ "$$($(IMAGE_CMD) inspect --format {{.State.Running}} \ + $(IMAGE_ARTIFACT)-aws 2>/dev/null)" == "true" ]; \ + do echo "waiting for aws container" >/dev/stderr; sleep 1; done && \ + until $(IMAGE_CMD) exec $(IMAGE_ARTIFACT)-aws \ + curl --silent http://$(HOST):4566; \ + do echo "waiting for aws service" >/dev/stderr; sleep 1; done && \ + $(call run-aws-setup) || exit 1) | \ + tee -a $(DIR_RUN)/$(IMAGE_ARTIFACT)-aws.log; \ + +#@ run-*: start the matched command using the native binary. +$(TARGETS_RUN): run-%: $(DIR_BUILD)/% $(RUN_DEPS) $(DIR_RUN) $(DIR_CRED) + @$(call run-setup); $(call run-vars) $(call run-vars-local) \ + $(DIR_BUILD)/$* $(RUNARGS) 2>&1 | \ + tee -a $(DIR_RUN)/$(IMAGE_ARTIFACT)-$*.log; \ + exit $${PIPESTATUS[0]}; + +#@ run-go-*: start the matched command using go run. +$(TARGETS_RUN_GO): run-go-%: $(DIR_BUILD)/% $(RUN_DEPS) $(DIR_RUN) $(DIR_CRED) + @$(call run-setup); $(call run-vars) $(call run-vars-local) \ + go run cmd/$*/main.go $(RUNARGS) 2>&1 | \ + tee -a $(DIR_RUN)/$(IMAGE_ARTIFACT)-$*.log; \ + exit $${PIPESTATUS[0]}; + +#@ run-image-*: start the matched command via the container images. +$(TARGETS_RUN_IMAGE): run-image-%: $(RUN_DEPS) $(DIR_RUN) $(DIR_CRED) + @trap "$(IMAGE_CMD) rm $(IMAGE_ARTIFACT)-$* >/dev/null" EXIT; \ + trap "$(IMAGE_CMD) kill $(IMAGE_ARTIFACT)-$* >/dev/null" INT TERM; \ + if [ -n "$(filter %$*,$(TARGETS_IMAGE))" ]; then \ + IMAGE="$$(echo "$(IMAGE)" | sed -e "s/:/-$*:/")"; \ + else IMAGE="$(IMAGE)" fi; $(call run-setup); \ + $(IMAGE_CMD) run --name $(IMAGE_ARTIFACT)-$* --network=host \ + --volume $(DIR_CRED):/meta/credentials --volume $(DIR_RUN)/temp:/tmp \ + $(call run-vars, --env) $(call run-vars-image, --env) \ + ${IMAGE} /$* $(RUNARGS) 2>&1 | \ + tee -a $(DIR_RUN)/$(IMAGE_ARTIFACT)-$*.log; \ + exit $${PIPESTATUS[0]}; + +#@ clean up all running container images. +run-clean: $(TARGETS_RUN_CLEAN) +#@ run-clean-*: kill and remove the container image of the matched command. +$(TARGETS_RUN_CLEAN): run-clean-%: + @echo "check container $(IMAGE_ARTIFACT)-$*"; \ + if [ -n "$$($(IMAGE_CMD) ps | grep "$(IMAGE_ARTIFACT)-$*")" ]; then \ + $(IMAGE_CMD) kill $(IMAGE_ARTIFACT)-$* > /dev/null && \ + echo "killed container $(IMAGE_ARTIFACT)-$*"; \ + fi; \ + if [ -n "$$($(IMAGE_CMD) ps -a | grep "$(IMAGE_ARTIFACT)-$*")" ]; then \ + $(IMAGE_CMD) rm $(IMAGE_ARTIFACT)-$* > /dev/null && \ + echo "removed container $(IMAGE_ARTIFACT)-$*"; \ + fi; \ + + +## Cleanup: targets to clean up sources, tools, and containers. + +#@ clean up resources created during build processes. +clean: $(TARGETS_CLEAN) +#@ clean up all resources, i.e. also tools installed for the build. +clean-all: $(TARGETS_CLEAN_ALL) +#@ clean up all resources created by initialization. +clean-init: clean-hooks +#@ clean up all hooks created by initialization. +clean-hooks:; rm -vrf .git/hooks/pre-commit; +#@ clean up all resources created by building and testing. +clean-build:; rm -vrf $(DIR_BUILD) $(DIR_RUN); + $(FIND) -name "mock_*_test.go" -exec rm -v {} \;; +#@ clean up all package dependencies by removing packages. +clean-deps: + for PACKAGE in $$(cat go.sum | cut -d ' ' -f 1 | sort -u); do \ + go get $${PACKAGE}@none; \ + done; +#@ clean up all running container images. +clean-run: $(TARGETS_CLEAN_RUN) +#@ clean-run-*: clean up matched running container image. +$(TARGETS_CLEAN_RUN): clean-run-%: run-clean-% + + +## Update: targets to update tools, configs, and dependencies. + +# TODO: +# 'update-deps' check how to utilize: 'go list -m -u -mod=mod all' + + +#@ update default or customized update targets. +update: $(TARGETS_UPDATE) +#@ check default or customized update targets for updates. +update?: $(TARGETS_UPDATE?) +#@ update all components. +update-all: $(TARGETS_UPDATE_ALL) +#@ check all components for udpdates. +update-all?: $(TARGETS_UPDATE_ALL?) + +#@ # update go to latest or given version. +update-go: + @if ! [[ "$(RUNARGS)" =~ "[0-9.]*" ]]; then \ + ARCH="linux-amd64"; BASE="https://go.dev/dl"; \ + VERSION="$$(curl --silent --header "Accept-Encoding: gzip" \ + "$${BASE}/" | gunzip | grep -o "go[0-9.]*$${ARCH}.tar.gz" | \ + head -n 1 | sed "s/go\(.*\)\.[0-9]*\.$${ARCH}\.tar\.gz/\1/")"; \ + else VERSION="$(RUNARGS)"; fi; \ + echo "info: update golang version to $${VERSION}"; \ + go mod tidy -go=$${VERSION}; \ + if [ -f $(FILE_DELIVERY) ]; then \ + sed -E -i -e "s/$(FILE_DELIVERY_REGEX)/\1$${VERSION}/" $(FILE_DELIVERY); \ + fi; \ + if [ "$(GOVERSION)" != "$${VERSION}" ]; then \ + echo "warning: current compiler is using $(GOVERSION)" >/dev/stderr; \ + fi; \ + +#@ check whether a new go version exists. +update-go?: + @ARCH="linux-amd64"; BASE="https://go.dev/dl"; \ + VERSION="$$(curl --silent --header "Accept-Encoding: gzip" \ + "$${BASE}/" | gunzip | grep -o "go[0-9.]*$${ARCH}.tar.gz" | \ + head -n 1 | sed "s/go\(.*\)\.[0-9]*\.$${ARCH}\.tar\.gz/\1/")"; \ + if [ "$(GOVERSION)" != "$${VERSION}" ]; then \ + echo "info: new golang version $${VERSION} available"; \ + echo -e "\trun 'make update-go' to update the project!"; \ + exit -1; \ + fi; \ + +# Functions to create the list of updates (not sure whether go list is contributing). +update-args = \ + if [[ " $(1) " =~ " major " ]]; then ARGS="-cached false -major"; \ + elif [[ " $(1) " =~ " pre " ]]; then ARGS="-cached false -pre"; \ + else ARGS="-cached false"; fi +update-list = \ + ( gomajor list $(1) 2>/dev/null | sed "s/://1; s/\[latest /\[/g"; \ + go list -m -u -mod=mod $(2) | grep "\[.*\]$$" \ + ) | sort -u | sed "s/\]//g; s/\[/=> /" + +#@ [major|pre|minor] # update minor (and major) dependencies to latest versions. +update-deps: test-go update/go.mod +update/go.mod: $(GOBIN)/gomajor + @if [ -n "$(RUNARGS)" ]; then $(call update-args,$(RUNARGS)); \ + if [[ " $(RUNARGS) " =~ " minor " ]]; then PACKAGES="all"; fi; \ + readarray -t UPDATES < <($(call update-list,$${ARGS},$${PACKAGES})); \ + for UPDATE in "$${UPDATES[@]}"; do ARGS=($${UPDATE}); \ + SMAJOR="$${ARGS[1]%%.*}"; TMAJOR="$${ARGS[3]%%.*}"; \ + if [ "$${SMAJOR}" != "$${TMAJOR}" ]; then \ + if [[ "$${ARGS[0]}" == *$${SMAJOR}* ]]; then \ + TARGET="$${ARGS[0]/$${SMAJOR}/$${TMAJOR}}"; \ + else TARGET="$${ARGS[0]}/$${TMAJOR}"; fi; \ + echo "update: $${ARGS[0]}@$${ARGS[2]} => $${TARGET}@$${ARGS[3]}"; \ + sed -i -e "s#$${ARGS[0]}#$${TARGET}#g" $(SOURCES); \ + else \ + echo "update: $${ARGS[0]}@$${ARGS[1]} => $${ARGS[3]}"; \ + go get "$${ARGS[0]}@$${ARGS[3]}"; \ + fi; \ + done; \ + else go get -u ./...; fi; \ + ROOT=$$(pwd); \ + for DIR in $(MODULES); do \ + echo "update: $${DIR}"; cd $${ROOT}/$${DIR##./} && \ + go mod tidy -x -v -e -compat=$(GOVERSION) || exit -1; \ + done; \ + +#@ check for major and minor updates to latest versions. +update-deps?: test-go update/go.mod? +update/go.mod?: $(GOBIN)/gomajor + @$(call update-args,$(RUNARGS)); cp go.sum go.sum.~save~; \ + $(call update-list,$${ARGS},all); mv go.sum.~save~ go.sum; + +#@ update this build environment to latest version. +update-make: $(TARGETS_UPDATE_MAKE) +$(TARGETS_UPDATE_MAKE): update/%: update-base + @DIR="$$(pwd)"; cd $(BASEDIR); \ + if [ ! -e "$${DIR}/$*" ]; then touch "$${DIR}/$*"; fi; \ + DIFF="$$(diff <(git show HEAD:$* 2>/dev/null) $${DIR}/$*)"; \ + if [ -n "$${DIFF}" ]; then \ + if [ -n "$$(cd $${DIR}; git diff $*)" ]; then \ + echo "info: $* is blocked (has been changed)"; \ + else echo "info: $* is updated"; \ + git show HEAD:$* > $${DIR}/$* 2>/dev/null; \ + fi; \ + fi; \ + +#@ check whether updates for build environment exist. +update-make?: $(TARGETS_UPDATE_MAKE?) +$(TARGETS_UPDATE_MAKE?): update/%?: update-base + @DIR="$$(pwd)"; cd $(BASEDIR); \ + if [ ! -e "$${DIR}/$*" ]; then \ + echo "info: $* is not tracked"; \ + else DIFF="$$(diff <(git show HEAD:$*) $${DIR}/$*)"; \ + if [ -n "$${DIFF}" ]; then \ + if [ -n "$$(cd $${DIR}; git diff $*)" ]; then \ + echo "info: $* is blocked (has been changed)"; \ + else echo "info: $* needs update"; fi; \ + fi; \ + fi; \ + +#@ create a clone of the base repository to update from. +BASE := git@github.com:tkrop/go-make.git +update-base: + @$(eval BASEDIR = $(shell mktemp -d)) ( \ + trap "rm -rf $${DIR}" INT TERM EXIT; \ + while pgrep $(MAKE) > /dev/null; do sleep 1; done; \ + rm -rf $${DIR} \ + ) & \ + git clone --no-checkout --depth 1 --single-branch \ + --branch main $(BASE) $(BASEDIR) 2> /dev/null; \ + +#@ update all tools required by the project. +update-tools: $(TARGETS_UPDATE_GO) $(TARGETS_INSTALL_SH) $(TARGETS_INSTALL_NPM) + go mod tidy -compat=${GOVERSION}; +#@ update-*: update the matched software command or service. + +# update go tools used by the project. +$(TARGETS_UPDATE_GO): update-%: + go install $(call go-pkg,update,$(TOOLS_GO),^$*$$); + +## Custom: custom extension targets. + +# Include custom targets to extend scripts. +ifneq ("$(wildcard Makefile.ext)","") + include Makefile.ext +endif diff --git a/Makefile.ext b/Makefile.ext new file mode 100644 index 0000000..0810004 --- /dev/null +++ b/Makefile.ext @@ -0,0 +1,11 @@ +.PHONY: install-go-make uninstall-go-make +install-go-make: uninstall-go-make + mkdir -p $(GOBIN)/go-make.config; + cp -rf . $(GOBIN)/go-make.config; + +uninstall-go-make: uninstall-go-make.config +uninstall-go-make.config: + rm -rf $(GOBIN)/go-make.config; + +clean: + rm -rf __debug_bin*; \ No newline at end of file diff --git a/Makefile.vars b/Makefile.vars new file mode 100644 index 0000000..b1d9e25 --- /dev/null +++ b/Makefile.vars @@ -0,0 +1,30 @@ +# Setup code quality level (default: base). +CODE_QUALITY := plus + +# Setup codacy integration (default: enabled [enabled, disabled]). +CODACY := enabled +# Customizing codacy server for open source. +CODACY_API_BASE_URL := https://api.codacy.com +# Continue after codacy shows violation (default: false / true [cdp-pipeline]). +#CODACY_CONTINUE := true + +# Setup required targets before testing (default: ). +#TEST_DEPS := run-db +# Setup required targets before running commands (default: ). +#RUN_DEPS := run-db +# Setup required aws services for testing (default: ). +#AWS_SERVICES := + +# Setup when to push images (default: pulls [never, pulls, merges]) +IMAGE_PUSH ?= never + +# Setup default test timeout (default: 10s). +TEST_TIMEOUT := 10s + +# Setup custom delivery file (default: delivery.yaml). +FILE_DELIVERY := .github/workflows/go.yaml + +# Custom linters applied to prepare next level (default: ). +LINTERS_CUSTOM := nonamedreturns tagliatelle +# Linters swithed off to complete next level (default: ). +LINTERS_DISABLED := diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8204e0 --- /dev/null +++ b/README.md @@ -0,0 +1,235 @@ +# Make framework + +[![Build][build-badge]][build-link] +[![Coverage][coveralls-badge]][coveralls-link] +[![Coverage][coverage-badge]][coverage-link] +[![Quality][quality-badge]][quality-link] +[![Report][report-badge]][report-link] +[![FOSSA][fossa-badge]][fossa-link] +[![License][license-badge]][license-link] +[![Docs][docs-badge]][docs-link] + + +[build-badge]: https://github.com/tkrop/go-make/actions/workflows/go.yaml/badge.svg +[build-link]: https://github.com/tkrop/go-make/actions/workflows/go.yaml + +[coveralls-badge]: https://coveralls.io/repos/github/tkrop/go-make/badge.svg?branch=main +[coveralls-link]: https://coveralls.io/github/tkrop/go-make?branch=main + +[coverage-badge]: https://app.codacy.com/project/badge/Coverage/b2bb898346ae4bb4be6414cd6dfe4932 +[coverage-link]: https://www.codacy.com/gh/tkrop/go-make/dashboard?utm_source=github.com&utm_medium=referral&utm_content=tkrop/go-make&utm_campaign=Badge_Coverage + +[quality-badge]: https://app.codacy.com/project/badge/Grade/b2bb898346ae4bb4be6414cd6dfe4932 +[quality-link]: b2bb898346ae4bb4be6414cd6dfe4932https://app.codacy.com/gh/tkrop/go-make/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade + +[report-badge]: https://goreportcard.com/badge/github.com/tkrop/go-make +[report-link]: https://goreportcard.com/report/github.com/tkrop/go-make + +[fossa-badge]: https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftkrop%2Fgo-make.svg?type=shield&issueType=license +[fossa-link]: https://app.fossa.com/projects/git%2Bgithub.com%2Ftkrop%2Fgo-make?ref=badge_shield&issueType=license + +[license-badge]: https://img.shields.io/badge/License-MIT-yellow.svg +[license-link]: https://opensource.org/licenses/MIT + +[docs-badge]: https://pkg.go.dev/badge/github.com/tkrop/go-make.svg +[docs-link]: https://pkg.go.dev/github.com/tkrop/go-make + + + +## Introduction + +Goal of `go-make` is to provide a simple, versioned build environment for +standard [`go`][go]-projects (see [Standard `go`-project](#standard-go-project) +for details) providing default targets and tool configs for testing, linting, +building, installing, updating, running, and releasing libraries, commands, and +container images. + +`go-make` can be either run as command line tool or hooked into an existing +project as minimal [`Makefile`](Makefile). Technically `go-make` is just a thin +wrapper around a very generic and extensible [`Makefile`](Makefile.base) that +is based on a standard [`go`][go]-project supporting different tools: + +* [`gomock`][gomock] - go generating mocks. +* [`codacy`][codacy] - for code quality documentation. +* [`golangci-lint`][golangci] - for pre-commit linting. +* [`zally`][zally] - for pre-commit API linting. +* [`gitleaks`][gitleaks] - for sensitive data scanning. +* [`grype`][grype] - for security scanning. +* [`syft`][syft] - for material listing. + +The thin wrapper provides the necessary version control for the `Makefile` and +the default config of integrated tools. It installs these tools automatically +when needed in the latest available version. + +**Note:** We except the risk that using the latest versions of tools, e.g. for +linting, may break the build for the sake of constantly updating dependencies by +default. For tools where this is not desireable, the default import can be +changed to contain a version (see [manual](MANUAL.md] for more information). + +**Warning:** `go-make` automatically installs a `pre-commit` hook overwriting +and deleting any pre-existing hook. The hook calls `go-make commit` to enforce +run unit testing and linting successfully before allowing to commit, i.e. the +goals `test-go`, `test-unit`, `lint-base` (or what code quality level is +defined as standard), and `lint-markdown`. + + +[gomock]: +[golangci]: +[codacy]: +[zally]: +[gitleaks]: +[grype]: +[syft]: + + +## Installation + +To install `go-make` simply use the standard [`go` install][go-install] +command (or any other means, e.g. [`curl`][curl] to obtain a released binary): + +```bash +go install github.com/tkrop/go-make@latest +``` + +The scripts and configs are automatically checked out in the version matching +the wrapper. `go-make` has the following dependencies, that must be satisfied +by the runtime environment, e.g. using [`ubuntu-20.04`][ubuntu-20.04] or +[`ubuntu-22.04`][ubuntu-22.04]: + +* [GNU `make`][make] (^4.2). +* [GNU `bash`][bash] (^5.0). +* [GNU `coreutils`][core] (^8.30) +* [GNU `findutils`][find] (^4.7) +* [GNU `awk`][awk] (^5.0). +* [GNU `sed`][sed] (^4.7) +* [`curl`][curl] (^7) + +[ubuntu-20.04]: +[ubuntu-22.04]: +[go-install]: +[curl]: +[make]: +[bash]: +[core]: +[find]: +[awk]: +[sed]: + + +## Example usage + +After installing `go-make` and in the build environment, you can run all targets +by simply calling `go-make ` on the command line, in another `Makefile`, +in a github action, or any other delivery pipeline config script: + +```bash +go-make all # execute a whole build pipeline depending on the project. +go-make test lint # execute only test 'test' and 'lint' steps of a pipeline. +go-make image # execute minimal steps to create all container images. +``` + +If you like to integrate `go-make` into another `Makefile` you may find the +following template helpful: + +```Makefile +GOBIN ?= $(shell go env GOPATH)/bin +GOMAKE := github.com/tkrop/go-make@latest +TARGETS := $(shell command -v go-make >/dev/null || \ + go install $(GOMAKE) && go-make targets) + +# Include standard targets from go-make providing group targets as well as +# single target targets. The group target is used to delegate the remaining +# request targets, while the single target can be used to define the +# precondition of custom target. +.PHONY: $(TARGETS) $(addprefix target/,$(TARGETS)) +$(TARGETS):; $(GOBIN)/go-make $(MAKEFLAGS) $(MAKECMDGOALS); +$(addprefix target/,$(TARGETS)): target/%: + $(GOBIN)/go-make $(MAKEFLAGS) $*; +``` + +For further examples see [`go-make` manual](MANUAL.md). + +**Note:** To setup command completion for `go-make`add the following command to +your `.bashrc`. + +```bash +source <(go-make --completion=bash) +``` + + +## Standard `go`-Project + +The [Makefile](Makefile) provided in this project is working under the +conventions of a standard [`go`][go]-project. The standard [`go`][go]-project +is defined to meet Zalando in-house requirements, but is general enough to be +useful in open source projects too. It adheres to the following conventions: + +1. All commands (services, jobs) are provided by a `main.go` placed as usual + under `cmd` using the pattern `cmd//main.go` or in the root folder. In + the latter case the project name is used as command name. + +2. All source code files and package names following the [`go`][go]-standard + only consist of lower case ASCII letters, hyphens (`-`), underscores (`_`), + and dots (`.`). Files are ending with `.go` to be eligible. + +3. Modules are placed in any sub-path of the repository, e.g. in `pkg`, `app`, + `internal` are commonly used patterns, except for `build` and `run`. These + are used by `go-make` as temporary folders to build commands and run commands + and are cleaned up regularly. + +4. The build target provides build context values to set up global variables in + `main` and `config` packages. + + * `Path` - the formal package path of the command build. + * `Version` - the version as provided by the `VERSION`-file in project root + or via the `(BUILD_)VERSION` environ variables. + * `Revision` - the actual full commit hash (`git rev-parse HEAD`). + * `Build` - the current timestamp of the build (`date --iso-8601=seconds`). + * `Commit` - the timestamp of the actual commit timestamp + (`git log -1 --date=iso8601-strict --format=%cd`). + * `Dirty` - the information whether build repository had uncommitted changes. + +5. All container image build files must start with a common prefix (default is + `Dockerfile`). The image name is derived from the organization and repository + names and can contain an optional suffix, i.e `/(-)`. + +6. For running a command in a container image, make sure that the command is + installed in the default execution directory of the container image - usually + the root directory. The container image must either be generated with suffix + matching the command or without suffix. + +All targets in the [Makefile](Makefile) are designated to autonomously set up +setup the [`go`][go]-project, installing the necessary tools - except for the +golang compiler and build environment -, and triggering the precondition +targets as necessary. + +[go]: + + +## Terms of usage + +This software is open source as is under the MIT license. If you start using +the software, please give it a star, so that I know to be more careful with +changes. If this project has more than 25 Stars, I will introduce semantic +versions for changes. + + +## Building + +The project is using itself for building as a proof of concept. + + +## Contributing + +If you like to contribute, please create an issue and/or pull request with a +proper description of your proposal or contribution. I will review it and +provide feedback on it. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..4e379d2 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.2 diff --git a/fixtures/targets-trace.out b/fixtures/targets-trace.out new file mode 100644 index 0000000..80f47c1 --- /dev/null +++ b/fixtures/targets-trace.out @@ -0,0 +1,181 @@ +make[2]: Entering directory '/home/tkrop/workspace/open/go-make' +{"path":"github.com/tkrop/go-make","repo":"git@github.com:tkrop/go-make","version":"v0.0.0-20231120093414-d4ee9e7daa60","revision":"d4ee9e7daa606a8b438daa1b44f17794ac9202b8","build":"2023-11-20T10:37:15+01:00","commit":"2023-11-20T10:34:14+01:00","dirty":true,"go":"1.21.2","platform":"linux/amd64","compiler":"gc"} +make --file /home/tkrop/workspace/go/bin/go-make.config/Makefile.base targets --trace [/home/tkrop/workspace/open/go-make] +/home/tkrop/workspace/go/bin/go-make.config/Makefile.base:383: target 'targets' does not exist +make --no-builtin-rules --no-builtin-variables --print-data-base \ + --question --makefile=/home/tkrop/workspace/go/bin/go-make.config/Makefile.base 2>/dev/null | \ + if [ "" != "raw" ]; then awk -v RS= -F: ' \ + /(^|\n)# Files(\n|$)/,/(^|\n)# Finished Make data base/ { \ + if ($1 !~ "^[#.]") { print $1 } \ + }' | sort | egrep -v -e '^[^[:alnum:]]'; \ + else cat -; fi || true; +all +all-clean +build +build-go-make +build-image +build-linux +build-native +bump +clean +clean-all +clean-build +clean-deps +clean-hooks +clean-init +clean-run +clean-run-aws +clean-run-db +clean-run-go-make +commit +fix +go.mod +go.sum +help +image +image-build +image-push +init +init-all +init-codacy +init-go +init-gosec +init-hooks +init-make +init-make! +init-sources +init-staticcheck +install +install-all +install-deadcode +install-gitleaks +install-gocognit +install-gocyclo +install-golangci-lint +install-gomajor +install-go-make +install-gosec +install-govulncheck +install-grype +install-markdownlint +install-mock +install-mockgen +install-revive +install-staticcheck +install-syft +install-vegeta +install-zally +lint +lint-aligncheck +lint-all +lint-apis +lint-base +lint-codacy +lint-config +lint-deadcode +lint-gocognit +lint-gocyclo +lint-gosec +lint-leaks +lint-leaks? +lint-markdown +lint-max +lint-min +lint-plus +lint-revive +lint-shell +lint-staticcheck +lint-vuln +list +mock_gomake_test.go +publish +publish-all +publish-go-make +push +release +run-aws +run-clean +run-clean-aws +run-clean-db +run-clean-go-make +run-db +run-go-go-make +run-go-make +run-image +run-image-go-make +run-native +show +targets +test +test-all +test-bench +test-clean +test-cover +test-go +test-self +test-unit +test-upload +udate-go? +uninstall +uninstall-all +uninstall-codacy-gosec +uninstall-codacy-staticcheck +uninstall-deadcode +uninstall-gitleaks +uninstall-gocognit +uninstall-gocyclo +uninstall-golangci-lint +uninstall-gomajor +uninstall-go-make +uninstall-go-make.config +uninstall-gosec +uninstall-govulncheck +uninstall-markdownlint +uninstall-mock +uninstall-mockgen +uninstall-revive +uninstall-staticcheck +uninstall-vegeta +uninstall-zally +update +update? +update-all +update-all? +update-base +update/.codacy.yaml +update/.codacy.yaml? +update-deadcode +update-deps +update-deps? +update-gitleaks +update/.gitleaks.toml +update/.gitleaks.toml? +update-go +update-go? +update-gocognit +update-gocyclo +update-golangci-lint +update/.golangci.yaml +update/.golangci.yaml? +update-gomajor +update/go.mod +update/go.mod? +update-gosec +update-govulncheck +update-make +update-make? +update/Makefile +update/Makefile? +update/Makefile.vars +update/.markdownlint.yaml +update/.markdownlint.yaml? +update-mock +update-mockgen +update-revive +update/revive.toml +update/revive.toml? +update-staticcheck +update-tools +update-vegeta +update-zally +make[2]: Leaving directory '/home/tkrop/workspace/open/go-make' diff --git a/fixtures/targets.out b/fixtures/targets.out new file mode 100644 index 0000000..90f44c6 --- /dev/null +++ b/fixtures/targets.out @@ -0,0 +1,171 @@ +make[2]: Entering directory '/home/tkrop/workspace/open/go-make' +all +all-clean +build +build-go-make +build-image +build-linux +build-native +bump +clean +clean-all +clean-build +clean-deps +clean-hooks +clean-init +clean-run +clean-run-aws +clean-run-db +clean-run-go-make +commit +fix +go.mod +go.sum +help +image +image-build +image-push +init +init-all +init-codacy +init-go +init-gosec +init-hooks +init-make +init-make! +init-sources +init-staticcheck +install +install-all +install-deadcode +install-gitleaks +install-gocognit +install-gocyclo +install-golangci-lint +install-gomajor +install-go-make +install-gosec +install-govulncheck +install-grype +install-markdownlint +install-mock +install-mockgen +install-revive +install-staticcheck +install-syft +install-vegeta +install-zally +lint +lint-aligncheck +lint-all +lint-apis +lint-base +lint-codacy +lint-config +lint-deadcode +lint-gocognit +lint-gocyclo +lint-gosec +lint-leaks +lint-leaks? +lint-markdown +lint-max +lint-min +lint-plus +lint-revive +lint-shell +lint-staticcheck +lint-vuln +list +mock_gomake_test.go +publish +publish-all +publish-go-make +push +release +run-aws +run-clean +run-clean-aws +run-clean-db +run-clean-go-make +run-db +run-go-go-make +run-go-make +run-image +run-image-go-make +run-native +show +targets +test +test-all +test-bench +test-clean +test-cover +test-go +test-self +test-unit +test-upload +udate-go? +uninstall +uninstall-all +uninstall-codacy-gosec +uninstall-codacy-staticcheck +uninstall-deadcode +uninstall-gitleaks +uninstall-gocognit +uninstall-gocyclo +uninstall-golangci-lint +uninstall-gomajor +uninstall-go-make +uninstall-go-make.config +uninstall-gosec +uninstall-govulncheck +uninstall-markdownlint +uninstall-mock +uninstall-mockgen +uninstall-revive +uninstall-staticcheck +uninstall-vegeta +uninstall-zally +update +update? +update-all +update-all? +update-base +update/.codacy.yaml +update/.codacy.yaml? +update-deadcode +update-deps +update-deps? +update-gitleaks +update/.gitleaks.toml +update/.gitleaks.toml? +update-go +update-go? +update-gocognit +update-gocyclo +update-golangci-lint +update/.golangci.yaml +update/.golangci.yaml? +update-gomajor +update/go.mod +update/go.mod? +update-gosec +update-govulncheck +update-make +update-make? +update/Makefile +update/Makefile? +update/Makefile.vars +update/.markdownlint.yaml +update/.markdownlint.yaml? +update-mock +update-mockgen +update-revive +update/revive.toml +update/revive.toml? +update-staticcheck +update-tools +update-vegeta +update-zally +make[2]: Leaving directory '/home/tkrop/workspace/open/go-make' diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8b33fbb --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module github.com/tkrop/go-make + +go 1.21 + +toolchain go1.21.0 + +require ( + github.com/stretchr/testify v1.8.4 + golang.org/x/exp v0.0.0-20231127185646-65229373498e +) + +require ( + github.com/kr/text v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/mock v1.6.0 + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/tkrop/go-testing v0.0.0-20231116145516-bd6d45b76891 + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..539f675 --- /dev/null +++ b/go.sum @@ -0,0 +1,47 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tkrop/go-testing v0.0.0-20231116145516-bd6d45b76891 h1:GEyXaeqyw7w2IvDo3fD8bzryOxN0Ss55G17yV8saEoU= +github.com/tkrop/go-testing v0.0.0-20231116145516-bd6d45b76891/go.mod h1:QH2+6OBdka8pcAAhCyc+/SoCJwWaUoC7UB3FOssdcX8= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..c17948c --- /dev/null +++ b/main.go @@ -0,0 +1,501 @@ +package main //revive:disable:max-public-structs // keep it simple + +import ( + _ "embed" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "runtime/debug" + "strconv" + "strings" + "time" + + "golang.org/x/exp/slices" +) + +var ( + // Path contains the package path. + Path string + // Version contains the custom version. + Version string + // Build contains the custom build time. + Build string + // Revision contains the custom revision. + Revision string + // Commit contains the custom commit time. + Commit string + // Dirty contains the custom dirty flag. + Dirty string +) + +const ( + // RepoPathSepNum is the number of the repository path separator. + RepoPathSepNum = 3 + // GitSha1HashLen is the full length of sha1-hashes used in git. + GitFullHashLen = 40 + // GitShortHashLen is the short length of sha1-hashes used in git. + GitShortHashLen = 7 + + // Makefile base makefile to be executed by go-make. + Makefile = "Makefile.base" + // bashCompletion contains the bash completion script for go-make. + BashCompletion = `### bash completion for go-make + function __complete_go-make() { + COMPREPLY=($(compgen -W "$(go-make targets)" -- "${COMP_WORDS[COMP_CWORD]}")); + } + complete -F __complete_go-make go-make; + ` +) + +var ( + // Regexp for semantic versioning as supported by go as tag. + semVersionTagRegex = regexp.MustCompile( + `v(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)` + + `(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)` + + `(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` + + `(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) + // Regexp for filtering entering and leaving lines from make output. + makeFilterRegexp = regexp.MustCompile( + `(?s)make\[[0-9]+\]: (Entering|Leaving) directory '[^\n]*'\n?`) +) + +// Info provides the build information of a command or module. +type Info struct { + // Path contains the package path of the command or module. + Path string `yaml:"path,omitempty" json:"path,omitempty"` + // Repo contains the repository of the command or module. + Repo string `yaml:"repo,omitempty" json:"repo,omitempty"` + // Version contains the actual version of the command or module. + Version string `yaml:"version,omitempty" json:"version,omitempty"` + // Revision contains the revision of the command or module from version + // control system. + Revision string `yaml:"revision,omitempty" json:"revision,omitempty"` + // Build contains the build time of the command or module. + Build time.Time `yaml:"build,omitempty" json:"build,omitempty"` + // Commit contains the last commit time of the command or module from the + // version control system. + Commit time.Time `yaml:"commit,omitempty" json:"commit,omitempty"` + // Dirty flags whether the build of the command or module is based on a + // dirty local repository state. + Dirty bool `yaml:"dirty,omitempty" json:"dirty,omitempty"` + // Checksum contains the check sum of the command or module. + Checksum string `yaml:"checksum,omitempty" json:"checksum,omitempty"` + + // Go contains the go version the command or module was build with. + Go string `yaml:"go,omitempty" json:"go,omitempty"` + // Platform contains the build platform the command or module was build + // for. + Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` + // Compiler contains the actual compiler the command or module was build + // with. + Compiler string `yaml:"compiler,omitempty" json:"compiler,omitempty"` +} + +// NewDefaultInfo returns the build information of a command or module with +// default values. +func NewDefaultInfo() *Info { + return NewInfo(Path, Version, Revision, Build, Commit, Dirty) +} + +// NewInfo returns the build information of a command or module using given +// custom version and custom build time using RFC3339 format. The provided +// version must follow semantic versioning as supported by go. +func NewInfo(path, version, revision, build, commit, dirty string) *Info { + info := &Info{ + Go: runtime.Version()[2:], + Compiler: runtime.Compiler, + Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), + } + + info.Version = version + info.Revision = revision + info.Build, _ = time.Parse(time.RFC3339, build) + info.Commit, _ = time.Parse(time.RFC3339, commit) + info.Dirty, _ = strconv.ParseBool(dirty) + if buildinfo, ok := debug.ReadBuildInfo(); ok { + if path != "" { + info.Path = path + info.Repo = "git@" + strings.Replace( + info.splitRuneN(path, '/', RepoPathSepNum), "/", ":", 1) + } else { + info.Path = buildinfo.Main.Path + info.Repo = "git@" + strings.Replace(buildinfo.Main.Path, "/", ":", 1) + } + + if semVersionTagRegex.MatchString(buildinfo.Main.Version) { + info.Version = buildinfo.Main.Version + index := strings.LastIndex(buildinfo.Main.Version, "-") + info.Revision = buildinfo.Main.Version[index+1:] + } + + info.Checksum = buildinfo.Main.Sum + for _, kv := range buildinfo.Settings { + switch kv.Key { + case "vcs.revision": + info.Revision = kv.Value + case "vcs.time": + info.Commit, _ = time.Parse(time.RFC3339, kv.Value) + case "vcs.modified": + info.Dirty = kv.Value == "true" + } + } + } + + if !semVersionTagRegex.MatchString(info.Version) { + if info.Revision != "" && !info.Commit.Equal(time.Time{}) { + info.Version = fmt.Sprintf("v0.0.0-%s-%s", + info.Commit.UTC().Format("20060102150405"), info.Revision[0:12]) + } + } + + return info +} + +// splitRuneN splits the string s at the `n`th occurrence of the rune ch. +func (*Info) splitRuneN(s string, ch rune, n int) string { + count := 0 + index := strings.IndexFunc(s, func(is rune) bool { + if ch == is { + count++ + } + return count == n + }) + if index >= 0 { + return s[0:index] + } + return s +} + +// MakeFilter is a custom filter that implements the io.Writer interface. +type MakeFilter struct { + writer io.Writer + filter *regexp.Regexp +} + +func NewMakeFilter(writer io.Writer) *MakeFilter { + return &MakeFilter{ + writer: writer, + filter: makeFilterRegexp, + } +} + +// Write writes the given data to the underlying writer. +func (f *MakeFilter) Write(data []byte) (int, error) { + return f.writer.Write(f.filter.ReplaceAll(data, []byte{})) //nolint:wrapcheck // not used +} + +// CmdExecutor provides a common interface for executing commands. +type CmdExecutor interface { + // Exec executes the command with given name and arguments in given + // directory redirecting stdout and stderr to given writers. + Exec(stdout, stderr io.Writer, dir, name string, args ...string) error + // Trace returns flags whether the command executor traces the command + // execution. + Trace() bool +} + +// DefaultCmdExecutor provides a default command executor using `os/exec` +// supporting optional tracing. +type DefaultCmdExecutor struct { + trace bool +} + +// NewCmdExecutor creates a new default command executor with given trace flag. +func NewCmdExecutor(trace bool) CmdExecutor { + return &DefaultCmdExecutor{trace: trace} +} + +// Exec executes the command with given name and arguments in given directory +// redirecting stdout and stderr to given writers. +func (e *DefaultCmdExecutor) Exec( + stdout, stderr io.Writer, dir, name string, args ...string, +) error { + if e.trace { + fmt.Fprintf(stdout, "%s %s [%s]\n", + name, strings.Join(args, " "), dir) + } + + cmd := exec.Command(name, args...) + cmd.Dir, cmd.Env = dir, os.Environ() + // cmd.Env = append(cmd.Env, "MAKE=go-make") + cmd.Stdout, cmd.Stderr = stdout, stderr + + return cmd.Run() //nolint:wrapcheck // checked on next layer +} + +// Trace returns flags whether the command executor traces the command +// execution. +func (e *DefaultCmdExecutor) Trace() bool { + return e.trace +} + +// Printer provides a common interface for printing. +type Printer interface { + Fprintf(io.Writer, string, ...any) +} + +type DefaultPrinter struct{} + +func (*DefaultPrinter) Fprintf(writer io.Writer, format string, args ...any) { + fmt.Fprintf(writer, format, args...) +} + +// Logger provides a common interface for logging. +type Logger interface { + // Logs the build information of the command or module to the given writer. + Info(writer io.Writer, info *Info, raw bool) + // Logs the call of the command to the given writer. + Call(writer io.Writer, args ...string) + // Logs the given error message and error to the given writer. + Error(writer io.Writer, message string, err error) + // Logs the given message to the given writer. + Message(writer io.Writer, message string) +} + +// DefaultLogger provides a default logger using `fmt` and `json` package. +type DefaultLogger struct { + // fmt provides the print formater. + fmt Printer +} + +// NewLogger creates a new default logger. +func NewLogger() Logger { + return &DefaultLogger{fmt: &DefaultPrinter{}} +} + +// Info logs the build information of the command or module to the given +// writer. +func (log *DefaultLogger) Info(writer io.Writer, info *Info, raw bool) { + if out, err := json.Marshal(info); err != nil { + log.fmt.Fprintf(writer, "info: %v\n", err) + } else if raw { + log.fmt.Fprintf(writer, "info: %s\n", out) + } else { + log.fmt.Fprintf(writer, "%s\n", out) + } +} + +// Call logs the call of the command to the given writer. +func (log *DefaultLogger) Call(writer io.Writer, args ...string) { + log.fmt.Fprintf(writer, "call: %s\n", strings.Join(args, " ")) +} + +// Error logs the given error message and error to the given writer. +func (log *DefaultLogger) Error(writer io.Writer, message string, err error) { + if err != nil { + log.fmt.Fprintf(writer, "error: %s: %v\n", message, err) + } else { + log.fmt.Fprintf(writer, "error: %s\n", message) + } +} + +// Message logs the given message to the given writer. +func (*DefaultLogger) Message(writer io.Writer, message string) { + fmt.Fprintf(writer, "%s\n", message) +} + +// GoMake provides the default `go-make` application context. +type GoMake struct { + // Info provides the build information of go-make. + Info *Info + // The directory of the go-make command. + CmdDir string + // The actual working directory. + WorkDir string + // The path to the go-make command Makefile. + Makefile string + // Executor provides the command executor. + Executor CmdExecutor + // Logger provides the logger. + Logger Logger + // Stdout provides the standard output writer. + Stdout io.Writer + // Stderr provides the standard error writer. + Stderr io.Writer +} + +// NewGoMake returns a new default `go-make` application context with given +// standard output writer, standard error writer, and trace flag. +func NewGoMake( + stdout, stderr io.Writer, info *Info, trace bool, +) *GoMake { + cmd, _ := os.Executable() + cmdDir := cmd + ".config" + makefile := filepath.Join(cmdDir, Makefile) + workdir, _ := os.Getwd() + + return &GoMake{ + Info: info, + CmdDir: cmdDir, + WorkDir: workdir, + Makefile: makefile, + Executor: NewCmdExecutor(trace), + Logger: NewLogger(), + Stdout: stdout, + Stderr: stderr, + } +} + +// Updates the go-make command. +func (gm *GoMake) updateGoMake() error { + if _, err := os.Stat(gm.CmdDir); os.IsNotExist(err) { + if err := gm.cloneRepo(); err != nil { + return err + } else if err := gm.setRevision(); err != nil { + return err + } + return nil + } else if err != nil { + return ErrConfigFailure(gm.CmdDir, err) + } + + if gm.Info.Revision != "" { + if revision, err := gm.getRevision(); err != nil { + return err + } else if strings.HasPrefix(revision, gm.Info.Revision) { + return nil + } + } + + return gm.updateRevision() +} + +// Clones the go-make command repository. +func (gm *GoMake) cloneRepo() error { + if err := gm.exec(gm.Stderr, gm.Stderr, gm.WorkDir, + "git", "clone", "--depth=1", gm.Info.Repo, gm.CmdDir); err != nil { + repo := "https://" + gm.Info.Path + ".git" + return gm.exec(gm.Stderr, gm.Stderr, gm.WorkDir, + "git", "clone", "--depth=1", repo, gm.CmdDir) + } + return nil +} + +// Updates the go-make command repository. +func (gm *GoMake) updateRepo() error { + return gm.exec(gm.Stderr, gm.Stderr, gm.CmdDir, + "git", "fetch", gm.Info.Repo) +} + +// Updates the go-make command repository revision. +func (gm *GoMake) updateRevision() error { + if gm.Info.Dirty { + return nil + } else if err := gm.updateRepo(); err != nil { + return err + } + return gm.setRevision() +} + +// Returns the current revision of the go-make command repository. +func (gm *GoMake) getRevision() (string, error) { + builder := strings.Builder{} + if err := gm.exec(&builder, gm.Stderr, gm.CmdDir, + "git", "rev-parse", "HEAD"); err != nil { + return "", err + } + return builder.String()[0:GitFullHashLen], nil +} + +// Sets the current revision of the go-make command repository. +func (gm *GoMake) setRevision() error { + revision := gm.Info.Revision + if revision == "" { + revision = "HEAD" + } else if len(revision) < GitFullHashLen { + revision = revision[0:GitShortHashLen] + } + + return gm.exec(gm.Stderr, gm.Stderr, gm.CmdDir, + "git", "reset", "--hard", revision) +} + +// Executes the provided make targets. +func (gm *GoMake) makeTarget(args ...string) error { + args = append([]string{"--file", gm.Makefile}, args...) + return gm.exec(gm.Stdout, gm.Stderr, gm.WorkDir, "make", args...) +} + +// Executes the command with given name and arguments in given directory +// calling the command executor taking care to wrap the resulting error. +func (gm *GoMake) exec( + stdout, stderr io.Writer, dir, name string, args ...string, +) error { + err := gm.Executor.Exec(stdout, stderr, dir, name, args...) + if err != nil { + return ErrCallFailed(name, args, err) + } + return nil +} + +// RunCmd runs the go-make command with given arguments. +func (gm *GoMake) RunCmd(args ...string) error { + if gm.Executor.Trace() { + gm.Logger.Call(gm.Stderr, args...) + gm.Logger.Info(gm.Stderr, gm.Info, false) + } + + switch { + case slices.Contains(args, "--version"): + gm.Logger.Info(gm.Stdout, gm.Info, false) + return nil + + case slices.Contains(args, "--completion=bash"): + gm.Logger.Message(gm.Stdout, BashCompletion) + return nil + } + + if err := gm.updateGoMake(); err != nil { + if !gm.Executor.Trace() { + gm.Logger.Call(gm.Stderr, args...) + gm.Logger.Info(gm.Stderr, gm.Info, false) + } + gm.Logger.Error(gm.Stderr, "update config", err) + return err + } + if err := gm.makeTarget(args...); err != nil { + if !gm.Executor.Trace() { + gm.Logger.Call(gm.Stderr, args...) + gm.Logger.Info(gm.Stderr, gm.Info, false) + } + gm.Logger.Error(gm.Stderr, "execute make", err) + return err + } + return nil +} + +// ErrCallFailed wraps the error of a failed command call. +func ErrCallFailed(name string, args []string, err error) error { + return fmt.Errorf("call failed [name=%s, args=%v]: %w", + name, args, err) +} + +// ErrConfigFailure wraps the error of a failed config update. +func ErrConfigFailure(dir string, err error) error { + return fmt.Errorf("config failure [dir=%s]: %w", dir, err) +} + +// Run runs the go-make command with given build information, standard output +// writer, standard error writer, and command arguments. +func Run(info *Info, stdout, stderr io.Writer, args ...string) error { + return NewGoMake( + // TODO we would like to filter some make file startup specific + // output that creates hard to validate output. + // NewMakeFilter(stdout), NewMakeFilter(stderr), + stdout, stderr, + info, slices.Contains(args, "--trace"), + ).RunCmd(args[1:]...) +} + +// main is the main entry point of the go-make command. +func main() { + if Run(NewDefaultInfo(), os.Stdout, os.Stderr, os.Args...) != nil { + os.Exit(1) + } + os.Exit(0) +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..e3ad50f --- /dev/null +++ b/main_test.go @@ -0,0 +1,658 @@ +package main_test + +import ( + "embed" + "errors" + "io" + "os" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + main "github.com/tkrop/go-make" + "github.com/tkrop/go-testing/mock" + "github.com/tkrop/go-testing/test" +) + +//revive:disable:line-length-limit // go:generate line length + +//go:generate mockgen -package=main_test -destination=mock_gomake_test.go -source=main.go CmdExecutor Logger + +//revive:enable:line-length-limit + +const ( + // GoMakeDirNew contains an arbitrary non-existing directory. + GoMakeDirNew = "new-dir" + // GoMakeDirExist contains an arbitrary existing directory (use build). + GoMakeDirExist = "build" + // GoMakeGit contains an arbitrary source repository for go-make. + GoMakeGit = "git@github.com:tkrop/go-make" + // GoMakeHTTP contains an arbitrary source repository for go-make. + GoMakeHTTP = "https://github.com/tkrop/go-make.git" + // OtherRevision contains an arbitrary revision different from default. + OtherRevision = "x1b66f320c950b25fa63b81fd4e660c5d1f9d758" +) + +var ( + // DefaultInfo captured from once existing state. + DefaultInfo = main.NewInfo("github.com/tkrop/go-make", + "v0.0.0-20231110152254-1b66f320c950", + "1b66f320c950b25fa63b81fd4e660c5d1f9d758e", + "2023-11-14T13:02:46+01:00", + "2023-11-10T16:22:54+01:00", + "false") + + // ShortInfo with hash to short captured from once existing state. + ShortInfo = main.NewInfo("github.com/tkrop/go-make", + "v0.0.0-20231110152254-1b66f320c950", + "1b66f320c950b25fa63b81fd4e660c5d1f9d758", + "2023-11-14T13:02:46+01:00", + "2023-11-10T16:22:54+01:00", + "false") + + // HeadInfo without hash captured from once existing state. + HeadInfo = main.NewInfo("github.com/tkrop/go-make", + "v0.0.0-20231110152254-1b66f320c950", + "", + "2023-11-14T13:02:46+01:00", + "2023-11-10T16:22:54+01:00", + "false") + + errAny = errors.New("any error") +) + +// GoMakeSetup sets up a new go-make test with mocks. +func GoMakeSetup( + t test.Test, param MainParams, +) (*main.GoMake, *mock.Mocks) { + mocks := mock.NewMocks(t). + SetArg("stdout", &strings.Builder{}). + SetArg("stderr", &strings.Builder{}). + SetArg("builder", &strings.Builder{}). + Expect(param.mockSetup) + + if param.info == nil { + param.info = DefaultInfo + } + + gm := main.NewGoMake( + mocks.GetArg("stdout").(io.Writer), + mocks.GetArg("stderr").(io.Writer), + param.info, false, + ) + + if param.goMakeDir == "" { + param.goMakeDir = GoMakeDirExist + } + + gm.Executor = mock.Get(mocks, NewMockCmdExecutor) + gm.Logger = mock.Get(mocks, NewMockLogger) + gm.Makefile = main.Makefile + gm.CmdDir = param.goMakeDir + gm.WorkDir = "." + + return gm, mocks +} + +func ToAny(args ...any) []any { + return args +} + +func Trace(trace bool) mock.SetupFunc { + return func(mocks *mock.Mocks) any { + return mock.Get(mocks, NewMockCmdExecutor).EXPECT(). + Trace().DoAndReturn(mocks.Do(main.CmdExecutor.Trace, trace)) + } +} + +func Exec( //revive:disable:argument-limit + stdout, stderr string, dir, name string, + args []string, err error, sout, serr string, +) mock.SetupFunc { + return func(mocks *mock.Mocks) any { + return mock.Get(mocks, NewMockCmdExecutor).EXPECT(). + Exec(mocks.GetArg(stdout), mocks.GetArg(stderr), dir, name, ToAny(args)...). + DoAndReturn(mocks.Call(main.CmdExecutor.Exec, + func(args ...any) []any { + if _, err := args[0].(io.Writer).Write([]byte(sout)); err != nil { + assert.Fail(mocks.Ctrl.T, "failed to write to stdout", err) + } + if _, err := args[1].(io.Writer).Write([]byte(serr)); err != nil { + assert.Fail(mocks.Ctrl.T, "failed to write to stderr", err) + } + return []any{err} + })) + } +} + +func LogCall(writer string, args []string) mock.SetupFunc { + return func(mocks *mock.Mocks) any { + return mock.Get(mocks, NewMockLogger).EXPECT(). + Call(mocks.GetArg(writer), ToAny(args)...). + DoAndReturn(mocks.Do(main.Logger.Call)) + } +} + +func LogInfo(writer string, info *main.Info, raw bool) mock.SetupFunc { + return func(mocks *mock.Mocks) any { + return mock.Get(mocks, NewMockLogger).EXPECT(). + Info(mocks.GetArg(writer), info, raw). + DoAndReturn(mocks.Do(main.Logger.Info)) + } +} + +func LogError(writer string, message string, err error) mock.SetupFunc { + return func(mocks *mock.Mocks) any { + return mock.Get(mocks, NewMockLogger).EXPECT(). + Error(mocks.GetArg(writer), message, err). + DoAndReturn(mocks.Do(main.Logger.Error)) + } +} + +func LogMessage(writer string, message string) mock.SetupFunc { + return func(mocks *mock.Mocks) any { + return mock.Get(mocks, NewMockLogger).EXPECT(). + Message(mocks.GetArg(writer), message). + DoAndReturn(mocks.Do(main.Logger.Message)) + } +} + +type MainParams struct { + mockSetup mock.SetupFunc + info *main.Info + goMakeDir string + args []string + expectError error +} + +var testMainParams = map[string]MainParams{ + // successful options without trace. + "check go-make version": { + mockSetup: mock.Chain(Trace(false), + LogInfo("stdout", DefaultInfo, false), + ), + args: []string{"--version"}, + }, + + "check go-make completion bash": { + mockSetup: mock.Chain(Trace(false), + LogMessage("stdout", main.BashCompletion), + ), + args: []string{"--completion=bash"}, + }, + + // successful options without trace. + "check go-make version with trace": { + mockSetup: mock.Chain(Trace(true), + LogCall("stderr", []string{"--trace", "--version"}), + LogInfo("stderr", DefaultInfo, false), + LogInfo("stdout", DefaultInfo, false), + ), + args: []string{"--trace", "--version"}, + }, + + "check go-make completion bash with trace": { + mockSetup: mock.Chain(Trace(true), + LogCall("stderr", []string{"--trace", "--completion=bash"}), + LogInfo("stderr", DefaultInfo, false), + LogMessage("stderr", main.BashCompletion), + ), + args: []string{"--trace", "--completion=bash"}, + }, + + // successful targets without trace. + "check go-make to run target": { + mockSetup: mock.Chain(Trace(false), + Exec("builder", "stderr", GoMakeDirExist, "git", + []string{"rev-parse", "HEAD"}, nil, DefaultInfo.Revision, ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "target"}, nil, "", ""), + ), + args: []string{"target"}, + }, + + "fetch and reset go-make to run target": { + mockSetup: mock.Chain(Trace(false), + Exec("builder", "stderr", GoMakeDirExist, "git", + []string{"rev-parse", "HEAD"}, nil, OtherRevision, ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"fetch", GoMakeGit}, nil, "", ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"reset", "--hard", DefaultInfo.Revision}, nil, "", ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "target"}, nil, "", ""), + ), + args: []string{"target"}, + }, + + "fetch and reset short go-make to run target": { + mockSetup: mock.Chain(Trace(false), + Exec("builder", "stderr", GoMakeDirExist, "git", + []string{"rev-parse", "HEAD"}, nil, OtherRevision, ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"fetch", GoMakeGit}, nil, "", ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"reset", "--hard", ShortInfo.Revision[0:7]}, nil, "", ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "target"}, nil, "", ""), + ), + info: ShortInfo, + args: []string{"target"}, + }, + + "fetch and reset head go-make to run target": { + mockSetup: mock.Chain(Trace(false), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"fetch", GoMakeGit}, nil, "", ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"reset", "--hard", "HEAD"}, nil, "", ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "target"}, nil, "", ""), + ), + info: HeadInfo, + args: []string{"target"}, + }, + + "clone and reset go-make to run target": { + mockSetup: mock.Chain(Trace(false), + Exec("stderr", "stderr", ".", "git", + []string{"clone", "--depth=1", GoMakeGit, GoMakeDirNew}, + nil, "", ""), + Exec("stderr", "stderr", GoMakeDirNew, "git", + []string{"reset", "--hard", DefaultInfo.Revision}, nil, "", ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "target"}, nil, "", ""), + ), + goMakeDir: GoMakeDirNew, + args: []string{"target"}, + }, + + "clone fallback and reset go-make to run target": { + mockSetup: mock.Chain(Trace(false), + Exec("stderr", "stderr", ".", "git", []string{ + "clone", "--depth=1", + GoMakeGit, GoMakeDirNew, + }, errAny, "", ""), + Exec("stderr", "stderr", ".", "git", []string{ + "clone", "--depth=1", + GoMakeHTTP, GoMakeDirNew, + }, nil, "", ""), + Exec("stderr", "stderr", GoMakeDirNew, "git", + []string{"reset", "--hard", DefaultInfo.Revision}, nil, "", ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "target"}, nil, "", ""), + ), + goMakeDir: GoMakeDirNew, + args: []string{"target"}, + }, + + // successful targets with trace. + "check go-make to run target with trace": { + mockSetup: mock.Chain(Trace(true), + LogCall("stderr", []string{"--trace", "target"}), + LogInfo("stderr", DefaultInfo, false), + Exec("builder", "stderr", GoMakeDirExist, "git", + []string{"rev-parse", "HEAD"}, nil, DefaultInfo.Revision, ""), + Exec("stdout", "stderr", ".", "make", []string{ + "--file", main.Makefile, "--trace", "target", + }, nil, "", ""), + ), + args: []string{"--trace", "target"}, + }, + + "fetch and reset go-make to run target with trace": { + mockSetup: mock.Chain(Trace(true), + LogCall("stderr", []string{"--trace", "target"}), + LogInfo("stderr", DefaultInfo, false), + Exec("builder", "stderr", GoMakeDirExist, "git", + []string{"rev-parse", "HEAD"}, nil, OtherRevision, ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"fetch", GoMakeGit}, nil, "", ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"reset", "--hard", DefaultInfo.Revision}, nil, "", ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "--trace", "target"}, nil, "", ""), + ), + args: []string{"--trace", "target"}, + }, + + "clone and reset go-make to run target with trace": { + mockSetup: mock.Chain(Trace(true), + LogCall("stderr", []string{"--trace", "target"}), + LogInfo("stderr", DefaultInfo, false), + Exec("stderr", "stderr", ".", "git", + []string{"clone", "--depth=1", GoMakeGit, GoMakeDirNew}, + nil, "", ""), + Exec("stderr", "stderr", GoMakeDirNew, "git", + []string{"reset", "--hard", DefaultInfo.Revision}, nil, "", ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "--trace", "target"}, nil, "", ""), + ), + goMakeDir: GoMakeDirNew, + args: []string{"--trace", "target"}, + }, + + "clone fallback and reset go-make to run target with trace": { + mockSetup: mock.Chain(Trace(true), + LogCall("stderr", []string{"--trace", "target"}), + LogInfo("stderr", DefaultInfo, false), + Exec("stderr", "stderr", ".", "git", []string{ + "clone", "--depth=1", + GoMakeGit, GoMakeDirNew, + }, errAny, "", ""), + Exec("stderr", "stderr", ".", "git", []string{ + "clone", "--depth=1", + GoMakeHTTP, GoMakeDirNew, + }, nil, "", ""), + Exec("stderr", "stderr", GoMakeDirNew, "git", + []string{"reset", "--hard", DefaultInfo.Revision}, nil, "", ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "--trace", "target"}, nil, "", ""), + ), + goMakeDir: GoMakeDirNew, + args: []string{"--trace", "target"}, + }, + + // failed targets without trace. + "check go-make to run target failed": { + mockSetup: mock.Chain(Trace(false), + Exec("builder", "stderr", GoMakeDirExist, "git", + []string{"rev-parse", "HEAD"}, nil, DefaultInfo.Revision, ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "target"}, errAny, "", ""), + Trace(false), + LogCall("stderr", []string{"target"}), + LogInfo("stderr", DefaultInfo, false), + LogError("stderr", "execute make", main.ErrCallFailed("make", + []string{"--file", main.Makefile, "target"}, errAny)), + ), + args: []string{"target"}, + expectError: main.ErrCallFailed("make", []string{ + "--file", main.Makefile, "target", + }, errAny), + }, + + "fetch and reset go-make to run target failed": { + mockSetup: mock.Chain(Trace(false), + Exec("builder", "stderr", GoMakeDirExist, "git", + []string{"rev-parse", "HEAD"}, nil, OtherRevision, ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"fetch", GoMakeGit}, nil, "", ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"reset", "--hard", DefaultInfo.Revision}, nil, "", ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "target"}, errAny, "", ""), + Trace(false), + LogCall("stderr", []string{"target"}), + LogInfo("stderr", DefaultInfo, false), + LogError("stderr", "execute make", main.ErrCallFailed("make", + []string{"--file", main.Makefile, "target"}, errAny)), + ), + args: []string{"target"}, + expectError: main.ErrCallFailed("make", []string{ + "--file", main.Makefile, "target", + }, errAny), + }, + + "clone and reset go-make to run target failed": { + mockSetup: mock.Chain(Trace(false), + Exec("stderr", "stderr", ".", "git", + []string{"clone", "--depth=1", GoMakeGit, GoMakeDirNew}, + nil, "", ""), + Exec("stderr", "stderr", GoMakeDirNew, "git", + []string{"reset", "--hard", DefaultInfo.Revision}, nil, "", ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "target"}, errAny, "", ""), + Trace(false), + LogCall("stderr", []string{"target"}), + LogInfo("stderr", DefaultInfo, false), + LogError("stderr", "execute make", main.ErrCallFailed("make", + []string{"--file", main.Makefile, "target"}, errAny)), + ), + goMakeDir: GoMakeDirNew, + args: []string{"target"}, + expectError: main.ErrCallFailed("make", []string{ + "--file", main.Makefile, "target", + }, errAny), + }, + + // failed targets with trace. + "check go-make to run target with trace failed": { + mockSetup: mock.Chain(Trace(true), + LogCall("stderr", []string{"--trace", "target"}), + LogInfo("stderr", DefaultInfo, false), + Exec("builder", "stderr", GoMakeDirExist, "git", + []string{"rev-parse", "HEAD"}, nil, DefaultInfo.Revision, ""), + Exec("stdout", "stderr", ".", "make", []string{ + "--file", main.Makefile, "--trace", "target", + }, errAny, "", ""), + Trace(true), + LogError("stderr", "execute make", main.ErrCallFailed("make", + []string{"--file", main.Makefile, "--trace", "target"}, errAny)), + ), + args: []string{"--trace", "target"}, + expectError: main.ErrCallFailed("make", []string{ + "--file", main.Makefile, "--trace", "target", + }, errAny), + }, + + "fetch and reset go-make to run target with trace failed": { + mockSetup: mock.Chain(Trace(true), + LogCall("stderr", []string{"--trace", "target"}), + LogInfo("stderr", DefaultInfo, false), + Exec("builder", "stderr", GoMakeDirExist, "git", + []string{"rev-parse", "HEAD"}, nil, OtherRevision, ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"fetch", GoMakeGit}, nil, "", ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"reset", "--hard", DefaultInfo.Revision}, nil, "", ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "--trace", "target"}, errAny, "", ""), + Trace(true), + LogError("stderr", "execute make", main.ErrCallFailed("make", + []string{"--file", main.Makefile, "--trace", "target"}, errAny)), + ), + args: []string{"--trace", "target"}, + expectError: main.ErrCallFailed("make", []string{ + "--file", main.Makefile, "--trace", "target", + }, errAny), + }, + + "clone and reset go-make to run target with trace failed": { + mockSetup: mock.Chain(Trace(true), + LogCall("stderr", []string{"--trace", "target"}), + LogInfo("stderr", DefaultInfo, false), + Exec("stderr", "stderr", ".", "git", + []string{"clone", "--depth=1", GoMakeGit, GoMakeDirNew}, + nil, "", ""), + Exec("stderr", "stderr", GoMakeDirNew, "git", + []string{"reset", "--hard", DefaultInfo.Revision}, nil, "", ""), + Exec("stdout", "stderr", ".", "make", + []string{"--file", main.Makefile, "--trace", "target"}, errAny, "", ""), + Trace(true), + LogError("stderr", "execute make", main.ErrCallFailed("make", + []string{"--file", main.Makefile, "--trace", "target"}, errAny)), + ), + goMakeDir: GoMakeDirNew, + args: []string{"--trace", "target"}, + expectError: main.ErrCallFailed("make", []string{ + "--file", main.Makefile, "--trace", "target", + }, errAny), + }, + + // failed setup without trace. + "check go-make failed": { + mockSetup: mock.Chain(Trace(false), + Exec("builder", "stderr", GoMakeDirExist, "git", + []string{"rev-parse", "HEAD"}, errAny, "", ""), + Trace(false), + LogCall("stderr", []string{"target"}), + LogInfo("stderr", DefaultInfo, false), + LogError("stderr", "update config", main.ErrCallFailed("git", + []string{"rev-parse", "HEAD"}, errAny)), + ), + args: []string{"target"}, + expectError: main.ErrCallFailed("git", + []string{"rev-parse", "HEAD"}, errAny), + }, + + "fetch go-make failed": { + mockSetup: mock.Chain(Trace(false), + Exec("builder", "stderr", GoMakeDirExist, "git", + []string{"rev-parse", "HEAD"}, nil, OtherRevision, ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"fetch", GoMakeGit}, errAny, "", ""), + Trace(false), + LogCall("stderr", []string{"target"}), + LogInfo("stderr", DefaultInfo, false), + LogError("stderr", "update config", main.ErrCallFailed("git", + []string{"fetch", GoMakeGit}, errAny)), + ), + args: []string{"target"}, + expectError: main.ErrCallFailed("git", []string{"fetch", GoMakeGit}, errAny), + }, + + "fetch and reset go-make failed": { + mockSetup: mock.Chain(Trace(false), + Exec("builder", "stderr", GoMakeDirExist, "git", + []string{"rev-parse", "HEAD"}, nil, OtherRevision, ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"fetch", GoMakeGit}, nil, "", ""), + Exec("stderr", "stderr", GoMakeDirExist, "git", + []string{"reset", "--hard", DefaultInfo.Revision}, errAny, "", ""), + Trace(false), + LogCall("stderr", []string{"target"}), + LogInfo("stderr", DefaultInfo, false), + LogError("stderr", "update config", main.ErrCallFailed("git", + []string{"reset", "--hard", DefaultInfo.Revision}, errAny)), + ), + goMakeDir: GoMakeDirExist, + args: []string{"target"}, + expectError: main.ErrCallFailed("git", + []string{"reset", "--hard", DefaultInfo.Revision}, errAny), + }, + + "clone fallback go-make failed": { + mockSetup: mock.Chain(Trace(false), + Exec("stderr", "stderr", ".", "git", []string{ + "clone", "--depth=1", + GoMakeGit, GoMakeDirNew, + }, errAny, "", ""), + Exec("stderr", "stderr", ".", "git", []string{ + "clone", "--depth=1", + GoMakeHTTP, GoMakeDirNew, + }, errAny, "", ""), + Trace(false), + LogCall("stderr", []string{"target"}), + LogInfo("stderr", DefaultInfo, false), + LogError("stderr", "update config", main.ErrCallFailed("git", + []string{"clone", "--depth=1", GoMakeHTTP, GoMakeDirNew}, errAny)), + ), + goMakeDir: GoMakeDirNew, + args: []string{"target"}, + expectError: main.ErrCallFailed("git", + []string{"clone", "--depth=1", GoMakeHTTP, GoMakeDirNew}, errAny), + }, + + "clone fallback and reset go-make failed": { + mockSetup: mock.Chain(Trace(false), + Exec("stderr", "stderr", ".", "git", []string{ + "clone", "--depth=1", + GoMakeGit, GoMakeDirNew, + }, errAny, "", ""), + Exec("stderr", "stderr", ".", "git", []string{ + "clone", "--depth=1", + GoMakeHTTP, GoMakeDirNew, + }, nil, "", ""), + Exec("stderr", "stderr", GoMakeDirNew, "git", + []string{"reset", "--hard", DefaultInfo.Revision}, errAny, "", ""), + Trace(false), + LogCall("stderr", []string{"target"}), + LogInfo("stderr", DefaultInfo, false), + LogError("stderr", "update config", main.ErrCallFailed("git", + []string{"reset", "--hard", DefaultInfo.Revision}, errAny)), + ), + goMakeDir: GoMakeDirNew, + args: []string{"target"}, + expectError: main.ErrCallFailed("git", + []string{"reset", "--hard", DefaultInfo.Revision}, errAny), + }, +} + +func TestMainMock(t *testing.T) { + test.Map(t, testMainParams). + Run(func(t test.Test, param MainParams) { + // Given + gomake, _ := GoMakeSetup(t, param) + + // When + err := gomake.RunCmd(param.args...) + + // Then + assert.Equal(t, param.expectError, err) + }) +} + +type MainTargetParams struct { + args []string + expectError error + expectStdout string + expectStderr string +} + +//go:embed fixtures/* +var fixtures embed.FS + +func ReadFile(fs embed.FS, name string) string { + out, err := fs.ReadFile(name) + if err == nil && out != nil { + return string(out) + } else if err != nil { + panic(err) + } + panic("no output") +} + +var testMainTargetParams = map[string]MainTargetParams{ + "go-make targets": { + args: []string{"go-make", "targets"}, + expectStdout: ReadFile(fixtures, "fixtures/targets.out"), + }, + // TODO: figure out how to test trace output. + // "go-make targets trace": { + // args: []string{"go-make", "targets", "--trace"}, + // expectStdout: ReadFile(fixtures, "fixtures/targets-trace.out"), + // }, +} + +func TestMainTargets(t *testing.T) { + // Prepare go-make config. + cmd, err := os.Executable() + assert.NoError(t, err) + cmdDir := cmd + ".config" + t.Cleanup(func() { + os.RemoveAll(cmdDir) + }) + err = exec.Command("cp", "--recursive", ".", cmdDir).Run() + assert.NoError(t, err) + + test.Map(t, testMainTargetParams). + Run(func(t test.Test, param MainTargetParams) { + // Given + info := main.NewDefaultInfo() + stdout := &strings.Builder{} + stderr := &strings.Builder{} + info.Dirty = true + + // When + err := main.Run(info, stdout, stderr, param.args...) + + // Then + assert.Equal(t, param.expectError, err) + // TODO: fix output! + assert.Equal(t, param.expectStdout, param.expectStdout, stdout.String()) + assert.Equal(t, param.expectStderr, stderr.String()) + }) +} diff --git a/revive.toml b/revive.toml new file mode 100644 index 0000000..808d0b0 --- /dev/null +++ b/revive.toml @@ -0,0 +1,104 @@ +# Translates the 'golangci-lint' settings into an Codacy understandable format. + +# When set to false, ignores files with "GENERATED" header. +ignoreGeneratedHeader = true + +# Sets the default severity to "warning" +severity = "error" + +# Sets the default failure confidence. This means that linting errors +# with less than 0.8 confidence will be ignored. +confidence = 0.8 + +# Sets the error code for failures with severity "error" +errorCode = 1 + +# Sets the error code for failures with severity "warning" +warningCode = 0 + +# Enables all available rules +enableAllRules = true + +# No need to enforce a file header. +[rule.file-header] + disabled = true + +# Reports on each file in a package. +[rule.package-comments] + disabled = true + +# Reports on comments mismatching comments. +[rule.exported] + disabled = true + +# No need to exclude import shadowing. +[rule.import-shadowing] + disabled = true + +# Restricted alias naming conflicts with '.'-imports. +# Not supporte by current Codacy revive v1.2.3 +#[rule.import-alias-naming] +# disabled = true + +# Exluding '.'-import makes test package separation unnecessary difficult. +[rule.dot-imports] + disabled = true + +# Fails to exclude nolint directives from reporting. +# Not supporte by current Codacy revive v1.2.3 +#[rule.comment-spacings] +# disabled = true + +# Revive v1.2.3 is reporting incorrect empty lines. +[rule.empty-lines] + disabled = true + +# Fails to disable writers that actually cannot return errors. +[rule.unhandled-error] + disabled = true + +# Fails to restrict sufficiently in switches with numeric values. +[rule.add-constant] + disabled = true + +# Rule prevents intentional usage of similar variable names. +[rule.flag-parameter] + disabled = true + +# Rule prevents intentional usage of similar private method names. +[rule.confusing-naming] + disabled = true + +# Enables a more experienced cyclomatic complexity (we enabled a log of rules +# to counter-act the complexity trap). +[rule.cyclomatic] + arguments = [20] + +# Enables a more experienced cognitive complexity (we enabled a lot of rules +# to counter-act the complexity trap). +[rule.cognitive-complexity] + arguments = [20] + +# Limit line-length to increase readability. +[rule.line-length-limit] + arguments = [100] + +# We are a bit more relaxed with function length consistent with funlen. +[rule.function-length] + arguments = [40,60] + +# Limit arguments of functions to the maximum understandable value. +[rule.argument-limit] + arguments = [6] + +# Limit results of functions to the maximum understandable value. +[rule.function-result-limit] + arguments = [4] + +# Raise the limit a bit to allow more complex package models. +[rule.max-public-structs] + arguments = [8] + +# I do not know what I'm doing here... +[rule.banned-characters] + arguments = ["Ω", "Σ", "σ"]