Compiling and Linking
TADS 3 provides "separate compilation," which means that you can arrange your program's source code into several modules, each of which is compiled separately from the others, then linked together with the others into the finished program.
The compiler includes a built-in "make" facility, which automatically recompiles the source modules that you've changed since they were last compiled. This similar to the Unix "make" tool, but TADS 3's isn't programmable like the Unix version; instead, the TADS 3 version is pre-programmed with the relationships among source, object, and image files.
To build a program using TADS 3, you run the compiler command, t3make, with all of the source modules listed as arguments. Each module is compiled separately. This saves time as you develop your game, because each time you change your source code and recompile, the compiler only has to rebuild the files that have actually changed since the last build. Most people find that as they work on a game, they tend to focus their work at any given time in a small part of the game - in one or two source files. On each rebuild, only those modified source files need to be compiled; the compiler automatically re-uses the compiled version of the rest of the files, which didn't change between builds.
To take maximum advantage of separate compilation, you should break your game up into multiple source files, and keep each file fairly small. There's no need to take this to extremes; the compiler is fairly fast, and you won't see a lot of difference in compilation time between, say, a 5k file and a 10k file (but go up to 100k or 200k you'll start to see a difference). Most people find that they want to divide their games up into multiple files anyway, just to make the navigation easier in the source code; and most games have natural ways of being divided, such as into geographical areas of the game world. Just go with what fits your game and your working style; the time savings of separate compilation will probably turn out to be a free bonus side effect.
The compilation process
When you run t3make, it first compiles each source file that has been modified since it was last compiled. The result of compiling a source module is an "object file"; a source file called "mygame.t" would have a corresponding object file called "mygame.t3o". ("t3o" stands for "T3 Object.") If a source file - or any of its included header files - has been modified since the corresponding object file was created, t3make compiles the source file. If the object file is up to date with the source (and all included headers), there's no need to recompile the source file, so t3make automatically skips it.
Note that the "object" in "object file" doesn't refer to Object Oriented Programming. Rather, this is a whole separate bit of computer jargon. In this context, "object file" refers to the file generated by the compiler, representing the compiled form of a source file. This kind of file isn't human-readable; it's a bunch of binary gibberish that's only meaningful to the compiler. It's also not something you can directly set running - it's not executable code. It's an intermediate file, halfway between the source code and the finished program. An object file contains a version of the source program that has been translated into machine code, but which usually contains references to external symbols that must be resolved by the linker before the code becomes an executable program.
After compiling all of the modified source files into object files, t3make looks to see if the image file is up to date by checking to see if any object files are newer. If at least one object file is newer than the image file, the image file must be re-linked. Of course, if any source files are compiled during this run of t3make, the image will have to be re-linked. If it is necessary to re-link, t3make loads all of the object files, resolves their mutual external references, and creates an image file.
If you divide your program into several source files, you might find that your t3make command line becomes long enough that it's tedious to type every time you rebuild. This doesn't matter if you're using an integrated environment like TADS Workbench, but it does if you're using the command-line compiler directly.
To make things easier for command-line users, t3make can read the command options from a file rather than making you type them all every time you build. This is known as a project file; it's simply a text file that contains the file names and options that you'd normally type on the t3make command line.
Project file syntax
For the most part, a project file simply contains the same text you'd put on the t3make command line. There are a few special features, though.
Comments: a line starting with a pound sign ("#") is a comment. t3make ignores comment lines.
Quoted elements: if a file name contains a space or "#", enclose the file name in double quotes. If the name contains a double quote mark, stutter (double) it: for my "file".txt, write "my ""file"".txt".
Portable path notation: filenames that contain directory path elements should be expressed in Unix-style notation, with "/" characters separating path elements. This applies to the names of source files, and also to option arguments that specify files or directories, such as -I, -Fs, -Fy, etc.
See Universal Paths for more details.
This will look a little weird if you're using Windows and you're accustomed to "\" as the path separator, or you're used to ":" separators on Mac OS, but it makes the project file portable. You can give it to someone using a different operating system, and it'll work without any changes. t3make automatically converts the "/" notation to the local path notation on each platform.
Use default extensions: another portability concern is filename suffixes, or extensions - the ".t" at the end of a source file's name for example. You're better off omitting these for source files (.t) and libraries (.tl) - the compiler will automatically add them for you. There are a few platforms with unusual conventions that require different extension formats, so it's more portable to allow the compiler to supply the default. The -o file is an exception; for this you should specify the exact name you want.
Sample project file
Here's a sample project file.
# # project file for calc # # image file -o exe/calc.t3 # source files -file "calc sources/calc" -file "tok sources/tok"
Using a project file
To read options from a file, use the -f compiler option:
t3make -f calc.t3m
The options read from an options file are appended after any options on the command line, so you can mix options from a file and the command line. For example, if you wanted to use the project file above to compile the program for debugging, you could enter a command like this:
t3make -f calc.t3m -d
Default Project File
If you run t3make and do not specify any modules (in other words, your command line consists only of options), and you also don't include a -f option to specify a project file, the compiler looks for a default project file called "makefile.t3m" in the current directory. If this file is present, the compiler reads the file as though you had specified it with the -f option. This makes it very easy to build your program; if you put your build options in the file makefile.t3m, you can build simply by typing "t3make".
The normal rules for project files apply to makefile.t3m, so you can specify additional options on the command line when building with the default project file. For example, to build for debugging using the default project file, you would simply enter this:
The t3make command line
The typical t3make command line looks like this:
t3make -o mygame.t3 -Fo obj -Fy sym game1.t game2.t game3.t
The -o option tells the linker the name of the image file; this is the T3 executable file that you run using the TADS 3 interpreter. The -Fo option, if you include it, tells the compiler where to put object files; by default, they'll go in the same place as the source file, but if you want to store object files in a separate directory, which you might wish to do to keep your source directory uncluttered, you can use this option. Similarly, the -Fy option tells the compiler where to put "symbol" files, which are another kind of generated file that the compiler creates; as with objects, you might want to keep these files in a separate directory from the source to avoid clutter. (You don't otherwise need to worry about symbol files; they're purely for the compiler's use.) Finally, you must list the source files that make up your game.
The complete syntax of the t3make command is as follows:
t3make [ option ... ] ] source [ source ... ] [ -res resourceOption ... ] ]
The options are special keywords that all start with a dash ("-") that control the way the compiler works; more on these in a moment. The sources are the names of the source files and library files that go into the build. The resourceOptions let you add multi-media resources into the final image file.
Here's a list of the compiler options that you can specify:
- -a - recompile all source files, even if the corresponding object files are up to date. You can use this option to ensure that everything is rebuilt, regardless of whether t3make's automatic dependency-tracking mechanism detects changes that require recompilation; you can use this to ensure a full build when you suspect that the compiler's automatic dependency analysis is failing to detect a necessary module recompilation.
- -al - re-link the image file, even if it's not out of date. This doesn't affect source file compilation; before re-linking, t3make will recompile any source files whose object files are out of date, as usual. This option ensures that t3make rebuilds the image file whether or not any source files need to be compiled.
- -c - compile the source files, but do not link the image file.
- -cs name - set the default source file character set to name. If you don't specify this option, the compiler will use the local operating system settings; you might need to override the local default if the source file was originally created on a different type of computer.
- -d - compile for debugging; this includes symbolic information in the image file that the source-level debugger needs in order to allow you to step through your program and examine variables and other program data during execution. You normally will not want to include debugging information in a released version of a program, since it makes the image file larger and includes symbolic information that you might not want end users to be able to see.
- -errnum - display the numeric error code for each warning and error message during compilation. Internally, the compiler uses a number to identify each type of error; each error number has a corresponding text message describing the error, which is what the compiler shows by default. In some cases, it's useful to know the numeric code for an error; for example, you might want to look up an error message in documentation that indexes the errors by numeric code.
- -f file - read additional command-line options from the given project file. The options from the file are appended after the options on the command line.
- -Gstg - generate sourceTextGroup property values for objects defined in the source files being compiled. By default, this information is not generated, because it takes additional space in the .t3 file and isn't required for most programs.
- -v - verbose mode. Shows longer explanations of errors that occur during compilation. If you do not understand an error message, recompiling with verbose error messages might help.
- -we, -we- - treat warning messages as errors (-we), or not (-we-). When the compiler finds an error in the course of compiling a module, it halts the build as soon as it finishes with that module. In contrast, warning messages don't, by default, stop the build. A warning indicates a situation that's technically correct, but which contains a common pitfall; the compiler assumes that you meant exactly what you said, but generates the warning to let you know that it suspects that your true intentions differed. Warning messages don't always indicate real problems in your code, but they do often enough that's it's always worth taking a closer look. The usual method of writing a program is incremental, so you typically will compile and recompile the same module many times in the course of your work, as you try out each new addition. So, if you write some code that generates a warning, and you don't change the code to make the warning go away, you're going to see that warning over and over as you work on the program. Now, some people figure that this is okay; they looked at the warning early on, decided it wasn't really a problem, so when they see it again on each new build they know they can just ignore it. The catch if you do this is that the warnings tend to pile up after a while; if a new warning comes up later when you add some new code, you could very likely miss it in the haystack of warnings that you've become accustomed to ignoring. Many programmers therefore adhere to a discipline of insisting on a "clean" build every time - that is, with zero warning messages. If you fix all of the warnings as a matter of course, you'll easily notice every new one. The -we option is a convenient way of enforcing this discipline. When you specify the -we option, the compiler halts the build after any module that generates a warning message, just as it does for full-blown error messages, forcing you to fix the warnings before proceeding. If you want to explicitly specify the default mode, where warnings do not halt the build, use the -we- option.
- -w0, -w1, -w2 - set the warning level, which controls how cautious the compiler will be in pointing out potential problems in your source code. By default, the warning level is 1, which shows standard warnings but suppresses some of the less dire warnings. If you don't want to see any warning messages, use -w0, which suppresses all warnings; you should avoid using this because the warning messages point out potential problems in your program that you should carefully examine. Warning level 2 shows some additional "pedantic" warnings; these warnings point out things in your code that are merely questionable. You might want to compile your program at warning level 2 occasionally for extra checking, but you probably won't want to do this every time, since you might decide it's safe to ignore some of the pedantic warnings.
- -w-nnn - disable the warning message identified by the error number nnn. The compiler will not display any warnings of the given type, and will not include any such warnings in the summary error count. This option can only be used to disable warnings (and pedantic warnings); attempting to disable errors of severity greater than warning will have no effect. This option can be repeated to suppress multiple warnings. To obtain the numeric code of a warning that the compiler is generating during a compilation, use the "-errnum" option when compiling the file.
- -w+nnn - re-enable the warning message identified by the error number nnn. This cancels the effect of a previous -w-nnn option for the same error number on the same command line. By default, all warnings are enabled, so the only reason to use this option is to enable an option previously disabled on the same command line; this can be useful in some cases with shell aliases or scripts that include -w-nnn options. Note in particular that this option cannot be used to selectively enable specific "pedantic" warnings without enabling pedantic mode in general; pedantic warnings are entirely separate from ordinary warnings, and can only be enabled as a group by enabling pedantic mode, with the -w2 option. If you want some pedantic warnings but not others, you must enable pedantic mode and then use -w-nnn to disable the pedantic warnings you don't want to see.
- -I directory - add directory to the list of places to look for #include files. The order in which you specify the -I options is the same order the compiler uses to look for included files.
- -FC - create directories for generated compiler output files. If
this option is specified, the compiler will create new directories for
the paths specified in the -Fy, -Fs, and -o options, if they don't
already exist. This makes it easier to move a project to a new folder
or a new machine, since it eliminates the need to manually create the
compiler output folders. The new directories are created as needed at
the start of the build process, before any files are compiled.
Some people like to keep all compiler output files in a single subfolder, to keep the various output files from cluttering the main project folder. As long as you're careful to use this subfolder only for compiler output files, this technique also gives you an easy way to clean up the project, in that you can delete all derived files simply by removing the subfolder. -FC supports this approach by re-creating the subfolder as needed, so that it comes back automatically on the next build whenever you delete it to discard old output files.
- -Fo directory - set the object file directory. You can use this option to tell the compiler to store the object files in a different location than the directory containing the source files. By default, each object file is written to the same directory that contains its source file.
- -Fs directory - add a directory to the source file search path. This option can appear multiple times to add multiple directories to the search path. For each source file name listed on the compiler command line (or in the .t3m file), the compiler first looks for the file in the current working directory; if the file isn't found, the compiler looks for the file in each directory specified with a -Fs option. The compiler searches the directories in the order in which the -Fs options appear. The compiler does not search the -Fs directories for files specified with "absolute" paths (the meaning of "absolute path" varies by system; on Unix, this is a path starting with a slash, and on Windows it's a path starting with a drive letter and a backslash).
- -Fy directory - set the symbol file directory. You can use this option to tell the compiler to store the symbol files in a different location than the directory containing the source files. By default, each symbol file is written to the same directory that contains its source file.
- -o imageFile - set the name of the image file. By default, the compiler will give the image file the same name as the first listed source file, with the source file's ".t" suffix (if any) removed and replaced by the suffix ".t3".
- -Os stringFile - capture strings from all compiled source files into a text file called stringFile. Any existing file of the same name will be overwritten by the new string file. If this option is used, all of the strings in the program are captured to the given file, one string per line, with the enclosing quote marks removed. This option lets you pull out all of the text in a program so that you can run it through a spell checker, for example.
- -P - preprocess only. The compiler preprocesses each source file and writes the results to the standard output. No compilation or linking is performed, and no symbol, object, or image files are created. This option is useful for debugging macro definitions, because it allows you to see the fully expanded results of each macro.
- -pre - explicitly run pre-initialization. The compiler normally skips this step when building a debugging version.
- -nodef - do not include the default modules in the build. Normally, the compiler will include the default start-up module in the build if the build links an image file. If you want to use your own start-up module instead, you can specify this option to prevent t3make from including the default version in the link.
- -nopre - explicitly skip pre-initialization. The compiler normally runs pre-initialization when building a non-debugging version, but this option avoids this step.
- -nobanner - suppress the compiler version and copyright information banner. You might need to use this option when you're capturing the output from a build to a file, and then processing the file with a filter tool of some sort (for example, a tool that extracts error messages from the build output); if the banner confuses the filter tool, you can use this option to eliminate the banner display.
Source and library files
After the options, you list all of the source modules involved in the compilation. Each source module can be a regular source file (a ".t" file), or it can be a "library" file, as explained below. The compiler attempts to infer the type of each file by checking its filename suffix: if the suffix is ".tl", the compiler assumes that the file is a library, otherwise it assumes it is a source file. If you don't use the conventional filename suffixes for your source and library files, you must explicitly tell the compiler the type each file by prefixing each file with a "-source" or "-lib" specifier. For example:
t3make -o mygame.t3 -source game1.tads -lib mylib.tadslib
Immediately following the name of a library, you can list one or more "-x" options to exclude modules that the library includes. This is explained below.
The "-source" and "-lib" type specifiers aren't actually options, in terms of the order in which they appear on the command line. They look like options, in that they start with a dash, but they can't be mixed in positionally with the regular options. They can only appear in the module list portion of the command line, which follows all of the options.
The compiler treats the "-" prefix as special everywhere in the command line. This means that if the name of one of your source files actually starts with "-", you must put a "-source" specifier immediately before that filename, even if it ends with the conventional suffix, because otherwise the compiler would be confused into thinking the filename was meant as a type specifier.
Note that you can always use the "-source" and "-lib" specifiers, even when you don't need to. If you're putting your build settings into a project makefile (a .t3m file), it's a good idea to use "-source" and "-lib" specifiers consistently for all of your sources, because it makes the project file easier to read. Also, when you use one a "-source" or "-lib" specifier, you can omit the ".t" or ".tl" suffix from the filename, because the compiler will know to automatically add the appropriate suffix using the appropriate local OS conventions. So using the specifier and omitting the suffix makes your project files more portable by eliminating any dependency on OS file naming conventions.
File inclusion order
The order of files listed in the project is important when you use "modify" or "replace". The reason is that "modify" and "replace" statements can be used to further modify a previously modified object. This makes it necessary to have an explicit, well-defined order in which modifications are applied - otherwise there would be ambiguity about which was the "final" version of an object.
The rule for carrying out "modify" and "replace" operations is that they're applied in "source order" within a single source file, and they're applied in project file order across source files. "Project file order" is simply the order in which the files are listed in the project file (the .t3m file). (It's almost as though you were to append all of your source files together into one big file, appending one after the other in the project file order. Of course, the compiler doesn't actually do this - that wouldn't work with the preprocessor, which handles each file's #define symbols separately, and it wouldn't work with certain compiler features that are scoped to individual source files. But for the purposes of "modify" and "replace", the overall effect is to put all of the object definitions into a single, linear order from the first file listed in the project to the last.)
Other than "modify" and "replace" dependencies, the order of source file inclusion doesn't matter. In particular, source code in one module can freely refer to functions and objects defined in other modules, regardless of the relative order of those modules within the project file.
It's usually not too hard to determine the correct ordering for your project files. The important thing is to put files with "modify" after the files that define the things they modify.
For nearly every project, it will work well to use an ordering like this:
- The core library - the TADS 3 system library - can always come first, since it doesn't modify anything.
- The adv3 library can come next: it might modify some things in the system library or in its own files, but it doesn't depend on anything else.
- Next, put any third-party extension libraries you're using. These third-party libraries might modify the system or adv3 libraries, so they need to go after those.
- This is unlikely, but you might run into a third-party extension that depends on another third-party extension. For the sake of argument, let's say that extension B depends upon extension A - meaning that in order to use B, you have to also include A in your project. If you ever run into such a situation, you'll need to include the files in your project in dependency order: since B depends on A, you add A to the project first, then B.
- Finally, list your own source files.
The common thread here is that we order the files from most general to most specific: we start with the base system and adv3 libraries that everyone depends on, then we add some custom libraries that some people use but aren't universal, then we add the private files that you wrote just for this project.
Source Filename Uniqueness
All source files in a project must have unique "root" filenames. (The root filename is the part of the filename that doesn't include any directory path prefix.) This applies even to source files in libraries-a source file in one library can't have the same root filename as a source file in any other library, and it can't have the same root name as any source file included in the project's main source file list.
The reason that source filenames must be unique is that the compiler uses the source name to determine the name of the object (.t3o) and symbol (.t3s) files generated from the source file. Since all of these generated files for a given project are usually placed into a single output directory, if two source modules had the same filename, they'd both correspond to the same object file, so one module's object file would overwrite the other with the same name. To avoid this problem, the compiler requires that each source file's name is unique.
Following the list of source and library files, you can specify a list of multi-media resource files to "bundle" into the image file. A bundled resource is appended to the .t3 file, along with catalog information that lets the interpreter recover the file when it's needed. Bundling resource files into the .t3 file makes your game more self-contained, because the .t3 file is the only file you have to distribute - you don't have to include the resources as separate files, since their contents are stored within the .t3 file. Bundling also prevents users from casually or accidentally browsing your image and sound files, since ordinary operating system tools don't know how to dissect the .t3 file's contents.
You can specify the list of multi-media resources to bundle into your image file by placing the "-res" option after all of your source and library modules, then listing your multi-media resources. After the "-res" option, you can specify one or more resource item. Each resource item can be one of the following:
- filename - this simply adds the named file as a multi-media resource. The name of the resource is the same as the name of the file, with any path separators converted from local file system notation to URL-style notation. For example, if you're using Windows, a file named "graphics\title.jpg" is stored with the resource name "graphics/title.jpg". Note the change from the Windows backslash to the URL-style forward slash. If you're using Mac OS 9, the file name ":graphics:title.jpg" turns into the resource name "graphics/title.jpg" - again, note how the local path conventions are converted into portable URL conventions.
- filename=alias - this adds the named file as a multi-media
resource, using alias as the resource name. This lets you specify
a resource name that's different from the original filename.
alias must be given in URL notation, not local
path conventions. For example:
That bundles the local file called graphics\title.jpg, but names the resource pics/main.jpg. This overrides the default resource name (graphics/title.jpg) that would have been used if the alias hadn't been specified.
- folderName - this adds all of the files contained in the given folder/directory. Note that the syntax is exactly the same as for an ordinary file - the compiler detects that the name refers to a directory by checking with the operating system. If the "-recurse" option is in effect, then the compiler adds all files in all subdirectories of the given directory; if "-norecurse" is in effect, then the compiler only includes the files directly in dir.
- -recurse - recurse into all subdirectories of any subsequent directory items.
- -norecurse - do not recurse into subdirectories for subsequent directory items.
A "resource name" is the name by which the resource is known at run-time. This is the name you use to access the resource from HTML; for example, it's the name you'd use in an <IMG SRC=xxx> tag. You'd also use this name to access the resource from the File class.
There are two special things to note about resource bundling.
First, when you compile in Debug mode, the compiler only stores "links" to the resources, not copies of their contents. A link records the resource name and its corresponding local file name; at run time, the HTML renderer uses this information to load the resource from the local file. This means that a Debug build is not self-contained with respect to resources - it still needs to have the local files available while running.
The reasoning here is that Debug builds are purely for your own testing, so you'll only be running them within your build environment on your own machine, where all of the resource files started out. Since the resource files are all available anyway, embedding copies of in the .t3 file would just waste time and disk space. By skipping the resource-copy step, the Debug build can run a little faster. This is important while you're actively developing a game, because you'll probably run through the edit/compile/test cycle many times.
Second, the resources are added to the final image file after the pre-initialization phase has finished. This means that you can include resources that are created during your pre-initialization phase, which is handy for things like the "gameinfo.txt" file (which the standard library builds automatically during pre-initialization). This also means that resources you access during pre-initialization won't come from the image file, but this should be transparent to your program, since the resource loader automatically looks for the resource files in the file system anyway.
It's often useful to take a set of source files and group them together as though they were a unit. For example, the standard Adventure Library included with TADS consists of a number of separate source files; for the most part, you don't want to have to think about which individual files are involved - you just want to include the entire Adventure Library as a unit.
The compiler has a mechanism to simplify working with groups of files like this: you can create a separate, special kind of file that lists the regular source files that make up the group, and then include only this special listing file in your compilation. The compiler automatically reads the list of files from the special file. This special file is called a "library file" (not to be confused with the more generic usage of "library" that refers to a group of files the provide reusable source code, such as the Adventure Library - a library file is a specially formatted file that specifies a group of source files to include in a compilation).
By convention, a library filename always has the suffix ".tl" (but note that the period might be replaced with another character on some platforms, and some platforms don't use filename suffixes at all).
Library headers (#include files)
The compiler automatically adds each library's directory to the include path. If you're distributing a library, this means that you can bundle all of your library's files together into a single directory, and the user will only have to specify the path to your library's install directory once, when listing the .tl file on the compiler command line.
A library file is simply a text file that obeys a specific format. A library file is formatted with one definition per line (blank lines are ignored, as are lines starting with the comment character "#"). A definition has this format:
keyword : value
The keyword is a word specifying what kind of information the line contains, and the value is the text of the information defined. The valid keywords are:
- name: the value provides a human-readable name for the library. The compiler ignores this information, but other tools can use this to provide a friendlier name in user interface displays; for example, TADS Workbench displays this name for the library in the Project window.
- source: specifies a source file to include in the compilation.
- library: specifies a sub-library file to include in the compilation.
- resource: specifies a multimedia resource file (such as a .jpg
image file or an .mp3 sound file) to bundle into the finished the .t3
file. As with resources bundled via the command-line "-res" option,
resource files mentioned with this keyword are included in the final
.t3 file only when compiling in release mode (not debug mode).
Store the resource files you wish to include in this manner in the same folder as the library (.tl) file itself, or in any subfolder within the library folder. The name of each resource will be the same as the name of each file, relative to the library folder. For example, if your library file is C:\tads\lib\MyLib\MyLib.tl, and you include a resource file C:\tads\lib\MyLib\image.jpg, the resource name will simply be image.jpg. If you include the resource file C:\tads\lib\MyLib\resources\sound.mp3, the resource name will be resources/sound.mp3. (As always with resource files, the local Windows path separator character "\" is automatically translated to the universal URL-style "/" separator.)
The best practice for library resource files is to keep them in a subfolder of the library folder, with a name based on the library's name. For example, for MyLib, you could use a subfolder called MyLibResources or MyLibRes. This reduces the chances of a naming conflict if the game includes multiple libraries that each include their own sets of resources.
In addition, certain directives can appear as simply keywords without associated values:
- nodef: this instructs the compiler that the library includes the standard modules that the compiler would normally include by default, or replacements for those standard modules. When this directive is present in a library, the compiler omits the standard modules from the build, since the library explicitly includes its own versions. This directive has the same effect as using the "-nodef" option on the compiler command line or in a build configuration (.t3m) file.
The values for the source, library, and resource keywords are filenames, given in a portable format. You must not specify a filename extension (such as ".t") on source and library values, because the compiler adds the appropriate extension for the local platform automatically. You must use slash ("/") characters to indicate directory path separators; the compiler automatically converts the "/" characters to the appropriate local conventions. If you specify any directory paths, you must specify only relative paths that are subdirectories of the folder containing the library; you are not allowed to specify absolute paths, and you are not allowed to use ".." or other OS-specific conventions to refer to parent directories. If you follow all of these conventions, you will ensure that your library files will be portable to all operating systems, so that other people using different operating systems won't have to modify your library files for their local conventions.
When the compiler reads a library file, it converts each source, library, and resource value to local conventions. The compiler makes the following changes to each name:
- First, the compiler adds the appropriate filename suffix to source and library filenames, following appropriate local platform conventions. For most platforms, this means adding ".t" to a source file and ".tl" to a library file. (The compiler doesn't add anything for resource files, since these refer to files with specific suffixes that are significant to the HTML TADS display engine. For resource files, you should specify the file's actual suffix, which will be something like ".jpg", ".png", ".mp3", etc.)
- Second, the compiler converts any "/" characters in the name to appropriate local conventions. For example, on the Macintosh, the compiler would convert "files/test" to ":files:test".
- Third, the compiler takes the directory path that was used to find the library file itself, and applies that same directory path to the new filename. For example, if you're using a Macintosh, and the library file's full path is "Hard Disk:TADS:Library:adv3.tl", and this library contains a source line with the value "files/test", the compiler forms the full path to the source file as "Hard Disk:TADS:Library:files:test.t".
Preprocessor Symbol (Macro) Substitution
Inside a library file, you can substitute the value of a preprocessor symbol defined with the -D option. This allows you to create a library that selects different sub-sections that depend on -D settings; the standard adv3 library, for example, uses this to select the language-specific library based on the setting of the LANGUAGE symbol.
To substitute the value of a preprocessor symbol, use the format "$(NAME)", where NAME is the name of the preprocessor symbol to substitute. For example, to substitute the value of the LANGUAGE variable, put "$(LANGUAGE)" where you want the value to be substituted:
In this example, the LANGUAGE variable's value is substituted into this line of text twice. If the command line contained the option "-D LANGUAGE=en_us", then the compiler would read the line above as though it were written like this:
The compiler substitutes all "$(NAME)" sequences before interpreting each line of text from the library file.
If a dollar sign appears in a library file, but isn't part of a complete "$(NAME)" sequence, the compiler displays an error message. To use a dollar sign literally in a library file (for example, if you want to refer to a file that has a dollar sign in its name), write two dollar signs: the compiler converts each "$$" sequence to a single dollar sign.
Any symbols used in "$(NAME)" constructions must be defined with -D options, either on the command line or in the project (.t3m) file, before the library is listed in the command line or project file. The compiler displays an error message if any symbol is used in a library file but isn't defined with a -D option.
Example library file
Here's an example of a library file:
name: Calculator Library source: display source: keypad source: arith source: sci source: trig
This library has the display name "Calculator Library", and includes five source files: display.t, keypad.t, arith.t, sci.t, and trig.t. Assuming you called this library "calc.tl", you would include the library in a compilation like so:
t3make -d -lib calc.tl mygame.t
Because the library name ends in ".tl", you don't need to include a "-lib" specifier before it - the compiler infers that the file is a library from the suffix. (You always can include a specifier for any file, but you don't need to unless the file has a non-standard suffix for its type.)
Sometimes, a library includes non-essential files that you don't need in your program. For example, a library might have some extended functionality that it includes for those programs that want it, but which can be omitted by programs that don't use it. Including extraneous code is generally not harmful, but it does unnecessarily increase the size of your program's compiled image file and its run-time memory needs, so it's best to avoid adding code you're not using.
The obvious way of eliminating unneeded code would be to edit the library file itself to remove the modules you don't need. This isn't ideal, though, because it would remove those modules from other projects you're working on that might need the extra code. To solve this problem, you could simply create a copy of the library file and remove the unneeded modules from it. This creates another problem, though: if you got the library file from someone else, and they later change the library by renaming a source file or adding or removing files, you'd have to make the same changes to your copy or copies of the library.
Fortunately, there's a way to exclude files from a library without changing the library itself. When you include a library on the compiler command line, the compiler lets you list one or more files to exclude, using "-x" specifiers. A "-x" specifier must be placed on the compiler command line immediately after the library to which it applies. Each "-x" is followed by the name of a source module to exclude, using the name as it appears in the library - that is, using the portable name format, without an extension and with "/" as the path separator. Simply use the exact text of the source value as it appears in the library file.
Note that you cannot exclude an entire sub-library from a library. If a library includes a sub-library, you must exclude the files from the sub-library individually. To do so, treat the sub-library name as a path prefix, and place a "/" after the sub-library name, then add the filename as it appears in the sub-library. For example, suppose that another library, desk.tl, includes the calc.tl library as a sub-library:
# desk accessory library name: Desk Accessory Library library: pen library: pencil library: calc
Now, suppose you compile a program including the desk.tl library, but you want to exclude the trig.t module included in the calc.tl library. To do this, you'd write a "-x" option like this:
t3make desk.tl -x calc/trig
TADS 3 includes a default source module called _main.t that contains some low-level support code that most programs need. Because most programs will not have any reason to customize this module, the compiler automatically includes the module; this saves you a little work, because you don't have to add the module to your t3make command line explicitly.
However, it is possible that you'll want to use a different version of the code in _main.t, in which case you will need to remove the default version from the build. To do this, use the compiler's -nodef option. This option tells the compiler not to include any default modules in the build; only the modules you explicitly list on the command line (or in a project file) are included in the build in this case.
Default Search Paths
Include files: The compiler automatically adds the default system header directory to the end of the include file search path. In effect, the compiler adds a -I option, after all user-specified -I options, specifying the system header directory. The location of the system header directory varies by system; on DOS and Windows, this is the directory containing the compiler. Refer to your system-specific release notes for details on other systems.
Source files: The compiler automatically adds the default system library source directory to the end of the source file search path. In effect, the compiler adds a -Fs option, after all user-specified -Fs options, specifying the system library source directory. The location of this directory varies by system; on DOS and Windows, this is the compiler install directory. Refer to your system-specific release notes for details on other systems.
Library files: On some systems, the compiler searches for library (.tl) files in additional locations when it can't find a .tl file in one of the regular source file locations. The particulars vary by system, so check your system-specific release notes for details. For example, on MS-DOS, Windows, and Unix systems, the compiler looks for an environment variable called TADSLIB, and searches in each directory listed there (on DOS and Windows, multiple directories can be listed in the TADSLIB variable by separating them with semicolons; on Unix, paths are separated with colons).
The t3make utility will compile a source file if it finds any of the following conditions:
- The source file's corresponding object file does not exist.
- The source file has been modified more recently than its corresponding object file, according to the file system timestamps associated with the files.
- Any header file included with the #include directive was modified more recently than the object file, according to the system timestamps. The compiler stores a list of #include files in the object file, so it doesn't have to scan the source file to check the include files.
- Any -D, -U, or -I options, or the order of these options, is different on this compilation than it was when the object file was created. The compiler stores information on these options in the object file so that it can check the old options against the new options. Changes to these options could affect the meaning of the source code, so t3make makes the conservative assumption that it must recompile the source code when these options change.
- The -d option was specified for this compilation but not the original one, or vice versa. Turning debugging on or off necessitates recompilation.
In addition, t3make will re-link the image (.t3) file under any of these conditions:
- The image file doesn't already exist.
- Any source file requires recompilation.
- Any object file has been modified more recently than the image file, according to the file system timestamps.
- The list of source files involved in the build is different than it was the last time the image file was linked.
The dependency tracking mechanism isn't perfect, and it can be fooled under certain circumstances:
- The #include file checking doesn't search the full include path, but simply looks for each header file in the same place the compiler found the file on the original compilation. If you create a new header file with the same name and put the new file in a directory that is earlier on the include path than the directory that contains the original header file, the compiler will miss it - the compiler will just see that the original header file is still there and hasn't been modified. The source file must now be recompiled, because the new version of the header file would now be included, but the compiler won't recognize this. The reason the compiler misses this case is that, each time a header file is included, the search path varies depending on the file that included the header. The compiler doesn't store the full context of each inclusion in the object file, so it can't reproduce the include path search for each header file. Because of this limitation of the dependency-tracking mechanism, you should avoid "overriding" header files by placing multiple files with the same name in different directories on the include path (this is a confusing practice anyway, so it's just as well to avoid it).
You can force a full recompilation with the t3make -r option; you can use this option if you encounter any situations where you suspect that t3make is missing some dependency and therefore failing to notice a file that requires recompilation.
Notes for TADS 2 Users
TADS 3 doesn't support the "pre-compiled header" feature of TADS 2. The point of TADS 2's pre-compiled headers is to reduce compilation time by letting you bundle up a part of your game that's basically done - a part that you're not actively working on - and compile it in advance, so that you don't have to compile it again each time through the edit-compile-test cycle.
TADS 3 doesn't have pre-compiled headers because it has full support for separate compilation, which is much better. With separate compilation, the compiler automatically figures out which parts of your program have changed since the last build, and recompiles only the changed parts. You get the same sort of time savings as with pre-compiled headers, but with virtually no work on your part, and no risk that you'll forget to recompile a file you've changed.
To take full advantage of TADS 3's separate compilation, don't use #include to assemble your program as you did in TADS 2. In TADS 2, you had to have a central file that #included all of your other source files. With TADS 3, don't do this. Instead, just add all of the source files to your project (the ".t3m" file, if you're using the command-line compiler).