Программирование ARM SCons: дополнительные возможности Sat, April 20 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

SCons: дополнительные возможности Печать
Добавил(а) microsin   

Псевдонимы целей (Alias Targets). Мы уже видели, как можно использовать функцию Alias, чтобы создать target с именем install:

env = Environment()
hello = env.Program('hello.c')
env.Install('/usr/bin', hello)
env.Alias('install', '/usr/bin')

Вы тогда можете использовать этот псевдоним (alias) в командной строке, чтобы более естественным образом указать SCons, что нужно провести установку файлов:

% scons -Q install
cc -o hello.o -c hello.c
cc -o hello hello.o
Install file: "hello" as "/usr/bin/hello"

Однако, как и другие методы сборщика, метод Alias вернет объект, представляющий собираемый псевдоним. Вы можете затем использовать этот объект как входной для другого сборщика. Это особенно полезно, если используется такой объект как входной для другого вызова сборщика Alias, что позволяет создать иерархию вложенных псевдонимов:

env = Environment()
p = env.Program('foo.c')
l = env.Library('bar.c')
env.Install('/usr/bin', p)
env.Install('/usr/lib', l)
ib = env.Alias('install-bin', '/usr/bin')
il = env.Alias('install-lib', '/usr/lib')
env.Alias('install', [ib, il])

Этот пример определяет отдельные псевдонимы install, install-bin и install-lib, позволяющие вам точнее управлять над тем, что будет установлено:

% scons -Q install-bin
cc -o foo.o -c foo.c
cc -o foo foo.o
Install file: "foo" as "/usr/bin/foo"
% scons -Q install-lib
cc -o bar.o -c bar.c
ar rc libbar.a bar.o
ranlib libbar.a
Install file: "libbar.a" as "/usr/lib/libbar.a"
% scons -Q -c /
Removed foo.o
Removed foo
Removed /usr/bin/foo
Removed bar.o
Removed libbar.a
Removed /usr/lib/libbar.a
% scons -Q install
cc -o foo.o -c foo.c
cc -o foo foo.o
Install file: "foo" as "/usr/bin/foo"
cc -o bar.o -c bar.c
ar rc libbar.a bar.o
ranlib libbar.a
Install file: "libbar.a" as "/usr/lib/libbar.a"

Мы уже рассматривали примеры сборки программ C и C++, чтобы продемонстрировать возможности SCons. Система SCons также поддерживает сборки программ Java, однако сборки Java обрабатываются несколько по-другому. Причина в том, что компилятор Java и инструментарий сборки выполняет свою работу способами, отличающимися от цепочек инструментов других языков программирования.

Сборка файлов Java Class: Java Builder. Базовая активность программирования на Java это взять один или большее количество файлов .java, содержащих исходный код Java, и вызвать компилятор Java, чтобы превратить их в один или большее количество .class-файлов. В системе SCons это делается указанием для сборщика Java Builder целевой директории (target directory), в которую будут помещены файлы .class, и директории исходного кода (source directory), которая содержит файлы .java:

Java('classes', 'src')

Если директория src содержит 3 файла исходного кода .java, то результат запуска SCons может выглядеть так:

% scons -Q
javac -d classes -sourcepath src src/Example1.java src/Example2.java src/Example3.java

SCons будет фактически искать в директории src дерево для всех файлов .java. Затем компилятор Java создаст необходимые файлы классов в подкаталоге classes, основываясь на именах, которые обнаружены для .java-файлов.

Как SCons обрабатывает зависимости Java. В дополнение к поиску по директории исходного кода на наличие файлов .java, SCons фактически пропускает файлы .java через урезанный парсер Java, который определяет, какие классы определены. Другими словами, SCons сама знает, без специальных для неё указаний, что генерируемые файлы .class будут сформированы вызовом javac. Итак, наш однострочный предыдущий пример из предыдущей секции:

Java('classes', 'src')

.. не только убедительно сообщает, что .class-файлы в подкаталоге classes находятся в актуальном состоянии (up-to-date):

% scons -Q
javac -d classes -sourcepath src src/Example1.java src/Example2.java src/Example3.java
% scons -Q classes
scons: `classes' is up to date.

.. но также удалит все созданные файлы .class, даже для внутренних классов, без необходимости указывать их вручную. Например, если файлы Example1.java и Example3.java оба определяют дополнительные классы, и класс, определенный в Example2.java, имеет внутренний класс, то запуск scons -c очистит также все файлы .class:

% scons -Q
javac -d classes -sourcepath src src/Example1.java src/Example2.java src/Example3.java
% scons -Q -c classes
Removed classes/Example1.class
Removed classes/AdditionalClass1.class
Removed classes/Example2$Inner2.class
Removed classes/Example2.class
Removed classes/Example3.class
Removed classes/AdditionalClass3.class

Чтобы гарантировать корректную обработку .class-зависимостей для всех случаев, нужно указать SCons какая используется версия Java. Это необходимо, потому что Java 1.5 поменял имена файлов .class для вложенных анонимных внутренних классов. Используйте construction-переменную JAVAVERSION, чтобы указать используемую версию. Для Java 1.6 однострочный пример может тогда быть определен следующим образом:

Java('classes', 'src', JAVAVERSION='1.6')

Для дополнительной информации см. страничку man-справки JAVAVERSION.

Сборка файлов Java-архива (.jar): Jar Builder. После сборки class-файлов их обычно собирают в архив Java (файл .jar), который делается вызовом сборщика Jar. Если вы хотите только собрать все class-файлы в подкаталоге, то можете только указать в файлы подкаталоге в качестве исходных для архива Jar:

Java(target = 'classes', source = 'src')
Jar(target = 'test.jar', source = 'classes')

Затем SCons передаст эту директорию команде jar, которая соберет все нижележащие .class-файлы в архив .jar:

% scons -Q
javac -d classes -sourcepath src src/Example1.java src/Example2.java src/Example3.java
jar cf test.jar classes

Если вы хотите держать все .class-файлы в одном месте, и архивировать в несколько .jar архивы только некоторые из них, то можете передать в сборщик Jar список исходных файлов. Таким способом очень просто можно создать несколько файлов .jar, используя списки файлов target class, созданные вызовом сборщика Java, для различных вызовов сборщика Jar:

prog1_class_files = Java(target = 'classes', source = 'prog1')
prog2_class_files = Java(target = 'classes', source = 'prog2')
Jar(target = 'prog1.jar', source = prog1_class_files)
Jar(target = 'prog2.jar', source = prog2_class_files)

Этот пример создаст prog1.jar и prog2.jar рядом с подкаталогами, которые содержат файлы .java:

% scons -Q
javac -d classes -sourcepath prog1 prog1/Example1.java prog1/Example2.java
javac -d classes -sourcepath prog2 prog2/Example3.java prog2/Example4.java
jar cf prog1.jar -C classes Example1.class -C classes Example2.class
jar cf prog2.jar -C classes Example3.class -C classes Example4.class

Сборка файлов C Header и Stub: JavaH Builder. Вы можете сгенерировать заголовок C и исходные файлы для реализации native-методов, используя сборщик JavaH. Существует несколько способов использования JavaH Builder. Один из типичных может выглядеть так:

classes = Java(target = 'classes', source = 'src/pkg/sub')
JavaH(target = 'native', source = classes)

Здесь source это список class-файлов, сгенерированных вызовом Java Builder, и target это выходная директория (цель), куда мы хотим поместить файлы заголовков C. Здесь target преобразовывается в -d, когда SCons запустит javah:

% scons -Q
javac -d classes -sourcepath src/pkg/sub src/pkg/sub/Example1.java src/pkg/sub/Example2.java
  src/pkg/sub/Example3.java
javah -d native -classpath classes pkg.sub.Example1 pkg.sub.Example2 pkg.sub.Example3

В этом примере вызов javah сгенерирует файлы заголовка native/pkg_sub_Example1.h, native/pkg_sub_Example2.h и native/pkg_sub_Example3.h. Обратите внимание, что SCons помнила, что class-файлы были сгенерированы с target-директорией classes, и что затем он указал этот целевой каталог как опцию -classpath для вызова javah.

Хотя более удобно использовать список class-файлов, возвращенных Java Builder, как исходные файлы для вызова JavaH Builder, вы можете при желании указать список class-файлов вручную. Если вы это делаете, то нужно установить construction-переменную $JAVACLASSDIR, когда вызываете JavaH:

Java(target='classes', source='src/pkg/sub')
class_file_list = [
    'classes/pkg/sub/Example1.class',
    'classes/pkg/sub/Example2.class',
    'classes/pkg/sub/Example3.class',
]
JavaH(target='native', source=class_file_list, JAVACLASSDIR='classes')

Затем значение $JAVACLASSDIR преобразуется в -classpath, когда SCons запускает javah:

% scons -Q
javac -d classes -sourcepath src/pkg/sub src/pkg/sub/Example1.java src/pkg/sub/Example2.java
  src/pkg/sub/Example3.java
javah -d native -classpath classes pkg.sub.Example1 pkg.sub.Example2 pkg.sub.Example3

И наконец, если вам не нужен отдельный заголовочный файл, генерируемый для каждого исходного файла, то можете явно указать File Node в качестве target для сборщика JavaH:

classes = Java(target='classes', source='src/pkg/sub')
JavaH(target=File('native.h'), source=classes)

Из-за того, что SCons по умолчанию подразумевает, что сборщика JavaH это директория, то нужно использовать функцию File, чтобы гарантировать, что SCons не создаст каталог с именем native.h. Хотя когда используется файл, SCons корректно преобразует имя файла в опцию -o утилиты javah:

% scons -Q
javac -d classes -sourcepath src/pkg/sub src/pkg/sub/Example1.java src/pkg/sub/Example2.java
  src/pkg/sub/Example3.java
javah -o native.h -classpath classes pkg.sub.Example1 pkg.sub.Example2 pkg.sub.Example3

Обратите внимание, что команда javah была удалена из таких версий JDK, как JDK 10, и разрешенный метод (доступный начиная с JDK 8) это использовать javac для генерации native-заголовков одновременно с компиляцией исходного кода Java. Так что сборщик JavaH имеет ограниченное применение в более поздних версиях Java.

Сборка файлов RMI Stub и Skeleton Class: RMIC Builder. Вы можете генерировать заглушки удаленного запуска метода (Remote Method Invocation stubs), используя сборщик RMIC. У него источник (source) это список директорий, обычно возвращенных из вызова сборщика Java, и target это выходная директория, где будут размещены файлы _Stub.class и _Skel.class:

classes = Java(target = 'classes', source = 'src/pkg/sub')
RMIC(target = 'outdir', source = classes)

Как это было и со сборщиком JavaH, система SCons помнит class-директорию, и передаст её через опцию -classpath для утилиты rmic:

% scons -Q
javac -d classes -sourcepath src/pkg/sub src/pkg/sub/Example1.java src/pkg/sub/Example2.java
rmic -d outdir -classpath classes pkg.sub.Example1 pkg.sub.Example2

Этот пример сгенерирует файлы outdir/pkg/sub/Example1_Skel.class, outdir/pkg/sub/Example1_Stub.class, outdir/pkg/sub/Example2_Skel.class и outdir/pkg/sub/Example2_Stub.class.

[Интернационализация и локализация вместе с gettext]

Набор инструментария gettext поддерживает интернационализацию и локализацию проектов, основанных на SCons. Сборщики, предоставленные gettext, автоматизируют генерацию и обновление файлов трансляции. Вы можете управлять шаблонами трансляции подобно тому, как это делается в autotools.

Обязательные требования. Чтобы заработали примеры, показанные в этой главе, настройте свою операционную систему для поддержки двух или большего количества языков. В следующих примерах используются локали en_US, de_DE и pl_PL.

Убедитесь, что утилиты GNU установлены в вашей системе.

Чтобы отредактировать файлы трансляции, вы можете захотеть установить редактор poedit.

Начнем с самого простого проекта, программы "Hello world".

/* hello.c */
#include < stdio.h>
 
int main(int argc, char* argv[])
{
   printf("Hello world\n");
   return 0;
}

Подготовим, как обычно, файл SConstruct для компиляции программы.

# SConstruct
env = Environment()
hello = Program(["hello.c"])

Теперь мы преобразуем проект в многоязычный. Если у вас еще не установлены утилиты GNU gettext, то установите их из вашего предпочтительного репозитория Linux, или загрузите по ссылке [2]. Чтобы заработал этот пример, у вас должны быть установлены в системе три локали: en_US, de_DE и pl_PL. К примеру, на Debian вы можете разрешить определенные локали через dpkg-reconfigure locales.

Сначала подготовьте программу hello.c для интернационализации. Поменяйте предыдущий код следующим образом:

/* hello.c */
#include < stdio.h>
#include < libintl.h>
#include < locale.h>
 
int main(int argc, char* argv[])
{
   bindtextdomain("hello", "locale");
   setlocale(LC_ALL, "");
   textdomain("hello");
   printf(gettext("Hello world\n"));
   return 0;
}

Подробные рецепты по таким преобразованиям см. по ссылке [3]. У функции gettext("...") два предназначения. Во-первых, она помечает сообщения для программы xgettext, которую мы будем использовать для извлечения из исходного кода сообщений локализации. Во-вторых она вызывает внутренности библиотеки gettext для перевода сообщений runtime.

Теперь нам нужно проинструктировать SCons, как генерировать и поддерживать файлы трансляции. Для этого используется сборщик Translate и сборщик MOFiles. Первый из них берет исходные файлы, извлекает из них интернационализируемые сообщения, создает так называемый POT-файл (шаблон перевода), и затем создает файлы перевода PO, по одному для каждого запрашиваемого языка. Позже, когда происходит работа по разработке ПО, сборщик поддерживает эти файлы актуальными. Сборщик MOFiles компилирует файлы PO в двоичный формат MO. Затем устанавливает файлы MO в директорию с именем locale.

Полный файл SConstruct получается такой:

# SConstruct
env = Environment( tools = ['default', 'gettext'] )
hello = env.Program(["hello.c"])
env['XGETTEXTFLAGS'] = [
  '--package-name=%s' % 'hello',
  '--package-version=%s' % '1.0',
]
po = env.Translate(["pl","en", "de"], ["hello.c"], POAUTOINIT = 1)
mo = env.MOFiles(po)
InstallAs(["locale/en/LC_MESSAGES/hello.mo"], ["en.mo"])
InstallAs(["locale/pl/LC_MESSAGES/hello.mo"], ["pl.mo"])
InstallAs(["locale/de/LC_MESSAGES/hello.mo"], ["de.mo"])

Сгенерируйте файлы трансляции с помощью команды scons po-update. Получится примерно такой вывод SCons:

user@host:$ scons po-update
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
Entering '/home/ptomulik/projects/tmp'
xgettext --package-name=hello --package-version=1.0 -o - hello.c
Leaving '/home/ptomulik/projects/tmp'
Writting 'messages.pot' (new file)
msginit --no-translator -l pl -i messages.pot -o pl.po
Created pl.po.
msginit --no-translator -l en -i messages.pot -o en.po
Created en.po.
msginit --no-translator -l de -i messages.pot -o de.po
Created de.po.
scons: done building targets.

Если все в порядке, то вы должны увидеть следующие новые файлы.

user@host:$ ls *.po*
de.po  en.po  messages.pot  pl.po

Отройте en.po в poedit, и предоставьте английский вариант сообщения для "Hello world\n". То же самое сделайте для de.po (deutsch, немецкий язык) и pl.po (polish, польский язык). Пусть перевод на языки будет следующим:

en: "Welcome to beautiful world!\n"
de: "Hallo Welt!\n"
pl: "Witaj swiecie!\n"

Теперь скомпилируем проект обычным запуском scons. Вывод должен быть подобен следующему:

user@host:$ scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
msgfmt -c -o de.mo de.po
msgfmt -c -o en.mo en.po
gcc -o hello.o -c hello.c
gcc -o hello hello.o
Install file: "de.mo" as "locale/de/LC_MESSAGES/hello.mo"
Install file: "en.mo" as "locale/en/LC_MESSAGES/hello.mo"
msgfmt -c -o pl.mo pl.po
Install file: "pl.mo" as "locale/pl/LC_MESSAGES/hello.mo"
scons: done building targets.

SCons автоматически скомпилирует файлы PO в двоичный формат MO, и строки InstallAs установят эти файлы в папку locale.

Теперь ваша программа должна быть готова. Можно попробовать её запустить (Linux):

user@host:$ LANG=en_US.UTF-8 ./hello
Welcome to beautiful world
user@host:$ LANG=de_DE.UTF-8 ./hello
Hallo Welt
user@host:$ LANG=pl_PL.UTF-8 ./hello
Witaj swiecie

Для демонстрации возможной дальнейшей эволюции файлов трансляции изменим польский перевод (poedit pl.po) на "Witaj drogi swiecie\n". Запустим scons и посмотрим, как SCons на это отреагирует:

user@host:$ scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
msgfmt -c -o pl.mo pl.po
Install file: "pl.mo" as "locale/pl/LC_MESSAGES/hello.mo"
scons: done building targets.

Откроем hello.c, и добавим одну строку с printf для нового сообщения.

/* hello.c */
#include < stdio.h>
#include < libintl.h>
#include < locale.h>
 
int main(int argc, char* argv[])
{
   bindtextdomain("hello", "locale");
   setlocale(LC_ALL, "");
   textdomain("hello");
   printf(gettext("Hello world\n"));
   printf(gettext("and good bye\n"));
   return 0;
}

Снова скомпилируем проект с помощью scons. На этот раз SCons использует программу msgmerge [3], чтобы обновить файл PO. Вывод компиляции получится примерно такой:

user@host:$ scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
Entering '/home/ptomulik/projects/tmp'
xgettext --package-name=hello --package-version=1.0 -o - hello.c
Leaving '/home/ptomulik/projects/tmp'
Writting 'messages.pot' (messages in file were outdated)
msgmerge --update de.po messages.pot
... done.
msgfmt -c -o de.mo de.po
msgmerge --update en.po messages.pot
... done.
msgfmt -c -o en.mo en.po
gcc -o hello.o -c hello.c
gcc -o hello hello.o
Install file: "de.mo" as "locale/de/LC_MESSAGES/hello.mo"
Install file: "en.mo" as "locale/en/LC_MESSAGES/hello.mo"
msgmerge --update pl.po messages.pot
... done.
msgfmt -c -o pl.mo pl.po
Install file: "pl.mo" as "locale/pl/LC_MESSAGES/hello.mo"
scons: done building targets.

Следующий пример показывает, что происходит, если мы меняем исходный код так, что интернационализируемые сообщения не меняются. В ответ ни один из файлов трансляции (POT, PO) не поменяется (т. е. не поменяется ни содержимое, ни дата создания/модификации). Добавим еще одну строку к программе (ниже последнего printf), код станет таким:

/* hello.c */
#include < stdio.h>
#include < libintl.h>
#include < locale.h>
 
int main(int argc, char* argv[])
{
   bindtextdomain("hello", "locale");
   setlocale(LC_ALL, "");
   textdomain("hello");
   printf(gettext("Hello world\n"));
   printf(gettext("and good bye\n"));
   printf("----------------\n");
   return a;
}

Скомпилируем проект, и увидим:

user@host:$ scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
Entering '/home/ptomulik/projects/tmp'
xgettext --package-name=hello --package-version=1.0 -o - hello.c
Leaving '/home/ptomulik/projects/tmp'
Not writting 'messages.pot' (messages in file found to be up-to-date)
gcc -o hello.o -c hello.c
gcc -o hello hello.o
scons: done building targets.

Как можно видеть, сообщения интернационализации не поменялись, потому что POT-файлы и остальные файлы трансляции остались неизменными.

[Дополнительная функциональность]

В этой главе описывается разные второстепенные функции SCons, которые не упоминались в других разделах этой документации.

Проверка версии Python: функция EnsurePythonVersion. Хотя сам код SCons будет работать на любой версии Python 2.x, 2.7 или более свежей, вы свободны в выборе синтаксиса Python и модулей из последних версий, когда пишете свои собственные файлы SConscript для локальных модулей. В этом случае полезно сконфигурировать SCons для корректного завершения, если произошел запуск с версией Python, которая не заработает с вашим кодом. Это особенно верно, когда используете SCons для сборки исходного кода, который планируете распространять публично, и нельзя быть заранее уверенным, на какой версии пользователь будет компилировать свое ПО.

Для этой цели SCons предоставляет функцию EnsurePythonVersion. Вы просто передаете в неё номера major и minor версии Python, который требуется исопользовать:

EnsurePythonVersion(2, 5)

В этом случае SCons завершит работу со следующим сообщением об ошибке, когда пользователь сделает запуск на неподдерживаемой, более старой версии Python:

% scons -Q
Python 2.5 or greater required, but you have Python 2.3.6

Проверка версии SCons: функция EnsureSConsVersion. Вы можете, конечно, писать свои файлы SConscript для использования функций, которые были добавлены только в свежих версиях SCons. Когда публикуется распространяемое ПО, компилируемое с помощью SCons, бывает полезным проверить версию SCons, которая используется при компиляции, и выполнить корректное завершение, если пользовательская версия SCons не заработает с вашими файлами SConscript. SCons предоставляет функцию EnsureSConsVersion, которая проверяет версию SCons в таком же стиле, как и функция EnsurePythonVersion проверяет версию Python, путем передачи номеров major и minor версии, которые требуются от SCons:

EnsureSConsVersion(1, 0)

И тогда SCons завершит работу со следующим сообщением, когда пользователь запустит компиляцию на неподдерживаемой, более старой версии, SCons:

% scons -Q
SCons 1.0 or greater required, but you have SCons 0.98.5

Явное завершение SCons при чтении файлов SConscript: функция Exit. SCons предоставляет функцию Exit, которая может использоваться для завершения SCons, когда она читает файлы SConscript, обычно из-за того, что вы обнаружили, что рабочие условия недопустимы:

if ARGUMENTS.get('FUTURE'):
    print("The FUTURE option is not supported yet!")
    Exit(2)
env = Environment()
env.Program('hello.c')

Результат запуска:

% scons -Q FUTURE=1
The FUTURE option is not supported yet!
% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o

Функция Exit принимает числовой аргумент, который будет показывать на выходе возвращаемое состояние SCons. Если вы не укажете значение, то по умолчанию выход будет со значением 0, что показывает успешное выполнение.

Обратите внимание, что функция Exit эквивалентна вызову Python-функции sys.exit (которую Exit фактически вызывает), однако потому что Exit это функция SCons, вам не нужно импортировать в скриптах SConscript модуль sys языка Python.

Поиск файлов: функция FindFile. Эта функция ищет файл в списке директорий. Если директория только одна, то она может быть передана в функцию обычной строкой. Функция FindFile возвратит узел файла (File node), если было найдено совпадение файла при поиске, или None, если файл не найден (см. документацию по функции Glob в качестве альтернативного способа поиска элементов в директории).

# одна директория
print("%s"%FindFile('missing', '.'))
t = FindFile('exists', '.')
print("%s %s"%(t.__class__, t))

Результат запуска:

% scons -Q
None
< class 'SCons.Node.FS.File'> exists
scons: `.' is up to date.

Пример для нескольких директорий:

includes = [ '.', 'include', 'src/include']
headers = [ 'nonesuch.h', 'config.h', 'private.h', 'dist.h']
for hdr in headers:
    print('%-12s: %s'%(hdr, FindFile(hdr, includes)))

Еще один запуск:

% scons -Q
nonesuch.h  : None
config.h    : config.h
rivate.h   : src/include/private.h
dist.h      : include/dist.h
scons: `.' is up to date.

Если файл существует в нескольких каталогах, то будет возвращено только первый обнаруженный результат.

print(FindFile('multiple', ['sub1', 'sub2', 'sub3']))
print(FindFile('multiple', ['sub2', 'sub3', 'sub1']))
print(FindFile('multiple', ['sub3', 'sub1', 'sub2']))

Результат запуска:

% scons -Q
sub1/multiple
sub2/multiple
sub3/multiple
scons: `.' is up to date.

В дополнение к существующим файлам FindFile также найдет производные файлы (т. е. не исходный код), которые еще не были собраны (исходный код должен существовать реально, иначе попытка сборки будет провальной!).

# Нет ни одного файла, поэтому сборка не получится
Command('derived', 'leaf', 'cat >$TARGET $SOURCE')
print(FindFile('leaf', '.'))
print(FindFile('derived', '.'))

Результат запуска:

% scons -Q
None
derived
scons: *** [derived] Source `leaf' not found, needed by target `derived'.

Еще пример, существует только 'leaf':

Command('derived', 'leaf', 'cat >$TARGET $SOURCE')
print(FindFile('leaf', '.'))
print(FindFile('derived', '.'))

Результат запуска:

% scons -Q
leaf
derived
cat > derived leaf

Если исходный файл существует, то FindFile корректно возвратит его имя в директории сборки.

# Существует только 'src/leaf'
VariantDir('build', 'src')
print(FindFile('leaf', 'build'))

Результат запуска:

% scons -Q
build/leaf
scons: `.' is up to date.

Обработка вложенных списков: функция Flatten. SCons поддерживает функцию Flatten, которая берет входную последовательность Python (список или кортеж), и возвращает плоский список, содержащий только отдельные элементы последовательности. Это может быть удобно при попытке проверить список, состоящий из списков, возвращенных различными сборщиками. Например, вы можете сделать коллекцию объектных файлов, собранных разными способами, и передать её в один вызов сборщика Program просто включив в список, следующим образом:

objects = [
    Object('prog1.c'),
    Object('prog2.c', CCFLAGS='-DFOO'),
]
Program(objects)

Поскольку сборщик вызывает flatten-функцию SCons, которая подготовит плоский список, это хорошо сработает для сборки конечной программы:

% scons -Q
cc -o prog1.o -c prog1.c
cc -o prog2.o -c -DFOO prog2.c
cc -o prog1 prog1.o prog2.o

Однако если происходит отладка сборки, и необходимо выводить абсолютные пути каждого объектного файла в списке objects, то можно попробовать следующий простой способ, печатающий в цикле атрибут abspath каждого Node:

objects = [
    Object('prog1.c'),
    Object('prog2.c', CCFLAGS='-DFOO'),
]
Program(objects)
 
for object_file in objects:
    print(object_file.abspath)

Это не сработает, как ожидалось, поскольку каждый вызов str работает со встроенным списком, возвращенным каждым вызовом Object, а не на нижележащих Node в этих списках:

% scons -Q
AttributeError: 'NodeList' object has no attribute 'abspath':
  File "/home/my/project/SConstruct", line 8:
    print(object_file.abspath)

Решение здесь будет в использовании функции Flatten, чтобы можно было передать каждый Node в вызов str по отдельности:

objects = [
    Object('prog1.c'),
    Object('prog2.c', CCFLAGS='-DFOO'),
]
Program(objects)
 
for object_file in Flatten(objects):
    print(object_file.abspath)

Результат запуска:

% scons -Q
/home/me/project/prog1.o
/home/me/project/prog2.o
cc -o prog1.o -c prog1.c
cc -o prog2.o -c -DFOO prog2.c
cc -o prog1 prog1.o prog2.o

Способ найти директорию запуска: функция GetLaunchDir. Если вам нужно выявить директорию, из которой пользователь запустил команду scons, то можете использовать функцию GetLaunchDir:

env = Environment(
    LAUNCHDIR = GetLaunchDir(),
)
env.Command('directory_build_info',
            '$LAUNCHDIR/build_info'
            Copy('$TARGET', '$SOURCE'))

Поскольку SCons обычно запускается из директории верхнего уровня, где находится файл SConstruct, то часто Python-функция os.getcwd() будет эквивалентной. Однако опции командной строки SCons -u, -U и -D, когда использовались из подкаталога, приведут к тому, что SCons поменяет директорию, где ищется файл SConstruct. Когда используются эти опции, функция GetLaunchDir все еще вернет путь, откуда пользователь осуществил запуск, позволяя конфигурации SConscript по-прежнему получать файлы конфигурации (или другие файлы) из исходной директории.

Декларация дополнительного вывода: функция SideEffect. Иногда способ определения action приводит к тому, что появляются файлы, которые SCons не распознает как цели target. Может использоваться метод SideEffect, чтобы информировать SCons о таких файлах. Это можно использовать только для того, чтобы пометить зависимость для использования на последующих шагах сборки, хотя для этого обычно есть способ получше. Основное использование метода SideEffect состоит в том, чтобы предотвратить одновременное изменение одного и того же файла (или предотвратить одновременный доступ к нему) двумя шагами сборки таким образом, чтобы эти шаги не повлияли друг на друга.

С этом примере правило для сборки file1 также поместит данные в лог, который используется как источник команды для генерации file2, но лог неизвестен для SCons при очистке (clean): он не существует, и не является выводом target ни одного из сборщиков. SConscript поэтому использует SideEffect для оповещения SCons о таком дополнительном выходном файле.

env = Environment()
f2 = env.Command(
    target='file2',
    source='log',
    action=Copy('$TARGET', '$SOURCE')
)
f1 = env.Command(
    target='file1',
    source=[],
    action='echo >$TARGET data1; echo >log updated file1'
)
env.SideEffect('log', f1)

Без SideEffect эта сборка не сработает с сообщением "Source 'log' not found", потому что лог нужен для цели file2, но теперь это сработает:

% scons -Q
echo > file1 data1; echo >log updated file1
Copy("file2", "log")

Однако было бы лучше по-настоящему идентифицировать log как target, поскольку в нашем случае это так и есть:

env = Environment()
f2 = env.Command(
    target='file2',
    source='log',
    action=Copy('$TARGET', '$SOURCE')
)
f1 = env.Command(
    target=['file1', 'log'],
    source=[],
    action='echo >$TARGET data1; echo >log updated file1'
)

Результат запуска:

% scons -Q
echo > file1 data1; echo >log updated file1
Copy("file2", "log")

Функция SideEffect как правило не предназначена для тех случаев, когда команда создает дополнительные файлы target (т. е. файлы, которые будут использоваться как исходные для других шагов сборки). Например, компилятор Microsoft Visual C/C++ может выполнять инкрементальную линковку, для чего он использует status-файл - такой, что линковка foo.exe также производит foo.ilk, или использует его, если он уже присутствовал в том случае, когда предоставлена опция /INCREMENTAL. Указание foo.ilk как побочного генерируемого файла при формировании foo.exe не рекомендуется для SideEffect, поскольку foo.ilk используется для линковки. SCons при анализе графа зависимостей обрабатывает побочно генерируемые файлы несколько иначе. Когда команда генерирует несколько выходных файлов, они должны быть указаны как несколько целей target вызова соответствующей функции сборщика. Саму функцию SideEffect следует использовать только когда важно обеспечить , чтобы команды не выполнялись параллельно, например когда "периферийный" файл (такой как файл лога) может быть фактически обновлен несколькими вызовами команд.

К сожалению утилита, которая настраивает сборщик Program для цепочки компиляторов MSVC, не поставляется с полным пониманием деталей примера с файлом .ilk - что список target должен быть изменен при наличии какой-то определенной опции компилятора. В отличие от тривиального примера выше, где мы могли просто указать сборщику Command, что были две цели target для действия, изменение цепочки событий для такого сборщика, как Program, хотя не является по сути сложной, определенно относятся к продвинутому использованию SCons. Для быстрого старта использовать здесь SideEffect это нормально, пока есть понимание, что это "не совсем правильно". Возможно хорошей идеей будет оставить в этом месте скрипта комментарий в качестве напоминания, что SideEffect в этом месте потенциально может оказаться источником проблем.

Таким образом, если основное использование состоит в предотвращении проблем параллелизма, ниже для иллюстрации показан соответствующий пример. Например, программа, которая должна быть вызвана для сборки target-файла, будет также обновлять файл лога, описывающего действия программы при создании этого target-файла. В следующей конфигурации SCons будет вызывать гипотетический скрипт с именем build (находящийся в локальной директории) с аргументами командной строки, указывающими на запись информации лога в общий файл logfile.txt:

env = Environment()
env.Command(
    target='file1.out',
    source='file1.in',
    action='./build --log logfile.txt $SOURCE $TARGET'
)
env.Command(
    target='file2.out',
    source='file2.in',
    action='./build --log logfile.txt $SOURCE $TARGET'
)

Это может создать проблемы, когда build запустится параллельно, если SCons решить обновить обе цели запуском обоих программ одновременно. Эти запуски программы могут пересечься при записи в общий файл лога, что в лучшем случае приведет к перемешанным, непонятным записям в лог, и в худшем случае сборка вообще не получится (последнее относится к системам наподобие Windows, где открыть файл на запись в одно и то же время может только один процесс).

Можно обеспечить, чтобы SCons не запускала эти команды одновременно, используя функцию SideEffect, чтобы указать, что обновление файла logfile.txt является побочным действием при сборке указанных целевых файлов file1 и file2:

env = Environment()
f1 = env.Command(
    target='file1.out',
    source='file1.in',
    action='./build --log logfile.txt $SOURCE $TARGET'
)
f2 = env.Command(
    target='file2.out',
    source='file2.in',
    action='./build --log logfile.txt $SOURCE $TARGET'
)
env.SideEffect('logfile.txt', f1 + f2)

Это гарантирует, что два шага запуска ./build будут выполнены последовательно, даже когда в командной строке была использована опция многопоточности --jobs=2:

% scons -Q --jobs=2
./build --log logfile.txt file1.in file1.out
./build --log logfile.txt file2.in file2.out

Функция SideEffect может быть вызвана несколько раз для одного и того же побочного файла. Фактически имя файла, используемого в SideEffect, даже не обязательно должно обозначать существование объекта как файла на диске - SCons все равно будет понимать, что соответствующие target будут обрабатываться последовательно, но никак не параллельно. Побочный файл является по сути pseudo-target, и SCons в основном заботится о том, перечислены ли node, зависящие от побочного файла, а не о его содержании.

env = Environment()
f1 = env.Command('file1.out', [], action='echo >$TARGET data1')
env.SideEffect('not_really_updated', f1)
f2 = env.Command('file2.out', [], action='echo >$TARGET data2')
env.SideEffect('not_really_updated', f2)

Результат запуска:

% scons -Q --jobs=2
echo > file1.out data1
echo > file2.out data2

Виртуальные окружения (virtualenv). Virtualenv это инструментарий для создания изолированных рабочих окружений Python. Приложение Python (такое как SCons) может выполняться в активированном virtualenv. Активация virtualenv модифицирует текущее окружение путем определения некоторых специфичных для virtualenv переменных, и путем модификации переменной PATH для путей поиска запускаемого кода, так что исполняемые бинарники, установленные в домашней директории virtualenv, получают предпочтение для запуска над другими бинарниками, установленными вне virtualenv.

Обычно SCons использует жестко закодированную переменную PATH, когда ищет внешние исполняемые бинарники (например, внешний кросс-компилятор), так что всегда будут запущены те исполняемые файлы, которые были найдены в этих предварительно определенных каталогах PATH. Это также применяется и к интерпретатору скриптов python, который запускается некоторыми своими инструментами SCons или наборами тестов. Это означает, что когда SCons запускается в virtualenv, вероятный вызов интерпретатора python из скрипта SCons скорее всего выскочит из virtualenv и запустит бинарник python, найденный в жестко закодированном PATH, а не в том, с которым запускалась SCons. Некоторые пользователи могут рассматривать это как несогласованность.

Эту проблему можно решить опцией --enable-virtualenv. Опция автоматически импортирует переменные окружения, связанные с virtualenv, для всех создаваемых construction-окружений env['ENV'], и модифицирует SCons PATH правильным образом, чтобы учесть предпочтения virtualenv для поиска исполняемого кода. Установка переменной окружения SCONS_ENABLE_VIRTUALENV=1 дает тот же эффект. Если же поддержка virtualenv разрешена на уровне системной переменной окружения, то эта поддержка virtualenv может быть подавлена опцией --ignore-virtualenv.

Внутри SConscript доступна глобальная функция Virtualenv. Она возвратит путь домашней директории virtualenv или None, если команда scons не была запущена из virtualenv. Обратите внимание, что эта функция вернет путь, таже если scons выполняется из неактивированного virtualenv.

[Использование SCons с другими инструментами сборки]

Иногда проект должен разными способами взаимодействовать с другими проектами. Например, множество open source проектов используют компоненты из других open source проектов, и хотят их использовать в том виде, как они были выпущены в релиз, без лишней перекомпиляции под управлением SCons. Как другой пример, иногда гибкость и продвинутые возможности SCons полезны для управления проектом в целом, но разработчики хотят получить ускоренные инкрементные сборки, когда были сделаны небольшие изменения другим инструментарием сборки.

В этой главе рассматриваются некоторое техники взаимодействия с другими проектами и инструментарием из среды SCons.

Создание базы данных компиляции. Инструментарий для выполнения анализа и модификации исходного кода часто нужен не только для самого исходного кода, но также для того, чтобы знать, как код компилируется, поскольку строка компиляции влияет на поведение макросов, операторов подключения #includes, и т. д. SCons записывает эту информацию, когда запускается, в форме действий (Actions), связанных с исходниками, и может выдать эту информацию так, что инструменты могут её использовать.

Проект Clang был определен как база данных компиляции в формате JSON (JSON Compilation Database). Эта база данных обычно используется как входные данные для инструментов Clang, и также для многих IDE и редакторов кода (для дополнительной информации см. описание формата JSON Compilation Database [4]). SCons может выдать базу данных компиляции в этом формате, чтобы разрешить инструмент compilation_db и вызов сборщика CompilationDatabase (доступно начиная с версии scons 4.0).

База данных компиляции может быть заполнена исходными и выходными файлами либо с путями относительно верхнего уровня каталога сборки, либо с абсолютными путями файлов. Это управляется переменной COMPILATIONDB_USE_ABSPATH=(True|False), которая по умолчанию False (соответствует относительным путям). Элементы в этом файле могут быть отфильтрованы с помощью COMPILATIONDB_PATH_FILTER='pattern' где шаблон фильтра pattern это строка, следующая Python-синтаксису fnmatch. Эта фильтрация может использоваться для вывода различных вариантов сборки для разных файлов базы данных компиляции.

Следующий пример показывает генерацию базы данных компиляции, содержащей абсолютные пути:

env = Environment(COMPILATIONDB_USE_ABSPATH=True)
env.Tool('compilation_db')
env.CompilationDatabase()
env.Program('hello.c')

Результат запуска:

% scons -Q
Building compilation database compile_commands.json
cc -o hello.o -c hello.c
cc -o hello hello.o

Файл compile_commands.json содержит:

[
    {
        "command": "gcc -o hello.o -c hello.c",
        "directory": "/home/user/sandbox",
        "file": "/home/user/sandbox/hello.c",
        "output": "/home/user/sandbox/hello.o"
    }
]

Обратите внимание, что сгенерированная база данных содержит только элемент пары hello.c/hello.o, и ничего для генерации конечного исполняемого файла hello - преобразование объектного файла hello.o в двоичный исполняемый файл hello не содержит никакой информации, которая влияет на интерпретацию исходного кода, так что для создания базы данных компиляции это не интересно.

Хотя на первый взгляд может быть немного удивительно, что target базы данных компиляции, как и любая другая цель, подчиняется правилам выбора цели scons. Это значит, что если вы установите цель по умолчанию default target (что не включает базу данных компиляции), или цели командной строки (command-line targets), то они могут быть не выбраны для сборки. Это может быть фактически преимуществом, поскольку вам не обязательно нужно перегенерировать базу данных компиляции для каждой сборки. Следующий пример показывает использование относительных путей (что установлено по умолчанию) для выходных файлов и исходников, при этом базе данных дается имя не по умолчанию. Чтобы иметь возможность генерации базы данных отдельно от сборки, установлен псевдоним (alias), относящийся к базе данных, который может быть затем использован как target - здесь мы только собираем target базы данных компиляции, но не код.

env = Environment()
env.Tool('compilation_db')
cdb = env.CompilationDatabase('compile_database.json')
Alias('cdb', cdb)
env.Program('test_main.c')

Результат запуска:

% scons -Q cdb
Building compilation database compile_database.json

Файл базы данных содержит:

[
    {
        "command": "gcc -o test_main.o -c test_main.c",
        "directory": "/home/user/sandbox",
        "file": "test_main.c",
        "output": "test_main.o"
    }
]

Следующий (неполный) пример показывает использование фильтрации для отдельных вариантов сборки. В случае использования вариантов вы хотите получить для каждого варианта отдельные базы данных, поскольку параметры сборки отличаются, так что анализ кода нуждается в просмотре корректных строк сборки для 32-битной и для 64-битной сборки, которые здесь показаны. Для упрощения презентации пример опускает подробности настройки директорий вариантов:

env = Environment()
env.Tool("compilation_db")
 
env1 = env.Clone()
env1["COMPILATIONDB_PATH_FILTER"] = "build/linux32/*"
env1.CompilationDatabase("compile_commands-linux32.json")
 
env2 = env.Clone()
env2["COMPILATIONDB_PATH_FILTER"] = "build/linux64/*"
env2.CompilationDatabase('compile_commands-linux64.json')

Файл compile_commands-linux32.json содержит:

[
    {
        "command": "gcc -o hello.o -c hello.c",
        "directory": "/home/mats/github/scons/exp/compdb",
        "file": "hello.c",
        "output": "hello.o"
    }
]

Файл compile_commands-linux64.json содежит:

[
    {
        "command": "gcc -m64 -o build/linux64/test_main.o -c test_main.c",
        "directory": "/home/user/sandbox",
        "file": "test_main.c",
        "output": "build/linux64/test_main.o"
    }
]

Ninja Build Generator. Ninja это маленькая система сборки, которая пытается работать быстро, не предпринимая никаких решений. SCons может быть иногда быть медленной, поскольку предпринимает много решений для обеспечения корректной работы. Эти два инструмента могут быть спарены, чтобы получить выгоду для некоторых сценариев сборки: при использовании ninja-инструментария SCons может сгенерировать файл сборки, который использует ninja (принимая основные решения и записывая их для ninja), и может запускать ninja для выполнения сборки. Это хорошо работает для ситуаций, где взаимные связи не меняются, когда просто повторяются итерации процедур "редактирование кода / сборка / отладка", что предоставляет ощутимый выигрыш в ускорении сборок сложных проектов. Подразумевается, что если осуществляются большие изменения, то ninja не подойдет - однако вы можете все равно использовать SCons для регенерации файла сборки. Это не рекомендуется использовать для производственных сборок.

Примечание: это новая, экспериментальная возможность, которая может быть изменена или даже удалена без процедуры устаревания кода.

Загрузка инструментария ninja в SCons приведет к значительным изменениям в обычном функционировании SCons. Теперь SCons больше не будет выполнять никакие команды напрямую, и только лишь создаст файл build.ninja и запустит ninja. Любые цели, указанные в командной строке, будут переданы в ninja.

Чтобы разрешить эту функцию, нужно использовать одно из следующего:

• Запустить в командной строке --experimental=ninja.
• Или в файле SConstruct выполнить SetOption('experimental', 'ninja').

Для использования инструментария ninja сначала надо установить пакет Python ninja, поскольку инструмент зависит от возможности импорта этого пакета. Установку можно выполнить следующими командами.

В окружении virtualenv, или в обычной среде выполнения python:

$ python -m pip install ninja

На Windows, с использованием Python launcher:

C:\>py -m pip install ninja

В среде Anaconda:

$ conda install -c conda-forge ninja

Помните, что как и в случае любого другого инструмента, не используемого по умолчанию, его надо предварительно инициализировать, например env.Tool('ninja').

Не ожидается, что с этого момента для всех сборок начнет работу сборщик Ninja. Этот функционал все еще находится в активной разработке. Если обнаружится, что это сборка не работает вместе с ninja, то сообщите о проблеме в mailing list пользователей, или в канал #scons-help сервера Discord разработчиков scons.

В частности, если в вашей сборки есть много (либо даже какие-нибудь) действий функции Python (action), то может обнаружиться, что сборка ninja будет происходить медленнее, потому что будет запускаться ninja, которая затем запустит SCons для каждой target, созданной Python action. Чтобы смягчить некоторые из них, особенно действия, основанные на Python, встроенные в SCons, существует специальная логика реализации этих действий через команды шелла в файле сборки ninja.

Когда ninja запускается вместе с сгенерированным для неё файлом сборки, ninja запустит scons как демон, будет передавать команды в этот процесс scons, которые ninja не может построить напрямую. Этот демон будет оставаться в работе по момента явного завершения, или до таймаута. Таймаут устанавливается через $NINJA_SCONS_DAEMON_KEEP_ALIVE.

Демон будет перезапущен, если любой файл (файлы) SConscript поменяется, или поменяется сборка каким-либо образом, что обнаружит ninja, и потребуется регенерация файла build.ninja.

Подробную информацию Ninja см. по ссылкам [5, 6].

[Ссылки]

1SCons: руководство пользователя, быстрый старт.
2. gnu/gettext http://ftp.gnu.org/gnu/gettext/.
3. gettext Preparing Program Sources site:gnu.org.
4. JSON Compilation Database Format Specification site:clang.llvm.org.
5. Ninja Build System site:ninja-build.org.
6. Ninja File Format Specification site:ninja-build.org.

 

Добавить комментарий


Защитный код
Обновить

Top of Page