The Wiert Corner – irregular stream of stuff

Jeroen W. Pluimers on .NET, C#, Delphi, databases, and personal interests

  • My work

  • My badges

  • Twitter Updates

  • My Flickr Stream

    20140508-Delphi-2007--Project-Options--Cannot-Edit-Application-Title-HelpFile-Icon-Theming

    20140430-Fiddler-Filter-Actions-Button-Run-Filterset-now

    20140424-Windows-7-free-disk-space

    More Photos
  • Pages

  • All categories

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 1,681 other followers

BeSharp.net: PowerShell script to show the component packages (BPL) files for all installed Delphi (actually: BDS) versions.

Posted by jpluimers on 2014/11/13

A while ago, I wrote a via PowerShell script to show the component packages (BPL) files for all installed Delphi (actually: BDS) versions for a couple of reasons:

  • I was creating installation instructions for getting new development machines set-up
  • The new machines had to either have a minimum subset of installed Delphi versions  + components, or the maximum superset of all the existing development machines
  • Sifting through the installed Packages in the IDE, or registry by hand was cumbersome

Note that in the mean time (I queued this blog entry somewhere in 2013) the script has moved to BitBucket, I’ve written more scripts (like Dependencies.bat which is documented in Dependencies.md and Run-Dependend-rsvars-From-Path.bat), all modified all scripts to support all BDS versions I had access to, and a write nice conference paper on Build Automation for Delphi that references the scripts.

Since none of the machines were using pre BDS installations, I could limit the script to BDS 1.0 and up.

The very first (1.0) version of BDS (also known as the Gailileo IDE foundation) was in fact not a Delphi version, but C# Builder 1.0. All Delphi versions since then are based on BDS. The script is based on the BDS registry keys I researched and wrote about in Files in your Delphi settings directory; How to relocate the Favourites on your Welcome page.

Since registry access can be very much flow based, the pipeline architecture of PowerShell is a good fit.

So I wrote a PowerShell script (:

Note Thomas Mueller has written a batch file around the same set of registry keys; the thread there also has some insight in the HKLM versus HKCU keys.

I will explain my script step by step, and start with the most important one: Set-StrictMode -Version Latest.

By default, PowerShell is very lenient. You’d think that is a good thing for PowerShell novices, but in practice it isn’t. It might be a good thing for PowerShell gurus (I’m far from that), but the default configuration makes PowerShell not complain until it really has no clue what you actually did wrong.

A few things that causes Set-StrictMode (you have to hack around to implement Get-StrictMode)  to stop with an error are at Powershell: Set-StrictMode -Version Latest.:

Set-StrictMode -Version Latest

PowerShell script best practices has some other tips on what you should add in the header of your scripts, and for legacy code: Set-StrictMode and legacy code issues.

Set-StrictMode -Version Latest

Main code at the end of your script

The functions in the file can be tested one-by-one in this block at the end of the file (you can only call functions declared earlier in the file), and indicate the versatility of this PowerShell file:

#Get-BDS-Versions
#Get-BDS-ProductNames
#Get-BDS-CompanyNames
#Get-BDS-BaseKeyPaths
#Get-BDS-HKCU-BaseKeyPaths
#Get-BDS-HKLM-BaseKeyPaths
#Get-BDS-ProductVersions
#Get-BDS-ProductFullNames
Filter-BDS-Packages-For-All-Versions

Registry paths, default parameters and error handling

Most functions have default parameter values, in these cases virtually everywhere the base registry path for BDS 8.0 (Delphi XE)

I know that you can specify Registry keys without quotes, but I’m accustomed using quotes in other languages, so I use quotes in PowerShell too.

One of the things that can happen is that you do not have all BDS editions installed on one machine, so one of the registry paths does not exist.

I’ve used -ErrorAction SilentlyContinue here to convert this potential error condition into a $null response which is handled by the if statement.
More detailed advice on such error handling can be found at Managing non-terminating errors – Windows PowerShell Blog – Site Home – MSDN Blogs.

Another thing you will see a lot in my script is hashes (#) in front of lines that I used for debugging purposes. A great feature of PowerShell is that anything you do not return, but produces something will be sent to the standard output. So removing the hashes below will reveal the content of $key and a table formatted $nameValues as text. Nice!

Note I use the “Known Packages” key, but skip the “Known IDE Packages” key. That is explained by Allen Bauer in Why my Delphi IDE Expert is not initialized when use the “Known IDE Packages” Key? – Stack Overflow and Mark Edington in his post Mark Edington’s Delphi Blog : Delphi Startup Times and the Kitchen Sink about Delphi personalities.

function Get-KnownPackages {
    param(
        [string]$basePath = 'hkcu:\Software\Embarcadero\BDS\8.0'
    )
    $path = $basePath + '\Known Packages'
    $key = Get-Item $path -ErrorAction SilentlyContinue
    if ($key) {
        # $key
        $names = $key.GetValueNames()
        $namevalues = $names |
            ForEach-Object { [PSCustomObject]@{ Name = $_; Value = $key.GetValue($_) } }
        # $namevalues | Format-Table
    }
    else {
        $namevalues = $null
    }
    $namevalues
}

Matching with Regular Expressions

Sifting through the packages, I wanted to know which ones were system installed (by the Delphi installer), and user installed. My empiric observation was that all packages installed into directories starting with $(BDS) or $(BDSLIB).

Since searching with -match or -notMatch involves regular expressions, the dollar and parenthesis need to be escaped, so you end up with \$(BDS) or \$(BDSLIB).

function Filter-BDS-Packages {
    param(
        [string]$basePath = 'hkcu:\Software\Embarcadero\BDS\8.0',
        [boolean]$excludeMatch = $True
    )
    $namevalues = Get-KnownPackages($basePath)

    if ($nameValues) {
        $bdsBinAtStart = '^\$\(BDSBIN\)'
        $bdsAtStart = '^\$\(BDS\)'
        if ($excludeMatch) {
            $matches = $namevalues | Where-Object {
                $_.Name -notMatch $bdsBinAtStart -and `
                $_.Name -notMatch $bdsAtStart
          }
        }
        else {
            $matches = $namevalues | Where-Object {
                $_.Name -match $bdsBinAtStart -or `
                $_.Name -match $bdsAtStart
            }
        }
    }
    else {
        $matches = $null
    }
    $matches
}

The next step is getting BDS Versions, product and company names. I’m following the lists from Files in your Delphi settings directory; How to relocate the Favourites on your Welcome page here.

First the singular versions of the functions.

Most of these use the PowerShell switch statement to map from BDS version to CompanyName.

function Get-BDS-CompanyName {
    param (
        $bdsVersion = 1
    )
    <#
     CompanyName=Borland (from BDS 1 until BDS 5)
     CompanyName=CodeGear (from BDS 6 until BDS 7)
     CompanyName=Embarcadero (BDS 8 and up)
     #>
    $borland = 'Borland'
    $codeGear = 'CodeGear'
    $embarcadero = 'Embarcadero'
    switch ($bdsVersion) {
        1 { return $borland }
        2 { return $borland }
        3 { return $borland }
        4 { return $borland }
        5 { return $borland }
        6 { return $codeGear }
        7 { return $codeGear }
        Default { return $embarcadero }
    }
}

function Get-BDS-ProductName {
    param (
        $bdsVersion = 1
    )

    switch ($bdsVersion) {
        1       { 'C# Builder' }
        Default { 'Delphi' }
    }
}

function Get-BDS-ProductVersion {
    param (
        $bdsVersion = 1
    )

    switch ($bdsVersion) {
        1 { '1' }
        2 { '8' }
        3 { '2005' }
        4 { '2006' }
        5 { '2007' }
        6 { '2009' }
        7 { '2010' }
        8 { 'XE' }
        9 { 'XE2' }
        10 { 'XE3' }
        11 { 'XE4' }
        12 { 'XE5' }
    }
}

and some of them use string formatting using the -f format operator:

function Get-BDS-BaseKeyPath {
    param (
        $bdsVersion = 1,
        $rootKey = 'hkcu'
    )
    $company = Get-BDS-CompanyName $bdsVersion
    $pathFormat = '{0}:\Software\{1}\BDS\{2}.0'
    # 'hkcu:\Software\Embarcadero\BDS\8.0' $True
    $path = $pathFormat -f $rootKey, $company, $bdsVersion
    $path
}

function Get-BDS-ProductFullName {
    param (
        $bdsVersion = 1
    )
    <#
     1. Borland C# Builder (contained only C# Builder)
     2. Borland Delphi 8 (added Delphi .net)
     3. Borland Delphi 2005 (added Delphi win32)
     4. Borland Delphi 2006 (added C++ Builder)
     5. Borland Delphi 2007
     6. CodeGear Delphi 2009 (Unicode; not a platform, but still)
     7. CodeGear Delphi 2010 (Generics done well)
     8. Embarcadero Delphi XE
     9. Embarcadero Delphi XE2 (added Delphi win64)
     10. Embarcadero Delphi XE3 (added C++ Builder win64, OS X x86 and iOS x86/arm through FreePascal)
     11. Embarcadero Delphi XE4 (replaced FreePascal with native Delphi compiler for iOS x86/arm)
     12. Embarcadero Delphi XE5 (added Android ARMv7)
     #>
    $company = Get-BDS-CompanyName $bdsVersion
    $name = Get-BDS-ProductName $bdsVersion
    $version = Get-BDS-ProductVersion $bdsVersion
    $fullNameFormat = '{0} {1} {2}'
    $fullName = $fullNameFormat -f $company, $name, $version
    $fullName
}

some are really simple calls to other methods (note that parameters in PowerShell are space separated, not comma separated):

function Get-BDS-HKCU-BaseKeyPath {
    param (
        $bdsVersion = 1
    )
    $path = Get-BDS-BaseKeyPath $bdsVersion
    $path
}

function Get-BDS-HKLM-BaseKeyPath {
    param (
        $bdsVersion = 1
    )
    $path = Get-BDS-BaseKeyPath $bdsVersion 'hklm'
    $path
}

The plural versions of the methods just call the singular versions, except for Get-BDS-Versions which initializes an array.

For arrays, I prefer strong typing. Instead of using @() to initialize an empty untyped array, I use [int[]] for a strongly typed array.

</span>
<pre>function Get-BDS-CompanyNames {
    $versions = Get-BDS-Versions
    $versions | ForEach-Object { Get-BDS-CompanyName $_ }
}

function Get-BDS-Versions {
    ## array initialization:
    ## http://stackoverflow.com/questions/226596/powershell-array-initialization
    ## http://get-powershell.com/post/2008/02/07/Powershell-function-New-Array.aspx
    ## http://technet.microsoft.com/en-us/library/ee692797.aspx
    [int[]] $versions = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
    $versions
}

function Get-BDS-ProductNames {
  $versions = Get-BDS-Versions
  $versions | ForEach-Object { Get-BDS-ProductName $_ }
}

function Get-BDS-ProductVersions {
    $versions = Get-BDS-Versions
    $versions | ForEach-Object { Get-BDS-ProductVersion $_ }
}

function Get-BDS-ProductFullNames {
    $fullNames = Get-BDS-FullNames
    $fullNames | ForEach-Object { Get-BDS-ProductFullName $_ }
    $fullNames
}

function Get-BDS-BaseKeyPaths {
    param (
        $rootKey = 'hkcu'
    )
    $bdsVersions = Get-BDS-Versions
    $bdsVersions | ForEach-Object {
        $path = Get-BDS-BaseKeyPath $_ $rootKey
        $path
    }
}

function Get-BDS-HKCU-BaseKeyPaths {
    $bdsVersions = Get-BDS-Versions
    $bdsVersions | ForEach-Object {
        $path = Get-BDS-HKCU-BaseKeyPath $_
        $path
    }
}

function Get-BDS-HKLM-BaseKeyPaths {
    $bdsVersions = Get-BDS-Versions
    $bdsVersions | ForEach-Object {
        $path = Get-BDS-HKLM-BaseKeyPath $_
        $path
    }
}

Finally, there is the Filter-BDS-Packages-For-All-Versions function which brings everything together.

It loops over user/system installed packages (parsing the PowerShell $True and $False boolean constants (though there are other ways to specify TRUE and FALSE in PowerShell).

The cool thing about arrays is that you can push them through pipes with ForEach-Object instead of classic for or do loops.

Every once in a while, you get this error when formatting output in PowerShell:

Object of type “Microsoft.PowerShell.Commands.Internal.Format.FormatStartData” is not valid or not in the correct sequence. This is likely caused by a user-specified “format-table” command which is conflicting with the default formatting.

It hardly occurs, but the trick solving only the symptom is to perform a pipe through Out-String to force the output into a series of strings. If anyone knows about the actual cause, please let me know. Especially if you also know how to resolve (:

Boolean not is either the ! or the -not operator. But since all other PowerShell logical operators start with a minus sign, I used -not for consistency.

There is also some $state debugging code left behind the # comment markers. Uncomment these when you want to trace through the code.

Finally a small trick: $anyInstalledPackages is used to mark if any packages have been found at all. If you have no Delphi or no packages installed, it will stay $False, and an appropriate message will be shown.

function Filter-BDS-Packages-For-All-Versions {
    $anyInstalledPackages = $False
    [bool[]]$filters = $True, $False
    $filters | ForEach-Object {
        $filter = $_
        $versions = Get-BDS-Versions
        $versions | ForEach-Object {
            $version = $_
            $basePath = Get-BDS-HKCU-BaseKeyPath $version
            # $state = [PSCustomObject]@{ Version = $version; Filter = $filter; Path = $basePath }
            # $state
            $userPacakgesNameValues = Filter-BDS-Packages $basePath $filter
            if ($userPacakgesNameValues) {
                $productFullName = Get-BDS-ProductFullName $version
                switch ($filter) {
                  $False  { $kind = 'System' }
                  Default { $kind = 'User' }
                }
                $line = '{0} installed packages for "{1}" in {2}' -f $kind, $productFullName, $basePath
                Write-Host $line
                $userPacakgesNameValues | Format-Table | Out-String
                $anyInstalledPackages = $True
            }
        }
    }
    if (-not $anyInstalledPackages) {
        Write-Host 'No installed Delphi packages found'
    }

}
<span style="font-size: 12px; line-height: 18px;">

Finally the test code again:

#Get-BDS-Versions
#Get-BDS-ProductNames
#Get-BDS-CompanyNames
#Get-BDS-BaseKeyPaths
#Get-BDS-HKCU-BaseKeyPaths
#Get-BDS-HKLM-BaseKeyPaths
#Get-BDS-ProductVersions
#Get-BDS-ProductFullNames
Filter-BDS-Packages-For-All-Versions

Have fun with it!

–jeroen

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: