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 |
---|---|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
Based on the location specified for the instance, the following variables are defined:
|
|
|
|
|
|
One additional variable provides the path from the build directory to the instance UFO, which for our example would be:
|
|
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
orgr
. - 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
orgr
. - 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 ofbuildlabel
plus the generated identifier based onbuildformat
. 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 thecmd()
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 acmd()
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 thetmp/
directory. It then processes that in place. For the most part authors do not have to consider this, and usingprocess()
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!