Программирование ARM SCons: иерархические сборки Sat, April 20 2024  

Поделиться

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

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

SCons: иерархические сборки Печать
Добавил(а) microsin   

Исходный код больших проектов редко находится в одной директории, и почти всегда распределен по иерархии директорий. Организация сборки крупного ПО с использованием SCons предполагает создание иерархии скриптов, используя функцию SConscript.

Файлы SConscript. Как мы уже видели, скрипт сборки на верхнем уровне дерева называется SConstruct. Этот файл верхнего уровня SConstruct может использовать фукцию SConscript для подключения в сборку других дочерних скиптов. Эти дочерние скрипты могут, в свою очередь, использовать функцию SConscript для подключения в сборку других скриптов. По соглашению эти дочерние скрипты обычно называются SConscript. Например, файл SConstruct верхнего уровня может обеспечить подключение в сборку четырех дочерних скриптов следующим образом:

SConscript(['drivers/display/SConscript',
            'drivers/mouse/SConscript',
            'parser/SConscript',
            'utilities/SConscript'])

В этом случае файл SConstruct явно перечисляет все файлы SConscript в сборке (однако имейте в виду, что не каждая директория в дереве обязательно содержит файл SConscript). Альтернативно подкаталог drivers может содержать промежуточный файл SConscript, в этом случае вызов SConscript в файле верхнего уровня SConstruct будет выглядеть следующим образом:

SConscript(['drivers/SConscript',
            'parser/SConscript',
            'utilities/SConscript'])

И дочерний файл SConscript в поддиректории drivers может выглядеть примерно так:

SConscript(['display/SConscript',
            'mouse/SConscript'])

Перечисление всех файлов SConscript в файле SConstruct верхнего уровня, или размещение дочернего файла SConscript в промежуточных каталогах, или использование некоторого сочетания этих двух схем зависит от вас и потребностей вашего ПО.

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

Для примера предположим, что нужно собрать две программы prog1 и prog2 в двух отдельных директориях с такими же именами, как имена файлов программ. Один из типовых методов для этого - создать примерно такой файл SConstruct верхнего уровня:

SConscript(['prog1/SConscript', 'prog2/SConscript'])

И дочерние файлы SConscript создаются следующим образом. Первый файл:

env = Environment()
env.Program('prog1', ['main.c', 'foo1.c', 'foo2.c'])

И второй файл:

env = Environment()
env.Program('prog2', ['main.c', 'bar1.c', 'bar2.c'])

Затем, когда мы запустим SCons в директории верхнего уровня, сборка будет выглядеть так:

% scons -Q
cc -o prog1/foo1.o -c prog1/foo1.c
cc -o prog1/foo2.o -c prog1/foo2.c
cc -o prog1/main.o -c prog1/main.c
cc -o prog1/prog1 prog1/main.o prog1/foo1.o prog1/foo2.o
cc -o prog2/bar1.o -c prog2/bar1.c
cc -o prog2/bar2.o -c prog2/bar2.c
cc -o prog2/main.o -c prog2/main.c
cc -o prog2/prog2 prog2/main.o prog2/bar1.o prog2/bar2.o

Обатите внимание на следующее: во-первых, можно использовать файлы с одинаковыми именами, находящимися в нескольких директориях, как в этом примере файлы main.c. Во-вторых, в отличие от стандартного рекурсивного использования Make, SCons остается в директории верхнего уровня (где живет файл SConstruct), и выдает команды, которые используют имена путей от директории верхнего уровня для target-файлов и файлов исходного кода, находящихся в иерархии сборки.

Имена путей в дочерних файлах относительно верхнего каталога. Если вам надо использовать файл их другой директории, иногда более удобно указать путь до файла в другой директории относительно каталога верхнего уровня SConstruct, даже когда вы используете файл в подкаталоге дочернего файла SConscript. Вы можете указать SCons интерпретировать имя пути относительно директории верхнего уровня SConstruct, не в локальной директории файла SConscript, если подставить спереди пути символ #. Вот пример такого файла SConscript:

env = Environment()
env.Program('prog', ['main.c', '#lib/foo1.c', 'foo2.c'])

В этом примере директория lib находится непосредствено в каталоге верхнего уровня SConstruct. Если показанный выше файл SConscript находится в подкаталоге src/prog, то вывод будет выглядеть так:

% scons -Q
cc -o lib/foo1.o -c lib/foo1.c
cc -o src/prog/foo2.o -c src/prog/foo2.c
cc -o src/prog/main.o -c src/prog/main.c
cc -o src/prog/prog src/prog/main.o lib/foo1.o src/prog/foo2.o

Обратите внимание, что объектный файл lib/foo1.o собран в том же каталоге, что и исходный файл foo1.c. См. далее главу "Разделение деревьев исходного кода и результатов сборки: Variant Directories" для информации, как сохранять объектный файл в другом подкаталоге.

Несколько замечаний по путям относительно каталога верхнего уровня:

● SCons не заботится о том, добавили ли вы слеш после #. Некоторые программисты считают, что '#/lib/foo1.c' более удобочитаемо, чем '#lib/foo1.c', однако функционально это эквиваленты.
● Синтаксис каталога относительно верхнего уровня # анализируется только SCons, сам язык Python это не понимает. Это становится особенно очевидным, когда вы хотите использовать print для отладки, или пишете функцию Python, которая вычисляет путь. Вы можете заставить SCons вычислить путь относительно верхнего уровня путем создания из него объекта Node:

path = "#/include"
 
print("path =", path)
print("force-interpreted path =", Entry(path))

Результат работы:

% scons -Q
path = #/include
force-interpreted path = include
scons: `.' is up to date.

Абсолютные пути. Конечно, вы можете всегда указать абсолютный путь до файла, например:

env = Environment()
env.Program('prog', ['main.c', '/usr/joe/lib/foo1.c', 'foo2.c'])

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

% scons -Q
cc -o src/prog/foo2.o -c src/prog/foo2.c
cc -o src/prog/main.o -c src/prog/main.c
cc -o /usr/joe/lib/foo1.o -c /usr/joe/lib/foo1.c
cc -o src/prog/prog src/prog/main.o /usr/joe/lib/foo1.o src/prog/foo2.o

Как было в случае имен путей относительно каталога верхнего уровня, объектный файл /usr/joe/lib/foo1.o будет собран в том же каталоге, что и исходный файл foo1.c. См. далее главу "Разделение деревьев исходного кода и результатов сборки: Variant Directories" для информации, как сохранять объектный файл в другом подкаталоге.

Общие окружения (и другие переменные) между файлами SConscript. В предыдущем примере каждый из дочерних файлов SConscript создал собственную среду construction-окружения путем раздельных вызовов Environment. Очевидно, что это хорошо работает, но если каждая программа собирается с одинаковыми construction-переменными, то утомительно и потенциально чревато ошибками инициализировать раздельные construction-окружения одинаковым образом, для каждого дочернего файла SConscript.

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

Экспорт переменных. Есть два способа экспорта переменной из файла SConscript. Первый из них - вызов функции Export. функция Export очень гибкая - в самой простой форме вы передаете строку, которая представляет имя переменной, и Export сохранит её вместе со знаением:

env = Environment()
Export('env')

Вы можете экспортировать за один раз больше одного имени переменной:

env = Environment()
debug = ARGUMENTS['debug']
Export('env', 'debug')

Из-за того, что идентификаторы Python не могут содержать в себе пробелы, функция Export подразумевает, что строка, содежащая пробелы, это шорткат для указания нескольких имен переменных для экспорта:

env = Environment()
debug = ARGUMENTS['debug']
Export('env debug')

Вы также можете передать функции Export словарь значений. Эта форма позволяе экспортировать переменную из текущей области под другим именем. В примере ниже значение foo экспортируется под именем "bar":

env = Environment()
foo = "FOO"
args = {"env": env, "bar": foo}
Export(args)

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

Export(MODE="DEBUG", TARGET="arm")

Стили можно микшировать, хотя синтаксис вызовов функций Python требует, чтобы в вызове все аргументы без ключевого слова предшествовали аргументу с ключевым словом.

Функция Export добавляет переменные в глобальное хранилище, из которого другие файлы SConscript могут сделать импорт переменных. Вызовы Export кумулятивные. Когда вы вызываете Export, то реально обновляете словарь Python, так что нормально экспортировать одну и ту же переменную, которая уже была ранее экспортирована, однако при таком раскладе значение из предыдущего вызова Export будет перезаписано.

Другой способ экспорта - можно указать список переменных в качестве второго аргумента вызова функции SConscript:

SConscript('src/SConscript', 'env')

Или (что предпочтительнее для читаемости), с использованием аргумента с ключевым словом exports:

SConscript('src/SConscript', exports='env')

Эти вызовы экспортируют указанные переменные только в перечисленный файл (или файлы) SConscript. Вы можете указать больше одного файла SConscript в списке:

SConscript(['src1/SConscript',
            'src2/SConscript'], exports='env')

Этот функционал эквивалентен вызову функции SConscript несколько раз с одинаковым аргументом exports, по одному вызову на каждый файл SConscript.

Импорт переменных. Как только переменная была экспортирована из вызова в файле SConscript, она может использоваться в других файлах SConscript путем вызова функции Import:

Import('env')
env.Program('prog', ['prog.c'])

Вызов Import в этом примере делает определенную ранее переменную env доступной для использования в файле SConscript. Если предположить, что env это construction-окружение, после импорта его можно использовать для сборки программ, библиотек, и т. п. Такое использование передачи construction-окружения почти всегда используется в больших сборках scons.

Как и функция Export, функция Import может быть вызвана с несколькими именами переменных:

Import('env', 'debug')
env = env.Clone(DEBUG=debug)
env.Program('prog', ['prog.c'])

В этом примере мы передали общее construction-окружение env, и используем значение переменной debug, чтобы модифицированную копию передачей её в вызов Clone.

Функция Import будет (как и Export) разделять входную строку с пробелами как отдельные имена переменных:

Import('env debug')
env = env.Clone(DEBUG=debug)
env.Program('prog', ['prog.c'])

Import предпочитает локальное определение глобальному, так что если существует глобальный экспорт foo, и вызывающий SConscript экспортировал foo в этот SConscript, то импорт найдет foo, экспортированную в этот SConscript.

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

Import('*')
env = env.Clone(DEBUG=debug)
env.Program('prog', ['prog.c'])

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

Возврат значений из файла SConscript. Иногда может понадобится каким-либо способом использовать информацию из дочернего файла SConscript. Для примера предположим, что вы хотите создать одну библиотеку из объектных файлов, собранных несколькими дочерними файлами SConscript. Это можно сделать функцией Return для возврата значений из дочерних файлов в вызывающий файл. Наподобие Import и Export, функция Return принимает строковое представление имени переменной, но не само имя переменной.

Если, например, у нас есть два подкаталога foo и bar, каждый из которых должен предоставить объектный файл для библиотеки, то следующее, что мы хотели бы сделать, это собрать объектные файлы из вызовов дочерних SConscript примерно так:

env = Environment()
Export('env')
objs = []
for subdir in ['foo', 'bar']:
    o = SConscript('%s/SConscript' % subdir)
    objs.append(o)
env.Library('prog', objs)

Мы можем это сделать, используя функцию WReturn в файле foo/SConscript следующим образом:

Import('env')
obj = env.Object('foo.c')
Return('obj')

Содержимое соответствующего файла bar/SConscript очевидно будет аналогичным. Затем когда мы запустим SCons, объектные файлы из дочерних подкаталогов будут корректно архивированы в желаемую библиотеку:

% scons -Q
cc -o bar/bar.o -c bar/bar.c
cc -o foo/foo.o -c foo/foo.c
ar rc libprog.a foo/foo.o bar/bar.o
ranlib libprog.a

[Разделение деревьев исходного кода и результатов сборки: Variant Directories]

Часто полезно разделять временные файлы сборки и исходный код. Рассмотрим случай, когда собирается ПО для различных вариантов аппаратуры контроллера. Эти платы могут совместно использовать часть кода, так что имеет смысл держать этот код в одном и том же дереве исходного кода, однако некоторые опции в исходном коде и заголовочных файлах отличаются. Если вы делаете сначала сборку "Controller A", затем сборку "Controller B", то контроллер "Controller B" каждый раз будет будет пересобираться, потому что SCons обнаружит, что инструкции сборки для каждой цели будут отличаться. Теперь если вернуться обратно к сборке "Controller A", то будет заново выполнена пересборка по той же причине. Однако если разделить места для выходных файлов, то этой проблемы можно избежать. Вы можете даже настроить выполнение обоих сборок сразу одним запуском SCons.

Можно реализовать такое разделение, путем установки одной или нескольких деревьев директорий вариантов (variant directory), используемых в сборке, и таким образом предоставления уникальных домашних каталогов для объектных файлов, библиотек, исполняемых файлов, и т. д. для специальных вариантов сборки. SCons отслеживает цели (targets) по их пути, так что когда подключена variant directory, то объекты, принадлежащие "Controller A", могут иметь отдельные инструкции сборки, отличающиеся от "Controller B", без необходимости лишних пересборок.

SCons предоставляет для этого два способа, один через функцию SConscript, которую мы уже видели, и второй через более гибкую функцию VariantDir.

Историческое замечание: функция VariantDir используется для вызова BuildDir, имя которой было удалено, потому что функциональность SCons отличается от привычной модели "директории сборки" (build), реализованной другими системами сборки, наподобие GNU Autotools. Вы все еще можете столкнуться в Интернет со ссылками на старое имя, которые остались в старых материалах по SCons, но это больше не работает.

Указание дерева директории варианта как части вызова SConscript. Более прямолинейный метод установки дерева variant directory полагается на то, что обычный способ настройки иерархии сборки - иметь файл SConscript в подкаталоге исходного кода. Если вы передадите аргумент variant_dir в вызов функции SConscript:

SConscript('src/SConscript', variant_dir='build')

.. то SCons тогда соберет все файлы в подкаталоге build:

% ls src
SConscript  hello.c
% scons -Q
cc -o build/hello.o -c build/hello.c
cc -o build/hello build/hello.o
% ls src
SConscript  hello.c
% ls build
SConscript  hello  hello.c  hello.o

Теперь никакие файлы не собираются в src, они попадают в build. Вывод сборки может показать маленький сюрприз: объектный файл build/hello.o и исполняемый файл build/hello собираются в подкаталог build, как и ожидалось. Однако даже хотя наш файл hello.c находится в подкаталоге src, SCons на самом деле компилирует файл build/hello.c для создания объектного файла, и это мы видим в выводе сборки.

Это происходит потому, что SCons дублирует в подкаталоге build файл hello.c, беря его из подкаталога src, и делает сборку там (также дублируется и SConscript). В следующей секции объясняется, почему SCons так делает.

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

Главная причина такого дублирования - некоторые утилиты (особенно старые версии) реализованы так, что делают сборку своих выходных файлов в том же каталоге, что и исходный код. Таким образом, остается два варианта - либо делать сборку без копирования дерева исходного кода, после чего переносить результаты сборки в указанный каталог варианта, либо делать копию дерева исходного кода в указанный каталог варианта и уже компилировать там.

Дополнительно могут создавать проблемы относительные ссылки между файлами исходного кода, если мы не дублируем иерархию каталогов исходного кода перед компиляцией в директорию варианта. Это можно увидеть на примере работы препроцессора C, когда используется механизм #include не с угловыми скобками, а с двойными кавычками:

#include "file.h"

Де-факто установился стандарт поведения для большинства компиляторов C, что сначала производится поиск заголовка в том же каталоге, где находится исходный файл, вызвавший директиву #include, и затем просматриваются настроенные для препроцессора директории поиска подключаемых файлов (обычно передаваемые флагами -I). Добавим к этому, что реализация SCons поддерживает репозитории кода (что описано далее), и это означает, что не все файлы будут найдены в той же иерархии директорий. Путаницы также добавляет тот факт, что одинаковые подключаемые файлы с разным содержимым могут находиться в разных подкаталогах библиотек. Тогда простейший способ гарантировать подключение правильных файлов - дублировать файлы исходного кода в директорию варианта, что обеспечит корректную сборку независимо от оригинальных мест нахождения файлов исходного кода.

Хотя дублирование исходного кода гарантирует корректную сборку в этих предельных случаях, его можно обычно безопасно запретить (см. следующую секцию).

Указание SCons не делать дублирование дерева исходников в директории варианта. Во многих случаях и со многими наборами инструментария, SCons может поместить свои target-файлы в подкаталог build без дублирования файлов исходного кода, и все будет прекрасно работать. Вы можете запретить поведение по умолчанию SCons для обработки директории варианта, указав duplicate=False, когда вызываете функцию SConscript:

SConscript('src/SConscript', variant_dir='build', duplicate=False)

Когда указан этот флаг, SCons использует ожидаемую variant-директорию - т. е. выходные файлы попадут в директорию варианта, в то в ремя как исходные файлы останутся в своей директории:

% ls src
SConscript
hello.c
% scons -Q
cc -c src/hello.c -o build/hello.o
cc -o build/hello build/hello.o
% ls build
hello
hello.o

Функция VariantDir. Используйте VariantDir для установки, какие target-файлы должны быть собраны из исходных файлов в отдельной директории:

VariantDir('build', 'src')
env = Environment()
env.Program('build/hello.c')

Обратите внимание, что в подкаталоге src не используется файл SConscript, вы должны явно указать, что программа должна собираться из файла build/hello.c, который SCons будет дублировать в подкаталоге build.

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

% ls src
hello.c
% scons -Q
cc -o build/hello.o -c build/hello.c
cc -o build/hello build/hello.o
% ls build
hello  hello.c  hello.o

Вы можете указать такой же аргумент duplicate=False, какой указывали в вызове SConscript:

VariantDir('build', 'src', duplicate=False)
env = Environment()
env.Program('build/hello.c')

В этом случае SCons запретит дублирование исходных файлов:

% ls src
hello.c
% scons -Q
cc -o build/hello.o -c src/hello.c
cc -o build/hello build/hello.o
% ls build
hello  hello.o

Использование VariantDir вместе с файлом SConscript. Даже при использовании функции VariantDir более естественно применить дочерний файл SConscript, потому что тогда вам не нужно подстраивать отдельные инструкции сборки, чтобы использовать путь директории варианта. Например, если файл src/SConscript выглядит так:

env = Environment()
env.Program('hello.c')

.. тогда наш файл SConstruct может выглядеть следующим образом:

VariantDir('build', 'src')
SConscript('build/SConscript')

В результате получится следующее:

% ls src
SConscript  hello.c
% scons -Q
cc -o build/hello.o -c build/hello.c
cc -o build/hello build/hello.o
% ls build
SConscript  hello  hello.c  hello.o

Обратите внимание, что это полностью эквивалентно использованию SConscript, как было показано в предыдущей секции.

Использование Glob вместе с VariantDir. Шаблон совпадения имен файлов Glob работает при использовании VariantDir как обычно. Например, если src/SConscript такой:

env = Environment()
env.Program('hello', Glob('*.c'))

.. тогда с таким же файлом SConstruct, как был показан в предыдущей секции, и с файлами f1.c и f2.c в подкаталоге src мы увидим следующий вывод:

% ls src
SConscript  f1.c  f2.c  f2.h
% scons -Q
cc -o build/f1.o -c build/f1.c
cc -o build/f2.o -c build/f2.c
cc -o build/hello build/f1.o build/f2.o
% ls build
SConscript  f1.c  f1.o  f2.c  f2.h  f2.o  hello

Функция Glob вернет узлы (Nodes) в дереве build/, как и ожидалось.

Примеры Variant-сборок. Аргумент ключевого слова variant_dir функции SConscript предоставляет все, что нам нужно, чтобы показать, как можно просто создать variant-сборки с помощью SCons. Для примера предположим, что мы хотим собрать программу для обоих платформ Windows и Linux, но хотим, чтобы они собирались в каталоге сетевого общего ресурса, с отдельными директориями сборки для Windows-версии и Linux-версии программы. Нам всего лишь надо немного позаботиться о создании construct-путей, чтобы обеспечить отсутствие нежелательного места расположения зависимостей. Здесь может быть полезным использование путей относительно каталога верхнего уровня. Чтобы избежать написание кода проверки условий в зависимости от платформы, мы можем динамически построить путь variant_dir:

platform = ARGUMENTS.get('OS', Platform())
 
include = "#export/$PLATFORM/include"
lib = "#export/$PLATFORM/lib"
bin = "#export/$PLATFORM/bin"
 
env = Environment(
    PLATFORM=platform,
    BINDIR=bin,
    INCDIR=include,
    LIBDIR=lib,
    CPPPATH=[include],
    LIBPATH=[lib],
    LIBS='world',
)
 
Export('env')
 
env.SConscript('src/SConscript', variant_dir='build/$PLATFORM')

Этот файл SConstruct при запуске на Linux выдаст следующее:

% scons -Q OS=linux
Install file: "build/linux/world/world.h" as "export/linux/include/world.h"
cc -o build/linux/hello/hello.o -c -Iexport/linux/include build/linux/hello/hello.c
cc -o build/linux/world/world.o -c -Iexport/linux/include build/linux/world/world.c
ar rc build/linux/world/libworld.a build/linux/world/world.o
ranlib build/linux/world/libworld.a
Install file: "build/linux/world/libworld.a" as "export/linux/lib/libworld.a"
cc -o build/linux/hello/hello build/linux/hello/hello.o -Lexport/linux/lib -lworld
Install file: "build/linux/hello/hello" as "export/linux/bin/hello"

Тот же самый файл SConstruct на Windows сделает такую сборку:

C:\>scons -Q OS=windows
Install file: "build/windows/world/world.h" as "export/windows/include/world.h"
cl /Fobuild\windows\hello\hello.obj /c build\windows\hello\hello.c /nologo /Iexport\windows\include
cl /Fobuild\windows\world\world.obj /c build\windows\world\world.c /nologo /Iexport\windows\include
lib /nologo /OUT:build\windows\world\world.lib build\windows\world\world.obj
Install file: "build/windows/world/world.lib" as "export/windows/lib/world.lib"
link /nologo /OUT:build\windows\hello\hello.exe /LIBPATH:export\windows\lib world.lib build\windows\hello\hello.obj
embedManifestExeCheck(target, source, env)
Install file: "build/windows/hello/hello.exe" as "export/windows/bin/hello.exe"

Чтобы собрать несколько вариантов за один раз при использовании аргумента variant_dir для SConscript, вы можете вызвать функцию повторно - в этом примере так сделано с помощью цикла. Обратите внимание, что трюк SConscript tс передачей списка script-файлов, или списка директорий исходно кода, не работает с variant_dir, SCons позволяет предоставить только один SConscript, если используется variant_dir.

env = Environment(OS=ARGUMENTS.get('OS'))
for os in ['newell', 'post']:
    SConscript('src/SConscript', variant_dir='build/' + os)

[Сборка из репозиториев кода]

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

Метод Repository. Часто полезно позволить нескольким программистам работать над проектом, чтобы собирать ПО из исходных файлов и/или производных файлов, которые централизованно хранятся в доступном центральном общем репозитории - копии дерева каталогов исходного кода (обратите внимание, что здесь не имеется в виду репозиторий, поддерживаемый системами управления проектами, такими как BitKeeper, CVS или Subversion). Вы используете метод Repository, чтобы указать SCons искать любые исходные файлы и производные файлы, которых нет в локальном дереве сборки, в одном или нескольких репозиториях кода (в определенном порядке):

env = Environment()
env.Program('hello.c')
Repository('/usr/repository1', '/usr/repository2')

Несколько вызовов метода Repository просто будут добавлять репозитории к глобальному списку, который ведет SCons, за исключением что SCons автоматически устраняет из списка текущий каталог и любые другие несуществующие каталоги.

Поиск файлов исходного кода в репозиториях. В примере выше указано, что SCons будет сначала искать файлы в дереве /usr/repository1 и затем в /usr/repository2. SCons ожидает, что любые искомые файлы будут найдены в одинаковой позиции относительно директории верхнего уровня. В примере, показанном выше, файл hello.c не был найден в локальном дереве сборки, и SCons будет искать его сначала для пути/usr/repository1/hello.c, и затем для /usr/repository2/hello.c.

С этим примером файла SConstruct, если файл hello.c существует в локальной директории сборки, то SCons выполнит пересборку программы hello как обычно:

% scons -Q
cc -o hello.o -c hello.c
cc -o hello hello.o

Однако если нет локального файла hello.c, но такой файл есть в /usr/repository1, то SCons перекомпилирует программу hello для исходного файла, найденного там:

% scons -Q
cc -o hello.o -c /usr/repository1/hello.c
cc -o hello hello.o

И подобным образом, когда hello.c нет ни в локальной директории, ни в /usr/repository1/hello.c, но он есть в /usr/repository2:

% scons -Q
cc -o hello.o -c /usr/repository2/hello.c
cc -o hello hello.o

Поиск файлов #include в репозиториях. Мы уже видели, что SCons будет сканировать содержимое файла исходного кода для поиска имен файлов в директивах #include, и учитывает, что исходный файл также зависит от этих подключаемых файлов. Для каждой директории в списке $CPPPATH система SCons будет автоматически искать соответствующие директории в любых деревьях репозиториев, и устанавливать корректные зависимости от любых файлов #include, которые будут найдены в директории репозитория.

Если компилятор C также не знает об этих директориях в деревьях репозиториев, то он не сможет найти файлы #include. Если, например, файл hello.c из нашего предыдущего примера подключает файл hello.h из текущей директории, и файл hello.h присутствует только в репозитории:

% scons -Q
cc -o hello.o -c hello.c
hello.c:1: hello.h: No such file or directory

Чтобы информировать компилятор C о репозиториях SCons будет добавлять подходящие флаги -I к командам компиляции для каждой директории в списке $CPPPATH. Так что если мы добавим текущую директорию для construction-окружения $CPPPATH следующим образом:

env = Environment(CPPPATH = ['.'])
env.Program('hello.c')
Repository('/usr/repository1')

То повторный запуск SCons будет успешным:

% scons -Q
cc -o hello.o -c -I. -I/usr/repository1 hello.c
cc -o hello hello.o

Порядок реплицирования опций -I для препроцессора C такой же, как список порядка поиска в деревьях репозиториев, который SCons использует для собственного анализа зависимостей. Если имеется несколько репозиториев и несколько директорий $CPPPATH, то SCons добавит директории репозиториев в начало каждой директории $CPPPATH, быстро умножая количество флагов -I. Например, если $CPPPATH содержит три директории (и более короткие пути репозиториев!):

env = Environment(CPPPATH = ['dir1', 'dir2', 'dir3'])
env.Program('hello.c')
Repository('/r1', '/r2')

.. то мы получим девять опций -I в командной строке, три (для каждой директории $CPPPATH) умножить на три (для каждой локальной директории плюс два репозитория):

% scons -Q
cc -o hello.o -c -Idir1 -I/r1/dir1 -I/r2/dir1 -Idir2 -I/r1/dir2 -I/r2/dir2 -Idir3 -I/r1/dir3 -I/r2/dir3 hello.c
cc -o hello hello.o

Ограничения на файлы #include в репозиториях. SCons использует опции -I компилятора C, чтобы управлять порядком, в котором будет искать по директориям репозиториев файлы #include. Однако это создает проблему с тем, как препроцессор C обрабатывает строки #include с именем файла, заключенным в двойные кавычки.

Как мы видели, SCons будет компилировать файл hello.c из репозитория, если он не существует в локальной директории. Однако если файл hello.c в репозитории содержит строку #include с именем файла в двойных кавычках:

#include "hello.h"
 
int main(int argc, char *argv[])
{
    printf(HELLO_MESSAGE);
    return (0);
}

.. то препроцессор C будет сначала всегда использовать файл hello.h из директории репозитория, даже когда файл hello.h присутствует в локальной директории, несмотря на то, что в командной строке опцией -I задан локальный каталог как первая опция:

% scons -Q
cc -o hello.o -c -I. -I/usr/repository1 /usr/repository1/hello.c
cc -o hello hello.o

Это поведение препроцессора C всегда искать файл #include в двойных кавычках сначала в том же каталоге, что и исходный файл, и только потом искать там, где указано опцией -I в общем случае изменить не получится. Другими словами это создает ограничение, с которым придется работать, если вы хотите таким методом использовать репозитории исходного кода. Существует 3 способа, с помощью которых вы можете обойти эту проблему препроцессора C:

1. У некоторых современных версий компиляторов C есть опция для запрета, или управления вышеупомянутым поведением препроцессора. Если это так, то добавьте эту опцию к переменной $CFLAGS (или к $CXXFLAGS, или к обоим этим опциям) в своем construction-окружении (окружениях). Убедитесь в том, что эта опция используется для всех construction-окружениях, использующих препроцессинг C!

2. Поменяйте все директивы #include "file.h" на #include < file.h>. Директивы #include с угловыми скобками не используют такое же поведение - для файлов #include сначала будет производиться поиск директорий в опциях -I, что дает для SCons прямое управление над списком директорий, по которому препроцессор C будет искать подключаемые файлы.

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

Поиск файла SConstruct в репозиториях. SCons будет также делать поиск в репозиториях на наличие файла SConstruct и любых указанных файлов SConscript. Однако это создает проблему: как SCons может искать дерево репозитория для файла SConstruct, если сам по себе файл SConstruct содержит информацию о пути репозитория? Для решения этой проблемы SCons позволяет указать директории репозитория в командной строке, используя опцию -Y:

% scons -Q -Y /usr/repository1 -Y /usr/repository2

При поиске исходных или производных файлов SCons сначала будет просматривать репозитории, указанные в командной строке, и затем будет делать поиск репозиториев, указанных в файлах SConstruct или SConscript.

Поиск в репозиториях производных файлов. Если репозиторий содержит не только файлы исходного кода, но также и производные файлы (такие как объектные файлы, библиотеки или исполняемый код), SCons будет использовать обычное вычисление сигнатуры MD5, чтобы определить, является ли производный файл актуальным (up-to-date), или же он устарел и должен быть пересобран в локальной директории сборки. Чтобы вычисление сигнатуры SCons работало корректно, дерево репозитория должно содержать .sconsign-файлы (например .sconsign.dblite), которые SCons использует для отслеживания изменения информации сигнатуры.

Обычно это выполняется интегратором сборки, который запускает SCons в репозитории для создания всех его производных файлов и .sconsign-файлов, или который запускает SCons в отдельной директории сборки, и копирует полученное дерево файлов в нужный репозиторий:

% cd /usr/repository1
% scons -Q
cc -o file1.o -c file1.c
cc -o file2.o -c file2.c
cc -o hello.o -c hello.c
cc -o hello hello.o file1.o file2.o

Обратите внимание, что это безопасно работает даже если файл SConstruct перечисляет /usr/repository1 как репозиторий, потому что для такого запуска SCons удалит текущую директорию сборки из списка репозитория.

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

% cd $HOME/build
% edit hello.c
% scons -Q -Y /usr/repository1
cc -c -o hello.o hello.c
cc -o hello hello.o /usr/repository1/file1.o /usr/repository1/file2.o

Обратите внимание, что SCons знает, что не нужно пересобирать локальные копии файлов file1.o и file2.o, используя вместо этого уже скомпилированные файлы из репозитория.

Гарантирование локальных копий файлов. Если дерево репозитория содержит полные результаты сборки, и мы пытаемся выполнить сборку без каки-либо файлов в нашем локальном дереве, то происходит кое-что довольно удивительное:

% mkdir $HOME/build2
% cd $HOME/build2
% scons -Q -Y /usr/all/repository hello
scons: `hello' is up-to-date.

Почему SCons говорит, что программа hello в актуальном состоянии и не требует компиляции (up-to-date), хотя программы hello нет в локальной директории сборки? Причина в том, то репозиторий (это не локальная директория) уже содержит актуальную скомпилированную программу hello, и SCons корректно определила, что ничего делать не надо, чтобы пересобрать копию выходного файла.

Однако часто бывает нужно, чтобы локальная копия файла все же присутствовала. Например, скрипты упаковки или тестирования могут подразумевать, что определенные сгенерированные файлы находятся локально. Чтобы указать SCons сделать копию актуального файла из репозитория в локальную директорию сборки, используйте функцию Local:

env = Environment()
hello = env.Program('hello.c')
Local(hello)

Если тогда мы запустим ту же команду, то SCons сделает копию программы из копии в репозитории, и сообщит нам об этом:

% scons -Y /usr/all/repository hello
Local copy of hello from /usr/all/repository/hello
scons: `hello' is up-to-date.

Обратите внимание, что поскольку создание локальной копии не считается "сборкой" файла hello, SCons все еще сообщает, что сборки не было, потому что выходной файл актуальный (up-to-date).

[Ссылки]

1. SCons: руководство пользователя, быстрый старт.
2. SCons: написание собственных сборщиков.

 

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


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

Top of Page