2026-09-17 14:45:23 +08:00

139 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Text;
using System.Windows.Forms;
internal static class Program
{
[STAThread]
private static int Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
using (var window = new PackagerForm())
{
if (args.Length == 1 && args[0] == "--smoke-test") return 0;
Application.Run(window);
}
return 0;
}
}
internal sealed class PackagerForm : Form
{
private readonly TextBox project = new TextBox();
private readonly TextBox flutter = new TextBox();
private readonly TextBox api = new TextBox();
private readonly TextBox output = new TextBox();
private readonly TextBox version = new TextBox();
private readonly TextBox code = new TextBox();
private readonly ComboBox mode = new ComboBox();
private readonly CheckBox tests = new CheckBox();
private readonly RichTextBox log = new RichTextBox();
private readonly Button build = new Button();
private readonly Button check = new Button();
private readonly Button install = new Button();
private readonly Label status = new Label();
private readonly ProgressBar progress = new ProgressBar();
private readonly string basePath = AppDomain.CurrentDomain.BaseDirectory;
private Process running;
private string apk = "";
private string lastOutput = "";
public PackagerForm()
{
Text = "跃动 · Android 应用打包工具";
Size = new Size(980, 790); MinimumSize = new Size(900, 720);
StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Microsoft YaHei UI", 10F);
BackColor = Color.FromArgb(244, 248, 255);
AutoScaleMode = AutoScaleMode.Dpi;
var root = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(22), ColumnCount = 1, RowCount = 5 };
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 66));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 272));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 58));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 42));
Controls.Add(root);
var heading = new Label { Text = "花样跳绳打分系统 / APK 打包\n选择配置 → 开始打包 → 获取 APK 或安装到设备", Dock = DockStyle.Fill, Font = new Font(Font.FontFamily, 13F, FontStyle.Bold), ForeColor = Color.FromArgb(20, 73, 136) };
root.Controls.Add(heading);
var fields = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 3, RowCount = 7 };
fields.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110));
fields.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
fields.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 92));
for (int i=0;i<7;i++) fields.RowStyles.Add(new RowStyle(SizeType.Absolute, 38));
root.Controls.Add(fields);
string inferred = Path.GetFullPath(Path.Combine(basePath, "..", ".."));
project.Text = File.Exists(Path.Combine(inferred, "pubspec.yaml")) ? inferred : @"C:\Project\Event\YD_Pattern_scoring_system";
output.Text = Path.Combine(project.Text, "packages");
api.Text = "http://127.0.0.1:9996/api";
AddField(fields, 0, "项目目录", project, delegate { PickFolder(project); });
AddField(fields, 1, "Flutter 程序", flutter, delegate { using(var d=new OpenFileDialog { Filter="Flutter|flutter.bat", Title="选择 Flutter SDK 中的 bin\\flutter.bat" }) if(d.ShowDialog()==DialogResult.OK) flutter.Text=d.FileName; });
AddField(fields, 2, "接口地址", api, null);
AddField(fields, 3, "输出目录", output, delegate { PickFolder(output); });
mode.DropDownStyle=ComboBoxStyle.DropDownList;
mode.Items.AddRange(new object[]{"Debug(调试 / 支持 HTTP)", "Release(优化 / 要求 HTTPS)"});
mode.SelectedIndex=0;
AddField(fields,4,"打包模式",mode,null);
var versions = new FlowLayoutPanel { Dock=DockStyle.Fill, WrapContents=false };
versions.Controls.Add(new Label { Text="版本名",AutoSize=true,Margin=new Padding(0,5,8,0) }); version.Width=100; versions.Controls.Add(version);
versions.Controls.Add(new Label { Text="版本号",AutoSize=true,Margin=new Padding(12,5,8,0) }); code.Width=90; versions.Controls.Add(code);
versions.Controls.Add(new Label { Text="留空沿用项目配置",AutoSize=true,Margin=new Padding(10,5,0,0) });
AddField(fields,5,"版本(可选)",versions,null);
tests.Text="打包前运行静态检查和全部测试"; tests.Checked=true; tests.AutoSize=true;
AddField(fields,6,"质量检查",tests,null);
var buttons=new FlowLayoutPanel { Dock=DockStyle.Fill, WrapContents=false };
root.Controls.Add(buttons);
SetupButton(check,"检查环境",delegate { StartJob("check"); }); buttons.Controls.Add(check);
SetupButton(build,"开始打包",delegate { StartJob("build"); }); build.BackColor=Color.FromArgb(0,130,246); build.ForeColor=Color.White; buttons.Controls.Add(build);
var open=new Button(); SetupButton(open,"打开输出目录",delegate { string p=Directory.Exists(lastOutput)?lastOutput:output.Text; if(Directory.Exists(p)) Process.Start("explorer.exe",Quote(p)); else MessageBox.Show("输出目录尚未生成。"); }); buttons.Controls.Add(open);
SetupButton(install,"安装刚生成的 APK",delegate { if(MessageBox.Show("将覆盖安装到唯一已授权的 Android 设备,保留应用数据。是否继续?","安装确认",MessageBoxButtons.YesNo)==DialogResult.Yes) StartJob("install"); }); install.Width=182; install.Enabled=false; buttons.Controls.Add(install);
var help=new Button(); SetupButton(help,"使用说明",delegate { Process.Start("notepad.exe",Quote(Path.Combine(basePath,"README.txt"))); }); buttons.Controls.Add(help);
log.Dock=DockStyle.Fill; log.ReadOnly=true; log.BackColor=Color.FromArgb(19,31,49); log.ForeColor=Color.FromArgb(220,234,249); log.Font=new Font("Consolas",10F); log.WordWrap=false; root.Controls.Add(log);
var footer=new TableLayoutPanel{Dock=DockStyle.Fill,ColumnCount=2}; footer.ColumnStyles.Add(new ColumnStyle(SizeType.Percent,100)); footer.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute,150));
status.Dock=DockStyle.Fill; status.Text="就绪 · Flutter 留空时自动检测 · 无需管理员权限"; status.TextAlign=ContentAlignment.MiddleLeft;
progress.Dock=DockStyle.Fill; progress.Style=ProgressBarStyle.Marquee; progress.Visible=false; footer.Controls.Add(status); footer.Controls.Add(progress); root.Controls.Add(footer);
Append("提示:已有 App 内的接口地址配置优先于打包默认地址。\nRelease 当前仍为调试签名,不可直接用于正式商店发布。\n连接多台设备时请先断开其他设备,工具不会猜测安装目标。");
FormClosing += delegate(object sender, FormClosingEventArgs e) { if(running!=null) { e.Cancel=true; MessageBox.Show("任务仍在运行,请等待完成后关闭,避免留下不完整安装包。"); } };
}
private void AddField(TableLayoutPanel p,int row,string label,Control input,Action browse)
{
p.Controls.Add(new Label{Text=label,Dock=DockStyle.Fill,TextAlign=ContentAlignment.MiddleLeft},0,row);
input.Dock=DockStyle.Fill; input.Margin=new Padding(3,4,8,4); p.Controls.Add(input,1,row);
if(browse!=null) { var b=new Button{Text="浏览…",Dock=DockStyle.Fill}; b.Click+=delegate { browse(); }; p.Controls.Add(b,2,row); }
}
private static void SetupButton(Button button,string text,Action action) { button.Text=text; button.Width=128; button.Height=40; button.FlatStyle=FlatStyle.Flat; button.Margin=new Padding(0,4,10,4); button.Click+=delegate { action(); }; }
private void PickFolder(TextBox target) { using(var d=new FolderBrowserDialog { SelectedPath=target.Text }) if(d.ShowDialog()==DialogResult.OK) target.Text=d.SelectedPath; }
private static string Quote(string value) { if(value.IndexOf('"')>=0 || value.IndexOf('\r')>=0 || value.IndexOf('\n')>=0) throw new ArgumentException("配置不能包含双引号或换行。"); return "\""+value.TrimEnd('\\')+"\""; }
private void Append(string text) { log.AppendText(text+Environment.NewLine); log.SelectionStart=log.TextLength; log.ScrollToCaret(); }
private void Ui(Action action) { if(!IsDisposed && IsHandleCreated) BeginInvoke(action); }
private void StartJob(string kind)
{
if(running!=null) return;
try {
string script=Path.Combine(basePath,"build-app.ps1");
if(!File.Exists(script)) throw new FileNotFoundException("请保留 EXE 同目录下的 build-app.ps1 文件。");
if(kind=="build" && mode.SelectedIndex==1 && MessageBox.Show("当前 Release 使用调试签名,仅用于内部测试。正式发布须另行配置正式签名。是否继续?","Release 签名提醒",MessageBoxButtons.YesNo,MessageBoxIcon.Warning)!=DialogResult.Yes) return;
var args=new StringBuilder("-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "+Quote(script));
args.Append(" -ProjectPath "+Quote(project.Text)+" -Mode "+(mode.SelectedIndex==1?"release":"debug"));
args.Append(" -ApiUrl "+Quote(api.Text)+" -OutputPath "+Quote(output.Text));
if(flutter.Text.Trim().Length>0) args.Append(" -FlutterPath "+Quote(flutter.Text));
if(version.Text.Trim().Length>0) args.Append(" -BuildName "+Quote(version.Text));
if(code.Text.Trim().Length>0) args.Append(" -BuildNumber "+Quote(code.Text));
if(kind=="check") args.Append(" -CheckOnly");
else if(kind=="install") args.Append(" -InstallOnly -ApkPath "+Quote(apk));
else if(tests.Checked) args.Append(" -RunTests");
if(kind=="build") { apk=""; install.Enabled=false; }
Append("\n===== "+DateTime.Now.ToString("HH:mm:ss")+" / "+kind+" =====");
var process=new Process { StartInfo=new ProcessStartInfo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System),@"WindowsPowerShell\v1.0\powershell.exe"),args.ToString()) { UseShellExecute=false,CreateNoWindow=true,RedirectStandardOutput=true,RedirectStandardError=true,StandardOutputEncoding=Encoding.UTF8,StandardErrorEncoding=Encoding.UTF8,WorkingDirectory=basePath } };
process.OutputDataReceived+=delegate(object s,DataReceivedEventArgs e) { if(e.Data!=null) Ui(delegate { Append(e.Data); if(e.Data.StartsWith("PACKAGER_APK=")) apk=e.Data.Substring(13); if(e.Data.StartsWith("PACKAGER_OUTPUT=")) lastOutput=e.Data.Substring(16); }); };
process.ErrorDataReceived+=delegate(object s,DataReceivedEventArgs e) { if(e.Data!=null) Ui(delegate { Append(e.Data); }); };
running=process; build.Enabled=false; check.Enabled=false; install.Enabled=false; progress.Visible=true; status.Text="任务执行中,请勿关闭窗口…";
process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine();
System.Threading.ThreadPool.QueueUserWorkItem(delegate { process.WaitForExit(); int result=process.ExitCode; Ui(delegate { running=null; process.Dispose(); build.Enabled=true; check.Enabled=true; install.Enabled=File.Exists(apk); progress.Visible=false; status.Text=result!=0?"执行失败 · 请查看日志中的 PACKAGER_ERROR":kind=="check"?"环境检查通过 · 可以开始打包":kind=="install"?"APK 安装完成":"打包完成 · 可打开输出目录查看 APK / 日志"; if(result==0 && kind=="build") MessageBox.Show("APK 已生成:\n"+apk,"打包成功",MessageBoxButtons.OK,MessageBoxIcon.Information); }); });
} catch(Exception e) { if(running!=null) running.Dispose(); running=null; build.Enabled=true; check.Enabled=true; progress.Visible=false; Append(e.Message); MessageBox.Show(e.Message,"无法启动",MessageBoxButtons.OK,MessageBoxIcon.Error); }
}
}