# 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.

# ***************************************************
# WARNING! If you edit this file, also edit Makefile!
# ***************************************************

include version.mk
-include install.mk

SHELL=cmd.exe
REBAR?=$(CURDIR)/bin/rebar.cmd
PYTHON=python.exe
ERLFMT?=$(CURDIR)/bin/erlfmt.cmd
MAKE=make -f Makefile.win
GRADLE?=$(CURDIR)/extra/nouveau/gradlew.bat
# REBAR?=$(shell where rebar.cmd)

# Handle the following scenarios:
#   1. When building from a tarball, use version.mk.
#   2. When building from a clean release tag (#.#.#), use that tag.
#   3. When building from a clean RC tag (#.#.#-RC#), use JUST the version
#      number inside the tarball, but use the full name for the name of the
#      tarball itself.
#   4. When not on a clean tag, use version.mk + git sha + dirty status.

COUCHDB_GIT_SHA=$(git_sha)

IN_RELEASE = $(shell if not exist .git echo true)

ifeq ($(IN_RELEASE), true)

# 1. Building from tarball, use version.mk.
COUCHDB_VERSION = $(vsn_major).$(vsn_minor).$(vsn_patch)

else

# Gather some additional information.
# We do it this way so we don't bake shell-isms into Makefile
# to make it easier to port to Windows. I know, I know. -jst
# IN_RC contains the -RCx suffix in the name if present
IN_RC = $(shell git describe --tags --always --first-parent \
        | grep -Eo -- '-RC[0-9]+' 2>nul)
# ON_TAG matches *ONLY* if we are on a release or RC tag
ON_TAG = $(shell git describe --tags --always --first-parent \
        | grep -Eo -- '^[0-9]+\.[0-9]\.[0-9]+(-RC[0-9]+)?$$' 2>nul)
# REL_TAG contains the #.#.# from git describe, which might be used
REL_TAG = $(shell git describe --tags --always --first-parent \
        | grep -Eo -- '^[0-9]+\.[0-9]\.[0-9]+' 2>nul)
# DIRTY identifies if we're not on a commit
DIRTY = $(shell git describe --dirty | grep -Eo -- '-dirty' 2>nul)
# COUCHDB_GIT_SHA is our current git hash.
COUCHDB_GIT_SHA=$(shell git rev-parse --short=7 --verify HEAD)

ifeq ($(ON_TAG),)
# 4. Not on a tag.
COUCHDB_VERSION_SUFFIX = $(COUCHDB_GIT_SHA)$(DIRTY)
COUCHDB_VERSION = $(vsn_major).$(vsn_minor).$(vsn_patch)-$(COUCHDB_VERSION_SUFFIX)
else
# 2 and 3. On a tag.
COUCHDB_VERSION = $(REL_TAG)$(DIRTY)
endif
endif

# needed to do text substitutions
comma:= ,
empty:=
space:= $(empty) $(empty)

DESTDIR=

# Rebar options
apps=
skip_deps=meck,mochiweb,triq,proper,snappy,ibrowse,gun,recon
suites=
tests=

# no sed on Windows (per default), but powershell
COMPILE_OPTS=$(shell powershell -command "('apps=${apps} ') -replace '[a-z_]+= ', ' '")
EUNIT_OPTS=$(shell powershell -command "('skip_deps=${skip_deps} suites=${suites} tests=${tests} ') -replace '[a-z]+= ', ' '")
DIALYZE_OPTS=$(shell powershell -command "('apps=${apps} skip_deps=${skip_deps} ') -replace '[a-z]+= ', ' '")

EXUNIT_OPTS=$(subst $(comma),$(space),$(tests))

TEST_OPTS="-c startup_jitter=0 -c default_security=admin_local -c iterations=9"

ifneq ($(ERLANG_COOKIE),)
TEST_OPTS+=" --erlang-cookie=$(ERLANG_COOKIE)"
endif

################################################################################
# Main commands
################################################################################


.PHONY: all
# target: all - Build everything
all: couch-core fauxton docs extra/nouveau


.PHONY: help
# target: help - Print this help
help:
	@powershell -Command 'Select-String Makefile.win -Pattern "^# target: (?<name>.*) - (?<description>.*)" | ForEach-Object { ("{0,-22} - {1}" -f $$_.Matches[0].Groups["name"], $$_.Matches[0].Groups["description"]) } | Sort'


################################################################################
# Building
################################################################################


.PHONY: couch-core
# target: couch-core - Build CouchDB core, use ERL_COMPILER_OPTIONS to provide custom compiler's options
couch-core: config.erl
	@set COUCHDB_VERSION=$(COUCHDB_VERSION) && set COUCHDB_GIT_SHA=$(COUCHDB_GIT_SHA) && $(REBAR) compile $(COMPILE_OPTS)
ifeq ($(with_spidermonkey), true)
	@copy src\couch\priv\couchjs.exe bin
endif


.PHONY: docs
# target: docs - Build documentation
ifeq ($(IN_RELEASE), true)
docs: share\docs\html
else
docs: src\docs\build
endif

.PHONY: fauxton
# target: fauxton - Build Fauxton web UI
fauxton: share\www


################################################################################
# Testing
################################################################################


.PHONY: check
# target: check - Test everything
check: all
	@$(MAKE) eunit
	@$(MAKE) mango-test
	@$(MAKE) elixir
	@$(MAKE) elixir-search
	@$(MAKE) nouveau-test

ifdef apps
EUNIT_SUBDIRS = $(strip $(subst $(comma),$(space),$(apps)))
else
EUNIT_SUBDIRS = $(filter-out fauxton docs, $(shell dir /b src))
endif

# target: eunit - Run EUnit tests, use EUNIT_OPTS to provide custom options
.PHONY: eunit $(EUNIT_SUBDIRS)
eunit: export ERL_AFLAGS = -config $(CURDIR)/rel/files/eunit.config
eunit: export COUCHDB_QUERY_SERVER_JAVASCRIPT = $(CURDIR)/bin/couchjs $(CURDIR)/share/server/main.js
eunit: export COUCHDB_TEST_ADMIN_PARTY_OVERRIDE=1
eunit: $(EUNIT_SUBDIRS)

$(EUNIT_SUBDIRS):
	-@rmdir /s /q tmp\$@
	@$(REBAR) setup_eunit app=$@
	@set BUILDDIR=$(CURDIR)/tmp/$@ && set COUCHDB_VERSION=$(COUCHDB_VERSION) && set COUCHDB_GIT_SHA=$(COUCHDB_GIT_SHA) && $(REBAR) -r eunit $(EUNIT_OPTS) apps=$@ && @rmdir /s/q tmp\$@

# cat together couch_log files after running eunit test
.PHONY: catlogs
catlogs:
	@type tmp/*/couch.log > tmp/couch.log || true

# target: erlfmt-check - Check Erlang source code formatting
erlfmt-check: export ERLFMT_PATH = $(ERLFMT)
erlfmt-check:
	@$(PYTHON) dev\format_check.py

# target: erlfmt-format - Apply Erlang source code format standards automatically
erlfmt-format: export ERLFMT_PATH = $(ERLFMT)
erlfmt-format:
	@$(PYTHON) dev\format_all.py

.venv/bin/black:
	@$(PYTHON) -m venv .venv
	@.venv\Scripts\pip3.exe install black==26.3.1

# target: python-black - Check Python code formatting (requires Python >= 3.6)
python-black: .venv/bin/black
	@$(PYTHON) -c "import sys; exit(1 if sys.version_info < (3,6) else 0)" || \
		echo 'Python formatter not supported on Python < 3.6; check results on a newer platform'
	@$(PYTHON) -c "import sys; exit(1 if sys.version_info >= (3,6) else 0)" || \
		.venv\Scripts\black.exe --check \
		--exclude="build/|buck-out/|dist/|_build/|\.git/|\.hg/|\.mypy_cache/|\.nox/|\.tox/|\.venv/|src/erlfmt|src/rebar/pr2relnotes.py|src/fauxton" \
		build-aux dev\run dev\format_*.py src\mango\test src\docs\src\conf.py src\docs\ext .

# target: python-black-update - Apply Python source code format standards automatically (requires Python >= 3.6)
python-black-update: .venv/bin/black
	@$(PYTHON) -c "import sys; exit(1 if sys.version_info < (3,6) else 0)" || \
		echo 'Python formatter not supported on Python < 3.6; check results on a newer platform'
	@$(PYTHON) -c "import sys; exit(1 if sys.version_info >= (3,6) else 0)" || \
		.venv\Scripts\black.exe \
		--exclude="build/|buck-out/|dist/|_build/|\.git/|\.hg/|\.mypy_cache/|\.nox/|\.tox/|\.venv/|src/erlfmt|src/rebar/pr2relnotes.py|src/fauxton" \
		build-aux dev\run dev\format_*.py src\mango\test src\docs\src\conf.py src\docs\ext .



ifeq ($(with_nouveau), false)
  exclude_nouveau=--exclude nouveau
endif

.PHONY: elixir-init
elixir-init: export MIX_ENV=integration
elixir-init: export ELIXIR_ERL_OPTIONS=+fnu
elixir-init: config.erl
	@mix local.rebar --force && mix local.hex --force && mix deps.get

.PHONY: elixir-cluster-without-quorum
elixir-cluster-without-quorum: export MIX_ENV=integration
elixir-cluster-without-quorum: elixir-init devclean
	@dev\run -n 3 -q -a adm:pass \
	    --degrade-cluster 2 \
        --no-eval 'mix test --trace --only without_quorum_test $(EXUNIT_OPTS)'

.PHONY: elixir-cluster-with-quorum
elixir-cluster-with-quorum: export MIX_ENV=integration
elixir-cluster-with-quorum: elixir-init devclean
	@dev\run -n 3 -q -a adm:pass \
	    --degrade-cluster 1 \
		--no-eval 'mix test --trace --only with_quorum_test $(EXUNIT_OPTS)'

.PHONY: elixir
# target: elixir - Run Elixir-based integration tests
elixir: export MIX_ENV=integration
elixir: elixir-init devclean
	@dev\run "$(TEST_OPTS)" -n 1 -q -a adm:pass \
		--enable-erlang-views \
		--no-join \
		--locald-config test/elixir/test/config/test-config.ini \
		--erlang-config rel/files/eunit.config \
		--no-eval 'mix test --trace --include test\elixir\test\config\suite.elixir --exclude test\elixir\test\config\skip.elixir $(EXUNIT_OPTS)'

ifneq ($(CLOUSEAU_DIR),)
_WITH_CLOUSEAU="--with-clouseau --clouseau-dir=$(CLOUSEAU_DIR)"
else ifeq ($(with_clouseau), true)
_WITH_CLOUSEAU="--with-clouseau"
endif

.PHONY: elixir-search
# target: elixir-search - Run search tests, requires a configured Clouseau instance
elixir-search: export MIX_ENV=integration
elixir-search: elixir-init devclean
ifneq ($(_WITH_CLOUSEAU), )
	@dev\run -n 1 -q -a adm:pass \
		"$(_WITH_CLOUSEAU)" \
		"$(TEST_OPTS)" \
		--locald-config test/config/test-config.ini \
		--no-eval 'mix test --trace --include test/elixir/test/config/search.elixir'
else
	@echo "Warning: Clouseau is not enabled, `elixir-search` cannot be run."
endif

.PHONY: elixir-source-checks
# target: elixir-source-checks - Check source code formatting of Elixir test files
elixir-source-checks: export MIX_ENV=integration
elixir-source-checks: export ELIXIR_ERL_OPTIONS=+fnu
elixir-source-checks: elixir-init
	@mix format --check-formatted
	@mix credo

.PHONY: build-report
# target: build-report - Generate a build report
build-report:
	@$(PYTHON) build-aux/show-test-results.py --suites=10 --tests=10 > test-results.log || true
	cat .\dev\logs\node1.log || true
	cat .\dev\logs\nouveau.log || true
	$(MAKE) catlogs || true
	cat .\tmp\couch.log || true
	cat test-results.log || true

.PHONY: check-qs
# target: check-qs - Run query server tests (ruby and rspec required!)
check-qs:
	@QS_LANG=js rspec test\view_server\query_server_spec.rb

.PHONY: list-eunit-apps
# target: list-eunit-apps - List EUnit target apps
list-eunit-apps:
	@powershell -Command 'Get-ChildItem -Path src -Recurse -Filter "*_tests?.erl" | Resolve-Path -Relative | ForEach-Object { $$_.Split("\") | Select -Skip 2 -First 1 } | Sort -Unique'


.PHONY: list-eunit-suites
# target: list-eunit-suites - List EUnit target test suites
list-eunit-suites:
	@powershell -Command 'Get-ChildItem -Path src -Recurse -Filter "*_tests?.erl" | ForEach-Object { "{0}" -f $$_.BaseName } | Sort'

.PHONY: mango-test
# target: mango-test - Run Mango tests
mango-test: devclean all
	@$(PYTHON) -m venv src\mango\.venv && \
		src\mango\.venv\Scripts\pip.exe install -r src\mango\requirements.txt
	@dev\run \
		"$(TEST_OPTS)" \
		"$(_WITH_CLOUSEAU)" \
		-n 1 \
		--admin=adm:pass \
		"\
env COUCH_USER=adm COUCH_PASS=pass \
src\mango\.venv\Scripts\nose2 -F -s src\mango\test -c src\mango\unittest.cfg $(MANGO_TEST_OPTS)"


################################################################################
# Developing
################################################################################


.PHONY: build-plt
# target: build-plt - Build project-specific PLT
build-plt:
	@$(REBAR) -r build-plt $(DIALYZE_OPTS)


.PHONY: check-plt
# target: check-plt - Check the PLT for consistency and rebuild it if it is not up-to-date
check-plt:
	@$(REBAR) -r check-plt $(DIALYZE_OPTS)


.PHONY: dialyze
# target: dialyze - Analyze the code for discrepancies
dialyze: .rebar
	@$(REBAR) -r dialyze $(DIALYZE_OPTS)

.PHONY: xref
# target: xref - Find unused exports etc
xref:
	@$(REBAR) --keep-going --recursive xref $(DIALYZE_OPTS)


################################################################################
# Distributing
################################################################################


.PHONY: dist
# target: dist - Make release tarball
dist: all derived
	@.\build-aux\couchdb-build-release.sh $(COUCHDB_VERSION)

	@copy -r share\www apache-couchdb-$(COUCHDB_VERSION)\share
	@mkdir apache-couchdb-$(COUCHDB_VERSION)\share\docs\html
	@copy -r src\docs\build\html apache-couchdb-$(COUCHDB_VERSION)\share\docs

	@mkdir apache-couchdb-$(COUCHDB_VERSION)\share\docs\man
	@copy src\docs\build\man\apachecouchdb.1 apache-couchdb-$(COUCHDB_VERSION)\share\docs\man

	@tar czf apache-couchdb-$(COUCHDB_VERSION).tar.gz apache-couchdb-$(COUCHDB_VERSION)
	@echo 'Done: apache-couchdb-$(COUCHDB_VERSION).tar.gz'


.PHONY: release
# target: release - Create an Erlang release including CouchDB!
release: all
	@echo 'Installing CouchDB into rel\couchdb\ ...'
	-@rmdir /s/q rel\couchdb >NUL 2>&1 || true
	@$(REBAR) generate
ifeq ($(with_spidermonkey), true)
	@mkdir rel\couchdb\share\server
	@copy src\couch\priv\couchjs.exe rel\couchdb\bin\couchjs.exe
	@copy share\server\main.js rel\couchdb\share\server\main.js
	@copy share\server\main-ast-bypass.js rel\couchdb\share\server\main-ast-bypass.js
	@copy share\server\main-coffee.js rel\couchdb\share\server\main-coffee.js
endif

ifeq ($(with_fauxton), true)
	-@mkdir rel\couchdb\share
	-@xcopy share\www rel\couchdb\share\www /E/I
endif

ifeq ($(with_docs), true)
	-@mkdir rel\couchdb\share\www\docs
	-@mkdir rel\couchdb\share\docs
ifeq ($(IN_RELEASE), true)
	@xcopy share\docs\html rel\couchdb\share\www\docs /E /I
	@copy share\docs\man\apachecouchdb.1 rel\couchdb\share\docs\couchdb.1
else
	@xcopy src\docs\build\html rel\couchdb\share\www\docs /E /I
	@copy src\docs\build\man\apachecouchdb.1 rel\couchdb\share\docs\couchdb.1
endif
endif

ifeq ($(with_nouveau), true)
	-@mkdir rel\couchdb\nouveau
	@cd extra\nouveau && $(GRADLE) installDist
	@xcopy extra\nouveau\build\install\nouveau rel\couchdb\nouveau /E /I
endif

	@echo ... done
	@echo .
	@echo     You can now copy the rel\couchdb directory anywhere on your system.
	@echo     Start CouchDB with .\bin\couchdb.cmd from within that directory.
	@echo .

.PHONY: install
# target: install - Install CouchDB :)
install: release
	@echo .
	@echo Notice: There is no 'make install' command for CouchDB 2.x+.
	@echo .
	@echo     To install CouchDB into your system, copy the rel\couchdb
	@echo     to your desired installation location. For example:
	@echo     xcopy /E rel\couchdb C:\CouchDB\
	@echo .

################################################################################
# Cleaning
################################################################################


.PHONY: clean
# target: clean - Remove build artifacts
clean:
	@$(REBAR) -r clean
	-@mix clean --deps || true
	-@rmdir /s/q .rebar >NUL 2>&1 || true
	-@del /f/q bin\couchjs.exe >NUL 2>&1 || true
	-@rmdir /s/q src\*\ebin >NUL 2>&1 || true
	-@rmdir /s/q src\*\.rebar >NUL 2>&1 || true
	-@del /f/q/s src\*.dll >NUL 2>&1 || true
	-@del /f/q src\couch\priv\*.exe >NUL 2>&1 || true
	-@del /f/q share\server\main.js share\server\main-ast-bypass.js share\server\main-coffee.js >NUL 2>&1 || true
	-@rmdir /s/q tmp >NUL 2>&1 || true
	-@rmdir /s/q dev\data >NUL 2>&1 || true
	-@rmdir /s/q dev\lib >NUL 2>&1 || true
	-@rmdir /s/q dev\logs >NUL 2>&1 || true
	-@rmdir /s/q src\mango\.venv >NUL 2>&1 || true
	-@del /f/q src\couch\priv\couch_js\config.h >NUL 2>&1 || true
	-@del /f/q dev\boot_node.beam dev\pbkdf2.pyc log\crash.log >NUL 2>&1 || true
	-@rmdir /s/q src\docs\build src\docs\.venv >NUL 2>&1 || true
ifeq ($(with_nouveau), true)
	@cd extra\nouveau && $(GRADLE) clean
endif

.PHONY: distclean
# target: distclean - Remove build and release artifacts
distclean: clean
	-@del install.mk >NUL 2>&1 || true
	-@del config.erl >NUL 2>&1 || true
	-@del rel\couchdb.config >NUL 2>&1 || true
	-@rmdir /s/q rel\couchdb >NUL 2>&1 || true
ifneq ($(IN_RELEASE), true)
    # when we are in a release, don’t delete Fauxton
	-@rmdir /s/q share\www >NUL 2>&1 || true
endif


.PHONY: devclean
# target: devclean - Remove dev cluster artifacts
devclean:
	-@rmdir /s/q dev\lib >NUL 2>&1 || true
	-@rmdir /s/q dev\logs >NUL 2>&1 || true
	-@del dev\devnode.boot >NUL 2>&1 || true
	-@del dev\devnode.rel >NUL 2>&1 || true
	-@del dev\devnode.script >NUL 2>&1 || true
	-@del dev\monitor_parent.beam >NUL 2>&1 || true


################################################################################
# Misc
################################################################################


.rebar: build-plt

config.erl:
	@echo Apache CouchDB has not been configured.
	@echo Try "powershell -ExecutionPolicy Bypass .\configure.ps1 -?" for help.
	@echo You probably want "powershell -ExecutionPolicy Bypass .\configure.ps1".
	@echo.
	@false


src\docs\build:
ifeq ($(with_docs), true)
	@echo 'Building docs...'
	@cd src\docs && setup.bat && make.bat html && make.bat man
endif


share\www:
ifeq ($(with_fauxton), true)
	@echo 'Building Fauxton'
	@cd src\fauxton && npm install && .\node_modules\.bin\grunt couchdb
endif

derived:
	@echo "COUCHDB_GIT_SHA:        $(COUCHDB_GIT_SHA)"
	@echo "COUCHDB_VERSION:        $(COUCHDB_VERSION)"
	@echo "COUCHDB_VERSION_SUFFIX: $(COUCHDB_VERSION_SUFFIX)"
	@echo "DIRTY:                  $(DIRTY)"
	@echo "IN_RC:                  $(IN_RC)"
	@echo "IN_RELEASE:             $(IN_RELEASE)"
	@echo "ON_TAG:                 $(ON_TAG)"
	@echo "REL_TAG:                $(REL_TAG)"
	@echo "SUB_VSN:                $(SUB_VSN)"


################################################################################
# Nouveau
################################################################################

.PHONY: extra/nouveau
# target: nouveau - Build nouveau
extra/nouveau:
ifeq ($(with_nouveau), true)
	@cd extra/nouveau && $(GRADLE) spotlessApply
	@cd extra/nouveau && $(GRADLE) build -x test
endif

.PHONY: nouveau-test
# target: nouveau-test - Run nouveau tests
nouveau-test: nouveau-test-gradle nouveau-test-elixir

.PHONY: nouveau-test-gradle
nouveau-test-gradle: couch-core extra/nouveau
ifeq ($(with_nouveau), true)
	@cd extra/nouveau && $(GRADLE) test --info --rerun
endif

.PHONY: nouveau-test-elixir
nouveau-test-elixir: export MIX_ENV=integration
nouveau-test-elixir: elixir-init devclean
nouveau-test-elixir: couch-core extra/nouveau
ifeq ($(with_nouveau), true)
	@dev\run "$(TEST_OPTS)" -n 1 -q -a adm:pass --with-nouveau \
		--locald-config test/elixir/test/config/test-config.ini \
		--no-eval 'mix test --trace --include test/elixir/test/config/nouveau.elixir'
endif
