↓ Skip to main content

NMAKE and Makefiles: A Practical Guide for BIOS Firmware Builds

NMAKE and Makefiles: A Practical Guide for BIOS Firmware Builds

BIOS and UEFI firmware projects often contain large numbers of .mak files that coordinate source compilation, assembly, linking, module generation, and firmware packaging. In many legacy AMI Aptio projects, these build scripts rely on Microsoft NMAKE.

Developers who only copy existing .mak templates can quickly run into problems when a source file is not rebuilt, a dependency is resolved from an unexpected directory, or a macro expands to an incorrect path. Understanding how NMAKE evaluates dependencies and executes commands makes these problems much easier to diagnose.

This guide covers the core NMAKE model, Makefile syntax, inference rules, macros, command-line options, and practical debugging scenarios relevant to BIOS firmware engineering.

🧩 What Is Microsoft NMAKE?
#

NMAKE (Microsoft Program Maintenance Utility) is Microsoft’s command-line build tool. It reads a Makefile, evaluates dependencies, and executes the commands required to build specified targets.

A Makefile commonly contains rules for:

  • Compiling C and C++ source files.
  • Assembling low-level firmware code.
  • Linking object files and libraries.
  • Generating intermediate build artifacts.
  • Copying files into build directories.
  • Packaging firmware modules and producing FFS-related outputs.

NMAKE is frequently associated with .mak files, although the file extension itself is not mandatory. By default, NMAKE looks for a file named Makefile in the current directory.

In AMI Aptio development, .mak scripts may be generated from component metadata or included by higher-level platform build scripts. Their exact layout depends on the Aptio release and project-specific build infrastructure.

1.1 Preparing the Build Environment
#

NMAKE is distributed with Microsoft Visual Studio and the Microsoft C++ Build Tools.

The Developer Command Prompt for Visual Studio is the usual way to launch NMAKE because it configures the environment variables required by the compiler, linker, assembler, libraries, and include files.

A normal CMD window can also run NMAKE when the executable and all required tool paths and environment variables have been configured correctly. The important requirement is not the shell itself, but the availability of the complete build environment.

Check that NMAKE is available:

nmake /?

A typical command-line invocation follows this format:

NMAKE [options] [macros] [targets] [@commandfile]
Parameter Description
options Execution options beginning with / or -
macros Macro definitions supplied on the command line
targets One or more targets to build
@commandfile A response file containing additional NMAKE arguments

If no target is specified, NMAKE builds the first target defined in the Makefile.

For example:

nmake /f Firmware.mak

This loads Firmware.mak and builds its first target.

⚙️ How NMAKE Works
#

NMAKE is built around three fundamental concepts:

  1. Targets and dependencies.
  2. Commands associated with build rules.
  3. File timestamps used to determine whether a target needs rebuilding.

2.1 Targets and Dependencies
#

A target is the output that a rule describes. It is commonly a file, such as an object file, executable, library, or firmware module.

A dependency is an input required to generate or update that target.

Consider this example:

foo.exe : first.obj second.obj
    link /OUT:foo.exe first.obj second.obj

The first line declares that foo.exe depends on first.obj and second.obj.

The second line invokes the linker when NMAKE determines that the target needs to be rebuilt.

If either object file is newer than foo.exe, or if foo.exe does not exist, NMAKE executes the associated command.

2.2 Timestamp-Based Rebuild Decisions
#

For an ordinary file target, NMAKE generally rebuilds when:

  • The target does not exist.
  • One or more dependencies are newer than the target.
  • A command-line option or Makefile directive forces the target to be updated.

The basic comparison is:

Dependency timestamp > Target timestamp
                 |
                 v
          Rebuild target

If all dependencies are older than the target, the target is normally considered up to date.

This is why dependency declarations are so important. NMAKE can only make reliable timestamp decisions when the Makefile accurately describes the inputs that affect an output.

For example:

module.obj : module.c module.h build_config.h
    $(CC) $(CFLAGS) /c module.c

If module.h changes, NMAKE can detect that module.obj is out of date and compile it again.

If module.h is omitted from the dependency list, changing that header alone may not trigger a rebuild.

2.3 Timestamp Resolution and the Two-Second Pitfall
#

A common NMAKE debugging issue involves files modified in rapid succession.

Some Windows filesystems and timestamp sources have limited time resolution. For example, FAT timestamps traditionally have a two-second granularity. Two modifications close together can therefore appear to have the same timestamp.

This is not a universal two-second precision limit imposed by NMAKE itself. It is a filesystem and timestamp-resolution issue that can affect timestamp-based build decisions.

NMAKE provides the /B option to force a rebuild when target and dependency timestamps are equal.

For example:

nmake /B /f Firmware.mak

Use /B when timestamp resolution makes normal comparisons unreliable. For a broader rebuild, /A is available and forces all evaluated targets to be built.

2.4 Dependency Execution Order
#

NMAKE evaluates dependencies before executing the commands needed to update their parent targets.

Consider:

A : B
    command_for_A

B : C
    command_for_B

C : D
    command_for_C

The dependency relationship is:

A
|
B
|
C
|
D

If all files must be generated, the effective build order is:

D -> C -> B -> A

The lowest-level dependencies are handled first, allowing higher-level targets to use their updated inputs.

This is especially important in firmware builds, where an object file may depend on generated headers, libraries, or intermediate build descriptions.

🗂️ The Main Components of a Makefile
#

A typical NMAKE Makefile combines several language constructs. Understanding how they interact is more useful than memorizing individual commands.

Component Purpose Rough C-language analogy
Explicit rules Define targets, dependencies, and commands Functions
Inference rules Define reusable source-to-output transformations Default compiler behavior
Macro definitions Store reusable text and command-line settings #define
Directives Include files and control conditional parsing #include, #if
Comments Document build logic // or /* */
Inline files Generate temporary or permanent input files Generated source or configuration text

3.1 Explicit Description Blocks
#

An explicit description block defines a target and the files it depends on.

Its general form is:

target : dependents
    command_1
    command_2

For example:

Firmware.obj : Firmware.c Firmware.h
    $(CC) $(CFLAGS) /c Firmware.c

The target is Firmware.obj, and the dependencies are Firmware.c and Firmware.h.

NMAKE executes the commands when the target needs updating.

3.2 Inference Rules
#

Inference rules allow NMAKE to reuse a command sequence for files that share a common extension relationship.

Instead of writing an explicit compilation rule for every C source file, define a reusable rule:

.c.obj:
    $(CC) $(CFLAGS) /c $<

This tells NMAKE how to produce an .obj file from a corresponding .c file.

Inference rules are especially useful in large projects with hundreds or thousands of source files.

3.3 Macro Definitions
#

Macros store reusable text, such as directory paths, compiler options, library names, and source-file lists.

BUILD_DIR = Build
SOURCE_DIR = Source
CFLAGS = /nologo /c

A macro is referenced using:

$(BUILD_DIR)

For example:

Firmware.obj : $(SOURCE_DIR)\Firmware.c
    $(CC) $(CFLAGS) $(SOURCE_DIR)\Firmware.c

Macros make a build script easier to maintain because common settings can be changed in one place.

3.4 Preprocessing Directives
#

NMAKE supports directives that control how a Makefile is processed.

Common examples include:

!INCLUDE Common.mak

!IF "$(ARCH)" == "X64"
ARCH_FLAGS = /DARCH_X64
!ELSE
ARCH_FLAGS = /DARCH_IA32
!ENDIF

Frequently used directives include:

Directive Purpose
!INCLUDE Includes another Makefile
!IF Begins a conditional block
!ELSE Defines the alternative conditional branch
!ELSEIF Adds another conditional test
!ENDIF Ends a conditional block
!IFDEF Tests whether a macro is defined
!IFNDEF Tests whether a macro is undefined
!MESSAGE Prints a message during Makefile processing
!ERROR Reports an error and stops processing
!UNDEF Removes a macro definition

These directives are evaluated as NMAKE processes the Makefile. They are not ordinary shell commands.

🔄 NMAKE Processing: From Makefile to Build Output
#

It is useful to think of NMAKE processing in two broad stages: Makefile interpretation and target execution.

4.1 Stage One: Reading and Analyzing the Makefile
#

NMAKE reads the specified Makefile and processes its contents.

The work includes:

  1. Reading the main Makefile.
  2. Processing files referenced by !INCLUDE.
  3. Evaluating conditional directives.
  4. Processing macro definitions and references.
  5. Loading explicit rules and inference rules.
  6. Establishing the dependency relationships needed for the requested targets.

This stage determines the build instructions that apply to the current configuration.

Macro expansion is contextual: some macros are expanded while Makefile constructs are processed, while special filename macros are evaluated in the context of a particular target or command. It is therefore inaccurate to assume that every macro is expanded in one identical global pass.

4.2 Stage Two: Evaluating and Building Targets
#

Once the applicable rules are available, NMAKE evaluates the requested targets and their dependencies.

The main activities are:

  1. Determine whether each target exists.
  2. Inspect the dependencies associated with that target.
  3. Compare timestamps where timestamp-based rebuilding applies.
  4. Select the applicable explicit or inference rule.
  5. Execute the associated commands when rebuilding is required.
  6. Continue through the remaining dependency graph.

This model explains why a build can skip a compilation command even when the Makefile contains that command: the corresponding target may already be up to date.

🔤 NMAKE Syntax and Special Characters
#

NMAKE uses several characters to identify macros, comments, directives, commands, and special syntax.

Understanding these characters is essential when reading older BIOS .mak files.

5.1 Special Character Reference
#

Character Meaning
^ Escape character for NMAKE special characters
# Makefile comment marker
\ Path separator and line-continuation character
% Filename-part syntax in commands; not a GNU Make pattern-rule substitute
! Preprocessor directive prefix and command modifier
@ Suppresses display of an executing command
: Separates targets from dependencies
:: Defines independent multiple rules for a target
; Separates a dependency declaration from an inline command
$ Introduces macro references and special filename macros
*, ? Filename wildcards

The caret (^) can escape NMAKE special characters when a literal character is required.

For example:

DEFINE_TEXT = ^#define FEATURE_ENABLED 1

This allows the macro value to contain a literal #.

A backslash followed by a newline can continue a command or macro definition, with the exact interpretation depending on the context.

5.2 Comments
#

A number sign (#) introduces a Makefile comment. NMAKE ignores the text from the comment marker to the end of the line.

For example:

# Build configuration
BUILD_DIR = Build  # Output directory

Firmware.exe : main.obj helper.obj
    link /OUT:Firmware.exe main.obj helper.obj

There is an important distinction between ordinary Makefile lines and command lines.

A standalone comment line can appear in a command block:

Firmware.exe : main.obj
# This is a Makefile comment
    link /OUT:Firmware.exe main.obj

However, a # appearing inside an actual command is passed to the command interpreter or executable. It is not automatically treated as an NMAKE comment.

Also, a comment cannot be appended to a command placed after a semicolon on the dependency line:

Firmware.exe : main.obj ; link /OUT:Firmware.exe main.obj

Adding # comment to the end of this line is not valid NMAKE comment syntax.

5.3 Dependency Lines and Command Blocks
#

A dependency line must begin at the start of the line, without indentation.

A command line must begin with one or more spaces or tabs.

Correct:

Firmware.obj : Firmware.c Firmware.h
    $(CC) $(CFLAGS) /c Firmware.c

Incorrect:

    Firmware.obj : Firmware.c Firmware.h
        $(CC) $(CFLAGS) /c Firmware.c

The second example indents the dependency declaration, causing NMAKE to interpret it as a command line rather than a rule.

For reliable formatting, place the first command immediately after the dependency line. Blank lines are permitted between command lines, but do not insert an empty line between a rule and the commands intended to belong to it.

5.4 Command Modifiers
#

NMAKE supports three useful command modifiers.

Modifier Example Purpose
@ @echo Building firmware Executes a command without displaying the command itself
- -copy file.bin backup.bin Ignores a nonzero return code for that command
! !print $? Executes a command separately for each dependent when $** or $? is used

The @ modifier is useful when a build should show concise status messages without printing every command.

The - modifier should be used carefully. Ignoring a failed command can allow the build to continue with missing or invalid intermediate files.

The ! modifier has a more specialized purpose. When used with $** or $?, it executes the command once for each dependent file.

For example:

print : first.txt second.txt third.txt
    !print $** LPT1:

NMAKE generates a separate command for each dependent file rather than passing the complete list to one command invocation.

🧠 Inference Rules: Automating Compilation
#

Inference rules are one of the most useful NMAKE features for large firmware projects.

They define how to generate one file type from another, allowing the same compilation logic to be reused across multiple targets.

6.1 Basic Inference Rule Syntax
#

A standard inference rule looks like this:

.from_extension.to_extension:
    commands

For example:

.c.obj:
    $(CC) $(CFLAGS) /c $<

The rule tells NMAKE how to generate an .obj file from a corresponding .c file.

The special macro $< represents the dependent file with a later timestamp than the target in an inference-rule command.

6.2 How NMAKE Selects an Inference Rule
#

An inference rule can be considered when:

  • A target needs to be updated but has no explicit command block.
  • A dependent needs to be generated from another file.
  • A target is specified on the command line without an explicit description block.

NMAKE uses the extensions and available files to determine whether an inference rule applies. The .SUFFIXES list influences the order in which applicable rules are considered.

For example:

.SUFFIXES : .c .cpp .asm .obj

.c.obj:
    $(CC) $(CFLAGS) /c $<

.cpp.obj:
    $(CPP) $(CPPFLAGS) /c $<

.asm.obj:
    $(AS) $(AFLAGS) /c $<

This defines reusable transformations for C, C++, and assembly sources.

6.3 Inference Rules with Search Paths
#

NMAKE also supports source and destination paths in inference rules.

The general form is:

{from_path}.from_extension{to_path}.to_extension:
    commands

For example:

{Source\}.c{Build\}.obj:
    $(CC) $(CFLAGS) /c Source\$(*B).c /FoBuild\$(*B).obj

This form associates a source directory with a destination directory.

The paths in an inference rule must match the paths involved in the dependency. A path-aware inference rule does not automatically search every directory for a source file with the same name.

This feature is particularly useful in BIOS projects where source files and generated object files are stored in different directory trees.

🔧 NMAKE Macros: Variables and Automatic Filename References
#

Macros are central to NMAKE. They reduce repeated text and allow a single Makefile to support multiple build configurations.

7.1 Defining a Macro
#

The basic syntax is:

MACRO_NAME = value

For example:

BUILD_DIR = Build
CORE_DIR = Core
BSP_DIR = Board
CC = cl
CFLAGS = /nologo /c

A macro is referenced using:

$(MACRO_NAME)

For example:

$(BUILD_DIR)\Firmware.obj : $(CORE_DIR)\Firmware.c
    $(CC) $(CFLAGS) $(CORE_DIR)\Firmware.c /Fo$(BUILD_DIR)\Firmware.obj

Macro names are case-sensitive. Spaces or tabs around the equal sign are ignored in Makefile macro definitions.

7.2 Macro Substitution
#

NMAKE supports text substitution within macro definitions.

The syntax is:

$(MACRO_NAME:old_text=new_text)

For example:

SOURCE_LIST = module.c driver.c helper.c

OBJECT_LIST = $(SOURCE_LIST:.c=.obj)

The substitution replaces occurrences of .c with .obj in the expanded value.

This is a textual transformation, not a compiler operation. It does not prove that the generated object files exist or that the corresponding source files can be compiled.

7.3 Macro Definition Precedence
#

When the same macro is defined in multiple places, NMAKE applies a precedence order.

The general order is:

  1. Command-line macro definitions.
  2. Makefile or included Makefile definitions.
  3. Inherited environment-variable macros.
  4. Tools.ini definitions.
  5. Predefined NMAKE macros.

The /E option changes the relationship between environment variables and Makefile definitions so that inherited environment macros can override Makefile definitions.

For example:

nmake /f Firmware.mak BUILD_MODE=DEBUG

A command-line definition of BUILD_MODE takes precedence over a conflicting definition in the Makefile.

This behavior is useful for testing different firmware configurations without editing shared build scripts.

7.4 Automatic Filename Macros
#

NMAKE provides special filename macros that automatically refer to the current target and its dependencies.

These are particularly useful inside command blocks.

Macro Meaning
$@ Full name of the current target
$* Current target’s path and base name without the extension
$** All dependent files of the current target
$? Dependencies with timestamps newer than the target
$< Dependent file with a later timestamp; used in inference-rule commands

For example:

Firmware.obj : Firmware.c Firmware.h
    $(CC) $(CFLAGS) /c $< /Fo$@

Here:

  • $< refers to the newer dependent file used for the inference-style command context.
  • $@ refers to the output target, Firmware.obj.

For more complicated rules, make sure the macro is being used in a context where its meaning is defined. In particular, $< is documented for commands in inference rules; explicit description blocks should not assume it behaves like the GNU Make version.

7.5 Useful Environment and Tool Macros
#

NMAKE also provides predefined macros for recursive builds and common Microsoft tools.

Macro Purpose
MAKE Command used to invoke NMAKE
MAKEDIR Directory from which NMAKE was originally invoked
MAKEFLAGS Current NMAKE option settings
CC C compiler command, normally cl
CFLAGS C compiler options
CPP C++ compiler command, normally cl
CPPFLAGS C++ compiler options
CXX C++ compiler command
CXXFLAGS C++ compiler options
AS Macro assembler command
AFLAGS Assembler options
RC Resource compiler command
RFLAGS Resource compiler options

For example:

CC = cl
CFLAGS = /nologo /c /W3

Firmware.obj : Firmware.c
    $(CC) $(CFLAGS) Firmware.c /FoFirmware.obj

In larger firmware projects, these macros may be defined in shared build files rather than inside individual module Makefiles.

📄 Inline Files: Generating Build Inputs from a Makefile
#

NMAKE supports inline files, which allow a Makefile command to generate a text file that another tool can consume.

This is useful when a build process needs to generate a linker response file, command file, or other intermediate input without maintaining a separate static file.

A simplified example is:

GenerateList :
    copy <<$(BUILD_DIR)\FileList.txt
Firmware.obj
Driver.obj
Library.obj
<<

The text between the opening and closing delimiters becomes the content of the inline file.

The delimiter must appear at the beginning of a separate line. NMAKE can create the file when the associated command is executed.

8.1 Inline File Processing Rules
#

Inline-file content is not processed like ordinary Makefile syntax.

Important distinctions include:

  • Macro expansions and substitutions can occur in inline-file content.
  • Makefile directives are not evaluated inside the inline-file content.
  • Makefile comments are not interpreted there.
  • Spaces, tabs, and newline characters are treated as literal content.
  • Text between the delimiters is written to the generated file.

This distinction matters when working with assembly source, linker scripts, or generated configuration files.

8.2 Why # and !IF Behave Differently Inside Inline Files
#

Consider:

GenerateAsm :
    copy <<$(BUILD_DIR)\Generated.asm
; Generated assembly input
mov eax, 1
ret
<<

The text inside the inline-file section is output as file content. It is not a new sequence of NMAKE commands.

Therefore, placing an NMAKE directive such as !IF inside that region does not make NMAKE evaluate a conditional branch. It is treated as literal inline-file content.

Likewise, a # inside the inline-file region is not removed as a Makefile comment. It becomes part of the generated file.

The practical rule is simple: keep Makefile directives outside inline-file blocks, and use the comment syntax appropriate to the file being generated.

🖥️ NMAKE Command-Line Options
#

NMAKE supports several command-line options that control how targets are evaluated and how build commands are executed.

The following options are particularly useful when debugging firmware builds.

Option Function
/A Forces all evaluated targets to be built, even when they appear up to date
/B Forces a build when timestamps are equal
/C Suppresses default output, including nonfatal warnings and the copyright message
/D Displays timestamp information for evaluated targets and dependencies
/E Gives environment-variable macros precedence over Makefile definitions
/F filename Specifies the Makefile to use
/K Continues building unrelated dependencies after an error
/N Displays commands without executing ordinary build commands
/P Displays macro definitions, inference rules, targets, and suffix information
/Q Checks whether targets are up to date without running the build
/R Clears predefined suffix rules and ignores Tools.ini settings and predefined macros
/S Suppresses display of executed commands
/T Updates target timestamps without running the normal build
/U Dumps inline files when used with /N
/X filename Sends NMAKE error output to a specified file
/Y Disables batch-mode inference rules

9.1 Common Build Commands
#

Build the first target in a specified Makefile:

nmake /f Firmware.mak

Build a named target:

nmake /f Firmware.mak all

Force all evaluated targets to rebuild:

nmake /A /f Firmware.mak

Force rebuilding when timestamps are equal:

nmake /B /f Firmware.mak

Display timestamp and dependency information:

nmake /D /f Firmware.mak

Display commands without executing the normal build:

nmake /N /f Firmware.mak

Check whether the requested targets are up to date:

nmake /Q /f Firmware.mak

Continue building unrelated dependencies when one target fails:

nmake /K /f Firmware.mak

A dry-run using /N is especially useful when debugging complicated Aptio build scripts. It allows engineers to inspect the commands NMAKE intends to execute without performing the ordinary compilation and linking operations.

Preprocessing commands may still execute, so /N should not be treated as a completely side-effect-free sandbox.

🧪 Practical NMAKE Debugging Scenarios
#

Understanding NMAKE syntax is only part of the job. BIOS firmware engineers also need to understand how the build system resolves files and interprets generated commands.

The following examples illustrate common problems.

10.1 Question One: Which Source File Does NMAKE Compile?
#

Consider:

$(BSP_IDR)\$(CORE_DIR)\a.obj : \
    $(BSP_IDR)\$(CORE_DIR)\b.asm

Suppose another directory contains a newer file with the same name:

BSP\CORE\b.asm
BSP\OEM\b.asm

Which file does NMAKE use?

Answer: The dependency explicitly names:

$(BSP_IDR)\$(CORE_DIR)\b.asm

NMAKE does not automatically search every project directory for files with the same name and select the newest version.

Timestamp comparison is performed against the dependency selected by the rule. A newer file in another directory is irrelevant unless the Makefile’s dependency or search-path configuration causes that file to be selected.

This is a common source of confusion in Aptio BSP and OEM customization work.

When source override logic is required, inspect:

  • The actual expanded macro values.
  • The dependency paths in the evaluated rule.
  • Any path-aware inference rules.
  • Directory search configuration.
  • The generated Makefile, if the original rules are produced by another tool.

Do not assume that NMAKE automatically chooses the newest same-named source file.

10.2 Question Two: Can # Comments Appear Inside an Inline File?
#

Consider this simplified structure:

GenerateAsm :
    copy <<$(BUILD_DIR)\Generated.asm
; Generated source file
mov eax, 1
ret
<<

Where can # comments be used?

Location NMAKE comment recognized? Explanation
Standalone Makefile line Yes # begins a Makefile comment
Macro definition line Yes Text after # is ignored as a comment
Dependency line Yes A comment can follow a dependency declaration
Command line No The command is passed to the command interpreter
Inline-file content No Content is written into the generated file

For example:

# This is a Makefile comment

Firmware.obj : Firmware.c
    $(CC) /c Firmware.c

GenerateAsm :
    copy <<$(BUILD_DIR)\Generated.asm
; This is generated-file content
mov eax, 1
ret
<<

The first comment belongs to the Makefile. The semicolon in the generated assembly content is part of the output file.

A # placed inside the inline-file region would also be written as literal output. Whether that is valid depends on the syntax of the generated file.

10.3 Question Three: Why Does a Target Not Rebuild After a Header Changes?
#

Consider:

Firmware.obj : Firmware.c
    $(CC) $(CFLAGS) /c Firmware.c /FoFirmware.obj

Suppose Firmware.c includes:

#include "Firmware.h"

If the dependency list does not mention Firmware.h, changing the header may not cause NMAKE to rebuild the object file.

A better rule is:

Firmware.obj : Firmware.c Firmware.h
    $(CC) $(CFLAGS) /c Firmware.c /FoFirmware.obj

The dependency list should represent the files that affect the target. This is particularly important for firmware modules that depend on shared headers, generated configuration files, and platform-specific include files.

10.4 Question Four: Why Does a Build Continue After a Command Fails?
#

Consider:

Firmware.bin : Firmware.obj
    -copy Firmware.obj Backup\
    link /OUT:Firmware.bin Firmware.obj

The dash preceding copy tells NMAKE to ignore a nonzero return code from that command.

This may be intentional for optional operations, but it can also hide important build failures.

For example, if the copy operation was required to prepare an input for a later step, ignoring its failure could result in an incomplete firmware build.

Use the - modifier only when the command’s failure is genuinely nonfatal.

🏗️ NMAKE in AMI Aptio Firmware Development
#

NMAKE is particularly relevant when working with older BIOS build environments that use .mak scripts to coordinate module compilation and packaging.

A simplified Aptio-style build flow may look like this:

Component metadata
       |
       v
    CIF / SDL
       |
       v
     CIF2MAK
       |
       v
   Generated MAK
       |
       v
     NMAKE
       |
       v
 Compile source files
       |
       v
 Link module outputs
       |
       v
 Generate firmware artifacts
       |
       v
 FFS / Firmware Volume integration

The exact sequence depends on the project. Some workflows generate intermediate Makefiles from component metadata; others include shared .mak files that define common compiler and packaging rules.

11.1 Typical Responsibilities of an Aptio MAK File
#

An AMI module Makefile may define:

  • The module’s source and object files.
  • Compiler and assembler flags.
  • Include directories and library dependencies.
  • Module-specific build macros.
  • Generated intermediate Makefiles.
  • Module GUID and entry-point parameters.
  • Packaging and firmware-file generation commands.

For example, a module build may invoke a generated Makefile recursively:

EFITrainingDriverBin :
    $(MAKE) /$(MAKEFLAGS) /f $(BUILD_DIR)\EFITrainingDriver.mak all \
        NAME=EFITrainingDriver \
        OBJECTS="$(BUILD_DIR)\EFITrainingDriver.obj" \
        ENTRY_POINT=EFITrainingDriverInit \
        TYPE=BS_DRIVER

This pattern demonstrates an important NMAKE feature: a Makefile can invoke another NMAKE process to build a separate target or module.

The MAKE and MAKEFLAGS macros help preserve the invocation settings across recursive builds. Command-line macros can also be passed to the child process.

11.2 Debugging Recursive Build Problems
#

When an Aptio build invokes NMAKE multiple times, debugging only the top-level Makefile may not be sufficient.

Inspect:

  1. The top-level target and its dependencies.
  2. The generated .mak file.
  3. The expanded directory and compiler macros.
  4. The recursive NMAKE command line.
  5. The child Makefile’s target and dependency definitions.
  6. The actual compiler and linker commands.

A successful top-level invocation does not necessarily mean that every module was compiled. The top-level target may simply have delegated work to another Makefile, which then determined that its outputs were already up to date.

11.3 Distinguish Compilation from Firmware Packaging
#

A successful .obj compilation does not prove that a module is present in the final BIOS image.

In an Aptio workflow, module compilation, linking, FFS generation, and firmware-volume integration are separate build responsibilities.

When a driver is missing from the resulting image, investigate the entire chain:

  • Was the source compiled?
  • Was the module linked?
  • Was the expected firmware file generated?
  • Was its GUID and module type configured correctly?
  • Was the resulting FFS integrated into the intended firmware volume?
  • Did the final image-generation stage consume the updated output?

This distinction helps avoid spending time debugging C code when the actual problem is in a Makefile dependency or packaging rule.

🔬 NMAKE and Modern Build Systems
#

Modern software projects often use CMake, MSBuild, Ninja, or other build tools. These systems differ in syntax and build-graph implementation, but many underlying concepts remain familiar:

  • Build targets depend on input files.
  • Changes to dependencies can invalidate generated outputs.
  • Build tools select commands based on rules.
  • Reusable variables and configuration settings control compilation.
  • Recursive build processes can divide large projects into smaller components.

NMAKE remains useful when maintaining legacy Windows-based firmware projects and AMI Aptio build infrastructure.

However, NMAKE and GNU Make are not interchangeable.

12.1 NMAKE Is Not GNU Make
#

Although the two tools share concepts such as targets, dependencies, macros, and inference rules, their syntax differs.

Feature Microsoft NMAKE GNU Make
Common rule style target : dependency target: dependency
Basic macro reference $(NAME) $(NAME)
All prerequisites $** $^
Changed prerequisites $? $?
First prerequisite in pattern rule $< $<
Filename without extension $* $*
Pattern-rule syntax Extension inference rules Pattern rules such as %.o: %.c

A GNU Make rule such as:

%.o : %.c
    $(CC) -c $< -o $@

cannot simply be copied into an NMAKE Makefile and expected to work.

NMAKE uses extension-based inference rules such as:

.c.obj:
    $(CC) $(CFLAGS) /c $<

It also uses different automatic filename macros. For example, $** represents all dependents in NMAKE, whereas GNU Make uses $^ for all prerequisites.

When porting build scripts, translate the rule syntax and macro semantics rather than performing a simple text replacement.

📚 Further Reading
#

For practical AMI Aptio examples, the following guides demonstrate how .mak files participate in module compilation and firmware integration:

For exact NMAKE behavior and syntax, consult Microsoft’s reference documentation:

Related