1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qbs.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 #include "productinstaller.h"
40
41 #include "artifact.h"
42 #include "productbuilddata.h"
43
44 #include <language/language.h>
45 #include <language/propertymapinternal.h>
46 #include <logging/translator.h>
47 #include <tools/qbsassert.h>
48 #include <tools/error.h>
49 #include <tools/fileinfo.h>
50 #include <tools/hostosinfo.h>
51 #include <tools/progressobserver.h>
52 #include <tools/qbsassert.h>
53 #include <tools/qttools.h>
54 #include <tools/stringconstants.h>
55
56 #include <QtCore/qdir.h>
57 #include <QtCore/qfileinfo.h>
58
59 namespace qbs {
60 namespace Internal {
61
ProductInstaller(TopLevelProjectPtr project,QVector<ResolvedProductPtr> products,InstallOptions options,ProgressObserver * observer,Logger logger)62 ProductInstaller::ProductInstaller(TopLevelProjectPtr project,
63 QVector<ResolvedProductPtr> products, InstallOptions options,
64 ProgressObserver *observer, Logger logger)
65 : m_project(std::move(project)),
66 m_products(std::move(products)),
67 m_options(std::move(options)),
68 m_observer(observer),
69 m_logger(std::move(logger))
70 {
71 if (!m_options.installRoot().isEmpty()) {
72 QFileInfo installRootFileInfo(m_options.installRoot());
73 QBS_ASSERT(installRootFileInfo.isAbsolute(), /* just complain */);
74 if (m_options.removeExistingInstallation()) {
75 const QString cfp = installRootFileInfo.canonicalFilePath();
76 if (cfp == QFileInfo(QDir::rootPath()).canonicalFilePath())
77 throw ErrorInfo(Tr::tr("Refusing to remove root directory."));
78 if (cfp == QFileInfo(QDir::homePath()).canonicalFilePath())
79 throw ErrorInfo(Tr::tr("Refusing to remove home directory."));
80 }
81 return;
82 }
83
84 if (m_options.installIntoSysroot()) {
85 if (m_options.removeExistingInstallation())
86 throw ErrorInfo(Tr::tr("Refusing to remove sysroot."));
87 }
88 initInstallRoot(m_project.get(), m_options);
89 }
90
install()91 void ProductInstaller::install()
92 {
93 m_targetFilePathsMap.clear();
94
95 if (m_options.removeExistingInstallation())
96 removeInstallRoot();
97
98 QList<const Artifact *> artifactsToInstall;
99 for (const auto &product : qAsConst(m_products)) {
100 QBS_CHECK(product->buildData);
101 for (const Artifact *artifact : filterByType<Artifact>(product->buildData->allNodes())) {
102 if (artifact->properties->qbsPropertyValue(StringConstants::installProperty()).toBool())
103 artifactsToInstall.push_back(artifact);
104 }
105 }
106 m_observer->initialize(Tr::tr("Installing"), artifactsToInstall.size());
107
108 for (const Artifact * const a : qAsConst(artifactsToInstall)) {
109 copyFile(a);
110 m_observer->incrementProgressValue();
111 }
112 }
113
targetFilePath(const TopLevelProject * project,const QString & productSourceDir,const QString & sourceFilePath,const PropertyMapConstPtr & properties,InstallOptions & options)114 QString ProductInstaller::targetFilePath(const TopLevelProject *project,
115 const QString &productSourceDir,
116 const QString &sourceFilePath, const PropertyMapConstPtr &properties,
117 InstallOptions &options)
118 {
119 if (!properties->qbsPropertyValue(StringConstants::installProperty()).toBool())
120 return {};
121 const QString relativeInstallDir
122 = properties->qbsPropertyValue(StringConstants::installDirProperty()).toString();
123 const QString installPrefix
124 = properties->qbsPropertyValue(StringConstants::installPrefixProperty()).toString();
125 const QString installSourceBase
126 = properties->qbsPropertyValue(StringConstants::installSourceBaseProperty()).toString();
127 initInstallRoot(project, options);
128 QString targetDir = options.installRoot();
129 if (targetDir.isEmpty())
130 targetDir = properties->qbsPropertyValue(StringConstants::installRootProperty()).toString();
131 targetDir.append(QLatin1Char('/')).append(installPrefix)
132 .append(QLatin1Char('/')).append(relativeInstallDir);
133 targetDir = QDir::cleanPath(targetDir);
134
135 QString targetFilePath;
136 if (installSourceBase.isEmpty()) {
137 if (!targetDir.startsWith(options.installRoot())) {
138 throw ErrorInfo(Tr::tr("Cannot install '%1', because target directory '%2' "
139 "is outside of install root '%3'")
140 .arg(sourceFilePath, targetDir, options.installRoot()));
141 }
142
143 // This has the same effect as if installSourceBase would equal the directory of the file.
144 targetFilePath = FileInfo::fileName(sourceFilePath);
145 } else {
146 const QString localAbsBasePath = FileInfo::resolvePath(QDir::cleanPath(productSourceDir),
147 QDir::cleanPath(installSourceBase));
148 targetFilePath = sourceFilePath;
149 if (!targetFilePath.startsWith(localAbsBasePath)) {
150 throw ErrorInfo(Tr::tr("Cannot install '%1', because it doesn't start with the"
151 " value of qbs.installSourceBase '%2'.").arg(sourceFilePath,
152 localAbsBasePath));
153 }
154
155 // Since there is a difference between X: and X:\\ on Windows, absolute paths can sometimes
156 // end with a slash, so only remove an extra character if there is no ending slash
157 targetFilePath.remove(0, localAbsBasePath.length()
158 + (localAbsBasePath.endsWith(QLatin1Char('/')) ? 0 : 1));
159 }
160
161 targetFilePath.prepend(targetDir + QLatin1Char('/'));
162 return QDir::cleanPath(targetFilePath);
163 }
164
initInstallRoot(const TopLevelProject * project,InstallOptions & options)165 void ProductInstaller::initInstallRoot(const TopLevelProject *project,
166 InstallOptions &options)
167 {
168 if (!options.installRoot().isEmpty())
169 return;
170
171 options.setInstallRoot(effectiveInstallRoot(options, project));
172 }
173
removeInstallRoot()174 void ProductInstaller::removeInstallRoot()
175 {
176 const QString nativeInstallRoot = QDir::toNativeSeparators(m_options.installRoot());
177 if (m_options.dryRun()) {
178 m_logger.qbsInfo() << Tr::tr("Would remove install root '%1'.").arg(nativeInstallRoot);
179 return;
180 }
181 m_logger.qbsDebug() << QStringLiteral("Removing install root '%1'.")
182 .arg(nativeInstallRoot);
183
184 QString errorMessage;
185 if (!removeDirectoryWithContents(m_options.installRoot(), &errorMessage)) {
186 const QString fullErrorMessage = Tr::tr("Cannot remove install root '%1': %2")
187 .arg(QDir::toNativeSeparators(m_options.installRoot()), errorMessage);
188 handleError(fullErrorMessage);
189 }
190 }
191
copyFile(const Artifact * artifact)192 void ProductInstaller::copyFile(const Artifact *artifact)
193 {
194 if (m_observer->canceled()) {
195 throw ErrorInfo(Tr::tr("Installation canceled for configuration '%1'.")
196 .arg(m_products.front()->project->topLevelProject()->id()));
197 }
198
199 const QString targetFilePath = this->targetFilePath(m_project.get(),
200 artifact->product->sourceDirectory, artifact->filePath(),
201 artifact->properties, m_options);
202 const QString targetDir = FileInfo::path(targetFilePath);
203 const QString nativeFilePath = QDir::toNativeSeparators(artifact->filePath());
204 const QString nativeTargetDir = QDir::toNativeSeparators(targetDir);
205 if (m_options.dryRun()) {
206 m_logger.qbsDebug() << Tr::tr("Would copy file '%1' into target directory '%2'.")
207 .arg(nativeFilePath, nativeTargetDir);
208 return;
209 }
210 m_logger.qbsDebug() << QStringLiteral("Copying file '%1' into target directory '%2'.")
211 .arg(nativeFilePath, nativeTargetDir);
212
213 if (!QDir::root().mkpath(targetDir)) {
214 handleError(Tr::tr("Directory '%1' could not be created.").arg(nativeTargetDir));
215 return;
216 }
217 QFileInfo fi(artifact->filePath());
218 if (fi.isDir() && !(HostOsInfo::isAnyUnixHost() && fi.isSymLink())) {
219 m_logger.qbsWarning() << Tr::tr("Not recursively copying directory '%1' into target "
220 "directory '%2'. Install the individual file artifacts "
221 "instead.")
222 .arg(nativeFilePath, nativeTargetDir);
223 }
224
225 if (m_targetFilePathsMap.contains(targetFilePath)) {
226 // We only want this error message when installing artifacts pointing to different file
227 // paths, to the same location. We do NOT want it when installing different artifacts
228 // pointing to the same file, to the same location. This reduces unnecessary noise: for
229 // example, when installing headers from a multiplexed product, the user does not need to
230 // do extra work to ensure the files are installed by only one of the instances.
231 if (artifact->filePath() != m_targetFilePathsMap[targetFilePath]) {
232 handleError(Tr::tr("Cannot install files '%1' and '%2' to the same location '%3'. "
233 "If you are attempting to install a directory hierarchy, consider "
234 "using the qbs.installSourceBase property.")
235 .arg(artifact->filePath(), m_targetFilePathsMap[targetFilePath],
236 targetFilePath));
237 }
238 }
239 m_targetFilePathsMap.insert(targetFilePath, artifact->filePath());
240
241 QString errorMessage;
242 if (!copyFileRecursion(artifact->filePath(), targetFilePath, true, false, &errorMessage))
243 handleError(Tr::tr("Installation error: %1").arg(errorMessage));
244 }
245
handleError(const QString & message)246 void ProductInstaller::handleError(const QString &message)
247 {
248 if (!m_options.keepGoing())
249 throw ErrorInfo(message);
250 m_logger.qbsWarning() << message;
251 }
252
253 } // namespace Internal
254 } // namespace qbs
255