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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions GodotEnv/src/features/godot/commands/GodotListCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ out var failedInstallations
var activeTag = installation.IsActiveVersion ? " *" : "";
log.Print(godotRepo.InstallationVersionName(installation) + activeTag);
}
// TODO - Recognize Custom Builds
foreach (var unrecognized in unrecognizedDirectories) {
log.Warn("Unrecognized subfolder in Godot installation directory (may be a non-conforming version identifier):");
log.Warn($" {unrecognized}");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
namespace Chickensoft.GodotEnv.Features.Godot.Commands;

using System.Threading.Tasks;
using Chickensoft.GodotEnv.Common.Models;
using Chickensoft.GodotEnv.Features.Godot.Models;
using CliFx;
using CliFx.Attributes;
using CliFx.Infrastructure;
using CliWrap;

// FIX - dependent files not working (e.g. steam_api64.dll)
[Command("godot register", Description = "Register a version of Godot.")]
public class GodotRegisterCommand :
ICommand, ICliCommand, IWindowsElevationEnabled {
[CommandParameter(
0,
Name = "Name",
Validators = [typeof(GodotVersionValidator)],
Description = "Godot version name: e.g., 4.1.0.CustomBuild, 4.2.0.GodotSteam, etc." +
" Should be a unique Godot version name"
)]
public string VersionName { get; set; } = default!;

[CommandParameter(
1,
Name = "Path",
Description = "Godot version path: e.g. /user/godot/version/path/"
)]
public string VersionPath { get; set; } = default!;

[CommandParameter(
2,
Name = "Executable Path",
Description = "Godot executable name: e.g. \"godot-steam-4.3/godotsteam.multiplayer.43.editor.windows.64.exe\""
)]
public string ExecutablePath { get; set; } = default!;

[CommandOption(
"no-dotnet", 'n',
Description =
"Specify to use the version of Godot that does not support C#/.NET."
)]
public bool NoDotnet { get; set; }

public IExecutionContext ExecutionContext { get; set; } = default!;

public bool IsWindowsElevationRequired => true;

public GodotRegisterCommand(IExecutionContext context) {
ExecutionContext = context;
}

public async ValueTask ExecuteAsync(IConsole console) {
var godotRepo = ExecutionContext.Godot.GodotRepo;
var platform = ExecutionContext.Godot.Platform;

var log = ExecutionContext.CreateLog(console);

// We know this won't throw because the validator okayed it
var version = godotRepo.VersionStringConverter.ParseVersion(VersionName);
var isDotnetVersion = !NoDotnet;

// TODO - Check if are necessary
var godotInstallationsPath = godotRepo.GodotInstallationsPath;
var godotCachePath = godotRepo.GodotCachePath;

var existingInstallation =
godotRepo.GetInstallation(version, isDotnetVersion);

// Log information to show we understood.
platform.Describe(log);
log.Info($"🤖 Godot v{VersionName}");
log.Info($"🍯 Parsed version: {version}");
log.Info(
isDotnetVersion ? "😁 Using Godot with .NET" : "😢 Using Godot without .NET"
);

// Check for existing installation.
if (existingInstallation is GodotInstallation installation) {
log.Warn(
$"🤔 Godot v{VersionName} is already installed:"
);
log.Warn(installation);
}

var newInstallation =
await godotRepo.ExtractGodotCustomBuild(VersionPath, ExecutablePath, version, isDotnetVersion, log);

await godotRepo.UpdateGodotSymlink(newInstallation, log);

await godotRepo.UpdateDesktopShortcut(newInstallation, log);

await godotRepo.AddOrUpdateGodotEnvVariable(log);
}
}
62 changes: 60 additions & 2 deletions GodotEnv/src/features/godot/domain/GodotRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ Task<GodotInstallation> ExtractGodotInstaller(
GodotCompressedArchive archive, ILog log
);

/// <summary>
/// Move the Godot custom build files into the correct directory.
/// </summary>
/// <param name="archivePath">Godot installation path.</param>
/// <param name="version">Godot version.</param>
/// <param name="isDotnetVersion">If the Godot build is a Dotnet build.</param>
/// <param name="log">Output log.</param>
/// <returns>Path to the subfolder in the Godot installations directory
/// containing the extracted contents.</returns>
Task<GodotInstallation> ExtractGodotCustomBuild(
string archivePath, string executableName, GodotVersion version, bool isDotnetVersion, ILog log
);

/// <summary>
/// Updates the symlink to point to the specified Godot installation.
/// </summary>
Expand Down Expand Up @@ -407,6 +420,49 @@ ILog log
);
}

public async Task<GodotInstallation> ExtractGodotCustomBuild(
string archivePath,
string executableName,
GodotVersion version,
bool isDotnetVersion,
ILog log
) {
var versionString = VersionStringConverter.VersionString(version);

var destinationDirName =
FileClient.Combine(GodotInstallationsPath, versionString);

var numFilesExtracted = await ZipClient.ExtractToDirectory(
archivePath,
destinationDirName,
new Progress<double>((percent) => {
var p = Math.Round(percent * 100);
log.InfoInPlace($"🗜 Extracting Godot: {p}%" + " ");
})
);
log.Print(""); // New line after progress.
log.ClearCurrentLine();
log.Print($" Destination: {destinationDirName}");
log.Success($"✅ Extracted {numFilesExtracted} file(s).");
log.Print("");

var execPath = GetExecutionPath(
installationPath: destinationDirName,
version: version,
isDotnetVersion: isDotnetVersion,
executableName
);

return new GodotInstallation(
Name: versionString,
IsActiveVersion: true, // we always switch to the newly installed version.
Version: version,
IsDotnetVersion: isDotnetVersion,
Path: destinationDirName,
ExecutionPath: execPath
);
}

public async Task UpdateGodotSymlink(
GodotInstallation installation, ILog log
) {
Expand Down Expand Up @@ -568,6 +624,8 @@ out List<string> failedGodotInstallations
foreach (var dir in FileClient.GetSubdirectories(GodotInstallationsPath)) {
DirectoryToVersion(dir.Name, out var version, out var isDotnetVersion);
if (version is null) {
// TODO - Add a godotenv.json to custom builds directories
// so we can identify it's version and dependencies?
unrecognizedDirectories.Add(dir.Name);
}
else {
Expand Down Expand Up @@ -647,11 +705,11 @@ public async Task<bool> Uninstall(
}

private string GetExecutionPath(
string installationPath, GodotVersion version, bool isDotnetVersion
string installationPath, GodotVersion version, bool isDotnetVersion, string? customExecutablePath = null
) =>
FileClient.Combine(
installationPath,
Platform.GetRelativeExtractedExecutablePath(version, isDotnetVersion)
customExecutablePath ?? Platform.GetRelativeExtractedExecutablePath(version, isDotnetVersion)
);

private string GetGodotSharpPath(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
namespace Chickensoft.GodotEnv.Features.Godot.Models;

using System;
using System.Text.RegularExpressions;

public partial class CustomBuildVersionStringConverter : IVersionStringConverter {
public GodotVersion ParseVersion(string version) {
var match = VersionStringRegex().Match(version);
if (!match.Success) {
throw new ArgumentException(
$"Couldn't match \"{version}\" to known Godot custom build version patterns."
);
}
// we can safely convert major and minor, since the regex only matches
// digit characters
var major = int.Parse(match.Groups[1].Value);
var minor = int.Parse(match.Groups[2].Value);
// patch string is optional "-\d+", so we can safely convert it after
// the first character if it has length
var patch = 0;
var patchStr = match.Groups[3].Value;
if (patchStr.Length > 0) {
patch = int.Parse(patchStr[1..]);
}

var label = match.Groups[4].Value;
var labelNum = -1;
if (label != "stable") {
label = match.Groups[5].Value;
var buildVersion = match.Groups[6].Value;

// Verify if group 6 exists
if (buildVersion != "") {
// If yes, get build version at group 8
labelNum = int.Parse(match.Groups[8].Value);
}

}
return new GodotVersion(major, minor, patch, label, labelNum, true);
}

public string VersionString(GodotVersion version) {
var result = $"{version.Major}.{version.Minor}";
if (version.Patch != 0) {
result += $".{version.Patch}";
}
result += $"-{LabelString(version)}";
return result;
}

public string LabelString(GodotVersion version) {
var result = version.Label;
if (result != "stable") {
result += version.LabelNumber;
}
return result;
}

// All published Godot 4+ packages have a label ("-stable" if not prerelease)
// "-stable" labels do not have a number, others do
// Versions with a patch number of 0 do not have a patch number
[GeneratedRegex(@"^(\d+)\.(\d+)(\.[1-9]\d*)?-(stable|([a-zA-Z]+)((\.)?\d+)?)$")]
public static partial Regex VersionStringRegex();
}
4 changes: 2 additions & 2 deletions GodotEnv/src/features/godot/models/GodotVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public partial record GodotVersion {
public string Label { get; }
public int LabelNumber { get; }

internal GodotVersion(int major, int minor, int patch, string label, int labelNumber) {
internal GodotVersion(int major, int minor, int patch, string label, int labelNumber, bool isCustomBuild = false) {
if (major < 0) {
throw new ArgumentException($"Major version {major} is invalid");
}
Expand All @@ -25,7 +25,7 @@ internal GodotVersion(int major, int minor, int patch, string label, int labelNu
if (char.IsDigit(label[^1])) {
throw new ArgumentException("Label \"{label}\" ambiguously ends with number");
}
if (label != "stable" && labelNumber <= 0) {
if (label != "stable" && !isCustomBuild && labelNumber <= 0) {
throw new ArgumentException($"Version label \"{label}\" with numeric identifier {labelNumber} is invalid");
}
if (label == "stable" && labelNumber >= 0) {
Expand Down
11 changes: 10 additions & 1 deletion GodotEnv/src/features/godot/models/IOVersionStringConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ namespace Chickensoft.GodotEnv.Features.Godot.Models;
public partial class IOVersionStringConverter : IVersionStringConverter {
private readonly ReleaseVersionStringConverter _releaseConverter = new();
private readonly SharpVersionStringConverter _sharpConverter = new();
private readonly CustomBuildVersionStringConverter _customBuildConverter = new();

public GodotVersion ParseVersion(string version) {
var trimmedVersion = version.TrimStart('v');
Exception releaseEx = null!;
Exception sharpEx = null!;

try {
return _releaseConverter.ParseVersion(trimmedVersion);
}
Expand All @@ -19,7 +22,13 @@ public GodotVersion ParseVersion(string version) {
return _sharpConverter.ParseVersion(trimmedVersion);
}
catch (Exception ex) {
throw new ArgumentException($"Version string {version} is neither release style ({releaseEx.Message}) nor GodotSharp style ({ex.Message})");
sharpEx = ex;
}
try {
return _customBuildConverter.ParseVersion(trimmedVersion);
}
catch (Exception ex) {
throw new ArgumentException($"Version string {version} is neither release style ({releaseEx.Message}), GodotSharp style ({sharpEx.Message}) nor custom build style ({ex.Message})");
}
}

Expand Down