diff --git a/.gitignore b/.gitignore
index c68b193..953f377 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ bin
obj
.vscode
Releases
+/.vs
diff --git a/Build/build-osx.sh b/Build/build-osx.sh
index 8838bfd..1c84995 100755
--- a/Build/build-osx.sh
+++ b/Build/build-osx.sh
@@ -1,25 +1,18 @@
#!/bin/zsh
PROJECT="Command/Command.csproj"
-TARGET="net6.0"
+TARGET="net7.0"
ARCHES=("osx-x64" "osx-arm64")
SIGN_IDENTITY="Developer ID Application: Feist GmbH (DHNHQKSSYT)"
-ASC_PROVIDER="DHNHQKSSYT"
ENTITLEMENTS="Build/notarization.entitlements"
-BUNDLE_ID="ch.sttz.install-unity"
# Mapping of arche names used by .Net to the ones used by lipo
typeset -A LIPO_ARCHES=()
LIPO_ARCHES[osx-x64]=x86_64
LIPO_ARCHES[osx-arm64]=arm64
-if [[ -z "$ASC_USER" ]]; then
- echo "ASC user not set in ASC_USER"
- exit 1
-fi
-
-if [[ -z "$ASC_KEYCHAIN" ]]; then
- echo "ASC keychain item not set in ASC_KEYCHAIN"
+if [[ -z "$NOTARY_PROFILE" ]]; then
+ echo "notarytool keychain profile not set in NOTARY_PROFILE"
exit 1
fi
@@ -42,9 +35,7 @@ for arch in $ARCHES; do
-r "$arch" \
-c release \
-f "$TARGET" \
- -p:PublishSingleFile=true \
- -p:PublishReadyToRun=true \
- -p:PublishTrimmed=true \
+ --self-contained \
"$PROJECT" \
|| exit 1
@@ -79,7 +70,7 @@ pushd "$ARCHIVE"
zip "../install-unity-$VERSION.zip" "install-unity" || exit 1
popd
-xcrun altool --notarize-app --primary-bundle-id "$BUNDLE_ID" --asc-provider "$ASC_PROVIDER" --username "$ASC_USER" --password "@keychain:$ASC_KEYCHAIN" --file "$ZIPARCHIVE" || exit 1
+xcrun notarytool submit --wait --keychain-profile "$NOTARY_PROFILE" --wait --progress "$ZIPARCHIVE" || exit 1
# Shasum for Homebrew
diff --git a/Changelog.md b/Changelog.md
index ae90932..fdcd643 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,5 +1,29 @@
# Changelog
+### 2.12.0 (2023-05-??)
+* Use Unity's official Release API to get release and package information
+ * Releases should appear quicker when Unity is slow to update their archive webpage
+ * Can directly request information of a specific Unity version from the API
+ * No need to load the whole archive, update can be stopped once the last known version is reached
+ * Reduces number of requests and amount of data transferred when updating cache
+ * Previously synthesized packages are now provided by Unity (Documentation, language packs and Android components)
+ * Legacy scraper and ini-based system can still be used for irregular Unity releases
+* Split platform and architecture options (e.g. `--platform macOSIntel` becomes `--platform mac_os --arch x68_64`)
+* Added `--clear-cache` to force clearing the versions and package cache
+* Added `--redownload` to force redownloading all files
+* Improve handling of already downloaded or partially downloaded files
+* Speed up detecting of current platform (.Net now reports Apple Silicon properly)
+* Speed up detecting installed Unity versions by keeping command line invocations to a minimum
+* Removed support for Unity patch releases
+* Update to .Net 7
+
+### 2.11.1 (2023-02-05)
+* Add warning when Spotlight is disabled and installations cannot be found
+* Update Android packages for Unity 2023.1
+* Fix discovery of beta and alpha releases
+* Fix Apple Silicon packages not saved in cache
+* Fix exception when cleaning up after installing additional packages to an installation at `/Applications/Unity`
+
### 2.11.0 (2022-09-03)
* Add "--upgrade <version>" to `run` command to upgrade a project to a specific Unity version
* Fix --allow-newer attempting to downgrade project if project Unity version is newer than installed versions
diff --git a/Command/Command.csproj b/Command/Command.csproj
index 1f090f8..6db3a55 100644
--- a/Command/Command.csproj
+++ b/Command/Command.csproj
@@ -2,12 +2,16 @@
Exe
- net6.0
+ net7.0
+ win-x64;osx-x64
latest
+ true
+ true
+ true
- 2.11.0
+ 2.12.0
Adrian Stutz (sttz.ch)
install-unity CLI
CLI for install-unity unofficial Unity installer library
@@ -16,10 +20,11 @@
https://github.com/sttz/install-unity
git
CLI;Unity;Installer
+ app.manifest
-
+
diff --git a/Command/Program.cs b/Command/Program.cs
index af6b162..18452e9 100644
--- a/Command/Program.cs
+++ b/Command/Program.cs
@@ -2,13 +2,13 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Reflection;
-using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using sttz.NiceConsoleLogger;
+using static sttz.InstallUnity.UnityReleaseAPIClient;
+
namespace sttz.InstallUnity
{
@@ -45,9 +45,17 @@ public class InstallUnityCLI
///
public bool update;
///
- /// Path to store all data at.
+ /// Clear the versions cache.
///
- public CachePlatform platform;
+ public bool clearCache;
+ ///
+ /// Platform of the editor to download.
+ ///
+ public Platform platform;
+ ///
+ /// Architecture of the editor to download and/or install.
+ ///
+ public Architecture architecture;
///
/// Path to store all data at.
///
@@ -88,6 +96,10 @@ public class InstallUnityCLI
///
public bool upgrade;
///
+ /// Force redownloading all files.
+ ///
+ public bool redownload;
+ ///
/// Skip size and hash checks for downloads.
///
public bool yolo;
@@ -158,11 +170,13 @@ public override string ToString()
var cmd = action ?? "";
if (help) cmd += " --help";
if (verbose > 0) cmd += string.Concat(Enumerable.Repeat(" --verbose", verbose));
+ if (clearCache) cmd += " --clear-cache";
if (update) cmd += " --update";
if (dataPath != null) cmd += " --data-path " + dataPath;
if (options.Count > 0) cmd += " --opt " + string.Join(" ", options);
- if (platform != CachePlatform.None) cmd += " --platform " + platform;
-
+ if (platform != Platform.None) cmd += " --platform " + platform;
+ if (architecture != Architecture.None) cmd += " --arch " + platform;
+
if (matchVersion != null) cmd += " " + matchVersion;
if (installed) cmd += " --installed";
@@ -205,6 +219,8 @@ public static Arguments ArgumentsDefinition {
.Description("Don't prompt for confirmation (use with care)")
.Option((InstallUnityCLI t, bool v) => t.update = v, "u", "update")
.Description("Force an update of the versions cache")
+ .Option((InstallUnityCLI t, bool v) => t.clearCache = v, "clear-cache")
+ .Description("Clear the versions cache before running any commands")
.Option((InstallUnityCLI t, string v) => t.dataPath = v, "data-path", "datapath")
.ArgumentName("")
.Description("Store all data at the given path, also don't delete packages after install")
@@ -221,18 +237,22 @@ public static Arguments ArgumentsDefinition {
.Description("Pattern to match Unity version")
.Option((InstallUnityCLI t, bool v) => t.installed = v, "i", "installed")
.Description("List installed versions of Unity")
- .Option((InstallUnityCLI t, CachePlatform v) => t.platform = v, "platform")
+ .Option((InstallUnityCLI t, Platform v) => t.platform = v, "platform")
.Description("Platform to list the versions for (default = current platform)")
-
+ .Option((InstallUnityCLI t, Architecture v) => t.architecture = v, "arch")
+ .Description("Architecture to list the versions for (default = current architecture)")
+
.Action("details", (t, a) => t.action = a)
.Description("Show version information and all its available packages")
-
+
.Option((InstallUnityCLI t, string v) => t.matchVersion = v, 0)
.ArgumentName("")
.Description("Pattern to match Unity version or release notes / unity hub url")
- .Option((InstallUnityCLI t, CachePlatform v) => t.platform = v, "platform")
+ .Option((InstallUnityCLI t, Platform v) => t.platform = v, "platform")
.Description("Platform to show the details for (default = current platform)")
-
+ .Option((InstallUnityCLI t, Architecture v) => t.architecture = v, "arch")
+ .Description("Architecture to show the details for (default = current architecture)")
+
.Action("install", (t, a) => t.action = a)
.DefaultAction()
.Description("Download and install a version of Unity")
@@ -249,8 +269,12 @@ public static Arguments ArgumentsDefinition {
.Description("Install previously downloaded packages (requires '--data-path')")
.Option((InstallUnityCLI t, bool v) => t.upgrade = v, "upgrade")
.Description("Replace existing matching Unity installation after successful install")
- .Option((InstallUnityCLI t, CachePlatform v) => t.platform = v, "platform")
+ .Option((InstallUnityCLI t, Platform v) => t.platform = v, "platform")
.Description("Platform to download the packages for (only valid with '--download', default = current platform)")
+ .Option((InstallUnityCLI t, Architecture v) => t.architecture = v, "arch")
+ .Description("Architecture to download the packages for (default = current architecture)")
+ .Option((InstallUnityCLI t, bool v) => t.redownload = v, "redownload")
+ .Description("Force redownloading all files")
.Option((InstallUnityCLI t, bool v) => t.yolo = v, "yolo")
.Description("Skip size and hash checks of downloaded files")
@@ -365,8 +389,8 @@ public void PrintHelp()
///
public string GetVersion()
{
- var assembly = Assembly.GetExecutingAssembly();
- return assembly.GetCustomAttribute().InformationalVersion;
+ var assembly = System.Reflection.Assembly.GetExecutingAssembly();
+ return System.Reflection.CustomAttributeExtensions.GetCustomAttribute(assembly).InformationalVersion;
}
///
@@ -411,13 +435,12 @@ public async Task Setup(bool avoidCacheUpate = false)
if (!enableColors) Logger.LogInformation("Console colors disabled");
// Set current platform
- if (platform == CachePlatform.None) {
- platform = await installer.Platform.GetCurrentPlatform();
+ if (platform == Platform.None || architecture == Architecture.None) {
+ var (defaultPlatform, defaultarch) = await installer.Platform.GetCurrentPlatform();
+ if (platform == Platform.None) platform = defaultPlatform;
+ if (architecture == Architecture.None) architecture = defaultarch;
}
- Logger.LogDebug($"Selected platform {platform}");
-
- // Enable generating virtual packages
- VirtualPackages.Enable();
+ Logger.LogDebug($"Selected platform {platform}-{architecture}");
// Parse version argument (positional argument)
var version = new UnityVersion(matchVersion);
@@ -432,8 +455,8 @@ public async Task Setup(bool avoidCacheUpate = false)
Environment.Exit(0);
} else if (options.Contains("save")) {
var configPath = installer.DataPath ?? installer.Platform.GetConfigurationDirectory();
- configPath = Path.Combine(configPath, UnityInstaller.CONFIG_FILENAME);
- if (File.Exists(configPath)) {
+ configPath = System.IO.Path.Combine(configPath, UnityInstaller.CONFIG_FILENAME);
+ if (System.IO.File.Exists(configPath)) {
Console.WriteLine($"Configuration file already exists:\n{configPath}");
} else {
installer.Configuration.Save(configPath);
@@ -452,6 +475,12 @@ public async Task Setup(bool avoidCacheUpate = false)
}
}
+ // Clear the versions cache (--clear-cache)
+ if (clearCache) {
+ installer.Versions.Clear();
+ installer.Versions.Save();
+ }
+
// Update cache if needed or requested (--update)
var updateType = version.type;
if (updateType == UnityVersion.Type.Undefined) {
@@ -465,7 +494,7 @@ public async Task Setup(bool avoidCacheUpate = false)
IEnumerable newVersionsData;
if (update || (!avoidCacheUpate && installer.IsCacheOutdated(updateType))) {
WriteTitle("Updating Cache...");
- newVersionsData = await installer.UpdateCache(platform, updateType);
+ newVersionsData = await installer.UpdateCache(platform, architecture, updateType);
var total = newVersionsData.Count();
var maxVersions = 10;
@@ -473,8 +502,9 @@ public async Task Setup(bool avoidCacheUpate = false)
Console.WriteLine("No new Unity versions");
} else if (total > 0) {
Console.WriteLine($"New Unity version{(total > 1 ? "s" : "")}:");
- foreach (var newVersion in newVersionsData.OrderByDescending(m => m.version).Take(maxVersions)) {
- Console.WriteLine($"- {newVersion.version} ({installer.Scraper.GetReleaseNotesUrl(newVersion)})");
+ foreach (var newVersion in newVersionsData.OrderByDescending(m => m.Version).Take(maxVersions)) {
+ var url = Scraper.GetReleaseNotesUrl(newVersion.release.stream, newVersion.Version);
+ Console.WriteLine($"- {newVersion.Version} ({url})");
}
if (total - maxVersions > 0) {
Console.WriteLine($"And {total - maxVersions} more...");
@@ -483,7 +513,7 @@ public async Task Setup(bool avoidCacheUpate = false)
if (total <= maxVersions) {
newVersions = new HashSet();
- foreach (var newVersion in newVersionsData.Select(d => d.version)) {
+ foreach (var newVersion in newVersionsData.Select(d => d.Version)) {
newVersions.Add(newVersion);
}
}
@@ -517,52 +547,74 @@ public async Task Setup(bool avoidCacheUpate = false)
Logger.LogInformation($"Got url instead of version, trying to find version at url...");
metadata = await installer.Scraper.LoadUrl(matchVersion);
- if (!metadata.version.IsValid) {
+ if (!metadata.Version.IsValid) {
throw new Exception("Could not find version at url: " + versionString);
}
}
- version = metadata.version;
+ version = metadata.Version;
} else {
// Locate version in cache or look it up
metadata = installer.Versions.Find(version);
- if (!metadata.version.IsValid) {
+ if (!metadata.Version.IsValid) {
if (installOnly) {
throw new Exception("Could not find version matching input: " + version);
}
try {
Logger.LogInformation($"Version {version} not found in cache, trying exact lookup");
- metadata = await installer.Scraper.LoadExact(version);
+ metadata.release = await installer.Releases.FindRelease(version, platform, architecture);
} catch (Exception e) {
Logger.LogInformation("Failed exact lookup: " + e.Message);
}
- if (!metadata.version.IsValid) {
+ if (!metadata.Version.IsValid) {
throw new Exception("Could not find version matching input: " + version);
}
installer.Versions.Add(metadata);
installer.Versions.Save();
- Console.WriteLine($"Guessed release notes URL to discover {metadata.version}");
+ Console.WriteLine($"Guessed release notes URL to discover {metadata.Version}");
}
}
- if (!metadata.version.MatchesVersionOrHash(version)) {
+ if (!metadata.Version.MatchesVersionOrHash(version)) {
Console.WriteLine();
- ConsoleLogger.WriteLine($"Selected {metadata.version} for input {version}");
+ ConsoleLogger.WriteLine($"Selected {metadata.Version} for input {version}");
}
// Load packages ini if needed
- if (!metadata.HasPackagesMetadata(platform)) {
+ var editor = metadata.GetEditorDownload(platform, architecture);
+ if (editor == null) {
if (installOnly) {
throw new Exception("Packages not found in versions cache (install only): " + version);
}
- Logger.LogInformation("Packages not yet loaded, loading ini now");
- metadata = await installer.Scraper.LoadPackages(metadata, platform);
- installer.Versions.Add(metadata);
- installer.Versions.Save();
+
+ // Try to load version from API
+ // The API doesn't allow lookup by hash, so we have to check if after loading
+ Logger.LogInformation($"Missing packages for {platform}-{architecture} in cache, loading from Release API");
+ var release = await installer.Releases.FindRelease(metadata.Version, platform, architecture);
+ if (release != null && (metadata.Version.hash == null || release.version.hash == metadata.Version.hash)) {
+ editor = release.downloads.Where(d => d.platform == platform && d.architecture == architecture).FirstOrDefault();
+ if (editor != null) {
+ metadata.SetEditorDownload(editor);
+ installer.Versions.Add(metadata);
+ installer.Versions.Save();
+ }
+ }
+
+ // Fall back to look up version using legacy INI system (for e.g. test releases of the editor)
+ if (editor == null && !string.IsNullOrEmpty(metadata.baseUrl)) {
+ Logger.LogInformation("Loading packages from legacy INI system");
+ metadata = await installer.Scraper.LoadPackages(metadata, platform, architecture);
+ installer.Versions.Add(metadata);
+ installer.Versions.Save();
+ }
+
+ if (editor == null) {
+ throw new Exception($"Could not load packages for {metadata.Version} on {platform}-{architecture}");
+ }
}
return (version, metadata);
@@ -619,15 +671,15 @@ public async Task ListUpdates()
if (!installs.Any()) {
var latest = installer.Versions
- .Where(m => m.IsFinalRelease)
- .OrderByDescending(m => m.version)
+ .Where(m => (m.release.stream & ReleaseStream.PrereleaseMask) == 0)
+ .OrderByDescending(m => m.Version)
.FirstOrDefault();
-
+
Console.WriteLine("No installed Unity versions found.");
- if (latest.version.IsValid) {
+ if (latest.Version.IsValid) {
Console.WriteLine();
- Console.WriteLine($"The latest Unity version available is {latest.version.ToString(verbose > 0)}.");
+ Console.WriteLine($"The latest Unity version available is {latest.Version.ToString(verbose > 0)}.");
}
} else {
@@ -635,9 +687,9 @@ public async Task ListUpdates()
var updates = new List<(Installation install, VersionMetadata update)>();
foreach (var install in installs.OrderByDescending(i => i.version)) {
var newerPatch = installer.Versions
- .Where(m => IsNewerPatch(m.version, install.version))
+ .Where(m => IsNewerPatch(m.Version, install.version))
.FirstOrDefault();
- if (newerPatch.version.IsValid) {
+ if (newerPatch.Version.IsValid) {
updates.Add((install, newerPatch));
}
}
@@ -647,29 +699,29 @@ public async Task ListUpdates()
} else {
WriteTitle("Minor updates to installed Unity versions:");
foreach (var update in updates) {
- Console.WriteLine($"- {update.install.version.ToString(verbose > 0)} ➤ {update.update.version.ToString(verbose > 0)}");
+ Console.WriteLine($"- {update.install.version.ToString(verbose > 0)} ➤ {update.update.Version.ToString(verbose > 0)}");
}
}
// Compile latest version for each major.minor release
var mms = installer.Versions
- .OrderByDescending(m => m.version)
- .GroupBy(m => (major: m.version.major, minor: m.version.minor))
+ .OrderByDescending(m => m.Version)
+ .GroupBy(m => (major: m.Version.major, minor: m.Version.minor))
.Select(g => g.First())
.Reverse();
// Find major/minor new Unity versions not yet installed
var latest = installs.OrderByDescending(i => i.version).First();
var newer = mms
- .Where(m => m.IsFinalRelease && m.version > latest.version);
+ .Where(m => (m.release.stream & ReleaseStream.PrereleaseMask) == 0 && m.Version > latest.version);
if (newer.Any()) {
WriteTitle("New major Unity versions:");
foreach (var m in newer) {
if (verbose > 0) {
- Console.WriteLine($"- {m.version}");
+ Console.WriteLine($"- {m.Version}");
} else {
- Console.WriteLine($"- {m.version.major}.{m.version.minor}");
+ Console.WriteLine($"- {m.Version.major}.{m.Version.minor}");
}
}
}
@@ -677,19 +729,19 @@ public async Task ListUpdates()
// Find alpha/beta/RC versions not yet installed
var abs = mms
.Where(m =>
- m.IsPrerelease
- && m.version > latest.version
- && (m.version.major != latest.version.major
- || m.version.minor != latest.version.minor)
+ (m.release.stream & ReleaseStream.PrereleaseMask) != 0
+ && m.Version > latest.version
+ && (m.Version.major != latest.version.major
+ || m.Version.minor != latest.version.minor)
);
if (abs.Any()) {
WriteTitle("New Unity release candidates, betas and alphas:");
foreach (var m in abs) {
if (verbose > 0) {
- Console.WriteLine($"- {m.version}");
+ Console.WriteLine($"- {m.Version}");
} else {
- Console.WriteLine($"- {m.version.major}.{m.version.minor}{(char)m.version.type}");
+ Console.WriteLine($"- {m.Version.major}.{m.Version.minor}{(char)m.Version.type}");
}
}
}
@@ -739,8 +791,8 @@ public void VersionsTable(UnityInstaller installer, UnityVersion version, IEnume
var currentList = new List();
int lastMajor = -1, lastMinor = -1;
foreach (var metadata in installer.Versions) {
- if (!metadata.IsFuzzyMatchedBy(version)) continue;
- var other = metadata.version;
+ if (!version.FuzzyMatches(metadata.Version)) continue;
+ var other = metadata.Version;
if (lastMinor < 0) lastMinor = other.minor;
else if (lastMinor != other.minor) {
@@ -769,10 +821,9 @@ public void VersionsTable(UnityInstaller installer, UnityVersion version, IEnume
// Write the generated columns line by line, wrapping to buffer size
var colWidth = (verbose > 0 ? ListVersionsWithHashColumnWith : ListVersionsColumnWidth);
var maxColumns = Math.Max(Console.BufferWidth / colWidth, 1);
- var hasReleaseCandidate = false;
foreach (var majorRow in majorRows) {
// Major version separator / title
- var major = majorRow[0][0].version.major;
+ var major = majorRow[0][0].Version.major;
WriteBigTitle(major.ToString());
var groupCount = (majorRow.Count - 1) / maxColumns + 1;
@@ -786,7 +837,7 @@ public void VersionsTable(UnityInstaller installer, UnityVersion version, IEnume
Console.SetCursorPosition((c - columnOffset) * colWidth, Console.CursorTop);
SetColors(ConsoleColor.White, ConsoleColor.DarkGray);
- var minorVersion = majorRow[c][0].version;
+ var minorVersion = majorRow[c][0].Version;
var title = minorVersion.major + "." + minorVersion.minor;
Console.Write(title);
@@ -797,14 +848,10 @@ public void VersionsTable(UnityInstaller installer, UnityVersion version, IEnume
Console.SetCursorPosition((c - columnOffset) * colWidth, Console.CursorTop);
var m = majorRow[c][r];
- Console.Write(m.version.ToString(verbose > 0));
- if (m.IsReleaseCandidate) {
- hasReleaseCandidate = true;
- Console.Write("*");
- }
+ Console.Write(m.Version.ToString(verbose > 0));
- var isNewVersion = (newVersions != null && newVersions.Contains(m.version));
- var isInstalled = (installed != null && installed.Contains(m.version));
+ var isNewVersion = (newVersions != null && newVersions.Contains(m.Version));
+ var isInstalled = (installed != null && installed.Contains(m.Version));
if (isNewVersion || isInstalled) {
SetColors(ConsoleColor.White, ConsoleColor.DarkGray);
@@ -819,10 +866,6 @@ public void VersionsTable(UnityInstaller installer, UnityVersion version, IEnume
Console.WriteLine();
}
}
-
- if (hasReleaseCandidate) {
- Console.WriteLine("* indicates a release candidate, it will only be selected by an exact match or as a beta version.");
- }
}
// -------- Version Details --------
@@ -834,82 +877,129 @@ public async Task Details()
ShowDetails(installer, metadata);
}
- void ShortPackagesList(VersionMetadata metadata)
+ void ShortModulesList(VersionMetadata metadata)
{
- var packageMetadata = metadata.GetPackages(platform);
- var list = string.Join(", ", packageMetadata
+ var editor = metadata.GetEditorDownload(platform, architecture);
+
+ var list = editor.AllModules.Values
.Where(p => !p.hidden)
- .Select(p => p.name + (p.install ? "*" : ""))
- .ToArray()
- );
- Console.WriteLine(packageMetadata.Count() + " Packages: " + list);
+ .Select(p => p.id + (p.preSelected ? "*" : ""))
+ .OrderBy(p => p)
+ .Prepend("unity");
+ if (list.Any()) {
+ Console.WriteLine(list.Count() + " Packages: " + string.Join(", ", list));
+ Console.WriteLine();
+ }
+
+ list = editor.AllModules.Values
+ .Where(p => p.hidden)
+ .Select(p => p.id + (p.preSelected ? "*" : ""))
+ .OrderBy(p => p);
+ if (list.Any()) {
+ Console.WriteLine(list.Count() + " Hidden Packages: " + string.Join(", ", list));
+ Console.WriteLine();
+ }
+
Console.WriteLine("* = default package");
Console.WriteLine();
}
- void DetailedPackagesList(IEnumerable packages)
+ void DetailedModulesList(IEnumerable modules)
{
fieldWidth = 14;
- foreach (var package in packages) {
+ foreach (var module in modules) {
SetColors(ConsoleColor.DarkGray, ConsoleColor.DarkGray);
Console.Write("--------------- ");
SetForeground(ConsoleColor.White);
- Console.Write(package.name + (package.install ? "* " : " "));
+ Console.Write($"{module.name} [{module.id}] " + (module.preSelected ? " *" : ""));
ResetColor();
Console.WriteLine();
- WriteField("Title", package.title);
- WriteField("Description", package.description);
- WriteField("URL", package.url);
- WriteField("Mandatory", (package.mandatory ? "yes" : null));
- WriteField("Hidden", (package.hidden ? "yes" : null));
- WriteField("Size", $"{Helpers.FormatSize(package.size)} ({Helpers.FormatSize(package.installedsize)} installed)");
- WriteField("EULA", package.eulamessage);
- if (package.eulalabel1 != null && package.eulaurl1 != null)
- WriteField("", package.eulalabel1 + ": " + package.eulaurl1);
- if (package.eulalabel2 != null && package.eulaurl2 != null)
- WriteField("", package.eulalabel2 + ": " + package.eulaurl2);
- WriteField("Install with", package.sync);
- WriteField("MD5", package.md5);
+ WriteField("Description", module.description);
+ WriteField("URL", module.url);
+ WriteField("Required", (module.required ? "yes" : null));
+ WriteField("Hidden", (module.hidden ? "yes" : null));
+ WriteField("Size", $"{Helpers.FormatSize(module.downloadSize.GetBytes())} ({Helpers.FormatSize(module.installedSize.GetBytes())} installed)");
+
+ if (module.eula?.Length > 0) {
+ WriteField("EULA", module.eula[0].message);
+ foreach (var eula in module.eula) {
+ WriteField("", eula.label + ": " + eula.url);
+ }
+ }
+
+ if (module.subModules?.Count > 0)
+ WriteField("Sub-Modules", string.Join(", ", module.subModules.Select(m => m.name)));
+ if (module.parentModuleId != null)
+ WriteField("Parent Module", module.parentModuleId);
+
+ WriteField("Hash", module.integrity);
Console.WriteLine();
}
}
- void ShowDetails(UnityInstaller installer, VersionMetadata metadata)
+ void EditorPackageDetails(EditorDownload module)
{
- var packageMetadata = metadata.GetPackages(platform);
+ fieldWidth = 14;
- WriteBigTitle($"Details for Unity {metadata.version}");
+ SetColors(ConsoleColor.DarkGray, ConsoleColor.DarkGray);
+ Console.Write("--------------- ");
+ SetForeground(ConsoleColor.White);
+ Console.Write($"Unity Editor [{module.Id}]" + " *");
+ ResetColor();
+ Console.WriteLine();
- if (metadata.IsReleaseCandidate) {
- ConsoleLogger.WriteLine(
- "This is a release candidate: "
- + "Even though it has an f version, it will only be selected "
- + "by an exact match or as a beta version.");
- Console.WriteLine();
- }
+ WriteField("Description", "Main Unity Editor package");
+ WriteField("URL", module.url);
+ WriteField("Size", $"{Helpers.FormatSize(module.downloadSize.GetBytes())} ({Helpers.FormatSize(module.installedSize.GetBytes())} installed)");
+
+ WriteField("Hash", module.integrity);
+
+ Console.WriteLine();
+ }
+
+ void ShowDetails(UnityInstaller installer, VersionMetadata metadata)
+ {
+ var editor = metadata.GetEditorDownload(platform, architecture);
+
+ WriteBigTitle($"Details for Unity {metadata.Version} {editor.platform}-{editor.architecture}");
if (metadata.baseUrl != null) {
Console.WriteLine("Base URL: " + metadata.baseUrl);
}
- var releaseNotes = installer.Scraper.GetReleaseNotesUrl(metadata);
+ var releaseNotes = Scraper.GetReleaseNotesUrl(metadata.release.stream, metadata.Version);
if (releaseNotes != null) {
Console.WriteLine("Release notes: " + releaseNotes);
}
Console.WriteLine();
- ShortPackagesList(metadata);
+ ShortModulesList(metadata);
- DetailedPackagesList(packageMetadata.Where(p => !p.hidden).OrderBy(p => p.name));
+ EditorPackageDetails(editor);
+ DetailedModulesList(IterateModulesRecursive(editor.modules, hidden: false));
- var hidden = packageMetadata.Where(p => p.hidden).OrderBy(p => p.name);
+ var hidden = IterateModulesRecursive(editor.modules, hidden: true);
if (hidden.Any()) {
WriteTitle("Hidden Packages");
Console.WriteLine();
- DetailedPackagesList(hidden);
+ DetailedModulesList(hidden);
+ }
+ }
+
+ IEnumerable IterateModulesRecursive(IEnumerable modules, bool hidden)
+ {
+ foreach (var module in modules) {
+ if (module.hidden == hidden)
+ yield return module;
+
+ if (module.subModules != null) {
+ foreach (var subModule in IterateModulesRecursive(module.subModules, hidden)) {
+ yield return subModule;
+ }
+ }
}
}
@@ -938,21 +1028,21 @@ public async Task Install()
}
if (op != UnityInstaller.InstallStep.Download) {
- var installable = await installer.Platform.GetInstallablePlatforms();
- if (!installable.Contains(platform)) {
- throw new Exception($"Cannot install {platform} on the current platform.");
+ var installable = await installer.Platform.GetInstallableArchitectures();
+ if (!installable.HasFlag(architecture)) {
+ throw new Exception($"Cannot install {architecture} on the current platform ({platform}).");
}
}
VersionMetadata metadata;
(version, metadata) = await SelectAndLoad(version, matchVersion, op == UnityInstaller.InstallStep.Install);
- var packageMetadata = metadata.GetPackages(platform);
+ var editor = metadata.GetEditorDownload(platform, architecture);
// Determine packages to install (-p / --packages or defaultPackages option)
IEnumerable selection = packages;
if (string.Equals(selection.FirstOrDefault(), "all", StringComparison.OrdinalIgnoreCase)) {
Logger.LogInformation("Found 'all', selecting all available packages");
- selection = packageMetadata.Select(p => p.name);
+ selection = editor.AllModules.Keys;
} else if (!selection.Any()) {
Console.WriteLine();
if (installer.Configuration.defaultPackages != null) {
@@ -960,34 +1050,34 @@ public async Task Install()
selection = installer.Configuration.defaultPackages;
} else {
Console.WriteLine("Selecting default packages (select packages with '--packages', see available packages with 'details')");
- selection = installer.GetDefaultPackages(metadata, platform);
+ selection = installer.GetDefaultPackages(metadata, platform, architecture);
}
}
var notFound = new List();
- var resolved = installer.ResolvePackages(metadata, platform, selection, notFound: notFound);
+ var resolved = installer.ResolvePackages(metadata, platform, architecture, selection, notFound: notFound);
// Check version to be installed against already installed
Installation uninstall = null;
if (upgrade || (op & UnityInstaller.InstallStep.Install) > 0) {
- var freshInstall = resolved.Any(p => p.name == PackageMetadata.EDITOR_PACKAGE_NAME);
+ var freshInstall = resolved.Any(p => p is EditorDownload);
var installs = await installer.Platform.FindInstallations();
- var existing = installs.FirstOrDefault(i => i.version == metadata.version);
+ var existing = installs.FirstOrDefault(i => i.version == metadata.Version);
if (!freshInstall && existing == null) {
- throw new Exception($"Installing additional packages but Unity {metadata.version} hasn't been installed yet (add the 'Unity' package to install it).");
+ throw new Exception($"Installing additional packages but Unity {metadata.Version} hasn't been installed yet (add the 'Unity' package to install it).");
} else if (freshInstall && existing != null) {
if (upgrade) {
- Console.WriteLine($"Unity {metadata.version} already installed at '{existing.path}', nothing to upgrade.");
+ Console.WriteLine($"Unity {metadata.Version} already installed at '{existing.path}', nothing to upgrade.");
Environment.Exit(0);
} else {
- throw new Exception($"Unity {metadata.version} already installed at '{existing.path}' (remove the 'Unity' package to install additional packages).");
+ throw new Exception($"Unity {metadata.Version} already installed at '{existing.path}' (remove the 'Unity' package to install additional packages).");
}
}
// Find version to upgrade
if (upgrade) {
uninstall = installs
- .Where(i => i.version <= metadata.version)
+ .Where(i => i.version <= metadata.Version)
.OrderByDescending(i => i.version)
.FirstOrDefault();
Console.WriteLine();
@@ -1003,21 +1093,21 @@ public async Task Install()
WriteTitle("Selected packages:");
long totalSpace = 0, totalDownload = 0;
- var packageList = resolved.OrderBy(p => p.name);
- foreach (var package in packageList.Where(p => !p.addedAutomatically)) {
- totalSpace += package.installedsize;
- totalDownload += package.size;
- Console.WriteLine($"- {package.name} ({Helpers.FormatSize(package.size)})");
+ var packageList = resolved.OrderBy(p => p.Id);
+ foreach (var package in packageList.Where(p => !(p is Module module) || !module.addedAutomatically)) {
+ totalSpace += package.installedSize.GetBytes();
+ totalDownload += package.downloadSize.GetBytes();
+ Console.WriteLine($"- {package.Id} ({Helpers.FormatSize(package.downloadSize.GetBytes())})");
}
- var deps = packageList.Where(p => p.addedAutomatically);
+ var deps = packageList.OfType().Where(m => m.addedAutomatically);
if (deps.Any()) {
WriteTitle("Additional dependencies:");
Console.WriteLine("Dependencies are added automatically, prefix a package with = to install only that package.");
foreach (var package in deps) {
- totalSpace += package.installedsize;
- totalDownload += package.size;
- Console.WriteLine($"- {package.name} ({Helpers.FormatSize(package.size)}) [from {package.sync}]");
+ totalSpace += package.installedSize.GetBytes();
+ totalDownload += package.downloadSize.GetBytes();
+ Console.WriteLine($"- {package.name} ({Helpers.FormatSize(package.downloadSize.GetBytes())}) [from {package.parentModule.Id}]");
}
}
@@ -1038,16 +1128,15 @@ public async Task Install()
// Make user accept additional EULAs
var hasEula = false;
- foreach (var package in resolved) {
- if (package.eulamessage == null) continue;
+ foreach (var module in resolved.OfType()) {
+ if (module.eula == null || module.eula.Length == 0) continue;
hasEula = true;
Console.WriteLine();
SetForeground(ConsoleColor.Yellow);
- Console.WriteLine($"Installing '{package.name}' requires accepting following EULA(s).");
- Console.WriteLine(package.eulamessage);
- Console.WriteLine($"- {package.eulalabel1}: {package.eulaurl1}");
- if (package.eulalabel2 != null) {
- Console.WriteLine($"- {package.eulalabel2}: {package.eulaurl2}");
+ Console.WriteLine($"Installing '{module.name}' requires accepting following EULA(s).");
+ foreach (var eula in module.eula) {
+ Console.WriteLine(eula.message);
+ Console.WriteLine($"- {eula.label}: {eula.url}");
}
ResetColor();
}
@@ -1077,10 +1166,14 @@ public async Task Install()
var downloadPath = installer.GetDownloadDirectory(metadata);
Logger.LogInformation($"Downloading packages to '{downloadPath}'");
+ var existingFile = Downloader.ExistingFile.Undefined;
+ if (redownload) existingFile = Downloader.ExistingFile.Redownload;
+ else if (yolo) existingFile = Downloader.ExistingFile.Skip;
+
Installation installed = null;
- var queue = installer.CreateQueue(metadata, platform, downloadPath, resolved);
- if (installer.Configuration.progressBar && !Console.IsOutputRedirected) {
- var processTask = installer.Process(op, queue, yolo);
+ var queue = installer.CreateQueue(metadata, platform, architecture, downloadPath, resolved);
+ if (installer.Configuration.progressBar && !Console.IsOutputRedirected && verbose < 2) {
+ var processTask = installer.Process(op, queue, existingFile);
try {
var refreshInterval = installer.Configuration.progressRefreshInterval;
@@ -1103,12 +1196,12 @@ public async Task Install()
}
} else {
Logger.LogInformation("Progress bar is disabled");
- installed = await installer.Process(op, queue, yolo);
+ installed = await installer.Process(op, queue, existingFile);
}
if (dataPath == null) {
Logger.LogInformation("Cleaning up downloaded packages ('--data-path' not set)");
- installer.CleanUpDownloads(metadata, downloadPath, resolved);
+ installer.CleanUpDownloads(queue);
}
if (uninstall != null) {
@@ -1128,7 +1221,7 @@ public async Task Install()
void WriteQueueStatus(UnityInstaller.Queue queue, long updateCount, int statusInterval)
{
- var longestName = Math.Max(queue.items.Max(i => i.package.name.Length), 12);
+ var longestName = Math.Max(queue.items.Max(i => i.package.Id.Length), 12);
var longestStatus = queue.items.Max(i => i.status?.Length ?? 37);
Console.Write(new string(' ', Console.BufferWidth));
@@ -1164,7 +1257,7 @@ void WriteQueueStatus(UnityInstaller.Queue queue, long updateCount, int statusIn
ResetColor();
Console.Write(" ");
- Console.Write(item.package.name);
+ Console.Write(item.package.Id);
Console.Write(new string(' ', Math.Max(barStartCol - Console.CursorLeft, 0)));
var progressWidth = Console.BufferWidth - longestName - 6; // 4 for status, 2 padding
@@ -1242,9 +1335,9 @@ public async Task Uninstall()
Installation uninstall = null;
var version = new UnityVersion(matchVersion);
if (!version.IsValid) {
- var fullPath = Path.GetFullPath(matchVersion);
+ var fullPath = System.IO.Path.GetFullPath(matchVersion);
foreach (var install in installs) {
- var fullInstallPath = Path.GetFullPath(install.path);
+ var fullInstallPath = System.IO.Path.GetFullPath(install.path);
if (fullPath == fullInstallPath) {
uninstall = install;
break;
@@ -1312,20 +1405,20 @@ public async Task Run()
version = default;
projectPath = matchVersion;
- if (!Directory.Exists(projectPath)) {
+ if (!System.IO.Directory.Exists(projectPath)) {
throw new Exception($"Project path '{projectPath}' does not exist.");
}
- var versionPath = Path.Combine(projectPath, "ProjectSettings", "ProjectVersion.txt");
- if (!File.Exists(versionPath)) {
+ var versionPath = System.IO.Path.Combine(projectPath, "ProjectSettings", "ProjectVersion.txt");
+ if (!System.IO.File.Exists(versionPath)) {
throw new Exception($"ProjectVersion.txt not found at expected path: {versionPath}");
}
// Use full path, Unity doesn't properly recognize short relative paths
// (as of Unity 2019.3)
- projectPath = Path.GetFullPath(projectPath);
+ projectPath = System.IO.Path.GetFullPath(projectPath);
- var lines = File.ReadAllLines(versionPath);
+ var lines = System.IO.File.ReadAllLines(versionPath);
foreach (var line in lines) {
if (line.StartsWith("m_EditorVersion:") || line.StartsWith("m_EditorVersionWithRevision:")) {
var colonIndex = line.IndexOf(':');
@@ -1405,7 +1498,7 @@ public async Task Run()
}
if (projectPath != null) {
- var projectName = Path.GetFileName(projectPath);
+ var projectName = System.IO.Path.GetFileName(projectPath);
if (installation == null) {
Logger.LogError($"Could not run project '{projectName}', Unity {version} not installed");
diff --git a/Command/app.manifest b/Command/app.manifest
new file mode 100644
index 0000000..438ee0d
--- /dev/null
+++ b/Command/app.manifest
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Command/rd.xml b/Command/rd.xml
deleted file mode 100644
index cd8be85..0000000
--- a/Command/rd.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Readme.md b/Readme.md
index db6521b..477d000 100644
--- a/Readme.md
+++ b/Readme.md
@@ -2,7 +2,7 @@
A command-line utility to install any recent version of Unity.
-Currently only supports macOS (Intel & Apple Silicon) but support for Windows/Linux is possible, PRs welcome.
+Currently only supports macOS (Intel & Apple Silicon) and Windows, but support for Linux is possible, PRs welcome.
## Table of Contents
@@ -25,6 +25,12 @@ Installing the latest release version of Unity is as simple as:
install-unity install f
+# How to build plugin on Windows
+
+```shell
+dotnet publish -r win-x64 -c Release --self-contained --framework net6.0
+```
+
## Versions
Most commands take a version as input, either to select the version to install or to filter the output.
@@ -111,19 +117,14 @@ The project will use Unity's default setup, including packages. Alternatively, y
install-unity create --type minimal 2020.1 ~/Desktop/my-project
-### Patch Releases
-
-With the switch to LTS versions, Unity has stopped creating patch releases for Unity 2017.3 and newer. install-unity no longer scans for patch releases but you can still install them by specifying the full version number.
-
- install-unity install 2017.2.3p3
-
## CLI Help
````
-install-unity v2.11.0
+install-unity v2.12.0
USAGE: install-unity [--help] [--version] [--verbose...] [--yes] [--update]
- [--data-path ] [--opt =...]
+ [--clear-cache] [--data-path ]
+ [--opt =...]
GLOBAL OPTIONS:
-h, --help Show this help
@@ -131,6 +132,7 @@ GLOBAL OPTIONS:
-v, --verbose Increase verbosity of output, can be repeated
-y, --yes Don't prompt for confirmation (use with care)
-u, --update Force an update of the versions cache
+ --clear-cache Clear the versions cache before running any commands
--data-path Store all data at the given path, also don't delete
packages after install
--opt = Set additional options. Use '--opt list' to show all
@@ -145,8 +147,9 @@ ACTIONS:
USAGE: install-unity [options] [install] [--packages ...]
[--download] [--install] [--upgrade]
- [--platform none|macosintel|macosarm|windows|linux]
- [--yolo] []
+ [--platform none|mac_os|linux|windows|all]
+ [--arch none|x86_64|arm64|all] [--redownload] [--yolo]
+ []
OPTIONS:
Pattern to match Unity version or release notes / unity hub
@@ -158,9 +161,12 @@ OPTIONS:
'--data-path')
--upgrade Replace existing matching Unity installation after successful
install
- --platform none|macosintel|macosarm|windows|linux Platform to download
- the packages for (only valid with '--download', default =
- current platform)
+ --platform none|mac_os|linux|windows|all Platform to download the
+ packages for (only valid with '--download', default = current
+ platform)
+ --arch none|x86_64|arm64|all Architecture to download the packages for
+ (default = current architecture)
+ --redownload Force redownloading all files
--yolo Skip size and hash checks of downloaded files
@@ -168,28 +174,32 @@ OPTIONS:
Get an overview of available or installed Unity versions
USAGE: install-unity [options] list [--installed]
- [--platform none|macosintel|macosarm|windows|linux]
- []
+ [--platform none|mac_os|linux|windows|all]
+ [--arch none|x86_64|arm64|all] []
OPTIONS:
Pattern to match Unity version
-i, --installed List installed versions of Unity
- --platform none|macosintel|macosarm|windows|linux Platform to list the
- versions for (default = current platform)
+ --platform none|mac_os|linux|windows|all Platform to list the versions
+ for (default = current platform)
+ --arch none|x86_64|arm64|all Architecture to list the versions for
+ (default = current architecture)
---- DETAILS:
Show version information and all its available packages
USAGE: install-unity [options] details
- [--platform none|macosintel|macosarm|windows|linux]
- []
+ [--platform none|mac_os|linux|windows|all]
+ [--arch none|x86_64|arm64|all] []
OPTIONS:
Pattern to match Unity version or release notes / unity hub
url
- --platform none|macosintel|macosarm|windows|linux Platform to show the
- details for (default = current platform)
+ --platform none|mac_os|linux|windows|all Platform to show the details for
+ (default = current platform)
+ --arch none|x86_64|arm64|all Architecture to show the details for
+ (default = current architecture)
---- UNINSTALL:
diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj
index 63039f8..05cb0df 100644
--- a/Tests/Tests.csproj
+++ b/Tests/Tests.csproj
@@ -1,8 +1,9 @@
- net6.0
- 7.1
+ net7.0
+ win-x64;osx-x64
+ latest
false
diff --git a/sttz.InstallUnity/Installer/Configuration.cs b/sttz.InstallUnity/Installer/Configuration.cs
index aeb2a7a..ca3037a 100644
--- a/sttz.InstallUnity/Installer/Configuration.cs
+++ b/sttz.InstallUnity/Installer/Configuration.cs
@@ -15,7 +15,10 @@ namespace sttz.InstallUnity
public class Configuration
{
[Description("After how many seconds the cache is considered to be outdated.")]
- public int cacheLifetime = 60 * 60 * 24; // 24 hours
+ public int cacheLifetime = 60 * 60 * 16; // 16 hours
+
+ [Description("Maximum age of Unity releases to load when refreshing the cache (days).")]
+ public int latestMaxAge = 90; // 90 days
[Description("Delay between requests when scraping.")]
public int scrapeDelayMs = 50;
@@ -62,6 +65,17 @@ public class Configuration
+ "/Applications/Unity {major}.{minor}.{patch}{type}{build};"
+ "/Applications/Unity {major}.{minor}.{patch}{type}{build} ({hash})";
+ [Description("Windows installation paths, separted by ; (first non-existing will be used, variables: {major} {minor} {patch} {type} {build} {hash} {ProgramFiles}).")]
+ public string installPathWindows =
+ "{ProgramFiles}\\Unity {major}.{minor}.{patch}{type}{build};"
+ + "{ProgramFiles}\\Unity {major}.{minor}.{patch}{type}{build} ({hash});";
+
+ [Description("Windows directories which are searched for Unity installations, separted by ; (variables: {ProgramFiles}).")]
+ public string searchPathWindows =
+ "{ProgramFiles};"
+ + "{ProgramFiles}\\Unity\\Editor;"
+ + "{ProgramFiles}\\Unity\\Hub\\Editor;";
+
// -------- Serialization --------
///
diff --git a/sttz.InstallUnity/Installer/Downloader.cs b/sttz.InstallUnity/Installer/Downloader.cs
index 6839035..97f3d6a 100644
--- a/sttz.InstallUnity/Installer/Downloader.cs
+++ b/sttz.InstallUnity/Installer/Downloader.cs
@@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.IO;
-using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
-using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
@@ -23,6 +20,31 @@ public class Downloader
{
// -------- Settings --------
+ ///
+ /// How to handle existing files.
+ ///
+ public enum ExistingFile
+ {
+ ///
+ /// Undefined behaviour, will default to Resume.
+ ///
+ Undefined,
+
+ ///
+ /// Always redownload, overwriting existing files.
+ ///
+ Redownload,
+ ///
+ /// Try to hash and/or resume existing file,
+ /// will fall back to redownloading and overwriting.
+ ///
+ Resume,
+ ///
+ /// Do not hash or touch existing files and complete immediately.
+ ///
+ Skip
+ }
+
///
/// Url of the file to download.
///
@@ -39,20 +61,14 @@ public class Downloader
public long ExpectedSize { get; protected set; }
///
- /// Expected hash of the file (computed with ).
+ /// Expected hash of the file (in WRC SRI format).
///
public string ExpectedHash { get; protected set; }
///
- /// Try to resume download of partially downloaded files.
- ///
- public bool Resume = true;
-
- ///
- /// Hash algorithm used to compute hash (null = don't compute hash).
+ /// How to handle existing files.
///
- [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
- public Type HashAlgorithm = typeof(MD5);
+ public ExistingFile Existing = ExistingFile.Resume;
///
/// Buffer size used when downloading.
@@ -132,7 +148,7 @@ public enum State
///
/// The hash after the file has been downloaded.
///
- public string Hash { get; protected set; }
+ public byte[] Hash { get; protected set; }
///
/// Event called for every of data processed.
@@ -184,6 +200,10 @@ public void Reset()
blocks = null;
watch = null;
}
+
+ if (Existing == ExistingFile.Undefined) {
+ Existing = ExistingFile.Resume;
+ }
}
///
@@ -192,8 +212,22 @@ public void Reset()
public bool CheckHash()
{
if (Hash == null) throw new InvalidOperationException("No Hash set.");
- if (ExpectedHash == null) throw new InvalidOperationException("No ExpectedHash set.");
- return string.Equals(ExpectedHash, Hash, StringComparison.OrdinalIgnoreCase);
+ if (string.IsNullOrEmpty(ExpectedHash)) throw new InvalidOperationException("No ExpectedHash set.");
+
+ var hash = SplitSRIHash(ExpectedHash);
+
+ var base64Hash = Convert.ToBase64String(Hash);
+ if (string.Equals(hash.value, base64Hash, StringComparison.OrdinalIgnoreCase))
+ return true;
+
+ // Unity generates their hashes in a non-standard way
+ // W3C SRI specifies the hash to be base64 encoded form the raw hash bytes
+ // but Unity takes the hex-encoded string of the hash and base64-encodes that
+ var hexBase64Hash = Convert.ToBase64String(Encoding.UTF8.GetBytes(Helpers.ToHexString(Hash)));
+ if (string.Equals(hash.value, hexBase64Hash, StringComparison.OrdinalIgnoreCase))
+ return true;
+
+ return false;
}
///
@@ -201,12 +235,13 @@ public bool CheckHash()
///
public async Task AssertExistingFileHash(CancellationToken cancellation = default)
{
- if (ExpectedHash == null) throw new InvalidOperationException("No ExpectedHash set.");
+ if (string.IsNullOrEmpty(ExpectedHash)) throw new InvalidOperationException("No ExpectedHash set.");
if (!File.Exists(TargetPath)) return;
+ var hash = SplitSRIHash(ExpectedHash);
HashAlgorithm hasher = null;
- if (HashAlgorithm != null) {
- hasher = CreateHashAlgorithm(HashAlgorithm);
+ if (hash.algorithm != null) {
+ hasher = CreateHashAlgorithm(hash.algorithm);
}
using (var input = File.Open(TargetPath, FileMode.Open, FileAccess.Read)) {
@@ -214,10 +249,10 @@ public async Task AssertExistingFileHash(CancellationToken cancellation = defaul
await CopyToAsync(input, Stream.Null, hasher, cancellation);
}
hasher.TransformFinalBlock(new byte[0], 0, 0);
- Hash = Helpers.ToHexString(hasher.Hash);
+ Hash = hasher.Hash;
if (!CheckHash()) {
- throw new Exception($"Existing file '{TargetPath}' does not match expected hash (got {Hash}, expected {ExpectedHash}).");
+ throw new Exception($"Existing file '{TargetPath}' does not match expected hash (got {Convert.ToBase64String(Hash)}, expected {hash.value}).");
}
}
@@ -231,8 +266,11 @@ public async Task Start(CancellationToken cancellation = default)
try {
HashAlgorithm hasher = null;
- if (HashAlgorithm != null) {
- hasher = CreateHashAlgorithm(HashAlgorithm);
+ if (!string.IsNullOrEmpty(ExpectedHash)) {
+ var hash = SplitSRIHash(ExpectedHash);
+ if (hash.algorithm != null) {
+ hasher = CreateHashAlgorithm(hash.algorithm);
+ }
}
var filename = Path.GetFileName(TargetPath);
@@ -240,42 +278,18 @@ public async Task Start(CancellationToken cancellation = default)
// Check existing file
var mode = FileMode.Create;
var startOffset = 0L;
- if (File.Exists(TargetPath) && Resume) {
- // Try to resume existing file
- var fileInfo = new FileInfo(TargetPath);
- if (ExpectedSize > 0 && fileInfo.Length >= ExpectedSize) {
- if (hasher != null) {
- using (var input = File.Open(TargetPath, FileMode.Open, FileAccess.Read)) {
- CurrentState = State.Hashing;
- await CopyToAsync(input, Stream.Null, hasher, cancellation);
- }
- hasher.TransformFinalBlock(new byte[0], 0, 0);
- Hash = Helpers.ToHexString(hasher.Hash);
- if (ExpectedHash != null) {
- if (CheckHash()) {
- Logger.LogInformation($"Existing file '{filename}' has matching hash, skipping...");
- CurrentState = State.Complete;
- return;
- } else {
- // Hash mismatch, force redownload
- Logger.LogWarning($"Existing file '{filename}' has different hash: Got {Hash} but expected {ExpectedHash}. Will redownload...");
- startOffset = 0;
- mode = FileMode.Create;
- }
- } else {
- Logger.LogInformation($"Existing file '{filename}' has hash {Hash} but we have nothing to check against, assuming it's ok...");
- }
- } else {
- // Assume file is good
- Logger.LogInformation($"Existing file '{filename}' cannot be checked for integrity, assuming it's ok...");
- CurrentState = State.Complete;
- return;
- }
-
- } else {
- Logger.LogInformation($"Resuming partial download of '{filename}' ({Helpers.FormatSize(fileInfo.Length)} already downloaded)...");
- startOffset = fileInfo.Length;
+ if (File.Exists(TargetPath)) {
+ // Handle existing file from a previous download
+ var existing = await HandleExistingFile(hasher, cancellation);
+ if (existing.complete) {
+ CurrentState = State.Complete;
+ return;
+ } else if (existing.startOffset > 0) {
+ startOffset = existing.startOffset;
mode = FileMode.Append;
+ } else {
+ startOffset = 0;
+ mode = FileMode.Create;
}
}
@@ -288,6 +302,13 @@ public async Task Start(CancellationToken cancellation = default)
if (client.Timeout != TimeSpan.FromSeconds(Timeout))
client.Timeout = TimeSpan.FromSeconds(Timeout);
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellation);
+
+ if (response.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable) {
+ // Disable resuming for next attempt
+ Existing = ExistingFile.Redownload;
+ throw new Exception($"Failed to resume, disabled resume for '{filename}' (HTTP Code 416)");
+ }
+
response.EnsureSuccessStatusCode();
// Redownload whole file if resuming fails
@@ -323,18 +344,18 @@ public async Task Start(CancellationToken cancellation = default)
if (hasher != null) {
hasher.TransformFinalBlock(new byte[0], 0, 0);
- Hash = Helpers.ToHexString(hasher.Hash);
+ Hash = hasher.Hash;
}
- if (Hash != null && ExpectedHash != null && !CheckHash()) {
- if (ExpectedHash == null) {
- Logger.LogInformation($"Downloaded file '{filename}' with hash {Hash}");
+ if (Hash != null && !string.IsNullOrEmpty(ExpectedHash) && !CheckHash()) {
+ if (string.IsNullOrEmpty(ExpectedHash)) {
+ Logger.LogInformation($"Downloaded file '{filename}' with hash {Convert.ToBase64String(Hash)}");
CurrentState = State.Complete;
} else if (CheckHash()) {
- Logger.LogInformation($"Downloaded file '{filename}' with expected hash {Hash}");
+ Logger.LogInformation($"Downloaded file '{filename}' with expected hash {Convert.ToBase64String(Hash)}");
CurrentState = State.Complete;
} else {
- throw new Exception($"Downloaded file '{filename}' does not match expected hash (got {Hash} but expected {ExpectedHash})");
+ throw new Exception($"Downloaded file '{filename}' does not match expected hash (got {Convert.ToBase64String(Hash)} but expected {ExpectedHash})");
}
} else {
Logger.LogInformation($"Downloaded file '{filename}'");
@@ -346,6 +367,54 @@ public async Task Start(CancellationToken cancellation = default)
}
}
+ async Task<(bool complete, long startOffset)> HandleExistingFile(HashAlgorithm hasher, CancellationToken cancellation)
+ {
+ if (Existing == ExistingFile.Skip) {
+ // Complete without checking or resuming
+ return (true, -1);
+ }
+
+ var filename = Path.GetFileName(TargetPath);
+
+ if (Existing == ExistingFile.Resume) {
+ var hashChecked = false;
+ if (!string.IsNullOrEmpty(ExpectedHash) && hasher != null) {
+ // If we have a hash, always check against hash first
+ using (var input = File.Open(TargetPath, FileMode.Open, FileAccess.Read)) {
+ CurrentState = State.Hashing;
+ await CopyToAsync(input, Stream.Null, hasher, cancellation);
+ }
+ hasher.TransformFinalBlock(new byte[0], 0, 0);
+ Hash = hasher.Hash;
+
+ if (CheckHash()) {
+ Logger.LogInformation($"Existing file '{filename}' has matching hash, skipping...");
+ return (true, -1);
+ } else {
+ hashChecked = true;
+ }
+ }
+
+ if (ExpectedSize > 0) {
+ var fileInfo = new FileInfo(TargetPath);
+ if (fileInfo.Length >= ExpectedSize && !hashChecked) {
+ // No hash and big enough, Assume file is good
+ Logger.LogInformation($"Existing file '{filename}' cannot be checked for integrity, assuming it's ok...");
+ return (true, -1);
+
+ } else {
+ // File smaller than it should be, try resuming
+ Logger.LogInformation($"Resuming partial download of '{filename}' ({Helpers.FormatSize(fileInfo.Length)} already downloaded)...");
+ return (false, fileInfo.Length);
+ }
+ }
+ }
+
+ // Force redownload from start
+ Logger.LogWarning($"Redownloading existing file '{filename}'");
+ return (false, 0);
+ }
+
///
/// Helper method to copy the stream with a progress callback.
///
@@ -393,17 +462,36 @@ async Task CopyToAsync(Stream input, Stream output, HashAlgorithm hasher, Cancel
}
///
- /// Create a HashAlgorithm instance from the given type that is subclass of HashAlgorithm.
- /// The type needs to implement a static Create method that takes no arguments and
- /// return the HashAlgorithm instance.
+ /// Split a WRC SRI string into hash algorithm and hash value.
///
- static HashAlgorithm CreateHashAlgorithm(Type type)
+ (string algorithm, string value) SplitSRIHash(string hash)
{
- var createMethod = type.GetMethod("Create", BindingFlags.Public | BindingFlags.Static, new Type[0]);
- if (createMethod == null) {
- throw new Exception($"Could not find static Create method on hash algorithm type '{type}'");
+ if (string.IsNullOrEmpty(hash))
+ return (null, null);
+
+ var firstDash = hash.IndexOf('-');
+ if (firstDash < 0) return (null, hash);
+
+ var hashName = hash.Substring(0, firstDash).ToLowerInvariant();
+ var hashValue = hash.Substring(firstDash + 1);
+
+ return (hashName, hashValue);
+ }
+
+ ///
+ /// Create a hash algorithm instance from a hash name.
+ ///
+ HashAlgorithm CreateHashAlgorithm(string hashName)
+ {
+ switch (hashName) {
+ case "md5": return MD5.Create();
+ case "sha256": return SHA256.Create();
+ case "sha512": return SHA512.Create();
+ case "sha384": return SHA384.Create();
}
- return (HashAlgorithm)createMethod.Invoke(null, null);
+
+ Logger.LogError($"Unsupported hash algorithm: '{hashName}'");
+ return null;
}
}
diff --git a/sttz.InstallUnity/Installer/IInstallerPlatform.cs b/sttz.InstallUnity/Installer/IInstallerPlatform.cs
index 19f1acd..dd60f81 100644
--- a/sttz.InstallUnity/Installer/IInstallerPlatform.cs
+++ b/sttz.InstallUnity/Installer/IInstallerPlatform.cs
@@ -2,6 +2,8 @@
using System.Threading;
using System.Threading.Tasks;
+using static sttz.InstallUnity.UnityReleaseAPIClient;
+
namespace sttz.InstallUnity
{
@@ -34,18 +36,27 @@ public interface IInstallerPlatform
///
/// The platform that should be used by default.
///
- Task GetCurrentPlatform();
+ Task<(Platform, Architecture)> GetCurrentPlatform();
///
- /// Get platforms that can be installed on the current platform.
+ /// Get architectures that can be installed on the current platform.
///
- Task> GetInstallablePlatforms();
+ Task GetInstallableArchitectures();
///
/// The path to the file where settings are stored.
///
string GetConfigurationDirectory();
+ ///
+ /// Set the configuration instance to use.
+ ///
+ ///
+ /// Note that other methods might be called before the configuraiton
+ /// is set, namely .
+ ///
+ void SetConfiguration(Configuration configuration);
+
///
/// The directory where cache files are stored.
///
@@ -103,7 +114,7 @@ public interface IInstallerPlatform
///
/// Uninstall a Unity installation.
///
- Task Uninstall(Installation instalation, CancellationToken cancellation = default);
+ Task Uninstall(Installation installation, CancellationToken cancellation = default);
///
/// Run a Unity installation with the given arguments.
diff --git a/sttz.InstallUnity/Installer/Platforms/MacPlatform.cs b/sttz.InstallUnity/Installer/Platforms/MacPlatform.cs
index 45af46f..ff477d4 100644
--- a/sttz.InstallUnity/Installer/Platforms/MacPlatform.cs
+++ b/sttz.InstallUnity/Installer/Platforms/MacPlatform.cs
@@ -2,12 +2,14 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using Claunia.PropertyList;
using Microsoft.Extensions.Logging;
+using static sttz.InstallUnity.UnityReleaseAPIClient;
+
namespace sttz.InstallUnity
{
@@ -44,34 +46,30 @@ public class MacPlatform : IInstallerPlatform
// -------- IInstallerPlatform --------
- public async Task GetCurrentPlatform()
+ public Task<(Platform, Architecture)> GetCurrentPlatform()
{
- var result = await Command.Run("uname", "-a");
- if (result.exitCode != 0) {
- throw new Exception($"ERROR: {result.error}");
- }
-
- if (result.output.Contains("_ARM64_")) {
- return CachePlatform.macOSArm;
- } else if (result.output.Contains("x86_64")) {
- return CachePlatform.macOSIntel;
+ #if !NET7_0_OR_GREATER
+ #error MacPlatform requires .Net 7 or newer to reliably detect Arm64 Macs
+ #endif
+
+ var arch = System.Runtime.InteropServices.RuntimeInformation.OSArchitecture;
+ switch (arch) {
+ case System.Runtime.InteropServices.Architecture.Arm64:
+ return Task.FromResult((Platform.Mac_OS, Architecture.ARM64));
+ case System.Runtime.InteropServices.Architecture.X64:
+ return Task.FromResult((Platform.Mac_OS, Architecture.X86_64));
+ default:
+ throw new Exception($"Unexpected macOS architecture: {arch}");
}
-
- throw new Exception($"Unknown runtime architecture: '{result.output.Trim()}'");
}
- public async Task> GetInstallablePlatforms()
+ public async Task GetInstallableArchitectures()
{
- var platform = await GetCurrentPlatform();
- if (platform == CachePlatform.macOSIntel) {
- return new CachePlatform[] {
- CachePlatform.macOSIntel
- };
+ var (_, arch) = await GetCurrentPlatform();
+ if (arch == Architecture.X86_64) {
+ return Architecture.X86_64;
} else {
- return new CachePlatform[] {
- CachePlatform.macOSIntel,
- CachePlatform.macOSArm
- };
+ return Architecture.ARM64 | Architecture.X86_64;
}
}
@@ -91,6 +89,11 @@ public string GetConfigurationDirectory()
return GetUserApplicationSupportDirectory();
}
+ public void SetConfiguration(Configuration configuration)
+ {
+ // Not used
+ }
+
public string GetCacheDirectory()
{
return GetUserApplicationSupportDirectory();
@@ -155,23 +158,20 @@ public async Task> FindInstallations(CancellationToken
continue;
}
- var versionResult = await Command.Run("/usr/bin/defaults", $"read \"{appPath}/Contents/Info\" CFBundleVersion", null, cancellation);
- if (versionResult.exitCode != 0) {
- throw new Exception($"ERROR: {versionResult.error}");
- }
+ // Extract version and build hash from Info.plist
+ var plistPath = Path.Combine(appPath, "Contents/Info.plist");
+ var rootDict = (NSDictionary)PropertyListParser.Parse(plistPath);
- var version = new UnityVersion(versionResult.output.Trim());
+ var versionString = rootDict.ObjectForKey("CFBundleVersion")?.ToString() ?? "";
+
+ var version = new UnityVersion(versionString);
if (!version.IsFullVersion) {
- Logger.LogWarning($"Could not determine Unity version at path '{appPath}': {versionResult.output.Trim()}");
+ Logger.LogWarning($"Could not determine Unity version at path '{appPath}': {versionString}");
continue;
}
- var hashResult = await Command.Run("/usr/bin/defaults", $"read \"{appPath}/Contents/Info\" UnityBuildNumber", null, cancellation);
- if (hashResult.exitCode != 0) {
- throw new Exception($"ERROR: {hashResult.error}");
- }
-
- version.hash = hashResult.output.Trim();
+ var hashString = rootDict.ObjectForKey("UnityBuildNumber")?.ToString();
+ version.hash = hashString;
var executable = ExecutableFromAppPath(appPath);
if (executable == null) continue;
@@ -184,83 +184,120 @@ public async Task> FindInstallations(CancellationToken
});
}
+ if (installations.Count == 0) {
+ // Check spotlight status if we couldn't find any installations
+ var spotlightResult = await Command.Run("/usr/bin/mdutil", "-s /Applications", null, cancellation);
+ if (spotlightResult.exitCode != 0) {
+ Logger.LogWarning($"Could not determine Spotlight status of '/Applications', finding Unity installations might not work.");
+ } else if (spotlightResult.output.Contains("disabled") || spotlightResult.output.Contains("No index")) {
+ Logger.LogWarning($"Spotlight is disabled for '/Applications', existing Unity installations might not be found.");
+ }
+ }
+
return installations;
}
public async Task PrepareInstall(UnityInstaller.Queue queue, string installationPaths, CancellationToken cancellation = default)
{
- if (installing.version.IsValid)
- throw new InvalidOperationException($"Already installing another version: {installing.version}");
+ if (installing.Version.IsValid)
+ throw new InvalidOperationException($"Already installing another version: {installing.Version}");
installing = queue.metadata;
this.installationPaths = installationPaths;
installedEditor = false;
- // Move existing installation out of the way
- movedExisting = false;
- if (Directory.Exists(INSTALL_PATH)) {
- if (Directory.Exists(INSTALL_PATH_TMP)) {
- throw new InvalidOperationException($"Fallback installation path '{INSTALL_PATH_TMP}' already exists.");
- }
- Logger.LogInformation("Temporarily moving existing installation at default install path: " + INSTALL_PATH);
- await Move(INSTALL_PATH, INSTALL_PATH_TMP, cancellation);
- movedExisting = true;
- }
-
// Check for upgrading installation
upgradeOriginalPath = null;
- if (!queue.items.Any(i => i.package.name == PackageMetadata.EDITOR_PACKAGE_NAME)) {
+ if (!queue.items.Any(i => i.package is EditorDownload)) {
var installs = await FindInstallations(cancellation);
- var existingInstall = installs.Where(i => i.version == queue.metadata.version).FirstOrDefault();
+ var existingInstall = installs.Where(i => i.version == queue.metadata.Version).FirstOrDefault();
if (existingInstall == null) {
- throw new InvalidOperationException($"Not installing editor but version {queue.metadata.version} not already installed.");
+ throw new InvalidOperationException($"Not installing editor but version {queue.metadata.Version} not already installed.");
}
upgradeOriginalPath = existingInstall.path;
+ }
- Logger.LogInformation($"Temporarily moving installation to upgrade from '{existingInstall}' to default install path");
- await Move(existingInstall.path, INSTALL_PATH, cancellation);
+ // Move existing installation out of the way
+ movedExisting = false;
+ if (upgradeOriginalPath != INSTALL_PATH) {
+ if (Directory.Exists(INSTALL_PATH)) {
+ if (Directory.Exists(INSTALL_PATH_TMP)) {
+ throw new InvalidOperationException($"Fallback installation path '{INSTALL_PATH_TMP}' already exists.");
+ }
+ Logger.LogInformation("Temporarily moving existing installation at default install path: " + INSTALL_PATH);
+ await Move(INSTALL_PATH, INSTALL_PATH_TMP, cancellation);
+ movedExisting = true;
+ }
+
+ // Check for upgrading installation
+ if (upgradeOriginalPath != null) {
+ Logger.LogInformation($"Temporarily moving installation to upgrade from '{upgradeOriginalPath}' to default install path");
+ await Move(upgradeOriginalPath, INSTALL_PATH, cancellation);
+ }
}
}
public async Task Install(UnityInstaller.Queue queue, UnityInstaller.QueueItem item, CancellationToken cancellation = default)
{
- if (item.package.name != PackageMetadata.EDITOR_PACKAGE_NAME && !installedEditor && upgradeOriginalPath == null) {
+ if (item.package is not EditorDownload && !installedEditor && upgradeOriginalPath == null) {
throw new InvalidOperationException("Cannot install package without installing editor first.");
}
- var extentsion = Path.GetExtension(item.filePath).ToLower();
- if (extentsion == ".pkg") {
+ var module = (item.package as Module);
+
+ if (item.package is EditorDownload editor) {
+ // Install main editor
+ if (Path.GetExtension(item.filePath).ToLowerInvariant() != ".pkg")
+ throw new Exception($"Unexpected file type for editor package (expected PKG but got '{Path.GetFileName(item.filePath)}')");
await InstallPkg(item.filePath, cancellation);
- } else if (extentsion == ".dmg") {
- await InstallDmg(item.filePath, cancellation);
- } else if (extentsion == ".zip") {
- await InstallZip(item.filePath, item.package.destination, item.package.renameFrom, item.package.renameTo, cancellation);
- } else if (extentsion == ".po") {
- await InstallFile(item.filePath, item.package.destination, cancellation);
+
} else {
- throw new Exception("Cannot install package of type: " + extentsion);
+ // Install additional module
+ var extension = Path.GetExtension(item.filePath);
+ switch (extension.ToLowerInvariant()) {
+ case ".pkg":
+ await InstallPkg(item.filePath, cancellation);
+ break;
+ case ".dmg":
+ await InstallDmg(item.filePath, module.destination, cancellation);
+ break;
+ case ".zip":
+ await InstallZip(item.filePath, module.destination, cancellation);
+ break;
+ case ".po":
+ await InstallFile(item.filePath, module.destination, cancellation);
+ break;
+ default:
+ throw new Exception("Cannot install package of type: " + module.type);
+ }
+ }
+
+ if (module?.extractedPathRename.IsSet == true) {
+ await Rename(item.filePath, module.extractedPathRename, cancellation);
}
- if (item.package.name == PackageMetadata.EDITOR_PACKAGE_NAME) {
+ if (item.package is EditorDownload) {
installedEditor = true;
}
}
public async Task CompleteInstall(bool aborted, CancellationToken cancellation = default)
{
- if (!installing.version.IsValid)
+ if (!installing.Version.IsValid)
throw new InvalidOperationException("Not installing any version to complete");
string destination = null;
if (upgradeOriginalPath != null) {
// Move back installation
destination = upgradeOriginalPath;
- Logger.LogInformation("Moving back upgraded installation to: " + destination);
- await Move(INSTALL_PATH, destination, cancellation);
+ if (upgradeOriginalPath != INSTALL_PATH) {
+ Logger.LogInformation("Moving back upgraded installation to: " + destination);
+ await Move(INSTALL_PATH, destination, cancellation);
+ }
} else if (!aborted) {
// Move new installations to "Unity VERSION"
- destination = GetUniqueInstallationPath(installing.version, installationPaths);
+ destination = GetUniqueInstallationPath(installing.Version, installationPaths);
Logger.LogInformation("Moving newly installed version to: " + destination);
await Move(INSTALL_PATH, destination, cancellation);
} else if (aborted) {
@@ -280,7 +317,7 @@ public async Task CompleteInstall(bool aborted, CancellationToken
if (executable == null) return default;
var installation = new Installation() {
- version = installing.version,
+ version = installing.Version,
executable = executable,
path = destination
};
@@ -318,10 +355,7 @@ public async Task Run(Installation installation, IEnumerable arguments,
Logger.LogInformation($"$ {cmd.StartInfo.FileName} {cmd.StartInfo.Arguments}");
cmd.Start();
-
- while (!cmd.HasExited) {
- await Task.Delay(100);
- }
+ await cmd.WaitForExitAsync();
} else {
if (!arguments.Contains("-logFile")) {
@@ -349,12 +383,7 @@ public async Task Run(Installation installation, IEnumerable arguments,
cmd.Start();
cmd.BeginOutputReadLine();
cmd.BeginErrorReadLine();
-
- while (!cmd.HasExited) {
- await Task.Delay(100);
- }
-
- cmd.WaitForExit(); // Let stdout and stderr flush
+ await cmd.WaitForExitAsync(); // Let stdout and stderr flush
Logger.LogInformation($"Unity exited with code {cmd.ExitCode}");
Environment.Exit(cmd.ExitCode);
}
@@ -390,8 +419,8 @@ string ExecutableFromAppPath(string appPath)
///
async Task InstallPkg(string filePath, CancellationToken cancellation = default)
{
- var platform = await GetCurrentPlatform();
- if (platform == CachePlatform.macOSIntel) {
+ var (_, arch) = await GetCurrentPlatform();
+ if (arch == Architecture.X86_64) {
var result = await Sudo("/usr/sbin/installer", $"-pkg \"{filePath}\" -target \"{INSTALL_VOLUME}\" -verbose", cancellation);
if (result.exitCode != 0) {
throw new Exception($"ERROR: {result.error}");
@@ -410,7 +439,7 @@ async Task InstallPkg(string filePath, CancellationToken cancellation = default)
///
/// Install a DMG package by mounting it and copying the app bundle.
///
- async Task InstallDmg(string filePath, CancellationToken cancellation = default)
+ async Task InstallDmg(string filePath, string destination = null, CancellationToken cancellation = default)
{
// Mount DMG
var result = await Command.Run("/usr/bin/hdiutil", $"attach -nobrowse -mountrandom /tmp \"{filePath}\"", cancellation: cancellation);
@@ -436,13 +465,26 @@ async Task InstallDmg(string filePath, CancellationToken cancellation = default)
if (apps.Length == 0) {
throw new Exception("No app bundles found in DMG.");
}
-
- var targetDir = Path.Combine(INSTALL_VOLUME, "Applications");
+
+ string targetDir;
+ if (!string.IsNullOrEmpty(destination)) {
+ targetDir = destination.Replace("{UNITY_PATH}", INSTALL_PATH);
+ } else {
+ targetDir = Path.Combine(INSTALL_VOLUME, "Applications");
+ }
+
foreach (var app in apps) {
- var dst = Path.Combine(targetDir, Path.GetFileName(app));
+ var dst = targetDir;
+ if (string.IsNullOrEmpty(destination)) {
+ // If we have a destination, we only copy the contents
+ // So only create an app bundle directory without destination
+ dst = Path.Combine(dst, Path.GetFileName(app));
+ }
+
if (Directory.Exists(dst) || File.Exists(dst)) {
await Delete(dst, cancellation);
}
+
await Copy(app, dst, cancellation);
}
} finally {
@@ -471,7 +513,7 @@ async Task InstallFile(string filePath, string destination, CancellationToken ca
///
/// Unpack a Zip file to the given destination.
///
- async Task InstallZip(string filePath, string destination, string renameFrom, string renameTo, CancellationToken cancellation = default)
+ async Task InstallZip(string filePath, string destination, CancellationToken cancellation = default)
{
if (string.IsNullOrEmpty(destination)) {
throw new Exception($"Cannot install {filePath}: Zip packages must have a destination set.");
@@ -508,15 +550,19 @@ async Task InstallZip(string filePath, string destination, string renameFrom, st
// Fix permissions, some files are not readable if installed with root
await Sudo("/bin/chmod", $"-R o+rX \"{target}\"", cancellation);
+ }
- if (!string.IsNullOrEmpty(renameFrom) && !string.IsNullOrEmpty(renameTo)) {
- var from = renameFrom.Replace("{UNITY_PATH}", INSTALL_PATH);
- var to = renameTo.Replace("{UNITY_PATH}", INSTALL_PATH);
- if (!Directory.Exists(from) && !File.Exists(from)) {
- throw new Exception($"{filePath}: renameFrom path does not exist: {from}");
- }
- await Move(from, to, cancellation);
+ ///
+ /// Rename an installed filed or folder after inital installation.
+ ///
+ async Task Rename(string filePath, PathRename rename, CancellationToken cancellation = default)
+ {
+ var from = rename.from.Replace("{UNITY_PATH}", INSTALL_PATH);
+ var to = rename.to.Replace("{UNITY_PATH}", INSTALL_PATH);
+ if (!Directory.Exists(from) && !File.Exists(from)) {
+ throw new Exception($"{filePath}: renameFrom path does not exist: {from}");
}
+ await Move(from, to, cancellation);
}
///
@@ -560,6 +606,15 @@ string GetUniqueInstallationPath(UnityVersion version, string installationPaths)
///
async Task Move(string sourcePath, string newPath, CancellationToken cancellation)
{
+ if (sourcePath.StartsWith(newPath + "/")) {
+ // We're moving something to replace one of its parent folders,
+ // we need to move it out of the parent first and then delte the parent
+ var tmpSource = Path.Combine(Path.GetTempPath(), UnityInstaller.PRODUCT_NAME, Path.GetFileName(newPath));
+ await Move(sourcePath, tmpSource, cancellation);
+ await Delete(newPath, cancellation);
+ sourcePath = tmpSource;
+ }
+
var baseDst = Path.GetDirectoryName(newPath);
try {
@@ -598,7 +653,7 @@ async Task Copy(string sourcePath, string newPath, CancellationToken cancellatio
throw new Exception($"ERROR: {result.error}");
}
- result = await Command.Run("/bin/cp", $"-R \"{sourcePath}\" \"{newPath}\"", cancellation: cancellation);
+ result = await Command.Run("/bin/cp", $"-a \"{sourcePath}\" \"{newPath}\"", cancellation: cancellation);
if (result.exitCode != 0) {
throw new Exception($"ERROR: {result.error}");
}
@@ -614,7 +669,7 @@ async Task Copy(string sourcePath, string newPath, CancellationToken cancellatio
throw new Exception($"ERROR: {result.error}");
}
- result = await Sudo("/bin/mv", $"\"{sourcePath}\" \"{newPath}\"", cancellation);
+ result = await Sudo("/bin/cp", $"-a \"{sourcePath}\" \"{newPath}\"", cancellation);
if (result.exitCode != 0) {
throw new Exception($"ERROR: {result.error}");
}
@@ -697,5 +752,4 @@ async Task CheckIsRoot(bool withSudo, CancellationToken cancellation)
}
}
}
-
}
diff --git a/sttz.InstallUnity/Installer/Platforms/WindowsPlatform.cs b/sttz.InstallUnity/Installer/Platforms/WindowsPlatform.cs
new file mode 100755
index 0000000..7ca8e36
--- /dev/null
+++ b/sttz.InstallUnity/Installer/Platforms/WindowsPlatform.cs
@@ -0,0 +1,392 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Security.Principal;
+using System.Threading;
+using System.Threading.Tasks;
+
+using static sttz.InstallUnity.UnityReleaseAPIClient;
+
+namespace sttz.InstallUnity
+{
+
+///
+/// Platform-specific installer code for Windows.
+///
+public class WindowsPlatform : IInstallerPlatform
+{
+ ///
+ /// Default installation path.
+ ///
+ static readonly string INSTALL_PATH = Path.Combine(ProgramFilesPath, "Unity");
+
+ ///
+ /// Paths where Unity installations are searched in.
+ ///
+ static readonly string[] INSTALL_LOCATIONS = new string[] {
+ ProgramFilesPath,
+ Path.Combine(ProgramFilesPath, "Unity", "Editor"),
+ Path.Combine(ProgramFilesPath, "Unity", "Hub", "Editor"),
+ };
+
+ ///
+ /// Path to the program files directory.
+ ///
+ static string ProgramFilesPath { get {
+ if (System.Runtime.InteropServices.RuntimeInformation.OSArchitecture != System.Runtime.InteropServices.Architecture.X86
+ && System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X86) {
+ // The unity editor since 2017.1 is 64bit
+ // If install-unity is run as X86 on a non-X86 system, GetFolderPath will return
+ // the "Program Files (x86)" directory instead of the main one where the editor
+ // is likely installed.
+ throw new Exception($"install-unity cannot run as X86 on a non-X86 Windows");
+ }
+ return Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
+ } }
+
+ string GetLocalApplicationDataDirectory()
+ {
+ return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ UnityInstaller.PRODUCT_NAME);
+ }
+
+ public void SetConfiguration(Configuration configuration)
+ {
+ this.configuration = configuration;
+ }
+
+ public async Task GetInstallableArchitectures()
+ {
+ var (_, arch) = await GetCurrentPlatform();
+ if (arch == Architecture.X86_64) {
+ return Architecture.X86_64;
+ } else {
+ return Architecture.ARM64 | Architecture.X86_64;
+ }
+ }
+
+ public Task> GetInstallablePlatforms()
+ {
+ IEnumerable platforms = new Platform[] { Platform.Windows };
+ return Task.FromResult(platforms);
+ }
+
+ public string GetCacheDirectory()
+ {
+ return GetLocalApplicationDataDirectory();
+ }
+
+ public Task<(Platform, Architecture)> GetCurrentPlatform()
+ {
+ return Task.FromResult((Platform.Windows, Architecture.X86_64));
+ }
+
+ public string GetConfigurationDirectory()
+ {
+ return GetLocalApplicationDataDirectory();
+ }
+
+ public string GetDownloadDirectory()
+ {
+ return Path.Combine(Path.GetTempPath(), UnityInstaller.PRODUCT_NAME);
+ }
+
+ public Task IsAdmin(CancellationToken cancellation = default)
+ {
+#pragma warning disable CA1416 // Validate platform compatibility
+ return Task.FromResult(new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator));
+#pragma warning restore CA1416 // Validate platform compatibility
+ }
+
+ public Task CompleteInstall(bool aborted, CancellationToken cancellation = default)
+ {
+ if (!installing.Version.IsValid)
+ throw new InvalidOperationException("Not installing any version to complete");
+
+ if (!aborted)
+ {
+ var executable = Path.Combine(installPath, "Editor", "Unity.exe");
+ if (!File.Exists(executable))
+ throw new Exception($"Unity exe not found at expected path after installation: {installPath}");
+
+ var installation = new Installation()
+ {
+ version = installing.Version,
+ executable = executable,
+ path = installPath
+ };
+
+ installing = default;
+
+ return Task.FromResult(installation);
+ }
+ else
+ {
+ return Task.FromResult(null);
+ }
+ }
+
+ public async Task> FindInstallations(CancellationToken cancellation = default)
+ {
+ var locations = INSTALL_LOCATIONS;
+ if (configuration != null && !string.IsNullOrEmpty(configuration.searchPathWindows)) {
+ locations = configuration.searchPathWindows.Split(';', StringSplitOptions.RemoveEmptyEntries);
+ var comparison = StringComparison.OrdinalIgnoreCase;
+ for (int i = 0; i < locations.Length; i++) {
+ locations[i] = Helpers.Replace(locations[i], "{ProgramFiles}", ProgramFilesPath, comparison);
+ }
+ }
+
+ var unityInstallations = new List();
+ foreach (var installPath in locations)
+ {
+ if (!Directory.Exists(installPath))
+ continue;
+
+ Logger.LogDebug($"Searching directory for Unity installations: {installPath}");
+
+ foreach (var unityCandidate in Directory.EnumerateDirectories(installPath))
+ {
+ var unityExePath = Path.Combine(unityCandidate, "Editor", "Unity.exe");
+ if (!File.Exists(unityExePath))
+ {
+ Logger.LogDebug($"No Unity.exe in {unityCandidate}\\Editor");
+ continue;
+ }
+
+ var versionInfo = FileVersionInfo.GetVersionInfo(unityExePath);
+ var splitCharacter = versionInfo.ProductVersion.Contains("_") ? '_' : '.'; // Versions are on format 2020.3.34f1_xxxx or 2020.3.34f1.xxxx
+
+ Logger.LogDebug($"Found version {versionInfo.ProductVersion} at path: {unityCandidate}");
+
+ unityInstallations.Add(new Installation {
+ executable = unityExePath,
+ path = unityCandidate,
+ version = new UnityVersion(versionInfo.ProductVersion.Substring(0, versionInfo.ProductVersion.LastIndexOf(splitCharacter)))
+ });
+ }
+ }
+
+ return await Task.FromResult(unityInstallations);
+ }
+
+ public async Task Install(UnityInstaller.Queue queue, UnityInstaller.QueueItem item, CancellationToken cancellation = default)
+ {
+ if (item.package is not EditorDownload && !installedEditor && upgradeOriginalPath == null)
+ {
+ throw new InvalidOperationException("Cannot install package without installing editor first.");
+ }
+
+ var result = await RunAsAdmin(item.filePath, $"/S /D={installPath}");
+ if (result.exitCode != 0)
+ {
+ throw new Exception($"Failed to install {item.filePath} output: {result.output} / {result.error}");
+ }
+
+ if (item.package is EditorDownload)
+ {
+ installedEditor = true;
+ }
+ }
+
+ public Task MoveInstallation(Installation installation, string newPath, CancellationToken cancellation = default)
+ {
+ // Don't need to move installation on Windows, Unity is installed in the correct location automatically.
+ return Task.CompletedTask;
+ }
+
+ public async Task PrepareInstall(UnityInstaller.Queue queue, string installationPaths, CancellationToken cancellation = default)
+ {
+ if (installing.Version.IsValid)
+ throw new InvalidOperationException($"Already installing another version: {installing.Version}");
+
+ installing = queue.metadata;
+ installedEditor = false;
+
+ // Check for upgrading installation
+ if (!queue.items.Any(i => i.package is EditorDownload))
+ {
+ var installs = await FindInstallations(cancellation);
+ var existingInstall = installs.Where(i => i.version == queue.metadata.Version).FirstOrDefault();
+ if (existingInstall == null)
+ {
+ throw new InvalidOperationException($"Not installing editor but version {queue.metadata.Version} not already installed.");
+ }
+
+ installedEditor = true;
+ }
+
+ installPath = GetInstallationPath(installing.Version, installationPaths);
+ }
+
+ public Task PromptForPasswordIfNecessary(CancellationToken cancellation = default)
+ {
+ // Don't care about password. The system will ask for elevated priviliges automatically
+ return Task.FromResult(true);
+ }
+
+ public async Task Uninstall(Installation installation, CancellationToken cancellation = default)
+ {
+ var result = await RunAsAdmin(Path.Combine(installation.path, "Editor", "Uninstall.exe"), "/AllUsers /Q /S");
+ if (result.exitCode != 0)
+ {
+ throw new Exception($"Could not uninstall Unity. output: {result.output}, error: {result.error}.");
+ }
+
+ // Uninstall.exe captures the files within the folder and retains sole access to them for some time even after returning a result
+ // We wait for a period of time and then make sure that the folder and contents are deleted
+ const int msDelay = 5000;
+ bool deletedFolder = false;
+
+ try
+ {
+ Logger.LogDebug($"Deleting folder path {installation.path} recursively in {msDelay}ms.");
+ await Task.Delay(msDelay); // Wait for uninstallation to let go of files in folder
+ Directory.Delete(installation.path, true);
+
+ Logger.LogDebug($"Folder path {installation.path} deleted.");
+ deletedFolder = true;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ try
+ {
+ // Sometimes access to folders and files are still in use by Uninstall.exe, so we wait some more
+ await Task.Delay(msDelay);
+ Directory.Delete(installation.path, true);
+
+ Logger.LogDebug($"Folder path {installation.path} deleted at second attempt.");
+ deletedFolder = true;
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // Ignore, path already deleted
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(e, $"Failed to delete folder path {installation.path} at second attempt. Ignoring excess files.");
+ // Continue even though errors occur deleting file path
+ }
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // Ignore, path already deleted
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(e, $"Failed to delete folder path {installation.path}.");
+ // Continue even though errors occur deleting file path
+ }
+
+ Logger.LogInformation($"Unity {installation.version} uninstalled successfully {(deletedFolder ? "and folder was deleted" : "but folder was not deleted")}.");
+ }
+
+ // -------- Helpers --------
+
+ ILogger Logger = UnityInstaller.CreateLogger();
+
+ Configuration configuration;
+
+ bool? isRoot;
+ string pwd;
+ VersionMetadata installing;
+ string installPath;
+ string upgradeOriginalPath;
+ bool movedExisting;
+ bool installedEditor;
+
+ async Task<(int exitCode, string output, string error)> RunAsAdmin(string filename, string arguments)
+ {
+ var startInfo = new ProcessStartInfo();
+ startInfo.FileName = filename;
+ startInfo.Arguments = arguments;
+ startInfo.CreateNoWindow = true;
+ startInfo.WindowStyle = ProcessWindowStyle.Hidden;
+ startInfo.RedirectStandardError = true;
+ startInfo.RedirectStandardOutput = true;
+ startInfo.UseShellExecute = false;
+ startInfo.WorkingDirectory = Environment.CurrentDirectory;
+ startInfo.Verb = "runas";
+ try
+ {
+ var p = Process.Start(startInfo);
+ await p.WaitForExitAsync();
+ return (p.ExitCode, p.StandardOutput.ReadToEnd(), p.StandardError.ReadToEnd());
+ } catch (Exception e)
+ {
+ Logger.LogError(e, $"Execution of {filename} with {arguments} failed!");
+ throw;
+ }
+ }
+
+ string GetInstallationPath(UnityVersion version, string installationPaths)
+ {
+ string expanded = null;
+ if (!string.IsNullOrEmpty(installationPaths))
+ {
+ var comparison = StringComparison.OrdinalIgnoreCase;
+ var paths = installationPaths.Split(';', StringSplitOptions.RemoveEmptyEntries);
+ foreach (var path in paths)
+ {
+ expanded = path.Trim();
+ expanded = Helpers.Replace(expanded, "{major}", version.major.ToString(), comparison);
+ expanded = Helpers.Replace(expanded, "{minor}", version.minor.ToString(), comparison);
+ expanded = Helpers.Replace(expanded, "{patch}", version.patch.ToString(), comparison);
+ expanded = Helpers.Replace(expanded, "{type}", ((char)version.type).ToString(), comparison);
+ expanded = Helpers.Replace(expanded, "{build}", version.build.ToString(), comparison);
+ expanded = Helpers.Replace(expanded, "{hash}", version.hash, comparison);
+ expanded = Helpers.Replace(expanded, "{ProgramFiles}", ProgramFilesPath, comparison);
+
+ return expanded;
+ }
+ }
+
+ if (expanded != null)
+ {
+ return Helpers.GenerateUniqueFileName(expanded);
+ }
+ else
+ {
+ return Helpers.GenerateUniqueFileName(INSTALL_PATH);
+ }
+ }
+
+ public async Task Run(Installation installation, IEnumerable arguments, bool child)
+ {
+ // child argument is ignored. We are always a child
+ if (!arguments.Contains("-logFile"))
+ {
+ arguments = arguments.Append("-logFile").Append("-");
+ }
+
+ var cmd = new System.Diagnostics.Process();
+ cmd.StartInfo.FileName = installation.executable;
+ cmd.StartInfo.Arguments = string.Join(" ", arguments);
+ cmd.StartInfo.UseShellExecute = false;
+
+ cmd.StartInfo.RedirectStandardOutput = true;
+ cmd.StartInfo.RedirectStandardError = true;
+ cmd.EnableRaisingEvents = true;
+
+ cmd.OutputDataReceived += (s, a) => {
+ if (a.Data == null) return;
+ Logger.LogInformation(a.Data);
+ };
+ cmd.ErrorDataReceived += (s, a) => {
+ if (a.Data == null) return;
+ Logger.LogError(a.Data);
+ };
+
+ cmd.Start();
+ cmd.BeginOutputReadLine();
+ cmd.BeginErrorReadLine();
+ await cmd.WaitForExitAsync(); // Let stdout and stderr flush
+
+ Logger.LogInformation($"Unity exited with code {cmd.ExitCode}");
+ Environment.Exit(cmd.ExitCode);
+ }
+}
+}
diff --git a/sttz.InstallUnity/Installer/Scraper.cs b/sttz.InstallUnity/Installer/Scraper.cs
index 9843b0c..9819ec0 100644
--- a/sttz.InstallUnity/Installer/Scraper.cs
+++ b/sttz.InstallUnity/Installer/Scraper.cs
@@ -1,16 +1,15 @@
using IniParser.Parser;
using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Net.Http;
-using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
+using static sttz.InstallUnity.UnityReleaseAPIClient;
+
namespace sttz.InstallUnity
{
@@ -33,44 +32,39 @@ public class Scraper
///
/// Base URL of Unity homepage.
///
- const string UNITY_BASE_URL = "https://unity3d.com";
+ const string UNITY_BASE_URL = "https://unity.com";
///
- /// Releases JSON used by Unity Hub ({0} should be either win32, darwin or linux).
+ /// HTML archive of Unity releases.
///
- const string UNITY_HUB_RELEASES = "https://public-cdn.cloud.unity3d.com/hub/prod/releases-{0}.json";
+ const string UNITY_ARCHIVE = "https://unity.com/releases/editor/archive";
///
- /// HTML archive of Unity releases.
+ /// Landing page for Unity beta releases.
///
- const string UNITY_ARCHIVE = "https://unity3d.com/get-unity/download/archive";
+ const string UNITY_BETA = "https://unity.com/releases/editor/beta";
///
- /// Landing page for Unity prereleases.
+ /// Landing page for Unity alpha releases.
///
- const string UNITY_PRERELEASES = "https://unity3d.com/unity/beta";
+ const string UNITY_ALPHA = "https://unity.com/releases/editor/alpha";
// -------- Release Notes --------
///
/// HTML release notes of final Unity releases (append a version without type or build number, e.g. 2018.2.1)
///
- const string UNITY_RELEASE_NOTES_FINAL = "https://unity3d.com/unity/whats-new/";
+ const string UNITY_RELEASE_NOTES_FINAL = "https://unity.com/releases/editor/whats-new/";
///
/// HTML release notes of alpha Unity releases (append a full alpha version string)
///
- const string UNITY_RELEASE_NOTES_ALPHA = "https://unity3d.com/unity/alpha/";
+ const string UNITY_RELEASE_NOTES_ALPHA = "https://unity.com/releases/editor/alpha/";
///
/// HTML release notes of beta Unity releases (append a full beta version string)
///
- const string UNITY_RELEASE_NOTES_BETA = "https://unity3d.com/unity/beta/";
-
- ///
- /// HTML release notes of patch Unity releases (append a full beta version string)
- ///
- const string UNITY_RELEASE_NOTES_PATCH = "https://unity3d.com/unity/qa/patch-releases/";
+ const string UNITY_RELEASE_NOTES_BETA = "https://unity.com/releases/editor/beta/";
// -------- INIs --------
@@ -102,14 +96,9 @@ public class Scraper
static readonly Regex UNITY_DOWNLOAD_RE = new Regex(@"https?:\/\/[\w.-]+unity3d\.com\/[\w\/.-]+\/([0-9a-f]{12})\/(?:[^\/]+\/)[\w\/.-]+-(\d+\.\d+\.\d+\w\d+)[\w\/.-]+");
///
- /// /// Regex to extract available prerelease major versions from landing page.
+ /// Regex to extract available prerelease versions from landing page.
///
- static readonly Regex UNITY_PRERELEASE_MAJOR_RE = new Regex(@"(?
- /// Regex to extract available prerelease major versions from landing page.
- ///
- static readonly Regex UNITY_PRERELEASE_RE = new Regex(@"\/unity\/(alpha|beta)\/(\d+\.\d+\.\d+\w\d+)");
+ static readonly Regex UNITY_PRERELEASE_RE = new Regex(@"\/releases\/editor\/(alpha|beta)\/(\d+\.\d+\.\d+\w\d+)");
// -------- Scraper --------
@@ -117,101 +106,6 @@ public class Scraper
ILogger Logger = UnityInstaller.CreateLogger();
- ///
- /// Load the latest Unity releases, using the same JSON as Unity Hub.
- ///
- /// Name of platform to load the JSON for
- /// Cancellation token
- /// Task returning the discovered versions
- public async Task> LoadLatest(CachePlatform cachePlatform, CancellationToken cancellation = default)
- {
- string platformName;
- switch (cachePlatform) {
- case CachePlatform.macOSIntel:
- platformName = "darwin";
- break;
- case CachePlatform.macOSArm:
- platformName = "silicon";
- break;
- case CachePlatform.Windows:
- platformName = "win32";
- break;
- case CachePlatform.Linux:
- platformName = "linux";
- break;
- default:
- throw new NotImplementedException("Invalid platform name: " + cachePlatform);
- }
-
- var url = string.Format(UNITY_HUB_RELEASES, platformName);
- Logger.LogInformation($"Loading latest releases for {platformName} from '{url}'");
- var response = await client.GetAsync(url, cancellation);
- response.EnsureSuccessStatusCode();
-
- var json = await response.Content.ReadAsStringAsync();
- Logger.LogDebug("Received response: {json}", json);
- var data = JsonConvert.DeserializeObject>(json);
-
- var result = new List();
- if (!data.ContainsKey("official")) {
- Logger.LogWarning("Unity Hub JSON does not contain expected 'official' array.");
- } else {
- ParseVersions(cachePlatform, data["official"], result);
- }
-
- if (data.ContainsKey("beta")) {
- ParseVersions(cachePlatform, data["beta"], result);
- }
-
- return result;
- }
-
- void ParseVersions(CachePlatform cachePlatform, HubUnityVersion[] versions, List results)
- {
- foreach (var version in versions) {
- // releases-darwin.json only contains intel, releases-silicon.json both arm and intel
- if (cachePlatform == CachePlatform.macOSArm && version.arch != "arm64")
- continue;
-
- var metadata = new VersionMetadata();
- metadata.version = new UnityVersion(version.version);
-
- var packages = new PackageMetadata[version.modules.Length + 1];
- packages[0] = new PackageMetadata() {
- name = "Unity ",
- title = "Unity " + version.version,
- description = "Unity Editor",
- url = version.downloadUrl,
- install = true,
- mandatory = false,
- size = long.Parse(version.downloadSize),
- installedsize = long.Parse(version.installedSize),
- version = version.version,
- md5 = version.checksum
- };
-
- var i = 1;
- foreach (var module in version.modules) {
- packages[i++] = new PackageMetadata() {
- name = module.id,
- title = module.name,
- description = module.description,
- url = module.downloadUrl,
- install = module.selected,
- mandatory = false,
- size = long.Parse(module.downloadSize),
- installedsize = long.Parse(module.installedSize),
- version = version.version,
- md5 = module.checksum
- };
- }
-
- Logger.LogDebug($"Found version {metadata.version} with {packages.Length} packages");
- metadata.SetPackages(cachePlatform, packages);
- results.Add(metadata);
- }
- }
-
///
/// Load the available final versions.
///
@@ -229,7 +123,7 @@ public async Task> LoadFinal(CancellationToken canc
var html = await response.Content.ReadAsStringAsync();
Logger.LogTrace($"Got response: {html}");
- return ExtractFromHtml(html).Values;
+ return ExtractFromHtml(html, ReleaseStream.None).Values;
}
///
@@ -239,63 +133,54 @@ public async Task> LoadFinal(CancellationToken canc
/// Task returning the discovered versions
public async Task> LoadPrerelease(bool includeAlpha, IEnumerable knownVersions = null, int scrapeDelay = 50, CancellationToken cancellation = default)
{
- // Load main prereleases page to discover which major versions are available as prerelease
- Logger.LogInformation($"Scraping latest prereleases with includeAlpha={includeAlpha} from '{UNITY_PRERELEASES}'");
+ var results = new Dictionary();
+
+ if (includeAlpha) {
+ await LoadPrerelease(UNITY_ALPHA, ReleaseStream.Alpha, results, knownVersions, scrapeDelay, cancellation);
+ }
+
+ await LoadPrerelease(UNITY_BETA, ReleaseStream.Beta, results, knownVersions, scrapeDelay, cancellation);
+
+ return results.Values;
+ }
+
+ ///
+ /// Load the available prerelase versions from a alpha/beta landing page.
+ ///
+ async Task LoadPrerelease(string url, ReleaseStream stream, Dictionary results, IEnumerable knownVersions = null, int scrapeDelay = 50, CancellationToken cancellation = default)
+ {
+ // Load major version's individual prerelease page to get individual versions
+ Logger.LogInformation($"Scraping latest prereleases from '{url}'");
await Task.Delay(scrapeDelay);
- var response = await client.GetAsync(UNITY_PRERELEASES, cancellation);
+ var response = await client.GetAsync(url, cancellation);
if (!response.IsSuccessStatusCode) {
- Logger.LogWarning($"Failed to scrape url '{UNITY_PRERELEASES}' ({response.StatusCode})");
- return Enumerable.Empty();
+ Logger.LogWarning($"Failed to scrape url '{url}' ({response.StatusCode})");
+ return;
}
var html = await response.Content.ReadAsStringAsync();
Logger.LogTrace($"Got response: {html}");
- var majorMatches = UNITY_PRERELEASE_MAJOR_RE.Matches(html);
- var visitedMajorVersions = new HashSet();
- var results = new Dictionary();
- foreach (Match majorMatch in majorMatches) {
- if (!visitedMajorVersions.Add(majorMatch.Groups[2].Value)) continue;
-
- var isAlpha = majorMatch.Groups[1].Value == "alpha";
- if (isAlpha && !includeAlpha) continue;
+ var versionMatches = UNITY_PRERELEASE_RE.Matches(html);
+ foreach (Match versionMatch in versionMatches) {
+ var version = new UnityVersion(versionMatch.Groups[2].Value);
+ if (results.ContainsKey(version)) continue;
+ if (knownVersions != null && knownVersions.Contains(version)) continue;
- // Load major version's individual prerelease page to get individual versions
- var archiveUrl = UNITY_BASE_URL + majorMatch.Value;
- Logger.LogInformation($"Scraping latest releases for {majorMatch.Groups[2].Value} from '{archiveUrl}'");
+ // Load version's release notes to get download links
+ var prereleaseUrl = UNITY_BASE_URL + versionMatch.Value;
+ Logger.LogInformation($"Scraping {versionMatch.Groups[1].Value} {version} from '{prereleaseUrl}'");
await Task.Delay(scrapeDelay);
- response = await client.GetAsync(archiveUrl, cancellation);
+ response = await client.GetAsync(prereleaseUrl, cancellation);
if (!response.IsSuccessStatusCode) {
- Logger.LogWarning($"Failed to scrape url '{archiveUrl}' ({response.StatusCode})");
- return Enumerable.Empty();
+ Logger.LogWarning($"Could not load release notes at url '{prereleaseUrl}' ({response.StatusCode})");
+ continue;
}
html = await response.Content.ReadAsStringAsync();
Logger.LogTrace($"Got response: {html}");
-
- var versionMatches = UNITY_PRERELEASE_RE.Matches(html);
- foreach (Match versionMatch in versionMatches) {
- var version = new UnityVersion(versionMatch.Groups[2].Value);
- if (results.ContainsKey(version)) continue;
- if (version.type == UnityVersion.Type.Alpha && !includeAlpha) continue;
- if (knownVersions != null && knownVersions.Contains(version)) continue;
-
- // Load version's release notes to get download links
- var prereleaseUrl = UNITY_BASE_URL + versionMatch.Value;
- Logger.LogInformation($"Scraping {versionMatch.Groups[1].Value} {version} from '{prereleaseUrl}'");
- await Task.Delay(scrapeDelay);
- response = await client.GetAsync(prereleaseUrl, cancellation);
- if (!response.IsSuccessStatusCode) {
- Logger.LogWarning($"Could not load release notes at url '{prereleaseUrl}' ({response.StatusCode})");
- continue;
- }
-
- html = await response.Content.ReadAsStringAsync();
- Logger.LogTrace($"Got response: {html}");
- ExtractFromHtml(html, true, results);
- }
+ ExtractFromHtml(html, stream, results);
}
- return results.Values;
}
///
@@ -313,7 +198,7 @@ string GetIniBaseUrl(UnityVersion.Type type)
///
/// Extract the versions and the base URLs from the html string.
///
- Dictionary ExtractFromHtml(string html, bool prerelease = false, Dictionary results = null)
+ Dictionary ExtractFromHtml(string html, ReleaseStream stream, Dictionary results = null)
{
var matches = UNITYHUB_RE.Matches(html);
results = results ?? new Dictionary();
@@ -323,11 +208,11 @@ Dictionary ExtractFromHtml(string html, bool prer
VersionMetadata metadata = default;
if (!results.TryGetValue(version, out metadata)) {
- metadata.version = version;
+ if (stream == ReleaseStream.None)
+ metadata = CreateEmptyVersion(version, stream);
}
metadata.baseUrl = GetIniBaseUrl(version.type) + version.hash + "/";
- metadata.prerelease = prerelease;
results[version] = metadata;
}
@@ -338,11 +223,10 @@ Dictionary ExtractFromHtml(string html, bool prer
VersionMetadata metadata = default;
if (!results.TryGetValue(version, out metadata)) {
- metadata.version = version;
+ metadata = CreateEmptyVersion(version, stream);
}
metadata.baseUrl = GetIniBaseUrl(version.type) + version.hash + "/";
- metadata.prerelease = prerelease;
results[version] = metadata;
}
@@ -363,8 +247,7 @@ public VersionMetadata UnityHubUrlToVersion(string url)
var version = new UnityVersion(match.Groups[1].Value);
version.hash = match.Groups[2].Value;
- var metadata = new VersionMetadata();
- metadata.version = version;
+ var metadata = CreateEmptyVersion(version, ReleaseStream.None);
metadata.baseUrl = GetIniBaseUrl(version.type) + version.hash + "/";
return metadata;
@@ -375,8 +258,8 @@ public VersionMetadata UnityHubUrlToVersion(string url)
///
///
/// The version must include major, minor and patch components.
- /// For patch and beta releases, it must also contain the build component.
- /// If no type is set, final is assumes.
+ /// For beta and alpha releases, it must also contain the build component.
+ /// If no type is set, final is assumed.
///
/// The version
/// The metadata or the default value if the version couldn't be found.
@@ -388,8 +271,9 @@ public async Task LoadExact(UnityVersion version, CancellationT
if (version.type != UnityVersion.Type.Final && version.type != UnityVersion.Type.Undefined && version.build < 0) {
throw new ArgumentException("The Unity version is incomplete (build missing)", nameof(version));
}
-
- var url = GetReleaseNotesUrl(version);
+
+ var stream = GuessStreamFromVersion(version);
+ var url = GetReleaseNotesUrl(stream, version);
if (url == null) {
throw new ArgumentException("The Unity version type is not supported: " + version.type, nameof(version));
}
@@ -414,7 +298,7 @@ public async Task LoadUrl(string url, CancellationToken cancell
var html = await response.Content.ReadAsStringAsync();
Logger.LogTrace($"Got response: {html}");
- return ExtractFromHtml(html).Values.FirstOrDefault();
+ return ExtractFromHtml(html, ReleaseStream.None).Values.FirstOrDefault();
}
///
@@ -422,40 +306,31 @@ public async Task LoadUrl(string url, CancellationToken cancell
/// The VersionMetadata must have iniUrl set.
///
/// Version metadata with iniUrl.
- /// Name of platform to load the packages for
+ /// Name of platform to load the packages for
/// A Task returning the metadata with packages filled in.
- public async Task LoadPackages(VersionMetadata metadata, CachePlatform cachePlatform, CancellationToken cancellation = default)
+ public async Task LoadPackages(VersionMetadata metadata, Platform platform, Architecture architecture, CancellationToken cancellation = default)
{
- if (!metadata.version.IsFullVersion) {
+ if (!metadata.Version.IsFullVersion) {
throw new ArgumentException("Unity version needs to be a full version", nameof(metadata));
}
- if (cachePlatform == CachePlatform.macOSArm && metadata.version < new UnityVersion(2021, 2)) {
+ if (platform == Platform.Mac_OS && architecture == Architecture.ARM64 && metadata.Version < new UnityVersion(2021, 2)) {
throw new ArgumentException("Apple Silicon builds are only available from Unity 2021.2", nameof(metadata));
}
- string platformName;
- switch (cachePlatform) {
- case CachePlatform.macOSIntel:
- case CachePlatform.macOSArm:
- platformName = "osx";
- break;
- case CachePlatform.Windows:
- platformName = "win";
- break;
- case CachePlatform.Linux:
- platformName = "linux";
- break;
- default:
- throw new NotImplementedException("Invalid platform name: " + cachePlatform);
- }
+ string platformName = platform switch {
+ Platform.Mac_OS => "osx",
+ Platform.Windows => "win",
+ Platform.Linux => "linux",
+ _ => throw new NotImplementedException("Invalid platform name: " + platform)
+ };
if (string.IsNullOrEmpty(metadata.baseUrl)) {
- throw new ArgumentException("VersionMetadata.baseUrl is not set for " + metadata.version, nameof(metadata));
+ throw new ArgumentException("VersionMetadata.baseUrl is not set for " + metadata.Version, nameof(metadata));
}
- var url = metadata.baseUrl + string.Format(UNITY_INI_FILENAME, metadata.version.ToString(false), platformName);
- Logger.LogInformation($"Loading packages for {metadata.version} and {platformName} from '{url}'");
+ var url = metadata.baseUrl + string.Format(UNITY_INI_FILENAME, metadata.Version.ToString(false), platformName);
+ Logger.LogInformation($"Loading packages for {metadata.Version} and {platformName} from '{url}'");
var response = await client.GetAsync(url, cancellation);
response.EnsureSuccessStatusCode();
@@ -472,176 +347,210 @@ public async Task LoadPackages(VersionMetadata metadata, CacheP
data = parser.Parse(ini);
}
- var packages = new PackageMetadata[data.Sections.Count];
- var i = 0;
+ var editorDownload = new EditorDownload();
+ editorDownload.platform = platform;
+ editorDownload.architecture = architecture;
+ editorDownload.modules = new List();
+
+ // Create modules from all entries
+ var allModules = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var section in data.Sections) {
- var meta = new PackageMetadata();
- meta.name = section.SectionName;
-
- foreach (var pair in section.Keys) {
- switch (pair.KeyName) {
- case "title":
- meta.title = pair.Value;
- break;
- case "description":
- meta.description = pair.Value;
- break;
- case "url":
- meta.url = pair.Value;
- break;
- case "install":
- meta.install = bool.Parse(pair.Value);
- break;
- case "mandatory":
- meta.mandatory = bool.Parse(pair.Value);
- break;
- case "size":
- meta.size = long.Parse(pair.Value);
- break;
- case "installedsize":
- meta.installedsize = long.Parse(pair.Value);
- break;
- case "version":
- meta.version = pair.Value;
- break;
- case "hidden":
- meta.hidden = bool.Parse(pair.Value);
- break;
- case "extension":
- meta.extension = pair.Value;
- break;
- case "sync":
- meta.sync = pair.Value;
- break;
- case "md5":
- meta.md5 = pair.Value;
- break;
- case "requires_unity":
- meta.requires_unity = bool.Parse(pair.Value);
- break;
- case "appidentifier":
- meta.appidentifier = pair.Value;
- break;
- case "eulamessage":
- meta.eulamessage = pair.Value;
- break;
- case "eulalabel1":
- meta.eulalabel1 = pair.Value;
- break;
- case "eulaurl1":
- meta.eulaurl1 = pair.Value;
- break;
- case "eulalabel2":
- meta.eulalabel2 = pair.Value;
- break;
- case "eulaurl2":
- meta.eulaurl2 = pair.Value;
- break;
- default:
- Logger.LogDebug($"Unknown ini field {pair.KeyName}: {pair.Value}");
- break;
- }
+ if (section.SectionName.Equals(EditorDownload.ModuleId, StringComparison.OrdinalIgnoreCase)) {
+ SetDownloadKeys(editorDownload, section);
+ continue;
}
- packages[i++] = meta;
+ var module = new Module();
+ module.id = section.SectionName;
+
+ SetDownloadKeys(module, section);
+ SetModuleKeys(module, section);
+
+ allModules.Add(module.id, module);
+ }
+
+ // Add virtual packages
+ foreach (var virutal in VirtualPackages.GeneratePackages(metadata.Version, editorDownload)) {
+ allModules.Add(virutal.id, virutal);
+ }
+
+ // Register sub-modules with their parents
+ foreach (var module in allModules.Values) {
+ if (module.parentModuleId == null) continue;
+
+ if (!allModules.TryGetValue(module.parentModuleId, out var parentModule))
+ throw new Exception($"Missing parent module '{module.parentModuleId}' for modules '{module.id}'");
+
+ if (parentModule.subModules == null)
+ parentModule.subModules = new List();
+
+ parentModule.subModules.Add(module);
+ module.parentModule = parentModule;
+ }
+
+ // Register remaining root modules with main editor download
+ foreach (var possibleRoot in allModules.Values) {
+ if (possibleRoot.parentModule != null)
+ continue;
+
+ editorDownload.modules.Add(possibleRoot);
}
+ Logger.LogInformation($"Found {allModules.Count} packages");
+
// Patch editor URL to point to Apple Silicon editor
// The old ini system probably won't be updated to include Apple Silicon variants
- if (cachePlatform == CachePlatform.macOSArm) {
- for (i = 0; i < packages.Length; i++) {
- if (packages[i].name == "Unity") {
- // Change e.g.
- // https://download.unity3d.com/download_unity/e50cafbb4399/MacEditorInstaller/Unity.pkg
- // to
- // https://download.unity3d.com/download_unity/e50cafbb4399/MacEditorInstallerArm64/Unity.pkg
- var editorUrl = packages[i].url;
- if (!editorUrl.StartsWith("MacEditorInstaller/")) {
- throw new Exception($"Cannot convert to Apple Silicon editor URL: Does not start with 'MacEditorInstaller' (got '{editorUrl}')");
- }
- editorUrl = editorUrl.Replace("MacEditorInstaller/", "MacEditorInstallerArm64/");
- packages[i].url = editorUrl;
- packages[i].description += " (Apple Silicon)";
- // Clear fields that are now invalid
- packages[i].md5 = null;
- }
+ if (platform == Platform.Mac_OS && architecture == Architecture.ARM64) {
+ // Change e.g.
+ // https://download.unity3d.com/download_unity/e50cafbb4399/MacEditorInstaller/Unity.pkg
+ // to
+ // https://download.unity3d.com/download_unity/e50cafbb4399/MacEditorInstallerArm64/Unity.pkg
+ var editorUrl = editorDownload.url;
+ if (!editorUrl.StartsWith("MacEditorInstaller/")) {
+ throw new Exception($"Cannot convert to Apple Silicon editor URL: Does not start with 'MacEditorInstaller' (got '{editorUrl}')");
}
- }
+ editorUrl = editorUrl.Replace("MacEditorInstaller/", "MacEditorInstallerArm64/");
+ editorDownload.url = editorUrl;
- Logger.LogInformation($"Found {packages.Length} packages");
- metadata.SetPackages(cachePlatform, packages);
+ // Clear fields that are now invalid
+ editorDownload.integrity = null;
+ }
+ metadata.SetEditorDownload(editorDownload);
return metadata;
}
- ///
- /// Guess the release notes URL for a version metadata.
- ///
- public string GetReleaseNotesUrl(VersionMetadata metadata)
+ void SetDownloadKeys(Download download, IniParser.Model.SectionData section)
{
- // Release candidates have a final version but are still on the beta page
- if (metadata.IsReleaseCandidate) {
- return UNITY_RELEASE_NOTES_BETA + metadata.version.ToString(false);
+ foreach (var pair in section.Keys) {
+ switch (pair.KeyName) {
+ case "url":
+ download.url = pair.Value;
+ break;
+ case "extension":
+ download.type = pair.Value switch {
+ "txt" => FileType.TEXT,
+ "zip" => FileType.ZIP,
+ "pkg" => FileType.PKG,
+ "exe" => FileType.EXE,
+ "po" => FileType.PO,
+ "dmg" => FileType.DMG,
+ _ => FileType.Undefined,
+ };
+ break;
+ case "size":
+ download.downloadSize.value = long.Parse(pair.Value);
+ download.downloadSize.unit = "BYTE";
+ break;
+ case "installedsize":
+ download.installedSize.value = long.Parse(pair.Value);
+ download.installedSize.unit = "BYTE";
+ break;
+ case "md5":
+ download.integrity = $"md5-{pair.Value}";
+ break;
+ }
}
+ }
- return GetReleaseNotesUrl(metadata.version);
+ void SetModuleKeys(Module download, IniParser.Model.SectionData section)
+ {
+ var eulaUrl1 = section.Keys["eulaurl1"];
+ if (eulaUrl1 != null) {
+ var eulaMessage = section.Keys["eulamessage"];
+ var eulaUrl2 = section.Keys["eulaurl2"];
+
+ var eulaCount = (eulaUrl2 != null ? 2 : 1);
+ download.eula = new Eula[eulaCount];
+
+ download.eula[0] = new Eula() {
+ message = eulaMessage,
+ label = section.Keys["eulalabel1"],
+ url = eulaUrl1
+ };
+
+ if (eulaCount > 1) {
+ download.eula[1] = new Eula() {
+ message = eulaMessage,
+ label = section.Keys["eulalabel2"],
+ url = eulaUrl2
+ };
+ }
+ }
+
+ foreach (var pair in section.Keys) {
+ switch (pair.KeyName) {
+ case "title":
+ download.name = pair.Value;
+ break;
+ case "description":
+ download.description = pair.Value;
+ break;
+ case "install":
+ download.preSelected = bool.Parse(pair.Value);
+ break;
+ case "mandatory":
+ download.required = bool.Parse(pair.Value);
+ break;
+ case "hidden":
+ download.hidden = bool.Parse(pair.Value);
+ break;
+ case "sync":
+ download.parentModuleId = pair.Value;
+ break;
+ }
+ }
}
///
- /// Guess the release notes URL for a version.
+ /// Create a new empty version.
///
- public string GetReleaseNotesUrl(UnityVersion version)
+ static VersionMetadata CreateEmptyVersion(UnityVersion version, ReleaseStream stream)
{
- switch (version.type) {
- case UnityVersion.Type.Undefined:
- case UnityVersion.Type.Final:
- return UNITY_RELEASE_NOTES_FINAL + version.major + "." + version.minor + "." + version.patch;
- case UnityVersion.Type.Patch:
- return UNITY_RELEASE_NOTES_PATCH + version.ToString(false);
- case UnityVersion.Type.Beta:
- return UNITY_RELEASE_NOTES_BETA + version.ToString(false);
- case UnityVersion.Type.Alpha:
- return UNITY_RELEASE_NOTES_ALPHA + version.ToString(false);
- default:
- return null;
- }
- }
+ var meta = new VersionMetadata();
+ meta.release = new Release();
+ meta.release.version = version;
+ meta.release.shortRevision = version.hash;
- // -------- Types --------
+ if (stream == ReleaseStream.None)
+ stream = GuessStreamFromVersion(version);
+ meta.release.stream = stream;
- // Disable never assigned warning, as the fields are
- // set dynamically in the JSON deserializer
- #pragma warning disable CS0649
+ return meta;
+ }
- struct HubUnityVersion
+ ///
+ /// Guess the release stream based on the Unity version.
+ ///
+ public static ReleaseStream GuessStreamFromVersion(UnityVersion version)
{
- public string version;
- public bool lts;
- public string downloadUrl;
- public string downloadSize;
- public string installedSize;
- public string checksum;
- public HubUnityModule[] modules;
- public string arch;
+ if (version.type == UnityVersion.Type.Alpha) {
+ return ReleaseStream.Alpha;
+ } else if (version.type == UnityVersion.Type.Beta) {
+ return ReleaseStream.Beta;
+ } else if (version.major >= 2017 && version.major <= 2019 && version.minor == 4) {
+ return ReleaseStream.LTS;
+ } else if (version.major >= 2020 && version.minor == 3) {
+ return ReleaseStream.LTS;
+ } else {
+ return ReleaseStream.Tech;
+ }
}
- struct HubUnityModule
+ ///
+ /// Guess the release notes URL for a version.
+ ///
+ public static string GetReleaseNotesUrl(ReleaseStream stream, UnityVersion version)
{
- public string id;
- public string name;
- public string description;
- public string downloadUrl;
- public string destination;
- public string category;
- public string installedSize;
- public string downloadSize;
- public bool visible;
- public bool selected;
- public string checksum;
+ switch (stream) {
+ case ReleaseStream.Alpha:
+ return UNITY_RELEASE_NOTES_ALPHA + version.ToString(false);
+ case ReleaseStream.Beta:
+ return UNITY_RELEASE_NOTES_BETA + version.ToString(false);
+ default:
+ return UNITY_RELEASE_NOTES_FINAL + $"{version.major}.{version.minor}.{version.patch}";
+ }
}
-
- #pragma warning restore CS0649
-
}
}
diff --git a/sttz.InstallUnity/Installer/UnityInstaller.cs b/sttz.InstallUnity/Installer/UnityInstaller.cs
index 9aa00e8..3699780 100644
--- a/sttz.InstallUnity/Installer/UnityInstaller.cs
+++ b/sttz.InstallUnity/Installer/UnityInstaller.cs
@@ -3,12 +3,12 @@
using System.IO;
using System.Linq;
using System.Net.Http;
-using System.Runtime.InteropServices;
-using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
+using static sttz.InstallUnity.UnityReleaseAPIClient;
+
namespace sttz.InstallUnity
{
@@ -67,6 +67,11 @@ public static ILogger CreateLogger(string categoryName)
///
public Scraper Scraper { get; protected set; }
+ ///
+ /// Client for the Unity Release API.
+ ///
+ public UnityReleaseAPIClient Releases { get; protected set; }
+
// -------- API --------
///
@@ -107,6 +112,7 @@ public enum InstallStep
public class Queue
{
public VersionMetadata metadata;
+ public string downloadPath;
public IList items;
}
@@ -118,7 +124,8 @@ public class QueueItem
///
/// Description of the item's current state.
///
- public enum State {
+ public enum State
+ {
///
/// Waiting for the download to start.
///
@@ -148,7 +155,7 @@ public enum State {
///
/// The package metadata of this item.
///
- public PackageMetadata package;
+ public Download package;
///
/// The item's current state.
///
@@ -212,11 +219,14 @@ public UnityInstaller(Configuration config = null, string dataPath = null, ILogg
GlobalLogger = CreateLogger("Global");
// Initialize platform-specific classes
- if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
+ if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX)) {
Logger.LogDebug("Loading platform integration for macOS");
Platform = new MacPlatform();
+ } else if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) {
+ Logger.LogDebug("Loading platform integration for Windows");
+ Platform = new WindowsPlatform();
} else {
- throw new NotImplementedException("Installer does not currently support the platform: " + RuntimeInformation.OSDescription);
+ throw new NotImplementedException("Installer does not currently support the platform: " + System.Runtime.InteropServices.RuntimeInformation.OSDescription);
}
DataPath = dataPath;
@@ -244,6 +254,7 @@ public UnityInstaller(Configuration config = null, string dataPath = null, ILogg
// Initialize components
Versions = new VersionsCache(GetCacheFilePath());
Scraper = new Scraper();
+ Releases = new UnityReleaseAPIClient();
}
///
@@ -270,7 +281,7 @@ public string GetCacheFilePath()
public string GetDownloadDirectory(VersionMetadata metadata)
{
var downloadPath = DataPath ?? Platform.GetDownloadDirectory();
- return Path.Combine(downloadPath, string.Format(Configuration.downloadSubdirectory, metadata.version));
+ return Path.Combine(downloadPath, string.Format(Configuration.downloadSubdirectory, metadata.Version));
}
///
@@ -289,57 +300,46 @@ public bool IsCacheOutdated(UnityVersion.Type type = UnityVersion.Type.Undefined
///
/// Update the Unity versions cache.
///
- /// Name of platform to update (only used for loading hub JSON)
+ /// Name of platform to update (only used for loading hub JSON)
/// Undefined = update latest, others = update archive of type and higher types
/// Task returning the newly discovered versions
- public async Task> UpdateCache(CachePlatform cachePlatform, UnityVersion.Type type = UnityVersion.Type.Undefined, CancellationToken cancellation = default)
+ public async Task> UpdateCache(Platform platform, UnityReleaseAPIClient.Architecture architecture, UnityVersion.Type type = UnityVersion.Type.Undefined, CancellationToken cancellation = default)
{
var added = new List();
- if (type == UnityVersion.Type.Undefined) {
- Logger.LogDebug("Loading UnityHub JSON with latest Unity versions...");
- var newVersions = await Scraper.LoadLatest(cachePlatform, cancellation);
- Logger.LogInformation($"Loaded {newVersions.Count()} versions from UnityHub JSON");
-
- Versions.Add(newVersions, added);
- Versions.SetLastUpdate(type, DateTime.Now);
- } else {
- switch (type) {
- case UnityVersion.Type.Final:
- case UnityVersion.Type.Patch:
- case UnityVersion.Type.Beta:
- case UnityVersion.Type.Alpha:
- Logger.LogDebug($"Updating Final Unity Versions...");
- var newVersions = await Scraper.LoadFinal(cancellation);
- Logger.LogInformation($"Scraped {newVersions.Count()} versions of type Final");
- Versions.Add(newVersions, added);
-
- Versions.SetLastUpdate(UnityVersion.Type.Final, DateTime.Now);
- break;
- }
- switch (type) {
- case UnityVersion.Type.Beta:
- case UnityVersion.Type.Alpha:
- Logger.LogDebug($"Updating Prerelease Unity Versions...");
- var newVersions = await Scraper.LoadPrerelease(
- type == UnityVersion.Type.Alpha,
- Versions.Select(m => m.version),
- Configuration.scrapeDelayMs,
- cancellation
- );
- Logger.LogInformation($"Scraped {newVersions.Count()} versions of type Beta/Alpha");
- Versions.Add(newVersions, added);
-
- Versions.SetLastUpdate(UnityVersion.Type.Beta, DateTime.Now);
- if (type == UnityVersion.Type.Alpha) {
- Versions.SetLastUpdate(UnityVersion.Type.Alpha, DateTime.Now);
- }
- break;
- }
+ var req = new UnityReleaseAPIClient.RequestParams();
+ req.platform = platform;
+ req.architecture = architecture;
+
+ req.stream = ReleaseStream.Tech | ReleaseStream.LTS;
+ if (type == UnityVersion.Type.Beta) req.stream |= ReleaseStream.Beta;
+ if (type == UnityVersion.Type.Alpha) req.stream |= ReleaseStream.Beta | ReleaseStream.Alpha;
+
+ var lastUpdate = Versions.GetLastUpdate(type);
+ var updatePeriod = DateTime.Now - lastUpdate;
+
+ var maxAge = TimeSpan.FromDays(Configuration.latestMaxAge);
+ if (updatePeriod > maxAge) updatePeriod = maxAge;
+
+ Logger.LogDebug($"Loading the latest Unity releases from the last {updatePeriod} days");
+
+ var releases = await Releases.LoadLatest(req, updatePeriod, cancellation);
+ Logger.LogInformation($"Loaded {releases.Count()} releases from the Unity Release API");
+
+ var metaReleases = releases.Select(r => VersionMetadata.FromRelease(r));
+ Versions.Add(metaReleases, added);
+
+ Versions.SetLastUpdate(UnityVersion.Type.Final, DateTime.Now);
+ if (type == UnityVersion.Type.Beta) {
+ Versions.SetLastUpdate(UnityVersion.Type.Beta, DateTime.Now);
}
+ if (type == UnityVersion.Type.Beta || type == UnityVersion.Type.Alpha) {
+ Versions.SetLastUpdate(UnityVersion.Type.Alpha, DateTime.Now);
+ }
+
Versions.Save();
- added.Sort((m1, m2) => m2.version.CompareTo(m1.version));
+ added.Sort((m1, m2) => m2.Version.CompareTo(m1.Version));
return added;
}
@@ -348,27 +348,32 @@ public async Task> UpdateCache(CachePlatform cacheP
///
/// Unity version
/// Name of platform
- public IEnumerable GetDefaultPackages(VersionMetadata metadata, CachePlatform cachePlatform)
+ public IEnumerable GetDefaultPackages(VersionMetadata metadata, Platform platform, UnityReleaseAPIClient.Architecture architecture)
{
- var packages = metadata.GetPackages(cachePlatform);
- if (packages == null) throw new ArgumentException($"Unity version contains no packages: {metadata.version}");
- return packages.Where(p => p.install).Select(p => p.name);
+ var editor = metadata.GetEditorDownload(platform, architecture);
+ if (editor == null) throw new ArgumentException($"No Unity version in cache for {platform}-{architecture}: {metadata.Version}");
+ return editor.modules.Where(p => p.preSelected).Select(p => p.id);
}
///
/// Resolve package patterns to package metadata.
/// This method also adds package dependencies.
///
- public IEnumerable ResolvePackages(
+ public IEnumerable ResolvePackages(
VersionMetadata metadata,
- CachePlatform cachePlatform,
+ Platform platform, UnityReleaseAPIClient.Architecture architecture,
IEnumerable packages,
IList notFound = null
) {
- var packageMetadata = metadata.GetPackages(cachePlatform);
- var metas = new List();
+ var editor = metadata.GetEditorDownload(platform, architecture);
+ var metas = new List();
foreach (var pattern in packages) {
var id = pattern;
+ if (id.Equals(EditorDownload.ModuleId, StringComparison.OrdinalIgnoreCase)) {
+ metas.Add(editor);
+ continue;
+ }
+
bool fuzzy = false, addDependencies = true;
while (id.StartsWith("~") || id.StartsWith("=")) {
if (id.StartsWith("~")) {
@@ -380,26 +385,26 @@ public IEnumerable ResolvePackages(
}
}
- PackageMetadata resolved = default;
+ Module resolved = null;
if (fuzzy) {
// Contains lookup
- foreach (var package in packageMetadata) {
- if (package.name.IndexOf(id, StringComparison.OrdinalIgnoreCase) >= 0) {
- if (resolved.name == null) {
- Logger.LogDebug($"Fuzzy lookup '{pattern}' matched package '{resolved.name}'");
+ foreach (var package in editor.AllModules.Values) {
+ if (package.id.IndexOf(id, StringComparison.OrdinalIgnoreCase) >= 0) {
+ if (resolved == null) {
+ Logger.LogDebug($"Fuzzy lookup '{pattern}' matched package '{package.id}'");
resolved = package;
} else {
- throw new Exception($"Fuzzy package match '{pattern}' is ambiguous between '{package.name}' and '{resolved.name}'");
+ throw new Exception($"Fuzzy package match '{pattern}' is ambiguous between '{package.id}' and '{resolved.id}'");
}
}
}
} else {
// Exact lookup
- resolved = metadata.GetPackage(cachePlatform, id);
+ editor.AllModules.TryGetValue(id, out resolved);
}
- if (resolved.name != null) {
- AddPackageWithDependencies(packageMetadata, metas, resolved, addDependencies);
+ if (resolved != null) {
+ AddPackageWithDependencies(editor, metas, resolved, addDependencies);
} else if (notFound != null) {
notFound.Add(id);
}
@@ -411,9 +416,9 @@ public IEnumerable ResolvePackages(
/// Recursive method to add package and dependencies.
///
void AddPackageWithDependencies(
- IEnumerable packages,
- List selected,
- PackageMetadata package,
+ EditorDownload editor,
+ List selected,
+ Module package,
bool addDependencies,
bool isDependency = false
) {
@@ -424,32 +429,76 @@ void AddPackageWithDependencies(
if (!addDependencies) return;
- foreach (var dep in packages) {
- if (dep.sync == package.name && !selected.Contains(dep)) {
- Logger.LogInformation($"Adding '{dep.name}' which '{package.name}' is synced with");
- AddPackageWithDependencies(packages, selected, dep, addDependencies, true);
- }
+ foreach (var subModule in package.subModules) {
+ if (selected.Contains(subModule))
+ continue;
+
+ Logger.LogInformation($"Adding '{subModule.id}' which '{package.id}' depends on");
+ AddPackageWithDependencies(editor, selected, subModule, addDependencies, true);
+ }
+ }
+
+ ///
+ /// Get the file name to use for the package.
+ ///
+ public string GetFileName(Download download)
+ {
+ string fileName;
+
+ // Try to get file name from URL
+ var uri = new Uri(download.url, UriKind.RelativeOrAbsolute);
+ if (uri.IsAbsoluteUri) {
+ fileName = uri.Segments.Last();
+ } else {
+ fileName = Path.GetFileName(download.url);
+ }
+
+ // Fallback to type-based extension
+ if (Path.GetExtension(fileName) == "" && download.type != FileType.Undefined) {
+ var typeExtension = download.type switch {
+ FileType.TEXT => ".txt",
+ FileType.TAR_GZ => ".tar.gz",
+ FileType.TAR_XZ => ".tar.xz",
+ FileType.ZIP => ".zip",
+ FileType.PKG => ".pkg",
+ FileType.EXE => ".exe",
+ FileType.PO => ".po",
+ FileType.DMG => ".dmg",
+ FileType.LZMA => ".lzma",
+ FileType.LZ4 => ".lz4",
+ FileType.PDF => ".pdf",
+ _ => throw new Exception($"Unhandled download type: {download.type}")
+ };
+
+ fileName = download.Id + typeExtension;
}
+
+ // Force an extension for older versions that have neither extension nor type
+ if (Path.GetExtension(fileName) == "") {
+ fileName = download.Id + ".pkg";
+ }
+
+ return fileName;
}
///
/// Create a download and install queue from the given version and packages.
///
/// The Unity version
- /// Name of platform
+ /// Name of platform
/// Location of the downloaded the packages
/// Packages to download and/or install
/// The queue list with the created queue items
- public Queue CreateQueue(VersionMetadata metadata, CachePlatform cachePlatform, string downloadPath, IEnumerable packages)
+ public Queue CreateQueue(VersionMetadata metadata, Platform platform, UnityReleaseAPIClient.Architecture architecture, string downloadPath, IEnumerable packages)
{
- if (!metadata.version.IsFullVersion)
+ if (!metadata.Version.IsFullVersion)
throw new ArgumentException("VersionMetadata.version needs to contain a full Unity version", nameof(metadata));
- var packageMetadata = metadata.GetPackages(cachePlatform);
+ var editor = metadata.GetEditorDownload(platform, architecture);
+
+ if (editor == null || !editor.modules.Any())
+ throw new ArgumentException("VersionMetadata.release cannot be null or empty", nameof(metadata));
- if (packageMetadata == null || !packageMetadata.Any())
- throw new ArgumentException("VersionMetadata.packages cannot be null or empty", nameof(metadata));
-
var items = new List();
foreach (var package in packages) {
var fullUrl = package.url;
@@ -457,8 +506,8 @@ public Queue CreateQueue(VersionMetadata metadata, CachePlatform cachePlatform,
fullUrl = metadata.baseUrl + package.url;
}
- var fileName = package.GetFileName();
- Logger.LogDebug($"{package.name}: Using file name '{fileName}' for url '{fullUrl}'");
+ var fileName = GetFileName(package);
+ Logger.LogDebug($"{package.Id}: Using file name '{fileName}' for url '{fullUrl}'");
var outputPath = Path.Combine(downloadPath, fileName);
items.Add(new QueueItem() {
@@ -471,6 +520,7 @@ public Queue CreateQueue(VersionMetadata metadata, CachePlatform cachePlatform,
return new Queue() {
metadata = metadata,
+ downloadPath = downloadPath,
items = items
};
}
@@ -481,7 +531,7 @@ public Queue CreateQueue(VersionMetadata metadata, CachePlatform cachePlatform,
/// Which steps to perform.
/// The queue to process
/// Cancellation token
- public async Task Process(InstallStep steps, Queue queue, bool skipChecks = false, CancellationToken cancellation = default)
+ public async Task Process(InstallStep steps, Queue queue, Downloader.ExistingFile existingFile = Downloader.ExistingFile.Undefined, CancellationToken cancellation = default)
{
if (queue == null) throw new ArgumentNullException(nameof(queue));
@@ -493,12 +543,12 @@ public async Task Process(InstallStep steps, Queue queue, bool ski
Logger.LogDebug($"download = {download}, install = {install}");
foreach (var item in queue.items) {
- var size = skipChecks ? -1 : item.package.size;
- var hash = skipChecks ? null : item.package.md5;
+ var size = item.package.downloadSize.GetBytes();
+ var hash = item.package.integrity;
if (!download) {
if (!File.Exists(item.filePath))
- throw new InvalidOperationException($"File for package {item.package.name} not found at path: {item.filePath}");
+ throw new InvalidOperationException($"File for package {item.package.Id} not found at path: {item.filePath}");
if (hash == null) {
Logger.LogWarning($"File exists but cannot be checked for completeness: {item.filePath}");
@@ -516,7 +566,13 @@ public async Task Process(InstallStep steps, Queue queue, bool ski
item.currentState = install ? QueueItem.State.WaitingForInstall : QueueItem.State.Complete;
} else {
item.downloader = new Downloader();
- item.downloader.Resume = Configuration.resumeDownloads;
+ if (existingFile != Downloader.ExistingFile.Undefined) {
+ item.downloader.Existing = existingFile;
+ } else {
+ item.downloader.Existing = Configuration.resumeDownloads
+ ? Downloader.ExistingFile.Resume
+ : Downloader.ExistingFile.Redownload;
+ }
item.downloader.Timeout = Configuration.requestTimeout;
item.downloader.Prepare(item.downloadUrl, item.filePath, size, hash);
}
@@ -525,17 +581,19 @@ public async Task Process(InstallStep steps, Queue queue, bool ski
if (install) {
string installationPaths = null;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
+ if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX)) {
installationPaths = Configuration.installPathMac;
+ } else if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) {
+ installationPaths = Configuration.installPathWindows;
} else {
- throw new NotImplementedException("Installer does not currently support the platform: " + RuntimeInformation.OSDescription);
+ throw new NotImplementedException("Installer does not currently support the platform: " + System.Runtime.InteropServices.RuntimeInformation.OSDescription);
}
await Platform.PrepareInstall(queue, installationPaths, cancellation);
}
try {
- var editorItem = queue.items.FirstOrDefault(i => i.package.name == PackageMetadata.EDITOR_PACKAGE_NAME);
+ var editorItem = queue.items.FirstOrDefault(i => i.package is EditorDownload);
while (!cancellation.IsCancellationRequested) {
// Check completed and count active
int downloading = 0, installing = 0, complete = 0;
@@ -549,18 +607,19 @@ public async Task Process(InstallStep steps, Queue queue, bool ski
item.retries--;
Logger.LogError(item.downloadTask.Exception.InnerException.Message
+ $" (retrying in {Configuration.retryDelay}s, {item.retries} retries remaining)");
+ Logger.LogInformation(item.downloadTask.Exception.InnerException.StackTrace);
item.waitUntil = DateTime.UtcNow + TimeSpan.FromSeconds(Configuration.retryDelay);
item.downloader.Reset();
item.currentState = QueueItem.State.WaitingForDownload;
}
} else {
item.currentState = install ? QueueItem.State.WaitingForInstall : QueueItem.State.Complete;
- Logger.LogDebug($"{item.package.name} download complete: now {item.currentState}");
+ Logger.LogDebug($"{item.package.Id} download complete: now {item.currentState}");
}
} else {
if (item.currentState == QueueItem.State.Hashing && item.downloader.CurrentState == Downloader.State.Downloading) {
item.currentState = QueueItem.State.Downloading;
- Logger.LogDebug($"{item.package.name} hashed: now {item.currentState}");
+ Logger.LogDebug($"{item.package.Id} hashed: now {item.currentState}");
}
downloading++;
}
@@ -571,7 +630,7 @@ public async Task Process(InstallStep steps, Queue queue, bool ski
throw item.installTask.Exception;
}
item.currentState = QueueItem.State.Complete;
- Logger.LogDebug($"{item.package.name}: install complete");
+ Logger.LogDebug($"{item.package.Id}: install complete");
} else {
installing++;
}
@@ -592,7 +651,7 @@ public async Task Process(InstallStep steps, Queue queue, bool ski
if (item.waitUntil > DateTime.UtcNow) {
continue;
}
- Logger.LogDebug($"{item.package.name}: Starting download");
+ Logger.LogDebug($"{item.package.Id}: Starting download");
if (download) {
item.downloadTask = item.downloader.Start(cancellation);
} else {
@@ -606,7 +665,7 @@ public async Task Process(InstallStep steps, Queue queue, bool ski
// Wait for the editor to complete installation
continue;
}
- Logger.LogDebug($"{item.package.name}: Starting install");
+ Logger.LogDebug($"{item.package.Id}: Starting install");
item.installTask = Platform.Install(queue, item, cancellation);
item.currentState = QueueItem.State.Installing;
installing++;
@@ -636,29 +695,34 @@ public async Task Process(InstallStep steps, Queue queue, bool ski
/// Where downloads were stored.
/// The Unity version downloaded
/// Downloaded packages.
- public void CleanUpDownloads(VersionMetadata metadata, string downloadPath, IEnumerable packages)
+ public void CleanUpDownloads(Queue queue)
{
- if (!Directory.Exists(downloadPath))
+ if (!Directory.Exists(queue.downloadPath))
return;
- foreach (var directory in Directory.GetDirectories(downloadPath)) {
+ foreach (var directory in Directory.GetDirectories(queue.downloadPath)) {
throw new Exception("Unexpected directory in downloads folder: " + directory);
}
- var packageFileNames = packages
- .Select(p => p.GetFileName())
+ var packageFilePaths = queue.items
+ .Select(p => Path.GetFullPath(p.filePath))
.ToList();
- foreach (var path in Directory.GetFiles(downloadPath)) {
+ foreach (var path in Directory.GetFiles(queue.downloadPath)) {
var fileName = Path.GetFileName(path);
if (fileName == ".DS_Store" || fileName == "thumbs.db" || fileName == "desktop.ini")
continue;
- if (!packageFileNames.Contains(fileName)) {
- throw new Exception("Unexpected file in downloads folder: " + path);
+ if (!packageFilePaths.Contains(path)) {
+ if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) {
+ // Don't throw on unexcpeted files in Windows Download folder
+ Logger.LogWarning("Unexpected file in downloads folder: " + path);
+ } else {
+ throw new Exception("Unexpected file in downloads folder: " + path);
+ }
}
}
- Directory.Delete(downloadPath, true);
+ Directory.Delete(queue.downloadPath, true);
}
}
diff --git a/sttz.InstallUnity/Installer/UnityReleaseAPIClient.cs b/sttz.InstallUnity/Installer/UnityReleaseAPIClient.cs
new file mode 100644
index 0000000..7e77407
--- /dev/null
+++ b/sttz.InstallUnity/Installer/UnityReleaseAPIClient.cs
@@ -0,0 +1,837 @@
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Runtime.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace sttz.InstallUnity
+{
+
+///
+/// Client for the official Unity Release API.
+/// Providing the latest Unity editor releases and associated packages.
+/// https://services.docs.unity.com/release/v1/index.html#tag/Release/operation/getUnityReleases
+///
+public class UnityReleaseAPIClient
+{
+ // -------- Types --------
+
+ ///
+ /// Different Unity release streams.
+ ///
+ [Flags]
+ public enum ReleaseStream
+ {
+ None = 0,
+
+ Alpha = 1<<0,
+ Beta = 1<<1,
+ Tech = 1<<2,
+ LTS = 1<<3,
+
+ PrereleaseMask = (Alpha | Beta),
+
+ All = -1,
+ }
+
+ ///
+ /// Platforms the Unity editor runs on.
+ ///
+ [Flags]
+ public enum Platform
+ {
+ None,
+
+ Mac_OS = 1<<0,
+ Linux = 1<<1,
+ Windows = 1<<2,
+
+ All = -1,
+ }
+
+ ///
+ /// CPU architectures the Unity editor supports (on some platforms).
+ ///
+ [Flags]
+ public enum Architecture
+ {
+ None = 0,
+
+ X86_64 = 1<<10,
+ ARM64 = 1<<11,
+
+ All = -1,
+ }
+
+ ///
+ /// Different file types of downloads and links.
+ ///
+ public enum FileType
+ {
+ Undefined,
+
+ TEXT,
+ TAR_GZ,
+ TAR_XZ,
+ ZIP,
+ PKG,
+ EXE,
+ PO,
+ DMG,
+ LZMA,
+ LZ4,
+ MD,
+ PDF
+ }
+
+ ///
+ /// Response from the Releases API.
+ ///
+ [JsonObject(MemberSerialization.Fields)]
+ public class Response
+ {
+ ///
+ /// Return wether the request was successful.
+ ///
+ public bool IsSuccess => ((int)status >= 200 && (int)status <= 299);
+
+ // -------- Response Fields --------
+
+ ///
+ /// Start offset from the returned results.
+ ///
+ public int offset;
+ ///
+ /// Limit of results returned.
+ ///
+ public int limit;
+ ///
+ /// Total number of results.
+ ///
+ public int total;
+
+ ///
+ /// The release results.
+ ///
+ public Release[] results;
+
+ // -------- Error fields --------
+
+ ///
+ /// Error code.
+ ///
+ public HttpStatusCode status;
+ ///
+ /// Title of the error.
+ ///
+ public string title;
+ ///
+ /// Error detail description.
+ ///
+ public string detail;
+ }
+
+ ///
+ /// A specific release of the Unity editor.
+ ///
+ [JsonObject(MemberSerialization.Fields)]
+ public class Release
+ {
+ ///
+ /// Version of the editor.
+ ///
+ public UnityVersion version;
+ ///
+ /// The Git Short Revision of the Unity Release.
+ ///
+ public string shortRevision;
+
+ ///
+ /// Date and time of the release.
+ ///
+ public DateTime releaseDate;
+ ///
+ /// Link to the release notes.
+ ///
+ public ReleaseNotes releaseNotes;
+ ///
+ /// Stream this release is part of.
+ ///
+ public ReleaseStream stream;
+ ///
+ /// The SKU family of the Unity Release.
+ /// Possible values: CLASSIC or DOTS
+ ///
+ public string skuFamily;
+ ///
+ /// The indicator for whether the Unity Release is the recommended LTS
+ ///
+ public bool recommended;
+ ///
+ /// Deep link to open this release in Unity Hub.
+ ///
+ public string unityHubDeepLink;
+
+ ///
+ /// Editor downloads of this release.
+ ///
+ public List downloads;
+
+ ///
+ /// The Third Party Notices of the Unity Release.
+ ///
+ public ThirdPartyNotice[] thirdPartyNotices;
+
+ [OnDeserialized]
+ internal void OnDeserializedMethod(StreamingContext context)
+ {
+ // Copy the short revision to the UnityVersion struct
+ if (string.IsNullOrEmpty(version.hash) && !string.IsNullOrEmpty(shortRevision)) {
+ version.hash = shortRevision;
+ }
+ }
+ }
+
+ ///
+ /// Unity editor release notes.
+ ///
+ [JsonObject(MemberSerialization.Fields)]
+ public struct ReleaseNotes
+ {
+ ///
+ /// Url to the release notes.
+ ///
+ public string url;
+ ///
+ /// Type of the release notes.
+ /// (Only seen "MD" so far.)
+ ///
+ public FileType type;
+ }
+
+ ///
+ /// Third party notices associated with a Unity release.
+ ///
+ [JsonObject(MemberSerialization.Fields)]
+ public struct ThirdPartyNotice
+ {
+ ///
+ /// The original file name of the Unity Release Third Party Notice.
+ ///
+ public string originalFileName;
+ ///
+ /// The URL of the Unity Release Third Party Notice.
+ ///
+ public string url;
+ ///
+ /// Type of the release notes.
+ ///
+ public FileType type;
+ }
+
+ ///
+ /// An Unity editor download, including available modules.
+ ///
+ [JsonObject(MemberSerialization.Fields)]
+ public abstract class Download
+ {
+ ///
+ /// Url to download.
+ ///
+ public string url;
+ ///
+ /// Integrity hash (hash prefixed by hash type plus dash, seen md5 and sha384).
+ ///
+ public string integrity;
+ ///
+ /// Type of download.
+ /// (Only seen "DMG", "PKG", "ZIP" and "PO" so far)
+ ///
+ public FileType type;
+ ///
+ /// Size of the download.
+ ///
+ public FileSize downloadSize;
+ ///
+ /// Size required on disk.
+ ///
+ public FileSize installedSize;
+
+ ///
+ /// ID of the download.
+ ///
+ public abstract string Id { get; }
+ }
+
+ ///
+ /// Main editor download.
+ ///
+ [JsonObject(MemberSerialization.Fields)]
+ public class EditorDownload : Download
+ {
+ ///
+ /// The Id of the editor download pseudo-module.
+ ///
+ public const string ModuleId = "unity";
+
+ ///
+ /// Platform of the editor.
+ ///
+ public Platform platform;
+ ///
+ /// Architecture of the editor.
+ ///
+ public Architecture architecture;
+ ///
+ /// Available modules for this editor version.
+ ///
+ public List modules;
+
+ ///
+ /// Editor downloads all have the fixed "Unity" ID.
+ ///
+ public override string Id => ModuleId;
+
+ ///
+ /// Dictionary of all modules, including sub-modules.
+ ///
+ public Dictionary AllModules { get {
+ if (_allModules == null) {
+ _allModules = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ if (modules != null) {
+ foreach (var module in modules) {
+ AddModulesRecursive(module);
+ }
+ }
+ }
+ return _allModules;
+ } }
+ [NonSerialized] Dictionary _allModules;
+
+ void AddModulesRecursive(Module module)
+ {
+ if (string.IsNullOrEmpty(module.id)) {
+ throw new Exception($"EditorDownload.AllModules: Module is missing ID");
+ }
+
+ if (!_allModules.TryAdd(module.id, module)) {
+ throw new Exception($"EditorDownload.AllModules: Multiple modules with id '{module.id}'");
+ }
+
+ if (module.subModules != null) {
+ foreach (var subModule in module.subModules) {
+ if (subModule == null) continue;
+ AddModulesRecursive(subModule);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Size description of a download or space required for install.
+ ///
+ [JsonObject(MemberSerialization.Fields)]
+ public struct FileSize
+ {
+ ///
+ /// Size value.
+ ///
+ public long value;
+ ///
+ /// Unit of the value.
+ /// Possible vaues: BYTE, KILOBYTE, MEGABYTE, GIGABYTE
+ /// (Only seen "BYTE" so far.)
+ ///
+ public string unit;
+
+ ///
+ /// Return the size in bytes, converting from the source unit when necessary.
+ ///
+ public long GetBytes()
+ {
+ switch (unit) {
+ case "BYTE":
+ return value;
+ case "KILOBYTE":
+ return value * 1024;
+ case "MEGABYTE":
+ return value * 1024 * 1024;
+ case "GIGABYTE":
+ return value * 1024 * 1024 * 1024;
+ default:
+ throw new Exception($"FileSize: Unhandled size unit '{unit}'");
+ }
+ }
+
+ ///
+ /// Create a new instance with the given amount of bytes.
+ ///
+ public static FileSize FromBytes(long bytes)
+ => new FileSize() { value = bytes, unit = "BYTE" };
+
+ ///
+ /// Create a new instance with the given amount of bytes.
+ ///
+ public static FileSize FromMegaBytes(long megaBytes)
+ => new FileSize() { value = megaBytes, unit = "MEGABYTE" };
+ }
+
+ ///
+ /// A module of an editor.
+ ///
+ [JsonObject(MemberSerialization.Fields)]
+ public class Module : Download
+ {
+ ///
+ /// Identifier of the module.
+ ///
+ public string id;
+ ///
+ /// Slug identifier of the module.
+ ///
+ public string slug;
+ ///
+ /// Name of the module.
+ ///
+ public string name;
+ ///
+ /// Description of the module.
+ ///
+ public string description;
+ ///
+ /// Category type of the module.
+ ///
+ public string category;
+ ///
+ /// Wether this module is required for its parent module.
+ ///
+ public bool required;
+ ///
+ /// Wether this module is hidden from the user.
+ ///
+ public bool hidden;
+ ///
+ /// Wether this module is installed by default.
+ ///
+ public bool preSelected;
+ ///
+ /// Where to install the module to (can contain the {UNITY_PATH} variable).
+ ///
+ public string destination;
+ ///
+ /// How to rename the installed directory.
+ ///
+ public PathRename extractedPathRename;
+ ///
+ /// EULAs the user should accept before installing.
+ ///
+ public Eula[] eula;
+ ///
+ /// Sub-Modules of this module.
+ ///
+ public List subModules;
+
+ ///
+ /// Modules return their dynamic id.
+ ///
+ public override string Id => id;
+ ///
+ /// Id of the parent module.
+ ///
+ [NonSerialized] public string parentModuleId;
+ ///
+ /// The parent module that lists this sub-module (null = part of main editor module).
+ ///
+ [NonSerialized] public Module parentModule;
+ ///
+ /// Used to track automatically added dependencies.
+ ///
+ [NonSerialized] public bool addedAutomatically;
+
+ [OnDeserialized]
+ internal void OnDeserializedMethod(StreamingContext context)
+ {
+ if (subModules != null) {
+ // Set ourself as parent module on sub-modules
+ foreach (var sub in subModules) {
+ if (sub == null) continue;
+ sub.parentModule = this;
+ sub.parentModuleId = id;
+ }
+ }
+ }
+ }
+
+ ///
+ /// EULA of a module.
+ ///
+ [JsonObject(MemberSerialization.Fields)]
+ public struct Eula
+ {
+ ///
+ /// URL to the EULA.
+ ///
+ public string url;
+ ///
+ /// Type of content at the url.
+ /// (Only seen "TEXT" so far.)
+ ///
+ public FileType type;
+ ///
+ /// Label for this EULA.
+ ///
+ public string label;
+ ///
+ /// Explanation message for the user.
+ ///
+ public string message;
+ }
+
+ ///
+ /// Path rename instruction.
+ ///
+ [JsonObject(MemberSerialization.Fields)]
+ public struct PathRename
+ {
+ ///
+ /// Path to rename from (can contain the {UNITY_PATH} variable).
+ ///
+ public string from;
+ ///
+ /// Path to rename to (can contain the {UNITY_PATH} variable).
+ ///
+ public string to;
+
+ ///
+ /// Wether both a from and to path are set.
+ ///
+ public bool IsSet => (!string.IsNullOrEmpty(from) && !string.IsNullOrEmpty(to));
+ }
+
+ // -------- API --------
+
+ ///
+ /// Order of the results returned by the API.
+ ///
+ [Flags]
+ public enum ResultOrder
+ {
+ ///
+ /// Default order (release date descending).
+ ///
+ Default = 0,
+
+ // -------- Sorting Cireteria --------
+
+ ///
+ /// Order by release date.
+ ///
+ ReleaseDate = 1<<0,
+
+ // -------- Sorting Order --------
+
+ ///
+ /// Return results in ascending order.
+ ///
+ Ascending = 1<<30,
+ ///
+ /// Return results in descending order.
+ ///
+ Descending = 1<<31,
+ }
+
+ ///
+ /// Request parameters of the Unity releases API.
+ ///
+ public class RequestParams
+ {
+ ///
+ /// Version filter, applied as full-text search on the version string.
+ ///
+ public string version = null;
+ ///
+ /// Unity release streams to load (can set multiple flags in bitmask).
+ ///
+ public ReleaseStream stream = ReleaseStream.All;
+ ///
+ /// Platforms to load (can set multiple flags in bitmask).
+ ///
+ public Platform platform = Platform.All;
+ ///
+ /// Architectures to load (can set multiple flags in bitmask).
+ ///
+ public Architecture architecture = Architecture.All;
+
+ ///
+ /// How many results to return (1-25).
+ ///
+ public int limit = 10;
+ ///
+ /// Offset of the first result returned
+ ///
+ public int offset = 0;
+ ///
+ /// Order of returned results.
+ ///
+ public ResultOrder order;
+ }
+
+ ///
+ /// Maximum number of requests that can be made per second.
+ ///
+ public const int MaxRequestsPerSecond = 10;
+ ///
+ /// Maximum number of requests that can be made per 30 minutes.
+ /// (Not currently tracked by the client.)
+ ///
+ public const int MaxRequestsPerHalfHour = 1000;
+
+ ///
+ /// Send a basic request to the Release API.
+ ///
+ public async Task Send(RequestParams request, CancellationToken cancellation = default)
+ {
+ var parameters = new List>();
+ parameters.Add(new (nameof(RequestParams.limit), request.limit.ToString("R")));
+ parameters.Add(new (nameof(RequestParams.offset), request.offset.ToString("R")));
+
+ if (!string.IsNullOrEmpty(request.version)) {
+ parameters.Add(new (nameof(RequestParams.version), request.version));
+ }
+ if (request.stream != ReleaseStream.All) {
+ AddArrayParameters(parameters, nameof(RequestParams.stream), StreamValues, request.stream);
+ }
+ if (request.platform != Platform.All) {
+ AddArrayParameters(parameters, nameof(RequestParams.platform), PlatformValues, request.platform);
+ }
+ if (request.architecture != Architecture.All) {
+ AddArrayParameters(parameters, nameof(RequestParams.architecture), ArchitectureValues, request.architecture);
+ }
+ if (request.order != ResultOrder.Default) {
+ if (request.order.HasFlag(ResultOrder.ReleaseDate)) {
+ var dir = (request.order.HasFlag(ResultOrder.Descending) ? "_DESC" : "_ASC");
+ parameters.Add(new (nameof(RequestParams.order), "RELEASE_DATE" + dir));
+ }
+ }
+
+ var query = await new FormUrlEncodedContent(parameters).ReadAsStringAsync(cancellation);
+ Logger.LogDebug($"Sending request to Unity Releases API with query '{Endpoint + query}'");
+
+ var timeSinceLastRequest = DateTime.Now - lastRequestTime;
+ var minRequestInterval = TimeSpan.FromSeconds(1) / MaxRequestsPerSecond;
+ if (timeSinceLastRequest < minRequestInterval) {
+ // Delay request to not exceed max requests per second
+ await Task.Delay(minRequestInterval - timeSinceLastRequest);
+ }
+
+ lastRequestTime = DateTime.Now;
+ var response = await client.GetAsync(Endpoint + query, cancellation);
+
+ var json = await response.Content.ReadAsStringAsync(cancellation);
+ Logger.LogTrace($"Received response from Unity Releases API ({response.StatusCode}): {json}");
+ if (string.IsNullOrEmpty(json)) {
+ throw new Exception($"Got empty response from Unity Releases API (code {response.StatusCode})");
+ }
+
+ var parsedResponse = JsonConvert.DeserializeObject(json);
+ if (parsedResponse.status == 0) {
+ parsedResponse.status = response.StatusCode;
+ }
+
+ return parsedResponse;
+ }
+
+ ///
+ /// Load all releases for the given request, making multiple
+ /// paginated requests to the API.
+ ///
+ /// The request to send, the limit and offset fields will be modified
+ /// Limit returned results to not make too many requests
+ /// Cancellation token
+ /// The results returned from the API
+ public async Task LoadAll(RequestParams request, int maxResults = 200, CancellationToken cancellation = default)
+ {
+ request.limit = 25;
+
+ int maxTotal = 0, currentOffset = 0;
+ Release[] releases = null;
+ Response response = null;
+ do {
+ response = await Send(request, cancellation);
+ if (!response.IsSuccess) {
+ throw new Exception($"Unity Release API request failed: {response.title} - {response.detail}");
+ }
+
+ maxTotal = Math.Min(response.total, maxResults);
+ if (releases == null) {
+ releases = new Release[maxTotal];
+ }
+
+ Array.Copy(response.results, 0, releases, currentOffset, response.results.Length);
+ currentOffset += response.results.Length;
+
+ request.offset += response.results.Length;
+
+ } while (currentOffset < maxTotal && response.results.Length > 0);
+
+ return releases;
+ }
+
+ ///
+ /// Load all latest releases from the given time period,
+ /// making multiple paginated requests to the API.
+ ///
+ /// The request to send, the limit, offset and order fields will be modified
+ /// The period to load releases from
+ /// Cancellation token
+ /// The results returned from the API, can contain releases older than the given period
+ public async Task> LoadLatest(RequestParams request, TimeSpan period, CancellationToken cancellation = default)
+ {
+ request.limit = 25;
+ request.order = ResultOrder.ReleaseDate | ResultOrder.Descending;
+
+ var releases = new List();
+ var now = DateTime.Now;
+ Response response = null;
+ do {
+ response = await Send(request, cancellation);
+ if (!response.IsSuccess) {
+ throw new Exception($"Unity Release API request failed: {response.title} - {response.detail}");
+ } else if (response.results.Length == 0) {
+ break;
+ }
+
+ releases.AddRange(response.results);
+ request.offset += response.results.Length;
+
+ var oldestReleaseDate = response.results[^1].releaseDate;
+ var releasedSince = now - oldestReleaseDate;
+ if (releasedSince > period) {
+ break;
+ }
+
+ } while (true);
+
+ return releases;
+ }
+
+ ///
+ /// Try to find a release based on version string search.
+ ///
+ public async Task FindRelease(UnityVersion version, Platform platform, Architecture architecture, CancellationToken cancellation = default)
+ {
+ var req = new RequestParams();
+ req.limit = 1;
+ req.order = ResultOrder.ReleaseDate | ResultOrder.Descending;
+
+ req.platform = platform;
+ req.architecture = architecture;
+
+ // Set release stream based on input version
+ req.stream = ReleaseStream.Tech | ReleaseStream.LTS;
+ if (version.type == UnityVersion.Type.Beta) req.stream |= ReleaseStream.Beta;
+ if (version.type == UnityVersion.Type.Alpha) req.stream |= ReleaseStream.Beta | ReleaseStream.Alpha;
+
+ // Only add version if not just release type
+ if (version.major >= 0) {
+ // Build up version for a sub-string search (e.g. 2022b won't return any results)
+ var searchString = version.major.ToString();
+ if (version.minor >= 0) {
+ searchString += "." + version.minor;
+ if (version.patch >= 0) {
+ searchString += "." + version.patch;
+ if (version.type != UnityVersion.Type.Undefined) {
+ searchString += (char)version.type;
+ if (version.build >= 0) {
+ searchString += version.build;
+ }
+ }
+ }
+ }
+ req.version = searchString;
+ }
+
+ var result = await Send(req, cancellation);
+ if (!result.IsSuccess) {
+ throw new Exception($"Unity Release API request failed: {result.title} - {result.detail}");
+ } else if (result.results.Length == 0) {
+ return null;
+ }
+
+ return result.results[0];
+ }
+
+ // -------- Implementation --------
+
+ ILogger Logger = UnityInstaller.CreateLogger();
+
+ static HttpClient client = new HttpClient();
+ static DateTime lastRequestTime = DateTime.MinValue;
+
+ ///
+ /// Endpoint of the releases API.
+ ///
+ const string Endpoint = "https://services.api.unity.com/unity/editor/release/v1/releases?";
+
+ ///
+ /// Query string values for streams.
+ ///
+ static readonly Dictionary StreamValues = new() {
+ { ReleaseStream.Alpha, "ALPHA" },
+ { ReleaseStream.Beta, "BETA" },
+ { ReleaseStream.Tech, "TECH" },
+ { ReleaseStream.LTS, "LTS" },
+ };
+ ///
+ /// Query string values for platforms.
+ ///
+ static readonly Dictionary PlatformValues = new() {
+ { Platform.Mac_OS, "MAC_OS" },
+ { Platform.Linux, "LINUX" },
+ { Platform.Windows, "WINDOWS" },
+ };
+ ///
+ /// Query string values for architectures.
+ ///
+ static readonly Dictionary ArchitectureValues = new() {
+ { Architecture.X86_64, "X86_64" },
+ { Architecture.ARM64, "ARM64" },
+ };
+
+ ///
+ /// Iterate all the single bits set in the given enum value.
+ /// (This does not check if the set bit is defined in the enum.)
+ ///
+ static IEnumerable IterateBits(T value)
+ where T : struct, System.Enum
+ {
+ var number = (int)(object)value;
+ for (int i = 0; i < 32; i++) {
+ var flag = 1 << i;
+ if ((number & flag) != 0)
+ yield return (T)(object)flag;
+ }
+ }
+
+ ///
+ /// Check the given bitmask enum for single set bits, look up those values
+ /// in the given dictionary and then add them to the query.
+ ///
+ static void AddArrayParameters(List> query, string name, Dictionary values, T bitmask)
+ where T : struct, System.Enum
+ {
+ foreach (var flag in IterateBits(bitmask)) {
+ if (!values.TryGetValue(flag, out var value)) {
+ // ERROR: Value not found
+ continue;
+ }
+ query.Add(new (name, value));
+ }
+ }
+}
+
+}
diff --git a/sttz.InstallUnity/Installer/UnityVersion.cs b/sttz.InstallUnity/Installer/UnityVersion.cs
index 72b1bbd..0b8c1b5 100644
--- a/sttz.InstallUnity/Installer/UnityVersion.cs
+++ b/sttz.InstallUnity/Installer/UnityVersion.cs
@@ -21,10 +21,6 @@ public enum Type: ushort {
///
Final = 'f',
///
- /// Unity patch release.
- ///
- Patch = 'p',
- ///
/// Unity beta release.
///
Beta = 'b',
@@ -86,8 +82,6 @@ public static int GetSortingForType(Type type)
{
switch (type) {
case Type.Final:
- return 4;
- case Type.Patch:
return 3;
case Type.Beta:
return 2;
@@ -102,7 +96,7 @@ public static int GetSortingForType(Type type)
/// Types sorted from unstable to stable.
///
public static readonly Type[] SortedTypes = new Type[] {
- Type.Alpha, Type.Beta, Type.Patch, Type.Final, Type.Undefined
+ Type.Alpha, Type.Beta, Type.Final, Type.Undefined
};
///
@@ -396,6 +390,11 @@ public bool Equals(UnityVersion other)
{
return lhs.CompareTo(rhs) >= 0;
}
+
+ public static explicit operator UnityVersion(string versionString)
+ {
+ return new UnityVersion(versionString);
+ }
}
}
diff --git a/sttz.InstallUnity/Installer/VersionsCache.cs b/sttz.InstallUnity/Installer/VersionsCache.cs
index 6fa8391..6f637ee 100644
--- a/sttz.InstallUnity/Installer/VersionsCache.cs
+++ b/sttz.InstallUnity/Installer/VersionsCache.cs
@@ -6,20 +6,10 @@
using System.IO;
using System.Linq;
-namespace sttz.InstallUnity
-{
+using static sttz.InstallUnity.UnityReleaseAPIClient;
-///
-/// Platforms supported by the cache.
-///
-public enum CachePlatform
+namespace sttz.InstallUnity
{
- None,
- macOSIntel,
- macOSArm,
- Windows,
- Linux,
-}
///
/// Information about a Unity version available to install.
@@ -27,392 +17,87 @@ public enum CachePlatform
public struct VersionMetadata
{
///
- /// Unity version.
+ /// Create a new version from a release.
///
- public UnityVersion version;
-
- ///
- /// Wether version was scraped from a beta/alpha page.
- ///
- ///
- /// Release candidates appear on the beta page but have versions that
- /// are indistinguishable from regular releases, we mark them here to
- /// distinguish between them.
- ///
- public bool prerelease;
-
- ///
- /// Returns wether the metadata represents a release candidate.
- /// (A final version published on the prerelease pages.)
- ///
- public bool IsReleaseCandidate {
- get {
- return prerelease && version.type == UnityVersion.Type.Final;
- }
+ public static VersionMetadata FromRelease(Release release)
+ {
+ return new VersionMetadata() { release = release };
}
///
- /// Returns wether the metadata represents a regular Unity release,
- /// excluding release candidates.
+ /// The release metadata, in the format of the Unity Release API.
///
- public bool IsFinalRelease {
- get {
- return version.type == UnityVersion.Type.Final && !prerelease;
- }
- }
+ public Release release;
///
- /// Returns wether the metadata represents a Unity prerelease,
- /// including alpha, beta and release candidates.
+ /// Shortcut to the Unity version of this release.
///
- public bool IsPrerelease {
- get {
- return version.type == UnityVersion.Type.Alpha
- || version.type == UnityVersion.Type.Beta
- || prerelease;
- }
- }
+ public UnityVersion Version => release?.version ?? default;
///
/// Base URL of where INIs are stored.
///
public string baseUrl;
- ///
- /// macOS packages.
- ///
- public PackageMetadata[] macPackages;
-
- ///
- /// macOS packages.
- ///
- public PackageMetadata[] macArmPackages;
-
- ///
- /// Windows packages.
- ///
- public PackageMetadata[] winPackages;
-
- ///
- /// Linux packages.
- ///
- public PackageMetadata[] linuxPackages;
-
- ///
- /// Virtual packages, generated dynamically for the current platform.
- ///
- [NonSerialized]
- public IEnumerable virtualPackages;
-
- ///
- /// Callback to add virtual packages.
- ///
- ///
- /// Don't call in the callback or you'll end up in
- /// an infinite recursion. Use instead.
- ///
- public static Func> OnGenerateVirtualPackages;
-
- ///
- /// Wrapper of that also checks that
- /// final versions don't match release candidates.
- ///
- public bool IsFuzzyMatchedBy(UnityVersion query)
- {
- if (query.type == UnityVersion.Type.Final && prerelease) {
- return false;
- }
-
- return query.FuzzyMatches(version);
- }
-
///
/// Determine wether the packages metadata has been loaded.
///
- public bool HasPackagesMetadata(CachePlatform platform)
+ public bool HasDownload(Platform platform, Architecture architecture)
{
- return GetRawPackages(platform) != null;
+ return GetEditorDownload(platform, architecture) != null;
}
///
/// Get platform specific packages without adding virtual packages.
///
- /// Platform to get.
- public IEnumerable GetRawPackages(CachePlatform platform)
+ public EditorDownload GetEditorDownload(Platform platform, Architecture architecture)
{
- switch (platform) {
- case CachePlatform.macOSIntel:
- return macPackages;
- case CachePlatform.macOSArm:
- return macArmPackages;
- case CachePlatform.Windows:
- return winPackages;
- case CachePlatform.Linux:
- return linuxPackages;
- default:
- throw new Exception("Invalid platform name: " + platform);
- }
- }
+ if (release.downloads == null)
+ return null;
- ///
- /// Get platform specific packages.
- ///
- /// Platform to get.
- public IEnumerable GetPackages(CachePlatform platform)
- {
- // Generate virtual packages
- if (virtualPackages == null) {
- if (OnGenerateVirtualPackages != null) {
- foreach (Func> func in OnGenerateVirtualPackages.GetInvocationList()) {
- var result = func(this, platform);
- if (result != null) {
- if (virtualPackages == null) {
- virtualPackages = result;
- } else {
- virtualPackages = virtualPackages.Concat(result);
- }
- }
- }
- }
- if (virtualPackages == null) {
- virtualPackages = Enumerable.Empty();
- }
+ foreach (var editor in release.downloads) {
+ if (editor.platform == platform && editor.architecture == architecture)
+ return editor;
}
- switch (platform) {
- case CachePlatform.macOSIntel:
- return macPackages.Concat(virtualPackages);
- case CachePlatform.macOSArm:
- return macArmPackages.Concat(virtualPackages);
- case CachePlatform.Windows:
- return winPackages.Concat(virtualPackages);
- case CachePlatform.Linux:
- return linuxPackages.Concat(virtualPackages);
- default:
- throw new Exception("Invalid platform name: " + platform);
- }
+ return null;
}
///
/// Set platform specific packages.
///
- /// Platform to set.
- public void SetPackages(CachePlatform platform, PackageMetadata[] packages)
+ public void SetEditorDownload(EditorDownload download)
{
- switch (platform) {
- case CachePlatform.macOSIntel:
- macPackages = packages;
- break;
- case CachePlatform.macOSArm:
- macArmPackages = packages;
- break;
- case CachePlatform.Windows:
- winPackages = packages;
- break;
- case CachePlatform.Linux:
- linuxPackages = packages;
- break;
- default:
- throw new Exception("Invalid platform name: " + platform);
- }
- }
-
- ///
- /// Find a package by name, ignoring case and excluding virtual packages.
- ///
- public PackageMetadata GetRawPackage(CachePlatform platform, string name)
- {
- var packages = GetRawPackages(platform);
- foreach (var package in packages) {
- if (package.name.Equals(name, StringComparison.OrdinalIgnoreCase)) {
- return package;
+ if (release.downloads == null)
+ release.downloads = new List();
+
+ for (int i = 0; i < release.downloads.Count; i++) {
+ var editor = release.downloads[i];
+ if (editor.platform == download.platform && editor.architecture == download.architecture) {
+ // Replace existing download
+ release.downloads[i] = download;
+ return;
}
}
- return default;
- }
- ///
- /// Find a package by name, ignoring case.
- ///
- public PackageMetadata GetPackage(CachePlatform platform, string name)
- {
- var packages = GetPackages(platform);
- foreach (var package in packages) {
- if (package.name.Equals(name, StringComparison.OrdinalIgnoreCase)) {
- return package;
- }
- }
- return default;
+ // Add new download
+ release.downloads.Add(download);
}
-}
-
-///
-/// Information about an version's individual package.
-///
-public struct PackageMetadata
-{
- ///
- /// Name of the main editor package.
- ///
- public const string EDITOR_PACKAGE_NAME = "Unity";
-
- ///
- /// Identifier of the package.
- ///
- public string name;
-
- ///
- /// Title of the package.
- ///
- public string title;
-
- ///
- /// Description of the package.
- ///
- public string description;
-
- ///
- /// Relative or absolute url to the package download.
- ///
- public string url;
-
- ///
- /// Wether the package is installed by default.
- ///
- public bool install;
-
- ///
- /// Wether the package is mandatory.
- ///
- public bool mandatory;
-
- ///
- /// The download size in bytes.
- ///
- public long size;
-
- ///
- /// The installed size in bytes.
- ///
- public long installedsize;
-
- ///
- /// The version of the package.
- ///
- public string version;
-
- ///
- /// File extension to use.
- ///
- public string extension;
-
- ///
- /// Wether the package is hidden.
- ///
- public bool hidden;
-
- ///
- /// Install this package together with another one.
- ///
- public string sync;
-
- ///
- /// The md5 hash of the package download.
- ///
- public string md5;
-
- ///
- /// Wether the package can be installed without the editor.
- ///
- public bool requires_unity;
-
- ///
- /// Bundle Identifier of app in package.
- ///
- public string appidentifier;
///
- /// Message for extra EULA terms.
+ /// Find a package by identifier, ignoring case.
///
- public string eulamessage;
-
- ///
- /// Label of first extra EULA.
- ///
- public string eulalabel1;
-
- ///
- /// URL of first extra EULA.
- ///
- public string eulaurl1;
-
- ///
- /// Label of second extra EULA.
- ///
- public string eulalabel2;
-
- ///
- /// URL of second extra EULA.
- ///
- public string eulaurl2;
-
- // -------- Fields used by virtual packages --------
-
- ///
- /// Where the archive should be extracted to (does not apply to installers).
- ///
- public string destination;
-
- ///
- /// Rename the extracted archive from this path.
- ///
- public string renameFrom;
-
- ///
- /// Rename the extracted archive to this path.
- ///
- public string renameTo;
-
- ///
- /// Target file name.
- ///
- public string fileName;
-
- ///
- /// Used to track automatically added dependencies.
- ///
- [NonSerialized] public bool addedAutomatically;
-
- ///
- /// Get the file name to use for the package.
- ///
- public string GetFileName()
+ public Module GetModule(Platform platform, Architecture architecture, string id)
{
- if (!string.IsNullOrEmpty(fileName)) {
- return fileName;
- }
-
- string guessedName;
-
- // Try to get file name from URL
- var uri = new Uri(url, UriKind.RelativeOrAbsolute);
- if (uri.IsAbsoluteUri) {
- guessedName = uri.Segments.Last();
- } else {
- guessedName = Path.GetFileName(url);
- }
+ var editor = GetEditorDownload(platform, architecture);
+ if (editor == null) return null;
- // Fallback to given extension if the url doesn't match
- if (extension != null
- && !string.Equals(Path.GetExtension(guessedName), "." + extension, StringComparison.OrdinalIgnoreCase)) {
- guessedName = name + "." + extension;
-
- // Force an extension for older versions that don't provide one
- } else if (Path.GetExtension(guessedName) == "") {
- guessedName = name + ".pkg";
+ foreach (var module in editor.modules) {
+ if (module.id.Equals(id, StringComparison.OrdinalIgnoreCase))
+ return module;
}
- return guessedName;
+ return null;
}
}
@@ -429,7 +114,7 @@ public class VersionsCache : IEnumerable
///
/// Version of cache format.
///
- const int CACHE_FORMAT = 2;
+ const int CACHE_FORMAT = 3;
///
/// Data written out to JSON file.
@@ -482,7 +167,7 @@ public VersionsCache(string dataFilePath)
///
void SortVersions()
{
- cache.versions.Sort((m1, m2) => m2.version.CompareTo(m1.version));
+ cache.versions.Sort((m1, m2) => m2.release.version.CompareTo(m1.release.version));
}
///
@@ -520,16 +205,16 @@ public void Clear()
public bool Add(VersionMetadata metadata)
{
for (int i = 0; i < cache.versions.Count; i++) {
- if (cache.versions[i].version == metadata.version) {
+ if (cache.versions[i].Version == metadata.Version) {
UpdateVersion(i, metadata);
- Logger.LogDebug($"Updated version in cache: {metadata.version}");
+ Logger.LogDebug($"Updated version in cache: {metadata.Version}");
return false;
}
}
cache.versions.Add(metadata);
SortVersions();
- Logger.LogDebug($"Added version to cache: {metadata.version}");
+ Logger.LogDebug($"Added version to cache: {metadata.Version}");
return true;
}
@@ -541,15 +226,15 @@ public void Add(IEnumerable metadatas, IList n
{
foreach (var metadata in metadatas) {
for (int i = 0; i < cache.versions.Count; i++) {
- if (cache.versions[i].version == metadata.version) {
+ if (cache.versions[i].Version == metadata.Version) {
UpdateVersion(i, metadata);
- Logger.LogDebug($"Updated version in cache: {metadata.version}");
+ Logger.LogDebug($"Updated version in cache: {metadata.Version}");
goto continueOuter;
}
}
cache.versions.Add(metadata);
if (newVersions != null) newVersions.Add(metadata);
- Logger.LogDebug($"Added version to cache: {metadata.version}");
+ Logger.LogDebug($"Added version to cache: {metadata.Version}");
continueOuter:;
}
@@ -562,11 +247,18 @@ public void Add(IEnumerable metadatas, IList n
void UpdateVersion(int index, VersionMetadata with)
{
var existing = cache.versions[index];
- existing.prerelease = with.prerelease;
- if (with.baseUrl != null) existing.baseUrl = with.baseUrl;
- if (with.macPackages != null) existing.macPackages = with.macPackages;
- if (with.winPackages != null) existing.macPackages = with.winPackages;
- if (with.linuxPackages != null) existing.macPackages = with.linuxPackages;
+
+ // Same release instance, nothing to update
+ if (existing.release == with.release)
+ return;
+
+ if (with.baseUrl != null) {
+ existing.baseUrl = with.baseUrl;
+ }
+ foreach (var editor in with.release.downloads) {
+ existing.SetEditorDownload(editor);
+ }
+
cache.versions[index] = existing;
}
@@ -581,7 +273,7 @@ public VersionMetadata Find(UnityVersion version)
if (version.IsFullVersion) {
// Do exact match
foreach (var metadata in cache.versions) {
- if (version.MatchesVersionOrHash(metadata.version)) {
+ if (version.MatchesVersionOrHash(metadata.Version)) {
return metadata;
}
}
@@ -590,7 +282,7 @@ public VersionMetadata Find(UnityVersion version)
// Do fuzzy match
foreach (var metadata in cache.versions) {
- if (metadata.IsFuzzyMatchedBy(version)) {
+ if (version.FuzzyMatches(metadata.Version)) {
return metadata;
}
}
diff --git a/sttz.InstallUnity/Installer/VirtualPackages.cs b/sttz.InstallUnity/Installer/VirtualPackages.cs
index 2180594..c1cfab7 100644
--- a/sttz.InstallUnity/Installer/VirtualPackages.cs
+++ b/sttz.InstallUnity/Installer/VirtualPackages.cs
@@ -2,6 +2,8 @@
using System.Linq;
using System;
+using static sttz.InstallUnity.UnityReleaseAPIClient;
+
namespace sttz.InstallUnity
{
@@ -10,37 +12,16 @@ namespace sttz.InstallUnity
///
public static class VirtualPackages
{
- ///
- /// Enable virtual packages.
- ///
- ///
- /// Packages will be injected into existing in
- /// their method.
- ///
- public static void Enable()
+ public static IEnumerable GeneratePackages(UnityVersion version, EditorDownload editor)
{
- VersionMetadata.OnGenerateVirtualPackages -= GeneratePackages;
- VersionMetadata.OnGenerateVirtualPackages += GeneratePackages;
- }
-
- ///
- /// Disable virtual packages. Virtual packages already generated will not be removed.
- ///
- public static void Disable()
- {
- VersionMetadata.OnGenerateVirtualPackages -= GeneratePackages;
- }
-
- static IEnumerable GeneratePackages(VersionMetadata version, CachePlatform platform)
- {
- return Generator(version, platform).ToList();
+ return Generator(version, editor).ToList();
}
static string[] Localizations_2018_1 = new string[] { "ja", "ko" };
static string[] Localizations_2018_2 = new string[] { "ja", "ko", "zh-cn" };
static string[] Localizations_2019_1 = new string[] { "ja", "ko", "zh-hans", "zh-hant" };
- static Dictionary LanguageNames = new Dictionary() {
+ static Dictionary LanguageNames = new Dictionary(StringComparer.OrdinalIgnoreCase) {
{ "ja", "日本語" },
{ "ko", "한국어" },
{ "zh-cn", "简体中文" },
@@ -48,22 +29,25 @@ static IEnumerable GeneratePackages(VersionMetadata version, Ca
{ "zh-hans", "简体中文" },
};
- static IEnumerable Generator(VersionMetadata version, CachePlatform platform)
+ static IEnumerable Generator(UnityVersion version, EditorDownload editor)
{
- var v = version.version;
+ var v = version;
+ var allPackages = editor.AllModules;
// Documentation
if (v.major >= 2018
- && version.GetRawPackage(platform, "Documentation").name == null
- && version.version.type != UnityVersion.Type.Alpha) {
- yield return new PackageMetadata() {
+ && !allPackages.ContainsKey("Documentation")
+ && v.type != UnityVersion.Type.Alpha) {
+ yield return new Module() {
+ id = "Documentation",
name = "Documentation",
description = "Offline Documentation",
url = $"https://storage.googleapis.com/docscloudstorage/{v.major}.{v.minor}/UnityDocumentation.zip",
- install = true,
+ type = FileType.ZIP,
+ preSelected = true,
destination = "{UNITY_PATH}",
- size = 350 * 1024 * 1024, // Conservative estimate based on 2019.2
- installedsize = 650 * 1024 * 1024, // "
+ downloadSize = FileSize.FromMegaBytes(350), // Conservative estimate based on 2019.2
+ installedSize = FileSize.FromMegaBytes(650), // "
};
}
@@ -79,164 +63,294 @@ static IEnumerable Generator(VersionMetadata version, CachePlat
}
foreach (var loc in localizations) {
- yield return new PackageMetadata() {
+ yield return new Module() {
+ id = LanguageNames[loc],
name = LanguageNames[loc],
description = $"{LanguageNames[loc]} Language Pack",
url = $"https://new-translate.unity3d.jp/v1/live/54/{v.major}.{v.minor}/{loc}",
- fileName = $"{loc}.po",
+ type = FileType.PO,
destination = "{UNITY_PATH}/Unity.app/Contents/Localization",
- size = 2 * 1024 * 1024, // Conservative estimate based on 2019.2
- installedsize = 2 * 1024 * 1024, // "
+ downloadSize = FileSize.FromMegaBytes(2), // Conservative estimate based on 2019.2
+ installedSize = FileSize.FromMegaBytes(2), // "
};
}
}
// Android dependencies
- if (v.major >= 2019 && version.GetRawPackage(platform, "Android").name != null) {
+ if (v.major >= 2019 && allPackages.ContainsKey("Android")) {
// Android SDK & NDK & stuff
- yield return new PackageMetadata() {
+ yield return new Module() {
+ id = "Android SDK & NDK Tools",
name = "Android SDK & NDK Tools",
description = "Android SDK & NDK Tools 26.1.1",
url = $"https://dl.google.com/android/repository/sdk-tools-darwin-4333796.zip",
destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK",
- size = 148 * 1024 * 1024,
- installedsize = 174 * 1024 * 1024,
- sync = "Android",
- eulaurl1 = "https://dl.google.com/dl/android/repository/repository2-1.xml",
- eulalabel1 = "Android SDK and NDK License Terms from Google",
- eulamessage = "Please review and accept the license terms before downloading and installing Android\'s SDK and NDK.",
+ downloadSize = FileSize.FromMegaBytes(148),
+ installedSize = FileSize.FromMegaBytes(174),
+ parentModuleId = "Android",
+ eula = new Eula[] {
+ new Eula() {
+ url = "https://dl.google.com/dl/android/repository/repository2-1.xml",
+ label = "Android SDK and NDK License Terms from Google",
+ message = "Please review and accept the license terms before downloading and installing Android\'s SDK and NDK.",
+ }
+ },
};
// Android platform tools
if (v.major < 2021) {
- yield return new PackageMetadata() {
+ yield return new Module() {
+ id = "Android SDK Platform Tools",
name = "Android SDK Platform Tools",
description = "Android SDK Platform Tools 28.0.1",
url = $"https://dl.google.com/android/repository/platform-tools_r28.0.1-darwin.zip",
destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK",
- size = 5 * 1024 * 1024,
- installedsize = 16 * 1024 * 1024,
+ downloadSize = FileSize.FromMegaBytes(5),
+ installedSize = FileSize.FromMegaBytes(16),
hidden = true,
- sync = "Android SDK & NDK Tools",
+ parentModuleId = "Android SDK & NDK Tools",
};
- } else {
- yield return new PackageMetadata() {
+ } else if (v.major <= 2022) {
+ yield return new Module() {
+ id = "Android SDK Platform Tools",
name = "Android SDK Platform Tools",
description = "Android SDK Platform Tools 30.0.4",
url = $"https://dl.google.com/android/repository/fbad467867e935dce68a0296b00e6d1e76f15b15.platform-tools_r30.0.4-darwin.zip",
destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK",
- size = 10 * 1024 * 1024,
- installedsize = 30 * 1024 * 1024,
+ downloadSize = FileSize.FromMegaBytes(10),
+ installedSize = FileSize.FromMegaBytes(30),
+ hidden = true,
+ parentModuleId = "Android SDK & NDK Tools",
+ };
+ } else {
+ yield return new Module() {
+ id = "Android SDK Platform Tools",
+ name = "Android SDK Platform Tools",
+ description = "Android SDK Platform Tools 32.0.0",
+ url = $"https://dl.google.com/android/repository/platform-tools_r32.0.0-darwin.zip",
+ destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK",
+ downloadSize = FileSize.FromBytes(18500000),
+ installedSize = FileSize.FromBytes(48684075),
hidden = true,
- sync = "Android SDK & NDK Tools",
+ parentModuleId = "Android SDK & NDK Tools"
};
}
// Android SDK platform & build tools
if (v.major == 2019 && v.minor <= 3) {
- yield return new PackageMetadata() {
+ yield return new Module() {
+ id = "Android SDK Build Tools",
name = "Android SDK Build Tools",
description = "Android SDK Build Tools 28.0.3",
url = $"https://dl.google.com/android/repository/build-tools_r28.0.3-macosx.zip",
destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/build-tools",
- size = 53 * 1024 * 1024,
- installedsize = 120 * 1024 * 1024,
+ downloadSize = FileSize.FromMegaBytes(53),
+ installedSize = FileSize.FromMegaBytes(120),
hidden = true,
- sync = "Android SDK & NDK Tools",
- renameFrom = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/build-tools/android-9",
- renameTo = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/build-tools/28.0.3"
+ parentModuleId = "Android SDK & NDK Tools",
+ extractedPathRename = new PathRename() {
+ from = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/build-tools/android-9",
+ to = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/build-tools/28.0.3"
+ },
};
- yield return new PackageMetadata() {
+ yield return new Module() {
+ id = "Android SDK Platforms",
name = "Android SDK Platforms",
description = "Android SDK Platforms 28 r06",
url = $"https://dl.google.com/android/repository/platform-28_r06.zip",
destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms",
- size = 61 * 1024 * 1024,
- installedsize = 121 * 1024 * 1024,
+ downloadSize = FileSize.FromMegaBytes(61),
+ installedSize = FileSize.FromMegaBytes(121),
hidden = true,
- sync = "Android SDK & NDK Tools",
- renameFrom = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms/android-9",
- renameTo = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms/android-28"
+ parentModuleId = "Android SDK & NDK Tools",
+ extractedPathRename = new PathRename() {
+ from = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms/android-9",
+ to = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms/android-28"
+ }
};
- } else {
- yield return new PackageMetadata() {
+ } else if (v.major <= 2022) {
+ yield return new Module() {
+ id = "Android SDK Build Tools",
name = "Android SDK Build Tools",
description = "Android SDK Build Tools 30.0.2",
url = $"https://dl.google.com/android/repository/5a6ceea22103d8dec989aefcef309949c0c42f1d.build-tools_r30.0.2-macosx.zip",
destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/build-tools",
- size = 49 * 1024 * 1024,
- installedsize = 129 * 1024 * 1024,
+ downloadSize = FileSize.FromMegaBytes(49),
+ installedSize = FileSize.FromMegaBytes(129),
hidden = true,
- sync = "Android SDK & NDK Tools",
- renameFrom = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/build-tools/android-11",
- renameTo = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/build-tools/30.0.2"
+ parentModuleId = "Android SDK & NDK Tools",
+ extractedPathRename = new PathRename() {
+ from = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/build-tools/android-11",
+ to = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/build-tools/30.0.2"
+ }
};
- yield return new PackageMetadata() {
+ yield return new Module() {
+ id = "Android SDK Platforms",
name = "Android SDK Platforms",
description = "Android SDK Platforms 30 r03",
url = $"https://dl.google.com/android/repository/platform-30_r03.zip",
destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms",
- size = 52 * 1024 * 1024,
- installedsize = 116 * 1024 * 1024,
+ downloadSize = FileSize.FromMegaBytes(52),
+ installedSize = FileSize.FromMegaBytes(116),
+ hidden = true,
+ parentModuleId = "Android SDK & NDK Tools",
+ extractedPathRename = new PathRename() {
+ from = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms/android-11",
+ to = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms/android-30"
+ }
+ };
+ } else {
+ yield return new Module() {
+ id = "Android SDK Build Tools",
+ name = "Android SDK Build Tools",
+ description = "Android SDK Build Tools 32.0.0",
+ url = $"https://dl.google.com/android/repository/5219cc671e844de73762e969ace287c29d2e14cd.build-tools_r32-macosx.zip",
+ destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/build-tools",
+ downloadSize = FileSize.FromBytes(50400000),
+ installedSize = FileSize.FromBytes(138655842),
+ hidden = true,
+ parentModuleId = "Android SDK & NDK Tools",
+ extractedPathRename = new PathRename() {
+ from = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/build-tools/android-12",
+ to = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/build-tools/32.0.0"
+ }
+ };
+ yield return new Module() {
+ id = "Android SDK Platforms",
+ name = "Android SDK Platforms",
+ description = "Android SDK Platforms 31",
+ url = $"https://dl.google.com/android/repository/platform-31_r01.zip",
+ destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms",
+ downloadSize = FileSize.FromBytes(53900000),
+ installedSize = FileSize.FromBytes(91868884),
+ hidden = true,
+ parentModuleId = "Android SDK & NDK Tools",
+ extractedPathRename = new PathRename() {
+ from = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms/android-12",
+ to = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms/android-31"
+ }
+ };
+ yield return new Module() {
+ id = "Android SDK Platforms",
+ name = "Android SDK Platforms",
+ description = "Android SDK Platforms 32",
+ url = $"https://dl.google.com/android/repository/platform-32_r01.zip",
+ destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms",
+ downloadSize = FileSize.FromBytes(63000000),
+ installedSize = FileSize.FromBytes(101630444),
hidden = true,
- sync = "Android SDK & NDK Tools",
- renameFrom = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms/android-11",
- renameTo = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms/android-30"
+ parentModuleId = "Android SDK & NDK Tools",
+ extractedPathRename = new PathRename() {
+ from = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms/android-12",
+ to = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/platforms/android-32"
+ }
+ };
+ yield return new Module() {
+ id = "Android SDK Command Line Tools",
+ name = "Android SDK Command Line Tools",
+ description = "Android SDK Command Line Tools 6.0",
+ url = $"https://dl.google.com/android/repository/commandlinetools-mac-8092744_latest.zip",
+ destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/cmdline-tools",
+ downloadSize = FileSize.FromBytes(119650616),
+ installedSize = FileSize.FromBytes(119651596),
+ hidden = true,
+ parentModuleId = "Android SDK & NDK Tools",
+ extractedPathRename = new PathRename() {
+ from = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/cmdline-tools/cmdline-tools",
+ to = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/SDK/cmdline-tools/6.0"
+ }
};
}
// Android NDK
if (v.major == 2019 && v.minor <= 2) {
- yield return new PackageMetadata() {
+ yield return new Module() {
+ id = "Android NDK 16b",
name = "Android NDK 16b",
description = "Android NDK r16b",
url = $"https://dl.google.com/android/repository/android-ndk-r16b-darwin-x86_64.zip",
destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer",
- size = 770 * 1024 * 1024,
- installedsize = 2700L * 1024 * 1024,
+ downloadSize = FileSize.FromMegaBytes(770),
+ installedSize = FileSize.FromMegaBytes(2700),
hidden = true,
- sync = "Android SDK & NDK Tools",
- renameFrom = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/android-ndk-r16b",
- renameTo = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/NDK"
+ parentModuleId = "Android SDK & NDK Tools",
+ extractedPathRename = new PathRename() {
+ from = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/android-ndk-r16b",
+ to = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/NDK"
+ }
};
} else if (v.major <= 2020) {
- yield return new PackageMetadata() {
+ yield return new Module() {
+ id = "Android NDK 19",
name = "Android NDK 19",
description = "Android NDK r19",
url = $"https://dl.google.com/android/repository/android-ndk-r19-darwin-x86_64.zip",
destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer",
- size = 770 * 1024 * 1024,
- installedsize = 2700L * 1024 * 1024,
+ downloadSize = FileSize.FromMegaBytes(770),
+ installedSize = FileSize.FromMegaBytes(2700),
hidden = true,
- sync = "Android SDK & NDK Tools",
- renameFrom = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/android-ndk-r19",
- renameTo = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/NDK"
+ parentModuleId = "Android SDK & NDK Tools",
+ extractedPathRename = new PathRename() {
+ from = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/android-ndk-r19",
+ to = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/NDK"
+ }
};
- } else {
- yield return new PackageMetadata() {
+ } else if (v.major <= 2022) {
+ yield return new Module() {
+ id = "Android NDK 21d",
name = "Android NDK 21d",
description = "Android NDK r21d",
url = $"https://dl.google.com/android/repository/android-ndk-r21d-darwin-x86_64.zip",
destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer",
- size = 1065 * 1024 * 1024,
- installedsize = 3922L * 1024 * 1024,
+ downloadSize = FileSize.FromMegaBytes(1065),
+ installedSize = FileSize.FromMegaBytes(3922),
hidden = true,
- sync = "Android SDK & NDK Tools",
- renameFrom = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/android-ndk-r21d",
- renameTo = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/NDK"
+ parentModuleId = "Android SDK & NDK Tools",
+ extractedPathRename = new PathRename() {
+ from = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/android-ndk-r21d",
+ to = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/NDK"
+ }
+ };
+ } else {
+ yield return new Module() {
+ id = "Android NDK 23b",
+ name = "Android NDK 23b",
+ description = "Android NDK r23b",
+ url = $"https://dl.google.com/android/repository/android-ndk-r23b-darwin.dmg",
+ destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/NDK",
+ downloadSize = FileSize.FromBytes(1400000000),
+ installedSize = FileSize.FromBytes(4254572698),
+ hidden = true,
+ parentModuleId = "Android SDK & NDK Tools",
+ extractedPathRename = new PathRename() {
+ from = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/NDK/Contents/NDK",
+ to = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/NDK"
+ }
};
}
// Android JDK
- if (v.major > 2019 || v.minor >= 2) {
- yield return new PackageMetadata() {
+ if (v.major >= 2023) {
+ yield return new Module() {
+ id = "OpenJDK",
+ name = "OpenJDK",
+ description = "Android Open JDK 11.0.14.1+1",
+ url = $"https://download.unity3d.com/download_unity/open-jdk/open-jdk-mac-x64/jdk11.0.14.1-1_236fc2e31a8b6da32fbcf8624815f509c51605580cb2c6285e55510362f272f8.zip",
+ destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/OpenJDK",
+ downloadSize = FileSize.FromBytes(118453231),
+ installedSize = FileSize.FromBytes(230230237),
+ parentModuleId = "Android",
+ };
+ } else if (v.major > 2019 || v.minor >= 2) {
+ yield return new Module() {
+ id = "OpenJDK",
name = "OpenJDK",
description = "Android Open JDK 8u172-b11",
url = $"http://download.unity3d.com/download_unity/open-jdk/open-jdk-mac-x64/jdk8u172-b11_4be8440cc514099cfe1b50cbc74128f6955cd90fd5afe15ea7be60f832de67b4.zip",
destination = "{UNITY_PATH}/PlaybackEngines/AndroidPlayer/OpenJDK",
- size = 73 * 1024 * 1024,
- installedsize = 165 * 1024 * 1024,
- sync = "Android",
+ downloadSize = FileSize.FromMegaBytes(73),
+ installedSize = FileSize.FromMegaBytes(165),
+ parentModuleId = "Android",
};
}
}
diff --git a/sttz.InstallUnity/sttz.InstallUnity.csproj b/sttz.InstallUnity/sttz.InstallUnity.csproj
index 959ac2b..57f618a 100644
--- a/sttz.InstallUnity/sttz.InstallUnity.csproj
+++ b/sttz.InstallUnity/sttz.InstallUnity.csproj
@@ -1,13 +1,14 @@
- net6.0
+ net7.0
+ win-x64;osx-x64
latest
sttz.InstallUnity
- 2.11.0
+ 2.12.0
Adrian Stutz (sttz.ch)
install-unity
install-unity unofficial Unity installer library
@@ -22,7 +23,8 @@
-
+
+