Build Architecture

From Open Watcom

Revision as of 13:57, 12 September 2007; view current revision
←Older revision | Newer revision→
Jump to: navigation, search

In an effort to clean up the build process, make it easier for projects to compile on various people's machines and allow for easier ports to other architectures, every project developed as part of Open Watcom should follow certain conventions as far as makefile layout is concerned. This page describes the conventions and requirements for these makefiles, as well as the steps needed to get projects to compile.

For those who do not desire a lecture on the preparation and maintenance of makefiles, feel free to skip straight to the Executive Summary at the end.

Every development and build machine must have the mif project (bld\build\mif) installed. That is taken care of by uncompressing the Open Watcom source archive and/or syncing up with Perforce.

Contents

makeinit

All the magic starts with makeinit. Every development machine must have a makeinit file with the following defined therein:

mif_path
must point to the directory in which the mif project has been installed
lang_root
the location of the installed Open Watcom compiler

A suitable makeinit is already provided as bat/makeinit in the source tree. This file does not need any changes under typical circumstances. Only persons who fully understand what they are changing and why should ever edit makeinit.

For each project with name X you wish to have on the build machine, X_dir must be set to the directory containing the project. That is, if you want the code generator on your machine (and who wouldn't?), it is officially named cg (see list of project names below) and so you would define cg_dir.

Alternatively, if all of your projects are in directories which correspond to their project names under a common directory, you can set dev_dir and !include cdirs.mif in your makeinit. This is the recommended setup and default for Open Watcom. You do not have to take any extra action to use it.

Alternatively, you can do the above and then redefine X_dir for any projects which are not under the dev_dir.

Project Names

Each project must be given a unique name, which should also be a valid directory name under FAT file systems (8.3 convention).

Makefiles

Each makefile should be located in the object file directory - ie. no silly cd'ing into the object directory based on crystal-balls and what not. The makefile must define the following:

host_os
os which the resulting executable code will run on
host_cpu
architecture which the resulting executable code will run on.
proj_name
the project name

Valid values for host_cpu are 386, i86, axp, ppc, mps. These should be self-explanatory. Valid values for host_os are dos, nt, os2, nov, qnx, win, osi, linux. These should be self-explanatory for the most part, with one possible exception: osi stands for OS Independent, the executables can run on multiple OSes if appropriate loader stub is provided.

The makefile must then include cproj.mif. This will define all sorts of make variables, which can then be used to build the project. A list of the most important of these variables and what they can be used for is included below.

A makefile should also include deftarg.mif, for definition of the required clean target, and defrule.mif, which includes the default build rules for C, C++ and assembly sources. A makefile is free to override these defaults as long as it follows the following conventions:

  • Tools which have macros defined for them must be referred to by the macros - these are currently (any additions or changes should be reflected here):
$(CC)
The C compiler
$(CPP)
The C++ compiler
$(LINKER)
The linker
$(LIBRARIAN)
The librarian
$(AS)
The assembler, if applicable
$(RC)
The resource compiler
$(EDIT)
Our VI editor
$(YACC)
Our version of yacc
$(RE2C)
The regular-expression to C compiler
  • When referring to other projects, a makefile should use the X_dir macro, where X is the name of the project.


Requirements To Build

A project should be able to build either a -d2 (if release_$(proj_name) != 1) or releaseable (if release_$(proj_name) == 1) executable provided the following are done:

  • the project is uptodate and $(proj_name)_dir is set correctly
  • the mif project is uptodate and make knows to look for .mif files in there
  • lang_root is set
  • all depended upon projects are uptodate and have $(proj_name)_dir set correctly
  • all depended upon projects have been built
  • any required executables from under bld/build are in the path

Note that there are no other requirements here - it is very annoying when a project requires you to define handles for tools, create directories in which it can deposit stuff, scrounge up obscure tools from who knows where or pretend to be Jim Welch in order to get a debuggable version of the executable.

There is more than one way to switch between development and release build. A DEBUG_BUILD environment variable provides global control. When set to 1, debug builds are produced, otherwise release builds are created. When building individual projects with wmake, it is also possible to give the release macro on the wmake command line (0 means debug build, 1 means release build).

Perhaps it should be noted that "releasable" build still contains debugging information, but only at the -d1 level and in a separate .sym file. In case of crashes or other highly unusual behaviour that never ever happens to us, release build should be enough to point you in the right direction but usually not sufficient to fully diagnose and fix the problem.

Now, if you wish to allow certain abberant behaviours based upon cryptic make variables, that is fine, as long as the project can build both a debuggable (ie full -d2) version as well as a release (ie no -d2, -d1 only and no memory tracker) version without these things being set. That is, if you want stupid stuff in your makeinit - fine, but don't require others to do this in order to build the project.

Any non-standard makefile variables which you do use should be prepended by the name of your project and an underscore, to prevent namespace clutter and clashes.

Tools required to build are an issue that will have to be handled on a case-by-case basis. For example, stuff to bind up DOS protected mode apps will likely be added to the standard suite of tools available, and macros made for them. Before we do this, we should standardize on one extender and use it wherever possible. Any small special purpose tools should be checked in along with the project and built as part of the build process (so that we don't have to check in zillions of binaries for all supported platforms). An important future consideration will be the ability to build on a different architecture. Please try and avoid weirdo tools that have no hope of running on an Alpha or PPC running NT or on Linux. More general tools (yacc, re2c, w32bind) that are likely to be used by several projects should be copied up into the bin directories under bld/build - bin for DOS, binp for OS/2, binl for Linux and binnt for some other OS, forget which.

The Runtime DLL Libraries

If you set $(proj_name)_rtdll = 1, the -br switch should be thrown for you automatically, as long as the target OS supports it.

Memory Trackers

The memory tracker is an useful development aid - it tracks all dynamic memory allocations and deallocations, making it easy to spot memory leaks and helping to pinpoint heap corruption or other unsociable behaviour that so rarely happens in our code.

If the memory tracker is an optional part of your project, and independant of the release mode, it is suggested that you enable it if $(proj_name)_trmem is set to 1, and disable it otherwise.

The source to the memory tracker can be found in bld/trmem.

The clean Target

Each makefile should support a clean target. This should not be part of the default target list, and should delete every generated file. Which means that after running "wmake clean", the directory should look exactly like a new installation of the project on a bare drive.  !including deftarg.mif should do for most people who do not get creative with file extensions or generated source files. If you do get creative, you may still use the default clean rule if you define the additional_cleanup macro that will identify your fancy file names and/or extensions.

Do not underestimate the importance of proper cleanup. It guarantees that every part of a project can be built from scratch, ensuring that there will be no nasty surprises when stuff breaks for people after a clean install just because you had a generated file hanging around and never discovered that it can no longer be made.

Pmake Support

Every makefile should contain a pmake line at the top. Pmake is a tool which was invented in order to make life easier with the clib project - most people are not interested in building all 40+ versions of the clib when they're working on just one version. Pmake, when run from a root directory, crawls down all subdirectories looking for files called makefile. When it finds one, it checks to see if there is a wmake comment which looks like:

#pmake: <some identifiers>

If there is such a comment, and any identifiers in the list given to pmake appear in the list after the colon, then wmake is invoked in that directory. This provides a handy way to control selective builds and cleans. Some tokens which should be used by the appropriate makefiles are:

all
is implicit in every makefile and does not need to be listed
build
indicates that wmake should be run in this directory as part of the build process
os_x
for each x in the list of the valid host_os tokens (os_nt, os_dos, etc)
cpu_x
for each x in the list of the valid host_cpu tokens (cpu_386, cpu_ppc, etc)
target_x
for each x in the list of valid host_cpu tokens (for compilers and targetted apps)
tiny, small, compact, medium, large, huge, flat, nomodel
the memory model
inline, calls
whether an app uses inline 8087 stuff or fp calls

For example, an executable which is going to run on the Alpha AXP version of NT should have a pmake line which contains, at a minimum:

#pmake: build os_nt cpu_axp

Pmake also supports the concept of priority. The priority is specified as /nnn after the #pmake but before the colon (:) like so:

#pmake/50: build os_nt cpu_ppc

Makefiles with lower priority are visited first. The default priority if not explicitly specified is 100. Pmake will visit subdirectories in depth first traversal order unless changed by the -r option or the priority value. Priotities are useful when certain subdirectories need to be built before others.

You are free to add as many mnemonic identifiers as you want, of course, but anything which you feel is an abstract classification that would apply to other projects, please add to the above list.

For an example of where this is useful, if we suddenly found out that our NT headers were bogus and everything including them needed a recompile, we could do the following on the build machine: "pmake os_nt -h clean & pmake os_nt -h".

Another very useful property of this setup is that it allows people to build libraries/binaries only for their host platform. This is especially convenient if they don't have all the necessary SDKs, Toolkits and whatnot installed and/or cannot run some or all of the platform specific tools required during builds.

Misc Conventions

To make it easy to see what projects are required to make a given project, all needed projects should be listed in a makefile comment in the main makefile of the dependant project. Hopefully, this main makefile should be called master.mif and be in the root directory, or a mif subdirectory thereof, of the project.

Also, it is suggested that the object file directory name be a combination of the host_os followed by the host_cpu, if convenient. For example, NT versions for the Alpha AXP should be genned into a ntaxp directory. If a directory structure which is different than this is used for some reason, then comments explaining exactly what is built where would be nice in the master.mif file.

Things get more interesting if cross compilers are thrown into the mix. In that case three components are required in the name: for instance a ntaxp.386 directory can hold the Alpha AXP NT compiler generating 386 code.

This is also why the macro names are somewhat counterintuitive - most people would think of the host_os and host_cpu, as target OS and CPU. However, the 'target' designation is reserved for the architecture targeted by the generated binary. In the above case of a compiler that runs on Alpha AXP NT and produces 386 code, the makefile contains:

host_os = nt

host_cpu = axp

target_cpu = 386

DLLs and Windowed Apps

Set host_os and host_cpu as normal, and then, if creating a windowed app, set sys_windowed = 1. If creating a DLL, set sys_dll = 1. Delightfully simple.

Include Paths

The inc_path macro is composed of several other variables. Projects are able to hook any of these variables by redefining them after cproj.mif is included. The current structure looks like this:

inc_path = inc_dirs | inc_dirs_$(host_os) | inc_dirs_sys

inc_dirs_sys = inc_dirs_lang | inc_dirs_sys_$(host_os)

inc_dirs_lang = $(lang_root)\h

So, a project should put any include directories it needs into inc_dirs - note that this does not include $(watcom_dir)\h which is part of the default include directory set.

If it needs to, a project can override any and all of these - for instance, the clib needs to be built with the next release header files, and so would redefine inc_dirs_lang.

Any OS-specific header files needed by the project can be set in inc_dirs_$(host_os) - again, this should not include the standard system header files, which will be defined in inc_dirs_sys_$(host_os).

Note that the build system previously used to set the INCLUDE environment variable to hold the contents of inc_dirs macro. This mechanism is now considered obsolete and should no longer used. Instead, include paths are passed directly on the command line. This also means that all include paths must be prepended with a -I switch, for example:

inc_dirs_sys_nt = -I$(lang_root)\h\nt

Executive Summary

In order to convert a project to this new structure or create a new (and conforming) project, do the following:

  • Create an object file directory for each combination of host_os/host_cpu under your project.
  • Give your project a name, for instance Foo.
  • Create a master.mif in the root of your project.
  • Put all the normal makefile gear in this master.mif.
  • Add proj_name = Foo to the top of master.mif.
  • Include the following files (in this order) cproj.mif, defrule.mif, deftarg.mif in master.mif.
  • Add inc_dirs = {list of directories, separated by spaces and each prepended with -I, which your project needs in include path - this does not include OS-specific includes (ie \lang\h\win)}
  • Add extra_c_flags = {list of c flags, not including optimization, -w4, -zq. -we and memory model info, needed to compile your application} These should be host_os/host_cpu independent.
  • Add extra_l_flags = {list of linker directives, not incuding system or debug directives} Should be host_os/host_cpu independent.
  • Use following to compile: $(cc) $(cflags) filename etc...
  • Use following to link: $(linker) $(lflags) file { list of obj files }
  • Use following to create libraries: $(librarian)
  • In each object file directory, create a makefile which looks like the following:

#pmake: build os_X cpu_Y

host_os = X

host_cpu = Y

!include ../master.mif

That's it!

Technical Notes

32-bit Windows run-time DLLs

Most of Open Watcom run-time Windows DLLs have predefined loading address. Bellow is table with address for each DLL.

0x69000000 wppdxxxx.dll (C++ compiler)

0x69400000 wccdxxxx.dll (C compiler)

0x69800000 wrc.dll (Resource compiler)

0x69900000 wr.dll (Resource library)

0x69c00000 wlink.dll (Linker)

0x6a000000 wlib.dll (Librarian)

0x6e800000 javavm.dll (Debugger DIP)

0x6e900000 all trap dlls (Debugger TRAP)

0x6eb00000 madx86.dll (Debugger MAD)

0x6ec00000 export.dll (Debugger DIP)

0x6ed00000 codeview.dll (Debugger DIP)

0x6ee00000 watcom.dll (Debugger DIP)

0x6ef00000 dwarf.dll (Debugger DIP)

0x6fa00000 wrtxxxx.dll (run-time DLL combined C, math and C++ library)

0x6fd00000 plbxxxx.dll (run-time DLL C++ library)

0x6fe00000 clbxxxx.dll (run-time DLL C library)

0x6ff00000 mtxxxx.dll (run-time DLL math library)

You shouldn't use these addresses for your own DLLs.

Build Process

We use the Open Watcom C/C++ compilers and Watcom wmake to build our tools, but at the top level we have a custom tool which oversees traversing the build tree, deciding which projects to build for what platforms, logging the results to a file, and copying the finished software into the release tree (rel2), making fully automated builds a possibility. If nothing goes wrong that is.

Builder

This wondrous tool is called builder. You can see bld/builder/builder.doc for detailed info on the tool and/or look at the source if the documentation doesn't satisfy you.

So how does builder work? Each project has a lang.ctl builder script file. If you go to a project directory and run builder, it will make only that project; if you go to bld and run builder, it will build everything under the sun. The overall build uses bat/lang.ctl which includes all of the individual project lang.ctl files that we use. Note that if you run builder, it will traverse directories upwards until it finds a lang.ctl (or it hits the root and still doesn't find anything, but then you must have surely done something wrong). Results are logged to build.log in the current project directory and the previous build.log file is copied to build.lo1. The log file contains captured console output (both stdout and stderr).

Common commands:

builder build
- build the software
builder rel2
- build the software, and copy it into the "rel2" release tree
builder clean
- erase object files, executables, etc. so you can build from scratch

Pmake

Many of the projects use the "pmake" features of builder (see builder.doc ) or standalone pmake tool. If you want to see its guts, the pmake source is in bld/pmake.

Each makefile has a comment line at the top of the file which is read by pmake. Most of our lang.ctl files will have a line similar to this:

pmake -d build -h ...

this will cause wmake to be run in every subdirectory where the makefile contains "build" on the #pmake line. See for instance the C compiler makefiles (in bld/cc) for an example.

You can also specify more parmeters to build a smaller subset of files. This is especially useful if you do not have all required tools/headers/libraries for all target platforms.

For example:

builder rel2 os_nt

will (generally) build only the NT version of the tools.

A word of warning: running a full build may take upwards of two hours on a 1GHz machine. There is a LOT to build! This is not your ol' OS kernel or a single-host, single-target C/C++ compiler.

It is generally possible to build specific binaries/libraries by going to their directory and running wmake. For instance to build the OS/2 version of wlink you can go to bld/wl/os2386 and run wmake there (note that the process won't successfully finish unless several required libraries had been built). Builder is useful for making full "release" builds while running wmake in the right spot is the thing to do during development. For building a debug version use wmake release =0 in the subproject. If you get some linker errors then do a wmake clean before trying to do the build.

Happy Building!

Personal tools