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