CubMakr – Version 2 BETA Release

Last week we released CubMakr V2, with a few new features. You can find a quick video here.

One of the new features was simply being able to specify custom ICE references as opposed to all the ICE messages appearing as ALKANEICE01. This was a request from Rory over at Rorymon.com.This can now be specified when you’re creating a new rule, at the very top:

ice_name If you’ve already created your rules you can edit each rule and change the name accordingly. This is useful so that you can refer to specific ICE messages like you can with normal MSI validation (ICE33, ICE03 etc). It also means you can set up a web page with references to your custom ICE messages and instructions on how they can be resolved. Unfortunately, the HelpURL part of the ICE message no longer appears to be supported by most products so we can’t include this in the CubMakr toolset (sad face).

The second new feature in Version 2 is Advanced Scripting. This is really interesting, as it enables advanced users to include their own custom VBScripts into the cub files. These scripts can be used to perform custom ICE routines (which cannot be handled by CubMakr natively, such as detecting empty components) and even integrate with your own application tracking tools for real-time validation!! The scripts can be sequenced to run before the CubMakr rules, or after the CubMakr rules. An example of advanced scripting can be found here.

advanced_scripting

CubMakr – Advanced Scripting Example

I thought I’d give a quick example of how advanced users can utilise advanced scripting in CubMakr.

Example 1: Integrating with your Application Tracker

The first example I want to show is how we can integrate the CubMakr with your in-house tracking tool. Some places use Sharepoint to track applications, some places use a SQL Server back-end (ok…so does Sharepoint….but I believe the preferred approach for querying Sharepoint is to use CAML and you can find a VBScript example on my blog here).

I’ll let you read the commented code to see what the script does, but basically:

  • In order to link your package to your tracker record, you’ll need a reference. In this example I create a property in the MSI called ALKANEREF and set the value to the unique reference for my package.
  • In the script, I read the value of ALKANEREF and then query the SQL Server database to retrieve various records (vendor, application and version).
  • I then create a session property (i.e, not a property which will persist in my database) called ALKANEMANUFACTURER which we can then use as part of a CubMakr rule to validate (for example) the Manufacturer property value.

Because we want to set the ALKANEMANUFACTURER property before the CubMakr rules are run, we would obviously set this script to run before the CubMakr rules. Here’s the script:

Dim objConnection : Set objConnection = CreateObject("ADODB.Connection") 
Dim objRecordSet : Set objRecordSet = CreateObject("ADODB.Recordset") 
'Connection string for our fictitious application tracker
objConnection.Open _ 
"Provider=SQLOLEDB;Data Source=.\SQLExpress;" & _ 
"Trusted_Connection=Yes;Initial Catalog=alkanetracker;" & _ 
"User ID=alkanetest;Password=24rHj7Zz4OFNhYV;" 
Dim packagevendor : packagevendor = ""
Dim packageapplication : packageapplication = ""
Dim packageversion : packageversion = ""
Const adVarChar = 200
Const adParamInput = 1
Dim sql : sql = ""
Dim oView, oRecord
'First get the package reference.  We use Session.ProductProperty here because the ALKANEREF property is part of our MSI, and 
'not a property which has been created in the current in-memory session.
alkaneRef = Session.ProductProperty("ALKANEREF")
'select all columns for our package with the specified package reference
set cmd = CreateObject("ADODB.Command")
with cmd
.ActiveConnection = objConnection
.CommandText = "SELECT * FROM packages WHERE packagereference = ?"
.Parameters.Append .CreateParameter("packagereference",adVarChar,adParamInput,50,alkaneRef)
end with
set objRecordSet = cmd.execute
Do While NOT objRecordSet.Eof
packagevendor = objRecordSet("packagevendor") 
packageapplication = objRecordSet("packageapplication") 
packageversion = objRecordSet("packageversion") 
'set ALKANEMANUFACTURER for our CubMakr rule.  Remember that Session.Property is read/write, Session.ProductProperty is read only!
Session.Property("ALKANEMANUFACTURER") = packagevendor
objRecordSet.MoveNext     
Loop
set cmd = nothing 
Set objConnection = Nothing
Set objRecordSet = Nothing

Example 2: Writing your own ICE routines

The second example I wanted to show you was writing your own custom ICE routines. CubMakr is used to provide a simplified way of checking for certain entries within the windows installer tables and summary information stream. It cannot handle more complex scenarios, such as checking for empty component, checking for duplicate registry entries etc. For these scenarios there is now the facility to add your own custom ICE routines. Here is an example of some custom ICE routines:

'initialise variables and constants
Const msiMessageTypeError =  &H01000000
Const msiMessageTypeWarning = &H02000000
Const msiMessageTypeUser = &H03000000
Dim messageIceName, messageDescription, messageDescriptionAddition, messageType, messageTable, messageColumn
'Message Types
'0	MessageFail
'1	MessageError
'2	MessageWarning
'3	MessageInfo
'call Custom ICEs in this order
Call checkEmptyComponents()
Call checkNoKeypath()
Call noDuplicateRegistry()
'This function checks for empty components
Function checkEmptyComponents()
'Here we specify:
'The name of the ICE message
messageIceName = "EXAMPLE01"
'The type of ICE message (see top of script)
messageType = 2
'The description we want to show in the ICE message
messageDescription = "'[1]' is an empty component.  Please delete."	
'The table that the ICE message relates to
messageTable = "Component"
'The column where the issue resides
messageColumn = "Component"
Dim componentsView, componentsRec, tableView, tableRec, dataView, dataRec
Dim emptyComponent : emptyComponent = True
Dim tempComponent : tempComponent = ""
If Session.Database.TablePersistent("Component") = 1 Then
Set componentsView = Session.Database.OpenView("Select `Component` From `Component` ORDER BY `Component`")
componentsView.Execute
Set componentsRec = componentsView.Fetch
Do While Not componentsRec is Nothing
tempComponent = componentsRec.StringData(1)
'list the tables that have 'Component_' (foreign key) columns
Set tableView = Session.Database.OpenView("SELECT `Table` FROM `_Columns` WHERE `Name`= 'Component_' AND `Table` <> 'FeatureComponents'") 
tableView.Execute
Set tableRec = tableView.Fetch
Do While Not tableRec is Nothing
emptyComponent = True
Set dataView = Session.Database.OpenView("SELECT  `Component_` FROM `" & tableRec.StringData(1) & "`  WHERE `Component_`='" & tempComponent & "'")
dataView.Execute
If Not dataView.Fetch is Nothing Then 'this table has a some data belonging to some component
'component contains data
emptyComponent = False
'skip component and move to next
Exit Do
End If
Set tableRec = tableView.Fetch
Loop
If emptyComponent Then				
componentsRec.StringData(0) = messageIceName & Chr(9) & messageType & Chr(9) & messageDescription & Chr(9) & "" & Chr(9) & messageTable & chr(9) & messageColumn & chr(9) & "[1]"
Session.Message msiMessageTypeUser,componentsRec
End If
Set componentsRec = componentsView.Fetch
Loop
Set tableRec = Nothing
Set tableView = Nothing
Set componentsView = Nothing
Set componentsRec = Nothing
Set dataView = Nothing
End If	
'return success
checkEmptyComponents = 1
End Function
'This function checks for components without a keypath, and if a keypath is available, suggests it.
Function checkNoKeypath()
Dim keypathView, keypathRec, blnKeypathSet, tempView, tempRec
messageIceName = "EXAMPLE02"
messageType = 2
messageDescription = "Component '[1]' does not have a keypath set."
messageTable = "Component"
messageColumn = "Component"
If Session.Database.TablePersistent("Component") = 1 Then
'find all components which do not have Keypaths
Set keypathView = Session.Database.OpenView("SELECT `Component`,`ComponentId`, `Attributes` FROM `Component` WHERE `KeyPath` IS Null")
keypathView.Execute
Set keypathRec = keypathView.Fetch
Do Until keypathRec Is Nothing
'initiate this to false
blnKeypathSet = False	
messageDescriptionAddition = " No suitable keypath entries were found."
If Session.Database.TablePersistent("File") = 1 Then
'Check file table			
Set Tempview = Session.Database.OpenView("SELECT `File`,`Component_` FROM `File` WHERE `Component_`='" & keypathRec.StringData(1) & "'")
Tempview.Execute
Set tempRec  = Tempview.Fetch
If Not tempRec Is Nothing Then
blnKeypathSet = True
messageDescriptionAddition = " A suitable keypath may be '" & tempRec.StringData(1) & "' in the File table."					
End If
Set Tempview = Nothing
Set tempRec = Nothing
End If
If Not blnKeypathSet Then
If Session.Database.TablePersistent("Registry") = 1 Then 
Set Tempview = Session.Database.OpenView("SELECT `Registry`, `Component_` FROM `Registry` WHERE `Component_`='" & keypathRec.StringData(1) & "'")
Tempview.Execute
Set tempRec = Tempview.fetch
If Not tempRec is Nothing Then	
blnKeypathSet = True
messageDescriptionAddition = " A suitable keypath may be '" & tempRec.StringData(1) & "' in the Registry table."
end If
Set Tempview = Nothing
Set tempRec = Nothing
End If
End If
If Not blnKeypathSet Then
If Session.Database.TablePersistent("ODBCDataSource") = 1 Then
'check ODBCDataSource table 
Set Tempview = Session.Database.OpenView("SELECT `DataSource`, `Component_` FROM `ODBCDataSource` WHERE `Component_`='" & keypathRec.StringData(1) & "'")
Tempview.Execute
Set tempRec = Tempview.fetch
If Not tempRec is Nothing Then
blnKeypathSet = True						
messageDescriptionAddition = " A suitable keypath may be '" & tempRec.StringData(1) & "' in the ODBCDataSource table."
end If
Set Tempview = Nothing
Set tempRec = Nothing
End If
End If
keypathRec.StringData(0) = messageIceName & Chr(9) & messageType & Chr(9) & messageDescription & messageDescriptionAddition & Chr(9) & "" & Chr(9) & messageTable & chr(9) & messageColumn & chr(9) & "[1]"
Session.Message msiMessageTypeUser,keypathRec
Set keypathRec = keypathView.Fetch
Loop
End If
Set keypathRec = Nothing
Set keypathView = Nothing
Set TempRec = Nothing
Set Tempview = Nothing
checkNoKeypath = 1
End Function
'This function checks that we do not have any duplicate registry.  Duplicate registry items usually occur when users start to import
'registry and they're not being careful!
Function noDuplicateRegistry()
messageIceName = "EXAMPLE03"
messageType = 2
messageTable = "Registry"
messageColumn = "Registry"
Dim registryView, registryRecord, duplicateView, duplicateRecord, tempRecord
If Session.Database.TablePersistent("Registry") = 1 And Session.Database.TablePersistent("Component") = 1  Then
Set registryView = Session.Database.OpenView("SELECT `Registry`,`Key`,`Name`,`Value`,`Component_` FROM `Registry`")
registryView.Execute
Set registryRecord = registryView.Fetch
Do Until registryRecord Is Nothing
Set duplicateView = Session.Database.OpenView("SELECT `Registry` FROM `Registry` WHERE `Key`=? AND `Name`=? AND `Value`=? AND `Registry` <> ?")
Set tempRecord = Session.Installer.CreateRecord(4)    
tempRecord.StringData(1) = registryRecord.StringData(2)    
tempRecord.StringData(2) = registryRecord.StringData(3)
tempRecord.StringData(3) = registryRecord.StringData(4)  
tempRecord.StringData(4) = registryRecord.StringData(1) 		
duplicateView.Execute(tempRecord)
Set tempRecord = Nothing
Set duplicateRecord = duplicateView.Fetch
While not duplicateRecord is Nothing
if not isMSMData(registryRecord.StringData(1)) and not isMSMData(duplicateRecord.StringData(1)) Then
messageDescription = "Registry entry '[1]' is a duplicate of registry entry '" & duplicateRecord.StringData(1) & "'.  Please investigate."
registryRecord.StringData(0) = messageIceName & Chr(9) & messageType & Chr(9) & messageDescription & Chr(9) & "" & Chr(9) & messageTable & chr(9) & messageColumn & chr(9) & "[1]"
Session.Message msiMessageTypeUser,registryRecord
end If
Set duplicateRecord = duplicateView.Fetch
Wend
Set registryRecord = registryView.Fetch
Loop
End If
Set registryView = Nothing
Set registryRecord = Nothing
Set duplicateView = Nothing
Set duplicateRecord = Nothing
noDuplicateRegistry = 1
End  Function
'returns true if tempData contains MSM decoration
Function isMSMData(tempData)
isMSMData = False
Dim Match
Dim regEx : Set regEx = New RegExp
regEx.MultiLine = vbTrue
regEx.global = vbTrue
regEx.Pattern = "[A-Za-z0-9]{8}_[A-Za-z0-9]{4}_[A-Za-z0-9]{4}_[A-Za-z0-9]{4}_[A-Za-z0-9]{12}"
For Each Match in regEx.Execute(tempData)
isMSMData = True
Next
Set regEx = Nothing
End Function

 

 

CubMakr – Simplified and Centralised Online Cub File Creation

Link to tool: CubMakr – Simplified and Centralised Online Cub File Creation

Link to video demonstration: CubMakr Video

Link to examples: CubMakr Examples

Link to V2 Updates: CubMakr V2 BETA Release

Link to video demonstration for V2: CubMakr V2 Video

When creating MSI packages we should always ensure that they conform to Windows logo standards and best practises. Adhering to these standards enables us to produce more reliable and well-authored MSI packages.

To perform this compliance check, we usually validate our MSIs using the Full MSI Validation Suite. What this validation process does is validate the MSI tables against certain rules which are specified in a .cub file called darice.cub. As a result of running this validation, we may be prompted with ICE warnings/errors which we should attempt (in most cases) to resolve.

This validation is great, but what if we need to do our own custom validation? For example, Alkane Solutions do work for multiple clients who all have different requirements:

Customer-specific values, such as Properties:
COMPANYNAME (“Customer 1”, “Customer 2” etc)
ARPNOREMOVE (Some like 1, others like 0)
REBOOT (Maybe we just want to ensure that this property exists, and that it is set correctly?)
Customer-specific logic, such as Custom Actions:
Some of our clients use Custom Actions to perform certain logic, and we must ensure that these are present in every package we deliver. 

Customer-specific naming conventions:
Some of our clients use specific naming conventions for things like the ProductName property. Some like to limit the number of periods in the ProductVersion.

In-house validation checks:
We like to check that our tables do not contain hard-coded paths, that we only append to (and not overwrite) the Path environment variable, that services use the ServiceInstall/ServiceControl tables and not the registry table, and that TNSNames.ora is not included with any package.

 

But how can we perform these validation checks when we have no scripting knowledge, and no knowledge of .cub files?

Easy – that’s where CubMakr comes in.

CubMakr is a custom toolset written using the Wix DTF (Deployment Tools Foundation) with the aim of generating custom CUB files to validate your MSI packages. No scripting knowledge is required!! CubMakr should be used to generate a CUB file which reflects the packaging standards that your company/client adheres to. Using it will:

  1. Increase Accuracy of Packages
  2. Reduce Errors
  3. Improve Quality and Reliability
  4. Reduce Packaging Time

Here are a list of features:

  • Validate ANY table, and ANY column
  • Validate the Summary Information Stream
  • Multiple search comparison operators such as ‘equals’, ‘contains’, ‘starts with’ and ‘end with’
  • Search using regular expressions!
  • Perform case-sensitive searches!
  • Resolve property names at validation time
  • Display custom messages!
  • Set conditions for each validation rule!
  • Display warning, error, info or failure ICE messages!
  • Re-order the rules you create, so messages are displayed in a specific order!

and here is an example of things that can be validated for:

  • Check that certain values exist – this could be a property/value pair, a registry key, a launch condition, a custom action….or anything else contained within a Windows Installer
  • Check the format of an entry is correct – for example, check that the ProductVersion property is of the form major.minor.build
  • Check for hard-coded paths
  • Check that the case of an entry is correct – for example, check that GUIDs are upper case and check that specific public properties are upper case
  • Check that files exist/don’t exist – for example, ensure that files such as TNSNames.ora/hosts/services are not captured.
  • Check that services use the ServiceInstall/ServiceControl tables instead of the Registry table (where possible)
  • Detect if a driver installation has been captured, and warn the user that it must be configured accordingly
  • Detect for darwin descriptors
  • Detect for junk files/registry which needs excluding
  • and much, much more…

Since the first release is just a BETA (to see if people find it useful), we have not included a way of saving cub files and retrieving them at a later date. Unfortunately whatever cubs you generate are only persistent for the current browsing session. The long term goal would be to enable people to save cubs for each client/project.

Example rules can be found here to get you started: CubMakr Detection Examples

Hopefully somebody will find this useful. If you do, please feel free to comment.