1. Introduction

(Last updated in August 2024)

Smith is a Python-based framework for building, testing and releasing fonts. It is based on waf. Smith orchestrates and integrates various utilities to make a standards-based open font design and production workflow much easier to manage.

Building a font involves numerous steps and various programs, which, if done by hand, would be prohibitively slow. Even working out what those steps are can take a lot of work. Smith uses a dedicated file at the root of the project (the file is python-based) to allow the user to describe how to build the font. By chaining the different build steps intelligently, smith reduces build times to seconds rather than minutes or hours, and makes build, test, fix, repeat cycles very manageable. By making these processes repeatable, including for a number of fonts at the same time, your project can be shared with others simply, or - better yet - it can be included in a CI (Continuous Integration) system. This allows for fonts (and their various source formats) to truly be libre/open source software and developed with open and collaborative methodologies.

Smith is Copyright (c) 2011-2024 SIL International (www.sil.org) and is released under the BSD license.

waf is Copyright (c) 2005-2011 Thomas Nagy.

1.1. Installation

A file called wscript needs to be created to control the build process. The convention is that this file is placed at the root of your font project source tree. This file is in fact a small python program, but the way it is run is designed to hide that as much as possible from the unsuspecting user.

Smith is really a larger toolchain with many dependencies that you install from a Docker registry which means you don’t have to install any of its fairly large number of components manually. In this manual, we assume you are using that approach as described at SIL Font Development Guide. Remember that smith is only the director and a wide range of utilities do the actual work. Installing just smith by itself (the python program) will not get you very far.

Smith is a command-line program, there is no graphical interface with menus. But to make things easier for you, there is a completion file installed to suggest all the smith subcommands (or targets), use the Tab key and it will autocomplete the remaining letters, for example: smith co, Tab and it will complete to configure. Similarly smith b, Tab and it will complete to build. (Currently this is only available to bash users).

1.2. Moving your project to smith

Besides taking inspiration from the way smith is used in public projects, like the ones available on github.com/silnrsi for example, you can use the following command to populate a brand new project with basic templates and standard folders:

smith start

Then you can start adjusting the various files to the specifics of your font project.

1.3. Running builds

The heart of the build system is the wscript file that controls the build process. This is done by the python program creating a set of component objects. It then takes these objects and allows the user to run various build commands on them.

Waf, on which smith is built, works by creating a build directory into which all the results are stored. This is by design and a useful feature as it leaves the source directories pristine and makes for easy clearing up.

Warning
Make sure that you don’t store past artifacts like .ttf files at the root of the project as various build targets will likely get confused and produce unexpected results. Use the references/ or the sources/archives/ folders instead.

The build directory is created using the command:

smith configure

This process creates the new results directory, checks that all the tools that smith needs to achieve the build as described in wscript are available, and sets up various internal environment variables. Thus, if any changes are made to the wscript that indicate what extra tools are needed, then the configure command needs to be rerun.

After configuration you can now launch a build. This is done using:

smith build

This creates development artifacts of the various components configured to be built. But it does not create any publishable releases - or packages that you can share with someone else - these need another command:

smith zip

This creates a zip of all the generated files and the corresponding documentation. Since this zip is targeted at Windows, text files have their line endings changed to CR LF. This is tagged with development version numbers.

smith tarball

This does the same work as the zip command except it uses LF Linux/macOS line endings and creates a .tar.xz, a compressed tarball. This is tagged with development version numbers.

smith release

This command does two things. First, it builds the various components, but marks the build for release. So things like font version strings no longer contain any development information in the form of git revision numbers, etc. Secondly, it builds the various release packages (zip and tarball). It also provides checksums, cryptographic signatures to allow comparison against the zip and tarball. This separate checksum file will allow to verify that what is distributed is really what has been produced.

smith graide

This creates a subdirectory called graide/ that contains one .cfg file per font. This config file will be read by Graide, the Graphite IDE to allow easier testing and development of smart font behaviours. If the font has no graphite smarts, no configuration file is created.

smith alltests

This creates font tests output by chaining all the available tests. The Tests section will go into more details.

smith clean

This removes the various files created by smith build in the build directory.

smith distclean

This removes the build directory completely, including any temporary files at the root of the project folder.

1.4. Writing your wscript

The wscript file is a Python program, but the internal environment is set up to minimise the amount of actual programming that needs to be done. There is no setup needed in the file, and object registration is automatic. It is possible to add waf specific extensions to the file, see details in the waf manual.

The basic steps needed to describe an entire build process is to create writing system component objects. These objects are font() or designspace and package(). Specific details on what information each of these objects requires is given in the corresponding sections of this document. Likewise examples are given in the sections.

2. Tutorial

In this tutorial we will examine a number of wscript files. The first section is the largest and builds up a complex font creation scenario from humble beginnings. We will use the UFO sources from the Andika-Mtihani project, and call our local copy "Example".

2.1. font1 - Simplicity

We start with a simple, single font.

1
2
font(target = 'Example-Regular.ttf',
     source = 'source/Example-Regular.ufo')

In line 1, we create a new font object that we want the system to build. We specify the target filename. This file will be created in the build tree (results or what is set in the out variable). Line 2 specifies where to find the source file. Notice that the target file is a .ttf file while the source is a .ufo file. Smith will use the necessary commands to build from one format to the other.

With this as our wscript file, we can build our font:

smith configure

This is the first step in building any project. This command tells smith to set up the build environment and search out all the programs that it may need for the various tasks. If a necessary program is missing smith will stop at that point and indicate an error. Some programs are not strictly necessary and smith can run with reduced functionality without them. It will just issue a warning. Such missing programs are listed in orange. All other programs that smith searches for and finds are listed, along with their locations, in green. So you can see exactly which program smith will use for any particular task. This is hepful especially in cases where you may have a locally installed self-compiled version: you can more easily see if smith has found the version in /usr/local/ (or other local paths) instead of the stock packaged version.

smith build

This command tells smith to go and build all the objects the wscript says to be built. In this case just the simple Example-Regular.ttf which will appear in results. Not very exciting, but a good start.

2.2. font2 - Multiple fonts

Most font packages consist of more than one font file and this project is no exception. Can we scale our project to handle more than one file?

1
2
3
for weight in ('-Regular', '-Bold'):
    font(target = 'Example' + weight + '.ttf',
        source = 'source/Example' + weight + '.ufo')

This example shows the power of integrating a description with a full programming language. wscript files are python programs, albeit very enhanced ones. So we can use any python-type constructs we might need. Usually the need is slight, and we show a typical example here.

Line 1 is the start of a loop. The lines below that are indented within the loop will be repeated for each value in the list. The first value is -Regular and the second is -Bold. Each time around the loop, the variable weight is set to the appropriate string. We will then use that variable to help set the appropriate values in the two font objects we are creating.

Each time around the loop, we create a new font object. In line 2 we create a new font object whose target font filename is dependent on the weight variable which is set to the various strings from the list at the start of the loop. So we will end up creating two fonts. One called Example-Regular.ttf as before, and one called Example-Bold.ttf. Line 3 gives the source files for each of these fonts.

It may seem easier just to expand out the loop and have two font() object commands but, as the complexity of this font() grows, we will see the value of using a loop. The advantage of adding the loop early is that we can make appropriate use of weight.

Now when we come to build this project, we will get two fonts:

smith configure
smith build

2.3. font3 - Packaged

It’s good that we can create multiple fonts, but what do we do with them then? There are two typical products that people want from a font project: a .zip file containing the fonts and corresponding files (with Windows line-endings CR+LF) and a tarball (with Linux/macOS line-endings LF). Smith can create these two products from a wscript, but it needs just a little more information to do so:

1
2
3
4
5
6
APPNAME = 'Example'
VERSION = '0.0.1'

for weight in ('-Regular', '-Bold', '-Italic', '-BoldItalic'):
    font(target = 'Example' + weight + '.ttf',
        source = 'source/Example' + weight + '.ufo')

Line 1 gives the base name of the products that will be created and line 2 gives the version of that product. Notice that the version variable is a string and does not have to be numeric. Case is important here, these are, in effect, magic variables we are setting that smith looks up.

To build this project, we do the same as before, but we can also use two extra commands:

smith configure
smith build
smith zip
smith tarball

smith zip will create Example-0.0.1-dev-(git-commit-id).zip in the releases folder inside the results folder. (The git-commit-id part, e.g. 66d16eM, will be the first 7 characters taken from the git revision id. We assume you are working from within a git repository.)

smith tarball will create Example-0.0.1-dev-(git-commit-id).tar.xz in the releases folder inside the results folder. (The git-commit-id part, e.g. 66d16eM, will be the first 7 characters taken from the git revision id. We assume you are working from within a git repository.)

Notice the -dev- suffix to indicate that these are development versions, which means they have not been tagged as a stable and tested release.

This zip, or tarball file, contains the four target fonts the build created, since we have now added Bold and Bold-Italic as extra weights in the loop.

We also added extra text files at the root of the project folder: README.txt, README.md, FONTLOG.txt, OFL-FAQ.txt. These are just text files and more documentation than font sources, but they are nice to have and will help users and other developers of your font. We will go into more detail on packaging in the dedicated section.

2.4. font4 - Internal processing

The part of the tutorial is being rewritten

2.5. font5 - Smarts and Basic Tests

The part of the tutorial is being rewritten

2.6. font6 - Metadata

The part of the tutorial is being rewritten

2.7. font7 - More Tests

The part of the tutorial is being rewritten

2.8. font8 - Designspace

This final example shows how to use the designspace() object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
APPNAME = 'Example'
VERSION = '0.0.3'

TESTDIR = 'tests'
TESTRESULTSDIR = 'tests'
DESC_SHORT = "Derivative"

# Get VERSION and BUILDLABEL from Regular UFO; must be first function call:
getufoinfo('source/masters/' + familyname + '-Regular' + '.ufo')

designspace('source/' +  process('source/Example' + weight + '.ufo'),
        target = process('${DS:FILENAME_BASE}.ttf', 
                instances = ['Example Regular']
         version = VERSION,
         woff = woff(),
         opentype = internal(),
         graphite = gdl('source/' + APPNAME + '.gdl', master = 'source/graphite/master.gdl'),
         ap = 'source/' + APPNAME + '.xml',
         script = 'latn',
         fret = fret(params = "-r")
        )

3. Font parameters and smarts

The minimum attributes a font object needs are target and source. For example, the following wscript file is about as simple as one can get:

1
2
font(target = 'results/Example-Regular.ttf',
     source = 'Example-Regular.ttf')

This short file does more than might be expected. First of all it copies the input file Example-Regular.ttf to an output file results/Example-Regular.ttf (We will use Unix style / for path separators). This copy may seem redundant, but it is necessary for the rest of the system to work, and not all source fonts are unmodified .ttf files. If there are tests .txt files in a directory called tests then these can be run against this font.

Notice that an input and an output file may not have the same name. Even if the output file ends up in results/ it still corresponds to a real input file that may or may not be in results/. So file paths must also be unique if the files are unique.

What if the source isn’t a .ttf file? Then, we can simply change the above example to:

1
2
font(target = 'results/Example-Regular.ttf',
     source = 'Example-Regular.ufo')

and the system will automatically convert the UFO source font to TrueType as it is copied into the results directory tree. Here we wouldn’t actually need the results/ prefix to the target because the filename isn’t the same as the source attribute.

The complete list of core attributes to a font are:

target

Output file for the generated font within results.

source

Basic design file used to generate the initial form of the output font.

params

Command-line parameters passed to the program that converts the source font into the target font. This program changes depending on the source file format. For UFO it is psfufo2ttf.

version

This takes a version number in the form 'x'.'y' (a floating point number) and sets the output font to have that version. It may also be a tuple of the form (x.y, "text") where the text will be appended to the version string inside the font. If the tuple form is not used, then "text" is set to the package buildversion. If this is not wanted then use (x.y, "").

sfd_master

This attribute specifies a FontForge file that should be merged with the source FontForge file when creating the target. If the sfd_master file is the same as the source, then sfdmeld is not run. (You will have to install FontForge yourself as it is no longer part of the smith toolchain default dependencies)

ap

Attachment point database associated with the source font.

ap_params

Parameters passed to the program that creates the ap database from the source font.

classes

An XML-structured file that allows glyph classes to be defined and then used by both Graphite and OpenType build processes.

no_test

If set to True will not include the font in the font tests. This can be set after the font object is created.

package

Package object to insert this font into. If not specified the global package is used.

typetuner

Specifies that typetuner should be run on the target and to use the given file as the typetuner configuration xml file.

3.1. OpenType

There are multiple ways of adding OpenType information to a font. One is to already have it in the source font. In this case, we need to indicate that we are working with an OpenType font, even if everything is internal to the font. The font builder needs to know for font testing purposes or if the font is generated from a legacy font.

1
2
3
font(target = 'results/Example-Regular.ttf',
     source = 'source/Example-Regular.ufo',
     opentype = internal())

This will generate tests pertinent to OpenType testing. See the section on font tests.

One approach sometimes used for FontForge based projects is to keep all the lookups in one font and then to share these lookups across all the fonts in a project. For this we simply specify a sfd_master attribute and the font builder will use sfdmeld to integrate the lookups in the master into each font as it is built. (You will have to install FontForge yourself as it is no longer part of the smith toolchain default dependencies)

1
2
3
font(target = 'results/Example-Regular.ttf',
     source = 'Example-Regular.sfd',
     sfd_master = 'mymaster.sfd')

Obviously, if the sfd_master attribute is the same as the source file then no merge occurs. This is an alternative way of specifying that the font is OpenType.

Another approach to adding OpenType tables to a font is to use an external tool or text file to create the lookups and then to have smith compile them into the font. Two formats for source files are supported: Microsoft’s VOLT (Visual OpenType Layout Tool) and Adobe’s Feature File.

3.1.1. VOLT

This approach uses a command-line VOLT compiler to integrate the .vtp source into the font. In addition, the .vtp source is autogenerated from a source and any other elements that go to make the final integrated source. For example, we show a maximal volt() integration to show all the components and then discuss them.

Notice that while the initial parameter to such objects as volt is required, all named parameters are optional.

1
2
3
4
5
6
font(target = 'results/Example-Regular.ttf',
     source = 'Example-Regular.ttf',
     opentype = volt('Example.vtp',
                     master = 'Example.vtp'),
     ap = 'Example.xml',
     classes = 'project_classes.xml')

We define the .vtp file to create for this font which will then be compiled into the font using volt2ttf as Example.vtp. We also declare a shared master volt project that is shared between all the fonts (well, at least this one!). In building a largely automated volt project, a program make_volt is used that can take attachment point information from an xml database Example.xml. This may be augmented with class information using project_classes.xml. These two file references are within the font rather than the volt details because they are shared with other smart font technologies particularly Graphite.

The complete list of attributes to Volt() are:

master

The volt source that is processed against the font to generate the font specific volt to be compiled into the font.

make_params

These parameters are passed to the make_volt process. The value is a string of parameters.

params

These parameters are passed to volt2ttf to modify the compiling of the volt source into OpenType tables.

no_make

If this attribute is present, make_volt isn’t run and the first parameter is assumed to be the specific .vtp for this font.

no_typetuner

The VOLT2TTF program used to compile the volt into opentype, also has the capability to emit an XML control file for typetuner. By default, if the font requests typetuner be run, the volt2ttf options will be set to generate this file. Setting this attribute stops this generation from happening and you will need to create the file some other way.

3.1.2. FEA

The Adobe Font Development Kit for OpenType (AFDKO) has defined a textual syntax for OpenType tables, called a feature file. smith handles .fea files by merging font-specific classes (built from the AP and classes files) with a provided master fea file, and the resulting font-specific fea file is then compiled into the font.

1
2
3
4
5
6
font(target = 'results/Example-Regular.ttf',
     source = 'Example-Regular.ttf',
     opentype = fea('Example.fea',
                    master = 'Example.fea'),
     ap = 'Example.xml',
     classes = 'project_classes.xml')

The complete list of attributes to fea() follow those of other classes:

master

The fea source that will be merged with autogenerated classes to create the font-specific .fea file.

make_params

Extra parameters to pass to makefea, the tool that is used to generate the font-specific .fea file.

no_make

If this attribute is present, then makefea isn’t run and the first parameter references a file that already exists rather than one that will be created by fea().

to_ufo

If this attribute is present and not false and also if the source file for the font ends in .ufo, the generated fea will be copied into the source .ufo as the features.fea file.

depends

A python list of additional source files on which the OpenType depends. Typically these are files mentioned via include() in the master fea file.

buildusingfontforge

If this attribute is present and not false, the FEA file will be compiled using FontForge instead of fonttools. (You will have to install FontForge yourself as it is no longer part of the smith toolchain default dependencies)

keep_feats

This boolean, used only when buildusingfontforge is true, tells FontForge to keep all the lookups associated with a given feature that are already in the font, and not wipe them when merging the feature file. For example, keeping the kern feature lookups, which are often best handled in a font design application rather than in fea files.

3.2. FEAX

Feax is a set of extensions to provide easier and more powerful ways to write fea code. It is a fea preprocessor. For the specification of the feax language see feaextensions.md.

makefea is the script to generate fea from a feax source file.

3.3. Graphite

Adding Graphite tables to a font is much like adding VOLT information. The relevant files are declared either to the font or a gdl() object. For example:

1
2
3
4
5
6
7
font(target = 'results/Example-Regular.ttf',
     source = 'Example-Regular.ttf',
     graphite = gdl('Example.gdl',
                     master = 'mymaster.gdl',
                     make_params = '-o "R C"'),
     ap = 'Example.xml',
     classes = 'project_classes.xml')

Notice that the ap and classes attributes have the same values and meaning as for OpenType tables. This is because the information is used in the creation of both sets of tables. The Example.gdl is created by the make_gdl process and it pulls in mymaster.gdl during compilation.

The complete list of attributes to a gdl() object are:

master

Non-font specific GDL that is #included into the font specific GDL.

make_params

Parameters passed to make_gdl.

params

Parameters to pass to grcompiler to control the compilation of Graphite source to Graphite tables in the font.

no_make

If this attribute is present, make_gdl is not run and the first parameter is assumed to be the gdl for the specific font.

depends

A python list of additional source files on which the GDL depends. Typically these are files mentioned via #include in the master GDL file.

3.4. Legacy Fonts

Fonts can also be built from another font, either legacy-encoded or generated from a source font or fonts. This can be achieved by giving a legacy() object as the source attribute for the font. For example, for a font generated from a legacy font using ttfbuilder we might do:

1
2
3
4
5
6
font(target = 'results/Example-Regular.ttf',
     source = legacy('myfont_src.ttf',
                     source = 'my_legacyfont.ttf',
                     xml = 'legacy_encoding.xml',
                     params = '-f ../roman_font.ttf',
                     ap = 'my_legacyfont.xml'))

The legacy object creates the source font that is then copied to the output and perhaps smarts are added too.

The complete set of attributes to a legacy() object is:

source

The legacy source font (.ttf) to use to convert to the Unicode source font.

xml

ttfbuilder configuration xml file to use for the conversion

params

Command line arguments to ttfbuilder. Note that files specified here need ../ prepended to them.

ap

Attachment point database of the legacy font that will be converted to the font.ap attribute file.

noap

Instructs the legacy converter not to create the ap database specified in the font. This would get used when another process, after legacy conversion, modifies the font and then you want the build system to autogenerate the ap database from that modified font rather than from the legacy font conversion process.

3.5. WOFF

WOFF and WOFF2 files are TTF files in special compressed formats used for webfont delivery. Smith can generate both WOFF and WOFF2 files. For example:

1
2
3
font(target = 'results/Example-Regular.ttf',
     source = 'Example-Regular.ttf',
     woff = woff('Example', metadata='metadata.xml')

The first parameter to woff() is the name of the woff file(s) to be generated. Filename extension, if present, is ignored.

The woff object takes these optional attributes:

params

This string is passed as additional command line options to the psfwoffit command.

metadata

Name of the xml file containing woff extended metadata

privdata

Name of the file containing woff private data

type

Indicates which type(s) of woff to generate; value can be 'woff' or 'woff2'. If not supplied or set to ('woff', 'woff2') then both woff and woff2 are generated.

cmd

A command string that should be used instead of psfwoffit to build woff file(s). Within the command string:

  • ${TGT} identifies the woff file to be built.

  • ${SRC[0]} identifies the TTF file to be used for input.

  • If the metadata attribute was provided, ${SRC[1]} will identify it.

  • If the privdata attribute was provided, the last item in the ${SRC} list will identify it.

By default, the font version is extracted from the input ttf and used as the version for the woff font. To override with a specific version use the params attribute:

1
2
3
font(target = 'results/Example-Regular.ttf',
     source = 'Example-Regular.ttf',
     woff = woff('Example', params='-v 3.2', metadata='metadata.xml')

To use a command other than psfwoffit to create woff files, the cmd attribute can provide the desired command and its options. For example, to use ttf2woff to create woff file:

1
2
3
4
font(target = 'results/Example-Regular.ttf',
     source = 'Example-Regular.ttf',
     woff = woff('Example', type='woff', metadata='metadata.xml',
                cmd='ttf2woff -m ${SRC[1]} -v 3.2 ${SRC[0]} ${TGT}')

3.6. Fret

Fret is a font reporting tool that generates a PDF report from a font file, giving information about all the glyphs in the font.

The fret object takes these attributes:

params

A parameter list to pass to fret. If not specified, then fret is run with the -r command line argument.

1
2
3
font(target = 'results/Example-Regular.ttf',
     source = 'Example-Regular.ttf',
     fret = fret('results/Example.pdf', params='-r -o i'))

3.7. DesignSpace

An alternative to the font object is the designspace object. A designspace specification normally defines a family of related fonts, and therefore the designspace object typically results in a number of fonts being generated — in essence the designspace object creates multiple font objects. Most of the attributes of a font object also apply to a designspace object, the differences are described below.

Instead of a source attribute, the designspace object uses a designspace file. Each instance described in the designspace file is treated as a source, and the designspace object iterates over all these instances and builds output from each.

Thus the minimum needed for the designspace object is a designspace file and target attribute:

1
2
designspace('source/Example.designspace',
    target = '${DS:FILENAME_BASE}.ttf')

Except for source and sfd_master, all other attributes of the font object can be used with the designspace object. Additionally the following attribute can be used:

instanceparams

Command line arguments to psfcreateinstances. A common usage is to supply the -W option to force weight-fixing for RIBBI font families.

instances

Sometimes it is not desirable to build all the instances in a designspace file. This attribute if not None is a list of instance names to build. If None, all instances will be built. This allows for such patterns as follows which limits a build to just one font in a set for quicker building:

1
2
3
opts = preprocess_args({'opt': '--quick'})
designspace('source/Example.designspace', # ...
    instances = ['Example Regular'] if '--quick' in opts else None)
shortcircuit

If this is set to True then if a design space instance has the same configuration parameters as a master, smith will not generate an instance, but use the master file directly. If False then a new instance is always created. Defaults to True.

Note, however, that in contrast to the simplest font object, the target attribute cannot be as simple as Example-Regular.ttf but must be an expression that yields an appropriate filename for each instance. This will also be true for some other attributes as well, for example the attachment point information specified by the ap attribute will need to be different for each instance.

To facilitate this, the designspace object provides a number of variables whose value is based on the particular instance being processed. To prevent possible name conflicts, the designspace object uses a DS: prefix for each of the variables it provides.

For a given instance, each attribute and each location introduce one or more variables. Consider the following instance definition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<instance
    familyname="Example"
    stylename="Bold"
    name="Example Bold"
    filename="instances/Example-Bold.ufo"
    >
    <location>
        <dimension name="weight" xvalue="700" />
        <dimension name="width"  xvalue="100" />
        <dimension name="custom" xvalue="0" />
    </location>
    <info />
    <kerning />
</instance>

Based on the corresponding instance attributes, the following variables will be defined:

variable string value

${DS:FAMILYNAME}

Example

${DS:STYLENAME}

Bold

${DS:NAME}

Example Bold

${DS:FILENAME}

instances/Example-Bold.ufo

Additionally, for each variable above, three additional variables are defined. Adding _DASH to the variable name results in a value where all spaces are replaced with a hyphen. Adding _NOSPC produces a value where all spaces are removed. Finally, adding _BASE provides a value which is the basename (without the extension) of the original value. For example:

${DS:NAME_DASH}

Example-Bold

${DS:NAME_NOSPC}

ExampleBold

${DS:FILENAME_BASE}

Example-Bold

Based on the location specified for the instance, the following variables are defined:

${DS:AXIS_WEIGHT}

700

${DS:AXIS_WIDTH}

100

${DS:AXIS_CUSTOM}

0

One additional variable provides the path from the build directory to the instance UFO, which for our example would be:

${DS:FILE}

source/instances/Example-Bold.ufo

4. Tests

Testing is an important part of development, particularly for fonts. Smith provides a number of testing mechanisms. The majority of this section is concerned with font testing.

4.1. File Types

There are various source file types that can be used as the basis of many tests. These are:

txt

A .txt file is considered to be simple text, one paragraph, phrase or word per line. Typically the test results display the text file using the font as simple text.

htxt

Sometimes, creating a simple text file in a complex script is hard work just because entering the characters and checking that they are right is problematic. A .htxt file is a simple text file with the added processing that any string of the form `\u`xxxx or `\U`xxxx where xxxx is a sequence of hex digits (and all such digits are used) is converted to the corresponding Unicode character.

htex

These are TeX files that contain all the information to run the test. They are converted to per font tests by adding the line \buildfont{"[fontfile]parameters"} as per a XeTeX font definition, to the start of the file. Everything else is simply \input into the file.

ftml

.xml files are treated the same as .ftml files.

Warning
Don’t put raw .tex files in the special tests/ folder since smith does not know how to handle them properly right now. You need to put them in another folder. But .odt (OpenDocument) and .indd (InDesign) are fine.

4.2. Standard commands (targets)

Test source files are stored in a standard place in the tree. The global variable TESTDIR can be used to specify where that place is, but the default is tests/. There are a number of test targets already defined in smith that can make use of these and other test information. The TESTDIR may also be a list of paths. The list of test directories can be extended with those specified in EXTRATESTDIR which may also be a string or list. These extra directories may be overridden using a ;-separated list specified in the SMITH_EXTRATESTDIR environment variable. This may, in its turn, be overridden by a ;-separated list specified in the command line option --extratestdir. If any of the directories in the list of test directories does not exist, it is quietly ignored.

Each test target, whether standard or user defined, creates a .html file in the results/tests directory (or as specified by the TESTRESULTSDIR variable). The file name is target\index.html from which links to the actual test results can be found.

4.2.1. pdfs

This target creates a pdf report for each font and its smart font technology and the script (if relevant) for each test file. XeTeX is used to render a .tex file, that is automatically created by smith for each test result, to a .pdf file.

Currently only .txt, .htxt and .htex file types are supported with this target.

It is possible to have a particular test file specify which language specialisation for the rendering of the file. If a test filename contains an underscore, the characters before the underscore are interpreted as a language tag and that language is passed to the font for rendering the text in that file only.

Text, by default, is rendered at 12pt. But this can be overridden using the TEXTSIZE global variable which is set to the size of text in points.

4.2.2. test

This test creates an html report describing the shaping (glyphs and positions) differences between the font created and a reference font found in references/ (or as specified in the STANDARDS variable). This allows a font developer to commit a known base reference version of the font to their git repository and then to see what has changed as a result of their work. In effect, this is a form of regression testing.

The standards directory is, in priority order: as specified in the test with a standards attribute, or via the command line --standards parameter, or from the STANDARDS global (or context) variable or references.

A regression report is generated for each .txt and .htxt test file, for each font, technology and script.

4.2.3. xtest

This tests creates a similar html report to that for regression testing, but it is concerned with the differences between the different smart font technologies and scripts. Thus a report is generated for each font and technology and script pair for each .txt and .htxt test file.

4.2.4. waterfall

This test does not use test files, instead it takes a single string and produces one file per font and technology and script of a single test string rendered at a number of font sizes. There are various variables that control the generated waterfalls:

TESTSTRING

This is the string to be rendered. Without it, the waterfall target does nothing.

WATERFALLSIZES

This is a python list (or tuple) of sizes to render the waterfall text at, in points. It defaults to [6, 8, 9, 10, 11, 12, 13, 14, 16, 18, 22, 24, 28, 32, 36, 42, 48, 56, 72].

TESTLINESPACINGFACTOR

This is a multiplier specifying what the interline space (space between baselines) should be based on the font size currently used. The default is 1.2.

4.2.5. xfont

This test is similar to the waterfall in that it uses a single test string, but it uses the test string to create a single report of the test string being rendered in all the fonts at a given point size. There are various variables that control the test.

TESTSTRING: This is the string to be rendered. Notice that it is the same string as for the waterfall. Lack of it means the test outputs nothing.

TESTFONTSIZE: The point size, in points, to render each font at. Defaults to 12.

4.2.6. ftml

FTML tests don’t generate any files (well perhaps some), instead all the interest is in the generated ftml_index.html file that creates complex links that run a particular xsl file against a particular ftml file giving the font and technology and script. The xsl file then generates an html report for the .ftml file given the font and that is then displayed in the browser.

In order to make the browser be able to load the various fonts and files, these and the necessary supporting files are copied into the results tests folder for this target. The test also supports .txt and .htxt test file types, converting them into .ftml as they are copied.

By default, smith does not come with any .ftml .xsl report generators already built in. Currently a wscript author has to specify where such an .xsl file may be found. They can do this using the ftmlTest() function that takes one parameter (a local path to an .xsl file) and various named parameters:

cmd

This specifies the particular test command to associate this xsl file with. The default is the default ftml target: ftml.

name

This is the name used to identify this xsl in the report. By default it is the xsl filename without the extension.

fonts

An optional list of fonts that will be passed along with the font under test to the xsl. This allows more than one font to be displayed in the same report.

addfontindex

This specifies where in the list of fonts specified in the fonts parameter, the test font should be inserted. Usually this is either 0 (the default) or len(fonts), the number of fonts in the fonts parameter list.

fontmode

This is the same as the fontmode parameter used in test creation. It can take 3 values, described later, with the following effects:

all

One link is created per font.

none

A single link is made passing all the fonts in the fonts list to the report.

collect

A single link is made passing all the fonts generated and any fonts in the fonts parameter list, to the report generator. The particular fontgroup used is called _allFonts.

shapers

This controls how many tests are produced per font. This is the same as the general shapers parameter found in tests, see that description for more details. There are 3 values this parameter may take, but ftml testing only supports 2:

0

Just produce one test per font, regardless of what smart font technologies are created.

1

Create one test per font and smart font technology and script.

4.2.7. sile

Smith can run sile (the SILE typesetter) for font testing. It processes .sil files.

sil

The file is assumed to be a fontproof based sile file. This means that sile will be called with the lua variable fontfile set to the fontfile the report is for.

4.2.8. alltests

There is one very simple test target: smith alltests. This runs all the test targets that smith can find, whether internal or user defined. If a test produces no output, it is skipped and no test_index.html file is created.

It may be that there are tests that a user wants to remove from the list of alltests. This can be achieved through listing the test commands to remove, as strings in a list under the NOALLTESTS global variable in the wscript file.

4.2.9. fbchecks

Another useful testing target is smith fbchecks. This runs all the generated fonts through the Font Bakery QA suite. It does so using the fontbakery profile in pysilfont which explicitly list certains checks, excludes others and provides new ones. Local project-specific checks can also be added in the form of a fontbakery.yaml file at the root of the project. A html report is generated with the results for each font family along with a summary at the command-line.

4.2.10. ots

This test target runs the fonts against the OTS the opentype sanitizer which is built-in various browser to reduce overflow risks. If the font does not pass the sanitizer it will be rejected by various browsers.

4.2.11. differ

smith differ allows fonts recently built to be compared against the corresponding fonts in references (or whichever folder is defined by the STANDARDS variable) using diffenator2. HTML reports are generated in results/diffenator2.

4.3. Adding test files

Sometimes you want to create test files as part of the build. This can be done using testFile(). It takes the same parameters as for a create() and it does create the file, but it also adds it to the list of source test files as if it was stored in the tests/ directory (or wherever you have specified that test files are stored).

4.4. User-defined Tests

It is possible to add your own tests to the smith test system. One can create a variant of one of the standard tests listed above, and associate it with a new target. Or one can run a separate command to execute the test. The test is specified using a testCommand() function that takes a single fixed parameter of the target the command is to be associated with and a list of named parameters. It is possible to specify more than one testCommand be associated with the same target, in which case all the testCommands will be run when that target is specified.

type

This specifies the type of the test. It may take various values:

test

A general type test with a given command. This is the default.

FTML

An ftml test that can take multiple xsl report generators.

TeX

A TeX based test

Waterfall

A Waterfull based test. It is possible to set various per test values that would otherwise come from global variables:

text

The text to output, defaults to that specified in TESTSTRING.

sizes

A list of sizes to override those in WATERFALLSIZES or the defaults.

sizefactor

Overrides the TESTLINESPACEFACTOR or its default.

CrossFont

A CrossFont based test. It is possible to set various per test values that would otherwise come from global variables:

text

The text to output, overriding the TESTSTRING value.

size

The font size, overriding the TESTFONTSIZE value or its default.

cmd

This is a string that contains the command to execute to run the test. There are various substitution values that can be used. The value is between ${ and }. The default is that the corresponding parameter passed to the test is looked up. Other more specific values are:

SRC[0]

The test source file (text or otherwise). The test is considered dependent on the test file.

SRC[1]

The first font in the list of fonts passed to the test. Usually there is only one such font. You can pass more fonts via the fonts test parameter. Referencing a font this way introduces a dependency between the test and the font such that if the font changes the test will be rerun.

SRC[2]

If usestandards is true there will be a second font that can be referenced and this is the standard base font.

TGT

The generated output filename.

shaper

The shaper used for the first font: ot or gr.

script

The script for the first font. May be the empty string if the shaper is gr.

altshaper

The shaper used for the second font when shapers=2: ot or gr.

altscript

The script used for the second font when shapers=2.

CMDNAME

This is a command name that has been looked up during smith configure and is referenced here.

shapers

This specifies how many tests will be produced per font based on the value of this parameter:

0

Produce one test per font regardless of how many shapers or scripts are specified.

1

Produce one test per font per shaper per script. Although the script is only relevant to the 'ot' shaper. [Default]

2

Produce one test per shaper script pair for a font.

fontmode

This specifies how fonts are handled in relation to the test:

all

One test (or more) is generated for each font the project creates. [Default]

none

Only one test is produced. There is no font, although you may pass fonts as a list via the fonts parameter.

collect

Only one test is produced, but all the fonts are passed to that one test.

fonts

A list of fonts to pass to the command.

ext

What is the extension of the target filename from the report. The filename is autogenerated with the given extension. The default is .html.

supports

Some test commands only support certain types of test data. The extensions supported are given in this list. Specifying .txt implies .htxt support as well (via conversion to .txt). The default is ['.txt', '.ftml', '.xml']. If you want to support ftml you should specify both .ftml and .xml.

usestandards

If True, this says that the test expects that there is a corresponding reference font for each font and that the command in some way compares the test font with the corresponding reference font to produce its results.

5. Packages

Once a set of writing system components have been created, we need to package them for distribution. Smith works to make that as simple but as powerful as appropriate. There is an optional package attribute. If this attribute is set, it is set to a package object corresponding to which package the component should be added to. In addition, a global package object is created into which go all the components for which no package attribute has been set.

The global package can take its parameters from the wscript file as global variables with the same name as the attribute, but with the name uppercased.

The attributes to package are:

appname

Base application name to use for the installer in lower case. This is required.

version

Version number of the installer. This is required.

desc_short

One line description of the package.

docdir

Directory tree to walk pulling in all the files as source files. Used for identifying documentation files that you want to include in a zip/tarball/release. It can also be used to add documentation files to supplementary fonts built from the same repository. If your wscript produces multiple packages with WOFF files, you need to use a special technique to get the WOFF files to appear in the appropriate web folders in each package. Otherwise you may end up with all the WOFFs from both font families appearing in both packages. There are two steps to setting this up:

  • Adjust the woff() command in the wscript supplemental package designspace routine to place the WOFFs in a temporary folder when built. You also need to add dontship=True so that the contents of the temporary folder don’t get duplicated.

1
2
    woff = woff('web_book/${DS:FILENAME_BASE}.woff',
    metadata=f'../source/familybook-WOFF-metadata.xml', dontship=True)
  • Set the docdir in the package definition to map the temporary folder to the normal web folder. Example:

1
2
    bookpackage = package(appname = "FamilyBook",
        docdir = {"documentation": "documentation", "web_book": "web"})

Those steps will place the appropriate WOFF fonts in the package web folders.

However if you want the CSS and HTML docs from the repo web folder to appear you need to:

  • Create a permanent web_book folder in the project root

  • Copy the CSS and HTML docs from web into web_book

  • Modify those files to use the supplemental family names

You can also use docdir to use an alternative folder in the project as the source for the “documentation” folder in the supplemental package. For example, if you want your “Book” family to instead include documentation from a separate documentation_book, you could do this:

1
2
    bookpackage = package(appname = "FamilyBook",
        docdir = {"documentation_book": "documentation", "web_book": "web"})
package_files

This is a dictionary of filename globs as keys, including the use of ** for recursion. The values are replacement paths. If the value path ends in \/ the path from the key, up to the first *, is replaced with this value.

zipfile

Name of zip file to use when creating smith zip. Is auto-generated if not set, based on appname and version.

zipdir

Directory to store generated zip file in, relative to build directory.

readme

Name of readme file to include in the package (default README.txt)

buildversion

This is often defaulted. For a release build (-r or smith release) it is set to empty. For a non-release build, the core of buildversion is based on the current VCS commit identifier. For git this is the sha. The specification of its format is in buildformat. buildversion is a combination of buildlabel plus the generated identifier based on buildformat. The buildversion is included in the generated names of the zip and tarball targets and the font version (if the font version attribute is not a tuple).

buildlabel

The development version label, for example alpha2.

buildformat

Formats the vcs commit identifier or whatever in the development buildversion. This is a str.format type string and the following named parameters are available. The default for this variable is dev-{vcssha:6}{vcsmodified}

vcssha

Unique commit identifier

vcsmodifier

Returns M if the sources we are building from are not the same as the commit.

vcstype

Specifies the VCS system being used: git, hg, svn.

buildnumber

Continuous integration build build number

5.1. Functions

5.1.1. getufoinfo

This function takes a path to a UFO font. It extracts information from the font and sets the corresponding variables in the wscript as if they had been entered directly. Thus those variables are usable elsewhere in the wscript. Important note: getufoinfo() must be the first function called in the wscript.

The variables set are:

version

Taken from the VersionMajor and VersionMinor

buildlabel

Taken from the openTypeNameVersion parsed to remove the initial Version d.ddd and any dev-ident.

6. Global Functions

The build process is about creating files from other files. Most of these processes are internal to the object, but it is possible to do some advanced configuration allowing the wscript writer to take more control over the build process. The functions described here should be considered advanced, and the beginning authors should not need to concern themselves with them initially.

cmd(cmd, [input files], **options)

The cmd() function specifies a command to be executed in the build directory and then a list of one or more dependent input files.

The first parameter to the function is the command string to execute, which is executed from the build directory. Placeholders within the string are replaced with filenames:

  • ${TGT} will be replaced by the name of the target file (which is given by the context of the cmd() function)

  • ${SRC} will be replace by the list of dependent input files

  • ${SRC[n]} will be replaced by the nth dependent input file (0 indexed)

There are various options that can be added to a cmd():

late

If set to non zero, this says that the command should be executed as late in the sequence of commands to be run on a file as possible.

targets

This is a list of extra targets that this command generates. So a single command can create more than one file.

shell

If set, says that the command should not be broken on spaces into elements to pass to an exec call, but to be passed through the shell for shell processing. Use this if you use file redirection, for example.

create()

The create() function takes an initial parameter of the filename of the file to be created. The next parameter is a command to create the specified file. Usually this is a cmd() function. For example, consider a processing path on an input font:

1
2
source = create('xyz.ufo', cmd('myfirstprocess ${SRC} ${TGT}', ['infile.ufo']),
                           cmd('mysecondprocess ${DEP} ${TGT}'))
process()

This function does an in place modification of the first parameter file that is assumed to already exist. The remaining functions are used to process the file in place. Often this is a cmd() function, but some other file type specific functions do exist. For details of them, see the relevant component type section. To reference the temporary input file referenced, use ${DEP}

1
target = process('outfile.ttf', cmd('ttfautohint ${DEP} ${TGT}'))

When process() is used on a source file, smith has to think a little harder. smith works to a strict rule that no files are created or changed in the main source tree. This means that smith cannot change a source file in its original position. For similar reasons (which file should one read?), smith does not allow there to be an identically named file with the same path in the source tree and in the build tree. So we can’t simply copy the source file into the build tree and work on it there. Instead, smith creates a copy of the source file in the buildtree by stripping its path component and storing it in the tmp/ directory. It then processes that in place. For the most part authors do not have to consider this, and using process() on a source file will 'just work'. But there are rare situations where knowledge of the underlying actions are necessary.

Parameters for this function are:

nochange

If set, tells the system that there is no need to copy the dependency file before running the task. This is an internal parameter that users are very unlikely to need to use.

test()

This function applies a process to its output file with no expected output, so any cmd() would only have a ${TGT} in the string. Of course other dependent inputs may be used. This is used for running files through checking processes that can fail, and give reports.

getversion(format)

This function is designed to help generate a version string of your choosing. It gives access to various values that can be formatted using a format string. The default format string is: dev-{vcssha:.6}{vcsmodified}. This function supports git, mercurial and svn. If the command line option --release or -r is given, then this function will return different values. The keys for which there is access are:

vcssha

A unique identifier, depending on version control system for this commit. Blank if --release.

vcsmodified

If the current code is different from that committed at this version, this value is M. Blank if --release.

buildnumber

If being auto built, this is the build number

7. Environment Variables

The following environment variables are supported by smith:

SMITHARGS

append this string to the end of each command line, e.g. -r

8. Dependencies

Smith makes use of a fair amount of dependencies. And then there are secondary dependencies for these dependencies.

There are two ways to install the smith toolchain and its many dependencies:

  • rely on freshly built binaries from the upstream git repositories and the integrated smith Docker image to do all the installation and the configuration for you

  • build the various components yourself directly from source

As you can imagine, we highly recommend the first option. We are using Ubuntu as the base layer for the entire smith toolchain. It is only cross-platform in the sense that Windows or macOS users can make use of the Docker container to use smith. It has not been natively ported to other platforms besides Ubuntu.

The installation steps are described in more details in the SIL Font Development Guide. We are using the Docker container technology. The Smith Docker image is built from the Dockerfile at the root of the smith project repository which in turn uses various files in docker/.

The various manual steps previously described in this section of the manual have been removed because they got out of date too quickly and were hard to maintain accurately. If you’d still like to do the whole process manually, then we recommend you study the steps in the Dockerfile as well as the dependency definitions in the docker/ folder of the smith project repository.

Let us know of any issues and please report bugs.

Enjoy!