Automating the build of a Visual Basic 6 project

Today I did something I should have done a long time ago,
I automated the build and deployment of out legacy vb6 projects.

There's quite a few moving parts to this, but this post will be about
automating the vb6 build (aka "make") from a powershell script.

The code

# Default.ps1 Psake build script
# Script assumes all directories exists, initialized by a parent script

properties {
        $buildDirectory = Split-Path $psake.build_script_file
        $outdir = "$buildDirectory\..\bin"
        $project = "$buildDirectory\..\src\project.vbp"
        $logfile = "$buildDirectory\..\log\project.log"
        $vb6bin = "c:\progam files\microsoft visual studio\vb98\vb6.exe"
}

Task Default -Depends Build

Task Build -Depends CreateLogFile {
        try {
                Exec {& $vb6bin /m $project /out $logfile /outdir $outdir}
        } catch {
                #yummy, exceptions
        }
    Start-Sleep -s 3 #Time may need tweeking
    if ((Select-String "failed" $logfile -Quiet) -or (Select-String "not found" $logfile -Quiet) -or !(Select-String "succeeded" $logfile -Quiet)) {
        Type $logfile
        throw "Build failed"
    }
    Type $logfile
}

Task CreateLogFile {
    if (Test-Path $logfile) {
        Remove-Item $logfile
    }
    $path = [IO.Path]::GetFullPath($logfile)
    New-Item -ItemType file  $path
}

This example is seasoned with some psake syntactical sugar, here's the same in vanilla powershell:

$buildDirectory = GetCurrentDirectory
$outdir = "$buildDirectory\..\bin"
$project = "$buildDirectory\..\src\project.vbp"
$logfile = "$buildDirectory\..\log\project.log"
$vb6bin = "c:\progam files\microsoft visual studio\vb98\vb6.exe"

function CreateLogFile($relativePathToLogFile) {
        if (Test-Path $relativePathToLogFile) {
        Remove-Item $relativePathToLogFile
    }
    $path = [IO.Path]::GetFullPath($relativePathToLogFile)
    New-Item -ItemType file  $path
}

CreateLogFile($logFile)

& $vb6bin /m $project /out $logfile /outdir $outdir

Start-Sleep -s 3 #Time may need tweeking

if ((Select-String "failed" $logfile -Quiet) -or (Select-String "not found" $logfile -Quiet) -or !(Select-String "succeeded" $logfile -Quiet)) {
  Type $logfile
  throw "Build failed"
}

Type $logfile

Working around

This may seem like a lot of code to run a single command, but it's all workarounds:

Vb6 defaults to showing compile errors in a messagebox

This is not very useful for automation..
Solution: Using the /out switch, compile errors (and success messages) are written to a log file.
This log file can then be output to the console (the type $logfile bit.)

Vb6 does not exit with an error code when the build is broken

Meaning, and automated build does not halt when vb6 fails to build a project, as it has no means of knowing that it failed.
Solution: Check the content for the log file for hints on how things went. Words like "failed" and "missing" are good indicators of a failure.
Vb6 will log "Build of project.vbp succeeded." upon a successful build. (I know my code could be simplified to just check for succeeded. But I have not found any documentation on possible output from a vb6 build, and it just might happen that something could both succeed and fail at the same time. One never knows with vb6.)

Vb6 shows an error if the log file does not exist

However, it does create the log file. And the build succeeds.
Solution: Create the log file before the build runs.

Vb6 does not write to the log file synchronously

As a result the log file may be empty when you check for success.
Solution: Wait until you're pretty sure something has been written to the logfile. My implementation is really naive, with just a static wait. You could of course do some clever looping and size checking to avoid excessive waiting.

Satisfaction

I always get really satisfied when I automate something I've been doing manually for a long time. It takes some time and effort, but it's really worth it to lower the effort of pushing a new release into production. And at the same time lowering the risc of human error in the process.

If something is puzzling to you , if you feel I've missed the mark completely, or if you'd like me to elaborate on something, do post a comment or ping me on twitter.

Improving

As has been pointed out to me in the comments, the hard coded sleep command, waiting for the build, is not very elegant.
After some time and tinkering, I found a solution I'm comfortable using.

Because I'm lazy I'm just going to paste the psake file:

#This build assumes the following directory structure
#
#  \Build          - This is where the project build code lives
#  \BuildArtifacts - This folder is created if it is missing and contains output of the build
#  \Code           - This folder contains the source code or solutions you want to build
#
Properties {
        $build_dir = Split-Path $psake.build_script_file       
        $build_artifacts_dir = "$build_dir\..\..\bins"
        $srcdir= "$build_dir\..\..\src"
        $vb6bin = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\vb98\VB6.EXE"
        $project = "$srcdir\project.vbp"
        $name= "The project"
        $logfile = "$build_dir\..\..\vb6log\$name.log"
}

function NormalizeOutDir($outdir) {
        if (-not ($outdir.EndsWith("\"))) {
                $outdir += '\'
        }

        if ($outdir.Contains(" ")) {
                $outdir = $outdir + "\"
        }

        return $outdir
}

function HasFailed($logFile) {
    return ((Select-String "failed" $logfile -Quiet) -or (Select-String "not found" $logfile -Quiet))
}

function HasSucceeded($logFile) {
    return (Select-String "succeeded" $logFile -Quiet)
}

FormatTaskName (("-"*25) + "[{0}]" + ("-"*25))

Task Default -Depends Build

Task Build -Depends CreateLogFile {
    $outdir = NormalizeOutDir("$build_artifacts_dir")
    $failed = $false
    $retries = 0
    $succeeded = $false
    Write-Host "Building $name" -ForegroundColor Green
        try {
                Exec {& $vb6bin /m $project /out $logfile /outdir $outdir}
        } catch {
                $failed = $true
        }

    while (!($failed -or $succeeded)) {
        Write-Host -NoNewline "."
        Start-Sleep -s 1
        $failed = HasFailed($logfile)
        $succeeded = HasSucceeded($logfile)
        $retries = $retries + 1
        $failed = ($failed -or ($retries -eq 60)) -and !$succeeded
    }

    if ($failed)
    {
        Type $logfile
        throw "Unable to build $name"
    }
    Type $logfile
}

Task CreateLogFile {
    if (Test-Path $logfile)
    {
        Remove-Item $logfile
    }
    $path = [IO.Path]::GetFullPath($logfile)
    New-Item -ItemType file  $path
}

Categories: