Current section
Files
Jump to
Current section
Files
Makefile
# Makefile for ExVrp NIF compilation
#
# This Makefile is used by elixir_make to compile the C++ NIF.
# Erlang paths
ERTS_INCLUDE_DIR ?= $(shell erl -noshell -eval "io:format(\"~ts/erts-~ts/include/\", [code:root_dir(), erlang:system_info(version)])." -s init stop)
ERL_INTERFACE_INCLUDE_DIR ?= $(shell erl -noshell -eval "io:format(\"~ts\", [code:lib_dir(erl_interface, include)])." -s init stop)
ERL_INTERFACE_LIB_DIR ?= $(shell erl -noshell -eval "io:format(\"~ts\", [code:lib_dir(erl_interface, lib)])." -s init stop)
# Platform detection
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
LDFLAGS += -dynamiclib -undefined dynamic_lookup
SO_EXT = so
else
LDFLAGS += -shared
SO_EXT = so
endif
# Compiler settings - use clang++ for sanitizers, otherwise default to g++
ifdef SANITIZE
CXX = clang++
else
CXX ?= g++
endif
# Base compiler flags
CXXFLAGS = -std=c++20 -Wall -Wextra -fPIC -fvisibility=hidden
CXXFLAGS += -I$(ERTS_INCLUDE_DIR)
CXXFLAGS += -Ic_src
CXXFLAGS += -Ic_src/ex_vrp
# Sanitizer support (set SANITIZE=1 to enable)
# Use with: task test:asan
ifdef SANITIZE
# Debug build with sanitizers - no LTO, keep frame pointers
CXXFLAGS += -O1 -g -fno-omit-frame-pointer -fno-optimize-sibling-calls
CXXFLAGS += -fsanitize=address,undefined
CXXFLAGS += -fno-sanitize-recover=all
LDFLAGS += -fsanitize=address,undefined
# ASan requires shared runtime for NIFs
ifeq ($(UNAME_S),Linux)
LDFLAGS += -shared-libasan
endif
else
# Release build: optimized with LTO, disable assertions
CXXFLAGS += -O3 -flto -DNDEBUG
LDFLAGS += -flto
# Disable dangling-reference warning from PyVRP's Route.h (false positive in GCC 14)
# Only add for GCC, Clang doesn't have this warning
ifneq (,$(findstring g++,$(shell $(CXX) --version)))
CXXFLAGS += -Wno-dangling-reference
endif
endif
# Fine includes (use -isystem for angle-bracket includes like <fine.hpp>)
ifdef FINE_INCLUDE_DIR
CXXFLAGS += -isystem $(FINE_INCLUDE_DIR)
endif
# Output - use MIX_APP_PATH/priv when available (elixir_make sets this),
# fall back to local priv/ for standalone builds
ifdef MIX_APP_PATH
PRIV_DIR = $(MIX_APP_PATH)/priv
else
PRIV_DIR = priv
endif
NIF_SO = $(PRIV_DIR)/ex_vrp_nif.$(SO_EXT)
# Source files - NIF bindings
NIF_SRC = c_src/ex_vrp_nif.cpp
# PyVRP core sources (from latest main branch)
PYVRP_CORE_SRC = \
c_src/ex_vrp/CostEvaluator.cpp \
c_src/ex_vrp/DurationSegment.cpp \
c_src/ex_vrp/DynamicBitset.cpp \
c_src/ex_vrp/LoadSegment.cpp \
c_src/ex_vrp/ProblemData.cpp \
c_src/ex_vrp/RandomNumberGenerator.cpp \
c_src/ex_vrp/Route.cpp \
c_src/ex_vrp/Solution.cpp \
c_src/ex_vrp/Trip.cpp
# PyVRP search sources
PYVRP_SEARCH_SRC = \
c_src/ex_vrp/search/LocalSearch.cpp \
c_src/ex_vrp/search/PerturbationManager.cpp \
c_src/ex_vrp/search/RelocateWithDepot.cpp \
c_src/ex_vrp/search/Route.cpp \
c_src/ex_vrp/search/SearchSpace.cpp \
c_src/ex_vrp/search/Solution.cpp \
c_src/ex_vrp/search/SwapRoutes.cpp \
c_src/ex_vrp/search/SwapTails.cpp \
c_src/ex_vrp/search/primitives.cpp
ALL_SRC = $(NIF_SRC) $(PYVRP_CORE_SRC) $(PYVRP_SEARCH_SRC)
# Object files - follow PRIV_DIR into MIX_APP_PATH when there is one. A fixed
# c_src/obj is shared by every build of this source tree, and the toolchain
# stamp that lives in it clears the whole directory whenever the hash differs.
# So the repo's own build and a consumer depending on it by path delete each
# other's objects and fail mid-link. One object directory per app path.
ifdef MIX_APP_PATH
OBJ_DIR = $(MIX_APP_PATH)/obj
else
OBJ_DIR = c_src/obj
endif
OBJS = $(patsubst c_src/%.cpp,$(OBJ_DIR)/%.o,$(ALL_SRC))
# Build fingerprint: force a full rebuild when the compiler, its system
# libraries, or the compile flags change. Without the first two, a devenv/nix
# update can swap the compiler or glibc under us, leaving stale .o files that
# link against the wrong libraries and silently degrade NIF performance by ~2x.
# The flags matter for the same reason: a SANITIZE=1 build would otherwise
# reuse the normal build's -flto objects, which are LLVM bitcode that the
# sanitizer link (no -flto) cannot read — "file format not recognized".
BUILD_ID := $(shell $(CXX) --version | head -1)$(shell $(CXX) -print-file-name=libc.so)$(CXXFLAGS)
SHASUM := $(shell command -v shasum 2>/dev/null || command -v sha1sum 2>/dev/null || echo "md5sum")
BUILD_HASH := $(shell echo "$(BUILD_ID)" | $(SHASUM) | cut -c1-16)
BUILD_STAMP = $(OBJ_DIR)/.build_$(BUILD_HASH)
all: $(PRIV_DIR) $(BUILD_STAMP) $(NIF_SO)
$(PRIV_DIR):
mkdir -p $(PRIV_DIR)
$(BUILD_STAMP):
@rm -rf $(OBJ_DIR)
@mkdir -p $(OBJ_DIR)/ex_vrp/search
@touch $@
# Header files — ALL objects depend on ALL headers so that any header
# change triggers a full rebuild. This is conservative but safe; the
# alternative (gcc -MMD dependency tracking) adds complexity and the
# full rebuild takes <10s.
HEADERS = $(wildcard c_src/*.h c_src/ex_vrp/*.h c_src/ex_vrp/search/*.h)
# Object files depend on the build stamp via order-only prerequisite
# to prevent parallel make from compiling while the stamp rule cleans obj/
$(OBJ_DIR)/%.o: c_src/%.cpp $(HEADERS) | $(BUILD_STAMP)
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c -o $@ $<
$(NIF_SO): $(OBJS)
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $^
# Standalone test binary for running under valgrind (no BEAM/NIF needed).
# Usage: make test-solver && valgrind --error-exitcode=1 ./solver_test
TEST_CXXFLAGS = -std=c++20 -O1 -g -Ic_src -Ic_src/ex_vrp
TEST_PYVRP_SRC = $(PYVRP_CORE_SRC) $(PYVRP_SEARCH_SRC)
test-solver: c_src/solver_test.cpp $(TEST_PYVRP_SRC) $(HEADERS)
$(CXX) $(TEST_CXXFLAGS) c_src/solver_test.cpp $(TEST_PYVRP_SRC) -o solver_test -lm
@echo "Built solver_test. Run: ./solver_test"
@echo "Under valgrind: valgrind --error-exitcode=1 ./solver_test"
clean:
rm -rf $(PRIV_DIR)/*.$(SO_EXT)
rm -rf $(OBJ_DIR)
rm -f solver_test
.PHONY: all clean test-solver