1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
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 "commandlineparser.h"
41
42 #include "commandlineoption.h"
43 #include "commandlineoptionpool.h"
44 #include "commandpool.h"
45 #include "parsercommand.h"
46 #include "../qbstool.h"
47 #include "../../shared/logging/consolelogger.h"
48
49 #include <logging/translator.h>
50 #include <tools/buildoptions.h>
51 #include <tools/cleanoptions.h>
52 #include <tools/error.h>
53 #include <tools/generateoptions.h>
54 #include <tools/hostosinfo.h>
55 #include <tools/installoptions.h>
56 #include <tools/preferences.h>
57 #include <tools/qbsassert.h>
58 #include <tools/qttools.h>
59 #include <tools/settings.h>
60 #include <tools/settingsrepresentation.h>
61
62 #include <QtCore/qcoreapplication.h>
63 #include <QtCore/qdir.h>
64 #include <QtCore/qmap.h>
65 #include <QtCore/qtextstream.h>
66
67 #include <utility>
68
69 #ifdef Q_OS_UNIX
70 #include <unistd.h>
71 #endif
72
73 namespace qbs {
74 using Internal::Tr;
75
76 class CommandLineParser::CommandLineParserPrivate
77 {
78 public:
79 CommandLineParserPrivate();
80
81 void doParse();
82 Command *commandFromString(const QString &commandString) const;
83 QList<Command *> allCommands() const;
84 QString generalHelp() const;
85
86 void setupProjectFile();
87 void setupBuildDirectory();
88 void setupProgress();
89 void setupLogLevel();
90 void setupBuildOptions();
91 void setupBuildConfigurations();
92 bool checkForExistingBuildConfiguration(const QList<QVariantMap> &buildConfigs,
93 const QString &configurationName);
94 bool withNonDefaultProducts() const;
95 bool dryRun() const;
settingsDir() const96 QString settingsDir() const { return optionPool.settingsDirOption()->settingsDir(); }
97
98 CommandEchoMode echoMode() const;
99
100 QString propertyName(const QString &aCommandLineName) const;
101
102 QStringList commandLine;
103 Command *command;
104 QString projectFilePath;
105 QString projectBuildDirectory;
106 BuildOptions buildOptions;
107 QList<QVariantMap> buildConfigurations;
108 CommandLineOptionPool optionPool;
109 CommandPool commandPool;
110 bool showProgress;
111 bool logTime;
112 };
113
114 CommandLineParser::CommandLineParser() = default;
115
116 CommandLineParser::~CommandLineParser() = default;
117
printHelp() const118 void CommandLineParser::printHelp() const
119 {
120 QTextStream stream(stdout);
121
122 Q_ASSERT(d->command == d->commandPool.getCommand(HelpCommandType));
123 const auto helpCommand = static_cast<const HelpCommand *>(d->command);
124 if (helpCommand->commandToDescribe().isEmpty()) {
125 stream << "Qbs " QBS_VERSION ", a cross-platform build tool.\n";
126 stream << d->generalHelp();
127 } else {
128 const Command * const commandToDescribe
129 = d->commandFromString(helpCommand->commandToDescribe());
130 if (commandToDescribe) {
131 stream << commandToDescribe->longDescription();
132 } else if (!QbsTool::tryToRunTool(helpCommand->commandToDescribe(),
133 QStringList(QStringLiteral("--help")))) {
134 throw ErrorInfo(Tr::tr("No such command '%1'.\n%2")
135 .arg(helpCommand->commandToDescribe(), d->generalHelp()));
136 }
137 }
138 }
139
command() const140 CommandType CommandLineParser::command() const
141 {
142 return d->command->type();
143 }
144
projectFilePath() const145 QString CommandLineParser::projectFilePath() const
146 {
147 return d->projectFilePath;
148 }
149
projectBuildDirectory() const150 QString CommandLineParser::projectBuildDirectory() const
151 {
152 return d->projectBuildDirectory;
153 }
154
buildOptions(const QString & profile) const155 BuildOptions CommandLineParser::buildOptions(const QString &profile) const
156 {
157 Settings settings(settingsDir());
158 Preferences preferences(&settings, profile);
159
160 if (d->buildOptions.maxJobCount() <= 0) {
161 d->buildOptions.setMaxJobCount(preferences.jobs());
162 }
163
164 if (d->buildOptions.echoMode() < 0) {
165 d->buildOptions.setEchoMode(preferences.defaultEchoMode());
166 }
167
168 return d->buildOptions;
169 }
170
cleanOptions(const QString & profile) const171 CleanOptions CommandLineParser::cleanOptions(const QString &profile) const
172 {
173 CleanOptions options;
174 options.setDryRun(buildOptions(profile).dryRun());
175 options.setKeepGoing(buildOptions(profile).keepGoing());
176 options.setLogElapsedTime(logTime());
177 return options;
178 }
179
generateOptions() const180 GenerateOptions CommandLineParser::generateOptions() const
181 {
182 GenerateOptions options;
183 options.setGeneratorName(d->optionPool.generatorOption()->generatorName());
184 return options;
185 }
186
installOptions(const QString & profile) const187 InstallOptions CommandLineParser::installOptions(const QString &profile) const
188 {
189 InstallOptions options;
190 options.setRemoveExistingInstallation(d->optionPool.removeFirstoption()->enabled());
191 options.setInstallRoot(d->optionPool.installRootOption()->installRoot());
192 options.setInstallIntoSysroot(d->optionPool.installRootOption()->useSysroot());
193 if (!options.installRoot().isEmpty()) {
194 QFileInfo fi(options.installRoot());
195 if (!fi.isAbsolute())
196 options.setInstallRoot(fi.absoluteFilePath());
197 }
198 options.setDryRun(buildOptions(profile).dryRun());
199 options.setKeepGoing(buildOptions(profile).keepGoing());
200 options.setLogElapsedTime(logTime());
201 return options;
202 }
203
forceTimestampCheck() const204 bool CommandLineParser::forceTimestampCheck() const
205 {
206 return d->optionPool.forceTimestampCheckOption()->enabled();
207 }
208
forceOutputCheck() const209 bool CommandLineParser::forceOutputCheck() const
210 {
211 return d->optionPool.forceOutputCheckOption()->enabled();
212 }
213
dryRun() const214 bool CommandLineParser::dryRun() const
215 {
216 return d->dryRun();
217 }
218
forceProbesExecution() const219 bool CommandLineParser::forceProbesExecution() const
220 {
221 return d->optionPool.forceProbesOption()->enabled();
222 }
223
waitLockBuildGraph() const224 bool CommandLineParser::waitLockBuildGraph() const
225 {
226 return d->optionPool.waitLockOption()->enabled();
227 }
228
disableFallbackProvider() const229 bool CommandLineParser::disableFallbackProvider() const
230 {
231 return d->optionPool.disableFallbackProviderOption()->enabled();
232 }
233
logTime() const234 bool CommandLineParser::logTime() const
235 {
236 return d->logTime;
237 }
238
withNonDefaultProducts() const239 bool CommandLineParser::withNonDefaultProducts() const
240 {
241 return d->withNonDefaultProducts();
242 }
243
buildBeforeInstalling() const244 bool CommandLineParser::buildBeforeInstalling() const
245 {
246 return !d->optionPool.noBuildOption()->enabled();
247 }
248
runArgs() const249 QStringList CommandLineParser::runArgs() const
250 {
251 Q_ASSERT(d->command->type() == RunCommandType);
252 return static_cast<RunCommand *>(d->command)->targetParameters();
253 }
254
products() const255 QStringList CommandLineParser::products() const
256 {
257 return d->optionPool.productsOption()->arguments();
258 }
259
runEnvConfig() const260 QStringList CommandLineParser::runEnvConfig() const
261 {
262 return d->optionPool.runEnvConfigOption()->arguments();
263 }
264
showProgress() const265 bool CommandLineParser::showProgress() const
266 {
267 return d->showProgress;
268 }
269
showVersion() const270 bool CommandLineParser::showVersion() const
271 {
272 return d->command->type() == VersionCommandType;
273 }
274
settingsDir() const275 QString CommandLineParser::settingsDir() const
276 {
277 return d->settingsDir();
278 }
279
commandName() const280 QString CommandLineParser::commandName() const
281 {
282 return d->command->representation();
283 }
284
commandCanResolve() const285 bool CommandLineParser::commandCanResolve() const
286 {
287 return d->command->canResolve();
288 }
289
commandDescription() const290 QString CommandLineParser::commandDescription() const
291 {
292 return d->command->longDescription();
293 }
294
getBuildConfigurationName(const QVariantMap & buildConfig)295 static QString getBuildConfigurationName(const QVariantMap &buildConfig)
296 {
297 return buildConfig.value(QStringLiteral("qbs.configurationName")).toString();
298 }
299
buildConfigurations() const300 QList<QVariantMap> CommandLineParser::buildConfigurations() const
301 {
302 return d->buildConfigurations;
303 }
304
parseCommandLine(const QStringList & args)305 bool CommandLineParser::parseCommandLine(const QStringList &args)
306 {
307 d = std::make_unique<CommandLineParserPrivate>();
308 d->commandLine = args;
309 try {
310 d->doParse();
311 return true;
312 } catch (const ErrorInfo &error) {
313 qbsError() << error.toString();
314 return false;
315 }
316 }
317
318
CommandLineParserPrivate()319 CommandLineParser::CommandLineParserPrivate::CommandLineParserPrivate()
320 : command(nullptr), commandPool(optionPool), showProgress(false), logTime(false)
321 {
322 }
323
doParse()324 void CommandLineParser::CommandLineParserPrivate::doParse()
325 {
326 if (commandLine.empty()) { // No command given, use default.
327 command = commandPool.getCommand(BuildCommandType);
328 } else {
329 command = commandFromString(commandLine.front());
330 if (command) {
331 commandLine.removeFirst();
332 } else { // No command given.
333 if (commandLine.front() == QLatin1String("-h")
334 || commandLine.front() == QLatin1String("--help")) {
335 command = commandPool.getCommand(HelpCommandType);
336 commandLine.takeFirst();
337 } else if (commandLine.front() == QLatin1String("-V")
338 || commandLine.front() == QLatin1String("--version")) {
339 command = commandPool.getCommand(VersionCommandType);
340 commandLine.takeFirst();
341 } else {
342 command = commandPool.getCommand(BuildCommandType);
343 }
344 }
345 }
346 command->parse(commandLine);
347
348 if (command->type() == HelpCommandType || command->type() == VersionCommandType)
349 return;
350
351 setupBuildDirectory();
352 setupBuildConfigurations();
353 setupProjectFile();
354 setupProgress();
355 setupLogLevel();
356 setupBuildOptions();
357 }
358
commandFromString(const QString & commandString) const359 Command *CommandLineParser::CommandLineParserPrivate::commandFromString(const QString &commandString) const
360 {
361 const auto commands = allCommands();
362 for (Command * const command : commands) {
363 if (command->representation() == commandString)
364 return command;
365 }
366 return nullptr;
367 }
368
allCommands() const369 QList<Command *> CommandLineParser::CommandLineParserPrivate::allCommands() const
370 {
371 return {commandPool.getCommand(GenerateCommandType),
372 commandPool.getCommand(ResolveCommandType),
373 commandPool.getCommand(BuildCommandType),
374 commandPool.getCommand(CleanCommandType),
375 commandPool.getCommand(RunCommandType),
376 commandPool.getCommand(ShellCommandType),
377 commandPool.getCommand(StatusCommandType),
378 commandPool.getCommand(UpdateTimestampsCommandType),
379 commandPool.getCommand(InstallCommandType),
380 commandPool.getCommand(DumpNodesTreeCommandType),
381 commandPool.getCommand(ListProductsCommandType),
382 commandPool.getCommand(VersionCommandType),
383 commandPool.getCommand(SessionCommandType),
384 commandPool.getCommand(HelpCommandType)};
385 }
386
extractToolDescription(const QString & tool,const QString & output)387 static QString extractToolDescription(const QString &tool, const QString &output)
388 {
389 if (tool == QLatin1String("create-project")) {
390 // This command uses QCommandLineParser, where the description is not in the first line.
391 const int eol1Pos = output.indexOf(QLatin1Char('\n'));
392 const int eol2Pos = output.indexOf(QLatin1Char('\n'), eol1Pos + 1);
393 return output.mid(eol1Pos + 1, eol2Pos - eol1Pos - 1);
394 }
395 return output.left(output.indexOf(QLatin1Char('\n')));
396 }
397
generalHelp() const398 QString CommandLineParser::CommandLineParserPrivate::generalHelp() const
399 {
400 QString help = Tr::tr("Usage: qbs [command] [command parameters]\n");
401 help += Tr::tr("Built-in commands:\n");
402 const int rhsIndentation = 30;
403
404 // Sorting the commands by name is nicer for the user.
405 QMap<QString, const Command *> commandMap;
406 const auto commands = allCommands();
407 for (const Command * command : commands)
408 commandMap.insert(command->representation(), command);
409
410 for (const Command * command : qAsConst(commandMap)) {
411 help.append(QLatin1String(" ")).append(command->representation());
412 const QString whitespace
413 = QString(rhsIndentation - 2 - command->representation().size(), QLatin1Char(' '));
414 help.append(whitespace).append(command->shortDescription()).append(QLatin1Char('\n'));
415 }
416
417 QStringList toolNames = QbsTool::allToolNames();
418 toolNames.sort();
419 if (!toolNames.empty()) {
420 help.append(QLatin1Char('\n')).append(Tr::tr("Auxiliary commands:\n"));
421 for (const QString &toolName : qAsConst(toolNames)) {
422 help.append(QLatin1String(" ")).append(toolName);
423 const QString whitespace = QString(rhsIndentation - 2 - toolName.size(),
424 QLatin1Char(' '));
425 QbsTool tool;
426 tool.runTool(toolName, QStringList(QStringLiteral("--help")));
427 if (tool.exitCode() != 0)
428 continue;
429 const QString shortDescription = extractToolDescription(toolName, tool.stdOut());
430 help.append(whitespace).append(shortDescription).append(QLatin1Char('\n'));
431 }
432 }
433
434 return help;
435 }
436
setupProjectFile()437 void CommandLineParser::CommandLineParserPrivate::setupProjectFile()
438 {
439 projectFilePath = optionPool.fileOption()->projectFilePath();
440 }
441
setupBuildDirectory()442 void CommandLineParser::CommandLineParserPrivate::setupBuildDirectory()
443 {
444 projectBuildDirectory = optionPool.buildDirectoryOption()->projectBuildDirectory();
445 }
446
setupBuildOptions()447 void CommandLineParser::CommandLineParserPrivate::setupBuildOptions()
448 {
449 buildOptions.setDryRun(dryRun());
450 QStringList changedFiles = optionPool.changedFilesOption()->arguments();
451 QDir currentDir;
452 for (QString &file : changedFiles)
453 file = QDir::fromNativeSeparators(currentDir.absoluteFilePath(file));
454 buildOptions.setChangedFiles(changedFiles);
455 buildOptions.setKeepGoing(optionPool.keepGoingOption()->enabled());
456 buildOptions.setForceTimestampCheck(optionPool.forceTimestampCheckOption()->enabled());
457 buildOptions.setForceOutputCheck(optionPool.forceOutputCheckOption()->enabled());
458 const JobsOption * jobsOption = optionPool.jobsOption();
459 buildOptions.setMaxJobCount(jobsOption->jobCount());
460 buildOptions.setLogElapsedTime(logTime);
461 buildOptions.setEchoMode(echoMode());
462 buildOptions.setInstall(!optionPool.noInstallOption()->enabled());
463 buildOptions.setRemoveExistingInstallation(optionPool.removeFirstoption()->enabled());
464 buildOptions.setJobLimits(optionPool.jobLimitsOption()->jobLimits());
465 buildOptions.setProjectJobLimitsTakePrecedence(
466 optionPool.respectProjectJobLimitsOption()->enabled());
467 buildOptions.setSettingsDirectory(settingsDir());
468 }
469
setupBuildConfigurations()470 void CommandLineParser::CommandLineParserPrivate::setupBuildConfigurations()
471 {
472 // first: configuration name, second: properties.
473 // Empty configuration name used for global properties.
474 using PropertyListItem = std::pair<QString, QVariantMap>;
475 QList<PropertyListItem> propertiesPerConfiguration;
476
477 const QString configurationNameKey = QStringLiteral("qbs.configurationName");
478 QString currentConfigurationName;
479 QVariantMap currentProperties;
480 const auto args = command->additionalArguments();
481 for (const QString &arg : args) {
482 const int sepPos = arg.indexOf(QLatin1Char(':'));
483 QBS_CHECK(sepPos > 0);
484 const QString key = arg.left(sepPos);
485 const QString rawValue = arg.mid(sepPos + 1);
486 if (key == QLatin1String("config") || key == configurationNameKey) {
487 propertiesPerConfiguration.push_back(std::make_pair(currentConfigurationName,
488 currentProperties));
489 currentConfigurationName = rawValue;
490 currentProperties.clear();
491 continue;
492 }
493 currentProperties.insert(propertyName(key), representationToSettingsValue(rawValue));
494 }
495 propertiesPerConfiguration.push_back(std::make_pair(currentConfigurationName,
496 currentProperties));
497
498 if (propertiesPerConfiguration.size() == 1) // No configuration name specified on command line.
499 propertiesPerConfiguration.push_back(PropertyListItem(QStringLiteral("default"),
500 QVariantMap()));
501
502 const QVariantMap globalProperties = propertiesPerConfiguration.takeFirst().second;
503 QList<QVariantMap> buildConfigs;
504 for (const PropertyListItem &item : qAsConst(propertiesPerConfiguration)) {
505 QVariantMap properties = item.second;
506 for (QVariantMap::ConstIterator globalPropIt = globalProperties.constBegin();
507 globalPropIt != globalProperties.constEnd(); ++globalPropIt) {
508 if (!properties.contains(globalPropIt.key()))
509 properties.insert(globalPropIt.key(), globalPropIt.value());
510 }
511
512 const QString configurationName = item.first;
513 if (checkForExistingBuildConfiguration(buildConfigs, configurationName)) {
514 qbsWarning() << Tr::tr("Ignoring redundant request to build for configuration '%1'.")
515 .arg(configurationName);
516 continue;
517 }
518
519 properties.insert(configurationNameKey, configurationName);
520 buildConfigs.push_back(properties);
521 }
522
523 buildConfigurations = buildConfigs;
524 }
525
setupProgress()526 void CommandLineParser::CommandLineParserPrivate::setupProgress()
527 {
528 const ShowProgressOption * const option = optionPool.showProgressOption();
529 showProgress = option->enabled();
530 #ifdef Q_OS_UNIX
531 if (showProgress && !isatty(STDOUT_FILENO)) {
532 showProgress = false;
533 qbsWarning() << Tr::tr("Ignoring option '%1', because standard output is "
534 "not connected to a terminal.").arg(option->longRepresentation());
535 }
536 #endif
537 }
538
setupLogLevel()539 void CommandLineParser::CommandLineParserPrivate::setupLogLevel()
540 {
541 const LogLevelOption * const logLevelOption = optionPool.logLevelOption();
542 const VerboseOption * const verboseOption = optionPool.verboseOption();
543 const QuietOption * const quietOption = optionPool.quietOption();
544 int logLevel = logLevelOption->logLevel();
545 logLevel += verboseOption->count();
546 logLevel -= quietOption->count();
547
548 if (showProgress && logLevel != LoggerMinLevel) {
549 const bool logLevelWasSetByUser
550 = logLevelOption->logLevel() != defaultLogLevel()
551 || verboseOption->count() > 0 || quietOption->count() > 0;
552 if (logLevelWasSetByUser) {
553 qbsInfo() << Tr::tr("Setting log level to '%1', because option '%2'"
554 " has been given.").arg(logLevelName(LoggerMinLevel),
555 optionPool.showProgressOption()->longRepresentation());
556 }
557 logLevel = LoggerMinLevel;
558 }
559 if (logLevel < LoggerMinLevel) {
560 qbsWarning() << Tr::tr("Cannot decrease log level as much as specified; using '%1'.")
561 .arg(logLevelName(LoggerMinLevel));
562 logLevel = LoggerMinLevel;
563 } else if (logLevel > LoggerMaxLevel) {
564 qbsWarning() << Tr::tr("Cannot increase log level as much as specified; using '%1'.")
565 .arg(logLevelName(LoggerMaxLevel));
566 logLevel = LoggerMaxLevel;
567 }
568
569 logTime = optionPool.logTimeOption()->enabled();
570 if (showProgress && logTime) {
571 qbsWarning() << Tr::tr("Options '%1' and '%2' are incompatible. Ignoring '%2'.")
572 .arg(optionPool.showProgressOption()->longRepresentation(),
573 optionPool.logTimeOption()->longRepresentation());
574 logTime = false;
575 }
576
577 ConsoleLogger::instance().logSink()->setLogLevel(static_cast<LoggerLevel>(logLevel));
578 }
579
propertyName(const QString & aCommandLineName) const580 QString CommandLineParser::CommandLineParserPrivate::propertyName(const QString &aCommandLineName) const
581 {
582 // Make fully-qualified, ie "platform" -> "qbs.platform"
583 if (aCommandLineName.contains(QLatin1Char('.')))
584 return aCommandLineName;
585 else
586 return QLatin1String("qbs.") + aCommandLineName;
587 }
588
checkForExistingBuildConfiguration(const QList<QVariantMap> & buildConfigs,const QString & configurationName)589 bool CommandLineParser::CommandLineParserPrivate::checkForExistingBuildConfiguration(
590 const QList<QVariantMap> &buildConfigs, const QString &configurationName)
591 {
592 for (const QVariantMap &buildConfig : buildConfigs) {
593 if (configurationName == getBuildConfigurationName(buildConfig))
594 return true;
595 }
596 return false;
597 }
598
withNonDefaultProducts() const599 bool CommandLineParser::CommandLineParserPrivate::withNonDefaultProducts() const
600 {
601 if (command->type() == GenerateCommandType)
602 return true;
603 return optionPool.buildNonDefaultOption()->enabled();
604 }
605
dryRun() const606 bool CommandLineParser::CommandLineParserPrivate::dryRun() const
607 {
608 if (command->type() == GenerateCommandType || command->type() == ListProductsCommandType)
609 return true;
610 return optionPool.dryRunOption()->enabled();
611 }
612
echoMode() const613 CommandEchoMode CommandLineParser::CommandLineParserPrivate::echoMode() const
614 {
615 if (command->type() == GenerateCommandType)
616 return CommandEchoModeSilent;
617
618 if (optionPool.commandEchoModeOption()->commandEchoMode() < CommandEchoModeInvalid)
619 return optionPool.commandEchoModeOption()->commandEchoMode();
620
621 return defaultCommandEchoMode();
622 }
623
624 } // namespace qbs
625