Compartir a través de


Creación de un módulo binario de biblioteca estándar

Recientemente tuve una idea para el módulo que quería implementar como módulo binario. Aún tengo que crear uno mediante la biblioteca de PowerShell estándar, por lo que me pareció que se trataba de una buena oportunidad. He usado la guía Crear un módulo binario multiplataforma para crear este módulo sin problemas. Vamos a caminar ese mismo proceso y agregaré un pequeño comentario adicional a lo largo del camino.

Nota

La versión original de este artículo apareció en el blog escrito por @KevinMarquette. El equipo de PowerShell agradece a Kevin por compartir este contenido con nosotros. Consulte su blog en PowerShellExplained.com.

¿Qué es la biblioteca estándar de PowerShell?

La biblioteca estándar de PowerShell nos permite crear módulos multiplataforma que funcionan tanto en PowerShell como en Windows PowerShell 5.1.

¿Por qué los módulos binarios?

Al escribir un módulo en C#, se pierde el acceso fácil a los cmdlets y funciones de PowerShell. Pero si va a crear un módulo que no depende de muchos otros comandos de PowerShell, la ventaja de rendimiento puede ser significativa. PowerShell se ha optimizado para el administrador, no para el equipo. Al cambiar a C#, puede deshacerse de la sobrecarga añadida por PowerShell.

Por ejemplo, tenemos un proceso crítico que funciona mucho con JSON y tablas hash. Hemos optimizado PowerShell tanto como podamos, pero el proceso tarda 12 minutos en completarse. El módulo ya contenía una gran cantidad de powerShell de estilo C#. Esto hace que la conversión a un módulo binario sea limpia y sencilla. Al convertir en un módulo binario, hemos reducido el tiempo de proceso de más de 12 minutos a menos de cuatro minutos.

Módulos híbridos

Puede combinar cmdlets binarios con funciones avanzadas de PowerShell. Todo lo que sabe sobre los módulos de script se aplica de la misma manera. El archivo psm1 vacío se incluye para que pueda agregar otras funciones de PowerShell más adelante.

Casi todos los cmdlets compilados que he creado se iniciaron primero como funciones de PowerShell. Todos nuestros módulos binarios son realmente módulos híbridos.

Scripts de compilación

He mantenido el script de compilación sencillo aquí. En general, se usa un script Invoke-Build de gran tamaño como parte de mi canalización de CI/CD. Realiza otros trucos mágicos, como la ejecución de pruebas de Pester, la ejecución de PSScriptAnalyzer, la administración de versiones y la publicación en PSGallery. Una vez que empecé a usar un script de compilación para mis módulos, pude encontrar muchas cosas que agregar a él.

Planeamiento del módulo

El plan de este módulo es crear una carpeta src para el código de C# y estructurar el resto como lo haría para un módulo de script. Esto incluye el uso de un script de compilación para compilar todo en una carpeta Output. La estructura de carpetas tiene este aspecto:

MyModule
├───src
├───Output
│   └───MyModule
├───MyModule
│   ├───Data
│   ├───Private
│   └───Public
└───Tests

Empezar

En primer lugar, necesito crear la carpeta y crear el repositorio de Git. Uso $module como marcador de posición para el nombre del módulo. Esto debería facilitar la reutilización de estos ejemplos si es necesario.

$module = 'MyModule'
New-Item -Path $module -Type Directory
Set-Location $module
git init

A continuación, cree las carpetas de nivel raíz.

New-Item -Path 'src' -Type Directory
New-Item -Path 'Output' -Type Directory
New-Item -Path 'Tests' -Type Directory
New-Item -Path $module -Type Directory

Configuración del módulo binario

Este artículo se centra en el módulo binario, así que empezaremos por ahí. En esta sección se extraen ejemplos de la guía Creación de un módulo binario multiplataforma. Revise esa guía si necesita más detalles o tiene algún problema.

Lo primero que queremos hacer es comprobar la versión del SDK de dotnet core que hemos instalado. Estoy usando la versión 2.1.4, pero debe tener la versión 2.0.0 o posterior antes de continuar.

PS> dotnet --version
2.1.4

Trabajo fuera de la carpeta src para esta sección.

Set-Location 'src'

Con el comando dotnet, cree una nueva biblioteca de clases.

dotnet new classlib --name $module

Esto creó el proyecto de biblioteca en una subcarpeta, pero no deseo ese nivel de anidamiento adicional. Voy a mover esos archivos a un nivel.

Move-Item -Path .\$module\* -Destination .\
Remove-Item $module -Recurse

Establezca la versión del SDK de .NET Core para el proyecto. Tengo el SDK 2.1, así que voy a especificar 2.1.0. Use 2.0.0 si usa el SDK 2.0.

dotnet new globaljson --sdk-version 2.1.0

Agregue el paquete NuGet de la biblioteca de PowerShell estándar al proyecto. Asegúrese de usar la versión más reciente disponible para el nivel de compatibilidad que necesita. El valor predeterminado es la versión más reciente, pero no creo que este módulo aproveche las características más recientes que PowerShell 3.0.

dotnet add package PowerShellStandard.Library --version 7.0.0-preview.1

Deberíamos tener una carpeta src que tenga este aspecto:

PS> Get-ChildItem
    Directory: \MyModule\src

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----        7/14/2018   9:51 PM                obj
-a----        7/14/2018   9:51 PM             86 Class1.cs
-a----        7/14/2018  10:03 PM            259 MyModule.csproj
-a----        7/14/2018  10:05 PM             45 global.json

Ahora estamos listos para agregar nuestro propio código al proyecto.

Creación de un cmdlet binario

Es necesario actualizar el src\Class1.cs para que contenga este cmdlet de inicio:

using System;
using System.Management.Automation;

namespace MyModule
{
    [Cmdlet( VerbsDiagnostic.Resolve , "MyCmdlet")]
    public class ResolveMyCmdletCommand : PSCmdlet
    {
        [Parameter(Position=0)]
        public Object InputObject { get; set; }

        protected override void EndProcessing()
        {
            this.WriteObject(this.InputObject);
            base.EndProcessing();
        }
    }
}

Cambie el nombre del archivo para que coincida con el nombre de clase.

Rename-Item .\Class1.cs .\ResolveMyCmdletCommand.cs

A continuación, podemos compilar nuestro módulo.

PS> dotnet build

Microsoft (R) Build Engine version 15.5.180.51428 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

Restore completed in 18.19 ms for C:\workspace\MyModule\src\MyModule.csproj.
MyModule -> C:\workspace\MyModule\src\bin\Debug\netstandard2.0\MyModule.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:02.19

Podemos llamar a Import-Module en el nuevo archivo DLL para cargar el nuevo cmdlet.

PS> Import-Module .\bin\Debug\netstandard2.0\$module.dll
PS> Get-Command -Module $module

CommandType Name                    Version Source
----------- ----                    ------- ------
Cmdlet      Resolve-MyCmdlet        1.0.0.0 MyModule

Si se produce un error en la importación en el sistema, intente actualizar .NET a 4.7.1 o posterior. La guía de Creación de un módulo binario multiplataforma ofrece más detalles sobre el soporte y la compatibilidad de .NET con versiones anteriores de .NET.

Manifiesto del módulo

Es interesante que podamos importar el archivo DLL y tener un módulo de trabajo. Me gusta continuar trabajando con él y crear un manifiesto de módulo. Necesitamos el manifiesto si queremos publicarlo en PSGallery más adelante.

Desde la raíz del proyecto, podemos ejecutar este comando para crear el manifiesto del módulo que necesitamos.

$manifestSplat = @{
    Path              = ".\$module\$module.psd1"
    Author            = 'Kevin Marquette'
    NestedModules     = @('bin\MyModule.dll')
    RootModule        = "$module.psm1"
    FunctionsToExport = @('Resolve-MyCmdlet')
}
New-ModuleManifest @manifestSplat

También voy a crear un módulo raíz vacío para futuras funciones de PowerShell.

Set-Content -Value '' -Path ".\$module\$module.psm1"

Esto me permite mezclar funciones normales de PowerShell y cmdlets binarios en el mismo proyecto.

Compilación del módulo completo

Reúno todo junto en una carpeta de salida. Es necesario crear un script de compilación para hacerlo. Normalmente agregaría esto a un script de Invoke-Build, pero podemos simplificarlo para este ejemplo. Agréguelo a un archivo build.ps1 en la raíz del proyecto.

$module = 'MyModule'
Push-Location $PSScriptRoot

dotnet build $PSScriptRoot\src -o $PSScriptRoot\output\$module\bin
Copy-Item "$PSScriptRoot\$module\*" "$PSScriptRoot\output\$module" -Recurse -Force

Import-Module "$PSScriptRoot\Output\$module\$module.psd1"
Invoke-Pester "$PSScriptRoot\Tests"

Estos comandos compilan nuestro archivo DLL y lo colocan en nuestra carpeta output\$module\bin. A continuación, copia los otros archivos del módulo en su lugar.

Output
└───MyModule
    ├───MyModule.psd1
    ├───MyModule.psm1
    └───bin
        ├───MyModule.deps.json
        ├───MyModule.dll
        └───MyModule.pdb

En este momento, podemos importar nuestro módulo con el archivo psd1.

Import-Module ".\Output\$module\$module.psd1"

Desde aquí, podemos colocar la carpeta .\Output\$module en nuestro directorio $Env:PSModulePath y cargará automáticamente nuestro comando siempre que lo necesitemos.

Actualización: nuevo PSModule de dotnet

He aprendido que la herramienta dotnet tiene una plantilla de PSModule.

Todos los pasos descritos anteriormente siguen siendo válidos, pero esta plantilla omite muchos de ellos. Sigue siendo una plantilla bastante nueva que está en proceso de refinamiento. Espere que siga mejorando desde aquí.

Así es como se usa la instalación y el uso de la plantilla PSModule.

dotnet new -i Microsoft.PowerShell.Standard.Module.Template
dotnet new psmodule
dotnet build
Import-Module "bin\Debug\netstandard2.0\$module.dll"
Get-Module $module

Esta plantilla mínimamente viable se encarga de agregar el SDK de .NET, la biblioteca estándar de PowerShell y crea una clase de ejemplo en el proyecto. Puede compilarlo y ejecutarlo inmediatamente.

Detalles importantes

Antes de finalizar este artículo, estos son algunos otros detalles que merece la pena mencionar.

Descargar archivos DLL

Una vez que se carga un módulo binario, no puede descargarlo realmente. El archivo DLL se bloquea hasta que se descarga. Esto puede ser molesto al desarrollar porque cada vez que realiza un cambio y desea compilarlo, el archivo a menudo está bloqueado. La única manera confiable de resolver esto es cerrar la sesión de PowerShell que cargó el archivo DLL.

Acción de recarga de ventana de VS Code

La mayoría de mis trabajos de desarrollo de PowerShell en VS Code. Cuando estoy trabajando en un módulo binario (o un módulo con clases), me he metido en el hábito de volver a cargar VS Code cada vez que se compila. Ctrl+Mayús+P extrae la ventana de comandos y Reload Window siempre se encuentra al principio de mi lista.

Sesiones de PowerShell anidadas

Otra opción es tener una buena cobertura de pruebas de Pester. A continuación, puede ajustar el script de build.ps1 para iniciar una nueva sesión de PowerShell, realizar la compilación, ejecutar las pruebas y cerrar la sesión.

Actualización de módulos instalados

Este bloqueo puede ser molesto al intentar actualizar el módulo instalado localmente. Si alguna sesión lo ha cargado, debe localizarlo y cerrarlo. Esto es menos problemático al instalar desde una PSGallery, porque el versionado de módulos coloca la nueva versión en una carpeta diferente.

Puede configurar una PSGallery local y publicar en ella como parte del proceso de compilación. A continuación, realice la instalación local desde ese PSGallery. Esto parece mucho trabajo, pero esto puede ser tan sencillo como iniciar un contenedor de Docker. Explico una forma de hacerlo en mi publicación en el artículo sobre cómo Uso de un servidor NuGet para un repositorio PSRepository.

Pensamientos finales

No toqué la sintaxis de C# para crear un cmdlet, pero hay mucha documentación sobre él en la SDK de Windows PowerShell. Definitivamente es algo que vale la pena experimentar, como un trampolín hacia una programación más seria en C#.