Катаем «смоляной шарик» или создание собственных правил сборки с помощью Qbs

в 13:37, , рубрики: qbs, qt, системы сборки

Qbs (Qt Build System) — система сборки, позволяющая описывать процесс сборки проектов на простом языке QML (javascript-подобный декларативный язык), ускоряющий процесс сборки продуктов за счет построения подробного графа зависимостей. Хоть эта система и создана разработчиками Qt, но она жестко не привязана к Qt и позволяет собирать любые продукты, для любых языков программирования и даже использоваться не для программирования, а например для администрирования. Как заявлено в официальной документации:

A product is the target of a build process, typically an application, library or maybe a tar ball

Сегодня и рассмотрим процесс создания своих продуктов. Поехали…

Знакомство с это системой сборки хорошо описал mapron в данной статье, мы же не будем на этом останавливаться и приступим сразу к делу.

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

Итак Qbs выполняет преобразования одних данных в другие с помощью элемента Rule. Данный элемент создает правило преобразования, которое преобразует входные данные inputs в выходные outputs при помощи набора команд, созданных в скрипте prepare. Каждая команда может как сама выполнять преобразования, так и делегировать эту работу внешней программе.

Входные артефакты

В качестве входных артефактов для правила преобразования выступают файлы собираемого продукта с тегом указанным в свойстве inputs, а также артефакты полученные из зависимостей собираемого продукта с тегом указанным в свойстве inputsFromDependencies (неявным является тег "installable", который присваивается всем файлам которые необходимо проинсталлировать, т.е. qbs.install: true).

Например:

---product1.qbs---
Application {
    name: "simpleApplication";
    targetName: "appSimple"
    files: ["main.cpp", "app.h", "app.cpp"]
    Depends {name: "cpp"}
}

---product2.qbs---
Product {
    type: "exampleArtefact"
    name: "example"
    targetName: "myArtefact"
    Depends {name: "simpleApplication" }
    Depends {name: "exampleModule" }
    Group {
        files: ["description.txt", "readme.txt"]
        fileTags: ["txtFiles"]
    }
    Group {
        files: ["imgA.png", "imgB.png", "imgC.png"]
        fileTags: ["pngFiles"]
    }
    Group {
        files: ["fontA.ttf", "fontB.ttf"]
        fileTags: ["fontFiles"]
    }
}

---exampleModule.qbs---
Module {        
    Rule {
        inputs: ["pngFiles", "fontFiles"]
        inputsFromDependencies: ["application"]
        Artifact {
            filePath: product.targetName + ".out"
            fileTags: ["exampleArtefact"]
        }
        ...
    }
    ...
}

В данном примере у нас есть 2 продукта и 1 модуль:

  1. Продукт с названием simpleApplication имеющий тип application (элемент Application по сути является Product {type: «application»}) содержащая 3 файла: main.cpp, app.h, app.cpp. Этот продукт зависит от модуля cpp, что указывает на то, что будет выполняться компиляция этого продукта компилятором C++ и на выходе получится артефакт помеченный тегом «application» и названием указанным в свойстве targetName, т.е. appSimple.exe (для windows или appSimple для unix платформ).
  2. Продукт с названием example имеющий тип exampleArtefact содержащий 7 файлов помеченных тремя тегами. Этот продукт зависит от продукта simpleApplication, что указывает на то, что он будет обрабатываться после создания продукта simpleApplication. А также от модуля exampleModule, что указывает на то, что для создания данного продукта будут браться правила преобразования и свойства из этого модуля. И на выходе ожидается артефакт с названием myArtefact типа (с тегом) exampleArtefact.
  3. Модуль exampleModule содержит правило преобразования файлов с тегами pngFiles и fontFiles, а также артефактов имеющих тег application, которые берутся из зависимостей собираемого продукта.

При сборке продукта будет определен список модулей от которых зависит продукт и сами модули. В них будет осуществлен поиск правил преобразования файлов помеченных входными тегами в выходные артефакты, которые соответствуют типу собираемого продукта. Сначала собирается продукт simpleApplication, т.к. он имеет зависимость только от модуля cpp. В нем ищутся правила преобразования файлов продукта в тип application. В модуле cpp есть элементы FileTagger которые по шаблону задают для файлов продукта теги. Преобразование входных файлов в выходной(ые) может быть выполнено как сразу, так и по цепочке преобразований файлов одного типа в другой, а затем в итоговый. На выходе обработки продукта simpleApplication, мы получим приложение appSimple имеющее тип (тег) application.

Затем начнется сборка продукта example. Для его файлов будет искаться правило, на выходе дающее артефакты типа exampleArtefact. Это правило требует для входа файлы типа (с тегом) pngFiles, fontFiles и application. При этом файлы типа application ищутся только в продуктах от которых зависит собираемый продукт. Так как продукт example уже содержит такие файлы, то на вход правила поступают файлы: imgA.png, imgB.png, imgC.png, fontA.ttf, fontB.ttf и appSimple.exe. А на выходе получим файл myArtefact.out типа exampleArtefact, который и будет являться нашим конечным продуктом.

Выходные артефакты

В качестве выходных артефактов для правила могут быть как один, так и несколько артефактов. Для описания выходных артефактов используется элемент Artifact:

Artifact {
    filePath: input.fileName + ".out"
    fileTags: ["txt_output"]
}

Этот элемент описывает, какой артефакт получается на выходе правила. Через свойство filePath — указывается имя выходного файла. Если указывать относительный путь, то Qbs будет создавать этот артефакт относительно каталога сборки текущего собираемого продукта. Через свойство fileTags указывается список тегов, которые будет иметь артефакт после его создания. Это необходимо, для того чтобы другие правила сборки могли использовать выходные артефакты данного правила, как свои входные артефакты. Также если продукт будет иметь этот же тег в качестве своего типа, то эти артефакты будут являться результатом сборки этого продукта.

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

Свойство outputArtifacts описывает список JavaScript объектов имеющих свойства, как у элемента Artifact. Используется это свойство для случаев, когда набор выходов не фиксирован, а зависит от содержания входных данных. Например:

outputArtifacts: [{
    var artifactNames = inputs["pngFiles"].map(function(file){
        return "pictures/"+file.fileName;
    });
    artifactNames = artifactNames.concat(inputs["fontFiles"].map(function(file){
        return "fonts/"+file.fileName;
    }));
    artifactNames = artifactNames.concat(inputs["application"].map(function(file){
        return "app/"+file.fileName;
    }));
    var artifacts = artifactNames.map(function(art){
        var a = {
            filePath: art,
            fileTags: ["exampleArtefact"]
        }
        return a;
    });
    return artifacts;
}]

В данном примере показано, что для входных файлов с тегом pngFiles, правило подготовит выходной артефакт с таким же названием и поместит его в папку pictures. Для тегов fontFiles и application, также поместив их соответственно в папки fonts и app. При этом т.к. пути будут относительные, то эти папки создадутся в папке сборки продукта.

Если мы решили использовать свойство outputArtifacts, то необходимо указать и свойство outputFileTags, которое является списком выходных тегов, которые правило потенциально производит. Для нашего примера:

outputFileTags:
 ["exampleArtefact"]

Все полученные при сборке продукта артефакты, помеченные выходным тегом совпадающим с типом продукта, при установке продукта, будут скопированы из каталога сборки в каталог установки.

Правило преобразования

Когда определены входные и выходные артефакты, необходимо подготовить саму последовательность команд для выполнения преобразования. Для этого используется свойство prepare элемента Rule. Это свойство является JavaScript сценарием, который возвращает список команд для преобразования входов в выходы. Код в этом скрипте рассматривается как функция с сигнатурой function(project, product, inputs, outputs, input, output).
Параметры input и output не определены (undefined), если для этого правила имеется несколько артефактов ввода (и вывода соответственно). Служат они как синтаксический сахар: input = inputs[0] и output = outputs[0] и являются списками с одним элементом. Параметры project и product, являются JavaScript объектами, через которые доступны свойства текущего проекта и продукта соответственно. Особый интерес вызывают объекты inputs и outputs. Рассмотрим их более подробно.

Объекты inputs и outputs

Параметры inputs и outputs являются объектами JavaScript, ключи свойств которых являются файловыми тегами, а значениями свойств — являются списки объектов, представляющих артефакты, соответствующие этим тегам. В нашем примере переменная inputs имеет 3 ключа: pngFiles, fontFiles, application. А каждый входной артефакт доступен через inputs[«pngFiles»] (или inputs.pngFiles что равнозначно). Каждый артефакт в этом списке имеет следующие свойства:

  • baseName — базовое имя файла (например для файла c:123test.plugin.dll это будет test)
  • completeBaseName — название файла без расширения (например для файла c:123test.plugin.dll это будет test.plugin)
  • fileName — название файла (например для файла c:123test.plugin.dll это будет test.plugin.dll)
  • filePath — полный путь до файла с его полным именем
  • fileTags — список тегов присвоенных артефакту

Помимо этого артефакты содержат в себе все свойства для каждого модуля, который используется в продукте. Эта особенность может использоваться для доступа к свойствам модуля. Например, свойство inputs.application[0].cpp.defines вернет для артефакта simpleApplication список определений, который будет передан при компиляции соответствующего файла. Это очень удобный и важный момент, позволяющий артефактам задать через свойства какого-нибудь модуля свои значения и группировать такие артефакты или обрабатывать их как-то по особенному.

* Было подмечено на Qbs версии 1.7.2, что если продукт подменяет свойства модуля в котором находится правило сборки, то эти свойства недоступны в артефакте. По этому эти свойства я выносил в отдельный модуль.

* Также inputs.application[0].cpp.defines не всегда срабатывает, по этому я использую функцию inputs.application[0].moduleProperty(«cpp», «defines»). Если эту функцию применять к входному артефакту, то будут возвращаться свойства которые использует артефакт в указанном модуле. Если же применять ее к продукту (например product.moduleProperty(«cpp», «defines»), то возвращаться будут свойства указанного модуля, которые использует собираемый в данный момент конечный продукт.

* Очень удобной функцией является dumpObject(object) из модуля ModUtils, которая выводит в консоль информацию о свойствах переданного в него параметра. Правда и она не всегда показывает свойства используемых в артефактах модулей.

Команды

В качестве результата выполнения скрипта выступает список команд. Команда — это то, что Qbs выполняет во время сборки. Команда всегда создается в сценарии подготовки правила. Команды бывают двух типов:

  1. Command, который запускает внешний процесс
  2. JavaScriptCommand, который выполняет произвольный код JavaScript (хотя может также запускать внешние процессы)

Для обоих типов команд доступны свойства:

  • description — строка, показываемая в консоли при выполнении данной команды
  • extendedDescription — строка детальной информации, показываемой в консоли при расширенном выводе
  • highlight — тип (тег) команды. Влияет на то, как будет показываться description в консоли. Может быть: compiler, linker, codegen, filegen и пр.
  • silent — скрывать ли вывод description при выполнении команды

Для использования Command, необходимо создать объект передав в его конструктор полный путь до исполняемой программы и список аргументов.

var myCommand = new Command("dir", ["/B", "/O", ">>", output.filePath]);

Например данная команда запишет вывод команды dir в выходной файл правила сборки.
Полезными свойствами, позволяющими обойти ограничение Windows на длину командной строки являются свойства:

  • responseFileThreshold содержит значение. Если это значение больше нуля и меньше длины полной командной строки, и если responseFileUsagePrefix не пуст, то содержимое командной строки перемещается во временный файл, путь которого становится полным содержимым списка аргументов. Затем программа должна прочитать полный список аргументов из этого файла. Этот механизм будет работать только с программами, которые явно поддерживают его.
  • responseFileArgumentIndex — указывает индекс первого аргумента для включения в файл ответов.
  • responseFileUsagePrefix — содержит префикс, который сообщает программе, что остальная часть аргумента — это путь к файлу, содержащему фактическую командную строку.

Для обработки вывода выполняемой команды могут применяться свойства:

  • stderrFilterFunction — функция с входным параметром равным выводу программы в stderr и которая должна вернуть преобразованную строку для дальнейшего вывода.
    Если не задана, то обработки не будет производиться.
  • stdoutFilterFunction — аналогичная stderrFilePath, только работающая с stdout
  • stdoutFilePath — название файла, кода направляется отфильтрованный ранее вывод stdout. Если не задан, то вывод будет производиться в консоль
  • stderrFilePath — аналогичная stdoutFilePath, только работает с stderr

JavaScriptCommand команда — представляет собой JavaScript функцию, которая будет выполняться при сборке. Задается это функция в свойстве sourceCode. В исходном коде функции доступны project, product, inputs и outputs (дающий доступ к свойствам проекта, продукта, входных и выходных артефактов соответственно). Чтобы передать в функцию произвольные данные, то для команды надо добавить произвольные свойства и присвоить им нужные значения. Например так:

var cmd = new JavaScriptCommand();
cmd.myFirstData = "This is 1 string";
cmd.mySecondData = "This is 2 string";
cmd.sourceCode = function() {
    console.info("String from source code");    // -->> "String from source code"
    console.info("Property 1: "+myFirstData);   // -->> "Property 1: This is 1 string"
    console.info("Property 2: "+mySecondData);  // -->> "Property 2: This is 2 string"
};

В функции, также можно использовать разные доступные сервисы:

  • Environment Service — для доступа к системной среде и среде разработки
  • File Service — для работы с файлами (копирование, перемещение, удаление файлов, проверки существования файла и даты модификации, создания путей и сканирования директории на содержимое)
  • FileInfo Service — операции обработки пути к файлу (получение базового имени файла, пути к нему, получение относительного пути и т.п.)
  • General Services — дополнительные функции расширяющие функционал обработки строк, массивов и т.п.
  • Process Service — функции позволяющие запускать и управлять внешними программами, а также работать с его входом и выходом
  • PropertyList Service — функции для работы со списками свойств в форматах JSON, XML, binaty и OpenStep для платформ Darwin (iOS, macOS, tvOS, и watchOS)
  • TemporaryDir Service — функции для создания и управления временным каталогом
  • TextFile Service — функции для работы с текстовыми файлами и позволяющие читать/писать из/в них.
  • Utilities Service — функции для получения хешей

Прочие свойства элемента Rule

У элемента Rule также есть несколько дополнительных свойств:

Свойство multiplex, необходимо для указания порядка обработки входных артефактов. При multiplex=true, создается одна копия правила для всех входных артефактов и они обрабатываются все скопом. Таким образом в свойстве inputs будут все входные артефакты. Применяется в случаях, когда надо производить групповую обработку входных артефактов. Если же multiplex=false, то для каждого входного артефакта будет создаваться отдельная копия правила преобразования и будет создаваться свой выходной артефакт. Таким образом свойство inputs будет содержать всегда один элемент. По умолчанию это свойство имеет значение false.

Свойство condition, указывает на условие выполнения правила. Например, указать, что преобразование будет выполняться только в режиме release и для платформы windows.

 qbs.buildVariant === "release" && qbs.targetOS.contains("windows") 

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

Свойство alwaysRun указывает, на условия выполнения команд в правиле. Если alwaysRun=true, то команды будут всегда выполняться, даже не взирая на то, что выходные артефакты уже обновлены. По умолчанию равно false.

Пример подготовки правила преобразования

В качестве примера правила приведу следующую задачу:

Имеется проект, из нескольких программ и библиотек, для которого необходимо скопировать все необходимые для работы программ Qt библиотеки и плагины в заданную директорию. Для этого создаем модуль с правилом преобразования и самостоятельный продукт, который будет зависеть от всех продуктов проекта, для которых и необходимо подготовить Qt библиотеки. Дополнительно надо подготовить тестовый файл с указанием всех подготовленных Qt библиотек.

winDeployQt - деплой Qt библиотек для проекта

-- MyWindowsDeploy.qbs --
import qbs
import qbs.ModUtils

Module {
Rule {
        condition: qbs.targetOS.contains("windows") //запускается только для windows
        multiplex: true //обрабатываем все входные артефакты одним трансформером 
        alwaysRun: true //всегда выполнять правило

        inputsFromDependencies: ["installable"] //брать устанавливаемые артефакты от зависимостей собираемого продукта
        Artifact {
            filePath: "Copied_qt_libs.txt";
            fileTags: ["deployQt"];
        }

        prepare: {
            var cmdQt = new JavaScriptCommand();
            //определяем путь до windeployqt.exe
            cmdQt.windeployqt = FileInfo.joinPaths(product.moduleProperty("Qt.core", "binPath"), "windeployqt.exe");
            //задаем строку для вывода в консоли
            cmdQt.description = "Copy Qt libs and generate text file: "+output.fileName;
            //указываем развернутую информацию по выполняемой команде
            cmdQt.extendedDescription = cmdQt.windeployqt + ".exe " +
                    ["--json"].concat(args).concat(binaryFilePaths).join(" ");
            
            //путь до папки, куда надо копировать qt библиотеки ("<папка установки>/QtLibs")
            var deployDir = FileInfo.joinPaths(product.moduleProperty("qbs","installRoot"),
                                            product.moduleProperty("qbs","installDir"));
            deployDir = FileInfo.joinPaths(deployDir, "QtLibs");
            cmdQt.qtLibsPath = deployDir;
            
            //определяем аргументы запуска программы
            cmdQt.args = [];
            cmdQt.args.push("--libdir", deployDir);
            cmdQt.args.push("--plugindir", deployDir);
            cmdQt.args.push("--no-translations");
            cmdQt.args.push("--release");
            
            //полное имя файла для записи вывода.
            cmdQt.outputFilePath = output.filePath;
            
            //определяем список путей установки программ и библиотек, от которых зависит продукт
            cmdQt.binaryFilePaths = inputs.installable.filter(function (artifact) {
                return artifact.fileTags.contains("application")
                        || artifact.fileTags.contains("dynamiclibrary");
            }).map(function(a) { return ModUtils.artifactInstalledFilePath(a); });
          
            cmdQt.sourceCode = function(){
                var process;
                var tf;
                try {
                    //выводим значения параметров
                    console.info("windeployqtRule: outputFilePath: "+outputFilePath);
                    console.info("windeployqtRule: qtLibsPath: "+qtLibsPath);
                    console.info("windeployqtRule: windeployqt: "+windeployqt);
                    console.info("windeployqtRule: windeployqtArgs: "+windeployqtArgs.join(", "));
                    console.info("windeployqtRule: binaryFilePaths: "+binaryFilePaths.join(", "));
            
                    //создаем папку куда будут скопированы библиотеки Qt
                    File.makePath(qtLibsPath);
                    //создаем процесс
                    process = new Process();
                    //запускаем процесс
                    process.exec(windeployqt,
                                 ["--json"].concat(windeployqtArgs).concat(binaryFilePaths), true);
                    //читаем вывод программы
                    var out = process.readStdOut();  
                    //парсим выходной JSON
                    var inputFilePaths = JSON.parse(out)["files"].map(function (obj) {
                        //определяем полный путь доя скопированной библиотеки
                        var fn = FileInfo.joinPaths(
                                    FileInfo.fromWindowsSeparators(obj.target),
                                    FileInfo.fileName(
                                        FileInfo.fromWindowsSeparators(
                                            obj.source)));
                        return fn;
                    });
                    //создаем файл
                    tf = new TextFile(outputFilePath, TextFile.WriteOnly);
                    //пишем заголовок
                    tf.writeLine("Copied Qt files:");
                    inputFilePaths.forEach(function(qtLib){
                        tf.writeLine(qtLib); //записываем в выходной файл полный путь до скопированной библиотеки
                    });
                } finally {
                    if (process)
                        process.close();
                    if (tf)
                        tf.close();
                }
            }

            return [cmdQt];
        }
    }
}

Пример продукта

-- ProductDeploy.qbs --
import qbs

Product {
//продукт представляет собой результат деплоя Qt библиотек для продуктов от которых он 
//зависит в виде скопированных библиотек и текстового файла
    type: ["deployQt"] 

   // указываем зависимость от модуля в котором есть правило преобразования
    Depends { name: "MyWindowsDeploy" } 
   // указываем зависимость от продуктов, для которых 
   // надо подготовить Qt библиотеки, плагины и пр.
    Depends { name: "libratyA" }
    Depends { name: "libratyB" }
    Depends { name: "applicationA" }
    Depends { name: "applicationB" }
    Depends { name: "applicationC" }

    condition: qbs.targetOS.contains("windows")     // собирать только для windows
    builtByDefault: false // собирать ли продукт при общей сборке проекта

    qbs.install: true
    qbs.installDir: "MyProject/qtDeploy"
}

Ссылки

Автор: hooligan

Источник

Поделиться

* - обязательные к заполнению поля