diff options
Diffstat (limited to 'languages/cpp/includepathresolver.cpp')
-rw-r--r-- | languages/cpp/includepathresolver.cpp | 577 |
1 files changed, 577 insertions, 0 deletions
diff --git a/languages/cpp/includepathresolver.cpp b/languages/cpp/includepathresolver.cpp new file mode 100644 index 00000000..117c7732 --- /dev/null +++ b/languages/cpp/includepathresolver.cpp @@ -0,0 +1,577 @@ +/*************************************************************************** + copyright : (C) 2007 by David Nolden + email : david.nolden.kdevelop@art-master.de +***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +/** Compatibility: + * make/automake: Should work perfectly + * cmake: Thanks to the path-recursion, this works with cmake(tested with version "2.4-patch 6" tested 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 + **/ + +#include <stdio.h> +#include <unistd.h> +#include <memory> +#include "kurl.h" /* defines KURL */ +#include "qdir.h" /* defines QDir */ +#include "qregexp.h" /* defines QRegExp */ +#include "klocale.h" /* defines [function] i18n */ +#include "blockingkprocess.h" /* defines BlockingKProcess */ +#include "includepathresolver.h" +#include <sys/stat.h> +#include <sys/time.h> +#include <time.h> +#include <stdlib.h> + +#ifdef TEST +#include "blockingkprocess.cpp" + +#include <iostream> +using namespace std; +#endif + +#ifndef TEST +#define ifTest(x) {} +#else +#define ifTest(x) x +#endif + +///After how many seconds should we retry? +#define CACHE_FAIL_FOR_SECONDS 200 + +using namespace CppTools; + + +namespace CppTools { + ///Helper-class used to fake file-modification times + class FileModificationTimeWrapper { + public: + ///@param files list of files that should be fake-modified(modtime will be set to current time) + FileModificationTimeWrapper( const QStringList& files = QStringList() ) : m_newTime( time(0) ) { + for( QStringList::const_iterator it = files.begin(); it != files.end(); ++it ) { + ifTest( cout << "touching " << (*it).ascii() << endl ); + struct stat s; + if( stat( (*it).local8Bit().data(), &s ) == 0 ) { + ///Success + m_stat[*it] = s; + ///change the modification-time to m_newTime + struct timeval times[2]; + times[0].tv_sec = m_newTime; + times[0].tv_usec = 0; + times[1].tv_sec = m_newTime; + times[1].tv_usec = 0; + + if( utimes( (*it).local8Bit().data(), times ) != 0 ) + { + ifTest( cout << "failed to touch " << (*it).ascii() << endl ); + } + } + } + } + + //Not used yet, might be used to return LD_PRELOAD=.. FAKE_MODIFIED=.. etc. later + QString commandPrefix() const { + return QString(); + } + + ///Undo changed modification-times + void unModify() { + for( StatMap::const_iterator it = m_stat.begin(); it != m_stat.end(); ++it ) { + + ifTest( cout << "untouching " << it.key().ascii() << endl ); + + struct stat s; + if( stat( it.key().local8Bit().data(), &s ) == 0 ) { + if( s.st_mtime == m_newTime ) { + ///Still the modtime that we've set, change it back + struct timeval times[2]; + times[0].tv_usec = 0; + times[0].tv_sec = s.st_atime; + times[1].tv_usec = 0; + times[1].tv_sec = (*it).st_mtime; + if( utimes( it.key().local8Bit().data(), times ) != 0 ) { + ifTest( cout << "failed to untouch " << it.key().ascii() << endl ); + } + } else { + ///The file was modified since we changed the modtime + ifTest( cout << " will not untouch " << it.key().ascii() << " because the modification-time has changed" << endl ); + } + } + } + }; + + ~FileModificationTimeWrapper() { + unModify(); + } + + private: + typedef QMap<QString, struct stat> StatMap; + StatMap m_stat; + time_t m_newTime; + }; + + class SourcePathInformation { + public: + SourcePathInformation( const QString& path ) : m_path( path ), m_isUnsermake(false), m_shouldTouchFiles(false) { + m_isUnsermake = isUnsermakePrivate( path ); + + ifTest( if( m_isUnsermake ) cout << "unsermake detected" << endl ); + } + + bool isUnsermake() const { + return m_isUnsermake; + } + + ///When this is set, the file-modification times are changed no matter whether it is unsermake or make + void setShouldTouchFiles(bool b) { + m_shouldTouchFiles = b; + } + + QString getCommand( const QString& sourceFile, const QString& makeParameters ) const { + if( isUnsermake() ) + return "unsermake -k --no-real-compare -n " + makeParameters; + else + return "make -k --no-print-directory -W \'" + sourceFile + "\' -n " + makeParameters; + } + + bool hasMakefile() const { + QFileInfo makeFile( m_path, "Makefile" ); + return makeFile.exists(); + } + + bool shouldTouchFiles() const { + return isUnsermake() || m_shouldTouchFiles; + } + + QStringList possibleTargets( const QString& targetBaseName ) const { + QStringList ret; + if( isUnsermake() ) { + //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"; + } + return ret; + } + + private: + bool isUnsermakePrivate( const QString& path ) { + bool ret = false; + QFileInfo makeFile( path, "Makefile" ); + QFile f( makeFile.absFilePath() ); + if( f.open( IO_ReadOnly ) ) { + QString firstLine; + f.readLine( firstLine, 1000 ); + if( firstLine.find( "generated by unsermake" ) != -1 ) { + ret = true; + } + f.close(); + } + return ret; + } + + QString m_path; + bool m_isUnsermake; + bool m_shouldTouchFiles; + }; + +}; + +bool IncludePathResolver::executeCommandPopen ( const QString& command, const QString& workingDirectory, QString& result ) const +{ + ifTest( cout << "executing " << command.ascii() << endl ); + + char* oldWd = getcwd(0,0); + chdir( workingDirectory.local8Bit() ); + + FILE* fp; + const int BUFSIZE = 2048; + char buf [BUFSIZE]; + + result = QString(); + + int status = 1; + if ((fp = popen(command.local8Bit(), "r")) != NULL) { + while (fgets(buf, sizeof (buf), fp)) + result += QString(buf); + + status = pclose(fp); + } + + if( oldWd ) { + chdir( oldWd ); + free( oldWd ); + } + return status == 0; +} + +IncludePathResolver::IncludePathResolver( bool continueEventLoop ) : m_isResolving(false), m_outOfSource(false), m_continueEventLoop(continueEventLoop) { +/* m_continueEventLoop = false; +#warning DEBUGGING TEST, REMOVE THIS*/ +} + +///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. +PathResolutionResult IncludePathResolver::resolveIncludePath( const QString& file ) { + QFileInfo fi( file ); + return resolveIncludePath( fi.fileName(), fi.dirPath(true) ); +} + +PathResolutionResult IncludePathResolver::resolveIncludePath( const QString& file, const QString& workingDirectory ) { + + struct Enabler { + bool& b; + Enabler( bool& bb ) : b(bb) { + b = true; + } + ~Enabler() { + b = false; + } + }; + + if( m_isResolving ) + return PathResolutionResult(false, i18n("tried include-path-resolution while another resolution-process was still running") ); + + Enabler e( m_isResolving ); + + ///STEP 1: CACHING + QDir dir( workingDirectory ); + dir = QDir( dir.absPath() ); + QFileInfo makeFile( dir, "Makefile" ); + if( !makeFile.exists() ) + return PathResolutionResult(false, i18n("Makefile is missing in folder \"%1\"").arg(dir.absPath()), i18n("problem while trying to resolve include-paths for %1").arg(file) ); + + QStringList cachedPath; //If the call doesn't succeed, use the cached not up-to-date version + QDateTime makeFileModification = makeFile.lastModified(); + Cache::iterator it = m_cache.find( dir.path() ); + if( it != m_cache.end() ) { + cachedPath = (*it).path; + if( makeFileModification == (*it).modificationTime ) { + if( !(*it).failed ) { + //We have a valid cached result + PathResolutionResult ret(true); + ret.path = (*it).path; + return ret; + } else { + //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 + if( /*((*it).failedFiles.size() > 3 || (*it).failedFiles.find( file ) != (*it).failedFiles.end()) &&*/ (*it).failTime.secsTo( QDateTime::currentDateTime() ) < CACHE_FAIL_FOR_SECONDS ) { + PathResolutionResult ret(false); //Fake that the result is ok + ret.errorMessage = i18n("Cached: ") + (*it).errorMessage; + ret.longErrorMessage = (*it).longErrorMessage; + ret.path = (*it).path; + return ret; + } else { + //Try getting a correct result again + } + } + } + } + + ///STEP 1: Prepare paths + QString targetName; + QFileInfo fi( file ); + + QString absoluteFile = file; + if( !file.startsWith("/") ) + absoluteFile = dir.path() + "/" + file; + KURL u( absoluteFile ); + u.cleanPath(); + absoluteFile = u.path(); + + int dot; + if( (dot = file.findRev( '.' )) == -1 ) + return PathResolutionResult( false, i18n( "Filename %1 seems to be malformed" ).arg(file) ); + + targetName = file.left( dot ); + + QString wd = dir.path(); + if( !wd.startsWith("/") ) { + wd = QDir::currentDirPath() + "/" + wd; + KURL u( wd ); + u.cleanPath(); + wd = u.path(); + } + if( m_outOfSource ) { + if( wd.startsWith( m_source ) ) { + //Move the current working-directory out of source, into the build-system + wd = m_build + "/" + wd.mid( m_source.length() ); + KURL u( wd ); + u.cleanPath(); + wd = u.path(); + } + } + + SourcePathInformation source( wd ); + QStringList possibleTargets = source.possibleTargets( targetName ); + + 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 + for( QStringList::const_iterator it = possibleTargets.begin(); it != possibleTargets.end(); ++it ) { + res = resolveIncludePathInternal( absoluteFile, wd, *it, source ); + if( res ) break; + } + if( res ) { + CacheEntry ce; + ce.errorMessage = res.errorMessage; + ce.longErrorMessage = res.longErrorMessage; + ce.modificationTime = makeFileModification; + ce.path = res.path; + m_cache[dir.path()] = ce; + + return res; + } + + + ///STEP 3.2: Try resolution using the relative path + QString relativeFile = KURL::relativePath(wd, absoluteFile); + for( QStringList::const_iterator it = possibleTargets.begin(); it != possibleTargets.end(); ++it ) { + res = resolveIncludePathInternal( relativeFile, wd, *it, source ); + if( res ) break; + } + + if( res.path.isEmpty() ) + res.path = cachedPath; //We failed, maybe there is an old cached result, use that. + + if( it == m_cache.end() ) + it = m_cache.insert( dir.path(), CacheEntry() ); + + CacheEntry& ce(*it); + ce.modificationTime = makeFileModification; + ce.path = res.path; + if( !res ) { + 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(); + } + + return res; +} + +PathResolutionResult IncludePathResolver::getFullOutput( const QString& command, const QString& workingDirectory, QString& output ) const{ + if( m_continueEventLoop ) { + BlockingKProcess proc; + proc.setWorkingDirectory( workingDirectory ); + proc.setUseShell( true ); + proc << command; + if ( !proc.start(KProcess::NotifyOnExit, KProcess::Stdout) ) { + return PathResolutionResult( false, i18n("Could not start the make-process") ); + } + + output = proc.stdOut(); + if( proc.exitStatus() != 0 ) + return PathResolutionResult( false, i18n("make-process finished with nonzero exit-status"), i18n("output: %1").arg( output ) ); + } else { + bool ret = executeCommandPopen(command, workingDirectory, output); + + if( !ret ) + return PathResolutionResult( false, i18n("make-process failed"), i18n("output: %1").arg( output ) ); + } + return PathResolutionResult(true); +} + +PathResolutionResult IncludePathResolver::resolveIncludePathInternal( const QString& file, const QString& workingDirectory, const QString& makeParameters, const SourcePathInformation& source ) { + + QString processStdout; + + QStringList touchFiles; + if( source.shouldTouchFiles() ) + touchFiles << file; + + FileModificationTimeWrapper touch( touchFiles ); + + QString fullOutput; + PathResolutionResult res = getFullOutput( source.getCommand( file, makeParameters ), workingDirectory, fullOutput ); + if( !res ) + return res; + + QRegExp newLineRx("\\\\\\n"); + fullOutput.replace( newLineRx, "" ); + ///@todo collect multiple outputs at the same time for performance-reasons + QString firstLine = fullOutput; + int lineEnd; + if( (lineEnd = fullOutput.find('\n')) != -1 ) + firstLine.truncate( lineEnd ); //Only look at the first line of output + + /** + * There's two possible cases this can currently handle. + * 1.: gcc is called, with the parameters we are searching for(so we parse the parameters) + * 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 + QRegExp makeRx( "\\bmake\\s" ); + int offset = 0; + while( (offset = makeRx.search( firstLine, offset )) != -1 ) + { + QString prefix = firstLine.left( offset ).stripWhiteSpace(); + if( prefix.endsWith( "&&") || prefix.endsWith( ";" ) || prefix.isEmpty() ) + { + QString newWorkingDirectory = workingDirectory; + ///Extract the new working-directory + if( !prefix.isEmpty() ) { + if( prefix.endsWith( "&&" ) ) + prefix.truncate( prefix.length() - 2 ); + else if( prefix.endsWith( ";" ) ) + prefix.truncate( prefix.length() - 1 ); + ///Now test if what we have as prefix is a simple "cd /foo/bar" call. + if( prefix.startsWith( "cd ") && !prefix.contains( ";") && !prefix.contains("&&") ) { + newWorkingDirectory = prefix.right( prefix.length() - 3 ).stripWhiteSpace(); + if( !newWorkingDirectory.startsWith("/") ) + newWorkingDirectory = workingDirectory + "/" + newWorkingDirectory; + KURL u( newWorkingDirectory ); + u.cleanPath(); + newWorkingDirectory = u.path(); + } + } + QFileInfo d( newWorkingDirectory ); + if( d.exists() ) { + ///The recursive working-directory exists. + QString makeParams = firstLine.mid( offset+5 ); + if( !makeParams.contains( ";" ) && !makeParams.contains( "&&" ) ) { + ///Looks like valid parameters + ///Make the file-name absolute, so it can be referenced from any directory + QString absoluteFile = file; + if( !absoluteFile.startsWith("/") ) + absoluteFile = workingDirectory + "/" + file; + KURL u( absoluteFile ); + u.cleanPath(); + ///Try once with absolute, and if that fails with relative path of the file + SourcePathInformation newSource( newWorkingDirectory ); + PathResolutionResult res = resolveIncludePathInternal( u.path(), newWorkingDirectory, makeParams, newSource ); + if( res ) + return res; + return resolveIncludePathInternal( KURL::relativePath(newWorkingDirectory,u.path()), newWorkingDirectory, makeParams , newSource ); + }else{ + return PathResolutionResult( false, i18n("Recursive make-call failed"), i18n("The parameter-string \"%1\" does not seem to be valid. Output was: %2").arg(makeParams).arg(fullOutput) ); + } + } else { + return PathResolutionResult( false, i18n("Recursive make-call failed"), i18n("The directory \"%1\" does not exist. Output was: %2").arg(newWorkingDirectory).arg(fullOutput) ); + } + + } else { + return PathResolutionResult( false, i18n("Recursive make-call malformed"), i18n("Output was: %2").arg(fullOutput) ); + } + + ++offset; + if( offset >= firstLine.length() ) break; + } + + ///STEP 2: Search the output for include-paths + QRegExp validRx( "\\b([cg]\\+\\+|gcc)" ); + if( validRx.search( fullOutput ) == -1 ) + return PathResolutionResult( false, i18n("Output seems not to be a valid gcc or g++ call"), i18n("Folder: \"%1\" Command: \"%2\" Output: \"%3\"").arg(workingDirectory).arg( source.getCommand(file, makeParameters) ).arg(fullOutput) ); + + PathResolutionResult ret( true ); + ret.longErrorMessage = fullOutput; + + QString includeParameterRx( "\\s(-I|--include-dir=|-I\\s)" ); + QString quotedRx( "(\\').*(\\')|(\\\").*(\\\")" ); //Matches "hello", 'hello', 'hello"hallo"', etc. + QString escapedPathRx( "(([^)(\"'\\s]*)(\\\\\\s)?)*" ); //Matches /usr/I\ am \ a\ strange\ path/include + + QRegExp includeRx( QString( "%1(%2|%3)(?=\\s)" ).arg( includeParameterRx ).arg( quotedRx ).arg( escapedPathRx ) ); + includeRx.setMinimal( true ); + includeRx.setCaseSensitive( true ); + offset = 0; + while( (offset = includeRx.search( fullOutput, offset )) != -1 ) { + offset += 1; ///The previous white space + int pathOffset = 2; + if( fullOutput[offset+1] == '-' ) { + ///Must be --include-dir=, with a length of 14 characters + pathOffset = 14; + } + if( fullOutput.length() <= offset + pathOffset ) + break; + + if( fullOutput[offset+pathOffset].isSpace() ) + pathOffset++; + + + + int start = offset + pathOffset; + int end = offset + includeRx.matchedLength(); + + QString path = fullOutput.mid( start, end-start ).stripWhiteSpace(); + 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 ); + } + } + if( !path.startsWith("/") ) + path = workingDirectory + (workingDirectory.endsWith("/") ? "" : "/") + path; + + KURL u( path ); + u.cleanPath(); + + ret.path << u.path(); + + offset = end-1; + } + + + return ret; +} + +void IncludePathResolver::setOutOfSourceBuildSystem( const QString& source, const QString& build ) { + m_outOfSource = true; + m_source = source; + m_build = build; +} + +#ifdef TEST +/** This can be used for testing and debugging the system. To compile it use + * gcc includepathresolver.cpp -I /usr/share/qt3/include -I /usr/include/kde -I ../../lib/util -DTEST -lkdecore -g -o includepathresolver + * */ + +int main(int argc, char **argv) { + QApplication app(argc,argv); + IncludePathResolver resolver; + if( argc < 3 ) { + cout << "params: 1. file-name, 2. working-directory [3. source-directory 4. build-directory]" << endl; + return 1; + } + if( argc >= 5 ) { + cout << "mapping " << argv[3] << " -> " << argv[4] << endl; + resolver.setOutOfSourceBuildSystem( argv[3], argv[4] ); + } + PathResolutionResult res = resolver.resolveIncludePath( argv[1], argv[2] ); + cout << "success: " << res.success << "\n"; + if( !res.success ) { + cout << "error-message: \n" << res.errorMessage << "\n"; + cout << "long error-message: \n" << res.longErrorMessage << "\n"; + } + cout << "path: \n" << res.path.join("\n"); + return res.success; +} + +#endif |