版控的下一步-Build System – Private Nuget Server

前篇文章提及,VSTS其實可以自動透過Nuget Restore來取得專案所使用的套件,這樣不僅可以避免直接在專案中硬性加入參考,也可以解決套件散亂難以管理的問題,當然,專案中許多套件是屬於自行研發,也不打算放上Public Nuget Server給外部人使用的,這時候就需要自行架構Nuget Server。

 

 

 

文/黃忠成

 

 

Private Nuget Server

 

  前篇文章提及,VSTS其實可以自動透過Nuget Restore來取得專案所使用的套件,這樣不僅可以避免直接在專案中硬性加入參考,也可以解決套件散亂難以管理的問題,當然,專案中許多套件是屬於自行研發,也不打算放上Public Nuget Server給外部人使用的,這時候就需要自行架構Nuget Server。

  透過ASP.NET,自行建構Nuget Server其實很簡單,只要建立一個新的ASP.NET 專案,接著透過Nuget取得Nuget.Server套件安裝加上一些簡易設定就可完成。

 

圖001

裝完後透過web.config來調整設定。

圖002

只有一個部份需注意,那就是appKey的部分,這用於upload nuget package時所需要的application key,請自行輸入即可,完成後執行便可看到下圖畫面。

圖003

要特別注意的是,如果是要讓VSTS使用,那麼這個Nuget Server必須處於公開的網路環境,也就是任何人都可以透過網際網路存取到這個Nuget Server,這我想不是你所想要的,因此我們需要為這個Nuget Server加上安全機制,本例中用最簡單的Basic Authenticate,如果是真實環境下,建議至少加上SSL。

首先在專案中加入以下的程式碼。


BasicAuthenticateModule.cs

using System;

using System.Collections.Generic;

using System.Linq;

using System.Net.Http.Headers;

using System.Security.Principal;

using System.Text;

using System.Threading;

using System.Web;



namespace CiNugetServer

{

    public class BasicAuthHttpModule : IHttpModule

    {

        private const string Realm = "My Realm";



        public void Init(HttpApplication context)

        {

            // Register event handlers

            context.AuthenticateRequest += OnApplicationAuthenticateRequest;

            context.EndRequest += OnApplicationEndRequest;

        }



        private static void SetPrincipal(IPrincipal principal)

        {

            Thread.CurrentPrincipal = principal;

            if (HttpContext.Current != null)

            {

                HttpContext.Current.User = principal;

            }

        }



        // TODO: Here is where you would validate the username and password.

        private static bool CheckPassword(string username, string password)

        {

            return username == "user" && password == "password";

        }



        private static void AuthenticateUser(string credentials)

        {

            try

            {

                var encoding = Encoding.GetEncoding("iso-8859-1");

                credentials = encoding.GetString(Convert.FromBase64String(credentials));



                int separator = credentials.IndexOf(':');

                string name = credentials.Substring(0, separator);

                string password = credentials.Substring(separator + 1);



                if (CheckPassword(name, password))

                {

                    var identity = new GenericIdentity(name);

                    SetPrincipal(new GenericPrincipal(identity, null));

                }

                else

                {

                    // Invalid username or password.

                    HttpContext.Current.Response.StatusCode = 401;

                }

            }

            catch (FormatException)

            {

                // Credentials were not formatted correctly.

                HttpContext.Current.Response.StatusCode = 401;

            }

        }



        private static void OnApplicationAuthenticateRequest(object sender, EventArgs e)

        {

            var request = HttpContext.Current.Request;

            var authHeader = request.Headers["Authorization"];

            if (authHeader != null)

            {

                var authHeaderVal = AuthenticationHeaderValue.Parse(authHeader);



                // RFC 2617 sec 1.2, "scheme" name is case-insensitive

                if (authHeaderVal.Scheme.Equals("basic",

                        StringComparison.OrdinalIgnoreCase) &&

                    authHeaderVal.Parameter != null)

                {

                    AuthenticateUser(authHeaderVal.Parameter);

                }

            }

        }



        // If the request was unauthorized, add the WWW-Authenticate header

        // to the response.

        private static void OnApplicationEndRequest(object sender, EventArgs e)

        {

            var response = HttpContext.Current.Response;

            if (response.StatusCode == 401)

            {

                response.Headers.Add("WWW-Authenticate",

                    string.Format("Basic realm=\"{0}\"", Realm));

            }

        }



        public void Dispose()

        {

        }

    }



}

 

接著修改web.config,加入HTTP Module設定。

 

web.config

 

<system.web>

   …….

    <authorization>

        <deny users="?"/>

    </authorization>

  </system.web>



<system.webServer>

    ……..

      <add name="MyBasicAuthenticationModule" type="CiNugetServer.BasicAuthHttpModule, CiNugetServer"/>

    </modules>

注意type的定義,這裡是<TypeName>, <Assembly Name>

完成後執行就可以看到以下的畫面。

圖004

接著就可以這個專案直接部署到Azure Web Sites去了。

由於已經加入了驗證,但Visual Studio卻沒有地方可以輸入驗證資訊,所以得用命令列方式來加入。

nuget sources add -Name "Cinuget" -Source "http://xxxxxx.azurewebsites.net/nuget" -UserName "nugettest" -Password "nugettest123"

如果已經透過Visual Stidio加入過,那麼就得使用update命令。

nuget sources update -Name "Cinuget" -Source "http://xxxxxx.azurewebsites.net/nuget" -UserName "nugettest" -Password "nugettest123"

 

 

發佈Package

 

  其實很簡單,透過Nuget Package Explorer工具,就可以輕易地發佈自己撰寫的Package了。

https://github.com/NuGetPackageExplorer/NuGetPackageExplorer

對於.NET/Web相關的Package,Nuget Package Explorer其實就已經很夠用了,網路上也有很多說明文件,這裡就不再贅述,有興趣的可以參考黑大的文章。

http://blog.darkthread.net/post-2011-03-29-create-nuget-package.aspx

  但如果要發佈的Package是一個Native,也就是C++的Package,那問題就複雜許多了,在目前的Nuget中已經支援C++類型的Package,首先必須安裝CoApp Tools。

http://coapp.org/pages/releases.html

  接著準備要發佈的套件內容,本例中以Direct X SDK June為例,一般來說需要的就是include及lib目錄,這裡我建立一個新目錄,接著把SDK中的include、lib都複製過來。

圖005

然後要建立一個.autopkg檔案,這是CoApp工具所需要的檔案,他必須存放在套件的根目錄下,以本例來說就是DirectXJune目錄下。


Dxnugetsdk.autopkg

nuget{

   nuspec{

        id = DXSDKJUNE;

        version : 11.0;

        title: DirectX SDK(June);

        authors: {Microsoft Corporation};

        owners: {code6421, GIS};

        tags: { native, SDK, DirectX };

    }

   files {



        include: { "include\*" };

        [x86] {  // x86, dll

            lib: { lib\x86\*.lib };

        };

        [x64] {  // x64, dll

            lib: { lib\x64\*.lib };

        }

    }

}


最後透過CoApp的工具來產生.nupkg檔案及上傳至Nuget Server,注意,這是Powershell。內容不難懂,重點在於include區塊,多數的C++ SDK都包含兩種平台:x86、x64,不同平台需要Link不同的lib。

 

產生nupk的命令列

Write-NuGetPackage .\dxjunesdk.autopkg

上傳

nuget.exe push .\DXSDKJUNE.11.0.nupkg -s http://yourserver.azurewebsites.net/ yourkey

過程中會詢問user/password。

一切無誤的話,就可以在Visual Studio使用這個套件了。

圖006

 

透過VSTS來建置

 

  基本上與之前提過的方式差不多,一樣是先加入版控,接著透過VSTS建立Build,唯一要特別注意的是,這裡使用的是Private Nuget Server,而且是具備驗證機制的,所以必須透過上傳Nuget.config的方式來讓VSTS取得這些資訊,nuget.config存放於User\AppData\Roaming\Nuget目錄下。

圖007

這個檔案需要放置在版控中,通常我會放置於方案跟目錄,在建立Build的時候就可以直接指定。

圖008

習慣上我會把Clean跟Nuget package restore打勾。

圖009

對於C++這種分平台的專案來說,必須要特別注意要Build的平台 $(BuildPlatform)參數的值,這裡指定為x64(其實當開發的是混合型的專案時,這裡會很有趣,後面有機會再談談這個)。

圖010

結果如圖11,通常不會有太大問題。

圖11

 

必須注意的事

 

  在我的環境中曾經發生過VSTS無法使用Nuget.config中存放的驗證資訊來Restore Package,這通常會導致Build Fail,如果你有發生這種情況,那麼試著使用以下指令來存放明碼試試。

另外一種情況比較詭異,出現莫名的MSBuild Task錯誤,這種情況後來是把Packages目錄由版控exclude後,讓VSTS在每次Build前還原整個Nuget Packages後解決。

PS: 事實上Packages本來就不該在版控中,但有時候Nuget Server或是Package上傳者的錯誤,導致某個Package在短時間出現無法由Nuget取得而導致錯誤,所以對於線上的專案,我還是會把Packages包進去。

 

nuget sources update -Name "Cinuget" -Source "http://xxxxxx.azurewebsites.net/nuget" -UserName "nugettest" -Password "nugettest123" -StorePasswordInClearText