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

博客 分享
0 224
优雅殿下
优雅殿下 2022-05-21 17:59:29
悬赏:0 积分 收藏

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

我的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,

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

  2. 创建文件 Properties\app.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>

  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:\Desktop\test"))    {        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)packages\Clowd.Squirrel.2.9.40\tools\Squirrel.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,以便后续用户能更新到正常版本

快捷方式

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

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

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

改进 .csproj 项目文件 内容

$(SolutionDir)packages\Clowd.Squirrel.2.9.40\tools\Squirrel.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:\Users\yourname\AppData\Local\yourcompany\MyApp.exe_Url_qdx0no02b2yzg0ddn33isevehzmexfmy\1.3.4.0\user.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;    }}
posted @ 2022-05-21 17:46 Shayne Chow 阅读(0) 评论(0) 编辑 收藏 举报
回帖
    优雅殿下

    优雅殿下 (王者 段位)

    2018 积分 (2)粉丝 (47)源码

    小小码农,大大世界

     

    温馨提示

    亦奇源码

    最新会员