Enero de 2017

Volumen 32, número 1

Essential .NET - Essential MSBuild: introducción a Build Engine para las herramientas de .NET

Por Mark Michaelis | Enero de 2017

Mark MichaelisAquellos que hayan estado siguiendo .NET Core durante los últimos años (¿hace tanto?) sabrán muy bien que el "sistema de compilación" ha experimentado una cantidad de flujo considerable, ya sea con la compatibilidad integrada de Gulp o el fin de Project.json. Para mí, como columnista, estos cambios han supuesto un reto, ya que no quería que mis queridos lectores tuvieran que invertir demasiado tiempo aprendiendo las características y los detalles que, en definitiva, solo estarían disponibles unos pocos meses. Este, por ejemplo, es el motivo por el que todos mis artículos relacionados con .NET Core se basaban en archivos *.CSPROJ basados en Visual Studio .NET 4.6 que hacían referencia a paquetes NuGet de .NET Core, en lugar de a proyectos de .NET Core compilados realmente.

Este mes, mes complace notificar que el archivo de proyecto para los proyectos de .NET Core se ha estabilizado (créanme) en un archivo de MSBuild. Sin embargo, no es el mismo archivo de MSBuild que en generaciones anteriores de Visual Studio, sino un archivo de MSBuild mejorado (simplificado). Es un archivo que, sin entrar en guerras religiosas sobre el uso de llaves o corchetes angulares, incluye las características de Project.json, pero con la compatibilidad con la herramienta adicional del archivo MSBuild tradicional que conocemos (¿y quizás que nos encanta?) desde el lanzamiento de Visual Studio 2005. En resumen, las características incluyen compatibilidad multiplataforma de código abierto, un formato simplificado que se puede editar y, por último, compatibilidad total con la herramienta .NET moderna, incluidas las referencias a archivos de comodines.

Compatibilidad de herramientas

A modo de aclaración, las características como los comodines siempre fueron compatibles en MSBuild, pero ahora las herramientas de Visual Studio también funcionan con estos. En otros términos, las noticias más importantes sobre MSBuild son que está firmemente integrado como base del sistema de compilación para todas las herramientas nuevas de .NET (DotNet.exe, Visual Studio 2017, Visual Studio Code y Visual Studio para Mac) y que admite los runtimes de .NET Core 1.0 y .NET Core 1.1.

La gran ventaja del acoplamiento firme entre las herramientas de .NET y MSBuild es que cualquier archivo de MSBuild que cree será compatible con todas las herramientas de .NET y podrá compilarse desde cualquier plataforma.

Las herramientas de .NET para la integración de MSBuild se acoplan a través de la API de MSBuild, no solo de un proceso de línea de comandos. Por ejemplo, la ejecución del comando Dotnet.exe Build de .NET CLI no genera internamente el proceso msbuild.exe. No obstante, llama a la API de MSBuild en proceso para ejecutar el trabajo (tanto el archivo MSBuild.dll como los ensamblados Microsoft.Build.*). Aún así, la salida (independientemente de la herramienta) es similar entre plataformas porque existe una plataforma de registro compartida con la que se registran todas las herramientas de .NET.

Estructura de archivos *.CSPROJ/MSBuild

Como he mencionado, el formato de archivo en sí se simplifica al mínimo. Admite comodines, referencias de paquetes de proyecto y NuGet, y varias plataformas. Además, los GUID de tipo de proyecto que se encuentran en los archivos de proyecto creados por Visual Studio del pasado han desaparecido.

En la Figura 1 se muestra un archivo *.CSPROJ/MSBuild de ejemplo.

Figura 1 Archivo CSProj/MSBuild básico de muestra

<Project>
  <PropertyGroup>
    <TargetFramework>netcoreapp1.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="**\*.cs" />
    <EmbeddedResource Include="**\*.resx" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NETCore.App">
      <Version>1.0.1</Version>
    </PackageReference>
    <PackageReference Include="Microsoft.NET.Sdk">
      <Version>1.0.0-*</Version>
      <PrivateAssets>All</PrivateAssets>
    </PackageReference>
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

Revisemos la estructura y las funcionalidades en profundidad:

Encabezado simplificado: Para empezar, observe que el elemento raíz es simplemente un elemento Project. Ya no se necesitan ni los atributos de espacio de nombres y versión:

ToolsVersion="15.0" xmlns="https://schemas.microsoft.com/developer/msbuild/2003"

(Sin embargo, aún se crean en las herramientas de la versión candidata para lanzamiento).  De manera similar, incluso la necesidad de importar las propiedades comunes es simplemente opcional:

<Import Project=
  "$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />

Referencias del proyecto: Desde el archivo del proyecto, puede agregar entradas a los elementos del grupo de elementos:

  • Paquetes NuGet:
<PackageReference Include="Microsoft.Extensions.Configuration">
  <Version>1.1.0</Version>
</PackageReference>
  • Referencias del proyecto:
<ProjectReference Include="..\ClassLibrary\ClassLibrary.csproj" />
  • Referencias de ensamblado:
<Reference Include="MSBuild">
  <HintPath>...</HintPath>
</Reference>

Una referencia de ensamblado directa debería ser la excepción, ya que, por lo general, se prefiere una referencia NuGet.

El comodín incluye: Archivos de código compilados y archivos de recursos que se pueden incluir a través de comodines.

<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
<Compile Remove="CodeTemplates\**" />
<EmbeddedResource Remove="CodeTemplates\**" />

No obstante, puede seguir seleccionando archivos específicos para ignorar el uso del atributo remove. (Tenga en cuenta que la compatibilidad de los comodines suele denominarse "englobamiento").

Compatibilidad con múltiples versiones: Para identificar la plataforma de destino, junto con el tipo de salida (opcional), puede usar un grupo de propiedades con los elementos TargetFramework:

<PropertyGroup>
  <TargetFramework>netcoreapp1.0</TargetFramework>
  <TargetFramework>netstandard1.3</TargetFramework>
</PropertyGroup>

Dadas estas entradas, la salida de cada destino se compilará en el directorio bin\Debug o bin\Release (según la configuración que especifique). Si existe más de un destino, la ejecución de la compilación colocará la salida en una carpeta correspondiente al nombre de la plataforma de destino.

Ningún GUID de tipo de proyecto: Observe que ya no es necesario incluir un GUID De tipo de proyecto que identifique el tipo de proyecto.

Integración de Visual Studio 2017

Cuando se trata de Visual Studio 2017, Microsoft sigue proporcionando una completa interfaz de usuario para editar el archivo de proyecto CSPROJ/MSBuild. En la Figura 2, por ejemplo, se muestra Visual Studio 2017 equipado con un listado de archivos CSPROJ, ligeramente modificado respecto al de la Figura 1, que incluye elementos de la plataforma de destino de netcoreapp1.0 y net45, junto con referencias de paquetes de Microsoft.Extensions.Configuration, Microsoft.NETCore.App y Microsoft.NET.Sdk, además de una referencia de ensamblado a MSBuild y una referencia de proyecto a SampleLib.                                                                                                           

El Explorador de soluciones es una completa interfaz de usuario sobre un archivo CSProj
Figura 2 El Explorador de soluciones es una completa interfaz de usuario sobre un archivo CSProj

Observe cómo cada tipo de dependencia (ensamblado, paquete NuGet o referencia de proyecto) tiene un nodo de grupo correspondiente en el árbol Dependencias del Explorador de soluciones.

Además, Visual Studio 2017 admite la recarga dinámica del proyecto y la solución. Por ejemplo, si se agrega un nuevo archivo al directorio del proyecto (uno que coincida con el de los comodines globales), Visual Studio 2017 detecta automáticamente los cambios y muestra los archivos en el Explorador de soluciones. De manera similar, si se excluye un archivo de un proyecto de Visual Studio (a través de la opción de menú de Visual Studio o la ventana de propiedades de Visual Studio), Visual Studio actualizará automáticamente el archivo de proyecto según corresponda. (Por ejemplo, agregará un elemento <Compile Remove="CommandLine.cs" /> para excluir el archivo CommandLine.cs de la compilación dentro del proyecto). 

Además, las ediciones en el archivo de proyecto se detectarán y volverán a cargar automáticamente en Visual Studio 2017. De hecho, el nodo de proyecto de Visual Studio en el Explorador de soluciones admite ahora una opción de menú Editar <archivo de proyecto>, que abre el archivo de proyecto en la ventana de edición de Visual Studio sin solicitar primero que se descargue el proyecto.

Visual Studio 2017 incluye soporte de migración integrado para convertir proyectos al nuevo formato MSBuild. Si acepta esta solicitud de confirmación, el proyecto se actualizará automáticamente del tipo Project.json/*.XPROJ al tipo MSBUILD/*.CSPROJ. Tenga en cuenta que esta actualización eliminará la compatibilidad con versiones anteriores de proyectos de .NET Core de Visual Studio 2015, por lo que no puede tener a parte de su equipo trabajando en el mismo proyecto de .NET Core en Visual Studio 2017 mientras que otros usan Visual Studio 2015.

MSBuild

Sería un descuido no señalar que en marzo de 2016, Microsoft lanzó MSBuild como código abierto en GitHub (github.com/Microsoft/msbuild), que se sumó a .NET Foundation (dotnetfoundation.org). Al establecer MSBuild como código abierto, lo encamina a la portabilidad de la plataforma a Mac y Linux, lo que, en última instancia, permite convertirlo en el motor de compilación subyacente para todas las herramientas de .NET.

Aparte del elemento PackageReference del archivo CSPROJ\MSBuild identificado anteriormente, MSBuild versión 15 no introduce muchas características adicionales más allá del código abierto y la capacidad multiplataforma. De hecho, al comparar la ayuda de la línea de comandos, se demuestra que las opciones son idénticas. Para aquellos que no estén familiarizados, esta es una lista de las opciones más comunes que debe conocer de la sintaxis general MSBuild.exe [options] [project file]:

/target:<target>: identifica el destino de compilación en el archivo de proyecto que se debe ejecutar junto con todas las dependencias que pueda tener (/t es la abreviatura).

/property:<n>=<v>: establece o reemplaza todas las propiedades del proyecto (identificadas en el elemento ProjectGroup de un archivo de proyecto). Por ejemplo, puede usar la propiedad para cambiar la configuración o el directorio de salida, como en /property:Configuration=Release;OutDir=bin\ (/p es la abreviatura).

/maxcpucount[:n]: especifica el número de CPU que se va a usar. De manera predeterminada, msbuild se ejecuta en una sola CPU (uniproceso). Si la sincronización no es un problema, puede especificar el nivel de simultaneidad para aumentar esta cantidad. Si especifica la opción /maxcpucount sin proporcionar un valor, msbuild usará el número de procesadores del equipo.

/preprocess[:file]: genera un archivo de proyecto agregado mediante la alineación de todos los destinos incluidos. Puede resultar útil para la depuración cuando surge un problema.

@file: proporciona uno (o más) archivos de respuesta que contienen opciones. Estos archivos tienen cada opción de línea de comandos en una línea diferente (los comentarios presentan el prefijo "#"). De manera predeterminada, MSBuild importará un archivo denominado msbuild.rsp del primer proyecto o solución creados. Por ejemplo, el archivo de respuesta resulta útil para identificar diferentes destinos y propiedades de compilación según el entorno (desarrollo, pruebas o producción) donde se realice la compilación.

Dotnet.exe

La línea de comandos dotnet.exe de .NET se introdujo hace un año aproximadamente en un mecanismo multiplataforma para generar, compilar y ejecutar proyectos basados en .NET Core. Como ya he mencionado, se ha actualizado y ahora depende enormemente de MSBuild como motor interno para el peso de su trabajo donde cobra sentido.

A continuación, se incluye una introducción de los distintos comandos:

dotnet new: Crea el proyecto inicial. De manera predefinida, este generador de proyectos admite los tipos de proyecto Consola, Web, Lib, MSTest y XUnitTest. No obstante, en el futuro puede esperar que le permita proporcionar plantillas personalizadas, de modo que pueda generar sus propios tipos de proyecto. (Cuando esto sucede, el nuevo comando no depende de MSBuild para generar el proyecto).

dotnet restore: lee las dependencias del proyecto especificadas en el archivo de proyecto y descarga los paquetes NuGet que faltan y las herramientas allí identificadas. El archivo de proyecto en sí puede especificarse como un argumento o implicarse desde el directorio actual (si existe más de un archivo de proyecto en el directorio actual, se debe especificar cuál se va a usar). Observe que, dado que restore aprovecha el motor de MSBuild para su trabajo, el comando dotnet permite opciones de línea de comandos adicionales de MSBuild.

dotnet build: recurre al motor de MSBuild para ejecutar el destino de compilación (de manera predeterminada) en el archivo de proyecto. Del mismo modo que el comando restore, puede pasar argumentos de MSBuild al comando dotnet build. Por ejemplo, un comando como dotnet build /property:configuration=Release desencadenará la salida de una compilación de versión en lugar de una compilación de depuración (opción predeterminada). De manera similar, puede especificar el destino de MSBuild mediante /target (o /t). El comando dotnet build /t:compile, por ejemplo, ejecutará el destino de compilación.

dotnet clean: quita toda la salida de compilación, de modo que se ejecutará una compilación completa en lugar de una compilación incremental.

dotnet migrate: actualiza un proyecto basado en Project.json/*.XPROJ en formato *.CSPROJ/MSBuild.

dotnet publish: combina toda la salida de compilación junto con todas las dependencias en una sola carpeta, para almacenarla de manera provisional con la finalidad de implementarla en otra máquina. Esto resulta especialmente útil para la implementación autocontenida que incluye no solo la salida de compilación y los paquetes de dependencias, sino también el propio runtime de .NET Core. Una aplicación autocontenida no tiene ningún requisito previo de que una versión concreta de la plataforma .NET esté instalada en la máquina de destino.

dotnet run: inicia .NET Runtime y hospeda el proyecto y/o los ensamblados compilados para ejecutar el programa. Observe que, para ASP.NET, la compilación no es necesaria, ya que el propio proyecto se puede hospedar.

Existe una superposición considerable entre ejecutar msbuild.exe y dotnet.exe, lo que deja a su elección cuál se debe ejecutar. Si está compilando el destino de msbuild predeterminado, puede ejecutar simplemente el comando "msbuild.exe" desde el directorio del proyecto y este compilará y generará el destino por usted. El comando dotnet.exe equivalente es "dotnet.exe msbuild". Por otro lado, si ejecuta un destino "limpio", el comando es "msbuild.exe /t:clean" con MSBuild, frente a "dotnet.exe clean" con dotnet. Además, ambas herramientas son compatibles con las extensiones. MSBuild presenta un marco de extensibilidad completo tanto en el propio archivo del proyecto como a través de los ensamblados de .NET (consulte bit.ly/2flUBza). De manera similar, dotnet se puede extender, aunque la recomendación al respecto también implica esencialmente la extensión de MSBuild y un poco más de ceremonia.

Aunque me gusta la idea de dotnet.exe, al final, no parece ofrecer muchas ventajas comparado con MSBuild, excepto en que hace cosas que MSBuild no admite (de las cuales, dotnet new y dotnet run son quizás las más importantes). Después de todo, creo que MSBuild permite hacer las cosas sencillas fácilmente y permite realizar las complicadas cuando es necesario. Además, incluso las cosas complicadas de MSBuild se pueden simplificar proporcionando valores predeterminados razonables. Básicamente, si se prefiere dotnet o MSBuild es solo una cuestión de preferencia y el tiempo dirá por cuál se decide en general la comunidad de desarrollo para el front-end de la CLI.

global.json

Aunque la funcionalidad Project.json ha migrado a CSPROJ, global.json sigue siendo totalmente compatible. El archivo permite la especificación de directorios de proyectos y directorios de paquetes, e identifica la versión del SDK que se va a usar. Este es un archivo global.json de muestra:

{
  "projects": [ "src", "test" ],
  "packages": "packages",
  "sdk": {
    "version": "1.0.0-preview3",
    "runtime": "clr",
    "architecture": "x64"
  }
}

Las tres secciones se corresponden con los principales propósitos del archivo global.json:

projects: identifica los directorios raíz donde se encuentran los proyectos de .NET. El nodo projects es crítico para la depuración en el código fuente de .NET Core. Después de clonar el código fuente, puede agregar el directorio en el nodo projects y, a continuación, Visual Studio lo cargará automáticamente como en un proyecto en la solución.

packages: indica la ubicación de la carpeta de paquetes NuGet.

sdk: especifica la versión de runtime que se va a usar.

Resumen

En este artículo, he proporcionado una amplia introducción de todas las ubicaciones donde se usa MSBuild en el conjunto de herramientas de .NET. Antes de acabar, me gustaría proporcionar algunos consejos extraídos de mi experiencia con el trabajo en proyectos con miles de líneas de MSBuild. No caiga en la trampa de excederse con el scripting en un lenguaje declarativo escrito libremente, como el esquema XML de MSBuild. No es ese su propósito. En su lugar, los archivos de proyecto deben ser contenedores relativamente finos que identifiquen el orden y las dependencias entre sus destinos de compilación. Si permite que el archivo de proyecto de MSBuild crezca demasiado, puede resultar difícil de mantener. No espere demasiado antes de refactorizarlo en algo como, por ejemplo, tareas de MSBuild para C# que puedan depurarse y someterse fácilmente a pruebas unitarias.


Mark Michaelis es el fundador de IntelliTect y trabaja de arquitecto técnico como jefe y formador. Durante casi dos décadas, ha sido MVP de Microsoft y director regional de Microsoft desde 2007. Michaelis trabaja con varios equipos de revisión de diseño de software de Microsoft, como C#, Microsoft Azure, SharePoint y Visual Studio ALM. Hace presentaciones en conferencias de desarrolladores y ha escrito varios libros, el más reciente de ellos "Essential C# 6.0 (5th Edition)" (itl.tc/EssentialCSharp). Póngase en contacto con él en Facebook en facebook.com/Mark.Michaelis, en su blog IntelliTect.com/Mark, en Twitter @markmichaelis o a través de la dirección de correo electrónico mark@IntelliTect.com.

Gracias a los siguientes expertos técnicos de IntelliTect por revisar este artículo: Kevin Bost, Grant Erickson, Chris Finlayson, Phil Spokas y Michael Stokesbary