分析nuget源码,用nuget + nuget.server实现winform程序的自动更新
源起
(个人理解)包管理最开始应该是从java平台下的maven开始吧,因为java的开发大多数是基于开源组件开发的,一个开源包在使用时很可能要去依赖其他的开源包,而且必须是特定的版本才可以。以往在找到一个开源包后,往往要用很多时间去把依赖的包找齐,于是maven出现了,它能自动搜索一个包的依赖项并下载到本地,免去找各种引用包的时间。
在maven出现不久后,.net也出现了自己的包管理工具,nuget,相信园子里的人都有所了解,nuget的官方源和microsoft源上集成了很多开源组件,供大家使用,而且在下载过程会进行相应解析,下载对应的依赖包。
上面是对包管理的一些介绍,理解包管理,那么很容易想到,有没有可能用包管理现成的组件来开发一个面向程序的自动更新?
主要有以下的好处:
1.更新的服务器端是现成的(nuget.server,nuget.galley)
2.发布工具是责成的(nuget command)
那么,主要就是要完成自动更新部分的检测,下载,以及解析。
先分析一下VS中包管理的方式:
1.所有包都维护在项目下的packages.config文件中;
2.在检测更新时,会连接到服务器上去进行检测(不同的包源)
3.要下载包
4.在包下载后,要将包解开,加到工程引用中;
那么,我们读源码的工作,主要如下:
1.理解怎么通过packages.config文件得到包的引用
2.得到包的引用后,如何去检测更新
3.怎么对包进行解析
下面和大家分享我的做法。
1.理解源码的第一步,需要懂得nuget.core中是怎么对这个packages.config进行解析,按照这种思路,在nuget.core中找到PackageReferenceFile这个类(直接全工程搜“package.config",最后定位于此)
namespace NuGet { public class PackageReferenceFile { public PackageReferenceFile(string path); public PackageReferenceFile(IFileSystem fileSystem, string path); public void AddEntry(string id, SemanticVersion version); public void AddEntry(string id, SemanticVersion version, FrameworkName targetFramework); public bool DeleteEntry(string id, SemanticVersion version); public bool EntryExists(string packageId, SemanticVersion version); [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This might be expensive")] public IEnumerable<PackageReference> GetPackageReferences(); public IEnumerable<PackageReference> GetPackageReferences(bool requireVersion); public void MarkEntryForReinstallation(string id, SemanticVersion version, FrameworkName targetFramework, bool requireReinstallation); } }
注意到一个很直接的构造函数
namespace NuGet { public class PackageReferenceFile { public PackageReferenceFile(string path); //other codes } }
在本地调用一下,发现成功能生成PackageReferenceFile类,同时用GetPackageReferences,能够得到一个IEnumerable<PackageReference>
继续代码,查到PackageReference定义:
namespace NuGet { public class PackageReference : IEquatable<PackageReference> { public PackageReference(string id, SemanticVersion version, IVersionSpec versionConstraint, FrameworkName targetFramework, bool isDevelopmentDependency, bool requireReinstallation = false); public string Id { get; } public bool IsDevelopmentDependency { get; } public bool RequireReinstallation { get; } public FrameworkName TargetFramework { get; } public SemanticVersion Version { get; } public IVersionSpec VersionConstraint { get; set; } public override bool Equals(object obj); public bool Equals(PackageReference other); public override int GetHashCode(); public override string ToString(); } }
可以看到,在这个类中包的ID,版本都有对应的属性来表达,那么这应该就是我们可以用来解析包引用的类,这样,我们第一步工作已经完成了,通过解析本地的文件得到了包的引用关系。
2.第二步,要理解怎么去检测更新。第一个直观的想法是查查有没有包函类似于update,getupdate方法的类,或者是接口,成功的找到最终的接口 IServiceBasedRepository
namespace NuGet { public interface IServiceBasedRepository : IPackageRepository { IEnumerable<IPackage> GetUpdates(IEnumerable<IPackage> packages, bool includePrerelease, bool includeAllVersions, IEnumerable<System.Runtime.Versioning.FrameworkName> targetFrameworks, IEnumerable<IVersionSpec> versionConstraints); IQueryable<IPackage> Search(string searchTerm, IEnumerable<string> targetFrameworks, bool allowPrereleaseVersions); } }
再查找实现这个接口的类,OK,我幸运的找到了表示服务器资源的类DataServicePackageRepository
namespace NuGet { public class DataServicePackageRepository : PackageRepositoryBase, IHttpClientEvents, IProgressProvider, IServiceBasedRepository, ICloneableRepository, ICultureAwareRepository, IOperationAwareRepository, IPackageLookup, IPackageRepository, ILatestPackageLookup, IWeakEventListener { public DataServicePackageRepository(IHttpClient client); public DataServicePackageRepository(Uri serviceRoot); public DataServicePackageRepository(IHttpClient client, PackageDownloader packageDownloader); public CultureInfo Culture { get; } public PackageDownloader PackageDownloader { get; } public override string Source { get; } public override bool SupportsPrereleasePackages { get; } public event EventHandler<ProgressEventArgs> ProgressAvailable; public event EventHandler<WebRequestEventArgs> SendingRequest; public IPackageRepository Clone(); public bool Exists(string packageId, SemanticVersion version); public IPackage FindPackage(string packageId, SemanticVersion version); public IEnumerable<IPackage> FindPackagesById(string packageId); public override IQueryable<IPackage> GetPackages(); public IEnumerable<IPackage> GetUpdates(IEnumerable<IPackage> packages, bool includePrerelease, bool includeAllVersions, IEnumerable<System.Runtime.Versioning.FrameworkName> targetFrameworks, IEnumerable<IVersionSpec> versionConstraints); public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e); public IQueryable<IPackage> Search(string searchTerm, IEnumerable<string> targetFrameworks, bool allowPrereleaseVersions); public IDisposable StartOperation(string operation, string mainPackageId, string mainPackageVersion); public bool TryFindLatestPackageById(string id, out SemanticVersion latestVersion); public bool TryFindLatestPackageById(string id, bool includePrerelease, out IPackage package); } }
这里面有两个方法一眼可以得知,一个是GetUpdates方法,显而易见,是查到有更新的包
另一个是构造函数 DataServicePackageRepository(Uri serviceRoot),即以nuget的源地址初始化,但是有一个问题,我们目前得到的是PackageReference,而函数里要调用的是IPackage,它的定义如下:
namespace NuGet { public interface IPackage : IPackageMetadata, IServerPackageMetadata { IEnumerable<IPackageAssemblyReference> AssemblyReferences { get; } bool IsAbsoluteLatestVersion { get; } bool IsLatestVersion { get; } bool Listed { get; } DateTimeOffset? Published { get; } [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This might be expensive")] IEnumerable<IPackageFile> GetFiles(); [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This might be expensive")] Stream GetStream(); [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This might be expensive")] IEnumerable<System.Runtime.Versioning.FrameworkName> GetSupportedFrameworks(); } }
同时,我又看了IPackage的每一个类(在源码中),没有一个可以从PackageReference直接进行构造,而其他的逻辑又太复杂(我比较懒哈),怎么办?看了一下GetUpdates的源码,发现在检测更新时只用到了Package类里的两个字段, 即id和version,OK,那么这样就好办了,我们自己定义一个IPackge的实现,只要实现id和version就可以:
class TempPackage :NuGet.IPackage { public string Id { get; internal set; } public NuGet.SemanticVersion Version { get; internal set; } //other codes that not Implemented }
OK,那么得,定义了这个,我们在检测更新前进行一下转换即可:
var localFiles = File.GetPackageReferences();//File is NuGet.PackageReferenceFile foreach (var i in localFiles) { localPacks.Add(new TempPackage() { Id = i.Id, Version = i.Version }); } var updatepacks = Source.GetUpdates(localPacks, false, false, null, null);//Source is DataServicePackageRepository
哈哈,至此,我们已经得到要更新的包。。 那么进入第三步,包的解析。
3.第三步,解析包,从自己定义TempPackage时,我们得到了IPackage的定义,发现有一个方法,MS不错,他是:
public interface IPackage : IPackageMetadata, IServerPackageMetadata { //other codes [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This might be expensive")] IEnumerable<IPackageFile> GetFiles(); }
什么意思,可以得到包中包含的文件么?IPackageFile又是什么??
namespace NuGet { public interface IPackageFile : IFrameworkTargetable { string EffectivePath { get; } string Path { get; } FrameworkName TargetFramework { get; } [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This might be expensive")] Stream GetStream(); } }
OK,在没有读源码的情况下,抱着试一试的感觉,我写了如下的代码:
var installFiles = localfile.GetFiles(); foreach (var savefile in installFiles) { byte[] data = new byte[savefile.GetStream().Length]; savefile.GetStream().Read(data, 0, data.Length); var fileinfo = new System.IO.FileInfo(startDir + "\\" + savefile.EffectivePath); if (System.IO.Directory.Exists(fileinfo.Directory.FullName) == false) System.IO.Directory.CreateDirectory(fileinfo.Directory.FullName); using (var filewrite = System.IO.File.Create(fileinfo.FullName)) { filewrite.Write(data, 0, data.Length); } }
哈哈,运行以后,我发现本地已经成功的解析出一个包里的相应文件 !!
到此,对源码的研究已经结束,下面就是按这个思路进行写软件了,至此,我们要解决的问题都已经全部完成,我又看了一下其他的部分,还有更优化的方案,即,可以用一个临时目录初始化一个LocalPackageRepository类,用服务器源和本地LocalPackageRepository的可以直接初始化PackageManager,在检测更新以后,直接用InstallPackage即可将包下载到本地。
4.更新文件的替换,因为程序在启动后,会自动加载相关的dll,那么怎么对更新文件进替换?其实很简单,用dynamic直接动态加载主窗体即可。
5.一些其他的技巧
看了一些人关于建立本地源以后自动化打包的方案,感觉都很麻烦,在一段时间摸索以后,发现要在post-build命令行中加如下命令,即可完成在编译后自动上传:
nuget pack "$(ProjectPath)" -o “本地临时目录” nuget push 本地临时目录$(TargetName).*.nupkg apiKey -S 本地服务器包源 move 本地临时目录*.nupkg 本地包源
当然,一编译就上传这个事儿有点过份哈,不过如果版本号不改话,客户端是检测不到更新的,所以,在测试没问题时,可以将版本号进行更新,这样客户端就能检测到相应的更新了。
本人写东西的能力一般,这些源码里穿插着如此多的废话主要是想和大家分享自己去目的性研究一些代码的方法和思路,如果说的不对或是不当,还请拍砖。
附: nuget官网 http://www.nuget.org/
建立self-host包源 http://docs.nuget.org/docs/creating-packages/hosting-your-own-nuget-feeds