From c5043de6941c28bbd9124dbbbbfe58bcaf41c1a0 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sat, 15 Jan 2022 23:45:29 -0600 Subject: [PATCH 01/91] Move publish models to their own namespace --- ThunderstoreCLI/API/ApiHelper.cs | 2 +- ThunderstoreCLI/Commands/PublishCommand.cs | 2 +- ThunderstoreCLI/Config/Config.cs | 2 +- ThunderstoreCLI/Models/PublishModels.cs | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ThunderstoreCLI/API/ApiHelper.cs b/ThunderstoreCLI/API/ApiHelper.cs index 6b272cb..731670b 100644 --- a/ThunderstoreCLI/API/ApiHelper.cs +++ b/ThunderstoreCLI/API/ApiHelper.cs @@ -1,6 +1,6 @@ using System.Net.Http.Headers; using System.Text; -using ThunderstoreCLI.Models; +using ThunderstoreCLI.Models.Publish; using ThunderstoreCLI.Utils; namespace ThunderstoreCLI.API; diff --git a/ThunderstoreCLI/Commands/PublishCommand.cs b/ThunderstoreCLI/Commands/PublishCommand.cs index 810b8c5..b3d8381 100644 --- a/ThunderstoreCLI/Commands/PublishCommand.cs +++ b/ThunderstoreCLI/Commands/PublishCommand.cs @@ -1,7 +1,7 @@ using System.Net; using System.Security.Cryptography; using System.Text; -using ThunderstoreCLI.Models; +using ThunderstoreCLI.Models.Publish; using static Crayon.Output; namespace ThunderstoreCLI.Commands; diff --git a/ThunderstoreCLI/Config/Config.cs b/ThunderstoreCLI/Config/Config.cs index 73d7203..ddd3d22 100644 --- a/ThunderstoreCLI/Config/Config.cs +++ b/ThunderstoreCLI/Config/Config.cs @@ -1,6 +1,6 @@ using System.Net.Http.Headers; using ThunderstoreCLI.API; -using ThunderstoreCLI.Models; +using ThunderstoreCLI.Models.Publish; namespace ThunderstoreCLI.Config; diff --git a/ThunderstoreCLI/Models/PublishModels.cs b/ThunderstoreCLI/Models/PublishModels.cs index 9da5d97..e7070e5 100644 --- a/ThunderstoreCLI/Models/PublishModels.cs +++ b/ThunderstoreCLI/Models/PublishModels.cs @@ -1,9 +1,9 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -namespace ThunderstoreCLI.Models; +namespace ThunderstoreCLI.Models.Publish; -[ExcludeFromCodeCoverageAttribute] +[ExcludeFromCodeCoverage] public class PackageUploadMetadata : BaseJson { [JsonPropertyName("author_name")] @@ -23,10 +23,10 @@ public class PackageUploadMetadata : BaseJson { public class UserMediaData @@ -76,7 +76,7 @@ public class UploadPartData [ExcludeFromCodeCoverage] public partial class UploadInitiateDataContext : JsonSerializerContext { } -[ExcludeFromCodeCoverageAttribute] +[ExcludeFromCodeCoverage] public class FileData : BaseJson { [JsonPropertyName("filename")] @@ -90,7 +90,7 @@ public class FileData : BaseJson [ExcludeFromCodeCoverage] public partial class FileDataContext : JsonSerializerContext { } -[ExcludeFromCodeCoverageAttribute] +[ExcludeFromCodeCoverage] public class CompletedUpload : BaseJson { public class CompletedPartData @@ -110,7 +110,7 @@ public class CompletedPartData public partial class CompletedUploadContext : JsonSerializerContext { } // JSON response structure for publish package request. -[ExcludeFromCodeCoverageAttribute] +[ExcludeFromCodeCoverage] public class PublishData : BaseJson { public class AvailableCommunityData From e6afd0d7fada6dfeb7710662b90eac8f0f98558a Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sun, 16 Jan 2022 03:22:41 -0600 Subject: [PATCH 02/91] Refactor Program.Main --- ThunderstoreCLI/Config/Config.cs | 10 +- .../Models/Interaction/BaseInteraction.cs | 39 ++++++++ ThunderstoreCLI/Options.cs | 44 ++++++++- ThunderstoreCLI/Program.cs | 96 +++---------------- ThunderstoreCLI/UpdateChecker.cs | 47 +++++++++ 5 files changed, 147 insertions(+), 89 deletions(-) create mode 100644 ThunderstoreCLI/Models/Interaction/BaseInteraction.cs create mode 100644 ThunderstoreCLI/UpdateChecker.cs diff --git a/ThunderstoreCLI/Config/Config.cs b/ThunderstoreCLI/Config/Config.cs index ddd3d22..28a675f 100644 --- a/ThunderstoreCLI/Config/Config.cs +++ b/ThunderstoreCLI/Config/Config.cs @@ -1,4 +1,3 @@ -using System.Net.Http.Headers; using ThunderstoreCLI.API; using ThunderstoreCLI.Models.Publish; @@ -28,6 +27,15 @@ private Config(GeneralConfig generalConfig, PackageMeta packageMeta, InitConfig PublishConfig = publishConfig; AuthConfig = authConfig; } + public static Config FromCLI(IConfigProvider cliConfig) + { + return Parse( + cliConfig, + new EnvironmentConfig(), + new ProjectFileConfig(), + new BaseConfig() + ); + } public string? GetProjectBasePath() { diff --git a/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs b/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs new file mode 100644 index 0000000..53e6d8f --- /dev/null +++ b/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace ThunderstoreCLI.Models.Interaction; + +public enum InteractionOutputType +{ + HUMAN, + JSON, +} + +public static class InteractionOptions +{ + public static InteractionOutputType OutputType { get; set; } = InteractionOutputType.HUMAN; +} + +public abstract class BaseInteraction : BaseJson + where T : BaseInteraction + where Context : JsonSerializerContext +{ + public abstract string GetHumanString(); + + public string GetString() + { + switch (InteractionOptions.OutputType) + { + case InteractionOutputType.HUMAN: + return GetHumanString(); + case InteractionOutputType.JSON: + return Serialize(); + default: + throw new NotSupportedException(); + } + } + + public void Write() + { + Console.WriteLine(GetString()); + } +} diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index ad6b435..6487b7d 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -1,10 +1,12 @@ using CommandLine; +using ThunderstoreCLI.Commands; +using ThunderstoreCLI.Models.Interaction; using static Crayon.Output; -/// Options are arguments passed from command line. namespace ThunderstoreCLI.Options; -public class PackageOptions +/// Options are arguments passed from command line. +public abstract class PackageOptions { [Option("config-path", Required = false, Default = Defaults.PROJECT_CONFIG_PATH, HelpText = "Path for the project configuration file")] public string? ConfigPath { get; set; } @@ -18,6 +20,15 @@ public class PackageOptions [Option("package-version", SetName = "build", Required = false, HelpText = "Version number for the package")] public string? VersionNumber { get; set; } + [Option("output", Required = false, HelpText = "The output format for all output. Valid options are HUMAN and JSON.")] + public InteractionOutputType? OutputType { get; set; } + + public virtual void Init() + { + if (OutputType.HasValue) + InteractionOptions.OutputType = OutputType.Value; + } + public virtual bool Validate() { if (String.IsNullOrWhiteSpace(ConfigPath)) @@ -40,6 +51,8 @@ public virtual bool Validate() return true; } + + public abstract int Execute(); } [Verb("init", HelpText = "Initialize a new project configuration")] @@ -49,10 +62,27 @@ public class InitOptions : PackageOptions [Option(OVERWRITE_FLAG, Required = false, Default = false, HelpText = "If present, overwrite current configuration")] public bool Overwrite { get; set; } + + public override int Execute() + { + var updateChecker = UpdateChecker.CheckForUpdates(); + var exitCode = InitCommand.Run(Config.Config.FromCLI(new Config.CLIInitCommandConfig(this))); + UpdateChecker.WriteUpdateNotification(updateChecker); + return exitCode; + } } [Verb("build", HelpText = "Build a package")] -public class BuildOptions : PackageOptions { } +public class BuildOptions : PackageOptions +{ + public override int Execute() + { + var updateChecker = UpdateChecker.CheckForUpdates(); + var exitCode = BuildCommand.Run(Config.Config.FromCLI(new Config.CLIBuildCommandConfig(this))); + UpdateChecker.WriteUpdateNotification(updateChecker); + return exitCode; + } +} [Verb("publish", HelpText = "Publish a package. By default will also build a new package.")] public class PublishOptions : PackageOptions @@ -88,4 +118,12 @@ public override bool Validate() return true; } + + public override int Execute() + { + var updateChecker = UpdateChecker.CheckForUpdates(); + var exitCode = PublishCommand.Run(Config.Config.FromCLI(new Config.CLIPublishCommandConfig(this))); + UpdateChecker.WriteUpdateNotification(updateChecker); + return exitCode; + } } diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index 1c0b6ec..0907c66 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -4,99 +4,25 @@ namespace ThunderstoreCLI; -class Program +internal static class Program { - static int Main(string[] args) + private static int Main(string[] args) { return Parser.Default.ParseArguments(args) .MapResult( - (InitOptions o) => o.Validate() ? Init(o) : 1, - (BuildOptions o) => o.Validate() ? Build(o) : 1, - (PublishOptions o) => o.Validate() ? Publish(o) : 1, - errs => HandleError(errs) + (InitOptions o) => HandleParsed(o), + (BuildOptions o) => HandleParsed(o), + (PublishOptions o) => HandleParsed(o), + _ => 1 // failure to parse ); } - static Config.Config GetConfig(Config.IConfigProvider cliConfig) + private static int HandleParsed(PackageOptions parsed) { - return Config.Config.Parse( - cliConfig, - new Config.EnvironmentConfig(), - new Config.ProjectFileConfig(), - new Config.BaseConfig() - ); - } - - static int HandleError(IEnumerable errors) - { - return 1; - } - - static int Init(InitOptions options) - { - var updateChecker = CheckForUpdates(); - var exitCode = InitCommand.Run(GetConfig(new Config.CLIInitCommandConfig(options))); - WriteUpdateNotification(updateChecker); - return exitCode; - } - - static int Build(BuildOptions options) - { - var updateChecker = CheckForUpdates(); - var exitCode = BuildCommand.Run(GetConfig(new Config.CLIBuildCommandConfig(options))); - WriteUpdateNotification(updateChecker); - return exitCode; - } - - static int Publish(PublishOptions options) - { - var updateChecker = CheckForUpdates(); - var exitCode = PublishCommand.Run(GetConfig(new Config.CLIPublishCommandConfig(options))); - WriteUpdateNotification(updateChecker); - return exitCode; - } - - private static async Task CheckForUpdates() - { - var current = MiscUtils.GetCurrentVersion(); - int[] latest; - - try - { - var responseContent = await MiscUtils.FetchReleaseInformation(); - latest = MiscUtils.ParseLatestVersion(responseContent); - } - catch (Exception) - { - return ""; - } - - if ( - latest[0] > current[0] || - (latest[0] == current[0] && latest[1] > current[1]) || - (latest[0] == current[0] && latest[1] == current[1] && latest[2] > current[2]) - ) - { - var version = $"{latest[0]}.{latest[1]}.{latest[2]}"; - return $"Newer version {version} of Thunderstore CLI is available"; - } - - return ""; - } - - private static void WriteUpdateNotification(Task checkTask) - { - if (!checkTask.IsCompleted) - { - return; - } - - var notification = checkTask.GetAwaiter().GetResult(); - - if (notification != "") - { - Write.Note(notification); - } + parsed.Init(); + if (!parsed.Validate()) + return 1; + return parsed.Execute(); } } diff --git a/ThunderstoreCLI/UpdateChecker.cs b/ThunderstoreCLI/UpdateChecker.cs new file mode 100644 index 0000000..b5f1a01 --- /dev/null +++ b/ThunderstoreCLI/UpdateChecker.cs @@ -0,0 +1,47 @@ +namespace ThunderstoreCLI; + +public static class UpdateChecker +{ + public static async Task CheckForUpdates() + { + var current = MiscUtils.GetCurrentVersion(); + int[] latest; + + try + { + var responseContent = await MiscUtils.FetchReleaseInformation(); + latest = MiscUtils.ParseLatestVersion(responseContent); + } + catch (Exception) + { + return ""; + } + + if ( + latest[0] > current[0] || + (latest[0] == current[0] && latest[1] > current[1]) || + (latest[0] == current[0] && latest[1] == current[1] && latest[2] > current[2]) + ) + { + var version = $"{latest[0]}.{latest[1]}.{latest[2]}"; + return $"Newer version {version} of Thunderstore CLI is available"; + } + + return ""; + } + + public static void WriteUpdateNotification(Task checkTask) + { + if (!checkTask.IsCompleted) + { + return; + } + + var notification = checkTask.GetAwaiter().GetResult(); + + if (notification != "") + { + Write.Note(notification); + } + } +} From 814779e91160a6aa07552ad11e43e22477624f98 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 31 Jan 2022 22:11:46 -0600 Subject: [PATCH 03/91] Add TCLI_WAIT_DEBUGGER and move update check to Main --- ThunderstoreCLI/Options.cs | 15 +++------------ ThunderstoreCLI/Program.cs | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index 6487b7d..c641c65 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -65,10 +65,7 @@ public class InitOptions : PackageOptions public override int Execute() { - var updateChecker = UpdateChecker.CheckForUpdates(); - var exitCode = InitCommand.Run(Config.Config.FromCLI(new Config.CLIInitCommandConfig(this))); - UpdateChecker.WriteUpdateNotification(updateChecker); - return exitCode; + return InitCommand.Run(Config.Config.FromCLI(new Config.CLIInitCommandConfig(this))); } } @@ -77,10 +74,7 @@ public class BuildOptions : PackageOptions { public override int Execute() { - var updateChecker = UpdateChecker.CheckForUpdates(); - var exitCode = BuildCommand.Run(Config.Config.FromCLI(new Config.CLIBuildCommandConfig(this))); - UpdateChecker.WriteUpdateNotification(updateChecker); - return exitCode; + return BuildCommand.Run(Config.Config.FromCLI(new Config.CLIBuildCommandConfig(this))); } } @@ -121,9 +115,6 @@ public override bool Validate() public override int Execute() { - var updateChecker = UpdateChecker.CheckForUpdates(); - var exitCode = PublishCommand.Run(Config.Config.FromCLI(new Config.CLIPublishCommandConfig(this))); - UpdateChecker.WriteUpdateNotification(updateChecker); - return exitCode; + return PublishCommand.Run(Config.Config.FromCLI(new Config.CLIPublishCommandConfig(this))); } } diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index 0907c66..31ebf1d 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -1,5 +1,5 @@ +using System.Diagnostics; using CommandLine; -using ThunderstoreCLI.Commands; using ThunderstoreCLI.Options; namespace ThunderstoreCLI; @@ -8,13 +8,23 @@ internal static class Program { private static int Main(string[] args) { - return Parser.Default.ParseArguments(args) +#if DEBUG + if (Environment.GetEnvironmentVariable("TCLI_WAIT_DEBUGGER") is not null) + while (!Debugger.IsAttached) + { } +#endif + + var updateChecker = UpdateChecker.CheckForUpdates(); + var exitCode = Parser.Default.ParseArguments(args) .MapResult( (InitOptions o) => HandleParsed(o), (BuildOptions o) => HandleParsed(o), (PublishOptions o) => HandleParsed(o), + (InstallOptions o) => HandleParsed(o), _ => 1 // failure to parse ); + UpdateChecker.WriteUpdateNotification(updateChecker); + return exitCode; } private static int HandleParsed(PackageOptions parsed) From 88f74190704656a7c2271c0db73815c7009b65ef Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sun, 13 Mar 2022 22:45:39 -0500 Subject: [PATCH 04/91] Switch JSON (de)serialization to Newtonsoft.Json --- ThunderstoreCLI.Tests/Models/BaseJson.cs | 10 +- .../ThunderstoreCLI.Tests.csproj | 1 + ThunderstoreCLI/Commands/BuildCommand.cs | 6 +- ThunderstoreCLI/Models/BaseJson.cs | 27 +-- .../Models/Interaction/BaseInteraction.cs | 7 +- ThunderstoreCLI/Models/PublishModels.cs | 174 +++++------------- ThunderstoreCLI/PackageManifestV1.cs | 20 +- ThunderstoreCLI/ThunderstoreCLI.csproj | 1 + ThunderstoreCLI/Utils/MiscUtils.cs | 2 +- 9 files changed, 80 insertions(+), 168 deletions(-) diff --git a/ThunderstoreCLI.Tests/Models/BaseJson.cs b/ThunderstoreCLI.Tests/Models/BaseJson.cs index dec6b61..299bc30 100644 --- a/ThunderstoreCLI.Tests/Models/BaseJson.cs +++ b/ThunderstoreCLI.Tests/Models/BaseJson.cs @@ -1,13 +1,11 @@ -using System; -using System.Text.Json; using System.Text.Json.Serialization; -using NuGet.Frameworks; +using Newtonsoft.Json; using ThunderstoreCLI.Models; using Xunit; namespace ThunderstoreCLI.Tests; -public class TestJson : BaseJson +public class TestJson : BaseJson { public class Location { @@ -77,9 +75,9 @@ public void Serialize_WhenGivenFilled_ReturnsExpected() [Fact] public void Serialize_WhenAskedToIndent_Indents() { - var options = new JsonSerializerOptions() + var options = new JsonSerializerSettings { - WriteIndented = true + Formatting = Formatting.Indented }; Assert.Equal( diff --git a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj index a7fa5b7..0d0c802 100644 --- a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj +++ b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj @@ -9,6 +9,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ThunderstoreCLI/Commands/BuildCommand.cs b/ThunderstoreCLI/Commands/BuildCommand.cs index ddb9289..2fe8852 100644 --- a/ThunderstoreCLI/Commands/BuildCommand.cs +++ b/ThunderstoreCLI/Commands/BuildCommand.cs @@ -1,6 +1,6 @@ using System.IO.Compression; using System.Text; -using System.Text.Json; +using Newtonsoft.Json; using static Crayon.Output; namespace ThunderstoreCLI.Commands; @@ -301,9 +301,9 @@ public static string SerializeManifest(Config.Config config) WebsiteUrl = config.PackageMeta.WebsiteUrl, Dependencies = dependencies.Select(x => $"{x.Key}-{x.Value}").ToArray() }; - var serializerOptions = new JsonSerializerOptions + var serializerOptions = new JsonSerializerSettings { - WriteIndented = true + Formatting = Formatting.Indented }; return manifest.Serialize(serializerOptions); } diff --git a/ThunderstoreCLI/Models/BaseJson.cs b/ThunderstoreCLI/Models/BaseJson.cs index cdac1a6..32249c2 100644 --- a/ThunderstoreCLI/Models/BaseJson.cs +++ b/ThunderstoreCLI/Models/BaseJson.cs @@ -1,29 +1,22 @@ using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization; +using Newtonsoft.Json; namespace ThunderstoreCLI.Models; -public abstract class BaseJson - where T : BaseJson - where Context : JsonSerializerContext +public abstract class BaseJson<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> + where T : BaseJson { - public string Serialize(JsonSerializerOptions? options = null) + public string Serialize(JsonSerializerSettings? options = null) { - var context = (Context) Activator.CreateInstance(typeof(Context), options)!; - - return JsonSerializer.Serialize(this, typeof(T), context); + return JsonConvert.SerializeObject(this, options); } - public static T? Deserialize(string json, JsonSerializerOptions? options = null) + public static T? Deserialize(string json, JsonSerializerSettings? options = null) { - var context = (Context) Activator.CreateInstance(typeof(Context), options)!; - - return (T?) JsonSerializer.Deserialize(json, typeof(T), context); + return JsonConvert.DeserializeObject(json); } - public static T? Deserialize(Stream json, JsonSerializerOptions? options) + public static async Task Deserialize(Stream json, JsonSerializerSettings? options = null) { - var context = (Context) Activator.CreateInstance(typeof(Context), options)!; - - return (T?) JsonSerializer.Deserialize(json, typeof(T), context); + using StreamReader reader = new(json); + return Deserialize(await reader.ReadToEndAsync(), options); } } diff --git a/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs b/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs index 53e6d8f..a7e22ba 100644 --- a/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs +++ b/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace ThunderstoreCLI.Models.Interaction; public enum InteractionOutputType @@ -13,9 +11,8 @@ public static class InteractionOptions public static InteractionOutputType OutputType { get; set; } = InteractionOutputType.HUMAN; } -public abstract class BaseInteraction : BaseJson - where T : BaseInteraction - where Context : JsonSerializerContext +public abstract class BaseInteraction : BaseJson + where T : BaseInteraction { public abstract string GetHumanString(); diff --git a/ThunderstoreCLI/Models/PublishModels.cs b/ThunderstoreCLI/Models/PublishModels.cs index e7070e5..bd1e44d 100644 --- a/ThunderstoreCLI/Models/PublishModels.cs +++ b/ThunderstoreCLI/Models/PublishModels.cs @@ -1,202 +1,128 @@ using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; +using Newtonsoft.Json; namespace ThunderstoreCLI.Models.Publish; -[ExcludeFromCodeCoverage] -public class PackageUploadMetadata : BaseJson +public class PackageUploadMetadata : BaseJson { - [JsonPropertyName("author_name")] - public string? AuthorName { get; set; } + [JsonProperty("author_name")] public string? AuthorName { get; set; } - [JsonPropertyName("categories")] - public string[]? Categories { get; set; } + [JsonProperty("categories")] public string[]? Categories { get; set; } - [JsonPropertyName("communities")] - public string[]? Communities { get; set; } + [JsonProperty("communities")] public string[]? Communities { get; set; } - [JsonPropertyName("has_nsfw_content")] - public bool HasNsfwContent { get; set; } + [JsonProperty("has_nsfw_content")] public bool HasNsfwContent { get; set; } - [JsonPropertyName("upload_uuid")] - public string? UploadUUID { get; set; } + [JsonProperty("upload_uuid")] public string? UploadUUID { get; set; } } -[JsonSerializable(typeof(PackageUploadMetadata))] -[ExcludeFromCodeCoverage] -public partial class PackageUploadMetadataContext : JsonSerializerContext { } - -[ExcludeFromCodeCoverage] -public class UploadInitiateData : BaseJson +public class UploadInitiateData : BaseJson { public class UserMediaData { - [JsonPropertyName("uuid")] - public string? UUID { get; set; } + [JsonProperty("uuid")] public string? UUID { get; set; } - [JsonPropertyName("filename")] - public string? Filename { get; set; } + [JsonProperty("filename")] public string? Filename { get; set; } - [JsonPropertyName("size")] - public long Size { get; set; } + [JsonProperty("size")] public long Size { get; set; } - [JsonPropertyName("datetime_created")] - public DateTime TimeCreated { get; set; } + [JsonProperty("datetime_created")] public DateTime TimeCreated { get; set; } - [JsonPropertyName("expiry")] - public DateTime? ExpireTime { get; set; } + [JsonProperty("expiry")] public DateTime? ExpireTime { get; set; } - [JsonPropertyName("status")] - public string? Status { get; set; } + [JsonProperty("status")] public string? Status { get; set; } } public class UploadPartData { - [JsonPropertyName("part_number")] - public int PartNumber { get; set; } + [JsonProperty("part_number")] public int PartNumber { get; set; } - [JsonPropertyName("url")] - public string? Url { get; set; } + [JsonProperty("url")] public string? Url { get; set; } - [JsonPropertyName("offset")] - public long Offset { get; set; } + [JsonProperty("offset")] public long Offset { get; set; } - [JsonPropertyName("length")] - public int Length { get; set; } + [JsonProperty("length")] public int Length { get; set; } } - [JsonPropertyName("user_media")] - public UserMediaData? Metadata { get; set; } + [JsonProperty("user_media")] public UserMediaData? Metadata { get; set; } - [JsonPropertyName("upload_urls")] - public UploadPartData[]? UploadUrls { get; set; } + [JsonProperty("upload_urls")] public UploadPartData[]? UploadUrls { get; set; } } -[JsonSerializable(typeof(UploadInitiateData))] -[ExcludeFromCodeCoverage] -public partial class UploadInitiateDataContext : JsonSerializerContext { } - -[ExcludeFromCodeCoverage] -public class FileData : BaseJson +public class FileData : BaseJson { - [JsonPropertyName("filename")] - public string? Filename { get; set; } + [JsonProperty("filename")] public string? Filename { get; set; } - [JsonPropertyName("file_size_bytes")] - public long Filesize { get; set; } + [JsonProperty("file_size_bytes")] public long Filesize { get; set; } } -[JsonSerializable(typeof(FileData))] -[ExcludeFromCodeCoverage] -public partial class FileDataContext : JsonSerializerContext { } - -[ExcludeFromCodeCoverage] -public class CompletedUpload : BaseJson +public class CompletedUpload : BaseJson { public class CompletedPartData { - [JsonPropertyName("ETag")] - public string? ETag { get; set; } + [JsonProperty("ETag")] public string? ETag { get; set; } - [JsonPropertyName("PartNumber")] - public int PartNumber { get; set; } + [JsonProperty("PartNumber")] public int PartNumber { get; set; } } - [JsonPropertyName("parts")] - public CompletedPartData[]? Parts { get; set; } + [JsonProperty("parts")] public CompletedPartData[]? Parts { get; set; } } -[JsonSerializable(typeof(CompletedUpload))] -public partial class CompletedUploadContext : JsonSerializerContext { } - // JSON response structure for publish package request. -[ExcludeFromCodeCoverage] -public class PublishData : BaseJson +public class PublishData : BaseJson { public class AvailableCommunityData { public class CommunityData { - [JsonPropertyName("identifier")] - public string? Identifier { get; set; } + [JsonProperty("identifier")] public string? Identifier { get; set; } - [JsonPropertyName("name")] - public string? Name { get; set; } + [JsonProperty("name")] public string? Name { get; set; } - [JsonPropertyName("discord_url")] - public string? DiscordUrl { get; set; } + [JsonProperty("discord_url")] public string? DiscordUrl { get; set; } - [JsonPropertyName("wiki_url")] - public object? WikiUrl { get; set; } + [JsonProperty("wiki_url")] public object? WikiUrl { get; set; } - [JsonPropertyName("require_package_listing_approval")] + [JsonProperty("require_package_listing_approval")] public bool RequirePackageListingApproval { get; set; } } - public class CategoryData - { - [JsonPropertyName("name")] - public string? Name { get; set; } + [JsonProperty("community")] public CommunityData? Community { get; set; } - [JsonPropertyName("slug")] - public string? Slug { get; set; } - } - - [JsonPropertyName("community")] - public CommunityData? Community { get; set; } + [JsonProperty("categories")] public List? Categories { get; set; } - [JsonPropertyName("categories")] - public List? Categories { get; set; } - - [JsonPropertyName("url")] - public string? Url { get; set; } + [JsonProperty("url")] public string? Url { get; set; } } public class PackageVersionData { - [JsonPropertyName("namespace")] - public string? Namespace { get; set; } + [JsonProperty("namespace")] public string? Namespace { get; set; } - [JsonPropertyName("name")] - public string? Name { get; set; } + [JsonProperty("name")] public string? Name { get; set; } - [JsonPropertyName("version_number")] - public string? VersionNumber { get; set; } + [JsonProperty("version_number")] public string? VersionNumber { get; set; } - [JsonPropertyName("full_name")] - public string? FullName { get; set; } + [JsonProperty("full_name")] public string? FullName { get; set; } - [JsonPropertyName("description")] - public string? Description { get; set; } + [JsonProperty("description")] public string? Description { get; set; } - [JsonPropertyName("icon")] - public string? Icon { get; set; } + [JsonProperty("icon")] public string? Icon { get; set; } - [JsonPropertyName("dependencies")] - public List? Dependencies { get; set; } + [JsonProperty("dependencies")] public List? Dependencies { get; set; } - [JsonPropertyName("download_url")] - public string? DownloadUrl { get; set; } + [JsonProperty("download_url")] public string? DownloadUrl { get; set; } - [JsonPropertyName("downloads")] - public int Downloads { get; set; } + [JsonProperty("downloads")] public int Downloads { get; set; } - [JsonPropertyName("date_created")] - public DateTime DateCreated { get; set; } + [JsonProperty("date_created")] public DateTime DateCreated { get; set; } - [JsonPropertyName("website_url")] - public string? WebsiteUrl { get; set; } + [JsonProperty("website_url")] public string? WebsiteUrl { get; set; } - [JsonPropertyName("is_active")] - public bool IsActive { get; set; } + [JsonProperty("is_active")] public bool IsActive { get; set; } } - [JsonPropertyName("available_communities")] + [JsonProperty("available_communities")] public List? AvailableCommunities { get; set; } - [JsonPropertyName("package_version")] - public PackageVersionData? PackageVersion { get; set; } + [JsonProperty("package_version")] public PackageVersionData? PackageVersion { get; set; } } - -[JsonSerializable(typeof(PublishData))] -public partial class PublishDataContext : JsonSerializerContext { } diff --git a/ThunderstoreCLI/PackageManifestV1.cs b/ThunderstoreCLI/PackageManifestV1.cs index b865472..236cb29 100644 --- a/ThunderstoreCLI/PackageManifestV1.cs +++ b/ThunderstoreCLI/PackageManifestV1.cs @@ -1,29 +1,25 @@ -using System.Text.Json.Serialization; +using Newtonsoft.Json; using ThunderstoreCLI.Models; namespace ThunderstoreCLI; -[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute] -public class PackageManifestV1 : BaseJson +public class PackageManifestV1 : BaseJson { - [JsonPropertyName("namespace")] + [JsonProperty("namespace")] public string? Namespace { get; set; } - [JsonPropertyName("name")] + [JsonProperty("name")] public string? Name { get; set; } - [JsonPropertyName("description")] + [JsonProperty("description")] public string? Description { get; set; } - [JsonPropertyName("version_number")] + [JsonProperty("version_number")] public string? VersionNumber { get; set; } - [JsonPropertyName("dependencies")] + [JsonProperty("dependencies")] public string[]? Dependencies { get; set; } - [JsonPropertyName("website_url")] + [JsonProperty("website_url")] public string? WebsiteUrl { get; set; } } - -[JsonSerializable(typeof(PackageManifestV1))] -public partial class PackageManifestV1Context : JsonSerializerContext { } diff --git a/ThunderstoreCLI/ThunderstoreCLI.csproj b/ThunderstoreCLI/ThunderstoreCLI.csproj index 0b004b0..8e027f3 100644 --- a/ThunderstoreCLI/ThunderstoreCLI.csproj +++ b/ThunderstoreCLI/ThunderstoreCLI.csproj @@ -30,6 +30,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/ThunderstoreCLI/Utils/MiscUtils.cs b/ThunderstoreCLI/Utils/MiscUtils.cs index 71990cb..37f57ba 100644 --- a/ThunderstoreCLI/Utils/MiscUtils.cs +++ b/ThunderstoreCLI/Utils/MiscUtils.cs @@ -14,7 +14,7 @@ public static int[] GetCurrentVersion() try { - version = Assembly.GetEntryAssembly()! + version = typeof(Program).Assembly .GetCustomAttribute()! .InformationalVersion; } From bc877e0d85307097c6840bd0f9ea8bdc51460d09 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sun, 13 Mar 2022 23:05:54 -0500 Subject: [PATCH 05/91] Reformat options --- ThunderstoreCLI/Config/CLIParameterConfig.cs | 9 +++- ThunderstoreCLI/Options.cs | 50 +++++++++++++++----- ThunderstoreCLI/Program.cs | 7 ++- 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/ThunderstoreCLI/Config/CLIParameterConfig.cs b/ThunderstoreCLI/Config/CLIParameterConfig.cs index 8825f7e..011f36f 100644 --- a/ThunderstoreCLI/Config/CLIParameterConfig.cs +++ b/ThunderstoreCLI/Config/CLIParameterConfig.cs @@ -2,14 +2,19 @@ namespace ThunderstoreCLI.Config; -public abstract class CLIParameterConfig : EmptyConfig where T : PackageOptions +public abstract class BaseConfig : EmptyConfig where T : BaseOptions { protected T options; - public CLIParameterConfig(T options) + public BaseConfig(T options) { this.options = options; } +} + +public abstract class CLIParameterConfig : BaseConfig where T : PackageOptions +{ + public CLIParameterConfig(T opts) : base(opts) { } public override GeneralConfig GetGeneralConfig() { diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index c641c65..da48274 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -6,7 +6,37 @@ namespace ThunderstoreCLI.Options; /// Options are arguments passed from command line. -public abstract class PackageOptions +public abstract class BaseOptions +{ + [Option("output", Required = false, HelpText = "The output format for all output. Valid options are HUMAN and JSON.")] + public InteractionOutputType OutputType { get; set; } = InteractionOutputType.HUMAN; + + [Option("tcli-directory", Required = false, HelpText = "Directory where TCLI keeps its data, %APPDATA%/ThunderstoreCLI on Windows and ~/.config/ThunderstoreCLI on Linux")] + // will be initialized in Init if null + public string TcliDirectory { get; set; } = null!; + + public virtual void Init() + { + InteractionOptions.OutputType = OutputType; + + // ReSharper disable once ConstantNullCoalescingCondition + TcliDirectory ??= Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ThunderstoreCLI"); + } + + public virtual bool Validate() + { + if (!Directory.Exists(TcliDirectory)) + { + Directory.CreateDirectory(TcliDirectory!); + } + + return true; + } + + public abstract int Execute(); +} + +public abstract class PackageOptions : BaseOptions { [Option("config-path", Required = false, Default = Defaults.PROJECT_CONFIG_PATH, HelpText = "Path for the project configuration file")] public string? ConfigPath { get; set; } @@ -20,18 +50,14 @@ public abstract class PackageOptions [Option("package-version", SetName = "build", Required = false, HelpText = "Version number for the package")] public string? VersionNumber { get; set; } - [Option("output", Required = false, HelpText = "The output format for all output. Valid options are HUMAN and JSON.")] - public InteractionOutputType? OutputType { get; set; } - - public virtual void Init() + public override bool Validate() { - if (OutputType.HasValue) - InteractionOptions.OutputType = OutputType.Value; - } + if (!base.Validate()) + { + return false; + } - public virtual bool Validate() - { - if (String.IsNullOrWhiteSpace(ConfigPath)) + if (string.IsNullOrWhiteSpace(ConfigPath)) { Write.ErrorExit("Invalid value for --config-path argument"); return false; @@ -51,8 +77,6 @@ public virtual bool Validate() return true; } - - public abstract int Execute(); } [Verb("init", HelpText = "Initialize a new project configuration")] diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index 31ebf1d..1904045 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -15,19 +15,18 @@ private static int Main(string[] args) #endif var updateChecker = UpdateChecker.CheckForUpdates(); - var exitCode = Parser.Default.ParseArguments(args) + var exitCode = Parser.Default.ParseArguments(args) .MapResult( (InitOptions o) => HandleParsed(o), (BuildOptions o) => HandleParsed(o), (PublishOptions o) => HandleParsed(o), - (InstallOptions o) => HandleParsed(o), _ => 1 // failure to parse ); UpdateChecker.WriteUpdateNotification(updateChecker); return exitCode; } - private static int HandleParsed(PackageOptions parsed) + private static int HandleParsed(BaseOptions parsed) { parsed.Init(); if (!parsed.Validate()) @@ -36,7 +35,7 @@ private static int HandleParsed(PackageOptions parsed) } } -class CommandException : Exception +internal class CommandException : Exception { public CommandException(string message) : base(message) { } } From 24f6dfb336eff14b46d27479ea0b1003ed27ce0a Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sun, 13 Mar 2022 23:16:30 -0500 Subject: [PATCH 06/91] Run pre-commit and small fixes --- ThunderstoreCLI/Utils/StringUtils.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ThunderstoreCLI/Utils/StringUtils.cs b/ThunderstoreCLI/Utils/StringUtils.cs index be2c35b..bd23464 100644 --- a/ThunderstoreCLI/Utils/StringUtils.cs +++ b/ThunderstoreCLI/Utils/StringUtils.cs @@ -4,6 +4,8 @@ namespace ThunderstoreCLI; public static class StringUtils { + private static readonly Regex SemVerRegex = new(@"^[0-9]+\.[0-9]+\.[0-9]+$"); + /// /// Validate the given string adheres to MAJOR.MINOR.PATCH format /// @@ -13,7 +15,6 @@ public static class StringUtils /// public static bool IsSemVer(string version) { - var regex = new Regex(@"^[0-9]+\.[0-9]+\.[0-9]+$"); - return regex.IsMatch(version); + return SemVerRegex.IsMatch(version); } } From 23a402d40dc7dd69dd28a154a3b67b11ee139325 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 15 Mar 2022 00:35:24 -0500 Subject: [PATCH 07/91] More option/config reorganizing --- ThunderstoreCLI/Commands/BuildCommand.cs | 19 +++++----- ThunderstoreCLI/Commands/InitCommand.cs | 18 +++++----- ThunderstoreCLI/Commands/PublishCommand.cs | 2 +- ThunderstoreCLI/Config/BaseConfig.cs | 13 ++----- ThunderstoreCLI/Config/CLIParameterConfig.cs | 17 ++++----- ThunderstoreCLI/Config/Config.cs | 38 ++++++++++---------- ThunderstoreCLI/Config/EmptyConfig.cs | 2 +- ThunderstoreCLI/Config/IConfigProvider.cs | 2 +- ThunderstoreCLI/Config/ProjectFileConfig.cs | 22 ++++++------ ThunderstoreCLI/Models/PublishModels.cs | 1 - ThunderstoreCLI/Program.cs | 2 +- 11 files changed, 66 insertions(+), 70 deletions(-) diff --git a/ThunderstoreCLI/Commands/BuildCommand.cs b/ThunderstoreCLI/Commands/BuildCommand.cs index 2fe8852..d329dda 100644 --- a/ThunderstoreCLI/Commands/BuildCommand.cs +++ b/ThunderstoreCLI/Commands/BuildCommand.cs @@ -1,6 +1,7 @@ using System.IO.Compression; using System.Text; using Newtonsoft.Json; +using ThunderstoreCLI.Models; using static Crayon.Output; namespace ThunderstoreCLI.Commands; @@ -291,14 +292,14 @@ public static string FormatArchivePath(string path, bool validate = true) public static string SerializeManifest(Config.Config config) { - var dependencies = config.PackageMeta.Dependencies ?? new Dictionary(); + var dependencies = config.PackageConfig.Dependencies ?? new Dictionary(); var manifest = new PackageManifestV1() { - Namespace = config.PackageMeta.Namespace, - Name = config.PackageMeta.Name, - Description = config.PackageMeta.Description, - VersionNumber = config.PackageMeta.VersionNumber, - WebsiteUrl = config.PackageMeta.WebsiteUrl, + Namespace = config.PackageConfig.Namespace, + Name = config.PackageConfig.Name, + Description = config.PackageConfig.Description, + VersionNumber = config.PackageConfig.VersionNumber, + WebsiteUrl = config.PackageConfig.WebsiteUrl, Dependencies = dependencies.Select(x => $"{x.Key}-{x.Value}").ToArray() }; var serializerOptions = new JsonSerializerSettings @@ -311,9 +312,9 @@ public static string SerializeManifest(Config.Config config) public static List ValidateConfig(Config.Config config, bool throwIfErrors = true) { var v = new Config.Validator("build"); - v.AddIfEmpty(config.PackageMeta.Namespace, "Package Namespace"); - v.AddIfEmpty(config.PackageMeta.Name, "Package Name"); - v.AddIfNotSemver(config.PackageMeta.VersionNumber, "Package VersionNumber"); + v.AddIfEmpty(config.PackageConfig.Namespace, "Package Namespace"); + v.AddIfEmpty(config.PackageConfig.Name, "Package Name"); + v.AddIfNotSemver(config.PackageConfig.VersionNumber, "Package VersionNumber"); v.AddIfEmpty(config.BuildConfig.OutDir, "Build OutDir"); if (throwIfErrors) diff --git a/ThunderstoreCLI/Commands/InitCommand.cs b/ThunderstoreCLI/Commands/InitCommand.cs index 458806d..dac32a6 100644 --- a/ThunderstoreCLI/Commands/InitCommand.cs +++ b/ThunderstoreCLI/Commands/InitCommand.cs @@ -67,22 +67,22 @@ public static int Run(Config.Config config) public static string BuildReadme(Config.Config config) { return $@" -# {config.PackageMeta.Namespace}-{config.PackageMeta.Name} +# {config.PackageConfig.Namespace}-{config.PackageConfig.Name} -{config.PackageMeta.Description} +{config.PackageConfig.Description} ".Trim(); } private static void ValidateConfig(Config.Config config) { var v = new Config.Validator("init"); - v.AddIfEmpty(config.PackageMeta.Namespace, "Package Namespace"); - v.AddIfEmpty(config.PackageMeta.Name, "Package Name"); - v.AddIfNotSemver(config.PackageMeta.VersionNumber, "Package VersionNumber"); - v.AddIfNull(config.PackageMeta.Description, "Package Description"); - v.AddIfNull(config.PackageMeta.WebsiteUrl, "Package WebsiteUrl"); - v.AddIfNull(config.PackageMeta.ContainsNsfwContent, "Package ContainsNsfwContent"); - v.AddIfNull(config.PackageMeta.Dependencies, "Package Dependencies"); + v.AddIfEmpty(config.PackageConfig.Namespace, "Package Namespace"); + v.AddIfEmpty(config.PackageConfig.Name, "Package Name"); + v.AddIfNotSemver(config.PackageConfig.VersionNumber, "Package VersionNumber"); + v.AddIfNull(config.PackageConfig.Description, "Package Description"); + v.AddIfNull(config.PackageConfig.WebsiteUrl, "Package WebsiteUrl"); + v.AddIfNull(config.PackageConfig.ContainsNsfwContent, "Package ContainsNsfwContent"); + v.AddIfNull(config.PackageConfig.Dependencies, "Package Dependencies"); v.AddIfEmpty(config.BuildConfig.IconPath, "Build IconPath"); v.AddIfEmpty(config.BuildConfig.ReadmePath, "Build ReadmePath"); v.AddIfEmpty(config.BuildConfig.OutDir, "Build OutDir"); diff --git a/ThunderstoreCLI/Commands/PublishCommand.cs b/ThunderstoreCLI/Commands/PublishCommand.cs index b3d8381..4a70338 100644 --- a/ThunderstoreCLI/Commands/PublishCommand.cs +++ b/ThunderstoreCLI/Commands/PublishCommand.cs @@ -193,7 +193,7 @@ private static void PublishPackageRequest(Config.Config config, string uploadUui throw new PublishCommandException(); } - Write.Success($"Successfully published {Cyan($"{config.PackageMeta.Namespace}-{config.PackageMeta.Name}")}"); + Write.Success($"Successfully published {Cyan($"{config.PackageConfig.Namespace}-{config.PackageConfig.Name}")}"); Write.Line($"It's available at {Cyan(jsonData.PackageVersion.DownloadUrl)}"); } diff --git a/ThunderstoreCLI/Config/BaseConfig.cs b/ThunderstoreCLI/Config/BaseConfig.cs index 5e261ef..9357a48 100644 --- a/ThunderstoreCLI/Config/BaseConfig.cs +++ b/ThunderstoreCLI/Config/BaseConfig.cs @@ -2,18 +2,11 @@ namespace ThunderstoreCLI.Config; class BaseConfig : EmptyConfig { - public override GeneralConfig GetGeneralConfig() + public override PackageConfig GetPackageMeta() { - return new GeneralConfig() - { - ProjectConfigPath = Defaults.PROJECT_CONFIG_PATH - }; - } - - public override PackageMeta GetPackageMeta() - { - return new PackageMeta() + return new PackageConfig() { + ProjectConfigPath = Defaults.PROJECT_CONFIG_PATH, Namespace = "AuthorName", Name = "PackageName", VersionNumber = "0.0.1", diff --git a/ThunderstoreCLI/Config/CLIParameterConfig.cs b/ThunderstoreCLI/Config/CLIParameterConfig.cs index 011f36f..ad3d88d 100644 --- a/ThunderstoreCLI/Config/CLIParameterConfig.cs +++ b/ThunderstoreCLI/Config/CLIParameterConfig.cs @@ -10,26 +10,27 @@ public BaseConfig(T options) { this.options = options; } -} - -public abstract class CLIParameterConfig : BaseConfig where T : PackageOptions -{ - public CLIParameterConfig(T opts) : base(opts) { } public override GeneralConfig GetGeneralConfig() { return new GeneralConfig() { - ProjectConfigPath = options.ConfigPath + TcliConfig = options.TcliDirectory }; } +} + +public abstract class CLIParameterConfig : BaseConfig where T : PackageOptions +{ + public CLIParameterConfig(T opts) : base(opts) { } - public override PackageMeta? GetPackageMeta() + public override PackageConfig? GetPackageMeta() { if (options == null) return null; - return new PackageMeta() + return new PackageConfig() { + ProjectConfigPath = options.ConfigPath, Namespace = options.Namespace, Name = options.Name, VersionNumber = options.VersionNumber diff --git a/ThunderstoreCLI/Config/Config.cs b/ThunderstoreCLI/Config/Config.cs index 28a675f..8767892 100644 --- a/ThunderstoreCLI/Config/Config.cs +++ b/ThunderstoreCLI/Config/Config.cs @@ -7,7 +7,7 @@ public class Config { // ReSharper disable AutoPropertyCanBeMadeGetOnly.Local public GeneralConfig GeneralConfig { get; private set; } - public PackageMeta PackageMeta { get; private set; } + public PackageConfig PackageConfig { get; private set; } public InitConfig InitConfig { get; private set; } public BuildConfig BuildConfig { get; private set; } public PublishConfig PublishConfig { get; private set; } @@ -17,11 +17,11 @@ public class Config private readonly Lazy api; public ApiHelper Api => api.Value; - private Config(GeneralConfig generalConfig, PackageMeta packageMeta, InitConfig initConfig, BuildConfig buildConfig, PublishConfig publishConfig, AuthConfig authConfig) + private Config(GeneralConfig generalConfig, PackageConfig packageConfig, InitConfig initConfig, BuildConfig buildConfig, PublishConfig publishConfig, AuthConfig authConfig) { api = new Lazy(() => new ApiHelper(this)); GeneralConfig = generalConfig; - PackageMeta = packageMeta; + PackageConfig = packageConfig; InitConfig = initConfig; BuildConfig = buildConfig; PublishConfig = publishConfig; @@ -29,12 +29,13 @@ private Config(GeneralConfig generalConfig, PackageMeta packageMeta, InitConfig } public static Config FromCLI(IConfigProvider cliConfig) { - return Parse( - cliConfig, - new EnvironmentConfig(), - new ProjectFileConfig(), - new BaseConfig() - ); + List providers = new(); + providers.Add(cliConfig); + providers.Add(new EnvironmentConfig()); + if (cliConfig.GetType().IsSubclassOf(typeof(CLIParameterConfig<>))) + providers.Add(new ProjectFileConfig()); + providers.Add(new BaseConfig()); + return Parse(providers.ToArray()); } public string? GetProjectBasePath() @@ -67,11 +68,11 @@ public string GetPackageReadmePath() public string GetProjectConfigPath() { - if (GeneralConfig.ProjectConfigPath is null) + if (PackageConfig.ProjectConfigPath is null) { throw new Exception("GeneralConfig.ProjectConfigPath can't be null"); } - return Path.GetFullPath(GeneralConfig.ProjectConfigPath); + return Path.GetFullPath(PackageConfig.ProjectConfigPath); } public string GetBuildOutputDir() @@ -85,7 +86,7 @@ public string GetBuildOutputDir() public string GetPackageId() { - return $"{PackageMeta.Namespace}-{PackageMeta.Name}-{PackageMeta.VersionNumber}"; + return $"{PackageConfig.Namespace}-{PackageConfig.Name}-{PackageConfig.VersionNumber}"; } public string GetBuildOutputFile() @@ -97,18 +98,18 @@ public PackageUploadMetadata GetUploadMetadata(string fileUuid) { return new PackageUploadMetadata() { - AuthorName = PackageMeta.Namespace, + AuthorName = PackageConfig.Namespace, Categories = PublishConfig.Categories, Communities = PublishConfig.Communities, - HasNsfwContent = PackageMeta.ContainsNsfwContent ?? false, + HasNsfwContent = PackageConfig.ContainsNsfwContent ?? false, UploadUUID = fileUuid }; } - public static Config Parse(params IConfigProvider[] configProviders) + public static Config Parse(IConfigProvider[] configProviders) { var generalConfig = new GeneralConfig(); - var packageMeta = new PackageMeta(); + var packageMeta = new PackageConfig(); var initConfig = new InitConfig(); var buildConfig = new BuildConfig(); var publishConfig = new PublishConfig(); @@ -150,11 +151,12 @@ public static void Merge(T target, T source, bool overwrite) public class GeneralConfig { - public string? ProjectConfigPath { get; set; } + public string TcliConfig { get; set; } = null!; } -public class PackageMeta +public class PackageConfig { + public string? ProjectConfigPath { get; set; } public string? Namespace { get; set; } public string? Name { get; set; } public string? VersionNumber { get; set; } diff --git a/ThunderstoreCLI/Config/EmptyConfig.cs b/ThunderstoreCLI/Config/EmptyConfig.cs index c0071a0..11b2ccd 100644 --- a/ThunderstoreCLI/Config/EmptyConfig.cs +++ b/ThunderstoreCLI/Config/EmptyConfig.cs @@ -9,7 +9,7 @@ public virtual void Parse(Config currentConfig) { } return null; } - public virtual PackageMeta? GetPackageMeta() + public virtual PackageConfig? GetPackageMeta() { return null; } diff --git a/ThunderstoreCLI/Config/IConfigProvider.cs b/ThunderstoreCLI/Config/IConfigProvider.cs index 16c736b..a3ec170 100644 --- a/ThunderstoreCLI/Config/IConfigProvider.cs +++ b/ThunderstoreCLI/Config/IConfigProvider.cs @@ -5,7 +5,7 @@ public interface IConfigProvider void Parse(Config currentConfig); GeneralConfig? GetGeneralConfig(); - PackageMeta? GetPackageMeta(); + PackageConfig? GetPackageMeta(); InitConfig? GetInitConfig(); BuildConfig? GetBuildConfig(); PublishConfig? GetPublishConfig(); diff --git a/ThunderstoreCLI/Config/ProjectFileConfig.cs b/ThunderstoreCLI/Config/ProjectFileConfig.cs index d63fb00..0883b6c 100644 --- a/ThunderstoreCLI/Config/ProjectFileConfig.cs +++ b/ThunderstoreCLI/Config/ProjectFileConfig.cs @@ -6,7 +6,7 @@ namespace ThunderstoreCLI.Config; class ProjectFileConfig : EmptyConfig { - private PackageMeta? PackageMeta { get; set; } + private PackageConfig? PackageMeta { get; set; } private BuildConfig? BuildConfig { get; set; } @@ -38,7 +38,7 @@ public override void Parse(Config currentConfig) PublishConfig = ParsePublishConfig(tomlData); } - protected static PackageMeta? ParsePackageMeta(TomlTable tomlData) + protected static PackageConfig? ParsePackageMeta(TomlTable tomlData) { if (!tomlData.HasKey("package")) return null; @@ -46,7 +46,7 @@ public override void Parse(Config currentConfig) var packageMeta = tomlData["package"]; // TODO: Add warnings on missing values - var result = new PackageMeta() + var result = new PackageConfig() { Namespace = TomlUtils.SafegetString(packageMeta, "namespace"), Name = TomlUtils.SafegetString(packageMeta, "name"), @@ -128,7 +128,7 @@ public override void Parse(Config currentConfig) }; } - public override PackageMeta? GetPackageMeta() + public override PackageConfig? GetPackageMeta() { return PackageMeta; } @@ -160,7 +160,7 @@ public override void Parse(Config currentConfig) public static void Write(Config config, string path) { - var dependencies = config.PackageMeta.Dependencies ?? new Dictionary(); + var dependencies = config.PackageConfig.Dependencies ?? new Dictionary(); var copyPaths = config.BuildConfig.CopyPaths ?? new List(); var toml = new TomlTable { @@ -171,12 +171,12 @@ public static void Write(Config config, string path) ["package"] = new TomlTable { - ["namespace"] = config.PackageMeta.Namespace, - ["name"] = config.PackageMeta.Name, - ["versionNumber"] = config.PackageMeta.VersionNumber, - ["description"] = config.PackageMeta.Description, - ["websiteUrl"] = config.PackageMeta.WebsiteUrl, - ["containsNsfwContent"] = config.PackageMeta.ContainsNsfwContent, + ["namespace"] = config.PackageConfig.Namespace, + ["name"] = config.PackageConfig.Name, + ["versionNumber"] = config.PackageConfig.VersionNumber, + ["description"] = config.PackageConfig.Description, + ["websiteUrl"] = config.PackageConfig.WebsiteUrl, + ["containsNsfwContent"] = config.PackageConfig.ContainsNsfwContent, ["dependencies"] = TomlUtils.DictToTomlTable(dependencies) }, diff --git a/ThunderstoreCLI/Models/PublishModels.cs b/ThunderstoreCLI/Models/PublishModels.cs index bd1e44d..0767da5 100644 --- a/ThunderstoreCLI/Models/PublishModels.cs +++ b/ThunderstoreCLI/Models/PublishModels.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; namespace ThunderstoreCLI.Models.Publish; diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index 1904045..004433f 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -9,7 +9,7 @@ internal static class Program private static int Main(string[] args) { #if DEBUG - if (Environment.GetEnvironmentVariable("TCLI_WAIT_DEBUGGER") is not null) + if (Environment.GetEnvironmentVariable("TCLI_WAIT_DEBUGGER") == "1") while (!Debugger.IsAttached) { } #endif From 5f76a1840d19f2421e63a3caf8e3f1386fe21f88 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 15 Mar 2022 00:36:25 -0500 Subject: [PATCH 08/91] Add more JSON helpers --- ThunderstoreCLI/Commands/BuildCommand.cs | 7 ++----- ThunderstoreCLI/Models/BaseJson.cs | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/ThunderstoreCLI/Commands/BuildCommand.cs b/ThunderstoreCLI/Commands/BuildCommand.cs index d329dda..eaafdce 100644 --- a/ThunderstoreCLI/Commands/BuildCommand.cs +++ b/ThunderstoreCLI/Commands/BuildCommand.cs @@ -302,11 +302,8 @@ public static string SerializeManifest(Config.Config config) WebsiteUrl = config.PackageConfig.WebsiteUrl, Dependencies = dependencies.Select(x => $"{x.Key}-{x.Value}").ToArray() }; - var serializerOptions = new JsonSerializerSettings - { - Formatting = Formatting.Indented - }; - return manifest.Serialize(serializerOptions); + + return manifest.Serialize(BaseJson.IndentedSettings); } public static List ValidateConfig(Config.Config config, bool throwIfErrors = true) diff --git a/ThunderstoreCLI/Models/BaseJson.cs b/ThunderstoreCLI/Models/BaseJson.cs index 32249c2..888489e 100644 --- a/ThunderstoreCLI/Models/BaseJson.cs +++ b/ThunderstoreCLI/Models/BaseJson.cs @@ -19,4 +19,26 @@ public string Serialize(JsonSerializerSettings? options = null) using StreamReader reader = new(json); return Deserialize(await reader.ReadToEndAsync(), options); } + + public static List? DeserializeList(string json, JsonSerializerSettings? options = null) + { + return JsonConvert.DeserializeObject>(json, options); + } +} + +public static class BaseJson +{ + public static readonly JsonSerializerSettings IndentedSettings = new() + { + Formatting = Formatting.Indented + }; +} + +public static class BaseJsonExtensions +{ + public static string SerializeList(this List list, JsonSerializerSettings? options = null) + where T : BaseJson + { + return JsonConvert.SerializeObject(list, options); + } } From 784d0e5dd149c7fa03ccad0b67f148cb4388f5f0 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 6 Jun 2022 15:19:33 -0500 Subject: [PATCH 09/91] Start moving TOML parsing to structured object --- .../ThunderstoreCLI.Tests.csproj | 1 + ThunderstoreCLI.Tests/Utils/TomlUtils.cs | 153 ------------------ ThunderstoreCLI/Config/ProjectFileConfig.cs | 27 +--- ThunderstoreCLI/Models/BaseJson.cs | 16 +- ThunderstoreCLI/Models/BaseToml.cs | 19 +++ ThunderstoreCLI/Models/ISerialize.cs | 12 ++ ThunderstoreCLI/Models/ThunderstoreProject.cs | 99 ++++++++++++ ThunderstoreCLI/Program.cs | 3 + ThunderstoreCLI/ThunderstoreCLI.csproj | 3 +- ThunderstoreCLI/Utils/TomlUtils.cs | 93 ----------- 10 files changed, 150 insertions(+), 276 deletions(-) delete mode 100644 ThunderstoreCLI.Tests/Utils/TomlUtils.cs create mode 100644 ThunderstoreCLI/Models/BaseToml.cs create mode 100644 ThunderstoreCLI/Models/ISerialize.cs create mode 100644 ThunderstoreCLI/Models/ThunderstoreProject.cs delete mode 100644 ThunderstoreCLI/Utils/TomlUtils.cs diff --git a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj index 0d0c802..6814bf0 100644 --- a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj +++ b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj @@ -10,6 +10,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ThunderstoreCLI.Tests/Utils/TomlUtils.cs b/ThunderstoreCLI.Tests/Utils/TomlUtils.cs deleted file mode 100644 index a39674c..0000000 --- a/ThunderstoreCLI.Tests/Utils/TomlUtils.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System.IO; -using Tommy; -using Xunit; - -namespace ThunderstoreCLI.Tests; - -public class ThunderstoreCLI_TomlUtils -{ - public static string _key = "key"; - - public static TheoryData InvalidValues => new TheoryData - { - $"{_key} = 0", - $"{_key} = +3.1415", - $"{_key} = -2E-2", - $"{_key} = nan", - $"{_key} = 1970-01-01T00:00:00Z", - $"{_key} = 1970-01-01", - $"{_key} = 00:00:00", - $"{_key} = {{ x = 1, y = 2 }}" - }; - - public static TomlTable CreateTomlTable(string input) - { - using var reader = new StringReader(input); - return TOML.Parse(reader); - } - - [Fact] - public void SafegetString_WhenKeyIsNotFound_ReturnsNull() - { - var table = CreateTomlTable(""); - - var actual = TomlUtils.SafegetString(table, _key); - - Assert.Null(actual); - } - - public static TheoryData ValidStringValues => new TheoryData - { - { $"{_key} = \"value\"", "value" }, - { $"{_key} = \"\"", "" }, - { $"foo = \"foo\"\n{_key} = \"value\"\nbar = \"bar\"", "value" }, - { $"{_key} = 'literal string'", "literal string" }, - }; - - [Theory] - [MemberData(nameof(ValidStringValues))] - public void SafegetString_WhenValueIsString_ReturnsValue(string input, string expected) - { - var table = CreateTomlTable(input); - - var actual = TomlUtils.SafegetString(table, _key); - - Assert.Equal(expected, actual); - } - - [Theory] - [MemberData(nameof(InvalidValues))] - [InlineData("key = true")] - [InlineData("key = [\"foo\", \"bar\"]")] - public void SafegetString_WhenValueIsNotString_ReturnsNull(string input) - { - var table = CreateTomlTable(input); - - var actual = TomlUtils.SafegetString(table, _key); - - Assert.Null(actual); - } - - [Fact] - public void SafegetBool_WhenKeyIsNotFound_ReturnsNull() - { - var table = CreateTomlTable(""); - - var actual = TomlUtils.SafegetBool(table, _key); - - Assert.Null(actual); - } - - public static TheoryData ValidBoolValues => new TheoryData - { - { $"{_key} = true", true }, - { $"{_key} = false", false } - }; - - [Theory] - [MemberData(nameof(ValidBoolValues))] - public void SafegetBool_WhenValueIsBool_ReturnsValue(string input, bool expected) - { - var table = CreateTomlTable(input); - - var actual = TomlUtils.SafegetBool(table, _key); - - Assert.Equal(expected, actual); - } - - [Theory] - [MemberData(nameof(InvalidValues))] - [InlineData("key = \"true\"")] - [InlineData("key = [\"foo\", \"bar\"]")] - public void SafegetBool_WhenValueIsNotBool_ReturnsNull(string input) - { - var table = CreateTomlTable(input); - - var actual = TomlUtils.SafegetBool(table, _key); - - Assert.Null(actual); - } - - [Fact] - public void SafegetStringArray_WhenKeyIsNotFound_ReturnsNull() - { - var table = CreateTomlTable(""); - - var actual = TomlUtils.SafegetBool(table, _key); - - Assert.Null(actual); - } - - public static TheoryData ValidStringArrayValues => new TheoryData - { - { $"{_key} = []", new string[] { } }, - { $"{_key} = [\"\"]", new [] { "" } }, - { $"{_key} = [\"value\"]", new [] { "value" } }, - { $"{_key} = [\"value1\", \"value2\"]", new [] { "value1", "value2" } }, - { $"{_key} = ['literal']", new [] { "literal" } } - }; - - [Theory] - [MemberData(nameof(ValidStringArrayValues))] - public void SafegetStringArray_WhenValueIsStringArray_ReturnsValue(string input, string[] expected) - { - var table = CreateTomlTable(input); - - var actual = TomlUtils.SafegetStringArray(table, _key); - - Assert.Equal(expected, actual); - } - - [Theory] - [MemberData(nameof(InvalidValues))] - [InlineData("key = \"value\"")] - [InlineData("key = true")] - public void SafegetStringArray_WhenValueIsNotStringArray_ReturnsNull(string input) - { - var table = CreateTomlTable(input); - - var actual = TomlUtils.SafegetStringArray(table, _key); - - Assert.Null(actual); - } -} diff --git a/ThunderstoreCLI/Config/ProjectFileConfig.cs b/ThunderstoreCLI/Config/ProjectFileConfig.cs index 0883b6c..eaa689b 100644 --- a/ThunderstoreCLI/Config/ProjectFileConfig.cs +++ b/ThunderstoreCLI/Config/ProjectFileConfig.cs @@ -1,4 +1,4 @@ -using Tommy; +using ThunderstoreCLI.Models; using static Crayon.Output; namespace ThunderstoreCLI.Config; @@ -14,25 +14,7 @@ class ProjectFileConfig : EmptyConfig public override void Parse(Config currentConfig) { - var tomlData = Read(currentConfig); - if (tomlData == null) - return; - - if (!tomlData.HasKey("config") || !tomlData["config"].HasKey("schemaVersion")) - { - ThunderstoreCLI.Write.Warn( - "Project configuration is lacking schema version", - "Might not be able to parse configuration as expected" - ); - } - if (tomlData["config"]["schemaVersion"] != "0.0.1") - { - ThunderstoreCLI.Write.Warn( - "Unknown project configuration schema version", - "Might not be able to parse configuration as expected" - ); - } - + GeneralConfig = ParseGeneralConfig(tomlData); PackageMeta = ParsePackageMeta(tomlData); BuildConfig = ParseBuildConfig(tomlData); PublishConfig = ParsePublishConfig(tomlData); @@ -143,7 +125,7 @@ public override void Parse(Config currentConfig) return PublishConfig; } - public static TomlTable? Read(Config config) + public static ThunderstoreProject? Read(Config config) { var configPath = config.GetProjectConfigPath(); if (!File.Exists(configPath)) @@ -154,8 +136,7 @@ public override void Parse(Config currentConfig) ); return null; } - using var reader = new StreamReader(File.OpenRead(configPath)); - return TOML.Parse(reader); + return ThunderstoreProject.Deserialize(configPath); } public static void Write(Config config, string path) diff --git a/ThunderstoreCLI/Models/BaseJson.cs b/ThunderstoreCLI/Models/BaseJson.cs index 888489e..06115e7 100644 --- a/ThunderstoreCLI/Models/BaseJson.cs +++ b/ThunderstoreCLI/Models/BaseJson.cs @@ -3,18 +3,24 @@ namespace ThunderstoreCLI.Models; -public abstract class BaseJson<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> +public abstract class BaseJson<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : ISerialize where T : BaseJson { - public string Serialize(JsonSerializerSettings? options = null) + public string Serialize() => Serialize(null); + public string Serialize(JsonSerializerSettings? options) { return JsonConvert.SerializeObject(this, options); } - public static T? Deserialize(string json, JsonSerializerSettings? options = null) + + public static T? Deserialize(string json) => Deserialize(json, null); + public static T? Deserialize(string json, JsonSerializerSettings? options) { - return JsonConvert.DeserializeObject(json); + return JsonConvert.DeserializeObject(json, options); } - public static async Task Deserialize(Stream json, JsonSerializerSettings? options = null) + + public static ValueTask DeserializeAsync(string json) => new(Deserialize(json)); + public static ValueTask DeserializeAsync(Stream json) => new(DeserializeAsync(json, null)); + public static async Task DeserializeAsync(Stream json, JsonSerializerSettings? options) { using StreamReader reader = new(json); return Deserialize(await reader.ReadToEndAsync(), options); diff --git a/ThunderstoreCLI/Models/BaseToml.cs b/ThunderstoreCLI/Models/BaseToml.cs new file mode 100644 index 0000000..daf825f --- /dev/null +++ b/ThunderstoreCLI/Models/BaseToml.cs @@ -0,0 +1,19 @@ +using System.Diagnostics.CodeAnalysis; +using Tomlet; + +namespace ThunderstoreCLI.Models; + +public abstract class BaseToml<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : ISerialize + where T : BaseToml +{ + public string Serialize() => TomletMain.TomlStringFrom(this); + + public static T? Deserialize(string toml) => TomletMain.To(toml); + + public static ValueTask DeserializeAsync(string toml) => new(Deserialize(toml)); + public static async ValueTask DeserializeAsync(Stream toml) + { + using StreamReader reader = new(toml); + return Deserialize(await reader.ReadToEndAsync()); + } +} diff --git a/ThunderstoreCLI/Models/ISerialize.cs b/ThunderstoreCLI/Models/ISerialize.cs new file mode 100644 index 0000000..736b9c9 --- /dev/null +++ b/ThunderstoreCLI/Models/ISerialize.cs @@ -0,0 +1,12 @@ +using System.Diagnostics.CodeAnalysis; + +namespace ThunderstoreCLI.Models; + +public interface ISerialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> + where T : ISerialize +{ + public string Serialize(); + public static abstract T? Deserialize(string input); + public static abstract ValueTask DeserializeAsync(string input); + public static abstract ValueTask DeserializeAsync(Stream input); +} diff --git a/ThunderstoreCLI/Models/ThunderstoreProject.cs b/ThunderstoreCLI/Models/ThunderstoreProject.cs new file mode 100644 index 0000000..54951f2 --- /dev/null +++ b/ThunderstoreCLI/Models/ThunderstoreProject.cs @@ -0,0 +1,99 @@ +using Tomlet.Attributes; + +namespace ThunderstoreCLI.Models; + +[TomlDoNotInlineObject] +public class ThunderstoreProject : BaseToml +{ + [TomlDoNotInlineObject] + public class ConfigData + { + [TomlProperty("schemaVersion")] + public string SchemaVersion { get; set; } = "0.0.1"; + } + [TomlProperty("config")] + public ConfigData? Config { get; set; } + + [TomlDoNotInlineObject] + public class PackageData + { + [TomlProperty("namespace")] + public string Namespace { get; set; } = "AuthorName"; + [TomlProperty("name")] + public string Name { get; set; } = "PackageName"; + [TomlProperty("versionNumber")] + public string VersionNumber { get; set; } = "0.0.1"; + [TomlProperty("description")] + public string Description { get; set; } = "Example mod description"; + [TomlProperty("websiteUrl")] + public string WebsiteUrl { get; set; } = "https://thunderstore.io"; + [TomlProperty("containsNsfwContent")] + public bool ContainsNsfwContent { get; set; } = false; + [TomlProperty("dependencies")] + public Dictionary Dependencies { get; set; } = new() + { + { "AuthorName-PackageName", "0.0.1" } + }; + } + [TomlProperty("package")] + public PackageData? Package { get; set; } + + [TomlDoNotInlineObject] + public class BuildData + { + [TomlProperty("icon")] + public string Icon { get; set; } = "./icon.png"; + [TomlProperty("readme")] + public string Readme { get; set; } = "./README.md"; + [TomlProperty("outdir")] + public string OutDir { get; set; } = "./build"; + + [TomlDoNotInlineObject] + public class CopyPath + { + [TomlProperty("source")] + public string Source { get; set; } = "./dist"; + [TomlProperty("target")] + public string Target { get; set; } = ""; + } + [TomlProperty("copy")] + public CopyPath[] CopyPaths { get; set; } = + { + new() + }; + } + [TomlProperty("build")] + public BuildData? Build { get; set; } + + [TomlDoNotInlineObject] + public class PublishData + { + [TomlProperty("repository")] + public string Repository { get; set; } = "https://thunderstore.io"; + [TomlProperty("communtities")] + public string[] Communities { get; set; } = + { + "riskofrain2" + }; + [TomlProperty("categories")] + public string[] Categories { get; set; } = + { + "items", "skills" + }; + } + [TomlProperty("publish")] + public PublishData? Publish { get; set; } + + public ThunderstoreProject() { } + + public ThunderstoreProject(bool initialize) + { + if (!initialize) + return; + + Config = new(); + Package = new(); + Build = new(); + Publish = new(); + } +} diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index 004433f..c2adbce 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using CommandLine; +using ThunderstoreCLI.Models; using ThunderstoreCLI.Options; namespace ThunderstoreCLI; @@ -13,6 +14,8 @@ private static int Main(string[] args) while (!Debugger.IsAttached) { } #endif + Console.WriteLine(ThunderstoreProject.Deserialize(File.ReadAllText("thunderstore.toml"))!.Serialize()); + return 0; var updateChecker = UpdateChecker.CheckForUpdates(); var exitCode = Parser.Default.ParseArguments(args) diff --git a/ThunderstoreCLI/ThunderstoreCLI.csproj b/ThunderstoreCLI/ThunderstoreCLI.csproj index 8e027f3..1953c10 100644 --- a/ThunderstoreCLI/ThunderstoreCLI.csproj +++ b/ThunderstoreCLI/ThunderstoreCLI.csproj @@ -31,7 +31,7 @@ all - + @@ -53,5 +53,4 @@ Resources.Designer.cs - diff --git a/ThunderstoreCLI/Utils/TomlUtils.cs b/ThunderstoreCLI/Utils/TomlUtils.cs deleted file mode 100644 index b455ebc..0000000 --- a/ThunderstoreCLI/Utils/TomlUtils.cs +++ /dev/null @@ -1,93 +0,0 @@ -using ThunderstoreCLI.Config; -using Tommy; - -namespace ThunderstoreCLI; - -public static class TomlUtils -{ - public static TomlTable DictToTomlTable(Dictionary dict) - { - var result = new TomlTable(); - foreach (var kvp in dict) - { - result.Add(kvp.Key, kvp.Value); - } - return result; - } - - public static TomlArray BuildCopyPathTable(List list) - { - var result = new TomlArray() { IsTableArray = true }; - foreach (var entry in list) - { - result.Add(DictToTomlTable(new() - { - { "source", entry.From }, - { "target", entry.To } - })); - } - return result; - } - - public static string FormatToml(TomlTable toml) - { - using (var writer = new StringWriter()) - { - writer.NewLine = "\n"; - toml.WriteTo(writer); - writer.Flush(); - return writer.ToString().Trim(); - } - } - - public static string? SafegetString(TomlNode parentNode, string key) - { - try - { - var textNode = parentNode[key]; - return textNode.IsString ? textNode.ToString() : null; - } - catch (NullReferenceException) - { - return null; - } - } - - public static bool? SafegetBool(TomlNode parentNode, string key) - { - try - { - var boolNode = parentNode[key]; - return boolNode.IsBoolean ? boolNode : null; - } - catch (NullReferenceException) - { - return null; - } - } - - public static string[]? SafegetStringArray(TomlNode parentNode, string key, string[]? defaultValue = null) - { - try - { - var arrayNode = parentNode[key]; - return arrayNode.IsArray - ? arrayNode.AsArray.RawArray.Select(x => x.AsString.Value).ToArray() - : defaultValue; - } - catch (NullReferenceException) - { - return defaultValue; - } - } - - public static TomlArray FromArray(string[] array) - { - var ret = new TomlArray(); - if (array == null) - return ret; - foreach (var val in array) - ret.Add(val); - return ret; - } -} From 5763bb0253f1c4be2def5445c8a46c4a8625cfbf Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 11 Oct 2022 22:34:01 -0500 Subject: [PATCH 10/91] Move namespaces --- ThunderstoreCLI.Tests/Models/BaseJson.cs | 6 +- .../ThunderstoreCLI.Tests.csproj | 1 + ThunderstoreCLI.Tests/Utils/Comparers.cs | 5 +- ThunderstoreCLI.Tests/Utils/MiscUtils.cs | 3 +- ThunderstoreCLI.Tests/Utils/Spinner.cs | 3 +- ThunderstoreCLI.Tests/Utils/StringUtils.cs | 3 +- ThunderstoreCLI.Tests/Utils/Write.cs | 3 +- ThunderstoreCLI/API/ApiHelper.cs | 7 +- ThunderstoreCLI/Commands/BuildCommand.cs | 16 +- ThunderstoreCLI/Commands/InitCommand.cs | 12 +- ThunderstoreCLI/Commands/PublishCommand.cs | 16 +- ThunderstoreCLI/Config/ProjectFileConfig.cs | 181 ------------------ .../{Config => Configuration}/BaseConfig.cs | 2 +- .../CLIParameterConfig.cs | 4 +- .../{Config => Configuration}/Config.cs | 5 +- .../{Config => Configuration}/EmptyConfig.cs | 2 +- .../EnvironmentConfig.cs | 2 +- .../IConfigProvider.cs | 2 +- .../Configuration/ProjectFileConfig.cs | 73 +++++++ .../{Config => Configuration}/Validator.cs | 4 +- ThunderstoreCLI/Models/PublishModels.cs | 2 +- ThunderstoreCLI/Models/ThunderstoreProject.cs | 38 +++- ThunderstoreCLI/Options.cs | 10 +- ThunderstoreCLI/Program.cs | 1 - ThunderstoreCLI/ThunderstoreCLI.csproj | 2 + ThunderstoreCLI/UpdateChecker.cs | 2 + ThunderstoreCLI/Utils/Comparers.cs | 2 +- ThunderstoreCLI/Utils/MiscUtils.cs | 4 +- ThunderstoreCLI/Utils/Spinner.cs | 2 +- ThunderstoreCLI/Utils/StringUtils.cs | 2 +- ThunderstoreCLI/Utils/Write.cs | 2 +- 31 files changed, 172 insertions(+), 245 deletions(-) delete mode 100644 ThunderstoreCLI/Config/ProjectFileConfig.cs rename ThunderstoreCLI/{Config => Configuration}/BaseConfig.cs (96%) rename ThunderstoreCLI/{Config => Configuration}/CLIParameterConfig.cs (96%) rename ThunderstoreCLI/{Config => Configuration}/Config.cs (98%) rename ThunderstoreCLI/{Config => Configuration}/EmptyConfig.cs (94%) rename ThunderstoreCLI/{Config => Configuration}/EnvironmentConfig.cs (96%) rename ThunderstoreCLI/{Config => Configuration}/IConfigProvider.cs (87%) create mode 100644 ThunderstoreCLI/Configuration/ProjectFileConfig.cs rename ThunderstoreCLI/{Config => Configuration}/Validator.cs (96%) diff --git a/ThunderstoreCLI.Tests/Models/BaseJson.cs b/ThunderstoreCLI.Tests/Models/BaseJson.cs index 299bc30..190791a 100644 --- a/ThunderstoreCLI.Tests/Models/BaseJson.cs +++ b/ThunderstoreCLI.Tests/Models/BaseJson.cs @@ -1,9 +1,10 @@ using System.Text.Json.Serialization; using Newtonsoft.Json; +using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Models; using Xunit; -namespace ThunderstoreCLI.Tests; +namespace ThunderstoreCLI.Tests.Models; public class TestJson : BaseJson { @@ -17,9 +18,6 @@ public class Location public Location home { get; set; } } -[JsonSerializable(typeof(TestJson))] -public partial class TestJsonContext : JsonSerializerContext { } - public class ThunderstoreCLI_BaseJson { [Fact] diff --git a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj index 6814bf0..d7207eb 100644 --- a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj +++ b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj @@ -5,6 +5,7 @@ false win-x64 latest + true diff --git a/ThunderstoreCLI.Tests/Utils/Comparers.cs b/ThunderstoreCLI.Tests/Utils/Comparers.cs index c043be2..c6e5bd1 100644 --- a/ThunderstoreCLI.Tests/Utils/Comparers.cs +++ b/ThunderstoreCLI.Tests/Utils/Comparers.cs @@ -1,10 +1,11 @@ +using ThunderstoreCLI.Utils; using Xunit; -namespace ThunderstoreCLI.Tests; +namespace ThunderstoreCLI.Tests.Utils; public class ThunderstoreCLI_Comparers_SemVer { - private readonly ThunderstoreCLI.Comparers.SemVer _semVer = new ThunderstoreCLI.Comparers.SemVer(); + private readonly SemVer _semVer = new SemVer(); public static TheoryData EqualValues => new TheoryData { diff --git a/ThunderstoreCLI.Tests/Utils/MiscUtils.cs b/ThunderstoreCLI.Tests/Utils/MiscUtils.cs index f82a9f9..371357b 100644 --- a/ThunderstoreCLI.Tests/Utils/MiscUtils.cs +++ b/ThunderstoreCLI.Tests/Utils/MiscUtils.cs @@ -1,7 +1,8 @@ using System; +using ThunderstoreCLI.Utils; using Xunit; -namespace ThunderstoreCLI.Tests; +namespace ThunderstoreCLI.Tests.Utils; public class ThunderstoreCLI_MiscUtils { diff --git a/ThunderstoreCLI.Tests/Utils/Spinner.cs b/ThunderstoreCLI.Tests/Utils/Spinner.cs index e2f3da0..09e3fd4 100644 --- a/ThunderstoreCLI.Tests/Utils/Spinner.cs +++ b/ThunderstoreCLI.Tests/Utils/Spinner.cs @@ -1,8 +1,9 @@ using System; using System.Threading.Tasks; +using ThunderstoreCLI.Utils; using Xunit; -namespace ThunderstoreCLI.Tests; +namespace ThunderstoreCLI.Tests.Utils; public class ThunderstoreCLI_ProgresSpinner { diff --git a/ThunderstoreCLI.Tests/Utils/StringUtils.cs b/ThunderstoreCLI.Tests/Utils/StringUtils.cs index 47ceae9..da1b5e0 100644 --- a/ThunderstoreCLI.Tests/Utils/StringUtils.cs +++ b/ThunderstoreCLI.Tests/Utils/StringUtils.cs @@ -1,6 +1,7 @@ +using ThunderstoreCLI.Utils; using Xunit; -namespace ThunderstoreCLI.Tests; +namespace ThunderstoreCLI.Tests.Utils; public class ThunderstoreCLI_StringUtils { diff --git a/ThunderstoreCLI.Tests/Utils/Write.cs b/ThunderstoreCLI.Tests/Utils/Write.cs index 64c88e6..d8e241d 100644 --- a/ThunderstoreCLI.Tests/Utils/Write.cs +++ b/ThunderstoreCLI.Tests/Utils/Write.cs @@ -1,8 +1,9 @@ using System; using System.IO; +using ThunderstoreCLI.Utils; using Xunit; -namespace ThunderstoreCLI.Tests; +namespace ThunderstoreCLI.Tests.Utils; public class FakeConsole : IDisposable { diff --git a/ThunderstoreCLI/API/ApiHelper.cs b/ThunderstoreCLI/API/ApiHelper.cs index 731670b..9f511b6 100644 --- a/ThunderstoreCLI/API/ApiHelper.cs +++ b/ThunderstoreCLI/API/ApiHelper.cs @@ -1,20 +1,21 @@ using System.Net.Http.Headers; using System.Text; -using ThunderstoreCLI.Models.Publish; +using ThunderstoreCLI.Configuration; +using ThunderstoreCLI.Models; using ThunderstoreCLI.Utils; namespace ThunderstoreCLI.API; public class ApiHelper { - private Config.Config Config { get; } + private Config Config { get; } private RequestBuilder BaseRequestBuilder { get; } private readonly Lazy authHeader; private AuthenticationHeaderValue AuthHeader => authHeader.Value; - public ApiHelper(Config.Config config) + public ApiHelper(Config config) { Config = config; BaseRequestBuilder = new RequestBuilder(config.PublishConfig.Repository ?? throw new Exception("The target repository cannot be empty")); diff --git a/ThunderstoreCLI/Commands/BuildCommand.cs b/ThunderstoreCLI/Commands/BuildCommand.cs index eaafdce..1d667dc 100644 --- a/ThunderstoreCLI/Commands/BuildCommand.cs +++ b/ThunderstoreCLI/Commands/BuildCommand.cs @@ -1,7 +1,9 @@ using System.IO.Compression; using System.Text; using Newtonsoft.Json; +using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Models; +using ThunderstoreCLI.Utils; using static Crayon.Output; namespace ThunderstoreCLI.Commands; @@ -10,7 +12,7 @@ public static class BuildCommand { public class ArchivePlan { - public Config.Config Config { get; protected set; } + public Config Config { get; protected set; } public bool HasWarnings { get; protected set; } public bool HasErrors { get; protected set; } @@ -19,7 +21,7 @@ public class ArchivePlan protected HashSet directories; protected HashSet files; - public ArchivePlan(Config.Config config) + public ArchivePlan(Config config) { Config = config; plan = new(); @@ -103,7 +105,7 @@ public Dictionary>.Enumerator GetEnumerator() } } - public static int Run(Config.Config config) + public static int Run(Config config) { try { @@ -117,7 +119,7 @@ public static int Run(Config.Config config) return DoBuild(config); } - public static int DoBuild(Config.Config config) + public static int DoBuild(Config config) { var packageId = config.GetPackageId(); Write.WithNL($"Building {Cyan(packageId)}", after: true); @@ -290,7 +292,7 @@ public static string FormatArchivePath(string path, bool validate = true) return result; } - public static string SerializeManifest(Config.Config config) + public static string SerializeManifest(Config config) { var dependencies = config.PackageConfig.Dependencies ?? new Dictionary(); var manifest = new PackageManifestV1() @@ -306,9 +308,9 @@ public static string SerializeManifest(Config.Config config) return manifest.Serialize(BaseJson.IndentedSettings); } - public static List ValidateConfig(Config.Config config, bool throwIfErrors = true) + public static List ValidateConfig(Config config, bool throwIfErrors = true) { - var v = new Config.Validator("build"); + var v = new Validator("build"); v.AddIfEmpty(config.PackageConfig.Namespace, "Package Namespace"); v.AddIfEmpty(config.PackageConfig.Name, "Package Name"); v.AddIfNotSemver(config.PackageConfig.VersionNumber, "Package VersionNumber"); diff --git a/ThunderstoreCLI/Commands/InitCommand.cs b/ThunderstoreCLI/Commands/InitCommand.cs index dac32a6..7747506 100644 --- a/ThunderstoreCLI/Commands/InitCommand.cs +++ b/ThunderstoreCLI/Commands/InitCommand.cs @@ -1,11 +1,11 @@ -using ThunderstoreCLI.Config; -using ThunderstoreCLI.Options; +using ThunderstoreCLI.Configuration; +using ThunderstoreCLI.Utils; namespace ThunderstoreCLI.Commands; public static class InitCommand { - public static int Run(Config.Config config) + public static int Run(Config config) { try { @@ -64,7 +64,7 @@ public static int Run(Config.Config config) } } - public static string BuildReadme(Config.Config config) + public static string BuildReadme(Config config) { return $@" # {config.PackageConfig.Namespace}-{config.PackageConfig.Name} @@ -73,9 +73,9 @@ public static string BuildReadme(Config.Config config) ".Trim(); } - private static void ValidateConfig(Config.Config config) + private static void ValidateConfig(Config config) { - var v = new Config.Validator("init"); + var v = new Validator("init"); v.AddIfEmpty(config.PackageConfig.Namespace, "Package Namespace"); v.AddIfEmpty(config.PackageConfig.Name, "Package Name"); v.AddIfNotSemver(config.PackageConfig.VersionNumber, "Package VersionNumber"); diff --git a/ThunderstoreCLI/Commands/PublishCommand.cs b/ThunderstoreCLI/Commands/PublishCommand.cs index 4a70338..57e8886 100644 --- a/ThunderstoreCLI/Commands/PublishCommand.cs +++ b/ThunderstoreCLI/Commands/PublishCommand.cs @@ -1,7 +1,9 @@ using System.Net; using System.Security.Cryptography; using System.Text; -using ThunderstoreCLI.Models.Publish; +using ThunderstoreCLI.Configuration; +using ThunderstoreCLI.Models; +using ThunderstoreCLI.Utils; using static Crayon.Output; namespace ThunderstoreCLI.Commands; @@ -18,7 +20,7 @@ static PublishCommand() HttpClient.Timeout = TimeSpan.FromHours(1); } - public static int Run(Config.Config config) + public static int Run(Config config) { try { @@ -47,7 +49,7 @@ public static int Run(Config.Config config) return PublishFile(config, packagePath); } - public static int PublishFile(Config.Config config, string filepath) + public static int PublishFile(Config config, string filepath) { Write.WithNL($"Publishing {Cyan(filepath)}", before: true, after: true); @@ -131,7 +133,7 @@ public static int PublishFile(Config.Config config, string filepath) return 0; } - private static UploadInitiateData InitiateUploadRequest(Config.Config config, string filepath) + private static UploadInitiateData InitiateUploadRequest(Config config, string filepath) { var response = HttpClient.Send(config.Api.StartUploadMedia(filepath)); @@ -174,7 +176,7 @@ private static UploadInitiateData InitiateUploadRequest(Config.Config config, st return uploadData; } - private static void PublishPackageRequest(Config.Config config, string uploadUuid) + private static void PublishPackageRequest(Config config, string uploadUuid) { var response = HttpClient.Send(config.Api.SubmitPackage(uploadUuid)); @@ -298,10 +300,10 @@ private static void HandleRequestError( throw new PublishCommandException(); } - private static void ValidateConfig(Config.Config config, bool justReturnErrors = false) + private static void ValidateConfig(Config config, bool justReturnErrors = false) { var buildConfigErrors = BuildCommand.ValidateConfig(config, false); - var v = new Config.Validator("publish", buildConfigErrors); + var v = new Validator("publish", buildConfigErrors); v.AddIfEmpty(config.AuthConfig.AuthToken, "Auth AuthToken"); v.ThrowIfErrors(); } diff --git a/ThunderstoreCLI/Config/ProjectFileConfig.cs b/ThunderstoreCLI/Config/ProjectFileConfig.cs deleted file mode 100644 index eaa689b..0000000 --- a/ThunderstoreCLI/Config/ProjectFileConfig.cs +++ /dev/null @@ -1,181 +0,0 @@ -using ThunderstoreCLI.Models; -using static Crayon.Output; - -namespace ThunderstoreCLI.Config; - -class ProjectFileConfig : EmptyConfig -{ - - private PackageConfig? PackageMeta { get; set; } - - private BuildConfig? BuildConfig { get; set; } - - private PublishConfig? PublishConfig { get; set; } - - public override void Parse(Config currentConfig) - { - GeneralConfig = ParseGeneralConfig(tomlData); - PackageMeta = ParsePackageMeta(tomlData); - BuildConfig = ParseBuildConfig(tomlData); - PublishConfig = ParsePublishConfig(tomlData); - } - - protected static PackageConfig? ParsePackageMeta(TomlTable tomlData) - { - if (!tomlData.HasKey("package")) - return null; - - var packageMeta = tomlData["package"]; - - // TODO: Add warnings on missing values - var result = new PackageConfig() - { - Namespace = TomlUtils.SafegetString(packageMeta, "namespace"), - Name = TomlUtils.SafegetString(packageMeta, "name"), - VersionNumber = TomlUtils.SafegetString(packageMeta, "versionNumber"), - Description = TomlUtils.SafegetString(packageMeta, "description"), - WebsiteUrl = TomlUtils.SafegetString(packageMeta, "websiteUrl"), - ContainsNsfwContent = TomlUtils.SafegetBool(packageMeta, "containsNsfwContent"), - Dependencies = new() - }; - - if (packageMeta.HasKey("dependencies")) - { - var packageDependencies = packageMeta["dependencies"]; - foreach (var packageName in packageDependencies.Keys) - { - // TODO: Validate both are strings if needed? - result.Dependencies[packageName] = packageDependencies[packageName]; - } - } - - return result; - } - - protected static BuildConfig? ParseBuildConfig(TomlTable tomlData) - { - if (!tomlData.HasKey("build")) - return null; - - var buildConfig = tomlData["build"]; - - var result = new BuildConfig - { - IconPath = TomlUtils.SafegetString(buildConfig, "icon"), - ReadmePath = TomlUtils.SafegetString(buildConfig, "readme"), - OutDir = TomlUtils.SafegetString(buildConfig, "outdir"), - CopyPaths = new() - }; - - if (buildConfig.HasKey("copy")) - { - var pathSets = buildConfig["copy"]; - foreach (var entry in pathSets) - { - if (!(entry is TomlNode)) - { - ThunderstoreCLI.Write.Warn($"Unable to properly parse build config: {entry}", "Skipping entry"); - continue; - } - - var node = (TomlNode) entry; - if (!node.HasKey("source") || !node.HasKey("target")) - { - ThunderstoreCLI.Write.Warn( - $"Build config instruction is missing parameters: {node}", - "Make sure both 'source' and 'target' are defined", - "Skipping entry" - ); - continue; - } - - result.CopyPaths.Add(new CopyPathMap(node["source"], node["target"])); - } - } - return result; - } - - protected static PublishConfig? ParsePublishConfig(TomlTable tomlData) - { - if (!tomlData.HasKey("publish")) - return null; - - var publishConfig = tomlData["publish"]; - - return new PublishConfig - { - Repository = TomlUtils.SafegetString(publishConfig, "repository"), - Communities = TomlUtils.SafegetStringArray(publishConfig, "communities", Array.Empty()), - Categories = TomlUtils.SafegetStringArray(publishConfig, "categories", Array.Empty()) - }; - } - - public override PackageConfig? GetPackageMeta() - { - return PackageMeta; - } - - public override BuildConfig? GetBuildConfig() - { - return BuildConfig; - } - - public override PublishConfig? GetPublishConfig() - { - return PublishConfig; - } - - public static ThunderstoreProject? Read(Config config) - { - var configPath = config.GetProjectConfigPath(); - if (!File.Exists(configPath)) - { - ThunderstoreCLI.Write.Warn( - "Unable to find project configuration file", - $"Looked from {Dim(configPath)}" - ); - return null; - } - return ThunderstoreProject.Deserialize(configPath); - } - - public static void Write(Config config, string path) - { - var dependencies = config.PackageConfig.Dependencies ?? new Dictionary(); - var copyPaths = config.BuildConfig.CopyPaths ?? new List(); - var toml = new TomlTable - { - ["config"] = - { - ["schemaVersion"] = "0.0.1" - }, - - ["package"] = new TomlTable - { - ["namespace"] = config.PackageConfig.Namespace, - ["name"] = config.PackageConfig.Name, - ["versionNumber"] = config.PackageConfig.VersionNumber, - ["description"] = config.PackageConfig.Description, - ["websiteUrl"] = config.PackageConfig.WebsiteUrl, - ["containsNsfwContent"] = config.PackageConfig.ContainsNsfwContent, - ["dependencies"] = TomlUtils.DictToTomlTable(dependencies) - }, - - ["build"] = new TomlTable - { - ["icon"] = config.BuildConfig.IconPath, - ["readme"] = config.BuildConfig.ReadmePath, - ["outdir"] = config.BuildConfig.OutDir, - ["copy"] = TomlUtils.BuildCopyPathTable(copyPaths) - }, - - ["publish"] = new TomlTable - { - ["repository"] = config.PublishConfig.Repository, - ["communities"] = TomlUtils.FromArray(config.PublishConfig.Communities ?? new string[0]), - ["categories"] = TomlUtils.FromArray(config.PublishConfig.Categories ?? new string[0]) - } - }; - File.WriteAllText(path, TomlUtils.FormatToml(toml)); - } -} diff --git a/ThunderstoreCLI/Config/BaseConfig.cs b/ThunderstoreCLI/Configuration/BaseConfig.cs similarity index 96% rename from ThunderstoreCLI/Config/BaseConfig.cs rename to ThunderstoreCLI/Configuration/BaseConfig.cs index 9357a48..c10d8c4 100644 --- a/ThunderstoreCLI/Config/BaseConfig.cs +++ b/ThunderstoreCLI/Configuration/BaseConfig.cs @@ -1,4 +1,4 @@ -namespace ThunderstoreCLI.Config; +namespace ThunderstoreCLI.Configuration; class BaseConfig : EmptyConfig { diff --git a/ThunderstoreCLI/Config/CLIParameterConfig.cs b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs similarity index 96% rename from ThunderstoreCLI/Config/CLIParameterConfig.cs rename to ThunderstoreCLI/Configuration/CLIParameterConfig.cs index ad3d88d..40f5d3d 100644 --- a/ThunderstoreCLI/Config/CLIParameterConfig.cs +++ b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs @@ -1,6 +1,4 @@ -using ThunderstoreCLI.Options; - -namespace ThunderstoreCLI.Config; +namespace ThunderstoreCLI.Configuration; public abstract class BaseConfig : EmptyConfig where T : BaseOptions { diff --git a/ThunderstoreCLI/Config/Config.cs b/ThunderstoreCLI/Configuration/Config.cs similarity index 98% rename from ThunderstoreCLI/Config/Config.cs rename to ThunderstoreCLI/Configuration/Config.cs index 8767892..01b9336 100644 --- a/ThunderstoreCLI/Config/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -1,7 +1,7 @@ using ThunderstoreCLI.API; -using ThunderstoreCLI.Models.Publish; +using ThunderstoreCLI.Models; -namespace ThunderstoreCLI.Config; +namespace ThunderstoreCLI.Configuration; public class Config { @@ -152,6 +152,7 @@ public static void Merge(T target, T source, bool overwrite) public class GeneralConfig { public string TcliConfig { get; set; } = null!; + public string Repository { get; set; } = null!; } public class PackageConfig diff --git a/ThunderstoreCLI/Config/EmptyConfig.cs b/ThunderstoreCLI/Configuration/EmptyConfig.cs similarity index 94% rename from ThunderstoreCLI/Config/EmptyConfig.cs rename to ThunderstoreCLI/Configuration/EmptyConfig.cs index 11b2ccd..6c96b23 100644 --- a/ThunderstoreCLI/Config/EmptyConfig.cs +++ b/ThunderstoreCLI/Configuration/EmptyConfig.cs @@ -1,4 +1,4 @@ -namespace ThunderstoreCLI.Config; +namespace ThunderstoreCLI.Configuration; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute] public abstract class EmptyConfig : IConfigProvider diff --git a/ThunderstoreCLI/Config/EnvironmentConfig.cs b/ThunderstoreCLI/Configuration/EnvironmentConfig.cs similarity index 96% rename from ThunderstoreCLI/Config/EnvironmentConfig.cs rename to ThunderstoreCLI/Configuration/EnvironmentConfig.cs index cf08dcf..3d64dbf 100644 --- a/ThunderstoreCLI/Config/EnvironmentConfig.cs +++ b/ThunderstoreCLI/Configuration/EnvironmentConfig.cs @@ -1,6 +1,6 @@ using static System.Environment; -namespace ThunderstoreCLI.Config; +namespace ThunderstoreCLI.Configuration; class EnvironmentConfig : EmptyConfig { diff --git a/ThunderstoreCLI/Config/IConfigProvider.cs b/ThunderstoreCLI/Configuration/IConfigProvider.cs similarity index 87% rename from ThunderstoreCLI/Config/IConfigProvider.cs rename to ThunderstoreCLI/Configuration/IConfigProvider.cs index a3ec170..e77644c 100644 --- a/ThunderstoreCLI/Config/IConfigProvider.cs +++ b/ThunderstoreCLI/Configuration/IConfigProvider.cs @@ -1,4 +1,4 @@ -namespace ThunderstoreCLI.Config; +namespace ThunderstoreCLI.Configuration; public interface IConfigProvider { diff --git a/ThunderstoreCLI/Configuration/ProjectFileConfig.cs b/ThunderstoreCLI/Configuration/ProjectFileConfig.cs new file mode 100644 index 0000000..f4c76c3 --- /dev/null +++ b/ThunderstoreCLI/Configuration/ProjectFileConfig.cs @@ -0,0 +1,73 @@ +using ThunderstoreCLI.Models; +using static Crayon.Output; + +namespace ThunderstoreCLI.Configuration; + +internal class ProjectFileConfig : EmptyConfig +{ + private string SourcePath = null!; + private ThunderstoreProject Project = null!; + + public override void Parse(Config currentConfig) + { + SourcePath = currentConfig.GetProjectConfigPath(); + if (!File.Exists(SourcePath)) + { + Utils.Write.Warn( + "Unable to find project configuration file", + $"Looked from {Dim(SourcePath)}" + ); + Project = new ThunderstoreProject(false); + return; + } + Project = ThunderstoreProject.Deserialize(SourcePath)!; + } + + public override GeneralConfig? GetGeneralConfig() + { + return new GeneralConfig() + { + Repository = Project.Publish?.Repository! + }; + } + + public override PackageConfig? GetPackageMeta() + { + return new PackageConfig() + { + Namespace = Project.Package?.Namespace, + Name = Project.Package?.Name, + VersionNumber = Project.Package?.VersionNumber, + ProjectConfigPath = SourcePath, + Description = Project.Package?.Description, + Dependencies = Project.Package?.Dependencies, + ContainsNsfwContent = Project.Package?.ContainsNsfwContent, + WebsiteUrl = Project.Package?.WebsiteUrl + }; + } + + public override BuildConfig? GetBuildConfig() + { + return new BuildConfig() + { + CopyPaths = Project.Build?.CopyPaths.Select(static path => new CopyPathMap(path.Source, path.Target)).ToList(), + IconPath = Project.Build?.Icon, + OutDir = Project.Build?.OutDir, + ReadmePath = Project.Build?.Readme + }; + } + + public override PublishConfig? GetPublishConfig() + { + return new PublishConfig() + { + Categories = Project.Publish?.Categories, + Communities = Project.Publish?.Communities + }; + } + + public static void Write(Config config, string path) + { + File.WriteAllText(path, new ThunderstoreProject(config).Serialize()); + } +} diff --git a/ThunderstoreCLI/Config/Validator.cs b/ThunderstoreCLI/Configuration/Validator.cs similarity index 96% rename from ThunderstoreCLI/Config/Validator.cs rename to ThunderstoreCLI/Configuration/Validator.cs index 9c1a67a..f39b696 100644 --- a/ThunderstoreCLI/Config/Validator.cs +++ b/ThunderstoreCLI/Configuration/Validator.cs @@ -1,4 +1,6 @@ -namespace ThunderstoreCLI.Config; +using ThunderstoreCLI.Utils; + +namespace ThunderstoreCLI.Configuration; /// Helper for validating command-specific configurations public class Validator diff --git a/ThunderstoreCLI/Models/PublishModels.cs b/ThunderstoreCLI/Models/PublishModels.cs index 0767da5..79b9288 100644 --- a/ThunderstoreCLI/Models/PublishModels.cs +++ b/ThunderstoreCLI/Models/PublishModels.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace ThunderstoreCLI.Models.Publish; +namespace ThunderstoreCLI.Models; public class PackageUploadMetadata : BaseJson { diff --git a/ThunderstoreCLI/Models/ThunderstoreProject.cs b/ThunderstoreCLI/Models/ThunderstoreProject.cs index 54951f2..9f12210 100644 --- a/ThunderstoreCLI/Models/ThunderstoreProject.cs +++ b/ThunderstoreCLI/Models/ThunderstoreProject.cs @@ -1,3 +1,4 @@ +using ThunderstoreCLI.Configuration; using Tomlet.Attributes; namespace ThunderstoreCLI.Models; @@ -11,8 +12,9 @@ public class ConfigData [TomlProperty("schemaVersion")] public string SchemaVersion { get; set; } = "0.0.1"; } + [TomlProperty("config")] - public ConfigData? Config { get; set; } + public ConfigData? Config { get; set; } = new(); [TomlDoNotInlineObject] public class PackageData @@ -57,10 +59,7 @@ public class CopyPath public string Target { get; set; } = ""; } [TomlProperty("copy")] - public CopyPath[] CopyPaths { get; set; } = - { - new() - }; + public CopyPath[] CopyPaths { get; set; } = Array.Empty(); } [TomlProperty("build")] public BuildData? Build { get; set; } @@ -91,9 +90,30 @@ public ThunderstoreProject(bool initialize) if (!initialize) return; - Config = new(); - Package = new(); - Build = new(); - Publish = new(); + Package = new PackageData(); + Build = new BuildData(); + Publish = new PublishData(); + } + + public ThunderstoreProject(Config config) + { + Package = new PackageData() + { + Namespace = config.PackageConfig.Namespace!, + Name = config.PackageConfig.Name! + }; + Build = new BuildData() + { + Icon = config.GetPackageIconPath(), + OutDir = config.GetBuildOutputDir(), + Readme = config.GetPackageReadmePath(), + CopyPaths = config.BuildConfig.CopyPaths!.Select(x => new BuildData.CopyPath { Source = x.From, Target = x.To }).ToArray()! + }; + Publish = new PublishData() + { + Categories = config.PublishConfig.Categories!, + Communities = config.PublishConfig.Communities!, + Repository = config.GeneralConfig.Repository + }; } } diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index da48274..d457814 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -1,9 +1,11 @@ using CommandLine; using ThunderstoreCLI.Commands; +using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Models.Interaction; +using ThunderstoreCLI.Utils; using static Crayon.Output; -namespace ThunderstoreCLI.Options; +namespace ThunderstoreCLI; /// Options are arguments passed from command line. public abstract class BaseOptions @@ -89,7 +91,7 @@ public class InitOptions : PackageOptions public override int Execute() { - return InitCommand.Run(Config.Config.FromCLI(new Config.CLIInitCommandConfig(this))); + return InitCommand.Run(Config.FromCLI(new CLIInitCommandConfig(this))); } } @@ -98,7 +100,7 @@ public class BuildOptions : PackageOptions { public override int Execute() { - return BuildCommand.Run(Config.Config.FromCLI(new Config.CLIBuildCommandConfig(this))); + return BuildCommand.Run(Config.FromCLI(new CLIBuildCommandConfig(this))); } } @@ -139,6 +141,6 @@ public override bool Validate() public override int Execute() { - return PublishCommand.Run(Config.Config.FromCLI(new Config.CLIPublishCommandConfig(this))); + return PublishCommand.Run(Config.FromCLI(new CLIPublishCommandConfig(this))); } } diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index c2adbce..b0e9f74 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -1,7 +1,6 @@ using System.Diagnostics; using CommandLine; using ThunderstoreCLI.Models; -using ThunderstoreCLI.Options; namespace ThunderstoreCLI; diff --git a/ThunderstoreCLI/ThunderstoreCLI.csproj b/ThunderstoreCLI/ThunderstoreCLI.csproj index 1953c10..a7f72f1 100644 --- a/ThunderstoreCLI/ThunderstoreCLI.csproj +++ b/ThunderstoreCLI/ThunderstoreCLI.csproj @@ -3,6 +3,8 @@ Exe net6.0 + preview + true ThunderstoreCLI tcli ThunderstoreCLI.Program diff --git a/ThunderstoreCLI/UpdateChecker.cs b/ThunderstoreCLI/UpdateChecker.cs index b5f1a01..909b16e 100644 --- a/ThunderstoreCLI/UpdateChecker.cs +++ b/ThunderstoreCLI/UpdateChecker.cs @@ -1,3 +1,5 @@ +using ThunderstoreCLI.Utils; + namespace ThunderstoreCLI; public static class UpdateChecker diff --git a/ThunderstoreCLI/Utils/Comparers.cs b/ThunderstoreCLI/Utils/Comparers.cs index 9657df9..465bb08 100644 --- a/ThunderstoreCLI/Utils/Comparers.cs +++ b/ThunderstoreCLI/Utils/Comparers.cs @@ -1,4 +1,4 @@ -namespace ThunderstoreCLI.Comparers; +namespace ThunderstoreCLI.Utils; public class SemVer : IComparer { diff --git a/ThunderstoreCLI/Utils/MiscUtils.cs b/ThunderstoreCLI/Utils/MiscUtils.cs index 37f57ba..024f80c 100644 --- a/ThunderstoreCLI/Utils/MiscUtils.cs +++ b/ThunderstoreCLI/Utils/MiscUtils.cs @@ -1,7 +1,7 @@ using System.Reflection; using System.Text.RegularExpressions; -namespace ThunderstoreCLI; +namespace ThunderstoreCLI.Utils; public static class MiscUtils { @@ -49,7 +49,7 @@ public static int[] ParseLatestVersion(string releaseJsonData) return matches .Select(match => match.Groups[1].ToString().Split('.')) .Select(ver => ver.Select(part => Int32.Parse(part)).ToArray()) - .OrderByDescending(ver => ver, new Comparers.SemVer()) + .OrderByDescending(ver => ver, new SemVer()) .First(); } diff --git a/ThunderstoreCLI/Utils/Spinner.cs b/ThunderstoreCLI/Utils/Spinner.cs index 004b7e0..22c1ab5 100644 --- a/ThunderstoreCLI/Utils/Spinner.cs +++ b/ThunderstoreCLI/Utils/Spinner.cs @@ -1,6 +1,6 @@ using static Crayon.Output; -namespace ThunderstoreCLI; +namespace ThunderstoreCLI.Utils; public class ProgressSpinner { diff --git a/ThunderstoreCLI/Utils/StringUtils.cs b/ThunderstoreCLI/Utils/StringUtils.cs index bd23464..5a53a1f 100644 --- a/ThunderstoreCLI/Utils/StringUtils.cs +++ b/ThunderstoreCLI/Utils/StringUtils.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace ThunderstoreCLI; +namespace ThunderstoreCLI.Utils; public static class StringUtils { diff --git a/ThunderstoreCLI/Utils/Write.cs b/ThunderstoreCLI/Utils/Write.cs index caea958..a015a2a 100644 --- a/ThunderstoreCLI/Utils/Write.cs +++ b/ThunderstoreCLI/Utils/Write.cs @@ -1,6 +1,6 @@ using static Crayon.Output; -namespace ThunderstoreCLI; +namespace ThunderstoreCLI.Utils; public static class Write { From 089b14443b9ac2a0e9b6e1099a9030db1a8d7e3f Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 11 Oct 2022 23:27:51 -0500 Subject: [PATCH 11/91] Don't parallelize tests that interact with console --- ThunderstoreCLI.Tests/NoParallel.cs | 6 ++++++ ThunderstoreCLI.Tests/Utils/Spinner.cs | 1 + ThunderstoreCLI.Tests/Utils/Write.cs | 1 + 3 files changed, 8 insertions(+) create mode 100644 ThunderstoreCLI.Tests/NoParallel.cs diff --git a/ThunderstoreCLI.Tests/NoParallel.cs b/ThunderstoreCLI.Tests/NoParallel.cs new file mode 100644 index 0000000..ba411c3 --- /dev/null +++ b/ThunderstoreCLI.Tests/NoParallel.cs @@ -0,0 +1,6 @@ +using Xunit; + +namespace ThunderstoreCLI.Tests; + +[CollectionDefinition(nameof(NoParallel), DisableParallelization = true)] +public sealed class NoParallel { } diff --git a/ThunderstoreCLI.Tests/Utils/Spinner.cs b/ThunderstoreCLI.Tests/Utils/Spinner.cs index 09e3fd4..e8258ce 100644 --- a/ThunderstoreCLI.Tests/Utils/Spinner.cs +++ b/ThunderstoreCLI.Tests/Utils/Spinner.cs @@ -5,6 +5,7 @@ namespace ThunderstoreCLI.Tests.Utils; +[Collection(nameof(NoParallel))] public class ThunderstoreCLI_ProgresSpinner { private async Task CreateTask(bool isSuccess, int delay = 1) diff --git a/ThunderstoreCLI.Tests/Utils/Write.cs b/ThunderstoreCLI.Tests/Utils/Write.cs index d8e241d..65d3d79 100644 --- a/ThunderstoreCLI.Tests/Utils/Write.cs +++ b/ThunderstoreCLI.Tests/Utils/Write.cs @@ -27,6 +27,7 @@ public void Dispose() } } +[Collection(nameof(NoParallel))] public class ThunderstoreCLI_Write { // ANSI escape codes From ffa12a76eb7e98037c832d0817e8677976f2f2e5 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Fri, 4 Nov 2022 18:36:18 -0500 Subject: [PATCH 12/91] Add missing bug fixes from master --- ThunderstoreCLI/Commands/InitCommand.cs | 3 ++- ThunderstoreCLI/Configuration/Config.cs | 2 +- ThunderstoreCLI/Configuration/ProjectFileConfig.cs | 2 +- ThunderstoreCLI/Models/PublishModels.cs | 9 ++++++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ThunderstoreCLI/Commands/InitCommand.cs b/ThunderstoreCLI/Commands/InitCommand.cs index 7747506..923be6e 100644 --- a/ThunderstoreCLI/Commands/InitCommand.cs +++ b/ThunderstoreCLI/Commands/InitCommand.cs @@ -1,4 +1,5 @@ using ThunderstoreCLI.Configuration; +using ThunderstoreCLI.Models; using ThunderstoreCLI.Utils; namespace ThunderstoreCLI.Commands; @@ -37,7 +38,7 @@ public static int Run(Config config) { Write.Line($"Project configuration already exists, overwriting"); } - ProjectFileConfig.Write(config, path); + File.WriteAllText(path, new ThunderstoreProject(true).Serialize()); var iconPath = config.GetPackageIconPath(); if (File.Exists(iconPath)) diff --git a/ThunderstoreCLI/Configuration/Config.cs b/ThunderstoreCLI/Configuration/Config.cs index 01b9336..4c36610 100644 --- a/ThunderstoreCLI/Configuration/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -45,7 +45,7 @@ public static Config FromCLI(IConfigProvider cliConfig) public string GetProjectRelativePath(string path) { - return Path.GetFullPath(Path.Join(GetProjectBasePath(), path)); + return Path.Join(GetProjectBasePath(), path); } public string GetPackageIconPath() diff --git a/ThunderstoreCLI/Configuration/ProjectFileConfig.cs b/ThunderstoreCLI/Configuration/ProjectFileConfig.cs index f4c76c3..731cf38 100644 --- a/ThunderstoreCLI/Configuration/ProjectFileConfig.cs +++ b/ThunderstoreCLI/Configuration/ProjectFileConfig.cs @@ -20,7 +20,7 @@ public override void Parse(Config currentConfig) Project = new ThunderstoreProject(false); return; } - Project = ThunderstoreProject.Deserialize(SourcePath)!; + Project = ThunderstoreProject.Deserialize(File.ReadAllText(SourcePath))!; } public override GeneralConfig? GetGeneralConfig() diff --git a/ThunderstoreCLI/Models/PublishModels.cs b/ThunderstoreCLI/Models/PublishModels.cs index 79b9288..8ec7fb4 100644 --- a/ThunderstoreCLI/Models/PublishModels.cs +++ b/ThunderstoreCLI/Models/PublishModels.cs @@ -86,9 +86,16 @@ public class CommunityData public bool RequirePackageListingApproval { get; set; } } + public class CategoryData + { + [JsonProperty("name")] public string? Name { get; set; } + + [JsonProperty("slug")] public string? Slug { get; set; } + } + [JsonProperty("community")] public CommunityData? Community { get; set; } - [JsonProperty("categories")] public List? Categories { get; set; } + [JsonProperty("categories")] public List? Categories { get; set; } [JsonProperty("url")] public string? Url { get; set; } } From ebc4bdc612177c5d2f0ec5dcc4534102170f76f3 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 7 Nov 2022 14:24:13 -0600 Subject: [PATCH 13/91] Remove launchSettings.json --- ThunderstoreCLI/Properties/launchSettings.json | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 ThunderstoreCLI/Properties/launchSettings.json diff --git a/ThunderstoreCLI/Properties/launchSettings.json b/ThunderstoreCLI/Properties/launchSettings.json deleted file mode 100644 index 9453f85..0000000 --- a/ThunderstoreCLI/Properties/launchSettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "profiles": { - "ThunderstoreCLI": { - "commandName": "Project", - "commandLineArgs": "publish --config-path test/thunderstore.toml" - } - } -} From 99cf21b2c4477e68779a5d617067cde69d34420e Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 14 Nov 2022 07:14:06 -0600 Subject: [PATCH 14/91] Address review comments --- ThunderstoreCLI/Models/ThunderstoreProject.cs | 2 +- ThunderstoreCLI/Program.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/ThunderstoreCLI/Models/ThunderstoreProject.cs b/ThunderstoreCLI/Models/ThunderstoreProject.cs index 9f12210..f39ab60 100644 --- a/ThunderstoreCLI/Models/ThunderstoreProject.cs +++ b/ThunderstoreCLI/Models/ThunderstoreProject.cs @@ -69,7 +69,7 @@ public class PublishData { [TomlProperty("repository")] public string Repository { get; set; } = "https://thunderstore.io"; - [TomlProperty("communtities")] + [TomlProperty("communities")] public string[] Communities { get; set; } = { "riskofrain2" diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index b0e9f74..bd6d660 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -13,8 +13,6 @@ private static int Main(string[] args) while (!Debugger.IsAttached) { } #endif - Console.WriteLine(ThunderstoreProject.Deserialize(File.ReadAllText("thunderstore.toml"))!.Serialize()); - return 0; var updateChecker = UpdateChecker.CheckForUpdates(); var exitCode = Parser.Default.ParseArguments(args) From 2cf7416da871f3e63b7de86e6a3866962d888f8b Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Thu, 24 Nov 2022 22:14:14 -0600 Subject: [PATCH 15/91] Upgrade to .NET 7 --- .github/workflows/publish.yml | 6 ++-- .github/workflows/release.yml | 29 ++++++++++--------- .github/workflows/test.yml | 8 ++--- .../ThunderstoreCLI.Tests.csproj | 7 ++--- ThunderstoreCLI/ThunderstoreCLI.csproj | 15 +++++----- 5 files changed, 32 insertions(+), 33 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index af3760c..38aefe1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,16 +2,16 @@ name: Publish on: release: - types: [released] + types: [released, prereleased] jobs: nuget: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0.x' + dotnet-version: '7' - name: Fetch Latest .nupkg uses: dsaltares/fetch-gh-release-asset@0efe227dedb360b09ea0e533795d584b61c461a9 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 94e9af3..0007180 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,26 +27,26 @@ jobs: platform-binary: needs: validate-tag if: github.event.base_ref == 'refs/heads/master' - name: Create binary ${{ matrix.os }} + name: Create binary ${{ matrix.target }} runs-on: ${{ matrix.os }} strategy: matrix: kind: ['linux', 'windows', 'macOS'] include: - kind: linux - os: ubuntu-latest target: linux-x64 + os: ubuntu-latest - kind: windows - os: windows-latest target: win-x64 + os: windows-latest - kind: macOS - os: macos-latest target: osx-x64 + os: macos-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0.x' + dotnet-version: '7' - id: tag uses: dawidd6/action-get-tag@v1 - name: Install dependencies @@ -56,15 +56,15 @@ jobs: shell: bash run: | release_name="tcli-${{ steps.tag.outputs.tag }}-${{ matrix.target }}" - dotnet publish ThunderstoreCLI/ThunderstoreCLI.csproj -c Release -r "${{ matrix.target }}" -o "$release_name" -p:PublishReadyToRun=true + dotnet publish ThunderstoreCLI/ThunderstoreCLI.csproj -c Release -r "${{ matrix.target }}" -o "${release_name}" if [ "${{ matrix.target }}" == "win-x64" ]; then - 7z a -tzip "${release_name}.zip" "./${release_name}/*" + 7z a -tzip "${release_name}.zip" "./${release_name}" else - tar czvf "${release_name}.tar.gz" "$release_name" + tar czvf "${release_name}.tar.gz" "./${release_name}" fi - rm -r "$release_name" + rm -r ${release_name} - name: Publish to GitHub uses: softprops/action-gh-release@v1 @@ -82,9 +82,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0.x' + dotnet-version: '7' - id: tag uses: dawidd6/action-get-tag@v1 - name: Install dependencies @@ -92,10 +92,13 @@ jobs: - name: Build shell: bash - run: dotnet pack ThunderstoreCLI/ThunderstoreCLI.csproj -c Release -o "." -p:SelfContained=false -p:PublishTrimmed=false -p:PublishSingleFile=false -p:StartupObject="" -p:RuntimeIdentifier="" -p:PublishReadyToRun=false -p:PackAsTool=true + run: dotnet pack ThunderstoreCLI/ThunderstoreCLI.csproj -c Release -o "." -p:PublishSelfContained=false -p:PublishSingleFile=false -p:PublishTrimmed=false -p:PublishReadyToRun=false - name: Publish to GitHub uses: softprops/action-gh-release@v1 with: files: "tcli*" + name: "Thunderstore CLI ${{ steps.tag.outputs.tag }}" + body_path: ${{ github.workspace }}/.github/RELEASE_TEMPLATE.md draft: true + prerelease: ${{ startsWith(steps.tag.outputs.tag, '0.') }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1486f95..52e5b17 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,9 +8,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0.x' + dotnet-version: '7' - uses: actions/setup-python@v2 with: python-version: '3.8' @@ -31,9 +31,9 @@ jobs: OS: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0.x' + dotnet-version: '7' - name: Install dependencies run: dotnet restore - name: Build diff --git a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj index d7207eb..0606282 100644 --- a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj +++ b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj @@ -1,17 +1,14 @@ - net6.0 + net7.0 false - win-x64 - latest - true - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ThunderstoreCLI/ThunderstoreCLI.csproj b/ThunderstoreCLI/ThunderstoreCLI.csproj index a7f72f1..c132a8f 100644 --- a/ThunderstoreCLI/ThunderstoreCLI.csproj +++ b/ThunderstoreCLI/ThunderstoreCLI.csproj @@ -2,9 +2,8 @@ Exe - net6.0 - preview - true + net7.0 + Major ThunderstoreCLI tcli ThunderstoreCLI.Program @@ -12,15 +11,15 @@ Thunderstore CLI https://thunderstore.io/ Thunderstore CLI is a command-line utility for building and uploading packages to Thunderstore - win-x64 - true true - true + partial true + true + true true enable None - latest + true $(AssemblyName) enable @@ -33,7 +32,7 @@ all - + From ca1894356a2c0dc5174e992aee9460d246b5ef64 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sat, 26 Nov 2022 03:16:45 -0600 Subject: [PATCH 16/91] Multitarget NET 6 --- .github/workflows/release.yml | 2 +- ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj | 4 +++- ThunderstoreCLI/Configuration/Config.cs | 3 ++- ThunderstoreCLI/Models/BaseJson.cs | 2 +- ThunderstoreCLI/Models/ISerialize.cs | 2 ++ ThunderstoreCLI/Models/Interaction/BaseInteraction.cs | 4 +++- ThunderstoreCLI/ThunderstoreCLI.csproj | 3 ++- 7 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0007180..f38d3b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: shell: bash run: | release_name="tcli-${{ steps.tag.outputs.tag }}-${{ matrix.target }}" - dotnet publish ThunderstoreCLI/ThunderstoreCLI.csproj -c Release -r "${{ matrix.target }}" -o "${release_name}" + dotnet publish ThunderstoreCLI/ThunderstoreCLI.csproj -c Release -r "${{ matrix.target }}" -f net7.0 -o "${release_name}" if [ "${{ matrix.target }}" == "win-x64" ]; then 7z a -tzip "${release_name}.zip" "./${release_name}" diff --git a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj index 0606282..4994f5b 100644 --- a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj +++ b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj @@ -1,7 +1,9 @@ - net7.0 + net7.0;net6.0 + Major + 11 false diff --git a/ThunderstoreCLI/Configuration/Config.cs b/ThunderstoreCLI/Configuration/Config.cs index 4c36610..254f9cc 100644 --- a/ThunderstoreCLI/Configuration/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using ThunderstoreCLI.API; using ThunderstoreCLI.Models; @@ -128,7 +129,7 @@ public static Config Parse(IConfigProvider[] configProviders) return result; } - public static void Merge(T target, T source, bool overwrite) + public static void Merge<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T target, T source, bool overwrite) { if (source == null) return; diff --git a/ThunderstoreCLI/Models/BaseJson.cs b/ThunderstoreCLI/Models/BaseJson.cs index 06115e7..829a3d2 100644 --- a/ThunderstoreCLI/Models/BaseJson.cs +++ b/ThunderstoreCLI/Models/BaseJson.cs @@ -42,7 +42,7 @@ public static class BaseJson public static class BaseJsonExtensions { - public static string SerializeList(this List list, JsonSerializerSettings? options = null) + public static string SerializeList<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this List list, JsonSerializerSettings? options = null) where T : BaseJson { return JsonConvert.SerializeObject(list, options); diff --git a/ThunderstoreCLI/Models/ISerialize.cs b/ThunderstoreCLI/Models/ISerialize.cs index 736b9c9..8cdd635 100644 --- a/ThunderstoreCLI/Models/ISerialize.cs +++ b/ThunderstoreCLI/Models/ISerialize.cs @@ -6,7 +6,9 @@ public interface ISerialize<[DynamicallyAccessedMembers(DynamicallyAccessedMembe where T : ISerialize { public string Serialize(); +#if NET7_0 public static abstract T? Deserialize(string input); public static abstract ValueTask DeserializeAsync(string input); public static abstract ValueTask DeserializeAsync(Stream input); +#endif } diff --git a/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs b/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs index a7e22ba..e734375 100644 --- a/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs +++ b/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace ThunderstoreCLI.Models.Interaction; public enum InteractionOutputType @@ -11,7 +13,7 @@ public static class InteractionOptions public static InteractionOutputType OutputType { get; set; } = InteractionOutputType.HUMAN; } -public abstract class BaseInteraction : BaseJson +public abstract class BaseInteraction<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : BaseJson where T : BaseInteraction { public abstract string GetHumanString(); diff --git a/ThunderstoreCLI/ThunderstoreCLI.csproj b/ThunderstoreCLI/ThunderstoreCLI.csproj index c132a8f..7176c8a 100644 --- a/ThunderstoreCLI/ThunderstoreCLI.csproj +++ b/ThunderstoreCLI/ThunderstoreCLI.csproj @@ -2,7 +2,8 @@ Exe - net7.0 + net7.0;net6.0 + 11 Major ThunderstoreCLI tcli From e399c41e828240e8672561f8c935c9d57b73a355 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sat, 26 Nov 2022 03:21:54 -0600 Subject: [PATCH 17/91] Fix spinner test failure --- ThunderstoreCLI/Utils/Spinner.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/ThunderstoreCLI/Utils/Spinner.cs b/ThunderstoreCLI/Utils/Spinner.cs index 22c1ab5..3031e64 100644 --- a/ThunderstoreCLI/Utils/Spinner.cs +++ b/ThunderstoreCLI/Utils/Spinner.cs @@ -66,6 +66,7 @@ public async Task Start() if (completed == _tasks.Length) { Write.Empty(); + await Task.WhenAll(_tasks); return; } From 2de1919d3441036d0a05611adc69ede22f4d9a94 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sat, 26 Nov 2022 21:22:45 -0600 Subject: [PATCH 18/91] Fix build command on net7.0 --- ThunderstoreCLI/Configuration/CLIParameterConfig.cs | 4 +++- ThunderstoreCLI/Configuration/Config.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs index 40f5d3d..9bc8fac 100644 --- a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs +++ b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs @@ -18,7 +18,9 @@ public override GeneralConfig GetGeneralConfig() } } -public abstract class CLIParameterConfig : BaseConfig where T : PackageOptions +public interface CLIConfig { } + +public abstract class CLIParameterConfig : BaseConfig, CLIConfig where T : PackageOptions { public CLIParameterConfig(T opts) : base(opts) { } diff --git a/ThunderstoreCLI/Configuration/Config.cs b/ThunderstoreCLI/Configuration/Config.cs index 254f9cc..28d3aac 100644 --- a/ThunderstoreCLI/Configuration/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -33,7 +33,7 @@ public static Config FromCLI(IConfigProvider cliConfig) List providers = new(); providers.Add(cliConfig); providers.Add(new EnvironmentConfig()); - if (cliConfig.GetType().IsSubclassOf(typeof(CLIParameterConfig<>))) + if (cliConfig is CLIConfig) providers.Add(new ProjectFileConfig()); providers.Add(new BaseConfig()); return Parse(providers.ToArray()); From 4cb3441af8ed8a8f354cd2f0ef9864ae9a920d53 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 17 Jan 2022 20:03:01 -0600 Subject: [PATCH 19/91] Add some structure for mod management --- ThunderstoreCLI/Game/GameDefinition.cs | 36 ++++++++++++++++++++++++++ ThunderstoreCLI/Game/InstallHelpers.cs | 6 +++++ ThunderstoreCLI/Game/ModManager.cs | 14 ++++++++++ ThunderstoreCLI/Game/ModProfile.cs | 26 +++++++++++++++++++ ThunderstoreCLI/Options.cs | 5 ++++ 5 files changed, 87 insertions(+) create mode 100644 ThunderstoreCLI/Game/GameDefinition.cs create mode 100644 ThunderstoreCLI/Game/InstallHelpers.cs create mode 100644 ThunderstoreCLI/Game/ModManager.cs create mode 100644 ThunderstoreCLI/Game/ModProfile.cs diff --git a/ThunderstoreCLI/Game/GameDefinition.cs b/ThunderstoreCLI/Game/GameDefinition.cs new file mode 100644 index 0000000..9f8ab45 --- /dev/null +++ b/ThunderstoreCLI/Game/GameDefinition.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using ThunderstoreCLI.Models; + +namespace ThunderstoreCLI.Game; + +public class GameDefinition : BaseJson +{ + private const string FILE_NAME = "GameDefintions.json"; + public string Identifier { get; } + public string Name { get; } + public string ModManager { get; } + public string InstallDirectory { get; } + public List Profiles { get; private set; } = new(); + public ModProfile GlobalProfile { get; } + + internal GameDefinition(string id, string name, string modManager, string tcliDirectory) + { + Identifier = id; + Name = name; + ModManager = modManager; + GlobalProfile = new ModProfile(this, true, "Global", tcliDirectory); + // TODO: actually find install dir instead of manually setting the path in json + // yes im lazy + } + + public static List GetGameDefinitions(string tcliDirectory) + { + return DeserializeList(File.ReadAllText(Path.Combine(tcliDirectory, FILE_NAME))) ?? new(); + } + + public static void SetGameDefinitions(string tcliDirectory, List list) + { + File.WriteAllText(Path.Combine(tcliDirectory, FILE_NAME), list.SerializeList(BaseJson.IndentedSettings)); + } +} diff --git a/ThunderstoreCLI/Game/InstallHelpers.cs b/ThunderstoreCLI/Game/InstallHelpers.cs new file mode 100644 index 0000000..6945e1e --- /dev/null +++ b/ThunderstoreCLI/Game/InstallHelpers.cs @@ -0,0 +1,6 @@ +namespace ThunderstoreCLI.Game; + +public static class InstallHelpers +{ + +} diff --git a/ThunderstoreCLI/Game/ModManager.cs b/ThunderstoreCLI/Game/ModManager.cs new file mode 100644 index 0000000..d024c23 --- /dev/null +++ b/ThunderstoreCLI/Game/ModManager.cs @@ -0,0 +1,14 @@ +using System.IO.Compression; + +namespace ThunderstoreCLI.Game; + +public interface ModManager +{ + public static abstract string Identifier { get; } + public bool SupportsProfiles(GameDefinition gameDef); + public void InstallLoader(GameDefinition gameDef, ModProfile profile, PackageManifestV1 manifest, ZipArchive loaderArchive); + public void UninstallLoader(GameDefinition gameDef, ModProfile profile); + public void InstallMod(GameDefinition gameDef, ModProfile profile, PackageManifestV1 manifest, ZipArchive modArchive); + public void UninstallMod(GameDefinition gameDef, ModProfile profile, PackageManifestV1 manifest); + public int RunGame(GameDefinition gameDef, ModProfile profile); +} diff --git a/ThunderstoreCLI/Game/ModProfile.cs b/ThunderstoreCLI/Game/ModProfile.cs new file mode 100644 index 0000000..afb877c --- /dev/null +++ b/ThunderstoreCLI/Game/ModProfile.cs @@ -0,0 +1,26 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using ThunderstoreCLI.Models; + +namespace ThunderstoreCLI.Game; + +public class ModProfile : BaseJson +{ + public bool IsGlobal { get; } + public string Name { get; } + public string ProfileDirectory { get; } + public List InstalledMods { get; set; } = new(); + + internal ModProfile(GameDefinition gameDef, bool global, string name, string tcliDirectory) + { + IsGlobal = global; + Name = name; + + var directory = Path.Combine(tcliDirectory, "Profiles", gameDef.Identifier, name); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + ProfileDirectory = directory; + } +} diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index d457814..f1fff12 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -77,6 +77,11 @@ public override bool Validate() return false; } + if (!Directory.Exists(TcliDirectory)) + { + Directory.CreateDirectory(TcliDirectory!); + } + return true; } } From 3b2fcc1f58b397490d94d8d0ea29033a9e8a2e43 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 31 Jan 2022 22:13:37 -0600 Subject: [PATCH 20/91] Add install command, config, and options --- ThunderstoreCLI/Commands/InstallCommand.cs | 20 +++++++++++++++++++ .../Configuration/CLIParameterConfig.cs | 13 ++++++++++++ ThunderstoreCLI/Configuration/Config.cs | 12 +++++++++-- ThunderstoreCLI/Configuration/EmptyConfig.cs | 5 +++++ .../Configuration/IConfigProvider.cs | 1 + ThunderstoreCLI/Options.cs | 11 ++++++++++ ThunderstoreCLI/Plugins/PluginManager.cs | 11 ++++++++++ 7 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 ThunderstoreCLI/Commands/InstallCommand.cs create mode 100644 ThunderstoreCLI/Plugins/PluginManager.cs diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs new file mode 100644 index 0000000..fba2348 --- /dev/null +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -0,0 +1,20 @@ +using ThunderstoreCLI.Configuration; +using ThunderstoreCLI.Game; +using ThunderstoreCLI.Plugins; + +namespace ThunderstoreCLI.Commands; + +public static class InstallCommand +{ + public static int Run(Config config) + { + throw new NotImplementedException(); + } + + public static int InstallLoader(Config config) + { + var managerTypes = PluginManager.GetAllOfType(); + + throw new NotImplementedException(); + } +} diff --git a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs index 9bc8fac..68a7795 100644 --- a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs +++ b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs @@ -77,3 +77,16 @@ public override AuthConfig GetAuthConfig() }; } } + +public class CLIInstallCommandConfig : CLIParameterConfig +{ + public CLIInstallCommandConfig(InstallOptions options) : base(options) { } + + public override InstallConfig? GetInstallConfig() + { + return new InstallConfig() + { + + }; + } +} diff --git a/ThunderstoreCLI/Configuration/Config.cs b/ThunderstoreCLI/Configuration/Config.cs index 28d3aac..22566c5 100644 --- a/ThunderstoreCLI/Configuration/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -13,12 +13,13 @@ public class Config public BuildConfig BuildConfig { get; private set; } public PublishConfig PublishConfig { get; private set; } public AuthConfig AuthConfig { get; private set; } + public InstallConfig InstallConfig { get; private set; } // ReSharper restore AutoPropertyCanBeMadeGetOnly.Local private readonly Lazy api; public ApiHelper Api => api.Value; - private Config(GeneralConfig generalConfig, PackageConfig packageConfig, InitConfig initConfig, BuildConfig buildConfig, PublishConfig publishConfig, AuthConfig authConfig) + private Config(GeneralConfig generalConfig, PackageConfig packageConfig, InitConfig initConfig, BuildConfig buildConfig, PublishConfig publishConfig, AuthConfig authConfig, InstallConfig installConfig) { api = new Lazy(() => new ApiHelper(this)); GeneralConfig = generalConfig; @@ -27,6 +28,7 @@ private Config(GeneralConfig generalConfig, PackageConfig packageConfig, InitCon BuildConfig = buildConfig; PublishConfig = publishConfig; AuthConfig = authConfig; + InstallConfig = installConfig; } public static Config FromCLI(IConfigProvider cliConfig) { @@ -115,7 +117,8 @@ public static Config Parse(IConfigProvider[] configProviders) var buildConfig = new BuildConfig(); var publishConfig = new PublishConfig(); var authConfig = new AuthConfig(); - var result = new Config(generalConfig, packageMeta, initConfig, buildConfig, publishConfig, authConfig); + var installConfig = new InstallConfig(); + var result = new Config(generalConfig, packageMeta, initConfig, buildConfig, publishConfig, authConfig, installConfig); foreach (var provider in configProviders) { provider.Parse(result); @@ -210,3 +213,8 @@ public class AuthConfig { public string? AuthToken { get; set; } } + +public class InstallConfig +{ + public string? ManagerIdentifier { get; set; } +} diff --git a/ThunderstoreCLI/Configuration/EmptyConfig.cs b/ThunderstoreCLI/Configuration/EmptyConfig.cs index 6c96b23..d4c23e8 100644 --- a/ThunderstoreCLI/Configuration/EmptyConfig.cs +++ b/ThunderstoreCLI/Configuration/EmptyConfig.cs @@ -33,4 +33,9 @@ public virtual void Parse(Config currentConfig) { } { return null; } + + public virtual InstallConfig? GetInstallConfig() + { + return null; + } } diff --git a/ThunderstoreCLI/Configuration/IConfigProvider.cs b/ThunderstoreCLI/Configuration/IConfigProvider.cs index e77644c..587e32c 100644 --- a/ThunderstoreCLI/Configuration/IConfigProvider.cs +++ b/ThunderstoreCLI/Configuration/IConfigProvider.cs @@ -10,4 +10,5 @@ public interface IConfigProvider BuildConfig? GetBuildConfig(); PublishConfig? GetPublishConfig(); AuthConfig? GetAuthConfig(); + InstallConfig? GetInstallConfig(); } diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index f1fff12..d2de2fb 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -149,3 +149,14 @@ public override int Execute() return PublishCommand.Run(Config.FromCLI(new CLIPublishCommandConfig(this))); } } + +[Verb("install", HelpText = "Installs a modloader or mod")] +public class InstallOptions : PackageOptions +{ + public string? ManagerId { get; set; } + + public override int Execute() + { + return InstallCommand.Run(Config.FromCLI(new CLIInstallCommandConfig(this))); + } +} diff --git a/ThunderstoreCLI/Plugins/PluginManager.cs b/ThunderstoreCLI/Plugins/PluginManager.cs new file mode 100644 index 0000000..be7ede4 --- /dev/null +++ b/ThunderstoreCLI/Plugins/PluginManager.cs @@ -0,0 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + +namespace ThunderstoreCLI.Plugins; + +public static class PluginManager +{ + public static List GetAllOfType<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() + { + return typeof(PluginManager).Assembly.GetTypes().Where(x => x.IsAssignableTo(typeof(T))).ToList(); + } +} From 14e77e989cc6573ee7226238c6121688f5e1f60a Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sat, 14 May 2022 21:23:39 -0500 Subject: [PATCH 21/91] Add Rust BepInEx installer impl --- ThunderstoreCLI/Commands/InstallCommand.cs | 8 - ThunderstoreCLI/Game/InstallHelpers.cs | 6 - ThunderstoreCLI/Game/ModManager.cs | 14 - ThunderstoreCLI/Plugins/PluginManager.cs | 11 - ThunderstoreCLI/Program.cs | 9 +- tcli-bepinex-installer/.gitignore | 1 + tcli-bepinex-installer/Cargo.lock | 651 +++++++++++++++++++++ tcli-bepinex-installer/Cargo.toml | 21 + tcli-bepinex-installer/src/main.rs | 243 ++++++++ 9 files changed, 924 insertions(+), 40 deletions(-) delete mode 100644 ThunderstoreCLI/Game/InstallHelpers.cs delete mode 100644 ThunderstoreCLI/Game/ModManager.cs delete mode 100644 ThunderstoreCLI/Plugins/PluginManager.cs create mode 100644 tcli-bepinex-installer/.gitignore create mode 100644 tcli-bepinex-installer/Cargo.lock create mode 100644 tcli-bepinex-installer/Cargo.toml create mode 100644 tcli-bepinex-installer/src/main.rs diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index fba2348..0ba1ffd 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -1,6 +1,5 @@ using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Game; -using ThunderstoreCLI.Plugins; namespace ThunderstoreCLI.Commands; @@ -10,11 +9,4 @@ public static int Run(Config config) { throw new NotImplementedException(); } - - public static int InstallLoader(Config config) - { - var managerTypes = PluginManager.GetAllOfType(); - - throw new NotImplementedException(); - } } diff --git a/ThunderstoreCLI/Game/InstallHelpers.cs b/ThunderstoreCLI/Game/InstallHelpers.cs deleted file mode 100644 index 6945e1e..0000000 --- a/ThunderstoreCLI/Game/InstallHelpers.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ThunderstoreCLI.Game; - -public static class InstallHelpers -{ - -} diff --git a/ThunderstoreCLI/Game/ModManager.cs b/ThunderstoreCLI/Game/ModManager.cs deleted file mode 100644 index d024c23..0000000 --- a/ThunderstoreCLI/Game/ModManager.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.IO.Compression; - -namespace ThunderstoreCLI.Game; - -public interface ModManager -{ - public static abstract string Identifier { get; } - public bool SupportsProfiles(GameDefinition gameDef); - public void InstallLoader(GameDefinition gameDef, ModProfile profile, PackageManifestV1 manifest, ZipArchive loaderArchive); - public void UninstallLoader(GameDefinition gameDef, ModProfile profile); - public void InstallMod(GameDefinition gameDef, ModProfile profile, PackageManifestV1 manifest, ZipArchive modArchive); - public void UninstallMod(GameDefinition gameDef, ModProfile profile, PackageManifestV1 manifest); - public int RunGame(GameDefinition gameDef, ModProfile profile); -} diff --git a/ThunderstoreCLI/Plugins/PluginManager.cs b/ThunderstoreCLI/Plugins/PluginManager.cs deleted file mode 100644 index be7ede4..0000000 --- a/ThunderstoreCLI/Plugins/PluginManager.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace ThunderstoreCLI.Plugins; - -public static class PluginManager -{ - public static List GetAllOfType<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() - { - return typeof(PluginManager).Assembly.GetTypes().Where(x => x.IsAssignableTo(typeof(T))).ToList(); - } -} diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index bd6d660..9e00416 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -15,11 +15,18 @@ private static int Main(string[] args) #endif var updateChecker = UpdateChecker.CheckForUpdates(); - var exitCode = Parser.Default.ParseArguments(args) + var exitCode = Parser.Default.ParseArguments(args) .MapResult( (InitOptions o) => HandleParsed(o), (BuildOptions o) => HandleParsed(o), (PublishOptions o) => HandleParsed(o), +#if DEBUG + (InstallOptions o) => HandleParsed(o), +#endif _ => 1 // failure to parse ); UpdateChecker.WriteUpdateNotification(updateChecker); diff --git a/tcli-bepinex-installer/.gitignore b/tcli-bepinex-installer/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/tcli-bepinex-installer/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/tcli-bepinex-installer/Cargo.lock b/tcli-bepinex-installer/Cargo.lock new file mode 100644 index 0000000..21ec841 --- /dev/null +++ b/tcli-bepinex-installer/Cargo.lock @@ -0,0 +1,651 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "opaque-debug", +] + +[[package]] +name = "anyhow" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64ct" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bzip2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "clap" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "lazy_static", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +dependencies = [ + "cfg-if", + "lazy_static", +] + +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "flate2" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "indexmap" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" + +[[package]] +name = "miniz_oxide" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +dependencies = [ + "adler", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "os_str_bytes" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" + +[[package]] +name = "password-hash" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "serde" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tcli-bepinex-installer" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "serde", + "serde_json", + "thiserror", + "zip", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unicode-xid" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zip" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf225bcf73bb52cbb496e70475c7bd7a3f769df699c0020f6c7bd9a96dcf0b8d" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.10.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4a6bd64f22b5e3e94b4e238669ff9f10815c27a5180108b849d24174a83847" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.6+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b61c51bb270702d6167b8ce67340d2754b088d0c091b06e593aa772c3ee9bb" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" +dependencies = [ + "cc", + "libc", +] diff --git a/tcli-bepinex-installer/Cargo.toml b/tcli-bepinex-installer/Cargo.toml new file mode 100644 index 0000000..8cd5242 --- /dev/null +++ b/tcli-bepinex-installer/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tcli-bepinex-installer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.57" +zip = "0.6.2" +thiserror = "1.0.31" +serde_json = "1.0.81" +serde = { version = "1.0.137", features = ["derive"] } + +[dependencies.clap] +version = "3.1.18" +features = ["derive", "cargo"] + +[profile.release] +lto = true +strip = true diff --git a/tcli-bepinex-installer/src/main.rs b/tcli-bepinex-installer/src/main.rs new file mode 100644 index 0000000..7c3d0da --- /dev/null +++ b/tcli-bepinex-installer/src/main.rs @@ -0,0 +1,243 @@ +use std::{ + collections::HashMap, + ffi::OsString, + fs::{self, OpenOptions}, + io::{self, Read, Seek}, + path::{Path, PathBuf}, +}; + +use anyhow::{bail, Result}; +use clap::{Parser, Subcommand}; +use serde::Deserialize; +use zip::ZipArchive; + +#[derive(Parser)] +#[clap(author, version, about)] +struct Args { + #[clap(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Install { + game_directory: PathBuf, + bepinex_directory: PathBuf, + zip_path: PathBuf, + }, + Uninstall { + game_directory: PathBuf, + bepinex_directory: PathBuf, + name: String, + }, +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Path does not exist: {0}")] + PathDoesNotExist(PathBuf), + #[error("ZIP file does not contain a Thunderstore manifest")] + NoZipManifest, + #[error("Invalid manifest in ZIP")] + InvalidManifest, + #[error("Malformed zip")] + MalformedZip, + #[error("Manifest does not contain namespace in manifest, which is required for mod installs")] + MissingNamespace, +} + +#[derive(Deserialize)] +#[allow(unused)] +struct ManifestV1 { + pub namespace: Option, + pub name: String, + pub description: String, + pub version_number: String, + pub dependencies: Vec, + pub website_url: String, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + match args.command { + Commands::Install { + game_directory, + bepinex_directory, + zip_path, + } => { + if !game_directory.exists() { + bail!(Error::PathDoesNotExist(game_directory)); + } + if !bepinex_directory.exists() { + bail!(Error::PathDoesNotExist(bepinex_directory)); + } + if !zip_path.exists() { + bail!(Error::PathDoesNotExist(zip_path)); + } + install(game_directory, bepinex_directory, zip_path) + } + Commands::Uninstall { + game_directory, + bepinex_directory, + name, + } => { + if !game_directory.exists() { + bail!(Error::PathDoesNotExist(game_directory)); + } + if !bepinex_directory.exists() { + bail!(Error::PathDoesNotExist(bepinex_directory)); + } + uninstall(game_directory, bepinex_directory, name) + } + } +} + +fn install(game_dir: PathBuf, bep_dir: PathBuf, zip_path: PathBuf) -> Result<()> { + let mut zip = ZipArchive::new(std::fs::File::open(zip_path)?)?; + + if !zip.file_names().any(|name| name == "manifest.json") { + bail!(Error::NoZipManifest); + } + + let manifest_file = zip.by_name("manifest.json")?; + + let manifest: ManifestV1 = + serde_json::from_reader(manifest_file).map_err(|_| Error::InvalidManifest)?; + + if manifest.name.starts_with("BepInExPack") { + install_bepinex(game_dir, bep_dir, zip) + } else { + install_mod(bep_dir, zip, manifest) + } +} + +fn install_bepinex( + game_dir: PathBuf, + bep_dir: PathBuf, + mut zip: ZipArchive, +) -> Result<()> { + let write_opts = OpenOptions::new().write(true).create(true).clone(); + + for i in 0..zip.len() { + let mut file = zip.by_index(i)?; + + if file.is_dir() { + continue; + } + + let filepath = file.enclosed_name().ok_or(Error::MalformedZip)?.to_owned(); + + if !top_level_directory_name(&filepath) + .or(Some("".to_string())) + .unwrap() + .starts_with("BepInExPack") + { + continue; + } + + let dir_to_use = if filepath.ancestors().any(|part| { + part.file_name() + .or(Some(&OsString::new())) + .unwrap() + .to_string_lossy() + == "BepInEx" + }) { + &bep_dir + } else { + &game_dir + }; + + // this removes the BepInExPack*/ from the path + let resolved_path = remove_first_n_directories(&filepath, 1); + + fs::create_dir_all(dir_to_use.join(resolved_path.parent().unwrap()))?; + io::copy( + &mut file, + &mut write_opts.open(dir_to_use.join(resolved_path))?, + )?; + } + + Ok(()) +} + +fn install_mod( + bep_dir: PathBuf, + mut zip: ZipArchive, + manifest: ManifestV1, +) -> Result<()> { + let write_opts = OpenOptions::new().write(true).create(true).clone(); + + let full_name = format!( + "{}-{}", + manifest.namespace.ok_or(Error::MissingNamespace)?, + manifest.name + ); + + let mut remaps: HashMap<&str, PathBuf> = HashMap::new(); + remaps.insert( + "plugins", + Path::new("BepInEx").join("plugins").join(&full_name), + ); + remaps.insert( + "patchers", + Path::new("BepInEx").join("patchers").join(&full_name), + ); + remaps.insert( + "monomod", + Path::new("BepInEx").join("monomod").join(&full_name), + ); + remaps.insert("config", Path::new("BepInEx").join("config")); + + for i in 0..zip.len() { + let mut file = zip.by_index(i)?; + + if file.is_dir() { + continue; + } + + let filepath = file.enclosed_name().ok_or(Error::MalformedZip)?.to_owned(); + + let out_path: PathBuf = if let Some(root) = top_level_directory_name(&filepath) { + if let Some(remap) = remaps.get(&root as &str) { + remap.join(remove_first_n_directories(&filepath, 1)) + } else { + remaps["plugins"].join(filepath) + } + } else { + remaps["plugins"].join(filepath) + }; + + let full_out_path = bep_dir.join(out_path); + + fs::create_dir_all(full_out_path.parent().unwrap())?; + io::copy(&mut file, &mut write_opts.open(full_out_path)?)?; + } + + Ok(()) +} + +fn uninstall(game_dir: PathBuf, bep_dir: PathBuf, name: String) -> Result<()> { + todo!(); +} + +fn top_level_directory_name(path: &Path) -> Option { + path.ancestors() + .skip(1) + .filter(|x| !x.to_string_lossy().is_empty()) + .last() + .map(|root| root.to_string_lossy().to_string()) +} + +// removes the first n directories from a path, eg a/b/c/d.txt with an n of 2 gives c/d.txt +fn remove_first_n_directories(path: &Path, n: usize) -> PathBuf { + PathBuf::from_iter( + path.ancestors() + .collect::>() + .into_iter() + .rev() + .filter(|x| !x.to_string_lossy().is_empty()) + .skip(n) + .map(|part| part.file_name().unwrap()), + ) +} From b4c005cfff5ce9c95050161f074d27c8df737cf3 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sun, 15 May 2022 18:38:55 -0500 Subject: [PATCH 22/91] Add uninstall logic --- tcli-bepinex-installer/src/main.rs | 61 ++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/tcli-bepinex-installer/src/main.rs b/tcli-bepinex-installer/src/main.rs index 7c3d0da..f4eacb3 100644 --- a/tcli-bepinex-installer/src/main.rs +++ b/tcli-bepinex-installer/src/main.rs @@ -42,8 +42,12 @@ pub enum Error { InvalidManifest, #[error("Malformed zip")] MalformedZip, - #[error("Manifest does not contain namespace in manifest, which is required for mod installs")] + #[error("Manifest does not contain a namespace, which is required for mod installs")] MissingNamespace, + #[error("Mod name is invalid (eg doesn't use a - between namespace and name)")] + InvalidModName, + #[error("Mod either is not installed or is not accessable to the uninstaller. Tried directory: {0}")] + ModNotInstalled(PathBuf), } #[derive(Deserialize)] @@ -218,7 +222,38 @@ fn install_mod( } fn uninstall(game_dir: PathBuf, bep_dir: PathBuf, name: String) -> Result<()> { - todo!(); + if name.split_once('-').ok_or(Error::InvalidModName)?.1.starts_with("BepInExPack") { + uninstall_bepinex(game_dir, bep_dir) + } else { + uninstall_mod(bep_dir, name) + } +} + +fn uninstall_bepinex(game_dir: PathBuf, bep_dir: PathBuf) -> Result<()> { + delete_file_if_not_deleted(game_dir.join("winhttp.dll"))?; + delete_file_if_not_deleted(game_dir.join("doorstop_config.ini"))?; + delete_file_if_not_deleted(game_dir.join("run_bepinex.sh"))?; + delete_dir_if_not_deleted(game_dir.join("doorstop_libs"))?; + delete_dir_if_not_deleted(bep_dir.join("BepInEx"))?; + + Ok(()) +} + +fn uninstall_mod(bep_dir: PathBuf, name: String) -> Result<()> { + let actual_bep = bep_dir.join("BepInEx"); + + let main_dir = actual_bep.join("plugins").join(&name); + + if !main_dir.exists() { + bail!(Error::ModNotInstalled(main_dir)); + } + + fs::remove_dir_all(main_dir)?; + + delete_dir_if_not_deleted(actual_bep.join("patchers"))?; + delete_dir_if_not_deleted(actual_bep.join("monomod").join(&name))?; + + Ok(()) } fn top_level_directory_name(path: &Path) -> Option { @@ -229,7 +264,7 @@ fn top_level_directory_name(path: &Path) -> Option { .map(|root| root.to_string_lossy().to_string()) } -// removes the first n directories from a path, eg a/b/c/d.txt with an n of 2 gives c/d.txt +/// removes the first n directories from a path, eg a/b/c/d.txt with an n of 2 gives c/d.txt fn remove_first_n_directories(path: &Path, n: usize) -> PathBuf { PathBuf::from_iter( path.ancestors() @@ -241,3 +276,23 @@ fn remove_first_n_directories(path: &Path, n: usize) -> PathBuf { .map(|part| part.file_name().unwrap()), ) } + +fn delete_file_if_not_deleted>(path: T) -> io::Result<()> { + match fs::remove_file(path) { + Ok(_) => Ok(()), + Err(e) => match e.kind() { + io::ErrorKind::NotFound => Ok(()), + _ => Err(e), + }, + } +} + +fn delete_dir_if_not_deleted>(path: T) -> io::Result<()> { + match fs::remove_dir_all(path) { + Ok(_) => Ok(()), + Err(e) => match e.kind() { + io::ErrorKind::NotFound => Ok(()), + _ => Err(e), + }, + } +} From 21f8ab780d1c17169d4b412398091e9e716abece Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Thu, 2 Jun 2022 00:43:52 -0500 Subject: [PATCH 23/91] Add install command for ror2 and vrising + derivatives also a ton of other busywork related to that --- ThunderstoreCLI/API/ApiHelper.cs | 10 +- ThunderstoreCLI/Commands/InitCommand.cs | 2 +- ThunderstoreCLI/Commands/InstallCommand.cs | 103 +++++++++++++++++- ThunderstoreCLI/Commands/PublishCommand.cs | 1 + ThunderstoreCLI/Configuration/BaseConfig.cs | 11 +- .../Configuration/CLIParameterConfig.cs | 11 +- ThunderstoreCLI/Configuration/Config.cs | 7 +- ThunderstoreCLI/Game/GameDefinition.cs | 56 ++++++++-- ThunderstoreCLI/Game/ModProfile.cs | 6 +- ThunderstoreCLI/Models/PublishModels.cs | 76 ++++++++++--- ThunderstoreCLI/Options.cs | 36 ++++-- ThunderstoreCLI/Program.cs | 8 +- ThunderstoreCLI/ThunderstoreCLI.csproj | 18 +++ ThunderstoreCLI/Utils/SteamUtils.cs | 103 ++++++++++++++++++ 14 files changed, 395 insertions(+), 53 deletions(-) create mode 100644 ThunderstoreCLI/Utils/SteamUtils.cs diff --git a/ThunderstoreCLI/API/ApiHelper.cs b/ThunderstoreCLI/API/ApiHelper.cs index 9f511b6..7dc3ef5 100644 --- a/ThunderstoreCLI/API/ApiHelper.cs +++ b/ThunderstoreCLI/API/ApiHelper.cs @@ -18,7 +18,7 @@ public class ApiHelper public ApiHelper(Config config) { Config = config; - BaseRequestBuilder = new RequestBuilder(config.PublishConfig.Repository ?? throw new Exception("The target repository cannot be empty")); + BaseRequestBuilder = new RequestBuilder(config.GeneralConfig.Repository ?? throw new Exception("Repository can't be empty")); authHeader = new Lazy(() => { if (string.IsNullOrEmpty(Config.AuthConfig.AuthToken)) @@ -73,6 +73,14 @@ public HttpRequestMessage AbortUploadMedia(string uuid) .GetRequest(); } + public HttpRequestMessage GetPackageMetadata(string author, string name) + { + return BaseRequestBuilder + .StartNew() + .WithEndpoint(EXPERIMENTAL + $"package/{author}/{name}/") + .GetRequest(); + } + private static string SerializeFileData(string filePath) { return new FileData() diff --git a/ThunderstoreCLI/Commands/InitCommand.cs b/ThunderstoreCLI/Commands/InitCommand.cs index 923be6e..6aaa02f 100644 --- a/ThunderstoreCLI/Commands/InitCommand.cs +++ b/ThunderstoreCLI/Commands/InitCommand.cs @@ -87,7 +87,7 @@ private static void ValidateConfig(Config config) v.AddIfEmpty(config.BuildConfig.IconPath, "Build IconPath"); v.AddIfEmpty(config.BuildConfig.ReadmePath, "Build ReadmePath"); v.AddIfEmpty(config.BuildConfig.OutDir, "Build OutDir"); - v.AddIfEmpty(config.PublishConfig.Repository, "Publish Repository"); + v.AddIfEmpty(config.GeneralConfig.Repository, "Publish Repository"); v.ThrowIfErrors(); } } diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index 0ba1ffd..25ac226 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -1,12 +1,113 @@ using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Game; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using ThunderstoreCLI.Game; +using ThunderstoreCLI.Models; +using ThunderstoreCLI.Utils; namespace ThunderstoreCLI.Commands; public static class InstallCommand { + private static readonly Dictionary IDToHardcoded = new() + { + { "ror2", HardcodedGame.ROR2 }, + { "vrising", HardcodedGame.VRISING }, + { "vrising_dedicated", HardcodedGame.VRISING_SERVER }, + { "vrising_builtin", HardcodedGame.VRISING_SERVER_BUILTIN } + }; + + private static readonly Regex FullPackageNameRegex = new(@"^([a-zA-Z0-9_]+)-([a-zA-Z0-9_]+)$"); + public static int Run(Config config) { - throw new NotImplementedException(); + List defs = GameDefinition.GetGameDefinitions(config.GeneralConfig.TcliConfig); + GameDefinition? def = defs.FirstOrDefault(x => x.Identifier == config.InstallConfig.GameIdentifer); + if (def == null && IDToHardcoded.TryGetValue(config.InstallConfig.GameIdentifer!, out var hardcoded)) + { + def = GameDefinition.FromHardcodedIdentifier(config.GeneralConfig.TcliConfig, hardcoded); + defs.Add(def); + } + else + { + Write.ErrorExit($"Not configured for the game: {config.InstallConfig.GameIdentifer}"); + return 1; + } + + ModProfile? profile; + if (config.InstallConfig.Global!.Value) + { + profile = def.GlobalProfile; + } + else + { + profile = def.Profiles.FirstOrDefault(x => x.Name == config.InstallConfig.ProfileName); + } + profile ??= new ModProfile(def, false, config.InstallConfig.ProfileName!, config.GeneralConfig.TcliConfig); + + string zipPath = config.InstallConfig.Package!; + bool isTemp = false; + if (!File.Exists(zipPath)) + { + var match = FullPackageNameRegex.Match(zipPath); + if (!match.Success) + { + Write.ErrorExit($"Package name does not exist as a file and is not a valid package name (namespace-author): {zipPath}"); + } + HttpClient http = new(); + var packageResponse = http.Send(config.Api.GetPackageMetadata(match.Groups[1].Value, match.Groups[2].Value)); + using StreamReader responseReader = new(packageResponse.Content.ReadAsStream()); + if (!packageResponse.IsSuccessStatusCode) + { + Write.ErrorExit($"Could not find package {zipPath}, got:\n{responseReader.ReadToEnd()}"); + return 1; + } + var data = PackageData.Deserialize(responseReader.ReadToEnd()); + + zipPath = Path.GetTempFileName(); + isTemp = true; + using var outFile = File.OpenWrite(zipPath); + + using var downloadStream = http.Send(new HttpRequestMessage(HttpMethod.Get, data!.LatestVersion!.DownloadUrl)).Content.ReadAsStream(); + + downloadStream.CopyTo(outFile); + } + + string installerName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "tcli-bepinex-installer.exe" : "tcli-bepinex-installer"; + var bepinexInstallerPath = Path.Combine(Path.GetDirectoryName(typeof(InstallCommand).Assembly.Location)!, installerName); + + ProcessStartInfo installerInfo = new(bepinexInstallerPath) + { + ArgumentList = + { + "install", + def.InstallDirectory, + profile.ProfileDirectory, + zipPath + }, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + Process installerProcess = Process.Start(installerInfo)!; + installerProcess.WaitForExit(); + + Write.Light(installerProcess.StandardOutput.ReadToEnd()); + string errors = installerProcess.StandardError.ReadToEnd(); + if (!string.IsNullOrWhiteSpace(errors)) + { + Write.Error(errors); + } + + if (isTemp) + { + File.Delete(zipPath); + } + + GameDefinition.SetGameDefinitions(config.GeneralConfig.TcliConfig, defs); + + return installerProcess.ExitCode; } } diff --git a/ThunderstoreCLI/Commands/PublishCommand.cs b/ThunderstoreCLI/Commands/PublishCommand.cs index 57e8886..a1965ae 100644 --- a/ThunderstoreCLI/Commands/PublishCommand.cs +++ b/ThunderstoreCLI/Commands/PublishCommand.cs @@ -4,6 +4,7 @@ using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Models; using ThunderstoreCLI.Utils; +using ThunderstoreCLI.Models; using static Crayon.Output; namespace ThunderstoreCLI.Commands; diff --git a/ThunderstoreCLI/Configuration/BaseConfig.cs b/ThunderstoreCLI/Configuration/BaseConfig.cs index c10d8c4..16f32a1 100644 --- a/ThunderstoreCLI/Configuration/BaseConfig.cs +++ b/ThunderstoreCLI/Configuration/BaseConfig.cs @@ -2,6 +2,14 @@ namespace ThunderstoreCLI.Configuration; class BaseConfig : EmptyConfig { + public override GeneralConfig? GetGeneralConfig() + { + return new GeneralConfig() + { + Repository = Defaults.REPOSITORY_URL + }; + } + public override PackageConfig GetPackageMeta() { return new PackageConfig() @@ -46,8 +54,7 @@ public override PublishConfig GetPublishConfig() { return new PublishConfig() { - File = null, - Repository = Defaults.REPOSITORY_URL + File = null }; } } diff --git a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs index 68a7795..67b93d3 100644 --- a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs +++ b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs @@ -64,8 +64,7 @@ public override PublishConfig GetPublishConfig() { return new PublishConfig() { - File = options.File, - Repository = options.Repository + File = options.File }; } @@ -78,7 +77,7 @@ public override AuthConfig GetAuthConfig() } } -public class CLIInstallCommandConfig : CLIParameterConfig +public class CLIInstallCommandConfig : BaseConfig { public CLIInstallCommandConfig(InstallOptions options) : base(options) { } @@ -86,7 +85,11 @@ public CLIInstallCommandConfig(InstallOptions options) : base(options) { } { return new InstallConfig() { - + //ManagerIdentifier = options.ManagerId + GameIdentifer = options.GameName, + Global = options.Global, + ProfileName = options.Profile, + Package = options.Package }; } } diff --git a/ThunderstoreCLI/Configuration/Config.cs b/ThunderstoreCLI/Configuration/Config.cs index 22566c5..2eff251 100644 --- a/ThunderstoreCLI/Configuration/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -204,7 +204,6 @@ public class BuildConfig public class PublishConfig { public string? File { get; set; } - public string? Repository { get; set; } public string[]? Communities { get; set; } public string[]? Categories { get; set; } } @@ -216,5 +215,9 @@ public class AuthConfig public class InstallConfig { - public string? ManagerIdentifier { get; set; } + public string? GameIdentifer { get; set; } + //public string? ManagerIdentifier { get; set; } + public string? Package { get; set; } + public bool? Global { get; set; } + public string? ProfileName { get; set; } } diff --git a/ThunderstoreCLI/Game/GameDefinition.cs b/ThunderstoreCLI/Game/GameDefinition.cs index 9f8ab45..8049c23 100644 --- a/ThunderstoreCLI/Game/GameDefinition.cs +++ b/ThunderstoreCLI/Game/GameDefinition.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using ThunderstoreCLI.Models; +using ThunderstoreCLI.Utils; namespace ThunderstoreCLI.Game; @@ -9,28 +10,65 @@ public class GameDefinition : BaseJson private const string FILE_NAME = "GameDefintions.json"; public string Identifier { get; } public string Name { get; } - public string ModManager { get; } - public string InstallDirectory { get; } + public string InstallDirectory { get; private set; } public List Profiles { get; private set; } = new(); public ModProfile GlobalProfile { get; } - internal GameDefinition(string id, string name, string modManager, string tcliDirectory) +#pragma warning disable CS8618 + private GameDefinition() { } +#pragma warning restore CS8618 + + internal GameDefinition(string id, string name, string installDirectory, string tcliDirectory) { Identifier = id; Name = name; - ModManager = modManager; + InstallDirectory = installDirectory; GlobalProfile = new ModProfile(this, true, "Global", tcliDirectory); - // TODO: actually find install dir instead of manually setting the path in json - // yes im lazy } - public static List GetGameDefinitions(string tcliDirectory) + internal static List GetGameDefinitions(string tcliDirectory) + { + var filename = Path.Combine(tcliDirectory, FILE_NAME); + if (File.Exists(filename)) + return DeserializeList(File.ReadAllText(filename)) ?? new(); + else + return new(); + } + + internal static GameDefinition FromHardcodedIdentifier(string tcliDir, HardcodedGame game) + { + return game switch + { + HardcodedGame.ROR2 => FromSteamId(tcliDir, 632360, "ror2", "Risk of Rain 2"), + HardcodedGame.VRISING => FromSteamId(tcliDir, 1604030, "vrising", "V Rising"), + HardcodedGame.VRISING_SERVER => FromSteamId(tcliDir, 1829350, "vrising_server", "V Rising Dedicated Server"), + HardcodedGame.VRISING_SERVER_BUILTIN => FromSteamId(tcliDir, 1604030, "VRising_Server", "virsing_server_builtin", "V Rising Built-in Server"), + _ => throw new ArgumentException("Invalid enum value", nameof(game)) + }; + } + + internal static GameDefinition FromSteamId(string tcliDir, uint steamId, string id, string name) { - return DeserializeList(File.ReadAllText(Path.Combine(tcliDirectory, FILE_NAME))) ?? new(); + return new GameDefinition(id, name, SteamUtils.FindInstallDirectory(steamId), tcliDir); } - public static void SetGameDefinitions(string tcliDirectory, List list) + internal static GameDefinition FromSteamId(string tcliDir, uint steamId, string subdirectory, string id, string name) + { + var gameDef = FromSteamId(tcliDir, steamId, id, name); + gameDef.InstallDirectory = Path.Combine(gameDef.InstallDirectory, subdirectory); + return gameDef; + } + + internal static void SetGameDefinitions(string tcliDirectory, List list) { File.WriteAllText(Path.Combine(tcliDirectory, FILE_NAME), list.SerializeList(BaseJson.IndentedSettings)); } } + +internal enum HardcodedGame +{ + ROR2, + VRISING, + VRISING_SERVER, + VRISING_SERVER_BUILTIN +} diff --git a/ThunderstoreCLI/Game/ModProfile.cs b/ThunderstoreCLI/Game/ModProfile.cs index afb877c..1d45585 100644 --- a/ThunderstoreCLI/Game/ModProfile.cs +++ b/ThunderstoreCLI/Game/ModProfile.cs @@ -9,7 +9,10 @@ public class ModProfile : BaseJson public bool IsGlobal { get; } public string Name { get; } public string ProfileDirectory { get; } - public List InstalledMods { get; set; } = new(); + +#pragma warning disable CS8618 + private ModProfile() { } +#pragma warning restore CS8618 internal ModProfile(GameDefinition gameDef, bool global, string name, string tcliDirectory) { @@ -22,5 +25,6 @@ internal ModProfile(GameDefinition gameDef, bool global, string name, string tcl Directory.CreateDirectory(directory); } ProfileDirectory = directory; + gameDef.Profiles.Add(this); } } diff --git a/ThunderstoreCLI/Models/PublishModels.cs b/ThunderstoreCLI/Models/PublishModels.cs index 8ec7fb4..482fe83 100644 --- a/ThunderstoreCLI/Models/PublishModels.cs +++ b/ThunderstoreCLI/Models/PublishModels.cs @@ -100,35 +100,75 @@ public class CategoryData [JsonProperty("url")] public string? Url { get; set; } } - public class PackageVersionData - { - [JsonProperty("namespace")] public string? Namespace { get; set; } + [JsonProperty("available_communities")] + public List? AvailableCommunities { get; set; } - [JsonProperty("name")] public string? Name { get; set; } + [JsonProperty("package_version")] public PackageVersionData? PackageVersion { get; set; } +} - [JsonProperty("version_number")] public string? VersionNumber { get; set; } +public class PackageData : BaseJson +{ + [JsonProperty("namespace")] public string? Namespace { get; set; } - [JsonProperty("full_name")] public string? FullName { get; set; } + [JsonProperty("name")] public string? Name { get; set; } - [JsonProperty("description")] public string? Description { get; set; } + [JsonProperty("full_name")] public string? Fullname { get; set; } - [JsonProperty("icon")] public string? Icon { get; set; } + [JsonProperty("owner")] public string? Owner { get; set; } - [JsonProperty("dependencies")] public List? Dependencies { get; set; } + [JsonProperty("package_url")] public string? PackageUrl { get; set; } - [JsonProperty("download_url")] public string? DownloadUrl { get; set; } + [JsonProperty("date_created")] public DateTime DateCreated { get; set; } - [JsonProperty("downloads")] public int Downloads { get; set; } + [JsonProperty("date_updated")] public DateTime DateUpdated { get; set; } - [JsonProperty("date_created")] public DateTime DateCreated { get; set; } + [JsonProperty("rating_score")] public string? RatingScore { get; set; } - [JsonProperty("website_url")] public string? WebsiteUrl { get; set; } + [JsonProperty("is_pinned")] public bool IsPinned { get; set; } - [JsonProperty("is_active")] public bool IsActive { get; set; } - } + [JsonProperty("is_deprecated")] public bool IsDeprecated { get; set; } - [JsonProperty("available_communities")] - public List? AvailableCommunities { get; set; } + [JsonProperty("total_downloads")] public string? TotalDownloads { get; set; } - [JsonProperty("package_version")] public PackageVersionData? PackageVersion { get; set; } + [JsonProperty("latest")] public PackageVersionData? LatestVersion { get; set; } + + [JsonProperty("community_listings")] public PackageListingData[]? CommunityListings { get; set; } +} + +public class PackageVersionData : BaseJson +{ + [JsonProperty("namespace")] public string? Namespace { get; set; } + + [JsonProperty("name")] public string? Name { get; set; } + + [JsonProperty("version_number")] public string? VersionNumber { get; set; } + + [JsonProperty("full_name")] public string? FullName { get; set; } + + [JsonProperty("description")] public string? Description { get; set; } + + [JsonProperty("icon")] public string? Icon { get; set; } + + [JsonProperty("dependencies")] public List? Dependencies { get; set; } + + [JsonProperty("download_url")] public string? DownloadUrl { get; set; } + + [JsonProperty("downloads")] public int Downloads { get; set; } + + [JsonProperty("date_created")] public DateTime DateCreated { get; set; } + + [JsonProperty("website_url")] public string? WebsiteUrl { get; set; } + + [JsonProperty("is_active")] public bool IsActive { get; set; } +} + +public class PackageListingData : BaseJson +{ + [JsonProperty("has_nsfw_content")] public bool HasNsfwContent { get; set; } + + [JsonProperty("categories")] public string[]? Categories { get; set; } + + [JsonProperty("community")] public string? Community { get; set; } + + [JsonProperty("review_status")] public string? ReviewStatus { get; set; } } diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index d2de2fb..ff6f7a7 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -10,13 +10,16 @@ namespace ThunderstoreCLI; /// Options are arguments passed from command line. public abstract class BaseOptions { - [Option("output", Required = false, HelpText = "The output format for all output. Valid options are HUMAN and JSON.")] + [Option("output", Required = false, HelpText = "The output format for all output. Valid options are HUMAN and JSON. (does nothing)")] public InteractionOutputType OutputType { get; set; } = InteractionOutputType.HUMAN; [Option("tcli-directory", Required = false, HelpText = "Directory where TCLI keeps its data, %APPDATA%/ThunderstoreCLI on Windows and ~/.config/ThunderstoreCLI on Linux")] // will be initialized in Init if null public string TcliDirectory { get; set; } = null!; + [Option("repository", Required = false, HelpText = "URL of the default repository")] + public string Repository { get; set; } = null!; + public virtual void Init() { InteractionOptions.OutputType = OutputType; @@ -118,9 +121,6 @@ public class PublishOptions : PackageOptions [Option("token", Required = false, HelpText = "Authentication token to use for publishing.")] public string? Token { get; set; } - [Option("repository", Required = false, HelpText = "URL of the repository where to publish.")] - public string? Repository { get; set; } - public override bool Validate() { if (!base.Validate()) @@ -150,10 +150,32 @@ public override int Execute() } } -[Verb("install", HelpText = "Installs a modloader or mod")] -public class InstallOptions : PackageOptions +[Verb("install", HelpText = "Installs a mod")] +public class InstallOptions : BaseOptions { - public string? ManagerId { get; set; } + //public string? ManagerId { get; set; } + + [Value(0, MetaName = "Game Name", Required = true, HelpText = "Can be any of: ror2, vrising, vrising_dedicated, vrising_builtin")] + public string GameName { get; set; } = null!; + + [Value(1, MetaName = "Package", Required = true, HelpText = "Path to package zip or package name in the format namespace-name")] + public string Package { get; set; } = null!; + + [Option(HelpText = "Profile to install to", Default = "Default")] + public string? Profile { get; set; } + + [Option(HelpText = "Set to install mods globally instead of into a profile", Default = false)] + public bool Global { get; set; } + + public override bool Validate() + { +#if NOINSTALLERS + Write.ErrorExit("Installers are not supported when installed through dotnet tool (yet) (i hate nuget)"); + return false; +#endif + + return base.Validate(); + } public override int Execute() { diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index 9e00416..17b7ca1 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -15,18 +15,12 @@ private static int Main(string[] args) #endif var updateChecker = UpdateChecker.CheckForUpdates(); - var exitCode = Parser.Default.ParseArguments(args) + var exitCode = Parser.Default.ParseArguments(args) .MapResult( (InitOptions o) => HandleParsed(o), (BuildOptions o) => HandleParsed(o), (PublishOptions o) => HandleParsed(o), -#if DEBUG (InstallOptions o) => HandleParsed(o), -#endif _ => 1 // failure to parse ); UpdateChecker.WriteUpdateNotification(updateChecker); diff --git a/ThunderstoreCLI/ThunderstoreCLI.csproj b/ThunderstoreCLI/ThunderstoreCLI.csproj index 7176c8a..a3ddf9c 100644 --- a/ThunderstoreCLI/ThunderstoreCLI.csproj +++ b/ThunderstoreCLI/ThunderstoreCLI.csproj @@ -55,4 +55,22 @@ Resources.Designer.cs + + + + + --release + debug + release + + + + + + + + + + NOINSTALLERS + diff --git a/ThunderstoreCLI/Utils/SteamUtils.cs b/ThunderstoreCLI/Utils/SteamUtils.cs new file mode 100644 index 0000000..83ddf84 --- /dev/null +++ b/ThunderstoreCLI/Utils/SteamUtils.cs @@ -0,0 +1,103 @@ +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace ThunderstoreCLI.Utils; + +public static class SteamUtils +{ + public static string FindInstallDirectory(uint steamAppId) + { + string primarySteamApps = FindSteamAppsDirectory(); + List libraryPaths = new() { primarySteamApps }; + foreach (var file in Directory.EnumerateFiles(primarySteamApps)) + { + if (!Path.GetFileName(file).Equals("libraryfolders.vdf", StringComparison.OrdinalIgnoreCase)) + continue; + libraryPaths.AddRange(SteamAppsPathsRegex.Matches(File.ReadAllText(file)).Select(x => x.Groups[1].Value).Select(x => Path.Combine(x, "steamapps"))); + break; + } + + string acfName = $"appmanifest_{steamAppId}.acf"; + foreach (var library in libraryPaths) + { + foreach (var file in Directory.EnumerateFiles(library)) + { + if (!Path.GetFileName(file).Equals(acfName, StringComparison.OrdinalIgnoreCase)) + continue; + + var folderName = ManifestInstallLocationRegex.Match(File.ReadAllText(file)).Groups[1].Value; + + return Path.GetFullPath(Path.Combine(library, "common", folderName)); + } + } + throw new FileNotFoundException($"Could not find {acfName}, tried the following paths:\n{string.Join('\n', libraryPaths)}"); + } + + private static readonly Regex SteamAppsPathsRegex = new(@"""path""\s+""(.+)"""); + private static readonly Regex ManifestInstallLocationRegex = new(@"""installdir""\s+""(.+)"""); + + public static string FindSteamAppsDirectory() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return FindSteamAppsDirectoryWin(); + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return FindSteamAppsDirectoryOsx(); + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return FindSteamAppsDirectoryLinux(); + else + throw new NotSupportedException("Unknown operating system"); + } + private static string FindSteamAppsDirectoryWin() + { + throw new NotImplementedException(); + } + private static string FindSteamAppsDirectoryOsx() + { + throw new NotImplementedException(); + } + private static string FindSteamAppsDirectoryLinux() + { + string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string[] possiblePaths = { + Path.Combine(homeDir, ".local", "share", "Steam"), + Path.Combine(homeDir, ".steam", "steam"), + Path.Combine(homeDir, ".steam", "root"), + Path.Combine(homeDir, ".steam"), + Path.Combine(homeDir, ".var", "app", "com.valvesoftware.Steam", ".local", "share", "Steam"), + Path.Combine(homeDir, ".var", "app", "com.valvesoftware.Steam", ".steam", "steam"), + Path.Combine(homeDir, ".var", "app", "com.valvesoftware.Steam", ".steam", "root"), + Path.Combine(homeDir, ".var", "app", "com.valvesoftware.Steam", ".steam") + }; + string steamPath = null!; + foreach (var path in possiblePaths) + { + if (Directory.Exists(path)) + { + steamPath = path; + goto FoundSteam; + } + } + throw new DirectoryNotFoundException($"Could not find Steam directory, tried these paths:\n{string.Join('\n', possiblePaths)}"); +FoundSteam: + + possiblePaths = new[] + { + Path.Combine(steamPath, "steamapps"), // most distros + Path.Combine(steamPath, "steam", "steamapps"), // ubuntu apparently + Path.Combine(steamPath, "root", "steamapps"), // no idea + }; + string steamAppsPath = null!; + foreach (var path in possiblePaths) + { + if (Directory.Exists(path)) + { + steamAppsPath = path; + goto FoundSteamApps; + } + } + throw new DirectoryNotFoundException($"Could not find steamapps directory, tried these paths:\n{string.Join('\n', possiblePaths)}"); +FoundSteamApps: + + return steamAppsPath; + } +} From 1b8fb3297419f0c398e1ed0b3abc6a33b46df401 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 3 Oct 2022 21:40:34 -0500 Subject: [PATCH 24/91] Beginnings of mod installation --- ThunderstoreCLI/Commands/BuildCommand.cs | 1 - ThunderstoreCLI/Commands/InstallCommand.cs | 131 ++++++++----- ThunderstoreCLI/Commands/PublishCommand.cs | 35 +--- ThunderstoreCLI/Commands/UninstallCommand.cs | 11 ++ .../Configuration/CLIParameterConfig.cs | 11 +- ThunderstoreCLI/Configuration/Config.cs | 10 +- ThunderstoreCLI/Configuration/EmptyConfig.cs | 2 +- .../Configuration/IConfigProvider.cs | 2 +- .../Configuration/ProjectFileConfig.cs | 1 + ThunderstoreCLI/Game/GameDefinition.cs | 4 +- ThunderstoreCLI/Game/ModProfile.cs | 2 + ThunderstoreCLI/Models/BaseJson.cs | 5 + .../Models/Interaction/BaseInteraction.cs | 38 ---- ThunderstoreCLI/Options.cs | 44 +++-- ThunderstoreCLI/Plugins/PluginManager.cs | 25 +++ ThunderstoreCLI/Program.cs | 20 +- ThunderstoreCLI/ThunderstoreCLI.csproj | 1 + .../Utils/CommandFatalException.cs | 10 + ThunderstoreCLI/Utils/ModDependencyTree.cs | 47 +++++ tcli-bepinex-installer/Cargo.lock | 173 +++++++----------- tcli-bepinex-installer/Cargo.toml | 10 +- tcli-bepinex-installer/src/main.rs | 18 +- 22 files changed, 345 insertions(+), 256 deletions(-) create mode 100644 ThunderstoreCLI/Commands/UninstallCommand.cs delete mode 100644 ThunderstoreCLI/Models/Interaction/BaseInteraction.cs create mode 100644 ThunderstoreCLI/Plugins/PluginManager.cs create mode 100644 ThunderstoreCLI/Utils/CommandFatalException.cs create mode 100644 ThunderstoreCLI/Utils/ModDependencyTree.cs diff --git a/ThunderstoreCLI/Commands/BuildCommand.cs b/ThunderstoreCLI/Commands/BuildCommand.cs index 1d667dc..b37abb1 100644 --- a/ThunderstoreCLI/Commands/BuildCommand.cs +++ b/ThunderstoreCLI/Commands/BuildCommand.cs @@ -1,6 +1,5 @@ using System.IO.Compression; using System.Text; -using Newtonsoft.Json; using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Models; using ThunderstoreCLI.Utils; diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index 25ac226..3bc1a05 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -1,8 +1,11 @@ using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Game; +using System.Collections.ObjectModel; using System.Diagnostics; +using System.IO.Compression; using System.Runtime.InteropServices; using System.Text.RegularExpressions; +using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Game; using ThunderstoreCLI.Models; using ThunderstoreCLI.Utils; @@ -11,70 +14,116 @@ namespace ThunderstoreCLI.Commands; public static class InstallCommand { - private static readonly Dictionary IDToHardcoded = new() + internal static readonly Dictionary IDToHardcoded = new() { { "ror2", HardcodedGame.ROR2 }, { "vrising", HardcodedGame.VRISING }, - { "vrising_dedicated", HardcodedGame.VRISING_SERVER }, - { "vrising_builtin", HardcodedGame.VRISING_SERVER_BUILTIN } + { "vrising_dedicated", HardcodedGame.VRISING_SERVER } }; - private static readonly Regex FullPackageNameRegex = new(@"^([a-zA-Z0-9_]+)-([a-zA-Z0-9_]+)$"); + internal static readonly Regex FullPackageNameRegex = new(@"^([a-zA-Z0-9_]+)-([a-zA-Z0-9_]+)$"); - public static int Run(Config config) + public static async Task Run(Config config) { List defs = GameDefinition.GetGameDefinitions(config.GeneralConfig.TcliConfig); - GameDefinition? def = defs.FirstOrDefault(x => x.Identifier == config.InstallConfig.GameIdentifer); - if (def == null && IDToHardcoded.TryGetValue(config.InstallConfig.GameIdentifer!, out var hardcoded)) + GameDefinition? def = defs.FirstOrDefault(x => x.Identifier == config.ModManagementConfig.GameIdentifer); + if (def == null && IDToHardcoded.TryGetValue(config.ModManagementConfig.GameIdentifer!, out var hardcoded)) { def = GameDefinition.FromHardcodedIdentifier(config.GeneralConfig.TcliConfig, hardcoded); defs.Add(def); } else { - Write.ErrorExit($"Not configured for the game: {config.InstallConfig.GameIdentifer}"); + Write.ErrorExit($"Not configured for the game: {config.ModManagementConfig.GameIdentifer}"); return 1; } ModProfile? profile; - if (config.InstallConfig.Global!.Value) + if (config.ModManagementConfig.Global!.Value) { profile = def.GlobalProfile; } else { - profile = def.Profiles.FirstOrDefault(x => x.Name == config.InstallConfig.ProfileName); + profile = def.Profiles.FirstOrDefault(x => x.Name == config.ModManagementConfig.ProfileName); } - profile ??= new ModProfile(def, false, config.InstallConfig.ProfileName!, config.GeneralConfig.TcliConfig); + profile ??= new ModProfile(def, false, config.ModManagementConfig.ProfileName!, config.GeneralConfig.TcliConfig); + + string package = config.ModManagementConfig.Package!; + + HttpClient http = new(); - string zipPath = config.InstallConfig.Package!; - bool isTemp = false; - if (!File.Exists(zipPath)) + int returnCode; + if (File.Exists(package)) { - var match = FullPackageNameRegex.Match(zipPath); - if (!match.Success) - { - Write.ErrorExit($"Package name does not exist as a file and is not a valid package name (namespace-author): {zipPath}"); - } - HttpClient http = new(); - var packageResponse = http.Send(config.Api.GetPackageMetadata(match.Groups[1].Value, match.Groups[2].Value)); - using StreamReader responseReader = new(packageResponse.Content.ReadAsStream()); - if (!packageResponse.IsSuccessStatusCode) - { - Write.ErrorExit($"Could not find package {zipPath}, got:\n{responseReader.ReadToEnd()}"); - return 1; - } - var data = PackageData.Deserialize(responseReader.ReadToEnd()); + returnCode = await InstallZip(config, http, def, profile, package, null); + } + else if (FullPackageNameRegex.IsMatch(package)) + { + returnCode = await InstallFromRepository(config, http, def, profile, package); + } + else + { + throw new CommandFatalException($"Package given does not exist as a zip and is not a valid package identifier (namespace-name): {package}"); + } - zipPath = Path.GetTempFileName(); - isTemp = true; - using var outFile = File.OpenWrite(zipPath); + if (returnCode != 0) + return returnCode; - using var downloadStream = http.Send(new HttpRequestMessage(HttpMethod.Get, data!.LatestVersion!.DownloadUrl)).Content.ReadAsStream(); + GameDefinition.SetGameDefinitions(config.GeneralConfig.TcliConfig, defs); + + return 0; + } - downloadStream.CopyTo(outFile); + private static async Task InstallFromRepository(Config config, HttpClient http, GameDefinition game, ModProfile profile, string packageId) + { + var packageParts = packageId.Split('-'); + var packageResponse = await http.SendAsync(config.Api.GetPackageMetadata(packageParts[0], packageParts[1])); + packageResponse.EnsureSuccessStatusCode(); + var package = (await PackageData.DeserializeAsync(await packageResponse.Content.ReadAsStreamAsync()))!; + var tempZipPath = await DownloadTemp(http, package); + var returnCode = await InstallZip(config, http, game, profile, tempZipPath, package.Namespace); + File.Delete(tempZipPath); + return returnCode; + } + + private static async Task InstallZip(Config config, HttpClient http, GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace) + { + using var zip = ZipFile.OpenRead(zipPath); + var manifestFile = zip.GetEntry("manifest.json") ?? throw new CommandFatalException("Package zip needs a manifest.json!"); + var manifest = await PackageManifestV1.DeserializeAsync(manifestFile.Open()) + ?? throw new CommandFatalException("Package manifest.json is invalid! Please check against https://thunderstore.io/tools/manifest-v1-validator/"); + + var modsToInstall = ModDependencyTree.Generate(config, http, manifest).ToArray(); + + var downloadTasks = modsToInstall.Select(mod => DownloadTemp(http, mod)).ToArray(); + var spinner = new ProgressSpinner("mods downloaded", downloadTasks); + await spinner.Start(); + + foreach (var (tempZipPath, package) in downloadTasks.Select(x => x.Result).Zip(modsToInstall)) + { + int returnCode = RunInstaller(game, profile, tempZipPath, package.Namespace); + File.Delete(tempZipPath); + if (returnCode != 0) + return returnCode; } + return RunInstaller(game, profile, zipPath, backupNamespace); + } + + private static async Task DownloadTemp(HttpClient http, PackageData package) + { + string path = Path.GetTempFileName(); + await using var file = File.OpenWrite(path); + using var response = await http.SendAsync(new HttpRequestMessage(HttpMethod.Get, package.LatestVersion!.DownloadUrl!)); + response.EnsureSuccessStatusCode(); + var zipStream = await response.Content.ReadAsStreamAsync(); + await zipStream.CopyToAsync(file); + return path; + } + + private static int RunInstaller(GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace) + { string installerName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "tcli-bepinex-installer.exe" : "tcli-bepinex-installer"; var bepinexInstallerPath = Path.Combine(Path.GetDirectoryName(typeof(InstallCommand).Assembly.Location)!, installerName); @@ -83,15 +132,20 @@ public static int Run(Config config) ArgumentList = { "install", - def.InstallDirectory, + game.InstallDirectory, profile.ProfileDirectory, zipPath }, RedirectStandardOutput = true, RedirectStandardError = true }; + if (backupNamespace != null) + { + installerInfo.ArgumentList.Add("--namespace-backup"); + installerInfo.ArgumentList.Add(backupNamespace); + } - Process installerProcess = Process.Start(installerInfo)!; + var installerProcess = Process.Start(installerInfo)!; installerProcess.WaitForExit(); Write.Light(installerProcess.StandardOutput.ReadToEnd()); @@ -101,13 +155,6 @@ public static int Run(Config config) Write.Error(errors); } - if (isTemp) - { - File.Delete(zipPath); - } - - GameDefinition.SetGameDefinitions(config.GeneralConfig.TcliConfig, defs); - return installerProcess.ExitCode; } } diff --git a/ThunderstoreCLI/Commands/PublishCommand.cs b/ThunderstoreCLI/Commands/PublishCommand.cs index a1965ae..ed00940 100644 --- a/ThunderstoreCLI/Commands/PublishCommand.cs +++ b/ThunderstoreCLI/Commands/PublishCommand.cs @@ -209,41 +209,14 @@ private static void PublishPackageRequest(Config config, string uploadUuid) stream.Seek(part.Offset, SeekOrigin.Begin); byte[] hash; - var chunk = new MemoryStream(); - const int blocksize = 65536; - - using (var reader = new BinaryReader(stream, Encoding.Default, true)) + using (var md5 = MD5.Create()) { - using (var md5 = MD5.Create()) - { - md5.Initialize(); - var length = part.Length; - - while (length > blocksize) - { - length -= blocksize; - var bytes = reader.ReadBytes(blocksize); - md5.TransformBlock(bytes, 0, blocksize, null, 0); - await chunk.WriteAsync(bytes); - } - - var finalBytes = reader.ReadBytes(length); - md5.TransformFinalBlock(finalBytes, 0, length); - - if (md5.Hash is null) - { - Write.ErrorExit($"MD5 hashing failed for part #{part.PartNumber})"); - throw new PublishCommandException(); - } - - hash = md5.Hash; - await chunk.WriteAsync(finalBytes); - chunk.Position = 0; - } + hash = await md5.ComputeHashAsync(stream); } var request = new HttpRequestMessage(HttpMethod.Put, part.Url); - request.Content = new StreamContent(chunk); + stream.Seek(part.Offset, SeekOrigin.Begin); + request.Content = new StreamContent(stream); request.Content.Headers.ContentMD5 = hash; request.Content.Headers.ContentLength = part.Length; diff --git a/ThunderstoreCLI/Commands/UninstallCommand.cs b/ThunderstoreCLI/Commands/UninstallCommand.cs new file mode 100644 index 0000000..d3d97cb --- /dev/null +++ b/ThunderstoreCLI/Commands/UninstallCommand.cs @@ -0,0 +1,11 @@ +using ThunderstoreCLI.Configuration; + +namespace ThunderstoreCLI.Commands; + +public static class UninstallCommand +{ + public static int Run(Config config) + { + return 0; + } +} diff --git a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs index 67b93d3..c9626bb 100644 --- a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs +++ b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs @@ -13,7 +13,8 @@ public override GeneralConfig GetGeneralConfig() { return new GeneralConfig() { - TcliConfig = options.TcliDirectory + TcliConfig = options.TcliDirectory, + Repository = options.Repository }; } } @@ -77,13 +78,13 @@ public override AuthConfig GetAuthConfig() } } -public class CLIInstallCommandConfig : BaseConfig +public class ModManagementCommandConfig : BaseConfig { - public CLIInstallCommandConfig(InstallOptions options) : base(options) { } + public ModManagementCommandConfig(ModManagementOptions options) : base(options) { } - public override InstallConfig? GetInstallConfig() + public override ModManagementConfig? GetInstallConfig() { - return new InstallConfig() + return new ModManagementConfig() { //ManagerIdentifier = options.ManagerId GameIdentifer = options.GameName, diff --git a/ThunderstoreCLI/Configuration/Config.cs b/ThunderstoreCLI/Configuration/Config.cs index 2eff251..9dba97c 100644 --- a/ThunderstoreCLI/Configuration/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -13,13 +13,13 @@ public class Config public BuildConfig BuildConfig { get; private set; } public PublishConfig PublishConfig { get; private set; } public AuthConfig AuthConfig { get; private set; } - public InstallConfig InstallConfig { get; private set; } + public ModManagementConfig ModManagementConfig { get; private set; } // ReSharper restore AutoPropertyCanBeMadeGetOnly.Local private readonly Lazy api; public ApiHelper Api => api.Value; - private Config(GeneralConfig generalConfig, PackageConfig packageConfig, InitConfig initConfig, BuildConfig buildConfig, PublishConfig publishConfig, AuthConfig authConfig, InstallConfig installConfig) + private Config(GeneralConfig generalConfig, PackageConfig packageConfig, InitConfig initConfig, BuildConfig buildConfig, PublishConfig publishConfig, AuthConfig authConfig, ModManagementConfig modManagementConfig) { api = new Lazy(() => new ApiHelper(this)); GeneralConfig = generalConfig; @@ -28,7 +28,7 @@ private Config(GeneralConfig generalConfig, PackageConfig packageConfig, InitCon BuildConfig = buildConfig; PublishConfig = publishConfig; AuthConfig = authConfig; - InstallConfig = installConfig; + ModManagementConfig = modManagementConfig; } public static Config FromCLI(IConfigProvider cliConfig) { @@ -117,7 +117,7 @@ public static Config Parse(IConfigProvider[] configProviders) var buildConfig = new BuildConfig(); var publishConfig = new PublishConfig(); var authConfig = new AuthConfig(); - var installConfig = new InstallConfig(); + var installConfig = new ModManagementConfig(); var result = new Config(generalConfig, packageMeta, initConfig, buildConfig, publishConfig, authConfig, installConfig); foreach (var provider in configProviders) { @@ -213,7 +213,7 @@ public class AuthConfig public string? AuthToken { get; set; } } -public class InstallConfig +public class ModManagementConfig { public string? GameIdentifer { get; set; } //public string? ManagerIdentifier { get; set; } diff --git a/ThunderstoreCLI/Configuration/EmptyConfig.cs b/ThunderstoreCLI/Configuration/EmptyConfig.cs index d4c23e8..e348f6c 100644 --- a/ThunderstoreCLI/Configuration/EmptyConfig.cs +++ b/ThunderstoreCLI/Configuration/EmptyConfig.cs @@ -34,7 +34,7 @@ public virtual void Parse(Config currentConfig) { } return null; } - public virtual InstallConfig? GetInstallConfig() + public virtual ModManagementConfig? GetInstallConfig() { return null; } diff --git a/ThunderstoreCLI/Configuration/IConfigProvider.cs b/ThunderstoreCLI/Configuration/IConfigProvider.cs index 587e32c..71ab859 100644 --- a/ThunderstoreCLI/Configuration/IConfigProvider.cs +++ b/ThunderstoreCLI/Configuration/IConfigProvider.cs @@ -10,5 +10,5 @@ public interface IConfigProvider BuildConfig? GetBuildConfig(); PublishConfig? GetPublishConfig(); AuthConfig? GetAuthConfig(); - InstallConfig? GetInstallConfig(); + ModManagementConfig? GetInstallConfig(); } diff --git a/ThunderstoreCLI/Configuration/ProjectFileConfig.cs b/ThunderstoreCLI/Configuration/ProjectFileConfig.cs index 731cf38..2afec61 100644 --- a/ThunderstoreCLI/Configuration/ProjectFileConfig.cs +++ b/ThunderstoreCLI/Configuration/ProjectFileConfig.cs @@ -1,4 +1,5 @@ using ThunderstoreCLI.Models; +using ThunderstoreCLI.Utils; using static Crayon.Output; namespace ThunderstoreCLI.Configuration; diff --git a/ThunderstoreCLI/Game/GameDefinition.cs b/ThunderstoreCLI/Game/GameDefinition.cs index 8049c23..89e550d 100644 --- a/ThunderstoreCLI/Game/GameDefinition.cs +++ b/ThunderstoreCLI/Game/GameDefinition.cs @@ -42,7 +42,6 @@ internal static GameDefinition FromHardcodedIdentifier(string tcliDir, Hardcoded HardcodedGame.ROR2 => FromSteamId(tcliDir, 632360, "ror2", "Risk of Rain 2"), HardcodedGame.VRISING => FromSteamId(tcliDir, 1604030, "vrising", "V Rising"), HardcodedGame.VRISING_SERVER => FromSteamId(tcliDir, 1829350, "vrising_server", "V Rising Dedicated Server"), - HardcodedGame.VRISING_SERVER_BUILTIN => FromSteamId(tcliDir, 1604030, "VRising_Server", "virsing_server_builtin", "V Rising Built-in Server"), _ => throw new ArgumentException("Invalid enum value", nameof(game)) }; } @@ -69,6 +68,5 @@ internal enum HardcodedGame { ROR2, VRISING, - VRISING_SERVER, - VRISING_SERVER_BUILTIN + VRISING_SERVER } diff --git a/ThunderstoreCLI/Game/ModProfile.cs b/ThunderstoreCLI/Game/ModProfile.cs index 1d45585..a86f8c6 100644 --- a/ThunderstoreCLI/Game/ModProfile.cs +++ b/ThunderstoreCLI/Game/ModProfile.cs @@ -9,6 +9,7 @@ public class ModProfile : BaseJson public bool IsGlobal { get; } public string Name { get; } public string ProfileDirectory { get; } + public List InstalledMods { get; } #pragma warning disable CS8618 private ModProfile() { } @@ -18,6 +19,7 @@ internal ModProfile(GameDefinition gameDef, bool global, string name, string tcl { IsGlobal = global; Name = name; + InstalledMods = new(); var directory = Path.Combine(tcliDirectory, "Profiles", gameDef.Identifier, name); if (!Directory.Exists(directory)) diff --git a/ThunderstoreCLI/Models/BaseJson.cs b/ThunderstoreCLI/Models/BaseJson.cs index 829a3d2..2d509cc 100644 --- a/ThunderstoreCLI/Models/BaseJson.cs +++ b/ThunderstoreCLI/Models/BaseJson.cs @@ -13,6 +13,11 @@ public string Serialize(JsonSerializerSettings? options) } public static T? Deserialize(string json) => Deserialize(json, null); + public static T? Deserialize(Stream stream) + { + using var reader = new StreamReader(stream); + return JsonConvert.DeserializeObject(reader.ReadToEnd()); + } public static T? Deserialize(string json, JsonSerializerSettings? options) { return JsonConvert.DeserializeObject(json, options); diff --git a/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs b/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs deleted file mode 100644 index e734375..0000000 --- a/ThunderstoreCLI/Models/Interaction/BaseInteraction.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace ThunderstoreCLI.Models.Interaction; - -public enum InteractionOutputType -{ - HUMAN, - JSON, -} - -public static class InteractionOptions -{ - public static InteractionOutputType OutputType { get; set; } = InteractionOutputType.HUMAN; -} - -public abstract class BaseInteraction<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : BaseJson - where T : BaseInteraction -{ - public abstract string GetHumanString(); - - public string GetString() - { - switch (InteractionOptions.OutputType) - { - case InteractionOutputType.HUMAN: - return GetHumanString(); - case InteractionOutputType.JSON: - return Serialize(); - default: - throw new NotSupportedException(); - } - } - - public void Write() - { - Console.WriteLine(GetString()); - } -} diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index ff6f7a7..f2e9fb9 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -1,7 +1,6 @@ using CommandLine; using ThunderstoreCLI.Commands; using ThunderstoreCLI.Configuration; -using ThunderstoreCLI.Models.Interaction; using ThunderstoreCLI.Utils; using static Crayon.Output; @@ -10,9 +9,6 @@ namespace ThunderstoreCLI; /// Options are arguments passed from command line. public abstract class BaseOptions { - [Option("output", Required = false, HelpText = "The output format for all output. Valid options are HUMAN and JSON. (does nothing)")] - public InteractionOutputType OutputType { get; set; } = InteractionOutputType.HUMAN; - [Option("tcli-directory", Required = false, HelpText = "Directory where TCLI keeps its data, %APPDATA%/ThunderstoreCLI on Windows and ~/.config/ThunderstoreCLI on Linux")] // will be initialized in Init if null public string TcliDirectory { get; set; } = null!; @@ -20,11 +16,13 @@ public abstract class BaseOptions [Option("repository", Required = false, HelpText = "URL of the default repository")] public string Repository { get; set; } = null!; + [Option("config-path", Required = false, Default = Defaults.PROJECT_CONFIG_PATH, HelpText = "Path for the project configuration file")] + public string? ConfigPath { get; set; } + public virtual void Init() { - InteractionOptions.OutputType = OutputType; - // ReSharper disable once ConstantNullCoalescingCondition + // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract TcliDirectory ??= Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ThunderstoreCLI"); } @@ -43,9 +41,6 @@ public virtual bool Validate() public abstract class PackageOptions : BaseOptions { - [Option("config-path", Required = false, Default = Defaults.PROJECT_CONFIG_PATH, HelpText = "Path for the project configuration file")] - public string? ConfigPath { get; set; } - [Option("package-name", SetName = "build", Required = false, HelpText = "Name for the package")] public string? Name { get; set; } @@ -150,8 +145,7 @@ public override int Execute() } } -[Verb("install", HelpText = "Installs a mod")] -public class InstallOptions : BaseOptions +public abstract class ModManagementOptions : BaseOptions { //public string? ManagerId { get; set; } @@ -167,6 +161,14 @@ public class InstallOptions : BaseOptions [Option(HelpText = "Set to install mods globally instead of into a profile", Default = false)] public bool Global { get; set; } + protected enum CommandInner + { + Install, + Uninstall + } + + protected abstract CommandInner CommandType { get; } + public override bool Validate() { #if NOINSTALLERS @@ -179,6 +181,24 @@ public override bool Validate() public override int Execute() { - return InstallCommand.Run(Config.FromCLI(new CLIInstallCommandConfig(this))); + var config = Config.FromCLI(new ModManagementCommandConfig(this)); + return CommandType switch + { + CommandInner.Install => InstallCommand.Run(config).GetAwaiter().GetResult(), + CommandInner.Uninstall => UninstallCommand.Run(config), + _ => throw new NotSupportedException() + }; } } + +[Verb("install")] +public class InstallOptions : ModManagementOptions +{ + protected override CommandInner CommandType => CommandInner.Install; +} + +[Verb("uninstall")] +public class UninstallOptions : ModManagementOptions +{ + protected override CommandInner CommandType => CommandInner.Uninstall; +} diff --git a/ThunderstoreCLI/Plugins/PluginManager.cs b/ThunderstoreCLI/Plugins/PluginManager.cs new file mode 100644 index 0000000..c89a8eb --- /dev/null +++ b/ThunderstoreCLI/Plugins/PluginManager.cs @@ -0,0 +1,25 @@ +using ThunderstoreCLI.Configuration; + +namespace ThunderstoreCLI.Plugins; + +public class PluginManager +{ + private class Plugin + { + + } + + private class PluginInfo + { + + } + + public string PluginDirectory { get; } + + private List LoadedPlugins = new(); + + public PluginManager(GeneralConfig config) + { + PluginDirectory = Path.Combine(config.TcliConfig, "Plugins"); + } +} diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index 17b7ca1..c0ca4e6 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using CommandLine; using ThunderstoreCLI.Models; +using ThunderstoreCLI.Utils; namespace ThunderstoreCLI; @@ -15,12 +16,13 @@ private static int Main(string[] args) #endif var updateChecker = UpdateChecker.CheckForUpdates(); - var exitCode = Parser.Default.ParseArguments(args) + var exitCode = Parser.Default.ParseArguments(args) .MapResult( (InitOptions o) => HandleParsed(o), (BuildOptions o) => HandleParsed(o), (PublishOptions o) => HandleParsed(o), (InstallOptions o) => HandleParsed(o), + (UninstallOptions o) => HandleParsed(o), _ => 1 // failure to parse ); UpdateChecker.WriteUpdateNotification(updateChecker); @@ -31,8 +33,22 @@ private static int HandleParsed(BaseOptions parsed) { parsed.Init(); if (!parsed.Validate()) + { return 1; - return parsed.Execute(); + } + try + { + return parsed.Execute(); + } + catch (CommandFatalException cfe) + { + Write.Error(cfe.ErrorMessage); +#if DEBUG + throw; +#else + return 1; +#endif + } } } diff --git a/ThunderstoreCLI/ThunderstoreCLI.csproj b/ThunderstoreCLI/ThunderstoreCLI.csproj index a3ddf9c..e22810c 100644 --- a/ThunderstoreCLI/ThunderstoreCLI.csproj +++ b/ThunderstoreCLI/ThunderstoreCLI.csproj @@ -23,6 +23,7 @@ true $(AssemblyName) enable + true diff --git a/ThunderstoreCLI/Utils/CommandFatalException.cs b/ThunderstoreCLI/Utils/CommandFatalException.cs new file mode 100644 index 0000000..8e9434b --- /dev/null +++ b/ThunderstoreCLI/Utils/CommandFatalException.cs @@ -0,0 +1,10 @@ +namespace ThunderstoreCLI.Utils; + +public sealed class CommandFatalException : Exception +{ + public string ErrorMessage { get; } + public CommandFatalException(string errorMessage) : base(errorMessage) + { + ErrorMessage = errorMessage; + } +} diff --git a/ThunderstoreCLI/Utils/ModDependencyTree.cs b/ThunderstoreCLI/Utils/ModDependencyTree.cs new file mode 100644 index 0000000..ed32a69 --- /dev/null +++ b/ThunderstoreCLI/Utils/ModDependencyTree.cs @@ -0,0 +1,47 @@ +using System.Collections.Concurrent; +using ThunderstoreCLI.Configuration; +using ThunderstoreCLI.Models; + +namespace ThunderstoreCLI.Utils; + +public static class ModDependencyTree +{ + public static IEnumerable Generate(Config config, HttpClient http, PackageManifestV1 root) + { + HashSet alreadyGottenPackages = new(); + foreach (var dependency in root.Dependencies!) + { + var depParts = dependency.Split('-'); + var depRequest = http.Send(config.Api.GetPackageMetadata(depParts[0], depParts[1])); + depRequest.EnsureSuccessStatusCode(); + var depData = PackageData.Deserialize(depRequest.Content.ReadAsStream()); + foreach (var package in GenerateInternal(config, http, depData!, package => alreadyGottenPackages.Contains(package.Fullname!))) + { + // this can happen on cyclical references, oh well + if (alreadyGottenPackages.Contains(package.Fullname!)) + continue; + + alreadyGottenPackages.Add(package.Fullname!); + yield return package; + } + } + } + private static IEnumerable GenerateInternal(Config config, HttpClient http, PackageData root, Predicate alreadyGotten) + { + if (alreadyGotten(root)) + yield break; + + foreach (var dependency in root.LatestVersion!.Dependencies!) + { + var depParts = dependency.Split('-'); + var depRequest = http.Send(config.Api.GetPackageMetadata(depParts[0], depParts[1])); + depRequest.EnsureSuccessStatusCode(); + var depData = PackageData.Deserialize(depRequest.Content.ReadAsStream()); + foreach (var package in GenerateInternal(config, http, depData!, alreadyGotten)) + { + yield return package; + } + } + yield return root; + } +} diff --git a/tcli-bepinex-installer/Cargo.lock b/tcli-bepinex-installer/Cargo.lock index 21ec841..fbe02c6 100644 --- a/tcli-bepinex-installer/Cargo.lock +++ b/tcli-bepinex-installer/Cargo.lock @@ -22,9 +22,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.57" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" [[package]] name = "atty" @@ -37,12 +37,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - [[package]] name = "base64ct" version = "1.0.1" @@ -57,9 +51,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] @@ -117,26 +111,24 @@ dependencies = [ [[package]] name = "clap" -version = "3.1.18" +version = "4.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +checksum = "30607dd93c420c6f1f80b544be522a0238a7db35e6a12968d28910983fee0df0" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", - "indexmap", - "lazy_static", + "once_cell", "strsim", "termcolor", - "textwrap", ] [[package]] name = "clap_derive" -version = "3.1.18" +version = "4.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +checksum = "a4a307492e1a34939f79d3b6b9650bd2b971513cd775436bf2b78defeb5af00b" dependencies = [ "heck", "proc-macro-error", @@ -147,9 +139,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" dependencies = [ "os_str_bytes", ] @@ -162,9 +154,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] @@ -180,19 +172,18 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" dependencies = [ "cfg-if", - "lazy_static", ] [[package]] name = "crypto-common" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -200,9 +191,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ "block-buffer", "crypto-common", @@ -211,32 +202,24 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ - "cfg-if", "crc32fast", - "libc", "miniz_oxide", ] [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", ] -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" - [[package]] name = "heck" version = "0.4.0" @@ -261,48 +244,32 @@ dependencies = [ "digest", ] -[[package]] -name = "indexmap" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" -dependencies = [ - "autocfg", - "hashbrown", -] - [[package]] name = "itoa" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "jobserver" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" dependencies = [ "libc", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" -version = "0.2.125" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "miniz_oxide" -version = "0.5.1" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] @@ -316,6 +283,12 @@ dependencies = [ "libc", ] +[[package]] +name = "once_cell" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -324,9 +297,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "os_str_bytes" -version = "6.0.1" +version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" [[package]] name = "password-hash" @@ -383,48 +356,48 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.38" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa" +checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.18" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "ryu" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "serde" -version = "1.0.137" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -433,9 +406,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.81" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa", "ryu", @@ -444,9 +417,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", @@ -455,9 +428,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.2" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", @@ -478,13 +451,13 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.94" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -508,26 +481,20 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -536,9 +503,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.9" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" dependencies = [ "itoa", "libc", @@ -559,10 +526,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] -name = "unicode-xid" -version = "0.2.3" +name = "unicode-ident" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "version_check" diff --git a/tcli-bepinex-installer/Cargo.toml b/tcli-bepinex-installer/Cargo.toml index 8cd5242..93f0acb 100644 --- a/tcli-bepinex-installer/Cargo.toml +++ b/tcli-bepinex-installer/Cargo.toml @@ -6,14 +6,14 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.57" +anyhow = "1.0.65" zip = "0.6.2" -thiserror = "1.0.31" -serde_json = "1.0.81" -serde = { version = "1.0.137", features = ["derive"] } +thiserror = "1.0.37" +serde_json = "1.0.85" +serde = { version = "1.0.145", features = ["derive"] } [dependencies.clap] -version = "3.1.18" +version = "4.0.9" features = ["derive", "cargo"] [profile.release] diff --git a/tcli-bepinex-installer/src/main.rs b/tcli-bepinex-installer/src/main.rs index f4eacb3..5675580 100644 --- a/tcli-bepinex-installer/src/main.rs +++ b/tcli-bepinex-installer/src/main.rs @@ -13,7 +13,7 @@ use zip::ZipArchive; #[derive(Parser)] #[clap(author, version, about)] -struct Args { +struct ClapArgs { #[clap(subcommand)] pub command: Commands, } @@ -24,6 +24,8 @@ enum Commands { game_directory: PathBuf, bepinex_directory: PathBuf, zip_path: PathBuf, + #[arg(long)] + namespace_backup: Option, }, Uninstall { game_directory: PathBuf, @@ -42,7 +44,7 @@ pub enum Error { InvalidManifest, #[error("Malformed zip")] MalformedZip, - #[error("Manifest does not contain a namespace, which is required for mod installs")] + #[error("Manifest does not contain a namespace and no backup was given, namespaces are required for mod installs")] MissingNamespace, #[error("Mod name is invalid (eg doesn't use a - between namespace and name)")] InvalidModName, @@ -62,13 +64,14 @@ struct ManifestV1 { } fn main() -> Result<()> { - let args = Args::parse(); + let args = ClapArgs::parse(); match args.command { Commands::Install { game_directory, bepinex_directory, zip_path, + namespace_backup, } => { if !game_directory.exists() { bail!(Error::PathDoesNotExist(game_directory)); @@ -79,7 +82,7 @@ fn main() -> Result<()> { if !zip_path.exists() { bail!(Error::PathDoesNotExist(zip_path)); } - install(game_directory, bepinex_directory, zip_path) + install(game_directory, bepinex_directory, zip_path, namespace_backup) } Commands::Uninstall { game_directory, @@ -97,7 +100,7 @@ fn main() -> Result<()> { } } -fn install(game_dir: PathBuf, bep_dir: PathBuf, zip_path: PathBuf) -> Result<()> { +fn install(game_dir: PathBuf, bep_dir: PathBuf, zip_path: PathBuf, namespace_backup: Option) -> Result<()> { let mut zip = ZipArchive::new(std::fs::File::open(zip_path)?)?; if !zip.file_names().any(|name| name == "manifest.json") { @@ -112,7 +115,7 @@ fn install(game_dir: PathBuf, bep_dir: PathBuf, zip_path: PathBuf) -> Result<()> if manifest.name.starts_with("BepInExPack") { install_bepinex(game_dir, bep_dir, zip) } else { - install_mod(bep_dir, zip, manifest) + install_mod(bep_dir, zip, manifest, namespace_backup) } } @@ -169,12 +172,13 @@ fn install_mod( bep_dir: PathBuf, mut zip: ZipArchive, manifest: ManifestV1, + namespace_backup: Option, ) -> Result<()> { let write_opts = OpenOptions::new().write(true).create(true).clone(); let full_name = format!( "{}-{}", - manifest.namespace.ok_or(Error::MissingNamespace)?, + manifest.namespace.or(namespace_backup).ok_or(Error::MissingNamespace)?, manifest.name ); From cdffd300bc59441217eabf9a1467824794a7317c Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 11 Oct 2022 03:04:07 -0500 Subject: [PATCH 25/91] Add support for specifying mod version to install --- ThunderstoreCLI.Tests/Utils/Spinner.cs | 6 +- ThunderstoreCLI/API/ApiHelper.cs | 8 ++ ThunderstoreCLI/Commands/InstallCommand.cs | 90 +++++++++++-------- ThunderstoreCLI/Commands/PublishCommand.cs | 2 +- .../Configuration/CLIParameterConfig.cs | 2 - ThunderstoreCLI/Configuration/Config.cs | 2 - ThunderstoreCLI/Game/GameDefinition.cs | 55 +++++++----- ThunderstoreCLI/Game/ModProfile.cs | 12 ++- ThunderstoreCLI/Options.cs | 3 - ThunderstoreCLI/PackageManifestV1.cs | 15 ++++ ThunderstoreCLI/Utils/RequestBuilder.cs | 4 + ThunderstoreCLI/Utils/Spinner.cs | 18 +++- 12 files changed, 139 insertions(+), 78 deletions(-) diff --git a/ThunderstoreCLI.Tests/Utils/Spinner.cs b/ThunderstoreCLI.Tests/Utils/Spinner.cs index e8258ce..fae60e2 100644 --- a/ThunderstoreCLI.Tests/Utils/Spinner.cs +++ b/ThunderstoreCLI.Tests/Utils/Spinner.cs @@ -31,7 +31,7 @@ public async Task WhenTaskFails_ThrowsSpinnerException() CreateTask(false) }); - await Assert.ThrowsAsync(async () => await spinner.Start()); + await Assert.ThrowsAsync(async () => await spinner.Spin()); } [Fact] @@ -41,7 +41,7 @@ public async Task WhenReceivesSingleTask_ItJustWorks() CreateTask(true) }); - await spinner.Start(); + await spinner.Spin(); } [Fact] @@ -53,6 +53,6 @@ public async Task WhenReceivesMultipleTasks_ItJustWorks() CreateTask(true) }); - await spinner.Start(); + await spinner.Spin(); } } diff --git a/ThunderstoreCLI/API/ApiHelper.cs b/ThunderstoreCLI/API/ApiHelper.cs index 7dc3ef5..214d471 100644 --- a/ThunderstoreCLI/API/ApiHelper.cs +++ b/ThunderstoreCLI/API/ApiHelper.cs @@ -81,6 +81,14 @@ public HttpRequestMessage GetPackageMetadata(string author, string name) .GetRequest(); } + public HttpRequestMessage GetPackageVersionMetadata(string author, string name, string version) + { + return BaseRequestBuilder + .StartNew() + .WithEndpoint(EXPERIMENTAL + $"package/{author}/{name}/{version}/") + .GetRequest(); + } + private static string SerializeFileData(string filePath) { return new FileData() diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index 3bc1a05..60edb09 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -21,33 +21,27 @@ public static class InstallCommand { "vrising_dedicated", HardcodedGame.VRISING_SERVER } }; - internal static readonly Regex FullPackageNameRegex = new(@"^([a-zA-Z0-9_]+)-([a-zA-Z0-9_]+)$"); + // will match either ab-cd or ab-cd-123.456.7890 + internal static readonly Regex FullPackageNameRegex = new(@"^(\w+)-(\w+)(?:|-(\d+\.\d+\.\d+))$"); public static async Task Run(Config config) { - List defs = GameDefinition.GetGameDefinitions(config.GeneralConfig.TcliConfig); + using var defCollection = GameDefintionCollection.FromDirectory(config.GeneralConfig.TcliConfig); + var defs = defCollection.List; GameDefinition? def = defs.FirstOrDefault(x => x.Identifier == config.ModManagementConfig.GameIdentifer); if (def == null && IDToHardcoded.TryGetValue(config.ModManagementConfig.GameIdentifer!, out var hardcoded)) { def = GameDefinition.FromHardcodedIdentifier(config.GeneralConfig.TcliConfig, hardcoded); defs.Add(def); } - else + else if (def == null) { Write.ErrorExit($"Not configured for the game: {config.ModManagementConfig.GameIdentifer}"); return 1; } - ModProfile? profile; - if (config.ModManagementConfig.Global!.Value) - { - profile = def.GlobalProfile; - } - else - { - profile = def.Profiles.FirstOrDefault(x => x.Name == config.ModManagementConfig.ProfileName); - } - profile ??= new ModProfile(def, false, config.ModManagementConfig.ProfileName!, config.GeneralConfig.TcliConfig); + ModProfile? profile = def.Profiles.FirstOrDefault(x => x.Name == config.ModManagementConfig.ProfileName); + profile ??= new ModProfile(def, config.ModManagementConfig.ProfileName!, config.GeneralConfig.TcliConfig); string package = config.ModManagementConfig.Package!; @@ -67,22 +61,32 @@ public static async Task Run(Config config) throw new CommandFatalException($"Package given does not exist as a zip and is not a valid package identifier (namespace-name): {package}"); } - if (returnCode != 0) - return returnCode; + if (returnCode == 0) defCollection.Validate(); - GameDefinition.SetGameDefinitions(config.GeneralConfig.TcliConfig, defs); - - return 0; + return returnCode; } private static async Task InstallFromRepository(Config config, HttpClient http, GameDefinition game, ModProfile profile, string packageId) { var packageParts = packageId.Split('-'); - var packageResponse = await http.SendAsync(config.Api.GetPackageMetadata(packageParts[0], packageParts[1])); - packageResponse.EnsureSuccessStatusCode(); - var package = (await PackageData.DeserializeAsync(await packageResponse.Content.ReadAsStreamAsync()))!; - var tempZipPath = await DownloadTemp(http, package); - var returnCode = await InstallZip(config, http, game, profile, tempZipPath, package.Namespace); + + PackageVersionData version; + if (packageParts.Length == 3) + { + var versionResponse = await http.SendAsync(config.Api.GetPackageVersionMetadata(packageParts[0], packageParts[1], packageParts[2])); + versionResponse.EnsureSuccessStatusCode(); + version = (await PackageVersionData.DeserializeAsync(await versionResponse.Content.ReadAsStreamAsync()))!; + } + else + { + var packageResponse = await http.SendAsync(config.Api.GetPackageMetadata(packageParts[0], packageParts[1])); + packageResponse.EnsureSuccessStatusCode(); + version = (await PackageData.DeserializeAsync(await packageResponse.Content.ReadAsStreamAsync()))!.LatestVersion!; + } + + + var tempZipPath = await DownloadTemp(http, version); + var returnCode = await InstallZip(config, http, game, profile, tempZipPath, version.Namespace!); File.Delete(tempZipPath); return returnCode; } @@ -94,28 +98,42 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe var manifest = await PackageManifestV1.DeserializeAsync(manifestFile.Open()) ?? throw new CommandFatalException("Package manifest.json is invalid! Please check against https://thunderstore.io/tools/manifest-v1-validator/"); - var modsToInstall = ModDependencyTree.Generate(config, http, manifest).ToArray(); + manifest.Namespace ??= backupNamespace; - var downloadTasks = modsToInstall.Select(mod => DownloadTemp(http, mod)).ToArray(); - var spinner = new ProgressSpinner("mods downloaded", downloadTasks); - await spinner.Start(); + var dependenciesToInstall = ModDependencyTree.Generate(config, http, manifest) + .Where(dependency => !profile.InstalledModVersions.ContainsKey(dependency.Fullname!)) + .ToArray(); - foreach (var (tempZipPath, package) in downloadTasks.Select(x => x.Result).Zip(modsToInstall)) + if (dependenciesToInstall.Length > 0) { - int returnCode = RunInstaller(game, profile, tempZipPath, package.Namespace); - File.Delete(tempZipPath); - if (returnCode != 0) - return returnCode; + var downloadTasks = dependenciesToInstall.Select(mod => DownloadTemp(http, mod.LatestVersion!)).ToArray(); + + var spinner = new ProgressSpinner("mods downloaded", downloadTasks); + await spinner.Spin(); + + foreach (var (tempZipPath, package) in downloadTasks.Select(x => x.Result).Zip(dependenciesToInstall)) + { + int returnCode = RunInstaller(game, profile, tempZipPath, package.Namespace); + File.Delete(tempZipPath); + if (returnCode != 0) + return returnCode; + profile.InstalledModVersions[package.Fullname!] = new PackageManifestV1(package.LatestVersion!); + } } - return RunInstaller(game, profile, zipPath, backupNamespace); + var exitCode = RunInstaller(game, profile, zipPath, backupNamespace); + if (exitCode == 0) + { + profile.InstalledModVersions[$"{manifest.Namespace ?? backupNamespace}-{manifest.Name}"] = manifest; + } + return exitCode; } - private static async Task DownloadTemp(HttpClient http, PackageData package) + private static async Task DownloadTemp(HttpClient http, PackageVersionData version) { string path = Path.GetTempFileName(); await using var file = File.OpenWrite(path); - using var response = await http.SendAsync(new HttpRequestMessage(HttpMethod.Get, package.LatestVersion!.DownloadUrl!)); + using var response = await http.SendAsync(new HttpRequestMessage(HttpMethod.Get, version.DownloadUrl!)); response.EnsureSuccessStatusCode(); var zipStream = await response.Content.ReadAsStreamAsync(); await zipStream.CopyToAsync(file); @@ -125,7 +143,7 @@ private static async Task DownloadTemp(HttpClient http, PackageData pack private static int RunInstaller(GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace) { string installerName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "tcli-bepinex-installer.exe" : "tcli-bepinex-installer"; - var bepinexInstallerPath = Path.Combine(Path.GetDirectoryName(typeof(InstallCommand).Assembly.Location)!, installerName); + var bepinexInstallerPath = Path.Combine(AppContext.BaseDirectory, installerName); ProcessStartInfo installerInfo = new(bepinexInstallerPath) { diff --git a/ThunderstoreCLI/Commands/PublishCommand.cs b/ThunderstoreCLI/Commands/PublishCommand.cs index ed00940..559e530 100644 --- a/ThunderstoreCLI/Commands/PublishCommand.cs +++ b/ThunderstoreCLI/Commands/PublishCommand.cs @@ -92,7 +92,7 @@ public static int PublishFile(Config config, string filepath) try { var spinner = new ProgressSpinner("chunks uploaded", uploadTasks); - spinner.Start().GetAwaiter().GetResult(); + spinner.Spin().GetAwaiter().GetResult(); } catch (SpinnerException) { diff --git a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs index c9626bb..6a29d97 100644 --- a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs +++ b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs @@ -86,9 +86,7 @@ public ModManagementCommandConfig(ModManagementOptions options) : base(options) { return new ModManagementConfig() { - //ManagerIdentifier = options.ManagerId GameIdentifer = options.GameName, - Global = options.Global, ProfileName = options.Profile, Package = options.Package }; diff --git a/ThunderstoreCLI/Configuration/Config.cs b/ThunderstoreCLI/Configuration/Config.cs index 9dba97c..577ae34 100644 --- a/ThunderstoreCLI/Configuration/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -216,8 +216,6 @@ public class AuthConfig public class ModManagementConfig { public string? GameIdentifer { get; set; } - //public string? ManagerIdentifier { get; set; } public string? Package { get; set; } - public bool? Global { get; set; } public string? ProfileName { get; set; } } diff --git a/ThunderstoreCLI/Game/GameDefinition.cs b/ThunderstoreCLI/Game/GameDefinition.cs index 89e550d..1d42e83 100644 --- a/ThunderstoreCLI/Game/GameDefinition.cs +++ b/ThunderstoreCLI/Game/GameDefinition.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; +using System.Collections; using ThunderstoreCLI.Models; using ThunderstoreCLI.Utils; @@ -7,12 +6,10 @@ namespace ThunderstoreCLI.Game; public class GameDefinition : BaseJson { - private const string FILE_NAME = "GameDefintions.json"; - public string Identifier { get; } - public string Name { get; } - public string InstallDirectory { get; private set; } + public string Identifier { get; set; } + public string Name { get; set; } + public string InstallDirectory { get; set; } public List Profiles { get; private set; } = new(); - public ModProfile GlobalProfile { get; } #pragma warning disable CS8618 private GameDefinition() { } @@ -23,16 +20,6 @@ internal GameDefinition(string id, string name, string installDirectory, string Identifier = id; Name = name; InstallDirectory = installDirectory; - GlobalProfile = new ModProfile(this, true, "Global", tcliDirectory); - } - - internal static List GetGameDefinitions(string tcliDirectory) - { - var filename = Path.Combine(tcliDirectory, FILE_NAME); - if (File.Exists(filename)) - return DeserializeList(File.ReadAllText(filename)) ?? new(); - else - return new(); } internal static GameDefinition FromHardcodedIdentifier(string tcliDir, HardcodedGame game) @@ -48,7 +35,7 @@ internal static GameDefinition FromHardcodedIdentifier(string tcliDir, Hardcoded internal static GameDefinition FromSteamId(string tcliDir, uint steamId, string id, string name) { - return new GameDefinition(id, name, SteamUtils.FindInstallDirectory(steamId), tcliDir); + return new GameDefinition(id, name, SteamUtils.FindInstallDirectory(steamId)!, tcliDir); } internal static GameDefinition FromSteamId(string tcliDir, uint steamId, string subdirectory, string id, string name) @@ -57,10 +44,38 @@ internal static GameDefinition FromSteamId(string tcliDir, uint steamId, string gameDef.InstallDirectory = Path.Combine(gameDef.InstallDirectory, subdirectory); return gameDef; } +} + +public sealed class GameDefintionCollection : IEnumerable, IDisposable +{ + private const string FILE_NAME = "GameDefintions.json"; + + private readonly string tcliDirectory; + private bool shouldWrite = true; + public List List { get; } + + internal static GameDefintionCollection FromDirectory(string tcliDirectory) => new(tcliDirectory); + + private GameDefintionCollection(string tcliDir) + { + tcliDirectory = tcliDir; + var filename = Path.Combine(tcliDirectory, FILE_NAME); + if (File.Exists(filename)) + List = GameDefinition.DeserializeList(File.ReadAllText(filename)) ?? new(); + else + List = new(); + } + + public void Validate() => shouldWrite = true; + + public IEnumerator GetEnumerator() => List.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => List.GetEnumerator(); - internal static void SetGameDefinitions(string tcliDirectory, List list) + public void Dispose() { - File.WriteAllText(Path.Combine(tcliDirectory, FILE_NAME), list.SerializeList(BaseJson.IndentedSettings)); + if (!shouldWrite) return; + File.WriteAllText(Path.Combine(tcliDirectory, FILE_NAME), List.SerializeList(BaseJson.IndentedSettings)); + shouldWrite = false; } } diff --git a/ThunderstoreCLI/Game/ModProfile.cs b/ThunderstoreCLI/Game/ModProfile.cs index a86f8c6..5dc1d70 100644 --- a/ThunderstoreCLI/Game/ModProfile.cs +++ b/ThunderstoreCLI/Game/ModProfile.cs @@ -6,20 +6,18 @@ namespace ThunderstoreCLI.Game; public class ModProfile : BaseJson { - public bool IsGlobal { get; } - public string Name { get; } - public string ProfileDirectory { get; } - public List InstalledMods { get; } + public string Name { get; set; } + public string ProfileDirectory { get; set; } + public Dictionary InstalledModVersions { get; } = new(); #pragma warning disable CS8618 private ModProfile() { } #pragma warning restore CS8618 - internal ModProfile(GameDefinition gameDef, bool global, string name, string tcliDirectory) + internal ModProfile(GameDefinition gameDef, string name, string tcliDirectory) { - IsGlobal = global; Name = name; - InstalledMods = new(); + InstalledModVersions = new(); var directory = Path.Combine(tcliDirectory, "Profiles", gameDef.Identifier, name); if (!Directory.Exists(directory)) diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index f2e9fb9..d966c92 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -158,9 +158,6 @@ public abstract class ModManagementOptions : BaseOptions [Option(HelpText = "Profile to install to", Default = "Default")] public string? Profile { get; set; } - [Option(HelpText = "Set to install mods globally instead of into a profile", Default = false)] - public bool Global { get; set; } - protected enum CommandInner { Install, diff --git a/ThunderstoreCLI/PackageManifestV1.cs b/ThunderstoreCLI/PackageManifestV1.cs index 236cb29..6b1fc6c 100644 --- a/ThunderstoreCLI/PackageManifestV1.cs +++ b/ThunderstoreCLI/PackageManifestV1.cs @@ -22,4 +22,19 @@ public class PackageManifestV1 : BaseJson [JsonProperty("website_url")] public string? WebsiteUrl { get; set; } + + private string? fullName; + public string FullName => fullName ??= $"{Namespace}-{Name}"; + + public PackageManifestV1() { } + + public PackageManifestV1(PackageVersionData version) + { + Namespace = version.Namespace; + Name = version.Name; + Description = version.Description; + VersionNumber = version.VersionNumber; + Dependencies = version.Dependencies?.ToArray() ?? Array.Empty(); + WebsiteUrl = version.WebsiteUrl; + } } diff --git a/ThunderstoreCLI/Utils/RequestBuilder.cs b/ThunderstoreCLI/Utils/RequestBuilder.cs index 18cd77b..1fcf8d7 100644 --- a/ThunderstoreCLI/Utils/RequestBuilder.cs +++ b/ThunderstoreCLI/Utils/RequestBuilder.cs @@ -40,6 +40,10 @@ public HttpRequestMessage GetRequest() public RequestBuilder WithEndpoint(string endpoint) { + if (!endpoint.EndsWith('/')) + { + endpoint += '/'; + } builder.Path = endpoint; return this; } diff --git a/ThunderstoreCLI/Utils/Spinner.cs b/ThunderstoreCLI/Utils/Spinner.cs index 3031e64..da47afc 100644 --- a/ThunderstoreCLI/Utils/Spinner.cs +++ b/ThunderstoreCLI/Utils/Spinner.cs @@ -9,8 +9,9 @@ public class ProgressSpinner private static readonly char[] _spinChars = { '|', '/', '-', '\\' }; private readonly string _label; private readonly Task[] _tasks; + private readonly int _offset; - public ProgressSpinner(string label, Task[] tasks) + public ProgressSpinner(string label, Task[] tasks, int offset = 0) { if (tasks.Length == 0) { @@ -19,9 +20,10 @@ public ProgressSpinner(string label, Task[] tasks) _label = label; _tasks = tasks; + _offset = offset; } - public async Task Start() + public async Task Spin() { // Cursor operations are not always available e.g. in GitHub Actions environment. // Done up here to minimize exception usage (throws and catches are expensive and all) @@ -37,6 +39,14 @@ public async Task Start() canUseCursor = false; } + if (!canUseCursor && _offset != 0) + { + for (int i = 1; i <= _offset; i++) + { + Console.Write(Green($"{0}/{_tasks.Length + _offset} {_label}")); + } + } + while (true) { IEnumerable faultedTasks; @@ -52,13 +62,13 @@ public async Task Start() { var spinner = completed == _tasks.Length ? '✓' : _spinChars[_spinIndex++ % _spinChars.Length]; Console.SetCursorPosition(0, Console.CursorTop); - Console.Write(Green($"{completed}/{_tasks.Length} {_label} {spinner}")); + Console.Write(Green($"{completed + _offset}/{_tasks.Length + _offset} {_label} {spinner}")); } else { if (completed > _lastSeenCompleted) { - Write.Success($"{completed}/{_tasks.Length} {_label}"); + Write.Success($"{completed + _offset}/{_tasks.Length + _offset} {_label}"); _lastSeenCompleted = completed; } } From e7154411ce4291ce2fa7f48b57b507f3366513d2 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 11 Oct 2022 03:04:25 -0500 Subject: [PATCH 26/91] Add additional Steam searching for Windows/macOS --- ThunderstoreCLI/Utils/SteamUtils.cs | 48 +++++++++++++++++++---------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/ThunderstoreCLI/Utils/SteamUtils.cs b/ThunderstoreCLI/Utils/SteamUtils.cs index 83ddf84..f4386dc 100644 --- a/ThunderstoreCLI/Utils/SteamUtils.cs +++ b/ThunderstoreCLI/Utils/SteamUtils.cs @@ -1,13 +1,20 @@ using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Security.AccessControl; using System.Text.RegularExpressions; +using Microsoft.Win32; namespace ThunderstoreCLI.Utils; public static class SteamUtils { - public static string FindInstallDirectory(uint steamAppId) + public static string? FindInstallDirectory(uint steamAppId) { - string primarySteamApps = FindSteamAppsDirectory(); + string? primarySteamApps = FindSteamAppsDirectory(); + if (primarySteamApps == null) + { + return null; + } List libraryPaths = new() { primarySteamApps }; foreach (var file in Directory.EnumerateFiles(primarySteamApps)) { @@ -36,7 +43,7 @@ public static string FindInstallDirectory(uint steamAppId) private static readonly Regex SteamAppsPathsRegex = new(@"""path""\s+""(.+)"""); private static readonly Regex ManifestInstallLocationRegex = new(@"""installdir""\s+""(.+)"""); - public static string FindSteamAppsDirectory() + public static string? FindSteamAppsDirectory() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return FindSteamAppsDirectoryWin(); @@ -47,15 +54,24 @@ public static string FindSteamAppsDirectory() else throw new NotSupportedException("Unknown operating system"); } - private static string FindSteamAppsDirectoryWin() + + [SupportedOSPlatform("Windows")] + private static string? FindSteamAppsDirectoryWin() { - throw new NotImplementedException(); + return Registry.LocalMachine.OpenSubKey(@"Software\WOW6432Node\Valve\Steam", false)?.GetValue("InstallPath") as string; } - private static string FindSteamAppsDirectoryOsx() + + private static string? FindSteamAppsDirectoryOsx() { - throw new NotImplementedException(); + return Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Library", + "Application Support", + "Steam" + ); } - private static string FindSteamAppsDirectoryLinux() + + private static string? FindSteamAppsDirectoryLinux() { string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); string[] possiblePaths = { @@ -68,17 +84,19 @@ private static string FindSteamAppsDirectoryLinux() Path.Combine(homeDir, ".var", "app", "com.valvesoftware.Steam", ".steam", "root"), Path.Combine(homeDir, ".var", "app", "com.valvesoftware.Steam", ".steam") }; - string steamPath = null!; + string? steamPath = null; foreach (var path in possiblePaths) { if (Directory.Exists(path)) { steamPath = path; - goto FoundSteam; + break; } } - throw new DirectoryNotFoundException($"Could not find Steam directory, tried these paths:\n{string.Join('\n', possiblePaths)}"); -FoundSteam: + if (steamPath == null) + { + return null; + } possiblePaths = new[] { @@ -86,17 +104,15 @@ private static string FindSteamAppsDirectoryLinux() Path.Combine(steamPath, "steam", "steamapps"), // ubuntu apparently Path.Combine(steamPath, "root", "steamapps"), // no idea }; - string steamAppsPath = null!; + string? steamAppsPath = null; foreach (var path in possiblePaths) { if (Directory.Exists(path)) { steamAppsPath = path; - goto FoundSteamApps; + break; } } - throw new DirectoryNotFoundException($"Could not find steamapps directory, tried these paths:\n{string.Join('\n', possiblePaths)}"); -FoundSteamApps: return steamAppsPath; } From 75b4fa6970b50587169e1bcfa8747cda5b342a0b Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 11 Oct 2022 03:04:38 -0500 Subject: [PATCH 27/91] Add uninstall command --- ThunderstoreCLI/Commands/UninstallCommand.cs | 89 ++++++++++++++++++++ ThunderstoreCLI/Utils/Write.cs | 4 + tcli-bepinex-installer/Cargo.lock | 32 +++---- tcli-bepinex-installer/src/main.rs | 14 +-- 4 files changed, 111 insertions(+), 28 deletions(-) diff --git a/ThunderstoreCLI/Commands/UninstallCommand.cs b/ThunderstoreCLI/Commands/UninstallCommand.cs index d3d97cb..53d575e 100644 --- a/ThunderstoreCLI/Commands/UninstallCommand.cs +++ b/ThunderstoreCLI/Commands/UninstallCommand.cs @@ -1,4 +1,8 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; using ThunderstoreCLI.Configuration; +using ThunderstoreCLI.Game; +using ThunderstoreCLI.Utils; namespace ThunderstoreCLI.Commands; @@ -6,6 +10,91 @@ public static class UninstallCommand { public static int Run(Config config) { + using var defCollection = GameDefintionCollection.FromDirectory(config.GeneralConfig.TcliConfig); + GameDefinition? def = defCollection.FirstOrDefault(def => def.Identifier == config.ModManagementConfig.GameIdentifer); + if (def == null) + { + throw new CommandFatalException($"No installed mods for game ${config.ModManagementConfig.GameIdentifer}"); + } + ModProfile? profile = def.Profiles.FirstOrDefault(p => p.Name == config.ModManagementConfig.ProfileName); + if (profile == null) + { + throw new CommandFatalException($"No profile with the name {config.ModManagementConfig.ProfileName}"); + } + + HashSet modsToRemove = new() { config.ModManagementConfig.Package! }; + Queue modsToSearch = new(); + modsToSearch.Enqueue(config.ModManagementConfig.Package!); + while (modsToSearch.TryDequeue(out var search)) + { + var searchWithDash = search + '-'; + foreach (var mod in profile.InstalledModVersions.Values) + { + if (mod.Dependencies!.Any(s => s.StartsWith(searchWithDash))) + { + if (modsToRemove.Add(mod.FullName)) + { + modsToSearch.Enqueue(mod.FullName); + } + } + } + } + + foreach (var mod in modsToRemove) + { + profile.InstalledModVersions.Remove(mod); + } + + Write.Line($"The following mods will be uninstalled:\n{string.Join('\n', modsToRemove)}"); + char key; + do + { + Write.NoLine("Continue? (y/n): "); + key = Console.ReadKey().KeyChar; + Write.Empty(); + } + while (key is not 'y' and not 'n'); + + if (key == 'n') return 0; + + List failedMods = new(); + foreach (var toRemove in modsToRemove) + { + string installerName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "tcli-bepinex-installer.exe" : "tcli-bepinex-installer"; + var bepinexInstallerPath = Path.Combine(AppContext.BaseDirectory, installerName); + + ProcessStartInfo installerInfo = new(bepinexInstallerPath) + { + ArgumentList = + { + "uninstall", + def.InstallDirectory, + profile.ProfileDirectory, + toRemove + }, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + var installerProcess = Process.Start(installerInfo)!; + installerProcess.WaitForExit(); + + Write.Light(installerProcess.StandardOutput.ReadToEnd()); + string errors = installerProcess.StandardError.ReadToEnd(); + if (!string.IsNullOrWhiteSpace(errors) || installerProcess.ExitCode != 0) + { + Write.Error(errors); + failedMods.Add(toRemove); + } + } + + if (failedMods.Count != 0) + { + throw new CommandFatalException($"The following mods failed to uninstall:\n{string.Join('\n', failedMods)}"); + } + + defCollection.Validate(); + return 0; } } diff --git a/ThunderstoreCLI/Utils/Write.cs b/ThunderstoreCLI/Utils/Write.cs index a015a2a..b682c3c 100644 --- a/ThunderstoreCLI/Utils/Write.cs +++ b/ThunderstoreCLI/Utils/Write.cs @@ -9,6 +9,7 @@ public static class Write private static void _Regular(string msg) => Console.WriteLine(msg); private static void _Success(string msg) => Console.WriteLine(Green(msg)); private static void _Warn(string msg) => Console.WriteLine(Yellow(msg)); + private static void _NoLine(string msg) => Console.Write(msg); private static void _WriteMultiline(Action write, string msg, string[] submsgs) { @@ -49,6 +50,9 @@ public static void Light(string message, params string[] submessages) /// Write regular line to stdout public static void Line(string message) => _Regular(message); + /// Write a string to stdout with no newline + public static void NoLine(string message) => _NoLine(message); + /// Write message with highlight color to stdout public static void Note(string message, params string[] submessages) { diff --git a/tcli-bepinex-installer/Cargo.lock b/tcli-bepinex-installer/Cargo.lock index fbe02c6..a0bd4d6 100644 --- a/tcli-bepinex-installer/Cargo.lock +++ b/tcli-bepinex-installer/Cargo.lock @@ -111,9 +111,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.9" +version = "4.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30607dd93c420c6f1f80b544be522a0238a7db35e6a12968d28910983fee0df0" +checksum = "385007cbbed899260395a4107435fead4cad80684461b3cc78238bdcb0bad58f" dependencies = [ "atty", "bitflags", @@ -126,9 +126,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.0.9" +version = "4.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a307492e1a34939f79d3b6b9650bd2b971513cd775436bf2b78defeb5af00b" +checksum = "db342ce9fda24fb191e2ed4e102055a4d381c1086a06630174cd8da8d5d917ce" dependencies = [ "heck", "proc-macro-error", @@ -246,9 +246,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" [[package]] name = "jobserver" @@ -261,9 +261,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.134" +version = "0.2.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" +checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" [[package]] name = "miniz_oxide" @@ -406,9 +406,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" dependencies = [ "itoa", "ryu", @@ -451,9 +451,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" +checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" dependencies = [ "proc-macro2", "quote", @@ -503,9 +503,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" dependencies = [ "itoa", "libc", @@ -527,9 +527,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "unicode-ident" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" [[package]] name = "version_check" diff --git a/tcli-bepinex-installer/src/main.rs b/tcli-bepinex-installer/src/main.rs index 5675580..2063f87 100644 --- a/tcli-bepinex-installer/src/main.rs +++ b/tcli-bepinex-installer/src/main.rs @@ -48,8 +48,6 @@ pub enum Error { MissingNamespace, #[error("Mod name is invalid (eg doesn't use a - between namespace and name)")] InvalidModName, - #[error("Mod either is not installed or is not accessable to the uninstaller. Tried directory: {0}")] - ModNotInstalled(PathBuf), } #[derive(Deserialize)] @@ -245,16 +243,8 @@ fn uninstall_bepinex(game_dir: PathBuf, bep_dir: PathBuf) -> Result<()> { fn uninstall_mod(bep_dir: PathBuf, name: String) -> Result<()> { let actual_bep = bep_dir.join("BepInEx"); - - let main_dir = actual_bep.join("plugins").join(&name); - - if !main_dir.exists() { - bail!(Error::ModNotInstalled(main_dir)); - } - - fs::remove_dir_all(main_dir)?; - - delete_dir_if_not_deleted(actual_bep.join("patchers"))?; + delete_dir_if_not_deleted(actual_bep.join("plugins").join(&name))?; + delete_dir_if_not_deleted(actual_bep.join("patchers").join(&name))?; delete_dir_if_not_deleted(actual_bep.join("monomod").join(&name))?; Ok(()) From 4cede38135171d0841321b8f324cb4adb7e7411e Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 11 Oct 2022 16:08:06 -0500 Subject: [PATCH 28/91] Add todos and logging to install/uninstall --- ThunderstoreCLI/Commands/InstallCommand.cs | 23 +++++++++++++++++--- ThunderstoreCLI/Commands/UninstallCommand.cs | 8 ++++++- ThunderstoreCLI/Game/GameDefinition.cs | 10 ++------- ThunderstoreCLI/Program.cs | 1 + 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index 60edb09..eb173c8 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -14,6 +14,7 @@ namespace ThunderstoreCLI.Commands; public static class InstallCommand { + // TODO: stop hardcoding this, ecosystem-schema (also applies to logic in GameDefintion) internal static readonly Dictionary IDToHardcoded = new() { { "ror2", HardcodedGame.ROR2 }, @@ -61,7 +62,8 @@ public static async Task Run(Config config) throw new CommandFatalException($"Package given does not exist as a zip and is not a valid package identifier (namespace-name): {package}"); } - if (returnCode == 0) defCollection.Validate(); + if (returnCode == 0) + defCollection.Validate(); return returnCode; } @@ -115,8 +117,15 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe { int returnCode = RunInstaller(game, profile, tempZipPath, package.Namespace); File.Delete(tempZipPath); - if (returnCode != 0) + if (returnCode == 0) + { + Write.Success($"Installed mod: {package.Fullname}-{package.LatestVersion!.VersionNumber}"); + } + else + { + Write.Error($"Failed to install mod: {package.Fullname}-{package.LatestVersion!.VersionNumber}"); return returnCode; + } profile.InstalledModVersions[package.Fullname!] = new PackageManifestV1(package.LatestVersion!); } } @@ -124,11 +133,17 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe var exitCode = RunInstaller(game, profile, zipPath, backupNamespace); if (exitCode == 0) { - profile.InstalledModVersions[$"{manifest.Namespace ?? backupNamespace}-{manifest.Name}"] = manifest; + profile.InstalledModVersions[manifest.FullName] = manifest; + Write.Success($"Installed mod: {manifest.FullName}-{manifest.VersionNumber}"); + } + else + { + Write.Error($"Failed to install mod: {manifest.FullName}-{manifest.VersionNumber}"); } return exitCode; } + // TODO: replace with a mod cache private static async Task DownloadTemp(HttpClient http, PackageVersionData version) { string path = Path.GetTempFileName(); @@ -140,8 +155,10 @@ private static async Task DownloadTemp(HttpClient http, PackageVersionDa return path; } + // TODO: conflict handling private static int RunInstaller(GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace) { + // TODO: how to decide which installer to run? string installerName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "tcli-bepinex-installer.exe" : "tcli-bepinex-installer"; var bepinexInstallerPath = Path.Combine(AppContext.BaseDirectory, installerName); diff --git a/ThunderstoreCLI/Commands/UninstallCommand.cs b/ThunderstoreCLI/Commands/UninstallCommand.cs index 53d575e..00a069f 100644 --- a/ThunderstoreCLI/Commands/UninstallCommand.cs +++ b/ThunderstoreCLI/Commands/UninstallCommand.cs @@ -22,6 +22,11 @@ public static int Run(Config config) throw new CommandFatalException($"No profile with the name {config.ModManagementConfig.ProfileName}"); } + if (!profile.InstalledModVersions.ContainsKey(config.ModManagementConfig.Package!)) + { + throw new CommandFatalException($"The package {config.ModManagementConfig.Package} is not installed in the profile {profile.Name}"); + } + HashSet modsToRemove = new() { config.ModManagementConfig.Package! }; Queue modsToSearch = new(); modsToSearch.Enqueue(config.ModManagementConfig.Package!); @@ -55,7 +60,8 @@ public static int Run(Config config) } while (key is not 'y' and not 'n'); - if (key == 'n') return 0; + if (key == 'n') + return 0; List failedMods = new(); foreach (var toRemove in modsToRemove) diff --git a/ThunderstoreCLI/Game/GameDefinition.cs b/ThunderstoreCLI/Game/GameDefinition.cs index 1d42e83..5650766 100644 --- a/ThunderstoreCLI/Game/GameDefinition.cs +++ b/ThunderstoreCLI/Game/GameDefinition.cs @@ -37,13 +37,6 @@ internal static GameDefinition FromSteamId(string tcliDir, uint steamId, string { return new GameDefinition(id, name, SteamUtils.FindInstallDirectory(steamId)!, tcliDir); } - - internal static GameDefinition FromSteamId(string tcliDir, uint steamId, string subdirectory, string id, string name) - { - var gameDef = FromSteamId(tcliDir, steamId, id, name); - gameDef.InstallDirectory = Path.Combine(gameDef.InstallDirectory, subdirectory); - return gameDef; - } } public sealed class GameDefintionCollection : IEnumerable, IDisposable @@ -73,7 +66,8 @@ private GameDefintionCollection(string tcliDir) public void Dispose() { - if (!shouldWrite) return; + if (!shouldWrite) + return; File.WriteAllText(Path.Combine(tcliDirectory, FILE_NAME), List.SerializeList(BaseJson.IndentedSettings)); shouldWrite = false; } diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index c0ca4e6..67d2ba2 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -29,6 +29,7 @@ private static int Main(string[] args) return exitCode; } + // TODO: replace return codes with exceptions completely private static int HandleParsed(BaseOptions parsed) { parsed.Init(); From 22668b302b8fcc88d9dab58e60587456f17752d9 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 11 Oct 2022 23:12:58 -0500 Subject: [PATCH 29/91] Add back things lost in rebase, run dotnet format --- ThunderstoreCLI/Commands/InstallCommand.cs | 3 --- ThunderstoreCLI/Commands/PublishCommand.cs | 2 -- ThunderstoreCLI/Configuration/CLIParameterConfig.cs | 2 +- ThunderstoreCLI/Configuration/Config.cs | 5 +++-- ThunderstoreCLI/Configuration/EmptyConfig.cs | 2 +- ThunderstoreCLI/Configuration/IConfigProvider.cs | 2 +- ThunderstoreCLI/Game/ModProfile.cs | 1 - 7 files changed, 6 insertions(+), 11 deletions(-) diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index eb173c8..a9b3c6b 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -1,6 +1,3 @@ -using ThunderstoreCLI.Configuration; -using ThunderstoreCLI.Game; -using System.Collections.ObjectModel; using System.Diagnostics; using System.IO.Compression; using System.Runtime.InteropServices; diff --git a/ThunderstoreCLI/Commands/PublishCommand.cs b/ThunderstoreCLI/Commands/PublishCommand.cs index 559e530..5d5bed5 100644 --- a/ThunderstoreCLI/Commands/PublishCommand.cs +++ b/ThunderstoreCLI/Commands/PublishCommand.cs @@ -1,10 +1,8 @@ using System.Net; using System.Security.Cryptography; -using System.Text; using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Models; using ThunderstoreCLI.Utils; -using ThunderstoreCLI.Models; using static Crayon.Output; namespace ThunderstoreCLI.Commands; diff --git a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs index 6a29d97..f1d09c4 100644 --- a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs +++ b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs @@ -82,7 +82,7 @@ public class ModManagementCommandConfig : BaseConfig { public ModManagementCommandConfig(ModManagementOptions options) : base(options) { } - public override ModManagementConfig? GetInstallConfig() + public override ModManagementConfig? GetModManagementConfig() { return new ModManagementConfig() { diff --git a/ThunderstoreCLI/Configuration/Config.cs b/ThunderstoreCLI/Configuration/Config.cs index 577ae34..aad72f3 100644 --- a/ThunderstoreCLI/Configuration/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -117,8 +117,8 @@ public static Config Parse(IConfigProvider[] configProviders) var buildConfig = new BuildConfig(); var publishConfig = new PublishConfig(); var authConfig = new AuthConfig(); - var installConfig = new ModManagementConfig(); - var result = new Config(generalConfig, packageMeta, initConfig, buildConfig, publishConfig, authConfig, installConfig); + var modManagementConfig = new ModManagementConfig(); + var result = new Config(generalConfig, packageMeta, initConfig, buildConfig, publishConfig, authConfig, modManagementConfig); foreach (var provider in configProviders) { provider.Parse(result); @@ -128,6 +128,7 @@ public static Config Parse(IConfigProvider[] configProviders) Merge(buildConfig, provider.GetBuildConfig(), false); Merge(publishConfig, provider.GetPublishConfig(), false); Merge(authConfig, provider.GetAuthConfig(), false); + Merge(modManagementConfig, provider.GetModManagementConfig(), false); } return result; } diff --git a/ThunderstoreCLI/Configuration/EmptyConfig.cs b/ThunderstoreCLI/Configuration/EmptyConfig.cs index e348f6c..77a2d6f 100644 --- a/ThunderstoreCLI/Configuration/EmptyConfig.cs +++ b/ThunderstoreCLI/Configuration/EmptyConfig.cs @@ -34,7 +34,7 @@ public virtual void Parse(Config currentConfig) { } return null; } - public virtual ModManagementConfig? GetInstallConfig() + public virtual ModManagementConfig? GetModManagementConfig() { return null; } diff --git a/ThunderstoreCLI/Configuration/IConfigProvider.cs b/ThunderstoreCLI/Configuration/IConfigProvider.cs index 71ab859..9d51556 100644 --- a/ThunderstoreCLI/Configuration/IConfigProvider.cs +++ b/ThunderstoreCLI/Configuration/IConfigProvider.cs @@ -10,5 +10,5 @@ public interface IConfigProvider BuildConfig? GetBuildConfig(); PublishConfig? GetPublishConfig(); AuthConfig? GetAuthConfig(); - ModManagementConfig? GetInstallConfig(); + ModManagementConfig? GetModManagementConfig(); } diff --git a/ThunderstoreCLI/Game/ModProfile.cs b/ThunderstoreCLI/Game/ModProfile.cs index 5dc1d70..89f5869 100644 --- a/ThunderstoreCLI/Game/ModProfile.cs +++ b/ThunderstoreCLI/Game/ModProfile.cs @@ -17,7 +17,6 @@ private ModProfile() { } internal ModProfile(GameDefinition gameDef, string name, string tcliDirectory) { Name = name; - InstalledModVersions = new(); var directory = Path.Combine(tcliDirectory, "Profiles", gameDef.Identifier, name); if (!Directory.Exists(directory)) From 769cefdf2d72c9f4cb28ca467dec97dccc8a9346 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 24 Oct 2022 23:43:18 -0500 Subject: [PATCH 30/91] Implement run command for Windows and some proton --- .../ThunderstoreCLI.Tests.csproj | 5 + ThunderstoreCLI/Commands/ImportGameCommand.cs | 40 ++++++ ThunderstoreCLI/Commands/InstallCommand.cs | 22 +-- ThunderstoreCLI/Commands/RunCommand.cs | 128 ++++++++++++++++++ .../Configuration/CLIParameterConfig.cs | 27 ++++ ThunderstoreCLI/Configuration/Config.cs | 23 +++- ThunderstoreCLI/Configuration/EmptyConfig.cs | 10 ++ .../Configuration/IConfigProvider.cs | 2 + ThunderstoreCLI/Game/GameDefinition.cs | 34 ++--- ThunderstoreCLI/Models/BaseToml.cs | 7 - ThunderstoreCLI/Models/BaseYaml.cs | 22 +++ ThunderstoreCLI/Models/ISerialize.cs | 11 +- ThunderstoreCLI/Models/R2mmGameDescription.cs | 31 +++++ ThunderstoreCLI/Options.cs | 51 ++++++- ThunderstoreCLI/Program.cs | 4 +- ThunderstoreCLI/ThunderstoreCLI.csproj | 6 + ThunderstoreCLI/Utils/SteamUtils.cs | 91 +++++++++++-- ThunderstoreCLI/dsp.yml | 19 +++ tcli-bepinex-installer/src/main.rs | 40 ++++-- 19 files changed, 505 insertions(+), 68 deletions(-) create mode 100644 ThunderstoreCLI/Commands/ImportGameCommand.cs create mode 100644 ThunderstoreCLI/Commands/RunCommand.cs create mode 100644 ThunderstoreCLI/Models/BaseYaml.cs create mode 100644 ThunderstoreCLI/Models/R2mmGameDescription.cs create mode 100644 ThunderstoreCLI/dsp.yml diff --git a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj index 4994f5b..13b2e78 100644 --- a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj +++ b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj @@ -10,6 +10,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -20,6 +24,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/ThunderstoreCLI/Commands/ImportGameCommand.cs b/ThunderstoreCLI/Commands/ImportGameCommand.cs new file mode 100644 index 0000000..e8560eb --- /dev/null +++ b/ThunderstoreCLI/Commands/ImportGameCommand.cs @@ -0,0 +1,40 @@ +using ThunderstoreCLI.Configuration; +using ThunderstoreCLI.Game; +using ThunderstoreCLI.Models; +using ThunderstoreCLI.Utils; + +namespace ThunderstoreCLI.Commands; + +public static class ImportGameCommand +{ + public static int Run(Config config) + { + R2mmGameDescription? desc; + try + { + desc = R2mmGameDescription.Deserialize(File.ReadAllText(config.GameImportConfig.FilePath!)); + } + catch (Exception e) + { + throw new CommandFatalException($"Failed to read game description file: {e}"); + } + if (desc is null) + { + throw new CommandFatalException("Game description file was empty"); + } + + var def = desc.ToGameDefintion(config); + if (def == null) + { + throw new CommandFatalException("Game not installed"); + } + + using GameDefintionCollection collection = GameDefintionCollection.FromDirectory(config.GeneralConfig.TcliConfig); + collection.List.Add(def); + collection.Validate(); + + Write.Success($"Successfully imported {def.Name} ({def.Identifier}) with install folder \"{def.InstallDirectory}\""); + + return 0; + } +} diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index a9b3c6b..ad023d5 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -11,14 +11,6 @@ namespace ThunderstoreCLI.Commands; public static class InstallCommand { - // TODO: stop hardcoding this, ecosystem-schema (also applies to logic in GameDefintion) - internal static readonly Dictionary IDToHardcoded = new() - { - { "ror2", HardcodedGame.ROR2 }, - { "vrising", HardcodedGame.VRISING }, - { "vrising_dedicated", HardcodedGame.VRISING_SERVER } - }; - // will match either ab-cd or ab-cd-123.456.7890 internal static readonly Regex FullPackageNameRegex = new(@"^(\w+)-(\w+)(?:|-(\d+\.\d+\.\d+))$"); @@ -27,12 +19,7 @@ public static async Task Run(Config config) using var defCollection = GameDefintionCollection.FromDirectory(config.GeneralConfig.TcliConfig); var defs = defCollection.List; GameDefinition? def = defs.FirstOrDefault(x => x.Identifier == config.ModManagementConfig.GameIdentifer); - if (def == null && IDToHardcoded.TryGetValue(config.ModManagementConfig.GameIdentifer!, out var hardcoded)) - { - def = GameDefinition.FromHardcodedIdentifier(config.GeneralConfig.TcliConfig, hardcoded); - defs.Add(def); - } - else if (def == null) + if (def == null) { Write.ErrorExit($"Not configured for the game: {config.ModManagementConfig.GameIdentifer}"); return 1; @@ -83,14 +70,13 @@ private static async Task InstallFromRepository(Config config, HttpClient h version = (await PackageData.DeserializeAsync(await packageResponse.Content.ReadAsStreamAsync()))!.LatestVersion!; } - var tempZipPath = await DownloadTemp(http, version); - var returnCode = await InstallZip(config, http, game, profile, tempZipPath, version.Namespace!); + var returnCode = await InstallZip(config, http, game, profile, tempZipPath, version.Namespace!, true); File.Delete(tempZipPath); return returnCode; } - private static async Task InstallZip(Config config, HttpClient http, GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace) + private static async Task InstallZip(Config config, HttpClient http, GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace, bool requiredDownload = false) { using var zip = ZipFile.OpenRead(zipPath); var manifestFile = zip.GetEntry("manifest.json") ?? throw new CommandFatalException("Package zip needs a manifest.json!"); @@ -107,7 +93,7 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe { var downloadTasks = dependenciesToInstall.Select(mod => DownloadTemp(http, mod.LatestVersion!)).ToArray(); - var spinner = new ProgressSpinner("mods downloaded", downloadTasks); + var spinner = new ProgressSpinner("mods downloaded", downloadTasks, 1); await spinner.Spin(); foreach (var (tempZipPath, package) in downloadTasks.Select(x => x.Result).Zip(dependenciesToInstall)) diff --git a/ThunderstoreCLI/Commands/RunCommand.cs b/ThunderstoreCLI/Commands/RunCommand.cs new file mode 100644 index 0000000..3033d97 --- /dev/null +++ b/ThunderstoreCLI/Commands/RunCommand.cs @@ -0,0 +1,128 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using ThunderstoreCLI.Configuration; +using ThunderstoreCLI.Game; +using ThunderstoreCLI.Utils; +using YamlDotNet.Core.Tokens; + +namespace ThunderstoreCLI.Commands; + +public static class RunCommand +{ + public static int Run(Config config) + { + GameDefintionCollection collection = GameDefintionCollection.FromDirectory(config.GeneralConfig.TcliConfig); + var def = collection.FirstOrDefault(g => g.Identifier == config.RunGameConfig.GameName); + + if (def == null) + { + throw new CommandFatalException($"No mods installed for game {config.RunGameConfig.GameName}"); + } + + var profile = def.Profiles.FirstOrDefault(p => p.Name == config.RunGameConfig.ProfileName); + + if (profile == null) + { + throw new CommandFatalException($"No profile found with the name {config.RunGameConfig.ProfileName}"); + } + + ProcessStartInfo startInfo = new(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "tcli-bepinex-installer.exe" : "tcli-bepinex-installer") + { + ArgumentList = + { + "start-instructions", + def.InstallDirectory, + profile.ProfileDirectory + }, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + var gameIsProton = SteamUtils.IsProtonGame(def.PlatformId); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + startInfo.ArgumentList.Add("--game-platform"); + startInfo.ArgumentList.Add(gameIsProton switch + { + true => "windows", + false => "linux" + }); + } + + var installerProcess = Process.Start(startInfo)!; + installerProcess.WaitForExit(); + + string errors = installerProcess.StandardError.ReadToEnd(); + if (!string.IsNullOrWhiteSpace(errors) || installerProcess.ExitCode != 0) + { + throw new CommandFatalException($"Installer failed with errors:\n{errors}"); + } + + string runArguments = ""; + List<(string key, string value)> runEnvironment = new(); + string[] wineDlls = Array.Empty(); + + string[] outputLines = installerProcess.StandardOutput.ReadToEnd().Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in outputLines) + { + var firstColon = line.IndexOf(':'); + if (firstColon == -1) + { + continue; + } + var command = line[..firstColon]; + var args = line[(firstColon + 1)..]; + switch (command) + { + case "ARGUMENTS": + runArguments = args; + break; + case "WINEDLLOVERRIDE": + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + throw new NotSupportedException(); + } + wineDlls = args.Split(':'); + break; + } + } + + var steamDir = SteamUtils.FindSteamDirectory(); + if (steamDir == null) + { + throw new CommandFatalException("Couldn't find steam install directory!"); + } + string steamExeName; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + steamExeName = "steam.sh"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + steamExeName = "steam.exe"; + } + else + { + throw new NotImplementedException(); + } + + if (gameIsProton) + { + // TODO: force wine DLL overrides with registry + } + + ProcessStartInfo runSteamInfo = new(Path.Combine(steamDir, steamExeName)) + { + Arguments = $"-applaunch {def.PlatformId} {runArguments}" + }; + + Write.Note($"Starting appid {def.PlatformId} with arguments: {runArguments}"); + var steamProcess = Process.Start(runSteamInfo)!; + steamProcess.WaitForExit(); + Write.Success($"Started game with appid {def.PlatformId}"); + + return 0; + } +} diff --git a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs index f1d09c4..851e760 100644 --- a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs +++ b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs @@ -92,3 +92,30 @@ public ModManagementCommandConfig(ModManagementOptions options) : base(options) }; } } + +public class GameImportCommandConfig : BaseConfig +{ + public GameImportCommandConfig(GameImportOptions options) : base(options) { } + + public override GameImportConfig? GetGameImportConfig() + { + return new GameImportConfig() + { + FilePath = options.FilePath + }; + } +} + +public class RunGameCommandConfig : BaseConfig +{ + public RunGameCommandConfig(RunGameOptions options) : base(options) { } + + public override RunGameConfig? GetRunGameConfig() + { + return new RunGameConfig() + { + GameName = options.GameName, + ProfileName = options.Profile + }; + } +} diff --git a/ThunderstoreCLI/Configuration/Config.cs b/ThunderstoreCLI/Configuration/Config.cs index aad72f3..27a992b 100644 --- a/ThunderstoreCLI/Configuration/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -14,12 +14,14 @@ public class Config public PublishConfig PublishConfig { get; private set; } public AuthConfig AuthConfig { get; private set; } public ModManagementConfig ModManagementConfig { get; private set; } + public GameImportConfig GameImportConfig { get; private set; } + public RunGameConfig RunGameConfig { get; private set; } // ReSharper restore AutoPropertyCanBeMadeGetOnly.Local private readonly Lazy api; public ApiHelper Api => api.Value; - private Config(GeneralConfig generalConfig, PackageConfig packageConfig, InitConfig initConfig, BuildConfig buildConfig, PublishConfig publishConfig, AuthConfig authConfig, ModManagementConfig modManagementConfig) + private Config(GeneralConfig generalConfig, PackageConfig packageConfig, InitConfig initConfig, BuildConfig buildConfig, PublishConfig publishConfig, AuthConfig authConfig, ModManagementConfig modManagementConfig, GameImportConfig gameImportConfig, RunGameConfig runGameConfig) { api = new Lazy(() => new ApiHelper(this)); GeneralConfig = generalConfig; @@ -29,6 +31,8 @@ private Config(GeneralConfig generalConfig, PackageConfig packageConfig, InitCon PublishConfig = publishConfig; AuthConfig = authConfig; ModManagementConfig = modManagementConfig; + GameImportConfig = gameImportConfig; + RunGameConfig = runGameConfig; } public static Config FromCLI(IConfigProvider cliConfig) { @@ -118,7 +122,9 @@ public static Config Parse(IConfigProvider[] configProviders) var publishConfig = new PublishConfig(); var authConfig = new AuthConfig(); var modManagementConfig = new ModManagementConfig(); - var result = new Config(generalConfig, packageMeta, initConfig, buildConfig, publishConfig, authConfig, modManagementConfig); + var gameImportConfig = new GameImportConfig(); + var runGameConfig = new RunGameConfig(); + var result = new Config(generalConfig, packageMeta, initConfig, buildConfig, publishConfig, authConfig, modManagementConfig, gameImportConfig, runGameConfig); foreach (var provider in configProviders) { provider.Parse(result); @@ -129,6 +135,8 @@ public static Config Parse(IConfigProvider[] configProviders) Merge(publishConfig, provider.GetPublishConfig(), false); Merge(authConfig, provider.GetAuthConfig(), false); Merge(modManagementConfig, provider.GetModManagementConfig(), false); + Merge(gameImportConfig, provider.GetGameImportConfig(), false); + Merge(runGameConfig, provider.GetRunGameConfig(), false); } return result; } @@ -220,3 +228,14 @@ public class ModManagementConfig public string? Package { get; set; } public string? ProfileName { get; set; } } + +public class GameImportConfig +{ + public string? FilePath { get; set; } +} + +public class RunGameConfig +{ + public string? GameName { get; set; } + public string? ProfileName { get; set; } +} diff --git a/ThunderstoreCLI/Configuration/EmptyConfig.cs b/ThunderstoreCLI/Configuration/EmptyConfig.cs index 77a2d6f..c3074b5 100644 --- a/ThunderstoreCLI/Configuration/EmptyConfig.cs +++ b/ThunderstoreCLI/Configuration/EmptyConfig.cs @@ -38,4 +38,14 @@ public virtual void Parse(Config currentConfig) { } { return null; } + + public virtual GameImportConfig? GetGameImportConfig() + { + return null; + } + + public virtual RunGameConfig? GetRunGameConfig() + { + return null; + } } diff --git a/ThunderstoreCLI/Configuration/IConfigProvider.cs b/ThunderstoreCLI/Configuration/IConfigProvider.cs index 9d51556..89b2e70 100644 --- a/ThunderstoreCLI/Configuration/IConfigProvider.cs +++ b/ThunderstoreCLI/Configuration/IConfigProvider.cs @@ -11,4 +11,6 @@ public interface IConfigProvider PublishConfig? GetPublishConfig(); AuthConfig? GetAuthConfig(); ModManagementConfig? GetModManagementConfig(); + GameImportConfig? GetGameImportConfig(); + RunGameConfig? GetRunGameConfig(); } diff --git a/ThunderstoreCLI/Game/GameDefinition.cs b/ThunderstoreCLI/Game/GameDefinition.cs index 5650766..9c88b11 100644 --- a/ThunderstoreCLI/Game/GameDefinition.cs +++ b/ThunderstoreCLI/Game/GameDefinition.cs @@ -9,33 +9,35 @@ public class GameDefinition : BaseJson public string Identifier { get; set; } public string Name { get; set; } public string InstallDirectory { get; set; } + public GamePlatform Platform { get; set; } + public string PlatformId { get; set; } public List Profiles { get; private set; } = new(); #pragma warning disable CS8618 private GameDefinition() { } #pragma warning restore CS8618 - internal GameDefinition(string id, string name, string installDirectory, string tcliDirectory) + internal GameDefinition(string id, string name, string installDirectory, GamePlatform platform, string platformId, string tcliDirectory) { Identifier = id; Name = name; InstallDirectory = installDirectory; + Platform = platform; + PlatformId = platformId; } - internal static GameDefinition FromHardcodedIdentifier(string tcliDir, HardcodedGame game) + internal static GameDefinition? FromPlatformInstall(string tcliDir, GamePlatform platform, string platformId, string id, string name) { - return game switch + var gameDir = platform switch { - HardcodedGame.ROR2 => FromSteamId(tcliDir, 632360, "ror2", "Risk of Rain 2"), - HardcodedGame.VRISING => FromSteamId(tcliDir, 1604030, "vrising", "V Rising"), - HardcodedGame.VRISING_SERVER => FromSteamId(tcliDir, 1829350, "vrising_server", "V Rising Dedicated Server"), - _ => throw new ArgumentException("Invalid enum value", nameof(game)) + GamePlatform.steam => SteamUtils.FindInstallDirectory(platformId), + _ => null }; - } - - internal static GameDefinition FromSteamId(string tcliDir, uint steamId, string id, string name) - { - return new GameDefinition(id, name, SteamUtils.FindInstallDirectory(steamId)!, tcliDir); + if (gameDir == null) + { + return null; + } + return new GameDefinition(id, name, gameDir, platform, platformId, tcliDir); } } @@ -73,9 +75,9 @@ public void Dispose() } } -internal enum HardcodedGame +public enum GamePlatform { - ROR2, - VRISING, - VRISING_SERVER + steam, + egs, + other } diff --git a/ThunderstoreCLI/Models/BaseToml.cs b/ThunderstoreCLI/Models/BaseToml.cs index daf825f..b69d16c 100644 --- a/ThunderstoreCLI/Models/BaseToml.cs +++ b/ThunderstoreCLI/Models/BaseToml.cs @@ -9,11 +9,4 @@ public abstract class BaseToml<[DynamicallyAccessedMembers(DynamicallyAccessedMe public string Serialize() => TomletMain.TomlStringFrom(this); public static T? Deserialize(string toml) => TomletMain.To(toml); - - public static ValueTask DeserializeAsync(string toml) => new(Deserialize(toml)); - public static async ValueTask DeserializeAsync(Stream toml) - { - using StreamReader reader = new(toml); - return Deserialize(await reader.ReadToEndAsync()); - } } diff --git a/ThunderstoreCLI/Models/BaseYaml.cs b/ThunderstoreCLI/Models/BaseYaml.cs new file mode 100644 index 0000000..dc9e474 --- /dev/null +++ b/ThunderstoreCLI/Models/BaseYaml.cs @@ -0,0 +1,22 @@ +using System.Diagnostics.CodeAnalysis; +using YamlDotNet.Serialization; + +namespace ThunderstoreCLI.Models; + +public abstract class BaseYaml<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : ISerialize where T : BaseYaml +{ + public string Serialize() + { + return BaseYamlHelper.Serializer.Serialize(this); + } + public static T? Deserialize(string input) + { + return BaseYamlHelper.Deserializer.Deserialize(input); + } +} + +file static class BaseYamlHelper +{ + public static readonly Serializer Serializer = new(); + public static readonly Deserializer Deserializer = new(); +} diff --git a/ThunderstoreCLI/Models/ISerialize.cs b/ThunderstoreCLI/Models/ISerialize.cs index 8cdd635..43fe459 100644 --- a/ThunderstoreCLI/Models/ISerialize.cs +++ b/ThunderstoreCLI/Models/ISerialize.cs @@ -8,7 +8,14 @@ public interface ISerialize<[DynamicallyAccessedMembers(DynamicallyAccessedMembe public string Serialize(); #if NET7_0 public static abstract T? Deserialize(string input); - public static abstract ValueTask DeserializeAsync(string input); - public static abstract ValueTask DeserializeAsync(Stream input); + public static virtual ValueTask DeserializeAsync(string input) + { + return new(T.Deserialize(input)); + } + public static virtual async ValueTask DeserializeAsync(Stream input) + { + using StreamReader reader = new(input); + return T.Deserialize(await reader.ReadToEndAsync()); + } #endif } diff --git a/ThunderstoreCLI/Models/R2mmGameDescription.cs b/ThunderstoreCLI/Models/R2mmGameDescription.cs new file mode 100644 index 0000000..9f6996c --- /dev/null +++ b/ThunderstoreCLI/Models/R2mmGameDescription.cs @@ -0,0 +1,31 @@ +using ThunderstoreCLI.Configuration; +using ThunderstoreCLI.Game; + +namespace ThunderstoreCLI.Models; + +public class R2mmGameDescription : BaseYaml +{ + public required string uuid { get; set; } + public required string label { get; set; } + public required DescriptionMetadata meta { get; set; } + public required PlatformDistribution[] distributions { get; set; } + public required object? legacy { get; set; } + + public GameDefinition? ToGameDefintion(Config config) + { + var platform = distributions.First(p => p.platform == GamePlatform.steam); + return GameDefinition.FromPlatformInstall(config.GeneralConfig.TcliConfig, platform.platform, platform.identifier, label, meta.displayName); + } +} + +public class PlatformDistribution +{ + public required GamePlatform platform { get; set; } + public required string identifier { get; set; } +} + +public class DescriptionMetadata +{ + public required string displayName { get; set; } + public required string iconUrl { get; set; } +} diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index d966c92..bbcc532 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -147,8 +147,6 @@ public override int Execute() public abstract class ModManagementOptions : BaseOptions { - //public string? ManagerId { get; set; } - [Value(0, MetaName = "Game Name", Required = true, HelpText = "Can be any of: ror2, vrising, vrising_dedicated, vrising_builtin")] public string GameName { get; set; } = null!; @@ -199,3 +197,52 @@ public class UninstallOptions : ModManagementOptions { protected override CommandInner CommandType => CommandInner.Uninstall; } + +[Verb("import-game")] +public class GameImportOptions : BaseOptions +{ + [Value(0, MetaName = "File Path", Required = true, HelpText = "Path to game description file to import")] + public required string FilePath { get; set; } + + public override bool Validate() + { + if (!File.Exists(FilePath)) + { + Write.ErrorExit($"Could not locate game description file at {FilePath}"); + } + + return base.Validate(); + } + + public override int Execute() + { + var config = Config.FromCLI(new GameImportCommandConfig(this)); + return ImportGameCommand.Run(config); + } +} + +[Verb("run")] +public class RunGameOptions : BaseOptions +{ + [Value(0, MetaName = "Game Name", Required = true, HelpText = "Can be any of: ror2, vrising, vrising_dedicated, vrising_builtin")] + public required string GameName { get; set; } = null!; + + [Option(HelpText = "Profile to install to", Default = "Default")] + public required string Profile { get; set; } + + public override bool Validate() + { +#if NOINSTALLERS + Write.ErrorExit("Installers are not supported when installed through dotnet tool (yet) (i hate nuget)"); + return false; +#endif + + return base.Validate(); + } + + public override int Execute() + { + var config = Config.FromCLI(new RunGameCommandConfig(this)); + return RunCommand.Run(config); + } +} diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index 67d2ba2..17f0028 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -16,13 +16,15 @@ private static int Main(string[] args) #endif var updateChecker = UpdateChecker.CheckForUpdates(); - var exitCode = Parser.Default.ParseArguments(args) + var exitCode = Parser.Default.ParseArguments(args) .MapResult( (InitOptions o) => HandleParsed(o), (BuildOptions o) => HandleParsed(o), (PublishOptions o) => HandleParsed(o), (InstallOptions o) => HandleParsed(o), (UninstallOptions o) => HandleParsed(o), + (GameImportOptions o) => HandleParsed(o), + (RunGameOptions o) => HandleParsed(o), _ => 1 // failure to parse ); UpdateChecker.WriteUpdateNotification(updateChecker); diff --git a/ThunderstoreCLI/ThunderstoreCLI.csproj b/ThunderstoreCLI/ThunderstoreCLI.csproj index e22810c..ea0e9ea 100644 --- a/ThunderstoreCLI/ThunderstoreCLI.csproj +++ b/ThunderstoreCLI/ThunderstoreCLI.csproj @@ -34,7 +34,13 @@ all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/ThunderstoreCLI/Utils/SteamUtils.cs b/ThunderstoreCLI/Utils/SteamUtils.cs index f4386dc..285de79 100644 --- a/ThunderstoreCLI/Utils/SteamUtils.cs +++ b/ThunderstoreCLI/Utils/SteamUtils.cs @@ -8,7 +8,37 @@ namespace ThunderstoreCLI.Utils; public static class SteamUtils { - public static string? FindInstallDirectory(uint steamAppId) + public static string? FindInstallDirectory(string steamAppId) + { + var path = GetAcfPath(steamAppId); + if (path == null) + { + return null; + } + + var folderName = ManifestInstallLocationRegex.Match(File.ReadAllText(path)).Groups[1].Value; + + return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path)!, "common", folderName)); + } + + public static bool IsProtonGame(string steamAppId) + { + var path = GetAcfPath(steamAppId); + if (path == null) + { + throw new ArgumentException($"{steamAppId} is not installed!"); + } + + var source = PlatformOverrideSourceRegex.Match(File.ReadAllText(path)).Groups[1].Value; + return source switch + { + "" => false, + "linux" => false, + _ => true + }; + } + + private static string? GetAcfPath(string steamAppId) { string? primarySteamApps = FindSteamAppsDirectory(); if (primarySteamApps == null) @@ -29,19 +59,30 @@ public static class SteamUtils { foreach (var file in Directory.EnumerateFiles(library)) { - if (!Path.GetFileName(file).Equals(acfName, StringComparison.OrdinalIgnoreCase)) - continue; - - var folderName = ManifestInstallLocationRegex.Match(File.ReadAllText(file)).Groups[1].Value; - - return Path.GetFullPath(Path.Combine(library, "common", folderName)); + if (Path.GetFileName(file).Equals(acfName, StringComparison.OrdinalIgnoreCase)) + { + return file; + } } } - throw new FileNotFoundException($"Could not find {acfName}, tried the following paths:\n{string.Join('\n', libraryPaths)}"); + return null; } private static readonly Regex SteamAppsPathsRegex = new(@"""path""\s+""(.+)"""); private static readonly Regex ManifestInstallLocationRegex = new(@"""installdir""\s+""(.+)"""); + private static readonly Regex PlatformOverrideSourceRegex = new(@"""platform_override_source""\s+""(.+)"""); + + public static string? FindSteamDirectory() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return FindSteamDirectoryWin(); + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return FindSteamDirectoryOsx(); + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return FindSteamDirectoryLinux(); + else + throw new NotSupportedException("Unknown operating system"); + } public static string? FindSteamAppsDirectory() { @@ -56,12 +97,22 @@ public static class SteamUtils } [SupportedOSPlatform("Windows")] - private static string? FindSteamAppsDirectoryWin() + private static string? FindSteamDirectoryWin() { return Registry.LocalMachine.OpenSubKey(@"Software\WOW6432Node\Valve\Steam", false)?.GetValue("InstallPath") as string; } - private static string? FindSteamAppsDirectoryOsx() + private static string? FindSteamAppsDirectoryWin() + { + var steamDir = FindSteamDirectory(); + if (steamDir == null) + { + return null; + } + return Path.Combine(steamDir, "steamapps"); + } + + private static string? FindSteamDirectoryOsx() { return Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), @@ -71,7 +122,17 @@ public static class SteamUtils ); } - private static string? FindSteamAppsDirectoryLinux() + private static string? FindSteamAppsDirectoryOsx() + { + var steamDir = FindSteamDirectory(); + if (steamDir == null) + { + return null; + } + return Path.Combine(steamDir, "steamapps"); + } + + private static string? FindSteamDirectoryLinux() { string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); string[] possiblePaths = { @@ -93,12 +154,18 @@ public static class SteamUtils break; } } + return steamPath; + } + + private static string? FindSteamAppsDirectoryLinux() + { + var steamPath = FindSteamDirectory(); if (steamPath == null) { return null; } - possiblePaths = new[] + var possiblePaths = new[] { Path.Combine(steamPath, "steamapps"), // most distros Path.Combine(steamPath, "steam", "steamapps"), // ubuntu apparently diff --git a/ThunderstoreCLI/dsp.yml b/ThunderstoreCLI/dsp.yml new file mode 100644 index 0000000..8e96649 --- /dev/null +++ b/ThunderstoreCLI/dsp.yml @@ -0,0 +1,19 @@ +uuid: "b4ee10ce-d22c-4da3-b084-e97ced4fec85" +label: "dsp" +meta: + displayName: "Dyson Sphere Program" + iconUrl: "DysonSphereProgram.jpg" +distributions: + - platform: "steam" + identifier: "1366540" +legacy: + internalFolderName: "DysonSphereProgram" + dataFolderName: "DSPGAME_Data" + settingsIdentifier: "DysonSphereProgram" + packageIndex: "https://dsp.thunderstore.io/api/v1/package/" + exclusionsUrl: "https://raw.githubusercontent.com/ebkr/r2modmanPlus/master/modExclusions.md" + steamFolderName: "Dyson Sphere Program" + exeNames: + - "DSPGAME.exe" + gameInstancetype: "game" + gameSelectionDisplayMode: "visible" diff --git a/tcli-bepinex-installer/src/main.rs b/tcli-bepinex-installer/src/main.rs index 2063f87..e3721fe 100644 --- a/tcli-bepinex-installer/src/main.rs +++ b/tcli-bepinex-installer/src/main.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, ffi::OsString, fs::{self, OpenOptions}, - io::{self, Read, Seek}, + io::{self, Read, Seek, Write}, path::{Path, PathBuf}, }; @@ -32,6 +32,12 @@ enum Commands { bepinex_directory: PathBuf, name: String, }, + StartInstructions { + game_directory: PathBuf, + bepinex_directory: PathBuf, + #[arg(long)] + game_platform: Option, + } } #[derive(Debug, thiserror::Error)] @@ -95,6 +101,14 @@ fn main() -> Result<()> { } uninstall(game_directory, bepinex_directory, name) } + Commands::StartInstructions { + bepinex_directory, + game_platform, + .. + } => { + output_instructions(bepinex_directory, game_platform); + Ok(()) + } } } @@ -110,7 +124,7 @@ fn install(game_dir: PathBuf, bep_dir: PathBuf, zip_path: PathBuf, namespace_bac let manifest: ManifestV1 = serde_json::from_reader(manifest_file).map_err(|_| Error::InvalidManifest)?; - if manifest.name.starts_with("BepInExPack") { + if manifest.name.starts_with("BepInEx") { install_bepinex(game_dir, bep_dir, zip) } else { install_mod(bep_dir, zip, manifest, namespace_backup) @@ -134,8 +148,7 @@ fn install_bepinex( let filepath = file.enclosed_name().ok_or(Error::MalformedZip)?.to_owned(); if !top_level_directory_name(&filepath) - .or(Some("".to_string())) - .unwrap() + .unwrap_or_else(|| "".to_string()) .starts_with("BepInExPack") { continue; @@ -143,10 +156,8 @@ fn install_bepinex( let dir_to_use = if filepath.ancestors().any(|part| { part.file_name() - .or(Some(&OsString::new())) - .unwrap() - .to_string_lossy() - == "BepInEx" + .unwrap_or(&OsString::new()) + .to_string_lossy() == "BepInEx" }) { &bep_dir } else { @@ -250,6 +261,19 @@ fn uninstall_mod(bep_dir: PathBuf, name: String) -> Result<()> { Ok(()) } +fn output_instructions(bep_dir: PathBuf, platform: Option) { + if platform.as_ref().map(|p| p == "windows").unwrap_or(true) { + let drive_prefix = match platform { + Some(_) => "Z:", + None => "" + }; + + println!("ARGUMENTS:--doorstop-enable true --doorstop-target {}{}", drive_prefix, bep_dir.join("BepInEx").join("core").join("BepInEx.Preloader.dll").to_string_lossy().replace('/', "\\")); + } else { + eprintln!("native linux not implmented"); + } +} + fn top_level_directory_name(path: &Path) -> Option { path.ancestors() .skip(1) From 1b1a5d5c93693fa9d8e9338b523ccac95626e94f Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 25 Oct 2022 21:13:48 -0500 Subject: [PATCH 31/91] Add automatic proton DLL override support --- ThunderstoreCLI/Commands/InstallCommand.cs | 2 - ThunderstoreCLI/Commands/RunCommand.cs | 10 +-- ThunderstoreCLI/Utils/SteamUtils.cs | 72 ++++++++++++++++++++++ ThunderstoreCLI/ror2.yml | 8 +++ tcli-bepinex-installer/src/main.rs | 1 + 5 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 ThunderstoreCLI/ror2.yml diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index ad023d5..adc97cb 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -154,7 +154,6 @@ private static int RunInstaller(GameDefinition game, ModProfile profile, string profile.ProfileDirectory, zipPath }, - RedirectStandardOutput = true, RedirectStandardError = true }; if (backupNamespace != null) @@ -166,7 +165,6 @@ private static int RunInstaller(GameDefinition game, ModProfile profile, string var installerProcess = Process.Start(installerInfo)!; installerProcess.WaitForExit(); - Write.Light(installerProcess.StandardOutput.ReadToEnd()); string errors = installerProcess.StandardError.ReadToEnd(); if (!string.IsNullOrWhiteSpace(errors)) { diff --git a/ThunderstoreCLI/Commands/RunCommand.cs b/ThunderstoreCLI/Commands/RunCommand.cs index 3033d97..71cf08b 100644 --- a/ThunderstoreCLI/Commands/RunCommand.cs +++ b/ThunderstoreCLI/Commands/RunCommand.cs @@ -61,7 +61,6 @@ public static int Run(Config config) } string runArguments = ""; - List<(string key, string value)> runEnvironment = new(); string[] wineDlls = Array.Empty(); string[] outputLines = installerProcess.StandardOutput.ReadToEnd().Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); @@ -82,7 +81,7 @@ public static int Run(Config config) case "WINEDLLOVERRIDE": if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - throw new NotSupportedException(); + break; } wineDlls = args.Split(':'); break; @@ -108,9 +107,12 @@ public static int Run(Config config) throw new NotImplementedException(); } - if (gameIsProton) + if (gameIsProton && wineDlls.Length > 0) { - // TODO: force wine DLL overrides with registry + if (!SteamUtils.ForceLoadProton(def.PlatformId, wineDlls)) + { + throw new CommandFatalException($"No compat files could be found for app id {def.PlatformId}, please run the game at least once."); + } } ProcessStartInfo runSteamInfo = new(Path.Combine(steamDir, steamExeName)) diff --git a/ThunderstoreCLI/Utils/SteamUtils.cs b/ThunderstoreCLI/Utils/SteamUtils.cs index 285de79..fe5d71e 100644 --- a/ThunderstoreCLI/Utils/SteamUtils.cs +++ b/ThunderstoreCLI/Utils/SteamUtils.cs @@ -183,4 +183,76 @@ public static bool IsProtonGame(string steamAppId) return steamAppsPath; } + + public static bool ForceLoadProton(string steamAppId, string[] dllsToEnable) + { + var path = Path.Combine(Path.GetDirectoryName(GetAcfPath(steamAppId))!, "compatdata", steamAppId, "pfx", "user.reg"); + if (!Path.Exists(path)) + { + return false; + } + + string[] lines = File.ReadAllLines(path); + + int start = -1; + for (int i = 0; i < lines.Length; i++) + { + if (lines[i].StartsWith(@"[Software\\Wine\\DllOverrides]")) + { + start = i + 2; + break; + } + } + if (start == -1) + { + return false; + } + + int end = lines.Length - 1; + for (int i = start; i < lines.Length; i++) + { + if (lines[i].Length == 0) + { + end = i; + break; + } + } + + bool written = false; + foreach (var dll in dllsToEnable) + { + string wineOverride = $@"""{dll}""=""native,builtin"""; + bool existed = false; + for (int i = start; i < end; i++) + { + if (lines[i].StartsWith($@"""{dll}""")) + { + existed = true; + if (lines[i] != wineOverride) + { + lines[i] = wineOverride; + written = true; + } + break; + } + } + + if (!existed) + { + // resizes then moves the end and all lines past it over by 1, this is basically a manual List.Insert on an array + Array.Resize(ref lines, lines.Length + 1); + lines.AsSpan()[end..^1].CopyTo(lines.AsSpan()[(end + 1)..]); + lines[end] = wineOverride; + written = true; + } + } + + if (written) + { + File.Move(path, path + ".bak", true); + File.WriteAllLines(path, lines); + } + + return true; + } } diff --git a/ThunderstoreCLI/ror2.yml b/ThunderstoreCLI/ror2.yml new file mode 100644 index 0000000..1849582 --- /dev/null +++ b/ThunderstoreCLI/ror2.yml @@ -0,0 +1,8 @@ +uuid: "a672812c-5a54-4fb2-bcb1-0bf4b9ca1781" +label: "ror2" +meta: + displayName: "Risk of Rain 2" + iconUrl: "RiskOfRain2.jpg" +distributions: + - platform: "steam" + identifier: "632360" diff --git a/tcli-bepinex-installer/src/main.rs b/tcli-bepinex-installer/src/main.rs index e3721fe..fe78738 100644 --- a/tcli-bepinex-installer/src/main.rs +++ b/tcli-bepinex-installer/src/main.rs @@ -269,6 +269,7 @@ fn output_instructions(bep_dir: PathBuf, platform: Option) { }; println!("ARGUMENTS:--doorstop-enable true --doorstop-target {}{}", drive_prefix, bep_dir.join("BepInEx").join("core").join("BepInEx.Preloader.dll").to_string_lossy().replace('/', "\\")); + println!("WINEDLLOVERRIDE:winhttp") } else { eprintln!("native linux not implmented"); } From fa112dd27788e03cb653dad262b279b174e39747 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 25 Oct 2022 21:15:05 -0500 Subject: [PATCH 32/91] Fix BepInEx override directory behavior r2mm makes it so SomeDir/patchers/somefile.txt still maps to patchers/Namespace-Name/somefile.txt --- tcli-bepinex-installer/src/main.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/tcli-bepinex-installer/src/main.rs b/tcli-bepinex-installer/src/main.rs index fe78738..b6e6a19 100644 --- a/tcli-bepinex-installer/src/main.rs +++ b/tcli-bepinex-installer/src/main.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, ffi::OsString, fs::{self, OpenOptions}, - io::{self, Read, Seek, Write}, + io::{self, Read, Seek}, path::{Path, PathBuf}, }; @@ -215,14 +215,14 @@ fn install_mod( let filepath = file.enclosed_name().ok_or(Error::MalformedZip)?.to_owned(); - let out_path: PathBuf = if let Some(root) = top_level_directory_name(&filepath) { - if let Some(remap) = remaps.get(&root as &str) { - remap.join(remove_first_n_directories(&filepath, 1)) + let out_path: PathBuf = if let Some((root, count)) = search_for_directory(&filepath, &["plugins", "patchers", "monomod", "config"]) { + if let Some(remap) = remaps.get(root) { + remap.join(remove_first_n_directories(&filepath, count)) } else { - remaps["plugins"].join(filepath) + remaps["plugins"].join(filepath.file_name().unwrap()) } } else { - remaps["plugins"].join(filepath) + remaps["plugins"].join(filepath.file_name().unwrap()) }; let full_out_path = bep_dir.join(out_path); @@ -283,6 +283,23 @@ fn top_level_directory_name(path: &Path) -> Option { .map(|root| root.to_string_lossy().to_string()) } +fn search_for_directory<'a>(path: &Path, targets: &[&'a str]) -> Option<(&'a str, usize)> { + let mut path_parts = path + .ancestors() + .filter(|x| !x.to_string_lossy().is_empty()) + .map(|x| x.file_name().unwrap()) + .collect::>(); + path_parts.reverse(); + for (index, part) in path_parts.into_iter().enumerate() { + for target in targets { + if part.to_string_lossy() == *target { + return Some((target, index + 1)); + } + } + } + None +} + /// removes the first n directories from a path, eg a/b/c/d.txt with an n of 2 gives c/d.txt fn remove_first_n_directories(path: &Path, n: usize) -> PathBuf { PathBuf::from_iter( From a6a3af29adc8762e5c41049a95f6bbacb0dad022 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Wed, 26 Oct 2022 18:59:29 -0500 Subject: [PATCH 33/91] Fix BepInEx override directory behavior (attempt 2) --- tcli-bepinex-installer/src/main.rs | 81 ++++++++++++------------------ 1 file changed, 33 insertions(+), 48 deletions(-) diff --git a/tcli-bepinex-installer/src/main.rs b/tcli-bepinex-installer/src/main.rs index b6e6a19..0cb49fe 100644 --- a/tcli-bepinex-installer/src/main.rs +++ b/tcli-bepinex-installer/src/main.rs @@ -148,7 +148,7 @@ fn install_bepinex( let filepath = file.enclosed_name().ok_or(Error::MalformedZip)?.to_owned(); if !top_level_directory_name(&filepath) - .unwrap_or_else(|| "".to_string()) + .unwrap_or("") .starts_with("BepInExPack") { continue; @@ -191,20 +191,22 @@ fn install_mod( manifest.name ); - let mut remaps: HashMap<&str, PathBuf> = HashMap::new(); + let mut remaps = HashMap::new(); remaps.insert( - "plugins", + Path::new("BepInEx").join("plugins"), Path::new("BepInEx").join("plugins").join(&full_name), ); remaps.insert( - "patchers", + Path::new("BepInEx").join("patchers"), Path::new("BepInEx").join("patchers").join(&full_name), ); remaps.insert( - "monomod", + Path::new("BepInEx").join("monomod"), Path::new("BepInEx").join("monomod").join(&full_name), ); - remaps.insert("config", Path::new("BepInEx").join("config")); + remaps.insert(Path::new("BepInEx").join("config"), Path::new("BepInEx").join("config")); + + let default_remap = &remaps[&Path::new("BepInEx").join("plugins")]; for i in 0..zip.len() { let mut file = zip.by_index(i)?; @@ -215,17 +217,20 @@ fn install_mod( let filepath = file.enclosed_name().ok_or(Error::MalformedZip)?.to_owned(); - let out_path: PathBuf = if let Some((root, count)) = search_for_directory(&filepath, &["plugins", "patchers", "monomod", "config"]) { - if let Some(remap) = remaps.get(root) { - remap.join(remove_first_n_directories(&filepath, count)) - } else { - remaps["plugins"].join(filepath.file_name().unwrap()) + let mut out_path = None; + 'outer: for remap in remaps.keys() { + for variant in get_path_variants(remap) { + if let Ok(p) = filepath.strip_prefix(variant) { + out_path = Some(remaps[remap].join(p)); + break 'outer; + } } - } else { - remaps["plugins"].join(filepath.file_name().unwrap()) - }; + } + if out_path.is_none() { + out_path = Some(default_remap.join(filepath.file_name().unwrap())); + } - let full_out_path = bep_dir.join(out_path); + let full_out_path = bep_dir.join(out_path.unwrap()); fs::create_dir_all(full_out_path.parent().unwrap())?; io::copy(&mut file, &mut write_opts.open(full_out_path)?)?; @@ -235,7 +240,7 @@ fn install_mod( } fn uninstall(game_dir: PathBuf, bep_dir: PathBuf, name: String) -> Result<()> { - if name.split_once('-').ok_or(Error::InvalidModName)?.1.starts_with("BepInExPack") { + if name.split_once('-').ok_or(Error::InvalidModName)?.1.starts_with("BepInEx") { uninstall_bepinex(game_dir, bep_dir) } else { uninstall_mod(bep_dir, name) @@ -275,42 +280,22 @@ fn output_instructions(bep_dir: PathBuf, platform: Option) { } } -fn top_level_directory_name(path: &Path) -> Option { - path.ancestors() - .skip(1) - .filter(|x| !x.to_string_lossy().is_empty()) - .last() - .map(|root| root.to_string_lossy().to_string()) -} - -fn search_for_directory<'a>(path: &Path, targets: &[&'a str]) -> Option<(&'a str, usize)> { - let mut path_parts = path - .ancestors() - .filter(|x| !x.to_string_lossy().is_empty()) - .map(|x| x.file_name().unwrap()) - .collect::>(); - path_parts.reverse(); - for (index, part) in path_parts.into_iter().enumerate() { - for target in targets { - if part.to_string_lossy() == *target { - return Some((target, index + 1)); - } - } - } - None +fn top_level_directory_name(path: &Path) -> Option<&str> { + path.components().next().and_then(|n| n.as_os_str().to_str()) } /// removes the first n directories from a path, eg a/b/c/d.txt with an n of 2 gives c/d.txt fn remove_first_n_directories(path: &Path, n: usize) -> PathBuf { - PathBuf::from_iter( - path.ancestors() - .collect::>() - .into_iter() - .rev() - .filter(|x| !x.to_string_lossy().is_empty()) - .skip(n) - .map(|part| part.file_name().unwrap()), - ) + PathBuf::from_iter(path.iter().skip(n)) +} + +fn get_path_variants(path: &Path) -> Vec { + let mut res = vec![path.into()]; + let components: Vec<_> = path.components().collect(); + for i in 1usize..components.len() { + res.push(PathBuf::from_iter(components.iter().skip(i))) + } + res } fn delete_file_if_not_deleted>(path: T) -> io::Result<()> { From 71225c39d88c8742fca9bd789fc8743614cdb269 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Wed, 26 Oct 2022 19:02:39 -0500 Subject: [PATCH 34/91] Update Cargo.lock --- tcli-bepinex-installer/Cargo.lock | 87 +++++++++++++++++-------------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/tcli-bepinex-installer/Cargo.lock b/tcli-bepinex-installer/Cargo.lock index a0bd4d6..3c9ee1d 100644 --- a/tcli-bepinex-installer/Cargo.lock +++ b/tcli-bepinex-installer/Cargo.lock @@ -22,9 +22,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.65" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" [[package]] name = "atty" @@ -39,9 +39,9 @@ dependencies = [ [[package]] name = "base64ct" -version = "1.0.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" [[package]] name = "bitflags" @@ -111,9 +111,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.12" +version = "4.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385007cbbed899260395a4107435fead4cad80684461b3cc78238bdcb0bad58f" +checksum = "335867764ed2de42325fafe6d18b8af74ba97ee0c590fa016f157535b42ab04b" dependencies = [ "atty", "bitflags", @@ -126,9 +126,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.0.10" +version = "4.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db342ce9fda24fb191e2ed4e102055a4d381c1086a06630174cd8da8d5d917ce" +checksum = "16a1b0f6422af32d5da0c58e2703320f379216ee70198241c84173a8c5ac28f3" dependencies = [ "heck", "proc-macro-error", @@ -261,9 +261,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.135" +version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] name = "miniz_oxide" @@ -303,9 +303,9 @@ checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" [[package]] name = "password-hash" -version = "0.3.2" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" dependencies = [ "base64ct", "rand_core", @@ -314,9 +314,9 @@ dependencies = [ [[package]] name = "pbkdf2" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ "digest", "hmac", @@ -326,9 +326,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "proc-macro-error" @@ -356,9 +356,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ "unicode-ident", ] @@ -386,18 +386,18 @@ checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "serde" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" dependencies = [ "proc-macro2", "quote", @@ -406,9 +406,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" +checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" dependencies = [ "itoa", "ryu", @@ -451,9 +451,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" dependencies = [ "proc-macro2", "quote", @@ -503,21 +503,32 @@ dependencies = [ [[package]] name = "time" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" +checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca" dependencies = [ "itoa", "libc", "num_threads", + "serde", + "time-core", "time-macros", ] +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + [[package]] name = "time-macros" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "65bb801831d812c562ae7d2bfb531f26e66e4e1f6b17307ba4149c5064710e5b" +dependencies = [ + "time-core", +] [[package]] name = "typenum" @@ -570,9 +581,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "zip" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf225bcf73bb52cbb496e70475c7bd7a3f769df699c0020f6c7bd9a96dcf0b8d" +checksum = "537ce7411d25e54e8ae21a7ce0b15840e7bfcff15b51d697ec3266cc76bdf080" dependencies = [ "aes", "byteorder", @@ -590,18 +601,18 @@ dependencies = [ [[package]] name = "zstd" -version = "0.10.2+zstd.1.5.2" +version = "0.11.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4a6bd64f22b5e3e94b4e238669ff9f10815c27a5180108b849d24174a83847" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "4.1.6+zstd.1.5.2" +version = "5.0.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b61c51bb270702d6167b8ce67340d2754b088d0c091b06e593aa772c3ee9bb" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" dependencies = [ "libc", "zstd-sys", @@ -609,9 +620,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "1.6.3+zstd.1.5.2" +version = "2.0.1+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" +checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b" dependencies = [ "cc", "libc", From 0d4a51ceb6025736587900986f6fad0cee5e02ae Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Wed, 26 Oct 2022 20:25:28 -0500 Subject: [PATCH 35/91] Add logging and fix CI --- ThunderstoreCLI/Commands/InstallCommand.cs | 7 ++-- ThunderstoreCLI/Commands/RunCommand.cs | 19 ++-------- ThunderstoreCLI/Commands/UninstallCommand.cs | 4 +-- ThunderstoreCLI/Utils/Spinner.cs | 16 ++------- ThunderstoreCLI/Utils/SteamUtils.cs | 38 ++++++++++++++++++++ 5 files changed, 50 insertions(+), 34 deletions(-) diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index adc97cb..f642ab4 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -57,6 +57,7 @@ private static async Task InstallFromRepository(Config config, HttpClient h var packageParts = packageId.Split('-'); PackageVersionData version; + Write.Light($"Downloading main package: {packageId}"); if (packageParts.Length == 3) { var versionResponse = await http.SendAsync(config.Api.GetPackageVersionMetadata(packageParts[0], packageParts[1], packageParts[2])); @@ -71,12 +72,12 @@ private static async Task InstallFromRepository(Config config, HttpClient h } var tempZipPath = await DownloadTemp(http, version); - var returnCode = await InstallZip(config, http, game, profile, tempZipPath, version.Namespace!, true); + var returnCode = await InstallZip(config, http, game, profile, tempZipPath, version.Namespace!); File.Delete(tempZipPath); return returnCode; } - private static async Task InstallZip(Config config, HttpClient http, GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace, bool requiredDownload = false) + private static async Task InstallZip(Config config, HttpClient http, GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace) { using var zip = ZipFile.OpenRead(zipPath); var manifestFile = zip.GetEntry("manifest.json") ?? throw new CommandFatalException("Package zip needs a manifest.json!"); @@ -93,7 +94,7 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe { var downloadTasks = dependenciesToInstall.Select(mod => DownloadTemp(http, mod.LatestVersion!)).ToArray(); - var spinner = new ProgressSpinner("mods downloaded", downloadTasks, 1); + var spinner = new ProgressSpinner("dependencies downloaded", downloadTasks); await spinner.Spin(); foreach (var (tempZipPath, package) in downloadTasks.Select(x => x.Result).Zip(dependenciesToInstall)) diff --git a/ThunderstoreCLI/Commands/RunCommand.cs b/ThunderstoreCLI/Commands/RunCommand.cs index 71cf08b..9f4d058 100644 --- a/ThunderstoreCLI/Commands/RunCommand.cs +++ b/ThunderstoreCLI/Commands/RunCommand.cs @@ -88,24 +88,11 @@ public static int Run(Config config) } } - var steamDir = SteamUtils.FindSteamDirectory(); - if (steamDir == null) + var steamExePath = SteamUtils.FindSteamExecutable(); + if (steamExePath == null) { throw new CommandFatalException("Couldn't find steam install directory!"); } - string steamExeName; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - steamExeName = "steam.sh"; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - steamExeName = "steam.exe"; - } - else - { - throw new NotImplementedException(); - } if (gameIsProton && wineDlls.Length > 0) { @@ -115,7 +102,7 @@ public static int Run(Config config) } } - ProcessStartInfo runSteamInfo = new(Path.Combine(steamDir, steamExeName)) + ProcessStartInfo runSteamInfo = new(steamExePath) { Arguments = $"-applaunch {def.PlatformId} {runArguments}" }; diff --git a/ThunderstoreCLI/Commands/UninstallCommand.cs b/ThunderstoreCLI/Commands/UninstallCommand.cs index 00a069f..016c371 100644 --- a/ThunderstoreCLI/Commands/UninstallCommand.cs +++ b/ThunderstoreCLI/Commands/UninstallCommand.cs @@ -78,14 +78,14 @@ public static int Run(Config config) profile.ProfileDirectory, toRemove }, - RedirectStandardOutput = true, RedirectStandardError = true }; var installerProcess = Process.Start(installerInfo)!; installerProcess.WaitForExit(); - Write.Light(installerProcess.StandardOutput.ReadToEnd()); + Write.Success($"Uninstalled mod: {toRemove}"); + string errors = installerProcess.StandardError.ReadToEnd(); if (!string.IsNullOrWhiteSpace(errors) || installerProcess.ExitCode != 0) { diff --git a/ThunderstoreCLI/Utils/Spinner.cs b/ThunderstoreCLI/Utils/Spinner.cs index da47afc..4360044 100644 --- a/ThunderstoreCLI/Utils/Spinner.cs +++ b/ThunderstoreCLI/Utils/Spinner.cs @@ -9,9 +9,8 @@ public class ProgressSpinner private static readonly char[] _spinChars = { '|', '/', '-', '\\' }; private readonly string _label; private readonly Task[] _tasks; - private readonly int _offset; - public ProgressSpinner(string label, Task[] tasks, int offset = 0) + public ProgressSpinner(string label, Task[] tasks) { if (tasks.Length == 0) { @@ -20,7 +19,6 @@ public ProgressSpinner(string label, Task[] tasks, int offset = 0) _label = label; _tasks = tasks; - _offset = offset; } public async Task Spin() @@ -39,14 +37,6 @@ public async Task Spin() canUseCursor = false; } - if (!canUseCursor && _offset != 0) - { - for (int i = 1; i <= _offset; i++) - { - Console.Write(Green($"{0}/{_tasks.Length + _offset} {_label}")); - } - } - while (true) { IEnumerable faultedTasks; @@ -62,13 +52,13 @@ public async Task Spin() { var spinner = completed == _tasks.Length ? '✓' : _spinChars[_spinIndex++ % _spinChars.Length]; Console.SetCursorPosition(0, Console.CursorTop); - Console.Write(Green($"{completed + _offset}/{_tasks.Length + _offset} {_label} {spinner}")); + Console.Write(Green($"{completed}/{_tasks.Length} {_label} {spinner}")); } else { if (completed > _lastSeenCompleted) { - Write.Success($"{completed + _offset}/{_tasks.Length + _offset} {_label}"); + Write.Success($"{completed}/{_tasks.Length} {_label}"); _lastSeenCompleted = completed; } } diff --git a/ThunderstoreCLI/Utils/SteamUtils.cs b/ThunderstoreCLI/Utils/SteamUtils.cs index fe5d71e..2d80644 100644 --- a/ThunderstoreCLI/Utils/SteamUtils.cs +++ b/ThunderstoreCLI/Utils/SteamUtils.cs @@ -72,6 +72,44 @@ public static bool IsProtonGame(string steamAppId) private static readonly Regex ManifestInstallLocationRegex = new(@"""installdir""\s+""(.+)"""); private static readonly Regex PlatformOverrideSourceRegex = new(@"""platform_override_source""\s+""(.+)"""); + public static string? FindSteamExecutable() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var mainDir = FindSteamDirectory(); + if (mainDir == null) + { + return null; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return Path.Combine(mainDir, "steam.exe"); + } + else + { + return Path.Combine(mainDir, "steam.sh"); + } + } + + string appDir; + string rooted = Path.Combine("/", "Applications", "Steam.app"); + string user = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Applications", "Steam.app"); + if (Directory.Exists(user)) + { + appDir = user; + } + else if (Directory.Exists(rooted)) + { + appDir = rooted; + } + else + { + return null; + } + + return Path.Combine(appDir, "Contents", "MacOS", "steam_osx"); + } + public static string? FindSteamDirectory() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) From 991c2efe843dabbee1b111ec455b8df78d5a7b43 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 1 Nov 2022 18:15:00 -0500 Subject: [PATCH 36/91] Add installers feature flag --- ThunderstoreCLI/Options.cs | 20 -------------------- ThunderstoreCLI/Program.cs | 8 +++++++- ThunderstoreCLI/ThunderstoreCLI.csproj | 10 +++++++--- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index bbcc532..d25fe8e 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -164,16 +164,6 @@ protected enum CommandInner protected abstract CommandInner CommandType { get; } - public override bool Validate() - { -#if NOINSTALLERS - Write.ErrorExit("Installers are not supported when installed through dotnet tool (yet) (i hate nuget)"); - return false; -#endif - - return base.Validate(); - } - public override int Execute() { var config = Config.FromCLI(new ModManagementCommandConfig(this)); @@ -230,16 +220,6 @@ public class RunGameOptions : BaseOptions [Option(HelpText = "Profile to install to", Default = "Default")] public required string Profile { get; set; } - public override bool Validate() - { -#if NOINSTALLERS - Write.ErrorExit("Installers are not supported when installed through dotnet tool (yet) (i hate nuget)"); - return false; -#endif - - return base.Validate(); - } - public override int Execute() { var config = Config.FromCLI(new RunGameCommandConfig(this)); diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index 17f0028..6e99f00 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -16,15 +16,21 @@ private static int Main(string[] args) #endif var updateChecker = UpdateChecker.CheckForUpdates(); - var exitCode = Parser.Default.ParseArguments(args) + var exitCode = Parser.Default.ParseArguments(args) .MapResult( (InitOptions o) => HandleParsed(o), (BuildOptions o) => HandleParsed(o), (PublishOptions o) => HandleParsed(o), +#if INSTALLERS (InstallOptions o) => HandleParsed(o), (UninstallOptions o) => HandleParsed(o), (GameImportOptions o) => HandleParsed(o), (RunGameOptions o) => HandleParsed(o), +#endif _ => 1 // failure to parse ); UpdateChecker.WriteUpdateNotification(updateChecker); diff --git a/ThunderstoreCLI/ThunderstoreCLI.csproj b/ThunderstoreCLI/ThunderstoreCLI.csproj index ea0e9ea..e9ab141 100644 --- a/ThunderstoreCLI/ThunderstoreCLI.csproj +++ b/ThunderstoreCLI/ThunderstoreCLI.csproj @@ -63,8 +63,12 @@ + + true + + - + --release debug @@ -77,7 +81,7 @@ - - NOINSTALLERS + + INSTALLERS From 7090e0081d3d2763891f78c455ad9d98e08da37d Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 14 Nov 2022 07:05:29 -0600 Subject: [PATCH 37/91] Various fixes for gigamod --- ThunderstoreCLI/API/ApiHelper.cs | 8 ++ ThunderstoreCLI/Commands/InstallCommand.cs | 74 ++++++------ ThunderstoreCLI/Configuration/Config.cs | 5 + ThunderstoreCLI/Game/GameDefinition.cs | 2 +- ThunderstoreCLI/Models/PackageListingV1.cs | 125 +++++++++++++++++++++ ThunderstoreCLI/Models/PublishModels.cs | 2 +- ThunderstoreCLI/PackageManifestV1.cs | 10 ++ ThunderstoreCLI/Utils/DownloadCache.cs | 53 +++++++++ ThunderstoreCLI/Utils/ModDependencyTree.cs | 93 +++++++++++---- tcli-bepinex-installer/src/main.rs | 25 ++++- 10 files changed, 332 insertions(+), 65 deletions(-) create mode 100644 ThunderstoreCLI/Models/PackageListingV1.cs create mode 100644 ThunderstoreCLI/Utils/DownloadCache.cs diff --git a/ThunderstoreCLI/API/ApiHelper.cs b/ThunderstoreCLI/API/ApiHelper.cs index 214d471..ac77097 100644 --- a/ThunderstoreCLI/API/ApiHelper.cs +++ b/ThunderstoreCLI/API/ApiHelper.cs @@ -89,6 +89,14 @@ public HttpRequestMessage GetPackageVersionMetadata(string author, string name, .GetRequest(); } + public HttpRequestMessage GetPackagesV1() + { + return BaseRequestBuilder + .StartNew() + .WithEndpoint(V1 + "package/") + .GetRequest(); + } + private static string SerializeFileData(string filePath) { return new FileData() diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index f642ab4..4984e40 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.IO.Compression; +using System.Net.Http.Headers; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using ThunderstoreCLI.Configuration; @@ -9,10 +10,11 @@ namespace ThunderstoreCLI.Commands; -public static class InstallCommand +public static partial class InstallCommand { // will match either ab-cd or ab-cd-123.456.7890 - internal static readonly Regex FullPackageNameRegex = new(@"^(\w+)-(\w+)(?:|-(\d+\.\d+\.\d+))$"); + [GeneratedRegex(@"^(?(?[\w-\.]+)-(?\w+))(?:|-(?\d+\.\d+\.\d+))$")] + internal static partial Regex FullPackageNameRegex(); public static async Task Run(Config config) { @@ -33,13 +35,14 @@ public static async Task Run(Config config) HttpClient http = new(); int returnCode; + Match packageMatch; if (File.Exists(package)) { returnCode = await InstallZip(config, http, def, profile, package, null); } - else if (FullPackageNameRegex.IsMatch(package)) + else if ((packageMatch = FullPackageNameRegex().Match(package)).Success) { - returnCode = await InstallFromRepository(config, http, def, profile, package); + returnCode = await InstallFromRepository(config, http, def, profile, packageMatch); } else { @@ -52,28 +55,29 @@ public static async Task Run(Config config) return returnCode; } - private static async Task InstallFromRepository(Config config, HttpClient http, GameDefinition game, ModProfile profile, string packageId) + private static async Task InstallFromRepository(Config config, HttpClient http, GameDefinition game, ModProfile profile, Match packageMatch) { - var packageParts = packageId.Split('-'); + PackageVersionData versionData; + Write.Light($"Downloading main package: {packageMatch.Groups["fullname"].Value}"); - PackageVersionData version; - Write.Light($"Downloading main package: {packageId}"); - if (packageParts.Length == 3) + var ns = packageMatch.Groups["namespace"]; + var name = packageMatch.Groups["name"]; + var version = packageMatch.Groups["version"]; + if (version.Success) { - var versionResponse = await http.SendAsync(config.Api.GetPackageVersionMetadata(packageParts[0], packageParts[1], packageParts[2])); + var versionResponse = await http.SendAsync(config.Api.GetPackageVersionMetadata(ns.Value, name.Value, version.Value)); versionResponse.EnsureSuccessStatusCode(); - version = (await PackageVersionData.DeserializeAsync(await versionResponse.Content.ReadAsStreamAsync()))!; + versionData = (await PackageVersionData.DeserializeAsync(await versionResponse.Content.ReadAsStreamAsync()))!; } else { - var packageResponse = await http.SendAsync(config.Api.GetPackageMetadata(packageParts[0], packageParts[1])); + var packageResponse = await http.SendAsync(config.Api.GetPackageMetadata(ns.Value, name.Value)); packageResponse.EnsureSuccessStatusCode(); - version = (await PackageData.DeserializeAsync(await packageResponse.Content.ReadAsStreamAsync()))!.LatestVersion!; + versionData = (await PackageData.DeserializeAsync(await packageResponse.Content.ReadAsStreamAsync()))!.LatestVersion!; } - var tempZipPath = await DownloadTemp(http, version); - var returnCode = await InstallZip(config, http, game, profile, tempZipPath, version.Namespace!); - File.Delete(tempZipPath); + var zipPath = await config.Cache.GetFileOrDownload($"{versionData.FullName}.zip", versionData.DownloadUrl!); + var returnCode = await InstallZip(config, http, game, profile, zipPath, versionData.Namespace!); return returnCode; } @@ -92,25 +96,39 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe if (dependenciesToInstall.Length > 0) { - var downloadTasks = dependenciesToInstall.Select(mod => DownloadTemp(http, mod.LatestVersion!)).ToArray(); + double totalSize = dependenciesToInstall.Select(d => (double) d.Versions![0].FileSize).Sum(); + string[] suffixes = { "B", "KB", "MB", "GB", "TB" }; + int suffixIndex = 0; + while (totalSize >= 1024 && suffixIndex < suffixes.Length) + { + totalSize /= 1024; + suffixIndex++; + } + Write.Light($"Total estimated download size: {totalSize:F2} {suffixes[suffixIndex]}"); + + var downloadTasks = dependenciesToInstall.Select(mod => + { + var version = mod.Versions![0]; + return config.Cache.GetFileOrDownload($"{mod.Fullname}-{version.VersionNumber}.zip", version.DownloadUrl!); + }).ToArray(); var spinner = new ProgressSpinner("dependencies downloaded", downloadTasks); await spinner.Spin(); foreach (var (tempZipPath, package) in downloadTasks.Select(x => x.Result).Zip(dependenciesToInstall)) { - int returnCode = RunInstaller(game, profile, tempZipPath, package.Namespace); - File.Delete(tempZipPath); + var packageVersion = package.Versions![0]; + int returnCode = RunInstaller(game, profile, tempZipPath, package.Owner); if (returnCode == 0) { - Write.Success($"Installed mod: {package.Fullname}-{package.LatestVersion!.VersionNumber}"); + Write.Success($"Installed mod: {package.Fullname}-{packageVersion.VersionNumber}"); } else { - Write.Error($"Failed to install mod: {package.Fullname}-{package.LatestVersion!.VersionNumber}"); + Write.Error($"Failed to install mod: {package.Fullname}-{packageVersion.VersionNumber}"); return returnCode; } - profile.InstalledModVersions[package.Fullname!] = new PackageManifestV1(package.LatestVersion!); + profile.InstalledModVersions[package.Fullname!] = new PackageManifestV1(package, packageVersion); } } @@ -127,18 +145,6 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe return exitCode; } - // TODO: replace with a mod cache - private static async Task DownloadTemp(HttpClient http, PackageVersionData version) - { - string path = Path.GetTempFileName(); - await using var file = File.OpenWrite(path); - using var response = await http.SendAsync(new HttpRequestMessage(HttpMethod.Get, version.DownloadUrl!)); - response.EnsureSuccessStatusCode(); - var zipStream = await response.Content.ReadAsStreamAsync(); - await zipStream.CopyToAsync(file); - return path; - } - // TODO: conflict handling private static int RunInstaller(GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace) { diff --git a/ThunderstoreCLI/Configuration/Config.cs b/ThunderstoreCLI/Configuration/Config.cs index 27a992b..951272e 100644 --- a/ThunderstoreCLI/Configuration/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using ThunderstoreCLI.API; using ThunderstoreCLI.Models; +using ThunderstoreCLI.Utils; namespace ThunderstoreCLI.Configuration; @@ -21,9 +22,13 @@ public class Config private readonly Lazy api; public ApiHelper Api => api.Value; + private readonly Lazy cache; + public DownloadCache Cache => cache.Value; + private Config(GeneralConfig generalConfig, PackageConfig packageConfig, InitConfig initConfig, BuildConfig buildConfig, PublishConfig publishConfig, AuthConfig authConfig, ModManagementConfig modManagementConfig, GameImportConfig gameImportConfig, RunGameConfig runGameConfig) { api = new Lazy(() => new ApiHelper(this)); + cache = new Lazy(() => new DownloadCache(Path.Combine(GeneralConfig!.TcliConfig, "ModCache"))); GeneralConfig = generalConfig; PackageConfig = packageConfig; InitConfig = initConfig; diff --git a/ThunderstoreCLI/Game/GameDefinition.cs b/ThunderstoreCLI/Game/GameDefinition.cs index 9c88b11..3837191 100644 --- a/ThunderstoreCLI/Game/GameDefinition.cs +++ b/ThunderstoreCLI/Game/GameDefinition.cs @@ -46,7 +46,7 @@ public sealed class GameDefintionCollection : IEnumerable, IDisp private const string FILE_NAME = "GameDefintions.json"; private readonly string tcliDirectory; - private bool shouldWrite = true; + private bool shouldWrite = false; public List List { get; } internal static GameDefintionCollection FromDirectory(string tcliDirectory) => new(tcliDirectory); diff --git a/ThunderstoreCLI/Models/PackageListingV1.cs b/ThunderstoreCLI/Models/PackageListingV1.cs new file mode 100644 index 0000000..4a6743d --- /dev/null +++ b/ThunderstoreCLI/Models/PackageListingV1.cs @@ -0,0 +1,125 @@ +using Newtonsoft.Json; + +namespace ThunderstoreCLI.Models; + +public class PackageListingV1 : BaseJson +{ + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("full_name")] + public string? Fullname { get; set; } + + [JsonProperty("owner")] + public string? Owner { get; set; } + + [JsonProperty("package_url")] + public string? PackageUrl { get; set; } + + [JsonProperty("date_created")] + public DateTime DateCreated { get; set; } + + [JsonProperty("date_updated")] + public DateTime DateUpdated { get; set; } + + [JsonProperty("uuid4")] + public string? Uuid4 { get; set; } + + [JsonProperty("rating_score")] + public int RatingScore { get; set; } + + [JsonProperty("is_pinned")] + public bool IsPinned { get; set; } + + [JsonProperty("is_deprecated")] + public bool IsDeprecated { get; set; } + + [JsonProperty("has_nsfw_content")] + public bool HasNsfwContent { get; set; } + + [JsonProperty("categories")] + public string[]? Categories { get; set; } + + [JsonProperty("versions")] + public PackageVersionV1[]? Versions { get; set; } + + public PackageListingV1() { } + + public PackageListingV1(PackageData package) + { + Name = package.Name; + Fullname = package.Fullname; + Owner = package.Namespace; + PackageUrl = package.PackageUrl; + DateCreated = package.DateCreated; + DateUpdated = package.DateUpdated; + Uuid4 = null; + RatingScore = int.Parse(package.RatingScore!); + IsPinned = package.IsPinned; + IsDeprecated = package.IsDeprecated; + HasNsfwContent = package.CommunityListings!.Any(l => l.HasNsfwContent); + Categories = Array.Empty(); + Versions = new[] { new PackageVersionV1(package.LatestVersion!) }; + } +} + +public class PackageVersionV1 +{ + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("full_name")] + public string? FullName { get; set; } + + [JsonProperty("description")] + public string? Description { get; set; } + + [JsonProperty("icon")] + public string? Icon { get; set; } + + [JsonProperty("version_number")] + public string? VersionNumber { get; set; } + + [JsonProperty("dependencies")] + public string[]? Dependencies { get; set; } + + [JsonProperty("download_url")] + public string? DownloadUrl { get; set; } + + [JsonProperty("downloads")] + public int Downloads { get; set; } + + [JsonProperty("date_created")] + public DateTime DateCreated { get; set; } + + [JsonProperty("website_url")] + public string? WebsiteUrl { get; set; } + + [JsonProperty("is_active")] + public bool IsActive { get; set; } + + [JsonProperty("uuid4")] + public string? Uuid4 { get; set; } + + [JsonProperty("file_size")] + public int FileSize { get; set; } + + public PackageVersionV1() { } + + public PackageVersionV1(PackageVersionData version) + { + Name = version.Name; + FullName = version.FullName; + Description = version.Description; + Icon = version.Icon; + VersionNumber = version.VersionNumber; + Dependencies = version.Dependencies; + DownloadUrl = version.DownloadUrl; + Downloads = version.Downloads; + DateCreated = version.DateCreated; + WebsiteUrl = version.WebsiteUrl; + IsActive = version.IsActive; + Uuid4 = null; + FileSize = 0; + } +} diff --git a/ThunderstoreCLI/Models/PublishModels.cs b/ThunderstoreCLI/Models/PublishModels.cs index 482fe83..f4aa90d 100644 --- a/ThunderstoreCLI/Models/PublishModels.cs +++ b/ThunderstoreCLI/Models/PublishModels.cs @@ -149,7 +149,7 @@ public class PackageVersionData : BaseJson [JsonProperty("icon")] public string? Icon { get; set; } - [JsonProperty("dependencies")] public List? Dependencies { get; set; } + [JsonProperty("dependencies")] public string[]? Dependencies { get; set; } [JsonProperty("download_url")] public string? DownloadUrl { get; set; } diff --git a/ThunderstoreCLI/PackageManifestV1.cs b/ThunderstoreCLI/PackageManifestV1.cs index 6b1fc6c..3ea2b1b 100644 --- a/ThunderstoreCLI/PackageManifestV1.cs +++ b/ThunderstoreCLI/PackageManifestV1.cs @@ -37,4 +37,14 @@ public PackageManifestV1(PackageVersionData version) Dependencies = version.Dependencies?.ToArray() ?? Array.Empty(); WebsiteUrl = version.WebsiteUrl; } + + public PackageManifestV1(PackageListingV1 listing, PackageVersionV1 version) + { + Namespace = listing.Owner; + Name = listing.Name; + Description = version.Description; + VersionNumber = version.VersionNumber; + Dependencies = version.Dependencies?.ToArray() ?? Array.Empty(); + WebsiteUrl = version.WebsiteUrl; + } } diff --git a/ThunderstoreCLI/Utils/DownloadCache.cs b/ThunderstoreCLI/Utils/DownloadCache.cs new file mode 100644 index 0000000..26169ad --- /dev/null +++ b/ThunderstoreCLI/Utils/DownloadCache.cs @@ -0,0 +1,53 @@ +namespace ThunderstoreCLI.Utils; + +public sealed class DownloadCache +{ + public string CacheDirectory { get; } + + private HttpClient Client { get; } = new() + { + Timeout = TimeSpan.FromHours(1) + }; + + public DownloadCache(string cacheDirectory) + { + CacheDirectory = cacheDirectory; + } + + // Task instead of ValueTask here because these Tasks will be await'd multiple times (ValueTask does not allow that) + public Task GetFileOrDownload(string filename, string downloadUrl) + { + string fullPath = Path.Combine(CacheDirectory, filename); + if (File.Exists(fullPath)) + { + return Task.FromResult(fullPath); + } + + return DownloadFile(fullPath, downloadUrl); + } + + private async Task DownloadFile(string fullpath, string downloadUrl) + { + int tryCount = 0; +Retry: + try + { + var tempPath = fullpath + ".tmp"; + // copy into memory first to prevent canceled downloads creating files on the disk + await using FileStream tempStream = new(tempPath, FileMode.Create, FileAccess.Write); + await using var downloadStream = await Client.GetStreamAsync(downloadUrl); + await downloadStream.CopyToAsync(tempStream); + + File.Move(tempPath, fullpath); + } + catch + { + if (++tryCount == 5) + { + throw; + } + goto Retry; + } + return fullpath; + } +} diff --git a/ThunderstoreCLI/Utils/ModDependencyTree.cs b/ThunderstoreCLI/Utils/ModDependencyTree.cs index ed32a69..4211ba0 100644 --- a/ThunderstoreCLI/Utils/ModDependencyTree.cs +++ b/ThunderstoreCLI/Utils/ModDependencyTree.cs @@ -1,4 +1,7 @@ -using System.Collections.Concurrent; +using System.Diagnostics; +using System.Net; +using System.Text.RegularExpressions; +using ThunderstoreCLI.Commands; using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Models; @@ -6,42 +9,86 @@ namespace ThunderstoreCLI.Utils; public static class ModDependencyTree { - public static IEnumerable Generate(Config config, HttpClient http, PackageManifestV1 root) + public static IEnumerable Generate(Config config, HttpClient http, PackageManifestV1 root) { - HashSet alreadyGottenPackages = new(); - foreach (var dependency in root.Dependencies!) + var cachePath = Path.Combine(config.GeneralConfig.TcliConfig, "package-ror2.json"); + string packagesJson; + if (!File.Exists(cachePath) || new FileInfo(cachePath).LastWriteTime.AddMinutes(5) < DateTime.Now) { - var depParts = dependency.Split('-'); - var depRequest = http.Send(config.Api.GetPackageMetadata(depParts[0], depParts[1])); - depRequest.EnsureSuccessStatusCode(); - var depData = PackageData.Deserialize(depRequest.Content.ReadAsStream()); - foreach (var package in GenerateInternal(config, http, depData!, package => alreadyGottenPackages.Contains(package.Fullname!))) + var packageResponse = http.Send(config.Api.GetPackagesV1()); + packageResponse.EnsureSuccessStatusCode(); + using var responseReader = new StreamReader(packageResponse.Content.ReadAsStream()); + packagesJson = responseReader.ReadToEnd(); + File.WriteAllText(cachePath, packagesJson); + } + else + { + packagesJson = File.ReadAllText(cachePath); + } + + var packages = PackageListingV1.DeserializeList(packagesJson)!; + + HashSet visited = new(); + foreach (var originalDep in root.Dependencies!) + { + var match = InstallCommand.FullPackageNameRegex().Match(originalDep); + var fullname = match.Groups["fullname"].Value; + var depPackage = packages.Find(p => p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match, root.FullName); + if (depPackage == null) { - // this can happen on cyclical references, oh well - if (alreadyGottenPackages.Contains(package.Fullname!)) + continue; + } + foreach (var dependency in GenerateInner(packages, config, http, depPackage, p => visited.Contains(p.Fullname!))) + { + // can happen on cycles, oh well + if (visited.Contains(dependency.Fullname!)) + { continue; - - alreadyGottenPackages.Add(package.Fullname!); - yield return package; + } + visited.Add(dependency.Fullname!); + yield return dependency; } } } - private static IEnumerable GenerateInternal(Config config, HttpClient http, PackageData root, Predicate alreadyGotten) + + private static IEnumerable GenerateInner(List packages, Config config, HttpClient http, PackageListingV1 root, Predicate visited) { - if (alreadyGotten(root)) + if (visited(root)) + { yield break; + } - foreach (var dependency in root.LatestVersion!.Dependencies!) + foreach (var dependency in root.Versions!.First().Dependencies!) { - var depParts = dependency.Split('-'); - var depRequest = http.Send(config.Api.GetPackageMetadata(depParts[0], depParts[1])); - depRequest.EnsureSuccessStatusCode(); - var depData = PackageData.Deserialize(depRequest.Content.ReadAsStream()); - foreach (var package in GenerateInternal(config, http, depData!, alreadyGotten)) + var match = InstallCommand.FullPackageNameRegex().Match(dependency); + var fullname = match.Groups["fullname"].Value; + var package = packages.Find(p => p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match, root.Fullname!); + if (package == null) + { + continue; + } + foreach (var innerPackage in GenerateInner(packages, config, http, package, visited)) { - yield return package; + yield return innerPackage; } } + yield return root; } + + private static PackageListingV1? AttemptResolveExperimental(Config config, HttpClient http, Match nameMatch, string neededBy) + { + var response = http.Send(config.Api.GetPackageMetadata(nameMatch.Groups["namespace"].Value, nameMatch.Groups["name"].Value)); + if (response.StatusCode == HttpStatusCode.NotFound) + { + Write.Warn($"Failed to resolve dependency {nameMatch.Groups["fullname"].Value} for {neededBy}, continuing without it."); + return null; + } + response.EnsureSuccessStatusCode(); + using var reader = new StreamReader(response.Content.ReadAsStream()); + var data = PackageData.Deserialize(reader.ReadToEnd()); + + Write.Warn($"Package {data!.Fullname} (needed by {neededBy}) exists in different community, ignoring"); + return null; + } } diff --git a/tcli-bepinex-installer/src/main.rs b/tcli-bepinex-installer/src/main.rs index 0cb49fe..2df0504 100644 --- a/tcli-bepinex-installer/src/main.rs +++ b/tcli-bepinex-installer/src/main.rs @@ -46,8 +46,8 @@ pub enum Error { PathDoesNotExist(PathBuf), #[error("ZIP file does not contain a Thunderstore manifest")] NoZipManifest, - #[error("Invalid manifest in ZIP")] - InvalidManifest, + #[error("Invalid manifest in ZIP, serde_json error: {0}")] + InvalidManifest(serde_json::Error), #[error("Malformed zip")] MalformedZip, #[error("Manifest does not contain a namespace and no backup was given, namespaces are required for mod installs")] @@ -63,6 +63,7 @@ struct ManifestV1 { pub name: String, pub description: String, pub version_number: String, + #[serde(default)] pub dependencies: Vec, pub website_url: String, } @@ -119,12 +120,20 @@ fn install(game_dir: PathBuf, bep_dir: PathBuf, zip_path: PathBuf, namespace_bac bail!(Error::NoZipManifest); } - let manifest_file = zip.by_name("manifest.json")?; + let mut manifest_file = zip.by_name("manifest.json")?; + + let mut manifest_text = String::new(); + manifest_file.read_to_string(&mut manifest_text).unwrap(); + if manifest_text.starts_with('\u{FEFF}') { + manifest_text.remove(0); + } + + drop(manifest_file); let manifest: ManifestV1 = - serde_json::from_reader(manifest_file).map_err(|_| Error::InvalidManifest)?; + serde_json::from_str(&manifest_text).map_err(Error::InvalidManifest)?; - if manifest.name.starts_with("BepInEx") { + if manifest.name.starts_with("BepInEx") && zip.file_names().any(|f| f.ends_with("winhttp.dll")) { install_bepinex(game_dir, bep_dir, zip) } else { install_mod(bep_dir, zip, manifest, namespace_backup) @@ -216,6 +225,10 @@ fn install_mod( } let filepath = file.enclosed_name().ok_or(Error::MalformedZip)?.to_owned(); + let filename = match filepath.file_name() { + Some(name) => name, + None => continue + }; let mut out_path = None; 'outer: for remap in remaps.keys() { @@ -227,7 +240,7 @@ fn install_mod( } } if out_path.is_none() { - out_path = Some(default_remap.join(filepath.file_name().unwrap())); + out_path = Some(default_remap.join(filename)); } let full_out_path = bep_dir.join(out_path.unwrap()); From 20c71420556defe77b32cee9eef4aa9c413679b8 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 14 Nov 2022 09:22:40 -0600 Subject: [PATCH 38/91] Cache all community package.json lists, not just ror2 --- ThunderstoreCLI/Commands/InstallCommand.cs | 21 ++++++----- ThunderstoreCLI/Utils/ModDependencyTree.cs | 42 ++++++++++++---------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index 4984e40..dad751d 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -38,7 +38,7 @@ public static async Task Run(Config config) Match packageMatch; if (File.Exists(package)) { - returnCode = await InstallZip(config, http, def, profile, package, null); + returnCode = await InstallZip(config, http, def, profile, package, null, null); } else if ((packageMatch = FullPackageNameRegex().Match(package)).Success) { @@ -57,7 +57,7 @@ public static async Task Run(Config config) private static async Task InstallFromRepository(Config config, HttpClient http, GameDefinition game, ModProfile profile, Match packageMatch) { - PackageVersionData versionData; + PackageVersionData? versionData = null; Write.Light($"Downloading main package: {packageMatch.Groups["fullname"].Value}"); var ns = packageMatch.Groups["namespace"]; @@ -69,19 +69,18 @@ private static async Task InstallFromRepository(Config config, HttpClient h versionResponse.EnsureSuccessStatusCode(); versionData = (await PackageVersionData.DeserializeAsync(await versionResponse.Content.ReadAsStreamAsync()))!; } - else - { - var packageResponse = await http.SendAsync(config.Api.GetPackageMetadata(ns.Value, name.Value)); - packageResponse.EnsureSuccessStatusCode(); - versionData = (await PackageData.DeserializeAsync(await packageResponse.Content.ReadAsStreamAsync()))!.LatestVersion!; - } + var packageResponse = await http.SendAsync(config.Api.GetPackageMetadata(ns.Value, name.Value)); + packageResponse.EnsureSuccessStatusCode(); + var packageData = await PackageData.DeserializeAsync(await packageResponse.Content.ReadAsStreamAsync()); + + versionData ??= packageData!.LatestVersion!; var zipPath = await config.Cache.GetFileOrDownload($"{versionData.FullName}.zip", versionData.DownloadUrl!); - var returnCode = await InstallZip(config, http, game, profile, zipPath, versionData.Namespace!); + var returnCode = await InstallZip(config, http, game, profile, zipPath, versionData.Namespace!, packageData!.CommunityListings!.First().Community); return returnCode; } - private static async Task InstallZip(Config config, HttpClient http, GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace) + private static async Task InstallZip(Config config, HttpClient http, GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace, string? sourceCommunity) { using var zip = ZipFile.OpenRead(zipPath); var manifestFile = zip.GetEntry("manifest.json") ?? throw new CommandFatalException("Package zip needs a manifest.json!"); @@ -90,7 +89,7 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe manifest.Namespace ??= backupNamespace; - var dependenciesToInstall = ModDependencyTree.Generate(config, http, manifest) + var dependenciesToInstall = ModDependencyTree.Generate(config, http, manifest, sourceCommunity) .Where(dependency => !profile.InstalledModVersions.ContainsKey(dependency.Fullname!)) .ToArray(); diff --git a/ThunderstoreCLI/Utils/ModDependencyTree.cs b/ThunderstoreCLI/Utils/ModDependencyTree.cs index 4211ba0..a43ab28 100644 --- a/ThunderstoreCLI/Utils/ModDependencyTree.cs +++ b/ThunderstoreCLI/Utils/ModDependencyTree.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Net; using System.Text.RegularExpressions; using ThunderstoreCLI.Commands; @@ -9,31 +8,36 @@ namespace ThunderstoreCLI.Utils; public static class ModDependencyTree { - public static IEnumerable Generate(Config config, HttpClient http, PackageManifestV1 root) + public static IEnumerable Generate(Config config, HttpClient http, PackageManifestV1 root, string? sourceCommunity) { - var cachePath = Path.Combine(config.GeneralConfig.TcliConfig, "package-ror2.json"); - string packagesJson; - if (!File.Exists(cachePath) || new FileInfo(cachePath).LastWriteTime.AddMinutes(5) < DateTime.Now) - { - var packageResponse = http.Send(config.Api.GetPackagesV1()); - packageResponse.EnsureSuccessStatusCode(); - using var responseReader = new StreamReader(packageResponse.Content.ReadAsStream()); - packagesJson = responseReader.ReadToEnd(); - File.WriteAllText(cachePath, packagesJson); - } - else + List? packages = null; + + if (sourceCommunity != null) { - packagesJson = File.ReadAllText(cachePath); - } + var cachePath = Path.Combine(config.GeneralConfig.TcliConfig, $"package-{sourceCommunity}.json"); + string packagesJson; + if (!File.Exists(cachePath) || new FileInfo(cachePath).LastWriteTime.AddMinutes(5) < DateTime.Now) + { + var packageResponse = http.Send(config.Api.GetPackagesV1()); + packageResponse.EnsureSuccessStatusCode(); + using var responseReader = new StreamReader(packageResponse.Content.ReadAsStream()); + packagesJson = responseReader.ReadToEnd(); + File.WriteAllText(cachePath, packagesJson); + } + else + { + packagesJson = File.ReadAllText(cachePath); + } - var packages = PackageListingV1.DeserializeList(packagesJson)!; + packages = PackageListingV1.DeserializeList(packagesJson)!; + } HashSet visited = new(); foreach (var originalDep in root.Dependencies!) { var match = InstallCommand.FullPackageNameRegex().Match(originalDep); var fullname = match.Groups["fullname"].Value; - var depPackage = packages.Find(p => p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match, root.FullName); + var depPackage = packages?.Find(p => p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match, root.FullName); if (depPackage == null) { continue; @@ -51,7 +55,7 @@ public static IEnumerable Generate(Config config, HttpClient h } } - private static IEnumerable GenerateInner(List packages, Config config, HttpClient http, PackageListingV1 root, Predicate visited) + private static IEnumerable GenerateInner(List? packages, Config config, HttpClient http, PackageListingV1 root, Predicate visited) { if (visited(root)) { @@ -62,7 +66,7 @@ private static IEnumerable GenerateInner(List p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match, root.Fullname!); + var package = packages?.Find(p => p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match, root.Fullname!); if (package == null) { continue; From 3a52213ad9be6e901a4e6b0d9788aaa5dc9c6a44 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 14 Nov 2022 09:27:32 -0600 Subject: [PATCH 39/91] Fix weird incorrect MD5 logic --- ThunderstoreCLI/Commands/PublishCommand.cs | 36 +++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/ThunderstoreCLI/Commands/PublishCommand.cs b/ThunderstoreCLI/Commands/PublishCommand.cs index 5d5bed5..2ab0db3 100644 --- a/ThunderstoreCLI/Commands/PublishCommand.cs +++ b/ThunderstoreCLI/Commands/PublishCommand.cs @@ -1,5 +1,6 @@ using System.Net; using System.Security.Cryptography; +using System.Text; using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Models; using ThunderstoreCLI.Utils; @@ -207,14 +208,41 @@ private static void PublishPackageRequest(Config config, string uploadUuid) stream.Seek(part.Offset, SeekOrigin.Begin); byte[] hash; - using (var md5 = MD5.Create()) + var chunk = new MemoryStream(); + const int blocksize = 65536; + + using (var reader = new BinaryReader(stream, Encoding.Default, true)) { - hash = await md5.ComputeHashAsync(stream); + using (var md5 = MD5.Create()) + { + md5.Initialize(); + var length = part.Length; + + while (length > blocksize) + { + length -= blocksize; + var bytes = reader.ReadBytes(blocksize); + md5.TransformBlock(bytes, 0, blocksize, null, 0); + await chunk.WriteAsync(bytes); + } + + var finalBytes = reader.ReadBytes(length); + md5.TransformFinalBlock(finalBytes, 0, length); + + if (md5.Hash is null) + { + Write.ErrorExit($"MD5 hashing failed for part #{part.PartNumber})"); + throw new PublishCommandException(); + } + + hash = md5.Hash; + await chunk.WriteAsync(finalBytes); + chunk.Position = 0; + } } var request = new HttpRequestMessage(HttpMethod.Put, part.Url); - stream.Seek(part.Offset, SeekOrigin.Begin); - request.Content = new StreamContent(stream); + request.Content = new StreamContent(chunk); request.Content.Headers.ContentMD5 = hash; request.Content.Headers.ContentLength = part.Length; From 30fd3bd41b7f924e9a3ee837b411d09ccd9cd0b0 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Fri, 18 Nov 2022 04:01:25 -0600 Subject: [PATCH 40/91] Address review comments --- ThunderstoreCLI/Commands/ImportGameCommand.cs | 4 +- ThunderstoreCLI/Commands/InstallCommand.cs | 15 ++-- ThunderstoreCLI/Commands/PublishCommand.cs | 11 +-- ThunderstoreCLI/Commands/RunCommand.cs | 2 +- ThunderstoreCLI/Commands/UninstallCommand.cs | 4 +- ThunderstoreCLI/Configuration/Config.cs | 69 +++++++++---------- ThunderstoreCLI/Game/GameDefinition.cs | 22 +++--- ThunderstoreCLI/ThunderstoreCLI.csproj | 9 ++- ThunderstoreCLI/Utils/DownloadCache.cs | 6 +- ThunderstoreCLI/Utils/MiscUtils.cs | 13 ++++ ThunderstoreCLI/Utils/SteamUtils.cs | 22 ++---- 11 files changed, 77 insertions(+), 100 deletions(-) diff --git a/ThunderstoreCLI/Commands/ImportGameCommand.cs b/ThunderstoreCLI/Commands/ImportGameCommand.cs index e8560eb..8fe76a4 100644 --- a/ThunderstoreCLI/Commands/ImportGameCommand.cs +++ b/ThunderstoreCLI/Commands/ImportGameCommand.cs @@ -29,9 +29,9 @@ public static int Run(Config config) throw new CommandFatalException("Game not installed"); } - using GameDefintionCollection collection = GameDefintionCollection.FromDirectory(config.GeneralConfig.TcliConfig); + var collection = GameDefinitionCollection.FromDirectory(config.GeneralConfig.TcliConfig); collection.List.Add(def); - collection.Validate(); + collection.Write(); Write.Success($"Successfully imported {def.Name} ({def.Identifier}) with install folder \"{def.InstallDirectory}\""); diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index dad751d..e26a20e 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -18,7 +18,7 @@ public static partial class InstallCommand public static async Task Run(Config config) { - using var defCollection = GameDefintionCollection.FromDirectory(config.GeneralConfig.TcliConfig); + var defCollection = GameDefinitionCollection.FromDirectory(config.GeneralConfig.TcliConfig); var defs = defCollection.List; GameDefinition? def = defs.FirstOrDefault(x => x.Identifier == config.ModManagementConfig.GameIdentifer); if (def == null) @@ -50,7 +50,7 @@ public static async Task Run(Config config) } if (returnCode == 0) - defCollection.Validate(); + defCollection.Write(); return returnCode; } @@ -95,15 +95,8 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe if (dependenciesToInstall.Length > 0) { - double totalSize = dependenciesToInstall.Select(d => (double) d.Versions![0].FileSize).Sum(); - string[] suffixes = { "B", "KB", "MB", "GB", "TB" }; - int suffixIndex = 0; - while (totalSize >= 1024 && suffixIndex < suffixes.Length) - { - totalSize /= 1024; - suffixIndex++; - } - Write.Light($"Total estimated download size: {totalSize:F2} {suffixes[suffixIndex]}"); + var totalSize = MiscUtils.GetSizeString(dependenciesToInstall.Select(d => d.Versions![0].FileSize).Sum()); + Write.Light($"Total estimated download size: "); var downloadTasks = dependenciesToInstall.Select(mod => { diff --git a/ThunderstoreCLI/Commands/PublishCommand.cs b/ThunderstoreCLI/Commands/PublishCommand.cs index 2ab0db3..0c78956 100644 --- a/ThunderstoreCLI/Commands/PublishCommand.cs +++ b/ThunderstoreCLI/Commands/PublishCommand.cs @@ -161,16 +161,7 @@ private static UploadInitiateData InitiateUploadRequest(Config config, string fi throw new PublishCommandException(); } - string[] suffixes = { "B", "KB", "MB", "GB", "TB" }; - int suffixIndex = 0; - long size = uploadData.Metadata.Size; - while (size >= 1024 && suffixIndex < suffixes.Length) - { - size /= 1024; - suffixIndex++; - } - - var details = $"({size}{suffixes[suffixIndex]}) in {uploadData.UploadUrls.Length} chunks..."; + var details = $"({MiscUtils.GetSizeString(uploadData.Metadata.Size)}) in {uploadData.UploadUrls.Length} chunks..."; Write.WithNL($"Uploading {Cyan(uploadData.Metadata.Filename)} {details}", after: true); return uploadData; diff --git a/ThunderstoreCLI/Commands/RunCommand.cs b/ThunderstoreCLI/Commands/RunCommand.cs index 9f4d058..066e2e8 100644 --- a/ThunderstoreCLI/Commands/RunCommand.cs +++ b/ThunderstoreCLI/Commands/RunCommand.cs @@ -12,7 +12,7 @@ public static class RunCommand { public static int Run(Config config) { - GameDefintionCollection collection = GameDefintionCollection.FromDirectory(config.GeneralConfig.TcliConfig); + var collection = GameDefinitionCollection.FromDirectory(config.GeneralConfig.TcliConfig); var def = collection.FirstOrDefault(g => g.Identifier == config.RunGameConfig.GameName); if (def == null) diff --git a/ThunderstoreCLI/Commands/UninstallCommand.cs b/ThunderstoreCLI/Commands/UninstallCommand.cs index 016c371..ed57c79 100644 --- a/ThunderstoreCLI/Commands/UninstallCommand.cs +++ b/ThunderstoreCLI/Commands/UninstallCommand.cs @@ -10,7 +10,7 @@ public static class UninstallCommand { public static int Run(Config config) { - using var defCollection = GameDefintionCollection.FromDirectory(config.GeneralConfig.TcliConfig); + var defCollection = GameDefinitionCollection.FromDirectory(config.GeneralConfig.TcliConfig); GameDefinition? def = defCollection.FirstOrDefault(def => def.Identifier == config.ModManagementConfig.GameIdentifer); if (def == null) { @@ -99,7 +99,7 @@ public static int Run(Config config) throw new CommandFatalException($"The following mods failed to uninstall:\n{string.Join('\n', failedMods)}"); } - defCollection.Validate(); + defCollection.Write(); return 0; } diff --git a/ThunderstoreCLI/Configuration/Config.cs b/ThunderstoreCLI/Configuration/Config.cs index 951272e..0e6b2d3 100644 --- a/ThunderstoreCLI/Configuration/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -8,15 +8,15 @@ namespace ThunderstoreCLI.Configuration; public class Config { // ReSharper disable AutoPropertyCanBeMadeGetOnly.Local - public GeneralConfig GeneralConfig { get; private set; } - public PackageConfig PackageConfig { get; private set; } - public InitConfig InitConfig { get; private set; } - public BuildConfig BuildConfig { get; private set; } - public PublishConfig PublishConfig { get; private set; } - public AuthConfig AuthConfig { get; private set; } - public ModManagementConfig ModManagementConfig { get; private set; } - public GameImportConfig GameImportConfig { get; private set; } - public RunGameConfig RunGameConfig { get; private set; } + public required GeneralConfig GeneralConfig { get; init; } + public required PackageConfig PackageConfig { get; init; } + public required InitConfig InitConfig { get; init; } + public required BuildConfig BuildConfig { get; init; } + public required PublishConfig PublishConfig { get; init; } + public required AuthConfig AuthConfig { get; init; } + public required ModManagementConfig ModManagementConfig { get; init; } + public required GameImportConfig GameImportConfig { get; init; } + public required RunGameConfig RunGameConfig { get; init; } // ReSharper restore AutoPropertyCanBeMadeGetOnly.Local private readonly Lazy api; @@ -25,19 +25,10 @@ public class Config private readonly Lazy cache; public DownloadCache Cache => cache.Value; - private Config(GeneralConfig generalConfig, PackageConfig packageConfig, InitConfig initConfig, BuildConfig buildConfig, PublishConfig publishConfig, AuthConfig authConfig, ModManagementConfig modManagementConfig, GameImportConfig gameImportConfig, RunGameConfig runGameConfig) + private Config() { api = new Lazy(() => new ApiHelper(this)); cache = new Lazy(() => new DownloadCache(Path.Combine(GeneralConfig!.TcliConfig, "ModCache"))); - GeneralConfig = generalConfig; - PackageConfig = packageConfig; - InitConfig = initConfig; - BuildConfig = buildConfig; - PublishConfig = publishConfig; - AuthConfig = authConfig; - ModManagementConfig = modManagementConfig; - GameImportConfig = gameImportConfig; - RunGameConfig = runGameConfig; } public static Config FromCLI(IConfigProvider cliConfig) { @@ -120,28 +111,30 @@ public PackageUploadMetadata GetUploadMetadata(string fileUuid) public static Config Parse(IConfigProvider[] configProviders) { - var generalConfig = new GeneralConfig(); - var packageMeta = new PackageConfig(); - var initConfig = new InitConfig(); - var buildConfig = new BuildConfig(); - var publishConfig = new PublishConfig(); - var authConfig = new AuthConfig(); - var modManagementConfig = new ModManagementConfig(); - var gameImportConfig = new GameImportConfig(); - var runGameConfig = new RunGameConfig(); - var result = new Config(generalConfig, packageMeta, initConfig, buildConfig, publishConfig, authConfig, modManagementConfig, gameImportConfig, runGameConfig); + Config result = new() + { + GeneralConfig = new GeneralConfig(), + PackageConfig = new PackageConfig(), + InitConfig = new InitConfig(), + BuildConfig = new BuildConfig(), + PublishConfig = new PublishConfig(), + AuthConfig = new AuthConfig(), + ModManagementConfig = new ModManagementConfig(), + GameImportConfig = new GameImportConfig(), + RunGameConfig = new RunGameConfig(), + }; foreach (var provider in configProviders) { provider.Parse(result); - Merge(generalConfig, provider.GetGeneralConfig(), false); - Merge(packageMeta, provider.GetPackageMeta(), false); - Merge(initConfig, provider.GetInitConfig(), false); - Merge(buildConfig, provider.GetBuildConfig(), false); - Merge(publishConfig, provider.GetPublishConfig(), false); - Merge(authConfig, provider.GetAuthConfig(), false); - Merge(modManagementConfig, provider.GetModManagementConfig(), false); - Merge(gameImportConfig, provider.GetGameImportConfig(), false); - Merge(runGameConfig, provider.GetRunGameConfig(), false); + Merge(result.GeneralConfig, provider.GetGeneralConfig(), false); + Merge(result.PackageConfig, provider.GetPackageMeta(), false); + Merge(result.InitConfig, provider.GetInitConfig(), false); + Merge(result.BuildConfig, provider.GetBuildConfig(), false); + Merge(result.PublishConfig, provider.GetPublishConfig(), false); + Merge(result.AuthConfig, provider.GetAuthConfig(), false); + Merge(result.ModManagementConfig, provider.GetModManagementConfig(), false); + Merge(result.GameImportConfig, provider.GetGameImportConfig(), false); + Merge(result.RunGameConfig, provider.GetRunGameConfig(), false); } return result; } diff --git a/ThunderstoreCLI/Game/GameDefinition.cs b/ThunderstoreCLI/Game/GameDefinition.cs index 3837191..7b54739 100644 --- a/ThunderstoreCLI/Game/GameDefinition.cs +++ b/ThunderstoreCLI/Game/GameDefinition.cs @@ -41,17 +41,16 @@ internal GameDefinition(string id, string name, string installDirectory, GamePla } } -public sealed class GameDefintionCollection : IEnumerable, IDisposable +public sealed class GameDefinitionCollection : IEnumerable { - private const string FILE_NAME = "GameDefintions.json"; + private const string FILE_NAME = "GameDefinitions.json"; private readonly string tcliDirectory; - private bool shouldWrite = false; public List List { get; } - internal static GameDefintionCollection FromDirectory(string tcliDirectory) => new(tcliDirectory); + internal static GameDefinitionCollection FromDirectory(string tcliDirectory) => new(tcliDirectory); - private GameDefintionCollection(string tcliDir) + private GameDefinitionCollection(string tcliDir) { tcliDirectory = tcliDir; var filename = Path.Combine(tcliDirectory, FILE_NAME); @@ -61,18 +60,13 @@ private GameDefintionCollection(string tcliDir) List = new(); } - public void Validate() => shouldWrite = true; - - public IEnumerator GetEnumerator() => List.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => List.GetEnumerator(); - - public void Dispose() + public void Write() { - if (!shouldWrite) - return; File.WriteAllText(Path.Combine(tcliDirectory, FILE_NAME), List.SerializeList(BaseJson.IndentedSettings)); - shouldWrite = false; } + + public IEnumerator GetEnumerator() => List.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => List.GetEnumerator(); } public enum GamePlatform diff --git a/ThunderstoreCLI/ThunderstoreCLI.csproj b/ThunderstoreCLI/ThunderstoreCLI.csproj index e9ab141..3b04b80 100644 --- a/ThunderstoreCLI/ThunderstoreCLI.csproj +++ b/ThunderstoreCLI/ThunderstoreCLI.csproj @@ -23,7 +23,6 @@ true $(AssemblyName) enable - true @@ -77,8 +76,12 @@ - - + + true + + + true + diff --git a/ThunderstoreCLI/Utils/DownloadCache.cs b/ThunderstoreCLI/Utils/DownloadCache.cs index 26169ad..04fceec 100644 --- a/ThunderstoreCLI/Utils/DownloadCache.cs +++ b/ThunderstoreCLI/Utils/DownloadCache.cs @@ -28,17 +28,15 @@ public Task GetFileOrDownload(string filename, string downloadUrl) private async Task DownloadFile(string fullpath, string downloadUrl) { + var tempPath = fullpath + ".tmp"; int tryCount = 0; Retry: try { - var tempPath = fullpath + ".tmp"; // copy into memory first to prevent canceled downloads creating files on the disk await using FileStream tempStream = new(tempPath, FileMode.Create, FileAccess.Write); await using var downloadStream = await Client.GetStreamAsync(downloadUrl); await downloadStream.CopyToAsync(tempStream); - - File.Move(tempPath, fullpath); } catch { @@ -48,6 +46,8 @@ private async Task DownloadFile(string fullpath, string downloadUrl) } goto Retry; } + + File.Move(tempPath, fullpath); return fullpath; } } diff --git a/ThunderstoreCLI/Utils/MiscUtils.cs b/ThunderstoreCLI/Utils/MiscUtils.cs index 024f80c..84d3626 100644 --- a/ThunderstoreCLI/Utils/MiscUtils.cs +++ b/ThunderstoreCLI/Utils/MiscUtils.cs @@ -69,4 +69,17 @@ public static async Task FetchReleaseInformation() return await response.Content.ReadAsStringAsync(); } + + public static string GetSizeString(long byteSize) + { + double finalSize = byteSize; + string[] suffixes = { "B", "KB", "MB", "GB", "TB" }; + int suffixIndex = 0; + while (finalSize >= 1024 && suffixIndex < suffixes.Length) + { + finalSize /= 1024; + suffixIndex++; + } + return $"{byteSize:F2} {suffixes[suffixIndex]}"; + } } diff --git a/ThunderstoreCLI/Utils/SteamUtils.cs b/ThunderstoreCLI/Utils/SteamUtils.cs index 2d80644..f9d7bd9 100644 --- a/ThunderstoreCLI/Utils/SteamUtils.cs +++ b/ThunderstoreCLI/Utils/SteamUtils.cs @@ -232,28 +232,18 @@ public static bool ForceLoadProton(string steamAppId, string[] dllsToEnable) string[] lines = File.ReadAllLines(path); - int start = -1; - for (int i = 0; i < lines.Length; i++) - { - if (lines[i].StartsWith(@"[Software\\Wine\\DllOverrides]")) - { - start = i + 2; - break; - } - } + int start = Array.FindIndex(lines, l => l.StartsWith(@"[Software\\Wine\\DllOverrides]")); if (start == -1) { return false; } + start += 2; - int end = lines.Length - 1; - for (int i = start; i < lines.Length; i++) + int end = Array.FindIndex(lines, start, l => l.Length == 0); + + if (end == -1) { - if (lines[i].Length == 0) - { - end = i; - break; - } + end = lines.Length - 1; } bool written = false; From 3f96286e150fc4d550a04055e25d50370b03aac6 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sat, 26 Nov 2022 04:27:32 -0600 Subject: [PATCH 41/91] Fix multitargeting errors; further review comments --- ThunderstoreCLI/Commands/InstallCommand.cs | 7 +++-- ThunderstoreCLI/Utils/ActionUtils.cs | 30 ++++++++++++++++++++++ ThunderstoreCLI/Utils/DownloadCache.cs | 15 +++-------- ThunderstoreCLI/Utils/ModDependencyTree.cs | 4 +-- ThunderstoreCLI/Utils/SteamUtils.cs | 2 +- 5 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 ThunderstoreCLI/Utils/ActionUtils.cs diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index e26a20e..65f610c 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -13,8 +13,7 @@ namespace ThunderstoreCLI.Commands; public static partial class InstallCommand { // will match either ab-cd or ab-cd-123.456.7890 - [GeneratedRegex(@"^(?(?[\w-\.]+)-(?\w+))(?:|-(?\d+\.\d+\.\d+))$")] - internal static partial Regex FullPackageNameRegex(); + internal static Regex FullPackageNameRegex = new Regex(@"^(?(?[\w-\.]+)-(?\w+))(?:|-(?\d+\.\d+\.\d+))$"); public static async Task Run(Config config) { @@ -35,12 +34,12 @@ public static async Task Run(Config config) HttpClient http = new(); int returnCode; - Match packageMatch; + Match packageMatch = FullPackageNameRegex.Match(package); if (File.Exists(package)) { returnCode = await InstallZip(config, http, def, profile, package, null, null); } - else if ((packageMatch = FullPackageNameRegex().Match(package)).Success) + else if (packageMatch.Success) { returnCode = await InstallFromRepository(config, http, def, profile, packageMatch); } diff --git a/ThunderstoreCLI/Utils/ActionUtils.cs b/ThunderstoreCLI/Utils/ActionUtils.cs new file mode 100644 index 0000000..5d11c8f --- /dev/null +++ b/ThunderstoreCLI/Utils/ActionUtils.cs @@ -0,0 +1,30 @@ +namespace ThunderstoreCLI.Utils; + +public static class ActionUtils +{ + public static void Retry(int maxTryCount, Action action) + { + for (int i = 0; i < maxTryCount; i++) + { + try + { + action(); + return; + } + catch { } + } + } + + public static async Task RetryAsync(int maxTryCount, Func action) + { + for (int i = 0; i < maxTryCount; i++) + { + try + { + await action(); + return; + } + catch { } + } + } +} diff --git a/ThunderstoreCLI/Utils/DownloadCache.cs b/ThunderstoreCLI/Utils/DownloadCache.cs index 04fceec..dc3b936 100644 --- a/ThunderstoreCLI/Utils/DownloadCache.cs +++ b/ThunderstoreCLI/Utils/DownloadCache.cs @@ -29,23 +29,14 @@ public Task GetFileOrDownload(string filename, string downloadUrl) private async Task DownloadFile(string fullpath, string downloadUrl) { var tempPath = fullpath + ".tmp"; - int tryCount = 0; -Retry: - try + + await ActionUtils.RetryAsync(5, async () => { // copy into memory first to prevent canceled downloads creating files on the disk await using FileStream tempStream = new(tempPath, FileMode.Create, FileAccess.Write); await using var downloadStream = await Client.GetStreamAsync(downloadUrl); await downloadStream.CopyToAsync(tempStream); - } - catch - { - if (++tryCount == 5) - { - throw; - } - goto Retry; - } + }); File.Move(tempPath, fullpath); return fullpath; diff --git a/ThunderstoreCLI/Utils/ModDependencyTree.cs b/ThunderstoreCLI/Utils/ModDependencyTree.cs index a43ab28..0343736 100644 --- a/ThunderstoreCLI/Utils/ModDependencyTree.cs +++ b/ThunderstoreCLI/Utils/ModDependencyTree.cs @@ -35,7 +35,7 @@ public static IEnumerable Generate(Config config, HttpClient h HashSet visited = new(); foreach (var originalDep in root.Dependencies!) { - var match = InstallCommand.FullPackageNameRegex().Match(originalDep); + var match = InstallCommand.FullPackageNameRegex.Match(originalDep); var fullname = match.Groups["fullname"].Value; var depPackage = packages?.Find(p => p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match, root.FullName); if (depPackage == null) @@ -64,7 +64,7 @@ private static IEnumerable GenerateInner(List p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match, root.Fullname!); if (package == null) diff --git a/ThunderstoreCLI/Utils/SteamUtils.cs b/ThunderstoreCLI/Utils/SteamUtils.cs index f9d7bd9..fab73e3 100644 --- a/ThunderstoreCLI/Utils/SteamUtils.cs +++ b/ThunderstoreCLI/Utils/SteamUtils.cs @@ -225,7 +225,7 @@ public static bool IsProtonGame(string steamAppId) public static bool ForceLoadProton(string steamAppId, string[] dllsToEnable) { var path = Path.Combine(Path.GetDirectoryName(GetAcfPath(steamAppId))!, "compatdata", steamAppId, "pfx", "user.reg"); - if (!Path.Exists(path)) + if (!File.Exists(path)) { return false; } From 528ec084c70d223a04c75e4f1f9a766bbe58ec91 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sat, 26 Nov 2022 05:12:31 -0600 Subject: [PATCH 42/91] Hopefully last round of errors --- ThunderstoreCLI/API/ApiHelper.cs | 9 +++++++++ ThunderstoreCLI/Utils/ActionUtils.cs | 20 ++++++++++++++++---- ThunderstoreCLI/Utils/DownloadCache.cs | 4 ++++ ThunderstoreCLI/Utils/ModDependencyTree.cs | 2 +- ThunderstoreCLI/Utils/SteamUtils.cs | 8 ++++++-- 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/ThunderstoreCLI/API/ApiHelper.cs b/ThunderstoreCLI/API/ApiHelper.cs index ac77097..afd05ef 100644 --- a/ThunderstoreCLI/API/ApiHelper.cs +++ b/ThunderstoreCLI/API/ApiHelper.cs @@ -29,6 +29,7 @@ public ApiHelper(Config config) private const string V1 = "api/v1/"; private const string EXPERIMENTAL = "api/experimental/"; + private const string COMMUNITY = "c/"; public HttpRequestMessage SubmitPackage(string fileUuid) { @@ -97,6 +98,14 @@ public HttpRequestMessage GetPackagesV1() .GetRequest(); } + public HttpRequestMessage GetPackagesV1(string community) + { + return BaseRequestBuilder + .StartNew() + .WithEndpoint(COMMUNITY + community + "/api/v1/package/") + .GetRequest(); + } + private static string SerializeFileData(string filePath) { return new FileData() diff --git a/ThunderstoreCLI/Utils/ActionUtils.cs b/ThunderstoreCLI/Utils/ActionUtils.cs index 5d11c8f..2e61fc8 100644 --- a/ThunderstoreCLI/Utils/ActionUtils.cs +++ b/ThunderstoreCLI/Utils/ActionUtils.cs @@ -4,27 +4,39 @@ public static class ActionUtils { public static void Retry(int maxTryCount, Action action) { - for (int i = 0; i < maxTryCount; i++) + for (int i = 1; i <= maxTryCount; i++) { try { action(); return; } - catch { } + catch + { + if (i == maxTryCount) + { + throw; + } + } } } public static async Task RetryAsync(int maxTryCount, Func action) { - for (int i = 0; i < maxTryCount; i++) + for (int i = 1; i <= maxTryCount; i++) { try { await action(); return; } - catch { } + catch + { + if (i == maxTryCount) + { + throw; + } + } } } } diff --git a/ThunderstoreCLI/Utils/DownloadCache.cs b/ThunderstoreCLI/Utils/DownloadCache.cs index dc3b936..c1ae67a 100644 --- a/ThunderstoreCLI/Utils/DownloadCache.cs +++ b/ThunderstoreCLI/Utils/DownloadCache.cs @@ -12,6 +12,10 @@ public sealed class DownloadCache public DownloadCache(string cacheDirectory) { CacheDirectory = cacheDirectory; + if (!Directory.Exists(CacheDirectory)) + { + Directory.CreateDirectory(cacheDirectory); + } } // Task instead of ValueTask here because these Tasks will be await'd multiple times (ValueTask does not allow that) diff --git a/ThunderstoreCLI/Utils/ModDependencyTree.cs b/ThunderstoreCLI/Utils/ModDependencyTree.cs index 0343736..8d421f8 100644 --- a/ThunderstoreCLI/Utils/ModDependencyTree.cs +++ b/ThunderstoreCLI/Utils/ModDependencyTree.cs @@ -18,7 +18,7 @@ public static IEnumerable Generate(Config config, HttpClient h string packagesJson; if (!File.Exists(cachePath) || new FileInfo(cachePath).LastWriteTime.AddMinutes(5) < DateTime.Now) { - var packageResponse = http.Send(config.Api.GetPackagesV1()); + var packageResponse = http.Send(config.Api.GetPackagesV1(sourceCommunity)); packageResponse.EnsureSuccessStatusCode(); using var responseReader = new StreamReader(packageResponse.Content.ReadAsStream()); packagesJson = responseReader.ReadToEnd(); diff --git a/ThunderstoreCLI/Utils/SteamUtils.cs b/ThunderstoreCLI/Utils/SteamUtils.cs index fab73e3..d7f703b 100644 --- a/ThunderstoreCLI/Utils/SteamUtils.cs +++ b/ThunderstoreCLI/Utils/SteamUtils.cs @@ -29,8 +29,12 @@ public static bool IsProtonGame(string steamAppId) throw new ArgumentException($"{steamAppId} is not installed!"); } - var source = PlatformOverrideSourceRegex.Match(File.ReadAllText(path)).Groups[1].Value; - return source switch + var source = PlatformOverrideSourceRegex.Match(File.ReadAllText(path)); + if (!source.Success) + { + return Directory.Exists(Path.Combine(Path.GetDirectoryName(path)!, "compatdata", steamAppId)); + } + return source.Groups[1].Value switch { "" => false, "linux" => false, From 999cd024531417baa6280130cd11d40d74e7a24d Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sat, 3 Dec 2022 02:39:57 -0600 Subject: [PATCH 43/91] Disable compiling installers when running tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 52e5b17..cf6a83b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ jobs: - name: Build run: dotnet build --configuration Release --no-restore - name: Run xUnit tests - run: dotnet test --collect:"XPlat Code Coverage" + run: dotnet test -p:EnableInstallers=false --collect:"XPlat Code Coverage" - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 with: From 156918bb0f68695974e02e43470331c80e705774 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sat, 31 Dec 2022 04:21:03 -0600 Subject: [PATCH 44/91] Fix organization issues in default thunderstore.toml --- .../Configuration/ProjectFileConfig.cs | 2 +- ThunderstoreCLI/Models/ThunderstoreProject.cs | 45 +++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/ThunderstoreCLI/Configuration/ProjectFileConfig.cs b/ThunderstoreCLI/Configuration/ProjectFileConfig.cs index 2afec61..7056f28 100644 --- a/ThunderstoreCLI/Configuration/ProjectFileConfig.cs +++ b/ThunderstoreCLI/Configuration/ProjectFileConfig.cs @@ -41,7 +41,7 @@ public override void Parse(Config currentConfig) VersionNumber = Project.Package?.VersionNumber, ProjectConfigPath = SourcePath, Description = Project.Package?.Description, - Dependencies = Project.Package?.Dependencies, + Dependencies = Project.Package?.Dependencies.Wrapped, ContainsNsfwContent = Project.Package?.ContainsNsfwContent, WebsiteUrl = Project.Package?.WebsiteUrl }; diff --git a/ThunderstoreCLI/Models/ThunderstoreProject.cs b/ThunderstoreCLI/Models/ThunderstoreProject.cs index f39ab60..8566bf5 100644 --- a/ThunderstoreCLI/Models/ThunderstoreProject.cs +++ b/ThunderstoreCLI/Models/ThunderstoreProject.cs @@ -1,11 +1,48 @@ using ThunderstoreCLI.Configuration; +using Tomlet; using Tomlet.Attributes; +using Tomlet.Models; namespace ThunderstoreCLI.Models; [TomlDoNotInlineObject] public class ThunderstoreProject : BaseToml { + static ThunderstoreProject() + { + TomletMain.RegisterMapper( + instance => + { + var table = new TomlTable + { + ForceNoInline = true + }; + foreach (var (key, val) in instance!.Wrapped) + { + table.Entries.Add(key, new TomlString(val)); + } + return table; + }, + table => + { + var dict = new Dictionary(); + foreach (var (key, value) in ((TomlTable) table).Entries) + { + dict[key] = value.StringValue; + } + return new DictionaryWrapper + { + Wrapped = dict + }; + } + ); + } + + public class DictionaryWrapper + { + public required Dictionary Wrapped { get; init; } + } + [TomlDoNotInlineObject] public class ConfigData { @@ -31,10 +68,11 @@ public class PackageData public string WebsiteUrl { get; set; } = "https://thunderstore.io"; [TomlProperty("containsNsfwContent")] public bool ContainsNsfwContent { get; set; } = false; + [TomlProperty("dependencies")] - public Dictionary Dependencies { get; set; } = new() + public DictionaryWrapper Dependencies { get; set; } = new() { - { "AuthorName-PackageName", "0.0.1" } + Wrapped = new Dictionary() { { "AuthorName-PackageName", "0.0.1" } } }; } [TomlProperty("package")] @@ -58,8 +96,9 @@ public class CopyPath [TomlProperty("target")] public string Target { get; set; } = ""; } + [TomlProperty("copy")] - public CopyPath[] CopyPaths { get; set; } = Array.Empty(); + public CopyPath[] CopyPaths { get; set; } = new CopyPath[] { new CopyPath() }; } [TomlProperty("build")] public BuildData? Build { get; set; } From 95a3218fdc05f057167d19fdc14b9b035c091973 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Fri, 20 Jan 2023 00:27:16 -0600 Subject: [PATCH 45/91] Improve help messages --- ThunderstoreCLI/Commands/InstallCommand.cs | 2 +- ThunderstoreCLI/Options.cs | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index 65f610c..0eb6942 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -95,7 +95,7 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe if (dependenciesToInstall.Length > 0) { var totalSize = MiscUtils.GetSizeString(dependenciesToInstall.Select(d => d.Versions![0].FileSize).Sum()); - Write.Light($"Total estimated download size: "); + Write.Light($"Total estimated download size: {totalSize}"); var downloadTasks = dependenciesToInstall.Select(mod => { diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index d25fe8e..e998229 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -123,7 +123,7 @@ public override bool Validate() return false; } - if (!(File is null)) + if (File is not null) { var filePath = Path.GetFullPath(File); if (!System.IO.File.Exists(filePath)) @@ -147,13 +147,13 @@ public override int Execute() public abstract class ModManagementOptions : BaseOptions { - [Value(0, MetaName = "Game Name", Required = true, HelpText = "Can be any of: ror2, vrising, vrising_dedicated, vrising_builtin")] + [Value(0, MetaName = "Game Name", Required = true, HelpText = "The identifier of the game to manage mods for")] public string GameName { get; set; } = null!; - [Value(1, MetaName = "Package", Required = true, HelpText = "Path to package zip or package name in the format namespace-name")] + [Value(1, MetaName = "Package", Required = true, HelpText = "Path to package zip or package name in the format namespace-name(-version)")] public string Package { get; set; } = null!; - [Option(HelpText = "Profile to install to", Default = "Default")] + [Option(HelpText = "Profile to install to", Default = "DefaultProfile")] public string? Profile { get; set; } protected enum CommandInner @@ -176,19 +176,19 @@ public override int Execute() } } -[Verb("install")] +[Verb("install", HelpText = "Installs a mod to a profile")] public class InstallOptions : ModManagementOptions { protected override CommandInner CommandType => CommandInner.Install; } -[Verb("uninstall")] +[Verb("uninstall", HelpText = "Uninstalls a mod from a profile")] public class UninstallOptions : ModManagementOptions { protected override CommandInner CommandType => CommandInner.Uninstall; } -[Verb("import-game")] +[Verb("import-game", HelpText = "Imports a new game to use with TCLI")] public class GameImportOptions : BaseOptions { [Value(0, MetaName = "File Path", Required = true, HelpText = "Path to game description file to import")] @@ -211,13 +211,13 @@ public override int Execute() } } -[Verb("run")] +[Verb("run", HelpText = "Run a game modded")] public class RunGameOptions : BaseOptions { - [Value(0, MetaName = "Game Name", Required = true, HelpText = "Can be any of: ror2, vrising, vrising_dedicated, vrising_builtin")] + [Value(0, MetaName = "Game", Required = true, HelpText = "The identifier of the game to run.")] public required string GameName { get; set; } = null!; - [Option(HelpText = "Profile to install to", Default = "Default")] + [Option(HelpText = "Profile to install to", Default = "DefaultProfile")] public required string Profile { get; set; } public override int Execute() From 38a2489e50351b4ad1112688f4fc401f0ae79422 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Fri, 20 Jan 2023 00:27:47 -0600 Subject: [PATCH 46/91] Enable installers by default in Release --- .github/workflows/release.yml | 4 ++-- ThunderstoreCLI/ThunderstoreCLI.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f38d3b2..e141a4d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: shell: bash run: | release_name="tcli-${{ steps.tag.outputs.tag }}-${{ matrix.target }}" - dotnet publish ThunderstoreCLI/ThunderstoreCLI.csproj -c Release -r "${{ matrix.target }}" -f net7.0 -o "${release_name}" + dotnet publish ThunderstoreCLI/ThunderstoreCLI.csproj -c Release -r "${{ matrix.target }}" --self-contained true -f net7.0 -o "${release_name}" if [ "${{ matrix.target }}" == "win-x64" ]; then 7z a -tzip "${release_name}.zip" "./${release_name}" @@ -92,7 +92,7 @@ jobs: - name: Build shell: bash - run: dotnet pack ThunderstoreCLI/ThunderstoreCLI.csproj -c Release -o "." -p:PublishSelfContained=false -p:PublishSingleFile=false -p:PublishTrimmed=false -p:PublishReadyToRun=false + run: dotnet pack ThunderstoreCLI/ThunderstoreCLI.csproj -c Release -o "." -p:EnableInstallers=false -p:PublishSelfContained=false -p:PublishSingleFile=false -p:PublishTrimmed=false -p:PublishReadyToRun=false - name: Publish to GitHub uses: softprops/action-gh-release@v1 diff --git a/ThunderstoreCLI/ThunderstoreCLI.csproj b/ThunderstoreCLI/ThunderstoreCLI.csproj index 3b04b80..7075597 100644 --- a/ThunderstoreCLI/ThunderstoreCLI.csproj +++ b/ThunderstoreCLI/ThunderstoreCLI.csproj @@ -62,7 +62,7 @@ - + true From 6ba25462001b0eaa0896898fd5dca2d756fee834 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Fri, 20 Jan 2023 00:28:07 -0600 Subject: [PATCH 47/91] Add mod installation usage instructions to README --- README.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a32bd24..7a34836 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,61 @@ [![codecov](https://codecov.io/gh/thunderstore-io/thunderstore-cli/branch/master/graph/badge.svg)](https://codecov.io/gh/thunderstore-io/thunderstore-cli) -Command line tool for building and uploading mod packages to -[Thunderstore](https://thunderstore.io/) mod database. +Thunderstore CLI (just "TCLI" from here on) is a command line tool for building and uploading mod packages to +[Thunderstore](https://thunderstore.io/) mod database, and installing mods via the command line. -## pre-commit +## Installation +If all you're interested in is the package building/publishing capabilities of TCLI, then you can simply do: +``` +dotnet tool install -g tcli +``` +In your command line of choice, which will install the tool via NuGet. +This version doesn't come with the mod installation functionality however. + +Otherwise, just download the latest release from [here](https://github.com/thunderstore-io/thunderstore-cli/releases) and extract the ZIP wherever you'll be using TCLI. + +## Usage +For building packages, see [the wiki](https://github.com/thunderstore-io/thunderstore-cli/wiki). + +For managing mods via TCLI, see the next section. +## Mod Management + +### Installation +TCLI will automatically download and install mods in the correct format for you, and will run the game with whatever arguments your mod loader requires. + +For all of these commands, `tcli.exe` can be swapped for `./tcli` on Linux. Everything else should be the same. + +To get started, import your game from the Thunderstore API using: +``` +tcli.exe import-game {game identifier, e.g. ror2 or valheim} +``` +To install mods, use the command: +``` +tcli.exe install {game identifier} {namespace-modname(-version)} +``` +You can also add `--profile ProfileName` to set a custom name for the profile. By default it uses the name `DefaultProfile`. + +Mod uninstalls are done in a similar fashion: +``` +tcli.exe uninstall {game identifier} {namespace-modname} +``` +And running the game is done with: +``` +tcli.exe run {game identifier} +``` +The `--profile` snippet from above still applies to both of those commands. + +The installed mods by default will go into `%APPDATA%\ThunderstoreCLI` on Windows and `~/.config/ThunderstoreCLI` on Linux. This is configurable by using `--tcli-directory` with any command. + +The ThunderstoreCLI directory will contain a file called `GameDefintions.json`, which contains metadata about the configured games, the profiles for each game, and a copy of the manifests of each installed mod. You shouldn't normally be editing this manually. + +The same directory also contains a cache for mod ZIPS named `ModCache`, the actual profile files in a folder called `Profiles`, and a caches for the API responses from the Thunderstore API for packages. + +All in all, the structure is very similar to that of TMM/r2mm, but on the command line! + +## Contributing + +### pre-commit This project uses [Pre-commit](https://pre-commit.com/) to enforce code style practices. In addition to having .NET and pre-commit installed locally, you'll @@ -16,7 +67,7 @@ with: dotnet tool install -g dotnet-format ``` -## Versioning +### Versioning This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Versioning is handled with [MinVer](https://github.com/adamralph/minver) via Git From d5d47d8ed02efe51c437e7eb869bdfe24873050b Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Thu, 19 Jan 2023 21:37:42 -0600 Subject: [PATCH 48/91] Add native game installs --- README.md | 4 + ThunderstoreCLI/Commands/ImportGameCommand.cs | 23 +- ThunderstoreCLI/Commands/RunCommand.cs | 86 ++++-- .../Configuration/CLIParameterConfig.cs | 6 +- ThunderstoreCLI/Configuration/Config.cs | 4 +- ThunderstoreCLI/Game/GameDefinition.cs | 45 ++- ThunderstoreCLI/Models/R2mmGameDescription.cs | 31 --- ThunderstoreCLI/Models/SchemaResponse.cs | 52 ++++ ThunderstoreCLI/Options.cs | 18 +- tcli-bepinex-installer/Cargo.lock | 263 ++++++++++++------ tcli-bepinex-installer/src/main.rs | 144 ++++++++-- 11 files changed, 479 insertions(+), 197 deletions(-) delete mode 100644 ThunderstoreCLI/Models/R2mmGameDescription.cs create mode 100644 ThunderstoreCLI/Models/SchemaResponse.cs diff --git a/README.md b/README.md index 7a34836..8cd8eeb 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ To get started, import your game from the Thunderstore API using: ``` tcli.exe import-game {game identifier, e.g. ror2 or valheim} ``` +To run the game from a specific file instead of say, through Steam (you probably want this for servers!) use `--exepath {path/to/server/launcher.exe}`. Passing in a script file also works fine. + To install mods, use the command: ``` tcli.exe install {game identifier} {namespace-modname(-version)} @@ -46,6 +48,8 @@ tcli.exe run {game identifier} ``` The `--profile` snippet from above still applies to both of those commands. +If you want to run the game with a specific set of arguments, you can use `--args "--flag parameter1 parameter2"` + The installed mods by default will go into `%APPDATA%\ThunderstoreCLI` on Windows and `~/.config/ThunderstoreCLI` on Linux. This is configurable by using `--tcli-directory` with any command. The ThunderstoreCLI directory will contain a file called `GameDefintions.json`, which contains metadata about the configured games, the profiles for each game, and a copy of the manifests of each installed mod. You shouldn't normally be editing this manually. diff --git a/ThunderstoreCLI/Commands/ImportGameCommand.cs b/ThunderstoreCLI/Commands/ImportGameCommand.cs index 8fe76a4..45a6bfb 100644 --- a/ThunderstoreCLI/Commands/ImportGameCommand.cs +++ b/ThunderstoreCLI/Commands/ImportGameCommand.cs @@ -9,21 +9,20 @@ public static class ImportGameCommand { public static int Run(Config config) { - R2mmGameDescription? desc; - try - { - desc = R2mmGameDescription.Deserialize(File.ReadAllText(config.GameImportConfig.FilePath!)); - } - catch (Exception e) - { - throw new CommandFatalException($"Failed to read game description file: {e}"); - } - if (desc is null) + var http = new HttpClient(); + + var response = http.Send(new HttpRequestMessage(HttpMethod.Get, "https://gcdn.thunderstore.io/static/dev/schema/ecosystem-schema.0.0.2.json")); + + response.EnsureSuccessStatusCode(); + + var schema = SchemaResponse.Deserialize(response.Content.ReadAsStream())!; + + if (!schema.games.TryGetValue(config.GameImportConfig.GameId!, out var game)) { - throw new CommandFatalException("Game description file was empty"); + throw new CommandFatalException($"Could not find game with ID {config.GameImportConfig.GameId}"); } - var def = desc.ToGameDefintion(config); + var def = game.ToGameDefintion(config); if (def == null) { throw new CommandFatalException("Game not installed"); diff --git a/ThunderstoreCLI/Commands/RunCommand.cs b/ThunderstoreCLI/Commands/RunCommand.cs index 066e2e8..64d947d 100644 --- a/ThunderstoreCLI/Commands/RunCommand.cs +++ b/ThunderstoreCLI/Commands/RunCommand.cs @@ -27,6 +27,8 @@ public static int Run(Config config) throw new CommandFatalException($"No profile found with the name {config.RunGameConfig.ProfileName}"); } + var isSteam = def.Platform == GamePlatform.Steam; + ProcessStartInfo startInfo = new(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "tcli-bepinex-installer.exe" : "tcli-bepinex-installer") { ArgumentList = @@ -39,17 +41,17 @@ public static int Run(Config config) RedirectStandardError = true }; - var gameIsProton = SteamUtils.IsProtonGame(def.PlatformId); + var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + // TODO: Wine without Steam + var gameIsProton = isSteam && SteamUtils.IsProtonGame(def.PlatformId!); - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + startInfo.ArgumentList.Add("--game-platform"); + startInfo.ArgumentList.Add((isWindows, gameIsProton) switch { - startInfo.ArgumentList.Add("--game-platform"); - startInfo.ArgumentList.Add(gameIsProton switch - { - true => "windows", - false => "linux" - }); - } + (true, _) => "windows", + (false, true) => "proton", + (false, false) => "linux" + }); var installerProcess = Process.Start(startInfo)!; installerProcess.WaitForExit(); @@ -62,6 +64,7 @@ public static int Run(Config config) string runArguments = ""; string[] wineDlls = Array.Empty(); + List> environ = new(); string[] outputLines = installerProcess.StandardOutput.ReadToEnd().Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in outputLines) @@ -85,32 +88,65 @@ public static int Run(Config config) } wineDlls = args.Split(':'); break; + case "ENVIRONMENT": + var parts = args.Split('='); + environ.Add(new(parts[0], parts[1])); + break; } } - var steamExePath = SteamUtils.FindSteamExecutable(); - if (steamExePath == null) - { - throw new CommandFatalException("Couldn't find steam install directory!"); - } + var allArgs = string.Join(' ', runArguments, config.RunGameConfig.UserArguments); - if (gameIsProton && wineDlls.Length > 0) + if (isSteam) { - if (!SteamUtils.ForceLoadProton(def.PlatformId, wineDlls)) + var steamExePath = SteamUtils.FindSteamExecutable(); + if (steamExePath == null) { - throw new CommandFatalException($"No compat files could be found for app id {def.PlatformId}, please run the game at least once."); + throw new CommandFatalException("Couldn't find steam install directory!"); } - } - ProcessStartInfo runSteamInfo = new(steamExePath) + if (gameIsProton && wineDlls.Length > 0) + { + if (!SteamUtils.ForceLoadProton(def.PlatformId!, wineDlls)) + { + throw new CommandFatalException($"No compat files could be found for app id {def.PlatformId}, please run the game at least once."); + } + } + + ProcessStartInfo runSteamInfo = new(steamExePath) + { + Arguments = $"-applaunch {def.PlatformId} {allArgs}" + }; + + Write.Note($"Starting appid {def.PlatformId} with arguments: {allArgs}"); + var steamProcess = Process.Start(runSteamInfo)!; + steamProcess.WaitForExit(); + Write.Success($"Started game with appid {def.PlatformId}"); + } + else if (def.Platform == GamePlatform.Other) { - Arguments = $"-applaunch {def.PlatformId} {runArguments}" - }; + var exePath = def.ExePath!; + + if (!File.Exists(exePath)) + { + throw new CommandFatalException($"Executable {exePath} could not be found."); + } - Write.Note($"Starting appid {def.PlatformId} with arguments: {runArguments}"); - var steamProcess = Process.Start(runSteamInfo)!; - steamProcess.WaitForExit(); - Write.Success($"Started game with appid {def.PlatformId}"); + ProcessStartInfo process = new(exePath) + { + Arguments = allArgs, + WorkingDirectory = def.InstallDirectory, + }; + foreach (var (key, val) in environ) + { + Write.Line($"{key}: {val}"); + process.Environment.Add(key, val); + } + + Write.Note($"Starting {exePath} with arguments: {allArgs}"); + + Process.Start(process)!.WaitForExit(); + } return 0; } diff --git a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs index 851e760..18c9e5c 100644 --- a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs +++ b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs @@ -101,7 +101,8 @@ public GameImportCommandConfig(GameImportOptions options) : base(options) { } { return new GameImportConfig() { - FilePath = options.FilePath + ExePath = options.ExePath, + GameId = options.GameId, }; } } @@ -115,7 +116,8 @@ public RunGameCommandConfig(RunGameOptions options) : base(options) { } return new RunGameConfig() { GameName = options.GameName, - ProfileName = options.Profile + ProfileName = options.Profile, + UserArguments = options.Args, }; } } diff --git a/ThunderstoreCLI/Configuration/Config.cs b/ThunderstoreCLI/Configuration/Config.cs index 0e6b2d3..af7ba0d 100644 --- a/ThunderstoreCLI/Configuration/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -229,11 +229,13 @@ public class ModManagementConfig public class GameImportConfig { - public string? FilePath { get; set; } + public string? ExePath { get; set; } + public string? GameId { get; set; } } public class RunGameConfig { public string? GameName { get; set; } public string? ProfileName { get; set; } + public string? UserArguments { get; set; } } diff --git a/ThunderstoreCLI/Game/GameDefinition.cs b/ThunderstoreCLI/Game/GameDefinition.cs index 7b54739..d3047cb 100644 --- a/ThunderstoreCLI/Game/GameDefinition.cs +++ b/ThunderstoreCLI/Game/GameDefinition.cs @@ -1,4 +1,9 @@ using System.Collections; +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Models; using ThunderstoreCLI.Utils; @@ -9,15 +14,16 @@ public class GameDefinition : BaseJson public string Identifier { get; set; } public string Name { get; set; } public string InstallDirectory { get; set; } + public string? ExePath { get; set; } public GamePlatform Platform { get; set; } - public string PlatformId { get; set; } + public string? PlatformId { get; set; } public List Profiles { get; private set; } = new(); #pragma warning disable CS8618 private GameDefinition() { } #pragma warning restore CS8618 - internal GameDefinition(string id, string name, string installDirectory, GamePlatform platform, string platformId, string tcliDirectory) + internal GameDefinition(string id, string name, string installDirectory, GamePlatform platform, string? platformId, string tcliDirectory) { Identifier = id; Name = name; @@ -26,19 +32,37 @@ internal GameDefinition(string id, string name, string installDirectory, GamePla PlatformId = platformId; } - internal static GameDefinition? FromPlatformInstall(string tcliDir, GamePlatform platform, string platformId, string id, string name) + internal static GameDefinition? FromPlatformInstall(Config config, GamePlatform platform, string platformId, string id, string name) { var gameDir = platform switch { - GamePlatform.steam => SteamUtils.FindInstallDirectory(platformId), + GamePlatform.Steam => SteamUtils.FindInstallDirectory(platformId), _ => null }; if (gameDir == null) { return null; } - return new GameDefinition(id, name, gameDir, platform, platformId, tcliDir); + return new GameDefinition(id, name, gameDir, platform, platformId, config.GeneralConfig.TcliConfig); } + + internal static GameDefinition? FromNativeInstall(Config config, string id, string name) + { + if (!File.Exists(config.GameImportConfig.ExePath)) + { + return null; + } + + return new GameDefinition(id, name, Path.GetDirectoryName(Path.GetFullPath(config.GameImportConfig.ExePath))!, GamePlatform.Other, null, config.GeneralConfig.TcliConfig) + { + ExePath = Path.GetFullPath(config.GameImportConfig.ExePath) + }; + } + + [MemberNotNullWhen(true, nameof(ExePath))] + [MemberNotNullWhen(false, nameof(PlatformId))] + [JsonIgnore] + public bool IsNativeGame => Platform == GamePlatform.Other; } public sealed class GameDefinitionCollection : IEnumerable @@ -69,9 +93,14 @@ public void Write() IEnumerator IEnumerable.GetEnumerator() => List.GetEnumerator(); } +[JsonConverter(typeof(StringEnumConverter), typeof(KebabCaseNamingStrategy))] public enum GamePlatform { - steam, - egs, - other + Steam, + SteamDirect, + EGS, + XboxGamePass, + Oculus, + Origin, + Other } diff --git a/ThunderstoreCLI/Models/R2mmGameDescription.cs b/ThunderstoreCLI/Models/R2mmGameDescription.cs deleted file mode 100644 index 9f6996c..0000000 --- a/ThunderstoreCLI/Models/R2mmGameDescription.cs +++ /dev/null @@ -1,31 +0,0 @@ -using ThunderstoreCLI.Configuration; -using ThunderstoreCLI.Game; - -namespace ThunderstoreCLI.Models; - -public class R2mmGameDescription : BaseYaml -{ - public required string uuid { get; set; } - public required string label { get; set; } - public required DescriptionMetadata meta { get; set; } - public required PlatformDistribution[] distributions { get; set; } - public required object? legacy { get; set; } - - public GameDefinition? ToGameDefintion(Config config) - { - var platform = distributions.First(p => p.platform == GamePlatform.steam); - return GameDefinition.FromPlatformInstall(config.GeneralConfig.TcliConfig, platform.platform, platform.identifier, label, meta.displayName); - } -} - -public class PlatformDistribution -{ - public required GamePlatform platform { get; set; } - public required string identifier { get; set; } -} - -public class DescriptionMetadata -{ - public required string displayName { get; set; } - public required string iconUrl { get; set; } -} diff --git a/ThunderstoreCLI/Models/SchemaResponse.cs b/ThunderstoreCLI/Models/SchemaResponse.cs new file mode 100644 index 0000000..a2f9143 --- /dev/null +++ b/ThunderstoreCLI/Models/SchemaResponse.cs @@ -0,0 +1,52 @@ +using ThunderstoreCLI.Configuration; +using ThunderstoreCLI.Game; +using ThunderstoreCLI.Utils; + +namespace ThunderstoreCLI.Models; + +public class SchemaResponse : BaseJson +{ + public required Dictionary games { get; set; } +} + +public class SchemaGame +{ + public required string uuid { get; set; } + public required string label { get; set; } + public required DescriptionMetadata meta { get; set; } + public required PlatformDistribution[] distributions { get; set; } + public R2modmanInfo? r2modman { get; set; } + + public GameDefinition? ToGameDefintion(Config config) + { + var isServer = r2modman?.gameInstanceType == "server"; + var allowedDirectExe = distributions.Any(d => d.platform == GamePlatform.Other) || isServer; + if (allowedDirectExe && config.GameImportConfig.ExePath != null) + { + return GameDefinition.FromNativeInstall(config, label, meta.displayName); + } + if (!OperatingSystem.IsWindows()) + { + throw new CommandFatalException("Installs without passing --exepath are not yet supported on non-Windows"); + } + var platform = distributions.First(p => p.platform == GamePlatform.Steam); + return GameDefinition.FromPlatformInstall(config, platform.platform, platform.identifier, label, meta.displayName); + } +} + +public class PlatformDistribution +{ + public required GamePlatform platform { get; set; } + public required string identifier { get; set; } +} + +public class DescriptionMetadata +{ + public required string displayName { get; set; } + public required string iconUrl { get; set; } +} + +public class R2modmanInfo +{ + public required string gameInstanceType { get; set; } +} diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index e998229..4548802 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -191,14 +191,17 @@ public class UninstallOptions : ModManagementOptions [Verb("import-game", HelpText = "Imports a new game to use with TCLI")] public class GameImportOptions : BaseOptions { - [Value(0, MetaName = "File Path", Required = true, HelpText = "Path to game description file to import")] - public required string FilePath { get; set; } + [Option(HelpText = "Path to game exe to use when launching the game. Only works with servers.")] + public required string? ExePath { get; set; } + + [Value(0, Required = true, HelpText = "The identifier for the game to import.")] + public required string GameId { get; set; } public override bool Validate() { - if (!File.Exists(FilePath)) + if (!string.IsNullOrWhiteSpace(ExePath) && !File.Exists(ExePath)) { - Write.ErrorExit($"Could not locate game description file at {FilePath}"); + Write.ErrorExit($"Could not locate game exe at {ExePath}"); } return base.Validate(); @@ -215,11 +218,14 @@ public override int Execute() public class RunGameOptions : BaseOptions { [Value(0, MetaName = "Game", Required = true, HelpText = "The identifier of the game to run.")] - public required string GameName { get; set; } = null!; + public required string GameName { get; set; } - [Option(HelpText = "Profile to install to", Default = "DefaultProfile")] + [Option(HelpText = "Which profile to run the game under", Default = "DefaultProfile")] public required string Profile { get; set; } + [Option(HelpText = "Arguments to run the game with.")] + public string? Args { get; set; } + public override int Execute() { var config = Config.FromCLI(new RunGameCommandConfig(this)); diff --git a/tcli-bepinex-installer/Cargo.lock b/tcli-bepinex-installer/Cargo.lock index 3c9ee1d..272a2b7 100644 --- a/tcli-bepinex-installer/Cargo.lock +++ b/tcli-bepinex-installer/Cargo.lock @@ -22,20 +22,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.66" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "base64ct" @@ -66,9 +55,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bzip2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" dependencies = [ "bzip2-sys", "libc", @@ -87,9 +76,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.73" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" dependencies = [ "jobserver", ] @@ -111,14 +100,14 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.18" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335867764ed2de42325fafe6d18b8af74ba97ee0c590fa016f157535b42ab04b" +checksum = "4ec7a4128863c188deefe750ac1d1dfe66c236909f845af04beed823638dc1b2" dependencies = [ - "atty", "bitflags", "clap_derive", "clap_lex", + "is-terminal", "once_cell", "strsim", "termcolor", @@ -126,9 +115,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.0.18" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a1b0f6422af32d5da0c58e2703320f379216ee70198241c84173a8c5ac28f3" +checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" dependencies = [ "heck", "proc-macro-error", @@ -139,9 +128,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" dependencies = [ "os_str_bytes", ] @@ -172,9 +161,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if", ] @@ -191,20 +180,41 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", "subtle", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", "miniz_oxide", @@ -228,9 +238,9 @@ checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] @@ -245,10 +255,32 @@ dependencies = [ ] [[package]] -name = "itoa" +name = "io-lifetimes" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[package]] name = "jobserver" @@ -261,33 +293,30 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.137" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] -name = "miniz_oxide" -version = "0.5.4" +name = "linux-raw-sys" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" -dependencies = [ - "adler", -] +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] -name = "num_threads" -version = "0.1.6" +name = "miniz_oxide" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ - "libc", + "adler", ] [[package]] name = "once_cell" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "opaque-debug" @@ -297,9 +326,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "os_str_bytes" -version = "6.3.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "password-hash" @@ -356,18 +385,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] @@ -378,26 +407,40 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rustix" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" [[package]] name = "serde" -version = "1.0.147" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", @@ -406,9 +449,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.87" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" dependencies = [ "itoa", "ryu", @@ -451,9 +494,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", @@ -474,27 +517,27 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", @@ -503,13 +546,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ "itoa", - "libc", - "num_threads", "serde", "time-core", "time-macros", @@ -523,24 +564,24 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" [[package]] name = "time-macros" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bb801831d812c562ae7d2bfb531f26e66e4e1f6b17307ba4149c5064710e5b" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" dependencies = [ "time-core", ] [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "version_check" @@ -579,6 +620,63 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + [[package]] name = "zip" version = "0.6.3" @@ -620,10 +718,11 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.1+zstd.1.5.2" +version = "2.0.5+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b" +checksum = "edc50ffce891ad571e9f9afe5039c4837bede781ac4bb13052ed7ae695518596" dependencies = [ "cc", "libc", + "pkg-config", ] diff --git a/tcli-bepinex-installer/src/main.rs b/tcli-bepinex-installer/src/main.rs index 2df0504..92b2db0 100644 --- a/tcli-bepinex-installer/src/main.rs +++ b/tcli-bepinex-installer/src/main.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + env, ffi::OsString, fs::{self, OpenOptions}, io::{self, Read, Seek}, @@ -7,7 +8,7 @@ use std::{ }; use anyhow::{bail, Result}; -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, ValueEnum}; use serde::Deserialize; use zip::ZipArchive; @@ -36,8 +37,8 @@ enum Commands { game_directory: PathBuf, bepinex_directory: PathBuf, #[arg(long)] - game_platform: Option, - } + game_platform: GamePlatform, + }, } #[derive(Debug, thiserror::Error)] @@ -68,6 +69,13 @@ struct ManifestV1 { pub website_url: String, } +#[derive(Debug, Deserialize, Clone, Copy, ValueEnum)] +enum GamePlatform { + Windows, + Proton, + Linux, +} + fn main() -> Result<()> { let args = ClapArgs::parse(); @@ -87,7 +95,12 @@ fn main() -> Result<()> { if !zip_path.exists() { bail!(Error::PathDoesNotExist(zip_path)); } - install(game_directory, bepinex_directory, zip_path, namespace_backup) + install( + game_directory, + bepinex_directory, + zip_path, + namespace_backup, + ) } Commands::Uninstall { game_directory, @@ -103,17 +116,23 @@ fn main() -> Result<()> { uninstall(game_directory, bepinex_directory, name) } Commands::StartInstructions { + game_directory, bepinex_directory, game_platform, .. } => { - output_instructions(bepinex_directory, game_platform); + output_instructions(game_directory, bepinex_directory, game_platform); Ok(()) } } } -fn install(game_dir: PathBuf, bep_dir: PathBuf, zip_path: PathBuf, namespace_backup: Option) -> Result<()> { +fn install( + game_dir: PathBuf, + bep_dir: PathBuf, + zip_path: PathBuf, + namespace_backup: Option, +) -> Result<()> { let mut zip = ZipArchive::new(std::fs::File::open(zip_path)?)?; if !zip.file_names().any(|name| name == "manifest.json") { @@ -133,7 +152,8 @@ fn install(game_dir: PathBuf, bep_dir: PathBuf, zip_path: PathBuf, namespace_bac let manifest: ManifestV1 = serde_json::from_str(&manifest_text).map_err(Error::InvalidManifest)?; - if manifest.name.starts_with("BepInEx") && zip.file_names().any(|f| f.ends_with("winhttp.dll")) { + if manifest.name.starts_with("BepInEx") && zip.file_names().any(|f| f.ends_with("winhttp.dll")) + { install_bepinex(game_dir, bep_dir, zip) } else { install_mod(bep_dir, zip, manifest, namespace_backup) @@ -145,7 +165,11 @@ fn install_bepinex( bep_dir: PathBuf, mut zip: ZipArchive, ) -> Result<()> { - let write_opts = OpenOptions::new().write(true).create(true).clone(); + let write_opts = { + let mut opts = OpenOptions::new(); + opts.write(true).create(true); + opts + }; for i in 0..zip.len() { let mut file = zip.by_index(i)?; @@ -163,15 +187,14 @@ fn install_bepinex( continue; } - let dir_to_use = if filepath.ancestors().any(|part| { + let in_bep_folder = filepath.ancestors().any(|part| { part.file_name() .unwrap_or(&OsString::new()) - .to_string_lossy() == "BepInEx" - }) { - &bep_dir - } else { - &game_dir - }; + .to_string_lossy() + == "BepInEx" + }); + + let dir_to_use = if in_bep_folder { &bep_dir } else { &game_dir }; // this removes the BepInExPack*/ from the path let resolved_path = remove_first_n_directories(&filepath, 1); @@ -196,7 +219,10 @@ fn install_mod( let full_name = format!( "{}-{}", - manifest.namespace.or(namespace_backup).ok_or(Error::MissingNamespace)?, + manifest + .namespace + .or(namespace_backup) + .ok_or(Error::MissingNamespace)?, manifest.name ); @@ -213,7 +239,10 @@ fn install_mod( Path::new("BepInEx").join("monomod"), Path::new("BepInEx").join("monomod").join(&full_name), ); - remaps.insert(Path::new("BepInEx").join("config"), Path::new("BepInEx").join("config")); + remaps.insert( + Path::new("BepInEx").join("config"), + Path::new("BepInEx").join("config"), + ); let default_remap = &remaps[&Path::new("BepInEx").join("plugins")]; @@ -227,7 +256,7 @@ fn install_mod( let filepath = file.enclosed_name().ok_or(Error::MalformedZip)?.to_owned(); let filename = match filepath.file_name() { Some(name) => name, - None => continue + None => continue, }; let mut out_path = None; @@ -253,7 +282,12 @@ fn install_mod( } fn uninstall(game_dir: PathBuf, bep_dir: PathBuf, name: String) -> Result<()> { - if name.split_once('-').ok_or(Error::InvalidModName)?.1.starts_with("BepInEx") { + if name + .split_once('-') + .ok_or(Error::InvalidModName)? + .1 + .starts_with("BepInEx") + { uninstall_bepinex(game_dir, bep_dir) } else { uninstall_mod(bep_dir, name) @@ -279,22 +313,72 @@ fn uninstall_mod(bep_dir: PathBuf, name: String) -> Result<()> { Ok(()) } -fn output_instructions(bep_dir: PathBuf, platform: Option) { - if platform.as_ref().map(|p| p == "windows").unwrap_or(true) { - let drive_prefix = match platform { - Some(_) => "Z:", - None => "" - }; - - println!("ARGUMENTS:--doorstop-enable true --doorstop-target {}{}", drive_prefix, bep_dir.join("BepInEx").join("core").join("BepInEx.Preloader.dll").to_string_lossy().replace('/', "\\")); - println!("WINEDLLOVERRIDE:winhttp") +fn output_instructions(game_dir: PathBuf, bep_dir: PathBuf, platform: GamePlatform) { + let drive_prefix = if matches!(platform, GamePlatform::Proton) { + "Z:\\" } else { - eprintln!("native linux not implmented"); + "" + }; + + let bep_preloader_dll = bep_dir + .join("BepInEx") + .join("core") + .join("BepInEx.Preloader.dll") + .to_string_lossy() + .into_owned(); + + match platform { + GamePlatform::Windows | GamePlatform::Proton => { + println!( + "ARGUMENTS:--doorstop-enable true --doorstop-target {}{}", + drive_prefix, + bep_preloader_dll.replace('/', "\\") + ); + println!("WINEDLLOVERRIDE:winhttp"); + } + GamePlatform::Linux => { + println!("ENVIRONMENT:DOORSTOP_ENABLE=TRUE"); + println!("ENVIRONMENT:DOORSTOP_INVOKE_DLL_PATH={}", bep_preloader_dll); + println!( + "ENVIRONMENT:DOORSTOP_CORLIB_OVERRIDE_PATH={}", + game_dir.join("unstripped_corlib").to_string_lossy() + ); + + let mut ld_library = OsString::from(game_dir.join("doorstop_libs")); + match env::var_os("LD_LIBRARY") { + Some(orig) => { + ld_library.push(":"); + ld_library.push(orig); + } + None => {} + } + + println!("ENVIRONMENT:LD_LIBRARY={}", ld_library.to_string_lossy()); + + let mut ld_preload = OsString::from(game_dir.join("doorstop_libs").join({ + if cfg!(target_arch = "x86_64") { + "libdoorstop_x64.so" + } else { + "libdoorstop_x86.so" + } + })); + match env::var_os("LD_PRELOAD") { + Some(orig) => { + ld_preload.push(":"); + ld_preload.push(orig); + } + None => {} + } + + println!("ENVIRONMENT:LD_PRELOAD={}", ld_preload.to_string_lossy()); + } } } fn top_level_directory_name(path: &Path) -> Option<&str> { - path.components().next().and_then(|n| n.as_os_str().to_str()) + path.components() + .next() + .and_then(|n| n.as_os_str().to_str()) } /// removes the first n directories from a path, eg a/b/c/d.txt with an n of 2 gives c/d.txt From b34933a2a02afbd03215083d0231a894cb1a4a62 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sun, 22 Jan 2023 01:21:33 -0600 Subject: [PATCH 49/91] Add FIXME to LD_PRELOAD handler in the BepInEx installer for later --- tcli-bepinex-installer/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tcli-bepinex-installer/src/main.rs b/tcli-bepinex-installer/src/main.rs index 92b2db0..aea9e8f 100644 --- a/tcli-bepinex-installer/src/main.rs +++ b/tcli-bepinex-installer/src/main.rs @@ -356,6 +356,7 @@ fn output_instructions(game_dir: PathBuf, bep_dir: PathBuf, platform: GamePlatfo println!("ENVIRONMENT:LD_LIBRARY={}", ld_library.to_string_lossy()); let mut ld_preload = OsString::from(game_dir.join("doorstop_libs").join({ + // FIXME: properly determine arch of the game exe, instead of assuming its the same as this exe if cfg!(target_arch = "x86_64") { "libdoorstop_x64.so" } else { From 3fda0876e9d5463971a4db093bf5a7fbaab2e8ae Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sun, 22 Jan 2023 22:24:16 -0600 Subject: [PATCH 50/91] Add rustfmt, fix clippy lints --- tcli-bepinex-installer/rustfmt.toml | 4 ++++ tcli-bepinex-installer/src/main.rs | 18 ++++++------------ 2 files changed, 10 insertions(+), 12 deletions(-) create mode 100644 tcli-bepinex-installer/rustfmt.toml diff --git a/tcli-bepinex-installer/rustfmt.toml b/tcli-bepinex-installer/rustfmt.toml new file mode 100644 index 0000000..435a6ab --- /dev/null +++ b/tcli-bepinex-installer/rustfmt.toml @@ -0,0 +1,4 @@ +unstable_features = true + +group_imports = "StdExternalCrate" +imports_layout = "HorizontalVertical" diff --git a/tcli-bepinex-installer/src/main.rs b/tcli-bepinex-installer/src/main.rs index aea9e8f..546de59 100644 --- a/tcli-bepinex-installer/src/main.rs +++ b/tcli-bepinex-installer/src/main.rs @@ -345,12 +345,9 @@ fn output_instructions(game_dir: PathBuf, bep_dir: PathBuf, platform: GamePlatfo ); let mut ld_library = OsString::from(game_dir.join("doorstop_libs")); - match env::var_os("LD_LIBRARY") { - Some(orig) => { - ld_library.push(":"); - ld_library.push(orig); - } - None => {} + if let Some(orig) = env::var_os("LD_LIBRARY") { + ld_library.push(":"); + ld_library.push(orig); } println!("ENVIRONMENT:LD_LIBRARY={}", ld_library.to_string_lossy()); @@ -363,12 +360,9 @@ fn output_instructions(game_dir: PathBuf, bep_dir: PathBuf, platform: GamePlatfo "libdoorstop_x86.so" } })); - match env::var_os("LD_PRELOAD") { - Some(orig) => { - ld_preload.push(":"); - ld_preload.push(orig); - } - None => {} + if let Some(orig) = env::var_os("LD_PRELOAD") { + ld_preload.push(":"); + ld_preload.push(orig); } println!("ENVIRONMENT:LD_PRELOAD={}", ld_preload.to_string_lossy()); From efe272ea45bcca6c0b91302e80583b2b6ac5eb4d Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 31 Jan 2023 20:18:24 -0600 Subject: [PATCH 51/91] Args delimiter and chores --- .../ThunderstoreCLI.Tests.csproj | 15 ++++++------- ThunderstoreCLI/Commands/InstallCommand.cs | 8 ++++--- ThunderstoreCLI/Commands/RunCommand.cs | 1 - ThunderstoreCLI/Models/BaseYaml.cs | 22 ------------------- ThunderstoreCLI/Models/SchemaResponse.cs | 4 ---- ThunderstoreCLI/Options.cs | 2 +- ThunderstoreCLI/Program.cs | 18 ++++++++++++++- ThunderstoreCLI/ThunderstoreCLI.csproj | 13 +++++------ 8 files changed, 36 insertions(+), 47 deletions(-) delete mode 100644 ThunderstoreCLI/Models/BaseYaml.cs diff --git a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj index 13b2e78..9c4f2a8 100644 --- a/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj +++ b/ThunderstoreCLI.Tests/ThunderstoreCLI.Tests.csproj @@ -8,23 +8,22 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index 0eb6942..07bc894 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -13,7 +13,7 @@ namespace ThunderstoreCLI.Commands; public static partial class InstallCommand { // will match either ab-cd or ab-cd-123.456.7890 - internal static Regex FullPackageNameRegex = new Regex(@"^(?(?[\w-\.]+)-(?\w+))(?:|-(?\d+\.\d+\.\d+))$"); + internal static readonly Regex FullPackageNameRegex = new Regex(@"^(?(?[\w-\.]+)-(?\w+))(?:|-(?\d+\.\d+\.\d+))$"); public static async Task Run(Config config) { @@ -65,11 +65,13 @@ private static async Task InstallFromRepository(Config config, HttpClient h if (version.Success) { var versionResponse = await http.SendAsync(config.Api.GetPackageVersionMetadata(ns.Value, name.Value, version.Value)); - versionResponse.EnsureSuccessStatusCode(); + if (!versionResponse.IsSuccessStatusCode) + throw new CommandFatalException($"Couldn't find version {version} of mod {ns}-{name}"); versionData = (await PackageVersionData.DeserializeAsync(await versionResponse.Content.ReadAsStreamAsync()))!; } var packageResponse = await http.SendAsync(config.Api.GetPackageMetadata(ns.Value, name.Value)); - packageResponse.EnsureSuccessStatusCode(); + if (!packageResponse.IsSuccessStatusCode) + throw new CommandFatalException($"Could not find package with the name {ns}-{name}"); var packageData = await PackageData.DeserializeAsync(await packageResponse.Content.ReadAsStreamAsync()); versionData ??= packageData!.LatestVersion!; diff --git a/ThunderstoreCLI/Commands/RunCommand.cs b/ThunderstoreCLI/Commands/RunCommand.cs index 64d947d..d3b7de2 100644 --- a/ThunderstoreCLI/Commands/RunCommand.cs +++ b/ThunderstoreCLI/Commands/RunCommand.cs @@ -4,7 +4,6 @@ using ThunderstoreCLI.Configuration; using ThunderstoreCLI.Game; using ThunderstoreCLI.Utils; -using YamlDotNet.Core.Tokens; namespace ThunderstoreCLI.Commands; diff --git a/ThunderstoreCLI/Models/BaseYaml.cs b/ThunderstoreCLI/Models/BaseYaml.cs deleted file mode 100644 index dc9e474..0000000 --- a/ThunderstoreCLI/Models/BaseYaml.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using YamlDotNet.Serialization; - -namespace ThunderstoreCLI.Models; - -public abstract class BaseYaml<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : ISerialize where T : BaseYaml -{ - public string Serialize() - { - return BaseYamlHelper.Serializer.Serialize(this); - } - public static T? Deserialize(string input) - { - return BaseYamlHelper.Deserializer.Deserialize(input); - } -} - -file static class BaseYamlHelper -{ - public static readonly Serializer Serializer = new(); - public static readonly Deserializer Deserializer = new(); -} diff --git a/ThunderstoreCLI/Models/SchemaResponse.cs b/ThunderstoreCLI/Models/SchemaResponse.cs index a2f9143..ab607d8 100644 --- a/ThunderstoreCLI/Models/SchemaResponse.cs +++ b/ThunderstoreCLI/Models/SchemaResponse.cs @@ -25,10 +25,6 @@ public class SchemaGame { return GameDefinition.FromNativeInstall(config, label, meta.displayName); } - if (!OperatingSystem.IsWindows()) - { - throw new CommandFatalException("Installs without passing --exepath are not yet supported on non-Windows"); - } var platform = distributions.First(p => p.platform == GamePlatform.Steam); return GameDefinition.FromPlatformInstall(config, platform.platform, platform.identifier, label, meta.displayName); } diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index 4548802..23bb978 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -223,7 +223,7 @@ public class RunGameOptions : BaseOptions [Option(HelpText = "Which profile to run the game under", Default = "DefaultProfile")] public required string Profile { get; set; } - [Option(HelpText = "Arguments to run the game with.")] + [Option(HelpText = "Arguments to run the game with. Anything after a trailing -- will be prioritized over this argument.")] public string? Args { get; set; } public override int Execute() diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index 6e99f00..e069ef3 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -15,6 +15,15 @@ private static int Main(string[] args) { } #endif + string? trailingArgs = null; + + var argsDelimeterIndex = Array.IndexOf(args, "--"); + if (argsDelimeterIndex != -1) + { + trailingArgs = string.Join(' ', args, argsDelimeterIndex + 1, args.Length - argsDelimeterIndex); + args = args[..argsDelimeterIndex]; + } + var updateChecker = UpdateChecker.CheckForUpdates(); var exitCode = Parser.Default.ParseArguments HandleParsed(o), (UninstallOptions o) => HandleParsed(o), (GameImportOptions o) => HandleParsed(o), - (RunGameOptions o) => HandleParsed(o), + (RunGameOptions o) => + { + if (trailingArgs != null) + { + o.Args = trailingArgs; + } + return HandleParsed(o); + }, #endif _ => 1 // failure to parse ); diff --git a/ThunderstoreCLI/ThunderstoreCLI.csproj b/ThunderstoreCLI/ThunderstoreCLI.csproj index 7075597..93100d1 100644 --- a/ThunderstoreCLI/ThunderstoreCLI.csproj +++ b/ThunderstoreCLI/ThunderstoreCLI.csproj @@ -26,20 +26,19 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + From 1054d1db91a368e4a4e7de167b1567f81e6a8875 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 31 Jan 2023 21:35:05 -0600 Subject: [PATCH 52/91] Add a primative list command --- ThunderstoreCLI/Commands/ListCommand.cs | 30 +++++++++++++++++++ .../Configuration/CLIParameterConfig.cs | 5 ++++ ThunderstoreCLI/Options.cs | 10 +++++++ ThunderstoreCLI/Program.cs | 3 +- 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 ThunderstoreCLI/Commands/ListCommand.cs diff --git a/ThunderstoreCLI/Commands/ListCommand.cs b/ThunderstoreCLI/Commands/ListCommand.cs new file mode 100644 index 0000000..a2afd10 --- /dev/null +++ b/ThunderstoreCLI/Commands/ListCommand.cs @@ -0,0 +1,30 @@ +using ThunderstoreCLI.Configuration; +using ThunderstoreCLI.Game; +using ThunderstoreCLI.Utils; + +namespace ThunderstoreCLI.Commands; + +public static class ListCommand +{ + public static void Run(Config config) + { + var collection = GameDefinitionCollection.FromDirectory(config.GeneralConfig.TcliConfig); + + foreach (var game in collection) + { + Write.Line($"{game.Identifier}:"); + Write.Line($" Name: {game.Name}"); + Write.Line($" Path: {game.InstallDirectory}"); + Write.Line($" Profiles:"); + foreach (var profile in game.Profiles) + { + Write.Line($" Name: {profile.Name}"); + Write.Line($" Mods:"); + foreach (var mod in profile.InstalledModVersions.Values) + { + Write.Line($" {mod.FullName}-{mod.VersionNumber}"); + } + } + } + } +} diff --git a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs index 18c9e5c..759873e 100644 --- a/ThunderstoreCLI/Configuration/CLIParameterConfig.cs +++ b/ThunderstoreCLI/Configuration/CLIParameterConfig.cs @@ -121,3 +121,8 @@ public RunGameCommandConfig(RunGameOptions options) : base(options) { } }; } } + +public class ListConfig : BaseConfig +{ + public ListConfig(ListOptions options) : base(options) { } +} diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index 4548802..7521c24 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -232,3 +232,13 @@ public override int Execute() return RunCommand.Run(config); } } + +[Verb("list", HelpText = "List configured games, profiles, and mods")] +public class ListOptions : BaseOptions +{ + public override int Execute() + { + ListCommand.Run(Config.FromCLI(new ListConfig(this))); + return 0; + } +} diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index 6e99f00..14790c8 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -18,7 +18,7 @@ private static int Main(string[] args) var updateChecker = UpdateChecker.CheckForUpdates(); var exitCode = Parser.Default.ParseArguments(args) .MapResult( @@ -30,6 +30,7 @@ private static int Main(string[] args) (UninstallOptions o) => HandleParsed(o), (GameImportOptions o) => HandleParsed(o), (RunGameOptions o) => HandleParsed(o), + (ListOptions o) => HandleParsed(o), #endif _ => 1 // failure to parse ); From 3ad1433e3f210ebd799cbfb7b7eb535ddd0175e5 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Wed, 1 Feb 2023 05:19:54 -0600 Subject: [PATCH 53/91] Remove ThunderstoreProject formatting hack, fixed in Tomlet 5.1.2 --- .../Configuration/ProjectFileConfig.cs | 2 +- ThunderstoreCLI/Models/ThunderstoreProject.cs | 42 +------------------ 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/ThunderstoreCLI/Configuration/ProjectFileConfig.cs b/ThunderstoreCLI/Configuration/ProjectFileConfig.cs index 7056f28..2afec61 100644 --- a/ThunderstoreCLI/Configuration/ProjectFileConfig.cs +++ b/ThunderstoreCLI/Configuration/ProjectFileConfig.cs @@ -41,7 +41,7 @@ public override void Parse(Config currentConfig) VersionNumber = Project.Package?.VersionNumber, ProjectConfigPath = SourcePath, Description = Project.Package?.Description, - Dependencies = Project.Package?.Dependencies.Wrapped, + Dependencies = Project.Package?.Dependencies, ContainsNsfwContent = Project.Package?.ContainsNsfwContent, WebsiteUrl = Project.Package?.WebsiteUrl }; diff --git a/ThunderstoreCLI/Models/ThunderstoreProject.cs b/ThunderstoreCLI/Models/ThunderstoreProject.cs index 8566bf5..2b7bbba 100644 --- a/ThunderstoreCLI/Models/ThunderstoreProject.cs +++ b/ThunderstoreCLI/Models/ThunderstoreProject.cs @@ -8,41 +8,6 @@ namespace ThunderstoreCLI.Models; [TomlDoNotInlineObject] public class ThunderstoreProject : BaseToml { - static ThunderstoreProject() - { - TomletMain.RegisterMapper( - instance => - { - var table = new TomlTable - { - ForceNoInline = true - }; - foreach (var (key, val) in instance!.Wrapped) - { - table.Entries.Add(key, new TomlString(val)); - } - return table; - }, - table => - { - var dict = new Dictionary(); - foreach (var (key, value) in ((TomlTable) table).Entries) - { - dict[key] = value.StringValue; - } - return new DictionaryWrapper - { - Wrapped = dict - }; - } - ); - } - - public class DictionaryWrapper - { - public required Dictionary Wrapped { get; init; } - } - [TomlDoNotInlineObject] public class ConfigData { @@ -68,12 +33,9 @@ public class PackageData public string WebsiteUrl { get; set; } = "https://thunderstore.io"; [TomlProperty("containsNsfwContent")] public bool ContainsNsfwContent { get; set; } = false; - [TomlProperty("dependencies")] - public DictionaryWrapper Dependencies { get; set; } = new() - { - Wrapped = new Dictionary() { { "AuthorName-PackageName", "0.0.1" } } - }; + [TomlDoNotInlineObject] + public Dictionary Dependencies { get; set; } = new() { { "AuthorName-PackageName", "0.0.1" } }; } [TomlProperty("package")] public PackageData? Package { get; set; } From 2531ec96f7b58f01ff4e2fd99031be61e64eb60e Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 7 Feb 2023 19:37:09 -0600 Subject: [PATCH 54/91] Fix quoted arguments --- ThunderstoreCLI/Program.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ThunderstoreCLI/Program.cs b/ThunderstoreCLI/Program.cs index e82a4c8..3b5d9a4 100644 --- a/ThunderstoreCLI/Program.cs +++ b/ThunderstoreCLI/Program.cs @@ -20,7 +20,8 @@ private static int Main(string[] args) var argsDelimeterIndex = Array.IndexOf(args, "--"); if (argsDelimeterIndex != -1) { - trailingArgs = string.Join(' ', args, argsDelimeterIndex + 1, args.Length - argsDelimeterIndex); + var trailingWithQuotes = args.Skip(argsDelimeterIndex + 1).Select(arg => arg.Contains(' ') ? $"\"{arg}\"" : arg); + trailingArgs = string.Join(' ', trailingWithQuotes); args = args[..argsDelimeterIndex]; } @@ -46,7 +47,7 @@ private static int Main(string[] args) } return HandleParsed(o); }, - + (ListOptions o) => HandleParsed(o), #endif _ => 1 // failure to parse From ae5f692d18598f0fceb6df6b1fa0746d576bd319 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 7 Feb 2023 19:50:37 -0600 Subject: [PATCH 55/91] Fix copying of installer on windows builds --- ThunderstoreCLI/ThunderstoreCLI.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ThunderstoreCLI/ThunderstoreCLI.csproj b/ThunderstoreCLI/ThunderstoreCLI.csproj index 93100d1..7fdeeb0 100644 --- a/ThunderstoreCLI/ThunderstoreCLI.csproj +++ b/ThunderstoreCLI/ThunderstoreCLI.csproj @@ -75,7 +75,7 @@ - + true From 4bbf9cad2bfa513458f9bbe009e9b80619c22fcd Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Sat, 11 Feb 2023 00:01:05 -0600 Subject: [PATCH 56/91] Compile installer with +crt-static to remove the Visual C++ Runtime dependency --- tcli-bepinex-installer/.cargo/config.toml | 1 + 1 file changed, 1 insertion(+) create mode 100644 tcli-bepinex-installer/.cargo/config.toml diff --git a/tcli-bepinex-installer/.cargo/config.toml b/tcli-bepinex-installer/.cargo/config.toml new file mode 100644 index 0000000..4ae665f --- /dev/null +++ b/tcli-bepinex-installer/.cargo/config.toml @@ -0,0 +1 @@ +target.'x86_64-pc-windows-msvc'.rustflags = "-C target-feature=+crt-static" From 0645d88a7a40e0c730a4791d9a2d315569398de9 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Wed, 15 Feb 2023 22:58:29 -0600 Subject: [PATCH 57/91] Improve download size estimations --- ThunderstoreCLI/Commands/InstallCommand.cs | 10 ++++++++-- ThunderstoreCLI/Commands/PublishCommand.cs | 6 +----- ThunderstoreCLI/Utils/DownloadCache.cs | 5 +++++ ThunderstoreCLI/Utils/MiscUtils.cs | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index 07bc894..7d688d2 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -96,8 +96,14 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe if (dependenciesToInstall.Length > 0) { - var totalSize = MiscUtils.GetSizeString(dependenciesToInstall.Select(d => d.Versions![0].FileSize).Sum()); - Write.Light($"Total estimated download size: {totalSize}"); + var totalSize = dependenciesToInstall + .Where(d => !config.Cache.ContainsFile($"{d.Fullname}-{d.Versions![0].VersionNumber}.zip")) + .Select(d => d.Versions![0].FileSize) + .Sum(); + if (totalSize != 0) + { + Write.Light($"Total estimated download size: {MiscUtils.GetSizeString(totalSize)}"); + } var downloadTasks = dependenciesToInstall.Select(mod => { diff --git a/ThunderstoreCLI/Commands/PublishCommand.cs b/ThunderstoreCLI/Commands/PublishCommand.cs index 0c78956..4658229 100644 --- a/ThunderstoreCLI/Commands/PublishCommand.cs +++ b/ThunderstoreCLI/Commands/PublishCommand.cs @@ -239,11 +239,7 @@ private static void PublishPackageRequest(Config config, string uploadUuid) using var response = await HttpClient.SendAsync(request); - try - { - response.EnsureSuccessStatusCode(); - } - catch + if (!response.IsSuccessStatusCode) { Write.Empty(); Write.ErrorExit(await response.Content.ReadAsStringAsync()); diff --git a/ThunderstoreCLI/Utils/DownloadCache.cs b/ThunderstoreCLI/Utils/DownloadCache.cs index c1ae67a..119b1e3 100644 --- a/ThunderstoreCLI/Utils/DownloadCache.cs +++ b/ThunderstoreCLI/Utils/DownloadCache.cs @@ -18,6 +18,11 @@ public DownloadCache(string cacheDirectory) } } + public bool ContainsFile(string filename) + { + return File.Exists(Path.Combine(CacheDirectory, filename)); + } + // Task instead of ValueTask here because these Tasks will be await'd multiple times (ValueTask does not allow that) public Task GetFileOrDownload(string filename, string downloadUrl) { diff --git a/ThunderstoreCLI/Utils/MiscUtils.cs b/ThunderstoreCLI/Utils/MiscUtils.cs index 84d3626..3e93f21 100644 --- a/ThunderstoreCLI/Utils/MiscUtils.cs +++ b/ThunderstoreCLI/Utils/MiscUtils.cs @@ -80,6 +80,6 @@ public static string GetSizeString(long byteSize) finalSize /= 1024; suffixIndex++; } - return $"{byteSize:F2} {suffixes[suffixIndex]}"; + return $"{finalSize:F2} {suffixes[suffixIndex]}"; } } From be10037b59f74134aa647bf90de824a8222a3871 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 28 Feb 2023 21:00:28 -0600 Subject: [PATCH 58/91] Fix modpack installs --- ThunderstoreCLI/Commands/InstallCommand.cs | 33 +++++----- ThunderstoreCLI/Commands/UninstallCommand.cs | 2 +- ThunderstoreCLI/Game/ModProfile.cs | 4 +- ThunderstoreCLI/Models/PackageListingV1.cs | 7 ++ ThunderstoreCLI/Utils/ModDependencyTree.cs | 67 +++++++++----------- 5 files changed, 57 insertions(+), 56 deletions(-) diff --git a/ThunderstoreCLI/Commands/InstallCommand.cs b/ThunderstoreCLI/Commands/InstallCommand.cs index 7d688d2..1d7bb28 100644 --- a/ThunderstoreCLI/Commands/InstallCommand.cs +++ b/ThunderstoreCLI/Commands/InstallCommand.cs @@ -37,7 +37,7 @@ public static async Task Run(Config config) Match packageMatch = FullPackageNameRegex.Match(package); if (File.Exists(package)) { - returnCode = await InstallZip(config, http, def, profile, package, null, null); + returnCode = await InstallZip(config, http, def, profile, package, null, null, false); } else if (packageMatch.Success) { @@ -77,11 +77,11 @@ private static async Task InstallFromRepository(Config config, HttpClient h versionData ??= packageData!.LatestVersion!; var zipPath = await config.Cache.GetFileOrDownload($"{versionData.FullName}.zip", versionData.DownloadUrl!); - var returnCode = await InstallZip(config, http, game, profile, zipPath, versionData.Namespace!, packageData!.CommunityListings!.First().Community); + var returnCode = await InstallZip(config, http, game, profile, zipPath, versionData.Namespace!, packageData!.CommunityListings!.First().Community, packageData.CommunityListings!.First().Categories!.Contains("Modpacks")); return returnCode; } - private static async Task InstallZip(Config config, HttpClient http, GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace, string? sourceCommunity) + private static async Task InstallZip(Config config, HttpClient http, GameDefinition game, ModProfile profile, string zipPath, string? backupNamespace, string? sourceCommunity, bool isModpack) { using var zip = ZipFile.OpenRead(zipPath); var manifestFile = zip.GetEntry("manifest.json") ?? throw new CommandFatalException("Package zip needs a manifest.json!"); @@ -90,15 +90,15 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe manifest.Namespace ??= backupNamespace; - var dependenciesToInstall = ModDependencyTree.Generate(config, http, manifest, sourceCommunity) - .Where(dependency => !profile.InstalledModVersions.ContainsKey(dependency.Fullname!)) + var dependenciesToInstall = ModDependencyTree.Generate(config, http, manifest, sourceCommunity, isModpack) + .Where(dependency => !profile.InstalledModVersions.ContainsKey(dependency.FullNameParts["fullname"].Value)) .ToArray(); if (dependenciesToInstall.Length > 0) { var totalSize = dependenciesToInstall - .Where(d => !config.Cache.ContainsFile($"{d.Fullname}-{d.Versions![0].VersionNumber}.zip")) - .Select(d => d.Versions![0].FileSize) + .Where(d => !config.Cache.ContainsFile($"{d.FullName}-{d.VersionNumber}.zip")) + .Select(d => d.FileSize) .Sum(); if (totalSize != 0) { @@ -106,35 +106,32 @@ private static async Task InstallZip(Config config, HttpClient http, GameDe } var downloadTasks = dependenciesToInstall.Select(mod => - { - var version = mod.Versions![0]; - return config.Cache.GetFileOrDownload($"{mod.Fullname}-{version.VersionNumber}.zip", version.DownloadUrl!); - }).ToArray(); + config.Cache.GetFileOrDownload($"{mod.FullName}-{mod.VersionNumber}.zip", mod.DownloadUrl!) + ).ToArray(); var spinner = new ProgressSpinner("dependencies downloaded", downloadTasks); await spinner.Spin(); - foreach (var (tempZipPath, package) in downloadTasks.Select(x => x.Result).Zip(dependenciesToInstall)) + foreach (var (tempZipPath, pVersion) in downloadTasks.Select(x => x.Result).Zip(dependenciesToInstall)) { - var packageVersion = package.Versions![0]; - int returnCode = RunInstaller(game, profile, tempZipPath, package.Owner); + int returnCode = RunInstaller(game, profile, tempZipPath, pVersion.FullNameParts["namespace"].Value); if (returnCode == 0) { - Write.Success($"Installed mod: {package.Fullname}-{packageVersion.VersionNumber}"); + Write.Success($"Installed mod: {pVersion.FullName}"); } else { - Write.Error($"Failed to install mod: {package.Fullname}-{packageVersion.VersionNumber}"); + Write.Error($"Failed to install mod: {pVersion.FullName}"); return returnCode; } - profile.InstalledModVersions[package.Fullname!] = new PackageManifestV1(package, packageVersion); + profile.InstalledModVersions[pVersion.FullNameParts["fullname"].Value] = new InstalledModVersion(pVersion.FullNameParts["fullname"].Value, pVersion.VersionNumber!, pVersion.Dependencies!); } } var exitCode = RunInstaller(game, profile, zipPath, backupNamespace); if (exitCode == 0) { - profile.InstalledModVersions[manifest.FullName] = manifest; + profile.InstalledModVersions[manifest.FullName] = new InstalledModVersion(manifest.FullName, manifest.VersionNumber!, manifest.Dependencies!); Write.Success($"Installed mod: {manifest.FullName}-{manifest.VersionNumber}"); } else diff --git a/ThunderstoreCLI/Commands/UninstallCommand.cs b/ThunderstoreCLI/Commands/UninstallCommand.cs index ed57c79..c510fc5 100644 --- a/ThunderstoreCLI/Commands/UninstallCommand.cs +++ b/ThunderstoreCLI/Commands/UninstallCommand.cs @@ -35,7 +35,7 @@ public static int Run(Config config) var searchWithDash = search + '-'; foreach (var mod in profile.InstalledModVersions.Values) { - if (mod.Dependencies!.Any(s => s.StartsWith(searchWithDash))) + if (mod.Dependencies.Any(s => s.StartsWith(searchWithDash))) { if (modsToRemove.Add(mod.FullName)) { diff --git a/ThunderstoreCLI/Game/ModProfile.cs b/ThunderstoreCLI/Game/ModProfile.cs index 89f5869..e842c10 100644 --- a/ThunderstoreCLI/Game/ModProfile.cs +++ b/ThunderstoreCLI/Game/ModProfile.cs @@ -4,11 +4,13 @@ namespace ThunderstoreCLI.Game; +public record InstalledModVersion(string FullName, string VersionNumber, string[] Dependencies); + public class ModProfile : BaseJson { public string Name { get; set; } public string ProfileDirectory { get; set; } - public Dictionary InstalledModVersions { get; } = new(); + public Dictionary InstalledModVersions { get; } = new(); #pragma warning disable CS8618 private ModProfile() { } diff --git a/ThunderstoreCLI/Models/PackageListingV1.cs b/ThunderstoreCLI/Models/PackageListingV1.cs index 4a6743d..0f07ac4 100644 --- a/ThunderstoreCLI/Models/PackageListingV1.cs +++ b/ThunderstoreCLI/Models/PackageListingV1.cs @@ -1,4 +1,6 @@ +using System.Text.RegularExpressions; using Newtonsoft.Json; +using ThunderstoreCLI.Commands; namespace ThunderstoreCLI.Models; @@ -104,6 +106,11 @@ public class PackageVersionV1 [JsonProperty("file_size")] public int FileSize { get; set; } + [JsonIgnore] + private GroupCollection? _fullNameParts; + [JsonIgnore] + public GroupCollection FullNameParts => _fullNameParts ??= InstallCommand.FullPackageNameRegex.Match(FullName!).Groups; + public PackageVersionV1() { } public PackageVersionV1(PackageVersionData version) diff --git a/ThunderstoreCLI/Utils/ModDependencyTree.cs b/ThunderstoreCLI/Utils/ModDependencyTree.cs index 8d421f8..3ecde32 100644 --- a/ThunderstoreCLI/Utils/ModDependencyTree.cs +++ b/ThunderstoreCLI/Utils/ModDependencyTree.cs @@ -8,7 +8,7 @@ namespace ThunderstoreCLI.Utils; public static class ModDependencyTree { - public static IEnumerable Generate(Config config, HttpClient http, PackageManifestV1 root, string? sourceCommunity) + public static IEnumerable Generate(Config config, HttpClient http, PackageManifestV1 root, string? sourceCommunity, bool useExactVersions) { List? packages = null; @@ -32,67 +32,62 @@ public static IEnumerable Generate(Config config, HttpClient h packages = PackageListingV1.DeserializeList(packagesJson)!; } - HashSet visited = new(); - foreach (var originalDep in root.Dependencies!) + Queue toVisit = new(); + Dictionary dict = new(); + int currentId = 0; + foreach (var dep in root.Dependencies!) { - var match = InstallCommand.FullPackageNameRegex.Match(originalDep); + toVisit.Enqueue(dep); + } + while (toVisit.TryDequeue(out var packageString)) + { + var match = InstallCommand.FullPackageNameRegex.Match(packageString); var fullname = match.Groups["fullname"].Value; - var depPackage = packages?.Find(p => p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match, root.FullName); - if (depPackage == null) + if (dict.TryGetValue(fullname, out var current)) { + dict[fullname] = (currentId++, current.version); continue; } - foreach (var dependency in GenerateInner(packages, config, http, depPackage, p => visited.Contains(p.Fullname!))) + var package = packages?.Find(p => p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match); + if (package is null) + continue; + PackageVersionV1? version; + if (useExactVersions) { - // can happen on cycles, oh well - if (visited.Contains(dependency.Fullname!)) + string requiredVersion = match.Groups["version"].Value; + version = package.Versions!.FirstOrDefault(v => v.VersionNumber == requiredVersion); + if (version is null) { - continue; + Write.Warn($"Version {requiredVersion} could not be found for mod {fullname}, using latest instead"); + version = package.Versions!.First(); } - visited.Add(dependency.Fullname!); - yield return dependency; } - } - } - - private static IEnumerable GenerateInner(List? packages, Config config, HttpClient http, PackageListingV1 root, Predicate visited) - { - if (visited(root)) - { - yield break; - } - - foreach (var dependency in root.Versions!.First().Dependencies!) - { - var match = InstallCommand.FullPackageNameRegex.Match(dependency); - var fullname = match.Groups["fullname"].Value; - var package = packages?.Find(p => p.Fullname == fullname) ?? AttemptResolveExperimental(config, http, match, root.Fullname!); - if (package == null) + else { - continue; + version = package.Versions!.First(); } - foreach (var innerPackage in GenerateInner(packages, config, http, package, visited)) + dict[fullname] = (currentId++, version); + foreach (var dep in version.Dependencies!) { - yield return innerPackage; + toVisit.Enqueue(dep); } } - - yield return root; + return dict.Values.OrderByDescending(x => x.id).Select(x => x.version); } - private static PackageListingV1? AttemptResolveExperimental(Config config, HttpClient http, Match nameMatch, string neededBy) + private static PackageListingV1? AttemptResolveExperimental(Config config, HttpClient http, Match nameMatch) { var response = http.Send(config.Api.GetPackageMetadata(nameMatch.Groups["namespace"].Value, nameMatch.Groups["name"].Value)); if (response.StatusCode == HttpStatusCode.NotFound) { - Write.Warn($"Failed to resolve dependency {nameMatch.Groups["fullname"].Value} for {neededBy}, continuing without it."); + Write.Warn($"Failed to resolve dependency {nameMatch.Groups["fullname"].Value}, continuing without it."); return null; } response.EnsureSuccessStatusCode(); using var reader = new StreamReader(response.Content.ReadAsStream()); var data = PackageData.Deserialize(reader.ReadToEnd()); - Write.Warn($"Package {data!.Fullname} (needed by {neededBy}) exists in different community, ignoring"); + Write.Warn($"Package {data!.Fullname} exists in different community, ignoring"); return null; } } From 863912d63a6558202d0fe425543c2b53e14f3dfe Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Tue, 11 Apr 2023 16:05:07 -0500 Subject: [PATCH 59/91] Fix TCLI directory switch and LD_PRELOAD shenanigans --- ThunderstoreCLI/Game/ModProfile.cs | 2 +- ThunderstoreCLI/Options.cs | 5 ----- tcli-bepinex-installer/src/main.rs | 11 +++++++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/ThunderstoreCLI/Game/ModProfile.cs b/ThunderstoreCLI/Game/ModProfile.cs index e842c10..5a43fb0 100644 --- a/ThunderstoreCLI/Game/ModProfile.cs +++ b/ThunderstoreCLI/Game/ModProfile.cs @@ -20,7 +20,7 @@ internal ModProfile(GameDefinition gameDef, string name, string tcliDirectory) { Name = name; - var directory = Path.Combine(tcliDirectory, "Profiles", gameDef.Identifier, name); + var directory = Path.GetFullPath(Path.Combine(tcliDirectory, "Profiles", gameDef.Identifier, name)); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); diff --git a/ThunderstoreCLI/Options.cs b/ThunderstoreCLI/Options.cs index b442cf1..9a3a14f 100644 --- a/ThunderstoreCLI/Options.cs +++ b/ThunderstoreCLI/Options.cs @@ -75,11 +75,6 @@ public override bool Validate() return false; } - if (!Directory.Exists(TcliDirectory)) - { - Directory.CreateDirectory(TcliDirectory!); - } - return true; } } diff --git a/tcli-bepinex-installer/src/main.rs b/tcli-bepinex-installer/src/main.rs index 546de59..70b2618 100644 --- a/tcli-bepinex-installer/src/main.rs +++ b/tcli-bepinex-installer/src/main.rs @@ -345,21 +345,24 @@ fn output_instructions(game_dir: PathBuf, bep_dir: PathBuf, platform: GamePlatfo ); let mut ld_library = OsString::from(game_dir.join("doorstop_libs")); - if let Some(orig) = env::var_os("LD_LIBRARY") { + if let Some(orig) = env::var_os("LD_LIBRARY_PATH") { ld_library.push(":"); ld_library.push(orig); } - println!("ENVIRONMENT:LD_LIBRARY={}", ld_library.to_string_lossy()); + println!( + "ENVIRONMENT:LD_LIBRARY_PATH={}", + ld_library.to_string_lossy() + ); - let mut ld_preload = OsString::from(game_dir.join("doorstop_libs").join({ + let mut ld_preload = OsString::from({ // FIXME: properly determine arch of the game exe, instead of assuming its the same as this exe if cfg!(target_arch = "x86_64") { "libdoorstop_x64.so" } else { "libdoorstop_x86.so" } - })); + }); if let Some(orig) = env::var_os("LD_PRELOAD") { ld_preload.push(":"); ld_preload.push(orig); From 9de949ba975a55505fc4f7a81716ae9c1b75c814 Mon Sep 17 00:00:00 2001 From: Aaron Robinson Date: Mon, 24 Apr 2023 23:31:54 -0500 Subject: [PATCH 60/91] Add support for community-specific categories --- ThunderstoreCLI/Commands/BuildCommand.cs | 2 +- .../CommandValidator.cs} | 6 ++-- ThunderstoreCLI/Commands/InitCommand.cs | 2 +- ThunderstoreCLI/Commands/PublishCommand.cs | 2 +- ThunderstoreCLI/Configuration/Config.cs | 9 +++-- .../{BaseConfig.cs => DefaultConfig.cs} | 2 +- .../Configuration/ProjectFileConfig.cs | 2 +- ThunderstoreCLI/Models/PublishModels.cs | 2 ++ ThunderstoreCLI/Models/ThunderstoreProject.cs | 34 +++++++++++++++++-- ThunderstoreCLI/Utils/DictionaryExtensions.cs | 9 +++++ 10 files changed, 56 insertions(+), 14 deletions(-) rename ThunderstoreCLI/{Configuration/Validator.cs => Commands/CommandValidator.cs} (93%) rename ThunderstoreCLI/Configuration/{BaseConfig.cs => DefaultConfig.cs} (97%) create mode 100644 ThunderstoreCLI/Utils/DictionaryExtensions.cs diff --git a/ThunderstoreCLI/Commands/BuildCommand.cs b/ThunderstoreCLI/Commands/BuildCommand.cs index b37abb1..3cdbeac 100644 --- a/ThunderstoreCLI/Commands/BuildCommand.cs +++ b/ThunderstoreCLI/Commands/BuildCommand.cs @@ -309,7 +309,7 @@ public static string SerializeManifest(Config config) public static List ValidateConfig(Config config, bool throwIfErrors = true) { - var v = new Validator("build"); + var v = new CommandValidator("build"); v.AddIfEmpty(config.PackageConfig.Namespace, "Package Namespace"); v.AddIfEmpty(config.PackageConfig.Name, "Package Name"); v.AddIfNotSemver(config.PackageConfig.VersionNumber, "Package VersionNumber"); diff --git a/ThunderstoreCLI/Configuration/Validator.cs b/ThunderstoreCLI/Commands/CommandValidator.cs similarity index 93% rename from ThunderstoreCLI/Configuration/Validator.cs rename to ThunderstoreCLI/Commands/CommandValidator.cs index f39b696..fbd7ac6 100644 --- a/ThunderstoreCLI/Configuration/Validator.cs +++ b/ThunderstoreCLI/Commands/CommandValidator.cs @@ -1,14 +1,14 @@ using ThunderstoreCLI.Utils; -namespace ThunderstoreCLI.Configuration; +namespace ThunderstoreCLI.Commands; /// Helper for validating command-specific configurations -public class Validator +public class CommandValidator { private List _errors; private string _name; - public Validator(string commandName, List? errors = null) + public CommandValidator(string commandName, List? errors = null) { _name = commandName; _errors = errors ?? new List(); diff --git a/ThunderstoreCLI/Commands/InitCommand.cs b/ThunderstoreCLI/Commands/InitCommand.cs index 6aaa02f..a63bb57 100644 --- a/ThunderstoreCLI/Commands/InitCommand.cs +++ b/ThunderstoreCLI/Commands/InitCommand.cs @@ -76,7 +76,7 @@ public static string BuildReadme(Config config) private static void ValidateConfig(Config config) { - var v = new Validator("init"); + var v = new CommandValidator("init"); v.AddIfEmpty(config.PackageConfig.Namespace, "Package Namespace"); v.AddIfEmpty(config.PackageConfig.Name, "Package Name"); v.AddIfNotSemver(config.PackageConfig.VersionNumber, "Package VersionNumber"); diff --git a/ThunderstoreCLI/Commands/PublishCommand.cs b/ThunderstoreCLI/Commands/PublishCommand.cs index 4658229..de07e9b 100644 --- a/ThunderstoreCLI/Commands/PublishCommand.cs +++ b/ThunderstoreCLI/Commands/PublishCommand.cs @@ -290,7 +290,7 @@ private static void HandleRequestError( private static void ValidateConfig(Config config, bool justReturnErrors = false) { var buildConfigErrors = BuildCommand.ValidateConfig(config, false); - var v = new Validator("publish", buildConfigErrors); + var v = new CommandValidator("publish", buildConfigErrors); v.AddIfEmpty(config.AuthConfig.AuthToken, "Auth AuthToken"); v.ThrowIfErrors(); } diff --git a/ThunderstoreCLI/Configuration/Config.cs b/ThunderstoreCLI/Configuration/Config.cs index af7ba0d..ff35a83 100644 --- a/ThunderstoreCLI/Configuration/Config.cs +++ b/ThunderstoreCLI/Configuration/Config.cs @@ -37,7 +37,7 @@ public static Config FromCLI(IConfigProvider cliConfig) providers.Add(new EnvironmentConfig()); if (cliConfig is CLIConfig) providers.Add(new ProjectFileConfig()); - providers.Add(new BaseConfig()); + providers.Add(new DefaultConfig()); return Parse(providers.ToArray()); } @@ -102,7 +102,10 @@ public PackageUploadMetadata GetUploadMetadata(string fileUuid) return new PackageUploadMetadata() { AuthorName = PackageConfig.Namespace, - Categories = PublishConfig.Categories, + Categories = PublishConfig.Categories!.GetOrDefault("") ?? Array.Empty(), + CommunityCategories = PublishConfig.Categories! + .Where(kvp => kvp.Key != "") + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value), Communities = PublishConfig.Communities, HasNsfwContent = PackageConfig.ContainsNsfwContent ?? false, UploadUUID = fileUuid @@ -212,7 +215,7 @@ public class PublishConfig { public string? File { get; set; } public string[]? Communities { get; set; } - public string[]? Categories { get; set; } + public Dictionary? Categories { get; set; } } public class AuthConfig diff --git a/ThunderstoreCLI/Configuration/BaseConfig.cs b/ThunderstoreCLI/Configuration/DefaultConfig.cs similarity index 97% rename from ThunderstoreCLI/Configuration/BaseConfig.cs rename to ThunderstoreCLI/Configuration/DefaultConfig.cs index 16f32a1..c78cef7 100644 --- a/ThunderstoreCLI/Configuration/BaseConfig.cs +++ b/ThunderstoreCLI/Configuration/DefaultConfig.cs @@ -1,6 +1,6 @@ namespace ThunderstoreCLI.Configuration; -class BaseConfig : EmptyConfig +class DefaultConfig : EmptyConfig { public override GeneralConfig? GetGeneralConfig() { diff --git a/ThunderstoreCLI/Configuration/ProjectFileConfig.cs b/ThunderstoreCLI/Configuration/ProjectFileConfig.cs index 2afec61..33323a3 100644 --- a/ThunderstoreCLI/Configuration/ProjectFileConfig.cs +++ b/ThunderstoreCLI/Configuration/ProjectFileConfig.cs @@ -62,7 +62,7 @@ public override void Parse(Config currentConfig) { return new PublishConfig() { - Categories = Project.Publish?.Categories, + Categories = Project.Publish?.Categories.Categories, Communities = Project.Publish?.Communities }; } diff --git a/ThunderstoreCLI/Models/PublishModels.cs b/ThunderstoreCLI/Models/PublishModels.cs index f4aa90d..cb2e0af 100644 --- a/ThunderstoreCLI/Models/PublishModels.cs +++ b/ThunderstoreCLI/Models/PublishModels.cs @@ -10,6 +10,8 @@ public class PackageUploadMetadata : BaseJson [JsonProperty("communities")] public string[]? Communities { get; set; } + [JsonProperty("community_categories")] public Dictionary? CommunityCategories { get; set; } + [JsonProperty("has_nsfw_content")] public bool HasNsfwContent { get; set; } [JsonProperty("upload_uuid")] public string? UploadUUID { get; set; } diff --git a/ThunderstoreCLI/Models/ThunderstoreProject.cs b/ThunderstoreCLI/Models/ThunderstoreProject.cs index 2b7bbba..6b09b7a 100644 --- a/ThunderstoreCLI/Models/ThunderstoreProject.cs +++ b/ThunderstoreCLI/Models/ThunderstoreProject.cs @@ -8,6 +8,29 @@ namespace ThunderstoreCLI.Models; [TomlDoNotInlineObject] public class ThunderstoreProject : BaseToml { + public struct CategoryDictionary + { + public Dictionary Categories; + } + + static ThunderstoreProject() + { + TomletMain.RegisterMapper( + dict => TomletMain.ValueFrom(dict.Categories), + toml => toml switch + { + TomlArray arr => new CategoryDictionary + { + Categories = new Dictionary + { + { "", arr.ArrayValues.Select(v => v.StringValue).ToArray() } + } + }, + TomlTable table => new CategoryDictionary { Categories = TomletMain.To>(table) }, + _ => throw new NotSupportedException() + }); + } + [TomlDoNotInlineObject] public class ConfigData { @@ -75,10 +98,15 @@ public class PublishData { "riskofrain2" }; + [TomlProperty("categories")] - public string[] Categories { get; set; } = + [TomlDoNotInlineObject] + public CategoryDictionary Categories { get; set; } = new() { - "items", "skills" + Categories = new Dictionary + { + { "riskofrain2", new[] { "items", "skills" } } + } }; } [TomlProperty("publish")] @@ -112,7 +140,7 @@ public ThunderstoreProject(Config config) }; Publish = new PublishData() { - Categories = config.PublishConfig.Categories!, + Categories = new CategoryDictionary { Categories = config.PublishConfig.Categories! }, Communities = config.PublishConfig.Communities!, Repository = config.GeneralConfig.Repository }; diff --git a/ThunderstoreCLI/Utils/DictionaryExtensions.cs b/ThunderstoreCLI/Utils/DictionaryExtensions.cs new file mode 100644 index 0000000..6257624 --- /dev/null +++ b/ThunderstoreCLI/Utils/DictionaryExtensions.cs @@ -0,0 +1,9 @@ +namespace ThunderstoreCLI.Utils; + +public static class DictionaryExtensions +{ + public static TValue? GetOrDefault(this Dictionary dict, TKey key) where TKey : notnull + { + return dict.TryGetValue(key, out var value) ? value : default; + } +} From ede40973919344d0c1ffb7ad5e26426625d7dfa8 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 19:46:18 +0000 Subject: [PATCH 61/91] add setup-rust step --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cf6a83b..77f1bf8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,8 @@ jobs: - uses: actions/setup-dotnet@v3 with: dotnet-version: '7' + - name: Setup Rust/Cargo + uses: moonrepo/setup-rust@v1 - name: Install dependencies run: dotnet restore - name: Build From 646ea48112e03be1e6fff44a34255ab7d766d2aa Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 19:47:33 +0000 Subject: [PATCH 62/91] update actions/checkout from v2 to v4 --- .github/workflows/publish.yml | 2 +- .github/workflows/release.yml | 6 +++--- .github/workflows/test.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 38aefe1..812261a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,7 +8,7 @@ jobs: nuget: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v3 with: dotnet-version: '7' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e141a4d..6423ce6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: validate-tag: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - id: tag uses: dawidd6/action-get-tag@v1 - id: regex-match @@ -43,7 +43,7 @@ jobs: target: osx-x64 os: macos-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v3 with: dotnet-version: '7' @@ -81,7 +81,7 @@ jobs: name: Create .nupkg runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v3 with: dotnet-version: '7' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 77f1bf8..78124e4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: name: Code style check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v3 with: dotnet-version: '7' @@ -30,7 +30,7 @@ jobs: env: OS: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v3 with: dotnet-version: '7' From 31d0053b4897e550a5332b5212448c34a2fc198b Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 19:48:12 +0000 Subject: [PATCH 63/91] update actions/setup-dotnet from v3 to v4 --- .github/workflows/publish.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 812261a..e3ff8fd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v3 + - uses: actions/setup-dotnet@v4 with: dotnet-version: '7' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6423ce6..7f953d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: os: macos-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v3 + - uses: actions/setup-dotnet@v4 with: dotnet-version: '7' - id: tag @@ -82,7 +82,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v3 + - uses: actions/setup-dotnet@v4 with: dotnet-version: '7' - id: tag diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 78124e4..9ecae3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v3 + - uses: actions/setup-dotnet@v4 with: dotnet-version: '7' - uses: actions/setup-python@v2 @@ -31,7 +31,7 @@ jobs: OS: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v3 + - uses: actions/setup-dotnet@v4 with: dotnet-version: '7' - name: Setup Rust/Cargo From d40bcf74e6989452b89af496f617f26225cb27f2 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:31:17 +0000 Subject: [PATCH 64/91] reduce complexity of pure SemVer regex https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f953d4..da63e5b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: uses: actions-ecosystem/action-regex-match@v2 with: text: ${{ steps.tag.outputs.tag }} - regex: '^([1-9][0-9]*|[0-9])\.([1-9][0-9]*|[0-9])\.([1-9][0-9]*|[0-9])$' + regex: '^([1-9][0-9]*|0)\.([1-9][0-9]*|0)\.([1-9][0-9]*|0)$' - id: fail-fast if: ${{ steps.regex-match.outputs.match == '' }} uses: actions/github-script@v3 From 5b4dccd92e9da6e31bcfadc02c47534f4c414b7b Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:32:01 +0000 Subject: [PATCH 65/91] remove 'get tag' step from jobs that depend on `validate-tag` --- .github/workflows/release.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da63e5b..7e3c7b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,8 +47,7 @@ jobs: - uses: actions/setup-dotnet@v4 with: dotnet-version: '7' - - id: tag - uses: dawidd6/action-get-tag@v1 + - name: Install dependencies run: dotnet restore @@ -85,8 +84,7 @@ jobs: - uses: actions/setup-dotnet@v4 with: dotnet-version: '7' - - id: tag - uses: dawidd6/action-get-tag@v1 + - name: Install dependencies run: dotnet restore From 5c4c5b4ac4663123e984e8858aa714472113d935 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:33:47 +0000 Subject: [PATCH 66/91] add `.yml` entry to editorconfig --- .editorconfig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.editorconfig b/.editorconfig index fd10fe0..72bd5fd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,9 @@ charset = utf-8 indent_style = space indent_size = 4 +[*.yml] +indent_size = 2 + [*.{cs,vb}] # https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/formatting-rules csharp_new_line_before_open_brace = all From bc5ef4b3b7160e8ff06ff45ef8415cc1523808c1 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:35:22 +0000 Subject: [PATCH 67/91] add `tag` step output to `validate-tag` job outputs --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e3c7b2..e79b2dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,8 @@ jobs: # Validate tag with proper regex since the check above is very limited. validate-tag: runs-on: ubuntu-latest + outputs: + tag: ${{ steps.tag.outputs.tag }} steps: - uses: actions/checkout@v4 - id: tag From 83e673cc0e69042d34c24180d3aab97882b142c9 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:36:22 +0000 Subject: [PATCH 68/91] replace references to `steps.tag.outputs.tag` with `needs.validate-tag.outputs.tag` --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e79b2dd..95fa16c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: - name: Build shell: bash run: | - release_name="tcli-${{ steps.tag.outputs.tag }}-${{ matrix.target }}" + release_name="tcli-${{ needs.validate-tag.outputs.tag }}-${{ matrix.target }}" dotnet publish ThunderstoreCLI/ThunderstoreCLI.csproj -c Release -r "${{ matrix.target }}" --self-contained true -f net7.0 -o "${release_name}" if [ "${{ matrix.target }}" == "win-x64" ]; then @@ -71,7 +71,7 @@ jobs: uses: softprops/action-gh-release@v1 with: files: "tcli*" - name: "Thunderstore CLI ${{ steps.tag.outputs.tag }}" + name: "Thunderstore CLI ${{ needs.validate-tag.outputs.tag }}" body_path: ${{ github.workspace }}/.github/RELEASE_TEMPLATE.md draft: true prerelease: ${{ startsWith(steps.tag.outputs.tag, '0.') }} @@ -98,7 +98,7 @@ jobs: uses: softprops/action-gh-release@v1 with: files: "tcli*" - name: "Thunderstore CLI ${{ steps.tag.outputs.tag }}" + name: "Thunderstore CLI ${{ needs.validate-tag.outputs.tag }}" body_path: ${{ github.workspace }}/.github/RELEASE_TEMPLATE.md draft: true prerelease: ${{ startsWith(steps.tag.outputs.tag, '0.') }} From 6c8077b03f9f4d21ee7b786c5b43659b5f53f7c6 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:58:44 +0000 Subject: [PATCH 69/91] hoist job names to top, add step names for `release` workflow --- .github/workflows/release.yml | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95fa16c..63a7837 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,12 +8,15 @@ on: jobs: # Validate tag with proper regex since the check above is very limited. validate-tag: + name: Validate tag's semantic version runs-on: ubuntu-latest outputs: tag: ${{ steps.tag.outputs.tag }} steps: - - uses: actions/checkout@v4 - - id: tag + - name: Checkout ref that triggered workflow + uses: actions/checkout@v4 + - name: Ensure triggering ref is a tag + id: tag uses: dawidd6/action-get-tag@v1 - id: regex-match uses: actions-ecosystem/action-regex-match@v2 @@ -27,9 +30,9 @@ jobs: script: core.setFailed('Tag is invalid') platform-binary: + name: Create binary ${{ matrix.target }} needs: validate-tag if: github.event.base_ref == 'refs/heads/master' - name: Create binary ${{ matrix.target }} runs-on: ${{ matrix.os }} strategy: matrix: @@ -45,8 +48,10 @@ jobs: target: osx-x64 os: macos-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 + - name: Checkout ref that triggered workflow + uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 with: dotnet-version: '7' @@ -67,7 +72,7 @@ jobs: rm -r ${release_name} - - name: Publish to GitHub + - name: Add build artifacts to draft GitHub release uses: softprops/action-gh-release@v1 with: files: "tcli*" @@ -77,13 +82,15 @@ jobs: prerelease: ${{ startsWith(steps.tag.outputs.tag, '0.') }} nupkg: + name: Create .nupkg needs: validate-tag if: github.event.base_ref == 'refs/heads/master' - name: Create .nupkg runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 + - name: Checkout ref that triggered workflow + uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 with: dotnet-version: '7' @@ -94,7 +101,7 @@ jobs: shell: bash run: dotnet pack ThunderstoreCLI/ThunderstoreCLI.csproj -c Release -o "." -p:EnableInstallers=false -p:PublishSelfContained=false -p:PublishSingleFile=false -p:PublishTrimmed=false -p:PublishReadyToRun=false - - name: Publish to GitHub + - name: Add build artifacts to draft GitHub release uses: softprops/action-gh-release@v1 with: files: "tcli*" From cce90fee34d8fbd9abd5f37b124726a19e6205d0 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:59:15 +0000 Subject: [PATCH 70/91] standardise whitespace between job steps for `release` workflow --- .github/workflows/release.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63a7837..546d0b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,14 +15,17 @@ jobs: steps: - name: Checkout ref that triggered workflow uses: actions/checkout@v4 + - name: Ensure triggering ref is a tag id: tag uses: dawidd6/action-get-tag@v1 + - id: regex-match uses: actions-ecosystem/action-regex-match@v2 with: text: ${{ steps.tag.outputs.tag }} regex: '^([1-9][0-9]*|0)\.([1-9][0-9]*|0)\.([1-9][0-9]*|0)$' + - id: fail-fast if: ${{ steps.regex-match.outputs.match == '' }} uses: actions/github-script@v3 @@ -50,11 +53,12 @@ jobs: steps: - name: Checkout ref that triggered workflow uses: actions/checkout@v4 + - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '7' - + - name: Install dependencies run: dotnet restore @@ -89,11 +93,12 @@ jobs: steps: - name: Checkout ref that triggered workflow uses: actions/checkout@v4 + - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '7' - + - name: Install dependencies run: dotnet restore From b0bc3ba3b98a66c1a72e247f6aee88fbff6779d5 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:59:41 +0000 Subject: [PATCH 71/91] add steps to setup Cargo before running build command --- .github/workflows/release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 546d0b0..5a5a759 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,6 +62,9 @@ jobs: - name: Install dependencies run: dotnet restore + - name: Setup Cargo/Rust + uses: moonrepo/setup-rust@v1 + - name: Build shell: bash run: | @@ -102,6 +105,9 @@ jobs: - name: Install dependencies run: dotnet restore + - name: Setup Cargo/Rust + uses: moonrepo/setup-rust@v1 + - name: Build shell: bash run: dotnet pack ThunderstoreCLI/ThunderstoreCLI.csproj -c Release -o "." -p:EnableInstallers=false -p:PublishSelfContained=false -p:PublishSingleFile=false -p:PublishTrimmed=false -p:PublishReadyToRun=false From 25b059bae1488217bd0a4718a8bd71b7595874cc Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:07:06 +0000 Subject: [PATCH 72/91] add step names for `test` workflow --- .github/workflows/test.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9ecae3c..604269d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,14 +7,17 @@ jobs: name: Code style check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 + - name: Checkout ref that triggered workflow + uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 with: dotnet-version: '7' - - uses: actions/setup-python@v2 + - name: Setup Python + uses: actions/setup-python@v2 with: python-version: '3.8' - - name: Install pre-commit + - name: Install pre-commit framework run: curl https://pre-commit.com/install-local.py | python - - name: Install dotnet-format run: dotnet tool install -g dotnet-format @@ -30,11 +33,13 @@ jobs: env: OS: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 + - name: Checkout ref that triggered workflow + uses: actions/checkout@v4 + - name: setup .NET + uses: actions/setup-dotnet@v4 with: dotnet-version: '7' - - name: Setup Rust/Cargo + - name: Setup Cargo/Rust uses: moonrepo/setup-rust@v1 - name: Install dependencies run: dotnet restore From 3fbe8df98673ed9a55082dc406d5b5e04d04f2cc Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:14:42 +0000 Subject: [PATCH 73/91] standardise whitespace between job steps for `test` workflow --- .github/workflows/test.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 604269d..211da03 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,18 +9,23 @@ jobs: steps: - name: Checkout ref that triggered workflow uses: actions/checkout@v4 + - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '7' + - name: Setup Python uses: actions/setup-python@v2 with: python-version: '3.8' + - name: Install pre-commit framework run: curl https://pre-commit.com/install-local.py | python - + - name: Install dotnet-format run: dotnet tool install -g dotnet-format + - name: Run pre-commit run: ~/bin/pre-commit run --show-diff-on-failure --color=always --all-files @@ -35,18 +40,24 @@ jobs: steps: - name: Checkout ref that triggered workflow uses: actions/checkout@v4 + - name: setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: '7' + - name: Setup Cargo/Rust uses: moonrepo/setup-rust@v1 + - name: Install dependencies run: dotnet restore + - name: Build run: dotnet build --configuration Release --no-restore + - name: Run xUnit tests run: dotnet test -p:EnableInstallers=false --collect:"XPlat Code Coverage" + - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 with: From 5c795468071cdcaebdaf1d2bb43bf80b8d60065c Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:19:27 +0000 Subject: [PATCH 74/91] use more appropriate job names in `release` workflow --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a5a759..57f5ec5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ on: jobs: # Validate tag with proper regex since the check above is very limited. validate-tag: - name: Validate tag's semantic version + name: Validate tag semantic version runs-on: ubuntu-latest outputs: tag: ${{ steps.tag.outputs.tag }} @@ -33,7 +33,7 @@ jobs: script: core.setFailed('Tag is invalid') platform-binary: - name: Create binary ${{ matrix.target }} + name: Build binaries for ${{ matrix.target }} needs: validate-tag if: github.event.base_ref == 'refs/heads/master' runs-on: ${{ matrix.os }} @@ -89,7 +89,7 @@ jobs: prerelease: ${{ startsWith(steps.tag.outputs.tag, '0.') }} nupkg: - name: Create .nupkg + name: Build NuGet Package needs: validate-tag if: github.event.base_ref == 'refs/heads/master' runs-on: ubuntu-latest From a2c0591d5506f4c76d912967879f739a84528163 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:19:44 +0000 Subject: [PATCH 75/91] add job name and step names to `publish` workflow --- .github/workflows/publish.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e3ff8fd..362f640 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,10 +6,13 @@ on: jobs: nuget: + name: Publish NuGet Package runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 + - name: Checkout ref that triggered workflow + uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 with: dotnet-version: '7' From 61cc94e765c1a5a04cebcd9d11e0a434459206ef Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:20:26 +0000 Subject: [PATCH 76/91] standardise whitespace in `publish` workflow --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 362f640..f6eb4b2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,6 +11,7 @@ jobs: steps: - name: Checkout ref that triggered workflow uses: actions/checkout@v4 + - name: Setup .NET uses: actions/setup-dotnet@v4 with: @@ -24,7 +25,6 @@ jobs: file: "tcli.${{ github.ref_name }}.nupkg" target: "tcli.nupkg" - - name: Publish to NuGet shell: bash run: dotnet nuget push tcli.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} --skip-duplicate From db4fffd8cd4acaea48995057f9ce1bb5961c1c4f Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:32:03 +0000 Subject: [PATCH 77/91] update fetch-gh-release-asset action and remove redundant default argument --- .github/workflows/publish.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f6eb4b2..b545897 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,9 +18,8 @@ jobs: dotnet-version: '7' - name: Fetch Latest .nupkg - uses: dsaltares/fetch-gh-release-asset@0efe227dedb360b09ea0e533795d584b61c461a9 + uses: dsaltares/fetch-gh-release-asset@1.1.1 with: - token: "${{ secrets.GITHUB_TOKEN }}" version: "tags/${{ github.ref_name }}" file: "tcli.${{ github.ref_name }}.nupkg" target: "tcli.nupkg" From 0945b9a090ef69b725e500e40586ed265aa2aba1 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:37:26 +0000 Subject: [PATCH 78/91] update to codecov v3 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 211da03..a73f6a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,7 +59,7 @@ jobs: run: dotnet test -p:EnableInstallers=false --collect:"XPlat Code Coverage" - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: directory: ./ThunderstoreCLI.Tests/TestResults/ env_vars: OS From f2e35566432a802c9bdaccb04eb90e314d04d376 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:39:45 +0000 Subject: [PATCH 79/91] update triggers for the 'test' task --- .github/workflows/test.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a73f6a4..b1149aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,11 @@ name: Build & Test -on: [push] +on: + # Trigger on pushes to the main branch + push: + branches: [ main ] + # Trigger on any pull request + pull_request: jobs: pre-commit: From 264f219720899675c6b973ba2e6cd27654fc82c7 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:43:50 +0000 Subject: [PATCH 80/91] replace dawid66/action-get-tag with updated + verified fork devops-actions/action-get-tag --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57f5ec5..eddaf2f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Ensure triggering ref is a tag id: tag - uses: dawidd6/action-get-tag@v1 + uses: devops-actions/action-get-tag@v1.0.2 - id: regex-match uses: actions-ecosystem/action-regex-match@v2 From 14b4887a34b34c6fe95e7b08016093eb23befd1d Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:48:59 +0000 Subject: [PATCH 81/91] add 'Build and Test' status badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8cd8eeb..41478b2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # thunderstore-cli +[![Build & Test](https://github.com/thunderstore-io/thunderstore-cli/actions/workflows/test.yml/badge.svg)](https://github.com/thunderstore-io/thunderstore-cli/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/thunderstore-io/thunderstore-cli/branch/master/graph/badge.svg)](https://codecov.io/gh/thunderstore-io/thunderstore-cli) Thunderstore CLI (just "TCLI" from here on) is a command line tool for building and uploading mod packages to From 3f4e91567b973ed61dda1b92ec3dfdc030196c6e Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:49:11 +0000 Subject: [PATCH 82/91] add NuGet package version badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 41478b2..1fbb6e4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build & Test](https://github.com/thunderstore-io/thunderstore-cli/actions/workflows/test.yml/badge.svg)](https://github.com/thunderstore-io/thunderstore-cli/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/thunderstore-io/thunderstore-cli/branch/master/graph/badge.svg)](https://codecov.io/gh/thunderstore-io/thunderstore-cli) +[![NuGet Package](https://img.shields.io/nuget/v/tcli)](https://www.nuget.org/packages/tcli) Thunderstore CLI (just "TCLI" from here on) is a command line tool for building and uploading mod packages to [Thunderstore](https://thunderstore.io/) mod database, and installing mods via the command line. From 9dda3dcd0140fa511c63d521f848420de754c202 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:49:21 +0000 Subject: [PATCH 83/91] add NuGet Downloads badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1fbb6e4..4101faa 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Build & Test](https://github.com/thunderstore-io/thunderstore-cli/actions/workflows/test.yml/badge.svg)](https://github.com/thunderstore-io/thunderstore-cli/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/thunderstore-io/thunderstore-cli/branch/master/graph/badge.svg)](https://codecov.io/gh/thunderstore-io/thunderstore-cli) [![NuGet Package](https://img.shields.io/nuget/v/tcli)](https://www.nuget.org/packages/tcli) +[![downloads](https://img.shields.io/nuget/dt/tcli)](https://www.nuget.org/packages/tcli) Thunderstore CLI (just "TCLI" from here on) is a command line tool for building and uploading mod packages to [Thunderstore](https://thunderstore.io/) mod database, and installing mods via the command line. From 5a503a9b99aa2cabf05ea24afbec7448f89f14a9 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Wed, 6 Dec 2023 05:47:50 +0000 Subject: [PATCH 84/91] update github-script action --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eddaf2f..6afd7cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: - name: Checkout ref that triggered workflow uses: actions/checkout@v4 - - name: Ensure triggering ref is a tag + - name: Ensure triggering ref is a tag id: tag uses: devops-actions/action-get-tag@v1.0.2 @@ -28,7 +28,7 @@ jobs: - id: fail-fast if: ${{ steps.regex-match.outputs.match == '' }} - uses: actions/github-script@v3 + uses: actions/github-script@v7 with: script: core.setFailed('Tag is invalid') From ed7de60e12bf5c04eac22830b6397499a1f25ebd Mon Sep 17 00:00:00 2001 From: Mythic Date: Wed, 6 Dec 2023 18:53:57 +0200 Subject: [PATCH 85/91] Update test workflow branch name --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b1149aa..a02ea0a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ name: Build & Test on: # Trigger on pushes to the main branch push: - branches: [ main ] + branches: [ master ] # Trigger on any pull request pull_request: From 1aa408421bdbb82afe8d24c61cb2f4beaf9c74c0 Mon Sep 17 00:00:00 2001 From: Mythic Date: Wed, 6 Dec 2023 19:00:59 +0200 Subject: [PATCH 86/91] Run pre-commit --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a02ea0a..06c9fb2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,11 +1,11 @@ name: Build & Test -on: +on: # Trigger on pushes to the main branch push: branches: [ master ] # Trigger on any pull request - pull_request: + pull_request: jobs: pre-commit: @@ -62,7 +62,7 @@ jobs: - name: Run xUnit tests run: dotnet test -p:EnableInstallers=false --collect:"XPlat Code Coverage" - + - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: From c18cd03a60d55fc80bf8e4598fcb7af949e8c42f Mon Sep 17 00:00:00 2001 From: Mythic Date: Wed, 6 Dec 2023 19:12:14 +0200 Subject: [PATCH 87/91] Add codecov token to test workflow --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 06c9fb2..9c234ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,3 +69,4 @@ jobs: directory: ./ThunderstoreCLI.Tests/TestResults/ env_vars: OS fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} From bf7f268b63884f0a43d4d8097e84c66b1a45925e Mon Sep 17 00:00:00 2001 From: Joe Clack <28568841+Lordfirespeed@users.noreply.github.com> Date: Wed, 6 Dec 2023 19:49:09 +0000 Subject: [PATCH 88/91] Subscribe to 'published' instead --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b545897..3229fc7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,7 +2,7 @@ name: Publish on: release: - types: [released, prereleased] + types: [ published ] jobs: nuget: From f16512b3476a784635034ea320088e016bc1e35d Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Fri, 8 Dec 2023 05:35:35 +0000 Subject: [PATCH 89/91] fix incorrect references to 'tag' job output --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6afd7cc..dfc09ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,7 +86,7 @@ jobs: name: "Thunderstore CLI ${{ needs.validate-tag.outputs.tag }}" body_path: ${{ github.workspace }}/.github/RELEASE_TEMPLATE.md draft: true - prerelease: ${{ startsWith(steps.tag.outputs.tag, '0.') }} + prerelease: ${{ startsWith(needs.validate-tag.outputs.tag, '0.') }} nupkg: name: Build NuGet Package @@ -119,4 +119,4 @@ jobs: name: "Thunderstore CLI ${{ needs.validate-tag.outputs.tag }}" body_path: ${{ github.workspace }}/.github/RELEASE_TEMPLATE.md draft: true - prerelease: ${{ startsWith(steps.tag.outputs.tag, '0.') }} + prerelease: ${{ startsWith(needs.validate-tag.outputs.tag, '0.') }} From dad1673c0792ebc745aaab4212500fc8ba32e323 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Fri, 8 Dec 2023 05:43:33 +0000 Subject: [PATCH 90/91] strip build metadata from the application version as per SemVer spec https://semver.org/#spec-item-10 --- ThunderstoreCLI/Utils/MiscUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ThunderstoreCLI/Utils/MiscUtils.cs b/ThunderstoreCLI/Utils/MiscUtils.cs index 3e93f21..627863e 100644 --- a/ThunderstoreCLI/Utils/MiscUtils.cs +++ b/ThunderstoreCLI/Utils/MiscUtils.cs @@ -23,8 +23,8 @@ public static int[] GetCurrentVersion() throw new Exception("Reading app version from assembly failed"); } - // Drop possible pre-release cruft ("-alpha.0.1") from the end. - var versionParts = version.Split('-')[0].Split('.'); + // Drop possible pre-release or build metadata cruft ("-alpha.0.1", "+abcde") from the end. + var versionParts = version.Split('-', '+')[0].Split('.'); if (versionParts is null || versionParts.Length != 3) { From 8b2e1dba1541466e4070024eb6616b5367e7704f Mon Sep 17 00:00:00 2001 From: Digitalroot Date: Mon, 10 Jun 2024 00:30:24 -0700 Subject: [PATCH 91/91] Update PackageListingV1.cs to fix #103 This might be all that is needed to fix #103 --- ThunderstoreCLI/Models/PackageListingV1.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ThunderstoreCLI/Models/PackageListingV1.cs b/ThunderstoreCLI/Models/PackageListingV1.cs index 0f07ac4..b4b9f94 100644 --- a/ThunderstoreCLI/Models/PackageListingV1.cs +++ b/ThunderstoreCLI/Models/PackageListingV1.cs @@ -104,7 +104,7 @@ public class PackageVersionV1 public string? Uuid4 { get; set; } [JsonProperty("file_size")] - public int FileSize { get; set; } + public long FileSize { get; set; } [JsonIgnore] private GroupCollection? _fullNameParts;