From 6f825b2e396b362b648cbd572ba564bcb9719761 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 22 Oct 2024 14:18:42 +0700 Subject: [PATCH 1/2] Be less strict about FixFwData name and location (#410) On Linux, dotnet compiles binaries to filenames without .exe in them, so we should handle that scenario gracefully. Also, it's common to be running from a different directory than the one where FixFwData.exe is found (e.g., `dotnet run` in the project that calls LfMergeBridge), so we should also check the location of LfMergeBridge.dll since FixFwData will usually be installed alongside LfMergeBridge.dll. --- .../LanguageForgeSendReceiveActionHandler.cs | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/LfMergeBridge/LanguageForgeSendReceiveActionHandler.cs b/src/LfMergeBridge/LanguageForgeSendReceiveActionHandler.cs index 62df69933..346a7684e 100644 --- a/src/LfMergeBridge/LanguageForgeSendReceiveActionHandler.cs +++ b/src/LfMergeBridge/LanguageForgeSendReceiveActionHandler.cs @@ -30,15 +30,35 @@ namespace LfMergeBridge [Export(typeof(IBridgeActionTypeHandler))] internal sealed class LanguageForgeSendReceiveActionHandler : IBridgeActionTypeHandler { - private const string FwDataExe = "FixFwData.exe"; + private const string FwData = "FixFwData"; private const string syncBase = "Sync"; + private static string FindFwData() + { + // Try current working directory first + var cwd = Directory.GetCurrentDirectory(); + var withExe = Path.Combine(cwd, FwData + ".exe"); + if (File.Exists(withExe)) return withExe; + var withoutExe = Path.Combine(cwd, FwData); + if (File.Exists(withoutExe)) return withoutExe; + + // Then try loction of LfMergeBridge.dll, as FixFwData is probably installed alongside as a sister file + var assembly = Assembly.GetExecutingAssembly(); + var dir = Directory.GetParent(assembly.Location).FullName; + withExe = Path.Combine(dir, FwData + ".exe"); + if (File.Exists(withExe)) return withExe; + withoutExe = Path.Combine(dir, FwData); + if (File.Exists(withoutExe)) return withoutExe; + + return null; + } + /// /// Do a Send/Receive with the matching Language Depot project for the given Language Forge project's repository. /// /// This handler will *not* reset the workspace to another branch or long hash, /// since doing so would prevent any new changes in the fwdata file from being processed. - /// + /// /// If LF needs to sync with another commit (via its long hash), /// LF *must* first use the action handler "LanguageForgeUpdateToLongHashActionHandler", /// which will reset the workspace, and then LF can write new changes to the fwdata file, @@ -56,10 +76,10 @@ void IBridgeActionTypeHandler.StartWorking(IProgress progress, Dictionary Date: Thu, 14 Nov 2024 10:06:39 +0700 Subject: [PATCH 2/2] Command-line tools to split and/or reassemble .fwdata files (#403) These tools enable automated Send/Receive testing in LfMerge, and may allow future capabilities in Lexbox such as better history views. --- FLExBridge.sln | 6 ++ src/MkFwData/MkFwData.csproj | 30 +++++++ src/MkFwData/Program.cs | 151 +++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 src/MkFwData/MkFwData.csproj create mode 100644 src/MkFwData/Program.cs diff --git a/FLExBridge.sln b/FLExBridge.sln index 78a2e43ee..aef1ac96c 100644 --- a/FLExBridge.sln +++ b/FLExBridge.sln @@ -52,6 +52,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LfMergeBridgeTestApp", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LfMergeBridgeTests", "src\LfMergeBridgeTests\LfMergeBridgeTests.csproj", "{6CB1246D-956A-4759-AA13-D434CBB383FE}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MkFwData", "src\MkFwData\MkFwData.csproj", "{5CDB086A-79DE-4EF4-BB48-4AEAEEB0827B}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiftFileCheckerApp", "src\LiftFileCheckerApp\LiftFileCheckerApp.csproj", "{30AA046B-5E14-408C-89EF-8601BB27FB32}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibTriboroughBridge-ChorusPluginTests", "src\LibTriboroughBridge-ChorusPluginTests\LibTriboroughBridge-ChorusPluginTests.csproj", "{AA6CC4E2-6FD8-4B30-99EC-A446E9CAA176}" @@ -124,6 +126,10 @@ Global {6CB1246D-956A-4759-AA13-D434CBB383FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {6CB1246D-956A-4759-AA13-D434CBB383FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {6CB1246D-956A-4759-AA13-D434CBB383FE}.Release|Any CPU.Build.0 = Release|Any CPU + {5CDB086A-79DE-4EF4-BB48-4AEAEEB0827B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5CDB086A-79DE-4EF4-BB48-4AEAEEB0827B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5CDB086A-79DE-4EF4-BB48-4AEAEEB0827B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5CDB086A-79DE-4EF4-BB48-4AEAEEB0827B}.Release|Any CPU.Build.0 = Release|Any CPU {30AA046B-5E14-408C-89EF-8601BB27FB32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {30AA046B-5E14-408C-89EF-8601BB27FB32}.Debug|Any CPU.Build.0 = Debug|Any CPU {30AA046B-5E14-408C-89EF-8601BB27FB32}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/MkFwData/MkFwData.csproj b/src/MkFwData/MkFwData.csproj new file mode 100644 index 000000000..580f131a8 --- /dev/null +++ b/src/MkFwData/MkFwData.csproj @@ -0,0 +1,30 @@ + + + + + fwdata + Exe + net8.0 + net8.0 + enable + enable + true + $(MSBuildProjectDirectory) + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MkFwData/Program.cs b/src/MkFwData/Program.cs new file mode 100644 index 000000000..419e076be --- /dev/null +++ b/src/MkFwData/Program.cs @@ -0,0 +1,151 @@ +using Chorus.VcsDrivers.Mercurial; +using SIL.Progress; +using System.CommandLine; + +class Program +{ + static async Task Main(string[] args) + { + var rootCommand = new RootCommand("Make or split .fwdata file"); + + var verboseOption = new Option( + ["--verbose", "-v"], + "Display verbose output" + ); + rootCommand.AddGlobalOption(verboseOption); + + var quietOption = new Option( + ["--quiet", "-q"], + "Suppress all output (overrides --verbose if present)" + ); + rootCommand.AddGlobalOption(quietOption); + + var splitCommand = new Command("split", "Split .fwdata file (push Humpty off the wall)"); + var buildCommand = new Command("build", "Rebuild .fwdata file (put Humpty together again)"); + + rootCommand.Add(splitCommand); + rootCommand.Add(buildCommand); + + var filename = new Argument( + "file", + "Name of .fwdata file to create or split, or directory to create/split it in" + ); + splitCommand.Add(filename); + buildCommand.Add(filename); + + var hgRevOption = new Option( + ["--rev", "-r"], + "Revision to check out (default \"tip\")" + ); + hgRevOption.SetDefaultValue("tip"); + buildCommand.AddGlobalOption(hgRevOption); + + var timeoutOption = new Option( + ["--timeout", "-t"], + "Timeout in seconds for Hg commands (default 600)" + ); + timeoutOption.SetDefaultValue(600); + buildCommand.AddGlobalOption(timeoutOption); + + var cleanupOption = new Option( + ["--cleanup", "-c"], + "Clean repository after creating .fwdata file (CAUTION: deletes every other file except .fwdata)" + ); + buildCommand.Add(cleanupOption); + + buildCommand.SetHandler(BuildFwData, filename, verboseOption, quietOption, hgRevOption, timeoutOption, cleanupOption); + + var cleanupOptionForSplit = new Option( + ["--cleanup", "-c"], + "Delete .fwdata file after splitting" + ); + splitCommand.Add(cleanupOptionForSplit); + + splitCommand.SetHandler(SplitFwData, filename, verboseOption, quietOption, cleanupOptionForSplit); + + return await rootCommand.InvokeAsync(args); + } + + static FileInfo? MaybeLocateFwDataFile(string input) + { + if (Directory.Exists(input)) { + var dirInfo = new DirectoryInfo(input); + var fname = dirInfo.Name + ".fwdata"; + return new FileInfo(Path.Join(input, fname)); + } else if (File.Exists(input)) { + return new FileInfo(input); + } else if (File.Exists(input + ".fwdata")) { + return new FileInfo(input + ".fwdata"); + } else { + return null; + } + } + + static FileInfo LocateFwDataFile(string input) + { + var result = MaybeLocateFwDataFile(input); + if (result != null) return result; + if (input.EndsWith(".fwdata")) return new FileInfo(input); + return new FileInfo(input + ".fwdata"); + } + + static Task SplitFwData(string filename, bool verbose, bool quiet, bool cleanup) + { + IProgress progress = quiet ? new NullProgress() : new ConsoleProgress(); + progress.ShowVerbose = verbose; + var file = MaybeLocateFwDataFile(filename); + if (file == null || !file.Exists) { + progress.WriteError("Could not find {0}", filename); + return Task.FromResult(1); + } + string name = file.FullName; + progress.WriteVerbose("Splitting {0} ...", name); + LfMergeBridge.LfMergeBridge.DisassembleFwdataFile(progress, writeVerbose: true, name); + progress.WriteMessage("Finished splitting {0}", name); + if (cleanup) + { + progress.WriteVerbose("Cleaning up..."); + var fwdataFile = new FileInfo(name); + if (fwdataFile.Exists) { + fwdataFile.Delete(); + progress.WriteVerbose("Deleted {0}", fwdataFile.FullName); + } else { + progress.WriteVerbose("File not found, so not deleting: {0}", fwdataFile.FullName); + } + } + return Task.FromResult(0); + } + + static Task BuildFwData(string filename, bool verbose, bool quiet, string rev, int timeout, bool cleanup) + { + IProgress progress = quiet ? new NullProgress() : new ConsoleProgress(); + progress.ShowVerbose = verbose; + var file = LocateFwDataFile(filename); + if (file.Exists) { + progress.WriteWarning("File {0} already exists and will be overwritten", file.FullName); + } + var dir = file.Directory; + if (dir == null || !dir.Exists) { + progress.WriteError("Could not find directory {0}. MkFwData needs a Mercurial repo to work with.", dir?.FullName ?? "(null)"); + return Task.FromResult(1); + } + string name = file.FullName; + progress.WriteMessage("Checking out {0}", rev); + var result = HgRunner.Run($"hg checkout {rev}", dir.FullName, timeout, progress); + if (result.ExitCode != 0) + { + progress.WriteMessage("Could not find Mercurial repo in directory {0}. MkFwData needs a Mercurial repo to work with.", dir.FullName ?? "(null)"); + return Task.FromResult(result.ExitCode); + } + progress.WriteVerbose("Creating {0} ...", name); + LfMergeBridge.LfMergeBridge.ReassembleFwdataFile(progress, writeVerbose: true, name); + progress.WriteMessage("Created {0}", name); + if (cleanup) + { + progress.WriteVerbose("Cleaning up..."); + HgRunner.Run($"hg checkout null", dir.FullName, timeout, progress); + HgRunner.Run($"hg purge --no-confirm --exclude *.fwdata --exclude hgRunner.log", dir.FullName, timeout, progress); + } + return Task.FromResult(0); + } +}