Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 98 additions & 2 deletions Assets/Tests/Editor/NativeCliInstallerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,125 @@ public class NativeCliInstallerTests
[Test]
public void GetInstallCommand_OnMacUsesDirectInstallScriptWithoutNpm()
{
// Verifies that macOS installs through the native release script, not npm.
NativeCliInstallCommand command = NativeCliInstaller.GetInstallCommand(
RuntimePlatform.OSXEditor,
"3.0.0-beta.0");
"3.0.0-beta.0",
false);

Assert.That(command.FileName, Is.EqualTo("/bin/sh"));
Assert.That(command.Arguments, Does.Contain("https://github.com/hatayama/unity-cli-loop/releases/download/v3.0.0-beta.0/install.sh"));
Assert.That(command.Arguments, Does.Contain("ULOOP_VERSION='v3.0.0-beta.0'"));
Assert.That(command.Arguments, Does.Not.Contain("ULOOP_REMOVE_LEGACY"));
Assert.That(command.ManualCommand, Does.Contain("curl -fsSL"));
Assert.That(command.ManualCommand, Does.Not.Contain("npm"));
}

[Test]
public void GetInstallCommand_OnWindowsUsesPowerShellInstallScriptWithoutNpm()
{
// Verifies that Windows installs through the native release script, not npm.
NativeCliInstallCommand command = NativeCliInstaller.GetInstallCommand(
RuntimePlatform.WindowsEditor,
"3.0.0-beta.0");
"3.0.0-beta.0",
false);

Assert.That(command.FileName, Is.EqualTo("powershell"));
Assert.That(command.Arguments, Does.Contain("https://github.com/hatayama/unity-cli-loop/releases/download/v3.0.0-beta.0/install.ps1"));
Assert.That(command.Arguments, Does.Contain("$env:ULOOP_VERSION='v3.0.0-beta.0'"));
Assert.That(command.Arguments, Does.Not.Contain("ULOOP_REMOVE_LEGACY"));
Assert.That(command.ManualCommand, Does.Contain("irm"));
Assert.That(command.ManualCommand, Does.Not.Contain("npm"));
}

[Test]
public void GetInstallCommand_OnMacCanOptIntoLegacyNpmRemoval()
{
// Verifies that UI-triggered macOS installs can opt into removing the legacy npm launcher.
NativeCliInstallCommand command = NativeCliInstaller.GetInstallCommand(
RuntimePlatform.OSXEditor,
"3.0.0-beta.0",
true);

Assert.That(command.Arguments, Does.Contain("ULOOP_REMOVE_LEGACY='1'"));
Assert.That(command.ManualCommand, Does.Contain("ULOOP_REMOVE_LEGACY='1'"));
}

[Test]
public void GetInstallCommand_OnWindowsCanOptIntoLegacyNpmRemoval()
{
// Verifies that UI-triggered Windows installs can opt into removing the legacy npm launcher.
NativeCliInstallCommand command = NativeCliInstaller.GetInstallCommand(
RuntimePlatform.WindowsEditor,
"3.0.0-beta.0",
true);

Assert.That(command.Arguments, Does.Contain("$env:ULOOP_REMOVE_LEGACY='1'"));
Assert.That(command.ManualCommand, Does.Contain("$env:ULOOP_REMOVE_LEGACY='1'"));
}

[Test]
public void BuildPathWithInstallDirectory_OnWindowsPrependsMissingNativeInstallDir()
{
// Verifies that Unity's current Windows PATH prefers the freshly installed native CLI.
string result = NativeCliInstaller.BuildPathWithInstallDirectory(
"C:\\npm",
"C:\\Users\\masamichi\\Programs\\uloop\\bin",
RuntimePlatform.WindowsEditor);

Assert.That(result, Is.EqualTo("C:\\Users\\masamichi\\Programs\\uloop\\bin;C:\\npm"));
}

[Test]
public void BuildPathWithInstallDirectory_OnWindowsMovesExistingNativeInstallDirToFront()
{
// Verifies that a later Windows native install dir does not leave an earlier npm shim first.
string result = NativeCliInstaller.BuildPathWithInstallDirectory(
"C:\\npm;C:\\USERS\\MASAMICHI\\PROGRAMS\\ULOOP\\BIN",
"C:\\Users\\masamichi\\Programs\\uloop\\bin",
RuntimePlatform.WindowsEditor);

Assert.That(result, Is.EqualTo("C:\\Users\\masamichi\\Programs\\uloop\\bin;C:\\npm"));
}

[Test]
public void BuildPathWithInstallDirectory_OnMacPrependsMissingNativeInstallDir()
{
// Verifies that POSIX PATH prefers the freshly installed native CLI.
string result = NativeCliInstaller.BuildPathWithInstallDirectory(
"/usr/local/bin",
"/Users/masamichi/.local/bin",
RuntimePlatform.OSXEditor);

Assert.That(result, Is.EqualTo("/Users/masamichi/.local/bin:/usr/local/bin"));
}

[Test]
public void GetDefaultInstallDirectoryFromRoots_OnMacMatchesInstallerDefault()
{
// Verifies that Unity mirrors the POSIX installer default install directory.
string result = NativeCliInstaller.GetDefaultInstallDirectoryFromRoots(
RuntimePlatform.OSXEditor,
"/Users/masamichi",
null);

Assert.That(result, Is.EqualTo(System.IO.Path.Combine("/Users/masamichi", ".local", "bin")));
}

[Test]
public void GetDefaultInstallDirectoryFromRoots_OnWindowsMatchesInstallerDefault()
{
// Verifies that Unity mirrors the PowerShell installer default install directory.
string result = NativeCliInstaller.GetDefaultInstallDirectoryFromRoots(
RuntimePlatform.WindowsEditor,
null,
"C:\\Users\\masamichi\\AppData\\Local");

Assert.That(result, Is.EqualTo(System.IO.Path.Combine(
"C:\\Users\\masamichi\\AppData\\Local",
"Programs",
"uloop",
"bin")));
}
}
}
13 changes: 13 additions & 0 deletions Packages/src/Editor/CLI/CliConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,20 @@ public static class CliConstants
public const string RELEASE_DOWNLOAD_BASE_URL = "https://github.com/hatayama/unity-cli-loop/releases/download";
public const string POSIX_INSTALL_SCRIPT_NAME = "install.sh";
public const string WINDOWS_INSTALL_SCRIPT_NAME = "install.ps1";
public const string INSTALL_DIR_ENVIRONMENT_VARIABLE = "ULOOP_INSTALL_DIR";
public const string INSTALL_VERSION_ENVIRONMENT_VARIABLE = "ULOOP_VERSION";
public const string REMOVE_LEGACY_ENVIRONMENT_VARIABLE = "ULOOP_REMOVE_LEGACY";
public const string REMOVE_LEGACY_ENABLED_VALUE = "1";
public const string POSIX_HOME_ENVIRONMENT_VARIABLE = "HOME";
public const string POSIX_PATH_ENVIRONMENT_VARIABLE = "PATH";
public const string WINDOWS_LOCAL_APPDATA_ENVIRONMENT_VARIABLE = "LOCALAPPDATA";
public const string WINDOWS_PATH_ENVIRONMENT_VARIABLE = "Path";
public const string POSIX_LOCAL_DIR_NAME = ".local";
public const string WINDOWS_PROGRAMS_DIR_NAME = "Programs";
public const string NATIVE_INSTALL_DIR_NAME = "uloop";
public const string NATIVE_INSTALL_BIN_DIR_NAME = "bin";
public const string POSIX_PATH_SEPARATOR = ":";
public const string WINDOWS_PATH_SEPARATOR = ";";
public const string RELEASE_TAG_PREFIX = "v";
public const string SKILL_DIR_PREFIX = "uloop-";
public const string SKILL_DIR_GLOB = "uloop-*";
Expand Down
175 changes: 172 additions & 3 deletions Packages/src/Editor/CLI/NativeCliInstaller.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
Expand Down Expand Up @@ -29,7 +30,10 @@ public NativeCliInstallCommand(string fileName, string arguments, string manualC
/// </summary>
public static class NativeCliInstaller
{
public static NativeCliInstallCommand GetInstallCommand(RuntimePlatform platform, string packageVersion)
public static NativeCliInstallCommand GetInstallCommand(
RuntimePlatform platform,
string packageVersion,
bool removeLegacyNpm)
{
UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(packageVersion), "packageVersion must not be null or empty");

Expand All @@ -39,6 +43,7 @@ public static NativeCliInstallCommand GetInstallCommand(RuntimePlatform platform
string scriptUrl = BuildReleaseAssetUrl(releaseTag, CliConstants.WINDOWS_INSTALL_SCRIPT_NAME);
string command =
$"$env:{CliConstants.INSTALL_VERSION_ENVIRONMENT_VARIABLE}='{releaseTag}'; " +
BuildWindowsRemoveLegacyAssignment(removeLegacyNpm) +
$"irm '{scriptUrl}' | iex";
return new NativeCliInstallCommand(
"powershell",
Expand All @@ -49,16 +54,20 @@ public static NativeCliInstallCommand GetInstallCommand(RuntimePlatform platform
string posixScriptUrl = BuildReleaseAssetUrl(releaseTag, CliConstants.POSIX_INSTALL_SCRIPT_NAME);
string posixCommand =
$"curl -fsSL '{posixScriptUrl}' | " +
BuildPosixRemoveLegacyAssignment(removeLegacyNpm) +
$"{CliConstants.INSTALL_VERSION_ENVIRONMENT_VARIABLE}='{releaseTag}' sh";
return new NativeCliInstallCommand(
"/bin/sh",
$"-c \"{posixCommand}\"",
posixCommand);
}

public static async Task<CliInstallResult> InstallAsync(RuntimePlatform platform, string packageVersion)
public static async Task<CliInstallResult> InstallAsync(
RuntimePlatform platform,
string packageVersion,
bool removeLegacyNpm)
{
NativeCliInstallCommand command = GetInstallCommand(platform, packageVersion);
NativeCliInstallCommand command = GetInstallCommand(platform, packageVersion, removeLegacyNpm);
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = command.FileName,
Expand All @@ -74,6 +83,7 @@ public static async Task<CliInstallResult> InstallAsync(RuntimePlatform platform

await Task.Run(() =>
{
ApplyInstallerSearchPath(startInfo, platform);
Process process = ProcessStartHelper.TryStart(startInfo);
if (process == null)
{
Expand Down Expand Up @@ -102,10 +112,169 @@ await Task.Run(() =>
}
});

if (success)
{
ApplyInstallDirectoryToCurrentProcessPath(platform);
}

CliInstallationDetector.InvalidateCache();
return new CliInstallResult(success, errorOutput);
}

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 StringBuilder(installDirectory);
foreach (string entry in entries)
{
if (string.Equals(entry, installDirectory, comparison))
{
continue;
}

builder.Append(separator);
builder.Append(entry);
}

return builder.ToString();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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 ApplyInstallerSearchPath(ProcessStartInfo startInfo, RuntimePlatform platform)
{
UnityEngine.Debug.Assert(startInfo != null, "startInfo must not be null");

if (platform == RuntimePlatform.WindowsEditor)
{
return;
}

string loginShellPath = NodeEnvironmentResolver.GetLoginShellPathAtPlatform(platform);
if (string.IsNullOrEmpty(loginShellPath))
{
return;
}

startInfo.Environment[CliConstants.POSIX_PATH_ENVIRONMENT_VARIABLE] = loginShellPath;
}

private static void ApplyInstallDirectoryToCurrentProcessPath(RuntimePlatform platform)
{
string installDirectory = GetInstallDirectoryForCurrentUser(platform);
if (string.IsNullOrEmpty(installDirectory))
{
return;
}

string pathVariableName = GetPathEnvironmentVariableName(platform);
string currentPath = Environment.GetEnvironmentVariable(pathVariableName);
string updatedPath = BuildPathWithInstallDirectory(currentPath, installDirectory, platform);
Environment.SetEnvironmentVariable(pathVariableName, updatedPath);
}

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 StringComparison GetPathComparison(RuntimePlatform platform)
{
return platform == RuntimePlatform.WindowsEditor
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
}

private static string BuildWindowsRemoveLegacyAssignment(bool removeLegacyNpm)
{
if (!removeLegacyNpm)
{
return "";
}

return $"$env:{CliConstants.REMOVE_LEGACY_ENVIRONMENT_VARIABLE}='{CliConstants.REMOVE_LEGACY_ENABLED_VALUE}'; ";
}

private static string BuildPosixRemoveLegacyAssignment(bool removeLegacyNpm)
{
if (!removeLegacyNpm)
{
return "";
}

return $"{CliConstants.REMOVE_LEGACY_ENVIRONMENT_VARIABLE}='{CliConstants.REMOVE_LEGACY_ENABLED_VALUE}' ";
}

private static string BuildReleaseTag(string packageVersion)
{
if (packageVersion.StartsWith(CliConstants.RELEASE_TAG_PREFIX, StringComparison.Ordinal))
Expand Down
6 changes: 4 additions & 2 deletions Packages/src/Editor/UI/McpEditorWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -705,13 +705,15 @@ private async void HandleInstallCli()
{
CliInstallResult result = await NativeCliInstaller.InstallAsync(
Application.platform,
McpConstants.PackageInfo.version);
McpConstants.PackageInfo.version,
true);

if (!result.Success)
{
NativeCliInstallCommand command = NativeCliInstaller.GetInstallCommand(
Application.platform,
McpConstants.PackageInfo.version);
McpConstants.PackageInfo.version,
true);
EditorUtility.DisplayDialog(
"Installation Failed",
$"Failed to install uLoop CLI.\n\n{result.ErrorOutput}\n\nYou can try manually:\n{command.ManualCommand}",
Expand Down
Loading