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