Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ support is planned.
you develop in), overriding BOTH halves — a gitignored `go.work` replacing
`github.com/beetlebugorg/tile57/bindings/go => <path>/bindings/go` plus
`make TILE57=<path> …`. Never commit `go.work`.
- **Windows-native builds.** The core Make targets (`build`, `build-dock`,
`tile57-lib`, `serve`, `test`, `vet`, `tidy`, `clean`) must run on a Windows
host where make's recipe shell may be cmd.exe: set recipe env vars via
make-level `export` (never `VAR=x cmd` prefixes), do file tests in make
(`$(wildcard)`/`ifeq`), route file ops through the OS shim block at the top of
the Makefile, and keep recipe shell syntax to `cd x && y`. The bake/demo/docs
targets are POSIX-only by design (WSL/Git Bash on Windows).
- Use https://www.openbridge.no/ for design and icons.
- Match the style of the code around you.
- Never run `git add -A` or `git add .`. The repo holds large untracked files
Expand Down
146 changes: 115 additions & 31 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,44 @@
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
# ---- host-OS shim (native Windows builds) -------------------------------------
# The core dev targets — build, build-dock, tile57-lib, serve, test, vet, tidy,
# clean — also run on a Windows host, where GNU Make (winget/choco/scoop) usually
# executes recipes through cmd.exe, not a POSIX sh. Those targets therefore avoid
# shell-isms: recipe env vars are set via make-level `export` (never `VAR=x cmd`
# prefixes), file tests live in make ($(wildcard)/ifeq, not `[ ... ]`), and the
# only shell syntax in their recipes is `cd x && y`, which cmd and sh parse the
# same. File ops with no syntax common to both route through the shims below —
# an explicit `cmd /C "..."` on Windows, so they behave identically whether
# make's recipe shell is cmd.exe or an MSYS sh (Git Bash). Everything else
# (bake/demo/docs/screenshot targets, xbuild) stays POSIX-only: on Windows run
# those from WSL or Git Bash.
ifeq ($(OS),Windows_NT)
EXE := .exe
DEVNULL := NUL
# Pin the engine lib AND the C toolchain to the <arch>-windows-gnu triple that
# scripts/xbuild-tile57.sh already cross-links with, so a native build never
# mixes Zig's detected-native ABI (possibly msvc) with the gnu-ABI link the
# binding's `-lpthread` expects. Zig doubles as the C compiler — it's a hard
# build dep anyway, so no MinGW install is needed (`make build CC=gcc` to
# override).
GOARCH_HOST = $(shell go env GOARCH)
ZIG_HOST_TARGET = $(if $(filter arm64,$(GOARCH_HOST)),aarch64,x86_64)-windows-gnu
ZIG_BUILD_ARGS = -Dtarget=$(ZIG_HOST_TARGET)
# Zig names the Windows static lib tile57.lib, but the Go binding links the
# fixed path zig-out/lib/libtile57.a — normalize after every engine build (the
# same move xbuild-tile57.sh makes).
NORMALIZE_TILE57_LIB = cmd /C "copy /Y $(subst /,\,$(TILE57)/zig-out/lib/tile57.lib) $(subst /,\,$(TILE57_LIB))"
RM_BIN = cmd /C "del /F /Q $(subst /,\,$(BIN)) 2>NUL & exit /B 0"
RM_BUILD_DIRS = cmd /C "if exist bin rmdir /S /Q bin" && cmd /C "if exist dist rmdir /S /Q dist"
else
EXE :=
DEVNULL := /dev/null
NORMALIZE_TILE57_LIB =
RM_BIN = rm -f $(BIN)
RM_BUILD_DIRS = rm -rf bin dist
endif

VERSION ?= $(shell git describe --tags --always --dirty 2>$(DEVNULL) || echo dev)
LDFLAGS := -X main.version=$(VERSION)
BIN := bin/chartplotter
BIN := bin/chartplotter$(EXE)

# Cross-build matrix for `make xbuild`. Override for a subset, e.g.
# `make xbuild PLATFORMS=darwin/arm64` or `PLATFORMS="darwin/arm64 linux/amd64"`.
Expand All @@ -27,7 +65,7 @@ CACHE ?= $(if $(XDG_CACHE_HOME),$(XDG_CACHE_HOME),$(HOME)/.cache)/chartplotter
S101_PC ?= $(HOME)/Projects/s101-portrayal-catalogue/PortrayalCatalog
S101_FC ?= $(HOME)/Projects/s101-feature-catalogue/S-101FC/FeatureCatalogue.xml

.PHONY: build build-tile57 tile57-lib vendor-style-engine xbuild xbuild-tile57 test vet fmt fmt-check tidy clean clear-cache serve docs docs-shots bake-ienc bake-noaa serve-widget demo demo-chart1 serve-demo preslib-chart1 s64-pages
.PHONY: build build-tile57 build-dock tile57-lib check-engine-override vendor-style-engine xbuild xbuild-tile57 xbuild-dock package-macos test vet fmt fmt-check tidy clean clear-cache serve docs docs-shots bake-ienc bake-noaa serve-widget demo demo-chart1 serve-demo preslib-chart1 s64-pages

# Prebaked prod test set (US Inland ENC bundle + the NOAA world archive).
# NB: keep these as bare values with NO inline `#` comments — Make folds any
Expand Down Expand Up @@ -73,55 +111,90 @@ NOAA_ARCHIVES := $(foreach d,$(DISTRICTS),noaa-d$(d).pmtiles)
TILE57 ?= tile57
TILE57_LIB := $(TILE57)/zig-out/lib/libtile57.a

# A TILE57 override changes only the C lib make builds — the Go binding still
# resolves via go.mod's replace at ./tile57/bindings/go. Building against
# another engine checkout therefore needs a gitignored go.work overriding the
# binding too (README.md "Developing the engine"). Catch the mismatch here,
# up front, instead of letting go print the cryptic "replacement directory
# ./tile57/bindings/go does not exist". Pure-make check (parse-time wildcard +
# $(error)) so it works without a POSIX shell; GOWORK=off counts as absent,
# GOWORK=<file> as present.
check-engine-override:
ifneq ($(TILE57),tile57)
ifeq ($(filter-out off,$(GOWORK))$(wildcard go.work),)
$(error TILE57=$(TILE57) overrides the engine lib, but the Go binding still points at ./tile57/bindings/go — create a gitignored go.work: `go work init .` then `go work edit -replace github.com/beetlebugorg/tile57/bindings/go=$(TILE57)/bindings/go`; see README.md "Developing the engine")
endif
endif

# Engine-commit stamp: the tile57 checkout's HEAD, linked into the binary beside
# main.version so every bake can record WHICH engine produced its tiles (and the
# client can flag a mixed-engine cache). Resolves for the default ./tile57 submodule
# AND a TILE57=… override; "unknown" when git can't answer (submodule not yet
# initialized, tarball checkout). The `test -e .git` guard matters: git -C into a
# missing dir would walk up and report THIS repo's HEAD instead of failing (for the
# submodule .git is a gitdir FILE, for a plain clone a directory — test -e matches
# both, so either resolves cleanly).
ENGINE_COMMIT ?= $(shell test -e "$(TILE57)/.git" && git -C "$(TILE57)" rev-parse --short=9 HEAD 2>/dev/null || echo unknown)
# initialized, tarball checkout). The .git guard matters: git -C into a missing
# dir would walk up and report THIS repo's HEAD instead of failing. It's a make
# $(wildcard), not a shell `test -e`, so it needs no POSIX shell — wildcard
# matches the submodule's gitdir FILE and a plain clone's directory alike.
ifneq ($(wildcard $(TILE57)/.git),)
ENGINE_COMMIT ?= $(shell git -C "$(TILE57)" rev-parse --short=9 HEAD 2>$(DEVNULL) || echo unknown)
else
ENGINE_COMMIT ?= unknown
endif
LDFLAGS += -X main.engineCommit=$(ENGINE_COMMIT)

# Materialize the engine source if it isn't there yet. For the default ./tile57
# submodule this fetches it (and its nested IHO catalogues) with one `git submodule
# update --init --recursive`; for a TILE57=<path> override the checkout must already
# exist (we don't guess where an external engine tree should come from).
$(TILE57)/include/tile57.h:
@if [ "$(TILE57)" = "tile57" ] && [ -f .gitmodules ]; then \
echo "fetching the tile57 engine submodule (git submodule update --init --recursive)…"; \
git submodule update --init --recursive tile57; \
else \
echo "missing $(TILE57)/include/tile57.h — TILE57=$(TILE57) is not the default submodule; point it at a github.com/beetlebugorg/tile57 checkout"; \
exit 1; \
fi

# Build the static library on demand (only when absent). Needs Zig 0.16 on PATH.
ifneq ($(and $(filter tile57,$(TILE57)),$(wildcard .gitmodules)),)
@echo fetching the tile57 engine submodule: git submodule update --init --recursive tile57
git submodule update --init --recursive tile57
else
$(error missing $(TILE57)/include/tile57.h — TILE57=$(TILE57) is not the default submodule; point it at a github.com/beetlebugorg/tile57 checkout)
endif

# Build the static library on demand (only when absent). Needs Zig 0.16 on PATH
# (a missing zig surfaces as the shell's own command-not-found error).
$(TILE57_LIB): $(TILE57)/include/tile57.h
@command -v zig >/dev/null 2>&1 || { echo "Zig 0.16 not on PATH and $(TILE57_LIB) missing — install Zig or prebuild the lib"; exit 1; }
@echo "building libtile57.a (zig build in $(TILE57))…"
cd "$(TILE57)" && zig build
cd "$(TILE57)" && zig build $(ZIG_BUILD_ARGS)
$(NORMALIZE_TILE57_LIB)

tile57-lib: ## Force-rebuild $(TILE57)/zig-out/lib/libtile57.a (the native engine static lib)
@command -v zig >/dev/null 2>&1 || { echo "Zig 0.16 not on PATH"; exit 1; }
cd "$(TILE57)" && zig build
cd "$(TILE57)" && zig build $(ZIG_BUILD_ARGS)
$(NORMALIZE_TILE57_LIB)

# Build bin/chartplotter. libtile57 is the sole engine, so this is a CGO build that
# statically links the native lib; the S-101 catalogue lives inside libtile57, so
# there is no separate sync/embed step (web/ is still embedded). Fetches the ./tile57
# submodule on demand (see the $(TILE57)/include/tile57.h rule) + needs Zig 0.16.
build: $(TILE57_LIB) ## Build bin/chartplotter (CGO + native libtile57; fetches the ./tile57 submodule on demand + needs Zig 0.16)
@# Force the link: go's build-cache action ID does NOT hash external static-lib
@# content, so with an existing up-to-date-looking $(BIN) `go build` silently
@# skips the relink and a fresh libtile57.a never reaches the output.
@rm -f $(BIN)
CGO_ENABLED=1 go build -ldflags "$(LDFLAGS)" -o $(BIN) ./cmd/chartplotter
@echo "→ $(BIN) (native libtile57 engine)"
# The pre-build $(RM_BIN) forces the link: go's build-cache action ID does NOT
# hash external static-lib content, so with an existing up-to-date-looking
# $(BIN) `go build` silently skips the relink and a fresh libtile57.a never
# reaches the output.
build: export CGO_ENABLED = 1
ifeq ($(OS),Windows_NT)
build: export CC = zig cc -target $(ZIG_HOST_TARGET)
build: export CXX = zig c++ -target $(ZIG_HOST_TARGET)
endif
build: check-engine-override $(TILE57_LIB) ## Build bin/chartplotter (CGO + native libtile57; fetches the ./tile57 submodule on demand + needs Zig 0.16)
@$(RM_BIN)
go build -ldflags "$(LDFLAGS)" -o $(BIN) ./cmd/chartplotter
@echo → $(BIN) - native libtile57 engine

# Back-compat alias — libtile57 is now the default engine, so this is just `build`.
build-tile57: build ## Alias for `build` (libtile57 is the sole engine now)

# The dock is the desktop launcher (cmd/dock): a tray/menu-bar app that
# spawns `chartplotter serve` as a child. Pure Go on linux/windows (no libtile57,
# no Zig); systray needs cgo/Cocoa on darwin only. On windows, -H=windowsgui
# keeps a double-clicked dock.exe from opening a console window (matching
# scripts/xbuild-dock.sh).
GOOS_HOST = $(shell go env GOOS)
build-dock: export CGO_ENABLED = $(if $(filter darwin,$(GOOS_HOST)),1,0)
build-dock: ## Build bin/dock (desktop launcher tray app; no libtile57)
go build -ldflags "$(if $(filter windows,$(GOOS_HOST)),-H=windowsgui )-X main.version=$(VERSION)" -o bin/dock$(EXE) ./cmd/dock
@echo → bin/dock$(EXE)

# Quick cross-platform test builds. CGO is off, so this is pure `go build` per
# target — fast cold, near-instant on re-runs thanks to the build cache. Stamps
# the same version as `build`; strips symbols (-s -w) and paths (-trimpath) like a
Expand All @@ -134,8 +207,19 @@ build-tile57: build ## Alias for `build` (libtile57 is the sole engine now)
# (Security/CoreFoundation) that Zig doesn't bundle. The S-101 catalogue lives in
# libtile57, so there's no embed step. Fetches the ./tile57 submodule on demand + Zig 0.16.
# Outputs dist/chartplotter_<os>_<arch>[.exe].
xbuild xbuild-tile57: $(TILE57)/include/tile57.h ## Cross-compile CGO+libtile57 binaries with zig cc (linux+windows; darwin builds on a Mac runner)
xbuild xbuild-tile57: check-engine-override $(TILE57)/include/tile57.h ## Cross-compile CGO+libtile57 binaries with zig cc (linux+windows; darwin builds on a Mac runner)
VERSION="$(VERSION)" TILE57="$(TILE57)" ENGINE_COMMIT="$(ENGINE_COMMIT)" scripts/xbuild-tile57.sh
VERSION="$(VERSION)" scripts/xbuild-dock.sh

# Dock-only cross-builds (pure Go, no Zig): dist/dock_<os>_<arch>[.exe] plus the
# Linux desktop-integration files. darwin: see package-macos.
xbuild-dock: ## Cross-compile the dock launcher for linux+windows into dist/
VERSION="$(VERSION)" scripts/xbuild-dock.sh

# Assemble dist/ChartPlotter.app (+ zip) natively on macOS: engine + dock +
# Info.plist (LSUIElement) + AppIcon.icns, ad-hoc signed (IDENTITY= to override).
package-macos: ## Build ChartPlotter.app + zip into dist/ (run on macOS)
VERSION="$(VERSION)" scripts/macos-app.sh

serve: build ## Serve the web frontend + provisioning API on :8080 (HOST/PORT/ASSETS overridable)
$(BIN) serve --host $(HOST) --port $(PORT) --assets $(ASSETS)
Expand Down Expand Up @@ -321,7 +405,7 @@ tidy:
go mod tidy

clean:
rm -rf bin dist
$(RM_BUILD_DIRS)

clear-cache: ## Delete the provisioning cache (region zips, baked .pmtiles, charts-user, cell cache) for a clean slate
rm -rf "$(CACHE)"
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,19 @@ bin/chartplotter version
statically linking libtile57); [CLAUDE.md](CLAUDE.md) and the
[Makefile](Makefile) describe the build contract.

### Building on Windows

The same recipe works natively on Windows — you need **GNU Make**
(`winget install ezwinports.make`, or via scoop/choco) plus the Go/Zig/git
requirements above; no MinGW or MSVC install, Zig doubles as the C compiler.
The core targets (`make build`, `make build-dock`, `make tile57-lib`,
`make serve`, `make test`, `make vet`, `make clean`) run from PowerShell, cmd,
or Git Bash alike — their recipes avoid POSIX-shell syntax, so it doesn't
matter which shell make picks. Outputs gain the `.exe` suffix
(`bin\chartplotter.exe`, `bin\dock.exe`). The remaining targets (baking,
demo/docs bundles, `make xbuild`) drive bash scripts — run those from WSL or
Git Bash.

## 🚀 Get started

The frontend is built into the binary, so one file is all you need. Start the
Expand Down
Binary file added cmd/dock/appicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions cmd/dock/errno_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build !windows

package main

import "syscall"

// errConnRefused is the errno a dial to a closed local port unwraps to.
var errConnRefused error = syscall.ECONNREFUSED
14 changes: 14 additions & 0 deletions cmd/dock/errno_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build windows

package main

import "golang.org/x/sys/windows"

// errConnRefused is the errno a dial to a closed local port unwraps to.
// errors.Is(err, syscall.ECONNREFUSED) never matches on Windows — Go defines
// that constant as a synthetic APPLICATION_ERROR value, while Winsock dials
// actually fail with WSAECONNREFUSED (10061, absent from stdlib syscall) — so
// match the real thing. Without this the probe classified every refused port
// as "taken by another server" and the dock exhausted 8080..8090 without ever
// spawning the server.
var errConnRefused error = windows.WSAECONNREFUSED
97 changes: 97 additions & 0 deletions cmd/dock/gen_icons.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//go:build ignore

// gen_icons.go regenerates the dock's icon assets: a monochrome compass rose
// (placeholder until an OpenBridge asset replaces it) as 32px tray glyphs — PNG
// for macOS/Linux, PNG-in-ICO for Windows, plus an error-badged variant — and a
// 512px appicon.png that packaging turns into the macOS AppIcon.icns and the
// Linux .desktop icon. Run with `go generate ./cmd/dock`.
package main

import (
"bytes"
"encoding/binary"
"image"
"image/color"
"image/png"
"math"
"os"
)

func glyph(size int, badge bool) *image.NRGBA {
img := image.NewNRGBA(image.Rect(0, 0, size, size))
s := float64(size) / 32 // all geometry is designed at 32px and scaled
c := float64(size) / 2
set := func(x, y int, a float64) {
if a <= 0 {
return
}
if a > 1 {
a = 1
}
img.SetNRGBA(x, y, color.NRGBA{0, 0, 0, uint8(a * 255)})
}
for y := 0; y < size; y++ {
for x := 0; x < size; x++ {
fx, fy := float64(x)+0.5-c, float64(y)+0.5-c
r := math.Hypot(fx, fy)
// Outer ring at radius 13.5, softened for AA.
a := 1.5 - math.Abs(r-13.5*s)/(1.1*s)
// Four-point star: pinched diamond |x·y| ≤ k, |x|+|y| ≤ L.
if math.Abs(fx*fy) <= 14*s*s && math.Abs(fx)+math.Abs(fy) <= 11*s {
a = 1
}
set(x, y, a)
}
}
if badge {
// Filled disc, bottom-right, with a punched-out gap so the badge reads.
for y := 0; y < size; y++ {
for x := 0; x < size; x++ {
d := math.Hypot(float64(x)-25.5*s, float64(y)-25.5*s)
switch {
case d <= 5.5*s:
img.SetNRGBA(x, y, color.NRGBA{0, 0, 0, 255})
case d <= 7.5*s:
img.SetNRGBA(x, y, color.NRGBA{})
}
}
}
}
return img
}

func encode(size int, badge bool) []byte {
var b bytes.Buffer
if err := png.Encode(&b, glyph(size, badge)); err != nil {
panic(err)
}
return b.Bytes()
}

// ico wraps one PNG in a single-image ICO container (valid since Vista).
func ico(pngData []byte, size int) []byte {
var b bytes.Buffer
binary.Write(&b, binary.LittleEndian, []uint16{0, 1, 1}) // ICONDIR
b.Write([]byte{byte(size), byte(size), 0, 0}) // w, h, colors, reserved
binary.Write(&b, binary.LittleEndian, []uint16{1, 32}) // planes, bpp
binary.Write(&b, binary.LittleEndian, uint32(len(pngData))) // data size
binary.Write(&b, binary.LittleEndian, uint32(6+16)) // data offset
b.Write(pngData)
return b.Bytes()
}

func write(name string, data []byte) {
if err := os.WriteFile(name, data, 0o644); err != nil {
panic(err)
}
}

func main() {
tray := encode(32, false)
trayErr := encode(32, true)
write("icon.png", tray)
write("icon.ico", ico(tray, 32))
write("icon_error.png", trayErr)
write("icon_error.ico", ico(trayErr, 32))
write("appicon.png", encode(512, false))
}
Loading
Loading