From 85b724ec179133ca53e5d221b4cc0b0358a850c3 Mon Sep 17 00:00:00 2001 From: hatayama Date: Wed, 8 Jul 2026 23:05:22 +0900 Subject: [PATCH 1/2] Extract native CLI install path resolution Move install path calculation, comparison, ownership checks, and environment reads into a focused resolver. Keep current-process PATH mutation in the installer so value resolution remains separate from state changes. --- .../Tests/Editor/NativeCliInstallerTests.cs | 16 +- .../CLI/CliInstallationDetector.cs | 6 +- .../CLI/CliPathSetupProfileResolver.cs | 2 +- .../CLI/NativeCliInstallPathResolver.cs | 313 +++++++++++++++++ .../CLI/NativeCliInstallPathResolver.cs.meta | 11 + .../Infrastructure/CLI/NativeCliInstaller.cs | 331 ++---------------- 6 files changed, 358 insertions(+), 321 deletions(-) create mode 100644 Packages/src/Editor/Infrastructure/CLI/NativeCliInstallPathResolver.cs create mode 100644 Packages/src/Editor/Infrastructure/CLI/NativeCliInstallPathResolver.cs.meta diff --git a/Assets/Tests/Editor/NativeCliInstallerTests.cs b/Assets/Tests/Editor/NativeCliInstallerTests.cs index 6f2a265592..3bdb8ba9df 100644 --- a/Assets/Tests/Editor/NativeCliInstallerTests.cs +++ b/Assets/Tests/Editor/NativeCliInstallerTests.cs @@ -478,7 +478,7 @@ public void BuildUninstallCommand_OnWindowsRunsInstalledLauncher() public void BuildPathWithInstallDirectory_OnWindowsPrependsMissingNativeInstallDir() { // Verifies that Unity's current Windows PATH prefers the freshly installed native CLI. - string result = NativeCliInstaller.BuildPathWithInstallDirectory( + string result = NativeCliInstallPathResolver.BuildPathWithInstallDirectory( "C:\\npm", "C:\\Users\\ExampleUser\\Programs\\uloop\\bin", RuntimePlatform.WindowsEditor); @@ -506,7 +506,7 @@ private static NativeCliInstallCommand BuildLongRunningInstallCommand() public void BuildPathWithInstallDirectory_OnWindowsMovesExistingNativeInstallDirToFront() { // Verifies that a later Windows native install dir does not leave an earlier npm shim first. - string result = NativeCliInstaller.BuildPathWithInstallDirectory( + string result = NativeCliInstallPathResolver.BuildPathWithInstallDirectory( "C:\\npm;C:\\USERS\\EXAMPLEUSER\\PROGRAMS\\ULOOP\\BIN", "C:\\Users\\ExampleUser\\Programs\\uloop\\bin", RuntimePlatform.WindowsEditor); @@ -518,7 +518,7 @@ public void BuildPathWithInstallDirectory_OnWindowsMovesExistingNativeInstallDir public void BuildPathWithInstallDirectory_OnMacPrependsMissingNativeInstallDir() { // Verifies that POSIX PATH prefers the freshly installed native CLI. - string result = NativeCliInstaller.BuildPathWithInstallDirectory( + string result = NativeCliInstallPathResolver.BuildPathWithInstallDirectory( "/usr/local/bin", "/Users/ExampleUser/.local/bin", RuntimePlatform.OSXEditor); @@ -530,7 +530,7 @@ public void BuildPathWithInstallDirectory_OnMacPrependsMissingNativeInstallDir() public void BuildPathWithoutInstallDirectory_OnWindowsRemovesNativeInstallDir() { // Verifies that Windows uninstall removes the native CLI directory from PATH without removing npm. - string result = NativeCliInstaller.BuildPathWithoutInstallDirectory( + string result = NativeCliInstallPathResolver.BuildPathWithoutInstallDirectory( "C:\\npm;C:\\USERS\\EXAMPLEUSER\\PROGRAMS\\ULOOP\\BIN\\;C:\\Tools", "C:\\Users\\ExampleUser\\Programs\\uloop\\bin", RuntimePlatform.WindowsEditor); @@ -558,7 +558,7 @@ public void FinishSuccessfulInstall_WhenInstallerSucceededUpdatesCurrentProcessP public void GetDefaultInstallDirectoryFromRoots_OnMacMatchesInstallerDefault() { // Verifies that Unity mirrors the POSIX installer default install directory. - string result = NativeCliInstaller.GetDefaultInstallDirectoryFromRoots( + string result = NativeCliInstallPathResolver.GetDefaultInstallDirectoryFromRoots( RuntimePlatform.OSXEditor, "/Users/ExampleUser", null); @@ -570,7 +570,7 @@ public void GetDefaultInstallDirectoryFromRoots_OnMacMatchesInstallerDefault() public void GetDefaultInstallDirectoryFromRoots_OnWindowsMatchesInstallerDefault() { // Verifies that Unity mirrors the PowerShell installer default install directory. - string result = NativeCliInstaller.GetDefaultInstallDirectoryFromRoots( + string result = NativeCliInstallPathResolver.GetDefaultInstallDirectoryFromRoots( RuntimePlatform.WindowsEditor, null, "C:\\Users\\ExampleUser\\AppData\\Local"); @@ -586,7 +586,7 @@ public void GetDefaultInstallDirectoryFromRoots_OnWindowsMatchesInstallerDefault public void IsPackageOwnedInstallPath_WhenExecutableMatchesInstallDirectoryReturnsTrue() { // Verifies that uninstall is available only for the package-owned command path. - bool result = NativeCliInstaller.IsPackageOwnedInstallPath( + bool result = NativeCliInstallPathResolver.IsPackageOwnedInstallPath( "C:/Users/ExampleUser/AppData/Local/Programs/uloop/bin/uloop.exe", "C:\\Users\\ExampleUser\\AppData\\Local\\Programs\\uloop\\bin", RuntimePlatform.WindowsEditor); @@ -598,7 +598,7 @@ public void IsPackageOwnedInstallPath_WhenExecutableMatchesInstallDirectoryRetur public void IsPackageOwnedInstallPath_WhenExecutableIsSharedCommandReturnsFalse() { // Verifies that same-version shared commands do not route the settings button to uninstall. - bool result = NativeCliInstaller.IsPackageOwnedInstallPath( + bool result = NativeCliInstallPathResolver.IsPackageOwnedInstallPath( "C:\\Tools\\uloop.exe", "C:\\Users\\ExampleUser\\AppData\\Local\\Programs\\uloop\\bin", RuntimePlatform.WindowsEditor); diff --git a/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs b/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs index ead180ea42..e0a02f3bad 100644 --- a/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs +++ b/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs @@ -138,7 +138,7 @@ public Task IsCliVisibleFromShellAsync(RuntimePlatform platform, Cancellat return IsShellDetectionUsableForPathSetup( detection, platform, - NativeCliInstaller.IsPackageOwnedCurrentUserInstallPath, + NativeCliInstallPathResolver.IsPackageOwnedCurrentUserInstallPath, minimumDispatcherVersion); }, ct); @@ -185,7 +185,7 @@ private static CliInstallationDetection DetectPackageOwnedCliInstallationBlockin RuntimePlatform platform, CancellationToken ct) { - string executablePath = NativeCliInstaller.GetCurrentUserGlobalCliInstallPath(platform); + string executablePath = NativeCliInstallPathResolver.GetCurrentUserGlobalCliInstallPath(platform); if (string.IsNullOrEmpty(executablePath) || !File.Exists(executablePath)) { return new CliInstallationDetection(null, executablePath); @@ -243,7 +243,7 @@ internal static ProcessStartInfo BuildShellCliDetectionStartInfo( if (platform != RuntimePlatform.WindowsEditor) { startInfo.EnvironmentVariables[CliConstants.POSIX_PATH_ENVIRONMENT_VARIABLE] = - NativeCliInstaller.BuildPathWithoutInstallDirectory( + NativeCliInstallPathResolver.BuildPathWithoutInstallDirectory( currentPath, pathSetupPlan.InstallDirectory, platform); diff --git a/Packages/src/Editor/Infrastructure/CLI/CliPathSetupProfileResolver.cs b/Packages/src/Editor/Infrastructure/CLI/CliPathSetupProfileResolver.cs index d6be823527..dac1d17745 100644 --- a/Packages/src/Editor/Infrastructure/CLI/CliPathSetupProfileResolver.cs +++ b/Packages/src/Editor/Infrastructure/CLI/CliPathSetupProfileResolver.cs @@ -15,7 +15,7 @@ internal static class CliPathSetupProfileResolver { public static CliPathSetupPlan ResolveCurrentUserPlan(RuntimePlatform platform) { - string installDirectory = NativeCliInstaller.GetCurrentUserGlobalCliInstallDirectory(platform); + string installDirectory = NativeCliInstallPathResolver.GetCurrentUserGlobalCliInstallDirectory(platform); string shellPath = NodeEnvironmentResolver.GetUserShell(); return ResolvePlan( platform, diff --git a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstallPathResolver.cs b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstallPathResolver.cs new file mode 100644 index 0000000000..9661180f47 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstallPathResolver.cs @@ -0,0 +1,313 @@ +using System; +using System.IO; +using System.Text; + +using UnityEngine; + +using io.github.hatayama.UnityCliLoop.Domain; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Resolves and compares package-owned native CLI install paths. + /// + internal static class NativeCliInstallPathResolver + { + private const string WINDOWS_FILE_PATH_SEPARATOR = "\\"; + private const string POSIX_FILE_PATH_SEPARATOR = "/"; + + internal static string GetGlobalCliInstallPath(string installDirectory, RuntimePlatform platform) + { + UnityEngine.Debug.Assert(!string.IsNullOrEmpty(installDirectory), "installDirectory must not be null or empty"); + + string separator = GetFilePathSeparator(platform); + string normalizedInstallDirectory = installDirectory.TrimEnd('\\', '/'); + return normalizedInstallDirectory + separator + GetGlobalCliInstallFileName(platform); + } + + internal static string BuildPathWithInstallDirectory( + string currentPath, + string installDirectory, + RuntimePlatform platform) + { + UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); + + string normalizedPath = currentPath ?? ""; + if (string.IsNullOrEmpty(normalizedPath)) + { + return installDirectory; + } + + string separator = GetPathSeparator(platform); + string[] entries = normalizedPath.Split( + new[] { separator }, + StringSplitOptions.RemoveEmptyEntries); + StringComparison comparison = GetPathComparison(platform); + StringBuilder builder = new(installDirectory); + foreach (string entry in entries) + { + if (string.Equals(entry, installDirectory, comparison)) + { + continue; + } + + builder.Append(separator); + builder.Append(entry); + } + + return builder.ToString(); + } + + internal static string BuildPathWithoutInstallDirectory( + string currentPath, + string installDirectory, + RuntimePlatform platform) + { + UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); + + string normalizedPath = currentPath ?? ""; + if (string.IsNullOrEmpty(normalizedPath)) + { + return ""; + } + + string separator = GetPathSeparator(platform); + string[] entries = normalizedPath.Split( + new[] { separator }, + StringSplitOptions.RemoveEmptyEntries); + string normalizedInstallDirectory = NormalizePathForComparison(installDirectory, platform); + StringComparison comparison = GetPathComparison(platform); + StringBuilder builder = new StringBuilder(); + foreach (string entry in entries) + { + if (string.IsNullOrWhiteSpace(entry)) + { + continue; + } + + string normalizedEntry = NormalizePathForComparison(entry, platform); + if (string.Equals(normalizedEntry, normalizedInstallDirectory, comparison)) + { + continue; + } + + if (builder.Length > 0) + { + builder.Append(separator); + } + + builder.Append(entry); + } + + return builder.ToString(); + } + + internal static string GetDefaultInstallDirectoryFromRoots( + RuntimePlatform platform, + string homeDirectory, + string localAppData) + { + if (platform == RuntimePlatform.WindowsEditor) + { + if (string.IsNullOrWhiteSpace(localAppData)) + { + return null; + } + + return Path.Combine( + localAppData, + CliConstants.WINDOWS_PROGRAMS_DIR_NAME, + CliConstants.NATIVE_INSTALL_DIR_NAME, + CliConstants.NATIVE_INSTALL_BIN_DIR_NAME); + } + + if (string.IsNullOrWhiteSpace(homeDirectory)) + { + return null; + } + + return Path.Combine( + homeDirectory, + CliConstants.POSIX_LOCAL_DIR_NAME, + CliConstants.NATIVE_INSTALL_BIN_DIR_NAME); + } + + internal static bool IsPackageOwnedCurrentUserInstallPath( + string executablePath, + RuntimePlatform platform) + { + if (string.IsNullOrWhiteSpace(executablePath)) + { + return false; + } + + string installDirectory = GetInstallDirectoryForCurrentUser(platform); + if (string.IsNullOrWhiteSpace(installDirectory)) + { + return false; + } + + return IsPackageOwnedInstallPath(executablePath, installDirectory, platform); + } + + internal static bool IsPackageOwnedInstallPath( + string executablePath, + string installDirectory, + RuntimePlatform platform) + { + UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); + + if (string.IsNullOrWhiteSpace(executablePath)) + { + return false; + } + + string expectedPath = GetGlobalCliInstallPath(installDirectory, platform); + string normalizedExecutablePath = NormalizePathForComparison(executablePath, platform); + string normalizedExpectedPath = NormalizePathForComparison(expectedPath, platform); + return string.Equals( + normalizedExecutablePath, + normalizedExpectedPath, + GetPathComparison(platform)); + } + + internal static string GetCurrentUserGlobalCliInstallPath(RuntimePlatform platform) + { + string installDirectory = GetInstallDirectoryForCurrentUser(platform); + return string.IsNullOrWhiteSpace(installDirectory) + ? null + : GetGlobalCliInstallPath(installDirectory, platform); + } + + internal static string GetCurrentUserGlobalCliInstallDirectory(RuntimePlatform platform) + { + return GetInstallDirectoryForCurrentUser(platform); + } + + internal static bool HasPackageOwnedCurrentUserInstall(RuntimePlatform platform) + { + string executablePath = GetCurrentUserGlobalCliInstallPath(platform); + return !string.IsNullOrWhiteSpace(executablePath) && File.Exists(executablePath); + } + + internal static string GetInstallDirectoryForCurrentUser(RuntimePlatform platform) + { + string configuredInstallDirectory = Environment.GetEnvironmentVariable(CliConstants.INSTALL_DIR_ENVIRONMENT_VARIABLE); + if (!string.IsNullOrWhiteSpace(configuredInstallDirectory)) + { + return configuredInstallDirectory; + } + + string homeDirectory = Environment.GetEnvironmentVariable(CliConstants.POSIX_HOME_ENVIRONMENT_VARIABLE); + if (string.IsNullOrWhiteSpace(homeDirectory)) + { + homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + } + + string localAppData = Environment.GetEnvironmentVariable(CliConstants.WINDOWS_LOCAL_APPDATA_ENVIRONMENT_VARIABLE); + return GetDefaultInstallDirectoryFromRoots(platform, homeDirectory, localAppData); + } + + internal static string GetPathEnvironmentVariableName(RuntimePlatform platform) + { + return platform == RuntimePlatform.WindowsEditor + ? CliConstants.WINDOWS_PATH_ENVIRONMENT_VARIABLE + : CliConstants.POSIX_PATH_ENVIRONMENT_VARIABLE; + } + + internal static bool DoesUserPathContainInstallDirectory( + string installDirectory, + RuntimePlatform platform, + Func getEnvironmentVariable) + { + UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); + UnityEngine.Debug.Assert(getEnvironmentVariable != null, "getEnvironmentVariable must not be null"); + + if (platform != RuntimePlatform.WindowsEditor) + { + return false; + } + + string pathVariableName = GetPathEnvironmentVariableName(platform); + string currentUserPath = getEnvironmentVariable(pathVariableName, EnvironmentVariableTarget.User); + return DoesPathContainInstallDirectory(currentUserPath, installDirectory, platform); + } + + private static string GetPathSeparator(RuntimePlatform platform) + { + return platform == RuntimePlatform.WindowsEditor + ? CliConstants.WINDOWS_PATH_SEPARATOR + : CliConstants.POSIX_PATH_SEPARATOR; + } + + private static string GetFilePathSeparator(RuntimePlatform platform) + { + return platform == RuntimePlatform.WindowsEditor + ? WINDOWS_FILE_PATH_SEPARATOR + : POSIX_FILE_PATH_SEPARATOR; + } + + private static StringComparison GetPathComparison(RuntimePlatform platform) + { + return platform == RuntimePlatform.WindowsEditor + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; + } + + private static string NormalizePathForComparison(string path, RuntimePlatform platform) + { + UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(path), "path must not be null or empty"); + + string normalizedPath = path.Trim().Trim('"'); + if (platform != RuntimePlatform.WindowsEditor) + { + return normalizedPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } + + return normalizedPath.TrimEnd('\\', '/').Replace('/', '\\'); + } + + private static bool DoesPathContainInstallDirectory( + string currentPath, + string installDirectory, + RuntimePlatform platform) + { + UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); + + string normalizedPath = currentPath ?? ""; + if (string.IsNullOrEmpty(normalizedPath)) + { + return false; + } + + string separator = GetPathSeparator(platform); + string[] entries = normalizedPath.Split( + new[] { separator }, + StringSplitOptions.RemoveEmptyEntries); + string normalizedInstallDirectory = NormalizePathForComparison(installDirectory, platform); + StringComparison comparison = GetPathComparison(platform); + foreach (string entry in entries) + { + if (string.IsNullOrWhiteSpace(entry)) + { + continue; + } + + string normalizedEntry = NormalizePathForComparison(entry, platform); + if (string.Equals(normalizedEntry, normalizedInstallDirectory, comparison)) + { + return true; + } + } + + return false; + } + + private static string GetGlobalCliInstallFileName(RuntimePlatform platform) + { + return platform == RuntimePlatform.WindowsEditor + ? CliConstants.GLOBAL_WINDOWS_COMMAND_NAME + : CliConstants.GLOBAL_UNIX_COMMAND_NAME; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstallPathResolver.cs.meta b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstallPathResolver.cs.meta new file mode 100644 index 0000000000..9e4a5cffa4 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstallPathResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6f0435a28178b4125b2b9a1eb26360be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs index eb986e0c32..c52eac057c 100644 --- a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs +++ b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs @@ -20,8 +20,6 @@ public static class NativeCliInstaller private const int INSTALL_PROCESS_TIMEOUT_MS = 300000; private const int INSTALL_PROCESS_WAIT_SLICE_MS = 250; internal const int UNINSTALL_COMPLETION_TIMEOUT_MS = 30000; - private const string WINDOWS_FILE_PATH_SEPARATOR = "\\"; - private const string POSIX_FILE_PATH_SEPARATOR = "/"; public static NativeCliInstallCommand GetInstallCommand( RuntimePlatform platform, @@ -44,7 +42,7 @@ public static async Task InstallAsync( UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(cliReleaseTag), "cliReleaseTag must not be null or empty"); ct.ThrowIfCancellationRequested(); - string installDirectory = GetInstallDirectoryForCurrentUser(platform); + string installDirectory = NativeCliInstallPathResolver.GetInstallDirectoryForCurrentUser(platform); if (string.IsNullOrWhiteSpace(installDirectory)) { return new CliInstallResult( @@ -73,7 +71,7 @@ public static async Task UninstallAsync(RuntimePlatform platfo { ct.ThrowIfCancellationRequested(); - string installDirectory = GetInstallDirectoryForCurrentUser(platform); + string installDirectory = NativeCliInstallPathResolver.GetInstallDirectoryForCurrentUser(platform); if (string.IsNullOrWhiteSpace(installDirectory)) { return new CliInstallResult( @@ -90,7 +88,7 @@ public static async Task UninstallAsync(RuntimePlatform platfo return result; } - string installPath = GetGlobalCliInstallPath(installDirectory, platform); + string installPath = NativeCliInstallPathResolver.GetGlobalCliInstallPath(installDirectory, platform); CliInstallResult removalResult = await WaitForUninstallCompletionAsync( installPath, installDirectory, @@ -117,7 +115,7 @@ internal static NativeCliInstallCommand BuildUninstallCommand( { UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); - string installPath = GetGlobalCliInstallPath(installDirectory, platform); + string installPath = NativeCliInstallPathResolver.GetGlobalCliInstallPath(installDirectory, platform); return new NativeCliInstallCommand( installPath, "uninstall", @@ -336,15 +334,6 @@ internal static async Task WaitForUninstallCompletionAsync( } } - internal static string GetGlobalCliInstallPath(string installDirectory, RuntimePlatform platform) - { - UnityEngine.Debug.Assert(!string.IsNullOrEmpty(installDirectory), "installDirectory must not be null or empty"); - - string separator = GetFilePathSeparator(platform); - string normalizedInstallDirectory = installDirectory.TrimEnd('\\', '/'); - return normalizedInstallDirectory + separator + GetGlobalCliInstallFileName(platform); - } - internal static CliInstallResult FinishSuccessfulInstall( CliInstallResult installResult, string installDirectory, @@ -359,276 +348,40 @@ internal static CliInstallResult FinishSuccessfulInstall( return installResult; } - internal static string BuildPathWithInstallDirectory( - string currentPath, - string installDirectory, - RuntimePlatform platform) - { - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); - - string normalizedPath = currentPath ?? ""; - if (string.IsNullOrEmpty(normalizedPath)) - { - return installDirectory; - } - - string separator = GetPathSeparator(platform); - string[] entries = normalizedPath.Split( - new[] { separator }, - StringSplitOptions.RemoveEmptyEntries); - StringComparison comparison = GetPathComparison(platform); - StringBuilder builder = new(installDirectory); - foreach (string entry in entries) - { - if (string.Equals(entry, installDirectory, comparison)) - { - continue; - } - - builder.Append(separator); - builder.Append(entry); - } - - return builder.ToString(); - } - - internal static string BuildPathWithoutInstallDirectory( - string currentPath, - string installDirectory, - RuntimePlatform platform) - { - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); - - string normalizedPath = currentPath ?? ""; - if (string.IsNullOrEmpty(normalizedPath)) - { - return ""; - } - - string separator = GetPathSeparator(platform); - string[] entries = normalizedPath.Split( - new[] { separator }, - StringSplitOptions.RemoveEmptyEntries); - string normalizedInstallDirectory = NormalizePathForComparison(installDirectory, platform); - StringComparison comparison = GetPathComparison(platform); - StringBuilder builder = new StringBuilder(); - foreach (string entry in entries) - { - if (string.IsNullOrWhiteSpace(entry)) - { - continue; - } - - string normalizedEntry = NormalizePathForComparison(entry, platform); - if (string.Equals(normalizedEntry, normalizedInstallDirectory, comparison)) - { - continue; - } - - if (builder.Length > 0) - { - builder.Append(separator); - } - - builder.Append(entry); - } - - return builder.ToString(); - } - - internal static string GetDefaultInstallDirectoryFromRoots( - RuntimePlatform platform, - string homeDirectory, - string localAppData) - { - if (platform == RuntimePlatform.WindowsEditor) - { - if (string.IsNullOrWhiteSpace(localAppData)) - { - return null; - } - - return Path.Combine( - localAppData, - CliConstants.WINDOWS_PROGRAMS_DIR_NAME, - CliConstants.NATIVE_INSTALL_DIR_NAME, - CliConstants.NATIVE_INSTALL_BIN_DIR_NAME); - } - - if (string.IsNullOrWhiteSpace(homeDirectory)) - { - return null; - } - - return Path.Combine( - homeDirectory, - CliConstants.POSIX_LOCAL_DIR_NAME, - CliConstants.NATIVE_INSTALL_BIN_DIR_NAME); - } - private static void ApplyInstallDirectoryToCurrentProcessPath(RuntimePlatform platform) { - string installDirectory = GetInstallDirectoryForCurrentUser(platform); + string installDirectory = NativeCliInstallPathResolver.GetInstallDirectoryForCurrentUser(platform); if (string.IsNullOrEmpty(installDirectory)) { return; } - string pathVariableName = GetPathEnvironmentVariableName(platform); + string pathVariableName = NativeCliInstallPathResolver.GetPathEnvironmentVariableName(platform); string currentPath = Environment.GetEnvironmentVariable(pathVariableName); - string updatedPath = BuildPathWithInstallDirectory(currentPath, installDirectory, platform); + string updatedPath = NativeCliInstallPathResolver.BuildPathWithInstallDirectory( + currentPath, + installDirectory, + platform); Environment.SetEnvironmentVariable(pathVariableName, updatedPath); } private static void RemoveInstallDirectoryFromCurrentProcessPath(RuntimePlatform platform) { - string installDirectory = GetInstallDirectoryForCurrentUser(platform); + string installDirectory = NativeCliInstallPathResolver.GetInstallDirectoryForCurrentUser(platform); if (string.IsNullOrEmpty(installDirectory)) { return; } - string pathVariableName = GetPathEnvironmentVariableName(platform); + string pathVariableName = NativeCliInstallPathResolver.GetPathEnvironmentVariableName(platform); string currentPath = Environment.GetEnvironmentVariable(pathVariableName); - string updatedPath = BuildPathWithoutInstallDirectory(currentPath, installDirectory, platform); + string updatedPath = NativeCliInstallPathResolver.BuildPathWithoutInstallDirectory( + currentPath, + installDirectory, + platform); Environment.SetEnvironmentVariable(pathVariableName, updatedPath); } - internal static bool IsPackageOwnedCurrentUserInstallPath( - string executablePath, - RuntimePlatform platform) - { - if (string.IsNullOrWhiteSpace(executablePath)) - { - return false; - } - - string installDirectory = GetInstallDirectoryForCurrentUser(platform); - if (string.IsNullOrWhiteSpace(installDirectory)) - { - return false; - } - - return IsPackageOwnedInstallPath(executablePath, installDirectory, platform); - } - - internal static bool IsPackageOwnedInstallPath( - string executablePath, - string installDirectory, - RuntimePlatform platform) - { - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); - - if (string.IsNullOrWhiteSpace(executablePath)) - { - return false; - } - - string expectedPath = GetGlobalCliInstallPath(installDirectory, platform); - string normalizedExecutablePath = NormalizePathForComparison(executablePath, platform); - string normalizedExpectedPath = NormalizePathForComparison(expectedPath, platform); - return string.Equals( - normalizedExecutablePath, - normalizedExpectedPath, - GetPathComparison(platform)); - } - - internal static string GetCurrentUserGlobalCliInstallPath(RuntimePlatform platform) - { - string installDirectory = GetInstallDirectoryForCurrentUser(platform); - return string.IsNullOrWhiteSpace(installDirectory) - ? null - : GetGlobalCliInstallPath(installDirectory, platform); - } - - internal static string GetCurrentUserGlobalCliInstallDirectory(RuntimePlatform platform) - { - return GetInstallDirectoryForCurrentUser(platform); - } - - internal static bool HasPackageOwnedCurrentUserInstall(RuntimePlatform platform) - { - string executablePath = GetCurrentUserGlobalCliInstallPath(platform); - return !string.IsNullOrWhiteSpace(executablePath) && File.Exists(executablePath); - } - - private static string GetInstallDirectoryForCurrentUser(RuntimePlatform platform) - { - string configuredInstallDirectory = Environment.GetEnvironmentVariable(CliConstants.INSTALL_DIR_ENVIRONMENT_VARIABLE); - if (!string.IsNullOrWhiteSpace(configuredInstallDirectory)) - { - return configuredInstallDirectory; - } - - string homeDirectory = Environment.GetEnvironmentVariable(CliConstants.POSIX_HOME_ENVIRONMENT_VARIABLE); - if (string.IsNullOrWhiteSpace(homeDirectory)) - { - homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - } - - string localAppData = Environment.GetEnvironmentVariable(CliConstants.WINDOWS_LOCAL_APPDATA_ENVIRONMENT_VARIABLE); - return GetDefaultInstallDirectoryFromRoots(platform, homeDirectory, localAppData); - } - - private static string GetPathEnvironmentVariableName(RuntimePlatform platform) - { - return platform == RuntimePlatform.WindowsEditor - ? CliConstants.WINDOWS_PATH_ENVIRONMENT_VARIABLE - : CliConstants.POSIX_PATH_ENVIRONMENT_VARIABLE; - } - - private static string GetPathSeparator(RuntimePlatform platform) - { - return platform == RuntimePlatform.WindowsEditor - ? CliConstants.WINDOWS_PATH_SEPARATOR - : CliConstants.POSIX_PATH_SEPARATOR; - } - - private static string GetFilePathSeparator(RuntimePlatform platform) - { - return platform == RuntimePlatform.WindowsEditor - ? WINDOWS_FILE_PATH_SEPARATOR - : POSIX_FILE_PATH_SEPARATOR; - } - - private static StringComparison GetPathComparison(RuntimePlatform platform) - { - return platform == RuntimePlatform.WindowsEditor - ? StringComparison.OrdinalIgnoreCase - : StringComparison.Ordinal; - } - - private static string NormalizePathForComparison(string path, RuntimePlatform platform) - { - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(path), "path must not be null or empty"); - - string normalizedPath = path.Trim().Trim('"'); - if (platform != RuntimePlatform.WindowsEditor) - { - return normalizedPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - } - - return normalizedPath.TrimEnd('\\', '/').Replace('/', '\\'); - } - - private static bool DoesUserPathContainInstallDirectory( - string installDirectory, - RuntimePlatform platform, - Func getEnvironmentVariable) - { - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); - UnityEngine.Debug.Assert(getEnvironmentVariable != null, "getEnvironmentVariable must not be null"); - - if (platform != RuntimePlatform.WindowsEditor) - { - return false; - } - - string pathVariableName = GetPathEnvironmentVariableName(platform); - string currentUserPath = getEnvironmentVariable(pathVariableName, EnvironmentVariableTarget.User); - return DoesPathContainInstallDirectory(currentUserPath, installDirectory, platform); - } - private static bool ShouldWaitForUserPathRemoval( bool requireUserPathRemoval, string installDirectory, @@ -639,43 +392,10 @@ private static bool ShouldWaitForUserPathRemoval( UnityEngine.Debug.Assert(getEnvironmentVariable != null, "getEnvironmentVariable must not be null"); return requireUserPathRemoval - && DoesUserPathContainInstallDirectory(installDirectory, platform, getEnvironmentVariable); - } - - private static bool DoesPathContainInstallDirectory( - string currentPath, - string installDirectory, - RuntimePlatform platform) - { - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); - - string normalizedPath = currentPath ?? ""; - if (string.IsNullOrEmpty(normalizedPath)) - { - return false; - } - - string separator = GetPathSeparator(platform); - string[] entries = normalizedPath.Split( - new[] { separator }, - StringSplitOptions.RemoveEmptyEntries); - string normalizedInstallDirectory = NormalizePathForComparison(installDirectory, platform); - StringComparison comparison = GetPathComparison(platform); - foreach (string entry in entries) - { - if (string.IsNullOrWhiteSpace(entry)) - { - continue; - } - - string normalizedEntry = NormalizePathForComparison(entry, platform); - if (string.Equals(normalizedEntry, normalizedInstallDirectory, comparison)) - { - return true; - } - } - - return false; + && NativeCliInstallPathResolver.DoesUserPathContainInstallDirectory( + installDirectory, + platform, + getEnvironmentVariable); } private static string BuildUninstallCompletionTimeoutFailure( @@ -797,13 +517,6 @@ private static void KillProcessIfRunning(Process process) } } - private static string GetGlobalCliInstallFileName(RuntimePlatform platform) - { - return platform == RuntimePlatform.WindowsEditor - ? CliConstants.GLOBAL_WINDOWS_COMMAND_NAME - : CliConstants.GLOBAL_UNIX_COMMAND_NAME; - } - } /// @@ -813,12 +526,12 @@ public sealed class NativeCliInstallerService : INativeCliInstaller { public bool IsPackageOwnedCurrentUserInstallPath(string cliExecutablePath, RuntimePlatform platform) { - return NativeCliInstaller.IsPackageOwnedCurrentUserInstallPath(cliExecutablePath, platform); + return NativeCliInstallPathResolver.IsPackageOwnedCurrentUserInstallPath(cliExecutablePath, platform); } public bool HasPackageOwnedCurrentUserInstall(RuntimePlatform platform) { - return NativeCliInstaller.HasPackageOwnedCurrentUserInstall(platform); + return NativeCliInstallPathResolver.HasPackageOwnedCurrentUserInstall(platform); } public Task InstallGlobalCliAsync( From 64db3a31f70f7d17be4815e4964ea905853ca22a Mon Sep 17 00:00:00 2001 From: hatayama Date: Wed, 8 Jul 2026 23:06:44 +0900 Subject: [PATCH 2/2] Move native CLI uninstall command construction Place uninstall command assembly beside install command construction now that path resolution has a dedicated dependency. Restore process argument quoting to private visibility because it is again builder-internal. --- Assets/Tests/Editor/NativeCliInstallerTests.cs | 4 ++-- .../CLI/NativeCliCommandBuilder.cs | 15 ++++++++++++++- .../Infrastructure/CLI/NativeCliInstaller.cs | 17 +++-------------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/Assets/Tests/Editor/NativeCliInstallerTests.cs b/Assets/Tests/Editor/NativeCliInstallerTests.cs index 3bdb8ba9df..e9f87194e9 100644 --- a/Assets/Tests/Editor/NativeCliInstallerTests.cs +++ b/Assets/Tests/Editor/NativeCliInstallerTests.cs @@ -450,7 +450,7 @@ public void UninstallCompletionTimeout_IsLongEnoughForDeferredWindowsPowerShellC public void BuildUninstallCommand_OnMacRunsInstalledLauncher() { // Verifies that editor uninstall delegates removal to the installed uloop command. - NativeCliInstallCommand command = NativeCliInstaller.BuildUninstallCommand( + NativeCliInstallCommand command = NativeCliCommandBuilder.BuildUninstallCommand( "/Users/ExampleUser/.local/bin", RuntimePlatform.OSXEditor); @@ -463,7 +463,7 @@ public void BuildUninstallCommand_OnMacRunsInstalledLauncher() public void BuildUninstallCommand_OnWindowsRunsInstalledLauncher() { // Verifies that Windows editor uninstall delegates removal to the installed uloop command. - NativeCliInstallCommand command = NativeCliInstaller.BuildUninstallCommand( + NativeCliInstallCommand command = NativeCliCommandBuilder.BuildUninstallCommand( "C:\\Users\\ExampleUser\\AppData\\Local\\Programs\\uloop\\bin", RuntimePlatform.WindowsEditor); diff --git a/Packages/src/Editor/Infrastructure/CLI/NativeCliCommandBuilder.cs b/Packages/src/Editor/Infrastructure/CLI/NativeCliCommandBuilder.cs index c27cd37dd9..5698b28606 100644 --- a/Packages/src/Editor/Infrastructure/CLI/NativeCliCommandBuilder.cs +++ b/Packages/src/Editor/Infrastructure/CLI/NativeCliCommandBuilder.cs @@ -70,6 +70,19 @@ internal static NativeCliInstallCommand BuildInstallCommandWithPackagePath( loginShellCommand); } + internal static NativeCliInstallCommand BuildUninstallCommand( + string installDirectory, + RuntimePlatform platform) + { + UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); + + string installPath = NativeCliInstallPathResolver.GetGlobalCliInstallPath(installDirectory, platform); + return new NativeCliInstallCommand( + installPath, + "uninstall", + $"{QuoteProcessArgument(installPath)} uninstall"); + } + private static string BuildPosixRemoteInstallScriptCommand(string scriptUrl, string releaseTag) { UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(scriptUrl), "scriptUrl must not be null or empty"); @@ -155,7 +168,7 @@ private static string QuotePowerShellSingleQuotedValue(string value) return $"'{value.Replace("'", "''")}'"; } - internal static string QuoteProcessArgument(string value) + private static string QuoteProcessArgument(string value) { UnityEngine.Debug.Assert(value != null, "value must not be null"); return $"\"{value.Replace("\"", "\\\"")}\""; diff --git a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs index c52eac057c..1fafdf5400 100644 --- a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs +++ b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs @@ -79,7 +79,9 @@ public static async Task UninstallAsync(RuntimePlatform platfo $"Could not resolve the global CLI install directory. Set {CliConstants.INSTALL_DIR_ENVIRONMENT_VARIABLE} and try again."); } - NativeCliInstallCommand command = BuildUninstallCommand(installDirectory, platform); + NativeCliInstallCommand command = NativeCliCommandBuilder.BuildUninstallCommand( + installDirectory, + platform); CliInstallResult result = await Task.Run( () => RunUninstallCommand(command, installDirectory, ct, INSTALL_PROCESS_TIMEOUT_MS), ct); @@ -109,19 +111,6 @@ public static async Task UninstallAsync(RuntimePlatform platfo return result; } - internal static NativeCliInstallCommand BuildUninstallCommand( - string installDirectory, - RuntimePlatform platform) - { - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); - - string installPath = NativeCliInstallPathResolver.GetGlobalCliInstallPath(installDirectory, platform); - return new NativeCliInstallCommand( - installPath, - "uninstall", - $"{NativeCliCommandBuilder.QuoteProcessArgument(installPath)} uninstall"); - } - internal static CliInstallResult RunInstallCommand( NativeCliInstallCommand command, CancellationToken ct,