# nightme Makefile
# Minimal, opinionated build/test/dev workflow.
GO ?= go
VERSION ?= 0.1.0
GIT_COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "local")
BUILD_DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -X github.com/cnlangzi/nightme/internal/version.Version=$(VERSION) \
-X github.com/cnlangzi/nightme/internal/version.GitCommit=$(GIT_COMMIT) \
-X github.com/cnlangzi/nightme/internal/version.BuildDate=$(BUILD_DATE)
# Windows icon / manifest embedding runs through go-winres, which
# produces cmd/nightme/rsrc_windows_<arch>.syso from winres.json.
# Go's linker auto-includes any *.syso in the main package's source
# dir, so once winres runs the resulting binary carries the icon +
# DPI-aware manifest. The .syso files are git-ignored — they are
# generated, not source.
WINRES ?= go-winres
WINRES_JSON ?= assets/winres.json
SYSO_PKG ?= cmd/nightme
# System-tray icon assets. Per-OS targets that produce the byte
# payload that cmd/nightme/tray_assets.go embeds into the binary
# via //go:embed. Windows reuses the existing logo.ico (set up by
# the winres target above); macOS needs a single-color alpha
# "template" set so the menu bar auto-inverts for dark mode, plus
# a .icns for the NightMe.app bundle; Linux takes a plain 64x64
# PNG. tray-assets is a no-op on Windows (the .ico path is
# already covered by winres).
#
# Paths are repo-root-relative (tray-assets runs from the repo
# root, unlike the winres target which `cd`s into cmd/nightme/).
TRAY_TEMPLATE_22 := cmd/nightme/assets/trayTemplate.png
TRAY_TEMPLATE_44 := cmd/nightme/assets/trayTemplate@2x.png
TRAY_TEMPLATE_66 := cmd/nightme/assets/trayTemplate@3x.png
TRAY_ICNS := cmd/nightme/assets/trayTemplate.icns
TRAY_PNG := cmd/nightme/assets/tray.png
TRAY_ICONSET_DIR := cmd/nightme/assets/.iconset
# macOS .app bundle. app-bundle wraps bin/nightme into a Finder-
# navigable bundle so the menu-bar icon shows the proper icns
# (a raw binary's menu-bar icon is the generic executable glyph,
# not our logo). The bundle is NOT shipped in release artifacts;
# users on macOS run `make app-bundle` themselves and copy
# dist/NightMe.app to /Applications.
APP_NAME := NightMe
APP_BUNDLE := dist/$(APP_NAME).app
APP_PLIST := scripts/$(APP_NAME)/Info.plist
APP_ICON := cmd/nightme/assets/trayTemplate.icns
# Cross-compile knobs. Defaults track the host so `make build`
# on a developer laptop produces a native binary.
#
# The DEFAULT Linux build has no CGo dependency at all (the tray
# is behind -tags gui; see GO_TAGS below), so
# `GOOS=linux GOARCH=arm64 CGO_ENABLED=0 make build` cross-compiles
# cleanly. Every OTHER configuration is CGo — macOS (Cocoa),
# Windows (Win32) and `-tags gui` Linux (GTK3+AppIndicator) — and
# no cross toolchain is configured here, so CI builds those on
# their own native runner (see .github/workflows/{ci,release}.yml).
GOOS ?= $(shell go env GOOS)
GOARCH ?= $(shell go env GOARCH)
# EXT is .exe on Windows, empty elsewhere. Applied uniformly to
# both the `build` and `release` outputs so callers can `./bin/nightme`
# on Linux/macOS and `./bin/nightme.exe` on Windows. Windows's
# CreateProcess rejects a path without the .exe suffix, so the
# build target MUST use $(EXT) — see `make restart` below.
EXT ?= $(if $(filter windows,$(GOOS)),.exe,)
BIN_DIR ?= dist
BIN_NAME ?= nightme
BINARY ?= bin/$(BIN_NAME)$(EXT)
RELEASE_BIN := $(BIN_DIR)/$(BIN_NAME)-$(GOOS)-$(GOARCH)$(EXT)
# Go build tags. Currently only `gui` is meaningful: it opts the
# Linux build in to the system-tray implementation (cmd/nightme/
# tray_gui.go). Leaving it empty on Linux selects the no-op stub
# in cmd/nightme/tray_stub.go, which is what we want by default —
# see the header comment in cmd/nightme/tray.go for the full
# reasoning, but in short:
#
# getlantern/systray links libayatana-appindicator3.so.1 +
# libgtk-3.so.0, and a Linux host without the GTK3 runtime
# cannot exec the binary at all (ld.so refuses before main()).
# Linux boxes are mostly servers, so tray-off is the safe
# default and `make build-gui` is the opt-in.
#
# macOS and Windows ignore this knob — their tray backings (Cocoa /
# Win32) ship with the OS, so tray_gui.go is unconditional there.
GO_TAGS ?=
.PHONY: help
help: ## Show this help.
@awk 'BEGIN {FS = ":.*##"; printf "Usage: make <target>\n\nTargets:\n"} \
/^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
.PHONY: winres
winres: ## Embed Windows icon + manifest into cmd/nightme/rsrc_windows_*.syso (no-op off-Windows).
@if [ "$(GOOS)" = "windows" ]; then \
command -v $(WINRES) >/dev/null 2>&1 || { \
echo "[winres] $(WINRES) not on PATH; installing via 'go install'..."; \
$(GO) install github.com/tc-hib/go-winres@latest; \
}; \
cd $(SYSO_PKG) && \
$(WINRES) make \
--in $(WINRES_JSON) \
--out rsrc \
--arch 386,amd64,arm64 \
--product-version $(VERSION) \
--file-version $(VERSION); \
else \
echo "[winres] GOOS=$(GOOS) != windows; skipping"; \
fi
.PHONY: tray-assets
tray-assets: ## Build macOS .icns template + Linux tray.png (no-op on Windows).
ifeq ($(GOOS),darwin)
@command -v sips >/dev/null 2>&1 || { echo "sips required (Xcode CLT)"; exit 1; }
@command -v iconutil >/dev/null 2>&1 || { echo "iconutil required (Xcode CLT)"; exit 1; }
@mkdir -p $(TRAY_ICONSET_DIR)
@# Generate single-color alpha template (menu bar auto-inverts).
@sips -s format png --resampleWidth 22 logo.png --out $(TRAY_TEMPLATE_22) >/dev/null
@sips -s format png --resampleWidth 44 logo.png --out $(TRAY_TEMPLATE_44) >/dev/null
@sips -s format png --resampleWidth 66 logo.png --out $(TRAY_TEMPLATE_66) >/dev/null
@cp $(TRAY_TEMPLATE_22) $(TRAY_ICONSET_DIR)/icon_22x22.png
@cp $(TRAY_TEMPLATE_44) $(TRAY_ICONSET_DIR)/icon_44x44.png
@cp $(TRAY_TEMPLATE_66) $(TRAY_ICONSET_DIR)/icon_66x66.png
@iconutil -c icns $(TRAY_ICONSET_DIR) -o $(TRAY_ICNS)
@rm -rf $(TRAY_ICONSET_DIR)
@echo "[tray-assets] built $(TRAY_TEMPLATE_22) $(TRAY_TEMPLATE_44) $(TRAY_TEMPLATE_66) $(TRAY_ICNS)"
else ifeq ($(GOOS),linux)
@command -v convert >/dev/null 2>&1 || { echo "ImageMagick 'convert' required"; exit 1; }
@# Linux tray icon (full color, 64x64).
@convert logo.png -resize 64x64 $(TRAY_PNG)
@# macOS menu-bar template icons. Cocoa treats these as
# templates when systray.SetTemplateIcon is called: the
# alpha channel is the mask, the RGB is ignored (Cocoa
# draws the alpha shape in the menu-bar foreground
# color, which is black in light mode and white in
# dark mode). Plain -resize preserves the alpha; do
# NOT use -alpha extract — that strips the alpha
# channel and produces a grayscale PNG that the
# template API cannot use. On a real macOS dev box,
# 'make tray-assets' (GOOS=darwin) regenerates these
# via sips for sharper anti-aliasing.
@convert logo.png -resize 22x22 $(TRAY_TEMPLATE_22)
@convert logo.png -resize 44x44 $(TRAY_TEMPLATE_44)
@convert logo.png -resize 66x66 $(TRAY_TEMPLATE_66)
@echo "[tray-assets] built $(TRAY_PNG) + $(TRAY_TEMPLATE_22) $(TRAY_TEMPLATE_44) $(TRAY_TEMPLATE_66)"
else
@echo "[tray-assets] GOOS=$(GOOS): Windows uses logo.ico via go-winres; skipping"
endif
.PHONY: build
build: winres ## Compile binary to bin/nightme[.exe] with version metadata.
@mkdir -p bin
$(GO) build -tags '$(GO_TAGS)' -ldflags '$(LDFLAGS)' -o $(BINARY) ./cmd/nightme
# build-gui produces the Linux tray-enabled variant alongside the
# default tray-less one. Requires libgtk-3-dev +
# libayatana-appindicator3-dev at build time and libgtk-3-0 +
# libayatana-appindicator3-1 at run time.
#
# BIN_NAME is overridden rather than BINARY so $(EXT) still
# applies; the release pipeline packages this as
# nightme_<v>_linux_<arch>-gui.tar.gz with the binary renamed back
# to plain `nightme` so users can drop it straight over the
# default install.
#
# No-op off Linux: macOS and Windows already build the tray into
# the default binary, so a separate GUI variant would be identical
# to `make build`.
.PHONY: build-gui
build-gui: ## Compile the Linux tray-enabled binary to bin/nightme-gui (Linux only).
ifneq ($(GOOS),linux)
@echo "[build-gui] GOOS=$(GOOS): tray is already in the default binary; skipping"
else
@$(MAKE) build GO_TAGS=gui BIN_NAME=$(BIN_NAME)-gui
endif
.PHONY: release
release: winres ## Build a versioned binary into dist/nightme-<GOOS>-<GOARCH>[.exe] (host-only).
@mkdir -p $(BIN_DIR)
GOOS=$(GOOS) GOARCH=$(GOARCH) $(GO) build -tags '$(GO_TAGS)' -ldflags '$(LDFLAGS)' -o $(RELEASE_BIN) ./cmd/nightme
# build-stt compiles the nightme-stt worker into bin/nightme-stt[.exe].
# The worker is a SEPARATE binary from `nightme` — NightMe core spawns
# it on demand as a child process (see internal/stt/spawner.go + issue
# #381). Two non-default knobs from `make build`:
#
# -tags cgo_sherpa selects cmd/nightme-stt/internal/sherpa/sherpa_onnx.go
# (the real SenseVoice binding) instead of the
# stub at cmd/nightme-stt/internal/sherpa/stub.go.
# Without this tag the worker compiles but every
# transcription returns the placeholder text —
# fine for `make test` smoke runs, useless in
# production.
# CGO_ENABLED=1 required by the cgo_sherpa binding. The platform
# native lib (.a/.dylib/.lib) ships inside the
# sherpa-onnx-go-{linux,macos,windows} Go modules
# (go.mod), so no system .so/.dylib/.dll is needed
# at run time — the produced binary is self-
# contained.
#
# BIN_NAME=nightme-stt + the existing $(EXT) knob produce the right
# suffix on every platform (`.exe` on Windows, empty elsewhere),
# matching what internal/stt/spawner.go expects to find via
# FindNightmeSTT.
#
# Native-only matrix in release.yml (no cross-compile): the
# sherpa-onnx-go native libs are per-arch, and cgo cannot cross-
# compile between arches without a target sysroot. The release
# matrix's `os:` axis points at the matching native runner for
# each goos/goarch pair, same as the main `nightme` release matrix.
.PHONY: build-stt
build-stt: ## Compile nightme-stt worker to bin/nightme-stt[.exe] (CGO + cgo_sherpa).
@mkdir -p bin
CGO_ENABLED=1 $(GO) build -tags cgo_sherpa -ldflags '$(LDFLAGS)' \
-o bin/nightme-stt$(EXT) ./cmd/nightme-stt
# release-stt: same shape as `release` but for the worker. Used by
# dev / manual release checks; release.yml calls `make build-stt`
# directly (it already controls the surrounding stage/archive/upload
# steps and only needs the produced bin/nightme-stt[.exe]).
.PHONY: release-stt
release-stt: ## Build a versioned nightme-stt into dist/nightme-stt-<GOOS>-<GOARCH>[.exe] (host-only).
@mkdir -p $(BIN_DIR)
CGO_ENABLED=1 GOOS=$(GOOS) GOARCH=$(GOARCH) \
$(GO) build -tags cgo_sherpa -ldflags '$(LDFLAGS)' \
-o $(BIN_DIR)/nightme-stt-$(GOOS)-$(GOARCH)$(EXT) ./cmd/nightme-stt
# install-stt mirrors `install` for the worker. Useful for local dev
# against a $$GOPATH/bin that nightme's manager will later discover
# (the manager's FindNightmeSTT walks $$GOPATH/bin as a fallback
# before bailing to the user-facing "nightme stt install" prompt).
.PHONY: install-stt
install-stt: ## Install nightme-stt worker to $$GOBIN (CGO + cgo_sherpa).
CGO_ENABLED=1 $(GO) install -tags cgo_sherpa -ldflags '$(LDFLAGS)' ./cmd/nightme-stt
# tray-assets is intentionally NOT in the build/release dependency
# chain. The tray-icon byte payload that //go:embed resolves in
# cmd/nightme/tray_assets.go is committed to the repo (Linux:
# assets/tray.png; macOS: assets/trayTemplate{,_@2x,_@3x}.png;
# Windows: assets/logo-32.ico — produced by go-winres from
# assets/winres.json). CI and release pipelines consume the
# pre-built assets directly; ImageMagick is NOT installed on
# the hosted runners (the prior setup tried to invoke convert
# on every Linux build, which fails with 'ImageMagick
# required'). `make tray-assets` exists for dev use (e.g. a
# dev who wants to regenerate the macOS templates with sips
# for sharper anti-aliasing) and is otherwise a no-op.
.PHONY: app-bundle
app-bundle: build ## Wrap bin/nightme into dist/NightMe.app (macOS only).
ifneq ($(GOOS),darwin)
@echo "[app-bundle] GOOS=$(GOOS) != darwin; skipping"
else
@rm -rf $(APP_BUNDLE)
@mkdir -p $(APP_BUNDLE)/Contents/MacOS
@mkdir -p $(APP_BUNDLE)/Contents/Resources
@cp bin/nightme $(APP_BUNDLE)/Contents/MacOS/nightme
@cp $(APP_PLIST) $(APP_BUNDLE)/Contents/Info.plist
@[ -f $(APP_ICON) ] && cp $(APP_ICON) $(APP_BUNDLE)/Contents/Resources/AppIcon.icns || \
echo "warn: $(APP_ICON) not built; run 'make tray-assets' first"
@echo "Built: $(APP_BUNDLE)"
endif
# restart runs the local `nightme restart` subcommand on the
# freshly built binary. Requires the binary to actually exist
# with the platform-correct suffix (handled by BINARY above);
# Windows CreateProcess can't run a path missing the .exe.
.PHONY: restart
restart: build ## Build and restart nightme daemon.
$(BINARY) restart
.PHONY: test
test: ## Run all tests with race detector.
$(GO) test -race ./...
.PHONY: dev
dev: ## Run nightme in dev mode (uses example config).
$(GO) run ./cmd/nightme
.PHONY: clean
clean: ## Remove build artifacts.
rm -rf bin/ $(BIN_DIR)/
rm -f $(TRAY_TEMPLATE_22) $(TRAY_TEMPLATE_44) $(TRAY_TEMPLATE_66) $(TRAY_ICNS) $(TRAY_PNG)
rm -rf $(TRAY_ICONSET_DIR)
rm -f $(SYSO_PKG)/rsrc_windows_*.syso
.PHONY: lint
lint: ## Run go vet (matches CI).
$(GO) vet ./...
.PHONY: fmt
fmt: ## Format all Go source.
$(GO) fmt ./...
.PHONY: install
install: ## Install nightme to $$GOBIN.
$(GO) install -ldflags '$(LDFLAGS)' ./cmd/nightme