-
Notifications
You must be signed in to change notification settings - Fork 301
Build process
Rubberduck uses a customized build process, which is designed to target both the debug and release builds and it must maintain compatibility with AppVeyor when building the release builds. To make those customization possible, a whole project is dedicated to it, the Ruberduck.Deployment
project.
When using COM interop, a common approach to make COM components visible on the machine is to execute the regasm.exe
. In fact, Ruberduck's original installer basically does this at the install time. However, this has a host of problems and actually goes against the recommended approach when developing COM components. The traditional recommendation is to write registry entries directly, which has the advantage that the install becomes more atomic and is easier to transact, without requiring arbitrary code execution. However, the traditional recommendation can be difficult to implement due to large amount of registry keys required to be created and the maintenance of them.
The whole purpose of the Rubberduck.Deployment
project is to help manage the maintenance of the registry keys for COM registration plus few other installer-related aids such as maintaining the copyright dates.
The workflow will run for every build the developer or AppVeyor performs, as it is part of the build events of the project. It will run powershell scripts which acts as the coordinator. For COM registration, it will:
- Execute
tlbexp.exe
tool to generate both 32-bit and 64-bit TLB for a given DLL - Execute WiX's
heat.exe
tool to generate XML fils for the DLL and its 32-bit TLB. - Invoke the builder to parse the XML files generated in the previous step
- The builder will create a list of all registry entries based on the XML data and provide to the script
- The script will then invoke the writer to generate file content as a string with the registry commands
- The script will then write the returned string as a file to the specified location
The project contains the WiX toolset binaries within the folder WixToolset. This is where the
heat.exe` is located and will be referenced in the script. Should an update to WiX toolset binaries be needed, simply download the binaries from WiX's github and replace the contents of the folder.
Because, maintenance. We already had an Inno Setup script and we have complex scripting logic which would be difficult to replicate in a WiX project. Furthermore, the WiX documentation assumes too much from the developer, which can make it hard to use and troubleshoot. Because Rubberduck is an OSS project, it is in its best interest that we use tools that are easy to approach and usable by a larger audience. Despite using Pascal script, Inno Setup fits this requirement better than WiX at the time of writing.
Originally, the Rubberduck's main project had Register for COM Interop
checked, which was the equivalent of performing a regasm.exe
at the build time. With the Rubberduck.Deployment
, there is no need to use Reigster for COM Interop
checkbox anywhere. In fact, it is NOT recommended that it be checked as this can lead to "my machine only" bugs. The other objective for the Rubberduck.Deployment
is to ensure that the developer's debug build will have the same COM registration data in the machine's registry as the user who used an installer would have.
Furthermore, when modifying the COM-visible component, perhaps adding new or removing old components, there is the potential to create orphaned records in the registry with the checkbox because it only generates a new registry without necessarily cleaning up the previous entries. In fact, a previous build might have entries that no longer exist in the current build and thus won't be handled by the regasm.exe
tool. On the other hand, the Rubberduck.Deployment
project writes out a registry script with all keys enumerated for each build so that for the next build, the registry script is executed and all old keys are removed prior to writing the new keys.
Yes, it does and it is woefully insufficient. It contains only the data for the DLL itself but none of the type library as well as the additional keys that needs to be added when a type library is present. Furthermore, regasm.exe
disallow the use of the /regfile
switch with the /tlb
switch. Thus it is basically useless to us.
The project should reference any other assemblies within the Rubberduck's solution where there is COM registration needing to be done. Referencing ensures that those assemblies' output are then copied to the Rubberduck.Deployment
's output directory, which in turn simplify the macros used to execute the powershell script. We'd rather not have to use path that reach across the projects as that makes for a fragile build process since renaming of project could then break the script. Thus, the references are used to avoid the problem. However, because referencing can import much more than just one assembly from another project, it is necessary to manually specify which assembly needs to be processed for COM registration, as described in the next section.
The main file used at the build time is the powershell script, BuildRegistryScript.ps1
. We invoke it via the Post-Build event, using Visual Studio macros to pass on all the context it needs to know about. Here's how it is setup:
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)'
-includeDir '$(ProjectDir)InnoSetup\Includes\'
-filesToExtract 'Rubberduck.dll'"
The main purpose of the parameters is to allow the script to run off macros that is available only to the Visual studio so we can at least avoid hard-coding the absolute path to various things, including the tools tlbexp.exe
and heat.exe
which are located outside of the Rubberduck.Deployment
's project directory.
The -builderAssemblyPath
should refer to the output DLL of the Rubberduck.Deployment
which contains all the C# classes described later in the section.
The -netToolsDir
should refer to the directory where the tlbexp.exe
is located since it is not in the PATH
environment variable.
The -wixToolsDir
should refer to the directory where the WiX toolset is which is pulled by a nuget package and thus can be located within the package directory at the solution level.
The -sourceDir
and -targetDir
represents input and output directory to be used by the script for processing. They can be same if we don't need to write to a different directory. Because we need other assemblies that aren't part of the COM registration, it simplifies thing to use the same directory. The TLB files generated as the result will be placed into the -targetDir
.
The -includeDir
refers to the Includes
directory which is used by Inno Setup to pull in autogenerated .iss
files so therefore is where the output of the InnoSetupRegistryWriter
gets placed.
The -filesToExtract
accepts a |
-delimited lists of assembly to be extracted, so multiple assemblies can be registered for COM registration, though at the time of writing, only one is.
The script will then process each DLL through the tlbexp.exe
tool located in the netToolsDir
parameter twice, one for 32-bit and again for 64-bit. tlbexp.exe
is similar to the regasm.exe
except that it does not actually register the type library as the regasm.exe
would have.
The next step the script will perform is to generate XML files, one from the DLL and other from the 32-bit TLB file through WiX's heat.exe
tool. We do not need to do this for the 64-bit TLB file because the output will be same as the 32-bit TLB anyway. The XML files contains all the information that we need to build the registry entries. The script will in turn invoke the builder to transform the data in the XML files into a list of RegistryEntry
structs, so that we have a good abstraction of the registry entry we must create. The builder will return the list back to the script.
The script will then invoke a writer, providing the list from the builder. The writer will then generate a file that contains the registry entries in a format that is appropriate for its use. For example, InnoSetupRegistryWriter
class will generate content that is a suitable for a .iss
file containing registry commands for the Inno Setup installer to consume. The writer returns the appropriately formatted text to the script.
The script will finally write the writer's resulting string as a file to the filesystem at the specified path. In the case of the output from the InnoSetupRegistryWriter
, it would write the new .iss
file to the includesDir
so that it is available to be picked up by the Inno Setup when it compiles the install script.
The class is built within the Rubberduck.Deployment
assembly which is then used by the BuildRegistryEntries.ps1
powershell script, with this command:
$entries = $builder.Parse($tlbXml, $dllXml);
Basically it passes in the XML files generated by the WiX's heat.exe
tool to the builder's Parse
method. Within the parse method, the class will load the XML files then generate various mapping for different type of COM registrations. The XML already contains all the necessary data, so builder mainly needs to transform them into a useful shape, which is the RegistryEntry
struct. The builder will generate all RegistryEntry
structs it needs to describe every single registry entry that must be created, for all sub-branches of the Software\Class
branch. It will also collect information on whether 32-bit and 64-bit counterpart are needed as there are differences on what must be written to the 32-bit and 64-bit. For example, the TypeLib
registry branch will have win32
and win64
subkey that should be generated with no respect to the registry virtualization.
Its implementation requires a good understanding of how COM registration works. For details, refer to COM Registration for all the details on registry entries needed to register COM components.
There are a number of keys that requires specific data, notably a full path to a directory, the DLL or the TLB file. Those cannot be known until the install time. For that reason, the builder will locate any keys that contains the parameterization and insert a placeholder (see PlaceHolder
static class) in where it is needed. At the time of writing, they are only inserted into the Value
member of the RegistryEntry
struct.
It is then the writer's responsibility to convert those placeholders into an appropriate format so that it may either be appropriately expanded at the install time using the installer's convention of expansion or at the build time for the local builds.
Once the script has gotten the list of RegistryEntry
, it will invoke a writer which must implement the interface. At the time of writing, there are two implementations:
InnoSetupRegistryWriter
LocalDebugRegistryWriter
The only method implemented is Write
method, which takes the list of RegistryEntry
and transform them into a string. It is up to the writer to generate a string that's appropriate for whatever will consume it. The powershell script will then create an actual physical file at a specified location using the output as shown:
$content = $writer.Write($entries);
$regFile = ($includeDir + ($file -replace ".dll", ".reg.iss"))
$encoding = New-Object System.Text.UTF8Encoding $False
[System.IO.File]::WriteAllLines($regFile, $content, $encoding)
In the case of Inno Setup, a registry entry will typically look like this in a .iss
script:
Generic form:
Root: "<Hive to write to>"; Subkey: "<subkey path, without the hive>"; ValueType: <data type>; ValueName: "<name, if any>"; ValueData: "<value, if any>"; Flags: <Inno Setup specific flags> Check: <Inno Setup specific check functions>
Example:
Root: "HKCU64"; Subkey: "Software\Classes\CLSID\{{40F71F29-D63F-4481-8A7D-E04A4B054501}"; ValueType: string; ValueName: ""; ValueData: "Rubberduck.UnitTesting.PermissiveAssertClass"; Flags: uninsdeletekey; Check: IsWin64 and not InstallAllUsers
Therefore, the InnoSetupRegistryWriter
will generate a line that conforms to the Inno Setup's registry command, taking in the appropriate parameterization. It also uses the Bitness
information from the RegistryEntry
to help it decide how it should write for both 64-bit and 32-bit hives and to include appropriate check to prevent writing keys where it's not needed. For example, it will include IsWin64
to ensure that the registry entry will be only generated only when the installing machine is itself 64-bit when writing an entry to HKCU64
.
There need not be a one-to-one correspondence between the list of RegistryEntry
to the output from the InnoSetupRegistryWriter
. For some registry entries, there may be multiple lines written, mainly to handle all 64-bit, 32-bit and neutral variations. As an illustration, here is a possible output from a single registry entry. Note the Check
parameter at the end of the line.
Root: "HKCU64"; Subkey: "Software\Classes\CLSID\{{69E194DA-43F0-3B33-B105-9B8188A6F040}"; ValueType: string; ValueName: ""; ValueData: "Rubberduck.UnitTesting.AssertClass"; Flags: uninsdeletekey; Check: IsWin64 and not InstallAllUsers
Root: "HKCU32"; Subkey: "Software\Classes\CLSID\{{69E194DA-43F0-3B33-B105-9B8188A6F040}"; ValueType: string; ValueName: ""; ValueData: "Rubberduck.UnitTesting.AssertClass"; Flags: uninsdeletekey; Check: IsWin64 and not InstallAllUsers
Root: "HKCU"; Subkey: "Software\Classes\CLSID\{{69E194DA-43F0-3B33-B105-9B8188A6F040}"; ValueType: string; ValueName: ""; ValueData: "Rubberduck.UnitTesting.AssertClass"; Flags: uninsdeletekey; Check: not IsWin64 and not InstallAllUsers
Root: "HKLM64"; Subkey: "Software\Classes\CLSID\{{69E194DA-43F0-3B33-B105-9B8188A6F040}"; ValueType: string; ValueName: ""; ValueData: "Rubberduck.UnitTesting.AssertClass"; Flags: uninsdeletekey; Check: IsWin64 and InstallAllUsers
Root: "HKLM32"; Subkey: "Software\Classes\CLSID\{{69E194DA-43F0-3B33-B105-9B8188A6F040}"; ValueType: string; ValueName: ""; ValueData: "Rubberduck.UnitTesting.AssertClass"; Flags: uninsdeletekey; Check: IsWin64 and InstallAllUsers
Root: "HKLM"; Subkey: "Software\Classes\CLSID\{{69E194DA-43F0-3B33-B105-9B8188A6F040}"; ValueType: string; ValueName: ""; ValueData: "Rubberduck.UnitTesting.AssertClass"; Flags: uninsdeletekey; Check: not IsWin64 and InstallAllUsers
The writer is only used for debug and is what developers will consume. This primarily exists so that developers will be using the same data generated by the RegistryEntryBuilder
, making their debug build much more like the installed builds without actually installing via the Inno Setup installer.
However, it will have a side-effect of actually writing to the developer's HKCU hive as it processes the RegistryEntry
entries. It will then write out a command suitable in a .reg
format to delete the same key it just wrote, similar to the following:
Windows Registry Editor Version 5.00
[-HKEY_CURRENT_USER\Software\Classes\CLSID\{40F71F29-D63F-4481-8A7D-E04A4B054501}]
[-HKEY_CURRENT_USER\Software\Classes\CLSID\{40F71F29-D63F-4481-8A7D-E04A4B054501}\Implemented Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}]
...
and that script is returned to the BuildRegistryEntry.ps1
powershell. The file will be then stored in a different location, in Rubberduck.Deployment\LocalRegistryEntries
.
Thus, when the next time the developer builds the solution, the BuildRegistryEntry.ps1
script will check whether there is a previous script already saved into the folder, and if so, execute it. This has the effect of deleting all the keys from the previous build. That ensures that the developer is not left with stale registry keys as the developer makes changes to the COM visible components which may no longer exist in the next build.
As the name implies, the writer is only executed only for a Debug build, which is why the BuildRegistryEntry.ps1
takes the $(Configuration)
macro as one of its parameters.
Note that at the time of writing, whenever a build runs and the powershell script has executed the deletion script, it will rename the script with a imported_yyyyMMddhhmmss
suffix with UTC timestamp. That provides the developer with a history of what was deleted from the registry. Currently, those files will not be deleted and must be manually deleted.
The Rubberduck.Deployment
project is also used to help perform maintenance. In this case, we have a license which contains a copyright. Every year, it'd "expire" and someone has to update it manually since Inno Setup does not allow parameterization of a license file. To allay that, the Rubberduck.Deployment
will run the PreInnoSetupConfiguration.ps1
which will use the template license. At the time of writing the license.rtf
only has one parameter, $(YEAR$)
, which the powershell script will replace with the current year at the build time. It then copies the file into the appropriate location for the Inno Setup installer script to pick up, thus ensuring that the new builds will reflect the current year they were made.
rubberduckvba.com
© 2014-2021 Rubberduck project contributors
- Contributing
- Build process
- Version bump
- Architecture Overview
- IoC Container
- Parser State
- The Parsing Process
- How to view parse tree
- UI Design Guidelines
- Strategies for managing COM object lifetime and release
- COM Registration
- Internal Codebase Analysis
- Projects & Workflow
- Adding other Host Applications
- Inspections XML-Doc
-
VBE Events