Как работает система сборки Mozilla

Данный документ предназначен для разработчиков, которым необходимо работать над системой сборки Mozilla. Он разъясняет базовые концепции и термины системы сборки и то, как решать с её помощью повседневные задачи наподобие компиляции компонентов и создания jar-файлов.

Данный документ не предназначен для тех, кому надо лишь только собрать проект Mozilla. Для этой цели смотрите Инструкции по сборке.

Для большинства знание команды mach build для сборки из исходников вполне достаточно для успешной работы. Тем не менее, если вы жаждете знать больше, то сможете углубиться во все детали этого процесса.

Фазы

Когда вы набираете mach build для сборки из исходников, внутри системы сборки происходит 3 высокоуровневые фазы:

  1. Детектирование системы и валидация.
  2. Подготовка бэкэнда сборки.
  3. Вызов бэкэнда сборки.

Фаза 1: configure

Фаза 1 сосредоточена вокруг configure скрипта. Скрипт configure является bash скриптом. Он генерирутеся из файла configure.in, который написан на M4 и обрабатывается с помощью Autoconf 2.13 для создания конечного configure скрипта. Вам не нужно беспокоиться о том как получить configure файл: система сборки делает это за вас.

Основная задача configure - определить зарактеристики системы и компилятора, применить переданные опции, и подтвердить что все выглядит готовым к запуску процесса компиляции.

Наличие файла config.status может быть знакомым тем из вас, кто ранее уже работал с Autoconf. Однако, config.status в Mozilla отличается практически от любого config.status, который вы видели ранее: он написан на Python! Вместо того, чтобы сгенерировать shell скрипт, мы сгенерировали скрипт Python.

Сейчас самое лучшее время чтобы отметить тот факт, что Python превалирует в нашей системе сборки. Если нам нужно написать код для системы сборки, мы делаем это на Python. Именно так мы и поступаем.

config.status содержит 2 части: структуры данных, представляющие вывод configure и интерфейс командной строки для подготовки/настройки/генерации подходящего бэкенда сборки. (Бэкенд сборки - это просто утилита, используемая для сборки дерева исходников - типа GNU Make или Tup). Эти структуры данных в сущности описывают текущее состояние системы и то, на что похож существующая конфигурация сборки. Например, она задает какой компилятор использовать, как его вызывать, какие фичи приложения включены и т.п. Можете сами открыть config.status и посмотреть!

Как только мы сгенерировали файл config.status, мы передаём его в распоряжение фазы 2.

Фаза 2: Подготовка бэкенда сборки и описание сборки

Как только скрипт configure определил текущую конфигурацию сборки, нам нужно применить её к дереву исходников чтобы непосредственно осуществить сборку.

Суть происходящего заключается в том, что автоматически полученный скрипт config.status исполняется сразу же как только скрипт configure сгенерировал его. Задача скрипта config.status состоит в том, чтобы рассказать утилите сборки как собрать дерево исходников. Для этого config.status обязан сначала просканировать описание системы сборки.

Описание системы сборки состоит из иерархии различных файлов moz.build. Обычно один moz.build приходится на отдельный каталог или набор связанных каталогов. Каждый moz.build файл определяет, как работает та часть конфигурации сборки, за которую он отвечает. К примеру, он может говорить: Скомпилировать эти файлы C++ или Искать дополнительную информацию в этих каталогах. config.status начинает с основного moz.build файла и затем рекурсивно проходится по всем упомянутым в нём каталогам и файлам. Когда все moz.build файлы прочитаны, составляются структуры данных, задающие глобальное описание системы сборки. Затем эти структуры данных считываются генератором бэкенда сборки, который затем конвертирует их в файлы, вызовы функций и т.д. В случае make бэкенда, генератор пишет файлы Makefile.

После запуска config.status вы увидите следующий вывод:

Reticulating splines...
Finished reading 1096 moz.build files into 1276 descriptors in 2.40s
Backend executed in 2.39s
2188 total backend files. 0 created; 1 updated; 2187 unchanged
Total wall time: 5.03s; CPU time: 3.79s; Efficiency: 75%

Здесь говорится, что было прочитано 1096 moz.build файлов в сумме. Всего 1276 структур данных, описывающих конфигурацию сборки, получены из этих файлов. Одно чтение и обработка этих файлов заняли 2.4 сек. 1276 сгенерированных структур данных были переданы затем в бэкенд сборки, который затем сообщил что собирается обработать 2188 файлов, полученных из тех структур данных. Большая часть из них уже существовала на диске и потому не требовали правок. Тем не менне, 1 файл был обновлен в результате обновления конфигурации. Общий процесс занял 5.03 сек. Хотя при том только 3.79 сек был загружен процессор, что означает что грубо 25% времени мы потратили на ожидание ввода-вывода.

Фаза 3: Вызов бэкенда сборки

Когда большинство людей слышат о системе сборки, они представляют себе данную фазу. Это та фаза, где мы берем все дерево исходных кодов и создаём Firefox или какое-то другое приложение, над которым работаем. Фаза 3 фактически берёт всё, что было сгенерировано фазой 2 и запускает его. Начиная с самого зарождения проекта Mozilla это традиционно делалось утилитой make обрабатывающей файлы Makefile. Тем не менее, с переходом на moz.build файлы, вы можете повстречаться и с не-Make бэкендами сборки, такими как Tup или Visual Studio.

При сборке дерева, большая часть времени тратится на фазу 3. В ходе неё устанавливаются заголовочные файлы, компилируются файлы C++, файлы проходят через препроцессор и т.д.

Рекурсивный Make-бэкенд

The recursive make backend is the tried and true backend used to build the tree. It's what's been used since the dawn of Mozilla. Essentially, there are Makefiles in each directory. make starts processing the Makefile in the root directory and then recursively descends into child directories until it's done.

If only it were that simple.

The recursive make backend divides the source tree into tiers. A tier is a grouping of related directories containing Makefiles of their own. For example, there is a tier for the Netscape Portable Runtime (nspr), one for the JavaScript engine, one for the core Gecko platform, one for the XUL app being built, etc.

The main moz.build file defines the tiers and directories in them. In reality, the main moz.build files include other moz.build files such as /toolkit/toolkit.mozbuild which define the tiers. They do this via the add_tier_dir() function.

At build time, the tiers are traversed in the order they are defined. Typically, the traversal order looks something like base, nspr, nss, js, platform, app.

Each tier consists of 3 sub-tiers: export, libs, and tools. This roughly correspond to the actions of pre-build, main-build, and post-build. Although, that is poor naming because all 3 are really part of the build. export is used to do things like copy headers into place. libs is reserved for most of the work, like compiling C++ source files. tools is used to installing tests and other support tools.

When make is invoked, it starts at the export sub-tier of the first tier, and traverses all the directories in that tier. Then, it does the same thing for the libs sub-tier. Then the tools sub-tier. It then moves on to the next tier. And so forth until there are no tiers remaining.

To view information about the tiers, you can execute the following special make targets:

Command Effect
make echo-tiers Show the final list of tiers.
make echo-dirs Show the list of non-static source directories to iterate over, as determined by the tier list.
make echo-variable-STATIC_DIRS Show the list of static source directories to iterate over, as determined by the tier list.

moz.build файлы

moz.build файлы задают то, как каждая часть дерева исходников интегрируется в общую систему сборки. Можете считать каждый moz.build файл как структуру, сообщающую системе сборки что делать.

Во время генерации бэкенда сборки, все moz.build файлы, относящиеся к текущей конфигурации сборки считываются и конвертируются в файлы и действия нужные для сборки (например файлы Makefile). В этой части мы поговорим о том, как они работают на самом деле.

Отдельный moz.build файл на деле является скриптом Python. Тем не менее, они не похожи на обычные питоновские скрипты. Среда исполнения жестко контролируется, потому moz.build файлы могут выполнять только ограниченный набор операций. В сущности, moz.build файлы ограничены следующим набором действий :

  1. Вызов функций, которые явно отмечены доступными среде moz.build
  2. Присвоение значений четко определенному набору переменных с именами в ВЕРХНЕМ РЕГИСТРЕ.
  3. Создание новых переменных с именами не в ВЕРХНЕМ РЕГИСТРЕ (в том числе определение функций).

Важно отметить, что moz.build файлы не могут:

  • Импортировать модули.
  • Открывать файлы.
  • Использовать функцию или оператор print.
  • Ссылаться на множество встроенных/глобальных функций Python (они сделаны недоступными для среды исполнения).

Наиболее важные функции файлов moz.build - это номера 1 и 2 из списка выше. Это то, как moz.build файлы сообщают что делать системе сборки. К примеру, вы можете задать список DIRS для поиска дополнительных moz.build файлов в заданных каталогах.

Результат исполнения каждого отдельного moz.build файла - это словарь Python. Этот словарь состоит из переменных в ВЕРХНЕМ РЕГИСТРЕ, которые были заданы явно, а также специальных переменных, получивших свои значения неявно в результате вызовов функций, экспортированных в среду исполнения. Когда мы далее будем вести речь о moz.build файлах как о структурах данных, мы будем иметь в виду эти самые словари.

moz.build UPPERCASE Variables and Functions

The set of special symbols available to moz.build files is centrally defined and is under the purview of the build config module. To view the variables and functions available in your checkout of the tree, run the following:

mach mozbuild-reference

Or, you can view the raw file at /python/mozbuild/mozbuild/frontend/context.py.

How moz.build Processing Works

For most people, it is sufficient to just know that moz.build files are Python scripts that are executed and emit Python dicts describing the build config. If you insist on knowing more, this section is for you.

All the code for reading moz.build files lives under /python/mozbuild/mozbuild/frontend/. mozbuild is the name of our Python package that contains most of the code for defining how the build system works. Yes, it's easy to confuse with moz.build files. Sorry about that.

sandbox.py contains code for a generic Python sandbox. This is the code used to restrict the environment moz.build files are executed under.

reader.py contains the code that defines the actual moz.build sandbox (the MozbuildSandbox class) and the code for traversing a tree of moz.build files (by following DIRS and TIERS variables). The latter is the BuildReader class. A BuildReader is instantiated with a configuration and then is told to read the source tree. It emits a stream of MozbuildSandbox instances corresponding to the executed moz.build files.

The stream of MozbuildSandbox produced by the BuildReader is typically fed into the TreeMetadataEmitter class from emitter.py. The role of TreeMetadataEmitter is to convert the low-level MozbuildSandbox dictionaries into higher-level function-specific data structures. These data structures are the classes defined in data.py. Each class defines a specific aspect of the build system, such as directories to traverse, C++ files to compile, etc. The output of TreeMetadataEmitter is a stream of instances of these classes.

The stream of build system describing class instances emitted from TreeMetadataEmitter is then fed into a build backend. A build backend is simply an instance of a child class of BuildBackend from base.py (in the mozbuild.backend package now, not mozbuild.frontend). The child class implements methods for processing individual class instances as well as common hook points, such as processing has finished. See recursivemake.py for an implementation of a BuildBackend.

While we call the base class BuildBackend, the class doesn't need to be focused with building at all. If you wanted to create a consumer that performed a line count of all C++ files or generated a Clang compilation database, for example, this would be an acceptable use of a BuildBackend.

Technically, we don't need to feed TreeMetadataEmitter's output into a BuildBackend: it's possible to create your own consumer. However, BuildBackend provides a common framework from which to author consumers. Along the same vein, you don't need to use TreeMetadataEmitter to consume MozbuildSandbox instances. Nor do you need to use BuildReader to traverse the moz.build files! This is just the default framework we've established for our build system.

Legacy Content

THE CONTENT BELOW IS CONSIDERED LEGACY. IT IS PRESERVED FOR HISTORICAL REASONS UNTIL THIS ENTIRE PAGE IS REWRITTEN.

Makefile basics

Makefiles can be quite complicated, but Mozilla provides a number of built-in rules that should enable most Makefiles to be quite simple. Complete documentation for make is beyond the scope of this document, but is available here.

One concept you will need be familiar with is variables in make. Variables are defined by the syntax VARIABLE = VALUE, and the value of a variable is referenced by writing $(VARIABLE). All variables are strings.

All Makefile.in files in Mozilla have the same basic format:

DEPTH           = ../../../..
topsrcdir       = @top_srcdir@
srcdir          = @srcdir@
VPATH           = @srcdir@

include $(DEPTH)/config/autoconf.mk

# ... Main body of Makefile goes here ...

include $(topsrcdir)/config/rules.mk

# ... Additional rules go here ...
  • The DEPTH variable should be set to the relative path from your Makefile.in to the toplevel Mozilla directory.
  • topsrcdir is substituted in by configure, and points to the toplevel mozilla directory.
  • srcdir is also substituted in by configure, and points to the source directory for the current directory. In source tree builds, this will simply point to "." (the current directory).
  • VPATH is a list of directories where make will look for prerequisites (i.e. source files).

One other frequently used variable not specific to a particular build target is DIRS. DIRS is a list of subdirectories of the current directory to recursively build in. Subdirectories are traversed after their parent directories. For example, you could have:

DIRS = \
  public \
  resources \
  src \
  $(NULL)

This example demonstrates another concept, continuation lines. A backslash as the last character on a line allows the variable definition to be continued on the next line. The extra whitespace is compressed. The terminating $(NULL) is a method for consistency; it allows you to add and remove lines without worrying about whether the last line has an ending backslash or not.

Makefile examples

Building libraries

There are three main types of libraries that are built in Mozilla:

  • Components are shared libraries (except in static builds) which are installed to dist/bin/components. Components are not linked against by any other library.
  • Non-component shared libraries include libraries such as libxpcom, libmozjs. These libraries are installed to dist/bin and are linked against. You will probably not need to create a new library of this type.
  • Static libraries are often used as intermediate steps to building a shared library, if there are source files from several directories that are part of the shared library. Static libraries may also be linked into an executable.

Non-component shared libraries

A non-component shared library is useful when there is common code that several components need to share, and sharing it through XPCOM is not appropriate or not possible. As an example, let's look at a portion of the Makefile for libmsgbaseutil, which is linked against by all of the main news components:

 DEPTH           = ../../..
 topsrcdir       = @top_srcdir@
 srcdir          = @srcdir@
 VPATH           = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE          = msgbaseutil
 LIBRARY_NAME    = msgbaseutil
 EXPORT_LIBRARY = 1
 SHORT_LIBNAME   = msgbsutl

Notice that the only real change from the component example above is that IS_COMPONENT is not set. When this is not set, a shared library will be created and installed to dist/bin.

Static libraries

As mentioned above, static libraries are most commonly used as intermediate steps to building a larger library (usually a component). This allows you to spread out the source files in multiple subdirectories. Static libraries may also be linked into an executable. As an example, here is a portion of the Makefile from layout/base/src:

 DEPTH           = ../../..
 topsrcdir       = @top_srcdir@
 srcdir          = @srcdir@
 VPATH           = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE          = layout
 LIBRARY_NAME    = gkbase_s
 
 # REQUIRES and CPPSRCS omitted here for brevity #
 
 # we don't want the shared lib, but we want to force the creation of a static lib.
 FORCE_STATIC_LIB = 1
 
 include $(topsrcdir)/config/rules.mk

The key here is setting FORCE_STATIC_LIB = 1. This creates libgkbase_s.a (on UNIX) and gkbase_s.lib on Windows, and copies it to dist/lib. Now, let's take a look at how to link several static libraries together to create a component:

 DEPTH           = ../..
 topsrcdir       = @top_srcdir@
 srcdir          = @srcdir@
 VPATH           = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE          = layout
 LIBRARY_NAME    = gklayout
 EXPORT_LIBRARY = 1
 IS_COMPONENT    = 1
 MODULE_NAME     = nsLayoutModule
 
 CPPSRCS         = \
                 nsLayoutModule.cpp \
                 $(NULL)
 
 SHARED_LIBRARY_LIBS = \
                 $(DIST)/lib/$(LIB_PREFIX)gkhtmlbase_s.$(LIB_SUFFIX) \
                 $(DIST)/lib/$(LIB_PREFIX)gkhtmldoc_s.$(LIB_SUFFIX) \
                 $(DIST)/lib/$(LIB_PREFIX)gkhtmlforms_s.$(LIB_SUFFIX) \
                 $(DIST)/lib/$(LIB_PREFIX)gkhtmlstyle_s.$(LIB_SUFFIX) \
                 $(DIST)/lib/$(LIB_PREFIX)gkhtmltable_s.$(LIB_SUFFIX) \
                 $(DIST)/lib/$(LIB_PREFIX)gkxulbase_s.$(LIB_SUFFIX) \
                 $(DIST)/lib/$(LIB_PREFIX)gkbase_s.$(LIB_SUFFIX) \
                 $(DIST)/lib/$(LIB_PREFIX)gkconshared_s.$(LIB_SUFFIX) \
                 $(DIST)/lib/$(LIB_PREFIX)gkxultree_s.$(LIB_SUFFIX) \
                 $(DIST)/lib/$(LIB_PREFIX)gkxulgrid_s.$(LIB_SUFFIX) \
                 $(NULL)
 
 include $(topsrcdir)/config/rules.mk

SHARED_LIBRARY_LIBS is set to a list of static libraries which should be linked into this shared library. Note the use of LIB_PREFIX and LIB_SUFFIX to make this work on all platforms.

Building jar files

Jar files are used for packaging chrome files (XUL, JavaScript, and CSS). For more information on Jar packaging, you can read this document. Here we will only cover how to set up a Makefile to package jars. Here is an example:

 DEPTH           = ../../../..
 topsrcdir       = @top_srcdir@
 srcdir          = @srcdir@
 VPATH           = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 include $(topsrcdir)/config/rules.mk

That's right, there are no extra variables to define. If a jar.mn file exists in the same directory as this Makefile.in, it will automatically be processed. Although the common practice is to have a resources directory that contains the jar.mn and chrome files, you may also put a jar.mn in a directory that creates a library, and it will be processed.

See the glossary of Makefile variables for information about specific variables and how to use them.

Original Document Information

  • Author: Brian Ryner
  • Copyright Information: Portions of this content are © 1998–2006 by individual mozilla.org contributors; content available under a Creative Commons license

 

Метки документа и участники

Внесли вклад в эту страницу: theodysseus
Обновлялась последний раз: theodysseus,