Как указать имя target. Как мы уже видели, когда вызывается метод Program сборщика, он соберет программу на основе указанного имени файла исходного кода.
Таким образом, если компилируется модуль hello.c, то выходной исполняемый файл (target) получит имя hello в POSIX-системах, и hello.exe на Windows:
Program('hello.c')
Если вы хотите указать другое имя для выходного файла, то просто введите его как параметр для Program слева от имени исходного файла:
Program('new_hello', 'hello.c')
Примечание: SCons требует, чтобы имя target-файла шло в параметрах первым, за которым идут имена одного или нескольких исходных файлов. Так эмулируется поведение операторов присвоения большинства языков программирования, включая Python: "target = исходный код". Другие способы предоставления этой информации см. в секции 3.6, "Keyword Arguments" [1]).
Теперь SCons соберет выполняемую программу с именем new_hello, когда сборка происходит на системе POSIX:
% scons -Q
cc -o hello.o -c hello.c
cc -o new_hello hello.o
Файл исполняемого кода получит имя new_hello.exe, когда компиляция запущена на Windows:
Компиляция нескольких исходных файлов. Мы уже рассмотрели, как компилировать программу, исходный код которой состоит из одного файла. Однако чаще всего проект состоит из множества файлов исходного кода. Чтобы скомпилировать несколько файлов, необходимо передать их как список Python (в квадратных скобках), примерно так:
Program(['prog.c', 'file1.c', 'file2.c'])
Сборка этого примера будет выглядеть примерно так:
% scons -Q
cc -o file1.o -c file1.c
cc -o file2.o -c file2.c
cc -o prog.o -c prog.c
cc -o prog prog.o file1.o file2.o
Обратите внимание, что SCons сама вывела имя для выходного исполняемого файла из имени первого файла в списке. Поскольку первый файл был prog.c, SCons дала имя для исполняемой программы prog (или prog.exe на Windows). Если вы хотите указать другое имя для выходного файла, то (как мы уже рассматривали в предыдущей секции) укажите его слева от списка:
Функция Glob. Можно использовать функцию Glob, чтобы найти все файлы, подходящие под указанный шаблон, используя стандартные символы для совпадения *, ? и [abc], чтобы было совпадение с любым из символов a, b или c. Также поддерживается [!abc], что означает совпадение с любым символом, кроме a, b или c. Это значительно упрощает составление списков компиляции для большого количества исходных файлов:
Program('program', Glob('*.c'))
Документация SCons (man scons) более подробно показывает использование Glob с каталогами вариантов (variant directories) и репозиториями (см. главу 16, "Building From Code Repositories" [1]), исключая некоторые файлы и возвращая строки вместо Nodes.
Как мы только что показали, есть два способа указать исходные файлы для компиляции, один из них в виде списка:
Program('hello', ['file1.c', 'file2.c'])
И другой в виде одного файла:
Program('hello', 'hello.c')
Можно указать в списке только один файл, что кстати предпочтительнее, просто для целостности синтаксиса:
Program('hello', ['hello.c'])
Функции SCons воспринимают один входной файл в любой форме. Фактически внутри себя SCons обрабатывает все входные файлы как список файлов, однако позволяет опустить квадратные скобки, когда указан только один входной файл.
Важное замечание: хотя функции SCons прощают такое вольное обращение с параметрами входных файлов, язык Python в этом отношении более строг, поскольку обрабатывает списки и строки по-разному. Таким образом, SCons позволяет указать в качестве одного входного файла и строку, и список:
# Оба следующих вызова компиляции допустимы:
Program('program1', 'program1.c')
Program('program2', ['program2.c'])
Если попытаться заставить Python делать что-то подобное, перемешав строки и списки, то это приведет к ошибке или некорректным результатам:
common_sources = ['file1.c', 'file2.c']
# Этот оператор даст вызовет ошибку Python, потому что
# сделана попытка добавить строку к списку:
Program('program1', common_sources +'program1.c')
# Следующий оператор сработает нормально, потому что
# складываются друг с другом два списка:
Program('program2', common_sources + ['program2.c'])
Как улучшить составление списков. Один из недостатков использования списка Python для исходных файлов состоит в том, что каждое имя надо закрывать кавычками (одиночными или двойными). Это загромождает список и делает его трудно читаемым, когда список длинный. К счастью, SCons и Python предоставляют способы сделать файл SConstruct более удобочитаемым.
Чтобы обработать списки имен, SCons предоставляет функцию Split, которая принимает взятый в кавычки список имен, где имена отделены пробелами или другими символами отступа, и возвращает список, состоящий из отдельных имен файлов. Если использовать функцию Split, то предыдущий пример превращается в такой:
Примечание: если вы знакомы с Python, то наверняка заметите, что это похоже на метод split() method in the Python standard string module. Unlike the split() в стандартном Python-модуле strings, однако функция Split более умная и не требует на входе строку, и завершает один не-строковый объект в списке или возвращает его аргумент, если это уже список. Это становится удобным, потому что дает способ передавать любые значения в функции SCons, устраняя необходимость вручную проверять тип переменной.
Если поместить вызов функции Split в параметры Program, то это не приведет к значительному сокращению длины списка. Есть альтернатива присвоить список переменной, и уже передавать эту переменную в вызов Program:
Есть еще один способ составления списка, который будет полезен для больших списков. Он становится возможным, потому что Split дает возможность указывать между именами любое количество пробелов. Можно составлять списки, состоящие из нескольких строк, что упрощает их чтение и редактирование:
src_files = Split("""main.c
file1.c
file2.c""")
Program('program', src_files)
Обратите внимание, что в этом примере мы использовали тройные кавычки, что позволяет содержать в string несколько строк. Тройные кавычки могут состоять либо из символов " ("""), либо из символов ' (''').
Ключевые слова для аргументов. SCons также позволяет идентифицировать выходной файл и входные файлы, используя ключевые слова аргументов Python (keyword arguments) target и source. Получается вот такой синтаксис:
Использовать или нет ключевые слова для аргументов, и в каком порядке их указывать - целиком ваш выбор. SCons сработает в любом случае одинаково.
Компиляция нескольких программ. Чтобы скомпилировать несколько программ в одном файле SConstruct, просто вызовите метод Program несколько раз, по одному для каждой компилируемой программы:
Тогда SCons скомпилирует программы следующим образом:
% scons -Q
cc -o bar1.o -c bar1.c
cc -o bar2.o -c bar2.c
cc -o bar bar1.o bar2.o
cc -o foo.o -c foo.c
cc -o foo foo.o
Обратите внимание, что SCons необязательно выполнит компиляцию в том же порядке, в каком операторы Program были указаны в файле SConstruct. Однако SCons распознает отдельные объектные файлы, которые должны быть собраны перед тем, как собирается выходной результат программы. Более подробно это обсуждается далее в главе "Зависимости".
Общие исходные файлы для нескольких программ. Существует общепринятая практика наследования готового кода, когда один и тот же исходный код используется в разных программах. Один из способов такой практики - скомпилировать из исходного кода библиотеку, которую можно впоследствии ликовать в результирующие программы (создание библиотек обсуждается далее в главе "Сборка и линковка библиотек").
Более прямолинейный способ, но менее удобный - просто совместно использовать один и тот же исходный код в нескольких программах, подключая модули исходного кода в каждую программу:
SCons определит, что объектные файлы для исходных файлов common1.c и common2.c нужно сгенерировать только один раз, даже если они используются для обоих результирующих выходных программ:
% scons -Q
cc -o bar1.o -c bar1.c
cc -o bar2.o -c bar2.c
cc -o common1.o -c common1.c
cc -o common2.o -c common2.c
cc -o bar bar1.o bar2.o common1.o common2.o
cc -o foo.o -c foo.c
cc -o foo foo.o common1.o common2.o
Если у двух или большего количества программ используется общий набор исходных файлов, то повторение одних и тех же файлов в нескольких списках создаст некую проблему в обслуживании таких списков. Эту проблему можно решить, создав переменную, состоящую из списка общих исходных файлов, и объединить этот список с другими списками, используя оператор + языка Python:
common = ['common1.c', 'common2.c']
foo_files = ['foo.c'] + common
bar_files = ['bar1.c', 'bar2.c'] + common
Program('foo', foo_files)
Program('bar', bar_files)
Это будет работать так же, как и предыдущий пример.
[Сборка и линковка библиотек]
Часто полезно организовать большой проект ПО таким образом, чтобы собрать его части в одну или большее количество двоичных библиотек. SCons упрощает создание библиотек и их использование для программ.
Сборка библиотек. Сборку библиотеки можно выполнить, используя метод Library вместо метода Program:
Library('foo', ['f1.c', 'f2.c', 'f3.c'])
SCons использует подходящий префикс для выходного файла библиотеки, зависящий от целевой системы. На POSIX-системах или Linux этот пример скомпилируется следующим образом (хотя ranlib на некоторых системах может не вызываться):
% scons -Q
cc -o f1.o -c f1.c
cc -o f2.o -c f2.c
cc -o f3.o -c f3.c
ar rc libfoo.a f1.o f2.o f3.o
ranlib libfoo.a
На системе Windows сборка этого примера может выглядеть так:
Правила генерации выходного имени (target) для библиотеки похож на метод генерации имени для выходной программы: если вы не указали явно имя для целевой библиотеки, то SCons сама выведет его из имени первого исходного файла в списке, с добавлением подходящего суффикса в том случае, если суффикс вы не указали.
Сборка библиотек из исходного кода или объектных файлов. В предыдущем примере была показана сборка библиотеки из модулей исходного кода. Однако можно выполнить сборку библиотеки из объектных файлов. Фактически в списке входных файлов можно произвольно микшировать файлы исходного кода и объектные файлы:
Library('foo', ['f1.c', 'f2.o', 'f3.c', 'f4.o'])
SCons сама определит, что сначала надо скомпилировать файлы исходного кода, и только потом собрать из объектных файлов библиотеку:
% scons -Q
cc -o f1.o -c f1.c
cc -o f3.o -c f3.c
ar rc libfoo.a f1.o f2.o f3.o f4.o
ranlib libfoo.a
Конечно, для успешной сборки этого примера указанные в списке объектные файлы уже должны присутствовать на диске. См. далее главу "Объекты Node" для информации, как явно выполнить сборку объектных файлов и подключить собранные файлы в библиотеку.
Явная сборка статической библиотеки, StaticLibrary Builder. Метод Library собирает традиционную статическую библиотеку. Если вы хотите явно указать тип собираемой библиотеки, то можно использовать метод-синоним StaticLibrary вместо метода Library:
StaticLibrary('foo', ['f1.c', 'f2.c', 'f3.c'])
Между методами StaticLibrary и Library нет функциональных отличий.
Сборка загружаемых библиотек (DLL), SharedLibrary Builder. Если вы хотите собрать shared-библиотеку (на системах POSIX) или файл DLL (на Windows), то используйте метод SharedLibrary:
SharedLibrary('foo', ['f1.c', 'f2.c', 'f3.c'])
Вывод сборки этого примера на POSIX:
% scons -Q
cc -o f1.os -c f1.c
cc -o f2.os -c f2.c
cc -o f3.os -c f3.c
cc -o libfoo.so -shared f1.os f2.os f3.os
Снова обратите внимание, что SCons сама позаботится о корректной генерации имени выходного файла, добавив опцию -shared для компиляции POSIX и опцию /dll для Windows.
[Сборка с подключением библиотек]
Обычно вы собираете библиотеку, чтобы впоследствии подключить её в одну или несколько программ. Вы линкуете библиотеки с программой, указывая construction-переменную $LIBS, и указывая директорию, в которой находится библиотека, в construction-переменной $LIBPATH:
Опять-таки, обратите внимание, что вам не надо указывать префикс библиотеки (наподобие lib) или её суффикс (наподобие .a или .lib). SCons сама будет использовать корректный префикс или суффикс для текущей системы.
Для POSIX-системы или Linux сборка этого примера будет выглядеть примерно так:
% scons -Q
cc -o f1.o -c f1.c
cc -o f2.o -c f2.c
cc -o f3.o -c f3.c
ar rc libfoo.a f1.o f2.o f3.o
ranlib libfoo.a
cc -o prog.o -c prog.c
cc -o prog prog.o -L. -lfoo -lbar
Как обычно, обратите внимание, что SCons позаботится о конструировании корректных командных строк для линковки указанной библиотеки в каждой системе.
Если линкуется только одна библиотека, то можно указать имя библиотеки либо как список Python (квадратными скобками), либо одной строкой. Следующие два вызова работают одинаково:
// Указание одного файла библиотеки в виде Python-списка:
Program('prog.c', LIBS=['foo'], LIBPATH='.')
// Указание одного файла библиотеки в виде строки:
Program('prog.c', LIBS='foo', LIBPATH='.')
Это работает также, как SCons обрабатывает строку или список при указании одного файла исходного кода.
Поиск библиотек, переменная $LIBPATH. По умолчанию линковщик просматривает только отдельные директории для библиотек, определенные на уровне системы. SCons знает, что можно также найти библиотеки, которые вы указали в construction-переменной $LIBPATH. $LIBPATH содержит список имен директорий, примерно так:
Использовать список Python предпочтительнее, потому что это портируется между системами. Альтернативно можно было бы поместить все имена директорий в одну строку, где директории отделены друг от друга символом разделителя, зависящим от системы. На POSIX-системах это двоеточие:
LIBPATH ='/usr/lib:/usr/local/lib'
На Windows это точка с запятой:
LIBPATH ='C:\\lib;D:\\lib'
Обратите внимание, что символ обратного слеша, используемых в путях Windows, требует экранирования, чтобы он корректно обрабатывался в строках.
Когда запускается линкер, SCons создаст подходящие флаги, чтобы линкер нашел библиотеки. Так, на системах POSIX или Linux сборка примера выше будет выглядеть так:
% scons -Q
cc -o prog.o -c prog.c
cc -o prog prog.o -L/usr/lib -L/usr/local/lib -lm
На системах Windows сборка этого примера будет выглядеть так:
И опять, обратите внимание, что SCons сама позаботится о деталях, зависящих от используемой системы, когда создает опции командной строки.
[Объекты Node]
Внутри себя SCons представляет все файлы и директории как узлы (Nodes). Эти внутренние объекты (не объектные файлы!) могут использоваться разными способами, чтобы сделать файлы SConscript портируемыми и удобными для чтения.
Методы сборщика, возвращающие списки Target Nodes. Все методы сборщика возвратят список объектов Node, который идентифицирует target-файл или файлы, которые будут собраны. Эти возвращенные Nodes могут быть переданы как аргументы в другие методы сборщика.
Для примера предположим, что нужно собрать два объектных файла, которые составляют программу, с разными опциями. Это должно означать вызов метода Object сборщика для каждого объектного файла, с указанием нужных опций:
Один из способов комбинирования этих объектных файлов в результирующий файл исполняемой программы это вызов метода Program сборщика, с указанием имен объектных файлов в качестве входных:
Проблема здесь заключается в том, что файл SConstruct становится не портируемым между разными операционными системами. Он не сработает, например, на Windows, потому что объектные файлы получают имена с расширением .o вместо .obj.
Лучшее решение - назначить переменным список возвращаемых целевых файлов из методов Object, и затем передать их в метод Program:
Явное создание Nodes для файлов и директорий. Следует отметить, что SCons явно различает Node, представляющий файлы и Node, представляющий директории. SCons поддерживает методы File и Dir, которые соответственно возвратят Node для файлов и директорий:
hello_c = File('hello.c')
Program(hello_c)
classes = Dir('classes')
Java(classes, 'src')
Обычно не нужно вызывать File или Dir напрямую, потому что вызов метода сборщика автоматически обрабатывает строки как имена файлов или директорий, и для вас транслирует их в объекты Node. Функции File и Dir могут стать полезными в ситуациях, когда вам нужно явно инструктировать SCons о типе Node, передаваемого в сборщик или другие функции, или нужно однозначно сослаться на определенный файл в дереве директорий.
Также бывают случаи, когда вам может понадобиться ссылка на элемент в файловой системе, когда неизвестно заранее, является ли она файлом или каталогом. Для таких ситуаций SCons также предоставляет функцию Entry, которая вернет Node, который может представлять либо файл, либо директорию.
xyzzy = Entry('xyzzy')
Возвращенный xyzzy Node будет превращен в Node файла или Node директории методом сборщика или другой функцией, требующей файл или директорию.
Печать файловых имен Node. Чаще всего вы можете использовать Node, чтобы печатать имя файла, которое представляет Node. Однако имейте в виду, что поскольку объект, возвращенный вызовом сборщика, является списком Nodes, необходимо использовать дополнительные индексы Python для выборки из списка отдельных Node. Для примера рассмотрим следующий файл SConstruct:
На системе POSIX он должен напечатать следующие имена файлов:
% scons -Q
The object file is: hello.o
The program file is: hello
cc -o hello.o -c hello.c
cc -o hello hello.o
И на системе Windows:
C:\>scons -Q
The object file is: hello.obj
The program file is: hello.exe
cl /Fohello.obj /c hello.c /nologo
link /nologo /OUT:hello.exe hello.obj
embedManifestExeCheck(target, source, env)
Обратите внимание, что в этом примере object_list[0] извлекает реальный объект Node из списка, и функция print языка Python преобразует объект в строку для вывода на печать.
Использование имен файлов Node как строк. Печать имен Node, как было показано в предыдущей секции, работает потому, что строковое представление объекта Node это строка. Если нужно сделать что-то другое, кроме печати имени файла, то необходимо извлечь это имя с помощью встроенной функции str языка Python. Например, если вы хотите использовать os.path.exists, чтобы определить, существует ли файл, то можно преобразовать имя файла Node в строку следующим образом:
ifnot os.path.exists(program_name):
print("%s does not exist!"%program_name)
В результате на POSIX будет выполнено следующее:
% scons -Q
hello does not exist!
cc -o hello.o -c hello.c
cc -o hello hello.o
GetBuildPath: извлечение пути из Node или строки. Функция env.GetBuildPath(file_or_list) возвратит путь из Node или из строки, представляющей путь. Она так же может взять список Node и/или строк, и возвратить список путей. Если в функцию был передан один Node, то результат будет тот же, что и вызов str(node) (см. предыдущую секцию). Строка (строки) может иметь в себе встроенные construction-переменные, которые разворачиваются как обычно, используя набор переменных окружения. Пути могут быть файлами или директориями, и они могут не существовать.
env=Environment(VAR="value")
n=File("foo.c")
print(env.GetBuildPath([n, "sub/dir/$VAR"]))
Это напечатает следующие имена файлов:
% scons -Q
['foo.c', 'sub/dir/value']
scons: `.' is up to date.
Существует также версия функции GetBuildPath, которая может быть вызвана без Environment; она использует среду SCons Environment по умолчанию для подстановки любых строковых аргументов.
[Зависимости]
Пока что мы рассматривали, как SCons обрабатывает одиночные сборки. Однако одной из основных функций такого инструментария, как SCons - делать сборку только необходимых исходных файлов, т. е. только тех, что были изменены. Или, если другими словами, SCons не тратит время на пересборку тех элементов исходного кода, которые не нуждаются в этом. Можно увидеть, как это работает, если просто заново запустить SCons после нашей сборки простого примера hello:
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q
scons: `.' is up to date.
Когда сборка запускается второй раз, SCons знает о том, что программа hello уже находится в корректном состоянии, соответствующем текущему исходному коду hello.c (up-to-date), и избегает повторной его пересборки. Можно увидеть это еще более явно. если указать программу hello в командной строке:
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
Обратите внимание, что SCons сообщает "...is up to date" только для target-файлов, которые явно указаны в командной строке, чтобы не загромождать вывод.
Детектирование, какие файлы поменялись: функция Decider. Другим аспектом предотвращения ненужных перестроений является фундаментальное поведение сборщика при запуске повторных сборок - как он определяет факт изменения входного файла, чтобы создаваемое ПО оставалось актуальным. По умолчанию SCons отслеживает это с помощью сигнатуры содержимого каждого файла (хеша каждого файла), хотя можно упростить конфигурацию SCons для использования вместо этого даты модификации (метки времени) файла. Вы можете даже написать свою собственную функцию Python, которая будет определять, нужно ли пересобрать входной файл.
Криптографический хеш. По умолчанию SCons использует хеш содержимого исходного файла, а не его дату и время, чтобы определить факт его модификации. Это означает, что вы можете быть удивлены поведением SCons по умолчанию, если используете соглашение Make о принудительном перестроении путем обновления времени модификации файла (например, с помощью команды touch):
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% touch hello.c
% scons -Q hello
scons: `hello' is up to date.
Даже когда время модификации файла было изменено, SCons определит, что содержимое файла hello.c не было изменено, и по этой причине не будет делать пересборку. Это позволит избежать нежелательных сборок для случаев, например, когда файл был перезаписан без внесения в него изменений. Однако если содержимое файла на самом деле поменялось, то SCons определит изменение, и выполнит пересборку файла и целевой программы, так как это необходимо:
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% [изменение содержимого hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
Обратите внимание, что можно при желании указать поведение по умолчанию, чтобы явно использовать сигнатуры содержимого, используя функцию Decider:
Program('hello.c')
Decider('content')
Можно также использовать строку 'MD5' как синоним 'content', когда вызываете функцию Decider, это более старое имя, которое устарела с того момента, когда SCons начала поддерживать выбор хеш-фукций, так что стало возможным использовать для хеша не только MD5.
Последствия использования сигнатур содержимого. Использование сигнатур для определения изменения содержимого входного файла имеет одно удивительное преимущество: если исходный файл был изменен так, что содержимое целевых пересобранных файлов не поменялось, и осталось тем же самым, как было в момент предпоследней сборки, то любые "дочерние" целевые файлы, которые зависят от перестроенных, но не изменившихся целевых файлов, фактически не нуждаются в пересборке.
Так что если, к примеру, пользователь только лишь поменял комментарий в файле hello.c, то пересобранный файл hello.o останется таким же, как было до этого (предполагается, что компилятор не помещает в объектный файл никакую дополнительную информацию, связанную со сборкой). Тогда SCons определит, что не нужно запускать стадию линковки для пересборки выходного файла hello программы:
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% [изменение комментария в hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
scons: `hello' is up to date.
По сути SCons "зацикливает" любые зависимые сборки, когда обнаруживает, что целевой пересобранный файл такой же, как был в момент предыдущей последней сборки. Это занимает некоторое время обработки для чтения содержимого файла (в нашем примере hello.o), однако часто экономит время, когда пересборка, которой удалось избежать, была бы слишком затратной по времени.
Использование метки времени для определения изменения файла. Если вы захотите, то можете сконфигурировать SCons на использование времени модификации файла, а не его содержимого, чтобы принять решение - нужна ли перекомпиляция target или нет. SCons предоставляет 2 способа использования меток времени для определения изменения входного файла.
Наиболее привычный способ использования меток времени это способ Make: т. е. SCons решает, что target должна быть пересобрана, если время модификации исходного файла более новое, чем время target-файла. Для такого способа вызовите функцию Decider следующим образом:
Object('hello.c')
Decider('timestamp-newer')
Это заставит SCons действовать наподобие Make, когда обновляется время модификации файла (например, с помощью команды touch):
И поскольку это поведение такое же, как поведение Make, можно использовать синоним 'make' в качестве замены 'timestamp-newer' при вызове функции Decider:
Object('hello.c')
Decider('make')
Один из недостатков использование меток времени, как это делает Make, состоит в том, что если по какой-то причине время модификации входного файла внезапно станет старше, чем время целевого файла, то целевой файл не будет пересобран. Это может произойти, например, если старая копия исходного файла была восстановлена из архива бекапа. Содержимое восстановленного файла будет скорее всего значительно отличаться от его содержимого, когда он последний раз компилировался, однако target не будет пересобран, потому что время модификации файла исходного кода будет более менее новым, чем время файла target. По этой причине программисты часто делают команду clean, чтобы гарантированно пересобрать проект.
Поскольку SCons фактически сохраняет информацию о метках времени исходных файлов всякий раз, когда производится сборка target, SCons может обработать эту ситуацию, проверяя точное совпадение метки времени входного файла вместо проверки, что он более новый, чем target-файл. Чтобы это сделать, используйте аргумент 'timestamp-match' при вызове функции Decider:
Object('hello.c')
Decider('timestamp-match')
Когда выполнена такая конфигурация, SCons пересоберет target всякий раз, когда время модификации исходного файла было изменено. Так что если вы используете опцию -t для touch, чтобы изменить время модификации hello.c на более старую дату (1 января 1989 года), SCons пересоберет файл target:
В общем случае, единственная причина, по которой вместо совпадения метки времени (timestamp-match) используется проверка новизны (timestamp-newer), состоит в том, что у вас есть какая-то определенная причина обязательного использования поведения Make, чтобы не делать пересборку target, когда измененный исходный файл стал более старым.
Одновременное использование сигнатур MD и меток времени. В качестве повышения производительности SCons позволяет использовать сигнатуру содержимого файла, но считывать это содержимое только при изменении метки времени файла. Для этого вызовите функцию Decider с аргументом content-timestamp следующим образом:
Program('hello.c')
Decider('content-timestamp')
Настроенные так, SCons будут вести себя так же, как и при использовании решателя («content»):
При такой настройке SCons все еще будет вести себя так же, как и при использовании Decider('content'):
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% touch hello.c
% scons -Q hello
scons: `hello' is up to date.
% edit hello.c
[изменение содержимого hello.c]
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
Однако второй вызов SCons в показанном выше выводе когда сборка up-to-date, будет выполнен простым просмотром времени модификации файла hello.c, а не открытием его и вычислением сигнатуры по содержимому. Это может значительно ускорить многие up-to-date сборки.
Единственный недостаток использования Decider('content-timestamp') состоит в том, что SCons не будет пересобирать target-файл, если файл исходного кода был изменен в течение одной секунды после последней сборки файла. При программировании на практике это не создаст проблему, потому что маловероятно, что кто-то выполнит сборку, и затем в течение одной секунды внесет изменение и запустит сборку снова. Однако некоторые скрипты сборки или инструменты интеграции могут полагаться на возможность автоматического применения изменений к файлам, и затем как можно быстрее выполнить пересборку, в таком случае использование Decider('content-timestamp') может оказаться нецелесообразным.
Расширение SCons: написание своей собственной функции Decider. Различные строковые значения, которые мы передаем в функцию Decider, в сущности используются SCons для выбора одной из определенных внутренних функций, реализующих разные способы определения, была ли зависимость (обычно это исходный файл) изменена с момента последней сборки target-файла. Оказывается, вы также можете предоставить свою собственную функцию, которая будет определять изменение в зависимости.
Для примера предположим, что у нас есть входной файл, где содержится много данных в некотором специальном формате, который используется для пересборки различных target-файлов, однако каждый target-файл зависит в действительности только от одной определенной секции входного файла. Мы хотим, чтобы каждый target-файл зависел только от его раздела входного файла. Однако, поскольку входной файл может содержать много данных, мы хотим открыть входной файл, только если его временная метка изменилась. Это можно сделать с помощью пользовательской функции Decider, которая может выглядеть примерно так:
Program('hello.c')
defdecide_if_changed(dependency, target, prev_ni, repo_node=None):
if dependency.get_timestamp() != prev_ni.timestamp:
dep =str(dependency)
tgt =str(target)
if specific_part_of_file_has_changed(dep, tgt):
returnTruereturnFalse
Decider(decide_if_changed)
Обратите внимание, что в определении функции зависимость (входной файл) это первый аргумент, и затем идет target. Оба они передаются как Node-объекты SCons, которые мы преобразовываем в строки с помощью str().
Третий аргумент, prev_ni, это объект, который хранит содержимое сигнатуры и/или информацию метки времени, которая была записана по зависимости при последней пересборке target. Объект prev_ni может хранить различную информацию, в зависимости от типа аргумента зависимости. Для обычных файлов у объекта prev_ni атрибуты следующие:
csig Сигнатура содержимого, криптографический хеш, или контрольная сумма содержимого файла зависимости в момент последней сборки target.
size Размер в байтах файла зависимости в момент последней сборки target.
timestamp Метка времени модификации файла зависимости в момент последней сборки target.
Эти атрибуты могут не присутствовать при первом запуске. Без любой предварительной сборки не создан ни один из target-файлов, и файл базы данных .sconsign еще не существует. Так что нужно всегда проверять, присутствует ли опрашиваемый атрибут (используйте Python-метод hasattr или блок try-except).
Четвертый аргумент repo_node, это Node, который используется, если он не равен None при сравнении BuildInfo. Обычно он устанавливается только в случае, когда target Node присутствует только в репозитории.
Обратите внимание, что игнорирование некоторых аргументов в вашей версии функции Decider совершенно нормальная вещь, если они не влияют на способ обнаружения изменения зависимости.
В завершение приведем небольшой пример функции для Decider, основанной на csig. Обратите внимание, как инициализируется информация сигнатуры для файла зависимости через get_csig при каждом вызове функции (это обязательно!).
# Мы всегда должны инициализировать значение .csig
dep_csig = dependency.get_csig()
# .csig может отсутствовать, если target еще не был собранifnot prev_ni.hasattr("csig"):
returnTrue# Target-файл может пока еще не существоватьifnot os.path.exists(str(target.abspath)):
returnTrueif dep_csig != prev_ni.csig:
# Некоторое изменение в исходном файле => установлено одно обновлениеreturnTruereturnFalse
defupdate_file():
withopen("test.txt", "a") as f:
f.write("some line\n")
update_file()
# Активация нашей собственной функции обнаружения изменения зависимости
env.Decider(config_file_decider)
env.Install("install", "test.txt")
Микширование разных способов определения изменения файла. Все предыдущие примеры демонстрировали вызов глобальной функции Decider для настройки всех принятий решений об изменении зависимости, которые выполняет SCons. Однако иногда вы можете захотеть иметь возможность конфигурирования различных способов принятия решений для разных target. Когда это необходимо, вы можете использовать метод env.Decider, влияющий только на конфигурацию принятия решений для target-ов, собранных в определенном рабочем окружении сборки.
Например, если мы произвольно хотим собрать одну программу, используя содержимое сигнатур, и другую программу, используя метки времени изменения из одного и того же исходного кода, мы можем сконфигурировать это следующим способом:
Если обе программы подключают один и тот же файл inc.h, то обновление информации времени изменения inc.h (командой touch) приведет к тому, что пересоберется только программа prog-timestamp:
% scons -Q
cc -o program1.o -c -I. program1.c
cc -o prog-content program1.o
cc -o program2.o -c -I. program2.c
cc -o prog-timestamp program2.o
% touch inc.h
% scons -Q
cc -o program2.o -c -I. program2.c
cc -o prog-timestamp program2.o
Неявные зависимости: переменная сборки $CPPPATH. Теперь предположим, что наша программа "Hello, World!" содержит строку #include для подключения файла hello.h:
#include < hello.h>
intmain()
{
printf("Hello, %s!\n", string);
}
И, для полной ясности, файл hello.h выглядит вот так:
#define string "world"
В таком случае мы хотели бы, чтобы SCons определила, что если содержимое файла hello.h поменялось, то программа должна быть перекомпилирована. Чтобы достичь этого, нам нужно модифицировать файл SConstruct следующим образом:
Program('hello.c', CPPPATH='.')
Переменная $CPPPATH говорит SCons, что нужно искать в текущей директории ('.') любые файлы, подключаемые из исходного кода C (из файлов *.c или *.h). С таким назначением в файле SConstruct произойдет следующее:
% scons -Q hello
cc -o hello.o -c -I. hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
% [изменение содержимого hello.h]
% scons -Q hello
cc -o hello.o -c -I. hello.c
cc -o hello hello.o
Во-первых, обратите внимание, что SCons составлен с аргументом -I. для директории '.' в переменной $CPPPATH, чтобы компиляция нашла файл hello.h в локальной директории.
Во вторых следует понимать, что SCons знает, что программа hello должна быть пересобрана, потому что она сканирует содержимое файла hello.c для обнаружения строк #include, которые показывают, какие файлы подключаются в компиляции. SCons записывает эти неявные зависимости target-файла. Следовательно, когда файл hello.h был изменен, SCons определит, что файл hello.c его подключает, и выполнит пересборку программы hello, которая зависит как от файла hello.c, так и от файла hello.h.
Как и переменная $LIBPATH, переменная $CPPPATH может содержать в себе список директорий, или строку, где в качестве разделителя используется системный символ (':' на операционных системах POSIX/Linux, ';' на Windows). В любом случае SCons создаст правильную командную строк опций так, чтобы следующий пример:
Кэширование неявных зависимостей. Сканирование каждого файла на предмет просмотра строк #include занимает некоторое время. При полной сборке большого проекта время сканирования обычно занимает малую часть от общего времени, затрачиваемого на сборку. Однако вы скорее всего заметите время сканирования, когда делаете полную пересборку или часть сборки большого проекта: SCons скорее всего потребуется дополнительное время для того, чтобы "подумать" о том, что должно быть построено, прежде чем выдать первую команду сборки (или решит, что все и так свежее, и ничего пересобирать не надо).
На практике сканирование файлов системой SCons экономит время по сравнению с потенциальным временем, теряемым на отслеживание тонких проблем, связанных с некорректными зависимостями. Тем не менее "время ожидания" когда SCons сканирует файлы, может раздражать некоторых разработчиков, ожидающих завершения построения. Следовательно, SCons дает вам возможность кеширования неявных зависимостей, которые нашло сканирование, для использования в последующих сборках. Кэширование можно активировать, указав опцию --implicit-cache в командной строке:
% scons -Q --implicit-cache hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
Если вы не хотите каждый раз вставлять --implicit-cache в командную строку, то можете задать это как поведение по умолчанию для своей сборки, вставив опцию implicit_cache в файл SConscript:
SetOption('implicit_cache', 1)
SCons по умолчанию не делает кэширование неявных зависимостей, потому что --implicit-cache заставляет SCons просто использовать неявные зависимости, сохраненные в момент последнего запуска, без какой-либо проверки, корректны ли эти зависимости. В частности, это означает, что --implicit-cache инструктирует SCons не делать пересборку "корректно" в следующих случаях:
● Когда используется --implicit-cache, SCons будет игнорировать любые изменения, которые могли быть сделаны в путях поиска (наподобие $CPPPATH или $LIBPATH). Это может привести к тому, что SCons не пересоберет файл, если изменение $CPPPATH нормально привело бы к использованию файла с таким же именем, но из другой директории. ● Когда используется --implicit-cache, SCons не определяет, был ли добавлен файл с таким же именем в директорию, которая находится раньше в путях поиска, чем каталог, в котором подключаемый файл был найден в последний раз.
Опция --implicit-deps-changed. Когда используются кэшированные неявные зависимости, иногда вы можете захотеть сделать "запуск с чистого листа", чтобы SCons заново сканировала файлы, для которых ранее был сделан кэш зависимостей. Например, если у вас есть только что установленная версия внешнего кода, который используется в компиляции, внешние файлы заголовков будут изменены, и ранее закешированные неявные зависимости устареют. Вы можете обновить их запуском SCons с опцией --implicit-deps-changed:
% scons -Q --implicit-deps-changed hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
В этом случае SCons перезапустит сканирование всех неявных зависимостей и кэш обновится новой информацией.
Опция --implicit-deps-unchanged. По умолчанию, когда кэшируются зависимости, SCons замечает, когда файл был изменен, и заново сканирует файл для чтения любой обновленной информации неявной зависимости. Однако иногда вы можете захотеть заставить SCons использовать кэшированные неявные зависимости, даже если исходные файлы поменялись. Это, например, может ускорить сборку, когда вы поменяли свой исходный файл, однако знаете, что не поменяли и один из файлов, подключаемых строками #include. В этом случае можете использовать опцию --implicit-deps-unchanged:
% scons -Q --implicit-deps-unchanged hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
В этом случае SCons будет подразумевать, что кэшированные неявные зависимости корректны, и не будет напрягаться на пересканирование измененных файлов. Для типовых сборок с небольшими, инкрементными изменениями исходных файлов экономия времени может быть не очень большая, но иногда бывает важна каждая мелочь для ускорения производительности.
Явные зависимости: функция Depends. Иногда файл зависит от другого файла, который не детектируется сканером SCons. Для такой ситуации SCons позволяет явно указать, что определенный файл зависит от другого файла, и должен быть пересобран всякий раз, как файл зависимости поменялся. Это указывается с помощью метода Depends:
В этом случае зависимость или зависимости будут собираться перед target (одним или несколькими):
% scons -Q hello
cc -c goodbye.c -o goodbye.o
cc -o goodbye goodbye.o
cc -c hello.c -o hello.o
cc -o hello hello.o
Зависимости из внешних файлов: функция ParseDepends. В SCons встроены сканеры для нескольких языков программирования. Иногда эти сканеры не могут извлечь некоторые неявные зависимости из-за ограниченной реализации сканера.
Следующий пример иллюстрирует случай, когда встроенный сканер языка C не может извлечь неявную зависимость от заголовочного файла.
#define FOO_HEADER < foo.h>
#include FOO_HEADER
intmain() {
return FOO;
}
Результат компиляции:
% scons -Q
cc -o hello.o -c -I. hello.c
cc -o hello hello.o
% [изменение содержимого foo.h]
% scons -Q
scons: `.' is up to date.
Судя по всему, сканер не знает о зависимости от заголовка. Не являясь полноценным препроцессором C, сканер не разворачивает макрос.
В этих случаях вы можете также использовать компилятор для извлечения неявных зависимостей. ParseDepends может делать анализ содержимого вывода компилятора в стиле Make, и явно установить все перечисленные зависимости.
Следующий пример использует ParseDepends для обработки генерируемого компилятором файла зависимости, который генерируется как побочный эффект при компиляции объектного файла:
Парсинг зависимостей из генерируемого компилятора *.d-файла создает зависимость "курица или яйцо", что приводит к нежелательным пересборкам:
% scons -Q
cc -o hello.o -c -MD -MF hello.d -I. hello.c
cc -o hello hello.o
% scons -Q --debug=explain
scons: rebuilding `hello.o' because `foo.h' is a new dependency
cc -o hello.o -c -MD -MF hello.d -I. hello.c
% scons -Q
scons: `.' is up to date.
На первом проходе генерируется файл зависимости во время компиляции объектного файла. В это время SCons не знает о зависимости от foo.h. На втором проходе объектный файл генерируется заново, потому что foo.h определяется как новая зависимость.
ParseDepends немедленно считывает указанный файл при своем вызове, и просто делает возврат, если такой файл не существует. Файл зависимости, сгенерированный во время процесса сборки, не будет автоматически проанализирован повторно. Следовательно, извлеченные компилятором зависимости не сохраняются в базу данных сигнатур во время одного и того же прохода сборки. Это ограничение реализации ParseDepends приводит к нежелательным перекомпиляциям. Таким образом, ParseDepends следует использовать только если сканеры для используемого языка недоступны, или недостаточно продвинутые для специфической задачи.
Игнорирование зависимостей: функция Ignore. Иногда есть смысл не пересобирать программу, даже если файл зависимости поменялся. В этом случае вы специально указываете SCons игнорировать зависимость, используя функцию Ignore:
% scons -Q hello
cc -c -o hello.o hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
% edit hello.h
[изменение содержимого hello.h]
% scons -Q hello
scons: `hello' is up to date.
Приведенный выше пример немного надуманный, потому что трудно представить реальную ситуацию, когда не нужно перекомпилировать hello, если файл hello.h поменялся. Более классический пример может быть, если программа hello собирается в директории, которая совместно используется несколькими системами, имеющими разные копии подключаемого файла stdio.h. В этом случае SCons заметит различия между копиями stdio.h разных систем, и пересоберет hello каждый раз, когда вы меняете систему. Избежать таких перестроений можно следующим образом:
Ignore может также использоваться для предотвращения сборки сгенерированного файла по умолчанию. Это связано с тем, что фактически директории зависят от их содержимого. Таким образом, чтобы игнорировать сгенерированный файл из сборки по умолчанию, вы указываете. что директория должна игнорировать сгенерированный файл. Обратите внимание, что файл все еще будет собираться, если пользователь специально запрашивает target в командной строке scons, или если файл это зависимость другого файла, который запрашивается и/или собирается по умолчанию.
% scons -Q
scons: `.' is up to date.
% scons -Q hello
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello
scons: `hello' is up to date.
Зависимость только от порядка сборки: функция Requires. Иногда может быть полезно указать, что определенный айли или директория должна, если это необходимо, быть собранной или созданной до того, как будет собран некоторый другой target-объект, но что изменения этого файла или директории не требуют перекомпиляции самого target. Такая взаимосвязь называется зависимостью только порядка сборки (order-only dependency), потому что она влияет только на порядок, в котором должны быть собраны объекты компиляции - зависимость перед target - но она не является строгой взаимосвязью зависимости, потому что не должен меняться в ответ на изменение в зависимом файле.
Для примера представим, что вы хотите создать файл каждый раз, когда запускаете сборку, который определяет время сборки, номер версии, и т. д., и который подключается в каждую собираемую программу. Содержимое файла версии будет меняться при каждом построении. Если вы укажете обычную взаимосвязь зависимости, то каждая программа, которая зависит от этого файла, будет перестраиваться каждый раз, когда вы запускаете SCons. Например, можно использовать некоторый код на Python в файле SConstruct, чтобы создать новый файл version.c со строкой, содержащей текущую дату каждый раз, когда запускается SCons, и затем линковать программу с результирующим объектным файлом путем перечисления version.c в списке исходного кода:
importtime
version_c_text ="""
char *date = "%s";
"""% time.ctime(time.time())
open('version.c', 'w').write(version_c_text)
hello = Program(['hello.c', 'version.c'])
Однако если мы перечислим version.c как реальный исходный файл, то хотя файл version.o будет перестраиваться каждый раз, когда мы запустим SCons (потому что файл SConstruct сам меняет содержимое version.c), и исполняемый файл hello будет заново слинкован каждый раз (потому что поменялся файл version.o):
% scons -Q hello
cc -o hello.o -c hello.c
cc -o version.o -c version.c
cc -o hello hello.o version.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
cc -o hello hello.o version.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
cc -o hello hello.o version.o
Обратите внимание, что для работы в предыдущем примере мы делаем приостановку (sleep) на одну секунду между каждым запуском, так что файл SConstruct создаст файл version.c со строкой времени, которая на одну секунду позже предыдущей.
Одним из решением будет использовать функцию Requires, чтобы указать, что version.o должен быть перестроен перед использованием на шаге линковки, но что изменения version.o на самом деле не должны привести к повторной линковке исполняемого файла hello:
Обратите внимание, что поскольку мы больше не перечисляем version.c в качестве одного из исходных файлов для программы hello, здесь должен применяться некоторый другой способ добавить его в командную строку линкера. Для этого примера мы подставляем имя объектного файла (извлеченного из списка version_obj, возвращенного вызовом сборщика Object) в переменную $LINKFLAGS, потому что $LINKFLAGS уже подключен в командную строку $LINKCOM.
С этими изменениями мы получим нужное поведение, когда выполняется заново линковка исполняемого hello только при изменении hello.c, даже если перестроен version.o (потому что файл SConstruct по-прежнему непосредственно
% scons -Q hello
cc -o version.o -c version.c
cc -o hello.o -c hello.c
cc -o hello version.o hello.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
scons: `hello' is up to date.
% sleep 1
% [изменение содержимого hello.c]
% scons -Q hello
cc -o version.o -c version.c
cc -o hello.o -c hello.c
cc -o hello version.o hello.o
% sleep 1
% scons -Q hello
cc -o version.o -c version.c
scons: `hello' is up to date.
Функция AlwaysBuild. На то, как SCons обрабатывает зависимости, также можно повлиять методом AlwaysBuild. Когда файл передан в метод AlwaysBuild, вот так:
hello = Program('hello.c')
AlwaysBuild(hello)
.. то указанный target-файл (в нашем примере hello) будет всегда считаться устаревшим (out-of-date), и будет пересобран всякий раз, когда этот target-файл оценивается при обработке графа зависмостей:
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q
cc -o hello hello.o
Функция AlwaysBuild имеет несколько вводящее в заблуждение имя, потому что её вызов не означает, что target-файл будет пересобран при каждом вызове SCons. Вместо этого AlwaysBuild означает, что target будет фактически перестраиваться всякий раз, когда target-файл встречается при оценке target-ов, указанных в командной строке (и их зависимостей). Поэтому указание другого target в командной строке, который не зависит от AlwaysBuild target, все еще будет пересобираться только если он устарел по отношению к своим зависимостям:
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o
% scons -Q hello.o
scons: `hello.o' is up to date.