diff options
author | Christian Kandeler <christian.kandeler@digia.com> | 2013-01-07 15:48:45 +0100 |
---|---|---|
committer | Joerg Bornemann <joerg.bornemann@digia.com> | 2013-01-22 12:21:38 +0100 |
commit | 5cdf94de300e72987dfbe5c0fec5b86317ad6280 (patch) | |
tree | e484cd9ccd2faae387dd3978b6eb7783c99b84b3 | |
parent | 49060f9b1e2881a5e7801f59f6d237fd85da1ae1 (diff) |
Introduce the "install" command.
This decouples building and installing, e.g. allowing
the latter to be executed by a privileged user
to a system-wide directory.
In addition, the ability to install build artifacts
(typically executables or libraries) has been added.
Change-Id: I28e725e4c1168eebe88e12c75e3d3e9f5fe28ca5
Reviewed-by: Joerg Bornemann <joerg.bornemann@digia.com>
42 files changed, 853 insertions, 188 deletions
diff --git a/doc/qbs.qdoc b/doc/qbs.qdoc index 29cba8eb4..775533063 100644 --- a/doc/qbs.qdoc +++ b/doc/qbs.qdoc @@ -68,7 +68,7 @@ \li \l{Language Introduction} \li \l{Building Applications with Qbs} \li \l{Running Applications} - \li \l{Using Qbs Graph} + \li \l{Installing Files} \li \l{Using Qbs Shell} \endlist \li \l{Reference} @@ -306,7 +306,7 @@ \li \l{Language Introduction} \li \l{Building Applications with Qbs} \li \l{Running Applications} - \li \l{Using Qbs Graph} + \li \l{Installing Files} \li \l{Using Qbs Shell} \endlist @@ -671,7 +671,7 @@ \contentspage index.html \previouspage qbs-language-introduction.html \page qbs-building-applications.html - \nextpage qbs-running-applications.html + \nextpage qbs-installing-files.html \title Building Applications with Qbs @@ -695,54 +695,70 @@ This assumes you have already set up a profile called "Symbian". */ - /*! \contentspage index.html \previouspage qbs-building-applications.html - \page qbs-running-applications.html - \nextpage qbs-graph.html + \page qbs-installing-files.html + \nextpage qbs-running.html - \title Running Applications + \title Installing Files - Running ./targets/debug/CollidingMice fails if Qt 4.8 is not in your PATH - (in Windows) or LD_LIBRARY_PATH (in Linux). + If you want to install your project, the first thing to do is to specify the necessary + information in the project file: + \code + Application { + Group { + name: "Runtime resources" + files: "*.qml" + qbs.install: true + qbs.installDir: "share/myproject" + } + Group { + name: "The App itself" + fileTagsFilter: "application" + qbs.install: true + qbs.installDir: "bin" + } + } + \endcode - Therefore, enter the following command to run an application: + In this example, we want to install a couple of QML files and an executable. + The actual installation is then done like this (using the default profile): \code - qbs run --products CollidingMice + qbs install --install-root /tmp/myProjectRoot --remove-first \endcode + Here, we want the "installDir" properties from the project file to be interpreted relative + to the directory "/tmp/myProjectRoot", and we want that directory to be removed first. + If the "--install-root" option is not given, a default is being used, namely the value of the + property "qbs.sysroot" or, if that one is not set, "<build root>/install-root". */ - /*! \contentspage index.html - \previouspage qbs-running-applications.html - \page qbs-graph.html + \previouspage qbs-installing-files.html + \page qbs-running-applications.html \nextpage qbs-shell.html - \title Using Qbs Graph - - Qbs uses a very simple graph drawing algorithm to visualize the - build graph. - - This is currently mostly used to debug Qbs. + \title Running Applications - Download and install dot and add it to the system PATH. + Running ./targets/debug/CollidingMice fails if Qt 4.8 is not in your PATH + (in Windows) or LD_LIBRARY_PATH (in Linux). - To visualize the project structure, enter the following command: + Therefore, enter the following command to run an application: \code - qbs graph + qbs run --products CollidingMice \endcode + This command also builds and installs the given product, if necessary. */ /*! \contentspage index.html - \previouspage qbs-graph.html + \previouspage qbs-running-applications.html \page qbs-shell.html \nextpage qbs-reference.html @@ -854,8 +870,7 @@ This item is attached to a product and is used to group files that have something in common. For instance: \code - Product { - type: ["application", "installed_content"] + Application { Group { name: "common files" files: ["myclass.h", "myclass_common_impl.cpp"] @@ -872,7 +887,8 @@ } Group { name: "Files to install" - fileTags: "install" + qbs.install: true + qbs.installDir: "share" files "runtime_resource.txt" } } @@ -886,12 +902,25 @@ name: "Word processing documents" files: ["*.doc", "*.rtf"] recursive: true - fileTags: "install" + qbs.install: true + qbs.installDir: "share" excludeFiles: "do_not_install_this_file.*" } \endcode \note Wildcards can match only regular files, not directories. + A group can also be used to attach properties to build artifacts such as executables or + libraries. In the following example, an application is installed to "<install root>/bin". + \code + Application { + Group { + fileTagsFilter: "application" + qbs.install: true + qbs.installDir: "bin" + } + } + \endcode + \section1 Group Properties \table @@ -909,13 +938,20 @@ \li files \li list \li empty list - \li The files in the group. + \li The files in the group. Mutually exclusive with fileTagsFilter. \row \li prefix \li string \li empty string \li A prefix to append to all files. Slashes are allowed and have directory semantics. \row + \li fileTagsFilter + \li list + \li empty list + \li Artifact file tags to match. Any properties set in this group will be applied + to the product's generated artifacts whose file tags intersect with the ones + listed here. Mutually exclusive with files. + \row \li condition \li bool \li true diff --git a/share/qbs/imports/qbs/base/QmlApp.qbs b/share/qbs/imports/qbs/base/QmlApp.qbs index a5a1c9603..76f97530a 100644 --- a/share/qbs/imports/qbs/base/QmlApp.qbs +++ b/share/qbs/imports/qbs/base/QmlApp.qbs @@ -1,7 +1,7 @@ import qbs.base 1.0 Product { - type: [qbs.targetOS == 'mac' ? "applicationbundle" : "application", "installed_content"] + type: [qbs.targetOS == 'mac' ? "applicationbundle" : "application"] Depends { name: "qt"; submodules: ["core", "declarative"] } Depends { name: "cpp" } property string appViewerPath: localPath + "/qmlapplicationviewer" @@ -22,10 +22,5 @@ Product { appViewerPath + "/qmlapplicationviewer_qt5.cpp" ] } - - FileTagger { - pattern: "*.qml" - fileTags: ["install"] - } } diff --git a/share/qbs/modules/qbs/common.qbs b/share/qbs/modules/qbs/common.qbs index 6a0f7b0ed..25d41ff00 100644 --- a/share/qbs/modules/qbs/common.qbs +++ b/share/qbs/modules/qbs/common.qbs @@ -27,11 +27,9 @@ Module { property string toolchain property string architecture property string endianness - property string installDir: '.' + property bool install: false + property string installDir property string sysroot - property string installPrefix: "" - property string deployRoot: "./deployRoot" - property string deployInfoFile PropertyOptions { name: "buildVariant" @@ -44,77 +42,4 @@ Module { allowedValues: ['none', 'fast', 'small'] description: "optimization level" } - - Rule { - inputs: ["install"] - Artifact { - fileTags: ["installed_content"] - fileName: { - var targetPath = input.modules.qbs.installDir + "/" + input.fileName - if (input.modules.qbs.installPrefix && !FileInfo.isAbsolutePath(targetPath)) - targetPath = input.modules.qbs.installPrefix + "/" + targetPath - if (product.module.sysroot && FileInfo.isAbsolutePath(targetPath)) - targetPath = product.module.sysroot + targetPath - return targetPath - } - } - - prepare: { - var cmd = new JavaScriptCommand(); - cmd.sourceCode = function() { - File.remove(output.fileName); - if (!File.copy(input.fileName, output.fileName)) - throw "Cannot install '" + input.fileName + "' as '" + output.fileName + "'"; - } - cmd.description = "installing " + FileInfo.fileName(output.fileName); - cmd.highlight = "linker"; - return cmd; - } - } - - Rule { - inputs: "deploy" - multiplex: deployInfoFile != null - Artifact { - fileTags: "installed_content" - fileName: { - if (product.modules.qbs.deployInfoFile) - return product.modules.qbs.deployInfoFile - return input.modules.qbs.deployRoot + "/" + input.modules.qbs.installPrefix - + "/" + input.modules.qbs.installDir + "/" + input.fileName - } - } - - prepare: { - var cmd = new JavaScriptCommand() - cmd.deployInfo = [] - if (product.modules.qbs.deployInfoFile) { - for (var i in inputs.deploy) { - var sourceFile = inputs.deploy[i].fileName - var destFile = product.modules.qbs.installPrefix + "/" - + inputs.deploy[i].modules.qbs.installDir + "/" - + FileInfo.fileName(sourceFile) - destFile = destFile.replace(/\/+/g, "/") - destFile = destFile.replace(/\/\.\//g, "/") - cmd.deployInfo.push(sourceFile + "|" + destFile) - } - cmd.description = "Writing deployment information to '" + output.fileName + "'" - cmd.sourceCode = function() { - var deployInfoFile = new TextFile(output.fileName, TextFile.WriteOnly) - for (var i in deployInfo) - deployInfoFile.writeLine(deployInfo[i]) - deployInfoFile.close() - } - } else { - cmd.description = "deploying " + FileInfo.fileName(output.fileName) - cmd.sourceCode = function() { - File.remove(output.fileName) - if (!File.copy(input.fileName, output.fileName)) - throw "Cannot deploy '" + input.fileName + "' to '" + output.fileName + "'" - } - } - cmd.highlight = "linker" - return cmd - } - } } diff --git a/src/app/qbs/commandlinefrontend.cpp b/src/app/qbs/commandlinefrontend.cpp index d116a45ca..a60d1558a 100644 --- a/src/app/qbs/commandlinefrontend.cpp +++ b/src/app/qbs/commandlinefrontend.cpp @@ -73,6 +73,7 @@ void CommandLineFrontend::start() // Fall-through intended. case PropertiesCommandType: 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()); @@ -150,13 +151,32 @@ void CommandLineFrontend::handleJobFinished(bool success, AbstractJob *job) m_observer->incrementProgressValue(); if (m_resolveJobs.isEmpty()) handleProjectsResolved(); + } else if (qobject_cast<InstallJob *>(job)) { + if (m_parser.command() == RunCommandType) + qApp->exit(runTarget()); + else + qApp->quit(); } else { // Build or clean. m_buildJobs.removeOne(job); if (m_buildJobs.isEmpty()) { - if (m_parser.command() == RunCommandType) - qApp->exit(runTarget()); - else + switch (m_parser.command()) { + case RunCommandType: + case InstallCommandType: { + 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); + break; + } + case BuildCommandType: + case CleanCommandType: qApp->quit(); + break; + default: + Q_ASSERT_X(false, Q_FUNC_INFO, "Missing case in switch statement"); + } } } } @@ -272,6 +292,7 @@ void CommandLineFrontend::handleProjectsResolved() break; } case BuildCommandType: + case InstallCommandType: case RunCommandType: build(); break; @@ -353,7 +374,8 @@ int CommandLineFrontend::runTarget() const QList<ProductData> &products = productMap.begin().value(); Q_ASSERT(products.count() == 1); const ProductData productToRun = products.first(); - const QString executableFilePath = project.targetExecutable(productToRun); + const QString executableFilePath = project.targetExecutable(productToRun, + m_parser.installOptions().installRoot); if (executableFilePath.isEmpty()) { throw Error(Tr::tr("Cannot run: Product '%1' is not an application.") .arg(productToRun.name())); diff --git a/src/app/qbs/parser/command.cpp b/src/app/qbs/parser/command.cpp index 75d48f46a..ca4553fc8 100644 --- a/src/app/qbs/parser/command.cpp +++ b/src/app/qbs/parser/command.cpp @@ -160,7 +160,7 @@ QString BuildCommand::representation() const return buildCommandRepresentation(); } -QList<CommandLineOption::Type> BuildCommand::supportedOptions() const +static QList<CommandLineOption::Type> buildOptions() { return QList<CommandLineOption::Type>() << CommandLineOption::FileOptionType @@ -175,6 +175,11 @@ QList<CommandLineOption::Type> BuildCommand::supportedOptions() const << CommandLineOption::ChangedFilesOptionType; } +QList<CommandLineOption::Type> BuildCommand::supportedOptions() const +{ + return buildOptions(); +} + QString CleanCommand::shortDescription() const { return Tr::tr("Remove the files generated during a build."); @@ -194,16 +199,41 @@ QString CleanCommand::representation() const QList<CommandLineOption::Type> CleanCommand::supportedOptions() const { - return QList<CommandLineOption::Type>() - << CommandLineOption::FileOptionType - << CommandLineOption::LogLevelOptionType - << CommandLineOption::VerboseOptionType - << CommandLineOption::QuietOptionType - << CommandLineOption::ShowProgressOptionType - << CommandLineOption::KeepGoingOptionType - << CommandLineOption::DryRunOptionType - << CommandLineOption::ProductsOptionType - << CommandLineOption::AllArtifactsOptionType; + QList<CommandLineOption::Type> options = buildOptions(); + options.removeOne(CommandLineOption::ChangedFilesOptionType); + return options; +} + +QString InstallCommand::shortDescription() const +{ + return Tr::tr("Install (parts of) a project."); +} + +QString InstallCommand::longDescription() const +{ + QString description = Tr::tr("qbs %1 [options] [[variant] [property:value] ...]\n") + .arg(representation()); + description += Tr::tr("Install all files marked as installable " + "to their respective destinations.\n" + "The project is built first, if necessary.\n"); + return description += supportedOptionsDescription(); +} + +QString InstallCommand::representation() const +{ + return QLatin1String("install"); +} + +QList<CommandLineOption::Type> installOptions() +{ + return buildOptions() + << CommandLineOption::InstallRootOptionType + << CommandLineOption::RemoveFirstOptionType; +} + +QList<CommandLineOption::Type> InstallCommand::supportedOptions() const +{ + return installOptions(); } QString RunCommand::shortDescription() const @@ -230,17 +260,7 @@ QString RunCommand::representation() const QList<CommandLineOption::Type> RunCommand::supportedOptions() const { - return QList<CommandLineOption::Type>() - << CommandLineOption::FileOptionType - << CommandLineOption::LogLevelOptionType - << CommandLineOption::VerboseOptionType - << CommandLineOption::QuietOptionType - << CommandLineOption::ShowProgressOptionType - << CommandLineOption::JobsOptionType - << CommandLineOption::KeepGoingOptionType - << CommandLineOption::DryRunOptionType - << CommandLineOption::ProductsOptionType - << CommandLineOption::ChangedFilesOptionType; + return installOptions(); } void RunCommand::parseMore(QStringList &input) @@ -357,12 +377,13 @@ QString UpdateTimestampsCommand::longDescription() const { QString description = Tr::tr("qbs %1 [options] [[variant] [property:value] ...] ...\n") .arg(representation()); - return description += Tr::tr("Update the timestamps of all build artifacts, causing the next " + description += Tr::tr("Update the timestamps of all build artifacts, causing the next " "builds of the project to do nothing if no updates to source files happen in between.\n" "This functionality is useful if you know that the current changes to source files " "are irrelevant to the build.\n" "NOTE: Doing this causes a discrepancy between the \"real world\" and the information " "in the build graph, so use with care.\n"); + return description += supportedOptionsDescription(); } QString UpdateTimestampsCommand::representation() const diff --git a/src/app/qbs/parser/command.h b/src/app/qbs/parser/command.h index 4547077f1..6ebef1107 100644 --- a/src/app/qbs/parser/command.h +++ b/src/app/qbs/parser/command.h @@ -93,6 +93,19 @@ private: QList<CommandLineOption::Type> supportedOptions() const; }; +class InstallCommand : public Command +{ +public: + InstallCommand(CommandLineOptionPool &optionPool) : Command(optionPool) {} + +private: + CommandType type() const { return InstallCommandType; } + QString shortDescription() const; + QString longDescription() const; + QString representation() const; + QList<CommandLineOption::Type> supportedOptions() const; +}; + class RunCommand : public Command { public: diff --git a/src/app/qbs/parser/commandlineoption.cpp b/src/app/qbs/parser/commandlineoption.cpp index 5bc08c2c2..eafa198cd 100644 --- a/src/app/qbs/parser/commandlineoption.cpp +++ b/src/app/qbs/parser/commandlineoption.cpp @@ -31,6 +31,7 @@ #include <logging/logger.h> #include <logging/translator.h> #include <tools/error.h> +#include <tools/installoptions.h> namespace qbs { using namespace Internal; @@ -233,12 +234,15 @@ QString ChangedFilesOption::longRepresentation() const QString ProductsOption::description(CommandType command) const { const QString prefix = Tr::tr("%1|%2").arg(longRepresentation(), shortRepresentation()); - if (command == ShellCommandType || command == RunCommandType) { - return Tr::tr("%1 <name>\n" - "\tUse the specified product.\n").arg(prefix); + switch (command) { + case InstallCommandType: + case RunCommandType: + case ShellCommandType: + return Tr::tr("%1 <name>\n\tUse the specified product.\n").arg(prefix); + default: + return Tr::tr("%1 <name>[,<name>...]\n" + "\tTake only the specified products into account.\n").arg(prefix); } - return Tr::tr("%1 <name>[,<name>...]\n" - "\tTake only the specified products into account.\n").arg(prefix); } QString ProductsOption::shortRepresentation() const @@ -306,4 +310,42 @@ QString AllArtifactsOption::longRepresentation() const return QLatin1String("--all-artifacts"); } +QString InstallRootOption::description(CommandType command) const +{ + Q_ASSERT(command == InstallCommandType || command == RunCommandType); + Q_UNUSED(command); + return Tr::tr("%1 <directory>\n" + "\tInstall into the given directory. The default value is qbs.sysroot, " + "if it is defined; otherwise '<build dir>/%2' is used.\n" + "\tIf the directory does not exist, it will be created.\n") + .arg(longRepresentation(), InstallOptions::defaultInstallRoot()); +} + +QString InstallRootOption::longRepresentation() const +{ + return QLatin1String("--install-root"); +} + +void InstallRootOption::doParse(const QString &representation, QStringList &input) +{ + if (input.isEmpty()) { + throw Error(Tr::tr("Invalid use of option '%1: Argument expected.\n" + "Usage: %2").arg(representation, description(command()))); + } + m_installRoot = input.takeFirst(); +} + +QString RemoveFirstOption::description(CommandType command) const +{ + Q_ASSERT(command == InstallCommandType || command == RunCommandType); + Q_UNUSED(command); + return Tr::tr("%1\n\tRemove the installation base directory before installing.\n") + .arg(longRepresentation()); +} + +QString RemoveFirstOption::longRepresentation() const +{ + return QLatin1String("--remove-first"); +} + } // namespace qbs diff --git a/src/app/qbs/parser/commandlineoption.h b/src/app/qbs/parser/commandlineoption.h index c0837b4d3..8502751ec 100644 --- a/src/app/qbs/parser/commandlineoption.h +++ b/src/app/qbs/parser/commandlineoption.h @@ -47,7 +47,8 @@ public: ShowProgressOptionType, ChangedFilesOptionType, ProductsOptionType, - AllArtifactsOptionType + AllArtifactsOptionType, + InstallRootOptionType, RemoveFirstOptionType }; virtual ~CommandLineOption(); @@ -211,6 +212,29 @@ private: int m_logLevel; }; +class InstallRootOption : public CommandLineOption +{ +public: + QString installRoot() const { return m_installRoot; } + + QString description(CommandType command) const; + QString shortRepresentation() const { return QString(); } + QString longRepresentation() const; + +private: + void doParse(const QString &representation, QStringList &input); + + QString m_installRoot; +}; + +class RemoveFirstOption : public OnOffOption +{ +public: + QString description(CommandType command) const; + QString shortRepresentation() const { return QString(); } + QString longRepresentation() const; +}; + } // namespace qbs #endif // QBS_COMMANDLINEOPTION_H diff --git a/src/app/qbs/parser/commandlineoptionpool.cpp b/src/app/qbs/parser/commandlineoptionpool.cpp index 89757bc9f..b53a93bb3 100644 --- a/src/app/qbs/parser/commandlineoptionpool.cpp +++ b/src/app/qbs/parser/commandlineoptionpool.cpp @@ -73,6 +73,12 @@ CommandLineOption *CommandLineOptionPool::getOption(CommandLineOption::Type type case CommandLineOption::AllArtifactsOptionType: option = new AllArtifactsOption; break; + case CommandLineOption::InstallRootOptionType: + option = new InstallRootOption; + break; + case CommandLineOption::RemoveFirstOptionType: + option = new RemoveFirstOption; + break; } } return option; @@ -133,4 +139,14 @@ AllArtifactsOption *CommandLineOptionPool::allArtifactsOption() const return static_cast<AllArtifactsOption *>(getOption(CommandLineOption::AllArtifactsOptionType)); } +InstallRootOption *CommandLineOptionPool::installRootOption() const +{ + return static_cast<InstallRootOption *>(getOption(CommandLineOption::InstallRootOptionType)); +} + +RemoveFirstOption *CommandLineOptionPool::removeFirstoption() const +{ + return static_cast<RemoveFirstOption *>(getOption(CommandLineOption::RemoveFirstOptionType)); +} + } // namespace qbs diff --git a/src/app/qbs/parser/commandlineoptionpool.h b/src/app/qbs/parser/commandlineoptionpool.h index 7706f9b9b..3a1a16a8f 100644 --- a/src/app/qbs/parser/commandlineoptionpool.h +++ b/src/app/qbs/parser/commandlineoptionpool.h @@ -52,6 +52,8 @@ public: JobsOption *jobsOption() const; ProductsOption *productsOption() const; AllArtifactsOption *allArtifactsOption() const; + InstallRootOption *installRootOption() const; + RemoveFirstOption *removeFirstoption() const; private: mutable QHash<CommandLineOption::Type, CommandLineOption *> m_options; diff --git a/src/app/qbs/parser/commandlineparser.cpp b/src/app/qbs/parser/commandlineparser.cpp index ebd95fe57..8a32084fc 100644 --- a/src/app/qbs/parser/commandlineparser.cpp +++ b/src/app/qbs/parser/commandlineparser.cpp @@ -40,6 +40,7 @@ #include <tools/error.h> #include <tools/fileinfo.h> #include <tools/hostosinfo.h> +#include <tools/installoptions.h> #include <tools/settings.h> #include <QCoreApplication> @@ -117,6 +118,17 @@ BuildOptions CommandLineParser::buildOptions() const return d->buildOptions; } +InstallOptions CommandLineParser::installOptions() const +{ + Q_ASSERT(command() == InstallCommandType || command() == RunCommandType); + InstallOptions options; + options.removeFirst = d->optionPool.removeFirstoption()->enabled(); + options.installRoot = d->optionPool.installRootOption()->installRoot(); + options.dryRun = buildOptions().dryRun; + options.keepGoing = buildOptions().keepGoing; + return options; +} + QStringList CommandLineParser::runArgs() const { Q_ASSERT(d->command->type() == RunCommandType); @@ -270,6 +282,7 @@ QList<Command *> CommandLineParser::CommandLineParserPrivate::allCommands() cons << commandPool.getCommand(PropertiesCommandType) << commandPool.getCommand(StatusCommandType) << commandPool.getCommand(UpdateTimestampsCommandType) + << commandPool.getCommand(InstallCommandType) << commandPool.getCommand(HelpCommandType); } diff --git a/src/app/qbs/parser/commandlineparser.h b/src/app/qbs/parser/commandlineparser.h index 9b497e707..ba9dd58ce 100644 --- a/src/app/qbs/parser/commandlineparser.h +++ b/src/app/qbs/parser/commandlineparser.h @@ -36,6 +36,7 @@ namespace qbs { class BuildOptions; +class InstallOptions; class CommandLineParser { @@ -52,6 +53,7 @@ public: QString commandDescription() const; QString projectFilePath() const; BuildOptions buildOptions() const; + InstallOptions installOptions() const; QStringList runArgs() const; QStringList products() const; QList<QVariantMap> buildConfigurations() const; diff --git a/src/app/qbs/parser/commandpool.cpp b/src/app/qbs/parser/commandpool.cpp index 0f5138834..138a1eb2e 100644 --- a/src/app/qbs/parser/commandpool.cpp +++ b/src/app/qbs/parser/commandpool.cpp @@ -67,6 +67,9 @@ qbs::Command *CommandPool::getCommand(CommandType type) const case UpdateTimestampsCommandType: command = new UpdateTimestampsCommand(m_optionPool); break; + case InstallCommandType: + command = new InstallCommand(m_optionPool); + break; case HelpCommandType: command = new HelpCommand(m_optionPool); break; diff --git a/src/app/qbs/parser/commandtype.h b/src/app/qbs/parser/commandtype.h index c9366c8db..4cdcd8e5f 100644 --- a/src/app/qbs/parser/commandtype.h +++ b/src/app/qbs/parser/commandtype.h @@ -33,7 +33,8 @@ namespace qbs { enum CommandType { BuildCommandType, CleanCommandType, RunCommandType, ShellCommandType, - PropertiesCommandType, StatusCommandType, UpdateTimestampsCommandType, HelpCommandType + PropertiesCommandType, StatusCommandType, UpdateTimestampsCommandType, + InstallCommandType, HelpCommandType }; } // namespace qbs diff --git a/src/lib/api/internaljobs.cpp b/src/lib/api/internaljobs.cpp index 69079aa7a..4a4dc2aeb 100644 --- a/src/lib/api/internaljobs.cpp +++ b/src/lib/api/internaljobs.cpp @@ -34,6 +34,7 @@ #include <buildgraph/buildproduct.h> #include <buildgraph/buildproject.h> #include <buildgraph/executor.h> +#include <buildgraph/productinstaller.h> #include <buildgraph/rulesevaluationcontext.h> #include <language/language.h> #include <language/loader.h> @@ -299,6 +300,43 @@ void InternalCleanJob::doClean() } +InternalInstallJob::InternalInstallJob(QObject *parent) : InternalJob(parent) +{ +} + +InternalInstallJob::~InternalInstallJob() +{ +} + +void InternalInstallJob::install(const QList<BuildProductPtr> &products, + const InstallOptions &options) +{ + m_products = products; + m_options = options; + QMetaObject::invokeMethod(this, "start", Qt::QueuedConnection); +} + +void InternalInstallJob::handleFinished() +{ + emit finished(this); +} + +void InternalInstallJob::start() +{ + QFutureWatcher<void> * const watcher = new QFutureWatcher<void>(this); + connect(watcher, SIGNAL(finished()), SLOT(handleFinished())); + watcher->setFuture(QtConcurrent::run(this, &InternalInstallJob::doInstall)); +} + +void InternalInstallJob::doInstall() +{ + try { + ProductInstaller(m_products, m_options, observer()).install(); + } catch (const Error &error) { + setError(error); + } +} + ErrorJob::ErrorJob(QObject *parent) : InternalJob(parent) { } diff --git a/src/lib/api/internaljobs.h b/src/lib/api/internaljobs.h index 852a3047f..219a38a59 100644 --- a/src/lib/api/internaljobs.h +++ b/src/lib/api/internaljobs.h @@ -32,8 +32,10 @@ #include "projectdata.h" #include <buildgraph/forward_decls.h> #include <tools/buildoptions.h> +#include <tools/installoptions.h> #include <tools/error.h> +#include <QList> #include <QMutex> #include <QObject> #include <QProcessEnvironment> @@ -168,6 +170,28 @@ private: }; +// TODO: Common base class for all jobs that need to start a thread? +class InternalInstallJob : public InternalJob +{ + Q_OBJECT +public: + InternalInstallJob(QObject *parent = 0); + ~InternalInstallJob(); + + void install(const QList<BuildProductPtr> &products, const InstallOptions &options); + +private slots: + void handleFinished(); + +private: + Q_INVOKABLE void start(); + void doInstall(); + +private: + QList<BuildProductPtr> m_products; + InstallOptions m_options; +}; + class ErrorJob : public InternalJob { Q_OBJECT diff --git a/src/lib/api/jobs.cpp b/src/lib/api/jobs.cpp index 37698d4fb..9272be7c1 100644 --- a/src/lib/api/jobs.cpp +++ b/src/lib/api/jobs.cpp @@ -259,4 +259,18 @@ void CleanJob::clean(const QList<BuildProductPtr> &products, qobject_cast<InternalCleanJob *>(internalJob())->clean(products, options, cleanAll); } +/*! + * \class InstallJob + * \brief The \c InstallJob class represents an operation installing files. + */ + +InstallJob::InstallJob(QObject *parent) : AbstractJob(new InternalInstallJob, parent) +{ +} + +void InstallJob::install(const QList<BuildProductPtr> &products, const InstallOptions &options) +{ + qobject_cast<InternalInstallJob *>(internalJob())->install(products, options); +} + } // namespace qbs diff --git a/src/lib/api/jobs.h b/src/lib/api/jobs.h index 2e69780b7..c16800818 100644 --- a/src/lib/api/jobs.h +++ b/src/lib/api/jobs.h @@ -39,6 +39,7 @@ namespace qbs { class BuildOptions; +class InstallOptions; namespace Internal { class InternalJob; class ProjectPrivate; @@ -123,6 +124,17 @@ private: void clean(const QList<Internal::BuildProductPtr> &products, const BuildOptions &options, bool cleanAll); }; + +class InstallJob : public AbstractJob +{ + Q_OBJECT + friend class Internal::ProjectPrivate; +private: + InstallJob(QObject *parent); + + void install(const QList<Internal::BuildProductPtr> &products, const InstallOptions &options); +}; + } // namespace qbs #endif // QBS_JOBS_H diff --git a/src/lib/api/project.cpp b/src/lib/api/project.cpp index b97ba4098..ab38a2b11 100644 --- a/src/lib/api/project.cpp +++ b/src/lib/api/project.cpp @@ -41,6 +41,7 @@ #include <logging/translator.h> #include <tools/error.h> #include <tools/fileinfo.h> +#include <tools/installoptions.h> #include <tools/preferences.h> #include <tools/profile.h> #include <tools/scannerpluginmanager.h> @@ -94,6 +95,8 @@ public: QObject *jobOwner); CleanJob *cleanProducts(const QList<BuildProductPtr> &products, const BuildOptions &options, Project::CleanType cleanType, QObject *jobOwner); + InstallJob *installProducts(const QList<BuildProductPtr> &products, + const InstallOptions &options, QObject *jobOwner); QList<BuildProductPtr> internalProducts(const QList<ProductData> &products) const; BuildProductPtr internalProduct(const ProductData &product) const; @@ -141,6 +144,14 @@ CleanJob *ProjectPrivate::cleanProducts(const QList<BuildProductPtr> &products, return job; } +InstallJob *ProjectPrivate::installProducts(const QList<BuildProductPtr> &products, + const InstallOptions &options, QObject *jobOwner) +{ + InstallJob * const job = new InstallJob(jobOwner); + job->install(products, options); + return job; +} + QList<BuildProductPtr> ProjectPrivate::internalProducts(const QList<ProductData> &products) const { QList<Internal::BuildProductPtr> internalProducts; @@ -321,8 +332,10 @@ ProjectData Project::projectData() const /*! * \brief Returns the file path of the executable associated with the given product. * If the product is not an application, an empty string is returned. + * The \a installRoot parameter is used to look up the executable in case it is installable; + * otherwise the parameter is ignored. To specify the default install root, leave it empty. */ -QString Project::targetExecutable(const ProductData &product) const +QString Project::targetExecutable(const ProductData &product, const QString &_installRoot) const { if (!product.isEnabled()) return QString(); @@ -331,8 +344,30 @@ QString Project::targetExecutable(const ProductData &product) const return QString(); foreach (const Internal::Artifact * const artifact, buildProduct->targetArtifacts) { - if (artifact->fileTags.contains(QLatin1String("application"))) - return artifact->filePath(); + if (artifact->fileTags.contains(QLatin1String("application"))) { + if (!artifact->properties->qbsPropertyValue(QLatin1String("install")).toBool()) + return artifact->filePath(); + const QString fileName = FileInfo::fileName(artifact->filePath()); + QString installRoot = _installRoot; + + if (installRoot.isEmpty()) { + // Yes, the executable is unlikely to run in this case. But we should still + // follow the protocol. + installRoot = artifact->properties + ->qbsPropertyValue(QLatin1String("sysroot")).toString(); + } + + if (installRoot.isEmpty()) { + installRoot = artifact->product->project->resolvedProject()->buildDirectory + + QLatin1Char('/') + InstallOptions::defaultInstallRoot(); + } + QString installDir = artifact->properties + ->qbsPropertyValue(QLatin1String("installDir")).toString(); + if (!installDir.startsWith(QLatin1Char('/'))) + installDir.prepend(QLatin1Char('/')); + installDir.prepend(installRoot); + return installDir.append(QLatin1Char('/')).append(fileName); + } } return QString(); } @@ -392,7 +427,7 @@ CleanJob *Project::cleanAllProducts(const BuildOptions &options, CleanType clean /*! * \brief Removes the build artifacts of the given products. - * The function will finish immediately, returning a \c BuildJob identifiying this operation. + * The function will finish immediately, returning a \c CleanJob identifiying this operation. */ CleanJob *Project::cleanSomeProducts(const QList<ProductData> &products, const BuildOptions &options, CleanType cleanType, QObject *jobOwner) const @@ -411,6 +446,35 @@ CleanJob *Project::cleanOneProduct(const ProductData &product, const BuildOption } /*! + * \brief Installs the installable files of all products in the project. + * The function will finish immediately, returning an \c InstallJob identifiying this operation. + */ +InstallJob *Project::installAllProducts(const InstallOptions &options, QObject *jobOwner) const +{ + return d->installProducts(d->internalProject->buildProducts().toList(), options, jobOwner); +} + +/*! + * \brief Installs the installable files of the given products. + * The function will finish immediately, returning an \c InstallJob identifiying this operation. + */ +InstallJob *Project::installSomeProducts(const QList<ProductData> &products, + const InstallOptions &options, QObject *jobOwner) const +{ + return d->installProducts(d->internalProducts(products), options, jobOwner); +} + +/*! + * \brief Convenience function for \c installSomeProducts(). + * \sa Project::installSomeProducts(). + */ +InstallJob *Project::installOneProduct(const ProductData &product, const InstallOptions &options, + QObject *jobOwner) const +{ + return installSomeProducts(QList<ProductData>() << product, options, jobOwner); +} + +/*! * \brief Updates the timestamps of all build artifacts in the given products. * Afterwards, the build graph will have the same state as if a successful build had been done. */ diff --git a/src/lib/api/project.h b/src/lib/api/project.h index 15675c823..d73d5ddba 100644 --- a/src/lib/api/project.h +++ b/src/lib/api/project.h @@ -44,6 +44,8 @@ namespace qbs { class BuildJob; class BuildOptions; class CleanJob; +class InstallJob; +class InstallOptions; class ProductData; class ProjectData; class RunEnvironment; @@ -68,7 +70,8 @@ public: ~Project(); ProjectData projectData() const; - QString targetExecutable(const ProductData &product) const; + QString targetExecutable(const ProductData &product, + const QString &installRoot = QString()) const; RunEnvironment getRunEnvironment(const ProductData &product, const QProcessEnvironment &environment) const; @@ -87,6 +90,12 @@ public: CleanJob *cleanOneProduct(const ProductData &product, const BuildOptions &options, CleanType cleanType, QObject *jobOwner = 0) const; + InstallJob *installAllProducts(const InstallOptions &options, QObject *jobOwner = 0) const; + InstallJob *installSomeProducts(const QList<ProductData> &products, + const InstallOptions &options, QObject *jobOwner = 0) const; + InstallJob *installOneProduct(const ProductData &product, const InstallOptions &options, + QObject *jobOwner = 0) const; + void updateTimestamps(const QList<ProductData> &products); bool operator==(const Project &other) const { return d.data() == other.d.data(); } diff --git a/src/lib/buildgraph/buildgraph.pri b/src/lib/buildgraph/buildgraph.pri index 1aa460146..6dc23b647 100644 --- a/src/lib/buildgraph/buildgraph.pri +++ b/src/lib/buildgraph/buildgraph.pri @@ -15,6 +15,7 @@ SOURCES += \ $$PWD/inputartifactscanner.cpp \ $$PWD/artifactvisitor.cpp \ $$PWD/artifactcleaner.cpp \ + $$PWD/productinstaller.cpp \ $$PWD/cycledetector.cpp \ $$PWD/rulesevaluationcontext.cpp \ $$PWD/buildproduct.cpp \ @@ -39,6 +40,7 @@ HEADERS += \ $$PWD/inputartifactscanner.h \ $$PWD/artifactvisitor.h \ $$PWD/artifactcleaner.h \ + $$PWD/productinstaller.h \ $$PWD/cycledetector.h \ $$PWD/forward_decls.h \ $$PWD/rulesevaluationcontext.h \ diff --git a/src/lib/buildgraph/productinstaller.cpp b/src/lib/buildgraph/productinstaller.cpp new file mode 100644 index 000000000..892ee640a --- /dev/null +++ b/src/lib/buildgraph/productinstaller.cpp @@ -0,0 +1,145 @@ +/**************************************************************************** +** +** Copyright (C) 2013 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 "productinstaller.h" + +#include "artifact.h" +#include "buildproduct.h" +#include "buildproject.h" +#include "rulesevaluationcontext.h" +#include <language/language.h> +#include <logging/logger.h> +#include <logging/translator.h> +#include <tools/error.h> +#include <tools/fileinfo.h> +#include <tools/progressobserver.h> + +#include <QDir> +#include <QFileInfo> + +namespace qbs { +namespace Internal { + +ProductInstaller::ProductInstaller(const QList<BuildProductPtr> &products, + const InstallOptions &options, ProgressObserver *observer) + : m_products(products), m_options(options), m_observer(observer) +{ + if (!m_options.installRoot.isEmpty()) { + if (m_options.removeFirst) { + const QString cfp = QFileInfo(m_options.installRoot).canonicalFilePath(); + if (cfp == QFileInfo(QDir::rootPath()).canonicalFilePath()) + throw Error(Tr::tr("Refusing to remove root directory.")); + if (cfp == QFileInfo(QDir::homePath()).canonicalFilePath()) + throw Error(Tr::tr("Refusing to remove home directory.")); + } + return; + } + + if (m_products.isEmpty()) + throw Error(Tr::tr("Cannot deduce install root, because there are no products.")); + + const BuildProductConstPtr &product = m_products.first(); + m_options.installRoot = product->rProduct->properties + ->qbsPropertyValue(QLatin1String("sysroot")).toString(); + if (m_options.installRoot.isEmpty()) { + m_options.installRoot = product->project->resolvedProject()->buildDirectory + + QLatin1Char('/') + InstallOptions::defaultInstallRoot(); + } else if (m_options.removeFirst) { + throw Error(Tr::tr("Refusing to remove sysroot.")); + } +} + +void ProductInstaller::install() +{ + if (m_options.removeFirst) + removeInstallRoot(); + + QList<const Artifact *> artifactsToInstall; + foreach (const BuildProductConstPtr &product, m_products) { + foreach (const Artifact *artifact, product->artifacts) { + if (artifact->properties->qbsPropertyValue(QLatin1String("install")).toBool()) + artifactsToInstall += artifact; + } + } + m_observer->initialize(Tr::tr("Installing"), artifactsToInstall.count()); + + foreach (const Artifact * const a, artifactsToInstall) { + copyFile(a); + m_observer->incrementProgressValue(); + } +} + +void ProductInstaller::removeInstallRoot() +{ + if (m_options.dryRun) { + qbsInfo() << Tr::tr("Would remove install root '%1'.").arg(m_options.installRoot); + return; + } + QString errorMessage; + if (!removeDirectoryWithContents(m_options.installRoot, &errorMessage)) { + const QString fullErrorMessage = Tr::tr("Cannot remove install root '%1': %2") + .arg(QDir::toNativeSeparators(m_options.installRoot), errorMessage); + handleError(fullErrorMessage); + } +} + +void ProductInstaller::copyFile(const Artifact *artifact) +{ + if (m_observer->canceled()) + throw Error(Tr::tr("Installation canceled due to user request.")); + const QString relativeInstallDir + = artifact->properties->qbsPropertyValue(QLatin1String("installDir")).toString(); + QString targetDir = m_options.installRoot; + targetDir.append(QLatin1Char('/')).append(relativeInstallDir); + if (m_options.dryRun) { + qbsInfo() << Tr::tr("Would copy file '%1' into target directory '%2'.") + .arg(QDir::toNativeSeparators(artifact->filePath()), targetDir); + return; + } + + if (!QDir::root().mkpath(targetDir)) { + handleError(Tr::tr("Directory '%1' could not be created.") + .arg(QDir::toNativeSeparators(targetDir))); + return; + } + const QString targetFilePath + = targetDir + QLatin1Char('/') + FileInfo::fileName(artifact->filePath()); + QString errorMessage; + if (!copyFileRecursion(artifact->filePath(), targetFilePath, &errorMessage)) + handleError(Tr::tr("Installation error: %1").arg(errorMessage)); +} + +void ProductInstaller::handleError(const QString &message) +{ + if (!m_options.keepGoing) + throw Error(message); + qbsWarning() << message; +} + +} // namespace Internal +} // namespace qbs diff --git a/src/lib/buildgraph/productinstaller.h b/src/lib/buildgraph/productinstaller.h new file mode 100644 index 000000000..e5fecbb12 --- /dev/null +++ b/src/lib/buildgraph/productinstaller.h @@ -0,0 +1,61 @@ +/**************************************************************************** +** +** Copyright (C) 2013 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. +** +****************************************************************************/ +#ifndef QBS_PRODUCT_INSTALLER_H +#define QBS_PRODUCT_INSTALLER_H + +#include "forward_decls.h" +#include <tools/installoptions.h> + +#include <QList> + +namespace qbs { +namespace Internal { +class ProgressObserver; + +class ProductInstaller +{ +public: + ProductInstaller(const QList<BuildProductPtr> &products, const InstallOptions &options, + ProgressObserver *observer); + void install(); + +private: + void removeInstallRoot(); + void copyFile(const Artifact *artifact); + void handleError(const QString &message); + + const QList<BuildProductPtr> m_products; + InstallOptions m_options; + ProgressObserver * const m_observer; +}; + +} // namespace Internal +} // namespace qbs + +#endif // Header guard diff --git a/src/lib/language/language.cpp b/src/lib/language/language.cpp index d3c6f4681..93dc0c878 100644 --- a/src/lib/language/language.cpp +++ b/src/lib/language/language.cpp @@ -84,6 +84,13 @@ PropertyMap::PropertyMap(const PropertyMap &other) { } +QVariant PropertyMap::qbsPropertyValue(const QString &key) +{ + const QStringList fullKey + = QStringList() << QLatin1String("modules") << QLatin1String("qbs") << key; + return getConfigProperty(value(), fullKey); +} + void PropertyMap::setValue(const QVariantMap &map) { m_value = map; diff --git a/src/lib/language/language.h b/src/lib/language/language.h index a93629b21..42e4031a8 100644 --- a/src/lib/language/language.h +++ b/src/lib/language/language.h @@ -65,6 +65,7 @@ public: Ptr clone() const { return Ptr(new PropertyMap(*this)); } const QVariantMap &value() const { return m_value; } + QVariant qbsPropertyValue(const QString &key); // Convenience function. void setValue(const QVariantMap &value); QScriptValue toScriptValue(QScriptEngine *scriptEngine) const; diff --git a/src/lib/language/loader.cpp b/src/lib/language/loader.cpp index b38333daf..c008eecf0 100644 --- a/src/lib/language/loader.cpp +++ b/src/lib/language/loader.cpp @@ -1612,11 +1612,9 @@ void Loader::LoaderPrivate::resolveGroup(ResolvedProductPtr rproduct, Evaluation if (!files.isEmpty()) throw Error(Tr::tr("Group.files and Group.fileTagsFilters are exclusive."), group->instantiatingObject()->prototypeLocation); - ArtifactPropertiesPtr aprops = ArtifactProperties::create(); aprops->setFileTagsFilter(fileTagsFilter); - QVariantMap cfgval = evaluateAll(rproduct, group->scope); - cfgval.remove("fileTagsFilter"); + QVariantMap cfgval = evaluateModuleValues(rproduct, product, group->scope); PropertyMapPtr cfg = PropertyMap::create(); cfg->setValue(cfgval); aprops->setPropertyMap(cfg); diff --git a/src/lib/qbs.h b/src/lib/qbs.h index 6553e543f..80db1eeb5 100644 --- a/src/lib/qbs.h +++ b/src/lib/qbs.h @@ -35,6 +35,7 @@ #include "logging/logger.h" #include "tools/buildoptions.h" #include "tools/error.h" +#include "tools/installoptions.h" #include "tools/settings.h" #endif // QBS_H diff --git a/src/lib/tools/fileinfo.cpp b/src/lib/tools/fileinfo.cpp index 7adeb6b37..8eb251d15 100644 --- a/src/lib/tools/fileinfo.cpp +++ b/src/lib/tools/fileinfo.cpp @@ -343,11 +343,11 @@ bool removeDirectoryWithContents(const QString &path, QString *errorMessage) bool copyFileRecursion(const QString &srcFilePath, const QString &tgtFilePath, QString *errorMessage) { - QFileInfo srcFileInfo(srcFilePath); + FileInfo srcFileInfo(srcFilePath); if (srcFileInfo.isDir()) { QDir targetDir(tgtFilePath); targetDir.cdUp(); - if (!targetDir.mkpath(QFileInfo(tgtFilePath).fileName())) { + if (!targetDir.mkpath(FileInfo::fileName(tgtFilePath))) { *errorMessage = Tr::tr("The directory '%1' could not be created.") .arg(QDir::toNativeSeparators(tgtFilePath)); return false; @@ -362,7 +362,15 @@ bool copyFileRecursion(const QString &srcFilePath, const QString &tgtFilePath, return false; } } else { + FileInfo tgtFileInfo(tgtFilePath); + if (tgtFileInfo.exists() && srcFileInfo.lastModified() <= tgtFileInfo.lastModified()) + return true; QFile file(srcFilePath); + QFile targetFile(tgtFilePath); + if (targetFile.exists() && !targetFile.remove()) { + *errorMessage = Tr::tr("Could not remove file '%1'. %2") + .arg(QDir::toNativeSeparators(tgtFilePath), targetFile.errorString()); + } if (!file.copy(tgtFilePath)) { *errorMessage = Tr::tr("Could not copy file '%1' to '%2'. %3") .arg(QDir::toNativeSeparators(srcFilePath), QDir::toNativeSeparators(tgtFilePath), diff --git a/src/lib/tools/installoptions.cpp b/src/lib/tools/installoptions.cpp new file mode 100644 index 000000000..a6ff22e07 --- /dev/null +++ b/src/lib/tools/installoptions.cpp @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2013 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 "installoptions.h" + +namespace qbs { + +/*! + * \class InstallOptions + * \brief The \c InstallOptions class comprises parameters that influence the behavior of + * install operations. + */ + + /*! + * \variable InstallOptions::dryRun + * \brief if true, qbs will not actually copy any files, but just show what would happen + */ + +/*! + * \variable InstallOptions::keepGoing + * \brief if true, do not abort on errors + * If a file cannot be copied e.g. due to a permission problem, a warning will be printed and + * the installation will continue. If this flag is not set, then the installation will abort + * immediately in case of an error. + */ + +/*! + * \variable InstallOptions::installRoot + * \brief the base directory for the installation + * All "qbs.installDir" paths are relative to this root. If the string is empty, the value of + * qbs.sysroot will be used. If that is also empty, the base directory is + * "<build dir>/install-root". + */ + + /*! + * \variable InstallOptions::removeFirst + * \brief if true, removes the installRoot before installing any files. + * \note qbs may do some safety checks here and refuse to remove certain directories such as + * a user's home directory. You should still be careful with this option, since it + * deletes recursively. + */ + +InstallOptions::InstallOptions() : removeFirst(false), dryRun(false), keepGoing(false) +{ +} + +/*! + * \brief The default install root, relative to the build directory. + */ +QString InstallOptions::defaultInstallRoot() +{ + return QLatin1String("install-root"); +} + +} // namespace qbs diff --git a/src/lib/tools/installoptions.h b/src/lib/tools/installoptions.h new file mode 100644 index 000000000..610a07efe --- /dev/null +++ b/src/lib/tools/installoptions.h @@ -0,0 +1,50 @@ +/**************************************************************************** +** +** Copyright (C) 2013 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. +** +****************************************************************************/ +#ifndef QBS_INSTALLOPTIONS_H +#define QBS_INSTALLOPTIONS_H + +#include <QString> + +namespace qbs { + +class InstallOptions +{ +public: + InstallOptions(); + + static QString defaultInstallRoot(); + QString installRoot; + bool removeFirst; + bool dryRun; + bool keepGoing; +}; + +} // namespace qbs + +#endif // QBS_INSTALLOPTIONS_H diff --git a/src/lib/tools/tools.pri b/src/lib/tools/tools.pri index 89c64db14..490603d61 100644 --- a/src/lib/tools/tools.pri +++ b/src/lib/tools/tools.pri @@ -15,6 +15,7 @@ HEADERS += \ $$PWD/progressobserver.h \ $$PWD/hostosinfo.h \ $$PWD/buildoptions.h \ + $$PWD/installoptions.h \ $$PWD/persistentobject.h \ $$PWD/weakpointer.h @@ -28,7 +29,8 @@ SOURCES += \ $$PWD/preferences.cpp \ $$PWD/profile.cpp \ $$PWD/progressobserver.cpp \ - $$PWD/buildoptions.cpp + $$PWD/buildoptions.cpp \ + $$PWD/installoptions.cpp win32 { SOURCES += $$PWD/filetime_win.cpp diff --git a/tests/auto/blackbox/testdata/installed_artifact/installed_artifact.qbs b/tests/auto/blackbox/testdata/installed_artifact/installed_artifact.qbs new file mode 100644 index 000000000..8b42f2188 --- /dev/null +++ b/tests/auto/blackbox/testdata/installed_artifact/installed_artifact.qbs @@ -0,0 +1,12 @@ +import qbs.base 1.0 + +Application { + name: "installedApp" + Depends { name: "cpp" } + files: "main.cpp" + Group { + fileTagsFilter: "application" + qbs.install: true + qbs.installDir: "bin" + } +} diff --git a/tests/auto/blackbox/testdata/installed_artifact/main.cpp b/tests/auto/blackbox/testdata/installed_artifact/main.cpp new file mode 100644 index 000000000..237c8ce18 --- /dev/null +++ b/tests/auto/blackbox/testdata/installed_artifact/main.cpp @@ -0,0 +1 @@ +int main() {} diff --git a/tests/auto/blackbox/testdata/recursive_renaming/recursive_renaming.qbs b/tests/auto/blackbox/testdata/recursive_renaming/recursive_renaming.qbs index b30709210..6b2889a8b 100644 --- a/tests/auto/blackbox/testdata/recursive_renaming/recursive_renaming.qbs +++ b/tests/auto/blackbox/testdata/recursive_renaming/recursive_renaming.qbs @@ -1,9 +1,8 @@ import qbs.base 1.0 Product { - type: "installed_content" Group { - fileTags: "install" + qbs.install: true files: "dir" } } diff --git a/tests/auto/blackbox/testdata/recursive_wildcards/recursive_wildcards.qbs b/tests/auto/blackbox/testdata/recursive_wildcards/recursive_wildcards.qbs index e8e8db2a5..78b1cd0f2 100644 --- a/tests/auto/blackbox/testdata/recursive_wildcards/recursive_wildcards.qbs +++ b/tests/auto/blackbox/testdata/recursive_wildcards/recursive_wildcards.qbs @@ -1,9 +1,8 @@ Product { - type: "installed_content" Group { - fileTags: "install" files: "dir/*" recursive: true + qbs.install: true qbs.installDir: "dir" } } diff --git a/tests/auto/blackbox/testdata/wildcard_renaming/wildcard_renaming.qbs b/tests/auto/blackbox/testdata/wildcard_renaming/wildcard_renaming.qbs index fafe77ae5..9c9799dba 100644 --- a/tests/auto/blackbox/testdata/wildcard_renaming/wildcard_renaming.qbs +++ b/tests/auto/blackbox/testdata/wildcard_renaming/wildcard_renaming.qbs @@ -1,9 +1,8 @@ import qbs.base 1.0 Product { - type: "installed_content" Group { - fileTags: "install" + qbs.install: true files: "*" } } diff --git a/tests/auto/blackbox/tst_blackbox.cpp b/tests/auto/blackbox/tst_blackbox.cpp index 1f6e6442c..540df54c6 100644 --- a/tests/auto/blackbox/tst_blackbox.cpp +++ b/tests/auto/blackbox/tst_blackbox.cpp @@ -30,11 +30,13 @@ #include "tst_blackbox.h" #include <tools/fileinfo.h> #include <tools/hostosinfo.h> +#include <tools/installoptions.h> #include <QLocale> #include <QTemporaryFile> using qbs::HostOsInfo; +using qbs::InstallOptions; using qbs::Internal::removeDirectoryWithContents; static QString initQbsExecutableFilePath() @@ -51,6 +53,7 @@ TestBlackbox::TestBlackbox() qbsExecutableFilePath(initQbsExecutableFilePath()), buildProfile(QLatin1String("qbs_autotests")), buildDir(buildProfile + QLatin1String("-debug")), + defaultInstallRoot(buildDir + QLatin1Char('/') + InstallOptions::defaultInstallRoot()), buildGraphPath(buildDir + QLatin1Char('/') + buildDir + QLatin1String(".bg")) { QLocale::setDefault(QLocale::c()); @@ -452,34 +455,34 @@ void TestBlackbox::trackAddMocInclude() void TestBlackbox::wildcardRenaming() { QDir::setCurrent(testDataDir + "/wildcard_renaming"); - QCOMPARE(runQbs(QStringList()), 0); - QVERIFY(QFileInfo(buildDir + "/pioniere.txt").exists()); + QCOMPARE(runQbs(QStringList("install")), 0); + QVERIFY(QFileInfo(defaultInstallRoot + "/pioniere.txt").exists()); QFile::rename(QDir::currentPath() + "/pioniere.txt", QDir::currentPath() + "/fdj.txt"); - QCOMPARE(runQbs(QStringList()), 0); - QVERIFY(!QFileInfo(buildDir + "/pioniere.txt").exists()); - QVERIFY(QFileInfo(buildDir + "/fdj.txt").exists()); + QCOMPARE(runQbs(QStringList("install") << "--remove-first"), 0); + QVERIFY(!QFileInfo(defaultInstallRoot + "/pioniere.txt").exists()); + QVERIFY(QFileInfo(defaultInstallRoot + "/fdj.txt").exists()); } void TestBlackbox::recursiveRenaming() { QDir::setCurrent(testDataDir + "/recursive_renaming"); - QCOMPARE(runQbs(QStringList()), 0); - QVERIFY(QFileInfo(buildDir + "/dir/wasser.txt").exists()); - QVERIFY(QFileInfo(buildDir + "/dir/subdir/blubb.txt").exists()); + QCOMPARE(runQbs(QStringList("install")), 0); + QVERIFY(QFileInfo(defaultInstallRoot + "/dir/wasser.txt").exists()); + QVERIFY(QFileInfo(defaultInstallRoot + "/dir/subdir/blubb.txt").exists()); QTest::qWait(1000); QVERIFY(QFile::rename(QDir::currentPath() + "/dir/wasser.txt", QDir::currentPath() + "/dir/wein.txt")); - QCOMPARE(runQbs(QStringList()), 0); - QVERIFY(!QFileInfo(buildDir + "/dir/wasser.txt").exists()); - QVERIFY(QFileInfo(buildDir + "/dir/wein.txt").exists()); - QVERIFY(QFileInfo(buildDir + "/dir/subdir/blubb.txt").exists()); + QCOMPARE(runQbs(QStringList("install") << "--remove-first"), 0); + QVERIFY(!QFileInfo(defaultInstallRoot + "/dir/wasser.txt").exists()); + QVERIFY(QFileInfo(defaultInstallRoot + "/dir/wein.txt").exists()); + QVERIFY(QFileInfo(defaultInstallRoot + "/dir/subdir/blubb.txt").exists()); } void TestBlackbox::recursiveWildcards() { QDir::setCurrent(testDataDir + "/recursive_wildcards"); - QCOMPARE(runQbs(QStringList()), 0); - QVERIFY(QFileInfo(buildDir + "/dir/file1.txt").exists()); - QVERIFY(QFileInfo(buildDir + "/dir/file2.txt").exists()); + QCOMPARE(runQbs(QStringList("install")), 0); + QVERIFY(QFileInfo(defaultInstallRoot + "/dir/file1.txt").exists()); + QVERIFY(QFileInfo(defaultInstallRoot + "/dir/file2.txt").exists()); } void TestBlackbox::invalidWildcards() @@ -504,4 +507,25 @@ void TestBlackbox::updateTimestamps() QCOMPARE(runQbs(QStringList()), 0); // Build graph now up to date. } +void TestBlackbox::installedApp() +{ + QDir::setCurrent(testDataDir + "/installed_artifact"); + + QCOMPARE(runQbs(QStringList("install")), 0); + QVERIFY(QFile::exists(defaultInstallRoot + + HostOsInfo::appendExecutableSuffix(QLatin1String("/bin/installedApp")))); + + QCOMPARE(runQbs(QStringList("install") << "--install-root" << (testDataDir + "/installed-app")), 0); + QVERIFY(QFile::exists(testDataDir + "/installed-app/bin/installedApp")); + + QFile addedFile(defaultInstallRoot + QLatin1String("/blubb.txt")); + QVERIFY(addedFile.open(QIODevice::WriteOnly)); + addedFile.close(); + QVERIFY(addedFile.exists()); + QCOMPARE(runQbs(QStringList("install") << "--remove-first"), 0); + QVERIFY(QFile::exists(defaultInstallRoot + + HostOsInfo::appendExecutableSuffix(QLatin1String("/bin/installedApp")))); + QVERIFY(!addedFile.exists()); +} + QTEST_MAIN(TestBlackbox) diff --git a/tests/auto/blackbox/tst_blackbox.h b/tests/auto/blackbox/tst_blackbox.h index ad81fe106..db0837c3f 100644 --- a/tests/auto/blackbox/tst_blackbox.h +++ b/tests/auto/blackbox/tst_blackbox.h @@ -44,6 +44,7 @@ class TestBlackbox : public QObject const QString qbsExecutableFilePath; const QString buildProfile; const QString buildDir; + const QString defaultInstallRoot; const QString buildGraphPath; public: @@ -76,6 +77,7 @@ private slots: void recursiveWildcards(); void invalidWildcards(); void updateTimestamps(); + void installedApp(); private: QByteArray m_qbsStderr; diff --git a/tests/auto/language/testdata/jsimportsinmultiplescopes.js b/tests/auto/language/testdata/jsimportsinmultiplescopes.js index bd1331a0f..4e939505c 100644 --- a/tests/auto/language/testdata/jsimportsinmultiplescopes.js +++ b/tests/auto/language/testdata/jsimportsinmultiplescopes.js @@ -6,7 +6,7 @@ function getName(qbsModule) return "MyProduct"; } -function getInstallPrefix() +function getInstallDir() { return "somewhere"; } diff --git a/tests/auto/language/testdata/jsimportsinmultiplescopes.qbs b/tests/auto/language/testdata/jsimportsinmultiplescopes.qbs index 140b093ca..388cf974b 100644 --- a/tests/auto/language/testdata/jsimportsinmultiplescopes.qbs +++ b/tests/auto/language/testdata/jsimportsinmultiplescopes.qbs @@ -2,6 +2,6 @@ import "jsimportsinmultiplescopes.js" as MyFunctions Product { name: MyFunctions.getName(qbs) - qbs.installPrefix: MyFunctions.getInstallPrefix() + qbs.installDir: MyFunctions.getInstallDir() files: "main.cpp" } diff --git a/tests/auto/language/testdata/outerInGroup.qbs b/tests/auto/language/testdata/outerInGroup.qbs index b5ead48d9..50e6d13fe 100644 --- a/tests/auto/language/testdata/outerInGroup.qbs +++ b/tests/auto/language/testdata/outerInGroup.qbs @@ -3,12 +3,12 @@ import qbs.base 1.0 Project { Product { name: "OuterInGroup" - qbs.installPrefix: "/somewhere" + qbs.installDir: "/somewhere" files: ["main.cpp"] Group { name: "Special Group" files: ["aboutdialog.cpp"] - qbs.installPrefix: outer + "/else" + qbs.installDir: outer + "/else" } } } diff --git a/tests/auto/language/tst_language.cpp b/tests/auto/language/tst_language.cpp index 55322d2bc..460b82aa5 100644 --- a/tests/auto/language/tst_language.cpp +++ b/tests/auto/language/tst_language.cpp @@ -318,17 +318,15 @@ void TestLanguage::outerInGroup() QCOMPARE(group->name, product->name); QCOMPARE(group->files.count(), 1); SourceArtifactConstPtr artifact = group->files.first(); - QVariant installPrefix = getConfigProperty(artifact->properties->value(), - QStringList() << "modules" << "qbs" << "installPrefix"); - QCOMPARE(installPrefix.toString(), QString("/somewhere")); + QVariant installDir = artifact->properties->qbsPropertyValue("installDir"); + QCOMPARE(installDir.toString(), QString("/somewhere")); group = product->groups.at(1); QVERIFY(group); QCOMPARE(group->name, QString("Special Group")); QCOMPARE(group->files.count(), 1); artifact = group->files.first(); - installPrefix = getConfigProperty(artifact->properties->value(), - QStringList() << "modules" << "qbs" << "installPrefix"); - QCOMPARE(installPrefix.toString(), QString("/somewhere/else")); + installDir = artifact->properties->qbsPropertyValue("installDir"); + QCOMPARE(installDir.toString(), QString("/somewhere/else")); } catch (const Error &e) { exceptionCaught = true; |