repoindex/lib/alpm/resolvebuildorder.cpp

654 lines
20 KiB
C++

#include "./resolvebuildorder.h"
#include "./packagefinder.h"
#include "./manager.h"
#include "./config.h"
#include "./utilities.h"
#include "./repository.h"
#include <c++utilities/misc/memory.h>
#include <QStringBuilder>
#include <QEventLoop>
#include <iostream>
#include <sstream>
using namespace std;
using namespace ApplicationUtilities;
namespace RepoIndex {
/*!
* \brief The TaskInfo class defines a build task and provides the topo-sort required to resolve the build order.
*/
class TaskInfo
{
public:
TaskInfo(const QString &name, bool onlyDependency = false, const QSet<TaskInfo *> &deps = QSet<TaskInfo *>());
const QString &name() const;
void setName(const QString &name);
const QSet<TaskInfo *> deps() const;
void addDep(TaskInfo *dep);
bool areAllDepsAdded() const;
void setAllDepsAdded();
bool isDone() const;
bool isVisited() const;
bool isOnlyDependency() const;
void setIsOnlyDependency(bool isOnlyDependency);
bool isBinaryAvailable() const;
void setBinaryAvailable(bool binaryAvailable);
bool isPackageRequested() const;
void setPackageRequested();
QSet<Dependency> requiredFor() const;
void addRequiredFor(const Dependency &dependency);
void add(QList<TaskInfo *> &results);
Package *associatedPackage() const;
bool associatePackage(Package *package);
static void addAll(const QList<TaskInfo *> &tasks, QList<TaskInfo *> &results);
static TaskInfo *find(const QList<TaskInfo *> &tasks, const QString &name);
private:
QString m_name;
QSet<TaskInfo *> m_deps;
bool m_allDepsAdded;
bool m_done;
bool m_visited;
bool m_onlyDep;
bool m_binaryAvailable;
bool m_pkgRequested;
QSet<Dependency> m_requiredFor;
Package *m_associatedPackage;
};
/*!
* \brief Constructs a new task.
*/
inline TaskInfo::TaskInfo(const QString &name, bool onlyDependency, const QSet<TaskInfo *> &deps) :
m_name(name),
m_deps(deps),
m_allDepsAdded(false),
m_done(false),
m_visited(false),
m_onlyDep(onlyDependency),
m_binaryAvailable(false),
m_pkgRequested(false),
m_associatedPackage(nullptr)
{}
/*!
* \brief Returns the name of the task.
*
* This is usually the name which has been specified when constructing
* the task. However, the name might be adjusted to the package name
* when a package is associated.
*/
inline const QString &TaskInfo::name() const
{
return m_name;
}
inline void TaskInfo::setName(const QString &name)
{
m_name = name;
}
/*!
* \brief Returns the dependencies added via addDep().
*/
inline const QSet<TaskInfo *> TaskInfo::deps() const
{
return m_deps;
}
/*!
* \brief Adds another task as dependency.
*/
inline void TaskInfo::addDep(TaskInfo *dep)
{
m_deps << dep;
}
/*!
* \brief Returns whether setAllDepsAdded() has been called for this task.
*/
bool TaskInfo::areAllDepsAdded() const
{
return m_allDepsAdded;
}
/*!
* \brief Sets that all dependencies have been added for this task.
*/
void TaskInfo::setAllDepsAdded()
{
m_allDepsAdded = true;
}
/*!
* \brief Returns whether the task already has been added to the resulting list of tasks.
* \sa add()
*/
inline bool TaskInfo::isDone() const
{
return m_done;
}
/*!
* \brief Returns whether the task has been visited.
* \remarks Used to track circular dependencies (within add()).
*/
inline bool TaskInfo::isVisited() const
{
return m_visited;
}
/*!
* \brief Returns whether the task is only a dependency and none of the packages to be build.
* \remarks Circular dependencies between packages which are only dependencies can be ignored.
*/
inline bool TaskInfo::isOnlyDependency() const
{
return m_onlyDep;
}
/*!
* \brief Sets whether the package is only a dependency.
* \sa isOnlyDependency()
*/
inline void TaskInfo::setIsOnlyDependency(bool isOnlyDependency)
{
m_onlyDep = isOnlyDependency;
}
/*!
* \brief Returns whether a binary for the task is already available (from previous build).
* \remarks If a binary is available the resolver won't complain if this package is a
* cyclic dependency.
*/
inline bool TaskInfo::isBinaryAvailable() const
{
return m_binaryAvailable;
}
/*!
* \brief Sets whether a binary for the task is already available (from previous build).
* \sa isBinaryAvailable()
*/
inline void TaskInfo::setBinaryAvailable(bool binaryAvailable)
{
m_binaryAvailable = binaryAvailable;
}
/*!
* \brief Returns whether the package for this task has been requested yet.
*
* Used to avoid requesting the same package twice after the package
* couldn't be found when requesting it the first time.
*/
inline bool TaskInfo::isPackageRequested() const
{
return m_pkgRequested;
}
/*!
* \brief Sets whether the package has been requested.
*
* This is set just before a package for this task is requrested.
*/
inline void TaskInfo::setPackageRequested()
{
m_pkgRequested = true;
}
/*!
* \brief Returns the dependencies the task has been added for.
* \remarks
* - The same task can be added for multiple dependencies, eg.
* mingw-w64-extra-cmake-modules and mingw-w64-extra-cmake-modules=5.15.0.
* - The dependencies must be set explicitely using addRequiredFor().
*/
inline QSet<Dependency> TaskInfo::requiredFor() const
{
return m_requiredFor;
}
/*!
* \brief Adds the specified \a dependency to the list of dependencies the task has been added for.
* \sa requiredFor()
*/
inline void TaskInfo::addRequiredFor(const Dependency &dependency)
{
m_requiredFor << dependency;
}
/*!
* \brief Adds the task to the specified list of results. Ensures that all dependencies are added before.
* \throws If a circular dependency between the build tasks has been detected, the causing task is thrown.
* \remarks Dependencies must be added via addDep() before calling this method.
*/
void TaskInfo::add(QList<TaskInfo *> &results)
{
if(!isDone()) {
if(isVisited()) {
// cyclic dependency
if(isOnlyDependency() || isBinaryAvailable()) {
// if this is only a dependency (which we don't want to build) don't care about it
// if there is already a binary (from previous build) don't care about it either
return;
} else {
throw *this;
}
} else {
m_visited = true;
}
for(auto *dep : deps()) {
dep->add(results);
}
m_done = true;
if(!isOnlyDependency()) {
results << this;
}
}
}
/*!
* \brief Returns the package associated via associatePackage().
*/
inline Package *TaskInfo::associatedPackage() const
{
return m_associatedPackage;
}
/*!
* \brief Associates the specified \a package with this build task.
*
* Does nothing if there is already a package assigned which matches all
* dependencies the task has been added for. Returns true in this case.
*
* Fails if the specified \a package does not match all dependencies the task
* has been added for.
*
* \return Returns whether either the currenlty assigned package or the specified
* \a package matches all dependencies the task has been added for.
*/
bool TaskInfo::associatePackage(Package *package)
{
// check whether an appropriate package is already assigned
// and whether the package matches all dependencies this task is required for
bool assignmentRequired = !m_associatedPackage, assignmentPossible = true;
for(const Dependency &dep : m_requiredFor) {
if(!assignmentRequired && !m_associatedPackage->matches(dep)) {
assignmentRequired = true;
}
if(!package->matches(dep)) {
assignmentPossible = false;
}
}
if(!assignmentRequired) {
return true;
}
if(!assignmentPossible) {
return false;
}
m_associatedPackage = package;
// update the name to ensure we have the acutal package name and not just a "provides" name
m_name = package->name();
return true;
}
/*!
* \brief Adds the specified \a tasks and their dependencies to the specified list of results. Ensures that all
* tasks are added in the right order.
* \throws If a circular dependency between the build tasks has been detected, the causing task is thrown.
* \remarks Dependencies must be added to the \a tasks via addDep() before calling this method.
*/
void TaskInfo::addAll(const QList<TaskInfo *> &tasks, QList<TaskInfo *> &results)
{
for(auto *task : tasks) {
task->add(results);
}
}
/*!
* \brief Finds the task with the specified \a name in the specified list of \a tasks.
* \remarks Does not search dependencies the list does not contain.
*/
TaskInfo *TaskInfo::find(const QList<TaskInfo *> &tasks, const QString &name)
{
for(auto *task : tasks) {
if(task->name() == name) {
return task;
}
}
return nullptr;
}
/*!
* \class BuildOrderResolver
* \brief The BuildOrderResolver class resolves the build order for a specified array of packages.
*
* Resolving the build order is done in the following steps:
* 1. instantiation: initial tasks are added
* 2. findDependencies(): dependencies for each task are added, some packages might have to be requested first
* 3. if some packages must be requested: wait for requested packages, if requested packages are available, go to 2.
* 4. all dependencies are added: signal ready() is emitted
* 5. resolve(): build order is resolved, either resolvingFinished() or resolvingFailed() is emitted
*/
/*!
* \brief Creates a new BuildOrderResolver using the specified \a manager.
*/
BuildOrderResolver::BuildOrderResolver(Manager &manager, bool addSourceOnlyDeps, bool requireSources) :
m_manager(manager),
m_finder(nullptr),
m_addSourceOnlyDeps(addSourceOnlyDeps),
m_requireSources(requireSources),
m_hasFinished(false)
{}
/*!
* \brief Adds the specified \a errorMessage.
*/
void BuildOrderResolver::addError(const QString &errorMessage)
{
if(!m_errorMessage.isEmpty()) {
m_errorMessage.append(QStringLiteral(" \u2192 "));
}
m_errorMessage.append(errorMessage);
}
/*!
* \brief Destroys the object.
*/
BuildOrderResolver::~BuildOrderResolver()
{
qDeleteAll(m_tasks);
}
/*!
* \brief Finds the dependencies of all packages.
*
* Also internally called after requested packages are available.
*/
void BuildOrderResolver::findDependencies()
{
// find specified packages and their dependencies
for(int i = 0, size = m_tasks.size(); i != size; ++i) {
if(!addDependenciesToTask(m_tasks[i])) {
return;
}
}
// request dependencies to be requested
requestDependenciesToBeRequested();
}
/*!
* \brief Returns the resulting build order.
* \remarks Build order must have been resolved yet.
*/
QStringList BuildOrderResolver::resultNames()
{
QStringList names;
names.reserve(m_results.size());
for(const auto *res : m_results) {
names << res->name();
}
return names;
}
/*!
* \brief Prints relevant packages.
* \remarks All dependencies must have been found yet.
*/
void BuildOrderResolver::printRelevantPackages()
{
cerr << shchar << "Relevant packages:";
for(const auto *task : m_tasks) {
cerr << ' ' << task->name().toLocal8Bit().data();
}
cerr << endl;
}
/*!
* \brief Prints the resulting build order.
* \remarks Build order must have been resolved yet.
*/
void BuildOrderResolver::printResults()
{
if(useShSyntax) {
Utilities::printBashArray(cout, "REPOINDEX_RESULTS", resultNames());
} else {
Utilities::printValues(cout, "Results", resultNames());
}
}
/*!
* \brief Resolves the build order.
* \remarks All dependencies must have been found yet.
*
* Emits either resolvingFinished() or resolvingFailed() depending on whether
* the operation succeeded.
*/
void BuildOrderResolver::resolve()
{
try {
m_results.clear();
m_results.reserve(m_tasks.size());
TaskInfo::addAll(m_tasks, m_results);
m_hasFinished = true;
emit resolvingFinished();
} catch(const TaskInfo &cyclic) {
addError(QStringLiteral("Can't resolve build order; the package ") % cyclic.name() % QStringLiteral(" is a cyclic dependency."));
m_hasFinished = true;
emit resolvingFailed(m_errorMessage);
}
}
/*!
* \brief Returns the package for the specified \a dependency or nullptr if no package could be found.
*/
Package *BuildOrderResolver::findPackageForDependency(const Dependency &dependency, bool sourceRequired)
{
Package *pkg;
if(!sourceRequired && (pkg = m_manager.packageProviding(dependency))) {
return pkg;
} else if(m_finder && (pkg = m_finder->packageProviding(dependency))) {
return pkg;
}
return nullptr;
}
/*!
* \brief Finds the package for the specified \a task and then adds dependencies recursively.
*/
bool BuildOrderResolver::addDependenciesToTask(TaskInfo *task)
{
// check whether dependencies have already been added
if(task->areAllDepsAdded()) {
// package associated -> dependencies already added
return true;
}
Dependency dep(task->name(), QString());
if(const auto pkg = findPackageForDependency(dep, m_requireSources && !task->isOnlyDependency())) {
task->addRequiredFor(dep);
if(task->associatePackage(pkg)) {
if(pkg->repository()->isSourceOnly()) {
if(m_addSourceOnlyDeps) {
task->setIsOnlyDependency(false);
}
} else {
task->setBinaryAvailable(true);
}
// add dependencies to task
task->setAllDepsAdded();
if(!addDependenciesToTask(task, pkg->allDependencies())) {
addError(QStringLiteral("Can not add dependencies of the dependency \"") % dep.toString() % QStringLiteral("\"."));
m_hasFinished = true;
emit resolvingFailed(errorMessage());
return false;
}
} else {
addError(QStringLiteral("Can not associate the task \"") % task->name() % QStringLiteral("\" with the package required by dependency \"") % dep.toString() % QStringLiteral("\"."));
m_hasFinished = true;
emit resolvingFailed(errorMessage());
return false;
}
} else if(!task->isPackageRequested()) {
task->setPackageRequested();
m_dependenciesToBeRequested << dep;
} else {
addError(QStringLiteral("The specified package \"") % task->name() % QStringLiteral("\" could not be found."));
m_hasFinished = true;
emit resolvingFailed(errorMessage());
return false;
}
return true;
}
/*!
* \brief Finds packages for the the specified \a dependencies and adds them to the specified \a task.
* \remarks Adds dependencies of the dependencies recursively.
*/
bool BuildOrderResolver::addDependenciesToTask(TaskInfo *task, const QList<const QList<Dependency> *> &dependencies)
{
for(auto *deps : dependencies) {
for(auto &dep : *deps) {
auto *depTask = TaskInfo::find(m_tasks, dep.name);
auto *depPkg = findPackageForDependency(dep, m_requireSources && depTask && !depTask->isOnlyDependency());
const QString taskName = depPkg ? depPkg->name() : dep.name;
bool newTask;
if(depTask) {
// we've already added a task for this dependency
newTask = false;
} else {
// create new task
m_tasks << (depTask = new TaskInfo(taskName, true));
newTask = true;
}
// add dependency task to the dependencies of "parent" task
task->addDep(depTask);
depTask->addRequiredFor(dep);
if(depPkg) {
if(depTask->associatePackage(depPkg)) {
if(depPkg->repository()->isSourceOnly()) {
if(m_addSourceOnlyDeps) {
depTask->setIsOnlyDependency(false);
}
} else {
depTask->setBinaryAvailable(true);
}
if(!depTask->areAllDepsAdded()) {
// add dependencies of the dependency
depTask->setAllDepsAdded();
if(!addDependenciesToTask(depTask, depPkg->allDependencies())) {
addError(QStringLiteral("Can not add dependencies of the dependency \"") % dep.toString() % QStringLiteral("\"."));
return false;
}
}
} else {
addError(QStringLiteral("Can not associate the task \"") % depTask->name() % QStringLiteral("\" with the package required by dependency \"") % dep.toString() % QStringLiteral("\"."));
return false;
}
} else {
if(!m_dependenciesToBeRequested.contains(dep)) {
if(!depTask->isPackageRequested()) {
depTask->setPackageRequested();
m_dependenciesToBeRequested << dep;
} else {
addError(QStringLiteral("The specified package \"") % dep.toString() % QStringLiteral("\" could not be found."));
return false;
}
}
}
}
}
return true;
}
/*!
* \brief Requests dependencies to be requested.
*/
void BuildOrderResolver::requestDependenciesToBeRequested()
{
// check whether there are dependencies to be requested
if(!m_dependenciesToBeRequested.isEmpty()) {
// print dependencies to be requested
if(m_manager.config().isVerbose()) {
cerr << shchar << "Dependencies to be requested:";
for(const Dependency &dep : m_dependenciesToBeRequested) {
cerr << ' ' << dep.name.toStdString();
}
cerr << endl;
}
// do requests (using package finder)
m_finder = make_unique<PackageFinder>(m_manager, m_dependenciesToBeRequested, true);
// all dependencies requested -> clear dependencies to be requested
m_dependenciesToBeRequested.clear();
// add results
if(m_finder->areAllResultsAvailable()) {
// results are immediately available (already cached)
findDependencies();
} else {
// need to request actually
connect(m_finder.get(), &PackageFinder::resultsAvailable, this, &BuildOrderResolver::findDependencies);
}
} else {
// there are no dependencies to be requested -> ready to resolve build order
emit ready();
}
}
/*!
* \class BuildOrderResolverCli
* \brief The BuildOrderResolverCli class resolves the build order of the specified packages and prints the results stdout.
*/
/*!
* \brief Creates a new BuildOrderResolverCli for the specified \a packages using the specified \a manager.
*/
BuildOrderResolverCli::BuildOrderResolverCli(Manager &manager, const std::vector<const char *> &packages, bool addSourceOnlyDeps, bool requireSources) :
BuildOrderResolver(manager, addSourceOnlyDeps, requireSources)
{
cerr << shchar << "Getting package information ..." << endl;
tasks().clear();
tasks().reserve(packages.size() * 2);
// add a task for each specified package
for(const char *pkgName : packages) {
tasks() << new TaskInfo(QString::fromLocal8Bit(pkgName));
}
}
/*!
* \brief Determines dependencies, prints relevant packages, resolves build order, prints results/errors.
* \remarks Does not return until everything is done.
*/
int BuildOrderResolverCli::exec()
{
if(manager().config().isVerbose()) {
connect(this, &BuildOrderResolver::ready, this, &BuildOrderResolver::printRelevantPackages);
}
connect(this, &BuildOrderResolver::ready, this, &BuildOrderResolver::resolve);
connect(this, &BuildOrderResolver::resolvingFinished, this, &BuildOrderResolver::printResults);
connect(this, &BuildOrderResolver::resolvingFailed, static_cast<void(*)(const QString &)>(Utilities::printError));
QEventLoop loop;
connect(this, &BuildOrderResolver::resolvingFinished, &loop, &QEventLoop::quit);
connect(this, &BuildOrderResolver::resolvingFailed, &loop, &QEventLoop::quit);
findDependencies();
if(hasFinished()) {
// resolving might have been finished immidiately
return errorMessage().isEmpty();
} else {
// resolving not finished yet (deps must be requested)
return loop.exec();
}
}
} // namespace PackageManagement