MAKEFLAGS+=-j --no-print-directory
VERSION_FILE=./VERSION
VERSION:=$$(hack/derive-version.sh)
# a list of "dist/ec_{platform}_{arch}" that we support
ALL_SUPPORTED_OS_ARCH:=$(shell go tool dist list -json|jq -r '.[] | select((.FirstClass == true or .GOARCH == "ppc64le" or .GOARCH == "s390x") and .GOARCH != "386") | "dist/ec_\(.GOOS)_\(.GOARCH)"')
# a list of image_* targets that we do not support
UNSUPPORTED_OS_ARCH_IMG:=image_windows_amd64 image_darwin_amd64 image_darwin_arm64 image_linux_arm
# a list of image_* targets that we do support generated from
# ALL_SUPPORTED_OS_ARCH by replacing "dist/ec_" with "image_"
ALL_SUPPORTED_IMG_OS_ARCH:=$(filter-out $(UNSUPPORTED_OS_ARCH_IMG),$(subst dist/ec_,image_,$(ALL_SUPPORTED_OS_ARCH)))
_SHELL := bash
SHELL=$(if $@,$(info ❱ [1m$@[0m))$(_SHELL)
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
COPY:=The Conforma Contributors
COSIGN_VERSION=$(shell go list -f '{{.Version}}' -m github.com/sigstore/cosign/v3)
E2E_INSTRUMENTATION_FLAGS := $(if $(filter $(E2E_INSTRUMENTATION),true),-cover -covermode atomic)

##@ Information

.PHONY: help
help: ## Display this help
	@awk 'function ww(s) {\
		if (length(s) < 59) {\
			return s;\
		}\
		else {\
			r="";\
			l="";\
			split(s, arr, " ");\
			for (w in arr) {\
				if (length(l " " arr[w]) > 59) {\
					r=r l "\n                     ";\
					l="";\
				}\
				l=l " " arr[w];\
			}\
			r=r l;\
			return r;\
		}\
	} BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m<target>\033[0m\n"} /^[^: (]+:.*?##/ { printf "  \033[36m%-18s\033[0m %s\n", "make " $$1, ww($$2) } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

##@ Building

.PHONY: generate
generate: ## Code-generate files
	go generate ./...

# Set DEBUG_BUILD=1 to build a binary with gdb/dlv debugging support
BUILD_GC_FLAGS=$(if $(DEBUG_BUILD),-gcflags="-N -l",)
BUILD_TRIMPATH=$(if $(DEBUG_BUILD),,-trimpath)
BUILD_LD_FLAGS=$(if $(DEBUG_BUILD),,-s -w)
BUILD_BIN_SUFFIX=$(if $(DEBUG_BUILD),_debug,)

.PHONY: $(ALL_SUPPORTED_OS_ARCH)
$(ALL_SUPPORTED_OS_ARCH): generate ## Build binaries for specific platform/architecture, e.g. make dist/ec_linux_amd64
	@GOOS=$(word 2,$(subst _, ,$(notdir $@))); \
	GOARCH=$(word 3,$(subst _, ,$(notdir $@))); \
	GOOS=$${GOOS} GOARCH=$${GOARCH} CGO_ENABLED=0 go build $(E2E_INSTRUMENTATION_FLAGS) $(BUILD_TRIMPATH) $(BUILD_GC_FLAGS) -ldflags="$(BUILD_LD_FLAGS) -X github.com/conforma/cli/internal/version.Version=$(VERSION)" -o dist/ec_$${GOOS}_$${GOARCH}$(BUILD_BIN_SUFFIX); \
	sha256sum -b dist/ec_$${GOOS}_$${GOARCH}$(BUILD_BIN_SUFFIX) > dist/ec_$${GOOS}_$${GOARCH}$(BUILD_BIN_SUFFIX).sha256

.PHONY: dist
dist: $(ALL_SUPPORTED_OS_ARCH) ## Build binaries for all supported operating systems and architectures

# Dockerfile.dist is used by the Konflux build pipeline where it's built using
# buildah not podman. This is for testing that build locally.
.PHONY: dist-container
dist-container: clean
	buildah bud --file Dockerfile.dist \
	  --tag dist-container \
	  --platform $(BUILD_LOCAL_PLATFORM) \
	  --build-arg BUILD_SUFFIX=local \
	  --build-arg BUILD_LIST=$(BUILD_LOCAL_ARCH) \
	  --volume "$(GOCACHE_DIR)":/go/cache:Z \
	  --volume "$(GOMODCACHE_DIR)":/go/mod:Z \
	  --env GOCACHE=/go/cache \
	  --env GOMODCACHE=/go/mod \
	  .

# For local debugging of the above
dist-container-run:
	podman run --rm -it --entrypoint=/bin/bash dist-container

BUILD_LOCAL_PLATFORM:=$(shell go env GOOS)/$(shell go env GOARCH)
BUILD_LOCAL_ARCH:=$(shell go env GOOS)_$(shell go env GOARCH)
.PHONY: build
build: dist/ec_$(BUILD_LOCAL_ARCH) ## Build the ec binary for the current platform
	@ln -sf ec_$(BUILD_LOCAL_ARCH)$(BUILD_BIN_SUFFIX) dist/ec$(BUILD_BIN_SUFFIX)

BUILD_IMG_ARCH:=$(shell podman version -f {{.Server.OsArch}} | awk -F/ '{print $$1}')_$(shell podman version -f {{.Server.OsArch}} | awk -F/ '{print $$2}')
.PHONY: build-for-test
build-for-test: dist/ec_$(BUILD_IMG_ARCH)

# Assume `DEBUG_BUILD=1 make build` was run already
debug-run:
	dlv exec dist/ec_$(BUILD_LOCAL_ARCH)_debug # ...params here as required

.PHONY: clean
clean: ## Delete build output
	@rm -f dist/*

##@ Testing

# Declutter the output by grepping out the files where there are no
# tests at all, or no tests matching the specified tag
TEST_OUTPUT_FILTER=grep -vE '0.0% of statements|\[no test files\]'

.PHONY: test
test: ## Run all unit tests
	@echo "Unit tests:"
	@set -o pipefail && go test -race -covermode=atomic -coverprofile=coverage-unit.out -timeout 10s -tags=unit ./... | $(TEST_OUTPUT_FILTER)
	@echo "Integration tests:"
	@set -o pipefail && go test -race -covermode=atomic -coverprofile=coverage-integration.out -timeout 15s -tags=integration ./... | $(TEST_OUTPUT_FILTER)
# Given the nature of generative tests the test timeout is increased from 500ms
# to 30s to accommodate many samples being generated and test cases being run.
	@echo "Generative tests:"
	@set -o pipefail && go test -race -covermode=atomic -coverprofile=coverage-generative.out -timeout 30s -tags=generative ./... | $(TEST_OUTPUT_FILTER)

ACCEPTANCE_TIMEOUT:=20m
.ONESHELL:
.SHELLFLAGS=-e -c
.PHONY: acceptance

acceptance: ## Run all acceptance tests
	@SECONDS=0; \
	echo "[`date '+%H:%M:%S'`] Starting acceptance tests"; \
	ACCEPTANCE_WORKDIR="$$(mktemp -d)"; \
	cleanup() { \
		cp "$${ACCEPTANCE_WORKDIR}"/features/__snapshots__/* "$(ROOT_DIR)"/features/__snapshots__/ || true; \
		if [ -n "$${UPDATE_SNAPS}" ]; then \
			for f in "$(ROOT_DIR)"/features/__snapshots__/*.snap; do \
				[ -f "$$f" ] || continue; \
				if [ ! -f "$${ACCEPTANCE_WORKDIR}/features/__snapshots__/$$(basename $$f)" ]; then \
					rm -f "$$f"; \
				fi; \
			done; \
		fi; \
		rm -rf "$${ACCEPTANCE_WORKDIR}"; \
	}; \
	mkdir -p "$${ACCEPTANCE_WORKDIR}/coverage"; \
	trap cleanup EXIT; \
	cp -R . "$$ACCEPTANCE_WORKDIR"; \
	cd "$$ACCEPTANCE_WORKDIR"; \
	if ! $(MAKE) build E2E_INSTRUMENTATION=true; then \
		echo "[`date '+%H:%M:%S'`] Build failed"; \
		exit 1; \
	fi; \
	echo "[`date '+%H:%M:%S'`] Build done, running tests"; \
	export GOCOVERDIR="$${ACCEPTANCE_WORKDIR}/coverage"; \
	cd acceptance && go test -timeout $(ACCEPTANCE_TIMEOUT) ./... && test_passed=1 || test_passed=0; \
	echo "[`date '+%H:%M:%S'`] Tests finished in $$((SECONDS/60))m$$((SECONDS%60))s"; \
	go tool covdata textfmt -i=$${GOCOVERDIR} -o="$(ROOT_DIR)/coverage-acceptance.out" || true; \
	[ "$$test_passed" = "1" ]

# Add @focus above the feature you're hacking on to use this
# (Mainly for use with the feature-% target below)
.PHONY: focus-acceptance
focus-acceptance: build ## Run acceptance tests with @focus tag
	@cd acceptance && go test . -args -tags=@focus

# Uses sed hackery to insert a @focus tag and then remove it afterwards.
# (There might be a nicer way to run all scenarios in a single feature.)
# The `|| true` here is so the @focus tag still gets removed after a failure.
feature_%: ## Run acceptance tests for a single feature file, e.g. make feature_validate_image
	@echo "Testing feature '$*'"
	@#
	@sed -i '1i@focus' features/$*.feature
	@$(MAKE) focus-acceptance || true
	@#
	@# Remove @focus tag
	@sed -i '1d' features/$*.feature
	@#
	@# With UPDATE_SNAPS=true all the other snap files will be deleted. Let's put them back.
	@if [ -n "$$UPDATE_SNAPS" ]; then \
	  git ls-files --deleted -- 'features/__snapshots__/*.snap' | xargs -r git checkout --; \
	fi

# (Replace spaces with underscores in the scenario name.)
scenario_%: build ## Run acceptance tests for a single scenario, e.g. make scenario_inline_policy
	@cd acceptance && go test -test.run 'TestFeatures/$*'
	@# With UPDATE_SNAPS=true all the other snap files will be deleted. Let's put them back.
	@if [ -n "$$UPDATE_SNAPS" ]; then \
	  git ls-files --deleted -- '../features/__snapshots__/*.snap' | xargs -r git checkout --; \
	fi

benchmark/%/data.tar.gz:
	@cd benchmark/$*
	@./prepare_data.sh

.PHONY: benchmark_%
benchmark_%: benchmark/%/data.tar.gz
	@cd benchmark/$*
	@go run .

.PHONY: benchmark_data
benchmark_data: benchmark/simple/data.tar.gz ## Prepare data for benchmark

.PHONY: benchmark
benchmark: benchmark_simple ## Run benchmarks

.PHONY: tools-ci
tools-ci: ## Ensure all tools build cleanly
	@echo "• tkn:" && \
	go run -modfile tools/go.mod github.com/tektoncd/cli/cmd/tkn version && \
	echo "• kustomize:" && \
	go run -modfile tools/go.mod sigs.k8s.io/kustomize/kustomize/v5 version && \
	echo "• helm:" && \
	go run -modfile tools/go.mod helm.sh/helm/v3/cmd/helm version && \
	echo "• conftest:" && \
	go run -modfile tools/go.mod github.com/open-policy-agent/conftest --version

.PHONY: ci
ci: test lint-fix acceptance tools-ci ## Run the usual required CI tasks

##@ Linters

LICENSE_IGNORE=\
-ignore 'dist/cli-reference/*.yaml' \
-ignore 'acceptance/examples/**/*.yaml' \
-ignore 'configs/*/*.yaml' \
-ignore 'node_modules/**' \
-ignore 'hack/**/charts/**' \
-ignore '.tekton/*.yaml' \
-ignore '.ec/**'

LINT_TO_GITHUB_ANNOTATIONS='map(map(.)[])[][] as $$d | $$d.posn | split(":") as $$posn | "::warning file=\($$posn[0]),line=\($$posn[1]),col=\($$posn[2])::\($$d.message)"'

.PHONY: lint
lint: tekton-lint go-mod-lint ## Run linter
# addlicense doesn't give us a nice explanation so we prefix it with one
	@git ls-files -z | xargs -0 go run -modfile tools/go.mod github.com/google/addlicense -c '$(COPY)' -y '' -s -check $(LICENSE_IGNORE) | sed 's/^/Missing license header in: /g'
# piping to sed above looses the exit code, luckily addlicense is fast so we invoke it for the second time to exit 1 in case of issues
	@git ls-files -z | xargs -0 go run -modfile tools/go.mod github.com/google/addlicense -c '$(COPY)' -y '' -s -check $(LICENSE_IGNORE) >/dev/null 2>&1
	@go run -modfile tools/go.mod github.com/golangci/golangci-lint/v2/cmd/golangci-lint run $(if $(GITHUB_ACTIONS), --timeout=10m0s)
	@(cd acceptance && go run -modfile ../tools/go.mod github.com/golangci/golangci-lint/v2/cmd/golangci-lint run --path-prefix acceptance $(if $(GITHUB_ACTIONS), --timeout=10m0s))

.PHONY: lint-fix
lint-fix: ## Fix linting issues automagically
	@git ls-files -z | xargs -0 go run -modfile tools/go.mod github.com/google/addlicense -c '$(COPY)' -y '' -s $(LICENSE_IGNORE)
	@go run -modfile tools/go.mod github.com/golangci/golangci-lint/v2/cmd/golangci-lint run --fix
	@(cd acceptance && go run -modfile ../tools/go.mod github.com/golangci/golangci-lint/v2/cmd/golangci-lint run --path-prefix acceptance --fix)
# We don't apply the fixes from the internal (error handling) linter.
# TODO: fix the outstanding error handling lint issues and enable the fixer
#	@go run -modfile tools/go.mod ./internal/lint -fix $$(go list ./... | grep -v '/acceptance/')

node_modules: package-lock.json
	@npm ci

TEKTON_LINT_TO_GITHUB_ANNOTATIONS='.[] | "::error file=\(.path),line=\(.loc.startLine),endLine=\(.loc.endLine),col=\(.loc.startColumn),endColumn=\(.loc.endColumn)::\(.message)"'
# wildcard matches `tasks/<task_name>/<version>/*.yaml`
tekton-lint: node_modules $(wildcard tasks/*/*/*.yaml) ## Run tekton-lint for 'tasks' subdirectory.
# We execute tekton-lint for all yaml files contained within the tasks subdirectory, it's smart enough to ignore non-Tekton yaml files.
# All warnings are currently considered errors.
# When running on GitHub Actions, reformat to annotations
	@npm exec tekton-lint -- --max-warnings=0 --format=$(if $(GITHUB_ACTIONS),json,stylish) $(filter-out node_modules,$^)$(if $(GITHUB_ACTIONS), | jq -r $(TEKTON_LINT_TO_GITHUB_ANNOTATIONS))

.PHONY: go-mod-lint
go-mod-lint:
	@echo "Scanning for go.mod files and performing tidy..."
	@find . -name "go.mod" -execdir go mod tidy >/dev/null 2>&1 \;
	@echo "Checking for modified go.mod or go.sum files..."
	@if git status --porcelain | grep -q -e "go.mod" -e "go.sum"; then \
		echo "Ensure the following go.mod or go.sum files are added to the git commit:"; \
		git status --porcelain | grep -e "go.mod" -e "go.sum"; \
	else \
		echo "No go.mod or go.sum files need to be added to the git commit."; \
	fi

##@ Pushing images

IMAGE_TAG ?= latest
IMAGE_REPO ?= quay.io/conforma/cli
.PHONY: build-image
build-image: image_$(BUILD_IMG_ARCH) ## Build container image with ec

.PHONY: push-image
push-image: push_image_$(BUILD_IMG_ARCH) ## Push ec container image to default location

.PHONY: build-snapshot-image
build-snapshot-image: push-image ## Build the ec image and tag it with "snapshot"
	@podman tag $(IMAGE_REPO):$(IMAGE_TAG) $(IMAGE_REPO):snapshot

.PHONY: push-snapshot-image
push-snapshot-image: build-snapshot-image ## Push the ec image with the "snapshot" tag
	@podman push $(PODMAN_OPTS) $(IMAGE_REPO):snapshot

.PHONY: $(ALL_SUPPORTED_IMG_OS_ARCH)
# Targets are in the form of "image_{platform}_{arch}", we set
# TARGETOS={platform}, and TARGETARCH={arch}.
# Pre-evaluate Go cache directories
GOCACHE_DIR:=$(shell go env GOCACHE)
GOMODCACHE_DIR:=$(shell go env GOMODCACHE)

$(ALL_SUPPORTED_IMG_OS_ARCH): TARGETOS=$(word 2,$(subst _, ,$@))
$(ALL_SUPPORTED_IMG_OS_ARCH): TARGETARCH=$(word 3,$(subst _, ,$@))
$(ALL_SUPPORTED_IMG_OS_ARCH):
	@podman build -t $(IMAGE_REPO):$(IMAGE_TAG)-$(TARGETOS)-$(TARGETARCH) -f Dockerfile --platform $(TARGETOS)/$(TARGETARCH) --volume "$(GOCACHE_DIR)":/go/cache:Z --volume "$(GOMODCACHE_DIR)":/go/mod:Z --env GOCACHE=/go/cache --env GOMODCACHE=/go/mod

# Currently it shows the following:
#  image_linux_amd64
#  image_linux_arm64
#  image_linux_ppc64le
#  image_linux_s390x
show-supported-builds:
	@for b in $(ALL_SUPPORTED_IMG_OS_ARCH); do echo $$b; done

.PHONY: $(subst image_,push_image_,$(ALL_SUPPORTED_IMG_OS_ARCH))
# Ref: https://www.gnu.org/software/make/manual/make.html#Secondary-Expansion
.SECONDEXPANSION:
# Targets are in the form of "push_image_{platform}_{arch}", we set
# TARGETOS={platform}, and TARGETARCH={arch}. This target depends on the
# "image_{platform}_{arch}" target
$(subst image_,push_image_,$(ALL_SUPPORTED_IMG_OS_ARCH)): TARGETOS=$(word 3,$(subst _, ,$@))
$(subst image_,push_image_,$(ALL_SUPPORTED_IMG_OS_ARCH)): TARGETARCH=$(word 4,$(subst _, ,$@))
$(subst image_,push_image_,$(ALL_SUPPORTED_IMG_OS_ARCH)): image_$$(TARGETOS)_$$(TARGETARCH)
	@podman push $(PODMAN_OPTS) $(IMAGE_REPO):$(IMAGE_TAG)-$(TARGETOS)-$(TARGETARCH)

.PHONY: dist-image
# Depends on targets in the form of "image_{platform}_{arch}"
dist-image: $(ALL_SUPPORTED_IMG_OS_ARCH) ## Build images for all supported platforms/architectures

.PHONY: dist-image-push
ALL_IMAGE_REFS:=$(subst image-,$(IMAGE_REPO):$(IMAGE_TAG)-,$(subst _,-,$(ALL_SUPPORTED_IMG_OS_ARCH)))

dist-image-push: dist-image $(subst image_,push_image_,$(ALL_SUPPORTED_IMG_OS_ARCH)) ## Push images and multi-arch manifest
	@# Push each single-arch image
	@for img in $(ALL_IMAGE_REFS); do \
	  podman push $(PODMAN_OPTS) $$img; \
	done

	@# Recreate the manifest
	@podman manifest rm $(IMAGE_REPO):$(IMAGE_TAG) 2>/dev/null || true
	@podman manifest create $(IMAGE_REPO):$(IMAGE_TAG)

	@# Add each image with the correct os/arch
	@for img in $(ALL_IMAGE_REFS); do \
	  TAG=$${img##*:}; \
	  TARGETOS=$$(echo "$$TAG" | rev | cut -d- -f2 | rev); \
	  TARGETARCH=$$(echo "$$TAG" | rev | cut -d- -f1 | rev); \
	  podman manifest add $(IMAGE_REPO):$(IMAGE_TAG) $(PODMAN_OPTS) "$$img" \
	    --os "$$TARGETOS" --arch "$$TARGETARCH"; \
	done

	@# Push the manifest
	@podman manifest push $(IMAGE_REPO):$(IMAGE_TAG) $(IMAGE_REPO):$(IMAGE_TAG)

ifdef ADD_IMAGE_TAG
	@# Also push any additional tags (like “snapshot”)
	@for tag in $(ADD_IMAGE_TAG); do \
	  podman manifest push $(IMAGE_REPO):$(IMAGE_TAG) $(IMAGE_REPO):$$tag; \
	done
endif

verify-image:
	@podman run --rm $(IMAGE_REPO):$(IMAGE_TAG) version

.PHONY: dev
dev: REGISTRY_PORT=5000
dev: IMAGE_REPO=localhost:$(REGISTRY_PORT)/ec
dev: PODMAN_OPTS=--tls-verify=false
dev: TASK_REPO=localhost:$(REGISTRY_PORT)/ec-task-bundle
dev: SKOPEO_ARGS=--src-tls-verify=false --dest-tls-verify=false
dev: TASKS:=$(shell T=$$(mktemp) && yq e ".spec.steps[].image? = \"localhost:$(REGISTRY_PORT)/ec\"" \
    tasks/verify-enterprise-contract/*/verify-enterprise-contract.yaml \
    tasks/verify-conforma-konflux-ta/*/verify-conforma-konflux-ta.yaml \
    | yq 'select(. != null)' > "$${T}" && echo "$${T}")
dev: push-image task-bundle ## Push the ec and v-e-c Task Bundle to the kind cluster setup via hack/setup-dev-environment.sh
	@rm "$(TASKS)"

TASK_TAG ?= latest
TASK_REPO ?= quay.io/enterprise-contract/ec-task-bundle
TASKS ?= tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml,tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml
ifneq (,$(findstring localhost:,$(TASK_REPO)))
SKOPEO_ARGS=--src-tls-verify=false --dest-tls-verify=false
endif
TKN ?= $(shell command -v tkn 2>/dev/null || echo "go run -modfile tools/go.mod github.com/tektoncd/cli/cmd/tkn")
.PHONY: task-bundle
task-bundle: ## Push the Tekton Task bundle to an image repository
	@$(TKN) bundle push $(TASK_REPO):$(TASK_TAG) $(addprefix -f ,$(TASKS)) --annotate org.opencontainers.image.revision="$(TASK_TAG)"

.PHONY: task-bundle-snapshot
task-bundle-snapshot: task-bundle ## Push task bundle and then tag with "snapshot"
	@skopeo copy "docker://$(TASK_REPO):$(TASK_TAG)" "docker://$(TASK_REPO):snapshot" $(SKOPEO_ARGS)
	echo Tagged $(TASK_REPO):$(TASK_TAG) with snapshot tag
ifdef ADD_TASK_TAG
	@for tag in $(ADD_TASK_TAG); do
	  @skopeo copy "docker://$(TASK_REPO):$(TASK_TAG)" "docker://$(TASK_REPO):$${tag}"
	done
endif

# Useful to compare the `ec test` command source with the `conftest test`
# command source. They should be almost identical.
ifndef DIFF_TOOL
  # I like to use vimdiff for this
  DIFF_TOOL=diff --color=always
endif
.PHONY: conftest-test-cmd-diff
conftest-test-cmd-diff:
	@CONFTEST_VER=$$( go list -m -f '{{ .Version }}' github.com/open-policy-agent/conftest ) && \
	$(DIFF_TOOL) \
	  <(curl -s https://raw.githubusercontent.com/open-policy-agent/conftest/$${CONFTEST_VER}/internal/commands/test.go) \
	  cmd/test/test.go

fmt-all:
	@git ls-files '*.go' | xargs gofmt -w

# Useful while hacking on build numbers and versions
debug-version:
	@echo $(VERSION)

# It's not so hard to do this by hand, but let's save some typing
bump-minor-version:
	@yq '(. | to_string | split(".") | .[0] + "." + (.[1] | to_number + 1 | to_string))' -i $(VERSION_FILE) && \
	  git add $(VERSION_FILE) && \
	  git commit $(VERSION_FILE) \
	    -m "Bump minor version to $$(cat $(VERSION_FILE))" \
	    -m 'Commit generated with `make bump-minor-version`'
