Skip to content

fix: stop make run recompiling the dictionary on every source edit - #40

Merged
widgetii merged 2 commits into
mainfrom
fix/skip-dictionary-rebuild-on-run
Oct 20, 2026
Merged

widgetii merged 2 commits into
mainfrom
fix/skip-dictionary-rebuild-on-run

Conversation

@widgetii

Copy link
Copy Markdown
Member

The problem

make run recompiled the bundled dictionary whenever any Swift file under
HanReaderCore, HanReaderPersistence or hanreader-dictgen was newer than the
container. Those are the three modules the generator links, so the rule named
every .swift file in all of them — and during ordinary work on the reader or
the listening core, that is every edit. None of those edits can change the
container's bytes.

Measured on this machine:

Before After
No-op make generate 0.1 s 0.4 s
After touching one unrelated core file 16 s 0.4 s
…when the release build also had to recompile 35 s 0.4 s

The fix

The dictionary is built from generate only when it is missing, through a
test in the recipe:

@[ -f $(CEDICT_OUTPUT) ] || $(MAKE) --no-print-directory dict

An order-only prerequisite (generate: … | $(CEDICT_OUTPUT)) 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. That was
measured too, not assumed.

Why this is still safe

  • make dict keeps the full strict dependency list, unchanged.
  • CI's Bundled dictionary job runs make dict, so a container that does not
    match its sources fails there regardless of what a developer has locally.
  • A fresh clone has no container, so make run builds it — which is also what
    the app-build CI jobs do through make generate.
  • make run and make run-ios now print a line when the container on disk is
    older than its sources, so a stale one is visible rather than silent:
note: the bundled dictionary is older than Sources/HanReaderCore/… -- run `make dict` to rebuild it.

Verified

Each path exercised directly: no-op, after an unrelated core edit (notice fires,
no rebuild), with the container deleted (rebuilt), and make dict on its own
(still 15 s of real work).

🤖 Generated with Claude Code

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 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Avoid rebuilding the dictionary on routine app runs

🐞 Bug fix ✨ Enhancement 🕐 Less than 10 minutes

Grey Divider

AI Description

• Build the bundled dictionary during project generation only when it is missing.
• Warn on macOS and iOS runs when dictionary sources are newer than the container.
• Preserve strict rebuild checks through make dict and CI.
Diagram

graph TD
  G["Project generation"] --> E{"Dictionary missing?"}
  E -->|yes| D["make dict"] --> R["Run targets"]
  E -->|no| R
  R --> S{"Sources newer?"}
  S -->|yes| N["Staleness note"] --> B["App build"]
  S -->|no| B
Loading
High-Level Assessment

The missing-file recipe check fits the intended split between fast local runs and strict make dict validation in CI. An order-only prerequisite would still rebuild a stale container, so it would not solve the reported delay.

Files changed (1) +22 / -1

Bug fix (1) +22 / -1
MakefileBuild missing dictionaries and flag stale ones during runs +22/-1

Build missing dictionaries and flag stale ones during runs

• Removes the dictionary container as a prerequisite of 'generate' and invokes 'make dict' from its recipe only when the container is absent. Adds source-timestamp notices to 'run' and 'run-ios'; the strict dependencies of 'make dict' remain unchanged.

Makefile

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Apps reuse a dictionary that failed checks ✓ Resolved
Description
The generate recipe treats an existing dictionary as ready even when make dict wrote it but
subsequently failed its verify step. Because the output is newer than its sources, a later `make
run or make run-ios` skips verification, prints no stale-data note, and bundles the unchecked
file.
Code

Makefile[126]

+	@[ -f $(CEDICT_OUTPUT) ] || $(MAKE) --no-print-directory dict
Evidence
The dictionary recipe writes its output before running verification. Verification includes checks
that can fail after the write; the new existence test then bypasses the recipe, while the new notice
compares only modification times.

Makefile[94-98]
Sources/hanreader-dictgen/VerifyCommand.swift[55-91]
Makefile[125-126]
Makefile[147-151]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed verification can leave a newly written dictionary that `generate` subsequently accepts without warning.
## Fix Focus Areas
- Makefile[94-98]
- Makefile[125-126]
## Recommended Fix
Build and verify a temporary container, then move it to `$(CEDICT_OUTPUT)` only after verification succeeds. Ensure failure removes the temporary file and does not leave an unverified output.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Missing snapshots stop runs without guidance ✓ Resolved
Description
The new stale-data check passes every dictionary dependency to find, discards its errors, and runs
under pipefail. If the compiled dictionary exists but the pinned source snapshot is missing,
generate skips the dictionary rule and make run or make run-ios exits at this check before
that rule can print its recovery instructions.
Code

Makefile[148]

+	@stale=$$(find $(CEDICT_DEPS) -newer $(CEDICT_OUTPUT) 2>/dev/null | head -1); \
Evidence
The source rule has a specific missing-snapshot diagnostic, but the new existence test skips it when
the output remains. The new find pipelines fail on a missing path, and the configured shell flags
make that failure terminate the recipe.

Makefile[11-12]
Makefile[76-82]
Makefile[90-92]
Makefile[125-126]
Makefile[148-151]
Makefile[162-165]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The stale-data check fails without guidance when a dependency is absent but the compiled dictionary remains.
## Fix Focus Areas
- Makefile[76-82]
- Makefile[125-126]
- Makefile[148-151]
- Makefile[162-165]
## Recommended Fix
Check for missing required dictionary inputs before the stale-data comparison and print the existing snapshot recovery instructions when appropriate. Apply the same handling to both run targets rather than suppressing a `find` error.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can open the rule or relationship behind a finding in one click

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread Makefile
Comment thread Makefile Outdated
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 <noreply@anthropic.com>
@widgetii
widgetii merged commit 37eb1a5 into main Oct 20, 2026
8 checks passed
@widgetii
widgetii deleted the fix/skip-dictionary-rebuild-on-run branch October 20, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant