? 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