makefileresolver.cpp 24.4 KB
Newer Older
Kris Wong's avatar
Kris Wong committed
1
2
3
/*
* KDevelop C++ Language Support
*
4
* Copyright 2007 David Nolden <david.nolden.kdevelop@art-master.de>
5
* Copyright 2014 Kevin Funk <kfunk@kde.org>
Kris Wong's avatar
Kris Wong committed
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

23
#include "makefileresolver.h"
Kris Wong's avatar
Kris Wong committed
24

Kevin Funk's avatar
Kevin Funk committed
25
26
#include "helper.h"

Kris Wong's avatar
Kris Wong committed
27
#include <memory>
28
29
30
#include <cstdio>
#include <iostream>

Kris Wong's avatar
Kris Wong committed
31
#include <QDir>
32
#include <QFileInfo>
33
#include <QRegularExpression>
Kris Wong's avatar
Kris Wong committed
34
35
36
#include <QRegExp>

#include <kprocess.h>
37
#include <KLocalizedString>
Kris Wong's avatar
Kris Wong committed
38

39
#include <serialization/indexedstring.h>
40
#include <util/pushvalue.h>
Milian Wolff's avatar
Milian Wolff committed
41
#include <util/path.h>
Kris Wong's avatar
Kris Wong committed
42

43
// #define VERBOSE
44

45
#if defined(VERBOSE)
Kris Wong's avatar
Kris Wong committed
46
47
48
49
50
#define ifTest(x) x
#else
#define ifTest(x)
#endif

51
52
const int maximumInternalResolutionDepth = 3;

53
using namespace std;
Milian Wolff's avatar
Milian Wolff committed
54
using namespace KDevelop;
Kris Wong's avatar
Kris Wong committed
55

56
namespace {
57
58
  ///After how many seconds should we retry?
  static const int CACHE_FAIL_FOR_SECONDS = 200;
59

60
  static const int processTimeoutSeconds = 30;
61

62
63
64
  struct CacheEntry
  {
    CacheEntry()
Milian Wolff's avatar
Milian Wolff committed
65
      : failed(false)
66
    { }
Milian Wolff's avatar
Milian Wolff committed
67
    ModificationRevisionSet modificationTime;
68
    Path::List paths;
69
    QHash<QString, QString> defines;
70
71
72
73
74
75
    QString errorMessage, longErrorMessage;
    bool failed;
    QMap<QString,bool> failedFiles;
    QDateTime failTime;
  };
  typedef QMap<QString, CacheEntry> Cache;
76

77
78
  static Cache s_cache;
  static QMutex s_cacheMutex;
79
}
80

Kris Wong's avatar
Kris Wong committed
81
  ///Helper-class used to fake file-modification times
Milian Wolff's avatar
Milian Wolff committed
82
  class FileModificationTimeWrapper
83
84
85
  {
  public:
    ///@param files list of files that should be fake-modified(modtime will be set to current time)
Milian Wolff's avatar
Milian Wolff committed
86
    explicit FileModificationTimeWrapper(const QStringList& files = QStringList(), const QString& workingDirectory = QString())
Kevin Funk's avatar
Kevin Funk committed
87
      : m_newTime(QDateTime::currentDateTime())
88
    {
Milian Wolff's avatar
Milian Wolff committed
89
90
      for (QStringList::const_iterator it = files.constBegin(); it != files.constEnd(); ++it) {
        ifTest(cout << "touching " << it->toUtf8().constData() << endl);
91

David Nolden's avatar
David Nolden committed
92
        QFileInfo fileinfo(QDir(workingDirectory), *it);
93
94
95
96
97
        if (!fileinfo.exists()) {
          cout << "File does not exist: " << it->toUtf8().constData()
               << "in working dir " << QDir::currentPath().toUtf8().constData() << "\n";
          continue;
        }
Milian Wolff's avatar
Milian Wolff committed
98

Kevin Funk's avatar
Kevin Funk committed
99
100
        const QString filename = fileinfo.canonicalFilePath();
        if (m_stat.contains(filename)) {
101
102
103
          cout << "Duplicate file: " << filename.toUtf8().constData() << endl;
          continue;
        }
Milian Wolff's avatar
Milian Wolff committed
104

Kevin Funk's avatar
Kevin Funk committed
105
106
        QFileInfo info(filename);
        if (info.exists()) {
107
          ///Success
Kevin Funk's avatar
Kevin Funk committed
108
          m_stat[filename] = info.lastModified();
109

Kevin Funk's avatar
Kevin Funk committed
110
111
          ///change the modification-time to m_newTime
          if (Helper::changeAccessAndModificationTime(filename, m_newTime, m_newTime) != 0) {
Milian Wolff's avatar
Milian Wolff committed
112
            ifTest(cout << "failed to touch " << it->toUtf8().constData() << endl);
Kris Wong's avatar
Kris Wong committed
113
114
115
          }
        }
      }
116
    }
Kris Wong's avatar
Kris Wong committed
117

118
    ///Undo changed modification-times
Milian Wolff's avatar
Milian Wolff committed
119
120
    void unModify()
    {
Kevin Funk's avatar
Kevin Funk committed
121
      for (auto it = m_stat.constBegin(); it != m_stat.constEnd(); ++it) {
Milian Wolff's avatar
Milian Wolff committed
122
123
        ifTest(cout << "untouching " << it.key().toUtf8().constData() << endl);

Kevin Funk's avatar
Kevin Funk committed
124
125
126
        QFileInfo info(it.key());
        if (info.exists()) {
          if (info.lastModified() == m_newTime) {
127
            ///Still the modtime that we've set, change it back
Kevin Funk's avatar
Kevin Funk committed
128
            if (Helper::changeAccessAndModificationTime(it.key(), info.lastRead(), *it) != 0) {
129
              perror("Resetting modification time");
Milian Wolff's avatar
Milian Wolff committed
130
              ifTest(cout << "failed to untouch " << it.key().toUtf8().constData() << endl);
Kris Wong's avatar
Kris Wong committed
131
            }
132
133
          } else {
            ///The file was modified since we changed the modtime
Milian Wolff's avatar
Milian Wolff committed
134
            ifTest(cout << "will not untouch " << it.key().toUtf8().constData() << " because the modification-time has changed" << endl);
Kris Wong's avatar
Kris Wong committed
135
          }
136
137
        } else {
          perror("File status");
Kris Wong's avatar
Kris Wong committed
138
        }
Pino Toscano's avatar
Pino Toscano committed
139
      }
140
    }
Kris Wong's avatar
Kris Wong committed
141

Milian Wolff's avatar
Milian Wolff committed
142
143
    ~FileModificationTimeWrapper()
    {
144
145
      unModify();
    }
Kris Wong's avatar
Kris Wong committed
146
147

  private:
Kevin Funk's avatar
Kevin Funk committed
148
149
    QHash<QString, QDateTime>  m_stat;
    QDateTime                  m_newTime;
Kris Wong's avatar
Kris Wong committed
150
151
  };

152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
  /**
   * Compatibility:
   * make/automake: Should work perfectly
   * cmake: Thanks to the path-recursion, this works with cmake(tested with version "2.4-patch 6"
   *        with kdelibs out-of-source and with kdevelop4 in-source)
   *
   * unsermake:
   *   unsermake is detected by reading the first line of the makefile. If it contains
   *   "generated by unsermake" the following things are respected:
   *   1. Since unsermake does not have the -W command (which should tell it to recompile
   *      the given file no matter whether it has been changed or not), the file-modification-time of
   *      the file is changed temporarily and the --no-real-compare option is used to force recompilation.
   *   2. The targets seem to be called *.lo instead of *.o when using unsermake, so *.lo names are used.
   *   example-(test)command: unsermake --no-real-compare -n myfile.lo
   **/
Milian Wolff's avatar
Milian Wolff committed
167
168
169
170
171
172
173
174
175
  class SourcePathInformation
  {
  public:
    SourcePathInformation(const QString& path)
      : m_path(path)
      , m_isUnsermake(false)
      , m_shouldTouchFiles(false)
    {
      m_isUnsermake = isUnsermakePrivate(path);
Kris Wong's avatar
Kris Wong committed
176

Milian Wolff's avatar
Milian Wolff committed
177
      ifTest(if (m_isUnsermake) cout << "unsermake detected" << endl);
Kris Wong's avatar
Kris Wong committed
178
179
    }

Milian Wolff's avatar
Milian Wolff committed
180
181
    bool isUnsermake() const
    {
Kris Wong's avatar
Kris Wong committed
182
183
184
185
      return m_isUnsermake;
    }

    ///When this is set, the file-modification times are changed no matter whether it is unsermake or make
Milian Wolff's avatar
Milian Wolff committed
186
187
    void setShouldTouchFiles(bool b)
    {
Kris Wong's avatar
Kris Wong committed
188
189
190
      m_shouldTouchFiles = b;
    }

Milian Wolff's avatar
Milian Wolff committed
191
192
193
    QString getCommand(const QString& absoluteFile, const QString& workingDirectory, const QString& makeParameters) const
    {
      if (isUnsermake()) {
Kris Wong's avatar
Kris Wong committed
194
        return "unsermake -k --no-real-compare -n " + makeParameters;
195
      } else {
Milian Wolff's avatar
Milian Wolff committed
196
        QString relativeFile = Path(workingDirectory).relativePath(Path(absoluteFile));
197
198
        return "make -k --no-print-directory -W \'" + absoluteFile + "\' -W \'" + relativeFile + "\' -n " + makeParameters;
      }
Kris Wong's avatar
Kris Wong committed
199
200
    }

Milian Wolff's avatar
Milian Wolff committed
201
202
203
    bool hasMakefile() const
    {
        QFileInfo makeFile(m_path, "Makefile");
Kris Wong's avatar
Kris Wong committed
204
205
206
        return makeFile.exists();
    }

Milian Wolff's avatar
Milian Wolff committed
207
208
    bool shouldTouchFiles() const
    {
Kris Wong's avatar
Kris Wong committed
209
210
211
      return isUnsermake() || m_shouldTouchFiles;
    }

Milian Wolff's avatar
Milian Wolff committed
212
213
    QStringList possibleTargets(const QString& targetBaseName) const
    {
Kris Wong's avatar
Kris Wong committed
214
      QStringList ret;
215
      ///@todo open the make-file, and read the target-names from there.
Milian Wolff's avatar
Milian Wolff committed
216
      if (isUnsermake()) {
Kris Wong's avatar
Kris Wong committed
217
218
219
220
221
222
223
224
225
        //unsermake breaks if the first given target does not exist, so in worst-case 2 calls are necessary
        ret << targetBaseName + ".lo";
        ret << targetBaseName + ".o";
      } else {
        //It would be nice if both targets could be processed in one call, the problem is the exit-status of make, so for now make has to be called twice.
        ret << targetBaseName + ".o";
        ret << targetBaseName + ".lo";
        //ret << targetBaseName + ".lo " + targetBaseName + ".o";
      }
226
      ret << targetBaseName + ".ko";
Kris Wong's avatar
Kris Wong committed
227
228
229
      return ret;
    }

Milian Wolff's avatar
Milian Wolff committed
230
231
232
  private:
      bool isUnsermakePrivate(const QString& path)
      {
Kris Wong's avatar
Kris Wong committed
233
        bool ret = false;
Milian Wolff's avatar
Milian Wolff committed
234
235
236
237
238
        QFileInfo makeFile(path, "Makefile");
        QFile f(makeFile.absoluteFilePath());
        if (f.open(QIODevice::ReadOnly)) {
          QString firstLine = f.readLine(128);
          if (firstLine.indexOf("generated by unsermake") != -1) {
Kris Wong's avatar
Kris Wong committed
239
240
241
242
243
244
245
246
247
248
249
            ret = true;
          }
          f.close();
        }
        return ret;
      }

      QString m_path;
      bool m_isUnsermake;
      bool m_shouldTouchFiles;
  };
250

251
void PathResolutionResult::mergeWith(const PathResolutionResult& rhs)
252
{
253
    foreach(const Path& path, rhs.paths) {
254
255
256
257
        if(!paths.contains(path))
            paths.append(path);
    }
    includePathDependency += rhs.includePathDependency;
258
    defines.unite(rhs.defines);
259
260
261
262
263
264
265
266
267
268
269
270
}

PathResolutionResult::PathResolutionResult(bool success, const QString& errorMessage, const QString& longErrorMessage)
    : success(success)
    , errorMessage(errorMessage)
    , longErrorMessage(longErrorMessage)
{}

PathResolutionResult::operator bool() const
{
    return success;
}
Kris Wong's avatar
Kris Wong committed
271

272
ModificationRevisionSet MakeFileResolver::findIncludePathDependency(const QString& file)
Milian Wolff's avatar
Milian Wolff committed
273
{
274
275
  QString oldSourceDir = m_source;
  QString oldBuildDir = m_build;
Milian Wolff's avatar
Milian Wolff committed
276

Milian Wolff's avatar
Milian Wolff committed
277
  Path currentWd(mapToBuild(file));
Milian Wolff's avatar
Milian Wolff committed
278

279
  ModificationRevisionSet rev;
Milian Wolff's avatar
Milian Wolff committed
280
281
  while (currentWd.hasParent()) {
    currentWd = currentWd.parent();
282
    QString path = currentWd.toLocalFile();
Milian Wolff's avatar
Milian Wolff committed
283
284
285
286
    QFileInfo makeFile(QDir(path), "Makefile");
    if (makeFile.exists()) {
      IndexedString makeFileStr(makeFile.filePath());
      rev.addModificationRevision(makeFileStr, ModificationRevision::revisionForFile(makeFileStr));
287
288
289
      break;
    }
  }
Milian Wolff's avatar
Milian Wolff committed
290

291
  setOutOfSourceBuildSystem(oldSourceDir, oldBuildDir);
Milian Wolff's avatar
Milian Wolff committed
292

293
294
295
  return rev;
}

296
bool MakeFileResolver::executeCommand(const QString& command, const QString& workingDirectory, QString& result) const
Kris Wong's avatar
Kris Wong committed
297
{
Milian Wolff's avatar
Milian Wolff committed
298
299
  ifTest(cout << "executing " << command.toUtf8().constData() << endl);
  ifTest(cout << "in " << workingDirectory.toUtf8().constData() << endl);
Kris Wong's avatar
Kris Wong committed
300
301
302
303
304
305
306
307
308

  KProcess proc;
  proc.setWorkingDirectory(workingDirectory);
  proc.setOutputChannelMode(KProcess::MergedChannels);

  QStringList args(command.split(' '));
  QString prog = args.takeFirst();
  proc.setProgram(prog, args);

309
  int status = proc.execute(processTimeoutSeconds * 1000);
Kris Wong's avatar
Kris Wong committed
310
311
312
313
314
  result = proc.readAll();

  return status == 0;
}

315
MakeFileResolver::MakeFileResolver()
Milian Wolff's avatar
Milian Wolff committed
316
317
  : m_isResolving(false)
  , m_outOfSource(false)
318
319
320
{
}

Kris Wong's avatar
Kris Wong committed
321
///More efficient solution: Only do exactly one call for each directory. During that call, mark all source-files as changed, and make all targets for those files.
322
PathResolutionResult MakeFileResolver::resolveIncludePath(const QString& file)
Milian Wolff's avatar
Milian Wolff committed
323
{
324
325
326
327
  if (file.isEmpty()) {
    // for unit tests with temporary files
    return PathResolutionResult();
  }
328

Milian Wolff's avatar
Milian Wolff committed
329
330
  QFileInfo fi(file);
  return resolveIncludePath(fi.fileName(), fi.absolutePath());
Kris Wong's avatar
Kris Wong committed
331
332
}

Milian Wolff's avatar
Milian Wolff committed
333
QString MakeFileResolver::mapToBuild(const QString &path) const
Milian Wolff's avatar
Milian Wolff committed
334
{
Milian Wolff's avatar
Milian Wolff committed
335
  QString wd = QDir::cleanPath(path);
Milian Wolff's avatar
Milian Wolff committed
336
337
  if (m_outOfSource) {
    if (wd.startsWith(m_source) && !wd.startsWith(m_build)) {
338
        //Move the current working-directory out of source, into the build-system
Milian Wolff's avatar
Milian Wolff committed
339
        wd = QDir::cleanPath(m_build + '/' + wd.mid(m_source.length()));
340
341
      }
  }
Milian Wolff's avatar
Milian Wolff committed
342
  return wd;
343
344
}

345
void MakeFileResolver::clearCache()
Milian Wolff's avatar
Milian Wolff committed
346
347
348
{
  QMutexLocker l(&s_cacheMutex);
  s_cache.clear();
349
350
}

351
PathResolutionResult MakeFileResolver::resolveIncludePath(const QString& file, const QString& _workingDirectory, int maxStepsUp)
Milian Wolff's avatar
Milian Wolff committed
352
{
353
  //Prefer this result when returning a "fail". The include-paths of this result will always be added.
354
  PathResolutionResult resultOnFail;
Kris Wong's avatar
Kris Wong committed
355

Milian Wolff's avatar
Milian Wolff committed
356
357
  if (m_isResolving)
    return PathResolutionResult(false, i18n("Tried include path resolution while another resolution process was still running"));
Kris Wong's avatar
Kris Wong committed
358

359
360
  //Make the working-directory absolute
  QString workingDirectory = _workingDirectory;
Milian Wolff's avatar
Milian Wolff committed
361

Milian Wolff's avatar
Milian Wolff committed
362
363
  if (QFileInfo(workingDirectory).isRelative()) {
    QUrl u = QUrl::fromLocalFile(QDir::currentPath());
Milian Wolff's avatar
Milian Wolff committed
364
365

    if (workingDirectory == ".")
366
      workingDirectory = QString();
Milian Wolff's avatar
Milian Wolff committed
367
    else if (workingDirectory.startsWith("./"))
368
      workingDirectory = workingDirectory.mid(2);
Milian Wolff's avatar
Milian Wolff committed
369
370

    if (!workingDirectory.isEmpty())
Milian Wolff's avatar
Milian Wolff committed
371
372
      u = u.adjusted(QUrl::StripTrailingSlash);
      u.setPath(u.path() + '/' + workingDirectory);
373
    workingDirectory = u.toLocalFile();
374
375
376
  } else
    workingDirectory = _workingDirectory;

Milian Wolff's avatar
Milian Wolff committed
377
378
379
  ifTest(cout << "working-directory: " <<  workingDirectory.toLocal8Bit().data() << "  file: " << file.toLocal8Bit().data() << std::endl;)

  QDir sourceDir(workingDirectory);
Milian Wolff's avatar
Milian Wolff committed
380
  QDir dir = QDir(mapToBuild(sourceDir.absolutePath()));
Milian Wolff's avatar
Milian Wolff committed
381
382
383
384

  QFileInfo makeFile(dir, "Makefile");
  if (!makeFile.exists()) {
    if (maxStepsUp > 0) {
385
386
387
388
      //If there is no makefile in this directory, go one up and re-try from there
      QFileInfo fileName(file);
      QString localName = sourceDir.dirName();

Milian Wolff's avatar
Milian Wolff committed
389
      if (sourceDir.cdUp() && !fileName.isAbsolute()) {
390
391
        QString checkFor = localName + "/" + file;
        PathResolutionResult oneUp = resolveIncludePath(checkFor, sourceDir.path(), maxStepsUp-1);
Milian Wolff's avatar
Milian Wolff committed
392
        if (oneUp.success) {
393
          oneUp.mergeWith(resultOnFail);
394
          return oneUp;
395
        }
396
397
      }
    }
Milian Wolff's avatar
Milian Wolff committed
398
399

    if (!resultOnFail.errorMessage.isEmpty() || !resultOnFail.paths.isEmpty())
400
401
      return resultOnFail;
    else
Milian Wolff's avatar
Milian Wolff committed
402
      return PathResolutionResult(false, i18n("Makefile is missing in folder \"%1\"", dir.absolutePath()), i18n("Problem while trying to resolve include paths for %1", file));
403
404
  }

Milian Wolff's avatar
Milian Wolff committed
405
  PushValue<bool> e(m_isResolving, true);
Kris Wong's avatar
Kris Wong committed
406

407
  Path::List cachedPaths; //If the call doesn't succeed, use the cached not up-to-date version
408
  QHash<QString, QString> cachedDefines;
Milian Wolff's avatar
Milian Wolff committed
409
410
  ModificationRevisionSet dependency;
  dependency.addModificationRevision(IndexedString(makeFile.filePath()), ModificationRevision::revisionForFile(IndexedString(makeFile.filePath())));
411
  dependency += resultOnFail.includePathDependency;
Kris Wong's avatar
Kris Wong committed
412
413
  Cache::iterator it;
  {
Milian Wolff's avatar
Milian Wolff committed
414
415
416
    QMutexLocker l(&s_cacheMutex);
    it = s_cache.find(dir.path());
    if (it != s_cache.end()) {
417
418
419
420
      cachedPaths = it->paths;
      cachedDefines = it->defines;
      if (dependency == it->modificationTime) {
        if (!it->failed) {
Kris Wong's avatar
Kris Wong committed
421
422
          //We have a valid cached result
          PathResolutionResult ret(true);
423
424
425
          ret.paths = it->paths;
          ret.defines = it->defines;
          ret.mergeWith(resultOnFail);
Kris Wong's avatar
Kris Wong committed
426
427
          return ret;
        } else {
Milian Wolff's avatar
Milian Wolff committed
428
          //We have a cached failed result. We should use that for some time but then try again. Return the failed result if: (there were too many tries within this folder OR this file was already tried) AND The last tries have not expired yet
429
          if (/*(it->failedFiles.size() > 3 || it->failedFiles.find(file) != it->failedFiles.end()) &&*/ it->failTime.secsTo(QDateTime::currentDateTime()) < CACHE_FAIL_FOR_SECONDS) {
Kris Wong's avatar
Kris Wong committed
430
            PathResolutionResult ret(false); //Fake that the result is ok
431
432
433
434
435
            ret.errorMessage = i18n("Cached: %1", it->errorMessage);
            ret.longErrorMessage = it->longErrorMessage;
            ret.paths = it->paths;
            ret.defines = it->defines;
            ret.mergeWith(resultOnFail);
Kris Wong's avatar
Kris Wong committed
436
437
438
439
440
441
442
443
444
445
446
            return ret;
          } else {
            //Try getting a correct result again
          }
        }
      }
    }
  }

  ///STEP 1: Prepare paths
  QString targetName;
Milian Wolff's avatar
Milian Wolff committed
447
  QFileInfo fi(file);
Kris Wong's avatar
Kris Wong committed
448
449

  QString absoluteFile = file;
Milian Wolff's avatar
Milian Wolff committed
450
  if (fi.isRelative())
451
    absoluteFile = workingDirectory + '/' + file;
Milian Wolff's avatar
Milian Wolff committed
452
  absoluteFile = QDir::cleanPath(absoluteFile);
Kris Wong's avatar
Kris Wong committed
453
454

  int dot;
455
  if ((dot = file.lastIndexOf('.')) == -1) {
Milian Wolff's avatar
Milian Wolff committed
456
    if (!resultOnFail.errorMessage.isEmpty() || !resultOnFail.paths.isEmpty())
457
458
      return resultOnFail;
    else
Milian Wolff's avatar
Milian Wolff committed
459
      return PathResolutionResult(false, i18n("Filename %1 seems to be malformed", file));
460
  }
Kris Wong's avatar
Kris Wong committed
461

Milian Wolff's avatar
Milian Wolff committed
462
  targetName = file.left(dot);
Kris Wong's avatar
Kris Wong committed
463
464

  QString wd = dir.path();
Milian Wolff's avatar
Milian Wolff committed
465
466
  if (QFileInfo(wd).isRelative()) {
    wd = QDir::cleanPath(QDir::currentPath() + '/' + wd);
Kris Wong's avatar
Kris Wong committed
467
  }
468

Milian Wolff's avatar
Milian Wolff committed
469
  wd = mapToBuild(wd);
Kris Wong's avatar
Kris Wong committed
470

Milian Wolff's avatar
Milian Wolff committed
471
472
  SourcePathInformation source(wd);
  QStringList possibleTargets = source.possibleTargets(targetName);
Kris Wong's avatar
Kris Wong committed
473
474
475
476
477
478
479
480

  source.setShouldTouchFiles(true); //Think about whether this should be always enabled. I've enabled it for now so there's an even bigger chance that everything works.

  ///STEP 3: Try resolving the paths, by using once the absolute and once the relative file-path. Which kind is required differs from setup to setup.

  ///STEP 3.1: Try resolution using the absolute path
  PathResolutionResult res;
  //Try for each possible target
Milian Wolff's avatar
Milian Wolff committed
481
  res = resolveIncludePathInternal(absoluteFile, wd, possibleTargets.join(" "), source, maximumInternalResolutionDepth);
482
  if (!res) {
Milian Wolff's avatar
Milian Wolff committed
483
484
    ifTest(cout << "Try for absolute file " << absoluteFile.toLocal8Bit().data() << " and targets " << possibleTargets.join(", ").toLocal8Bit().data()
                 << " failed: " << res.longErrorMessage.toLocal8Bit().data() << endl;)
Kris Wong's avatar
Kris Wong committed
485
  }
Milian Wolff's avatar
Milian Wolff committed
486

487
  res.includePathDependency = dependency;
Kris Wong's avatar
Kris Wong committed
488

Milian Wolff's avatar
Milian Wolff committed
489
  if (res.paths.isEmpty())
Kris Wong's avatar
Kris Wong committed
490
      res.paths = cachedPaths; //We failed, maybe there is an old cached result, use that.
491
      res.defines = cachedDefines;
Kris Wong's avatar
Kris Wong committed
492
493

  {
Milian Wolff's avatar
Milian Wolff committed
494
495
496
    QMutexLocker l(&s_cacheMutex);
    if (it == s_cache.end())
      it = s_cache.insert(dir.path(), CacheEntry());
Kris Wong's avatar
Kris Wong committed
497
498
499

    CacheEntry& ce(*it);
    ce.paths = res.paths;
500
    ce.modificationTime = dependency;
Milian Wolff's avatar
Milian Wolff committed
501
502

    if (!res) {
Kris Wong's avatar
Kris Wong committed
503
504
505
506
507
508
509
510
511
512
513
      ce.failed = true;
      ce.errorMessage = res.errorMessage;
      ce.longErrorMessage = res.longErrorMessage;
      ce.failTime = QDateTime::currentDateTime();
      ce.failedFiles[file] = true;
    } else {
      ce.failed = false;
      ce.failedFiles.clear();
    }
  }

514

Milian Wolff's avatar
Milian Wolff committed
515
  if (!res && (!resultOnFail.errorMessage.isEmpty() || !resultOnFail.paths.isEmpty()))
516
517
    return resultOnFail;

Kris Wong's avatar
Kris Wong committed
518
519
520
  return res;
}

521
static QRegularExpression includeRegularExpression()
522
{
Kevin Funk's avatar
Kevin Funk committed
523
  static const QRegularExpression expression(
524
525
526
527
528
    "\\s(?:--include-dir=|-I\\s*|-isystem\\s+)("
    "\\'.*\\'|\\\".*\\\"" //Matches "hello", 'hello', 'hello"hallo"', etc.
    "|"
    "((?:\\\\.)?([\\S^\\\\]?))+" //Matches /usr/I\ am\ a\ strange\ path/include
    ")(?=\\s)"
Kevin Funk's avatar
Kevin Funk committed
529
  );
530
531
  Q_ASSERT(expression.isValid());
  return expression;
532
533
}

534
PathResolutionResult MakeFileResolver::resolveIncludePathInternal(const QString& file, const QString& workingDirectory,
Milian Wolff's avatar
Milian Wolff committed
535
                                                                      const QString& makeParameters, const SourcePathInformation& source,
536
                                                                      int maxDepth)
Milian Wolff's avatar
Milian Wolff committed
537
{
538
  --maxDepth;
Milian Wolff's avatar
Milian Wolff committed
539
  if (maxDepth < 0)
540
    return PathResolutionResult(false);
Milian Wolff's avatar
Milian Wolff committed
541

Kris Wong's avatar
Kris Wong committed
542
543
544
  QString processStdout;

  QStringList touchFiles;
Milian Wolff's avatar
Milian Wolff committed
545
  if (source.shouldTouchFiles()) {
Kris Wong's avatar
Kris Wong committed
546
    touchFiles << file;
547
  }
Kris Wong's avatar
Kris Wong committed
548

Milian Wolff's avatar
Milian Wolff committed
549
  FileModificationTimeWrapper touch(touchFiles, workingDirectory);
Kris Wong's avatar
Kris Wong committed
550
551

  QString fullOutput;
Milian Wolff's avatar
Milian Wolff committed
552
553
  executeCommand(source.getCommand(file, workingDirectory, makeParameters), workingDirectory, fullOutput);

554
555
556
557
  {
    QRegExp newLineRx("\\\\\\n");
    fullOutput.replace(newLineRx, "");
  }
Kris Wong's avatar
Kris Wong committed
558
559
560
  ///@todo collect multiple outputs at the same time for performance-reasons
  QString firstLine = fullOutput;
  int lineEnd;
Milian Wolff's avatar
Milian Wolff committed
561
562
  if ((lineEnd = fullOutput.indexOf('\n')) != -1)
    firstLine.truncate(lineEnd); //Only look at the first line of output
Kris Wong's avatar
Kris Wong committed
563
564
565

  /**
   * There's two possible cases this can currently handle.
Milian Wolff's avatar
Milian Wolff committed
566
   * 1.: gcc is called, with the parameters we are searching for (so we parse the parameters)
Kris Wong's avatar
Kris Wong committed
567
568
569
570
   * 2.: A recursive make is called, within another directory(so we follow the recursion and try again) "cd /foo/bar && make -f pi/pa/build.make pi/pa/po.o
   * */

  ///STEP 1: Test if it is a recursive make-call
Milian Wolff's avatar
Milian Wolff committed
571
  // Do not search for recursive make-calls if we already have include-paths available. Happens in kernel modules.
572
  if (!includeRegularExpression().match(fullOutput).hasMatch()) {
Milian Wolff's avatar
Milian Wolff committed
573
    QRegExp makeRx("\\bmake\\s");
574
    int offset = 0;
575
    while ((offset = makeRx.indexIn(firstLine, offset)) != -1) {
Milian Wolff's avatar
Milian Wolff committed
576
577
      QString prefix = firstLine.left(offset).trimmed();
      if (prefix.endsWith("&&") || prefix.endsWith(';') || prefix.isEmpty()) {
578
579
        QString newWorkingDirectory = workingDirectory;
        ///Extract the new working-directory
Milian Wolff's avatar
Milian Wolff committed
580
581
582
583
584
        if (!prefix.isEmpty()) {
          if (prefix.endsWith("&&"))
            prefix.truncate(prefix.length() - 2);
          else if (prefix.endsWith(';'))
            prefix.truncate(prefix.length() - 1);
585
586

          ///Now test if what we have as prefix is a simple "cd /foo/bar" call.
Milian Wolff's avatar
Milian Wolff committed
587

588
589
          //In cases like "cd /media/data/kdedev/4.0/build/kdevelop && cd /media/data/kdedev/4.0/build/kdevelop"
          //We use the second directory. For t hat reason we search for the last index of "cd "
Milian Wolff's avatar
Milian Wolff committed
590
591
592
          int cdIndex = prefix.lastIndexOf("cd ");
          if (cdIndex != -1) {
            newWorkingDirectory = prefix.right(prefix.length() - 3 - cdIndex).trimmed();
Milian Wolff's avatar
Milian Wolff committed
593
            if (QFileInfo(newWorkingDirectory).isRelative())
594
              newWorkingDirectory = workingDirectory + '/' + newWorkingDirectory;
Milian Wolff's avatar
Milian Wolff committed
595
            newWorkingDirectory = QDir::cleanPath(newWorkingDirectory);
596
          }
Kris Wong's avatar
Kris Wong committed
597
        }
Milian Wolff's avatar
Milian Wolff committed
598
599
600

        if (newWorkingDirectory == workingDirectory) {
          return PathResolutionResult(false, i18n("Failed to extract new working directory"), i18n("Output was: %1", fullOutput));
601
        }
Milian Wolff's avatar
Milian Wolff committed
602
603
604

        QFileInfo d(newWorkingDirectory);
        if (d.exists()) {
605
          ///The recursive working-directory exists.
Milian Wolff's avatar
Milian Wolff committed
606
607
          QString makeParams = firstLine.mid(offset+5);
          if (!makeParams.contains(';') && !makeParams.contains("&&")) {
608
609
610
            ///Looks like valid parameters
            ///Make the file-name absolute, so it can be referenced from any directory
            QString absoluteFile = file;
Milian Wolff's avatar
Milian Wolff committed
611
            if (QFileInfo(absoluteFile).isRelative())
612
              absoluteFile = workingDirectory +  '/' + file;
Milian Wolff's avatar
Milian Wolff committed
613
            Path absolutePath(absoluteFile);
614
            ///Try once with absolute, and if that fails with relative path of the file
Milian Wolff's avatar
Milian Wolff committed
615
            SourcePathInformation newSource(newWorkingDirectory);
Milian Wolff's avatar
Milian Wolff committed
616
            PathResolutionResult res = resolveIncludePathInternal(absolutePath.toLocalFile(), newWorkingDirectory, makeParams, newSource, maxDepth);
Milian Wolff's avatar
Milian Wolff committed
617
            if (res)
618
              return res;
Milian Wolff's avatar
Milian Wolff committed
619
620

            return resolveIncludePathInternal(Path(newWorkingDirectory).relativePath(absolutePath), newWorkingDirectory, makeParams , newSource, maxDepth);
621
          }else{
Milian Wolff's avatar
Milian Wolff committed
622
            return PathResolutionResult(false, i18n("Recursive make call failed"), i18n("The parameter string \"%1\" does not seem to be valid. Output was: %2.", makeParams, fullOutput));
623
624
          }
        } else {
Milian Wolff's avatar
Milian Wolff committed
625
          return PathResolutionResult(false, i18n("Recursive make call failed"), i18n("The directory \"%1\" does not exist. Output was: %2.", newWorkingDirectory, fullOutput));
Kris Wong's avatar
Kris Wong committed
626
        }
627

Kris Wong's avatar
Kris Wong committed
628
      } else {
Milian Wolff's avatar
Milian Wolff committed
629
        return PathResolutionResult(false, i18n("Malformed recursive make call"), i18n("Output was: %1", fullOutput));
Kris Wong's avatar
Kris Wong committed
630
631
      }

632
      ++offset;
Milian Wolff's avatar
Milian Wolff committed
633
      if (offset >= firstLine.length()) break;
Kris Wong's avatar
Kris Wong committed
634
635
636
    }
  }

637
  ///STEP 2: Search the output for include-paths
Kris Wong's avatar
Kris Wong committed
638

639
  PathResolutionResult ret = processOutput(fullOutput, workingDirectory);
640
641
642
643
644
645
646
  if (ret.paths.isEmpty())
    return PathResolutionResult(false, i18n("Could not extract include paths from make output"),
                                i18n("Folder: \"%1\"  Command: \"%2\"  Output: \"%3\"", workingDirectory,
                                     source.getCommand(file, workingDirectory, makeParameters), fullOutput));
  return ret;
}

647
648
static QRegularExpression defineRegularExpression()
{
Kevin Funk's avatar
Kevin Funk committed
649
  static const QRegularExpression pattern(
650
    "-D([^\\s=]+)(?:=(?:\"(.*?)(?<!\\\\)\"|([^\\s]*)))?"
Kevin Funk's avatar
Kevin Funk committed
651
  );
652
  Q_ASSERT(pattern.isValid());
653
654
655
  return pattern;
}

656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
static QString unescape(const QStringRef& input)
{
  QString output;
  output.reserve(input.length());
  bool isEscaped = false;
  for (auto it = input.data(), end = it + input.length(); it != end; ++it) {
    QChar c = *it;
    if (!isEscaped && c == '\\') {
      isEscaped = true;
    } else {
      output.append(c);
      isEscaped = false;
    }
  }
  return output;
}

673
PathResolutionResult MakeFileResolver::processOutput(const QString& fullOutput, const QString& workingDirectory) const
674
{
Milian Wolff's avatar
Milian Wolff committed
675
  PathResolutionResult ret(true);
Kris Wong's avatar
Kris Wong committed
676
  ret.longErrorMessage = fullOutput;
677
  ifTest(cout << "full output: " << qPrintable(fullOutput) << endl);
Kris Wong's avatar
Kris Wong committed
678

679
680
681
682
683
684
685
686
687
688
689
690
  {
    const auto& includeRx = includeRegularExpression();
    auto it = includeRx.globalMatch(fullOutput);
    while (it.hasNext()) {
      const auto match = it.next();
      QString path = match.captured(1);
      if (path.startsWith('"') || (path.startsWith('\'') && path.length() > 2)) {
        //probable a quoted path
        if (path.endsWith(path.left(1))) {
          //Quotation is ok, remove it
          path = path.mid(1, path.length() - 2);
        }
Kris Wong's avatar
Kris Wong committed
691
      }
692
693
694
695
      if (QFileInfo(path).isRelative())
        path = workingDirectory + '/' + path;

      ret.paths << Path(path);
Kris Wong's avatar
Kris Wong committed
696
    }
697
  }
Kris Wong's avatar
Kris Wong committed
698

699
700
701
702
703
  {
    const auto& defineRx = defineRegularExpression();
    auto it = defineRx.globalMatch(fullOutput);
    while (it.hasNext()) {
      const auto match = it.next();
704
705
706
707
708
      QString value;
      if (match.lastCapturedIndex() > 1) {
        value = unescape(match.capturedRef(match.lastCapturedIndex()));
      }
      ret.defines[match.captured(1)] = value;
709
    }
Kris Wong's avatar
Kris Wong committed
710
711
712
713
714
  }

  return ret;
}

715
void MakeFileResolver::resetOutOfSourceBuild()
Milian Wolff's avatar
Milian Wolff committed
716
{
717
718
719
  m_outOfSource = false;
}

720
void MakeFileResolver::setOutOfSourceBuildSystem(const QString& source, const QString& build)
Milian Wolff's avatar
Milian Wolff committed
721
722
723
724
725
{
  if (source == build) {
    resetOutOfSourceBuild();
    return;
  }
Kris Wong's avatar
Kris Wong committed
726
  m_outOfSource = true;
Milian Wolff's avatar
Milian Wolff committed
727
728
  m_source = QDir::cleanPath(source);
  m_build = QDir::cleanPath(m_build);
Kris Wong's avatar
Kris Wong committed
729
}