buildablehelperlibrary.cpp 15.5 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
2
**
3
** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
hjk's avatar
hjk committed
4
** Contact: http://www.qt-project.org/legal
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
7
**
hjk's avatar
hjk committed
8 9 10 11 12
** 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
Eike Ziller's avatar
Eike Ziller committed
13 14
** conditions see http://www.qt.io/licensing.  For further information
** use the contact form at http://www.qt.io/contact-us.
15 16
**
** GNU Lesser General Public License Usage
hjk's avatar
hjk committed
17
** Alternatively, this file may be used under the terms of the GNU Lesser
Eike Ziller's avatar
Eike Ziller committed
18 19 20 21 22 23
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file.  Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
hjk's avatar
hjk committed
24 25 26
**
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
con's avatar
con committed
27 28
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
29
****************************************************************************/
30 31

#include "buildablehelperlibrary.h"
Friedemann Kleint's avatar
Friedemann Kleint committed
32 33
#include "hostosinfo.h"
#include "synchronousprocess.h"
34

35 36 37
#include <QDir>
#include <QDateTime>
#include <QDebug>
38 39 40

namespace Utils {

41 42 43 44 45 46 47
bool BuildableHelperLibrary::isQtChooser(const QFileInfo &info)
{
    return info.isSymLink() && info.symLinkTarget().endsWith(QLatin1String("/qtchooser"));
}

QString BuildableHelperLibrary::qtChooserToQmakePath(const QString &path)
{
48
    const char toolDir[] = "QTTOOLDIR=\"";
49 50 51 52 53 54 55
    QProcess proc;
    proc.start(path, QStringList(QLatin1String("-print-env")));
    if (!proc.waitForStarted(1000))
        return QString();
    if (!proc.waitForFinished(1000))
        return QString();
    QByteArray output = proc.readAllStandardOutput();
56
    int pos = output.indexOf(toolDir);
57 58
    if (pos == -1)
        return QString();
59
    pos += int(sizeof(toolDir)) - 1;
60 61 62 63 64 65 66 67
    int end = output.indexOf('\"', pos);
    if (end == -1)
        return QString();

    QString result = QString::fromLocal8Bit(output.mid(pos, end - pos)) + QLatin1String("/qmake");
    return result;
}

68 69 70 71 72 73 74 75 76 77 78
static bool isQmake(const QString &path)
{
    if (path.isEmpty())
        return false;
    QFileInfo fi(path);
    if (BuildableHelperLibrary::isQtChooser(fi))
        fi.setFile(BuildableHelperLibrary::qtChooserToQmakePath(fi.symLinkTarget()));

    return !BuildableHelperLibrary::qtVersionForQMake(fi.absoluteFilePath()).isEmpty();
}

79
FileName BuildableHelperLibrary::findSystemQt(const Environment &env)
80
{
81
    const QString qmake = QLatin1String("qmake");
82 83
    QStringList paths = env.path();
    foreach (const QString &path, paths) {
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
        if (path.isEmpty())
            continue;

        QDir dir(path);

        if (dir.exists(qmake)) {
            const QString qmakePath = dir.absoluteFilePath(qmake);
            if (isQmake(qmakePath))
                return FileName::fromString(qmakePath);
        }

        // Prefer qmake-qt5 to qmake-qt4 by sorting the filenames in reverse order.
        foreach (const QFileInfo &fi, dir.entryInfoList(possibleQMakeCommands(), QDir::Files, QDir::Name | QDir::Reversed)) {
            if (fi.fileName() == qmake)
                continue;
99

100 101
            if (isQmake(fi.absoluteFilePath()))
                return FileName(fi);
102 103
        }
    }
104
    return FileName();
105 106 107 108
}

QString BuildableHelperLibrary::qtVersionForQMake(const QString &qmakePath)
{
109
    if (qmakePath.isEmpty())
110 111 112 113 114 115 116 117 118
        return QString();

    QProcess qmake;
    qmake.start(qmakePath, QStringList(QLatin1String("--version")));
    if (!qmake.waitForStarted()) {
        qWarning("Cannot start '%s': %s", qPrintable(qmakePath), qPrintable(qmake.errorString()));
        return QString();
    }
    if (!qmake.waitForFinished())      {
119
        SynchronousProcess::stopProcess(qmake);
120 121 122 123 124 125 126
        qWarning("Timeout running '%s'.", qPrintable(qmakePath));
        return QString();
    }
    if (qmake.exitStatus() != QProcess::NormalExit) {
        qWarning("'%s' crashed.", qPrintable(qmakePath));
        return QString();
    }
127

128
    const QString output = QString::fromLocal8Bit(qmake.readAllStandardOutput());
129 130
    static QRegExp regexp(QLatin1String("(QMake version|QMake version:)[\\s]*([\\d.]*)"),
                          Qt::CaseInsensitive);
131
    regexp.indexIn(output);
Tobias Hunger's avatar
Tobias Hunger committed
132 133 134
    const QString qmakeVersion = regexp.cap(2);
    if (qmakeVersion.startsWith(QLatin1String("2."))
            || qmakeVersion.startsWith(QLatin1String("3."))) {
135 136
        static QRegExp regexp2(QLatin1String("Using Qt version[\\s]*([\\d\\.]*)"),
                               Qt::CaseInsensitive);
137 138 139 140 141 142 143 144 145
        regexp2.indexIn(output);
        const QString version = regexp2.cap(1);
        return version;
    }
    return QString();
}

QStringList BuildableHelperLibrary::possibleQMakeCommands()
{
146 147 148 149 150
    // On Windows it is always "qmake.exe"
    // On Unix some distributions renamed qmake with a postfix to avoid clashes
    // On OS X, Qt 4 binary packages also has renamed qmake. There are also symbolic links that are
    // named "qmake", but the file dialog always checks against resolved links (native Cocoa issue)
    return QStringList(QLatin1String("qmake*"));
151 152 153 154 155 156 157 158
}

// Copy helper source files to a target directory, replacing older files.
bool BuildableHelperLibrary::copyFiles(const QString &sourcePath,
                                     const QStringList &files,
                                     const QString &targetDirectory,
                                     QString *errorMessage)
{
159
    // try remove the directory
160
    if (!FileUtils::removeRecursively(FileName::fromString(targetDirectory), errorMessage))
161
        return false;
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
    if (!QDir().mkpath(targetDirectory)) {
        *errorMessage = QCoreApplication::translate("ProjectExplorer::DebuggingHelperLibrary", "The target directory %1 could not be created.").arg(targetDirectory);
        return false;
    }
    foreach (const QString &file, files) {
        const QString source = sourcePath + file;
        const QString dest = targetDirectory + file;
        const QFileInfo destInfo(dest);
        if (destInfo.exists()) {
            if (destInfo.lastModified() >= QFileInfo(source).lastModified())
                continue;
            if (!QFile::remove(dest)) {
                *errorMessage = QCoreApplication::translate("ProjectExplorer::DebuggingHelperLibrary", "The existing file %1 could not be removed.").arg(destInfo.absoluteFilePath());
                return false;
            }
        }
178
        if (!destInfo.dir().exists())
179 180
            QDir().mkpath(destInfo.dir().absolutePath());

181 182 183 184 185 186 187 188
        if (!QFile::copy(source, dest)) {
            *errorMessage = QCoreApplication::translate("ProjectExplorer::DebuggingHelperLibrary", "The file %1 could not be copied to %2.").arg(source, dest);
            return false;
        }
    }
    return true;
}

189 190
// Helper: Run a build process with merged stdout/stderr
static inline bool runBuildProcessI(QProcess &proc,
191
                                    const FileName &binary,
192 193 194 195 196
                                    const QStringList &args,
                                    int timeoutMS,
                                    bool ignoreNonNullExitCode,
                                    QString *output, QString *errorMessage)
{
197
    proc.start(binary.toString(), args);
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
    if (!proc.waitForStarted()) {
        *errorMessage = QCoreApplication::translate("ProjectExplorer::BuildableHelperLibrary",
                                                    "Cannot start process: %1").
                                                    arg(proc.errorString());
        return false;
    }
    // Read stdout/err and check for timeouts
    QByteArray stdOut;
    QByteArray stdErr;
    if (!SynchronousProcess::readDataFromProcess(proc, timeoutMS, &stdOut, &stdErr, false)) {
        *errorMessage = QCoreApplication::translate("ProjectExplorer::BuildableHelperLibrary",
                                                    "Timeout after %1s.").
                                                    arg(timeoutMS / 1000);
        SynchronousProcess::stopProcess(proc);
        return false;
    }
    if (proc.exitStatus() != QProcess::NormalExit) {
        *errorMessage = QCoreApplication::translate("ProjectExplorer::BuildableHelperLibrary",
                                                    "The process crashed.");
        return false;
    }
    const QString stdOutS = QString::fromLocal8Bit(stdOut);
    if (!ignoreNonNullExitCode && proc.exitCode() != 0) {
            *errorMessage = QCoreApplication::translate("ProjectExplorer::BuildableHelperLibrary",
                                                        "The process returned exit code %1:\n%2").
                                                         arg(proc.exitCode()).arg(stdOutS);
        return false;
    }
    output->append(stdOutS);
    return true;
}

// Run a build process with merged stdout/stderr and qWarn about errors.
static bool runBuildProcess(QProcess &proc,
232
                            const FileName &binary,
233 234 235 236 237 238 239 240
                            const QStringList &args,
                            int timeoutMS,
                            bool ignoreNonNullExitCode,
                            QString *output, QString *errorMessage)
{
    const bool rc = runBuildProcessI(proc, binary, args, timeoutMS, ignoreNonNullExitCode, output, errorMessage);
    if (!rc) {
        // Fail - reformat error.
241
        QString cmd = binary.toString();
242 243
        if (!args.isEmpty()) {
            cmd += QLatin1Char(' ');
hjk's avatar
hjk committed
244
            cmd += args.join(QLatin1Char(' '));
245 246 247
        }
        *errorMessage =
                QCoreApplication::translate("ProjectExplorer::BuildableHelperLibrary",
248
                                            "Error running \"%1\" in %2: %3").
249 250 251 252 253 254 255
                                            arg(cmd, proc.workingDirectory(), *errorMessage);
        qWarning("%s", qPrintable(*errorMessage));
    }
    return rc;
}


256 257
bool BuildableHelperLibrary::buildHelper(const BuildHelperArguments &arguments,
                                         QString *log, QString *errorMessage)
258 259 260 261
{
    const QChar newline = QLatin1Char('\n');
    // Setup process
    QProcess proc;
262 263
    proc.setEnvironment(arguments.environment.toStringList());
    proc.setWorkingDirectory(arguments.directory);
264 265
    proc.setProcessChannelMode(QProcess::MergedChannels);

266
    log->append(QCoreApplication::translate("ProjectExplorer::BuildableHelperLibrary",
267
                                          "Building helper \"%1\" in %2\n").arg(arguments.helperName,
268 269
                                                                              arguments.directory));
    log->append(newline);
270

271
    const FileName makeFullPath = arguments.environment.searchInPath(arguments.makeCommand);
272
    if (QFileInfo::exists(arguments.directory + QLatin1String("/Makefile"))) {
273 274
        if (makeFullPath.isEmpty()) {
            *errorMessage = QCoreApplication::translate("ProjectExplorer::DebuggingHelperLibrary",
275
                                                       "%1 not found in PATH\n").arg(arguments.makeCommand);
276
            return false;
277
        }
278
        const QString cleanTarget = QLatin1String("distclean");
279
        log->append(QCoreApplication::translate("ProjectExplorer::BuildableHelperLibrary",
280 281
                                                   "Running %1 %2...\n")
                    .arg(makeFullPath.toUserOutput(), cleanTarget));
282
        if (!runBuildProcess(proc, makeFullPath, QStringList(cleanTarget), 30000, true, log, errorMessage))
283
            return false;
284
    }
285
    QStringList qmakeArgs;
286 287 288
    if (!arguments.targetMode.isEmpty())
        qmakeArgs << arguments.targetMode;
    if (!arguments.mkspec.isEmpty())
289
        qmakeArgs << QLatin1String("-spec") << arguments.mkspec.toUserOutput();
290 291
    qmakeArgs << arguments.proFilename;
    qmakeArgs << arguments.qmakeArguments;
Kai Koehne's avatar
Kai Koehne committed
292

293 294
    log->append(newline);
    log->append(QCoreApplication::translate("ProjectExplorer::BuildableHelperLibrary",
295
                                            "Running %1 %2 ...\n").arg(arguments.qmakeCommand.toUserOutput(),
hjk's avatar
hjk committed
296
                                                                       qmakeArgs.join(QLatin1Char(' '))));
Kai Koehne's avatar
Kai Koehne committed
297

298
    if (!runBuildProcess(proc, arguments.qmakeCommand, qmakeArgs, 30000, false, log, errorMessage))
299
        return false;
300
    log->append(newline);
301
    if (makeFullPath.isEmpty()) {
302 303
        *errorMessage = QCoreApplication::translate("ProjectExplorer::BuildableHelperLibrary",
                                                "%1 not found in PATH\n").arg(arguments.makeCommand);
304
        return false;
305
    }
306
    log->append(QCoreApplication::translate("ProjectExplorer::BuildableHelperLibrary",
307
                                            "Running %1 %2 ...\n")
hjk's avatar
hjk committed
308
                .arg(makeFullPath.toUserOutput(), arguments.makeArguments.join(QLatin1Char(' '))));
309
    if (!runBuildProcess(proc, makeFullPath, arguments.makeArguments, 120000, false, log, errorMessage))
310 311
        return false;
    return true;
312 313 314 315 316 317 318 319
}

bool BuildableHelperLibrary::getHelperFileInfoFor(const QStringList &validBinaryFilenames,
                                                  const QString &directory, QFileInfo* info)
{
    if (!info)
        return false;

320
    foreach (const QString &binaryFilename, validBinaryFilenames) {
321 322 323 324 325 326 327 328
        info->setFile(directory + binaryFilename);
        if (info->exists())
            return true;
    }

    return false;
}

329 330
QString BuildableHelperLibrary::byInstallDataHelper(const QString &sourcePath,
                                                    const QStringList &sourceFileNames,
331
                                                    const QStringList &installDirectories,
332 333
                                                    const QStringList &validBinaryFilenames,
                                                    bool acceptOutdatedHelper)
334
{
335 336
    // find the latest change to the sources
    QDateTime sourcesModified;
337 338 339 340 341 342
    if (!acceptOutdatedHelper) {
        foreach (const QString &sourceFileName, sourceFileNames) {
            const QDateTime fileModified = QFileInfo(sourcePath + sourceFileName).lastModified();
            if (fileModified.isValid() && (!sourcesModified.isValid() || fileModified > sourcesModified))
                sourcesModified = fileModified;
        }
343 344
    }

hjk's avatar
hjk committed
345 346 347 348
    // We pretend that the lastmodified of dumper.cpp is 5 minutes before what
    // the file system says because afer a installation from the package the
    // modified dates of dumper.cpp and the actual library are close to each
    // other, but not deterministic in one direction.
349 350
    if (sourcesModified.isValid())
        sourcesModified = sourcesModified.addSecs(-300);
351 352 353 354 355

    // look for the newest helper library in the different locations
    QString newestHelper;
    QDateTime newestHelperModified = sourcesModified; // prevent using one that's older than the sources
    QFileInfo fileInfo;
356
    foreach (const QString &installDirectory, installDirectories) {
357
        if (getHelperFileInfoFor(validBinaryFilenames, installDirectory, &fileInfo)) {
358 359
            if (!newestHelperModified.isValid()
                    || (fileInfo.lastModified() > newestHelperModified)) {
360 361 362 363 364 365 366 367 368
                newestHelper = fileInfo.filePath();
                newestHelperModified = fileInfo.lastModified();
            }
        }
    }
    return newestHelper;
}

} // namespace Utils