From da51873341d541df3da09f4c81bb91d517b6ccff Mon Sep 17 00:00:00 2001 From: Aigio Liu Date: Tue, 16 Nov 2021 15:53:53 +0800 Subject: [PATCH] =?UTF-8?q?#475=20Linux=20=E5=8F=91=E5=B8=83=E6=89=93?= =?UTF-8?q?=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 1 + README.md | 1 + SteamToolsV2+.sln | 33 ++- src/Common.CoreLib/FileEx.cs | 14 + .../ST.Client.Linux.csproj | 2 +- .../AppHostPatcher.Program.cs | 157 ++++++++++ src/ST.Tools.Publish/MSBuild/ITaskItem.cs | 28 ++ .../MSBuild/MessageImportance.cs | 30 ++ .../MSBuild/RequiredAttribute.cs | 14 + src/ST.Tools.Publish/MSBuild/Task.cs | 25 ++ src/ST.Tools.Publish/MSBuild/TaskItem.cs | 81 ++++++ .../MSBuild/TaskLoggingHelper.cs | 50 ++++ .../Models/AppIdWithPublicKey.cs | 15 + src/ST.Tools.Publish/Models/PublishDirInfo.cs | 43 +++ .../Models/PublishFileInfo.cs | 59 ++++ src/ST.Tools.Publish/Models/ToolConfig.cs | 78 +++++ .../Packaging.Targets/ArchiveBuilder2.cs | 18 ++ src/ST.Tools.Publish/Program.Main.cs | 45 +++ src/ST.Tools.Publish/Program.cs | 15 + .../Properties/SR.Designer.cs | 72 +++++ src/ST.Tools.Publish/Properties/SR.resx | 124 ++++++++ src/ST.Tools.Publish/README.md | 76 +++++ src/ST.Tools.Publish/ST.Tools.Publish.csproj | 98 +++++++ src/ST.Tools.Publish/Steam++.desktop | 5 + src/ST.Tools.Publish/Steps/Step0(Template).cs | 20 ++ src/ST.Tools.Publish/Steps/Step07.cs | 19 ++ src/ST.Tools.Publish/Steps/Step12.cs | 38 +++ .../Steps/StepAppHostPatcher.cs | 112 +++++++ .../Steps/StepReadMSALClientId.cs | 49 ++++ src/ST.Tools.Publish/Steps/Step_deb.cs | 149 ++++++++++ src/ST.Tools.Publish/Steps/Step_readdesc.cs | 30 ++ src/ST.Tools.Publish/Steps/Step_readrsa.cs | 72 +++++ src/ST.Tools.Publish/Steps/Step_rel.cs | 90 ++++++ src/ST.Tools.Publish/Steps/Step_rpm.cs | 131 +++++++++ src/ST.Tools.Publish/Steps/Step_start.cs | 200 +++++++++++++ src/ST.Tools.Publish/Steps/Step_tgz_7z_tbr.cs | 156 ++++++++++ src/ST.Tools.Publish/Utils.cs | 273 ++++++++++++++++++ 37 files changed, 2421 insertions(+), 2 deletions(-) create mode 100644 src/ST.Tools.Publish/AppHostPatcher.Program.cs create mode 100644 src/ST.Tools.Publish/MSBuild/ITaskItem.cs create mode 100644 src/ST.Tools.Publish/MSBuild/MessageImportance.cs create mode 100644 src/ST.Tools.Publish/MSBuild/RequiredAttribute.cs create mode 100644 src/ST.Tools.Publish/MSBuild/Task.cs create mode 100644 src/ST.Tools.Publish/MSBuild/TaskItem.cs create mode 100644 src/ST.Tools.Publish/MSBuild/TaskLoggingHelper.cs create mode 100644 src/ST.Tools.Publish/Models/AppIdWithPublicKey.cs create mode 100644 src/ST.Tools.Publish/Models/PublishDirInfo.cs create mode 100644 src/ST.Tools.Publish/Models/PublishFileInfo.cs create mode 100644 src/ST.Tools.Publish/Models/ToolConfig.cs create mode 100644 src/ST.Tools.Publish/Packaging.Targets/ArchiveBuilder2.cs create mode 100644 src/ST.Tools.Publish/Program.Main.cs create mode 100644 src/ST.Tools.Publish/Program.cs create mode 100644 src/ST.Tools.Publish/Properties/SR.Designer.cs create mode 100644 src/ST.Tools.Publish/Properties/SR.resx create mode 100644 src/ST.Tools.Publish/README.md create mode 100644 src/ST.Tools.Publish/ST.Tools.Publish.csproj create mode 100644 src/ST.Tools.Publish/Steam++.desktop create mode 100644 src/ST.Tools.Publish/Steps/Step0(Template).cs create mode 100644 src/ST.Tools.Publish/Steps/Step07.cs create mode 100644 src/ST.Tools.Publish/Steps/Step12.cs create mode 100644 src/ST.Tools.Publish/Steps/StepAppHostPatcher.cs create mode 100644 src/ST.Tools.Publish/Steps/StepReadMSALClientId.cs create mode 100644 src/ST.Tools.Publish/Steps/Step_deb.cs create mode 100644 src/ST.Tools.Publish/Steps/Step_readdesc.cs create mode 100644 src/ST.Tools.Publish/Steps/Step_readrsa.cs create mode 100644 src/ST.Tools.Publish/Steps/Step_rel.cs create mode 100644 src/ST.Tools.Publish/Steps/Step_rpm.cs create mode 100644 src/ST.Tools.Publish/Steps/Step_start.cs create mode 100644 src/ST.Tools.Publish/Steps/Step_tgz_7z_tbr.cs create mode 100644 src/ST.Tools.Publish/Utils.cs diff --git a/README.en.md b/README.en.md index 76925a96b..bf283b1a1 100644 --- a/README.en.md +++ b/README.en.md @@ -226,6 +226,7 @@ Read what we [milestones](https://github.com/SteamTools-Team/SteamTools/mileston * [QR Code Generator for .NET](https://github.com/manuelbl/QrCodeGenerator) * [TinyPinyin](https://github.com/promeG/TinyPinyin) * [TinyPinyin.Net](https://github.com/hueifeng/TinyPinyin.Net) +* [Packaging utilities for .NET Core](https://github.com/qmfrederik/dotnet-packaging) * [React](https://github.com/facebook/react) * [Ant Design](https://github.com/ant-design/ant-design) * [Ant Design Blazor](https://github.com/ant-design-blazor/ant-design-blazor) diff --git a/README.md b/README.md index 3bec20704..151b05d44 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,7 @@ OS | Version | Architectures | * [QR Code Generator for .NET](https://github.com/manuelbl/QrCodeGenerator) * [TinyPinyin](https://github.com/promeG/TinyPinyin) * [TinyPinyin.Net](https://github.com/hueifeng/TinyPinyin.Net) +* [Packaging utilities for .NET Core](https://github.com/qmfrederik/dotnet-packaging) * [React](https://github.com/facebook/react) * [Ant Design](https://github.com/ant-design/ant-design) * [Ant Design Blazor](https://github.com/ant-design-blazor/ant-design-blazor) diff --git a/SteamToolsV2+.sln b/SteamToolsV2+.sln index 3f3e82393..120c027a1 100644 --- a/SteamToolsV2+.sln +++ b/SteamToolsV2+.sln @@ -133,6 +133,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.X11", "src\Avaloni EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Reactive", "references\reactive\Rx.NET\Source\src\System.Reactive\System.Reactive.csproj", "{21C392C5-AE41-421A-A127-1A03C4ACA2AD}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ST.Tools.Publish", "src\ST.Tools.Publish\ST.Tools.Publish.csproj", "{29DE5086-0690-4AE7-9BA9-840F54B249DC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1523,6 +1525,34 @@ Global {21C392C5-AE41-421A-A127-1A03C4ACA2AD}.Release|x64.Build.0 = Release|Any CPU {21C392C5-AE41-421A-A127-1A03C4ACA2AD}.Release|x86.ActiveCfg = Release|Any CPU {21C392C5-AE41-421A-A127-1A03C4ACA2AD}.Release|x86.Build.0 = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|ARM.ActiveCfg = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|ARM.Build.0 = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|ARM64.Build.0 = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|iPhone.Build.0 = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|x64.ActiveCfg = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|x64.Build.0 = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|x86.ActiveCfg = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Debug|x86.Build.0 = Debug|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|Any CPU.Build.0 = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|ARM.ActiveCfg = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|ARM.Build.0 = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|ARM64.ActiveCfg = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|ARM64.Build.0 = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|iPhone.ActiveCfg = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|iPhone.Build.0 = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|x64.ActiveCfg = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|x64.Build.0 = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|x86.ActiveCfg = Release|Any CPU + {29DE5086-0690-4AE7-9BA9-840F54B249DC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1578,9 +1608,10 @@ Global {C8288498-CCAD-4274-9DDF-BD52201D1B85} = {910D6A0F-42AB-4B65-B532-852E864EC006} {E85E92BA-202F-42D3-8C5E-96DA452501B7} = {910D6A0F-42AB-4B65-B532-852E864EC006} {21C392C5-AE41-421A-A127-1A03C4ACA2AD} = {673FF8CD-9E85-4FA1-ABFC-D1149675E138} + {29DE5086-0690-4AE7-9BA9-840F54B249DC} = {E448F863-4C9D-47A4-BC4A-4A1381B54DA4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9313F2B4-10DA-43AA-A481-CAA7571C740C} RESX_NeutralResourcesLanguage = zh-Hans + SolutionGuid = {9313F2B4-10DA-43AA-A481-CAA7571C740C} EndGlobalSection EndGlobal diff --git a/src/Common.CoreLib/FileEx.cs b/src/Common.CoreLib/FileEx.cs index 94af590e3..df7291df8 100644 --- a/src/Common.CoreLib/FileEx.cs +++ b/src/Common.CoreLib/FileEx.cs @@ -77,6 +77,20 @@ namespace System public const string DownloadCache = ".download_cache"; + public const string RPM = ".rpm"; + + public const string CPIO = ".cpio"; + + public const string DEB = ".deb"; + + public const string DEB_TAR = ".deb.tar"; + + public const string DEB_TAR_XZ = ".deb.tar.xz"; + + public const string PKG = ".pkg"; + + public const string MSI = ".msi"; + public static string Clean(string extension, bool trimLeadingPeriod = false) { if (string.IsNullOrWhiteSpace(extension)) diff --git a/src/ST.Client.Desktop.Linux/ST.Client.Linux.csproj b/src/ST.Client.Desktop.Linux/ST.Client.Linux.csproj index 59f55ba65..4e6e2fea1 100644 --- a/src/ST.Client.Desktop.Linux/ST.Client.Linux.csproj +++ b/src/ST.Client.Desktop.Linux/ST.Client.Linux.csproj @@ -31,7 +31,7 @@ PreserveNewest - Steam.ico + Steam++.ico PreserveNewest diff --git a/src/ST.Tools.Publish/AppHostPatcher.Program.cs b/src/ST.Tools.Publish/AppHostPatcher.Program.cs new file mode 100644 index 000000000..56ca7fb6c --- /dev/null +++ b/src/ST.Tools.Publish/AppHostPatcher.Program.cs @@ -0,0 +1,157 @@ +/* + Copyright (C) 2014-2019 de4dot@gmail.com + + This file is part of dnSpy + + dnSpy is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + dnSpy is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with dnSpy. If not, see . +*/ + +using System; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace AppHostPatcher +{ + /// + /// https://github.com/dnSpy/dnSpy/blob/master/Build/AppHostPatcher/Program.cs + /// + static class Program + { + static void Usage() + { + Console.WriteLine("apphostpatcher "); + Console.WriteLine("apphostpatcher "); + Console.WriteLine("apphostpatcher -d "); + Console.WriteLine("example: apphostpatcher my.exe -d bin"); + } + + const int maxPathBytes = 1024; + + static string ChangeExecutableExtension(string apphostExe) => + // Windows apphosts have an .exe extension. Don't call Path.ChangeExtension() unless it's guaranteed + // to have an .exe extension, eg. 'some.file' => 'some.file.dll', not 'some.dll' + apphostExe.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? Path.ChangeExtension(apphostExe, ".dll") : apphostExe + ".dll"; + + static string GetPathSeparator(string apphostExe) => + apphostExe.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? @"\" : "/"; + + public static int M(string[] args) + { + try + { + string apphostExe, origPath, newPath; + if (args.Length == 3) + { + if (args[1] == "-d") + { + apphostExe = args[0]; + origPath = Path.GetFileName(ChangeExecutableExtension(apphostExe)); + newPath = args[2] + GetPathSeparator(apphostExe) + origPath; + } + else + { + apphostExe = args[0]; + origPath = args[1]; + newPath = args[2]; + } + } + else if (args.Length == 2) + { + apphostExe = args[0]; + origPath = Path.GetFileName(ChangeExecutableExtension(apphostExe)); + newPath = args[1]; + } + else + { + Usage(); + return 1; + } + if (!File.Exists(apphostExe)) + { + Console.WriteLine($"Apphost '{apphostExe}' does not exist"); + return 1; + } + if (origPath == string.Empty) + { + Console.WriteLine("Original path is empty"); + return 1; + } + var origPathBytes = Encoding.UTF8.GetBytes(origPath + "\0"); + Debug.Assert(origPathBytes.Length > 0); + var newPathBytes = Encoding.UTF8.GetBytes(newPath + "\0"); + if (origPathBytes.Length > maxPathBytes) + { + Console.WriteLine($"Original path is too long"); + return 1; + } + if (newPathBytes.Length > maxPathBytes) + { + Console.WriteLine($"New path is too long"); + return 1; + } + + var apphostExeBytes = File.ReadAllBytes(apphostExe); + int offset = GetOffset(apphostExeBytes, origPathBytes); + if (offset < 0) + { + Console.WriteLine($"Could not find original path '{origPath}'"); + return 1; + } + if (offset + newPathBytes.Length > apphostExeBytes.Length) + { + Console.WriteLine($"New path is too long: {newPath}"); + return 1; + } + for (int i = 0; i < newPathBytes.Length; i++) + apphostExeBytes[offset + i] = newPathBytes[i]; + File.WriteAllBytes(apphostExe, apphostExeBytes); + return 0; + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + return 1; + } + } + + static int GetOffset(byte[] bytes, byte[] pattern) + { + int si = 0; + var b = pattern[0]; + while (si < bytes.Length) + { + si = Array.IndexOf(bytes, b, si); + if (si < 0) + break; + if (Match(bytes, si, pattern)) + return si; + si++; + } + return -1; + } + + static bool Match(byte[] bytes, int index, byte[] pattern) + { + if (index + pattern.Length > bytes.Length) + return false; + for (int i = 0; i < pattern.Length; i++) + { + if (bytes[index + i] != pattern[i]) + return false; + } + return true; + } + } +} \ No newline at end of file diff --git a/src/ST.Tools.Publish/MSBuild/ITaskItem.cs b/src/ST.Tools.Publish/MSBuild/ITaskItem.cs new file mode 100644 index 000000000..517224f70 --- /dev/null +++ b/src/ST.Tools.Publish/MSBuild/ITaskItem.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Build.Framework +{ + public interface ITaskItem + { + /// + /// 获取或设置项“规格”,例如对于基于磁盘的项,这将是文件路径。 + /// + string ItemSpec { get; set; } + + /// + /// 获取该项上所有元数据的名称。 包含内置元数据,如“FullPath”。 + /// + ICollection MetadataNames { get; } + + /// + /// 允许查询此项的元数据的值。 + /// + /// + /// + string GetMetadata(string metadataName); + } +} diff --git a/src/ST.Tools.Publish/MSBuild/MessageImportance.cs b/src/ST.Tools.Publish/MSBuild/MessageImportance.cs new file mode 100644 index 000000000..a819a4ba8 --- /dev/null +++ b/src/ST.Tools.Publish/MSBuild/MessageImportance.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Build.Framework +{ + /// + /// 此枚举针对消息提供 3 个重要性级别。 + /// + [Serializable] + public enum MessageImportance + { + /// + /// 重要性高,出现在详细程度较低的日志中 + /// + High, + + /// + /// 重要性一般 + /// + Normal, + + /// + /// 重要性低,出现在详细程度较高的日志中 + /// + Low, + } +} diff --git a/src/ST.Tools.Publish/MSBuild/RequiredAttribute.cs b/src/ST.Tools.Publish/MSBuild/RequiredAttribute.cs new file mode 100644 index 000000000..81e4056a3 --- /dev/null +++ b/src/ST.Tools.Publish/MSBuild/RequiredAttribute.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Build.Framework +{ + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public sealed class RequiredAttribute : Attribute + { + + } +} diff --git a/src/ST.Tools.Publish/MSBuild/Task.cs b/src/ST.Tools.Publish/MSBuild/Task.cs new file mode 100644 index 000000000..2f137369a --- /dev/null +++ b/src/ST.Tools.Publish/MSBuild/Task.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Build.Utilities +{ + /// + /// 此帮助器基类提供用于任务的默认功能。 此类只能以派生形式实例化。 + /// + public abstract class Task + { + /// + /// 获取包含任务记录方法的 TaskLoggingHelper 类的实例。 taskLoggingHelper 是一个 MarshallByRef 对象,如果父任务正在创建 appdomain 并将该对象封送到其中,则该对象需要调用 MarkAsInactive。 如果在任务执行结束时未卸载 appdomain,并且未调用 MarkAsInactive 方法,则将导致在其中创建任务的 appdomain 中的任务实例泄漏。 + /// + public TaskLoggingHelper Log => TaskLoggingHelper.Instance; + + /// + /// 必须由派生类实现。 + /// + /// + public abstract bool Execute(); + } +} diff --git a/src/ST.Tools.Publish/MSBuild/TaskItem.cs b/src/ST.Tools.Publish/MSBuild/TaskItem.cs new file mode 100644 index 000000000..302f40006 --- /dev/null +++ b/src/ST.Tools.Publish/MSBuild/TaskItem.cs @@ -0,0 +1,81 @@ +using Microsoft.Build.Framework; +using System.Collections.Generic; + +namespace Microsoft.Build.Utilities +{ + public sealed class TaskItem : ITaskItem + { + public TaskItem(string itemSpec) + { + ItemSpec = itemSpec; + } + + public TaskItem(string itemSpec, IDictionary? metadatas) : this(itemSpec) + { + Metadatas = metadatas; + } + + public string ItemSpec { get; set; } + + public IDictionary? Metadatas { get; set; } + + internal const string FullPath = "FullPath"; + internal const string RootDir = "RootDir"; + internal const string Filename = "Filename"; + internal const string Extension = "Extension"; + internal const string RelativeDir = "RelativeDir"; + internal const string Directory = "Directory"; + internal const string RecursiveDir = "RecursiveDir"; + internal const string Identity = "Identity"; + internal const string ModifiedTime = "ModifiedTime"; + internal const string CreatedTime = "CreatedTime"; + internal const string AccessedTime = "AccessedTime"; + internal const string DefiningProjectFullPath = "DefiningProjectFullPath"; + internal const string DefiningProjectDirectory = "DefiningProjectDirectory"; + internal const string DefiningProjectName = "DefiningProjectName"; + internal const string DefiningProjectExtension = "DefiningProjectExtension"; + + internal static readonly string[] All = + { + FullPath, + RootDir, + Filename, + Extension, + RelativeDir, + Directory, + RecursiveDir, // <-- Not derivable. + Identity, + ModifiedTime, + CreatedTime, + AccessedTime, + DefiningProjectFullPath, + DefiningProjectDirectory, + DefiningProjectName, + DefiningProjectExtension + }; + + public ICollection MetadataNames + { + get + { + if (Metadatas == null) return All; + var list = new HashSet(All); + foreach (var item in Metadatas.Keys) + { + list.Add(item); + } + return list; + } + } + + public string GetMetadata(string metadataName) + { + if (Metadatas == null) return null; + if (Metadatas.ContainsKey(metadataName)) + { + return Metadatas[metadataName]; + } + return null; + } + } +} diff --git a/src/ST.Tools.Publish/MSBuild/TaskLoggingHelper.cs b/src/ST.Tools.Publish/MSBuild/TaskLoggingHelper.cs new file mode 100644 index 000000000..9a5038cbb --- /dev/null +++ b/src/ST.Tools.Publish/MSBuild/TaskLoggingHelper.cs @@ -0,0 +1,50 @@ +using Microsoft.Build.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Build.Utilities +{ + public class TaskLoggingHelper + { + private TaskLoggingHelper() { } + + public static TaskLoggingHelper Instance { get; } = new(); + + const string TAG = "MSBuild"; + + /// + /// 使用指定的字符串记录警告。 线程安全。 + /// + /// + /// + public void LogWarning(string message, params object[] messageArgs) + { + Log.Warn(TAG, message, messageArgs); + } + + /// + /// 使用指定的字符串记录具有给定重要性的消息。 线程安全。 + /// + /// + /// + /// + public void LogMessage(MessageImportance importance, string message, params object[] messageArgs) + { + switch (importance) + { + case MessageImportance.High: + Log.Warn(TAG, message, messageArgs); + break; + case MessageImportance.Normal: + Log.Info(TAG, message, messageArgs); + break; + case MessageImportance.Low: + Log.Debug(TAG, message, messageArgs); + break; + } + } + } +} diff --git a/src/ST.Tools.Publish/Models/AppIdWithPublicKey.cs b/src/ST.Tools.Publish/Models/AppIdWithPublicKey.cs new file mode 100644 index 000000000..65fa336c4 --- /dev/null +++ b/src/ST.Tools.Publish/Models/AppIdWithPublicKey.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace System.Application.Models +{ + public class AppIdWithPublicKey + { + public Guid AppId { get; set; } + + public string? PublicKey { get; set; } + } +} diff --git a/src/ST.Tools.Publish/Models/PublishDirInfo.cs b/src/ST.Tools.Publish/Models/PublishDirInfo.cs new file mode 100644 index 000000000..7fb035742 --- /dev/null +++ b/src/ST.Tools.Publish/Models/PublishDirInfo.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace System.Application.Models +{ + /// + /// 发布文件夹信息 + /// + internal class PublishDirInfo + { + public PublishDirInfo() + { + } + + public PublishDirInfo(string dirName, string dirPath, DeploymentMode deploymentMode) + { + Name = dirName; + Path = dirPath; + DeploymentMode = deploymentMode; + } + + /// + /// 发布文件夹名 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 发布文件夹路径 + /// + public string Path { get; set; } = string.Empty; + + /// + /// 文件列表 + /// + public List Files { get; set; } = new(); + + /// + /// 单文件打包哈希值 + /// + public Dictionary BuildDownloads { get; set; } = new(); + + public DeploymentMode DeploymentMode { get; set; } + } +} \ No newline at end of file diff --git a/src/ST.Tools.Publish/Models/PublishFileInfo.cs b/src/ST.Tools.Publish/Models/PublishFileInfo.cs new file mode 100644 index 000000000..56f7a065d --- /dev/null +++ b/src/ST.Tools.Publish/Models/PublishFileInfo.cs @@ -0,0 +1,59 @@ +using System.IO; + +namespace System.Application.Models +{ + /// + /// 发布文件信息 + /// + internal class PublishFileInfo + { + public PublishFileInfo() + { + } + + const string CEF = "CEF"; + const string Bin = "Bin"; + + public PublishFileInfo(string path, string relativeTo) + { + Path = path; + FileEx = IO.Path.GetExtension(path); + Length = new FileInfo(path).Length; + RelativePath = IO.Path.GetRelativePath(relativeTo, path); + if (RelativePath.StartsWith(CEF, StringComparison.OrdinalIgnoreCase)) + { + RelativePath = Bin + IO.Path.DirectorySeparatorChar + CEF + RelativePath.Substring(CEF.Length, RelativePath.Length - CEF.Length); + } + } + + /// + /// 文件路径 + /// + public string Path { get; set; } = string.Empty; + + /// + /// 文件相对路径 + /// + public string RelativePath { get; set; } = string.Empty; + + /// + /// 文件原哈希值 + /// + public string SHA256 { get; set; } = string.Empty; + + /// + /// 文件扩展名 + /// + public string FileEx { get; set; } = string.Empty; + + /// + /// 数字签名后的哈希值 + /// + public string DigitalSignSHA256 { get; set; } = string.Empty; + + /// + /// 文件大小 + /// + public long Length { get; set; } + } +} \ No newline at end of file diff --git a/src/ST.Tools.Publish/Models/ToolConfig.cs b/src/ST.Tools.Publish/Models/ToolConfig.cs new file mode 100644 index 000000000..03729b30a --- /dev/null +++ b/src/ST.Tools.Publish/Models/ToolConfig.cs @@ -0,0 +1,78 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; + +namespace System.Application.Models +{ + internal sealed class ToolConfig : IExplicitHasValue + { + const string fileName = "Config.json"; + static readonly string filePath = Path.Combine(IOPath.AppDataDirectory, fileName); + static readonly Lazy value = new(GetConfig); + + /// + /// 当前工具配置文件,位于程序目录下 + /// + public static ToolConfig Value => value.Value; + + static ToolConfig GetConfig() + { + if (File.Exists(filePath)) + { + try + { + var jsonStr = File.ReadAllText(filePath); + return Serializable.DJSON(jsonStr) ?? new(); + } + catch + { + } + } + + return new ToolConfig(); + } + + public void Save() + { + var jsonStr = Serializable.SJSON(this, writeIndented: true); + IOPath.FileIfExistsItDelete(filePath); + File.WriteAllText(filePath, jsonStr); + } + + static void Init() + { + if (!File.Exists(filePath)) + { + Value.Save(); + } + } + + public static void AddInitCommand(RootCommand rootCommand) + { + var init = new Command("init", "初始化") + { + Handler = CommandHandler.Create(() => + { + Init(); + }) + }; + rootCommand.Add(init); + } + + /// + /// 云端数据库连接字符串 + /// + public string ConnectionString { get; set; } = string.Empty; + + /// + /// Web目录,CDN静态站点目录 + /// + public string WwwRootPath { get; set; } = string.Empty; + + bool IExplicitHasValue.ExplicitHasValue() + { + return !string.IsNullOrWhiteSpace(ConnectionString) && + !string.IsNullOrWhiteSpace(WwwRootPath); + } + } +} \ No newline at end of file diff --git a/src/ST.Tools.Publish/Packaging.Targets/ArchiveBuilder2.cs b/src/ST.Tools.Publish/Packaging.Targets/ArchiveBuilder2.cs new file mode 100644 index 000000000..8cf568f62 --- /dev/null +++ b/src/ST.Tools.Publish/Packaging.Targets/ArchiveBuilder2.cs @@ -0,0 +1,18 @@ +using Microsoft.Build.Framework; +using Packaging.Targets.IO; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Packaging.Targets +{ + class ArchiveBuilder2 : ArchiveBuilder + { + public new void AddFile(string entry, string relativePath, string prefix, List value, ITaskItem[] metadata) + { + base.AddFile(entry, relativePath, prefix, value, metadata); + } + } +} diff --git a/src/ST.Tools.Publish/Program.Main.cs b/src/ST.Tools.Publish/Program.Main.cs new file mode 100644 index 000000000..11ae60cb2 --- /dev/null +++ b/src/ST.Tools.Publish/Program.Main.cs @@ -0,0 +1,45 @@ +using System; +using System.CommandLine; +using System.Linq; +using System.Reflection; + +static partial class Program +{ + static int _(string[] args, + string description = "", + Assembly? assembly = null, + string @namespace = "System.Application.Steps", + string methodName = "Add", + Func? init = null, + Action? action = null) + { + try + { + var result = init?.Invoke(); + if (result.HasValue) return result.Value; + + var rootCommand = new RootCommand(description); + + action?.Invoke(rootCommand); + + var steps = (assembly ?? Assembly.GetCallingAssembly()).GetTypes() + .Where(x => x.Namespace == @namespace && x.IsClass && !x.IsGenericType && x.IsAbstract && x.IsSealed); + foreach (var step in steps) + { + var method = step.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(RootCommand) }, null); + method?.Invoke(null, new[] { rootCommand }); + } + + return rootCommand.InvokeAsync(args).Result; + } + catch (Exception? e) + { + while (e != null) + { + Console.WriteLine(e.ToString()); + e = e.InnerException; + } + return 500; + } + } +} \ No newline at end of file diff --git a/src/ST.Tools.Publish/Program.cs b/src/ST.Tools.Publish/Program.cs new file mode 100644 index 000000000..9da9fbd9c --- /dev/null +++ b/src/ST.Tools.Publish/Program.cs @@ -0,0 +1,15 @@ +using System.Application.Models; +using System.Linq; + +return _(args, "客户端发布命令行工具(Command Line Tools/CLT)", init: () => +{ + if (args.FirstOrDefault() == "apphostpatcher") + { + var args_ = args.Skip(1).ToArray(); + return AppHostPatcher.Program.M(args_); + } + return null; +}, action: rootCommand => +{ + ToolConfig.AddInitCommand(rootCommand); +}); \ No newline at end of file diff --git a/src/ST.Tools.Publish/Properties/SR.Designer.cs b/src/ST.Tools.Publish/Properties/SR.Designer.cs new file mode 100644 index 000000000..24e513d1f --- /dev/null +++ b/src/ST.Tools.Publish/Properties/SR.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// 此代码由工具生成。 +// 运行时版本:4.0.30319.42000 +// +// 对此文件的更改可能会导致不正确的行为,并且如果 +// 重新生成代码,这些更改将会丢失。 +// +//------------------------------------------------------------------------------ + +namespace System.Application.Properties { + using System; + + + /// + /// 一个强类型的资源类,用于查找本地化的字符串等。 + /// + // 此类是由 StronglyTypedResourceBuilder + // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 + // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen + // (以 /str 作为命令选项),或重新生成 VS 项目。 + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class SR { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal SR() { + } + + /// + /// 返回此类使用的缓存的 ResourceManager 实例。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Application.Properties.SR", typeof(SR).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// 重写当前线程的 CurrentUICulture 属性,对 + /// 使用此强类型资源类的所有资源查找执行重写。 + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// 查找类似 {"z":"QXHucc-1AMcFOqOoEz_n7XQHm2Uq-hliGbGaR470kHRDIopkAzb77geEKXUnYF-_roPR5eLLMCTD2bsOH_StcgpA8EC_eeBg21IWd6namxMU5Xv_rDnYMQhKZMtbn4iMTggPZy90H5eXd0yqx8p_YUknUUgLx--yW5ar1J0eXcW1RG6Yq2LN22pb-eViQ7EUuTu5GYMPR5gJvPebpQuc4p7IBcCehAqVBQfUxN18D_jc40-fX75PkqLBveQbzI9TZLdJMH7g4Dce7Ps3BGt1DB3YYBk3lkf8wVoIYrG8UuhUwXHL6Uanr7Og_Gr_I97gDN5D39zZTKBC80-NpDiM_Q","x":"hlJZZxmEK8bO8ueNxAjZXeifSG5qMAoU5CFiriejwY2QUMItkPr-7g0X4RJFaNpvxkJYBPi48lOCTSZyEPpSmDgy-kj2k_8iXrSeHG1o8Qo2-Cd4kRvtWRGJVPSeWkS1i7oIheMLIhTDWqTy3D5gZE3t3sWIy [字符串的其余部分被截断]"; 的本地化字符串。 + /// + internal static string DevRSAPrivateKey { + get { + return ResourceManager.GetString("DevRSAPrivateKey", resourceCulture); + } + } + } +} diff --git a/src/ST.Tools.Publish/Properties/SR.resx b/src/ST.Tools.Publish/Properties/SR.resx new file mode 100644 index 000000000..cf5882e39 --- /dev/null +++ b/src/ST.Tools.Publish/Properties/SR.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + {"z":"QXHucc-1AMcFOqOoEz_n7XQHm2Uq-hliGbGaR470kHRDIopkAzb77geEKXUnYF-_roPR5eLLMCTD2bsOH_StcgpA8EC_eeBg21IWd6namxMU5Xv_rDnYMQhKZMtbn4iMTggPZy90H5eXd0yqx8p_YUknUUgLx--yW5ar1J0eXcW1RG6Yq2LN22pb-eViQ7EUuTu5GYMPR5gJvPebpQuc4p7IBcCehAqVBQfUxN18D_jc40-fX75PkqLBveQbzI9TZLdJMH7g4Dce7Ps3BGt1DB3YYBk3lkf8wVoIYrG8UuhUwXHL6Uanr7Og_Gr_I97gDN5D39zZTKBC80-NpDiM_Q","x":"hlJZZxmEK8bO8ueNxAjZXeifSG5qMAoU5CFiriejwY2QUMItkPr-7g0X4RJFaNpvxkJYBPi48lOCTSZyEPpSmDgy-kj2k_8iXrSeHG1o8Qo2-Cd4kRvtWRGJVPSeWkS1i7oIheMLIhTDWqTy3D5gZE3t3sWIymcqZFYnAxkcjQ0","c":"6W4B77ef29PbXMuBaC0joRznd-mV9Qeia6-uKVneXALigV9h98zNp0cAIycdQPHWYiNJg5veg8oaBcliTkzeGQNomRjSGvzpV6c5efc0C0x2bVDaNEeusurbNUitN2G9NMkSXIg2mrbxIra-cERiTfO1Ux-DUcPwBlMO6e4VOtU","v":"AQAB","b":"aC7WvY-k73mayyaG9vXfeO0AgtyyBl6SBWXvjHPHsq1B_xLX2Od9Zwef1eH_O464hptrBmc9rblndxQAao3hDHh29T0QPUYnqWTMacwRPUDAEmFK7aCPIbBaVF671RPEYoqTVIERwULvCJjeqFxhiZAmxQH10nbQQiyLiDM2Czk","n":"u4iAALIX2NLyh9-GeWbBdNK2gpa1qFx1S2fFoRKuzxspTP4oyJ2uMF7xZ1yWogup-M7x3BPYrXMTzmyYTmJFDDsWt3nEl-mk6ABJ5RKeCUO1GfHXo8jvHod_pfs8gmFmyzSoYdxUp6BayXT7LkxZz9pO2ZEK2JU1dkSQKRyy_U9ceDsy9D-xsmkX2MrtYTdG51CxdFD_SrStI1YOEhbBv_A97JYJv_F5UyP2CGJ_zWG-MVWO-2Ir2AiVQMBR9PUfNweAGEAsm-AAnRHqIGHjOIMm8PqmkY4Lft2zwDE2TMcw-yUBOwYUvWanIKbO-T3KdBIc7ZbNe9Vut8aVWJa8UQ","m":"wdtawtOmr7Rh4Ii_YV_ygg7BJcUL12OHtStHOM41eRcXLu32NWEWxS2eUz6aDq3jKY98484nDSkZ4US3jfdiKeQrLMXcA_S3Otx_3W2RP2rai96Vtp_wbGoiYWHf8zV0NuJzEpzXePgsA7MuJ87SYo6YtpW1J-bzicuijzdkcGc","a":"96Yzwld5A82amprVia_muuchGTWCMd5uIb3zOUfIl6XAR-I-Cus6MTwtwp0p-vk5xwiG9jNhKXxBzvbPBfSySeNQCr9bPq4fGZaYKT7KwhjSfxbmBVyu_iy7ezzngiV_pEIaj_HjyEJr79Ie8b9g8OpW-OrALWIumycZ1rIDGoc"} + 仅用于测试环境RSA私钥 + + \ No newline at end of file diff --git a/src/ST.Tools.Publish/README.md b/src/ST.Tools.Publish/README.md new file mode 100644 index 000000000..5c46bb927 --- /dev/null +++ b/src/ST.Tools.Publish/README.md @@ -0,0 +1,76 @@ +# 客户端发布命令行工具(Command Line Tools/CLT) + +## 发布新版本操作指南 +- 说明 + - (✔)Linux 上执行程序,需要程序权限中有执行,在 WinSCP 工具中右键属性,打勾执行权限 + - 发布工具程序文件名为 p 简短名字方便打命令 + - Linux 上文件在 /home/{user}/pub 文件夹中 + - Linux 上使用 ./p xxxx 执行程序 + - Windows 上使用 CMD p xxxx 执行程序 + - 发布本程序的 win-x64 在发布文件夹中打开 终端 执行命令,不可将程序复制到项目文件夹之外的地方执行 + +### 1. (云端)得到 RSA 公钥信息(如果是新版本号则**创建**,如果是之前的版本号发布另一个平台则使用**获取**) +- 创建新版本号并返回 RSA 公钥 +
./p nv -v 2.0.0 -desc "1. xxx;2. yyy;3. ddd" -use_last_skey // 创建一个新的版本号并使用上一个版本的密钥
+
./p nv -v 2.0.0 -desc "1. xxx;2. yyy;3. ddd" // 创建一个新的版本号
+- 获取之前创建新版本号 RSA 公钥 +
./p gv -v 2.0.0 // 获取一个已有的版本号
+ +### 2. (本地)**手动**复制终端窗口中的 RSA 公钥值到剪切板 + +### 3. (本地)读取剪切板公钥值写入 txt 的 pfx 文件中 +- 从剪切板中读取 +
p rr -dev // 写入到测试环境中
+
p rr // 写入到正式环境中
+- 从命令行中传入 +
p rr -dev -val XXXX // 写入到测试环境中
+
p rr -val XXXX // 写入到正式环境中
+ +### 4. (本地)**手动**在VS中发布任意一个或多个平台配置(pubxml),后续可改成命令行自动发布 +- (本地)将发布 Host 入口点重定向到 Bin 目录中,仅支持 win-x64 +
p hostpath
+
p hp
+ +### 5. (本地)验证发布文件夹与统计文件 +- 统计所有文件(忽略xml,pdb)写入 **Publish.json** +- 命令示例 +
p sta -val "win-x64" // 仅发布 Win 平台 64位
+
p sta -val "android-arm android-arm64" // 仅发布 Android 平台 ARM系列
+
p sta -val "win-x64 osx-x64 osx-arm64 linux-x64 linux-arm64 linux-arm" // 发布多个桌面平台
+
p sta -dev -val "win-x64" // (测试环境/Debug)仅发布 Win 平台 64位
+
p sta -dev -val "win-x64 osx-x64 linux-x64" // (测试环境/Debug)发布多个桌面平台
+ +### 8. (本地)读取上一步操作后的 **Publish.json** 生成压缩包并计算哈希值写入 **Publish.json** +- 命令示例 +
p 7z // 打包 7z 格式压缩包
+ +### 8.1 (本地)读取上一步操作后的 **Publish.json** 生成安装包并计算哈希值写入 **Publish.json** + +#### rpm +- Create a CentOS/RedHat Linux installer +
p rpm
+ +#### deb +- Create a Ubuntu/Debian Linux installer +
p deb
+ +#### ~~pkg~~ +- Create a macOS installer +
p pkg
+ +#### ~~msi~~ +- Create a Windows Installer (msi) package +
p msi
+ +### 10. (本地)将 **Publish.json** 上传至云端 +- 将本地 ```ST.Tools.Publish\bin\Release\Publish\win-x64\AppData\Publish.json``` 复制到云端 ```/home/{user}/pub/AppData/Publish.json``` + +### 11. (云端)读取上一步上传的数据写入数据库中 +- 读取 **Publish.json** 写入数据库 +- 命令示例 +
./p wdb -v 2.0.0 -dev -dev_custom "url1;url2;url3" // 在测试环境中创建 2.0.0 版本发布数据并附加下载链接
+
./p wdb -v 2.0.0 -gitee "url1;url2;url3" // 在正式环境中创建 2.0.0 版本发布数据并附加码云下载链接
+ +### 12. (本地)读取 **Publish.json** 中的 SHA256 值写入 release-template.md +- 命令示例 +
p rel
\ No newline at end of file diff --git a/src/ST.Tools.Publish/ST.Tools.Publish.csproj b/src/ST.Tools.Publish/ST.Tools.Publish.csproj new file mode 100644 index 000000000..d879aa3ce --- /dev/null +++ b/src/ST.Tools.Publish/ST.Tools.Publish.csproj @@ -0,0 +1,98 @@ + + + + Exe + net6.0 + p + System.Application + ..\..\..\SteamTools\src\ST.Client.Desktop.Avalonia\Application\UI\Assets\Icon.ico + NOT_XE;NOT_MP;NOT_NJSON;NOT_DI;$(DefineConstants) + true + + + + + Properties\AssemblyInfo.cs + + + Properties\AssemblyInfo.Version.cs + + + FileSystemDesktop.cs + + + ProjectPathUtil2.cs + + + Packaging.Targets\Deb + + + Packaging.Targets\IO + + + Packaging.Targets\Native + + + Packaging.Targets\Rpm + + + Packaging.Targets + + + Packaging.Targets + + + Packaging.Targets + + + Packaging.Targets + + + Packaging.Targets + + + Packaging.Targets + + + Packaging.Targets + + + PreserveNewest + + + + + + + + + + + + + + + + + + + True + True + SR.resx + + + + + + ResXFileCodeGenerator + SR.Designer.cs + + + + + + PreserveNewest + + + + diff --git a/src/ST.Tools.Publish/Steam++.desktop b/src/ST.Tools.Publish/Steam++.desktop new file mode 100644 index 000000000..e74d6c8cd --- /dev/null +++ b/src/ST.Tools.Publish/Steam++.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Name=Steam++ +Type=Application +Exec=/usr/share/Steam++/Steam++ +Icon=/usr/share/Steam++/Steam++.ico \ No newline at end of file diff --git a/src/ST.Tools.Publish/Steps/Step0(Template).cs b/src/ST.Tools.Publish/Steps/Step0(Template).cs new file mode 100644 index 000000000..5550f9369 --- /dev/null +++ b/src/ST.Tools.Publish/Steps/Step0(Template).cs @@ -0,0 +1,20 @@ +//using System; +//using System.Collections.Generic; +//using System.CommandLine; +//using System.CommandLine.Invocation; +//using System.Linq; +//using System.Text; +//using System.Threading.Tasks; +//using static System.Application.Utils; +//using static System.ProjectPathUtil; + +//namespace System.Application.Steps +//{ +// internal static class Step0 +// { +// public static void Add(RootCommand command) +// { + +// } +// } +//} \ No newline at end of file diff --git a/src/ST.Tools.Publish/Steps/Step07.cs b/src/ST.Tools.Publish/Steps/Step07.cs new file mode 100644 index 000000000..c46c1e303 --- /dev/null +++ b/src/ST.Tools.Publish/Steps/Step07.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static System.Application.Utils; +using static System.ProjectPathUtil; + +namespace System.Application.Steps +{ + internal static class Step7 + { + public static void Add(RootCommand command) + { + } + } +} diff --git a/src/ST.Tools.Publish/Steps/Step12.cs b/src/ST.Tools.Publish/Steps/Step12.cs new file mode 100644 index 000000000..13118e9fb --- /dev/null +++ b/src/ST.Tools.Publish/Steps/Step12.cs @@ -0,0 +1,38 @@ +//using System.Application.Models; +//using System.Collections.Generic; +//using System.CommandLine; +//using System.CommandLine.Invocation; +//using System.IO; +//using System.Security.Cryptography; +//using static System.Application.Utils; + +//namespace System.Application.Steps +//{ +// internal static class Step12 +// { +// public static void Add(RootCommand command) +// { +// var fix = new Command("fix", "12. 修复文件复制不正确"); +// fix.AddOption(new Option("-path", "要扫描的文件路径")); +// fix.Handler = CommandHandler.Create((string path) => +// { +// List list = new List(); + +// ScanPath(path, list); + +// foreach (var file in list) +// { +// Console.WriteLine($"正在计算哈希值:{file.Path}"); +// using (var fileStream = File.OpenRead(file.Path)) +// { +// file.Length = fileStream.Length; +// file.SHA256 = Hashs.String.SHA256(fileStream); +// } +// var buildFileId = file.SHA256 + file.Length; +// File.Move(file.Path, Path.Combine(path, buildFileId + FileEx.BIN)); +// } +// }); +// command.AddCommand(fix); +// } +// } +//} \ No newline at end of file diff --git a/src/ST.Tools.Publish/Steps/StepAppHostPatcher.cs b/src/ST.Tools.Publish/Steps/StepAppHostPatcher.cs new file mode 100644 index 000000000..05e90d64b --- /dev/null +++ b/src/ST.Tools.Publish/Steps/StepAppHostPatcher.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using System.Linq; +using static System.ProjectPathUtil; +using static System.Application.Utils; + +namespace System.Application.Steps +{ + internal static class StepAppHostPatcher + { + const string DirName = "Bin"; + static readonly string[] ignoreDirNames = new[] { "Assets", IOPath.DirName_AppData, IOPath.DirName_Cache, "Logs", "Bin" }; + + static void Handler(DeploymentMode d) + { + var pubPath = d switch + { + DeploymentMode.SCD => projPath + DirPublishWinX64, + DeploymentMode.FDE => projPath + DirPublishWinX64_FDE, + _ => throw new ArgumentOutOfRangeException(nameof(d), d, null), + }; + + var exePaths = new[] { + pubPath + Path.DirectorySeparatorChar + "Steam++.exe", + //pubPath + Path.DirectorySeparatorChar + "Steam++.Console.exe" + }; + + foreach (var exePath in exePaths) + { + if (!File.Exists(exePath)) + { + Console.WriteLine($"错误:找不到 exe 文件,路径:{exePath}"); + return; + } + } + + foreach (var exePath in exePaths) + { + // apphostpatcher "C:\Code\2021\SteamTools\src\ST.Client.Desktop.Avalonia.App\bin\Release\Publish\win-x64\Steam++.exe" -d Bin + _ = AppHostPatcher.Program.M(new string[] { exePath, "-d", DirName }); + } + + static string[] GetFiles(string dirPath) + { + return Directory.GetFiles(dirPath) + .Where(x => (!string.Equals(Path.GetExtension(x), ".exe", StringComparison.OrdinalIgnoreCase) + || string.Equals(Path.GetFileName(x), "createdump.exe", StringComparison.OrdinalIgnoreCase)) + && !x.EndsWith(".VisualElementsManifest.xml", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } + static string[] GetDirs(string dirPath) + { + return Directory.GetDirectories(dirPath) + .Where(x => !ignoreDirNames.Contains(new DirectoryInfo(x).Name, StringComparer.OrdinalIgnoreCase)).ToArray(); + } + static List GetAllFiles(string dirPath, List? list = null) + { + list ??= new List(); + var files = GetFiles(dirPath); + list.AddRange(files); + var dirs = GetDirs(dirPath); + foreach (var dir in dirs) + { + _ = GetAllFiles(dir, list); + } + return list; + } + + var allFiles = GetAllFiles(pubPath); + foreach (var file in allFiles) + { + var rPath = Path.GetRelativePath(pubPath, file); + rPath = Path.Combine(DirName, rPath); + var newPath = pubPath + Path.DirectorySeparatorChar + rPath; + Console.WriteLine($"正在移动文件:{file} -> {newPath}"); + var rootDirPath = Path.GetDirectoryName(newPath); + if (!Directory.Exists(rootDirPath)) Directory.CreateDirectory(rootDirPath); + File.Move(file, newPath); + } + + void ClearEmptyDir() + { + var dirs = Directory.GetDirectories(pubPath); + foreach (var dir in dirs) + { + if (Directory.GetDirectories(dir).Length == 0 && Directory.GetFiles(dir).Length == 0) + { + Directory.Delete(dir); + } + } + } + ClearEmptyDir(); + + Console.WriteLine("OK"); + } + + public static void Add(RootCommand command) + { + var hp = new Command("hostpath", "X. (本地)将发布 Host 入口点重定向到 Bin 目录中"); + hp.AddAlias("hp"); + hp.Handler = CommandHandler.Create(() => + { + Handler(DeploymentMode.SCD); + Handler(DeploymentMode.FDE); + }); + command.AddCommand(hp); + } + } +} \ No newline at end of file diff --git a/src/ST.Tools.Publish/Steps/StepReadMSALClientId.cs b/src/ST.Tools.Publish/Steps/StepReadMSALClientId.cs new file mode 100644 index 000000000..173d50dd5 --- /dev/null +++ b/src/ST.Tools.Publish/Steps/StepReadMSALClientId.cs @@ -0,0 +1,49 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using TextCopy; +using static System.Application.Utils; +using static System.ProjectPathUtil; + +namespace System.Application.Steps +{ + internal static class StepReadMSALClientId + { + public static void Add(RootCommand command) + { + var rmci = new Command("read_msal_client_id", "读取 MSALClientId 写入txt的pfx文件中"); + rmci.AddAlias("rmci"); + rmci.AddOption(new Option("-val", () => string.Empty, "通过命令行将值传入")); + rmci.Handler = CommandHandler.Create((string val) => + { + if (string.IsNullOrWhiteSpace(val)) + { + var text = ClipboardService.GetText(); + if (string.IsNullOrWhiteSpace(text)) + { + Console.WriteLine("错误:读取剪切板值无效!"); + return; + } + else + { + val = text; + } + } + + if (!Guid.TryParse(val, out var guid)) + { + Console.WriteLine("错误:值无效!"); + return; + } + + var filePath = Path.Combine(projPath, "masl-client-id.pfx"); + IOPath.FileIfExistsItDelete(filePath); + File.WriteAllBytes(filePath, guid.ToByteArray()); + + Console.WriteLine("完成。"); + Console.WriteLine(filePath); + }); + command.AddCommand(rmci); + } + } +} \ No newline at end of file diff --git a/src/ST.Tools.Publish/Steps/Step_deb.cs b/src/ST.Tools.Publish/Steps/Step_deb.cs new file mode 100644 index 000000000..1c72eb34f --- /dev/null +++ b/src/ST.Tools.Publish/Steps/Step_deb.cs @@ -0,0 +1,149 @@ +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Packaging.Targets; +using Packaging.Targets.Deb; +using Packaging.Targets.IO; +using Packaging.Targets.Rpm; +using System.Application.Models; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using System.Linq; +using static System.Application.Utils; + +namespace System.Application.Steps +{ + internal static class Step_deb + { + static void Handler() + { + var publish_json_path = PublishJsonFilePath; + var publish_json_str = File.ReadAllText(publish_json_path); + var dirNames = Serializable.DJSON(publish_json_str); + + if (!dirNames.Any_Nullable()) + { + Console.WriteLine($"错误:发布配置文件读取失败!{publish_json_path}"); + return; + } + + dirNames = dirNames.ThrowIsNull(nameof(dirNames)); + + foreach (var item in dirNames) + { + var isLinux = item.Name.StartsWith("linux"); + if (!isLinux) continue; + var debPath = GetPackPath(item, FileEx.DEB); + var debTarPath = GetPackPath(item, FileEx.DEB_TAR); + var debTarXzPath = GetPackPath(item, FileEx.DEB_TAR_XZ); + using var targetStream = File.Open(debPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); + using var tarStream = File.Open(debTarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); + + ArchiveBuilder2 archiveBuilder2 = new() + { + Log = TaskLoggingHelper.Instance, + }; + ArchiveBuilder archiveBuilder = archiveBuilder2; + + var archiveEntries = archiveBuilder.FromDirectory( + item.Path, + Constants.HARDCODED_APP_NAME, + LinuxPackConstants.Prefix, + Array.Empty()); + + LinuxPackConstants.AddFileNameDesktop(archiveBuilder2, archiveEntries); + + //archiveEntries.AddRange(archiveBuilder.FromLinuxFolders(this.LinuxFolders)); + DebTask.EnsureDirectories(archiveEntries); + + archiveEntries = archiveEntries + .OrderBy(e => e.TargetPathWithFinalSlash, StringComparer.Ordinal) + .ToList(); + + TarFileCreator.FromArchiveEntries(archiveEntries, tarStream); + tarStream.Position = 0; + + // Prepare the list of dependencies + List dependencies = new List(); + + if (item.DeploymentMode == DeploymentMode.FDE) + { + dependencies.Add(LinuxPackConstants.dotnet_runtime_6_0); + } + + //if (this.DebDependencies != null) + //{ + // var debDependencies = this.DebDependencies.Select(d => d.ItemSpec).ToArray(); + + // dependencies.AddRange(debDependencies); + //} + + //if (this.DebDotNetDependencies != null) + //{ + // var debDotNetDependencies = this.DebDotNetDependencies.Select(d => d.ItemSpec).ToArray(); + + // dependencies.AddRange(debDotNetDependencies); + //} + + // Prepare the list of recommended dependencies + List recommends = new List(); + + //if (this.DebRecommends != null) + //{ + // recommends.AddRange(this.DebRecommends.Select(d => d.ItemSpec)); + //} + + // XZOutputStream class has low quality (doesn't even know it's current position, + // needs to be disposed to finish compression, etc), + // So we are doing compression in a separate step + using (var tarXzStream = File.Open(debTarXzPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None)) + using (var xzStream = new XZOutputStream(tarXzStream)) + { + tarStream.CopyTo(xzStream); + } + + using (var tarXzStream = File.Open(debTarXzPath, FileMode.Open, FileAccess.Read, FileShare.None)) + { + var pkg = DebPackageCreator.BuildDebPackage( + archiveEntries, + LinuxPackConstants.PackageName, + LinuxPackConstants.Description, + LinuxPackConstants.DebMaintainer, + Utils.Version, + DebTask.GetPackageArchitecture(item.Name), + LinuxPackConstants.CreateUser, + LinuxPackConstants.UserName, + LinuxPackConstants.InstallService, + LinuxPackConstants.ServiceName, + LinuxPackConstants.Prefix, + LinuxPackConstants.DebSection, + LinuxPackConstants.DebPriority, + LinuxPackConstants.DebHomepage, + LinuxPackConstants.PreInstallScript, + LinuxPackConstants.PostInstallScript, + LinuxPackConstants.PreRemoveScript, + LinuxPackConstants.PostRemoveScript, + dependencies, + recommends, + null!); + + DebPackageCreator.WriteDebPackage( + archiveEntries, + tarXzStream, + targetStream, + pkg); + } + } + + Console.WriteLine("完成"); + } + + public static void Add(RootCommand command) + { + var deb = new Command("deb", "Create a Ubuntu/Debian Linux installer"); + deb.Handler = CommandHandler.Create(Handler); + command.AddCommand(deb); + } + } +} diff --git a/src/ST.Tools.Publish/Steps/Step_readdesc.cs b/src/ST.Tools.Publish/Steps/Step_readdesc.cs new file mode 100644 index 000000000..5b276c6ab --- /dev/null +++ b/src/ST.Tools.Publish/Steps/Step_readdesc.cs @@ -0,0 +1,30 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using TextCopy; + +namespace System.Application.Steps +{ + internal static class Step_readdesc + { + public static void Add(RootCommand command) + { + var rd = new Command("readdesc", "X. (本地)读取剪切板更新日志生成一行数据复制到剪切板中"); + rd.AddAlias("rd"); + rd.Handler = CommandHandler.Create(() => + { + var text = ClipboardService.GetText(); + if (string.IsNullOrWhiteSpace(text)) + { + Console.WriteLine("错误:读取剪切板值无效!"); + return; + } + var array = text.Split(Environment.NewLine); + text = string.Join(';', array); + text = $"\"{text}\""; + ClipboardService.SetText(text); + Console.WriteLine("OK"); + }); + command.AddCommand(rd); + } + } +} \ No newline at end of file diff --git a/src/ST.Tools.Publish/Steps/Step_readrsa.cs b/src/ST.Tools.Publish/Steps/Step_readrsa.cs new file mode 100644 index 000000000..36d643ae9 --- /dev/null +++ b/src/ST.Tools.Publish/Steps/Step_readrsa.cs @@ -0,0 +1,72 @@ +using System.Application.Models; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using TextCopy; +using static System.Application.Utils; +using static System.ProjectPathUtil; + +namespace System.Application.Steps +{ + internal static class Step3 + { + public static void Add(RootCommand command) + { + var rr = new Command("readrsa", "3. (本地)读取剪切板公钥值写入txt的pfx文件中"); + rr.AddAlias("rr"); + rr.AddOption(new Option("-val", () => string.Empty, "通过命令行将公钥传入")); + rr.AddOption(new Option("-dev", DevDesc)); + rr.Handler = CommandHandler.Create((string val, bool dev) => + { + if (string.IsNullOrWhiteSpace(val)) + { + var text = ClipboardService.GetText(); + if (string.IsNullOrWhiteSpace(text)) + { + Console.WriteLine("错误:读取剪切板值无效!"); + return; + } + else + { + val = text; + } + } + + var value = Serializable.DJSON(val); + if (value == default) + { + Console.WriteLine("错误:读取剪切板值不能为 Null!"); + return; + } + if (value.AppId == default) + { + Console.WriteLine("错误:读取剪切板值 AppId 无效!"); + return; + } + if (string.IsNullOrWhiteSpace(value.PublicKey)) + { + Console.WriteLine("错误:读取剪切板值 PublicKey 无效!"); + return; + } + + static string Env(bool dev) => dev ? "debug" : "release"; + var env = Env(dev); + + var pfxFilePath = Path.Combine(projPath, + $"rsa-public-key-{env}.pfx"); + IOPath.FileIfExistsItDelete(pfxFilePath); + File.WriteAllText(pfxFilePath, value.PublicKey); + + var appIdFilePath = Path.Combine(projPath, + $"app-id-{env}.pfx"); + IOPath.FileIfExistsItDelete(appIdFilePath); + File.WriteAllBytes(appIdFilePath, value.AppId.ToByteArray()); + + Console.WriteLine("完成。"); + Console.WriteLine(pfxFilePath); + Console.WriteLine(appIdFilePath); + }); + command.AddCommand(rr); + } + } +} \ No newline at end of file diff --git a/src/ST.Tools.Publish/Steps/Step_rel.cs b/src/ST.Tools.Publish/Steps/Step_rel.cs new file mode 100644 index 000000000..f266dfa37 --- /dev/null +++ b/src/ST.Tools.Publish/Steps/Step_rel.cs @@ -0,0 +1,90 @@ +using System; +using System.Application.Models; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static System.Application.Utils; +using static System.ProjectPathUtil; + +namespace System.Application.Steps +{ + internal static class StepRel + { + public static void Add(RootCommand command) + { + var rel = new Command("rel", "12. (本地)读取 **Publish.json** 中的 SHA256 值写入 release-template.md") + { + Handler = CommandHandler.Create(() => + { + var publish_json_path = PublishJsonFilePath; + var publish_json_str = File.ReadAllText(publish_json_path); + var dirNames = Serializable.DJSON(publish_json_str); + + if (!dirNames.Any_Nullable()) + { + Console.WriteLine($"错误:发布配置文件读取失败!{publish_json_path}"); + return; + } + + dirNames = dirNames.ThrowIsNull(nameof(dirNames)); + + + var release_template_md_path = projPath + release_template_md; + var lines = File.ReadAllLines(release_template_md_path); + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i]; + var rows = line.Split('|', StringSplitOptions.RemoveEmptyEntries); + if (rows.Length >= 2) + { + var left = rows[0].Trim(); + if (left.StartsWith("Steam++")) + { + var is7z = left.EndsWith(FileEx._7Z); + var isApk = left.EndsWith(FileEx.APK); + if (is7z || isApk) + { + foreach (var item in dirNames) + { + var isAndroid = item.Name.StartsWith("android"); + if (isAndroid && !isApk) continue; + + AppDownloadType? appDownloadType = null; + if (isAndroid && item.BuildDownloads.ContainsKey(AppDownloadType.Install)) + { + appDownloadType = AppDownloadType.Install; + } + else if (item.BuildDownloads.ContainsKey(AppDownloadType.Compressed_7z)) + { + appDownloadType = AppDownloadType.Compressed_7z; + } + + if (appDownloadType.HasValue) + { + var name = item.Name.Replace('-', '_'); + if (left.Contains(name)) + { + var sha256 = item.BuildDownloads[appDownloadType.Value].SHA256; + lines[i] = $"| {name} | {sha256} |"; + break; + } + } + } + } + } + } + } + + File.WriteAllLines(release_template_md_path, lines); + + Console.WriteLine("完成"); + }) + }; + command.AddCommand(rel); + } + } +} \ No newline at end of file diff --git a/src/ST.Tools.Publish/Steps/Step_rpm.cs b/src/ST.Tools.Publish/Steps/Step_rpm.cs new file mode 100644 index 000000000..3337ca70c --- /dev/null +++ b/src/ST.Tools.Publish/Steps/Step_rpm.cs @@ -0,0 +1,131 @@ +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Packaging.Targets; +using Packaging.Targets.IO; +using Packaging.Targets.Rpm; +using System.Application.Models; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using System.Linq; +using static System.Application.Utils; + +namespace System.Application.Steps +{ + internal static class Step_rpm + { + static void Handler() + { + var krgen = PgpSigner.GenerateKeyRingGenerator("dotnet", "dotnet"); + var secretKeyRing = krgen.GenerateSecretKeyRing(); + var privateKey = secretKeyRing.GetSecretKey().ExtractPrivateKey("dotnet".ToCharArray()); + var publicKey = secretKeyRing.GetPublicKey(); + + var publish_json_path = PublishJsonFilePath; + var publish_json_str = File.ReadAllText(publish_json_path); + var dirNames = Serializable.DJSON(publish_json_str); + + if (!dirNames.Any_Nullable()) + { + Console.WriteLine($"错误:发布配置文件读取失败!{publish_json_path}"); + return; + } + + dirNames = dirNames.ThrowIsNull(nameof(dirNames)); + + foreach (var item in dirNames) + { + var isLinux = item.Name.StartsWith("linux"); + if (!isLinux) continue; + var rpmPath = GetPackPath(item, FileEx.RPM); + var cpioPath = GetPackPath(item, FileEx.CPIO); + + using var targetStream = File.Open(rpmPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); + using var cpioStream = File.Open(cpioPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); + + ArchiveBuilder2 archiveBuilder2 = new() + { + Log = TaskLoggingHelper.Instance, + }; + ArchiveBuilder archiveBuilder = archiveBuilder2; + + var archiveEntries = archiveBuilder.FromDirectory( + item.Path, + Constants.HARDCODED_APP_NAME, + LinuxPackConstants.Prefix, + Array.Empty()); + + LinuxPackConstants.AddFileNameDesktop(archiveBuilder2, archiveEntries); + + //archiveEntries.AddRange(archiveBuilder.FromLinuxFolders(this.LinuxFolders)); + + archiveEntries = archiveEntries + .OrderBy(e => e.TargetPathWithFinalSlash, StringComparer.Ordinal) + .ToList(); + + CpioFileCreator cpioCreator = new CpioFileCreator(); + cpioCreator.FromArchiveEntries( + archiveEntries, + cpioStream); + cpioStream.Position = 0; + + // Prepare the list of dependencies + List dependencies = new List(); + + if (item.DeploymentMode == DeploymentMode.FDE) + { + dependencies.Add(new PackageDependency { Name = LinuxPackConstants.dotnet_runtime_6_0 }); + } + + //if (this.RpmDotNetDependencies != null) + //{ + // dependencies.AddRange( + // this.RpmDotNetDependencies.Select( + // d => GetPackageDependency(d))); + //} + + //if (this.RpmDependencies != null) + //{ + // dependencies.AddRange( + // this.RpmDependencies.Select( + // d => GetPackageDependency(d))); + //} + + RpmPackageCreator rpmCreator = new RpmPackageCreator(); + rpmCreator.CreatePackage( + archiveEntries, + cpioStream, + LinuxPackConstants.PackageName, + Utils.Version, + RpmTask.GetPackageArchitecture(item.Name), + LinuxPackConstants.Release, + LinuxPackConstants.CreateUser, + LinuxPackConstants.UserName, + LinuxPackConstants.InstallService, + LinuxPackConstants.ServiceName, + LinuxPackConstants.RpmVendor, + LinuxPackConstants.Description, + LinuxPackConstants.Url, + LinuxPackConstants.Prefix, + LinuxPackConstants.PreInstallScript, + LinuxPackConstants.PostInstallScript, + LinuxPackConstants.PreRemoveScript, + LinuxPackConstants.PostRemoveScript, + dependencies, + null!, + privateKey, + targetStream); + } + + Console.WriteLine("完成"); + } + + public static void Add(RootCommand command) + { + var rpm = new Command("rpm", "Create a CentOS/RedHat Linux installer"); + rpm.Handler = CommandHandler.Create(Handler); + command.AddCommand(rpm); + } + } +} diff --git a/src/ST.Tools.Publish/Steps/Step_start.cs b/src/ST.Tools.Publish/Steps/Step_start.cs new file mode 100644 index 000000000..de100dde3 --- /dev/null +++ b/src/ST.Tools.Publish/Steps/Step_start.cs @@ -0,0 +1,200 @@ +using System.Application.Models; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using static System.Application.Utils; +using static System.ProjectPathUtil; + +namespace System.Application.Steps +{ + internal static class Step5 + { + /// + /// 是否计算每个文件的哈希值,用于增量更新 + /// + static readonly bool calcHash = false; + + static PublishDirInfo[]? Handler(DeploymentMode d, string[] val, bool dev) + { + if (!val.Any_Nullable()) + { + Console.WriteLine(InputPubDirNameError); + return null; + } + + var pubPath = d switch + { + DeploymentMode.SCD => DirPublish_, + DeploymentMode.FDE => DirPublish_FDE_, + _ => throw new ArgumentOutOfRangeException(nameof(d), d, null), + }; + + var dirBasePath = projPath + string.Format(pubPath, dev ? "Debug" : "Release"); + var dirNames = val.Select(x => new PublishDirInfo(x, Path.Combine(dirBasePath, x), d)).ToArray(); + + var publish_json_path = PublishJsonFilePath; + IOPath.FileIfExistsItDelete(publish_json_path); + + foreach (var item in dirNames) + { + if (!Directory.Exists(item.Path)) + { + Console.WriteLine($"错误:找不到发布文件夹({item.Name}),{item.Path}"); + return null; + } + + List lazyActions = new(); + + lazyActions.Add(() => + { + ScanPath(item.Path, item.Files, ignoreRootDirNames: ignoreDirNames); + }); + + //if (item.Name.StartsWith("win", StringComparison.OrdinalIgnoreCase)) + //{ + // #region 适用于 Windwos 7 OS 的 疑难解答助手 + + // string win7Path; + // if (IsAigioPC) + // { + // win7Path = @"G:\Steam++.Win7"; + // } + // else + // { + // win7Path = Path.Combine(item.Path, "..", "Steam++.Win7"); + // } + + // var win7ExePath = Path.Combine(win7Path, "Steam++.Win7.exe"); + // if (!File.Exists(win7ExePath) || !File.Exists(win7ExePath + ".config")) + // { + // Console.WriteLine($"错误:Win7疑难助手程序不存在!{win7ExePath}"); + // return; + // } + + // lazyActions.Add(() => + // { + // ScanPath(win7Path, item.Files); + // }); + + // #endregion + + // var consoleExeFileName = Path.Combine(item.Path, "Steam++.Console.exe"); + // if (!File.Exists(consoleExeFileName)) + // { + // Console.WriteLine($"错误:Steam++.Console.exe 不存在!"); + // return; + // } + //} + + //if (item.Name.Equals("win-x86", StringComparison.OrdinalIgnoreCase)) + //{ + // #region Win10SDK.NET + + // const string netSDKFileName = "Microsoft.Windows.SDK.NET.dll"; + // var netSDKFileRefPath = Path.Combine(projPath, "references", netSDKFileName); + + // if (!File.Exists(netSDKFileRefPath)) + // { + // Console.WriteLine($"错误:Microsoft.Windows.SDK.NET.dll不存在!{netSDKFileRefPath}"); + // return; + // } + + // var netSDKFileRefInfoVersion = GetInfoVersion(netSDKFileRefPath); + // var netSDKFilePath = Path.Combine(item.Path, netSDKFileName); + // var netSDKFileInfoVersion = GetInfoVersion(netSDKFilePath); + // if (netSDKFileRefInfoVersion != netSDKFileInfoVersion) + // { + // Console.WriteLine("错误:Microsoft.Windows.SDK.NET.dll 版本不正确!"); + // Console.WriteLine($"Version: {netSDKFileInfoVersion}"); + // Console.WriteLine($"Version(Ref): {netSDKFileRefInfoVersion}"); + // return; + // } + + // lazyActions.Add(() => + // { + // foreach (var file in item.Files) + // { + // if (file.Path == netSDKFilePath) + // { + // file.Path = netSDKFileRefPath; + // file.Length = new FileInfo(netSDKFileRefPath).Length; + // } + // } + // }); + + // #endregion + //} + + //string cefRootPath; + //if (IsAigioPC) + //{ + // cefRootPath = @"G:\"; + //} + //else + //{ + // cefRootPath = Path.Combine(item.Path, ".."); + //} + + //var cefPath = Path.Combine(cefRootPath, "CEF", item.Name); + + //if (!Directory.Exists(cefPath)) + //{ + // Console.WriteLine($"错误:CEF库文件夹不存在!{cefPath}"); + // return; + //} + //else if (!Directory.GetFiles(cefPath).Any()) + //{ + // Console.WriteLine($"错误:CEF库文件不存在!{cefPath}"); + // return; + //} + + //lazyActions.Add(() => + //{ + // ScanPath(cefPath, item.Files, cefRootPath); + //}); + + lazyActions.ForEach(x => x()); + + Console.WriteLine($"{item.Name}, count: {item.Files.Count}"); + + if (calcHash) + { + foreach (var file in item.Files) + { + Console.WriteLine($"正在计算哈希值:{file.Path}"); + using var fileStream = File.OpenRead(file.Path); + file.SHA256 = Hashs.String.SHA256(fileStream); + } + } + } + + return dirNames; + } + + public static void Add(RootCommand command) + { + // sta -val "win-x86" + var sta = new Command("start", "5. (本地)验证发布文件夹与计算文件哈希"); + sta.AddAlias("sta"); + sta.AddOption(new Option("-val", InputPubDirNameDesc)); + sta.AddOption(new Option("-dev", DevDesc)); + sta.Handler = CommandHandler.Create((string[] val, bool dev) => + { + var dirNames = new[] { + Handler(DeploymentMode.SCD, val, dev), + Handler(DeploymentMode.FDE, val, dev), + }.Where(x => x != null).SelectMany(x => x!).ToArray(); + + if (dirNames.Any()) + { + SavePublishJson(dirNames); + Console.WriteLine("完成"); + } + }); + command.AddCommand(sta); + } + } +} \ No newline at end of file diff --git a/src/ST.Tools.Publish/Steps/Step_tgz_7z_tbr.cs b/src/ST.Tools.Publish/Steps/Step_tgz_7z_tbr.cs new file mode 100644 index 000000000..3dd97ccdc --- /dev/null +++ b/src/ST.Tools.Publish/Steps/Step_tgz_7z_tbr.cs @@ -0,0 +1,156 @@ +using System.Application.Models; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using System.Security.Cryptography; +using static System.Application.Utils; + +namespace System.Application.Steps +{ + internal static class Step8_tar_gz + { + public static void Add(RootCommand command) + { + // tgz + var tgz = new Command("tgz", "8. (本地)读取上一步操作后的 Publish.json 生成压缩包并计算哈希值写入 Publish.json"); + tgz.Handler = CommandHandler.Create(() => + { + var publish_json_path = PublishJsonFilePath; + var publish_json_str = File.ReadAllText(publish_json_path); + var dirNames = Serializable.DJSON(publish_json_str); + + if (!dirNames.Any_Nullable()) + { + Console.WriteLine($"错误:发布配置文件读取失败!{publish_json_path}"); + return; + } + + dirNames = dirNames.ThrowIsNull(nameof(dirNames)); + + foreach (var item in dirNames) + { + var packPath = GetPackPath(item, FileEx.TAR_GZ); + Console.WriteLine($"正在生成压缩包:{packPath}"); + IOPath.FileIfExistsItDelete(packPath); + + CreatePack(packPath, item.Files); + + using var fileStream = File.OpenRead(packPath); + var sha256 = Hashs.String.SHA256(fileStream); + + if (item.BuildDownloads.ContainsKey(AppDownloadType.Compressed_GZip)) + { + item.BuildDownloads[AppDownloadType.Compressed_GZip] = new PublishFileInfo { SHA256 = sha256, Length = fileStream.Length }; + } + else + { + item.BuildDownloads.Add(AppDownloadType.Compressed_GZip, new PublishFileInfo { SHA256 = sha256, Length = fileStream.Length }); + } + } + + SavePublishJson(dirNames, removeFiles: true); + + Console.WriteLine("完成"); + }); + command.AddCommand(tgz); + } + } + + internal static class Step8_7z + { + public static void Add(RootCommand command) + { + // 7z + var _7z = new Command("7z", "8. (本地)读取上一步操作后的 Publish.json 生成压缩包并计算哈希值写入 Publish.json"); + _7z.Handler = CommandHandler.Create(() => + { + var publish_json_path = PublishJsonFilePath; + var publish_json_str = File.ReadAllText(publish_json_path); + var dirNames = Serializable.DJSON(publish_json_str); + + if (!dirNames.Any_Nullable()) + { + Console.WriteLine($"错误:发布配置文件读取失败!{publish_json_path}"); + return; + } + + dirNames = dirNames.ThrowIsNull(nameof(dirNames)); + + foreach (var item in dirNames) + { + var packPath = GetPackPath(item, FileEx._7Z); + Console.WriteLine($"正在生成压缩包:{packPath}"); + IOPath.FileIfExistsItDelete(packPath); + + CreateSevenZipPack(packPath, item.Files); + + using var fileStream = File.OpenRead(packPath); + var sha256 = Hashs.String.SHA256(fileStream); + + if (item.BuildDownloads.ContainsKey(AppDownloadType.Compressed_7z)) + { + item.BuildDownloads[AppDownloadType.Compressed_7z] = new PublishFileInfo { SHA256 = sha256, Length = fileStream.Length }; + } + else + { + item.BuildDownloads.Add(AppDownloadType.Compressed_7z, new PublishFileInfo { SHA256 = sha256, Length = fileStream.Length }); + } + } + + SavePublishJson(dirNames, removeFiles: true); + + Console.WriteLine("完成"); + }); + command.AddCommand(_7z); + } + } + + internal static class Step8_tar_br + { + public static void Add(RootCommand command) + { + // tbr + var tar_br = new Command("tbr", "8. (本地)读取上一步操作后的 Publish.json 生成压缩包并计算哈希值写入 Publish.json"); + tar_br.Handler = CommandHandler.Create(() => + { + var publish_json_path = PublishJsonFilePath; + var publish_json_str = File.ReadAllText(publish_json_path); + var dirNames = Serializable.DJSON(publish_json_str); + + if (!dirNames.Any_Nullable()) + { + Console.WriteLine($"错误:发布配置文件读取失败!{publish_json_path}"); + return; + } + + dirNames = dirNames.ThrowIsNull(nameof(dirNames)); + + foreach (var item in dirNames) + { + var packPath = GetPackPath(item, FileEx.TAR_BR); + Console.WriteLine($"正在生成压缩包:{packPath}"); + IOPath.FileIfExistsItDelete(packPath); + + CreateBrotliPack(packPath, item.Files); + + using var fileStream = File.OpenRead(packPath); + var sha256 = Hashs.String.SHA256(fileStream); + + if (item.BuildDownloads.ContainsKey(AppDownloadType.Compressed_Br)) + { + item.BuildDownloads[AppDownloadType.Compressed_Br] = new PublishFileInfo { SHA256 = sha256, Length = fileStream.Length }; + } + else + { + item.BuildDownloads.Add(AppDownloadType.Compressed_Br, new PublishFileInfo { SHA256 = sha256, Length = fileStream.Length }); + } + } + + SavePublishJson(dirNames, removeFiles: true); + + Console.WriteLine("完成"); + }); + command.AddCommand(tar_br); + } + } +} \ No newline at end of file diff --git a/src/ST.Tools.Publish/Utils.cs b/src/ST.Tools.Publish/Utils.cs new file mode 100644 index 000000000..52f1b33de --- /dev/null +++ b/src/ST.Tools.Publish/Utils.cs @@ -0,0 +1,273 @@ +using ICSharpCode.SharpZipLib.GZip; +using ICSharpCode.SharpZipLib.Tar; +using ICSharpCode.SharpZipLib.Zip.Compression; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Packaging.Targets; +using Packaging.Targets.IO; +using SevenZip; +using System.Application.Models; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Properties; +using System.Security.Cryptography; +using System.Text; +using NCompressionMode = System.IO.Compression.CompressionMode; + +namespace System.Application +{ + internal static partial class Utils + { + static Utils() + { + if (OperatingSystem.IsWindows()) + { + var sevenZipLibraryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "7-Zip", "7z.dll"); + SevenZipBase.SetLibraryPath(sevenZipLibraryPath); + SevenZipCompressor.LzmaDictionarySize = 805306368; // 768MB + } + } + + //public static void AddDeploymentModeOption(Command command, string alias = "-d") + //{ + // var o = new Option(alias, () => DeploymentMode.SCD, DeploymentModeDesc); + // command.AddOption(o); + //} + + public const string InputPubDirNameDesc = "(必填)输入发布文件夹名"; + public const string InputPubDirNameError = "错误:必须输入发布文件夹名"; + public const string DevDesc = "是否为开发环境,默认值否"; + //const string DeploymentModeDesc = "应用部署模式,默认为SCD"; + //public const string DevTablePrefix = ""; + public const string PublishJsonFileName = "Publish.json"; + + static readonly Lazy mPublishJsonFilePath = new(() => Path.Combine(IOPath.AppDataDirectory, PublishJsonFileName)); + public static string PublishJsonFilePath => mPublishJsonFilePath.Value; + + public static readonly string[] ignoreDirNames = new[] { + IOPath.DirName_AppData, + IOPath.DirName_Cache, + "CEF" + }; + + /// + /// 检查版本号字符串是否符合要求 + /// + /// + /// + public static bool CheckVersion(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + Console.WriteLine("错误:必须输入一个版本号!"); + return false; + } + else if (!Version.TryParse(value, out var _)) + { + Console.WriteLine("错误:输入的版本号格式不正确!"); + return false; + } + return true; + } + + static readonly Lazy mIsAigioPC = new(() => + { + return Hashs.String.Crc32(Environment.MachineName, false) == "88DF9AB0" || + Hashs.String.Crc32(Environment.UserName, false) == "8AA383BC"; + }); + public static bool IsAigioPC => mIsAigioPC.Value; + + public static string GetInfoVersion(string assemblyFile) + { + var fileVersionInfo = FileVersionInfo.GetVersionInfo(assemblyFile); + return fileVersionInfo?.FileVersion ?? string.Empty; + //var a = Assembly.LoadFrom(assemblyFile); + //var attr = a.GetCustomAttribute(); + //return attr?.InformationalVersion ?? string.Empty; + } + + public static void ScanPath(string dirPath, List? list = null, string? relativeTo = null, string[]? ignoreRootDirNames = null) + { + list ??= new List(); + relativeTo ??= dirPath; + var files = Directory.GetFiles(dirPath).Where(x => + Path.GetExtension(x) != ".pdb" && + (Path.GetExtension(x) != ".xml" || x.EndsWith(".VisualElementsManifest.xml", StringComparison.OrdinalIgnoreCase)) && + !x.EndsWith(".runtimeconfig.dev.json", StringComparison.OrdinalIgnoreCase)); + if (files.Any()) + { + var files2 = files.Select(x => new PublishFileInfo(x, relativeTo)); + list.AddRange(files2); + } + var dirs = Directory.GetDirectories(dirPath); + foreach (var dir in dirs) + { + if (ignoreRootDirNames != null && ignoreRootDirNames.Contains(Path.GetDirectoryName(dir))) + { + // 忽略顶级文件夹 + continue; + } + ScanPath(dir, list, relativeTo); + } + } + + public static void CreatePack(string packPath, IEnumerable files) + { + using var fs = File.Create(packPath); + using var s = new GZipOutputStream(fs); + s.SetLevel(Deflater.BEST_COMPRESSION); + using var archive = TarArchive.CreateOutputTarArchive(s, + TarBuffer.DefaultBlockFactor, Encoding.UTF8); + foreach (var file in files) + { + Console.WriteLine($"正在压缩:{file.Path}"); + var entry = TarEntry.CreateEntryFromFile(file.Path); + entry.Name = file.RelativePath; + archive.WriteEntry(entry, false); + } + } + + public static void CreateBrotliPack(string packPath, IEnumerable files) + { + using var fs = File.Create(packPath); + using var s = new BrotliStream(fs, NCompressionMode.Compress); + using var archive = TarArchive.CreateOutputTarArchive(s, + TarBuffer.DefaultBlockFactor, Encoding.UTF8); + foreach (var file in files) + { + Console.WriteLine($"正在压缩:{file.Path}"); + var entry = TarEntry.CreateEntryFromFile(file.Path); + entry.Name = file.RelativePath; + archive.WriteEntry(entry, false); + } + } + + public static void CreateSevenZipPack(string packPath, IEnumerable files) + { + SevenZipCompressor compressor = new(); + compressor.ArchiveFormat = OutArchiveFormat.SevenZip; + compressor.CompressionLevel = SevenZip.CompressionLevel.Ultra; + compressor.CompressionMethod = CompressionMethod.Lzma2; + compressor.FastCompression = false; + compressor.ScanOnlyWritable = true; + compressor.DirectoryStructure = true; + compressor.FileCompressionStarted += (_, e) => + { + Console.WriteLine($"正在压缩:{e.FileName}"); + }; + var dict = files.ToDictionary(x => x.RelativePath, x => x.Path); + compressor.CompressFileDictionary(dict, packPath); + } + + public static void SavePublishJson(IEnumerable dirNames, bool removeFiles = false) + { + if (removeFiles) + { + foreach (var item in dirNames) + { + item.Files.Clear(); + } + } + var publish_json_str = Serializable.SJSON(dirNames); + IOPath.FileIfExistsItDelete(PublishJsonFilePath); + File.WriteAllText(PublishJsonFilePath, publish_json_str); + } + + public static string Version + { + get + { + var mainDllPath = ProjectPathUtil.projPath + ProjectPathUtil.MainDllPath; + var version = (File.Exists(mainDllPath) ? FileVersionInfo.GetVersionInfo(mainDllPath).FileVersion : null) ?? ThisAssembly.Version; + var versionArray = version.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (versionArray.Length > 3) version = string.Join('.', versionArray.Take(3)); + return version; + } + } + + public static string GetFileName(PublishDirInfo item, string fileEx) + { + var version = Version; + var fileName = item.DeploymentMode switch + { + DeploymentMode.SCD => + $"Steam++_{item.Name.Replace("-", "_")}_v{version}{fileEx}", + DeploymentMode.FDE => + $"Steam++_{item.Name.Replace("-", "_")}_fde_v{version}{fileEx}", + _ => throw new ArgumentOutOfRangeException(nameof(item.DeploymentMode), item.DeploymentMode, null), + }; + return fileName; + } + + public static string GetPackPath(PublishDirInfo item, string fileEx) + { + var fileName = GetFileName(item, fileEx); + var packPath = item.DeploymentMode switch + { + DeploymentMode.SCD => Path.Combine(item.Path, "..", fileName), + DeploymentMode.FDE => Path.Combine(item.Path, "..", "..", fileName), + _ => throw new ArgumentOutOfRangeException(nameof(item.DeploymentMode), item.DeploymentMode, null), + }; + return packPath; + } + + public static string[] GetAllRids(string[] rids, Platform platform) => !rids.Any() + ? platform switch + { + Platform.Unknown => new[] { "win-x64", "win-arm64", "linux-x64", "linux-arm64", "linux-arm", "osx-x64", "osx-arm64", }, + Platform.Windows => new[] { "win-x64", "win-arm64", }, + Platform.Linux => new[] { "linux-x64", "linux-arm64", "linux-arm", }, + Platform.Apple => new[] { "osx-x64", "osx-arm64", }, + _ => throw new ArgumentOutOfRangeException(nameof(platform), platform, null), + } + : rids; + + public static class LinuxPackConstants + { + public const string TargetName = Constants.HARDCODED_APP_NAME; + public const string PackagePrefix = TargetName; + public const string PackageName = PackagePrefix; + public const string Prefix = "/usr/share/" + PackagePrefix; + public const string Release = "0"; + public const bool CreateUser = false; + public const string UserName = Constants.HARDCODED_APP_NAME; + public const bool InstallService = false; + public const string ServiceName = PackagePrefix; + public const string RpmVendor = ThisAssembly.AssemblyCompany; + public const string Description = ThisAssembly.AssemblyDescription; + public const string Url = "https://steampp.net"; + public const string PreInstallScript = null!; + public const string PostInstallScript = null!; + public const string PreRemoveScript = null!; + public const string PostRemoveScript = null!; + public const string FileNameDesktop = Constants.HARDCODED_APP_NAME + ".desktop"; + public const string DebMaintainer = ThisAssembly.AssemblyCompany; + public const string DebSection = "misc"; + public const string DebPriority = "extra"; + public const string DebHomepage = Url; + public const string dotnet_runtime_6_0 = "dotnet-runtime-6.0"; + + public static void AddFileNameDesktop(ArchiveBuilder2 archiveBuilder2, List archiveEntries) + { + var metadata = new Dictionary() + { + { "CopyToPublishDirectory", "Always" }, + { "LinuxPath", "/usr/share/applications/" + FileNameDesktop }, + { "Link", FileNameDesktop }, + }; + + var taskItem = new TaskItem(FileNameDesktop, metadata); + var taskItems = new ITaskItem[] { taskItem }; + archiveBuilder2.AddFile( + Path.Combine(AppContext.BaseDirectory, FileNameDesktop), + FileNameDesktop, + Prefix, + archiveEntries, + taskItems); + } + } + } +} \ No newline at end of file