Skip to content

Anchor dev code and data directories on the running source tree - #1093

Open
johnml1135 wants to merge 1 commit into
mainfrom
fix/dev-dir-anchoring
Open

Anchor dev code and data directories on the running source tree#1093
johnml1135 wants to merge 1 commit into
mainfrom
fix/dev-dir-anchoring

Conversation

@johnml1135

@johnml1135 johnml1135 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

A Debug build now reads the DistFiles of the source tree it was built in. Before this, it read whichever tree the shared registry value HKCU\SOFTWARE\SIL\FieldWorks\9\RootCodeDir happened to name — on the machine that prompted this, a build in the main repo was loading parts, layouts, and configuration from an unrelated worktree under .tmp/worktrees/.

The reviewer's question here is "what did this break for installed FieldWorks?", and the answer is nothing: the new probe only matches a directory that has both DistFiles and FieldWorks.sln beside it, which no install has. The question worth your time is whether making the source tree outrank the registry is the right default.

Where to look

  • CodeDirectory / DataDirectory now return before consulting the registry when the running assembly is inside a source tree — the deliberate behavior change; FW_USE_REGISTRY_DIRS=1 restores the old precedence.
  • FindDevDistFiles walks up instead of assuming <assembly>/../../DistFiles, so it also works from Output/Debug/x64 and from a project's own bin folder. Three layouts pinned by tests.
  • The installed case is pinned by its own test: DistFiles present, no solution file, result null.
  • TidyRootDir is extracted from GetDirectory so both paths normalize trailing separators identically — the ~100 Path.Combine callers see no difference.
  • ProjectsDirectory is untouched. ProjectsDir stays a shared user preference across worktrees.

Deliberately not here

  • Src/FwParatextLexiconPlugin/ParatextLexiconPluginDirectoryFinder.cs keeps its registry-only resolution; it runs inside Paratext, never from a source tree.
  • No log line or warning when an existing RootCodeDir override stops applying.
  • Build/mkall.targets still writes RootCodeDir/RootDataDir; a source-tree build now ignores them.

Verification

.\build.ps1 -CommentHygiene -BuildTests succeeded (0 warnings, 0 errors; comment-hygiene clean). .\test.ps1 -CommentHygiene -SkipNative -TestProject Src\Common\FwUtils\FwUtilsTests\FwUtilsTests.csproj — 407/407 passed, including 6 new cases. Not run: the full suite, native tests, installer validation. Not done: a manual two-worktree launch.


Reading this a year from now — start here

This started as a question, not a bug report: "my local Debug build still looks at DistFiles — could it look at Output instead?" The investigation said no to the literal request and yes to the problem behind it. Both halves are recorded below, because the rejected half is the one that will otherwise be re-proposed.

There were no working documents to delete; the reasoning never existed anywhere but here.

Decisions, and why

The registry loses to the source tree, rather than the probe merely being fixed. Fixing the anchoring alone would have changed nothing on a real dev machine: GetDevDistFilesPath() only ever fed defaultDir, and GetDirectory returns the registry value whenever it is non-empty. On a dev machine it is always non-empty — Build/mkall.targets (setKeysInHKCU) writes it, and so does the winapp skill's launch script. The value is a single machine-wide slot shared by every worktree, so it names whichever tree ran last. That is the actual defect; the fixed-depth probe is a second, independent one.

FieldWorks.sln as the tree marker. The probe needs something that exists in a source tree and never beside an install. The installer harvests DistFiles\**\* — the contents, into the install root — so an install has neither a DistFiles folder nor a solution file at that level. Requiring both makes the installed path unreachable by construction rather than by convention.

FW_USE_REGISTRY_DIRS as the escape hatch, read through EnvironmentVariables.IsTrue. Reuses the repo's existing opt-in convention rather than adding a new registry value, which would have reintroduced the shared-slot problem it exists to escape.

No memoization. Each CodeDirectory get now walks up doing Directory.Exists + File.Exists per level. The old path opened and read a registry key on every get, so this is not a regression, and a static cache would freeze FW_USE_REGISTRY_DIRS for any future test that sets it in-process. Revisit only with a measurement.

Paths not taken

Pointing the code directory at Output/<Configuration> — the literal request. Output holds build artifacts only; the code/data payload exists solely in DistFiles. Counted in the tree at the time: Language Explorer 10 entries in DistFiles vs absent from Output/Debug; Parts 4 vs absent; Helps 7 vs absent; Icu70 present vs absent; Templates 42 vs 1. FlexStylesPath, FlexFolder, TemplateDirectory, and EditorialChecksDirectory would all have broken.

An overlay that probes Output first, then falls back to DistFiles. This cannot be expressed through the current API: CodeDirectory returns one string that ~100 call sites Path.Combine onto. An overlay needs a ResolveCodeFile(relativePath) seam instead — a much larger change, for a duplication problem that does not currently exist (only Templates overlaps at all, with one entry).

Just fixing the registry and stopping there. That is what unblocked the reporter (Resolve-FieldWorksDevRegistry.ps1 -Force, run before this branch existed), and it is what every worktree switch will need again tomorrow. It treats the symptom.

Evidence

Precedence, before this changeGetDirectory(RegistryKey, string, string) in Src/Common/FwUtils/FwDirectoryFinder.cs: rootDir is read from the registry, and defaultDir is used only if (string.IsNullOrEmpty(rootDir)). GetDevDistFilesPath() fed defaultDir. Hence: registry set → probe irrelevant.

The shared slotBuild/mkall.targets target setKeysInHKCU writes RootCodeDir, RootDataDir, and ProjectsDir to HKCU\SOFTWARE\SIL\FieldWorks\$(FWMAJOR) from $(dir-fwdistfiles). Nothing in Src/ writes those two values at runtime (searching for SetValue("RootCodeDir" outside tests returns no hits), so the value persists from whichever tree last built or launched.

Existing tests keep passing for a non-trivial reasonFwDirectoryFinderTests sets the registry to UtilsAssemblyDir/../../DistFiles, and InitializeFwRegistryHelperAttribute does the same. Under the new precedence those values are ignored, but the walk-up returns the same path for a test run out of Output/Debug, so the assertions still hold.

New coverageFindDevDistFiles_InsideSourceTree_FindsTreeDistFiles (Output/Debug, Output/Debug/x64, Src/Common/FwUtils/bin/Debug/net8.0), FindDevDistFiles_OutsideSourceTree_ReturnsNull, and CodeAndDataDirectory_PreferSourceTreeOverRegistry (RootCodeDir, RootDataDir), which points the registry at a fabricated other worktree and asserts both directories still resolve to this tree.

Preflight review details

The preflight found no Critical issues and two Important open questions, neither of which was put to the author (the author pre-authorized commit, push, and PR in the same instruction that requested the fix, so no interview was held). They are open questions for the reviewer, not dismissed findings:

  1. Registry precedence is inverted for dev builds. Anyone who deliberately pointed RootCodeDir at a non-tree location silently loses that override. Mitigated by FW_USE_REGISTRY_DIRS; not mitigated by any notification that the override stopped applying.
  2. The tree marker is FieldWorks.sln. If the solution is renamed or removed, every dev build silently falls back to the registry/Program Files path with no diagnostic. A second marker or a build-time assertion would harden this.

Minor: no memoization (considered and rejected, see Decisions); the Paratext plugin's parallel finder now differs in policy and neither file mentions the other; the new precedence test restores a fixture-owned registry value and would throw if the fixture stopped setting it.

Build and test evidence is in the Verification section above. The first build attempt failed on an unrelated ILRepack file lock on Output\Debug\SIL.LCModel.Core.dll.config; the rerun was clean. gitlint --commits HEAD~1..HEAD is clean.


This change is Reviewable

FwDirectoryFinder found the dev DistFiles by assuming the running
assembly sat exactly two levels below the tree root, and then let
HKCU RootCodeDir/RootDataDir override whatever it found. So a build
run from any other output folder missed DistFiles entirely, and
every worktree read the DistFiles named by the shared registry
value, which belongs to whichever tree last ran the build or the
launch script.

FindDevDistFiles now walks up from the running assembly to the
directory holding both DistFiles and FieldWorks.sln, and that tree
wins over the registry. An installed FieldWorks has no solution file
beside it, so it keeps reading the registry as before. Set
FW_USE_REGISTRY_DIRS to opt a dev build back into the registry.

Tests cover the walk from Output/Debug, from an architecture
subfolder, and from a project bin folder; the installed case; and
the precedence of the source tree over a registry value naming
another worktree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

NUnit Tests

    1 files  ±0      1 suites  ±0   10m 51s ⏱️ -13s
5 837 tests +6  5 756 ✅ +6  81 💤 ±0  0 ❌ ±0 
5 846 runs  +6  5 765 ✅ +6  81 💤 ±0  0 ❌ ±0 

Results for commit 2939a25. ± Comparison against base commit caeeeb1.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.60870% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.31%. Comparing base (caeeeb1) to head (2939a25).

Files with missing lines Patch % Lines
Src/Common/FwUtils/FwDirectoryFinder.cs 82.60% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1093   +/-   ##
=======================================
  Coverage   38.31%   38.31%           
=======================================
  Files        1507     1507           
  Lines      350524   350538   +14     
  Branches    40288    40290    +2     
=======================================
+ Hits       134302   134310    +8     
- Misses     186990   186994    +4     
- Partials    29232    29234    +2     
Files with missing lines Coverage Δ
Src/Common/FwUtils/FwDirectoryFinder.cs 66.51% <82.60%> (-3.96%) ⬇️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

2 participants