all:

-include Makefile.local # for optional local options e.g. threads

# Recipes for this Makefile

## Build the compiler
##   $ make
## Build the compiler with progress output
##   $ make progress=1
## Clean up built files then build the compiler
##   $ make clean crystal
## Build the compiler in release mode
##   $ make crystal release=1 interpreter=1
## Run tests
##   $ make test
## Run stdlib tests
##   $ make std_spec
## Run compiler tests
##   $ make compiler_spec
## Run generators (Unicode, SSL config, ...)
##   $ make -B generate_data

CRYSTAL ?= crystal## which previous crystal compiler use

release ?=        ## Compile in release mode
stats ?=          ## Enable statistics output
progress ?=       ## Enable progress output
threads ?=        ## Maximum number of threads to use
debug ?=          ## Add symbolic debug info
verbose ?=        ## Run specs in verbose mode
junit_output ?=   ## Path to output junit results
static ?=         ## Enable static linking
target ?=         ## Cross-compilation target
interpreter ?=    ## Enable interpreter feature
check ?=          ## Enable only check when running format
order ?=random    ## Enable order for spec execution (values: "default" | "random" | seed number)
deref_symlinks ?= ## Dereference symbolic links for `make install`
docs_sanitizer ?= ## Enable sanitization for documentation generation
sequential_codegen ?=$(if $(filter 0,$(supports_preview_mt)),true,)## Enforce sequential codegen in compiler builds. Base compiler before Crystal 1.8 cannot build with `-Dpreview_mt -Dexecution_context`

O := .build
SOURCES := $(shell find src -name '*.cr')
SPEC_SOURCES := $(shell find spec -name '*.cr')
MAN1PAGES := $(patsubst doc/man/%.adoc,man/%.1,$(wildcard doc/man/*.adoc))
override FLAGS += -D strict_multi_assign -D preview_overload_order $(if $(release),--release )$(if $(stats),--stats )$(if $(progress),--progress )$(if $(threads),--threads $(threads) )$(if $(debug),-d )$(if $(static),--static )$(if $(LDFLAGS),--link-flags="$(LDFLAGS)" )$(if $(target),--cross-compile --target $(target) )
# NOTE: USE_PCRE1 is only used for testing compatibility with legacy environments that don't provide libpcre2.
# Newly built compilers should never be distributed with libpcre to ensure syntax consistency.
override COMPILER_FLAGS += $(if $(interpreter),,-Dwithout_interpreter )$(if $(docs_sanitizer),,-Dwithout_libxml2 ) -Dwithout_openssl -Dwithout_zlib$(if $(sequential_codegen),, -Dpreview_mt -Dexecution_context) $(if $(USE_PCRE1),-Duse_pcre,-Duse_pcre2)
SPEC_WARNINGS_OFF := --exclude-warnings spec/std --exclude-warnings spec/compiler --exclude-warnings spec/primitives --exclude-warnings src/float/printer --exclude-warnings src/random.cr
override SPEC_FLAGS += $(if $(verbose),-v )$(if $(junit_output),--junit_output $(junit_output) )$(if $(order),--order=$(order) )
CRYSTAL_CONFIG_LIBRARY_PATH := '$$ORIGIN/../lib/crystal'
ifndef CRYSTAL_CONFIG_BUILD_COMMIT
	CRYSTAL_CONFIG_BUILD_COMMIT := $(shell git rev-parse --short HEAD 2> /dev/null)
endif
CRYSTAL_CONFIG_PATH := '$$ORIGIN/../share/crystal/src'
ifndef BASE_CRYSTAL_VERSION
	BASE_CRYSTAL_VERSION := $(shell $(CRYSTAL) env CRYSTAL_VERSION)
endif
ifndef CRYSTAL_VERSION
	CRYSTAL_VERSION := $(shell cat src/VERSION)
endif
ifndef SOURCE_DATE_EPOCH
	SOURCE_DATE_EPOCH := $(shell (cat src/SOURCE_DATE_EPOCH || (git show -s --format=%ct HEAD || stat -c "%Y" Makefile ||stat -f "%m" Makefile)) 2> /dev/null)
endif
check_lld := command -v ld.lld >/dev/null && case "$$(uname -s)" in MINGW32*|MINGW64*|Linux) echo 1;; esac
ifeq ($(shell $(check_lld)),1)
  EXPORT_CC ?= CC="$(CC) -fuse-ld=lld"
endif
override EXPORTS += \
  CRYSTAL_CONFIG_BUILD_COMMIT="$(CRYSTAL_CONFIG_BUILD_COMMIT)" \
	CRYSTAL_CONFIG_PATH=$(CRYSTAL_CONFIG_PATH) \
	SOURCE_DATE_EPOCH="$(SOURCE_DATE_EPOCH)"
override EXPORTS_BUILD += \
	$(EXPORT_CC) \
	CRYSTAL_CONFIG_LIBRARY_PATH=$(CRYSTAL_CONFIG_LIBRARY_PATH)
SHELL = sh

ifeq ($(LLVM_VERSION),)
	ifndef LLVM_CONFIG
  	LLVM_CONFIG := $(shell src/llvm/ext/find-llvm-config.sh)
	endif
	LLVM_VERSION := $(if $(LLVM_CONFIG),$(shell "$(LLVM_CONFIG)" --version 2> /dev/null))
endif

# Crystal versions before 1.8 cannot build a functional compiler with `-Dpreview_mt` (https://github.com/crystal-lang/crystal/pull/16380)
supports_preview_mt := $(if $(filter 1.8.0,$(shell printf "%s\n%s" "1.8.0" "$(BASE_CRYSTAL_VERSION)" | sort -V | tail -n1)),0,1)

LLVM_EXT_DIR = src/llvm/ext
LLVM_EXT_OBJ = $(LLVM_EXT_DIR)/llvm_ext.o
CXXFLAGS += $(if $(debug),-g -O0)

# MSYS2 support (native Windows should use `Makefile.win` instead)
ifeq ($(OS),Windows_NT)
  EXE := .exe
  WINDOWS := 1
else
  EXE :=
  WINDOWS :=
endif
CRYSTAL_BIN := crystal$(EXE)

DESTDIR ?=
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin
LIBDIR ?= $(PREFIX)/lib
DATADIR ?= $(PREFIX)/share
DOCDIR ?= $(DATADIR)/doc/crystal
MANDIR ?= $(DATADIR)/man
INSTALL ?= /usr/bin/install

ifeq ($(or $(TERM),$(TERM),dumb),dumb)
  colorize = $(shell printf "%s" "$1" >&2)
else
  colorize = $(shell printf "\033[33m%s\033[0m\n" "$1" >&2)
endif

DEPS = $(LLVM_EXT_OBJ)
ifneq ($(LLVM_VERSION),)
  ifeq ($(shell test $(firstword $(subst ., ,$(LLVM_VERSION))) -ge 18; echo $$?),0)
    DEPS =
  endif
endif

check_llvm_config = $(eval \
	check_llvm_config := $(if $(LLVM_VERSION),\
		$(call colorize,Using $(or $(LLVM_CONFIG),externally configured LLVM) [version=$(LLVM_VERSION)]),\
		$(error "Could not locate compatible llvm-config, make sure it is installed and in your PATH, or set LLVM_VERSION / LLVM_CONFIG. Compatible versions: $(shell cat src/llvm/ext/llvm-versions.txt)))\
	)

.PHONY: all
all: crystal ## Build all files (currently crystal only) [default]

.PHONY: test
test: spec ## Run tests

.PHONY: spec
spec: std_spec primitives_spec compiler_spec

.PHONY: std_spec
std_spec: $(O)/std_spec$(EXE) ## Run standard library specs
	$(O)/std_spec$(EXE) $(SPEC_FLAGS)

.PHONY: compiler_spec
compiler_spec: $(O)/compiler_spec$(EXE) ## Run compiler specs
	$(O)/compiler_spec$(EXE) $(SPEC_FLAGS)

.PHONY: primitives_spec
primitives_spec: $(O)/primitives_spec$(EXE) ## Run primitives specs
	$(O)/primitives_spec$(EXE) $(SPEC_FLAGS)

.PHONY: interpreter_spec
interpreter_spec: $(O)/interpreter_spec$(EXE) ## Run interpreter specs
	$(O)/interpreter_spec$(EXE) $(SPEC_FLAGS)

.PHONY: simple_smoke_test
simple_smoke_test: ## Build std specs as a smoke test
simple_smoke_test: $(O)/std_spec$(EXE)

.PHONY: smoke_test
smoke_test: ## Build std specs, compiler specs and compiler as a smoke test
smoke_test: $(O)/std_spec$(EXE) $(O)/compiler_spec$(EXE) $(O)/$(CRYSTAL_BIN)

SHELLCHECK_SOURCES := $(wildcard **/*.sh) $(wildcard **/*.bash) bin/crystal bin/ci bin/check-compiler-flag scripts/git/pre-commit

.PHONY: lint-shellcheck
lint-shellcheck:
	shellcheck --severity=warning $(SHELLCHECK_SOURCES)

.PHONY: all_spec
all_spec: $(O)/all_spec$(EXE) ## Run all specs (note: this builds a huge program; `test` recipe builds individual binaries and is recommended for reduced resource usage)
	$(O)/all_spec$(EXE) $(SPEC_FLAGS)

.PHONY: samples
samples: ## Build example programs
	$(MAKE) -C samples

.PHONY: docs
docs: ## Generate standard library documentation
	$(call check_llvm_config)
	./bin/crystal docs src/docs_main.cr $(DOCS_OPTIONS) --project-name=Crystal --project-version=$(CRYSTAL_VERSION) --source-refname=$(CRYSTAL_CONFIG_BUILD_COMMIT)
	cp -R -P -p doc/ docs/

.PHONY: crystal
crystal: $(O)/$(CRYSTAL_BIN) ## Build the compiler

.PHONY: deps llvm_ext
deps: $(DEPS) ## Build dependencies
llvm_ext: $(LLVM_EXT_OBJ)

.PHONY: format
format: ## Format sources
	./bin/crystal tool format$(if $(check), --check) src spec samples scripts

.PHONY: generate_data
generate_data: ## Run generator scripts for Unicode, SSL config, ...
	$(MAKE) -B -f scripts/generate_data.mk

.PHONY: install
install: $(O)/$(CRYSTAL_BIN) man/crystal.1.gz ## Install the compiler at DESTDIR
	$(INSTALL) -d -m 0755 "$(DESTDIR)$(BINDIR)/"
	$(INSTALL) -m 0755 "$(O)/$(CRYSTAL_BIN)" "$(DESTDIR)$(BINDIR)/$(CRYSTAL_BIN)"

	$(INSTALL) -d -m 0755 $(DESTDIR)$(DATADIR)/crystal
	cp -R -p $(if $(deref_symlinks),-L,-P) src "$(DESTDIR)$(DATADIR)/crystal/src"
	rm -rf "$(DESTDIR)$(DATADIR)/crystal/$(LLVM_EXT_OBJ)" # Don't install llvm_ext.o

	$(INSTALL) -d -m 0755 "$(DESTDIR)$(MANDIR)/man1/"
	$(INSTALL) -m 644 man/crystal.1.gz "$(DESTDIR)$(MANDIR)/man1/crystal.1.gz"
	$(INSTALL) -d -m 0755 "$(DESTDIR)$(DATADIR)/licenses/crystal/"
	$(INSTALL) -m 644 LICENSE "$(DESTDIR)$(DATADIR)/licenses/crystal/LICENSE"

	$(INSTALL) -d -m 0755 "$(DESTDIR)$(DATADIR)/bash-completion/completions/"
	$(INSTALL) -m 644 etc/completion.bash "$(DESTDIR)$(DATADIR)/bash-completion/completions/crystal"
	$(INSTALL) -d -m 0755 "$(DESTDIR)$(DATADIR)/zsh/site-functions/"
	$(INSTALL) -m 644 etc/completion.zsh "$(DESTDIR)$(DATADIR)/zsh/site-functions/_crystal"
	$(INSTALL) -d -m 0755 "$(DESTDIR)$(DATADIR)/fish/vendor_completions.d/"
	$(INSTALL) -m 644 etc/completion.fish "$(DESTDIR)$(DATADIR)/fish/vendor_completions.d/crystal.fish"

ifeq ($(WINDOWS),1)
.PHONY: install_dlls
install_dlls: $(O)/$(CRYSTAL_BIN) ## Install the compiler's dependent DLLs at DESTDIR (Windows only)
	$(INSTALL) -d -m 0755 "$(DESTDIR)$(BINDIR)/"
	@ldd $(O)/$(CRYSTAL_BIN) | grep -iv ' => /c/windows/system32' | sed 's/.* => //; s/ (.*//' | xargs -t -i $(INSTALL) -m 0755 '{}' "$(DESTDIR)$(BINDIR)/"
endif

.PHONY: uninstall
uninstall: ## Uninstall the compiler from DESTDIR
	rm -f "$(DESTDIR)$(BINDIR)/$(CRYSTAL_BIN)"

	rm -rf "$(DESTDIR)$(DATADIR)/crystal/src"

	rm -f "$(DESTDIR)$(MANDIR)/man1/crystal.1.gz"
	rm -f "$(DESTDIR)$(DATADIR)/licenses/crystal/LICENSE"

	rm -f "$(DESTDIR)$(DATADIR)/bash-completion/completions/crystal"
	rm -f "$(DESTDIR)$(DATADIR)/zsh/site-functions/_crystal"
	rm -f "$(DESTDIR)$(DATADIR)/fish/vendor_completions.d/crystal.fish"

.PHONY: install_docs
install_docs: docs ## Install docs at DESTDIR
	$(INSTALL) -d -m 0755 $(DESTDIR)$(DOCDIR)

	cp -R -P -p docs "$(DESTDIR)$(DOCDIR)/docs"
	cp -R -P -p samples "$(DESTDIR)$(DOCDIR)/examples"

.PHONY: uninstall_docs
uninstall_docs: ## Uninstall docs from DESTDIR
	rm -rf "$(DESTDIR)$(DOCDIR)/docs"
	rm -rf "$(DESTDIR)$(DOCDIR)/examples"

$(O)/all_spec$(EXE): $(DEPS) $(SOURCES) $(SPEC_SOURCES)
	$(call check_llvm_config)
	@mkdir -p $(O)
	$(EXPORT_CC) $(EXPORTS) ./bin/crystal build $(FLAGS) $(SPEC_WARNINGS_OFF) -o $@ spec/all_spec.cr

$(O)/std_spec$(EXE): $(DEPS) $(SOURCES) $(SPEC_SOURCES)
	$(call check_llvm_config)
	@mkdir -p $(O)
	$(EXPORT_CC) ./bin/crystal build $(FLAGS) $(SPEC_WARNINGS_OFF) -o $@ spec/std_spec.cr

$(O)/compiler_spec$(EXE): $(DEPS) $(SOURCES) $(SPEC_SOURCES)
	$(call check_llvm_config)
	@mkdir -p $(O)
	$(EXPORT_CC) $(EXPORTS) ./bin/crystal build $(FLAGS) $(COMPILER_FLAGS) $(SPEC_WARNINGS_OFF) -o $@ spec/compiler_spec.cr --release

$(O)/primitives_spec$(EXE): $(O)/$(CRYSTAL_BIN) $(DEPS) $(SOURCES) $(SPEC_SOURCES)
	@mkdir -p $(O)
	$(EXPORT_CC) ./bin/crystal build $(FLAGS) $(SPEC_WARNINGS_OFF) -o $@ spec/primitives_spec.cr

$(O)/interpreter_spec$(EXE): $(DEPS) $(SOURCES) $(SPEC_SOURCES)
	$(eval interpreter=1)
	@mkdir -p $(O)
	$(EXPORT_CC) ./bin/crystal build $(FLAGS) $(COMPILER_FLAGS) $(SPEC_WARNINGS_OFF) -o $@ spec/compiler/interpreter_spec.cr

$(O)/$(CRYSTAL_BIN): $(DEPS) $(SOURCES)
	$(call check_llvm_config)
	@mkdir -p $(O)
	$(EXPORTS) $(EXPORTS_BUILD) ./bin/crystal build $(FLAGS) $(COMPILER_FLAGS) -o $(if $(WINDOWS),$(O)/crystal-next.exe,$@) src/compiler/crystal.cr
	@# NOTE: on MSYS2 it is not possible to overwrite a running program, so the compiler must be first built with
	@# a different filename and then moved to the final destination.
	$(if $(WINDOWS),mv $(O)/crystal-next.exe $@)

$(LLVM_EXT_OBJ): $(LLVM_EXT_DIR)/llvm_ext.cc
	$(call check_llvm_config)
	$(CXX) -c $(CXXFLAGS) -o $@ $< $(if $(LLVM_CONFIG),$(shell $(LLVM_CONFIG) --cxxflags))

man/: $(MAN1PAGES)

man/%.gz: man/%
	gzip -c -9 $< > $@

man/%.1: doc/man/%.adoc
	SOURCE_DATE_EPOCH=$(SOURCE_DATE_EPOCH) asciidoctor -a crystal_version=$(CRYSTAL_VERSION) $< -b manpage -o $@

.PHONY: clean
clean: clean_crystal ## Clean up built directories and files
	rm -rf $(LLVM_EXT_OBJ)
	rm -rf man/*.gz

.PHONY: clean_crystal
clean_crystal: ## Clean up crystal built files
	rm -rf $(O)
	rm -rf ./docs

.PHONY: clean_cache
clean_cache: ## Clean up CRYSTAL_CACHE_DIR files
	rm -rf $(shell ./bin/crystal env CRYSTAL_CACHE_DIR)

.PHONY: help
help: ## Show this help
	@echo
	@printf '\033[34mtargets:\033[0m\n'
	@grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) |\
		sort |\
		awk 'BEGIN {FS = ":.*?## "}; {printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2}'
	@echo
	@printf '\033[34moptional variables:\033[0m\n'
	@grep -hE '^[a-zA-Z_-]+ \?=.*?## .*$$' $(MAKEFILE_LIST) |\
		sort |\
		awk 'BEGIN {FS = " \\?=.*?## "}; {printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2}'
	@echo
	@printf '\033[34mrecipes:\033[0m\n'
	@grep -hE '^##.*$$' $(MAKEFILE_LIST) |\
		awk 'BEGIN {FS = "## "}; /^## [a-zA-Z_-]/ {printf "  \033[36m%s\033[0m\n", $$2}; /^##  / {printf "  %s\n", $$2}'
