Make.bat for building stuff in windows command promt

This post is going to be about building stuff from Command Prompt in Windows. I really like Visual Studio now that I’ve gotten used to it, but this wasn’t the case always. Few years ago I really wanted to make a game with C and SDL2. I had some trouble setting up SDL2 for Visual Studio so I decided to just build it by hand. I was doing the development mainly with OS X and wanted just to test the build in Windows.

I was using Makefiles in OS X and figured that I’d install Make for windows. This turned out to be a chore and after a while I decided to continue without Make. Building the project in command prompt was easy enough while the project contained just few source files, but when the project got larger, building everything by hand became a chore.

So, I decided to dust off my rusty Batch skills and created a build script. Years later, at work, I decided to just test something out quickly and remembered that I once created a Batch-script for building C stuff in Windows. And that was what led me to write a post about the script.

The script has some limitations, but should be enough for simple projects.

I don’t remember anymore if I borrowed parts of this script somewhere, but it seems simple enough for being made by me.

Environment settings

The build script requires some environment settings to work, so in this first part I will walk you through them. We set the environment stuff in a separate script so that it can be used with other projects also. This script can be executed from the build script or you could run it when opening the Command Prompt.

@echo off
:: This BATCH file can be used with cmd.exe:
:: Create a new shortcut for cmd.exe with "/k <location_of>env.bat"
echo [NOTE] Running env.bat.. Setting up some really really important stuff!
set BENV=x86_amd64
echo [NOTE] Build environment is set to %BENV%
:: Comment USE_VIRTUAL_DRIVE if you don't want to use it
set USE_VIRTUAL_DRIVE=1

if defined USE_VIRTUAL_DRIVE (
    if exist "w:" (
        echo [NOTE] w:\ already initialized! 
        cd /D w:
    ) else (
        subst w: c:\work
        cd /D w:
    )
) else (
    echo [NOTE] Virtual Drive disabled
)

:: set SDL2 to PATH
set PATH=%PATH%;C:\work\SDL2\lib\x64\;
:: SSH PATH
set PATH=%PATH%;C:\CVS\OpenSSH-Win64;

:: CVS Path. 
set PATH=%PATH%;C:\CVS\;

:: Set RSH Thingie
set CVS_RSH=ssh

:: Initialize the Visual Studio environment (to get 'cl' and 'link')
set PATH=%PATH%;C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\
call vcvarsall %BENV%

:: Initialize GIT environment. Or don't..
set PATH=%PATH%;C:\Program Files\Git\bin;C:\Program Files\Git\cmd

:: Mark the development environment as set. Should be the last thing you do
set DEV_ENV_SET=1

Don’t mind the CVS or GIT if you don’t need them. This Batch-file sets the required environment variables and Visual Studio build environment settings.

It also ‘mounts’ the work directory to drive w:. It can be disabled by commenting out the setting of USE_VIRTUAL_DRIVE. 

The most important part in this is the call vcvarsall %BENV% which sets the build environment stuff. You can get the possible architecture choices with vcvarsall /help. For me the help outputs: x86_amd64, x86_arm store, x86_amd64 10.0.10240.0, x86_arm store 10.0.10240.0,  x64 8.1, x64 store 8.1.

In the final line we set the DEV_ENV_SET variable to 1. This value will be used by the build script to determine if environment settings were set.

Build script

Now that the required environment variable are set, we can use the build script. This a bit long, so I split the script into smaller chunks. The whole script and an example sources are in Github: batch_build (https://github.com/jkelanne/build_batch).

@echo off
if "%1" == "clean" goto clean

The first thing the script does, is to check if clean was given as an argument. If so, the script jumps to the line containing the clean-label (line 64) and deletes the build-directory.

:: Get the parent directory name. This will be used for the name of the executable
for %%a in ("%~dp0\.") do set "PARENT=%%~nxa"

If we’re not cleaning, the script will get the parent directory name which will be used as the name of the output executable later.

The for-loop will get the the files in  %~dp0\., which in this case will only be the parent directory where the script resides (because of the \.). The %~nxa will take only the filename and the extension of the of the output. The a in the rightmost part can be misleading, because a is also used for the file attribute when using %~-notation. But because it’s the last character in it, it is regarded as the variable a in the for loop.

if defined DEV_ENV_SET (
    echo [NOTE] development environment already initialized!
) else (
    if exist "..\env.bat" (
        call "..\env.bat"
    ) else (    
        echo No env.bat found!
    )
)

The script will then check that the env.bat was executed and the DEV_ENV_SET is defined. If the environment variable was not defined, it tries to execute the env.bat. I usually keep the file inside the parent directory.


:: The compilition process can be splitted into smaller chunks if needed
set BDIR=build
set ODIR=%BDIR%\obj
set IDIR=%BDIR%\include
set SRCDIR=src

:: If you need different include paths, set them here
set INCLUDES=

if not exist %BDIR% mkdir %BDIR%
if not exist %ODIR% mkdir %ODIR%
if not exist %IDIR% mkdir %IDIR%

After checking that the build environment is set, we define few directories. BDIR is the directory where compiler and linker outputs will be placed. ODIR stores the object files, and the script stores the project headers in IDIRSRCDIR is the source directory. Note: the script won’t work if you store source files in subdirectories under SRCDIR. I’ve been using it for small projects mainly.

The script then creates the directories if they don’t already exist.


:: copy incude files to %IDIR%
echo Copying includes..
for /r %SRCDIR%\ %%g in (*.h) do (
    copy %%g %cd%\%IDIR% >NUL
    echo   Copied %%g  %cd%%IDIR%
)

Next part of the script (lines 30-35) will copy the headers from the source directory to IDIR. I don’t remember what the point of this was, but there most likely was one.


:: Extra compiler options
set CFLAGS=/W4

:: Extra linker options
::set LDFLAGS=C:\work\SDL2\lib\x64\SDL2.lib C:\work\SDL2_image\lib\x64\SDL2_image.lib C:\work\SDL2_ttf\lib\x64\SDL2_ttf.lib
set LDFLAGS=

Up next are the compiler and linker flags. In this example we use only the /W4 argument, which enables the warning levels 1-4.

In this example we don’t use any externals libraries so LDFLAGS-variable can be left empty. In the comment above, we have an example for SDL2 libraries. Because we set the SDL2 library path to PATH in env.bat, we could use only the library names here.


echo Compiling..
:: We're compiling and linking in two steps. We could also run the cl with '/link /out:executable.exe' argument
:: For more information about the compiler opitons, see:
:: https://docs.microsoft.com/en-us/cpp/build/reference/compiler-options-listed-by-category?view=vs-2017
for /r src\ %%g in (*.c) do (
    echo   Compiled %%g
    if "%INCLUDES%" == "" (
        cl %CFLAGS% /Fo%cd%\%ODIR%\ /nologo /c %%g /D_WINDOWS
    ) else (
        cl %CFLAGS% /Fo%cd%\%ODIR%\ /c %%g /I %INCLUDES% /D_WINDOWS
    )
)
echo Done compiling...

Now that the variables are set, we can compile the sources. The cl-compiler doesn’t like if we have an empty set of includes after the ‘/I’ argument, so we need to check if include paths are set. Note: The if clause here isn’t totally safe and will probably break if there are quotation marks inside it.

The argument /Fo sets the object output directory. /nologo argument disables the the compiler logo. Because we’re compiling and linking separately, we have to use the /c argument when compiling. /I sets the include directories for the build. /D just passes a _WINDOWS definition to the C preprocessor. The /D is there for example only. I used it to determine how to handle operating system specific stuff.

echo Linking...
echo   Link directory: %cd%\%ODIR%\
link /nologo -debug %cd%\%ODIR%\*.obj %LDFLAGS% -out:%cd%\%BDIR%\%PARENT%.exe
goto done

After compiling comes linking. We use the same /nologo argument here to suppress the linker logo. Visual Studio C compiler and linker can both be used either with – or / argument prefixes. The outcome is same either way. The debug argument creates debugging information and I’m not sure if it’s even necessary here. The debugging information is stored in the program database file (PDB) and can be used with a debugger.

The next part in the linker command defines that we’re going to link against every obj-file inside the ODIR. After input files comes the libraries if we need any (LDFLAGS). And finally we set the output file to parent directory name.


:: If clean was given as an argument
:clean
echo Cleaning!
rmdir /s build
goto done

:done
echo Build done

The bottom part of the script contains two labels: clean and done. The ‘done’ label is just a way to skip the cleanin part if we’re not running ‘clean’.

Conclusion

This post turned out longer that was planning, but I felt that I had to explain some of the stuff in these scripts in more detail. The script has served me quite well, but it has it’s limitations. Batch-syntax is quite bad in my opinion and I don’t tend to use it much these days. I could have done this with python or some other language, but Batch comes installed in every windows.

The one thing I would improve, is the source file handling. At this point, the build works only if the sources are stored directly under the SRCDIR. I don’t plan using this in complicated projects, so there’s really no point. Don’t even know if I need this script anymore.

I was going to explain how to install the Visual Studio C/C++ compilers and linker without Visual Studio IDE, but apparently that is not possible anymore.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.