# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2022, Unikraft GmbH and The KraftKit Authors.
# Licensed under the BSD-3-Clause License (the "License").
# You may not use this file except in compliance with the License.

# Directories
WORKDIR     ?= $(CURDIR)
TESTDIR     ?= $(WORKDIR)/tests
DISTDIR     ?= $(WORKDIR)/dist
INSTALLDIR  ?= /usr/local/bin/
VENDORDIR   ?= $(WORKDIR)/third_party

# Arguments
REGISTRY    ?= kraftkit.sh
ORG         ?= unikraft
REPO        ?= kraftkit
BIN         ?= kraft \
               runu
TOOLS       ?= github-action \
               go-generate-qemu-devices \
               protoc-gen-go-netconn \
               webinstall
GOMOD       ?= kraftkit.sh
IMAGE_TAG   ?= latest
GO_VERSION  ?= 1.22

# Add a special version tag for pull requests
ifneq ($(shell grep 'refs/pull' $(WORKDIR)/.git/FETCH_HEAD),)
HASH_COMMIT ?= HEAD
HASH        += pr-$(shell cat $(WORKDIR)/.git/FETCH_HEAD | awk -F/ '{print $$3}')
endif

# Calculate the project version based on git history
ifeq ($(HASH),)
HASH_COMMIT ?= HEAD
HASH        ?= $(shell git update-index -q --refresh && \
                       git describe --tags)
# Others can't be dirty by definition
ifneq ($(HASH_COMMIT),HEAD)
HASH_COMMIT ?= HEAD
endif
DIRTY       ?= $(shell git update-index -q --refresh && \
                       git diff-index --quiet HEAD -- $(WORKDIR) || \
                       echo "-dirty")
endif
VERSION     ?= $(HASH)$(DIRTY)
GIT_SHA     ?= $(shell git update-index -q --refresh && \
                       git rev-parse --short HEAD)


# Tools
DOCKER      ?= docker
DOCKER_RUN  ?= $(DOCKER) run --rm $(1) \
               -e DOCKER= \
               -e GOOS=$(GOOS) \
               -e GOARCH=$(GOARCH) \
               -w /go/src/$(GOMOD) \
               --platform linux/amd64 \
               -v $(WORKDIR):/go/src/$(GOMOD) \
               $(REGISTRY)/$(2):$(IMAGE_TAG) \
                 $(3)
GO          ?= go
MKDIR       ?= mkdir
GIT         ?= git
CURL        ?= curl
CMAKE       ?= cmake

# Go tools
GOFUMPT_VERSION    ?= v0.6.0
GOFUMPT            ?= $(GO) run mvdan.cc/gofumpt@$(GOFUMPT_VERSION)
GOCILINT_VERSION   ?= v1.58.1
GOCILINT           ?= $(GO) run github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOCILINT_VERSION)
YTT_VERSION        ?= v0.49.0
YTT                ?= $(GO) run carvel.dev/ytt/cmd/ytt@$(YTT_VERSION)
GORELEASER_VERSION ?= v1.25.1
GORELEASER         ?= $(GO) run github.com/goreleaser/goreleaser@$(GORELEASER_VERSION)
GINKGO_VERSION     ?= v2.19.0
GINKGO             ?= $(GO) run github.com/onsi/ginkgo/v2/ginkgo@$(GINKGO_VERSION)

# Misc
Q           ?= @

UNAME_OS    ?= $(shell uname -s)
UNAME_ARCH  ?= $(shell uname -m)
GOOS        ?= linux
GOARCH      ?= amd64

# Don't try to pass the path to Darwin host's make into the container
ifeq ($(UNAME_OS),Darwin)
	MAKE_COMMAND = make
endif

# If on Darwin, we want to build a runnable binary.
# Check the OS and set GOOS/GOARCH flags accordingly.
# Note that we are still running a linux/amd64 container.
# TODO: For better performance, build an image for darwin/arm64 and darwin/amd64
ifeq ($(UNAME_OS),Darwin)
	GOOS = darwin
ifeq ($(UNAME_ARCH),arm64)
	GOARCH = arm64
else ifeq ($(UNAME_ARCH),x86_64)
	GOARCH = amd64
endif
endif

# If run with DOCKER= or within a container, unset DOCKER_RUN so all commands
# are not proxied via docker container.
ifeq ($(DOCKER),)
DOCKER_RUN  :=
else ifneq ($(wildcard /.dockerenv),)
DOCKER_RUN  :=
endif
.PROXY      :=
ifneq ($(DOCKER_RUN),)
.PROXY      := docker-proxy-
$(BIN): ENVIRONMENT ?= myself-full
$(BIN):
	$(info Running target via $(DOCKER)...)
	$(Q)$(call DOCKER_RUN,,$(ENVIRONMENT),$(MAKE) GOOS=$(GOOS) GOARCH=$(GOARCH) -e $@)
	$(Q)exit 0
endif

# Targets
.PHONY: all
.DEFAULT: all
all: help

.PHONY: build
build: CHANNEL ?= staging
build: $(WORKDIR)/goreleaser-$(CHANNEL).yaml
build: ## Build all KraftKit binary artifacts.
	$(GORELEASER) build --config $(WORKDIR)/goreleaser-$(CHANNEL).yaml --clean --skip-validate

$(WORKDIR)/goreleaser-$(CHANNEL).yaml: CHANNEL ?= staging
$(WORKDIR)/goreleaser-$(CHANNEL).yaml:
	$(YTT) -f .goreleaser-$(CHANNEL).yaml > goreleaser-$(CHANNEL).yaml

ifeq ($(DEBUG),y)
$(addprefix $(.PROXY), $(BIN)): GO_GCFLAGS ?= -N -l
else
$(addprefix $(.PROXY), $(BIN)): GO_LDFLAGS ?= -s -w
endif
$(addprefix $(.PROXY), $(BIN)): GO_LDFLAGS += -X "$(GOMOD)/internal/version.version=$(VERSION)"
$(addprefix $(.PROXY), $(BIN)): GO_LDFLAGS += -X "$(GOMOD)/internal/version.commit=$(GIT_SHA)"
$(addprefix $(.PROXY), $(BIN)): GO_LDFLAGS += -X "$(GOMOD)/internal/version.buildTime=$(shell date)"
$(addprefix $(.PROXY), $(BIN)): tidy
$(addprefix $(.PROXY), $(BIN)):
	GOOS=$(GOOS) \
	GOARCH=$(GOARCH) \
	$(GO) build \
		-v \
		-tags "containers_image_storage_stub,containers_image_openpgp" \
		-buildmode=pie \
		-gcflags=all='$(GO_GCFLAGS)' \
		-ldflags='$(GO_LDFLAGS)' \
		-o $(DISTDIR)/$@ \
		$(WORKDIR)/cmd/$@

.PHONY: tools
tools: $(TOOLS)

ifeq ($(DEBUG),y)
$(addprefix $(.PROXY), $(TOOLS)): GO_GCFLAGS ?= -N -l
else
$(addprefix $(.PROXY), $(TOOLS)): GO_LDFLAGS ?= -s -w
endif
$(addprefix $(.PROXY), $(TOOLS)): GO_LDFLAGS += -X "$(GOMOD)/internal/version.version=$(VERSION)"
$(addprefix $(.PROXY), $(TOOLS)): GO_LDFLAGS += -X "$(GOMOD)/internal/version.commit=$(GIT_SHA)"
$(addprefix $(.PROXY), $(TOOLS)): GO_LDFLAGS += -X "$(GOMOD)/internal/version.buildTime=$(shell date)"
$(addprefix $(.PROXY), $(TOOLS)):
	(cd $(WORKDIR)/tools/$@ && \
		$(GO) build -v \
		-tags "containers_image_storage_stub,containers_image_openpgp" \
		-o $(DISTDIR)/$@ \
		-gcflags=all='$(GO_GCFLAGS)' \
		-ldflags='$(GO_LDFLAGS)' \
		./...)

# Proxy all "build environment" (buildenvs) targets
buildenv-%:
	$(MAKE) -C $(WORKDIR)/buildenvs $*

# Run an environment where we can build
.PHONY: devenv
devenv: DOCKER_RUN_EXTRA ?= -it --name $(REPO)-devenv
devenv: WITH_KVM         ?= n
devenv: ## Start the development environment container.
ifeq ($(WITH_KVM),y)
	$(Q)$(call DOCKER_RUN,--device /dev/kvm $(DOCKER_RUN_EXTRA),myself-full,bash)
else
	$(Q)$(call DOCKER_RUN,$(DOCKER_RUN_EXTRA),myself-full,bash)
endif

.PHONY: tidy
tidy: ## Tidy import Go modules.
	$(GO) mod tidy -compat=$(GO_VERSION)

.PHONY: fmt
fmt: ## Format all files according to linting preferences.
	$(GOFUMPT) -e -l -w $(WORKDIR)

.PHONY: cicheck
cicheck: ## Run CI checks.
	$(GOCILINT) run --build-tags "containers_image_storage_stub,containers_image_openpgp"

.PHONY: test
test: test-unit test-framework test-e2e test-cloud-e2e ## Run all tests.

.PHONY: test-unit
test-unit: GOTEST_EXCLUDE := third_party/ test/ hack/ buildenvs/ dist/ docs/ tools/
test-unit: GOTEST_PKGS := $(foreach pkg,$(filter-out $(GOTEST_EXCLUDE),$(wildcard */)),$(pkg)...)
test-unit: ## Run unit tests.
	$(GINKGO)  -v -p -randomize-all -tags "containers_image_storage_stub,containers_image_openpgp" $(GOTEST_PKGS)

.PHONY: test-e2e
test-e2e: kraft ## Run CLI end-to-end tests.
	$(GINKGO) -v -p -randomize-all test/e2e/cli/...

.PHONY: test-framework
test-framework: kraft ## Run framework tests.
	$(GINKGO) -v -p -randomize-all ./test/e2e/framework/...

.PHONY: test-cloud-e2e
test-cloud-e2e: ## Run cloud end-to-end tests.
	$(GINKGO) -v -randomize-all --flake-attempts 2 --nodes 8 ./test/e2e/cloud/...

.PHONY: clean
clean:
	$(GO) clean -modcache -cache -i -r

.PHONY: properclean
properclean: ENVIRONMENT ?= myself-full
properclean: IMAGE       ?= $(REGISTRY)/$(ENVIRONMENT):$(IMAGE_TAG)
properclean: ## Completely clean the repository's build artifacts.
	rm -rf $(DISTDIR) $(TESTDIR)
	$(DOCKER) rmi $(IMAGE)

.PHONY: docs
docs: OUTDIR ?= $(WORKDIR)/docs/
docs: ## Generate Markdown documentation.
	$(GO) run $(WORKDIR)/tools/gendocs $(OUTDIR)

.PHONY: help
help: ## Show this help menu and exit.
	@awk 'BEGIN { \
		FS = ":.*##"; \
		printf "KraftKit developer build targets.\n\n"; \
		printf "\033[1mUSAGE\033[0m\n"; \
		printf "  make [VAR=... [VAR=...]] \033[36mTARGET\033[0m\n\n"; \
		printf "\033[1mTARGETS\033[0m\n"; \
	} \
	/^[a-zA-Z0-9_-]+:.*?##/ { \
		printf "  \033[36m%-23s\033[0m %s\n", $$1, $$2 \
	} \
	/^##@/ { \
		printf "\n\033[1m%s\033[0m\n", substr($$0, 5) \
	} ' $(MAKEFILE_LIST)

# Additional help entries
buildenv-base: ## OCI image used for building Unikraft unikernels with kraft.
buildenv-base-golang: ## OCI image used for building Unikraft unikernels with kraft and golang included.
buildenv-gcc: ## OCI image containing a Unikraft-centric build of gcc.
buildenv-myself-full: ## OCI image containing the build environment for KraftKit.
buildenv-myself: ## OCI image containing KraftKit binaries.
buildenv-qemu: ## OCI image containing a Unikraft-centric build of QEMU.
buildenv-github-action: ## OCI image used when building Unikraft unikernels in GitHub Actions.
tools: ## Build all tools.
kraft: ## The kraft binary.
runu: ## The runu binary.