From 041bd502c92a7c4db9bba920d451560721d3d38a Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:21:28 +0300 Subject: [PATCH 1/2] fix: stop `make run` recompiling the dictionary on every source edit Measured: a no-op `make run` was 0.1s, but touching one Swift file anywhere in HanReaderCore, HanReaderPersistence or the generator cost 15 seconds, and 35 when the release build had to recompile too. Those are the three modules `hanreader-dictgen` links, so the rule listed every `.swift` file in all of them -- and during ordinary work on the reader or the listening core, that is every edit. None of them can change the container's bytes. The dictionary is now built from `generate` only when it is **missing**, through a test in the recipe. An order-only prerequisite is the obvious reach and the wrong one: it stops the timestamp propagating to `generate` but still updates the container it names, so it changed nothing. `make dict` keeps the strict dependency and CI has a job of its own, so a stale container still cannot ship, and `make run` now says plainly when the one on disk is older than its sources. A fresh clone is unaffected: the container is missing, so it is built. Co-Authored-By: Claude Opus 5 --- Makefile | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e293509..aa7a32f 100644 --- a/Makefile +++ b/Makefile @@ -110,7 +110,20 @@ dict-force: ## Recompile the bundled dictionary unconditionally # ── Xcode project ──────────────────────────────────────────────────────────── -generate: $(TOOLS_STAMP) $(CEDICT_OUTPUT) ## Regenerate the Xcode project from project.yml +# The dictionary is built here only when it is **missing**, through a test in +# the recipe rather than a prerequisite -- including an order-only one, which +# still updates the container it names and only stops the timestamp +# propagating. +# +# Running the app is the command that happens dozens of times an hour, and the +# generator links three whole modules: an edit to anything in HanReaderCore, +# HanReaderPersistence or the generator itself made every `make run` spend +# fifteen seconds recompiling a container whose bytes almost never change. +# `make dict` keeps the strict dependency and CI has a job of its own, so a +# stale container still cannot ship -- and `make run` says so when the one on +# disk is older than its sources. +generate: $(TOOLS_STAMP) ## Regenerate the Xcode project from project.yml + @[ -f $(CEDICT_OUTPUT) ] || $(MAKE) --no-print-directory dict @# One shell block on purpose. Make runs each recipe line in its own shell, @# so an early `exit 0` in a guard would end only that line and the @# following commands would still run. @@ -132,6 +145,10 @@ open: generate ## Generate the project and open it in Xcode # ── Build and run ──────────────────────────────────────────────────────────── run: generate ## Build and launch the macOS app + @stale=$$(find $(CEDICT_DEPS) -newer $(CEDICT_OUTPUT) 2>/dev/null | head -1); \ + [ -z "$$stale" ] || printf \ + 'note: the bundled dictionary is older than %s -- run `make dict` to rebuild it.\n' \ + "$$stale" @if [ ! -d $(PROJECT) ]; then \ printf 'No app target yet. Until milestone M1 PR 3, use: make test\n'; \ exit 1; \ @@ -142,6 +159,10 @@ run: generate ## Build and launch the macOS app @open "$(BUILD_DIR)/Build/Products/Debug/HanReader.app" run-ios: generate ## Build and launch the app in the iOS Simulator + @stale=$$(find $(CEDICT_DEPS) -newer $(CEDICT_OUTPUT) 2>/dev/null | head -1); \ + [ -z "$$stale" ] || printf \ + 'note: the bundled dictionary is older than %s -- run `make dict` to rebuild it.\n' \ + "$$stale" @# Captured before grepping. Piping into `grep -q` lets it close the pipe on @# the first match, simctl takes SIGPIPE, and under pipefail the probe reports @# failure -- telling a machine that HAS a runtime that it has none. From ca2d67a4fffc814782089a6afcba2d7ac3789549 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:05:37 +0300 Subject: [PATCH 2/2] fix: answer the review on the dictionary build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings, both real. A container that failed `verify` was left on disk. Compiling straight to the destination meant a failed check still produced a file newer than its sources, and the new existence test then saw a file, asked no further questions and bundled it. It is written beside the destination now and moved into place only once it has passed. The staleness notice could stop a build. `find` exits non-zero when the pinned snapshot is missing, and under this file's `-euo pipefail` that aborted the recipe — before the dictionary rule could print how to fetch the snapshot. Reproduced directly: `set -euo pipefail` plus a missing path aborts the shell at the assignment. A courtesy line must not be the thing that stops a build, so it cannot fail any more. Co-Authored-By: Claude Opus 5 --- Makefile | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index aa7a32f..f2aab60 100644 --- a/Makefile +++ b/Makefile @@ -91,11 +91,19 @@ CEDICT_DEPS := $(CEDICT_SOURCE) Package.swift Package.resolved \ $(shell find Sources/HanReaderCore Sources/HanReaderPersistence \ Sources/hanreader-dictgen -name '*.swift' 2>/dev/null) +# Written beside the destination and moved into place only once it has been +# verified. Compiling straight to the destination left a container that had +# failed its own check sitting on disk, newer than its sources -- so the next +# `make run` saw a file, asked no further questions and bundled it. $(CEDICT_OUTPUT): $(CEDICT_DEPS) @mkdir -p Resources/Generated + @rm -f "$(CEDICT_OUTPUT).tmp" @printf 'Compiling the bundled dictionary...\n' - @swift run -q -c release hanreader-dictgen cedict "$(CEDICT_SOURCE)" -o "$(CEDICT_OUTPUT)" --verbose - @swift run -q -c release hanreader-dictgen verify "$(CEDICT_OUTPUT)" + @swift run -q -c release hanreader-dictgen cedict "$(CEDICT_SOURCE)" \ + -o "$(CEDICT_OUTPUT).tmp" --verbose + @swift run -q -c release hanreader-dictgen verify "$(CEDICT_OUTPUT).tmp" \ + || { rm -f "$(CEDICT_OUTPUT).tmp"; exit 1; } + @mv -f "$(CEDICT_OUTPUT).tmp" "$(CEDICT_OUTPUT)" dict-check: ## Report whether the pinned CC-CEDICT snapshot is out of date @./Scripts/update-cedict.sh --check @@ -145,7 +153,11 @@ open: generate ## Generate the project and open it in Xcode # ── Build and run ──────────────────────────────────────────────────────────── run: generate ## Build and launch the macOS app - @stale=$$(find $(CEDICT_DEPS) -newer $(CEDICT_OUTPUT) 2>/dev/null | head -1); \ + @# `|| true` is load-bearing: a missing pinned snapshot makes `find` exit + @# non-zero, and under this file's `-euo pipefail` that aborted the recipe + @# before the dictionary rule could print how to fetch it. A courtesy line + @# must not be the thing that stops a build. + @stale=$$(find $(CEDICT_DEPS) -newer $(CEDICT_OUTPUT) 2>/dev/null | head -1 || true); \ [ -z "$$stale" ] || printf \ 'note: the bundled dictionary is older than %s -- run `make dict` to rebuild it.\n' \ "$$stale" @@ -159,7 +171,11 @@ run: generate ## Build and launch the macOS app @open "$(BUILD_DIR)/Build/Products/Debug/HanReader.app" run-ios: generate ## Build and launch the app in the iOS Simulator - @stale=$$(find $(CEDICT_DEPS) -newer $(CEDICT_OUTPUT) 2>/dev/null | head -1); \ + @# `|| true` is load-bearing: a missing pinned snapshot makes `find` exit + @# non-zero, and under this file's `-euo pipefail` that aborted the recipe + @# before the dictionary rule could print how to fetch it. A courtesy line + @# must not be the thing that stops a build. + @stale=$$(find $(CEDICT_DEPS) -newer $(CEDICT_OUTPUT) 2>/dev/null | head -1 || true); \ [ -z "$$stale" ] || printf \ 'note: the bundled dictionary is older than %s -- run `make dict` to rebuild it.\n' \ "$$stale"