1 /****************************************************************************
2 **
3 ** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com)
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qbs.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39
40 #include "pkgconfig.h"
41 #include "pcparser.h"
42
43 #if HAS_STD_FILESYSTEM
44 # if __has_include(<filesystem>)
45 # include <filesystem>
46 # else
47 # include <experimental/filesystem>
48 // We need the alias from std::experimental::filesystem to std::filesystem
49 namespace std {
50 namespace filesystem = experimental::filesystem;
51 }
52 # endif
53 #else
54 # include <QtCore/QDir>
55 # include <QtCore/QFileInfo>
56 #endif
57
58 #include <algorithm>
59 #include <iostream>
60
61 namespace qbs {
62
63 namespace {
64
varToEnvVar(std::string_view pkg,std::string_view var)65 std::string varToEnvVar(std::string_view pkg, std::string_view var)
66 {
67 auto result = std::string("PKG_CONFIG_");
68 result += pkg;
69 result += '_';
70 result += var;
71
72 for (char &p : result) {
73 int c = std::toupper(p);
74
75 if (!std::isalnum(c))
76 c = '_';
77
78 p = char(c);
79 }
80
81 return result;
82 }
83
split(std::string_view str,const char delim)84 std::vector<std::string> split(std::string_view str, const char delim)
85 {
86 std::vector<std::string> result;
87 size_t prev = 0;
88 size_t pos = 0;
89 do {
90 pos = str.find(delim, prev);
91 if (pos == std::string::npos) pos = str.length();
92 std::string token(str.substr(prev, pos - prev));
93 if (!token.empty())
94 result.push_back(token);
95 prev = pos + 1;
96 } while (pos < str.length() && prev < str.length());
97 return result;
98 }
99
listSeparator()100 constexpr inline char listSeparator() noexcept
101 {
102 #if defined(WIN32)
103 return ';';
104 #else
105 return ':';
106 #endif
107 }
108
109 // based on https://stackoverflow.com/a/33135699/295518
compareVersions(std::string_view v1,std::string_view v2)110 int compareVersions(std::string_view v1, std::string_view v2)
111 {
112 for (size_t i = 0, j = 0; i < v1.length() || j < v2.length(); ) {
113 size_t acc1 = 0;
114 size_t acc2 = 0;
115
116 while (i < v1.length() && v1[i] != '.') {
117 acc1 = acc1 * 10 + (v1[i] - '0');
118 i++;
119 }
120 while (j < v2.length() && v2[j] != '.') {
121 acc2 = acc2 * 10 + (v2[j] - '0');
122 j++;
123 }
124
125 if (acc1 < acc2)
126 return -1;
127 if (acc1 > acc2)
128 return +1;
129
130 ++i;
131 ++j;
132 }
133 return 0;
134 }
135
136 using ComparisonType = PcPackage::RequiredVersion::ComparisonType;
137
versionTest(ComparisonType comparison,std::string_view a,std::string_view b)138 bool versionTest(ComparisonType comparison, std::string_view a, std::string_view b)
139 {
140 switch (comparison) {
141 case ComparisonType::LessThan: return compareVersions(a, b) < 0;
142 case ComparisonType::GreaterThan: return compareVersions(a, b) > 0;
143 case ComparisonType::LessThanEqual: return compareVersions(a, b) <= 0;
144 case ComparisonType::GreaterThanEqual: return compareVersions(a, b) >= 0;
145 case ComparisonType::Equal: return compareVersions(a, b) == 0;
146 case ComparisonType::NotEqual: return compareVersions(a, b) != 0;
147 case ComparisonType::AlwaysMatch: return true;
148 }
149
150 return false;
151 }
152
raizeUnknownPackageException(std::string_view package)153 [[noreturn]] void raizeUnknownPackageException(std::string_view package)
154 {
155 std::string message;
156 message += "Can't find package '";
157 message += package;
158 message += "'";
159 throw PcException(message);
160 }
161
162 template <class C>
operator <<(C & container,const C & other)163 C &operator<<(C &container, const C &other)
164 {
165 container.insert(container.end(), other.cbegin(), other.cend());
166 return container;
167 }
168
169 } // namespace
170
PkgConfig()171 PkgConfig::PkgConfig()
172 : PkgConfig(Options())
173 {
174 }
175
PkgConfig(Options options)176 PkgConfig::PkgConfig(Options options)
177 : m_options(std::move(options))
178 {
179 if (m_options.libDirs.empty())
180 m_options.libDirs = split(PKG_CONFIG_PC_PATH, listSeparator());
181
182 if (m_options.topBuildDir.empty())
183 m_options.topBuildDir = "$(top_builddir)"; // pkg-config sets this for automake =)
184
185 if (m_options.systemLibraryPaths.empty())
186 m_options.systemLibraryPaths = split(PKG_CONFIG_SYSTEM_LIBRARY_PATH, ':');
187
188 // this is weird on Windows, but that's what pkg-config does
189 if (m_options.sysroot.empty())
190 m_options.globalVariables["pc_sysrootdir"] = "/";
191 else
192 m_options.globalVariables["pc_sysrootdir"] = m_options.sysroot;
193 m_options.globalVariables["pc_top_builddir"] = m_options.topBuildDir;
194
195 m_packages = findPackages();
196 }
197
getPackage(std::string_view baseFileName) const198 const PcPackageVariant &PkgConfig::getPackage(std::string_view baseFileName) const
199 {
200 // heterogeneous comparator so we can search the package using string_view
201 const auto lessThan = [](const PcPackageVariant &package, const std::string_view &name)
202 {
203 return package.visit([name](auto &&value) noexcept {
204 return value.baseFileName < name;
205 });
206 };
207
208 const auto testPackage = [baseFileName](const PcPackageVariant &package) {
209 return package.visit([baseFileName](auto &&value) noexcept {
210 return baseFileName != value.baseFileName;
211 });
212 };
213
214 const auto it = std::lower_bound(m_packages.begin(), m_packages.end(), baseFileName, lessThan);
215 if (it == m_packages.end() || testPackage(*it))
216 raizeUnknownPackageException(baseFileName);
217 return *it;
218 }
219
packageGetVariable(const PcPackage & pkg,std::string_view var) const220 std::string_view PkgConfig::packageGetVariable(const PcPackage &pkg, std::string_view var) const
221 {
222 std::string_view varval;
223
224 if (var.empty())
225 return varval;
226
227 const auto &globals = m_options.globalVariables;
228 if (auto it = globals.find(var); it != globals.end())
229 varval = it->second;
230
231 // Allow overriding specific variables using an environment variable of the
232 // form PKG_CONFIG_$PACKAGENAME_$VARIABLE
233 if (!pkg.baseFileName.empty()) {
234 const std::string envVariable = varToEnvVar(pkg.baseFileName, var);
235 const auto it = m_options.systemVariables.find(envVariable);
236 if (it != m_options.systemVariables.end())
237 return it->second;
238 }
239
240 if (varval.empty()) {
241 const auto it = pkg.variables.find(var);
242 varval = (it != pkg.variables.end()) ? it->second : std::string_view();
243 }
244
245 return varval;
246 }
247
248 #if HAS_STD_FILESYSTEM
getPcFilePaths(const std::vector<std::string> & searchPaths)249 std::vector<std::string> getPcFilePaths(const std::vector<std::string> &searchPaths)
250 {
251 std::vector<std::filesystem::path> paths;
252
253 for (const auto &searchPath : searchPaths) {
254 if (!std::filesystem::exists(std::filesystem::directory_entry(searchPath).status()))
255 continue;
256 const auto dir = std::filesystem::directory_iterator(searchPath);
257 std::copy_if(
258 std::filesystem::begin(dir),
259 std::filesystem::end(dir),
260 std::back_inserter(paths),
261 [](const auto &entry) { return entry.path().extension() == ".pc"; }
262 );
263 }
264 std::vector<std::string> result;
265 std::transform(
266 std::begin(paths),
267 std::end(paths),
268 std::back_inserter(result),
269 [](const auto &path) { return path.generic_string(); }
270 );
271 return result;
272 }
273 #else
getPcFilePaths(const std::vector<std::string> & searchPaths)274 std::vector<std::string> getPcFilePaths(const std::vector<std::string> &searchPaths)
275 {
276 std::vector<std::string> result;
277 for (const auto &path : searchPaths) {
278 QDir dir(QString::fromStdString(path));
279 const auto paths = dir.entryList({QStringLiteral("*.pc")});
280 std::transform(
281 std::begin(paths),
282 std::end(paths),
283 std::back_inserter(result),
284 [&dir](const auto &path) { return dir.filePath(path).toStdString(); }
285 );
286 }
287 return result;
288 }
289 #endif
290
makeMissingDependency(const PcPackage & package,const PcPackage::RequiredVersion & depVersion)291 PcBrokenPackage makeMissingDependency(
292 const PcPackage &package, const PcPackage::RequiredVersion &depVersion)
293 {
294 std::string message;
295 message += "Package ";
296 message += package.name;
297 message += " requires package ";
298 message += depVersion.name;
299 message += " but it is not found";
300 return PcBrokenPackage{
301 package.filePath, package.baseFileName, std::move(message)};
302 }
303
makeBrokenDependency(const PcPackage & package,const PcPackage::RequiredVersion & depVersion)304 PcBrokenPackage makeBrokenDependency(
305 const PcPackage &package, const PcPackage::RequiredVersion &depVersion)
306 {
307 std::string message;
308 message += "Package ";
309 message += package.name;
310 message += " requires package ";
311 message += depVersion.name;
312 message += " but it is broken";
313 return PcBrokenPackage{
314 package.filePath, package.baseFileName, std::move(message)};
315 }
316
makeVersionMismatchDependency(const PcPackage & package,const PcPackage & depPackage,const PcPackage::RequiredVersion & depVersion)317 PcBrokenPackage makeVersionMismatchDependency(
318 const PcPackage &package,
319 const PcPackage &depPackage,
320 const PcPackage::RequiredVersion &depVersion)
321 {
322 std::string message;
323 message += "Package ";
324 message += package.name;
325 message += " requires version ";
326 message += PcPackage::RequiredVersion::comparisonToString(
327 depVersion.comparison);
328 message += depVersion.version;
329 message += " but ";
330 message += depPackage.version;
331 message += " is present";
332 return PcBrokenPackage{
333 package.filePath, package.baseFileName, std::move(message)};
334 }
335
mergeDependencies(const PkgConfig::Packages & packages) const336 PkgConfig::Packages PkgConfig::mergeDependencies(const PkgConfig::Packages &packages) const
337 {
338 std::unordered_map<std::string_view, const PcPackageVariant *> packageHash;
339
340 struct MergedHashEntry
341 {
342 PcPackageVariant package; // merged package or broken package
343 std::vector<const PcPackage *> deps; // unmerged transitive deps, including Package itself
344 };
345 std::unordered_map<std::string, MergedHashEntry> mergedHash;
346
347 for (const auto &package: packages)
348 packageHash[package.getBaseFileName()] = &package;
349
350 auto func = [&](const PcPackageVariant &package, auto &f) -> const MergedHashEntry &
351 {
352 const auto it = mergedHash.find(package.getBaseFileName());
353 if (it != mergedHash.end())
354 return it->second;
355
356 auto &entry = mergedHash[package.getBaseFileName()];
357
358 auto visitor = [&](auto &&package) -> PcPackageVariant {
359
360 using T = std::decay_t<decltype(package)>;
361 if constexpr (std::is_same_v<T, PcPackage>) { // NOLINT
362
363 using Flags = std::vector<PcPackage::Flag>;
364
365 // returns true if multiple copies of the flag can present in the same package
366 // we can't properly merge flags that have multiple parameters except for
367 // -framework which we handle correctly.
368 auto canHaveDuplicates = [](const PcPackage::Flag::Type &type) {
369 return type == PcPackage::Flag::Type::LinkerFlag
370 || type == PcPackage::Flag::Type::CompilerFlag;
371 };
372
373 std::unordered_set<PcPackage::Flag> visitedFlags;
374 // appends only those flags to the target that were not seen before (except for
375 // ones that can have duplicates)
376 auto mergeFlags = [&](Flags &target, const Flags &source)
377 {
378 for (const auto &flag: source) {
379 if (canHaveDuplicates(flag.type) || visitedFlags.insert(flag).second)
380 target.push_back(flag);
381 }
382 };
383
384 std::unordered_set<const PcPackage *> visitedDeps;
385
386 PcPackage result;
387 // copy only meta info for now
388 result.filePath = package.filePath;
389 result.baseFileName = package.baseFileName;
390 result.name = package.name;
391 result.version = package.version;
392 result.description = package.description;
393 result.url = package.url;
394 result.variables = package.variables;
395
396 auto allDependencies = package.requiresPublic;
397 if (m_options.staticMode)
398 allDependencies << package.requiresPrivate;
399
400 for (const auto &dependency: allDependencies) {
401 const auto it = packageHash.find(dependency.name);
402 if (it == packageHash.end())
403 return makeMissingDependency(result, dependency);
404
405 const auto childEntry = f(*it->second, f);
406 if (childEntry.package.isBroken())
407 return makeBrokenDependency(result, dependency);
408
409 const auto &mergedPackage = childEntry.package.asPackage();
410 const bool versionOk = versionTest(
411 dependency.comparison, mergedPackage.version, dependency.version);
412 if (!versionOk)
413 return makeVersionMismatchDependency(result, mergedPackage, dependency);
414
415 for (const auto *dep: childEntry.deps) {
416 if (visitedDeps.insert(dep).second)
417 entry.deps.push_back(dep);
418 }
419 }
420
421 entry.deps.push_back(&package);
422
423 for (const auto *dep: entry.deps) {
424 mergeFlags(result.libs, dep->libs);
425 mergeFlags(result.cflags, dep->cflags);
426 }
427
428 return result;
429 }
430 return package;
431 };
432 entry.package = package.visit(visitor);
433
434 return entry;
435 };
436
437 for (auto &package: packages)
438 func(package, func);
439
440 Packages result;
441 for (auto &[key, value]: mergedHash)
442 result.push_back(std::move(value.package));
443 return result;
444 }
445
findPackages() const446 PkgConfig::Packages PkgConfig::findPackages() const
447 {
448 Packages result;
449 PcParser parser(*this);
450
451 const auto systemLibraryPaths = !m_options.allowSystemLibraryPaths ?
452 std::unordered_set<std::string>(
453 m_options.systemLibraryPaths.begin(),
454 m_options.systemLibraryPaths.end()) : std::unordered_set<std::string>();
455
456 auto allSearchPaths = m_options.extraPaths;
457 allSearchPaths.insert(
458 allSearchPaths.end(), m_options.libDirs.begin(), m_options.libDirs.end());
459 const auto pcFilePaths = getPcFilePaths(allSearchPaths);
460
461 for (const auto &pcFilePath : pcFilePaths) {
462 if (m_options.disableUninstalled) {
463 if (pcFilePath.find("-uninstalled.pc") != std::string::npos)
464 continue;
465 }
466
467 auto pkg = parser.parsePackageFile(pcFilePath);
468 pkg.visit([&](auto &value) {
469 using T = std::decay_t<decltype(value)>;
470 if constexpr (std::is_same_v<T, PcPackage>) { // NOLINT
471 value = std::move(value)
472 // Weird, but pkg-config removes libs first and only then appends
473 // sysroot. Looks like sysroot has to be used with
474 // allowSystemLibraryPaths: true
475 .removeSystemLibraryPaths(systemLibraryPaths)
476 .prependSysroot(m_options.sysroot);
477 }
478 });
479 result.emplace_back(std::move(pkg));
480 }
481
482 if (m_options.mergeDependencies)
483 result = mergeDependencies(result);
484
485 const auto lessThanPackage = [](const PcPackageVariant &lhs, const PcPackageVariant &rhs)
486 {
487 return lhs.getBaseFileName() < rhs.getBaseFileName();
488 };
489 std::sort(result.begin(), result.end(), lessThanPackage);
490 return result;
491 }
492
493 } // namespace qbs
494