Ya hace bastante tiempo que NuGet salió y desde entonces se ha convertido en un compañero inseparable de todos nosotros. Y más que va a serlo cuando vNext salga de forma definitiva. En este post doy por supuesto que conoces NuGet y que lo has usado alguna vez (si no… ¡debes aprender a usarlo ya!). En este post quiero comentar los tres modos de funcionamiento que tiene NuGet y algunas cosillas más con las que me he encontrado.
Funcione NuGet en el modo en que funcione, cuando agregamos un paquete siempre ocurre lo mismo:
- Se crea (si no existe) un directorio packages a nivel de la solución (localizado en el mismo directorio que el .sln).
- Se descarga el paquete en dicho directorio
- Se crea (si no existe) un fichero packages.config en el proyecto al cual se haya agregado el paquete y se añade una línea indicando el paquete agregado.
- Se añade una referencia en el proyecto que apunta al ensamblado del paquete que se encuentra en el directorio packages. Y si el paquete tiene scripts de instalación adicionales, pues se ejecutan.
Recuerda que NuGet es básicamente un automatizador de agregar referencias en VS. Hace todo aquello que harías tu manualmente (descargar el ensamblado, guardarlo en algún sitio, agregar la referencia y cosas extra como editar web.config) y nada más (o nada menos, depende de como se mire).
Vamos a configurar NuGet para que funcione en el primero de los modos. Para ello abre VS2013 y en Tools –> Options –> NuGet Package Manager desmarca las dos checkboxes que están bajo el título de “Package Restore”. De esta manera NuGet funciona de la forma en que funcionaba originalmente. Y dicha forma consiste en que NuGet no hará nada más que lo que hemos descrito hasta ahora. Eso significa que cuando subas a tu repositorio de control de código fuente el proyecto debes incluir el directorio packages que contiene los binarios de los paquetes instalados por NuGet. Si no lo haces, cuando otra persona quiera descargarse el código fuente el proyecto no le compilará porque no encontrará las referencias a los paquetes NuGet:
¿Está todo perdido? Pues no, porque NuGet detectará que hay paquetes referenciados (eso lo sabe mirando el packages.config) que no existen en el directorio packages. Así mostrará el siguiente mensaje en la “Package Manager Console”:
Este mensaje aparece porque estamos en una versión nueva de NuGet. Cuando apareció NuGet este mensaje no aparecía y honestamente no sé cual era la solución entonces porque ese era un escenario no soportado: en la primera versión de NuGet los paquetes descargados debían subirse al control de código fuente.
Una versión de NuGet posterior habilitó el segundo modo de funcionamiento de NuGet. Para activarlo se debe pulsar sobre la solución en el “solution explorer” y seleccionar la opción “Enable NuGet Package Restore”:
Cuando se pulsa esta opción se crea una carpeta .nuget en la raíz de la solución que contiene tres ficheros (NuGet.config, NuGet.exe y NuGet.targets). Y además se modificarán los ficheros de la solución para agregarles por un lado una entrada nueva dentro de <PropertyGroup>:
- <RestorePackages>true</RestorePackages>
Y la ejecución de la tarea para que NuGet descargue los paquetes al compilar la solución:
- <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
- <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
- <PropertyGroup>
- <ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
- </PropertyGroup>
- <Error Condition="!Exists('$(SolutionDir)\.nuget\NuGet.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\.nuget\NuGet.targets'))" />
- </Target>
De este modo al compilar la solución NuGet se descargará los paquetes que no existan de forma automática. Habilitar esta opción te marcará automáticamente la primera de las checkboxes que habíamos desmarcado antes en Tools –> Options –> NuGet Package Manager.
Así, ahora, una de las preguntas más recurrentes sobre NuGet (¿Tengo que subir los paquetes a mi sistema de control de código fuente?) se respondía ahora diciendo que si los querías subir podías hacerlo sin problemas pero que si no, no era necesario siempre y cuando la solución tuviese habilitada la opción de “Package Restore”. En este último caso la carpeta .nuget si que debías subirla.
En principio con esos dos modos cubrimos la totalidad de los escenarios pero con la versión 2.7 de NuGet agregaron un tercer modo de funcionamiento. Dicho tercer modo es básicamente el modo anterior que acabamos de describir pero automatizado. Ya no tenemos que hacer nada, excepto marcar la segunda de las checkboxes que hemos desmarcado al principio (y es que este es, a partir de la versión 2.7, el modo por defecto de NuGet).
Para verla en acción marca la segunda checkbox, y luego borra la carpeta .nuget. Luego recompila la solución y recibirás un error: “This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is XXX.nugetNuget.targets”. Este error se da porque a pesar de que hemos borrado la carpeta .nuget, tenemos los proyectos todavía configurados para que la usen. Así, que no toca otra: abrir en modo texto los ficheros de proyecto y eliminar las líneas que se nos añadieron antes.
Una vez hecho esto, recargas los proyectos y al recompilar automáticamente NuGet descargará los paquetes. La ventaja de este modo de funcionamiento respecto al anterior es que no es intrusivo: no requiere modificar los ficheros de proyecto.
Si actualmente usas NuGet 2.7 o superior (que es de esperar que sí) y tienes la carpeta .nuget en tu repositorio de código fuente lo mejor que puedes hacer es eliminarla. Y luego modificar los proyectos para quitar las líneas indicadas anteriormente. Y con la segunda checkbox marcada (que es como está por defecto) ya tienes la descarga de paquetes automatizada. Por si tienes alguna duda sobre lo que tienes que eliminar en los ficheros de proyecto (aunque son las líneas mencionadas antes), todo el proceso está documentado en la propia web de NuGet. Si usas team build también son necesarias pequeñas modificaciones en TFS2012 o anterior (en TFS2013 así como Visual Studio Online o bien deploys en Azure web sites el proceso está ya integrado). De nuevo tienes toda la documentación en la web de NuGet sobre como configurar el team build.
De todos modos que tengas habilitada la descarga de paquetes automatizada no te impide colocar los paquetes (la carpeta packages) en el repositorio de control de código fuente: es una opción personal. Colocarlos en el sistema de control de código fuente te evita una dependencia con el propio NuGet (que aunque se cae pocas veces, a veces lo hace). Hace tiempo Juanma escribió en su blog un post sobre los peligros de depender del gestor de paquetes. Hay soluciones más elaboradas como no tener los paquetes en el control de código fuente pero usar un servidor de NuGet corporativo. Aquí ya, cada caso es un mundo.
Proyectos en varias soluciones
Vale, eso es un poco más frustrante y es un aviso más que otra cosa: si tienes un proyecto con paquetes gestionados por NuGet y este proyecto lo tienes en varias soluciones, asegúrate de que todas las soluciones (los ficheros .sln) están en el mismo directorio. En caso contrario puedes tener problemas. Es lógico una vez se entiende que hace NuGet y realmente es difícil que pueda hacer otra cosa que la que hace, así que bueno… es algo a tener en cuenta.
Vamos a reproducirlo paso a paso, para entender que ocurre. Para ello crea un directorio, yo lo he llamado nuroot. Luego crea otra carpeta (yo la he llamado folder1) dentro de nuroot:
Ahora crea una solución de VS (una aplicación de consola) dentro de folder1 (yo la he llamado DemoProject). Una vez hecho esto agrega un paquete de NuGet a la solución (p. ej. DotNetZip). Ahora la estructura de paquete debe ser como sigue:
El directorio packages está al nivel de la solución, y si miras en el proyecto verás que la referencia al ensamblado (en el caso de DotNetZip el ensamblado se llama Ionic.Zip) apunta al directorio packages. De hecho la referencia se guarda relativa al fichero de proyecto (si abres el .csproj en modo texto lo verás):
- <Reference Include="Ionic.Zip">
- <HintPath>..\packages\DotNetZip.1.9.3\lib\net20\Ionic.Zip.dll</HintPath>
- </Reference>
Perfecto. Ahora crea otra solución vacía (New Project –> Other Project Types –> Visual Studio Solution –> Empty Solution) y dale el nombre que quieras. Yo la he llamado SecondSolution. Lo importante es que la crees en nuroot, no en folder1. Por defecto VS crea un directorio para la solución, pero vamos a eliminarlo. Ve a nurootSecondSolution y mueve el fichero SecondSolution.sln a nuroot. Luego borra el directorio SecondSoution. En este punto la estructura de directorios es pues la misma de antes, con la salvedad de que en la carpeta nuroot hay el fichero SecondSolution.sln.
Finalmente agrega el proyecto existente (DemoProject) a la solución SecondSolution. ¡Una vez cargues el proyecto verás el mensaje de que faltan paquetes de NuGet! Dale a Restore para que NuGet se descargue los paquetes faltantes y la estructura de directorios será la siguiente:
Ten presente que NuGet funciona a nivel de solución. Cuando hemos agregado el proyecto DemoProject a la segunda solución, NuGet ha examinado el fichero packages.config del proyecto y ha visto una referencia a DotNetZip. Luego ha examinado el directorio packages de la solución. Y al estar esta otra solución en otro directorio que la anterior, NuGet no encuentra el directorio packages, y por lo tanto asume que debe descargarse los paquetes. Y al descargarlos es cuando nos aparece el otro directorio packages ahora colgando de nuroot (el directorio donde tenemos SecondSolution.sln).
Si compilas el proyecto todo funcionará pero hay un tema importante ahí. El proyecto DemoProject ya contenía una referencia al ensamblado de DotNetZip y al existir dicha referencia NuGet no la modificará. Es decir, la referencia sigue apuntando donde apuntaba inicialmente (nurootfolder1DemoProjectPackages).
Cierra VS y borra los dos directorios packages. Con esto simulas lo que le ocurriría a alguien que se descargase el código fuente (suponiendo que los paquetes no están subidos en él). Ahora carga SecondSolution.sln otra vez y de nuevo verás que NuGet dice que faltan paquetes (obvio, pues los hemos borrado todos). Restaura de nuevo y verás como NuGet crea otra vez el directorio nurootpackages. Pero el fichero csproj sigue teniendo la referencia a nurootfolder1DemoProject y por lo tanto no encontrará la referencia. Es decir, el código no compilará. Para que te compile debes abirlo con la solución DemoProject.sln, restaurar paquetes (o compilar simplemente, recuerda que al compilar se restauran automáticamente) y entonces ya te compilará (desde ambas soluciones).
En este escenario quizá no te parezca tan grave porque total, haces un readme.txt y que diga “Abrir primero DemoProject.sln” y listos. Hombre, es feo y un poco chapuza pero bueno…
Pero el problema lo tienes si luego añades otro paquete de NuGet al proyecto cuando lo tienes abierto con la solución SecondSolution.sln. P. ej. yo he instalado el CommandLineParser. Por supuesto este paquete está instalado en nurootpackages y la referencia del proyecto apunta a este directorio. Observa como han quedado las referencias del proyecto:
- <Reference Include="CommandLine">
- <HintPath>..\..\..\packages\CommandLineParser.1.9.71\lib\net45\CommandLine.dll</HintPath>
- </Reference>
- <Reference Include="Ionic.Zip">
- <HintPath>..\packages\DotNetZip.1.9.3\lib\net20\Ionic.Zip.dll</HintPath>
- </Reference>
Por lo tanto ahora si borras los directorios packages, debes abrir el proyecto en ambas soluciones y compilarlas en ambas (para forzar la restauración de paquetes). La primera solución que compiles te dará un error (le faltará el paquete que se añadió a través de la otra solución), da igual el orden en que lo hagas. La segunda que compiles si que compilará bien.
Al final terminarás con ambos paquetes instalados en ambos directorios packages pero solo se usará uno de cada (el referenciado por el proyecto).
Igual no te parece muy grave pero si tienes una build que compila una de esas soluciones dala por perdida: cada vez que se ejecuta la build se parte d un entorno nuevo, por lo que la build no compilará, está rota.
La mejor solución para ello es simplemente tenerlo presente: evita que un mismo proyecto esté en varias soluciones localizadas en directorios distintos (si las soluciones, los ficheros .sln, están todos en la misma carpeta no hay problema porque el directorio packages de todas ellas es el mismo).
Espero que el post os haya resultado interesante!
Saludos!