Using build event to run powershell script to run built DLL… abuse of build system?

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;







up vote
6
down vote

favorite












As a rule, whenever I find myself doing something that nobody else is doing, I have to be quite suspicious about what I'm doing. This is why I want to get code review to verify I'm not doing something insane with the build events for what it's not usually intended.



Contributing to Rubberduck VBA project, I have a PR that creates a new Visual Studio project, named Rubberduck.Deployment. The main purpose of the project is to assist at the build time the extraction of the data that the installer will need to perform its thing. Therefore it will generate files at build time which can vary from build to build as the codebase is updated. Those generated files are then consumed by Inno Setup. My constraint is to be able to support both building locally and remotely via the AppVeyor.



The key code to review is this entry in the Post Build:



(reformatted for readability)



<PreBuildEvent>
C:WindowsSystem32WindowsPowerShellv1.0powershell.exe
-ExecutionPolicy Bypass
-command "$(ProjectDir)PreInnoSetupConfiguration.ps1
-WorkingDir $(ProjectDir)"
</PreBuildEvent>
<PostBuildEvent>
C:WindowsSystem32WindowsPowerShellv1.0powershell.exe
-ExecutionPolicy Bypass
-command "$(ProjectDir)BuildRegistryScript.ps1
-config '$(ConfigurationName)'
-builderAssemblyPath '$(TargetPath)'
-netToolsDir '$(FrameworkSDKDir)binNETFX 4.6.1 Tools'
-wixToolsDir '$(ProjectDir)WixToolset'
-sourceDir '$(TargetDir)'
-targetDir '$(TargetDir)'
-projectDir '$(ProjectDir)'
-includeDir '$(ProjectDir)InnoSetupIncludes'
-filesToExtract 'Rubberduck.dll|Rubberduck.API.dll'"
</PostBuildEvent>


As indicated, we invoke PowerShell scripts which is included in the Visual Studio project (and being a script, it doesn't directly participate in the building of the Visual Studio project). Here's complete code for the post build script....



# 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:WindowsSystem32WindowsPowerShellv1.0powershell.exe
# -command "$(ProjectDir)BuildRegistryScript.ps1
# -config '$(ConfigurationName)'
# -builderAssemblyPath '$(TargetPath)'
# -netToolsDir '$(FrameworkSDKDir)binNETFX 4.6.1 Tools'
# -wixToolsDir '$(SolutionDir)packagesWiX.Toolset.3.9.1208.0toolswix'
# -sourceDir '$(TargetDir)'
# -targetDir '$(TargetDir)'
# -projectDir '$(ProjectDir)'
# -includeDir '$(ProjectDir)InnoSetupIncludes'
# -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
select-string '^([^=]*)=(.*)$'

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


# Restores the environment to a previous state.
function Restore-Environment
foreach-object set-item Env:$($_.Name) $_.Value


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

try

# Allow multiple DLL files to be registered if necessary
$separator = "
catch

Write-Error ($_);
# Cause the build to fail
throw;



In start of the script, we do this:



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


Which means we load the DLL that was just built by the project (it's literally the output of the Rubberduck.Deployment project) which the powershell script goes on to invoke methods and then eventually write out a file:



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

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


The autogenerated file is then used as a input to the Inno Setup compiler which is used during the AppVeyor build to build a complete installer for the Rubberduck addin. But for a local debug build where we don't use installer, we run this section:



# 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);



The writer will actually create the registry in the developer's HKCU registry to register the debug build to COM among other things. As it does that, it also generates a registry script which is then saved to the disk that enables deletions of all keys it created. That file is then used in subsequent build to delete the previous build's old keys, ensuring that developer don't end up with 1000s of stale keys as they make changes to the objects that may or may not change the registration.



This all works all nicely but as I said at start, this is quite unusual use of Post Build event and I'm wondering if that is a code smell in itself. I'm also interested in hearing whether we can do this better to make the process more seamless and less rube-goldsberg-esque.



Note that while I could have just invoked the powershell script and the DLL inside the AppVeyor, this would not achieve the goal of being able to build and generate the files when building locally which can't be then used for unit tests, debugging, or just simply inspecting. More importantly, I don't want to have one process for building locally and other process for building on AppVeyor, as that has potential to create bugs that we can't see due to missed differences in the build.



Questions to answer:



1) Is this a reasonable method to customize a build process? Are there better ways?



2) I don't like that the macro $(FrameworkSDKDir) is basically undocumented to a C# project. AFAICT, it's only used on a C++ project. Am I going to run into trouble for using that macro? In tests, it seems to work but... ? What about a new version? All this is just to be able to run tlbexp.exe which AFAICT isn't in the PATH variable. :



3) ATM, I have no exact error handling strategy; any error will cause the build as whole to fail. That might be acceptable since no output means build can't be completed anyway but it might violate the principle of least astonishment. I'm also annoyed that currently any errors will just give out a generic the command exited with exit code -1 in the Error List without giving any actual information about the errors. Currently, the contributors must go to the Output for the Build stream to see the actual error from the powershell script. Can we improve on that?



4) Can we improve the environment check? Because we use MIDL compiler which is technically a C++ build tool and thus not a part of a normal C# build process, we must configure the environment. However, I've found it quite hard to reliably detect whether the installed Visual Studio supports C++ build tools or not. I originally checked for existence of VsDevCmd.bat but this has had false positives which causes build process to attempt to compile using MIDL and then fail.







share|improve this question





















  • To be fair, building a VBIDE add-in that exposes a COM API isn't something everybody else does either ;-)
    – Mathieu Guindon
    Mar 22 at 19:36










  • Touché, @MathieuGuindon Still it's C# project, so funky stuff needs to be checked. I added the complete PS script.
    – this
    Mar 22 at 19:47
















up vote
6
down vote

favorite












As a rule, whenever I find myself doing something that nobody else is doing, I have to be quite suspicious about what I'm doing. This is why I want to get code review to verify I'm not doing something insane with the build events for what it's not usually intended.



Contributing to Rubberduck VBA project, I have a PR that creates a new Visual Studio project, named Rubberduck.Deployment. The main purpose of the project is to assist at the build time the extraction of the data that the installer will need to perform its thing. Therefore it will generate files at build time which can vary from build to build as the codebase is updated. Those generated files are then consumed by Inno Setup. My constraint is to be able to support both building locally and remotely via the AppVeyor.



The key code to review is this entry in the Post Build:



(reformatted for readability)



<PreBuildEvent>
C:WindowsSystem32WindowsPowerShellv1.0powershell.exe
-ExecutionPolicy Bypass
-command "$(ProjectDir)PreInnoSetupConfiguration.ps1
-WorkingDir $(ProjectDir)"
</PreBuildEvent>
<PostBuildEvent>
C:WindowsSystem32WindowsPowerShellv1.0powershell.exe
-ExecutionPolicy Bypass
-command "$(ProjectDir)BuildRegistryScript.ps1
-config '$(ConfigurationName)'
-builderAssemblyPath '$(TargetPath)'
-netToolsDir '$(FrameworkSDKDir)binNETFX 4.6.1 Tools'
-wixToolsDir '$(ProjectDir)WixToolset'
-sourceDir '$(TargetDir)'
-targetDir '$(TargetDir)'
-projectDir '$(ProjectDir)'
-includeDir '$(ProjectDir)InnoSetupIncludes'
-filesToExtract 'Rubberduck.dll|Rubberduck.API.dll'"
</PostBuildEvent>


As indicated, we invoke PowerShell scripts which is included in the Visual Studio project (and being a script, it doesn't directly participate in the building of the Visual Studio project). Here's complete code for the post build script....



# 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:WindowsSystem32WindowsPowerShellv1.0powershell.exe
# -command "$(ProjectDir)BuildRegistryScript.ps1
# -config '$(ConfigurationName)'
# -builderAssemblyPath '$(TargetPath)'
# -netToolsDir '$(FrameworkSDKDir)binNETFX 4.6.1 Tools'
# -wixToolsDir '$(SolutionDir)packagesWiX.Toolset.3.9.1208.0toolswix'
# -sourceDir '$(TargetDir)'
# -targetDir '$(TargetDir)'
# -projectDir '$(ProjectDir)'
# -includeDir '$(ProjectDir)InnoSetupIncludes'
# -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
select-string '^([^=]*)=(.*)$'

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


# Restores the environment to a previous state.
function Restore-Environment
foreach-object set-item Env:$($_.Name) $_.Value


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

try

# Allow multiple DLL files to be registered if necessary
$separator = "
catch

Write-Error ($_);
# Cause the build to fail
throw;



In start of the script, we do this:



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


Which means we load the DLL that was just built by the project (it's literally the output of the Rubberduck.Deployment project) which the powershell script goes on to invoke methods and then eventually write out a file:



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

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


The autogenerated file is then used as a input to the Inno Setup compiler which is used during the AppVeyor build to build a complete installer for the Rubberduck addin. But for a local debug build where we don't use installer, we run this section:



# 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);



The writer will actually create the registry in the developer's HKCU registry to register the debug build to COM among other things. As it does that, it also generates a registry script which is then saved to the disk that enables deletions of all keys it created. That file is then used in subsequent build to delete the previous build's old keys, ensuring that developer don't end up with 1000s of stale keys as they make changes to the objects that may or may not change the registration.



This all works all nicely but as I said at start, this is quite unusual use of Post Build event and I'm wondering if that is a code smell in itself. I'm also interested in hearing whether we can do this better to make the process more seamless and less rube-goldsberg-esque.



Note that while I could have just invoked the powershell script and the DLL inside the AppVeyor, this would not achieve the goal of being able to build and generate the files when building locally which can't be then used for unit tests, debugging, or just simply inspecting. More importantly, I don't want to have one process for building locally and other process for building on AppVeyor, as that has potential to create bugs that we can't see due to missed differences in the build.



Questions to answer:



1) Is this a reasonable method to customize a build process? Are there better ways?



2) I don't like that the macro $(FrameworkSDKDir) is basically undocumented to a C# project. AFAICT, it's only used on a C++ project. Am I going to run into trouble for using that macro? In tests, it seems to work but... ? What about a new version? All this is just to be able to run tlbexp.exe which AFAICT isn't in the PATH variable. :



3) ATM, I have no exact error handling strategy; any error will cause the build as whole to fail. That might be acceptable since no output means build can't be completed anyway but it might violate the principle of least astonishment. I'm also annoyed that currently any errors will just give out a generic the command exited with exit code -1 in the Error List without giving any actual information about the errors. Currently, the contributors must go to the Output for the Build stream to see the actual error from the powershell script. Can we improve on that?



4) Can we improve the environment check? Because we use MIDL compiler which is technically a C++ build tool and thus not a part of a normal C# build process, we must configure the environment. However, I've found it quite hard to reliably detect whether the installed Visual Studio supports C++ build tools or not. I originally checked for existence of VsDevCmd.bat but this has had false positives which causes build process to attempt to compile using MIDL and then fail.







share|improve this question





















  • To be fair, building a VBIDE add-in that exposes a COM API isn't something everybody else does either ;-)
    – Mathieu Guindon
    Mar 22 at 19:36










  • Touché, @MathieuGuindon Still it's C# project, so funky stuff needs to be checked. I added the complete PS script.
    – this
    Mar 22 at 19:47












up vote
6
down vote

favorite









up vote
6
down vote

favorite











As a rule, whenever I find myself doing something that nobody else is doing, I have to be quite suspicious about what I'm doing. This is why I want to get code review to verify I'm not doing something insane with the build events for what it's not usually intended.



Contributing to Rubberduck VBA project, I have a PR that creates a new Visual Studio project, named Rubberduck.Deployment. The main purpose of the project is to assist at the build time the extraction of the data that the installer will need to perform its thing. Therefore it will generate files at build time which can vary from build to build as the codebase is updated. Those generated files are then consumed by Inno Setup. My constraint is to be able to support both building locally and remotely via the AppVeyor.



The key code to review is this entry in the Post Build:



(reformatted for readability)



<PreBuildEvent>
C:WindowsSystem32WindowsPowerShellv1.0powershell.exe
-ExecutionPolicy Bypass
-command "$(ProjectDir)PreInnoSetupConfiguration.ps1
-WorkingDir $(ProjectDir)"
</PreBuildEvent>
<PostBuildEvent>
C:WindowsSystem32WindowsPowerShellv1.0powershell.exe
-ExecutionPolicy Bypass
-command "$(ProjectDir)BuildRegistryScript.ps1
-config '$(ConfigurationName)'
-builderAssemblyPath '$(TargetPath)'
-netToolsDir '$(FrameworkSDKDir)binNETFX 4.6.1 Tools'
-wixToolsDir '$(ProjectDir)WixToolset'
-sourceDir '$(TargetDir)'
-targetDir '$(TargetDir)'
-projectDir '$(ProjectDir)'
-includeDir '$(ProjectDir)InnoSetupIncludes'
-filesToExtract 'Rubberduck.dll|Rubberduck.API.dll'"
</PostBuildEvent>


As indicated, we invoke PowerShell scripts which is included in the Visual Studio project (and being a script, it doesn't directly participate in the building of the Visual Studio project). Here's complete code for the post build script....



# 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:WindowsSystem32WindowsPowerShellv1.0powershell.exe
# -command "$(ProjectDir)BuildRegistryScript.ps1
# -config '$(ConfigurationName)'
# -builderAssemblyPath '$(TargetPath)'
# -netToolsDir '$(FrameworkSDKDir)binNETFX 4.6.1 Tools'
# -wixToolsDir '$(SolutionDir)packagesWiX.Toolset.3.9.1208.0toolswix'
# -sourceDir '$(TargetDir)'
# -targetDir '$(TargetDir)'
# -projectDir '$(ProjectDir)'
# -includeDir '$(ProjectDir)InnoSetupIncludes'
# -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
select-string '^([^=]*)=(.*)$'

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


# Restores the environment to a previous state.
function Restore-Environment
foreach-object set-item Env:$($_.Name) $_.Value


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

try

# Allow multiple DLL files to be registered if necessary
$separator = "
catch

Write-Error ($_);
# Cause the build to fail
throw;



In start of the script, we do this:



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


Which means we load the DLL that was just built by the project (it's literally the output of the Rubberduck.Deployment project) which the powershell script goes on to invoke methods and then eventually write out a file:



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

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


The autogenerated file is then used as a input to the Inno Setup compiler which is used during the AppVeyor build to build a complete installer for the Rubberduck addin. But for a local debug build where we don't use installer, we run this section:



# 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);



The writer will actually create the registry in the developer's HKCU registry to register the debug build to COM among other things. As it does that, it also generates a registry script which is then saved to the disk that enables deletions of all keys it created. That file is then used in subsequent build to delete the previous build's old keys, ensuring that developer don't end up with 1000s of stale keys as they make changes to the objects that may or may not change the registration.



This all works all nicely but as I said at start, this is quite unusual use of Post Build event and I'm wondering if that is a code smell in itself. I'm also interested in hearing whether we can do this better to make the process more seamless and less rube-goldsberg-esque.



Note that while I could have just invoked the powershell script and the DLL inside the AppVeyor, this would not achieve the goal of being able to build and generate the files when building locally which can't be then used for unit tests, debugging, or just simply inspecting. More importantly, I don't want to have one process for building locally and other process for building on AppVeyor, as that has potential to create bugs that we can't see due to missed differences in the build.



Questions to answer:



1) Is this a reasonable method to customize a build process? Are there better ways?



2) I don't like that the macro $(FrameworkSDKDir) is basically undocumented to a C# project. AFAICT, it's only used on a C++ project. Am I going to run into trouble for using that macro? In tests, it seems to work but... ? What about a new version? All this is just to be able to run tlbexp.exe which AFAICT isn't in the PATH variable. :



3) ATM, I have no exact error handling strategy; any error will cause the build as whole to fail. That might be acceptable since no output means build can't be completed anyway but it might violate the principle of least astonishment. I'm also annoyed that currently any errors will just give out a generic the command exited with exit code -1 in the Error List without giving any actual information about the errors. Currently, the contributors must go to the Output for the Build stream to see the actual error from the powershell script. Can we improve on that?



4) Can we improve the environment check? Because we use MIDL compiler which is technically a C++ build tool and thus not a part of a normal C# build process, we must configure the environment. However, I've found it quite hard to reliably detect whether the installed Visual Studio supports C++ build tools or not. I originally checked for existence of VsDevCmd.bat but this has had false positives which causes build process to attempt to compile using MIDL and then fail.







share|improve this question













As a rule, whenever I find myself doing something that nobody else is doing, I have to be quite suspicious about what I'm doing. This is why I want to get code review to verify I'm not doing something insane with the build events for what it's not usually intended.



Contributing to Rubberduck VBA project, I have a PR that creates a new Visual Studio project, named Rubberduck.Deployment. The main purpose of the project is to assist at the build time the extraction of the data that the installer will need to perform its thing. Therefore it will generate files at build time which can vary from build to build as the codebase is updated. Those generated files are then consumed by Inno Setup. My constraint is to be able to support both building locally and remotely via the AppVeyor.



The key code to review is this entry in the Post Build:



(reformatted for readability)



<PreBuildEvent>
C:WindowsSystem32WindowsPowerShellv1.0powershell.exe
-ExecutionPolicy Bypass
-command "$(ProjectDir)PreInnoSetupConfiguration.ps1
-WorkingDir $(ProjectDir)"
</PreBuildEvent>
<PostBuildEvent>
C:WindowsSystem32WindowsPowerShellv1.0powershell.exe
-ExecutionPolicy Bypass
-command "$(ProjectDir)BuildRegistryScript.ps1
-config '$(ConfigurationName)'
-builderAssemblyPath '$(TargetPath)'
-netToolsDir '$(FrameworkSDKDir)binNETFX 4.6.1 Tools'
-wixToolsDir '$(ProjectDir)WixToolset'
-sourceDir '$(TargetDir)'
-targetDir '$(TargetDir)'
-projectDir '$(ProjectDir)'
-includeDir '$(ProjectDir)InnoSetupIncludes'
-filesToExtract 'Rubberduck.dll|Rubberduck.API.dll'"
</PostBuildEvent>


As indicated, we invoke PowerShell scripts which is included in the Visual Studio project (and being a script, it doesn't directly participate in the building of the Visual Studio project). Here's complete code for the post build script....



# 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:WindowsSystem32WindowsPowerShellv1.0powershell.exe
# -command "$(ProjectDir)BuildRegistryScript.ps1
# -config '$(ConfigurationName)'
# -builderAssemblyPath '$(TargetPath)'
# -netToolsDir '$(FrameworkSDKDir)binNETFX 4.6.1 Tools'
# -wixToolsDir '$(SolutionDir)packagesWiX.Toolset.3.9.1208.0toolswix'
# -sourceDir '$(TargetDir)'
# -targetDir '$(TargetDir)'
# -projectDir '$(ProjectDir)'
# -includeDir '$(ProjectDir)InnoSetupIncludes'
# -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
select-string '^([^=]*)=(.*)$'

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


# Restores the environment to a previous state.
function Restore-Environment
foreach-object set-item Env:$($_.Name) $_.Value


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

try

# Allow multiple DLL files to be registered if necessary
$separator = "
catch

Write-Error ($_);
# Cause the build to fail
throw;



In start of the script, we do this:



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


Which means we load the DLL that was just built by the project (it's literally the output of the Rubberduck.Deployment project) which the powershell script goes on to invoke methods and then eventually write out a file:



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

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


The autogenerated file is then used as a input to the Inno Setup compiler which is used during the AppVeyor build to build a complete installer for the Rubberduck addin. But for a local debug build where we don't use installer, we run this section:



# 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);



The writer will actually create the registry in the developer's HKCU registry to register the debug build to COM among other things. As it does that, it also generates a registry script which is then saved to the disk that enables deletions of all keys it created. That file is then used in subsequent build to delete the previous build's old keys, ensuring that developer don't end up with 1000s of stale keys as they make changes to the objects that may or may not change the registration.



This all works all nicely but as I said at start, this is quite unusual use of Post Build event and I'm wondering if that is a code smell in itself. I'm also interested in hearing whether we can do this better to make the process more seamless and less rube-goldsberg-esque.



Note that while I could have just invoked the powershell script and the DLL inside the AppVeyor, this would not achieve the goal of being able to build and generate the files when building locally which can't be then used for unit tests, debugging, or just simply inspecting. More importantly, I don't want to have one process for building locally and other process for building on AppVeyor, as that has potential to create bugs that we can't see due to missed differences in the build.



Questions to answer:



1) Is this a reasonable method to customize a build process? Are there better ways?



2) I don't like that the macro $(FrameworkSDKDir) is basically undocumented to a C# project. AFAICT, it's only used on a C++ project. Am I going to run into trouble for using that macro? In tests, it seems to work but... ? What about a new version? All this is just to be able to run tlbexp.exe which AFAICT isn't in the PATH variable. :



3) ATM, I have no exact error handling strategy; any error will cause the build as whole to fail. That might be acceptable since no output means build can't be completed anyway but it might violate the principle of least astonishment. I'm also annoyed that currently any errors will just give out a generic the command exited with exit code -1 in the Error List without giving any actual information about the errors. Currently, the contributors must go to the Output for the Build stream to see the actual error from the powershell script. Can we improve on that?



4) Can we improve the environment check? Because we use MIDL compiler which is technically a C++ build tool and thus not a part of a normal C# build process, we must configure the environment. However, I've found it quite hard to reliably detect whether the installed Visual Studio supports C++ build tools or not. I originally checked for existence of VsDevCmd.bat but this has had false positives which causes build process to attempt to compile using MIDL and then fail.









share|improve this question












share|improve this question




share|improve this question








edited Jun 1 at 13:03









t3chb0t

32.1k54195




32.1k54195









asked Mar 22 at 18:53









this

1,232317




1,232317











  • To be fair, building a VBIDE add-in that exposes a COM API isn't something everybody else does either ;-)
    – Mathieu Guindon
    Mar 22 at 19:36










  • Touché, @MathieuGuindon Still it's C# project, so funky stuff needs to be checked. I added the complete PS script.
    – this
    Mar 22 at 19:47
















  • To be fair, building a VBIDE add-in that exposes a COM API isn't something everybody else does either ;-)
    – Mathieu Guindon
    Mar 22 at 19:36










  • Touché, @MathieuGuindon Still it's C# project, so funky stuff needs to be checked. I added the complete PS script.
    – this
    Mar 22 at 19:47















To be fair, building a VBIDE add-in that exposes a COM API isn't something everybody else does either ;-)
– Mathieu Guindon
Mar 22 at 19:36




To be fair, building a VBIDE add-in that exposes a COM API isn't something everybody else does either ;-)
– Mathieu Guindon
Mar 22 at 19:36












Touché, @MathieuGuindon Still it's C# project, so funky stuff needs to be checked. I added the complete PS script.
– this
Mar 22 at 19:47




Touché, @MathieuGuindon Still it's C# project, so funky stuff needs to be checked. I added the complete PS script.
– this
Mar 22 at 19:47















active

oldest

votes











Your Answer




StackExchange.ifUsing("editor", function ()
return StackExchange.using("mathjaxEditing", function ()
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
);
);
, "mathjax-editing");

StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");

StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "196"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);

else
createEditor();

);

function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
convertImagesToLinks: false,
noModals: false,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);



);








 

draft saved


draft discarded


















StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f190227%2fusing-build-event-to-run-powershell-script-to-run-built-dll-abuse-of-build-sy%23new-answer', 'question_page');

);

Post as a guest



































active

oldest

votes













active

oldest

votes









active

oldest

votes






active

oldest

votes










 

draft saved


draft discarded


























 


draft saved


draft discarded














StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f190227%2fusing-build-event-to-run-powershell-script-to-run-built-dll-abuse-of-build-sy%23new-answer', 'question_page');

);

Post as a guest













































































Popular posts from this blog

Greedy Best First Search implementation in Rust

Function to Return a JSON Like Objects Using VBA Collections and Arrays

C++11 CLH Lock Implementation