版控的下一步

前篇文章提及,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



   …….

    

        

    

  





    ……..

      

    

注意type的定义,这里是,

完成后执行就可以看到以下的画面。

图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: { libx86*.lib };

        };

        [x64] {  // x64, dll

            lib: { libx64*.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存放于UserAppDataRoamingNuget目录下。

图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