基于NSIS的Unity发布后程序的自动制作安装包工具实现
前段时间,应公司要求,研究了一下把Unity发布后的程序自动做成安装包的实现方案。经过一段时间的研究,最终使用NSIS开源工具基本实现了这个需求。为了防止以后自己忘了这部分的东西,特此记录下实现思路,方便以后回顾。研究下来之后,我觉得可能很多流氓软件可能背后都有NSIS这个工具的功劳,哼哼~。
方案流程总结
整体思路: 基于NSIS的脚本模板,使用C#实现IPostprocessBuildWithReport接口的编辑器脚本,用Unity内部的Player Setting信息,以及打包好的程序信息,替换NSIS脚本模板中的对应信息,再自动执行makensis命令制作安装包。
安装NSIS
1.安装NSIS
NSIS (Nullsoft Scriptable Install System) 是一个专业开源的制作 windows 安装程序的工具。详细介绍就不多说了,愿意详细了解的,参考:NSIS百度百科 。下载链接:NSIS
安装完成之后把NSIS目录添加到系统环境变量,用于后续C#脚本调用,添加完成后可以使用makensis命令测试,如下图:
2.安装HM NIS EDIT
HM NIS EDIT 是一个免费的NSIS脚本编辑器IDE。下载链接:HM NIS EDIT
制作NSIS脚本模板
1.在Unity内打包一个简易的程序程序,用于制作NSIS脚本,以及打包测试。
2.使用HM NIS EDIT的“新建脚本向导”,创建Unity打包后程序的的NSIS脚本,并保存下来,保存的脚本后缀为“nsi”。参考:方便快捷的客户端打包工具“HM NIS Edit”。因为这里没有授权文件,所以删除脚本中关于授权的内容,如下图:
3.使用生成的NSIS脚本测试NSIS制作安装包。命令为 makensis xxxx.nsi
4.运行制作好的安装包,安装程序,运行安装好的程序,再卸载程序,测试整个流程。
解析NSIS脚本
1.程序信息
这里包含程序的基本信息,程序名称、版本号、发布者(公司名称)、产品网址、注册表信息等。
2.安装包设置:
这里包含安装包的设置信息,包含图标设置、页面设置、语言设置等。
3.安装信息
这里包含程序的安装设置信息,包含安装包名称、安装路径、安装注册表设置,以及要安装的文件及文件夹等。
4.卸载信息
这里包含程序的卸载设置信息,包含卸载程序设置、卸载页面提示,以及要删除的文件及文件夹设置等。
分析完NSIS脚本之后,基本的用代码制作安装包的思路就出来了。具体如下:
1. 将Unity的PlayerSetting里面的程序名、发布者、版本号等信息替换模板中对应的信息;
2. 遍历Unity打包出来的程序目录,将里面的文件和文件夹路径写入到安装信息里面;
3. 类似第二步,改写卸载信息中对应的删除的文件和文件夹路径信息;
4. 保存生成好的nsi脚本,调用makensis命令生成安装包。
Unity发布程序的自动安装包制作实现
首先,因为nsi脚本中的很多信息都是用代码去添加填写,为了避免出错和冲突,方便后面代码去改写,可以将模板脚本中一些没用的信息清除掉,以及进行修改。具体如下:
-
将所有用到产品名称的地方都用${PRODUCT_NAME}替换,你可以理解为这是表示产品名称的一个常量,涉及的部分有:
-
去掉Section “MainSection” SEC01 和下一个SectionEnd之间的安装文件信息,这些信息将在C#脚本里自动填写。
-
去掉Section Uninstall和 RMDir "$INSTDIR"之间的文件删除信息,这些信息将在C#脚本里自动填写。
修改完成后的nsi脚本如下:
; Script generated by the HM NIS Edit Script Wizard.
; HM NIS Edit Wizard helper defines
!define PRODUCT_NAME "NsisTool"
!define PRODUCT_VERSION "1.0"
!define PRODUCT_PUBLISHER "MyCompany, Inc."
!define PRODUCT_DIR_REGKEY "Software\Microsoft\Windows\CurrentVersion\App Paths\${PRODUCT_NAME}.exe"
!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
!define PRODUCT_UNINST_ROOT_KEY "HKLM"
; MUI 1.67 compatible ------
!include "MUI.nsh"
; MUI Settings
!define MUI_ABORTWARNING
!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\orange-install.ico"
!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\orange-uninstall.ico"
; Welcome page
!insertmacro MUI_PAGE_WELCOME
; Directory page
!insertmacro MUI_PAGE_DIRECTORY
; Instfiles page
!insertmacro MUI_PAGE_INSTFILES
; Finish page
!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_NAME}.exe"
!insertmacro MUI_PAGE_FINISH
; Uninstaller pages
!insertmacro MUI_UNPAGE_INSTFILES
; Language files
!insertmacro MUI_LANGUAGE "SimpChinese"
; MUI end ------
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
OutFile "${PRODUCT_NAME}_Setup.exe"
InstallDir "$PROGRAMFILES\${PRODUCT_NAME}"
InstallDirRegKey HKLM "${PRODUCT_DIR_REGKEY}" ""
ShowInstDetails show
ShowUnInstDetails show
Section "MainSection" SEC01
SectionEnd
Section -AdditionalIcons
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\uninst.exe"
SectionEnd
Section -Post
WriteUninstaller "$INSTDIR\uninst.exe"
WriteRegStr HKLM "${PRODUCT_DIR_REGKEY}" "" "$INSTDIR\${PRODUCT_NAME}.exe"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\uninst.exe"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_NAME}.exe"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
SectionEnd
Function un.onUninstSuccess
HideWindow
MessageBox MB_ICONINFORMATION|MB_OK "$(^Name) 已成功地从你的计算机移除。"
FunctionEnd
Function un.onInit
MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "你确实要完全移除 $(^Name) ,其及所有的组件?" IDYES +2
Abort
FunctionEnd
Section Uninstall
RMDir "$INSTDIR"
DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}"
DeleteRegKey HKLM "${PRODUCT_DIR_REGKEY}"
SetAutoClose true
SectionEnd`
将这个脚本放到工程里的StreamingAssets目录下,我的命名为Template_NSIS.nsi。
其次,因为这个脚本是要在Unity打包完成后编辑器自动运行的,所以脚本要放在Editor目录下,并且要实现IPostprocessBuildWithReport接口。
然后,上代码:
using UnityEngine;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using System.IO;
using System.Diagnostics;
using System.Text;
using System.Collections.Generic;
public class NsisTool : IPostprocessBuildWithReport
{
//要查询nsi脚本中的关键key
const string ProductText = "!define PRODUCT_NAME ";//产品名称
const string VersionText = "!define PRODUCT_VERSION ";//版本号
const string PublisherText = "!define PRODUCT_PUBLISHER ";//出版商
const string OutFileText = "OutFile \"";//安装输出路径
public int callbackOrder => 0;
private static StringBuilder sb;
private string path;
private static int rootLength;
private static List<string> outList = new List<string>();
public void OnPostprocessBuild(BuildReport report)
{
UnityEngine.Debug.Log("MyCustomBuildProcessor.OnPostprocessBuild at path " + report.summary.outputPath);
path = report.summary.outputPath;
CreateNsisScr(path.Replace("/","\\"));
}
/// <summary>
/// 床脚NSIS脚本
/// </summary>
/// <param name="path"></param>
public static void CreateNsisScr(string path)
{
if (string.IsNullOrEmpty(path))
{
return;
}
string dir = Path.GetDirectoryName(path);
rootLength = dir.Length;
sb = new StringBuilder();
string temScr =Path.Combine(Application.streamingAssetsPath,"Template_NSIS.nsi");
List<string> textList = new List<string>(File.ReadAllLines(temScr, Encoding.UTF8));
foreach (var item in textList)
{
if (item.StartsWith(ProductText))//写入软件名
{
sb.AppendLine(item.Remove(ProductText.Length) + "\"" + Application.productName + "\"");
}
else if (item.StartsWith(VersionText))//写入版本号
{
sb.AppendLine(item.Remove(VersionText.Length) + "\"" + Application.version + "\"");
}
else if (item.StartsWith(PublisherText))//写入公司名
{
sb.AppendLine(item.Remove(PublisherText.Length) + "\"" + Application.companyName + "\"");
}
else if (item.StartsWith(OutFileText))//写入输出路径
{
sb.AppendLine(item.Insert(OutFileText.Length, dir + "\\"));
}
else if (item == "Section \"MainSection\" SEC01")//写入安装文件列表
{
sb.AppendLine(item);
sb.AppendLine(" SetOutPath \"$INSTDIR\"");
sb.AppendLine(" SetOverwrite ifnewer");
sb.AppendLine(" File \"" + path + "\"");
sb.AppendLine(" CreateDirectory \"$SMPROGRAMS\\${PRODUCT_NAME}\"");
sb.AppendLine(" CreateShortCut \"$SMPROGRAMS\\${PRODUCT_NAME}\\${PRODUCT_NAME}.lnk\" \"$INSTDIR\\${PRODUCT_NAME}.exe\"");
sb.AppendLine(" CreateShortCut \"$DESKTOP\\${PRODUCT_NAME}.lnk\" \"$INSTDIR\\${PRODUCT_NAME}.exe\"");
sb.AppendLine(" SetOverwrite try");
//string file= Path.GetFileName(path);
HandDir(dir);
}
else if (item == "Section Uninstall")//写入卸载文件列表
{
sb.AppendLine(item);
sb.AppendLine(
" Delete \"$INSTDIR\\uninst.exe\"\n" +
" Delete \"$SMPROGRAMS\\${PRODUCT_NAME}\\Uninstall.lnk\"\n" +
" Delete \"$DESKTOP\\${PRODUCT_NAME}.lnk\"\n" +
" Delete \"$SMPROGRAMS\\${PRODUCT_NAME}\\${PRODUCT_NAME}.lnk\"\n" +
" RMDir \"$SMPROGRAMS\\${PRODUCT_NAME}\"\n");
foreach (var str in outList)
{
sb.AppendLine(str);
}
}
else
{
sb.AppendLine(item);
}
}
//Console.WriteLine(sb.ToString());
//输出nsi脚本
string output = Path.Combine(Application.streamingAssetsPath,"Output_NSIS.nsi");
File.WriteAllText(output, sb.ToString(), Encoding.UTF8);
//使用makensis命令生成安装包
Process.Start("makensis", output);
}
/// <summary>
/// 路径处理
/// </summary>
/// <param name="path"></param>
static void HandDir(string path)
{
string dir = path.Remove(0, rootLength);
foreach (var item in Directory.EnumerateDirectories(path))
{
//Console.WriteLine(item);
HandDir(item);
}
string str = "\"$INSTDIR" + dir + "\"";
sb.AppendLine(" SetOutPath " + str);//添加文件夹安装信息
outList.Add($" RMDir {str}");//添加文件夹卸载信息
foreach (var item in Directory.EnumerateFiles(path))
{
string file = item.Remove(0, rootLength);
sb.AppendLine(" File \"" + item + "\"");//添加文件安装信息
outList.Add(" Delete \"$INSTDIR" + file + "\"");//添加文件卸载信息
//Console.WriteLine(item);
}
}
}
然后,在Unity编辑器里面Build完之后,这个脚本就会在打包目录下创建一个安装包了。