- A+
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
-
安装 Clowd.Squirrel
-
通过 nuget包管理器安装 Clowd.Squirrel,
-
安装后,目录 ..packagesClowd.Squirrel.2.9.40tools 里是用到的工具
-
-
创建文件 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>
-
在程序启动入口增加检查更新相关代码
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!"); // 启动你的应用 }
-
版本号改成3段,需要符合SemVer规范
[assembly: AssemblyVersion("1.3.2")] [assembly: AssemblyFileVersion("1.3.2")]
-
.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模式,编译产生
exe 用于首次安装,先将它发到web服务器,供用户下载
后续更新
代码稍作修改后,提高版本号,再次编译多出以下文件
其中delta是相交于1.3.16的增量更新包,将RELEASES delta文件发到web服务器,UpdateManager类从该web服务器地址获取RELEASES,检查是否有更新,
你也可以再将Setup.exe文件发到web服务器覆盖旧的Setup.exe,以便新安装用户都能下载到最新的安装包
撤回更新
如果不小心发布了问题包。修改bug后,提高版本号,编译。
删除RELEASES文件中有问题的包信息,
发布full 和RELEASES,以便后续用户能更新到正常版本
快捷方式
根据下列顺序,第一个不为空的,作为快捷方式名称
[assembly: AssemblyProduct("MyApp")
(AssemblyInfo.cs)- Squirrel.exe
packTitle
参数 [assembly: AssemblyDescription("MyApp")
( AssemblyInfo.cs)- 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 目录变化,旧版本的用户设置在新版上不生效
搜索一番解决方法比较复杂,例如重写一个设置适配器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; } }