Ctrl K
ComunidadGuías…

Makefile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Makefile for Jan Electron App - Build, Lint, Test, and Clean

REPORT_PORTAL_URL ?= ""
REPORT_PORTAL_API_KEY ?= ""
REPORT_PORTAL_PROJECT_NAME ?= ""
REPORT_PORTAL_LAUNCH_NAME ?= "Jan App"
REPORT_PORTAL_DESCRIPTION ?= "Jan App report"

# Detect OS
ifeq ($(OS),Windows_NT)
    DETECTED_OS := Windows
else
    DETECTED_OS := $(shell uname -s)
endif

# On Windows make runs recipes through cmd.exe -- unless an sh.exe is on PATH,
# which is every Git Bash and MSYS invocation, CI included. The two share no
# spelling for mkdir or for a one-off environment variable, and the OS alone
# cannot tell them apart, so the shell make picked is what the Windows-only
# recipes have to branch on.
ifeq ($(OS),Windows_NT)
    RECIPE_SHELL_IS_CMD := $(if $(filter sh sh.exe bash bash.exe,$(notdir $(SHELL))),,yes)
else
    RECIPE_SHELL_IS_CMD :=
endif

ifeq ($(RECIPE_SHELL_IS_CMD),yes)
    MKDIR = if not exist "$(1)" mkdir "$(1)"
else
    MKDIR = mkdir -p $(1)
endif

# Default target, does nothing
all:
	@echo "Specify a target to run"

# Installs yarn dependencies and builds core and extensions
install-and-build:
ifeq ($(DETECTED_OS),Windows)
	echo "skip"
else ifeq ($(DETECTED_OS),Linux)
	chmod +x src-tauri/build-utils/*
endif
	yarn install
	yarn build:tauri:plugin:api
	yarn build:core
	yarn build:extensions

# Install required Rust targets for macOS universal builds
install-rust-targets:
ifeq ($(DETECTED_OS),Darwin)
	@echo "Detected macOS, installing universal build targets..."
	rustup target add x86_64-apple-darwin
	rustup target add aarch64-apple-darwin
	@echo "Rust targets installed successfully!"
else
	@echo "Not macOS; skipping Rust target installation."
endif

# Install required Rust targets for Android builds
install-android-rust-targets:
	@echo "Checking and installing Android Rust targets..."
	@rustup target list --installed | grep -q "aarch64-linux-android" || rustup target add aarch64-linux-android
	@rustup target list --installed | grep -q "armv7-linux-androideabi" || rustup target add armv7-linux-androideabi
	@rustup target list --installed | grep -q "i686-linux-android" || rustup target add i686-linux-android
	@rustup target list --installed | grep -q "x86_64-linux-android" || rustup target add x86_64-linux-android
	@echo "Android Rust targets ready!"

# Install required Rust targets for iOS builds
install-ios-rust-targets:
	@echo "Checking and installing iOS Rust targets..."
	@rustup target list --installed | grep -q "aarch64-apple-ios" || rustup target add aarch64-apple-ios
	@rustup target list --installed | grep -q "aarch64-apple-ios-sim" || rustup target add aarch64-apple-ios-sim
	@rustup target list --installed | grep -q "x86_64-apple-ios" || rustup target add x86_64-apple-ios
	@echo "iOS Rust targets ready!"

dev: install-and-build
	yarn download:bin
	$(MAKE) build-mlx-server-if-exists
	$(MAKE) build-engine-dev-if-possible
	yarn dev

# Web application targets
install-web-app:
	yarn install

dev-web-app: install-web-app
	yarn build:core
	yarn dev:web

build-web-app: install-web-app
	yarn build:core
	yarn build:web

serve-web-app:
	yarn workspace @janhq/web-app preview

build-serve-web-app: build-web-app
	yarn workspace @janhq/web-app preview

# Mobile
dev-android: install-and-build install-android-rust-targets
	@echo "Setting up Android development environment..."
	@if [ ! -d "src-tauri/gen/android" ]; then \
		echo "Android app not initialized. Initializing..."; \
		yarn tauri android init; \
	fi
	@echo "Sourcing Android environment setup..."
	@bash autoqa/scripts/setup-android-env.sh echo "Android environment ready"
	@echo "Starting Android development server..."
	yarn dev:android

dev-ios: install-and-build install-ios-rust-targets
	@echo "Setting up iOS development environment..."
ifeq ($(DETECTED_OS),Darwin)
	@if [ ! -d "src-tauri/gen/ios" ]; then \
		echo "iOS app not initialized. Initializing..."; \
		yarn tauri ios init; \
	fi
	@echo "Checking iOS development requirements..."
	@xcrun --version > /dev/null 2>&1 || (echo "❌ Xcode command line tools not found. Install with: xcode-select --install" && exit 1)
	@xcrun simctl list devices available | grep -q "iPhone\|iPad" || (echo "❌ No iOS simulators found. Install simulators through Xcode." && exit 1)
	@echo "Starting iOS development server..."
	yarn dev:ios
else
	@echo "❌ iOS development is only supported on macOS"
	@exit 1
endif

# Linting
lint: install-and-build
	yarn lint

# Testing
#
# `test` is the full local suite: it also builds the real MLX server and CLI
# binary. `test-ci` runs the same suites without those release builds -- neither
# is a declared Tauri resource or externalBin (bundle.resources is only
# resources/LICENSE) and no test executes them, so in CI they were just
# duplicating what `make build` already does on the release path.
#
# Both go through `test-rust`, which owns `stub-resources`: the app's build
# script fails on any missing bundle.resources path, and neither entry point
# builds the engine worker or the ggml backends.
test-prepare: test-ts rust-test-assets

# The two halves of test-ci. CI runs them as separate jobs so a pull request
# runs only the half its files can break, and the TypeScript half on one OS.
test-ts: lint
	yarn test

# The steps test-ci ran before test-rust, without the lint/vitest half. They
# need node_modules (download:bin unpacks with it, build:icon is the Tauri CLI),
# which lint's install-and-build used to provide.
rust-test-assets:
	yarn install
	yarn download:bin
	yarn copy:assets:tauri
	yarn build:icon

test-ci-rust: rust-test-assets
	$(MAKE) test-rust

test-rust: stub-resources
	cargo test --locked --manifest-path src-tauri/Cargo.toml --no-default-features --features test-tauri -- --test-threads=1
	cargo test --locked --manifest-path src-tauri/plugins/tauri-plugin-hardware/Cargo.toml
	cargo test --locked --manifest-path src-tauri/plugins/tauri-plugin-extensions/Cargo.toml
	cargo test --locked --manifest-path src-tauri/plugins/tauri-plugin-llamacpp/Cargo.toml
	cargo test --locked --manifest-path src-tauri/utils/Cargo.toml
	# No --locked: this crate gitignores its Cargo.lock, as rust-check.yml assumes.
	# The unit tests that run the shell for real are ignored on Windows, where
	# sandbox_spawn covers them (it needs its own main to host the helper re-exec).
	cargo test --manifest-path src-tauri/plugins/tauri-plugin-agent-tools/Cargo.toml --no-default-features --lib --test sandbox_spawn

# protocol/schema.json is committed, and core::cli::protocol_schema fails when it
# no longer matches the types that define the channel. This is the fix for that
# failure, not an optional extra: change a protocol type, run this, commit both.
# `cd` rather than `--manifest-path` because jan-cli is its own workspace and its
# .cargo/config.toml is what points the build at src-tauri/target. The feature
# set matches build-cli, so this does not compile a second configuration.
protocol-schema:
	cd src-tauri/jan-cli && cargo run --quiet --locked --no-default-features --features cli --bin jan -- cli agent schema --out ../../protocol/schema.json

# The same guard for the RPC surface: protocol/rpc-schema.json is committed, and
# core::cli::rpc_schema fails when it no longer matches the types that define the
# envelope. It covers this surface only - stream-json records are in
# protocol/schema.json, and ACP (`jan acp`) uses the upstream
# ACP schema.
protocol-rpc-schema:
	cd src-tauri/jan-cli && cargo run --quiet --locked --no-default-features --features cli --bin jan -- cli agent rpc-schema --out ../../protocol/rpc-schema.json

test: test-prepare install-rust-targets
	yarn build:mlx-server
	$(MAKE) build-cli
	$(MAKE) test-rust

# Placeholders for the binaries no test target builds. Each platform's
# tauri.<os>.conf.json declares these under bundle.resources, and
# generate_context!() fails the build script if a declared path is missing --
# but it only checks existence, and no test executes them. Every stub is guarded
# so we never clobber a real local build or churn the cargo:rerun-if-changed
# stamps these paths emit.
#
# scripts/stub-tauri-resources.mjs is the one implementation, shared with the
# coverage and rust-check workflows; keeping a second copy here is how the
# engine worker ended up stubbed in one place and not the other. It is Node and
# not shell so this recipe needs no cmd.exe arm -- `node` is spelled the same
# whichever shell make picked, and a stock Windows box has no `sh` on PATH.
stub-resources:
	@node scripts/stub-tauri-resources.mjs

test-ci: test-prepare
	$(MAKE) test-rust

# End-to-end smoke install of the sample extension: packs it, installs the
# tarball with the extensions plugin (tests/smoke.rs, ignored by default) and
# loads the result in the web-app's extension host (smoke.installed.test.ts).
test-extensions-smoke:
	node scripts/extensions-smoke.mjs

# Compile and test guard for the CLI feature set. The `jan` CLI is no longer
# bundled with the app; `make test` still builds the real binary via build-cli.
#
# jan-cli is a standalone crate and not a dependency of src-tauri, so the
# rust-check `cli` app entry never reaches it (that entry's --all-targets covers
# app_lib's own cfg(feature = "cli") test modules, a different crate). This
# target runs its tests; the CI lint guarantee is the jan-cli entry of the
# rust-check `crates` matrix, which `lint-cli` reproduces locally. A bare
# `cargo check` here once built the bin but not the #[cfg(test)] module, which
# is how the fixture in `compact_plugin_list_omits_long_metadata` silently rotted.
check-cli:
	cd src-tauri/jan-cli && cargo test --locked --no-default-features --features cli

# The same clippy invocation as the rust-check `crates` matrix jan-cli entry.
# Kept out of check-cli so the linter workflow does not lint it a second time.
lint-cli:
	cd src-tauri/jan-cli && cargo clippy --locked --no-default-features --features cli --all-targets -- -D warnings

# Build MLX server (macOS Apple Silicon only) - always builds, unless
# JAN_MLX_PREBUILT_DIR holds a cached build of the same inputs
# (build-utils/mlx-prebuilt.sh), which is then staged and signed the same way
build-mlx-server:
ifeq ($(DETECTED_OS),Darwin)
ifeq ($(JAN_MLX_PREBUILT_DIR),)
	@echo "Building MLX server for Apple Silicon..."
	# mlx-swift's Metal shaders are compiled by the PrepareMetalShaders
	# plugin, which only runs under Xcode -- `swift build` produces a
	# binary with no default.metallib and the app fails at runtime. See
	# https://github.com/ml-explore/mlx-swift README ("SwiftPM (command
	# line) cannot build the Metal shaders").
	cd mlx-server && xcodebuild build -scheme mlx-server -destination 'platform=OS X' -configuration Release OTHER_LDFLAGS="-dead_strip"
endif
	@echo "Finding build products..."
	@DERIVED_DATA=$${JAN_MLX_PREBUILT_DIR:-$$(find ~/Library/Developer/Xcode/DerivedData/mlx-server-*/Build/Products/Release -maxdepth 0 2>/dev/null | head -1)}; \
	if [ -z "$$DERIVED_DATA" ] || [ ! -f "$$DERIVED_DATA/mlx-server" ]; then \
		echo "Error: Could not find xcodebuild products under DerivedData"; \
		exit 1; \
	fi; \
	METALLIB=$$(find "$$DERIVED_DATA/mlx-swift_Cmlx.bundle" -name 'default.metallib' -print -quit 2>/dev/null); \
	if [ -z "$$METALLIB" ]; then \
		echo "Error: default.metallib missing under $$DERIVED_DATA/mlx-swift_Cmlx.bundle -- PrepareMetalShaders did not run"; \
		find "$$DERIVED_DATA/mlx-swift_Cmlx.bundle" -maxdepth 4 2>/dev/null; \
		exit 1; \
	fi; \
	mkdir -p src-tauri/resources/bin; \
	echo "Copying mlx-server from $$DERIVED_DATA..."; \
	cp "$$DERIVED_DATA/mlx-server" src-tauri/resources/bin/mlx-server; \
	rm -rf src-tauri/resources/bin/mlx-swift_Cmlx.bundle; \
	cp -r "$$DERIVED_DATA/mlx-swift_Cmlx.bundle" src-tauri/resources/bin/; \
	chmod +x src-tauri/resources/bin/mlx-server; \
	echo "MLX server built and copied successfully"; \
	echo "Checking for code signing identity..."; \
	SIGNING_IDENTITY=$$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | sed 's/.*"\(.*\)".*/\1/'); \
	if [ -n "$$SIGNING_IDENTITY" ]; then \
		echo "Signing mlx-server with identity: $$SIGNING_IDENTITY"; \
		codesign --force --options runtime --timestamp --sign "$$SIGNING_IDENTITY" src-tauri/resources/bin/mlx-server; \
		if ! find src-tauri/resources/bin/mlx-swift_Cmlx.bundle -name 'default.metallib' -print -quit 2>/dev/null | grep -q .; then \
			echo "Error: staged mlx-swift_Cmlx.bundle is missing default.metallib; refusing to sign an empty bundle"; \
			exit 1; \
		fi; \
		echo "Signing mlx-swift_Cmlx.bundle..."; \
		codesign --force --options runtime --timestamp --sign "$$SIGNING_IDENTITY" --deep src-tauri/resources/bin/mlx-swift_Cmlx.bundle; \
		echo "Code signing completed successfully"; \
	else \
		echo "Warning: No Developer ID Application identity found. Skipping code signing (notarization will fail)."; \
	fi
else
	@echo "Skipping MLX server build (macOS only)"
endif

# Build MLX server only if not already present (for dev)
build-mlx-server-if-exists:
ifeq ($(DETECTED_OS),Darwin)
	@if [ -f "src-tauri/resources/bin/mlx-server" ]; then \
		echo "MLX server already exists at src-tauri/resources/bin/mlx-server, skipping build..."; \
	else \
		make build-mlx-server; \
	fi
else
	@echo "Skipping MLX server build (macOS only)"
endif

# Build jan CLI (release, platform-aware) → src-tauri/resources/bin/jan[.exe]
build-cli:
ifeq ($(DETECTED_OS),Darwin)
	cd src-tauri/jan-cli && cargo build --release --no-default-features --features cli --target aarch64-apple-darwin
	cd src-tauri/jan-cli && cargo build --release --no-default-features --features cli --target x86_64-apple-darwin
	lipo -create \
		src-tauri/target/aarch64-apple-darwin/release/jan \
		src-tauri/target/x86_64-apple-darwin/release/jan \
		-output src-tauri/resources/bin/jan
	chmod +x src-tauri/resources/bin/jan
	$(call MKDIR,'src-tauri/target/universal-apple-darwin/release')

	echo "Checking for code signing identity..."; \
	SIGNING_IDENTITY=$$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | sed 's/.*"\(.*\)".*/\1/'); \
	if [ -n "$$SIGNING_IDENTITY" ]; then \
		echo "Signing jan with identity: $$SIGNING_IDENTITY"; \
		codesign --force --options runtime --timestamp --sign "$$SIGNING_IDENTITY" src-tauri/resources/bin/jan; \
		echo "Code signing completed successfully"; \
	else \
		echo "Warning: No Developer ID Application identity found. Skipping code signing (notarization will fail)."; \
	fi

	cp src-tauri/resources/bin/jan src-tauri/target/universal-apple-darwin/release/jan
else ifeq ($(DETECTED_OS),Windows)
	cd src-tauri/jan-cli && cargo build --release --no-default-features --features cli
	cp src-tauri/target/release/jan.exe src-tauri/resources/bin/jan.exe
else
	cd src-tauri/jan-cli && cargo build --release --no-default-features --features cli
	cp src-tauri/target/release/jan src-tauri/resources/bin/jan
endif

# ---------------------------------------------------------------------------
# llama.cpp engine worker
#
# JAN_ENGINE_VARIANT picks the GPU backends compiled into the worker. Vulkan is
# in every variant, not just the default: it is the fallback when the CUDA or
# ROCm runtime turns out to be missing or too old on the user's machine, and
# ggml chooses between the registered backends at runtime.
#
# The CPU backend is always multi-versioned (GGML_CPU_ALL_VARIANTS), so every
# variant ships the full set of x86-64/ARM microarchitecture modules and ggml
# scores them at load time. That is what "common CPUs" means here.
#
# The variant is a `-` separated list of backends, so a shipping build names
# every backend it carries and one worker covers the machine it lands on:
#
#   vulkan  (default)  vulkan
#   cuda13             cuda + vulkan   (needs CUDA 13 toolkit on PATH)
#   cuda12             cuda + vulkan   (needs CUDA 12 toolkit on PATH)
#   rocm               hip  + vulkan   (needs ROCm/HIP)
#   cuda13-hip-vulkan  all three       (needs the CUDA and the HIP toolkit)
#
# cuda12 and cuda13 cannot be combined: the CUDA major is whichever nvcc is on
# PATH, one toolkit per configure, and both emit libggml-cuda.so.
#
# CUDA 12 vs 13 is not a cmake flag -- it is whichever nvcc is found ($CUDACXX,
# else PATH). On x86_64, build.rs passes ggml-cuda's default arch list for that
# version with Turing as native code (#9185); it only adds the 50/61/70 virtual
# archs below CUDA 13. They are still
# separate variants because the resulting binaries cover different GPUs, so
# check-engine-toolchain asserts the toolkit matches the variant name rather
# than letting a cuda13 build ship labelled cuda12.
COMMA := ,
SPACE := $(subst ,, )
JAN_ENGINE_VARIANT ?= vulkan

# Shipping builds pair Vulkan with CUDA/ROCm so the app still offloads when the
# vendor runtime turns out to be missing or too old. That needs the Vulkan SDK,
# which a developer with only a CUDA toolkit has no reason to install, so it can
# be dropped locally. Never drop it for a release: the fallback is the point.
JAN_ENGINE_VULKAN_FALLBACK ?= 1

# Overrides the GPU architectures a CUDA build targets. Empty means upstream's
# own list, which covers discrete desktop/datacenter parts (75/80/86/89/90, plus
# 50/61/70 below CUDA 13) but has NO Jetson entry -- Orin is sm_87 and Xavier
# sm_72. Set it for an ARM/Jetson target, or to trim a build to one known GPU:
#     make build-engine JAN_ENGINE_VARIANT=cuda13 JAN_ENGINE_CUDA_ARCHS=87
#     make build-engine JAN_ENGINE_VARIANT=cuda13 JAN_ENGINE_CUDA_ARCHS=86-real
JAN_ENGINE_CUDA_ARCHS ?=
export JAN_ENGINE_CUDA_ARCHS

# Overrides the AMD GPUs a HIP build targets (cmake GPU_TARGETS). Empty means
# the list build.rs carries over from the previous engine's releases:
#     make build-engine JAN_ENGINE_VARIANT=rocm JAN_ENGINE_HIP_TARGETS=gfx1100
JAN_ENGINE_HIP_TARGETS ?=
export JAN_ENGINE_HIP_TARGETS

# One token to one cargo feature. `engine` alone is the CPU-only worker, and it
# is implied by every other feature, so `cpu` maps to it and the GPU tokens do
# not have to name it.
ENGINE_TOKENS := $(subst -, ,$(JAN_ENGINE_VARIANT))
ENGINE_FEATURE_cpu := engine
ENGINE_FEATURE_vulkan := engine-vulkan
ENGINE_FEATURE_metal := engine-metal
ENGINE_FEATURE_cuda12 := engine-cuda
ENGINE_FEATURE_cuda13 := engine-cuda
ENGINE_FEATURE_hip := engine-hip
ENGINE_FEATURE_rocm := engine-hip

ENGINE_UNKNOWN_TOKENS := $(strip $(foreach t,$(ENGINE_TOKENS),$(if $(ENGINE_FEATURE_$(t)),,$(t))))
ifneq ($(ENGINE_UNKNOWN_TOKENS),)
    $(error Unknown JAN_ENGINE_VARIANT '$(JAN_ENGINE_VARIANT)': no backend named '$(ENGINE_UNKNOWN_TOKENS)'. Tokens are cpu, vulkan, metal, cuda12, cuda13, hip/rocm, joined by '-')
endif
ifneq ($(word 2,$(filter cuda12 cuda13,$(ENGINE_TOKENS))),)
    $(error JAN_ENGINE_VARIANT '$(JAN_ENGINE_VARIANT)' names more than one CUDA major, which one build cannot carry)
endif

# The fallback adds vulkan whenever a vendor runtime is in play and the variant
# did not already ask for it. $(sort) dedupes, so naming vulkan twice is
# harmless and the feature list is deterministic.
ENGINE_VENDOR_FEATURES := $(filter engine-cuda engine-hip,$(foreach t,$(ENGINE_TOKENS),$(ENGINE_FEATURE_$(t))))
ENGINE_FEATURE_LIST := $(sort $(foreach t,$(ENGINE_TOKENS),$(ENGINE_FEATURE_$(t))) \
    $(if $(ENGINE_VENDOR_FEATURES),$(if $(filter 1,$(JAN_ENGINE_VULKAN_FALLBACK)),engine-vulkan,),))
ENGINE_FEATURES := $(subst $(SPACE),$(COMMA),$(ENGINE_FEATURE_LIST))

ENGINE_PLUGIN_DIR := src-tauri/plugins/tauri-plugin-llamacpp
ENGINE_BIN_DIR := src-tauri/resources/bin

# cmake and nvcc output goes here, and is tailed live while cargo runs -- cargo
# captures a build script's output and only shows it on failure, so without this
# a fifteen-minute engine build prints nothing and looks hung. Kept afterwards
# as the artifact to attach to a build report.
ENGINE_BUILD_LOG := $(CURDIR)/src-tauri/target/engine-build.log
export JAN_ENGINE_BUILD_LOG := $(ENGINE_BUILD_LOG)

# Parallelism for the engine build only.
#
# `make -j2` alone does NOT reach cmake: build.rs reads NUM_JOBS, which *cargo*
# sets from its own parallelism, and cargo ignores make's jobserver for that
# purpose -- it defaults to num_cpus. So `make -j2` silently built 8-wide and
# passed cmake -j8. Translating make's own -j into CARGO_BUILD_JOBS makes the
# flag mean what it says.
#
# Scoped to the engine rather than exported globally, because an 8-way nvcc
# build is the memory spike worth limiting; throttling the 1,200-crate app build
# to match would just make everything slower.
#
# JAN_ENGINE_JOBS overrides both, for limiting the engine without touching the
# rest of the tree:
#     make build-engine JAN_ENGINE_VARIANT=cuda13 JAN_ENGINE_JOBS=2
#
# A bare `make -j` (unlimited) yields an empty value and falls through to
# cargo's default, which is the right reading of "no limit".
# Deferred (`=`, not `:=`) on purpose: make puts -j into MAKEFLAGS only when it
# runs a recipe, not while parsing the makefile, so a simple assignment reads an
# empty value and the limit is silently lost -- the exact failure this is meant
# to fix. `--jobserver-auth=...` does not match `-j%` (it starts `--`).
MAKE_J = $(strip $(patsubst -j%,%,$(filter -j%,$(MAKEFLAGS))))
JAN_ENGINE_JOBS ?= $(MAKE_J)
ENGINE_CARGO_JOBS = $(if $(JAN_ENGINE_JOBS),CARGO_BUILD_JOBS=$(JAN_ENGINE_JOBS),)
# cmd.exe has no `VAR=value command` prefix, so the same limit is set with `set`
# there. The Windows recipes still run under sh when one is on PATH, where the
# prefix form is the only one that works.
ENGINE_CARGO_JOBS_WIN = $(if $(RECIPE_SHELL_IS_CMD),$(if $(JAN_ENGINE_JOBS),set CARGO_BUILD_JOBS=$(JAN_ENGINE_JOBS)&&,),$(ENGINE_CARGO_JOBS))

# cargo also finds the jobserver in MAKEFLAGS and tries to join it, but a recipe
# line make did not mark recursive never inherits its file descriptors, so cargo
# warns and falls back on every build. An empty CARGO_MAKEFLAGS takes precedence
# over MAKEFLAGS and reads as "no jobserver", which is the truth here:
# parallelism reaches cargo as CARGO_BUILD_JOBS above, and build.rs sizes
# cmake -j from the NUM_JOBS cargo derives from it.
export CARGO_MAKEFLAGS :=

# Streams the build log while $(1) runs, then stops the tail either way. Unix
# only -- the Windows recipes run cargo straight, so cmake output there arrives
# in cargo's own failure dump rather than live.
# `tail -F` tolerates the file not existing yet; the trap covers a cargo failure
# and a Ctrl-C so no reader is left behind.
define with_engine_log
	@mkdir -p $(dir $(ENGINE_BUILD_LOG))
	@: > $(ENGINE_BUILD_LOG)
	@tail -F -n +1 $(ENGINE_BUILD_LOG) 2>/dev/null | sed 's/^/[engine] /' & \
	tail_pid=$$!; \
	trap 'kill $$tail_pid 2>/dev/null || true' EXIT INT TERM; \
	$(1); \
	rc=$$?; \
	sleep 1; \
	kill $$tail_pid 2>/dev/null || true; \
	exit $$rc
endef

# `make dev` must not require a GPU toolchain: a contributor working on the UI
# has no reason to install the Vulkan SDK or CUDA, so the default variant skips
# rather than blocks.
#
# But a variant asked for *explicitly* is a request, not a default -- silently
# skipping it would leave someone waiting for an engine that was never built.
# `origin` distinguishes the two: `file` means the `?=` default below, anything
# else means the caller named it.
ENGINE_VARIANT_EXPLICIT := $(if $(filter-out file,$(origin JAN_ENGINE_VARIANT)),yes,)

build-engine-dev-if-possible:
ifeq ($(DETECTED_OS),Windows)
	@echo "Building the llama.cpp engine (skipped if the toolchain is incomplete)"
	-$(MAKE) build-engine-dev
else
	@if out=$$($(MAKE) --no-print-directory check-engine-toolchain 2>&1); then \
		$(MAKE) --no-print-directory build-engine-dev; \
	elif [ -n "$(ENGINE_VARIANT_EXPLICIT)" ]; then \
		echo "$$out" >&2; \
		echo "" >&2; \
		echo "You asked for JAN_ENGINE_VARIANT=$(JAN_ENGINE_VARIANT) explicitly, so this is a" >&2; \
		echo "hard failure rather than a skip. Note that PATH changes must be in the" >&2; \
		echo "same shell as make, e.g.:" >&2; \
		echo "    PATH=/usr/local/cuda-13/bin:\$$PATH make dev JAN_ENGINE_VARIANT=$(JAN_ENGINE_VARIANT)$(if $(filter 1,$(JAN_ENGINE_VULKAN_FALLBACK)), JAN_ENGINE_VULKAN_FALLBACK=0,)" >&2; \
		exit 1; \
	else \
		echo "=============================================================="; \
		echo "Skipping the llama.cpp engine -- toolchain incomplete:"; \
		echo "$$out" | sed 's/^/  /'; \
		echo ""; \
		echo "Local models will not run. Cloud providers are unaffected."; \
		echo "To build one, name a variant you have the toolchain for:"; \
		echo "    make build-engine-dev JAN_ENGINE_VARIANT=cpu"; \
		echo "=============================================================="; \
	fi
endif

# The vendored llama.cpp. The pin itself lives in build.rs and is read by
# fetch-engine-source.sh, so there is exactly one source of truth for it.
ENGINE_SRC_DIR := $(ENGINE_PLUGIN_DIR)/vendor/llama.cpp

engine-source:
	bash src-tauri/build-utils/fetch-engine-source.sh

# Drops the vendored clone. A `clean` prerequisite rather than part of its
# recipe so it runs *before* clean's `find . -name build -type d` sweeps the
# tree -- otherwise those walk the whole llama.cpp checkout on the way to
# deleting it.
clean-engine-source:
ifeq ($(DETECTED_OS),Windows)
	-powershell -Command "if (Test-Path '$(ENGINE_SRC_DIR)') { Remove-Item -Recurse -Force '$(ENGINE_SRC_DIR)' }"
else
	rm -rf $(ENGINE_SRC_DIR)
endif

check-engine-toolchain:
	bash src-tauri/build-utils/check-engine-toolchain.sh $(JAN_ENGINE_VARIANT) $(ENGINE_FEATURES)

# The S3 cache key for what build-engine would compile. Run through make so the
# script sees the same resolved features and exported environment cargo gets.
engine-prebuilt-key:
	@bash src-tauri/build-utils/engine-prebuilt.sh key $(ENGINE_FEATURES)

# Release worker plus the ggml runtime it loads.
build-engine: engine-source check-engine-toolchain
	@echo "Building llama.cpp engine worker (variant: $(JAN_ENGINE_VARIANT), features: $(ENGINE_FEATURES), jobs: $(if $(JAN_ENGINE_JOBS),$(JAN_ENGINE_JOBS),all cores))"
	$(call MKDIR,'$(ENGINE_BIN_DIR)')
ifeq ($(DETECTED_OS),Windows)
	cd $(ENGINE_PLUGIN_DIR) && $(ENGINE_CARGO_JOBS_WIN) cargo build --release --features $(ENGINE_FEATURES) --bin jan-llama-worker
	bash src-tauri/build-utils/stage-engine.sh release
else
	$(call with_engine_log,cd $(ENGINE_PLUGIN_DIR) && $(ENGINE_CARGO_JOBS) cargo build --release --features $(ENGINE_FEATURES) --bin jan-llama-worker)
	bash src-tauri/build-utils/stage-engine.sh release
endif
	bash src-tauri/build-utils/sign-engine.sh

# Type-checks and links the engine-gated test suites without running them.
#
# Every suite under tests/ is `#![cfg(feature = "engine")]`, and both
# rust-check.yml and a plain `cargo test --lib` run with the feature off, so all
# three report "0 passed; ok" -- green suites that assert nothing, over the whole
# FFI boundary. `--no-run` is the part that needs no model: the runtime half
# still wants JAN_TEST_GGUF, but a suite that stops compiling now fails loudly
# instead of passing vacuously. Depends on the same toolchain check as
# build-engine, and reuses ENGINE_FEATURES so it cannot drift from what the
# worker is built with.
check-engine-tests: engine-source check-engine-toolchain
	@echo "Compiling engine test suites (variant: $(JAN_ENGINE_VARIANT), features: $(ENGINE_FEATURES))"
	cd $(ENGINE_PLUGIN_DIR) && cargo test --features $(ENGINE_FEATURES) --no-run

# Debug worker for local dev. Same staging, so `make dev` behaves like a bundle.
build-engine-dev: engine-source check-engine-toolchain
	@echo "Building llama.cpp engine worker (dev, variant: $(JAN_ENGINE_VARIANT), jobs: $(if $(JAN_ENGINE_JOBS),$(JAN_ENGINE_JOBS),all cores))"
	$(call MKDIR,'$(ENGINE_BIN_DIR)')
ifeq ($(DETECTED_OS),Windows)
	cd $(ENGINE_PLUGIN_DIR) && $(ENGINE_CARGO_JOBS_WIN) cargo build --features $(ENGINE_FEATURES) --bin jan-llama-worker
	bash src-tauri/build-utils/stage-engine.sh debug
else
	$(call with_engine_log,cd $(ENGINE_PLUGIN_DIR) && $(ENGINE_CARGO_JOBS) cargo build --features $(ENGINE_FEATURES) --bin jan-llama-worker)
	bash src-tauri/build-utils/stage-engine.sh debug
endif

# Build the Jan agent CLI (the `jan` binary with the `cli` feature)
# The compiled binary lives at two locations after build:
#   1. src-tauri/resources/bin/jan[.exe] (bundled copy)
#   2. src-tauri/target/<triple>/release/jan[.exe] (Cargo output)
agent: build-agent

# Build the jan agent CLI binary (release, platform-aware)
# The binary is compiled into src-tauri/target/<triple>/release/jan[.exe]
# and then installed to src-tauri/resources/bin/jan[.exe] for bundling.
build-agent:
ifeq ($(DETECTED_OS),Windows)
	cd src-tauri/jan-cli && cargo build --release --no-default-features --features cli
	copy src-tauri\target\release\jan.exe src-tauri\resources\bin\jan.exe
	@echo "Jan agent built at:"
	@echo "  src-tauri/resources/bin/jan.exe"
	@echo "  src-tauri/target/release/jan.exe"
else
	cd src-tauri/jan-cli && cargo build --release --no-default-features --features cli
	install -m755 src-tauri/target/release/jan src-tauri/resources/bin/jan
	@echo "Jan agent built at:"
	@echo "  src-tauri/resources/bin/jan"
	@echo "  src-tauri/target/release/jan"
endif

# Build
build: install-and-build install-rust-targets
	$(MAKE) build-engine
	yarn build

clean: clean-engine-source
ifeq ($(DETECTED_OS),Windows)
	-powershell -Command "Get-ChildItem -Path . -Include node_modules, .next, dist, build, out, .turbo, .yarn -Recurse -Directory | Remove-Item -Recurse -Force"
	-powershell -Command "Get-ChildItem -Path . -Include package-lock.json, tsconfig.tsbuildinfo -Recurse -File | Remove-Item -Recurse -Force"
	-powershell -Command "Remove-Item -Recurse -Force ./extensions/*/*.tgz"
	-powershell -Command "Remove-Item -Recurse -Force ./src-tauri/resources"
	-powershell -Command "Remove-Item -Recurse -Force ./src-tauri/target"
	-powershell -Command "if (Test-Path \"$($env:USERPROFILE)\jan\extensions\") { Remove-Item -Path \"$($env:USERPROFILE)\jan\extensions\" -Recurse -Force }"
else ifeq ($(DETECTED_OS),Linux)
	find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
	find . -name ".next" -type d -exec rm -rf '{}' +
	find . -name "dist" -type d -exec rm -rf '{}' +
	find . -name "build" -type d -exec rm -rf '{}' +
	find . -name "out" -type d -exec rm -rf '{}' +
	find . -name ".turbo" -type d -exec rm -rf '{}' +
	find . -name ".yarn" -type d -exec rm -rf '{}' +
	find . -name "packake-lock.json" -type f -exec rm -rf '{}' +
	find . -name "package-lock.json" -type f -exec rm -rf '{}' +
	rm -rf ./extensions/*/*.tgz
	rm -rf ./src-tauri/resources
	rm -rf ./src-tauri/target
	rm -rf "~/jan/extensions"
	rm -rf "~/.cache/jan*"
	rm -rf "./.cache"
else
	find . -name "node_modules" -type d -prune -exec rm -rfv '{}' +
	find . -name ".next" -type d -exec rm -rfv '{}' +
	find . -name "dist" -type d -exec rm -rfv '{}' +
	find . -name "build" -type d -exec rm -rfv '{}' +
	find . -name "out" -type d -exec rm -rfv '{}' +
	find . -name ".turbo" -type d -exec rm -rfv '{}' +
	find . -name ".yarn" -type d -exec rm -rfv '{}' +
	find . -name "package-lock.json" -type f -exec rm -rfv '{}' +
	rm -rfv ./extensions/*/*.tgz
	rm -rfv ./src-tauri/resources
	rm -rfv ./src-tauri/target
	rm -rfv ~/jan/extensions
	rm -rfv ~/Library/Caches/jan*
endif