How to create a Standard Library binary module (Como criar um módulo binário da Standard Library)

Recentemente tive uma ideia para o módulo que queria implementar como um módulo binário. Eu ainda tenho que criar um usando a Biblioteca Padrão do PowerShell, então isso parecia uma boa oportunidade. Eu usei o guia Criando um módulo binário multiplataforma para criar este módulo sem quaisquer obstáculos. Vamos percorrer esse mesmo processo e vou acrescentar um pouco mais de comentário ao longo do caminho.

Nota

A versão original deste artigo apareceu no blog escrito por @KevinMarquette. A equipe do PowerShell agradece Kevin por compartilhar esse conteúdo conosco. Por favor, confira seu blog em PowerShellExplained.com.

O que é a Biblioteca Padrão do PowerShell?

A Biblioteca Padrão do PowerShell nos permite criar módulos entre plataformas que funcionam no PowerShell e no Windows PowerShell 5.1.

Porquê módulos binários?

Ao escrever um módulo em C#, você abre mão do acesso fácil aos cmdlets e funções do PowerShell. Mas se você estiver criando um módulo que não dependa de muitos outros comandos do PowerShell, o benefício de desempenho pode ser significativo. O PowerShell foi otimizado para o administrador, não para o computador. Ao mudar para C#, você elimina a sobrecarga adicionada pelo PowerShell.

Por exemplo, temos um processo crítico que faz muito trabalho com JSON e hashtables. Otimizamos o PowerShell o máximo que pudemos, mas o processo ainda leva 12 minutos para ser concluído. O módulo já continha muito PowerShell no estilo C#. Isso torna a conversão para um módulo binário limpa e simples. Ao converter para um módulo binário, reduzimos o tempo de processo de mais de 12 minutos para menos de quatro minutos.

Módulos híbridos

Você pode misturar cmdlets binários com funções avançadas do PowerShell. Tudo o que você sabe sobre módulos de script se aplica da mesma maneira. O arquivo vazio psm1 é incluído para que você possa adicionar outras funções do PowerShell mais tarde.

Quase todos os cmdlets compilados que criei começaram como funções do PowerShell primeiro. Todos os nossos módulos binários são realmente módulos híbridos.

Criar scripts

Eu mantive o script de construção simples aqui. Eu geralmente uso um script grande Invoke-Build como parte do meu pipeline de CI/CD. Ele faz mais mágica como executar testes Pester, executar PSScriptAnalyzer, gerenciar versionamento e publicar no PSGallery. Quando comecei a usar um script de construção para meus módulos, consegui encontrar muitas coisas para adicionar a ele.

Planear o módulo

O plano para este módulo é criar uma src pasta para o código C# e estruturar o resto como eu faria para um módulo de script. Isso inclui o uso de um script de construção para compilar tudo em uma Output pasta. A estrutura de pastas tem esta aparência:

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

Introdução

Primeiro eu preciso criar a pasta e criar o repositório git. Estou usando $module como um espaço reservado para o nome do módulo. Isso facilitará a reutilização desses exemplos, se necessário.

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

Em seguida, crie as pastas de nível raiz.

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

Configuração do módulo binário

Este artigo é focado no módulo binário, então é por aí que vamos começar. Esta seção extrai exemplos do guia Criando um módulo binário entre plataformas. Reveja esse guia se precisar de mais detalhes ou tiver algum problema.

A primeira coisa que queremos fazer é verificar a versão do dotnet core SDK que instalamos. Estou usando 2.1.4, mas você deve ter 2.0.0 ou mais recente antes de continuar.

PS> dotnet --version
2.1.4

Estou trabalhando fora da src pasta para esta seção.

Set-Location 'src'

Usando o comando dotnet, crie uma nova biblioteca de classes.

dotnet new classlib --name $module

Isso criou o projeto de biblioteca em uma subpasta, mas eu não quero esse nível extra de aninhamento. Vou mover esses arquivos até um nível.

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

Defina a versão do SDK principal do .NET para o projeto. Eu tenho o SDK 2.1, então vou especificar 2.1.0. Use 2.0.0 se você estiver usando o SDK 2.0.

dotnet new globaljson --sdk-version 2.1.0

Adicione o pacote NuGet da BibliotecaPadrão do PowerShell ao projeto. Certifique-se de usar a versão mais recente disponível para o nível de compatibilidade que você precisa. Eu usaria como padrão a versão mais recente, mas não acho que esse módulo aproveite nenhum recurso mais recente do que o PowerShell 3.0.

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

Devemos ter uma pasta src que se pareça com isto:

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

Agora estamos prontos para adicionar nosso próprio código ao projeto.

Criando um cmdlet binário

Precisamos atualizar o src\Class1.cs para conter este cmdlet inicial:

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();
        }
    }
}

Renomeie o arquivo para corresponder ao nome da classe.

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

Então podemos construir o nosso 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 chamar Import-Module a nova dll para carregar nosso novo 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

Se a importação falhar no seu sistema, tente atualizar o .NET para 4.7.1 ou mais recente. O guia Criando um módulo binário de plataforma cruzada entra em mais detalhes sobre o suporte e a compatibilidade do .NET para versões mais antigas do .NET.

Manifesto do módulo

É legal que podemos importar a dll e ter um módulo de trabalho. Eu gosto de continuar com ele e criar um manifesto de módulo. Precisamos do manifesto se quisermos publicar para o PSGallery mais tarde.

A partir da raiz do nosso projeto, podemos executar este comando para criar o manifesto do módulo de que precisamos.

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

Também vou criar um módulo raiz vazio para futuras funções do PowerShell.

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

Isso me permite misturar funções normais do PowerShell e cmdlets binários no mesmo projeto.

Construindo o módulo completo

Eu compilo tudo junto em uma pasta de saída. Precisamos criar um script de construção para fazer isso. Eu normalmente adicionaria isso a um Invoke-Build script, mas podemos mantê-lo simples para este exemplo. Adicione isso a um build.ps1 na raiz do projeto.

$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"

Esses comandos criam nossa DLL e a colocam em nossa output\$module\bin pasta. Em seguida, copia os outros arquivos do módulo no lugar.

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

Neste ponto, podemos importar nosso módulo com o arquivo psd1.

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

A partir daqui, podemos soltar a .\Output\$module pasta em nosso $env:PSModulePath diretório e ele carrega automaticamente nosso comando sempre que precisarmos.

Atualização: dotnet novo PSModule

Aprendi que a dotnet ferramenta tem um PSModule modelo.

Todas as etapas que descrevi acima ainda são válidas, mas este modelo corta muitas delas. Ainda é um modelo bastante novo que ainda está recebendo algum polimento colocado nele. Espere que continue a melhorar a partir daqui.

É assim que você usa, instala e usa o modelo PSModole.

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

Esse modelo minimamente viável cuida de adicionar o SDK do .NET, a Biblioteca Padrão do PowerShell e cria uma classe de exemplo no projeto. Você pode construí-lo e executá-lo imediatamente.

Detalhes importantes

Antes de terminarmos este artigo, aqui estão alguns outros detalhes que merecem ser mencionados.

Descarregando DLLs

Uma vez que um módulo binário é carregado, você não pode realmente descarregá-lo. O arquivo DLL é bloqueado até que você o descarregar. Isso pode ser irritante ao desenvolver, porque toda vez que você faz uma alteração e deseja criá-la, o arquivo é frequentemente bloqueado. A única maneira confiável de resolver isso é fechar a sessão do PowerShell que carregou a DLL.

Ação da janela de recarga do VS Code

Faço a maior parte do meu trabalho de desenvolvimento do PowerShell no VS Code. Quando estou trabalhando em um módulo binário (ou um módulo com classes), adquiri o hábito de recarregar o VS Code toda vez que construo. +Ctrl Shift+P aparece na janela de comando e Reload Window está sempre no topo da minha lista.

Sessões aninhadas do PowerShell

Uma outra opção é ter uma boa cobertura do teste Pester. Em seguida, você pode ajustar o script build.ps1 para iniciar uma nova sessão do PowerShell, executar a compilação, executar os testes e fechar a sessão.

Atualização dos módulos instalados

Este bloqueio pode ser irritante ao tentar atualizar o módulo instalado localmente. Se alguma sessão tiver carregado, você tem que ir caçá-lo e fechá-lo. Isso é menos um problema ao instalar a partir de um PSGallery porque o controle de versão do módulo coloca o novo em uma pasta diferente.

Você pode configurar um PSGallery local e publicá-lo como parte de sua compilação. Em seguida, faça a sua instalação local a partir desse PSGallery. Parece muito trabalhoso, mas pode ser tão simples quanto iniciar um contêiner docker. Eu abordo uma maneira de fazer isso no meu post sobre Usando um servidor NuGet para um PSRepository.

Considerações finais

Não toquei na sintaxe C# para criar um cmdlet, mas há muita documentação sobre ela no SDK do Windows PowerShell. É definitivamente algo que vale a pena experimentar como um trampolim para C# mais sério.