3
\$\begingroup\$

I'm replacing my Visual Studio project files with FASTBuild makefiles because it builds in a fraction of the time. Right now FASTBuild isn't well known. The official documentation is sorely lacking, and I can't find anyone else talking about it online.

I've spent quite a bit of time programming-by-error-message to figure out how FASTBuild really works, and I've come up with a functional makefile. It still lacks some features essential to IDE development work, but otherwise it's solid.

The downside is that it's long, verbose, repetitive, ugly, and it's going to be unpleasant to roll this one test project out to the other 95 project files in my solution.

If anyone out there knows FASTBuild, I need suggestions on how to make this less redundant and generally less awful.


; FASTBUILD makefile

;_______________
; Variables to customize this library build
;_______________


.LibraryName = 'LibraryName'
.InputFiles = { 'file1.cpp' 'file2.cpp' 'etc.cpp' }
.InputFilesManaged = { 'fileM1.cpp' 'fileM2.cpp' 'etcM.cpp' }
.PathToSolutionBase = '..\..'

;From <AdditionalIncludeDirectories>
.HeaderIncludePaths = '..\Lib1;..\Lib1\Src;..\Lib2;..\Lib3;'
.Includes   = ' /I"..\Lib1"'
            + ' /I"..\Lib1\Src"'
            + ' /I"..\Lib2"'
            + ' /I"..\Lib3"'

.PCHFile = 'stdafx.cpp'
.PCHHeader = 'StdAfx.h'
.ObjPath = 'obj'
.OutputPath = '$PathToSolutionBase$\bin'
.ForceUsingCommon   = ' /FU"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Core.dll"'
                    + ' /FU"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.dll"'
                    + ' /FU"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Drawing.dll"'
                    + ' /FU"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Windows.Forms.dll"'

; Source: grep "<ProjectReference" *.vcxproj | sed -e 's/.*=.//g' | sed -e 's/..$//g'
.ForceUsing_Debug_x86   = ' /FU"$OutputPath$\Debug-win32\DLL1.dll"'
                        + ' /FU"$OutputPath$\Debug-win32\DLL2.dll"'
                        + ' /FU"$OutputPath$\Debug-win32\DLL3.dll"'
                        + ' /FU"$OutputPath$\Debug-win32\DLL4.dll"'
                        + ' /FU"$OutputPath$\Debug-win32\DLL6.dll"'
                        + ' /FU"$OutputPath$\Debug-win32\DLL7.dll"'
.ForceUsing_Release_x86 = ' /FU"$OutputPath$\Release-win32\DLL1.dll"'
                        + ' /FU"$OutputPath$\Release-win32\DLL2.dll"'
                        + ' /FU"$OutputPath$\Release-win32\DLL3.dll"'
                        + ' /FU"$OutputPath$\Release-win32\DLL4.dll"'
                        + ' /FU"$OutputPath$\Release-win32\DLL5.dll"'
                        + ' /FU"$OutputPath$\Release-win32\DLL7.dll"'
.ForceUsing_Debug_x64     = ' /FU"$OutputPath$\Debug-x64\DLL1.dll"'
                        + ' /FU"$OutputPath$\Debug-x64\DLL2.dll"'
                        + ' /FU"$OutputPath$\Debug-x64\DLL3.dll"'
                        + ' /FU"$OutputPath$\Debug-x64\DLL4.dll"'
                        + ' /FU"$OutputPath$\Debug-x64\DLL5.dll"'
                        + ' /FU"$OutputPath$\Debug-x64\DLL7.dll"'
.ForceUsing_Release_x64 = ' /FU"$OutputPath$\Release-x64\DLL1.dll"'
                        + ' /FU"$OutputPath$\Release-x64\DLL2.dll"'
                        + ' /FU"$OutputPath$\Release-x64\DLL3.dll"'
                        + ' /FU"$OutputPath$\Release-x64\DLL4.dll"'
                        + ' /FU"$OutputPath$\Release-x64\DLL5.dll"'
                        + ' /FU"$OutputPath$\Release-x64\DLL7.dll"'

;_______________
; Generic boilerplate which will eventually be an #include
;_______________


; Common defines
.VSBasePath = 'C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin'

.CompilerSettingsC = '/Z7 /nologo /W4 /WX /MP /EHa /GS /Gy /Gd /wd"4945" /fp:precise /Zc:wchar_t /Zc:forScope /errorReport:queue'
.CompilerSettingsNativeC = '/c /Gm-'
.DefinesC   = ' /D_UNICODE'
            + ' /DUNICODE'
            + ' /D_AFXDLL'
            + ' /D_WINDOWS'

// Architecture
.ArchX86 =
[
    .Compiler = '$VSBasePath$\cl.exe'
    .Librarian = '$VSBasePath$\lib.exe'
    .Linker = '$VSBasePath$\link.exe'
    .LibrarianOptions = '/OUT:%2 %1 /MACHINE:X86'
    .LinkerOptions = '/OUT:%2 %1 /MACHINE:X86'
    .ArchName = 'win32'
    .CompilerSettingsA = ''
    .DefinesA = '/DWIN32'
]
.ArchX64 =
[
    .Compiler = '$VSBasePath$\x86_amd64\cl.exe'
    .Librarian = '$VSBasePath$\x86_amd64\lib.exe'
    .Linker = '$VSBasePath$\x86_amd64\link.exe'
    .LibrarianOptions = '/OUT:%2 %1 /MACHINE:X64'
    .LinkerOptions = '/OUT:%2 %1 /MACHINE:X64'
    .ArchName = 'x64'
    .CompilerSettingsA = ''
    .DefinesA = '/DWIN64'
]

// Build type
.BuildDebug =
[
    .CompilerSettingsB = '/Od /MDd'
    .CompilerSettingsBNative = '/RTC1 /analyze-'
    .DefinesB = '/D_DEBUG'
    .BuildName = 'Debug'
]
.BuildRelease =
[
    .CompilerSettingsB = '/O2 /Ob2 /Oi /Oy /GF /MD'
    .CompilerSettingsBNative = ''
    .DefinesB = '/DNDEBUG'
    .BuildName = 'Release'
]

// Complete build target
.Target_Debug_x86 = 
[
    Using( .ArchX86 )
    Using( .BuildDebug )

    .CompilerSettingsT = '/Oy-'
    .DefinesT = '/D "_CRTDBG_MAP_ALLOC"'
    .ForceUsing = '$ForceUsingCommon$ $ForceUsing_Debug_x86$'
]
.Target_Release_x86 = 
[
    Using( .ArchX86 )
    Using( .BuildRelease )

    .CompilerSettingsT = ''
    .DefinesT = ''
    .ForceUsing = '$ForceUsingCommon$ $ForceUsing_Release_x86$'
]
.Target_Debug_x64 = 
[
    Using( .ArchX64 )
    Using( .BuildDebug )

    .CompilerSettingsT = ''
    .DefinesT = ''
    .ForceUsing = '$ForceUsingCommon$ $ForceUsing_Debug_x64$'
]
.Target_Release_x64 = 
[
    Using( .ArchX64 )
    Using( .BuildRelease )

    .CompilerSettingsT = ''
    .DefinesT = ''
    .ForceUsing = '$ForceUsingCommon$ $ForceUsing_Release_x64$'
]
.Targets = { .Target_Debug_x86, .Target_Release_x86, .Target_Debug_x64, .Target_Release_x64 }

ForEach( .Target in .Targets )
{
    Using( .Target )    // Copy all variables from Target
    .TargetName = '$BuildName$_$ArchName$'
    .FullOutputPath = '$OutputPath$\$ArchName$\$BuildName$'
    .FullObjPath = '$ObjPath$\$ArchName$\$BuildName$\'

    Unity("Lib_Unity_$TargetName$")
    {
        .UnityInputFiles = .InputFiles
        .UnityInputIsolateWritableFiles = true
        .UnityOutputPath = .FullObjPath
        .UnityOutputPattern = 'Unity-native*.cpp'
        .UnityPCH = .PCHHeader
    }

    Unity("Lib_Unity_managed_$TargetName$")
    {
        .UnityInputFiles = .InputFilesManaged
        .UnityInputIsolateWritableFiles = true
        .UnityOutputPath = .FullObjPath
        .UnityOutputPattern = 'Unity-managed*.cpp'
    }

    ObjectList("Lib_managed_$TargetName$")
    {
        .CompilerInputUnity = 'Lib_Unity_managed_$TargetName$'

        .CompilerOptions = '"%1" /c /clr /Fo"%2" $ForceUsing$ $CompilerSettingsC$ $CompilerSettingsB$ $CompilerSettingsA$ $CompilerSettingsT$ $DefinesC$ $DefinesB$ $DefinesA$ $DefinesT$ $Includes$'
        .CompilerOutputPath = .FullObjPath
    }

    Library("Lib_$TargetName$")
    {
        .CompilerInputUnity = 'Lib_Unity_$TargetName$'

        .PCHInputFile = .PCHFile
        .PCHOutputFile = "$FullObjPath$\$LibraryName$.pch"
        .LibrarianOutput = '$FullOutputPath$\$LibraryName$.lib'
        .LibrarianAdditionalInputs = 'Lib_managed_$TargetName$'

        .CommonCompileOptions = '"%1" /Fp"$PCHOutputFile$" $CompilerSettingsBNative$ $CompilerSettingsNativeC$ $CompilerSettingsC$ $CompilerSettingsB$ $CompilerSettingsA$ $CompilerSettingsT$ $DefinesC$ $DefinesB$ $DefinesA$ $DefinesT$ $Includes$'

        .CompilerOptions = '$CommonCompileOptions$ /Yu"$PCHHeader$" /Fo"%2" /Fd"$FullOutputPath$\$LibraryName$.pdb"'
        .CompilerOutputPath = .FullObjPath

        .PCHOptions = '$CommonCompileOptions$ /Yc"$PCHHeader$" /Fo"%3"'
    }
}

VCXProject("Project")
{
    .ProjectOutput = '$LibraryName$.vcxproj'
    .ProjectInputPaths = {'.'}
    .ProjectAllowedFileExtensions = { '.h' '.cpp' '.c' '.rc' '.cs' }
    .ProjectInputPathsExclude = {'obj'}
    .ProjectBasePath = '.'
    .ProjectBuildCommand = '$PathToSolutionBase$\Build\fbuild -vs Lib_^$(Configuration)_^$(Platform)'
    .ProjectRebuildCommand = '$ProjectBuildCommand$ -clean'

    .IncludeSearchPath = .HeaderIncludePaths
}

Alias( 'all' )
{
 .Targets = { 'Lib_Debug_win32', 'Lib_Release_win32',  'Lib_Debug_x64', 'Lib_Release_x64' }
}
\$\endgroup\$
5
\$\begingroup\$

I am the author of FASTBuild.

I presume the config as you have it is ok, but you're worried about copying it 95 times for your other projects and it becoming unmaintainable.

With that in mind, the best advice I can offer is to continue to factorize the common configuration patterns into structures (as you've already started doing). In particular, it looks like your Unity and Project options have lots of settings that don't need to be unique per library. If you can start with a common base, you can always override it for any special cases that come up.

Also, don't be afraid to (ab)use the include system to isolate common configurations. You can #include files at any point and treat them like macros/templates. (A more obvious mechanism for this sort of thing is planned for the future)

Without knowing your overall code base, it's a bit tricky to be more explicit. If you like, private support can be provided here: http://fastbuild.org/docs/contact.html

I hope to extend the language syntax in the future to afford more flexibility and simplify initial setup and maintenance. You are right about the weakness of loops. Right now they can't impact items outside of the scope of the loop. I hope the syntax enhancements will resolve this issue.

PS - I'm sorry about the documentation - it's very much a reference only at the moment, and providing examples and more details is high on the list of priorities!

\$\endgroup\$
  • \$\begingroup\$ Thank you for your response. Indeed I have started using #includes more extensively, and using it as a more fine-grained macro system might work well. It appears that I can't use variables in my #includes, which makes things a little tougher. At this point I wish FB had a real scripting system with loops and control, but I'm figuring out how to make do. I expect a big speedup when I get everything working. \$\endgroup\$ – Sophit Oct 16 '14 at 18:03
  • \$\begingroup\$ I hope that some macro-like extension, combined with conditionals and improved string expansion should help de-duplicate a lot of the common annoying cases. Stay tuned :) When you get it up and running though, I'd love to hear about your results. \$\endgroup\$ – Franta Fulin Oct 28 '14 at 0:58
3
\$\begingroup\$

First of all, I don't know FASTBuild but it looks a lot like a Makefile, though I'm not an expert of that either.

The only redundancy I see is duplication in these and similar definitions:

.ForceUsingCommon   = ' /FU"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Core.dll"'
                    + ' /FU"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.dll"'
                    + ' /FU"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Drawing.dll"'
                    + ' /FU"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Windows.Forms.dll"'

.ForceUsing_Debug_x86   = ' /FU"$OutputPath$\Debug-win32\DLL1.dll"'
                        + ' /FU"$OutputPath$\Debug-win32\DLL2.dll"'
                        + ' /FU"$OutputPath$\Debug-win32\DLL3.dll"'
                        + ' /FU"$OutputPath$\Debug-win32\DLL4.dll"'
                        + ' /FU"$OutputPath$\Debug-win32\DLL6.dll"'
                        + ' /FU"$OutputPath$\Debug-win32\DLL7.dll"'

It would be good to use helper variables more aggressively, for example:

.FrameworkPath = 'C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0'
.ForceUsingCommon   = ' /FU"$FrameworkPath$\System.Core.dll"'
                    + ' /FU"$FrameworkPath$\System.dll"'
                    + ' /FU"$FrameworkPath$\System.Drawing.dll"'
                    + ' /FU"$FrameworkPath$\System.Windows.Forms.dll"'

.DebugWin32Dir = 'Debug-win32'
.FU_D86 = '$OutputPath$\$DebugWin32Dir$'
.ForceUsing_Debug_x86   = ' /FU"$FU_D86$\DLL1.dll"'
                        + ' /FU"$FU_D86$\DLL2.dll"'
                        + ' /FU"$FU_D86$\DLL3.dll"'
                        + ' /FU"$FU_D86$\DLL4.dll"'
                        + ' /FU"$FU_D86$\DLL6.dll"'
                        + ' /FU"$FU_D86$\DLL7.dll"'

Of course, it would be even better to use some kind of looping mechanism, for example something like this in a Makefile:

win32_dlls:=DLL1 DLL2 DLL3 DLL4 DLL6 DLL7
ForceUsing_Debug_x86:=$(patsubst %,'/FU"$(FU_D86)\%.dll"',$(win32_dlls))

Btw, I noticed that in your various ForceUsing_* definitions you seem to number from 1 to 7, with one missing, sometimes DLL5, sometimes DLL6. Makes me wonder if it's a typo or intentional, so you might want to review them.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your feedback. I've come to believe that nobody outside of Ubisoft Montreal has ever used FASTBuild, so I appreciate your effort. You're right that I should use a FrameworkPath, good catch. I wish I could figure out how to use loops, as that would really improve things. The problem is that while I can loop and build a string, there's no way to keep the string "in scope" after leaving the loop. The numbered DLL names are just my mistake when sanitizing the makefile for this question; I don't actually have libraries named DLL?.dll. \$\endgroup\$ – Sophit Sep 18 '14 at 18:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.