From 50feeb958ba1bbbe8b30a727e9801b3a52c4f037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20R=C3=A4tzel?= Date: Mon, 15 Mar 2021 18:19:42 +0000 Subject: [PATCH] Support projects with `elm.json` not at the root Expand the Elm Editor to support compiling modules for which we want to use an `elm.json` file that is not at the root of the project file tree: + Update methods in Elm Fullstack for more consistent APIs when modeling file paths: Use sequence of strings instead of depending on OS-specific separator character. + Expand the `ExecuteFileWithArguments` method in Elm Fullstack to support a working directory different from the root directory. + Expand the Elm Editor backend to forward a working directory for the `Elm make` command as given by the frontend. + Expand the Elm Editor frontend to select the matching `elm.json` file for the Elm make entry module chosen by the user. --- .../ExecutableFile.cs | 34 +++++++++------ .../PersistentProcess.Common/Filesystem.cs | 13 ++++-- .../LoadFromLocalFilesystem.cs | 8 +--- .../PersistentProcess.Common/Process.cs | 22 ++++------ .../PersistentProcess.WebHost/Program.cs | 2 +- implement/elm-fullstack/elm-fullstack.csproj | 4 +- .../elm-editor/src/Backend/VolatileHost.elm | 41 +++++++++---------- .../src/FrontendBackendInterface.elm | 3 +- .../elm-editor/src/FrontendWeb/Main.elm | 35 ++++++++++++---- implement/test-elm-fullstack/TestSetup.cs | 3 +- 10 files changed, 95 insertions(+), 70 deletions(-) diff --git a/implement/PersistentProcess/PersistentProcess.Common/ExecutableFile.cs b/implement/PersistentProcess/PersistentProcess.Common/ExecutableFile.cs index 36356527..496b9077 100644 --- a/implement/PersistentProcess/PersistentProcess.Common/ExecutableFile.cs +++ b/implement/PersistentProcess/PersistentProcess.Common/ExecutableFile.cs @@ -20,19 +20,22 @@ public struct ProcessOutput public int ExitCode; } - static public (ProcessOutput processOutput, IReadOnlyCollection<(string name, IImmutableList content)> resultingFiles) ExecuteFileWithArguments( - IImmutableList<(string name, IImmutableList content)> environmentFiles, + static public (ProcessOutput processOutput, IReadOnlyCollection<(IImmutableList path, IImmutableList content)> resultingFiles) ExecuteFileWithArguments( + IImmutableList<(IImmutableList path, IImmutableList content)> environmentFiles, byte[] executableFile, string arguments, - IDictionary environmentStrings) + IDictionary environmentStrings, + IImmutableList workingDirectory = null) { - var workingDirectory = Filesystem.CreateRandomDirectoryInTempDirectory(); + var containerDirectory = Filesystem.CreateRandomDirectoryInTempDirectory(); var executableFileName = "name-used-to-execute-file.exe"; + var executableFilePathRelative = ImmutableList.Create(executableFileName); + foreach (var environmentFile in environmentFiles) { - var environmentFilePath = Path.Combine(workingDirectory, environmentFile.name); + var environmentFilePath = Path.Combine(containerDirectory, Filesystem.MakePlatformSpecificPath(environmentFile.path)); var environmentFileDirectory = Path.GetDirectoryName(environmentFilePath); Directory.CreateDirectory(environmentFileDirectory); @@ -40,25 +43,30 @@ static public (ProcessOutput processOutput, IReadOnlyCollection<(string name, II File.WriteAllBytes(environmentFilePath, environmentFile.content.ToArray()); } - var executableFilePath = Path.Combine(workingDirectory, executableFileName); + var executableFilePathAbsolute = Path.Combine(containerDirectory, executableFileName); - File.WriteAllBytes(executableFilePath, executableFile); + File.WriteAllBytes(executableFilePathAbsolute, executableFile); if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - var unixFileInfo = new Mono.Unix.UnixFileInfo(executableFilePath); + var unixFileInfo = new Mono.Unix.UnixFileInfo(executableFilePathAbsolute); unixFileInfo.FileAccessPermissions |= FileAccessPermissions.GroupExecute | FileAccessPermissions.UserExecute | FileAccessPermissions.OtherExecute | FileAccessPermissions.GroupRead | FileAccessPermissions.UserRead | FileAccessPermissions.OtherRead; } + var workingDirectoryAbsolute = + Path.Combine( + containerDirectory, + Filesystem.MakePlatformSpecificPath(workingDirectory ?? ImmutableList.Empty)); + var process = new System.Diagnostics.Process { StartInfo = new ProcessStartInfo { - WorkingDirectory = workingDirectory, - FileName = executableFilePath, + WorkingDirectory = workingDirectoryAbsolute, + FileName = executableFilePathAbsolute, Arguments = arguments, UseShellExecute = false, RedirectStandardOutput = true, @@ -79,12 +87,12 @@ static public (ProcessOutput processOutput, IReadOnlyCollection<(string name, II var createdFiles = Filesystem.GetFilesFromDirectory( - directoryPath: workingDirectory, - filterByRelativeName: path => path != executableFileName); + directoryPath: containerDirectory, + filterByRelativeName: path => !path.SequenceEqual(executableFilePathRelative)); try { - Directory.Delete(path: workingDirectory, recursive: true); + Directory.Delete(path: containerDirectory, recursive: true); } // Avoid crash in scenario like https://forum.botengine.org/t/farm-manager-tribal-wars-2-farmbot/3038/170 catch (UnauthorizedAccessException) diff --git a/implement/PersistentProcess/PersistentProcess.Common/Filesystem.cs b/implement/PersistentProcess/PersistentProcess.Common/Filesystem.cs index f6e6af17..e2c4e03d 100644 --- a/implement/PersistentProcess/PersistentProcess.Common/Filesystem.cs +++ b/implement/PersistentProcess/PersistentProcess.Common/Filesystem.cs @@ -16,16 +16,18 @@ public class Filesystem RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "LOCALAPPDATA" : "HOME"), "kalmit", ".cache"); - static public IReadOnlyCollection<(string path, IImmutableList content)> GetAllFilesFromDirectory(string directoryPath) => + static public IReadOnlyCollection<(IImmutableList path, IImmutableList content)> GetAllFilesFromDirectory(string directoryPath) => GetFilesFromDirectory( directoryPath: directoryPath, filterByRelativeName: _ => true); - static public IReadOnlyCollection<(string path, IImmutableList content)> GetFilesFromDirectory( + static public IReadOnlyCollection<(IImmutableList path, IImmutableList content)> GetFilesFromDirectory( string directoryPath, - Func filterByRelativeName) => + Func, bool> filterByRelativeName) => Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories) - .Select(filePath => (absolutePath: filePath, relativePath: GetRelativePath(directoryPath, filePath))) + .Select(filePath => + (absolutePath: filePath, + relativePath: (IImmutableList)GetRelativePath(directoryPath, filePath).Split(Path.DirectorySeparatorChar).ToImmutableList())) .Where(filePath => filterByRelativeName(filePath.relativePath)) .Select(filePath => (filePath.relativePath, (IImmutableList)File.ReadAllBytes(filePath.absolutePath).ToImmutableList())) .ToList(); @@ -45,5 +47,8 @@ static public string CreateRandomDirectoryInTempDirectory() Directory.CreateDirectory(directory); return directory; } + + static public string MakePlatformSpecificPath(IImmutableList path) => + string.Join(Path.DirectorySeparatorChar.ToString(), path); } } \ No newline at end of file diff --git a/implement/PersistentProcess/PersistentProcess.Common/LoadFromLocalFilesystem.cs b/implement/PersistentProcess/PersistentProcess.Common/LoadFromLocalFilesystem.cs index f6fa1a4f..269d315f 100644 --- a/implement/PersistentProcess/PersistentProcess.Common/LoadFromLocalFilesystem.cs +++ b/implement/PersistentProcess/PersistentProcess.Common/LoadFromLocalFilesystem.cs @@ -14,13 +14,9 @@ static public Composition.TreeWithStringPath LoadSortedTreeFromPath(string path) if (!Directory.Exists(path)) return null; - var blobs = - Filesystem.GetAllFilesFromDirectory(path) - .Select(file => (path: (System.Collections.Immutable.IImmutableList)file.path.Split('/', '\\').ToImmutableList(), content: file.content)) - .ToImmutableList(); + var blobs = Filesystem.GetAllFilesFromDirectory(path); - return - Composition.SortedTreeFromSetOfBlobsWithStringPath(blobs); + return Composition.SortedTreeFromSetOfBlobsWithStringPath(blobs); } } } diff --git a/implement/PersistentProcess/PersistentProcess.Common/Process.cs b/implement/PersistentProcess/PersistentProcess.Common/Process.cs index 8ec70961..acea38f3 100644 --- a/implement/PersistentProcess/PersistentProcess.Common/Process.cs +++ b/implement/PersistentProcess/PersistentProcess.Common/Process.cs @@ -197,19 +197,17 @@ static public string CompileElm( */ var maxRetryCount = 2; - var command = "make " + MakePlatformSpecificPath(pathToFileWithElmEntryPoint) + " --output=\"" + outputFileName + "\" " + elmMakeCommandAppendix; + var command = "make " + Filesystem.MakePlatformSpecificPath(pathToFileWithElmEntryPoint) + " --output=\"" + outputFileName + "\" " + elmMakeCommandAppendix; - var attemptsResults = new List<(ExecutableFile.ProcessOutput processOutput, IReadOnlyCollection<(string name, IImmutableList content)> resultingFiles)>(); + var attemptsResults = new List<(ExecutableFile.ProcessOutput processOutput, IReadOnlyCollection<(IImmutableList path, IImmutableList content)> resultingFiles)>(); - var platformSpecificFiles = - elmCodeFiles - .Select(elmCodeFile => (MakePlatformSpecificPath(elmCodeFile.Key), elmCodeFile.Value)) - .ToImmutableList(); + var environmentFiles = + elmCodeFiles.Select(file => (path: file.Key, content: file.Value)).ToImmutableList(); do { var commandResults = ExecutableFile.ExecuteFileWithArguments( - platformSpecificFiles, + environmentFiles, GetElmExecutableFile, command, new Dictionary() @@ -243,13 +241,14 @@ An alternative would be retrying when this error is parsed from `commandResults. attemptsResults.Add(commandResults); - var platformSpecificNewFiles = + var newFiles = commandResults.resultingFiles - .Where(file => !platformSpecificFiles.Any(inputFile => inputFile.Item1 == file.name)) + .Where(file => !environmentFiles.Any(inputFile => inputFile.Item1.SequenceEqual(file.path))) .ToImmutableList(); var outputFileContent = - platformSpecificNewFiles.FirstOrDefault(resultFile => resultFile.name == outputFileName).content; + newFiles + .FirstOrDefault(resultFile => resultFile.path.SequenceEqual(ImmutableList.Create(outputFileName))).content; if (outputFileContent != null) return Encoding.UTF8.GetString(outputFileContent.ToArray()); @@ -271,9 +270,6 @@ An alternative would be retrying when this error is parsed from `commandResults. "\nStandard Error:\n'" + lastAttemptResults.processOutput.StandardError + "'"); } - static public string MakePlatformSpecificPath(IImmutableList path) => - string.Join(Path.DirectorySeparatorChar.ToString(), path); - static public byte[] GetElmExecutableFile => CommonConversion.DecompressGzip(GetElmExecutableFileCompressedGzip); diff --git a/implement/PersistentProcess/PersistentProcess.WebHost/Program.cs b/implement/PersistentProcess/PersistentProcess.WebHost/Program.cs index f42f38df..2c76d588 100644 --- a/implement/PersistentProcess/PersistentProcess.WebHost/Program.cs +++ b/implement/PersistentProcess/PersistentProcess.WebHost/Program.cs @@ -2,6 +2,6 @@ namespace Kalmit.PersistentProcess.WebHost { public class Program { - static public string AppVersionId => "2021-03-13"; + static public string AppVersionId => "2021-03-15"; } } diff --git a/implement/elm-fullstack/elm-fullstack.csproj b/implement/elm-fullstack/elm-fullstack.csproj index 21b17993..027ab46b 100644 --- a/implement/elm-fullstack/elm-fullstack.csproj +++ b/implement/elm-fullstack/elm-fullstack.csproj @@ -5,8 +5,8 @@ netcoreapp3.1 elm_fullstack elm-fs - 2021.0313.0.0 - 2021.0313.0.0 + 2021.0315.0.0 + 2021.0315.0.0 diff --git a/implement/example-apps/elm-editor/src/Backend/VolatileHost.elm b/implement/example-apps/elm-editor/src/Backend/VolatileHost.elm index c9d3369e..a458e5fb 100644 --- a/implement/example-apps/elm-editor/src/Backend/VolatileHost.elm +++ b/implement/example-apps/elm-editor/src/Backend/VolatileHost.elm @@ -49,8 +49,7 @@ volatileHostScript = // https://www.nuget.org/api/v2/package/Newtonsoft.Json/12.0.2 #r "sha256:b9b4e633ea6c728bad5f7cbbef7f8b842f7e10181731dbe5ec3cd995a6f60287" -// from elm-fullstack-separate-assemblies-52aa3ed298b05c93d37503d601de56211d2163be-linux-x64 -#r "sha256:bf80ff2fb8a61b6b2a0d22c49771aad07fcc07a282bf7768730b3a525c84fd2d" +#r "sha256:ffbbf81111c3981a7f09dfb9be6e3d3255a9669a95d468245350a644928cf1a4" using System; using System.Collections.Generic; @@ -91,7 +90,9 @@ public class ElmMakeRequestStructure { public IReadOnlyList files; - public IReadOnlyList entryPointFilePath; + public IReadOnlyList entryPointFilePathFromWorkingDirectory; + + public IReadOnlyList workingDirectoryPath; } public class FormatElmModuleTextResponseStructure @@ -275,25 +276,24 @@ ElmMakeResponseStructure ElmMake(ElmMakeRequestStructure elmMakeRequest) file => (IImmutableList)file.path.ToImmutableList(), file => (IImmutableList)Convert.FromBase64String(file.contentBase64).ToImmutableList()); - var platformSpecificFiles = - elmCodeFiles - .Select(elmCodeFile => (MakePlatformSpecificPath(elmCodeFile.Key), elmCodeFile.Value)) - .ToImmutableList(); + var environmentFiles = + elmCodeFiles.Select(file => (path: file.Key, content: file.Value)).ToImmutableList(); - var entryPointFilePath = MakePlatformSpecificPath(elmMakeRequest.entryPointFilePath); + var entryPointFilePathFromWorkingDirectory = + MakePlatformSpecificPath(elmMakeRequest.entryPointFilePathFromWorkingDirectory); var elmMakeOutputFileName = "elm-make-output.html"; - var commandLineCommonArguments = "make " + entryPointFilePath; + var commandLineCommonArguments = "make " + entryPointFilePathFromWorkingDirectory; var commandLineArguments = commandLineCommonArguments + " --output=" + elmMakeOutputFileName; var reportJsonCommandLineArguments = commandLineCommonArguments + " --report=json"; - (Kalmit.ExecutableFile.ProcessOutput processOutput, IReadOnlyCollection<(string name, IImmutableList content)> resultingFiles) commandResultsFromArguments(string arguments) + (Kalmit.ExecutableFile.ProcessOutput processOutput, IReadOnlyCollection<(IImmutableList path, IImmutableList content)> resultingFiles) commandResultsFromArguments(string arguments) { return Kalmit.ExecutableFile.ExecuteFileWithArguments( - platformSpecificFiles, + environmentFiles, GetElmExecutableFile, arguments, new Dictionary() @@ -323,21 +323,18 @@ ElmMakeResponseStructure ElmMake(ElmMakeRequestStructure elmMakeRequest) An alternative would be retrying when this error is parsed from `commandResults.processOutput.StandardError`. */ {"ELM_HOME", GetElmHomeDirectory()}, - }); + }, + workingDirectory: elmMakeRequest.workingDirectoryPath.ToImmutableList()); } var commandResults = commandResultsFromArguments(commandLineArguments); - var platformSpecificNewFiles = - commandResults.resultingFiles - .Where(file => !platformSpecificFiles.Any(inputFile => inputFile.Item1 == file.name)) - .ToImmutableList(); - var newFiles = - platformSpecificNewFiles + commandResults.resultingFiles + .Where(file => !environmentFiles.Any(inputFile => inputFile.Item1.SequenceEqual(file.path))) .Select(file => new FileWithPath { - path = file.name.Split('/', '\\\\'), + path = file.path, contentBase64 = Convert.ToBase64String(file.content.ToArray()), }) .ToImmutableList(); @@ -418,17 +415,19 @@ static public class ElmFormat { var elmModuleFileName = "ElmModuleToFormat.elm"; + var elmModuleFilePath = ImmutableList.Create(elmModuleFileName); + var elmFormatResult = Kalmit.ExecutableFile.ExecuteFileWithArguments( ImmutableList.Create( - (elmModuleFileName, (IImmutableList)System.Text.Encoding.UTF8.GetBytes(originalModuleText).ToImmutableList())), + ((IImmutableList)elmModuleFilePath, (IImmutableList)System.Text.Encoding.UTF8.GetBytes(originalModuleText).ToImmutableList())), GetElmFormatExecutableFile, " " + elmModuleFileName + " --yes", environmentStrings: null); var resultingFile = elmFormatResult.resultingFiles - .FirstOrDefault(file => file.name.EndsWith(elmModuleFileName)) + .FirstOrDefault(file => file.path.SequenceEqual(elmModuleFilePath)) .content; var formattedText = diff --git a/implement/example-apps/elm-editor/src/FrontendBackendInterface.elm b/implement/example-apps/elm-editor/src/FrontendBackendInterface.elm index 0cfa6ec1..91f5cc57 100644 --- a/implement/example-apps/elm-editor/src/FrontendBackendInterface.elm +++ b/implement/example-apps/elm-editor/src/FrontendBackendInterface.elm @@ -16,7 +16,8 @@ type ResponseStructure type alias ElmMakeRequestStructure = { files : List FileWithPath - , entryPointFilePath : List String + , workingDirectoryPath : List String + , entryPointFilePathFromWorkingDirectory : List String } diff --git a/implement/example-apps/elm-editor/src/FrontendWeb/Main.elm b/implement/example-apps/elm-editor/src/FrontendWeb/Main.elm index a16624e9..3b84d31e 100644 --- a/implement/example-apps/elm-editor/src/FrontendWeb/Main.elm +++ b/implement/example-apps/elm-editor/src/FrontendWeb/Main.elm @@ -868,19 +868,34 @@ elmMakeRequestForFileOpenedInEditor workspace = Nothing -> Nothing - Just filePath -> + Just entryPointFilePath -> let base64FromBytes : Bytes.Bytes -> String base64FromBytes = Base64.fromBytes >> Maybe.withDefault "Error encoding in base64" - entryPointFilePath = - filePath + allFilePaths = + workspace.fileTree + |> ProjectState.flatListOfBlobsFromFileTreeNode + |> List.map Tuple.first + + directoryContainsElmJson directoryPath = + allFilePaths |> List.member (directoryPath ++ [ "elm.json" ]) + + workingDirectoryPath = + entryPointFilePath + |> List.reverse + |> List.drop 1 + |> List.reverse + |> List.Extra.inits + |> List.sortBy List.length + |> List.filter directoryContainsElmJson + |> List.head + |> Maybe.withDefault [] in Just - { entryPointFilePath = entryPointFilePath - , files = + { files = workspace.fileTree |> ProjectState.flatListOfBlobsFromFileTreeNode |> List.map @@ -889,6 +904,8 @@ elmMakeRequestForFileOpenedInEditor workspace = , contentBase64 = content |> base64FromBytes } ) + , workingDirectoryPath = workingDirectoryPath + , entryPointFilePathFromWorkingDirectory = entryPointFilePath |> List.drop (List.length workingDirectoryPath) } @@ -1701,14 +1718,18 @@ viewOutputPaneContent state = Just elmMakeRequest.files == (elmMakeRequestFromCurrentState |> Maybe.map .files) + elmMakeRequestEntryPointFilePathAbs = + elmMakeRequest.workingDirectoryPath + ++ elmMakeRequest.entryPointFilePathFromWorkingDirectory + warnAboutOutdatedCompilationText = if - Just elmMakeRequest.entryPointFilePath + Just elmMakeRequestEntryPointFilePathAbs /= filePathOpenedInEditorFromWorkspace state then Just ("⚠️ Last compilation started for another file: '" - ++ String.join "/" elmMakeRequest.entryPointFilePath + ++ String.join "/" elmMakeRequestEntryPointFilePathAbs ++ "'" ) diff --git a/implement/test-elm-fullstack/TestSetup.cs b/implement/test-elm-fullstack/TestSetup.cs index 134bdf8d..7092bb64 100644 --- a/implement/test-elm-fullstack/TestSetup.cs +++ b/implement/test-elm-fullstack/TestSetup.cs @@ -91,8 +91,7 @@ static public IImmutableDictionary, IImmutableList> string directoryPath) => ElmApp.ToFlatDictionaryWithPathComparer( Filesystem.GetAllFilesFromDirectory(directoryPath) - .OrderBy(file => file.path) - .Select(filePathAndContent => ((IImmutableList)filePathAndContent.path.Split(new[] { '/', '\\' }).ToImmutableList(), filePathAndContent.content))); + .OrderBy(file => string.Join('/', file.path))); static public IImmutableDictionary, IImmutableList> AsLoweredElmApp( IImmutableDictionary, IImmutableList> originalAppFiles) =>