Skip to content

Commit 2939a25

Browse files
johnml1135claude
andcommitted
Anchor dev code and data directories on the running source tree
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>
1 parent caeeeb1 commit 2939a25

4 files changed

Lines changed: 149 additions & 16 deletions

File tree

.claude/skills/fieldworks-winapp/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ verifies that launch will succeed.
120120
- `scripts/Set-FieldWorksLegacyMode.ps1`: forces `UIMode=Legacy` — run before EVERY launch.
121121
- `scripts/Resolve-FieldWorksDevRegistry.ps1`: aligns the dev registry (`RootCodeDir`/`RootDataDir`) to
122122
this worktree before launch; auto-realigns when the other worktree is idle, else prints `RESULT=ASK_USER`.
123+
Current builds anchor on their own source tree, so this now matters mainly for older builds.
123124
- `references/headless-rendering.md`: why FieldWorks needs a display-bound desktop; what works/doesn't for
124125
invisible capture (winforms-mcp HEADLESS does NOT render FieldWorks; a Virtual Display Driver was tried
125126
and abandoned — see the doc for why; use visible capture, or RDP for true invisibility). Starts with the

.claude/skills/fieldworks-winapp/navigation/launch-or-attach.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ returns empty and `winforms_take_screenshot` is blank even though the process is
2828
when the other worktree is NOT active (no FieldWorks.exe running from it) and was NOT used in the last 24h
2929
(registry key write time). If it prints `RESULT=ASK_USER`, the other worktree may be active — **ask the
3030
user** before realigning, then re-run with `-Force` if they approve.
31+
Since `FwDirectoryFinder` learned to anchor on the source tree it runs from, an exe built from a current
32+
worktree already reads its own `DistFiles`; keep running the script for older builds, and for the other
33+
registry values (`ProjectsDir`), which are still shared across worktrees.
3134

3235
See the script headers and `../references/mcp-setup.md`.
3336

Src/Common/FwUtils/FwDirectoryFinder.cs

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -315,25 +315,62 @@ string defaultDir
315315
ResourceHelper.GetResourceString("kstidInvalidInstallation")
316316
);
317317
}
318-
// Hundreds of callers of this method are using Path.Combine with the results.
319-
// Combine only works with a root directory if it is followed by \ (e.g., c:\)
320-
// so we don't want to trim the \ in this situation.
318+
return TidyRootDir(rootDir);
319+
}
320+
321+
/// <summary>
322+
/// Strips the trailing separator that hundreds of callers would otherwise pass on to
323+
/// Path.Combine, except on a root directory (e.g. c:\), where Combine needs it.
324+
/// </summary>
325+
private static string TidyRootDir(string rootDir)
326+
{
321327
string dir = rootDir.TrimEnd(
322328
Path.DirectorySeparatorChar,
323329
Path.AltDirectorySeparatorChar
324330
);
325331
return dir.Length > 2 ? dir : dir + Path.DirectorySeparatorChar;
326332
}
327333

334+
/// <summary>The file that marks the root of a FieldWorks source tree.</summary>
335+
private const string ksSolutionFilename = "FieldWorks.sln";
336+
337+
/// <summary>Set this to let the registry name the directories again.</summary>
338+
private const string ksUseRegistryDirsVariable = "FW_USE_REGISTRY_DIRS";
339+
340+
/// <summary>
341+
/// Gets the DistFiles folder of the source tree that <paramref name="startDirectory"/>
342+
/// lies in, or <c>null</c> if it lies outside a source tree (the installed case).
343+
/// </summary>
344+
/// <remarks>
345+
/// Walking up to the tree root, rather than assuming a fixed depth, finds
346+
/// DistFiles from Output/&lt;Configuration&gt;, from its architecture
347+
/// subfolders, and from a project's own bin folder alike.
348+
/// </remarks>
349+
/// <param name="startDirectory">The directory to start searching upwards from.</param>
350+
public static string FindDevDistFiles(string startDirectory)
351+
{
352+
for (
353+
string dir = startDirectory;
354+
!string.IsNullOrEmpty(dir);
355+
dir = Path.GetDirectoryName(dir)
356+
)
357+
{
358+
string distFiles = Path.Combine(dir, "DistFiles");
359+
// The solution file is what keeps an installed FieldWorks from matching here.
360+
if (Directory.Exists(distFiles)
361+
&& File.Exists(Path.Combine(dir, ksSolutionFilename)))
362+
return distFiles;
363+
}
364+
return null;
365+
}
366+
328367
private static string GetDevDistFilesPath()
329368
{
369+
if (EnvironmentVariables.IsTrue(ksUseRegistryDirsVariable))
370+
return null;
371+
330372
string assemblyDir = Path.GetDirectoryName(FileUtils.StripFilePrefix(Assembly.GetExecutingAssembly().CodeBase));
331-
// Check if we are in Output/Debug or Output/Release
332-
// DistFiles is at ../../DistFiles
333-
string distFiles = Path.GetFullPath(Path.Combine(assemblyDir, "..", "..", "DistFiles"));
334-
if (Directory.Exists(distFiles))
335-
return distFiles;
336-
return null;
373+
return FindDevDistFiles(assemblyDir);
337374
}
338375

339376
/// ------------------------------------------------------------------------------------
@@ -349,16 +386,17 @@ public static string CodeDirectory
349386
{
350387
get
351388
{
389+
// The tree the assembly runs from owns its DistFiles, so it beats the registry.
390+
string devDistFiles = GetDevDistFilesPath();
391+
if (devDistFiles != null)
392+
return TidyRootDir(devDistFiles);
393+
352394
string defaultDir = Path.Combine(
353395
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
354396
CompanyName,
355397
$"FieldWorks {FwUtils.SuiteVersion}"
356398
);
357399

358-
string devDistFiles = GetDevDistFilesPath();
359-
if (devDistFiles != null)
360-
defaultDir = devDistFiles;
361-
362400
return GetDirectory("RootCodeDir", defaultDir);
363401
}
364402
}
@@ -377,11 +415,12 @@ public static string DataDirectory
377415
{
378416
get
379417
{
380-
string defaultDir = Path.Combine(LcmFileHelper.CommonApplicationData, CompanyName, ksFieldWorks);
381-
418+
// See CodeDirectory: the running tree outranks the shared registry.
382419
string devDistFiles = GetDevDistFilesPath();
383420
if (devDistFiles != null)
384-
defaultDir = devDistFiles;
421+
return TidyRootDir(devDistFiles);
422+
423+
string defaultDir = Path.Combine(LcmFileHelper.CommonApplicationData, CompanyName, ksFieldWorks);
385424

386425
return GetDirectory(
387426
ksRootDataDir,

Src/Common/FwUtils/FwUtilsTests/FwDirectoryFinderTests.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,96 @@ public void CodeDirectory()
6565
Assert.That(FwDirectoryFinder.CodeDirectory, Is.SamePath(currentDir));
6666
}
6767

68+
///-------------------------------------------------------------------------------------
69+
/// <summary>
70+
/// Tests that FindDevDistFiles locates DistFiles from anywhere inside a source tree,
71+
/// not only from the Output/&lt;Configuration&gt; folder two levels below its root.
72+
/// </summary>
73+
///-------------------------------------------------------------------------------------
74+
[TestCase("Output/Debug")]
75+
[TestCase("Output/Debug/x64")]
76+
[TestCase("Src/Common/FwUtils/bin/Debug/net8.0")]
77+
public void FindDevDistFiles_InsideSourceTree_FindsTreeDistFiles(string startSubDirectory)
78+
{
79+
var treeRoot = CreateFakeSourceTree(withSolutionFile: true);
80+
try
81+
{
82+
var startDir = Directory.CreateDirectory(Path.Combine(treeRoot, startSubDirectory)).FullName;
83+
84+
Assert.That(FwDirectoryFinder.FindDevDistFiles(startDir),
85+
Is.SamePath(Path.Combine(treeRoot, "DistFiles")));
86+
}
87+
finally
88+
{
89+
Directory.Delete(treeRoot, true);
90+
}
91+
}
92+
93+
///-------------------------------------------------------------------------------------
94+
/// <summary>
95+
/// Tests that FindDevDistFiles ignores a DistFiles folder that is not part of a source
96+
/// tree, which is what keeps an installed FieldWorks on its registry directories.
97+
/// </summary>
98+
///-------------------------------------------------------------------------------------
99+
[Test]
100+
public void FindDevDistFiles_OutsideSourceTree_ReturnsNull()
101+
{
102+
var installRoot = CreateFakeSourceTree(withSolutionFile: false);
103+
try
104+
{
105+
var startDir = Directory.CreateDirectory(Path.Combine(installRoot, "Output", "Debug")).FullName;
106+
107+
Assert.That(FwDirectoryFinder.FindDevDistFiles(startDir), Is.Null);
108+
}
109+
finally
110+
{
111+
Directory.Delete(installRoot, true);
112+
}
113+
}
114+
115+
///-------------------------------------------------------------------------------------
116+
/// <summary>
117+
/// Tests that the source tree the assembly runs from wins over a registry value naming
118+
/// another tree, so that worktrees do not read each other's DistFiles.
119+
/// </summary>
120+
///-------------------------------------------------------------------------------------
121+
[TestCase("RootCodeDir")]
122+
[TestCase("RootDataDir")]
123+
public void CodeAndDataDirectory_PreferSourceTreeOverRegistry(string registryValueName)
124+
{
125+
var expectedDir = Path.GetFullPath(Path.Combine(UtilsAssemblyDir, "../../DistFiles"));
126+
using (var fwHKCU = FwRegistryHelper.FieldWorksRegistryKey)
127+
{
128+
var originalValue = fwHKCU.GetValue(registryValueName);
129+
fwHKCU.SetValue(registryValueName, Path.Combine(Path.GetTempPath(), "SomeOtherWorktree", "DistFiles"));
130+
try
131+
{
132+
Assert.That(FwDirectoryFinder.CodeDirectory, Is.SamePath(expectedDir));
133+
Assert.That(FwDirectoryFinder.DataDirectory, Is.SamePath(expectedDir));
134+
}
135+
finally
136+
{
137+
fwHKCU.SetValue(registryValueName, originalValue);
138+
}
139+
}
140+
}
141+
142+
///-------------------------------------------------------------------------------------
143+
/// <summary>
144+
/// Creates a throw-away directory holding a DistFiles folder, and the solution file that
145+
/// marks a source tree unless <paramref name="withSolutionFile"/> says otherwise.
146+
/// </summary>
147+
///-------------------------------------------------------------------------------------
148+
private static string CreateFakeSourceTree(bool withSolutionFile)
149+
{
150+
var root = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(),
151+
"FwDirectoryFinderTests", Guid.NewGuid().ToString("N"))).FullName;
152+
Directory.CreateDirectory(Path.Combine(root, "DistFiles"));
153+
if (withSolutionFile)
154+
File.WriteAllText(Path.Combine(root, "FieldWorks.sln"), string.Empty);
155+
return root;
156+
}
157+
68158
/// <summary>
69159
/// Verify that the user project key falls back to the local machine.
70160
/// </summary>

0 commit comments

Comments
 (0)