/**************************************************************************** ** ** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Build Suite. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ****************************************************************************/ #include "commandlinefrontend.h" #include "application.h" #include "consoleprogressobserver.h" #include "status.h" #include "parser/commandlineoption.h" #include "../shared/logging/consolelogger.h" #include #include #include #include #include #include #include #include #include namespace qbs { using namespace Internal; CommandLineFrontend::CommandLineFrontend(const CommandLineParser &parser, Settings *settings, QObject *parent) : QObject(parent) , m_parser(parser) , m_settings(settings) , m_observer(0) , m_cancelStatus(CancelStatusNone) , m_cancelTimer(new QTimer(this)) { } CommandLineFrontend::~CommandLineFrontend() { m_cancelTimer->stop(); } // Called from interrupt handler. Don't do anything non-trivial here. void CommandLineFrontend::cancel() { m_cancelStatus = CancelStatusRequested; } void CommandLineFrontend::checkCancelStatus() { switch (m_cancelStatus) { case CancelStatusNone: break; case CancelStatusRequested: m_cancelStatus = CancelStatusCanceling; m_cancelTimer->stop(); if (m_resolveJobs.isEmpty() && m_buildJobs.isEmpty()) std::exit(EXIT_FAILURE); foreach (AbstractJob * const job, m_resolveJobs) job->cancel(); foreach (AbstractJob * const job, m_buildJobs) job->cancel(); break; case CancelStatusCanceling: QBS_ASSERT(false, return); break; } } void CommandLineFrontend::start() { try { switch (m_parser.command()) { case RunCommandType: case ShellCommandType: if (m_parser.products().count() > 1) { throw ErrorInfo(Tr::tr("Invalid use of command '%1': Cannot use more than one " "product.\nUsage: %2") .arg(m_parser.commandName(), m_parser.commandDescription())); } // Fall-through intended. case StatusCommandType: case InstallCommandType: if (m_parser.buildConfigurations().count() > 1) { QString error = Tr::tr("Invalid use of command '%1': There can be only one " "build configuration.\n").arg(m_parser.commandName()); error += Tr::tr("Usage: %1").arg(m_parser.commandDescription()); throw ErrorInfo(error); } break; default: break; } if (m_parser.showProgress()) m_observer = new ConsoleProgressObserver; SetupProjectParameters params; params.setProjectFilePath(m_parser.projectFilePath()); params.setIgnoreDifferentProjectFilePath(m_parser.force()); params.setDryRun(m_parser.dryRun()); params.setLogElapsedTime(m_parser.logTime()); params.setSettingsDirectory(m_settings->baseDirectoy()); if (!m_parser.buildBeforeInstalling()) params.setRestoreBehavior(SetupProjectParameters::RestoreOnly); foreach (const QVariantMap &buildConfig, m_parser.buildConfigurations()) { QVariantMap userConfig = buildConfig; const QString buildVariantKey = QLatin1String("qbs.buildVariant"); const QString profileKey = QLatin1String("qbs.profile"); const QString buildVariant = userConfig.take(buildVariantKey).toString(); QString profileName = userConfig.take(profileKey).toString(); if (profileName.isEmpty()) profileName = m_settings->defaultProfile(); if (profileName.isEmpty()) throw ErrorInfo(Tr::tr("No profile specified and no default profile exists.")); const Preferences prefs(m_settings, profileName); params.setSearchPaths(prefs.searchPaths(QDir::cleanPath(QCoreApplication::applicationDirPath() + QLatin1String("/" QBS_RELATIVE_SEARCH_PATH)))); params.setPluginPaths(prefs.pluginPaths(QDir::cleanPath(QCoreApplication::applicationDirPath() + QLatin1String("/" QBS_RELATIVE_PLUGINS_PATH)))); params.setTopLevelProfile(profileName); params.setBuildVariant(buildVariant); params.setBuildRoot(buildDirectory(profileName)); params.setOverriddenValues(userConfig); SetupProjectJob * const job = Project().setupProject(params, ConsoleLogger::instance().logSink(), this); connectJob(job); m_resolveJobs << job; } /* * Progress reporting on the terminal gets a bit tricky when resolving several projects * concurrently, since we cannot show multiple progress bars at the same time. Instead, * we just set the total effort to the number of projects and increase the progress * every time one of them finishes, ingoring the progress reports from the jobs themselves. * (Yes, that does mean it will take disproportionately long for the first progress * notification to arrive.) */ if (m_parser.showProgress() && resolvingMultipleProjects()) m_observer->initialize(tr("Setting up projects"), m_resolveJobs.count()); // Check every two seconds whether we received a cancel request. This value has been // experimentally found to be acceptable. // Note that this polling approach is not problematic here, since we are doing work anyway, // so there's no danger of waking up the processor for no reason. connect(m_cancelTimer, SIGNAL(timeout()), SLOT(checkCancelStatus())); m_cancelTimer->start(2000); } catch (const ErrorInfo &error) { qbsError() << error.toString(); if (m_buildJobs.isEmpty() && m_resolveJobs.isEmpty()) { qApp->exit(EXIT_FAILURE); } else { cancel(); checkCancelStatus(); } } } void CommandLineFrontend::handleCommandDescriptionReport(const QString &highlight, const QString &message) { qbsInfo() << MessageTag(highlight) << message; } void CommandLineFrontend::handleJobFinished(bool success, AbstractJob *job) { job->deleteLater(); if (!success) { qbsError() << job->error().toString(); m_resolveJobs.removeOne(job); m_buildJobs.removeOne(job); if (m_resolveJobs.isEmpty() && m_buildJobs.isEmpty()) { qApp->exit(EXIT_FAILURE); return; } cancel(); } else if (SetupProjectJob * const setupJob = qobject_cast(job)) { m_resolveJobs.removeOne(job); m_projects << setupJob->project(); if (m_observer && resolvingMultipleProjects()) m_observer->incrementProgressValue(); if (m_resolveJobs.isEmpty()) handleProjectsResolved(); } else if (qobject_cast(job)) { if (m_parser.command() == RunCommandType) qApp->exit(runTarget()); else qApp->quit(); } else { // Build or clean. m_buildJobs.removeOne(job); if (m_buildJobs.isEmpty()) { switch (m_parser.command()) { case RunCommandType: case InstallCommandType: install(); break; case BuildCommandType: case CleanCommandType: qApp->quit(); break; default: Q_ASSERT_X(false, Q_FUNC_INFO, "Missing case in switch statement"); } } } } void CommandLineFrontend::handleNewTaskStarted(const QString &description, int totalEffort) { // If the user does not want a progress bar, we just print the current activity. if (!m_parser.showProgress()) { if (!m_parser.logTime()) qbsInfo() << description; return; } if (isBuilding()) { m_totalBuildEffort += totalEffort; if (++m_buildEffortsRetrieved == m_buildEffortsNeeded) { m_observer->initialize(tr("Building"), m_totalBuildEffort); if (m_currentBuildEffort > 0) m_observer->setProgressValue(m_currentBuildEffort); } } else if (!resolvingMultipleProjects()) { m_observer->initialize(description, totalEffort); } } void CommandLineFrontend::handleTotalEffortChanged(int totalEffort) { // Can only happen when resolving. if (m_parser.showProgress() && !isBuilding() && !resolvingMultipleProjects()) m_observer->setMaximum(totalEffort); } void CommandLineFrontend::handleTaskProgress(int value, AbstractJob *job) { if (isBuilding()) { int ¤tJobEffort = m_buildEfforts[job]; m_currentBuildEffort += value - currentJobEffort; currentJobEffort = value; if (m_buildEffortsRetrieved == m_buildEffortsNeeded) m_observer->setProgressValue(m_currentBuildEffort); } else if (!resolvingMultipleProjects()) { m_observer->setProgressValue(value); } } void CommandLineFrontend::handleProcessResultReport(const qbs::ProcessResult &result) { bool hasOutput = !result.stdOut().isEmpty() || !result.stdErr().isEmpty(); if (!hasOutput && result.success()) return; (result.success() ? qbsInfo() : qbsError()) << result.executableFilePath() << " " << result.arguments().join(QLatin1String(" ")) << (hasOutput ? QString::fromLatin1("\n") : QString()) << (result.stdOut().isEmpty() ? QString() : result.stdOut().join(QLatin1String("\n"))) << (result.stdErr().isEmpty() ? QString() : result.stdErr().join(QLatin1String("\n"))); } bool CommandLineFrontend::resolvingMultipleProjects() const { return isResolving() && m_resolveJobs.count() + m_projects.count() > 1; } bool CommandLineFrontend::isResolving() const { return !m_resolveJobs.isEmpty(); } bool CommandLineFrontend::isBuilding() const { return !m_buildJobs.isEmpty(); } CommandLineFrontend::ProductMap CommandLineFrontend::productsToUse() const { ProductMap products; QStringList productNames; const bool useAll = m_parser.products().isEmpty(); foreach (const Project &project, m_projects) { QList &productList = products[project]; const ProjectData projectData = project.projectData(); foreach (const ProductData &product, projectData.allProducts()) { if (useAll || m_parser.products().contains(product.name())) { productList << product; productNames << product.name(); } } } foreach (const QString &productName, m_parser.products()) { if (!productNames.contains(productName)) throw ErrorInfo(Tr::tr("No such product '%1'.").arg(productName)); } return products; } void CommandLineFrontend::handleProjectsResolved() { try { if (m_cancelStatus != CancelStatusNone) throw ErrorInfo(Tr::tr("Execution canceled.")); switch (m_parser.command()) { case ResolveCommandType: qApp->quit(); break; case CleanCommandType: makeClean(); break; case ShellCommandType: checkForExactlyOneProduct(); qApp->exit(runShell()); break; case StatusCommandType: qApp->exit(printStatus(m_projects.first().projectData())); break; case BuildCommandType: build(); break; case InstallCommandType: case RunCommandType: if (m_parser.buildBeforeInstalling()) build(); else install(); break; case UpdateTimestampsCommandType: updateTimestamps(); qApp->quit(); break; case HelpCommandType: Q_ASSERT_X(false, Q_FUNC_INFO, "Impossible."); } } catch (const ErrorInfo &error) { qbsError() << error.toString(); qApp->exit(EXIT_FAILURE); } } void CommandLineFrontend::makeClean() { if (m_parser.products().isEmpty()) { foreach (const Project &project, m_projects) { m_buildJobs << project.cleanAllProducts(m_parser.cleanOptions(), this); } } else { const ProductMap &products = productsToUse(); for (ProductMap::ConstIterator it = products.begin(); it != products.end(); ++it) { m_buildJobs << it.key().cleanSomeProducts(it.value(), m_parser.cleanOptions(), this); } } connectBuildJobs(); } int CommandLineFrontend::runShell() { const ProductMap &productMap = productsToUse(); Q_ASSERT(productMap.count() == 1); const Project &project = productMap.begin().key(); const QList &products = productMap.begin().value(); Q_ASSERT(products.count() == 1); RunEnvironment runEnvironment = project.getRunEnvironment(products.first(), QProcessEnvironment::systemEnvironment(), m_settings); return runEnvironment.runShell(); } BuildOptions CommandLineFrontend::buildOptions(const Project &project) const { BuildOptions options = m_parser.buildOptions(); if (options.maxJobCount() <= 0) { const QString profileName = project.profile(); QBS_CHECK(!profileName.isEmpty()); options.setMaxJobCount(Preferences(m_settings, profileName).jobs()); } return options; } QString CommandLineFrontend::buildDirectory(const QString &profileName) const { QString buildDir = m_parser.projectBuildDirectory(); if (buildDir.isEmpty()) { buildDir = Preferences(m_settings, profileName).defaultBuildDirectory(); if (buildDir.isEmpty()) { qbsDebug() << "No project build directory given; using current directory."; buildDir = QDir::currentPath(); } else { qbsDebug() << "No project build directory given; using directory from preferences."; } } QDir projectDir(QFileInfo(m_parser.projectFilePath()).path()); buildDir.replace(BuildDirectoryOption::magicProjectString(), projectDir.dirName()); if (!QFileInfo(buildDir).isAbsolute()) buildDir = QDir::currentPath() + QLatin1Char('/') + buildDir; buildDir = QDir::cleanPath(buildDir); return buildDir; } void CommandLineFrontend::build() { if (m_parser.products().isEmpty()) { foreach (const Project &project, m_projects) m_buildJobs << project.buildAllProducts(buildOptions(project), this); } else { const ProductMap &products = productsToUse(); for (ProductMap::ConstIterator it = products.begin(); it != products.end(); ++it) m_buildJobs << it.key().buildSomeProducts(it.value(), buildOptions(it.key()), this); } connectBuildJobs(); /* * Progress reporting for the build jobs works as follows: We know that for every job, * the newTaskStarted() signal is emitted exactly once (unless there's an error). So we add up * the respective total efforts as they come in. Once all jobs have reported their total * efforts, we can start the overall progress report. */ m_buildEffortsNeeded = m_buildJobs.count(); m_buildEffortsRetrieved = 0; m_totalBuildEffort = 0; m_currentBuildEffort = 0; } int CommandLineFrontend::runTarget() { try { checkForExactlyOneProduct(); const ProductMap &productMap = productsToUse(); Q_ASSERT(productMap.count() == 1); const Project &project = productMap.begin().key(); const QList &products = productMap.begin().value(); Q_ASSERT(products.count() == 1); const ProductData productToRun = products.first(); const QString executableFilePath = project.targetExecutable(productToRun, m_parser.installOptions()); if (executableFilePath.isEmpty()) { throw ErrorInfo(Tr::tr("Cannot run: Product '%1' is not an application.") .arg(productToRun.name())); } RunEnvironment runEnvironment = project.getRunEnvironment(productToRun, QProcessEnvironment::systemEnvironment(), m_settings); return runEnvironment.runTarget(executableFilePath, m_parser.runArgs()); } catch (const ErrorInfo &error) { qbsError() << error.toString(); return EXIT_FAILURE; } } void CommandLineFrontend::updateTimestamps() { const ProductMap &products = productsToUse(); for (ProductMap::ConstIterator it = products.constBegin(); it != products.constEnd(); ++it) { Project p = it.key(); p.updateTimestamps(it.value()); } } void CommandLineFrontend::connectBuildJobs() { foreach (AbstractJob * const job, m_buildJobs) connectBuildJob(job); } void CommandLineFrontend::connectBuildJob(AbstractJob *job) { connectJob(job); BuildJob *bjob = qobject_cast(job); if (!bjob) return; connect(bjob, SIGNAL(reportCommandDescription(QString,QString)), this, SLOT(handleCommandDescriptionReport(QString,QString))); connect(bjob, SIGNAL(reportProcessResult(qbs::ProcessResult)), this, SLOT(handleProcessResultReport(qbs::ProcessResult))); } void CommandLineFrontend::connectJob(AbstractJob *job) { connect(job, SIGNAL(finished(bool,qbs::AbstractJob*)), SLOT(handleJobFinished(bool,qbs::AbstractJob*))); connect(job, SIGNAL(taskStarted(QString,int,qbs::AbstractJob*)), SLOT(handleNewTaskStarted(QString,int))); connect(job, SIGNAL(totalEffortChanged(int,qbs::AbstractJob*)), SLOT(handleTotalEffortChanged(int))); if (m_parser.showProgress()) { connect(job, SIGNAL(taskProgress(int,qbs::AbstractJob*)), SLOT(handleTaskProgress(int,qbs::AbstractJob*))); } } void CommandLineFrontend::checkForExactlyOneProduct() { if (m_parser.products().count() == 0 && m_projects.first().projectData().products().count() > 1) { throw ErrorInfo(Tr::tr("Ambiguous use of command '%1': No product given for project " "with more than one product.\nUsage: %2") .arg(m_parser.commandName(), m_parser.commandDescription())); } } void CommandLineFrontend::install() { Q_ASSERT(m_projects.count() == 1); const Project project = m_projects.first(); const ProductMap products = productsToUse(); InstallJob * const installJob = project.installSomeProducts( products.value(m_projects.first()), m_parser.installOptions()); connectJob(installJob); } } // namespace qbs