分析nuget原始碼,用nuget + nuget.server實現winform程式的自動更新

葛雲飛發表於2013-11-29

源起

(個人理解)包管理最開始應該是從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

 

 

 

 

 

 

相關文章