diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1ad1158 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: © 2024 Serhii “GooRoo” Olendarenko +# +# SPDX-License-Identifier: BSD-3-Clause + +root = true + +[*] +indent_style = tab +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_style = space +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..04e07ba --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: © 2024 Serhii “GooRoo” Olendarenko +# +# SPDX-License-Identifier: BSD-3-Clause + +github: + - GooRoo diff --git a/.github/workflows/chore-reuse-lint.yml b/.github/workflows/chore-reuse-lint.yml new file mode 100644 index 0000000..5c407fe --- /dev/null +++ b/.github/workflows/chore-reuse-lint.yml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: © 2024 Serhii “GooRoo” Olendarenko +# +# SPDX-License-Identifier: BSD-3-Clause + +name: 'chore: REUSE Compliance' + +on: [push, pull_request] + +jobs: + lint: + name: REUSE Compliance Check + runs-on: [ubuntu-latest] + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + - name: reuse lint + uses: fsfe/reuse-action@v4 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..119fea9 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,11 @@ +Copyright (c) 2024 Serhii “GooRoo” Olendarenko. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000..119fea9 --- /dev/null +++ b/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,11 @@ +Copyright (c) 2024 Serhii “GooRoo” Olendarenko. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0f68b7 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ + + +# `easy.deployqt.qbs` + +This modules helps to automatically deploy all dynamic Qt dependencies based on QML files. + +It is intended to work within `uni.qbs` environment. diff --git a/modules/easy/deployqt/deployqt.js b/modules/easy/deployqt/deployqt.js new file mode 100644 index 0000000..9c3d370 --- /dev/null +++ b/modules/easy/deployqt/deployqt.js @@ -0,0 +1,300 @@ +// SPDX-FileCopyrightText: © 2024 Serhii “GooRoo” Olendarenko +// +// SPDX-License-Identifier: BSD-3-Clause + +const FileInfo = require('qbs.FileInfo') +const Process = require('qbs.Process') +const TextFile = require('qbs.TextFile') + +function unionSets(lhs, rhs) { + var union = new Set(lhs) + rhs.forEach(function (elem) { + union.add(elem) + }) + return union +} + +function pluginNameToFileName(libName, os) { + if (os.contains('windows')) { + return libName + '.dll' + } else if (os.contains('macos')) { + return 'lib' + libName + '.dylib' + } +} + +function getRuntimeDependenciesMacOS(libPath, dynamicLib, arch) { + const otool = new Process() + const args = ['-L', dynamicLib, '-arch', arch] + + otool.exec('otool', args, true) + const deps = otool.readStdOut() + + return parseOtoolOutput( + dynamicLib, + deps + ) + .filter(function (dep) { return dep.startsWith('@rpath/Qt') }) + .map(function (dep) { + return FileInfo.joinPaths( + libPath, + dep.match(/^@rpath\/((.+)\.framework\/Versions\/A\/\2$)/)[1] + ) + }) +} + +//! Use `dumpbin.exe` to get the runtime dependencies of a dynamic library on Windows. +// +// The output of `dumpbin` is of the following format (example): +// ``` +// +// Dump of file .\Qt6Qml.dll +// +// File Type: DLL +// +// Image has the following dependencies: +// +// Qt6Network.dll +// SHELL32.dll +// Qt6Core.dll +// KERNEL32.dll +// MSVCP140.dll +// VCRUNTIME140.dll +// VCRUNTIME140_1.dll +// api-ms-win-crt-heap-l1-1-0.dll +// api-ms-win-crt-math-l1-1-0.dll +// api-ms-win-crt-stdio-l1-1-0.dll +// api-ms-win-crt-runtime-l1-1-0.dll +// api-ms-win-crt-string-l1-1-0.dll +// +// Summary +// +// 20000 .data +// 2E000 .pdata +// 142000 .rdata +// 8000 .reloc +// 1000 .rsrc +// 367000 .text +// ``` +function getRuntimeDependenciesWindows(toolchainPath, libPath, dynamicLib) { + const dumpbin = new Process() + const args = ['/nologo', '/dependents', dynamicLib] + + dumpbin.exec(FileInfo.joinPaths(toolchainPath, 'dumpbin.exe'), args, true) + const output = dumpbin.readStdOut() + + const outLines = output.split('\n') + const blockStart = outLines.indexOf(' Image has the following dependencies:') + 2 + const blockEnd = outLines.indexOf('', blockStart) + + return outLines.slice(blockStart, blockEnd) + .map(function (line) { return line.trim() }) + .filter(function (line) { return line.startsWith('Qt6') }) + .map(function (dep) { return FileInfo.joinPaths(libPath, dep) }) +} + +function getRecursiveRuntimeDependencies(os, toolchainPath, libPath, dynamicLib, arch, visited) { + visited = visited || new Set(); + + if (visited.has(dynamicLib)) { + return new Set(); + } + + visited.add(dynamicLib); + + var directDependencies = new Set( + os.contains('windows') + ? getRuntimeDependenciesWindows(toolchainPath, libPath, dynamicLib) + : getRuntimeDependenciesMacOS(libPath, dynamicLib, arch) + ); + var allDependencies = new Set(directDependencies); + + directDependencies.forEach(function (dependency) { + var recursiveDependencies = getRecursiveRuntimeDependencies(os, toolchainPath, libPath, dependency, arch, visited); + allDependencies = unionSets(allDependencies, recursiveDependencies); + }); + + return allDependencies; +} + +function frameworkLibToFrameworkContents(frameworkLibPath) { + const match = frameworkLibPath.match(/^(.+\/(.+)\.framework)\/.+\/\2$/) + if (!match) { + throw new Error('Invalid framework library path: ' + frameworkLibPath) + } + + const frameworkPath = match[1] + const frameworkName = match[2] + + const versionsPath = frameworkPath + '/Versions' + const versionName = 'A' + const versionPath = versionsPath + '/' + versionName + + var list = [] + + list.push(frameworkPath + '/' + frameworkName) + list.push(frameworkPath + '/Resources') + list.push(versionsPath + '/Current') + list.push(versionPath + '/Resources/Info.plist') + list.push(versionPath + '/' + frameworkName) + + return list +} + +//! Parse the `otool -L` output of the following format (example): +// ``` +// ../Imports/Qt/labs/platform/liblabsplatformplugin.dylib: +// @rpath/QtLabsPlatform.framework/Versions/A/QtLabsPlatform (compatibility version 6.0.0, current version 6.8.0) +// @rpath/QtQml.framework/Versions/A/QtQml (compatibility version 6.0.0, current version 6.8.0) +// @rpath/QtNetwork.framework/Versions/A/QtNetwork (compatibility version 6.0.0, current version 6.8.0) +// @rpath/QtCore.framework/Versions/A/QtCore (compatibility version 6.0.0, current version 6.8.0) +// /System/Library/Frameworks/IOKit.framework/Versions/A/IOKit (compatibility version 1.0.0, current version 275.0.0) +// /System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration (compatibility version 1.0.0, current version 1.0.0) +// /System/Library/Frameworks/UniformTypeIdentifiers.framework/Versions/A/UniformTypeIdentifiers (compatibility version 1.0.0, current version 709.0.0) +// /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1700.255.0) +// /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1345.100.2) +// ``` +function parseOtoolOutput(dynamicLib, output) { + const lines = output.split('\n') + + const deps = [] + + if (!lines[0].startsWith(dynamicLib)) { + throw new Error('Cannot parse otool output: ' + lines[0]) + } + + // Starting from 1 since lines[0] is the name of the library we explore + for (var i = 1; i < lines.length; ++i) { + const line = lines[i] + + const depMatch = line.match(/^\s+(.+) \(compatibility version \d+\.\d+\.\d+, current version \d+\.\d+\.\d+\)$/) + + if (depMatch) { + deps.push(depMatch[1]) + } else if (line.trim() !== '') { + console.error('Unmatched dependency: ' + line) + } + } + + return deps +} + +function collectQmlImports(scannerFilePath, qrcFiles, importPath, os) { + const qmlimportscanner = new Process() + var args = qrcFiles.reduce(function (acc, input) { + acc.push(input.filePath) + return acc + }, ['-qrcFiles']) + args.push('-importPath', importPath) + + qmlimportscanner.exec(scannerFilePath, args, true) + const imports = JSON.parse(qmlimportscanner.readStdOut()) + .filter(function (imp) { return imp.plugin && imp.name.startsWith('Qt') }) + .map(function (imp) { + return { + name: imp.name, + path: FileInfo.joinPaths(imp.path, pluginNameToFileName(imp.plugin, os)), + } + }) + + return imports +} + +function getLibFilesForQtModule(Qt, mod) { + if ((mod !== 'core') && !Qt[mod].hasLibrary) + return [] + + if (Qt[mod].isStaticLibrary) + return [] + + var list = [] + if (qbs.targetOS.contains('windows')) { + const dir = Qt.core.binPath + const basename = FileInfo.baseName(Qt[mod].libNameForLinker) + const suffix = qbs.buildVariant === 'debug' ? 'd' : '' + const libPath = FileInfo.joinPaths(dir, basename + suffix + '.dll') + list.push(libPath) + } else if (Qt.core.frameworkBuild) { + const fp = Qt[mod].libFilePathRelease + + const suffix = '.framework/' + const frameworkPath = fp.substr(0, fp.lastIndexOf(suffix) + suffix.length - 1) + const versionsPath = frameworkPath + '/Versions' + const versionName = 'A' + const versionPath = versionsPath + '/' + versionName + list.push(frameworkPath + '/' + FileInfo.fileName(fp)) + list.push(frameworkPath + '/Resources') + list.push(versionsPath + '/Current') + list.push(versionPath + '/Resources/Info.plist') + list.push(versionPath + '/' + FileInfo.fileName(fp)) + } + return list +} + +function readDepsFromFile(filePath) { + console.debug('Reading dependencies from ' + filePath) + var depsFile = new TextFile(filePath, TextFile.ReadOnly) + const deps = depsFile.readAll().split('\n').filter(function (line) { return line.trim() !== '' }) + depsFile.close() + + return deps +} + +function readDepsForTags(inputs, tags) { + return tags.reduce(function (acc, tag) { + if (inputs[tag] !== undefined) { + acc = unionSets(acc, readDepsFromFile(inputs[tag][0].filePath)) + } + return acc + }, new Set()) +} + +function readQmlImports(filePath) { + var importsFile = new TextFile(filePath, TextFile.ReadOnly) + const imports = JSON.parse(importsFile.readAll()) + importsFile.close() + return imports +} + +function toTargetPath(filePath, fromDir, installDir) { + return filePath.replace( + fromDir, + FileInfo.joinPaths( + product.qbs.installRoot, + project.installContentsPath, + installDir + ) + ) +} + +function collectAssets(dir) { + const dirs = File.directoryEntries(dir, File.Dirs | File.NoDotAndDotDot); + const files = File.directoryEntries(dir, File.Files) + .filter(function (entry) { + return entry.match(/\.(png|webp|qsb)$/) + }) + .map(function (entry) { + return FileInfo.joinPaths(dir, entry); + }) + + return dirs + .filter(function (subdir) { return subdir !== 'designer' }) + .filter(function (subdir) { return !File.exists(FileInfo.joinPaths(dir, subdir, 'qmldir')) }) + .reduce(function (acc, subdir) { + return acc.concat(collectAssets(FileInfo.joinPaths(dir, subdir))) + }, files); +} + +function pluginNamesToFileNames(pluginNames, os) { + return pluginNames.map(function (name) { + const pathParts = name.split('/') + + if (pathParts.length !== 2) { + throw Error('Please, specify plugins in format "plugintype/pluginname".') + } + + const pluginType = pathParts[0] + const pluginName = pathParts[1] + + return FileInfo.joinPaths(pluginType, pluginNameToFileName(pluginName, os)) + }) +} diff --git a/modules/easy/deployqt/deployqt.qbs b/modules/easy/deployqt/deployqt.qbs new file mode 100644 index 0000000..6193000 --- /dev/null +++ b/modules/easy/deployqt/deployqt.qbs @@ -0,0 +1,361 @@ +// SPDX-FileCopyrightText: © 2024 Serhii “GooRoo” Olendarenko +// +// SPDX-License-Identifier: BSD-3-Clause + +import qbs.File +import qbs.FileInfo +import qbs.Process +import qbs.TextFile + +import 'deployqt.js' as DeployQt +import 'qmldir-parser.js' as Qmldir + +Module { + additionalProductTypes: scanQml? ['dmg.input'] : [] + + property bool scanQml: false + + property stringList plugins: [] + property stringList excludePlugins: [] + + Depends { name: 'Qt.qml'; condition: scanQml } + + Group { + name: 'Known Qt dependencies' + files: { + const qtModules = Object.getOwnPropertyNames(product.Qt) + const libFiles = qtModules.reduce(function (acc, mod) { + return acc.concat(DeployQt.getLibFilesForQtModule(product.Qt, mod)) + }, []) + return libFiles + } + + fileTags: [] + + qbs.install: true + qbs.installPrefix: project.installContentsPath + qbs.installDir: project.installLibraryDir + qbs.installSourceBase: qbs.targetOS.contains('windows')? Qt.core.binPath : Qt.core.libPath + } + + Group { + name: 'Qt plugins' + prefix: product.Qt.core.pluginPath + '/' + files: DeployQt.pluginNamesToFileNames(plugins, qbs.targetOS) + fileTags: ['easy.deployqt.plugins'] + + excludeFiles: qbs.targetOS.contains('windows') + ? qbs.buildVariant === 'debug' + ? ['**/*[!d].dll'] + : ['**/*d.dll'] + : [] + } + + Rule { + // Copy the specified plugins into the install-root, and also scan the plugins' dynamic dependencies + // and save them into a file + + multiplex: true + + inputs: ['easy.deployqt.plugins'] + + outputFileTags: ['dmg.input', 'easy.deployqt.pluginsdeps'] + outputArtifacts: { + const plugins = inputs['easy.deployqt.plugins'] + + return plugins.map(function (plugin) { + return { + filePath: DeployQt.toTargetPath( + plugin.filePath, + product.Qt.core.pluginPath, + project.installPluginsDir + ), + fileTags: ['dmg.input'] + } + }).concat([ + { + filePath: FileInfo.joinPaths(product.buildDirectory, 'easy.deployqt', 'pluginsdeps.list'), + fileTags: ['easy.deployqt.pluginsdeps'] + } + ]) + } + + prepare: /*(project, product, inputs, outputs, input, output, explicitlyDependsOn) =>*/ { + var copyPlugins = new JavaScriptCommand() + copyPlugins.silent = true + copyPlugins.sourceCode = function () { + const plugins = inputs['easy.deployqt.plugins'] + + plugins.forEach(function (plugin) { + const targetPath = DeployQt.toTargetPath( + plugin.filePath, + product.Qt.core.pluginPath, + project.installPluginsDir + ) + + if (!File.exists(targetPath)) { + File.copy(plugin.filePath, targetPath) + } + }) + } + + var scanDynamicDeps = new JavaScriptCommand() + scanDynamicDeps.silent = true + scanDynamicDeps.sourceCode = function () { + const plugins = inputs['easy.deployqt.plugins'] + + var deps = new Set([]) + + plugins.forEach(function (plugin) { + const dynamicLib = plugin.filePath + + const qtLibs = DeployQt.getRecursiveRuntimeDependencies( + product.qbs.targetOS, + product.cpp.toolchainInstallPath, + product.qbs.targetOS.contains('windows')? product.Qt.core.binPath : product.Qt.core.libPath, + dynamicLib, + product.qbs.architecture + ) + + deps = DeployQt.unionSets(deps, qtLibs) + }) + + const depsList = product.qbs.targetOS.contains('darwin')? + Array.from(deps).flatMap(function (dep) { + return DeployQt.frameworkLibToFrameworkContents(dep) + }) : Array.from(deps) + + var file = new TextFile(outputs['easy.deployqt.pluginsdeps'][0].filePath, TextFile.WriteOnly) + file.write(depsList.join('\n')) + file.close() + } + + return [ + copyPlugins, + scanDynamicDeps, + ] + } + } + + Rule { + // Scan QML-files from QRC-files and save the list of imports into a file + + requiresInputs: false + multiplex: true + condition: scanQml + + inputs: ['qrc'] + + Artifact { + filePath: FileInfo.joinPaths(product.buildDirectory, 'easy.deployqt', 'qmlimports.json') + fileTags: ['easy.deployqt.qmlimports'] + } + + prepare: /*(project, product, inputs, outputs, input, output, explicitlyDependsOn) =>*/ { + var scanImports = new JavaScriptCommand() + scanImports.silent = true + scanImports.sourceCode = function () { + const imports = DeployQt.collectQmlImports( + product.Qt.qml.qmlImportScannerFilePath, + (inputs['qrc'] || []), + product.Qt.qml.qmlPath, + product.qbs.targetOS + ) + + var file = new TextFile(output.filePath, TextFile.WriteOnly) + file.write(JSON.stringify(imports, null, 2)) + file.close() + } + + return [ + scanImports, + ] + } + } + + Rule { + // Read the QML-imports of Qt's modules from the file, copy modules into the install-root, + // and save the list of dynamic dependencies into another file + + multiplex: false // the input is always one + condition: scanQml + + inputs: ['easy.deployqt.qmlimports'] + + outputFileTags: ['easy.deployqt.qmlimportsdeps', 'dmg.input'] + outputArtifacts: { + var list = [] + + const imports = DeployQt.readQmlImports(input.filePath) + + imports.forEach(function (imp) { + const sourcePath = imp.path + const folder = FileInfo.path(sourcePath) + + File.directoryEntries(folder, File.Files).forEach(function (entry) { + list.push({ + filePath: DeployQt.toTargetPath( + FileInfo.joinPaths(folder, entry), + product.Qt.qml.qmlPath, + project.installImportsDir + ), + fileTags: ['dmg.input'] + }) + }) + }) + + return list.concat([ + { + filePath: FileInfo.joinPaths(product.buildDirectory, 'easy.deployqt', 'qmlimportsdeps.list'), + fileTags: ['easy.deployqt.qmlimportsdeps'] + } + ]) + } + + prepare: /*(project, product, inputs, outputs, input, output, explicitlyDependsOn) =>*/ { + var copyQmlModules = new JavaScriptCommand() + copyQmlModules.silent = true + copyQmlModules.sourceCode = function () { + const imports = DeployQt.readQmlImports(input.filePath) + + imports.forEach(function (imp) { + const sourcePath = imp.path + const folder = FileInfo.path(sourcePath) + + var parser = new Qmldir.QmldirParser(FileInfo.joinPaths(folder, 'qmldir'), product.qbs.targetOS) + parser.parse() + + var allFiles = new Set([FileInfo.joinPaths(folder, 'qmldir')]) + + parser.files.forEach(function (file) { + allFiles.add(FileInfo.joinPaths(folder, file)) + }) + + parser.plugins.forEach(function (plugin) { + if (FileInfo.isAbsolutePath(plugin)) + allFiles.add(plugin) + else + allFiles.add(FileInfo.joinPaths(folder, plugin)) + }) + + DeployQt.collectAssets(folder).forEach(function (entry) { + allFiles.add(entry) + }) + + allFiles.forEach(function (entry) { + File.copy( + entry, + DeployQt.toTargetPath( + entry, + product.Qt.qml.qmlPath, + project.installImportsDir + ) + ) + }) + }) + } + + var scanDynamicDeps = new JavaScriptCommand() + scanDynamicDeps.silent = true + scanDynamicDeps.sourceCode = function () { + const imports = DeployQt.readQmlImports(input.filePath) + + var deps = new Set([]) + + for (var i in imports) { + const dynamicLib = imports[i].path + + const qtLibs = DeployQt.getRecursiveRuntimeDependencies( + product.qbs.targetOS, + product.cpp.toolchainInstallPath, + product.qbs.targetOS.contains('windows')? product.Qt.core.binPath : product.Qt.core.libPath, + dynamicLib, + product.qbs.architecture + ) + + const moreDeps = new Set(qtLibs) + + deps = DeployQt.unionSets(deps, moreDeps) + } + + const depsList = product.qbs.targetOS.contains('darwin')? + Array.from(deps).flatMap(function (dep) { + return DeployQt.frameworkLibToFrameworkContents(dep) + }) : Array.from(deps) + + var file = new TextFile(outputs['easy.deployqt.qmlimportsdeps'][0].filePath, TextFile.WriteOnly) + file.write(depsList.join('\n')) + file.close() + } + + return [ + copyQmlModules, + scanDynamicDeps, + ] + } + } + + Scanner { + inputs: ['easy.deployqt.qmlimportsdeps', 'easy.deployqt.pluginsdeps'] + scan: /*(project, product, input, filePath) =>*/ DeployQt.readDepsFromFile(filePath) + } + + Rule { + // Read the list of files to install from the files and copy them into the install-root + + multiplex: true // the input is always one + + inputs: ['easy.deployqt.qmlimportsdeps', 'easy.deployqt.pluginsdeps'] + + outputFileTags: ['dmg.input', 'easy.deployqt.empty'] + outputArtifacts: { + const installableDepsSet = DeployQt.readDepsForTags( + inputs, + ['easy.deployqt.qmlimportsdeps', 'easy.deployqt.pluginsdeps'] + ) + const installableDeps = Array.from(installableDepsSet) + + if (installableDeps.length === 0) { + return [ + {filePath: 'empty', fileTags: ['easy.deployqt.empty']} + ] + } + else { + return installableDeps.map(function (dep) { + return { + filePath: DeployQt.toTargetPath( + dep, + product.qbs.targetOS.contains('windows')? product.Qt.core.binPath : product.Qt.core.libPath, + project.installLibraryDir + ), + fileTags: ['dmg.input'] + } + }) + } + } + + prepare: /* (project, product, inputs, outputs, input, output, explicitlyDependsOn) => */ { + var cmd = new JavaScriptCommand() + cmd.silent = true + cmd.sourceCode = function () { + const deps = DeployQt.readDepsForTags( + inputs, + ['easy.deployqt.qmlimportsdeps', 'easy.deployqt.pluginsdeps'] + ) + + deps.forEach(function (dep) { + const targetPath = DeployQt.toTargetPath( + dep, + product.qbs.targetOS.contains('windows')? product.Qt.core.binPath : product.Qt.core.libPath, + project.installLibraryDir + ) + + if (!File.exists(targetPath)) { + File.copy(dep, targetPath) + } + }) + } + return [cmd] + } + } +} diff --git a/modules/easy/deployqt/qmldir-parser.js b/modules/easy/deployqt/qmldir-parser.js new file mode 100644 index 0000000..f17fb55 --- /dev/null +++ b/modules/easy/deployqt/qmldir-parser.js @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: © 2024 Serhii “GooRoo” Olendarenko +// +// SPDX-License-Identifier: BSD-3-Clause + +const FileInfo = require('qbs.FileInfo') +const TextFile = require('qbs.TextFile') + +var QmldirParser = (function () { + function Parser(qmldirFilePath, os) { + var qmldir = new TextFile(qmldirFilePath, TextFile.ReadOnly) + this.lines = qmldir.readAll().split('\n') + qmldir.close() + + this.os = os + + this.module = '' + this.plugins = new Set() + this.depends = new Set() + this.imports = new Set() + this.optionalImports = new Set() + this.files = new Set() + } + + Parser.prototype.constructor = Parser + + Parser.prototype.parseLine = function (line) { + if (line.trim() === '' + || line.startsWith('#') + || line.startsWith('linktarget') + || line.startsWith('prefer') + || line.startsWith('classname') + || line.startsWith('designersupported') + || line.startsWith('system')) { + // skip things I don't need now + return + } else if (line.startsWith('module')) { + this.parseModule(line) + } else if (line.startsWith('plugin') || /^optional\s+plugin/.test(line)) { + this.parsePlugin(line) + } else if (line.startsWith('depends')) { + this.parseDepends(line) + } else if (line.startsWith('import') || /^(optional|default)\s+import/.test(line)) { + this.parseImport(line) + } else if (line.startsWith('typeinfo')) { + this.parseTypeInfo(line) + } else if (line.startsWith('internal')) { + this.parseInternal(line) + } else { + this.parseObjectDeclaration(line) + } + } + + Parser.prototype.parseModule = function (moduleString) { + const modulePattern = /^module\s+(?[\w\.]+)?$/ + const match = moduleString.match(modulePattern) + if (match) { + this.module = match.groups.name + } else { + throw new Error('Cannot parse the module string: ' + moduleString) + } + } + + Parser.prototype.parseObjectDeclaration = function (objectDeclarationString) { + const pattern = /^(?:singleton\s+)?(?\w+)\s+(?\d+\.\d+)\s+(?.+)$/ + const match = objectDeclarationString.match(pattern) + if (match) { + this.files.add(match.groups.path) + } else { + throw new Error('Cannot parse the object declaration string: ' + objectDeclarationString) + } + } + + Parser.prototype.parseInternal = function (internalString) { + const internalPattern = /^internal\s+(?\w+)\s+(?.+)$/ + const match = internalString.match(internalPattern) + if (match) { + this.files.add(match.groups.path) + } else { + throw new Error('Cannot parse the internal string: ' + internalString) + } + } + + Parser.prototype.parseTypeInfo = function (typeInfoString) { + const typeInfoPattern = /^typeinfo\s+(?.+)$/ + const match = typeInfoString.match(typeInfoPattern) + if (match) { + this.files.add(match.groups.path) + } else { + throw new Error('Cannot parse the typeinfo string: ' + typeInfoString) + } + } + + Parser.prototype.parsePlugin = function (pluginString) { + const pluginPattern = /^(optional\s+)?plugin\s+(?\w+)(?:\s+(?[\w/]+))?$/ + const match = pluginString.match(pluginPattern) + if (match) { + var name = this.pluginNameToFileName(match.groups.name) + if (match.groups.path) { + name = FileInfo.joinPaths(match.groups.path, name) + } + this.plugins.add(name) + } else { + throw new Error('Cannot parse the plugin string: ' + pluginString) + } + } + + Parser.prototype.pluginNameToFileName = function (libName) { + if (this.os.contains('windows')) { + return libName + '.dll' + } else if (this.os.contains('macos')) { + return 'lib' + libName + '.dylib' + } + } + + Parser.prototype.parseDepends = function (dependsString) { + const dependsPattern = /^depends\s+(?[\w\.]+)(?:\s+(?(?:auto|\d+(?:\.\d+)*)))?$/ + const match = dependsString.match(dependsPattern) + if (match) { + this.depends.add(match.groups.name) + } else { + throw new Error('Cannot parse the depends string: ' + dependsString) + } + } + + Parser.prototype.parseImport = function (importString) { + const importPattern = /^(?:default\s+)?(?optional\s+)?import\s+(?[\w\.]+)(?:\s+(?(?:auto|\d+(?:\.\d+)*)))$/ + const match = importString.match(importPattern) + if (match) { + if (match.groups.optional) { + this.optionalImports.add(match.groups.name) + } else { + this.imports.add(match.groups.name) + } + } else { + throw new Error('Cannot parse the import string: ' + importString) + } + } + + Parser.prototype.parse = function () { + for (var i in this.lines) { + this.parseLine(this.lines[i]) + } + return { + plugins: this.plugins, + depends: this.depends, + imports: this.imports, + optionalImports: this.optionalImports, + files: this.files, + } + } + + return Parser +})()