#!/usr/bin/make -f

# Copyright 2019 The gVisor Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Helpful pretty-printer.
MAKEBANNER := \033[1;34mmake\033[0m
submake = echo -e '$(MAKEBANNER) $1' >&2; $(MAKE) $1

# Described below.
OPTIONS :=
STARTUP_OPTIONS :=
TARGETS := //runsc
ARGS    :=

default: runsc
.PHONY: default

## usage: make <target>
##         or
##        make <build|test|copy|run|sudo> STARTUP_OPTIONS="..." OPTIONS="..." TARGETS="..." ARGS="..."
##
## Basic targets.
##
##   This Makefile wraps basic build and test targets for ease-of-use. Bazel
##   is run inside a canonical Docker container in order to simplify up-front
##   requirements.
##
##   There are common arguments that may be passed to targets. These are:
##     STARTUP_OPTIONS - Bazel startup options.
##     OPTIONS - Build or test options.
##     TARGETS - The bazel targets.
##     ARGS    - Arguments for run or sudo.
##
##   Additionally, the copy target expects a DESTINATION to be provided.
##
##   For example, to build runsc using this Makefile, you can run:
##     make build OPTIONS="" TARGETS="//runsc"'
##
help: ## Shows all targets and help from the Makefile (this message).
	@grep --no-filename -E '^([a-z.A-Z_-]+:.*?|)##' $(MAKEFILE_LIST) | \
		awk 'BEGIN {FS = "(:.*?|)## ?"}; { \
			if (length($$1) > 0) { \
				printf "  \033[36m%-20s\033[0m %s\n", $$1, $$2; \
			} else { \
				printf "%s\n", $$2; \
			} \
		}'
build: ## Builds the given $(TARGETS) with the given $(OPTIONS). E.g. make build TARGETS=runsc
test:  ## Tests the given $(TARGETS) with the given $(OPTIONS). E.g. make test TARGETS=pkg/buffer:buffer_test
copy:  ## Copies the given $(TARGETS) to the given $(DESTINATION). E.g. make copy TARGETS=runsc DESTINATION=/tmp
run:   ## Runs the given $(TARGETS), built with $(OPTIONS), using $(ARGS). E.g. make run TARGETS=runsc ARGS=-version
sudo:  ## Runs the given $(TARGETS) as per run, but using "sudo -E". E.g. make sudo TARGETS=test/root:root_test ARGS=-test.v
.PHONY: help build test copy run sudo

# Load all bazel wrappers.
#
# This file should define the basic "build", "test", "run" and "sudo" rules, in
# addition to the $(BRANCH_NAME) variable.
ifneq (,$(wildcard tools/google.mk))
include tools/google.mk
else
include tools/bazel.mk
endif

##
## Docker image targets.
##
##   Images used by the tests must also be built and available locally.
##   The canonical test targets defined below will automatically load
##   relevant images. These can be loaded or built manually via these
##   targets.
##
##   (*) Note that you may provide an ARCH parameter in order to build
##   and load images from an alternate archiecture (using qemu). When
##   bazel is run as a server, this has the effect of running an full
##   cross-architecture chain, and can produce cross-compiled binaries.
##
define images
$(1)-%: ## Image tool: $(1) a given image (also may use 'all-images').
	@$(call submake,-C images $$@)
endef
rebuild-...: ## Rebuild the given image. Also may use 'rebuild-all-images'.
$(eval $(call images,rebuild))
push-...: ## Push the given image. Also may use 'push-all-images'.
$(eval $(call images,push))
pull-...: ## Pull the given image. Also may use 'pull-all-images'.
$(eval $(call images,pull))
load-...: ## Load (pull or rebuild) the given image. Also may use 'load-all-images'.
$(eval $(call images,load))
list-images: ## List all available images.
	@$(call submake, -C images $$@)

##
## Canonical build and test targets.
##
##   These targets are used by continuous integration and provide
##   convenient entrypoints for testing changes. If you're adding a
##   new subsystem or workflow, consider adding a new target here.
##
runsc: ## Builds the runsc binary.
	@$(call submake,build OPTIONS="-c opt" TARGETS="//runsc")
.PHONY: runsc

debian: ## Builds the debian packages.
	@$(call submake,build OPTIONS="-c opt" TARGETS="//debian:debian")
.PHONY: debian

smoke-tests: ## Runs a simple smoke test after build runsc.
	@$(call submake,run DOCKER_PRIVILEGED="" ARGS="--alsologtostderr --network none --debug --TESTONLY-unsafe-nonroot=true --rootless do true")
.PHONY: smoke-tests

fuse-tests:
	@$(call submake,test OPTIONS="--test_tag_filters fuse" TARGETS="test/fuse/...")
.PHONY: fuse-tests

unit-tests: ## Local package unit tests in pkg/..., runsc/, tools/.., etc.
	@$(call submake,test TARGETS="pkg/... runsc/... tools/...")
.PHONY: unit-tests

tests: ## Runs all unit tests and syscall tests.
tests: unit-tests syscall-tests
.PHONY: tests

integration-tests: ## Run all standard integration tests.
integration-tests: docker-tests overlay-tests hostnet-tests swgso-tests
integration-tests: do-tests kvm-tests containerd-test-1.3.4
.PHONY: integration-tests

network-tests: ## Run all networking integration tests.
network-tests: iptables-tests packetdrill-tests packetimpact-tests
.PHONY: network-tests

# Standard integration targets.
INTEGRATION_TARGETS := //test/image:image_test //test/e2e:integration_test

syscall-%-tests:
	@$(call submake,test OPTIONS="--test_tag_filters runsc_$*" TARGETS="test/syscalls/...")

syscall-native-tests:
	@$(call submake,test OPTIONS="--test_tag_filters native" TARGETS="test/syscalls/...")
.PHONY: syscall-native-tests

syscall-tests: ## Run all system call tests.
	@$(call submake,test TARGETS="test/syscalls/...")

%-runtime-tests: load-runtimes_%
ifeq ($(PARTITION),)
	@$(eval PARTITION := 1)
endif
ifeq ($(TOTAL_PARTITIONS),)
	@$(eval TOTAL_PARTITIONS := 1)
endif
	@$(call submake,install-runtime)
	@$(call submake,test-runtime OPTIONS="--test_timeout=10800 --test_arg=--partition=$(PARTITION) --test_arg=--total_partitions=$(TOTAL_PARTITIONS)" TARGETS="//test/runtimes:$*")

%-runtime-tests_vfs2: load-runtimes_%
ifeq ($(PARTITION),)
	@$(eval PARTITION := 1)
endif
ifeq ($(TOTAL_PARTITIONS),)
	@$(eval TOTAL_PARTITIONS := 1)
endif
	@$(call submake,install-runtime RUNTIME="vfs2" ARGS="--vfs2")
	@$(call submake,test-runtime RUNTIME="vfs2" OPTIONS="--test_timeout=10800 --test_arg=--partition=$(PARTITION) --test_arg=--total_partitions=$(TOTAL_PARTITIONS)" TARGETS="//test/runtimes:$*")

do-tests: runsc
	@$(call submake,run TARGETS="//runsc" ARGS="--rootless do true")
	@$(call submake,run TARGETS="//runsc" ARGS="--rootless -network=none do true")
	@$(call submake,sudo TARGETS="//runsc" ARGS="do true")
.PHONY: do-tests

simple-tests: unit-tests # Compatibility target.
.PHONY: simple-tests

docker-tests: load-basic-images
	@$(call submake,install-runtime RUNTIME="vfs1")
	@$(call submake,test-runtime RUNTIME="vfs1" TARGETS="$(INTEGRATION_TARGETS)")
	@$(call submake,install-runtime RUNTIME="vfs2" ARGS="--vfs2")
	@$(call submake,test-runtime RUNTIME="vfs2" TARGETS="$(INTEGRATION_TARGETS)")
.PHONY: docker-tests

overlay-tests: load-basic-images
	@$(call submake,install-runtime RUNTIME="overlay" ARGS="--overlay")
	@$(call submake,test-runtime RUNTIME="overlay" TARGETS="$(INTEGRATION_TARGETS)")
.PHONY: overlay-tests

swgso-tests: load-basic-images
	@$(call submake,install-runtime RUNTIME="swgso" ARGS="--software-gso=true --gso=false")
	@$(call submake,test-runtime RUNTIME="swgso" TARGETS="$(INTEGRATION_TARGETS)")
.PHONY: swgso-tests

hostnet-tests: load-basic-images
	@$(call submake,install-runtime RUNTIME="hostnet" ARGS="--network=host")
	@$(call submake,test-runtime RUNTIME="hostnet" OPTIONS="--test_arg=-checkpoint=false" TARGETS="$(INTEGRATION_TARGETS)")
.PHONY: hostnet-tests

kvm-tests: load-basic-images
	@(lsmod | grep -E '^(kvm_intel|kvm_amd)') || sudo modprobe kvm
	@if ! [[ -w /dev/kvm ]]; then sudo chmod a+rw /dev/kvm; fi
	@$(call submake,test TARGETS="//pkg/sentry/platform/kvm:kvm_test")
	@$(call submake,install-runtime RUNTIME="kvm" ARGS="--platform=kvm")
	@$(call submake,test-runtime RUNTIME="kvm" TARGETS="$(INTEGRATION_TARGETS)")
.PHONY: kvm-tests

iptables-tests: load-iptables
	@sudo modprobe iptable_filter
	@sudo modprobe ip6table_filter
	@$(call submake,test-runtime RUNTIME="runc" TARGETS="//test/iptables:iptables_test")
	@$(call submake,install-runtime RUNTIME="iptables" ARGS="--net-raw")
	@$(call submake,test-runtime RUNTIME="iptables" TARGETS="//test/iptables:iptables_test")
.PHONY: iptables-tests

# Run the iptables tests with runsc only. Useful for developing to skip runc
# testing.
iptables-runsc-tests: load-iptables
	@sudo modprobe iptable_filter
	@sudo modprobe ip6table_filter
	@$(call submake,install-runtime RUNTIME="iptables" ARGS="--net-raw")
	@$(call submake,test-runtime RUNTIME="iptables" TARGETS="//test/iptables:iptables_test")
.PHONY: iptables-runsc-tests

packetdrill-tests: load-packetdrill
	@$(call submake,install-runtime RUNTIME="packetdrill")
	@$(call submake,test-runtime RUNTIME="packetdrill" TARGETS="$(shell $(MAKE) query TARGETS='attr(tags, packetdrill, tests(//...))')")
.PHONY: packetdrill-tests

packetimpact-tests: load-packetimpact
	@sudo modprobe iptable_filter
	@sudo modprobe ip6table_filter
	@$(call submake,install-runtime RUNTIME="packetimpact")
	@$(call submake,test-runtime OPTIONS="--jobs=HOST_CPUS*3 --local_test_jobs=HOST_CPUS*3" RUNTIME="packetimpact" TARGETS="$(shell $(MAKE) query TARGETS='attr(tags, packetimpact, tests(//...))')")
.PHONY: packetimpact-tests

# Specific containerd version tests.
containerd-test-%: load-basic_alpine load-basic_python load-basic_busybox load-basic_resolv load-basic_httpd load-basic_ubuntu
	@$(call submake,install-runtime RUNTIME="root")
	@CONTAINERD_VERSION=$* $(MAKE) sudo TARGETS="tools/installers:containerd"
	@$(MAKE) sudo TARGETS="tools/installers:shim"
	@$(MAKE) sudo TARGETS="test/root:root_test" ARGS="--runtime=root -test.v"

# Note that we can't run containerd-test-1.1.8 tests here.
#
# Containerd 1.1.8 should work, but because of a bug in loading images locally
# (https://github.com/kubernetes-sigs/cri-tools/issues/421), we are unable to
# actually drive the tests. The v1 API is tested exclusively through 1.2.13.
containerd-tests: ## Runs all supported containerd version tests.
containerd-tests: containerd-test-1.2.13
containerd-tests: containerd-test-1.3.4
containerd-tests: containerd-test-1.4.0-beta.0

##
## Benchmarks.
##
## Targets to run benchmarks. See //test/benchmarks for details.
##
##   common arguments:
##     RUNTIME_ARGS - arguments to runsc placed in /etc/docker/daemon.json
##       e.g. "--platform=ptrace"
##     BENCHMARKS_PROJECT - BigQuery project to which to send data.
##     BENCHMARKS_DATASET - BigQuery dataset to which to send data.
##     BENCHMARKS_TABLE - BigQuery table to which to send data.
##     BENCHMARKS_SUITE - name of the benchmark suite. See //tools/bigquery/bigquery.go.
##     BENCHMARKS_UPLOAD - if true, upload benchmark data from the run.
##     BENCHMARKS_OFFICIAL - marks the data as official.
##     BENCHMARKS_PLATFORMS - platforms to run benchmarks (e.g. ptrace kvm).
##
BENCHMARKS_PROJECT   := gvisor-benchmarks
BENCHMARKS_DATASET   := kokoro
BENCHMARKS_TABLE     := benchmarks
BENCHMARKS_SUITE     := start
BENCHMARKS_UPLOAD    := false
BENCHMARKS_OFFICIAL  := false
BENCHMARKS_PLATFORMS := ptrace
BENCHMARKS_TARGETS   := //test/benchmarks/base:startup_test
BENCHMARKS_ARGS      := -test.bench=.

init-benchmark-table: ## Initializes a BigQuery table with the benchmark schema
## (see //tools/bigquery/bigquery.go). If the table alread exists, this is a noop.
	$(call submake, run TARGETS=//tools/parsers:parser ARGS="init --project=$(BENCHMARKS_PROJECT) \
	--dataset=$(BENCHMARKS_DATASET) --table=$(BENCHMARKS_TABLE)")
.PHONY: init-benchmark-table

benchmark-platforms: load-benchmarks-images ## Runs benchmarks for runc and all given platforms in BENCHMARK_PLATFORMS.
	$(call submake, run-benchmark RUNTIME="runc")
	$(foreach PLATFORM,$(BENCHMARKS_PLATFORMS), \
		$(call submake,install-runtime RUNTIME="$(PLATFORM)" ARGS="--platform=$(PLATFORM) --vfs2") && \
		$(call submake,run-benchmark RUNTIME="$(PLATFORM)") && \
		$(call submake,install-runtime RUNTIME="$(PLATFORM)_vfs1" ARGS="--platform=$(PLATFORM)") && \
		$(call submake,run-benchmark RUNTIME="$(PLATFORM)_vfs1") && \
	) \
	true
.PHONY: benchmark-platforms

run-benchmark: ## Runs single benchmark and optionally sends data to BigQuery.
	@set -xeuo pipefail; 	T=$$(mktemp --tmpdir logs.$(RUNTIME).XXXXXX); \
	$(call submake,sudo TARGETS="$(BENCHMARKS_TARGETS)" ARGS="--runtime=$(RUNTIME) $(BENCHMARKS_ARGS)" | tee $$T); \
	if [[ "$(BENCHMARKS_UPLOAD)" == "true" ]]; then \
		$(call submake,run TARGETS=tools/parsers:parser ARGS="parse --debug --file=$$T \
			--runtime=$(RUNTIME) --suite_name=$(BENCHMARKS_SUITE) \
			--project=$(BENCHMARKS_PROJECT) --dataset=$(BENCHMARKS_DATASET) \
			--table=$(BENCHMARKS_TABLE) --official=$(BENCHMARKS_OFFICIAL)"); \
	fi; \
	rm -rf $$T
.PHONY: run-benchmark

##
## Website & documentation helpers.
##
##   The website is built from repository documentation and wrappers, using
##   using a locally-defined Docker image (see images/jekyll). The following
##   variables may be set when using website-push:
##     WEBSITE_IMAGE   - The name of the container image.
##     WEBSITE_SERVICE - The backend service.
##     WEBSITE_PROJECT - The project id to use.
##     WEBSITE_REGION  - The region to deploy to.
##
WEBSITE_IMAGE   := gcr.io/gvisordev/gvisordev
WEBSITE_SERVICE := gvisordev
WEBSITE_PROJECT := gvisordev
WEBSITE_REGION  := us-central1

website-build: load-jekyll ## Build the site image locally.
	@$(call submake,run TARGETS="//website:website" ARGS="$(WEBSITE_IMAGE)")
.PHONY: website-build

website-server: website-build ## Run a local server for development.
	@docker run -i -p 8080:8080 $(WEBSITE_IMAGE)
.PHONY: website-server

website-push: website-build ## Push a new image and update the service.
	@docker push $(WEBSITE_IMAGE)
.PHONY: website-push

website-deploy: website-push ## Deploy a new version of the website.
	@gcloud run deploy $(WEBSITE_SERVICE) --platform=managed --region=$(WEBSITE_REGION) --project=$(WEBSITE_PROJECT) --image=$(WEBSITE_IMAGE)
.PHONY: website-deploy

##
## Repository builders.
##
##   This builds a local apt repository. The following variables may be set:
##     RELEASE_ROOT    - The repository root (default: "repo" directory).
##     RELEASE_KEY     - The repository GPG private key file (default: dummy key is created).
##     RELEASE_NIGHTLY - Set to true if a nightly release (default: false).
##     RELEASE_COMMIT  - The commit or Change-Id for the release (needed for tag).
##     RELEASE_NAME    - The name of the release in the proper format (needed for tag).
##     RELEASE_NOTES   - The file containing release notes (needed for tag).
##
RELEASE_ROOT    := $(CURDIR)/repo
RELEASE_KEY     := repo.key
RELEASE_NIGHTLY := false
RELEASE_COMMIT  :=
RELEASE_NAME    :=
RELEASE_NOTES   :=

GPG_TEST_OPTIONS := $(shell if gpg --pinentry-mode loopback --version >/dev/null 2>&1; then echo --pinentry-mode loopback; fi)
$(RELEASE_KEY):
	@echo "WARNING: Generating a key for testing ($@); don't use this."
	T=$$(mktemp --tmpdir keyring.XXXXXX); \
	C=$$(mktemp --tmpdir config.XXXXXX); \
	echo Key-Type: DSA >> $$C && \
	echo Key-Length: 1024 >> $$C && \
	echo Name-Real: Test >> $$C && \
	echo Name-Email: test@example.com >> $$C && \
	echo Expire-Date: 0 >> $$C && \
	echo %commit >> $$C && \
	gpg --batch $(GPG_TEST_OPTIONS) --passphrase '' --no-default-keyring --secret-keyring $$T --no-tty --gen-key $$C && \
	gpg --batch $(GPG_TEST_OPTIONS) --export-secret-keys --no-default-keyring --secret-keyring $$T > $@; \
	rc=$$?; rm -f $$T $$C; exit $$rc

release: $(RELEASE_KEY) ## Builds a release.
	@mkdir -p $(RELEASE_ROOT)
	@T=$$(mktemp -d --tmpdir release.XXXXXX); \
	  $(call submake,copy TARGETS="//runsc:runsc" DESTINATION=$$T) && \
	  $(call submake,copy TARGETS="//shim/v1:gvisor-containerd-shim" DESTINATION=$$T) && \
	  $(call submake,copy TARGETS="//shim/v2:containerd-shim-runsc-v1" DESTINATION=$$T) && \
	  $(call submake,copy TARGETS="//debian:debian" DESTINATION=$$T) && \
	  NIGHTLY=$(RELEASE_NIGHTLY) tools/make_release.sh $(RELEASE_KEY) $(RELEASE_ROOT) $$T/*; \
	rc=$$?; rm -rf $$T; exit $$rc
.PHONY: release

tag: ## Creates and pushes a release tag.
	@tools/tag_release.sh "$(RELEASE_COMMIT)" "$(RELEASE_NAME)" "$(RELEASE_NOTES)"
.PHONY: tag

##
## Development helpers and tooling.
##
##   These targets faciliate local development by automatically
##   installing and configuring a runtime. Several variables may
##   be used here to tweak the installation:
##     RUNTIME         - The name of the installed runtime (default: branch).
##     RUNTIME_DIR     - Where the runtime will be installed (default: temporary directory with the $RUNTIME).
##     RUNTIME_BIN     - The runtime binary (default: $RUNTIME_DIR/runsc).
##     RUNTIME_LOG_DIR - The logs directory (default: $RUNTIME_DIR/logs).
##     RUNTIME_LOGS    - The log pattern (default: $RUNTIME_LOG_DIR/runsc.log.%TEST%.%TIMESTAMP%.%COMMAND%).
##
ifeq (,$(BRANCH_NAME))
RUNTIME     := runsc
RUNTIME_DIR := $(shell dirname $(shell mktemp -u))/$(RUNTIME)
else
RUNTIME     := $(BRANCH_NAME)
RUNTIME_DIR := $(shell dirname $(shell mktemp -u))/$(RUNTIME)
endif
RUNTIME_BIN     := $(RUNTIME_DIR)/runsc
RUNTIME_LOG_DIR := $(RUNTIME_DIR)/logs
RUNTIME_LOGS    := $(RUNTIME_LOG_DIR)/runsc.log.%TEST%.%TIMESTAMP%.%COMMAND%

dev: ## Installs a set of local runtimes. Requires sudo.
	@$(call submake,refresh ARGS="--net-raw")
	@$(call submake,configure RUNTIME_NAME="$(RUNTIME)" ARGS="--net-raw")
	@$(call submake,configure RUNTIME_NAME="$(RUNTIME)-d" ARGS="--net-raw --debug --strace --log-packets")
	@$(call submake,configure RUNTIME_NAME="$(RUNTIME)-p" ARGS="--net-raw --profile")
	@$(call submake,configure RUNTIME_NAME="$(RUNTIME)-vfs2-d" ARGS="--net-raw --debug --strace --log-packets --vfs2")
	@sudo systemctl restart docker
.PHONY: dev

refresh: ## Refreshes the runtime binary (for development only). Must have called 'dev' or 'install-runtime' first.
	@mkdir -p "$(RUNTIME_DIR)"
	@$(call submake,copy TARGETS=runsc DESTINATION="$(RUNTIME_BIN)")
.PHONY: refresh

install-runtime: ## Installs the runtime for testing. Requires sudo.
	@$(call submake,refresh ARGS="--net-raw --TESTONLY-test-name-env=RUNSC_TEST_NAME $(ARGS)")
	@$(call submake,configure RUNTIME_NAME=runsc)
	@$(call submake,configure RUNTIME_NAME="$(RUNTIME)")
	@sudo systemctl restart docker
	@if [[ -f /etc/docker/daemon.json ]]; then \
		sudo chmod 0755 /etc/docker && \
		sudo chmod 0644 /etc/docker/daemon.json; \
	fi
.PHONY: install-runtime

install-debug-runtime: ## Installs the runtime for debugging. Requires sudo.
	@$(call submake,install-runtime ARGS="--debug --strace --log-packets $(ARGS)")
.PHONY: install-debug-runtime

configure: ## Configures a single runtime. Requires sudo. Typically called from dev or install-runtime.
	@sudo sudo "$(RUNTIME_BIN)" install --experimental=true --runtime="$(RUNTIME_NAME)" -- --debug-log "$(RUNTIME_LOGS)" $(ARGS)
	@echo -e "$(INFO) Installed runtime \"$(RUNTIME)\" @ $(RUNTIME_BIN)"
	@echo -e "$(INFO) Logs are in: $(RUNTIME_LOG_DIR)"
	@sudo rm -rf "$(RUNTIME_LOG_DIR)" && mkdir -p "$(RUNTIME_LOG_DIR)"
.PHONY: configure

test-runtime: ## A convenient wrapper around test that provides the runtime argument. Target must still be provided.
	@$(call submake,test OPTIONS="$(OPTIONS) --test_arg=--runtime=$(RUNTIME)")
.PHONY: test-runtime

nogo: ## Surfaces all nogo findings.
	@$(call submake,build OPTIONS="--build_tag_filters nogo" TARGETS="//...")
	@$(call submake,run TARGETS="//tools/github" ARGS="$(foreach dir,$(BUILD_ROOTS),-path=$(CURDIR)/$(dir)) -dry-run nogo")
.PHONY: nogo

gazelle: ## Runs gazelle to update WORKSPACE.
	@$(call submake,run TARGETS="//:gazelle" ARGS="update-repos -from_file=go.mod -prune")
.PHONY: gazelle