From b1252b29a591848cfb199d81b2c9dd75d346df7f Mon Sep 17 00:00:00 2001 From: Chris Yarbrough <17833862+chrisyarbrough@users.noreply.github.com> Date: Sun, 25 Jan 2026 02:34:42 +0100 Subject: [PATCH 1/3] Add instructions for publishing to and installing from Nuget --- README.md | 27 +++++++++++++++++++++++++- src/ucll.build/Publishing.md | 37 ++++++++++++++++++++++++++++++++---- src/ucll/ucll.csproj | 4 ++++ 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3a60082..6dbd36d 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,32 @@ If the `version` argument is omitted, an interactive prompt shows all installed ## Installation -### From Source (recommended) +### NuGet Global Tool + +The easiest way to install `ucll` is as a .NET global tool via NuGet: + +```shell +dotnet tool install --global UnityCommandLineLauncher +``` + +**Requirements:** +- [.NET 10.0](https://dotnet.microsoft.com/download) or newer + +> If you don't want to install .NET just for this tool, you can use a self-contained (standalone) binary, see below. + +**Updating:** +```shell +dotnet tool update --global UnityCommandLineLauncher +``` + +**Uninstalling:** +```shell +dotnet tool uninstall --global UnityCommandLineLauncher +``` + +After installation, the `ucll` command will be available globally in your terminal. + +### From Source 1. Clone the repository. 2. Checkout a release tag and take note of the signature, e.g. `git tag -v v1.0.0`. diff --git a/src/ucll.build/Publishing.md b/src/ucll.build/Publishing.md index 094ddeb..b418e28 100644 --- a/src/ucll.build/Publishing.md +++ b/src/ucll.build/Publishing.md @@ -1,14 +1,43 @@ # Publishing (for Maintainers) -The GitHub `release.yml` workflow takes care of running this project to produce binaries and then -signs them before uploading a new release. +## GitHub Release (Self-Contained Binary) -To publish locally: +The GitHub `release.yml` workflow takes care of running this project to produce binaries for each platform and then +signs them before uploading a new release. +If this is not available, and we need to publish locally: Install the `gpg` utility to let the publish process sign release artifacts with your personal key. ```shell dotnet run ``` -Artifacts are signed with the default key which you have configured (or the first secret key it finds). \ No newline at end of file +Artifacts are signed with the default key which you have configured (or the first secret key it finds). + +## Dotnet Tool + +```shell +PACK_DIR=bin/pack +BUILD_DIR=$PACK_DIR/build +dotnet pack ../ucll/ucll.csproj \ + -p:PackAsTool=true \ + -p:ToolCommandName=ucll \ + -p:PackageId=UnityCommandLineLauncher \ + -p:Authors="Chris Yarbrough" \ + -p:PackageLicenseExpression=MIT \ + -p:PackageReadmeFile=README.md \ + -p:PackageTags="Unity CLI Hub" \ + -p:RepositoryUrl=https://github.com/chrisyarbrough/UnityCommandLineLauncher \ + -p:RepositoryType=git \ + -p:OutputPath=$BUILD_DIR \ + -p:PublishDir=$BUILD_DIR \ + --output ../ucll/$PACK_DIR +``` + +```shell +dotnet nuget push ../ucll/bin/pack/UnityCommandLineLauncher.*.nupkg --api-key --source https://apiint.nugettest.org/v3/index.json +``` + +``` +https://api.nuget.org/v3/index.json +``` \ No newline at end of file diff --git a/src/ucll/ucll.csproj b/src/ucll/ucll.csproj index 8584f97..7e80641 100644 --- a/src/ucll/ucll.csproj +++ b/src/ucll/ucll.csproj @@ -40,4 +40,8 @@ + + + + From 8ac16a150a5263ec5c66eae121a5f82ba9eaca18 Mon Sep 17 00:00:00 2001 From: Chris Yarbrough <17833862+chrisyarbrough@users.noreply.github.com> Date: Mon, 26 Jan 2026 01:03:26 +0100 Subject: [PATCH 2/3] Add Windows test for FindInstallationRoot --- README.md | 5 ++- src/ucll.tests/PlatformSupportTests.cs | 29 ++++++--------- src/ucll/Shared/Platform/PlatformSupport.cs | 4 +-- src/ucll/Shared/Platform/WindowsSupport.cs | 5 +++ src/ucll/Shared/VersionUsage.cs | 39 +++++++++------------ 5 files changed, 37 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 6dbd36d..fc024cd 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A terminal command to open Unity projects quickly from the command line. - macOS - Windows -- Linux +- Linux (TBD) ## Commands @@ -53,16 +53,19 @@ dotnet tool install --global UnityCommandLineLauncher ``` **Requirements:** + - [.NET 10.0](https://dotnet.microsoft.com/download) or newer > If you don't want to install .NET just for this tool, you can use a self-contained (standalone) binary, see below. **Updating:** + ```shell dotnet tool update --global UnityCommandLineLauncher ``` **Uninstalling:** + ```shell dotnet tool uninstall --global UnityCommandLineLauncher ``` diff --git a/src/ucll.tests/PlatformSupportTests.cs b/src/ucll.tests/PlatformSupportTests.cs index 2b72b00..c6bdf39 100644 --- a/src/ucll.tests/PlatformSupportTests.cs +++ b/src/ucll.tests/PlatformSupportTests.cs @@ -1,25 +1,16 @@ -using System.Runtime.InteropServices; - public class PlatformSupportTests { - [Fact_PlatformOSX] - public void FindInstallationRootReturnsValidRoot() + [Theory] + [InlineData(typeof(MacSupport), + "/Applications/Unity/Hub/Editor/2022.3.10f1/Unity.app", + "/Applications/Unity/Hub/Editor/2022.3.10f1")] + [InlineData(typeof(WindowsSupport), + @"C:\Program Files\Unity\Hub\Editor\6000.0.59f2\Editor\Unity.exe", + @"C:\Program Files\Unity\Hub\Editor\6000.0.59f2")] + public void FindInstallationRootReturnsValidRoot(Type platformSupportType, string editorPath, string expectedRoot) { - PlatformSupport platformSupport = PlatformSupport.Create(); - const string editorPath = "/Applications/Unity/Hub/Editor/2022.3.10f1/Unity.app/Contents/MacOS/Unity"; + PlatformSupport platformSupport = (Activator.CreateInstance(platformSupportType) as PlatformSupport)!; string root = platformSupport.FindInstallationRoot(editorPath); - Assert.Equal("/Applications/Unity/Hub/Editor/2022.3.10f1", root); - } - - // ReSharper disable once InconsistentNaming - private sealed class Fact_PlatformOSX : FactAttribute - { - public Fact_PlatformOSX() - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - Skip = "Test only runs on macOS"; - } - } + Assert.Equal(expectedRoot, root); } } \ No newline at end of file diff --git a/src/ucll/Shared/Platform/PlatformSupport.cs b/src/ucll/Shared/Platform/PlatformSupport.cs index 003ec47..6a35d09 100644 --- a/src/ucll/Shared/Platform/PlatformSupport.cs +++ b/src/ucll/Shared/Platform/PlatformSupport.cs @@ -34,8 +34,8 @@ public static PlatformSupport Create() /// /// Given the path to an editor executable, returns the root directory path of the installation. /// - public string FindInstallationRoot(string editorPath) - => new DirectoryInfo(editorPath.Replace(RelativeEditorPathToExecutable, string.Empty)).Parent!.FullName; + public virtual string FindInstallationRoot(string editorPath) + => Path.GetDirectoryName(editorPath)!; /// /// Path to the Unity Hub executable or null if it doesn't exist (or couldn't be found). diff --git a/src/ucll/Shared/Platform/WindowsSupport.cs b/src/ucll/Shared/Platform/WindowsSupport.cs index a185988..2a17c81 100644 --- a/src/ucll/Shared/Platform/WindowsSupport.cs +++ b/src/ucll/Shared/Platform/WindowsSupport.cs @@ -14,6 +14,11 @@ public override ProcessStartInfo OpenFileWithApp(string filePath, string applica public override string RelativeEditorPathToExecutable => @"Editor\Unity.exe"; + public override string FindInstallationRoot(string editorPath) + { + return editorPath.Replace(@"Editor\Unity.exe", string.Empty).TrimEnd('\\'); + } + public override string UnityHubConfigDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "UnityHub"); diff --git a/src/ucll/Shared/VersionUsage.cs b/src/ucll/Shared/VersionUsage.cs index 94e08e7..837933e 100644 --- a/src/ucll/Shared/VersionUsage.cs +++ b/src/ucll/Shared/VersionUsage.cs @@ -36,32 +36,25 @@ public static IEnumerable FindUnityProjects(PlatformSupport platformSupp public string[] GetInstalledModules(string version) { - try - { - string editorPath = unityHub.GetEditorPath(version); - string installationRoot = platformSupport.FindInstallationRoot(editorPath); - string modulesJsonPath = Path.Combine(installationRoot, "modules.json"); + string editorPath = unityHub.GetEditorPath(version); + string installationRoot = platformSupport.FindInstallationRoot(editorPath); + string modulesJsonPath = Path.Combine(installationRoot, "modules.json"); - if (!File.Exists(modulesJsonPath)) - return []; + if (!File.Exists(modulesJsonPath)) + return []; - string json = File.ReadAllText(modulesJsonPath); + string json = File.ReadAllText(modulesJsonPath); - using JsonDocument doc = JsonDocument.Parse(json); + using JsonDocument doc = JsonDocument.Parse(json); - return doc.RootElement - .EnumerateArray() - .Where(module => module.TryGetProperty("selected", out var selected) && selected.GetBoolean()) - .Where(module => module.TryGetProperty("visible", out var visible) && visible.GetBoolean()) - .Where(module => module.TryGetProperty("name", out _)) - .Select(module => module.GetProperty("name").GetString()) - .Where(name => name != null) - .Cast() - .ToArray(); - } - catch - { - return []; - } + return doc.RootElement + .EnumerateArray() + .Where(module => module.TryGetProperty("selected", out var selected) && selected.GetBoolean()) + .Where(module => module.TryGetProperty("visible", out var visible) && visible.GetBoolean()) + .Where(module => module.TryGetProperty("name", out _)) + .Select(module => module.GetProperty("name").GetString()) + .Where(name => name != null) + .Cast() + .ToArray(); } } \ No newline at end of file From 9a89bb88e024218372887554b27f3c9c36319337 Mon Sep 17 00:00:00 2001 From: Chris Yarbrough <17833862+chrisyarbrough@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:07:04 +0100 Subject: [PATCH 3/3] Fix incorrect Unity open path for non-default installations on Windows --- README.md | 30 +--------------- src/ucll.build/Publishing.md | 37 +++---------------- src/ucll.tests/PlatformSupportTests.cs | 2 +- src/ucll/Shared/Platform/LinuxSupport.cs | 7 ++-- src/ucll/Shared/Platform/MacSupport.cs | 10 +++++- src/ucll/Shared/Platform/PlatformSupport.cs | 7 ++-- src/ucll/Shared/Platform/WindowsSupport.cs | 2 -- src/ucll/Shared/UnityHub.cs | 13 +++---- src/ucll/Shared/VersionUsage.cs | 39 ++++++++++++--------- src/ucll/ucll.csproj | 4 --- 10 files changed, 53 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index fc024cd..6d00f2f 100644 --- a/README.md +++ b/README.md @@ -44,35 +44,7 @@ If the `version` argument is omitted, an interactive prompt shows all installed ## Installation -### NuGet Global Tool - -The easiest way to install `ucll` is as a .NET global tool via NuGet: - -```shell -dotnet tool install --global UnityCommandLineLauncher -``` - -**Requirements:** - -- [.NET 10.0](https://dotnet.microsoft.com/download) or newer - -> If you don't want to install .NET just for this tool, you can use a self-contained (standalone) binary, see below. - -**Updating:** - -```shell -dotnet tool update --global UnityCommandLineLauncher -``` - -**Uninstalling:** - -```shell -dotnet tool uninstall --global UnityCommandLineLauncher -``` - -After installation, the `ucll` command will be available globally in your terminal. - -### From Source +### From Source (recommended) 1. Clone the repository. 2. Checkout a release tag and take note of the signature, e.g. `git tag -v v1.0.0`. diff --git a/src/ucll.build/Publishing.md b/src/ucll.build/Publishing.md index b418e28..094ddeb 100644 --- a/src/ucll.build/Publishing.md +++ b/src/ucll.build/Publishing.md @@ -1,43 +1,14 @@ # Publishing (for Maintainers) -## GitHub Release (Self-Contained Binary) - -The GitHub `release.yml` workflow takes care of running this project to produce binaries for each platform and then +The GitHub `release.yml` workflow takes care of running this project to produce binaries and then signs them before uploading a new release. -If this is not available, and we need to publish locally: +To publish locally: + Install the `gpg` utility to let the publish process sign release artifacts with your personal key. ```shell dotnet run ``` -Artifacts are signed with the default key which you have configured (or the first secret key it finds). - -## Dotnet Tool - -```shell -PACK_DIR=bin/pack -BUILD_DIR=$PACK_DIR/build -dotnet pack ../ucll/ucll.csproj \ - -p:PackAsTool=true \ - -p:ToolCommandName=ucll \ - -p:PackageId=UnityCommandLineLauncher \ - -p:Authors="Chris Yarbrough" \ - -p:PackageLicenseExpression=MIT \ - -p:PackageReadmeFile=README.md \ - -p:PackageTags="Unity CLI Hub" \ - -p:RepositoryUrl=https://github.com/chrisyarbrough/UnityCommandLineLauncher \ - -p:RepositoryType=git \ - -p:OutputPath=$BUILD_DIR \ - -p:PublishDir=$BUILD_DIR \ - --output ../ucll/$PACK_DIR -``` - -```shell -dotnet nuget push ../ucll/bin/pack/UnityCommandLineLauncher.*.nupkg --api-key --source https://apiint.nugettest.org/v3/index.json -``` - -``` -https://api.nuget.org/v3/index.json -``` \ No newline at end of file +Artifacts are signed with the default key which you have configured (or the first secret key it finds). \ No newline at end of file diff --git a/src/ucll.tests/PlatformSupportTests.cs b/src/ucll.tests/PlatformSupportTests.cs index c6bdf39..6c6bb78 100644 --- a/src/ucll.tests/PlatformSupportTests.cs +++ b/src/ucll.tests/PlatformSupportTests.cs @@ -2,7 +2,7 @@ public class PlatformSupportTests { [Theory] [InlineData(typeof(MacSupport), - "/Applications/Unity/Hub/Editor/2022.3.10f1/Unity.app", + "/Applications/Unity/Hub/Editor/2022.3.10f1/Unity.app/Contents/MacOS/Unity", "/Applications/Unity/Hub/Editor/2022.3.10f1")] [InlineData(typeof(WindowsSupport), @"C:\Program Files\Unity\Hub\Editor\6000.0.59f2\Editor\Unity.exe", diff --git a/src/ucll/Shared/Platform/LinuxSupport.cs b/src/ucll/Shared/Platform/LinuxSupport.cs index bb57d38..f182a6b 100644 --- a/src/ucll/Shared/Platform/LinuxSupport.cs +++ b/src/ucll/Shared/Platform/LinuxSupport.cs @@ -1,5 +1,10 @@ internal sealed class LinuxSupport : PlatformSupport { + public override string FindInstallationRoot(string editorPath) + { + throw new NotImplementedException(); + } + public override string FormatHubArgs(string args) => args; // Linux doesn't need the "--" prefix @@ -11,8 +16,6 @@ public override ProcessStartInfo OpenFileWithApp(string filePath, string applica return new ProcessStartInfo(applicationPath, filePath); } - public override string RelativeEditorPathToExecutable => "Editor/Unity"; - public override string UnityHubConfigDirectory => Path.Combine(UserHome, ".config/UnityHub"); public override ProcessStartInfo GetUnityProjectSearchProcess() diff --git a/src/ucll/Shared/Platform/MacSupport.cs b/src/ucll/Shared/Platform/MacSupport.cs index 83dcdb2..05064a9 100644 --- a/src/ucll/Shared/Platform/MacSupport.cs +++ b/src/ucll/Shared/Platform/MacSupport.cs @@ -8,7 +8,15 @@ public override ProcessStartInfo OpenFileWithApp(string filePath, string applica return new ProcessStartInfo("open", $"-a \"{applicationPath}\" \"{filePath}\""); } - public override string RelativeEditorPathToExecutable => "Contents/MacOS/Unity"; + public override string FindInstallationRoot(string editorPath) + { + return editorPath.Replace("/Unity.app/Contents/MacOS/Unity", string.Empty); + } + + public override string GetUnityExecutablePath(string path) + { + return Path.Combine(path, "Contents/MacOS/Unity"); + } public override string UnityHubConfigDirectory => Path.Combine(UserHome, "Library/Application Support/UnityHub"); diff --git a/src/ucll/Shared/Platform/PlatformSupport.cs b/src/ucll/Shared/Platform/PlatformSupport.cs index 6a35d09..bcd9d2f 100644 --- a/src/ucll/Shared/Platform/PlatformSupport.cs +++ b/src/ucll/Shared/Platform/PlatformSupport.cs @@ -34,8 +34,7 @@ public static PlatformSupport Create() /// /// Given the path to an editor executable, returns the root directory path of the installation. /// - public virtual string FindInstallationRoot(string editorPath) - => Path.GetDirectoryName(editorPath)!; + public abstract string FindInstallationRoot(string editorPath); /// /// Path to the Unity Hub executable or null if it doesn't exist (or couldn't be found). @@ -69,9 +68,9 @@ public virtual string FindInstallationRoot(string editorPath) public abstract ProcessStartInfo OpenFileWithApp(string filePath, string applicationPath); /// - /// The path from the installation bundle (macOS) or root (Windows) to the executable. + /// Converts an installation bundle path (macOS) to a path to the binary, if needed. /// - public abstract string RelativeEditorPathToExecutable { get; } + public virtual string GetUnityExecutablePath(string path) => path; /// /// The path to the directory that contains Unity Hub config files. diff --git a/src/ucll/Shared/Platform/WindowsSupport.cs b/src/ucll/Shared/Platform/WindowsSupport.cs index 2a17c81..cd55990 100644 --- a/src/ucll/Shared/Platform/WindowsSupport.cs +++ b/src/ucll/Shared/Platform/WindowsSupport.cs @@ -12,8 +12,6 @@ public override ProcessStartInfo OpenFileWithApp(string filePath, string applica return new ProcessStartInfo(applicationPath, $"\"{filePath}\""); } - public override string RelativeEditorPathToExecutable => @"Editor\Unity.exe"; - public override string FindInstallationRoot(string editorPath) { return editorPath.Replace(@"Editor\Unity.exe", string.Empty).TrimEnd('\\'); diff --git a/src/ucll/Shared/UnityHub.cs b/src/ucll/Shared/UnityHub.cs index 5441a19..deede6f 100644 --- a/src/ucll/Shared/UnityHub.cs +++ b/src/ucll/Shared/UnityHub.cs @@ -23,18 +23,19 @@ internal class UnityHub(PlatformSupport platformSupport) public string GetEditorPath(string version) { // Fast: try the default install location first. - string? executablePath = platformSupport.FindDefaultEditorPath(version); - if (executablePath != null) - return executablePath; + string? editorPathDefault = platformSupport.FindDefaultEditorPath(version); + if (editorPathDefault != null) + return editorPathDefault; // Fallback: query Unity Hub for custom installation locations. var editors = ListInstalledEditors(); - string? appBundlePath = editors.FirstOrDefault(p => p.Version == version).Path; + string? editorPathHub = editors.FirstOrDefault(p => p.Version == version).Path; - if (appBundlePath == null) + if (editorPathHub == null) throw new UserException($"Unity version {version} is not installed."); - return Path.Combine(appBundlePath, platformSupport.RelativeEditorPathToExecutable); + // On macOS, the Unity Hub returns a path to the app bundle (Unity.app), but we need the binary within. + return platformSupport.GetUnityExecutablePath(editorPathHub); } public IEnumerable GetRecentProjects(bool favoriteOnly = false) diff --git a/src/ucll/Shared/VersionUsage.cs b/src/ucll/Shared/VersionUsage.cs index 837933e..94e08e7 100644 --- a/src/ucll/Shared/VersionUsage.cs +++ b/src/ucll/Shared/VersionUsage.cs @@ -36,25 +36,32 @@ public static IEnumerable FindUnityProjects(PlatformSupport platformSupp public string[] GetInstalledModules(string version) { - string editorPath = unityHub.GetEditorPath(version); - string installationRoot = platformSupport.FindInstallationRoot(editorPath); - string modulesJsonPath = Path.Combine(installationRoot, "modules.json"); + try + { + string editorPath = unityHub.GetEditorPath(version); + string installationRoot = platformSupport.FindInstallationRoot(editorPath); + string modulesJsonPath = Path.Combine(installationRoot, "modules.json"); - if (!File.Exists(modulesJsonPath)) - return []; + if (!File.Exists(modulesJsonPath)) + return []; - string json = File.ReadAllText(modulesJsonPath); + string json = File.ReadAllText(modulesJsonPath); - using JsonDocument doc = JsonDocument.Parse(json); + using JsonDocument doc = JsonDocument.Parse(json); - return doc.RootElement - .EnumerateArray() - .Where(module => module.TryGetProperty("selected", out var selected) && selected.GetBoolean()) - .Where(module => module.TryGetProperty("visible", out var visible) && visible.GetBoolean()) - .Where(module => module.TryGetProperty("name", out _)) - .Select(module => module.GetProperty("name").GetString()) - .Where(name => name != null) - .Cast() - .ToArray(); + return doc.RootElement + .EnumerateArray() + .Where(module => module.TryGetProperty("selected", out var selected) && selected.GetBoolean()) + .Where(module => module.TryGetProperty("visible", out var visible) && visible.GetBoolean()) + .Where(module => module.TryGetProperty("name", out _)) + .Select(module => module.GetProperty("name").GetString()) + .Where(name => name != null) + .Cast() + .ToArray(); + } + catch + { + return []; + } } } \ No newline at end of file diff --git a/src/ucll/ucll.csproj b/src/ucll/ucll.csproj index 7e80641..8584f97 100644 --- a/src/ucll/ucll.csproj +++ b/src/ucll/ucll.csproj @@ -40,8 +40,4 @@ - - - -