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
1 change: 1 addition & 0 deletions .claude/skills/fieldworks-winapp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ verifies that launch will succeed.
- `scripts/Set-FieldWorksLegacyMode.ps1`: forces `UIMode=Legacy` — run before EVERY launch.
- `scripts/Resolve-FieldWorksDevRegistry.ps1`: aligns the dev registry (`RootCodeDir`/`RootDataDir`) to
this worktree before launch; auto-realigns when the other worktree is idle, else prints `RESULT=ASK_USER`.
Current builds anchor on their own source tree, so this now matters mainly for older builds.
- `references/headless-rendering.md`: why FieldWorks needs a display-bound desktop; what works/doesn't for
invisible capture (winforms-mcp HEADLESS does NOT render FieldWorks; a Virtual Display Driver was tried
and abandoned — see the doc for why; use visible capture, or RDP for true invisibility). Starts with the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ returns empty and `winforms_take_screenshot` is blank even though the process is
when the other worktree is NOT active (no FieldWorks.exe running from it) and was NOT used in the last 24h
(registry key write time). If it prints `RESULT=ASK_USER`, the other worktree may be active — **ask the
user** before realigning, then re-run with `-Force` if they approve.
Since `FwDirectoryFinder` learned to anchor on the source tree it runs from, an exe built from a current
worktree already reads its own `DistFiles`; keep running the script for older builds, and for the other
registry values (`ProjectsDir`), which are still shared across worktrees.

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

Expand Down
71 changes: 55 additions & 16 deletions Src/Common/FwUtils/FwDirectoryFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,25 +315,62 @@ string defaultDir
ResourceHelper.GetResourceString("kstidInvalidInstallation")
);
}
// Hundreds of callers of this method are using Path.Combine with the results.
// Combine only works with a root directory if it is followed by \ (e.g., c:\)
// so we don't want to trim the \ in this situation.
return TidyRootDir(rootDir);
}

/// <summary>
/// Strips the trailing separator that hundreds of callers would otherwise pass on to
/// Path.Combine, except on a root directory (e.g. c:\), where Combine needs it.
/// </summary>
private static string TidyRootDir(string rootDir)
{
string dir = rootDir.TrimEnd(
Path.DirectorySeparatorChar,
Path.AltDirectorySeparatorChar
);
return dir.Length > 2 ? dir : dir + Path.DirectorySeparatorChar;
}

/// <summary>The file that marks the root of a FieldWorks source tree.</summary>
private const string ksSolutionFilename = "FieldWorks.sln";

/// <summary>Set this to let the registry name the directories again.</summary>
private const string ksUseRegistryDirsVariable = "FW_USE_REGISTRY_DIRS";

/// <summary>
/// Gets the DistFiles folder of the source tree that <paramref name="startDirectory"/>
/// lies in, or <c>null</c> if it lies outside a source tree (the installed case).
/// </summary>
/// <remarks>
/// Walking up to the tree root, rather than assuming a fixed depth, finds
/// DistFiles from Output/&lt;Configuration&gt;, from its architecture
/// subfolders, and from a project's own bin folder alike.
/// </remarks>
/// <param name="startDirectory">The directory to start searching upwards from.</param>
public static string FindDevDistFiles(string startDirectory)
{
for (
string dir = startDirectory;
!string.IsNullOrEmpty(dir);
dir = Path.GetDirectoryName(dir)
)
{
string distFiles = Path.Combine(dir, "DistFiles");
// The solution file is what keeps an installed FieldWorks from matching here.
if (Directory.Exists(distFiles)
&& File.Exists(Path.Combine(dir, ksSolutionFilename)))
return distFiles;
}
return null;
}

private static string GetDevDistFilesPath()
{
if (EnvironmentVariables.IsTrue(ksUseRegistryDirsVariable))
return null;

string assemblyDir = Path.GetDirectoryName(FileUtils.StripFilePrefix(Assembly.GetExecutingAssembly().CodeBase));
// Check if we are in Output/Debug or Output/Release
// DistFiles is at ../../DistFiles
string distFiles = Path.GetFullPath(Path.Combine(assemblyDir, "..", "..", "DistFiles"));
if (Directory.Exists(distFiles))
return distFiles;
return null;
return FindDevDistFiles(assemblyDir);
}

/// ------------------------------------------------------------------------------------
Expand All @@ -349,16 +386,17 @@ public static string CodeDirectory
{
get
{
// The tree the assembly runs from owns its DistFiles, so it beats the registry.
string devDistFiles = GetDevDistFilesPath();
if (devDistFiles != null)
return TidyRootDir(devDistFiles);

string defaultDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
CompanyName,
$"FieldWorks {FwUtils.SuiteVersion}"
);

string devDistFiles = GetDevDistFilesPath();
if (devDistFiles != null)
defaultDir = devDistFiles;

return GetDirectory("RootCodeDir", defaultDir);
}
}
Expand All @@ -377,11 +415,12 @@ public static string DataDirectory
{
get
{
string defaultDir = Path.Combine(LcmFileHelper.CommonApplicationData, CompanyName, ksFieldWorks);

// See CodeDirectory: the running tree outranks the shared registry.
string devDistFiles = GetDevDistFilesPath();
if (devDistFiles != null)
defaultDir = devDistFiles;
return TidyRootDir(devDistFiles);

string defaultDir = Path.Combine(LcmFileHelper.CommonApplicationData, CompanyName, ksFieldWorks);

return GetDirectory(
ksRootDataDir,
Expand Down
90 changes: 90 additions & 0 deletions Src/Common/FwUtils/FwUtilsTests/FwDirectoryFinderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,96 @@ public void CodeDirectory()
Assert.That(FwDirectoryFinder.CodeDirectory, Is.SamePath(currentDir));
}

///-------------------------------------------------------------------------------------
/// <summary>
/// Tests that FindDevDistFiles locates DistFiles from anywhere inside a source tree,
/// not only from the Output/&lt;Configuration&gt; folder two levels below its root.
/// </summary>
///-------------------------------------------------------------------------------------
[TestCase("Output/Debug")]
[TestCase("Output/Debug/x64")]
[TestCase("Src/Common/FwUtils/bin/Debug/net8.0")]
public void FindDevDistFiles_InsideSourceTree_FindsTreeDistFiles(string startSubDirectory)
{
var treeRoot = CreateFakeSourceTree(withSolutionFile: true);
try
{
var startDir = Directory.CreateDirectory(Path.Combine(treeRoot, startSubDirectory)).FullName;

Assert.That(FwDirectoryFinder.FindDevDistFiles(startDir),
Is.SamePath(Path.Combine(treeRoot, "DistFiles")));
}
finally
{
Directory.Delete(treeRoot, true);
}
}

///-------------------------------------------------------------------------------------
/// <summary>
/// Tests that FindDevDistFiles ignores a DistFiles folder that is not part of a source
/// tree, which is what keeps an installed FieldWorks on its registry directories.
/// </summary>
///-------------------------------------------------------------------------------------
[Test]
public void FindDevDistFiles_OutsideSourceTree_ReturnsNull()
{
var installRoot = CreateFakeSourceTree(withSolutionFile: false);
try
{
var startDir = Directory.CreateDirectory(Path.Combine(installRoot, "Output", "Debug")).FullName;

Assert.That(FwDirectoryFinder.FindDevDistFiles(startDir), Is.Null);
}
finally
{
Directory.Delete(installRoot, true);
}
}

///-------------------------------------------------------------------------------------
/// <summary>
/// Tests that the source tree the assembly runs from wins over a registry value naming
/// another tree, so that worktrees do not read each other's DistFiles.
/// </summary>
///-------------------------------------------------------------------------------------
[TestCase("RootCodeDir")]
[TestCase("RootDataDir")]
public void CodeAndDataDirectory_PreferSourceTreeOverRegistry(string registryValueName)
{
var expectedDir = Path.GetFullPath(Path.Combine(UtilsAssemblyDir, "../../DistFiles"));
using (var fwHKCU = FwRegistryHelper.FieldWorksRegistryKey)
{
var originalValue = fwHKCU.GetValue(registryValueName);
fwHKCU.SetValue(registryValueName, Path.Combine(Path.GetTempPath(), "SomeOtherWorktree", "DistFiles"));
try
{
Assert.That(FwDirectoryFinder.CodeDirectory, Is.SamePath(expectedDir));
Assert.That(FwDirectoryFinder.DataDirectory, Is.SamePath(expectedDir));
}
finally
{
fwHKCU.SetValue(registryValueName, originalValue);
}
}
}

///-------------------------------------------------------------------------------------
/// <summary>
/// Creates a throw-away directory holding a DistFiles folder, and the solution file that
/// marks a source tree unless <paramref name="withSolutionFile"/> says otherwise.
/// </summary>
///-------------------------------------------------------------------------------------
private static string CreateFakeSourceTree(bool withSolutionFile)
{
var root = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(),
"FwDirectoryFinderTests", Guid.NewGuid().ToString("N"))).FullName;
Directory.CreateDirectory(Path.Combine(root, "DistFiles"));
if (withSolutionFile)
File.WriteAllText(Path.Combine(root, "FieldWorks.sln"), string.Empty);
return root;
}

/// <summary>
/// Verify that the user project key falls back to the local machine.
/// </summary>
Expand Down
Loading