diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index 1a324a993..c37030812 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Windows; using System.Windows.Forms; +using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; @@ -40,6 +41,12 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments PluginSettings = pluginSettings; } + /// + /// Resolves the configured executable path to an absolute path. + /// Supports both absolute paths and relative paths (relative to ProgramDirectory). + /// + private string ResolvedPluginsSettingsFilePath => Constant.ResolveAbsolutePath(PluginsSettingsFilePath); + internal IEnumerable Setup() { // If no plugin is using the language, return empty list @@ -48,13 +55,14 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments return new List(); } - if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath)) + var resolvedPath = ResolvedPluginsSettingsFilePath; + if (!string.IsNullOrEmpty(resolvedPath) && FilesFolders.FileExists(resolvedPath)) { // Ensure latest only if user is using Flow's environment setup. - if (PluginsSettingsFilePath.StartsWith(EnvPath, StringComparison.OrdinalIgnoreCase)) - EnsureLatestInstalled(ExecutablePath, PluginsSettingsFilePath, EnvPath); + if (resolvedPath.StartsWith(EnvPath, StringComparison.OrdinalIgnoreCase)) + EnsureLatestInstalled(ExecutablePath, resolvedPath, EnvPath); - return SetPathForPluginPairs(PluginsSettingsFilePath, Language); + return SetPathForPluginPairs(resolvedPath, Language); } var noRuntimeMessage = Localize.runtimePluginInstalledChooseRuntimePrompt(Language, EnvName, Environment.NewLine); @@ -103,9 +111,10 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments InstallEnvironment(); } - if (FilesFolders.FileExists(PluginsSettingsFilePath)) + resolvedPath = ResolvedPluginsSettingsFilePath; + if (FilesFolders.FileExists(resolvedPath)) { - return SetPathForPluginPairs(PluginsSettingsFilePath, Language); + return SetPathForPluginPairs(resolvedPath, Language); } else { diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs index 13da9f79f..57175930f 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -56,5 +56,25 @@ namespace Flow.Launcher.Infrastructure public const string Docs = "https://flowlauncher.com/docs"; public const string SystemLanguageCode = "system"; + + /// + /// Resolves a path that may be relative to an absolute path. + /// If the path is already absolute, returns it as-is. + /// If the path is relative (starts with . or doesn't contain a drive), resolves it relative to ProgramDirectory. + /// + /// The path to resolve + /// An absolute path + public static string ResolveAbsolutePath(string path) + { + if (string.IsNullOrEmpty(path)) + return path; + + // If already absolute, return as-is + if (Path.IsPathRooted(path)) + return path; + + // Resolve relative to ProgramDirectory + return Path.GetFullPath(Path.Combine(ProgramDirectory, path)); + } } } diff --git a/Flow.Launcher.Test/PathResolutionTest.cs b/Flow.Launcher.Test/PathResolutionTest.cs new file mode 100644 index 000000000..4e03a2d6f --- /dev/null +++ b/Flow.Launcher.Test/PathResolutionTest.cs @@ -0,0 +1,104 @@ +using System; +using System.IO; +using Xunit; +using Flow.Launcher.Infrastructure; + +namespace Flow.Launcher.Test +{ + public class PathResolutionTest + { + [Fact] + public void ResolveAbsolutePath_WithAbsolutePath_ReturnsOriginalPath() + { + // Arrange + var absolutePath = @"C:\Program Files\Python\python.exe"; + + // Act + var result = Constant.ResolveAbsolutePath(absolutePath); + + // Assert + Assert.Equal(absolutePath, result); + } + + [Fact] + public void ResolveAbsolutePath_WithRelativePath_ResolvesToProgramDirectory() + { + // Arrange + var relativePath = @".\runtimes\python\pythonw.exe"; + + // Act + var result = Constant.ResolveAbsolutePath(relativePath); + + // Assert + Assert.True(Path.IsPathRooted(result), "Result should be an absolute path"); + Assert.Contains(Constant.ProgramDirectory, result); + Assert.EndsWith(@"runtimes\python\pythonw.exe", result); + } + + [Fact] + public void ResolveAbsolutePath_WithDotDotPath_ResolvesCorrectly() + { + // Arrange + var relativePath = @"..\runtimes\node\node.exe"; + + // Act + var result = Constant.ResolveAbsolutePath(relativePath); + + // Assert + Assert.True(Path.IsPathRooted(result), "Result should be an absolute path"); + } + + [Fact] + public void ResolveAbsolutePath_WithNullPath_ReturnsNull() + { + // Arrange + string nullPath = null; + + // Act + var result = Constant.ResolveAbsolutePath(nullPath); + + // Assert + Assert.Null(result); + } + + [Fact] + public void ResolveAbsolutePath_WithEmptyPath_ReturnsEmpty() + { + // Arrange + var emptyPath = string.Empty; + + // Act + var result = Constant.ResolveAbsolutePath(emptyPath); + + // Assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void ResolveAbsolutePath_WithForwardSlashes_ResolvesCorrectly() + { + // Arrange + var relativePath = @"./runtimes/python/pythonw.exe"; + + // Act + var result = Constant.ResolveAbsolutePath(relativePath); + + // Assert + Assert.True(Path.IsPathRooted(result), "Result should be an absolute path"); + Assert.Contains(Constant.ProgramDirectory, result); + } + + [Fact] + public void ResolveAbsolutePath_WithUNCPath_ReturnsOriginalPath() + { + // Arrange + var uncPath = @"\\server\share\python\pythonw.exe"; + + // Act + var result = Constant.ResolveAbsolutePath(uncPath); + + // Assert + Assert.Equal(uncPath, result); + } + } +}