Используя создать событие, чтобы запустить скрипт PowerShell для запуска DLL построен


Как правило, всякий раз, когда я нахожу себя делать то, что больше никто не делает, я должен быть очень подозрительным то, что я делаю. Вот почему я хочу сделать проверку кода, чтобы убедиться, что я не делаю что-то безумное с событиями построить за то, что она обычно не предназначена.

Способствует Rubberduck проекта VBA, у меня есть пиар, который создает новый визуальный проект студии, по имени Rubberduck.Deployment. Основная цель проекта заключается в оказании помощи во время сборки извлечения данных, которые программа установки будет нужно выполнять свое дело. Поэтому она будет создать файлы во время построения которых может меняться от сборки к сборке, поскольку кодовая база обновляется. Эти сгенерированные файлы используются по Инно сетап. Мои ограничения, чтобы иметь возможность поддерживать оба здания локально и удаленно через AppVeyor.

Ключ код для обзора эту запись в пост сборки.

<PreBuildEvent>
  %SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe 
    -ExecutionPolicy Bypass 
    -command "& '$(ProjectDir)PreInnoSetupConfiguration.ps1' 
      -WorkingDir '$(ProjectDir)'"
</PreBuildEvent>
<PostBuildEvent>
  %SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe 
    -ExecutionPolicy Bypass 
    -command "& '$(ProjectDir)BuildRegistryScript.ps1' 
      -config '$(ConfigurationName)' 
      -builderAssemblyPath '$(TargetPath)' 
      -netToolsDir '$(FrameworkSDKDir)bin\NETFX 4.6.1 Tools\' 
      -wixToolsDir '$(ProjectDir)WixToolset\' 
      -sourceDir '$(TargetDir)' 
      -targetDir '$(TargetDir)' 
      -projectDir '$(ProjectDir)' 
      -includeDir '$(ProjectDir)InnoSetup\Includes\' 
      -filesToExtract 'Rubberduck.dll|Rubberduck.API.dll'"
</PostBuildEvent>

Как указано, Мы вызываем скрипты PowerShell, который включен в проект Visual Studio (и быть скрипт, он не будет напрямую участвовать в создании проекта в Visual студии). Вот полный код после построения сценария.

# The parameters should be supplied by the Build event of the project
# in order to take macros from Visual Studio to avoid hard-coding
# the paths. To simplify the process, the project should have a 
# reference to the projects that needs to be registered, so that 
# their DLL files will be present in the $(TargetDir) macro. 
#
# Possible syntax for Post Build event of the project to invoke this:
# C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe 
#  -command "$(ProjectDir)BuildRegistryScript.ps1 
#  -config '$(ConfigurationName)' 
#  -builderAssemblyPath '$(TargetPath)' 
#  -netToolsDir '$(FrameworkSDKDir)bin\NETFX 4.6.1 Tools\' 
#  -wixToolsDir '$(SolutionDir)packages\WiX.Toolset.3.9.1208.0\tools\wix\' 
#  -sourceDir '$(TargetDir)' 
#  -targetDir '$(TargetDir)' 
#  -projectDir '$(ProjectDir)'
#  -includeDir '$(ProjectDir)InnoSetup\Includes\'
#  -filesToExtract 'Rubberduck.dll'"
param (
    [Parameter(Mandatory=$true)][string]$config,
    [Parameter(Mandatory=$true)][string]$builderAssemblyPath,
    [Parameter(Mandatory=$true)][string]$netToolsDir,
    [Parameter(Mandatory=$true)][string]$wixToolsDir,
    [Parameter(Mandatory=$true)][string]$sourceDir,
    [Parameter(Mandatory=$true)][string]$targetDir,
    [Parameter(Mandatory=$true)][string]$projectDir,
    [Parameter(Mandatory=$true)][string]$includeDir,
    [Parameter(Mandatory=$true)][string]$filesToExtract
)

function Get-ScriptDirectory
{
  $Invocation = (Get-Variable MyInvocation -Scope 1).Value;
  Split-Path $Invocation.MyCommand.Path;
}

# Invokes a Cmd.exe shell script and updates the environment.
function Invoke-CmdScript {
  param(
    [String] $scriptName
  )
  $cmdLine = """$scriptName"" $args & set"
  & $Env:SystemRoot\system32\cmd.exe /c $cmdLine |
  select-string '^([^=]*)=(.*)$' | foreach-object {
    $varName = $_.Matches[0].Groups[1].Value
    $varValue = $_.Matches[0].Groups[2].Value
    set-item Env:$varName $varValue
  }
}

# Returns the current environment.
function Get-Environment {
  get-childitem Env:
}

# Restores the environment to a previous state.
function Restore-Environment {
  param(
    [parameter(Mandatory=$TRUE)]
      [System.Collections.DictionaryEntry[]] $oldEnv
  )
  # Remove any added variables.
  compare-object $oldEnv $(Get-Environment) -property Key -passthru |
  where-object { $_.SideIndicator -eq "=>" } |
  foreach-object { remove-item Env:$($_.Name) }
  # Revert any changed variables to original values.
  compare-object $oldEnv $(Get-Environment) -property Value -passthru |
  where-object { $_.SideIndicator -eq "<=" } |
  foreach-object { set-item Env:$($_.Name) $_.Value }
}

# Remove older imported registry scripts for debug builds.
function Clean-OldImports
{
    param(
        [String] $dir
    )
    $i = 0;
    Get-ChildItem $dir -Filter DebugRegistryEntries.reg.imported_*.txt | 
    Sort-Object Name -Descending |
    Foreach-Object {
        if($i -ge 10) {
            $_.Delete();
        }
        $i++;
    }
}

Set-StrictMode -Version latest;
$ErrorActionPreference = "Stop";
$DebugUnregisterRun = $false;

try
{
    # Clean imports older than 10 builds
    Clean-OldImports ((Get-ScriptDirectory) + "\LocalRegistryEntries");;

    # Allow multiple DLL files to be registered if necessary
    $separator = "|";
    $option = [System.StringSplitOptions]::RemoveEmptyEntries;
    $files = $filesToExtract.Split($separator, $option);

    # Load the Deployment DLL
    [System.Reflection.Assembly]::LoadFrom($builderAssemblyPath);

    # Determine if MIDL is available for building
    $devPath = $Env:ProgramFiles + "*\Microsoft Visual Studio\*\*\Common*\Tools\VsDevCmd.bat";
    $devPath = Resolve-Path -Path $devPath;
    if($devPath)
    {
        # Additional verifications as some versions of VsDevCmd.bat might not initialize the environment for C++ build tools
        $result = Get-Module -ListAvailable -Name "VSSetup" -ErrorAction SilentlyContinue;
        if(!$result)
        {
            Write-Warning "VSSetup not installed; extracting...";
            Expand-Archive "$projectDir\OleWoo\VSSetup.zip" "$([Environment]::GetFolderPath("MyDocuments"))\WindowsPowerShell\Modules\VSSetup" -Force
        }

        try {
            Import-Module VSSetup -Force:$true;
            $result = Get-VSSetupInstance | Select-VSSetupInstance -Latest -Require Microsoft.VisualStudio.Component.VC.Tools.x86.x64;
        } catch {
            $result = $null;
            Write-Warning "Error occurred with using VSSetup module";
            Write-Error ($_);
        }

        if(!$result)
        {
            $devPath = $null;
            Write-Warning "Cannot locate the VS Setup instance capable of building with C++ build tools";
        }
    }

    if(!$devPath)
    {
        Write-Warning "Cannot locate the VsDevCmd.bat to initialize C++ build tools; falling back to tlbexp.exe....";
    }

    Write-Host "";

    foreach($file in $files)
    {
        Write-Host "Processing '$file'";
        Write-Host "";

        $dllFile = [System.String]$file;
        $idlFile = [System.String]($file -replace ".dll", ".idl");
        $tlb32File = [System.String]($file -replace ".dll", ".x32.tlb");
        $tlb64File = [System.String]($file -replace ".dll", ".x64.tlb");

        $sourceDll = $sourceDir + $file;
        $targetDll = $targetDir + $file;
        $sourceTlb32 = $sourceDir + $tlb32File;
        $targetTlb32 = $targetDir + $tlb32File;
        $sourceTlb64 = $sourceDir + $tlb64File;
        $targetTlb64 = $targetDir + $tlb64File;
        $dllXml = $targetDll + ".xml";
        $tlbXml = $targetTlb32 + ".xml";

        # Write-Host "Variable printout:"
        # Write-Host "dllFile = $dllFile";
        # Write-Host "idlFile = $idlFile";
        # Write-Host "tlb32File = $tlb32File";
        # Write-Host "tlb64File = $tlb64File";
        # Write-Host "sourceDll = $sourceDll";
        # Write-Host "targetDll = $targetDll";
        # Write-Host "sourceTlb32 = $sourceTlb32";
        # Write-Host "targetTlb32 = $targetTlb32";
        # Write-Host "sourceTlb64 = $sourceTlb64";
        # Write-Host "targetTlb64 = $targetTlb64";
        # Write-Host "dllXml = $dllXml";
        # Write-Host "tlbXml = $tlbXml";
        # Write-Host "targetDir = $targetDir";
        # Write-Host "";

        # Use for debugging issues with passing parameters to the external programs
        # Note that it is not legal to have syntax like `& $cmdIncludingArguments` or `& $cmd $args`
        # For simplicity, the arguments are pass in literally.
        # & "C:\GitHub\Rubberduck\Rubberduck\Rubberduck.Deployment\echoargs.exe" ""$sourceDll"" /win32 /out:""$sourceTlb"";

        # Compile TLB files using MIDL
        if($devPath)
        {
            $idlGenerator = New-Object Rubberduck.Deployment.IdlGeneration.IdlGenerator;

            $idl = $idlGenerator.GenerateIdl($sourceDll);
            $encoding = New-Object System.Text.UTF8Encoding $true;
            [System.IO.File]::WriteAllLines($idlFile, $idl, $encoding);

            $origEnv = Get-Environment;
            try {
                Invoke-CmdScript "$devPath";

                if($targetDir.EndsWith("\"))
                {
                    $targetDirWithoutSlash = $targetDir.Substring(0,$targetDir.Length-1);
                }
                else
                {
                    $targetDirWithoutSlash = $targetDir;
                }

                & midl.exe /win32 /tlb ""$tlb32File"" ""$idlFile"" /out ""$targetDirWithoutSlash"";
                & midl.exe /amd64 /tlb ""$tlb64File"" ""$idlFile"" /out ""$targetDirWithoutSlash"";
            } catch {
                throw;
            } finally {
                Restore-Environment $origEnv;
            }
        }

        # Compile TLB files using tlbexp.exe
        if(!$devPath)
        {
            $cmd = "{0}tlbexp.exe" -f $netToolsDir;
            & $cmd ""$sourceDll"" /win32 /out:""$sourceTlb32"";
            & $cmd ""$sourceDll"" /win64 /out:""$sourceTlb64"";
        }

        # Harvest both DLL and TLB files using WiX's heat.exe, generating XML files
        $cmd = "{0}heat.exe" -f $wixToolsDir;
        & $cmd file ""$sourceDll"" -out ""$dllXml"";
        & $cmd file ""$sourceTlb32"" -out ""$tlbXml"";

        # Initialize the registry builder with the provided XML files
        $builder = New-Object Rubberduck.Deployment.Builders.RegistryEntryBuilder;
        $entries = $builder.Parse($tlbXml, $dllXml);

        # For debugging
        # $entries | Format-Table | Out-String |% {Write-Host $_};

        $writer = New-Object Rubberduck.Deployment.Writers.InnoSetupRegistryWriter;
        $content = $writer.Write($entries, $dllFile, $tlb32File, $tlb64File);

        # The file must be encoded in UTF-8 BOM
        $regFile = ($includeDir + ($file -replace ".dll", ".reg.iss"));
        $encoding = New-Object System.Text.UTF8Encoding $true;
        [System.IO.File]::WriteAllLines($regFile, $content, $encoding);
        $content = $null;

        # Register the debug build on the local machine
        if($config -eq "Debug")
        {
            if(!$DebugUnregisterRun) 
            {
                # First see if there are registry script from the previous build
                # If so, execute them to delete previous build's keys (which may
                # no longer exist for the current build and thus won't be overwritten)
                $dir = ((Get-ScriptDirectory) + "\LocalRegistryEntries");
                $regFileDebug = $dir + "\DebugRegistryEntries.reg";

                if (Test-Path -Path $dir -PathType Container)
                {
                    if (Test-Path -Path $regFileDebug -PathType Leaf)
                    {
                        $datetime = Get-Date;
                        if ([Environment]::Is64BitOperatingSystem)
                        {
                            & reg.exe import $regFileDebug /reg:32;
                            & reg.exe import $regFileDebug /reg:64;
                        }
                        else 
                        {
                            & reg.exe import $regFileDebug;
                        }
                        & reg.exe import ($dir + "\RubberduckAddinRegistry.reg");
                        Move-Item -Path $regFileDebug -Destination ($regFileDebug + ".imported_" + $datetime.ToUniversalTime().ToString("yyyyMMddHHmmss") + ".txt" );
                    }
                }
                else
                {
                    New-Item $dir -ItemType Directory;
                }

                $DebugUnregisterRun = $true;
            }

            # NOTE: The local writer will perform the actual registry changes; the return
            # is a registry script with deletion instructions for the keys to be deleted
            # in the next build.
            $writer = New-Object Rubberduck.Deployment.Writers.LocalDebugRegistryWriter;
            $content = $writer.Write($entries, $dllFile, $tlb32File, $tlb64File);

            $encoding = New-Object System.Text.ASCIIEncoding;
            [System.IO.File]::AppendAllText($regFileDebug, $content, $encoding);
        }

        Write-Host "Finished processing '$file'";
        Write-Host "";
    }

    Write-Host "Finished processing all files";
}
catch
{
    Write-Error ($_);
    # Cause the build to fail
    throw;
}

В начале скрипта, мы делаем это:

[System.Reflection.Assembly]::LoadFrom($builderAssemblyPath);

А значит, мы загрузить DLL, который был просто построен по проекту (это буквально выход из Rubberduck.Deployment проекта), который сценарий PowerShell продолжает вызывать методы, а затем записываем в файл:

$builder = New-Object Rubberduck.Deployment.Builders.RegistryEntryBuilder;
$entries = $builder.Parse($tlbXml, $dllXml);
....
$writer = New-Object Rubberduck.Deployment.Writers.InnoSetupRegistryWriter;
$content = $writer.Write($entries, $dllFile, $tlb32File, $tlb64File);

$regFile = ($includeDir + ($file -replace ".dll", ".reg.iss"));
$encoding = New-Object System.Text.UTF8Encoding $true;
[System.IO.File]::WriteAllLines($regFile, $content, $encoding);

Файл автоматически используется в качестве вклада в Инно установки компилятора, который используется во время AppVeyor сборки до полной сборки установки для встраивания Rubberduck. Но для локальной отладки строить там, где мы не используем установщик, мы бежим в этом разделе:

# Register the debug build on the local machine
if($config -eq "Debug")
{
    if(!$DebugUnregisterRun) 
    {
        # First see if there are registry script from the previous build
        # If so, execute them to delete previous build's keys (which may
        # no longer exist for the current build and thus won't be overwritten)
        $dir = ((Get-ScriptDirectory) + "\LocalRegistryEntries");
        $regFileDebug = $dir + "\DebugRegistryEntries.reg";
        if (Test-Path -Path $dir -PathType Container)
        {
            if (Test-Path -Path $regFileDebug -PathType Leaf)
            {
                $datetime = Get-Date;
                if ([Environment]::Is64BitOperatingSystem)
                {
                    & reg.exe import $regFileDebug /reg:32;
                    & reg.exe import $regFileDebug /reg:64;
                }
                else 
                {
                    & reg.exe import $regFileDebug;
                }
                & reg.exe import ($dir + "\RubberduckAddinRegistry.reg");
                Move-Item -Path $regFileDebug -Destination ($regFileDebug + ".imported_" + $datetime.ToUniversalTime().ToString("yyyyMMddHHmmss") + ".txt" );
            }
        }
        else
        {
            New-Item $dir -ItemType Directory;
        }
        $DebugUnregisterRun = $true;
    }

    # NOTE: The local writer will perform the actual registry changes; the return
    # is a registry script with deletion instructions for the keys to be deleted
    # in the next build.
    $writer = New-Object Rubberduck.Deployment.Writers.LocalDebugRegistryWriter;
    $content = $writer.Write($entries, $dllFile, $tlb32File, $tlb64File);

    $encoding = New-Object System.Text.ASCIIEncoding;
    [System.IO.File]::AppendAllText($regFileDebug, $content, $encoding);
}

Писатель на самом деле создать реестре в реестре застройщика в HKCU регистрировать отладочную сборку для COM между прочим. Как он это делает, он также генерирует скрипт реестр, который затем сохраняется на диск, что позволяет удалений все созданные ключи. Этот файл затем используется в последующих построить, чтобы удалить старые ключи от предыдущего построения, гарантируя, что разработчик не в конечном итоге с 1000 устаревшие ключи, как они делают изменения объектов, которые могут или не могут сменить прописку.

Все работает все красиво, но как я уже сказал в начале, это довольно необычное применение пост создать событие и я интересно, если это код запах в себя. Я также интересно узнать, сможем ли мы сделать это лучше, чтобы сделать процесс более плавный и менее руб-goldsberg-ЭСК.

Обратите внимание, что в то время как я мог бы просто ссылаться на скрипт PowerShell и ДЛЛ внутри AppVeyor, это не приведет к достижению цели, чтобы быть способным создавать и генерировать файлы при построении локально, которые не могут быть затем использованы для модульных тестов, отладки, или просто проверять. Что еще более важно, я не хочу, чтобы один процесс для построения локально и других процессов для построения на AppVeyor, как это имеет потенциал, чтобы создать ошибки, которые мы не можем видеть из-за пропущенных различия в построении.

Вопросы для ответа:

  1. Это разумный способ, чтобы настроить процесс построения? Существуют ли более эффективные способы?

  2. Мне не нравится, что макрос $(FrameworkSDKDir) в основном незарегистрированным в проекте C#. AFAICT, он используется только на проекте C++. Я нарваться на неприятности для использования этого макроса? В тестах, это, кажется, работает, но...? Что насчет новой версии? Все это только для того, чтобы иметь возможность работать tlbexp.exe AFAICT, которые не в PATH переменной.

  3. В настоящее время у меня нет точной стратегии обработки ошибок; любая ошибка приведет к построению в целом на провал. Это может быть приемлемо, так как нет выхода значит строить нельзя в любом случае, но это может нарушить принцип наименьшего удивления. Я тоже раздражало, что в настоящее время все ошибки будут просто выдавать родовой the command exited with exit code -1 в Error List не давая никаких реальных сведений об ошибках. В настоящее время все участники должны перейти на Output для Build поток, чтобы увидеть фактические ошибки из скрипта PowerShell. Мы можем улучшить это?

  4. Мы можем улучшить окружающую среду проверить? Потому что мы используем компилятором MIDL, который является технически инструмент построения C++ и, следовательно, не является частью нормального на C# процесс построения, мы должны настроить среду. Однако, я нашел его довольно трудно надежно определить, является ли установлена среда Visual Studio поддерживает C++ для создания инструментов или нет. Я изначально проверил на наличие VsDevCmd.bat но это было ложных срабатываний, который вызывает процесс сборки, предназначенный для компиляции через мидл и затем потерпеть неудачу.

Обратите внимание на #4: с оригинальной путь, автор нашел дыру в логике и сценарию был так затянут на его чек, с раздела, посвященного анализу присутствия на C++ построить инструменты, начиная на линии 106



743
10
задан 22 марта 2018 в 06:03 Источник Поделиться
Комментарии
1 ответ

Через несколько месяцев использования, я склонен думать, что, хотя этот метод действительно работает и, вероятно, будет лучшей инвестицией на более распространенный сценарий, где вы хотите настроить автоматический процесс сборки на специальном сервере, она мало подходила для нашей конкретной ситуации, что делает его легко построить в любом месте на любой машине с минимальными изменениями. Вот несколько причин:

1) в Visual Studio и PowerShell фактически не интегрированы.

На первый взгляд кажется, что существует высокий уровень интеграции -- Package Manager Console на самом деле PowerShell в консоли, верно? Однако, на практике, мы не имеем никакого контроля над версии в PowerShell. Некоторые могут застрять из-за корпоративной политики или по другим причинам с помощью PowerShell 2.0, и, следовательно, не может использовать функцию управления пакетами, которая существует в PowerShell 3.0/4.0/5.0.

То есть политику выполнения. Хотя это можно обойти, это является еще одним препятствием.

2) плохое отчетов об ошибках

Потому что PowerShell-это показать, если произошла ошибка, все визуальные студии заканчивается, показывая это "завершилась с кодом -1" в Error List без деталей. Одним придется рыскать через отладочный вывод, чтобы найти фактические ошибки, вызвавшей сценарий на неудачу. Это не делает его легким для диагностики и устранения ошибок с мнением пользователя.

3) дабл-дабл цитировать

Потому что команду на выполнение скрипта идет через cmd.exeэто требует вызова консоли PowerShell, затем передав в фактическое командование. Это делает для более сложной настройки, как вы должны цитатой/выход для обоих cmd.exe и PowerShell.

4) зачем смешивать код?

У нас есть скрипт вызова некоторые C# код в сборке, вот только построили. Но если мы пишем на C#... почему бы не делать это в C#? На самом деле создание пользовательской задачи построить не так сложно сделать. Она сводится к 3 общие шаги:

1) создать класс c#, производный от MSBuild по ITask интерфейс

2) Добавить UsingTask XML-элемент в csproj файл, который указывает на сборку, содержащую класс.

3) Добавить задачу (возможно, в течение Target например)

Кроме того, это дает мощную печати даже в формате csproj как Visual Studio является возможность видеть свойства и обеспечивает поддержку IntelliSense для создания пользовательского узла задачи.

5) соблазн написать быстрый код'dirty Н

Это больше отражает программист, чем язык. Можно писать чистый код в Буе PowerShell, в связи с характером PowerShell, то это тоже слишком просто написать одну функцию Бога с глобального государства и т. д. и т. д. Таким образом, требуется больше дисциплины, чтобы писать чистый код в PowerShell. Это же противоречит предпосылке, что вы должны написать в PowerShell, так как это быстро делать то, что вам нужно с минимальными усилиями. Мы столкнулись и пришлось исправлять ошибок, возникающих в результате глобального загрязнения пространства имен, использованных переменных.

Короче говоря - скрипты PowerShell для поддержки настраиваемого построения действия может работать, но требует слишком много работы и специального кожуха в разных средах. Установка пользовательских задач построения гарантирует, что он будет работать в той же версии Visual Studio, что само решение необходимо, так что нет никаких дополнительных зависимостей, которые должны быть удовлетворены для того, чтобы задача построения для выполнения.

2
ответ дан 6 декабря 2018 в 08:12 Источник Поделиться