' Windows Installer utility to generate file cabinets from MSI database ' For use with Windows Scripting Host, CScript.exe or WScript.exe ' Copyright (c) 1999-2000, Microsoft Corporation ' Demonstrates the access to install engine and actions ' Option Explicit ' FileSystemObject.CreateTextFile and FileSystemObject.OpenTextFile Const OpenAsASCII = 0 Const OpenAsUnicode = -1 ' FileSystemObject.CreateTextFile Const OverwriteIfExist = -1 Const FailIfExist = 0 ' FileSystemObject.OpenTextFile Const OpenAsDefault = -2 Const CreateIfNotExist = -1 Const FailIfNotExist = 0 Const ForReading = 1 Const ForWriting = 2 Const ForAppending = 8 Const msiOpenDatabaseModeReadOnly = 0 Const msiOpenDatabaseModeTransact = 1 Const msiViewModifyInsert = 1 Const msiViewModifyUpdate = 2 Const msiViewModifyAssign = 3 Const msiViewModifyReplace = 4 Const msiViewModifyDelete = 6 Const msiUILevelNone = 2 Const msiRunModeSourceShortNames = 9 Const msidbFileAttributesNoncompressed = &h00002000 Dim argCount:argCount = Wscript.Arguments.Count Dim iArg:iArg = 0 If argCount > 0 Then If InStr(1, Wscript.Arguments(0), "?", vbTextCompare) > 0 Then argCount = 0 If (argCount < 2) Then Wscript.Echo "Windows Installer utility to generate compressed file cabinets from MSI database" &_ vbNewLine & " The 1st argument is the path to MSI database, at the source file root" &_ vbNewLine & " The 2nd argument is the base name used for the generated files (DDF, INF, RPT)" &_ vbNewLine & " The 3rd argument can optionally specify separate source location from the MSI" &_ vbNewLine & " The following options may be specified at any point on the command line" &_ vbNewLine & " /L to use LZX compression instead of MSZIP" &_ vbNewLine & " /F to limit cabinet size to 1.44 MB floppy size rather than CD" &_ vbNewLine & " /C to run compression, else only generates the .DDF file" &_ vbNewLine & " /U to update the MSI database to reference the generated cabinet" &_ vbNewLine & " /E to embed the cabinet file in the installer package as a stream" &_ vbNewLine & " /S to sequence number file table, ordered by directories" &_ vbNewLine & " /R to revert to non-cabinet install, removes cabinet if /E specified" &_ vbNewLine & " Notes:" &_ vbNewLine & " In order to generate a cabinet, MAKECAB.EXE must be on the PATH" &_ vbNewLine & " base name used for files and cabinet stream is case-sensitive" &_ vbNewLine & " If source type set to compressed, all files will be opened at the root" &_ vbNewLine & " (The /R option removes the compressed bit - SummaryInfo property 15 & 2)" &_ vbNewLine & " To replace an embedded cabinet, include the options: /R /C /U /E" &_ vbNewLine & " Does not handle updating of Media table to handle multiple cabinets" &_ vbNewLine &_ vbNewLine & "Copyright (C) Microsoft Corporation, 1999-2000. All rights reserved." Wscript.Quit 1 End If ' Get argument values, processing any option flags Dim compressType : compressType = "MSZIP" Dim cabSize : cabSize = "CDROM" Dim makeCab : makeCab = False Dim embedCab : embedCab = False Dim updateMsi : updateMsi = False Dim sequenceFile : sequenceFile = False Dim removeCab : removeCab = False Dim databasePath : databasePath = NextArgument Dim baseName : baseName = NextArgument Dim sourceFolder : sourceFolder = NextArgument If Not IsEmpty(NextArgument) Then Fail "More than 3 arguments supplied" ' process any trailing options If Len(baseName) < 1 Or Len(baseName) > 8 Then Fail "Base file name must be from 1 to 8 characters" If Not IsEmpty(sourceFolder) And Right(sourceFolder, 1) <> "\" Then sourceFolder = sourceFolder & "\" Dim cabFile : cabFile = baseName & ".CAB" Dim cabName : cabName = cabFile : If embedCab Then cabName = "#" & cabName ' Connect to Windows Installer object On Error Resume Next Dim installer : Set installer = Nothing Set installer = Wscript.CreateObject("WindowsInstaller.Installer") : CheckError ' Open database Dim database, openMode, view, record, updateMode, sumInfo, sequence, lastSequence If updateMsi Or sequenceFile Or removeCab Then openMode = msiOpenDatabaseModeTransact Else openMode = msiOpenDatabaseModeReadOnly Set database = installer.OpenDatabase(databasePath, openMode) : CheckError ' Remove existing cabinet(s) and revert to source tree install if options specified If removeCab Then Set view = database.OpenView("SELECT DiskId, LastSequence, Cabinet FROM Media ORDER BY DiskId") : CheckError view.Execute : CheckError updateMode = msiViewModifyUpdate Set record = view.Fetch : CheckError If Not record Is Nothing Then ' Media table not empty If Not record.IsNull(3) Then If record.StringData(3) <> cabName Then Wscript.Echo "Warning, cabinet name in media table, " & record.StringData(3) & " does not match " & cabName record.StringData(3) = Empty End If record.IntegerData(2) = 9999 ' in case of multiple cabinets, force all files from 1st media view.Modify msiViewModifyUpdate, record : CheckError Do Set record = view.Fetch : CheckError If record Is Nothing Then Exit Do view.Modify msiViewModifyDelete, record : CheckError 'remove other cabinet records Loop End If Set sumInfo = database.SummaryInformation(3) : CheckError sumInfo.Property(11) = Now sumInfo.Property(13) = Now sumInfo.Property(15) = sumInfo.Property(15) And Not 2 sumInfo.Persist Set view = database.OpenView("SELECT `Name`,`Data` FROM _Streams WHERE `Name`= '" & cabFile & "'") : CheckError view.Execute : CheckError Set record = view.Fetch If record Is Nothing Then Wscript.Echo "Warning, cabinet stream not found in package: " & cabFile Else view.Modify msiViewModifyDelete, record : CheckError End If Set sumInfo = Nothing ' must release stream database.Commit : CheckError If Not updateMsi Then Wscript.Quit 0 End If ' Create an install session and execute actions in order to perform directory resolution installer.UILevel = msiUILevelNone Dim session : Set session = installer.OpenPackage(database,1) : If Err <> 0 Then Fail "Database: " & databasePath & ". Invalid installer package format" Dim shortNames : shortNames = session.Mode(msiRunModeSourceShortNames) : CheckError If Not IsEmpty(sourceFolder) Then session.Property("OriginalDatabase") = sourceFolder : CheckError Dim stat : stat = session.DoAction("CostInitialize") : CheckError If stat <> 1 Then Fail "CostInitialize failed, returned " & stat ' Check for non-cabinet files to avoid sequence number collisions lastSequence = 0 If sequenceFile Then Set view = database.OpenView("SELECT Sequence,Attributes FROM File") : CheckError view.Execute : CheckError Do Set record = view.Fetch : CheckError If record Is Nothing Then Exit Do sequence = record.IntegerData(1) If (record.IntegerData(2) And msidbFileAttributesNoncompressed) <> 0 And sequence > lastSequence Then lastSequence = sequence Loop End If ' Join File table to Component table in order to find directories Dim orderBy : If sequenceFile Then orderBy = "Directory_" Else orderBy = "Sequence" Set view = database.OpenView("SELECT File,FileName,Directory_,Sequence,File.Attributes FROM File,Component WHERE Component_=Component ORDER BY " & orderBy) : CheckError view.Execute : CheckError ' Create DDF file and write header properties Dim FileSys : Set FileSys = CreateObject("Scripting.FileSystemObject") : CheckError Dim outStream : Set outStream = FileSys.CreateTextFile(baseName & ".DDF", OverwriteIfExist, OpenAsASCII) : CheckError outStream.WriteLine "; Generated from " & databasePath & " on " & Now outStream.WriteLine ".Set CabinetNameTemplate=" & baseName & "*.CAB" outStream.WriteLine ".Set CabinetName1=" & cabFile outStream.WriteLine ".Set ReservePerCabinetSize=8" outStream.WriteLine ".Set MaxDiskSize=" & cabSize outStream.WriteLine ".Set CompressionType=" & compressType outStream.WriteLine ".Set InfFileLineFormat=(*disk#*) *file#*: *file* = *Size*" outStream.WriteLine ".Set InfFileName=" & baseName & ".INF" outStream.WriteLine ".Set RptFileName=" & baseName & ".RPT" outStream.WriteLine ".Set InfHeader=" outStream.WriteLine ".Set InfFooter=" outStream.WriteLine ".Set DiskDirectoryTemplate=." outStream.WriteLine ".Set Compress=ON" outStream.WriteLine ".Set Cabinet=ON" ' Fetch each file and request the source path, then verify the source path Dim fileKey, fileName, folder, sourcePath, delim, message, attributes Do Set record = view.Fetch : CheckError If record Is Nothing Then Exit Do fileKey = record.StringData(1) fileName = record.StringData(2) folder = record.StringData(3) sequence = record.IntegerData(4) attributes = record.IntegerData(5) If (attributes And msidbFileAttributesNoncompressed) = 0 Then If sequence <= lastSequence Then If Not sequenceFile Then Fail "Duplicate sequence numbers in File table, use /S option" sequence = lastSequence + 1 record.IntegerData(4) = sequence view.Modify msiViewModifyUpdate, record End If lastSequence = sequence delim = InStr(1, fileName, "|", vbTextCompare) If delim <> 0 Then If shortNames Then fileName = Left(fileName, delim-1) Else fileName = Right(fileName, Len(fileName) - delim) End If sourcePath = session.SourcePath(folder) & fileName outStream.WriteLine """" & sourcePath & """" & " " & fileKey If installer.FileAttributes(sourcePath) = -1 Then message = message & vbNewLine & sourcePath End If Loop outStream.Close REM Wscript.Echo "SourceDir = " & session.Property("SourceDir") If Not IsEmpty(message) Then Fail "The following files were not available:" & message ' Generate compressed file cabinet If makeCab Then Dim WshShell : Set WshShell = Wscript.CreateObject("Wscript.Shell") : CheckError Dim cabStat : cabStat = WshShell.Run("MakeCab.exe /f " & baseName & ".DDF", 1, True) : CheckError If cabStat <> 0 Then Fail "MAKECAB.EXE failed, possibly could not find source files, or invalid DDF format" End If ' Update Media table and SummaryInformation if requested If updateMsi Then Set view = database.OpenView("SELECT DiskId, LastSequence, Cabinet FROM Media ORDER BY DiskId") : CheckError view.Execute : CheckError updateMode = msiViewModifyUpdate Set record = view.Fetch : CheckError If record Is Nothing Then ' Media table empty Set record = Installer.CreateRecord(3) record.IntegerData(1) = 1 updateMode = msiViewModifyInsert End If record.IntegerData(2) = lastSequence record.StringData(3) = cabName view.Modify updateMode, record Set sumInfo = database.SummaryInformation(3) : CheckError sumInfo.Property(11) = Now sumInfo.Property(13) = Now sumInfo.Property(15) = (shortNames And 1) + 2 sumInfo.Persist End If ' Embed cabinet if requested If embedCab Then Set view = database.OpenView("SELECT `Name`,`Data` FROM _Streams") : CheckError view.Execute : CheckError Set record = Installer.CreateRecord(2) record.StringData(1) = cabFile record.SetStream 2, cabFile : CheckError view.Modify msiViewModifyAssign, record : CheckError 'replace any existing stream of that name End If ' Commit database in case updates performed database.Commit : CheckError Wscript.Quit 0 ' Extract argument value from command line, processing any option flags Function NextArgument Dim arg Do ' loop to pull in option flags until an argument value is found If iArg >= argCount Then Exit Function arg = Wscript.Arguments(iArg) iArg = iArg + 1 If (AscW(arg) <> AscW("/")) And (AscW(arg) <> AscW("-")) Then Exit Do Select Case UCase(Right(arg, Len(arg)-1)) Case "C" : makeCab = True Case "E" : embedCab = True Case "F" : cabSize = "1.44M" Case "L" : compressType = "LZX" Case "R" : removeCab = True Case "S" : sequenceFile = True Case "U" : updateMsi = True Case Else: Wscript.Echo "Invalid option flag:", arg : Wscript.Quit 1 End Select Loop NextArgument = arg End Function Sub CheckError Dim message, errRec If Err = 0 Then Exit Sub message = Err.Source & " " & Hex(Err) & ": " & Err.Description If Not installer Is Nothing Then Set errRec = installer.LastErrorRecord If Not errRec Is Nothing Then message = message & vbNewLine & errRec.FormatText End If Fail message End Sub Sub Fail(message) Wscript.Echo message Wscript.Quit 2 End Sub