# spacesh — local build helpers (macOS). # `make` or `make help` lists targets. APP_DIR := app TAURI_TARGET := universal-apple-darwin DMG_DIR := $(APP_DIR)/src-tauri/target/$(TAURI_TARGET)/release/bundle/dmg NATIVE_DMG_DIR := $(APP_DIR)/src-tauri/target/release/bundle/dmg NATIVE_TRIPLE := $(shell rustc -vV 2>/dev/null | awk '/^host:/{print $$2}') SIDECAR_DIR := $(APP_DIR)/src-tauri/bin ENTITLEMENTS := $(APP_DIR)/src-tauri/Entitlements.plist BUNDLE_CONFIG := src-tauri/tauri.bundle.conf.json APP_BUNDLE := $(APP_DIR)/src-tauri/target/$(TAURI_TARGET)/release/bundle/macos/spaceshell.app NATIVE_APP_BUNDLE := $(APP_DIR)/src-tauri/target/release/bundle/macos/spaceshell.app APP_VERSION := $(shell node -p "require('./$(APP_DIR)/src-tauri/tauri.conf.json').version" 2>/dev/null || echo 0.0.0) LANDING_IMAGE := spacesh-landing LANDING_VERSION := $(shell cat landing/VERSION 2>/dev/null || echo 0.0.0) REGISTRY ?= git.realmanual.ru REPO ?= spacesh # Stable code-signing identity. Without a STABLE signature the app is ad-hoc # signed and its code identity changes every build, so macOS attributes child # processes (the daemon → Claude Code) to a different "responsible app" each time: # TCC permissions reset and agents lose their Keychain login on every rebuild. # Defaults to the Developer ID (Team 3PNKDC6L42) — a stable designated requirement # (anchor apple generic + TeamID) that Keychain/TCC trust survives across rebuilds. # Override with `SIGN_IDENTITY="" make reinstall`, or `SIGN_IDENTITY=` # to fall back to ad-hoc. Tauri reads APPLE_SIGNING_IDENTITY for the bundle + sidecar. SIGN_IDENTITY ?= Developer ID Application: Vassiliy Yegorov (3PNKDC6L42) ifneq ($(strip $(SIGN_IDENTITY)),) export APPLE_SIGNING_IDENTITY := $(SIGN_IDENTITY) endif # Notarization (required to distribute the DMG — Gatekeeper blocks un-notarized apps # on other Macs). Secrets: put them in a gitignored `.signing.env` (make syntax, # e.g. `APPLE_ID := you@example.com`) or pass on the CLI. NEVER commit them. # APPLE_ID — your Apple ID email # APPLE_PASSWORD — an app-specific password (appleid.apple.com → App-Specific Passwords) # APPLE_TEAM_ID — 3PNKDC6L42 (defaulted below) # When all three are present, `tauri build` auto-notarizes + staples the bundle. -include .signing.env APPLE_ID ?= APPLE_PASSWORD ?= APPLE_TEAM_ID ?= 3PNKDC6L42 ifneq ($(strip $(APPLE_ID)),) export APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID endif # ---- Gitea generic package registry (versioned .dmg downloads) ---- GITEA_URL ?= https://git.realmanual.ru GITEA_OWNER ?= pub GITEA_PKG ?= spacesh GITEA_TOKEN ?= # token with package:write; pass via env/CLI, never commit # ---- Prod deploy (SSH) ---- SSH_HOST ?= 192.168.8.5 SSH_USER ?= root SSH_REMOTE_DIR ?= /srv/spaceshell SSH_KEY ?= $(HOME)/.ssh/id_rsa SSH_OPTS := -i $(SSH_KEY) -o StrictHostKeyChecking=accept-new .DEFAULT_GOAL := help .PHONY: help help: ## show this help @echo "spacesh — make targets (app v$(APP_VERSION)):" @grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | \ awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}' # ---- App / DMG (macOS) ---- .PHONY: deps deps: ## install frontend deps (npm ci) cd $(APP_DIR) && npm ci .PHONY: targets targets: ## add rust targets for the universal build rustup target add aarch64-apple-darwin x86_64-apple-darwin .PHONY: bump bump: ## increment the patch version for BOTH the GUI (tauri.conf.json) and the daemon (workspace Cargo.toml) @node scripts/bump_version.mjs .PHONY: dmg dmg: bump targets ## bump version + build universal .dmg (signed; notarized if .signing.env set) # Tauri's universal build needs BOTH the per-arch sidecars (resolved during each # arch sub-build) AND a fat spaceshd-universal-apple-darwin (copied into the final # bundle — Tauri does not lipo sidecars itself). spaceshd ships inside # spacesh.app/Contents/MacOS else the GUI is offline. cargo build --release -p spaceshd --target aarch64-apple-darwin cargo build --release -p spaceshd --target x86_64-apple-darwin rm -rf $(SIDECAR_DIR) && mkdir -p $(SIDECAR_DIR) # avoid stale sidecars poisoning the bundle cp target/aarch64-apple-darwin/release/spaceshd $(SIDECAR_DIR)/spaceshd-aarch64-apple-darwin cp target/x86_64-apple-darwin/release/spaceshd $(SIDECAR_DIR)/spaceshd-x86_64-apple-darwin lipo -create -output $(SIDECAR_DIR)/spaceshd-universal-apple-darwin \ target/aarch64-apple-darwin/release/spaceshd \ target/x86_64-apple-darwin/release/spaceshd cd $(APP_DIR) && npm run tauri build -- --target $(TAURI_TARGET) --config $(BUNDLE_CONFIG) @echo "→ $(DMG_DIR)" && ls -lh $(DMG_DIR)/*.dmg .PHONY: dmg-native dmg-native: bump ## bump version + build a .dmg for the current arch only (faster) cargo build --release -p spaceshd rm -rf $(SIDECAR_DIR) && mkdir -p $(SIDECAR_DIR) # avoid stale sidecars poisoning the bundle cp target/release/spaceshd $(SIDECAR_DIR)/spaceshd-$(NATIVE_TRIPLE) cd $(APP_DIR) && npm run tauri build -- --config $(BUNDLE_CONFIG) @ls -lh $(NATIVE_DMG_DIR)/*.dmg .PHONY: app-bundle app-bundle: ## build just the native .app (no .dmg/hdiutil — fast, for self-install) cargo build --release -p spaceshd rm -rf $(SIDECAR_DIR) && mkdir -p $(SIDECAR_DIR) cp target/release/spaceshd $(SIDECAR_DIR)/spaceshd-$(NATIVE_TRIPLE) cd $(APP_DIR) && npm run tauri build -- --bundles app --config $(BUNDLE_CONFIG) .PHONY: dev dev: ## run the app in dev mode (tauri dev) cd $(APP_DIR) && npm run tauri dev .PHONY: daemon daemon: ## build & run the daemon cargo run -p spaceshd .PHONY: kill-daemon kill-daemon: ## stop a running spaceshd so a freshly-built one takes over -pkill -x spaceshd -rm -f $$HOME/.spacesh/sock .PHONY: install install: kill-daemon ## install the native .app to /Applications, restart daemon, clear quarantine rm -rf /Applications/spacesh.app /Applications/spaceshell.app # drop the pre-rename app too cp -R "$(NATIVE_APP_BUNDLE)" /Applications/ xattr -dr com.apple.quarantine /Applications/spaceshell.app ifneq ($(strip $(SIGN_IDENTITY)),) # Belt-and-suspenders: re-sign inside-out with the stable identity so neither the # embedded daemon nor the app is left ad-hoc if Tauri skipped the sidecar. codesign --force --options runtime --timestamp --entitlements "$(ENTITLEMENTS)" --sign "$(SIGN_IDENTITY)" /Applications/spaceshell.app/Contents/MacOS/spaceshd codesign --force --options runtime --timestamp --entitlements "$(ENTITLEMENTS)" --sign "$(SIGN_IDENTITY)" /Applications/spaceshell.app @codesign -dvv /Applications/spaceshell.app 2>&1 | grep -E "TeamIdentifier|Signature" || true endif @echo "Installed (native). Quit & relaunch spaceshell; the bundled daemon restarts." @echo "Tip: on first launch grant Full Disk Access (System Settings → Privacy & Security)" @echo " so terminals inside the app can run tmutil / reach protected folders." .PHONY: install-universal install-universal: kill-daemon ## install the universal .app to /Applications rm -rf /Applications/spacesh.app /Applications/spaceshell.app cp -R "$(APP_BUNDLE)" /Applications/ xattr -dr com.apple.quarantine /Applications/spaceshell.app .PHONY: reinstall reinstall: app-bundle install ## fast self-update: build .app (no dmg), reinstall, restart daemon # ---- Tests ---- .PHONY: test test: ## run all tests (cargo + tsc) cargo test cd $(APP_DIR) && npx tsc --noEmit # ---- Landing ---- .PHONY: landing-image landing-image: ## build the landing nginx image docker build -t $(LANDING_IMAGE):$(LANDING_VERSION) -t $(LANDING_IMAGE):latest landing .PHONY: landing-run landing-run: landing-image ## serve the landing locally on http://localhost:8088 docker run --rm -p 8088:80 $(LANDING_IMAGE):latest .PHONY: landing-push landing-push: landing-image ## tag & push the landing image to the registry docker tag $(LANDING_IMAGE):latest $(REGISTRY)/$(REPO)/$(LANDING_IMAGE):$(LANDING_VERSION) docker tag $(LANDING_IMAGE):latest $(REGISTRY)/$(REPO)/$(LANDING_IMAGE):latest docker push $(REGISTRY)/$(REPO)/$(LANDING_IMAGE):$(LANDING_VERSION) docker push $(REGISTRY)/$(REPO)/$(LANDING_IMAGE):latest # ---- Prod deploy ---- .PHONY: deploy-dmg deploy-dmg: dmg ## upload .dmg + manifest to prod, and publish the versioned .dmg to Gitea Packages @VER=$$(node -p "require('./$(APP_DIR)/src-tauri/tauri.conf.json').version"); \ printf '{"version":"%s","url":"https://spaceshell.ru/download/spacesh.dmg"}\n' "$$VER" > /tmp/spacesh-latest.json; \ echo "manifest version → $$VER" ssh $(SSH_OPTS) $(SSH_USER)@$(SSH_HOST) "mkdir -p $(SSH_REMOTE_DIR)/download" scp $(SSH_OPTS) $(DMG_DIR)/*.dmg "$(SSH_USER)@$(SSH_HOST):$(SSH_REMOTE_DIR)/download/spacesh.dmg" scp $(SSH_OPTS) /tmp/spacesh-latest.json "$(SSH_USER)@$(SSH_HOST):$(SSH_REMOTE_DIR)/download/latest.json" @echo "Uploaded → https://spaceshell.ru/download/spacesh.dmg + latest.json" @$(MAKE) --no-print-directory _publish-dmg .PHONY: publish-dmg publish-dmg: dmg _publish-dmg ## build + publish the versioned .dmg to the Gitea package registry # Internal: upload the most recently built .dmg to Gitea's generic registry under # the current version. No build dependency, so deploy-dmg can call it without a # second bump/rebuild. Skips (doesn't fail) when GITEA_TOKEN is unset. .PHONY: _publish-dmg _publish-dmg: @if [ -z "$(GITEA_TOKEN)" ]; then echo "GITEA_TOKEN unset — skipping Gitea Packages publish"; exit 0; fi; \ VER=$$(node -p "require('./$(APP_DIR)/src-tauri/tauri.conf.json').version"); \ DMG=$$(ls -t $(DMG_DIR)/*.dmg 2>/dev/null | head -1); \ if [ -z "$$DMG" ]; then echo "no .dmg in $(DMG_DIR) — run make dmg first"; exit 1; fi; \ URL="$(GITEA_URL)/api/packages/$(GITEA_OWNER)/generic/$(GITEA_PKG)/$$VER/spaceshell-$$VER.dmg"; \ echo "Publishing $$DMG → $$URL"; \ curl --fail-with-body -sS -H "Authorization: token $(GITEA_TOKEN)" --upload-file "$$DMG" "$$URL" && \ echo "Published spaceshell-$$VER.dmg to Gitea Packages ($(GITEA_OWNER)/$(GITEA_PKG)@$$VER)" .PHONY: deploy-stack deploy-stack: ## sync compose+proxy.conf to prod and pull/up (manual; CI does this on push) ssh $(SSH_OPTS) $(SSH_USER)@$(SSH_HOST) "mkdir -p $(SSH_REMOTE_DIR)/download" scp $(SSH_OPTS) deploy/docker-compose.yaml deploy/proxy.conf "$(SSH_USER)@$(SSH_HOST):$(SSH_REMOTE_DIR)/" ssh $(SSH_OPTS) $(SSH_USER)@$(SSH_HOST) "cd $(SSH_REMOTE_DIR) && docker compose pull && docker compose up -d" # ---- Clean ---- .PHONY: clean clean: ## remove build artifacts cargo clean rm -rf $(APP_DIR)/dist