Add relative path resolution support for Python and Node.js executables

Co-authored-by: Jack251970 <53996452+Jack251970@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-01-24 07:47:57 +00:00
parent 438bb8a2fc
commit d61ec1828f
3 changed files with 139 additions and 6 deletions

View file

@ -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;
}
/// <summary>
/// Resolves the configured executable path to an absolute path.
/// Supports both absolute paths and relative paths (relative to ProgramDirectory).
/// </summary>
private string ResolvedPluginsSettingsFilePath => Constant.ResolveAbsolutePath(PluginsSettingsFilePath);
internal IEnumerable<PluginPair> Setup()
{
// If no plugin is using the language, return empty list
@ -48,13 +55,14 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
return new List<PluginPair>();
}
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
{

View file

@ -56,5 +56,25 @@ namespace Flow.Launcher.Infrastructure
public const string Docs = "https://flowlauncher.com/docs";
public const string SystemLanguageCode = "system";
/// <summary>
/// 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.
/// </summary>
/// <param name="path">The path to resolve</param>
/// <returns>An absolute path</returns>
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));
}
}
}

View file

@ -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);
}
}
}