139 lines
12 KiB
C#
Raw Permalink Normal View History

2026-09-17 14:45:23 +08:00
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); }
}
}