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