-
Notifications
You must be signed in to change notification settings - Fork 3k
Expand file tree
/
Copy pathMakefile
More file actions
610 lines (555 loc) · 27.7 KB
/
Copy pathMakefile
File metadata and controls
610 lines (555 loc) · 27.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
# 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-cli-dev
$(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-app
build-web-app: install-web-app
yarn build:core
yarn build:web-app
serve-web-app:
yarn serve:web-app
build-serve-web-app: build-web-app
yarn serve:web-app
# 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: lint
yarn download:bin
yarn test
yarn copy:assets:tauri
yarn build:icon
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-llamacpp/Cargo.toml
cargo test --locked --manifest-path src-tauri/utils/Cargo.toml
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.sh 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. The PowerShell
# arm exists only because cmd.exe cannot run it: CI runs make from a shell where
# sh.exe is on PATH, so it takes the script.
stub-resources:
ifeq ($(RECIPE_SHELL_IS_CMD),yes)
-powershell -Command "New-Item -ItemType Directory -Force -Path src-tauri/resources/bin | Out-Null; foreach ($$f in @('jan-cli.exe','jan-llama-worker.exe','ggml-base.dll')) { $$p = Join-Path 'src-tauri/resources/bin' $$f; if (-not (Test-Path $$p)) { New-Item -ItemType File -Path $$p | Out-Null } }"
else
@./scripts/stub-tauri-resources.sh
endif
test-ci: test-prepare
$(MAKE) test-rust
# Cheap compile guard for the CLI feature set, covering what test-ci no longer
# builds. `make build` still builds the real binary on every platform.
check-cli:
cd src-tauri && cargo check --locked --no-default-features --features cli --bin jan
# Build MLX server (macOS Apple Silicon only) - always builds
build-mlx-server:
ifeq ($(DETECTED_OS),Darwin)
@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"
@echo "Finding build products..."
@DERIVED_DATA=$$(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 && cargo build --release --no-default-features --features cli --bin jan --target aarch64-apple-darwin
cd src-tauri && cargo build --release --no-default-features --features cli --bin jan --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 && cargo build --release --no-default-features --features cli --bin jan
cp src-tauri/target/release/jan.exe src-tauri/resources/bin/jan.exe
else
cd src-tauri && cargo build --release --no-default-features --features cli --bin jan
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, and
# llama.cpp adapts CMAKE_CUDA_ARCHITECTURES to it on its own (ggml-cuda's
# CMakeLists 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)
# 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
# Debug build for local dev (faster, native arch only)
build-cli-dev:
$(call MKDIR,'src-tauri/resources/bin')
cd src-tauri && cargo build --no-default-features --features cli --bin jan
ifeq ($(DETECTED_OS),Windows)
copy src-tauri\target\debug\jan.exe src-tauri\resources\bin\jan.exe
else
install -m755 src-tauri/target/debug/jan src-tauri/resources/bin/jan
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 && cargo build --release --no-default-features --features cli --bin jan
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 && cargo build --release --no-default-features --features cli --bin jan
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 ./pre-install/*.tgz"
-powershell -Command "Remove-Item -Recurse -Force ./extensions/*/*.tgz"
-powershell -Command "Remove-Item -Recurse -Force ./electron/pre-install/*.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 ./pre-install/*.tgz
rm -rf ./extensions/*/*.tgz
rm -rf ./electron/pre-install/*.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 ./pre-install/*.tgz
rm -rfv ./extensions/*/*.tgz
rm -rfv ./electron/pre-install/*.tgz
rm -rfv ./src-tauri/resources
rm -rfv ./src-tauri/target
rm -rfv ~/jan/extensions
rm -rfv ~/Library/Caches/jan*
endif