Windows 程序安装与更新方案: Clowd.Squirrel

  • Windows 程序安装与更新方案: Clowd.Squirrel已关闭评论
  • 200 次浏览
  • A+
所属分类:.NET技术
摘要

我的NotionSquirrel.Windows 是一组工具和适用于.Net的库,用于管理 Desktop Windows 应用程序的安装和更新。 Squirrel.Windows 对 Windows 应用程序的实现语言没有任何要求,甚至无需服务端即可完成增量更新。

我的Notion

Clowd.Squirrel

Squirrel.Windows 是一组工具和适用于.Net的库,用于管理 Desktop Windows 应用程序的安装和更新。 Squirrel.Windows 对 Windows 应用程序的实现语言没有任何要求,甚至无需服务端即可完成增量更新。

Clowd.Squirrel 是 Squirrel.Windows 的一个优秀分支。2019 年 Squirrel.Windows 宣布不再维护,虽然 2020 年又重新恢复维护,但其不再处于积极开发阶段,依赖库开始陈旧。所以推荐转移到 Clowd.Squirrel,用法也更加简单。

快速使用

下面以 .net 程序 和 vs 2022 为例,介绍如何使用 Clowd.Squirrel

  1. 安装 Clowd.Squirrel

    1. 通过 nuget包管理器安装 Clowd.Squirrel,

      Windows 程序安装与更新方案: Clowd.Squirrel

    2. 安装后,目录 ..packagesClowd.Squirrel.2.9.40tools 里是用到的工具

      Windows 程序安装与更新方案: Clowd.Squirrel

  2. 创建文件 Propertiesapp.manifest,并在项目属性→生成→设置清单设置该文件

    这一步是为了指定该项目exe需要创建快捷方式,否则安装时会将所有exe文件都建立一个快捷方式

    <?xml version="1.0" encoding="utf-8"?> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">   <SquirrelAwareVersion xmlns="urn:schema-squirrel-com:asm.v1">1</SquirrelAwareVersion> </assembly> 

    Windows 程序安装与更新方案: Clowd.Squirrel

  3. 在程序启动入口增加检查更新相关代码

    public static void Main(string[] args) {     // run Squirrel first, as the app may exit after these run     SquirrelAwareApp.HandleEvents(         onInitialInstall: OnAppInstall,         onAppUninstall: OnAppUninstall,         onEveryRun: OnAppRun);  	//本地文件夹或服务器地址 	using (var mgr = new UpdateManager(@"D:Desktoptest"))     {         var newVersion = await mgr.UpdateApp();          // optionally restart the app automatically, or ask the user if/when they want to restart         if (newVersion != null)         {             UpdateManager.RestartApp();         }     }     // ... other app init code after ... }  private static void OnAppInstall(SemanticVersion version, IAppTools tools) {     tools.CreateShortcutForThisExe(ShortcutLocation.StartMenu | ShortcutLocation.Desktop); }  private static void OnAppUninstall(SemanticVersion version, IAppTools tools) {     tools.RemoveShortcutForThisExe(ShortcutLocation.StartMenu | ShortcutLocation.Desktop); }  private static void OnAppRun(SemanticVersion version, IAppTools tools, bool firstRun) {     tools.SetProcessAppUserModelId();     // show a welcome message when the app is first installed     if (firstRun) MessageBox.Show("Thanks for installing my application!");  	// 启动你的应用 } 
  4. 版本号改成3段,需要符合SemVer规范

    [assembly: AssemblyVersion("1.3.2")] [assembly: AssemblyFileVersion("1.3.2")] 
  5. .csproj 项目文件增加下面的代码,编译 Release 时自动打包

    <Target Name="AfterReleaseBuild" AfterTargets="AfterBuild" Condition=" '$(Configuration)' == 'Release'">     <GetAssemblyIdentity AssemblyFiles="$(TargetPath)">       <Output TaskParameter="Assemblies" ItemName="myAssemblyInfo" />     </GetAssemblyIdentity>     <Exec Command="$(SolutionDir)packagesClowd.Squirrel.2.9.40toolsSquirrel.exe pack --packId $(ProjectName) --packVersion $([System.Version]::Parse(%(myAssemblyInfo.Version)).ToString(3)) --packAuthors XXX --packDirectory $(OutDir)" /> </Target> 

Squirrel.exe 参数

Squirrel pack`  --releaseDir .Release             # 更新输出到该目录  --framework net6,vcredist143-x86`  # Install .NET 6.0 (x64) and vcredist143 (x86) during setup, if not installed  --packId "YourApp"`                # Application / package name  --packTitle "YourApp"`                # Application / package name  --packVersion "1.0.0"`             # Version to build. Should be supplied by your CI  --packAuthors "YourCompany"`       # Your name, or your company name  --packDirectory ".publish"`       # The directory the application was published to  --icon "mySetupIcon.ico"`     # Icon for Setup.exe  --splashImage "install.gif"        # The splash artwork (or animation) to be shown during install 

发布更新

首次发布

切换Release模式,编译产生

Windows 程序安装与更新方案: Clowd.Squirrel

exe 用于首次安装,先将它发到web服务器,供用户下载

后续更新

代码稍作修改后,提高版本号,再次编译多出以下文件

Windows 程序安装与更新方案: Clowd.Squirrel

其中delta是相交于1.3.16的增量更新包,将RELEASES delta文件发到web服务器,UpdateManager类从该web服务器地址获取RELEASES,检查是否有更新,

你也可以再将Setup.exe文件发到web服务器覆盖旧的Setup.exe,以便新安装用户都能下载到最新的安装包

撤回更新

如果不小心发布了问题包。修改bug后,提高版本号,编译。

删除RELEASES文件中有问题的包信息,

发布full 和RELEASES,以便后续用户能更新到正常版本

快捷方式

根据下列顺序,第一个不为空的,作为快捷方式名称

  1. [assembly: AssemblyProduct("MyApp") (AssemblyInfo.cs)
  2. Squirrel.exe packTitle 参数
  3. [assembly: AssemblyDescription("MyApp") ( AssemblyInfo.cs)
  4. exe 文件名

这里我使用 packTitle ,方便控制Release与Test用不同的名称打包。

改进 .csproj 项目文件 内容

$(SolutionDir)packagesClowd.Squirrel.2.9.40toolsSquirrel.exe pack  --packTitle 我的APP$(Configuration) --packId $(Configuration).$(ProjectName) --packVersion $([System.Version]::Parse(%(myAssemblyInfo.Version)).ToString(3)) --packAuthors 作者 --packDirectory $(OutDir) --releaseDir .Publish$(Configuration) --icon $(ProjectDir)logo.ico 

user.config 问题

如果你的应用也使用user.config,会出现”更新版本后设置丢失,变成默认设置“的问题。根本原因是新版 exe 和旧版 exe 目录不同。

user.config 保存在

%LocalAppData%公司名MyApp.exe_[Url|StrongName]_Hash码版本号user.config

例如

C:UsersyournameAppDataLocalyourcompanyMyApp.exe_Url_qdx0no02b2yzg0ddn33isevehzmexfmy1.3.4.0user.config

其中Hash码是根据exe所在目录,exe名称等计算所得

而 Squirrel 更新会产生一个新的 app-版本号 文件夹,导致 user.config 目录变化,旧版本的用户设置在新版上不生效

Windows 程序安装与更新方案: Clowd.Squirrel

搜索一番解决方法比较复杂,例如重写一个设置适配器SettingsProvider

我的思路

在设置目录里查找,把MyApp.exe_Url_或MyApp.exe_StrongName_开头的文件夹,把低版本的user.config设置复制过来就行了

具体的代码逻辑

wpf

//检查本地配置文件夹 var configPath = GetDefaultExeConfigPath(ConfigurationUserLevel.PerUserRoamingAndLocal); var configName = "user.config"; var exeName = Assembly.GetExecutingAssembly().GetName().Name + ".exe"; var companyDirectoryName = configPath.Split(new string[1] { exeName }, StringSplitOptions.RemoveEmptyEntries)[0]; var companyDirectory = new DirectoryInfo(companyDirectoryName); if (companyDirectory.Exists) {     configPath = configPath.TrimEnd((@"" + configName).ToCharArray());     configPath = configPath.TrimEnd((@"" + Assembly.GetExecutingAssembly().GetName().Version).ToCharArray());      var configDirectory = new DirectoryInfo(configPath);     if (!configDirectory.Exists)     {          var urltargetName = exeName + "_Url_";//非强签名Url         var strongNametargetName = exeName + "_StrongName_";//强签名StrongName          var drs = companyDirectory.GetDirectories();         var theDrs = drs.Where(x => x.Name.StartsWith(urltargetName)).Concat(drs.Where(x => x.Name.StartsWith(strongNametargetName)));         if (theDrs.Count() > 0)         {             configDirectory.Create();             foreach (var theDr in theDrs)             {                 foreach (var d in theDr.GetDirectories())                 {                     CopyDirectory(d.FullName, configDirectory.FullName + @"" + d, true);                 }             }         }     } }  //最后,把低版本配置升级到最新版。 //新版本号下是否有user.config,如果没有从旧版本升级配置 if (!ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).HasFile) {     Settings.Default.Upgrade(); } 

winfrom(exeName 和 Version 获取方式和 wpf 不一样)

//检查本地配置文件夹 var configPath = Config.GetDefaultExeConfigPath(ConfigurationUserLevel.PerUserRoamingAndLocal); var configName = "user.config"; var exeName = ResourceAssembly.GetName().Name + ".exe"; var companyDirectoryName = configPath.Split(new string[1] { exeName }, StringSplitOptions.RemoveEmptyEntries)[0]; var companyDirectory = new DirectoryInfo(companyDirectoryName); if (companyDirectory.Exists) {     configPath = configPath.TrimEnd((@"" + configName).ToCharArray());     configPath = configPath.TrimEnd((@"" + ResourceAssembly.GetName().Version.ToString()).ToCharArray());     var configDirectory = new DirectoryInfo(configPath);     if (!configDirectory.Exists)     {          var urltargetName = exeName + "_Url_";//非强签名Url         var strongNametargetName = exeName + "_StrongName_";//强签名StrongName          var drs = companyDirectory.GetDirectories();         var theDrs = drs.Where(x => x.Name.StartsWith(urltargetName)).Concat(drs.Where(x => x.Name.StartsWith(strongNametargetName)));         if (theDrs.Count() > 0)         {             configDirectory.Create();             foreach (var theDr in theDrs)             {                 foreach (var d in theDr.GetDirectories())                 {                     CopyDirectory(d.FullName, configDirectory.FullName + @"" + d, true);                 }             }         }     } }  //最后,把低版本配置升级到最新版。 //新版本号下是否有user.config,如果没有从旧版本升级配置 if (!ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).HasFile) {     Print.Properties.Settings.Default.Upgrade();     Settings.Default.Upgrade(); } 
static void CopyDirectory(string sourceDir, string destinationDir, bool recursive) {     var dir = new DirectoryInfo(sourceDir);     if (!dir.Exists)         return;     DirectoryInfo[] dirs = dir.GetDirectories();     Directory.CreateDirectory(destinationDir);     foreach (FileInfo file in dir.GetFiles())     {         string targetFilePath = Path.Combine(destinationDir, file.Name);         if (!new FileInfo(destinationDir + @"" + file.Name).Exists)         {             file.CopyTo(targetFilePath, true);         }     }      if (recursive)     {         foreach (DirectoryInfo subDir in dirs)         {             string newDestinationDir = Path.Combine(destinationDir, subDir.Name);             CopyDirectory(subDir.FullName, newDestinationDir, true);         }     } } static string GetDefaultExeConfigPath(ConfigurationUserLevel userLevel) {     try     {         var UserConfig = ConfigurationManager.OpenExeConfiguration(userLevel);         return UserConfig.FilePath;     }     catch (ConfigurationException e)     {         return e.Filename;     } }