/* * Copyright (C) 2016 Matthias Klumpp * * Licensed under the GNU Lesser General Public License Version 3 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the license, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this software. If not, see . */ module asgen.engine; import std.stdio; import std.parallelism; import std.string : format, count, toLower, startsWith; import std.array : Appender, appender, empty; import std.path : buildPath, buildNormalizedPath; import std.file : mkdirRecurse, rmdirRecurse; import std.algorithm : canFind, sort, SwapStrategy; import std.typecons : scoped; static import std.file; import appstream.Component; import asgen.config; import asgen.logging; import asgen.extractor; import asgen.datastore; import asgen.contentsstore; import asgen.result; import asgen.hint; import asgen.reportgenerator; import asgen.utils : copyDir, stringArrayToByteArray; import asgen.backends.interfaces; import asgen.backends.dummy; import asgen.backends.debian; import asgen.backends.ubuntu; import asgen.backends.archlinux; import asgen.backends.rpmmd; import asgen.handlers.iconhandler; final class Engine { private: Config conf; PackageIndex pkgIndex; DataStore dstore; ContentsStore cstore; bool m_forced; public: this () { this.conf = Config.get (); switch (conf.backend) { case Backend.Dummy: pkgIndex = new DummyPackageIndex (conf.archiveRoot); break; case Backend.Debian: pkgIndex = new DebianPackageIndex (conf.archiveRoot); break; case Backend.Ubuntu: pkgIndex = new UbuntuPackageIndex (conf.archiveRoot); break; case Backend.Archlinux: pkgIndex = new ArchPackageIndex (conf.archiveRoot); break; case Backend.RpmMd: pkgIndex = new RPMPackageIndex (conf.archiveRoot); break; default: throw new Exception ("No backend specified, can not continue!"); } // create cache in cache directory on workspace dstore = new DataStore (); dstore.open (conf); // open package contents cache cstore = new ContentsStore (); cstore.open (conf); // for Cairo/Fontconfig issues with multithreading import asgen.image : setupFontconfigMutex; if (conf.featureEnabled (GeneratorFeature.PROCESS_FONTS)) setupFontconfigMutex (); } @property bool forced () { return m_forced; } @property void forced (bool v) { m_forced = v; } private void gcCollect () { static import core.memory; logDebug ("GC collection cycle triggered explicitly."); core.memory.GC.collect (); } /** * Extract metadata from a software container (usually a distro package). * The result is automatically stored in the database. */ private void processPackages (Package[] pkgs, IconHandler iconh) { auto mde = scoped!DataExtractor (dstore, iconh); foreach (ref pkg; parallel (pkgs)) { immutable pkid = pkg.id; if (dstore.packageExists (pkid)) continue; auto res = mde.processPackage (pkg); synchronized (dstore) { // write resulting data into the database dstore.addGeneratorResult (this.conf.metadataType, res); logInfo ("Processed %s, components: %s, hints: %s", res.pkid, res.componentsCount (), res.hintsCount ()); } // we don't need this package anymore pkg.close (); } } /** * Populate the contents index with new contents data. While we are at it, we can also mark * some uninteresting packages as to-be-ignored, so we don't waste time on them * during the following metadata extraction. * * Returns: True in case we have new interesting packages, false otherwise. **/ private bool seedContentsData (Suite suite, string section, string arch) { bool packageInteresting (const string[] contents) { foreach (ref c; contents) { if (c.startsWith ("/usr/share/applications/")) return true; if (c.startsWith ("/usr/share/metainfo/")) return true; if (c.startsWith ("/usr/share/appdata/")) return true; } return false; } // check if the index has changed data, skip the update if there's nothing new if ((!pkgIndex.hasChanges (dstore, suite.name, section, arch)) && (!this.forced)) { logDebug ("Skipping contents cache update for %s/%s [%s], index has not changed.", suite.name, section, arch); return false; } logInfo ("Scanning new packages for %s/%s [%s]", suite.name, section, arch); // get contents information for packages and add them to the database auto interestingFound = false; // First get the contents (only) of all packages in the base suite if (!suite.baseSuite.empty) { logInfo ("Scanning new packages for base suite %s/%s [%s]", suite.baseSuite, section, arch); auto baseSuitePkgs = pkgIndex.packagesFor (suite.baseSuite, section, arch); foreach (ref pkg; parallel (baseSuitePkgs, 8)) { immutable pkid = pkg.id; if (!cstore.packageExists (pkid)) { cstore.addContents (pkid, pkg.contents); logInfo ("Scanned %s for base suite.", pkid); } } } // And then scan the suite itself - here packages can be 'interesting' // in that they might end up in the output. auto pkgs = pkgIndex.packagesFor (suite.name, section, arch); foreach (ref pkg; parallel (pkgs, 8)) { immutable pkid = pkg.id; string[] contents; if (cstore.packageExists (pkid)) { if (dstore.packageExists (pkid)) { // TODO: Unfortunately, packages can move between suites without changing their ID. // This means as soon as we have an interesting package, even if we already processed it, // we need to regenerate the output metadata. // For that to happen, we set interestingFound to true here. Later, a more elegent solution // would be desirable here, ideally one which doesn't force us to track which package is // in which suite as well. if (!dstore.isIgnored (pkid)) interestingFound = true; continue; } // we will complement the main database with ignore data, in case it // went missing. contents = cstore.getContents (pkid); } else { // add contents to the index contents = pkg.contents; cstore.addContents (pkid, contents); } // check if we can already mark this package as ignored, and print some log messages if (!packageInteresting (contents)) { dstore.setPackageIgnore (pkid); logInfo ("Scanned %s, no interesting files found.", pkid); // we won't use this anymore pkg.close (); } else { logInfo ("Scanned %s, could be interesting.", pkid); interestingFound = true; } } return interestingFound; } private string getMetadataHead (Suite suite, string section) { import std.datetime : Clock; version (GNU) import core.time : FracSec; else import core.time : Duration; string head; immutable origin = "%s-%s-%s".format (conf.projectName.toLower, suite.name.toLower, section.toLower); auto time = Clock.currTime (); version (GNU) time.fracSec = FracSec.zero; // we don't want fractional seconds. else time.fracSecs = Duration.zero; // for newer Phobos immutable timeStr = time.toISOString (); string mediaPoolUrl = buildPath (conf.mediaBaseUrl, "pool"); if (conf.featureEnabled (GeneratorFeature.IMMUTABLE_SUITES)) { mediaPoolUrl = buildPath (conf.mediaBaseUrl, suite.name); } if (conf.metadataType == DataType.XML) { head = "\n"; head ~= format ("