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,
-
安裝後,目錄 ..\packages\Clowd.Squirrel.2.9.40\tools 裡是用到的工具
-
-
建立檔案 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>
-
在程式啟動入口增加檢查更新相關程式碼
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!"); // 啟動你的應用 }
-
版本號改成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)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,以便後續使用者能更新到正常版本
快捷方式
根據下列順序,第一個不為空的,作為快捷方式名稱
[assembly: AssemblyProduct("MyApp")
(AssemblyInfo.cs)- Squirrel.exe
packTitle
引數 [assembly: AssemblyDescription("MyApp")
( AssemblyInfo.cs)- 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;
}
}