aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristian Kandeler <christian.kandeler@digia.com>2013-01-07 15:48:45 +0100
committerJoerg Bornemann <joerg.bornemann@digia.com>2013-01-22 12:21:38 +0100
commit5cdf94de300e72987dfbe5c0fec5b86317ad6280 (patch)
treee484cd9ccd2faae387dd3978b6eb7783c99b84b3
parent49060f9b1e2881a5e7801f59f6d237fd85da1ae1 (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>
-rw-r--r--doc/qbs.qdoc94
-rw-r--r--share/qbs/imports/qbs/base/QmlApp.qbs7
-rw-r--r--share/qbs/modules/qbs/common.qbs79
-rw-r--r--src/app/qbs/commandlinefrontend.cpp30
-rw-r--r--src/app/qbs/parser/command.cpp67
-rw-r--r--src/app/qbs/parser/command.h13
-rw-r--r--src/app/qbs/parser/commandlineoption.cpp52
-rw-r--r--src/app/qbs/parser/commandlineoption.h26
-rw-r--r--src/app/qbs/parser/commandlineoptionpool.cpp16
-rw-r--r--src/app/qbs/parser/commandlineoptionpool.h2
-rw-r--r--src/app/qbs/parser/commandlineparser.cpp13
-rw-r--r--src/app/qbs/parser/commandlineparser.h2
-rw-r--r--src/app/qbs/parser/commandpool.cpp3
-rw-r--r--src/app/qbs/parser/commandtype.h3
-rw-r--r--src/lib/api/internaljobs.cpp38
-rw-r--r--src/lib/api/internaljobs.h24
-rw-r--r--src/lib/api/jobs.cpp14
-rw-r--r--src/lib/api/jobs.h12
-rw-r--r--src/lib/api/project.cpp72
-rw-r--r--src/lib/api/project.h11
-rw-r--r--src/lib/buildgraph/buildgraph.pri2
-rw-r--r--src/lib/buildgraph/productinstaller.cpp145
-rw-r--r--src/lib/buildgraph/productinstaller.h61
-rw-r--r--src/lib/language/language.cpp7
-rw-r--r--src/lib/language/language.h1
-rw-r--r--src/lib/language/loader.cpp4
-rw-r--r--src/lib/qbs.h1
-rw-r--r--src/lib/tools/fileinfo.cpp12
-rw-r--r--src/lib/tools/installoptions.cpp80
-rw-r--r--src/lib/tools/installoptions.h50
-rw-r--r--src/lib/tools/tools.pri4
-rw-r--r--tests/auto/blackbox/testdata/installed_artifact/installed_artifact.qbs12
-rw-r--r--tests/auto/blackbox/testdata/installed_artifact/main.cpp1
-rw-r--r--tests/auto/blackbox/testdata/recursive_renaming/recursive_renaming.qbs3
-rw-r--r--tests/auto/blackbox/testdata/recursive_wildcards/recursive_wildcards.qbs3
-rw-r--r--tests/auto/blackbox/testdata/wildcard_renaming/wildcard_renaming.qbs3
-rw-r--r--tests/auto/blackbox/tst_blackbox.cpp54
-rw-r--r--tests/auto/blackbox/tst_blackbox.h2
-rw-r--r--tests/auto/language/testdata/jsimportsinmultiplescopes.js2
-rw-r--r--tests/auto/language/testdata/jsimportsinmultiplescopes.qbs2
-rw-r--r--tests/auto/language/testdata/outerInGroup.qbs4
-rw-r--r--tests/auto/language/tst_language.cpp10
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;