arch-repo-manager/libpkg/algo/licenses.cpp

336 lines
13 KiB
C++

#include "../data/config.h"
#include <c++utilities/conversion/stringbuilder.h>
#include <fstream>
#include <sstream>
#include <iostream>
using namespace std;
using namespace CppUtilities;
namespace LibPkg {
bool Config::addLicenseInfo(LicenseResult &result, const Dependency &dependency)
{
// find the referenced package
auto searchResult = findPackage(dependency);
const auto &package = searchResult.pkg;
if (!package) {
result.success = false;
result.notes.emplace_back("Unable to locate " + dependency.toString());
return false;
}
auto packageID = addLicenseInfo(result, searchResult, package);
if (packageID.empty()) {
result.ignoredPackages.emplace_back(package->name % '-' + package->version);
return false;
}
result.consideredPackages.emplace_back(package->name % '-' + package->version);
if (result.mainProject.empty()) {
result.mainProject = move(packageID);
} else {
result.dependendProjects.emplace(move(packageID));
}
return true;
}
std::string Config::addLicenseInfo(LicenseResult &result, PackageSearchResult &searchResult, const std::shared_ptr<Package> &package)
{
// make up some identifier to refer to the package in the license summary
// * use the "regular" package name (e.g. gcc instead of mingw-w64-gcc)
// * include only the upstream version (but not epoch and pkgrel)
// * the licensing files for the whole MinGW-w64 project is contained by the mingw-w64-headers package
// * the licensing files for GCC is contained by the gcc-libs package
// * the licensing files for all Qt modules are contained within qt5-base; don't distinguish the modules for our purposes
// * use the real project name if known
const auto upstreamVersion = PackageVersion::fromString(package->version).upstream;
auto regularPackageName = package->computeRegularPackageName();
auto packageID = regularPackageName.empty() ? package->name : regularPackageName;
static const auto displayNames = unordered_map<string, string>{
{ "mingw-w64-headers", "MinGW-w64" },
{ "gcc", "GCC" },
{ "gcc-libs", "GCC" },
{ "freetype2", "FreeType" },
{ "harfbuzz", "HarfBuzz" },
{ "graphite", "Graphite" },
{ "openssl", "OpenSSL" },
{ "pcre", "PCRE" },
{ "pcre2", "PCRE2" },
{ "glib2", "GLib" },
{ "numix-icon-theme", "Numix icon theme" },
{ "breeze-icons", "Breeze icons (from KDE)" },
{ "go", "Go" },
{ "syncthing", "Syncthing" },
{ "syncthingtray", "Syncthing Tray" },
{ "tageditor", "Tag Editor" },
{ "passwordmanager", "Password Manager" },
};
if (endsWith(packageID, "-git") || endsWith(packageID, "-svn")) {
packageID.resize(packageID.size() - 4);
} else if (endsWith(packageID, "-hg")) {
packageID.resize(packageID.size() - 3);
}
if (const auto displayName = displayNames.find(packageID); displayName != displayNames.cend()) {
packageID = displayName->second;
} else if (startsWith(packageID, "qt5-")) {
packageID = "Qt 5";
regularPackageName = "qt5-base";
}
// skip package if custom license has already been added
if (const auto customLicenses = result.customLicences.find(packageID);
customLicenses != result.customLicences.end() && !customLicenses->second.empty()) {
return packageID;
}
// check whether the package has a standard license and/or a custom license
bool hasCustomLicense = package->licenses.empty();
if (packageID == "Qt 5") {
// consider Qt's licenses custom as it has special variants of the standard licenses FDL, GPL and LGPL
hasCustomLicense = true;
} else {
// read the package's license field (see https://wiki.archlinux.org/index.php/PKGBUILD#license)
for (const auto &license : package->licenses) {
// check for custom licenses and licenses which contain project specific references and are therefore considered custom as well
if (startsWith(license, "custom") || license == "BSD" || license == "ISC" || license == "MIT" || license == "ZLIB"
|| license == "Python") {
hasCustomLicense = true;
continue;
}
// map Arch Linux generic way to say e.g. "GPL2 and above" to a concrete license e.g. "GPL2"
auto concreteLicense = license;
if (license == "GPL") {
concreteLicense = "GPL2";
} else if (license == "LGPL" || license == "LGPL2") {
concreteLicense = "LGPL2.1";
} else if (license == "FDL") {
concreteLicense = "FDL1.2";
} else if (license == "AGPL") {
concreteLicense = "AGPL3";
} else if (license == "APACHE") {
concreteLicense = "Apache";
}
result.commonLicenses[concreteLicense].relevantPackages.emplace(packageID);
}
}
if (!hasCustomLicense) {
return packageID;
}
// read custom license
if (!regularPackageName.empty()) {
const auto regularPackageSearchResult = findPackage(Dependency(regularPackageName, package->version));
if (regularPackageSearchResult.pkg) {
const auto regularUpstreamVersion = PackageVersion::fromString(regularPackageSearchResult.pkg->version).upstream;
if (upstreamVersion != regularUpstreamVersion) {
result.success = false; // likely the license hasn't change; let's continue but don't consider it a successful run
result.notes.emplace_back(
"Regular package for " % searchResult.pkg->name % '-' % searchResult.pkg->version % " has different upstream version "
+ regularUpstreamVersion);
}
searchResult.db = regularPackageSearchResult.db;
searchResult.pkg = regularPackageSearchResult.pkg;
}
}
// -> locate package
const auto *const db = std::get<Database *>(searchResult.db);
if (!db) {
packageID.clear();
return packageID;
}
const auto &packageInfo = searchResult.pkg->packageInfo;
if (!packageInfo) {
packageID.clear();
return packageID;
}
const auto path = db->localPkgDir % '/' + packageInfo->fileName;
try {
auto directories = extractFiles(path, &Package::isLicense);
auto &licenses = result.customLicences[packageID];
for (auto &dir : directories) {
for (auto &file : dir.second) {
if (file.type != ArchiveFileType::Regular) {
// skip anything but regular files (symlinks might point to other packages so resolving them is tricky)
continue;
}
licenses.emplace_back(dir.first % '/' + file.name, move(file.content));
}
}
if (licenses.empty()) {
const auto &packageName = searchResult.pkg->name;
string fallback;
if (startsWith(packageName, "qt5-")) {
fallback = "qt5-base"; // the qt5-base package contains licenses for the other qt5-* packages
} else if (packageName == "gcc") {
fallback = "gcc-libs"; // the gcc-libs packages contains licenses for the gcc package
}
if (!fallback.empty() && fallback != packageName) {
result.notes.emplace_back("No license file contained in " % path % ", falling back to " + fallback);
if (!addLicenseInfo(result, Dependency(fallback, searchResult.pkg->version))) {
packageID.clear();
}
return packageID;
}
result.success = false;
result.notes.emplace_back("No license file contained in " + path);
packageID.clear();
return packageID;
}
} catch (const runtime_error &e) {
result.success = false;
result.notes.emplace_back(e.what());
packageID.clear();
return packageID;
}
return packageID;
}
static void printQuoted(ostream &os, const string &str)
{
bool needQuote = true;
for (auto c : str) {
if (needQuote) {
os << "> ";
needQuote = false;
}
os << c;
if (c == '\n') {
needQuote = true;
}
}
}
LicenseResult Config::computeLicenseInfo(const std::vector<string> &dependencyDenotations)
{
LicenseResult result;
// find "licenses" package containing common licenses
const auto commonLicensePackageSearch = findPackage(Dependency("licenses"));
const auto &licensesPackage = commonLicensePackageSearch.pkg;
const auto *const db = std::get<Database *>(commonLicensePackageSearch.db);
if (!db || !licensesPackage || !licensesPackage->packageInfo) {
result.success = false;
result.notes.emplace_back("Unable to find licenses package.");
return result;
}
// extract common licenses
const auto path = db->localPkgDir % '/' + licensesPackage->packageInfo->fileName;
decltype(extractFiles(path, &Package::isLicense)) licensesDirs;
try {
licensesDirs = extractFiles(path, &Package::isLicense);
if (licensesDirs.empty()) {
result.success = false;
result.notes.emplace_back("No relevant files found in licenses package.");
return result;
}
} catch (const runtime_error &e) {
result.success = false;
result.notes.emplace_back(e.what());
return result;
}
// add required common licenses and custom licenses
for (const auto &dependencyDenotation : dependencyDenotations) {
addLicenseInfo(result, Dependency(dependencyDenotation.data(), dependencyDenotation.size()));
}
// find relevant common licenses in licenses package
for (auto &commonLicense : result.commonLicenses) {
const auto &licenseName = commonLicense.first;
auto &license = commonLicense.second;
const auto dir = licensesDirs.find("usr/share/licenses/common/" + licenseName);
if (dir == licensesDirs.end() || dir->second.empty()) {
result.success = false;
result.notes.emplace_back("Unable to find license dir for common license " + licenseName);
continue;
}
for (auto &file : dir->second) {
license.files.emplace_back(move(file.name), move(file.content));
}
}
// move "unique" common licenses to project specific licenses
for (auto i = result.commonLicenses.begin(); i != result.commonLicenses.end();) {
auto &license = i->second;
if (license.relevantPackages.size() != 1) {
++i;
continue;
}
auto &customLicenses = result.customLicences[*license.relevantPackages.cbegin()];
customLicenses.insert(customLicenses.begin(), license.files.begin(), license.files.end());
result.commonLicenses.erase(i++);
}
if (result.dependendProjects.empty()) {
return result;
}
// start summary
stringstream summary;
summary << "This file contains licensing information for `" << result.mainProject << "` and libraries distributed with it:\n";
for (const auto &dependency : result.dependendProjects) {
summary << " * `" << dependency << "`\n";
}
// add common licenses to summary
for (const auto &commonLicense : result.commonLicenses) {
const auto &licenseName = commonLicense.first;
auto &license = commonLicense.second;
if (summary.tellp()) {
summary << "\n---\n\n";
}
summary << "License";
if (license.files.size() != 1) {
summary << 's';
}
summary << " `" << licenseName << "` of " << joinStrings(license.relevantPackages, ", ", false, "`", "`") << ":\n\n";
for (const auto &licenseFile : license.files) {
if (license.files.size() > 1) {
summary << '`' << licenseFile.filename << "`: \n";
}
printQuoted(summary, licenseFile.content);
}
}
// add custom licenses to summary
for (const auto &customLicense : result.customLicences) {
if (customLicense.second.empty()) {
continue;
}
if (summary.tellp()) {
summary << "\n---\n\n";
}
summary << "License";
if (customLicense.second.size() != 1) {
summary << 's';
}
summary << " of `" << customLicense.first << "`:\n\n";
for (const auto &license : customLicense.second) {
if (customLicense.second.size() > 1) {
summary << '`' << license.filename << "`: \n";
}
printQuoted(summary, license.content);
}
}
result.licenseSummary = summary.str();
// print the summary for debugging (FIXME: remove debug code)
fstream("license-summary.md", ios_base::out | ios_base::trunc) << result.licenseSummary;
// add project specific licenses
return result;
}
} // namespace LibPkg