How to attach the build process to Visual Studio Code

A lot of people asked for a sample on how to compile a project from within Visual Studio Code and how to get nice formatted compiler messages. VSCode comes with a configurable interface that allows you to define

  • a general command to be executed on each build
  • parameters to that command to build the main project
  • parameters to that command to build and run the test project
  • and a problem matcher that extracts warnings and errors from the compiler output.

This configuration is stored in ${workspaceRoot}/.vscode/tasks.json where ${workspaceRoot} is the path of the folder opened in VSCode.

Technical overview

We create a batch file which accepts two different values as the first parameter – build and test. Depending on its value we execute the corresponding commands. We define a problem matcher in order to convert the compiler’s StdOut into displayable notifications, hints and errors.
After compiling the test command we call the test executable to see its result inside the editor.
Here are two sample configurations. Pick the one that helps you the most.

Configuration for a Delphi project using MSBuild

Compile.bat:

@echo off

SET MSBUILD="C:\Program Files (x86)\MSBuild\12.0\Bin\MSBuild.exe"
SET RSVARS="C:\Program Files (x86)\Embarcadero\Studio\16.0\bin\rsvars.bat"

if /i %1%==test (
  SET PROJECT=TestProject\UnitTests.dproj
) else (
  SET PROJECT=MainProject\AwesomeExe.dproj
) 

call %RSVARS%   
%MSBUILD% %PROJECT% "/t:Clean,Make" "/p:config=Debug" "/verbosity:minimal"

if %ERRORLEVEL% NEQ 0 GOTO END

echo. 
if /i %1%==test (
  TestProject\UnitTests.exe
)
:END

tasks.json:

{
    "version": "0.1.0",
    "windows": {
        "command": "${workspaceRoot}/Compile.bat"
    },
    "isShellCommand": true,
    "showOutput": "always",
    "tasks": [
        {
            "taskName": "build",
            "isBuildCommand": true,
            "problemMatcher": {
                "owner": "external",
                "fileLocation": ["relative", "${workspaceRoot}"],                        
                "pattern": {
                    "regexp": "((([A-Za-z]):\\\\(?:[^\\/:*?\\\"<>|\\r\\n]+\\\\)*)?[^\\/\\s\\(:*?\\\"<>|\\r\\n]*)\\((\\d+)\\):\\s.*(fatal|error|warning|hint)\\s(.*):\\s(.*)",
                    "file": 1, 
                    "line": 4,
                    "severity": 5,
                    "code": 6,
                    "message": 7
                }
            }                
        },
        {
            "taskName": "test",
            "isTestCommand": true,
            "problemMatcher": {
                "owner": "external",
                "fileLocation": ["relative", "${workspaceRoot}"],                        
                "pattern": {
                    "regexp": "((([A-Za-z]):\\\\(?:[^\\/:*?\\\"<>|\\r\\n]+\\\\)*)?[^\\/\\s\\(:*?\\\"<>|\\r\\n]*)\\((\\d+)\\):\\s.*(fatal|error|warning|hint)\\s(.*):\\s(.*)",
                    "file": 1, 
                    "line": 4,
                    "severity": 5,
                    "code": 6,
                    "message": 7
                }
            }               
        }                         
    ]
}   

Known issues

  • The Delphi compiler provides file name and line number but it doesn’t provide the column number where a message belongs to. VSCode has to guess the correct column. This can cause the error marks to appear in the wrong columns within the correct lines.

  • File names are reported with paths relative to the .dproj file when the file is part of the project. When a file is not part of the project but inside the search path the Delphi compiler uses the absolute path for that file. VSCode’s problem matcher currently can’t handle both types of file names at the same time. You need to decide via the fileLocation key which kind of file names you want to support. You can’t open messages with relative file names when you set "fileLocation": ["absolute"] and vice versa.

Configuration for a Lazarus project using lazbuild

Compile.bat:

@echo off

SET LAZBUILD=C:\development\lazarus\lazbuild.exe

if /i %1%==test (
  SET PROJECT=TestProject\UnitTests.lpi
) else (
  SET PROJECT=MainProject\AwesomeExe.lpi
) 

%LAZBUILD% %PROJECT% --verbose 

if %ERRORLEVEL% NEQ 0 GOTO END

echo. 
if /i %1%==test (
  TestProject\UnitTests.exe --format=plain -a
)
:END

tasks.json:

{
    "version": "0.1.0",
    "windows": {
        "command": "${workspaceRoot}/Compile.bat"
    },
    "isShellCommand": true,
    "showOutput": "always",
    "tasks": [
        {
            "taskName": "build",
            "isBuildCommand": true,
            "severity": "info",
            "problemMatcher": {
                "owner": "external",
                "fileLocation": ["absolute"],                        
                "pattern": {
                    "regexp": "(([A-Za-z]):\\\\(?:[^\\/:*?\"<>|\\r\\n]+\\\\)*[^\\/\\s\\(:*?\"<>|\\r\\n]*)\\((\\d+),(\\d+)\\)\\s.*(Fatal|Error|Warning|Hint|Note):\\s\\((\\d+)\\)\\s(.*)$",
                    "file": 1, 
                    "line": 3,
                    "column": 4,
                    "severity": 5,
                    "code": 6,
                    "message": 7
                }
            }                
        },
        {
            "taskName": "test",
            "isTestCommand": true,
            "severity": "info",
            "problemMatcher": {
                "owner": "external",
                "fileLocation": ["absolute"],                        
                "pattern": {
                    "regexp": "(([A-Za-z]):\\\\(?:[^\\/:*?\"<>|\\r\\n]+\\\\)*[^\\/\\s\\(:*?\"<>|\\r\\n]*)\\((\\d+),(\\d+)\\)\\s.*(Fatal|Error|Warning|Hint|Note):\\s\\((\\d+)\\)\\s(.*)$",
                    "file": 1, 
                    "line": 3,
                    "column": 4,
                    "severity": 5,
                    "code": 6,
                    "message": 7
                }
            }               
        }                         
    ]
}   

When it’s done

When everything works fine then you can

  • run the build command by pressing CTRL+SHIFT+B
  • run the test command by pressing CTRL+SHIFT+T
  • jump to the next compiler message in the current file with F8
  • jump to the previous compiler message in the current file with SHIFT+F8

Compiler messages

Learn more

11 Comments

  • Makox

    April 16, 2016

    Hi is there any way to link it with DevPascal??

    Reply
    • Wosi

      April 17, 2016

      To be honest I’ve never heard of DevPascal. Is it still alive? I’m asking because the project website http://www.bloodshed.net/devpascal.html seems to be down.
      You can attach any compiler via command line. If DevPascal comes with its own compiler then you can attach it. If DevPascal defines its own Pascal dialect then OmniPascal won’t be able to deal with it correctly.

      Reply
  • Alex

    May 5, 2016

    Hi, I’ve put the config files in the appropriate place but upon trying to compile I get the error “ERROR: package not found: MainProject\AwesomeExe.lpi” – what do I do? Many thanks

    Reply
    • Wosi

      May 6, 2016

      Hi, it’d be better if you asked your question on stackoverflow.com using the OmniPascal tag. Please provide your configuration and tasks.json in your question. It’s the better place to solve problems like yours.

      Reply
  • Julian Henao

    January 11, 2017

    I can debug with Omnipascal my proyects or no?
    I need this!

    Reply
    • Wosi

      January 11, 2017

      Unfortunately you cannot debug your projects in Visual Studio Code. The debugger is exclusively available inside the Delphi IDE.

      Reply
  • Sorien

    January 26, 2017

    i had to change regexp for delphi 2009 to

    "pattern": {
    "regexp": "([\\w]+\\.(pas|dpr|dpk))\\((\\d+)\\)\\s(Fatal|Error|Warning|Hint):\\s([^ ]+)\\s(.*)\\s\\[",
    "file": 1,
    "line": 3,
    "severity": 4,
    "code": 5,
    "message": 6
    }

    Reply
    • Wosi

      January 26, 2017

      Thanks for sharing!

      Reply
  • Mark O'Hare

    September 20, 2017

    The following is an example tasks.json for version 2.0.0 format.

    {
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    “version”: “2.0.0”,
    “tasks”: [
    {
    “taskName”: “build”,
    “type”: “shell”,
    “windows”: {
    “command”: “${workspaceRoot}/.vscode/Compile.bat”
    },
    “group”: {
    “kind”: “build”,
    “isDefault”: true
    },
    “presentation”: {
    “echo”: true,
    “reveal”: “always”,
    “focus”: false,
    “panel”: “shared”
    },
    “problemMatcher”: {
    “owner”: “external”,
    “fileLocation”: [“absolute”],
    “pattern”: {
    “regexp”: “((([A-Za-z]):\\\\(?:[^\\/:*?\\\”|\\r\\n]+\\\\)*)?[^\\/\\s\\(:*?\\\”|\\r\\n]*)\\((\\d+)\\):\\s.*(fatal|error|warning|hint)\\s(.*):\\s(.*)”,
    “file”: 1,
    “line”: 4,
    “severity”: 5,
    “code”: 6,
    “message”: 7
    }
    }
    },
    {
    “taskName”: “test”,
    “type”: “shell”,
    “windows”: {
    “command”: “${workspaceRoot}/.vscode/Compile.bat test”
    },
    “group”: {
    “kind”: “test”,
    “isDefault”: true
    },
    “presentation”: {
    “echo”: true,
    “reveal”: “always”,
    “focus”: false,
    “panel”: “shared”
    },
    “problemMatcher”: {
    “owner”: “external”,
    “fileLocation”: [“absolute”],
    “pattern”: {
    “regexp”: “((([A-Za-z]):\\\\(?:[^\\/:*?\\\”|\\r\\n]+\\\\)*)?[^\\/\\s\\(:*?\\\”|\\r\\n]*)\\((\\d+)\\):\\s.*(fatal|error|warning|hint)\\s(.*):\\s(.*)”,
    “file”: 1,
    “line”: 4,
    “severity”: 5,
    “code”: 6,
    “message”: 7
    }
    }
    }
    ]
    }

    Reply
  • Bee

    September 30, 2017

    How about on Mac? With FPC v3. Thanks.

    Reply
    • Wosi

      October 12, 2017

      It’s working fine on Mac đŸ˜‰

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *