• 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;
        }
    }
    
  • 相关阅读:
    MVC把表格导出到Excel
    MVC借助Masonry实现图文瀑布流
    MVC Ajax Helper或jQuery异步方式加载部分视图
    MVC使用Entity Framework Code First,用漂亮表格显示1对多关系
    MVC使用jQuery从视图向控制器传递Model的2种方法
    MVC使用AdditionalMetadata为Model属性添加额外信息
    MVC日期格式化的2种方式
    MVC使用百度开源文本编辑器UEditor实现图文并茂,字数限制,上传图片或涂鸦
    MVC使用StructureMap实现依赖注入Dependency Injection
    通过扩展jQuery UI Widget Factory实现手动调整Accordion高度
  • 原文地址:https://www.cnblogs.com/ohzxc/p/16295615.html
Copyright © 2020-2023  润新知