/*************************************************************************** copyright : (C) 2007 by David Nolden email : david.nolden.tdevelop@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 tdelibs out-of-source and with tdevelop4 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 #include #include #include "kurl.h" /* defines KURL */ #include "tqdir.h" /* defines TQDir */ #include "tqregexp.h" /* defines TQRegExp */ #include "klocale.h" /* defines [function] i18n */ #include "blockingkprocess.h" /* defines BlockingKProcess */ #include "includepathresolver.h" #include #include #include #include #ifdef TEST #include "blockingkprocess.cpp" #include 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 TQStringList& files = TQStringList() ) : m_newTime( time(0) ) { for( TQStringList::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 TQString commandPrefix() const { return TQString(); } ///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 TQMap StatMap; StatMap m_stat; time_t m_newTime; }; class SourcePathInformation { public: SourcePathInformation( const TQString& 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; } TQString getCommand( const TQString& sourceFile, const TQString& 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 { TQFileInfo makeFile( m_path, "Makefile" ); return makeFile.exists(); } bool shouldTouchFiles() const { return isUnsermake() || m_shouldTouchFiles; } TQStringList possibleTargets( const TQString& targetBaseName ) const { TQStringList 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 TQString& path ) { bool ret = false; TQFileInfo makeFile( path, "Makefile" ); TQFile f( makeFile.absFilePath() ); if( f.open( IO_ReadOnly ) ) { TQString firstLine; f.readLine( firstLine, 1000 ); if( firstLine.find( "generated by unsermake" ) != -1 ) { ret = true; } f.close(); } return ret; } TQString m_path; bool m_isUnsermake; bool m_shouldTouchFiles; }; }; bool IncludePathResolver::executeCommandPopen ( const TQString& command, const TQString& workingDirectory, TQString& 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 = TQString(); int status = 1; if ((fp = popen(command.local8Bit(), "r")) != NULL) { while (fgets(buf, sizeof (buf), fp)) result += TQString(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 TQString& file ) { TQFileInfo fi( file ); return resolveIncludePath( fi.fileName(), fi.dirPath(true) ); } PathResolutionResult IncludePathResolver::resolveIncludePath( const TQString& file, const TQString& 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 TQDir dir( workingDirectory ); dir = TQDir( dir.absPath() ); TQFileInfo makeFile( dir, "Makefile" ); if( !makeFile.exists() ) return PathResolutionResult(false, i18n("Makefile is missing in folder \"%1\"").tqarg(dir.absPath()), i18n("problem while trying to resolve include-paths for %1").tqarg(file) ); TQStringList cachedPath; //If the call doesn't succeed, use the cached not up-to-date version TQDateTime 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( TQDateTime::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 TQString targetName; TQFileInfo fi( file ); TQString 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" ).tqarg(file) ); targetName = file.left( dot ); TQString wd = dir.path(); if( !wd.startsWith("/") ) { wd = TQDir::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 ); TQStringList 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( TQStringList::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 TQString relativeFile = KURL::relativePath(wd, absoluteFile); for( TQStringList::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 = TQDateTime::currentDateTime(); ce.failedFiles[file] = true; } else { ce.failed = false; ce.failedFiles.clear(); } return res; } PathResolutionResult IncludePathResolver::getFullOutput( const TQString& command, const TQString& workingDirectory, TQString& 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").tqarg( output ) ); } else { bool ret = executeCommandPopen(command, workingDirectory, output); if( !ret ) return PathResolutionResult( false, i18n("make-process failed"), i18n("output: %1").tqarg( output ) ); } return PathResolutionResult(true); } PathResolutionResult IncludePathResolver::resolveIncludePathInternal( const TQString& file, const TQString& workingDirectory, const TQString& makeParameters, const SourcePathInformation& source ) { TQString processStdout; TQStringList touchFiles; if( source.shouldTouchFiles() ) touchFiles << file; FileModificationTimeWrapper touch( touchFiles ); TQString fullOutput; PathResolutionResult res = getFullOutput( source.getCommand( file, makeParameters ), workingDirectory, fullOutput ); if( !res ) return res; TQRegExp newLineRx("\\\\\\n"); fullOutput.replace( newLineRx, "" ); ///@todo collect multiple outputs at the same time for performance-reasons TQString 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 TQRegExp makeRx( "\\bmake\\s" ); int offset = 0; while( (offset = makeRx.search( firstLine, offset )) != -1 ) { TQString prefix = firstLine.left( offset ).stripWhiteSpace(); if( prefix.endsWith( "&&") || prefix.endsWith( ";" ) || prefix.isEmpty() ) { TQString 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(); } } TQFileInfo d( newWorkingDirectory ); if( d.exists() ) { ///The recursive working-directory exists. TQString 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 TQString 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").tqarg(makeParams).tqarg(fullOutput) ); } } else { return PathResolutionResult( false, i18n("Recursive make-call failed"), i18n("The directory \"%1\" does not exist. Output was: %2").tqarg(newWorkingDirectory).tqarg(fullOutput) ); } } else { return PathResolutionResult( false, i18n("Recursive make-call malformed"), i18n("Output was: %2").tqarg(fullOutput) ); } ++offset; if( offset >= firstLine.length() ) break; } ///STEP 2: Search the output for include-paths TQRegExp 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\"").tqarg(workingDirectory).tqarg( source.getCommand(file, makeParameters) ).tqarg(fullOutput) ); PathResolutionResult ret( true ); ret.longErrorMessage = fullOutput; TQString includeParameterRx( "\\s(-I|--include-dir=|-I\\s)" ); TQString quotedRx( "(\\').*(\\')|(\\\").*(\\\")" ); //Matches "hello", 'hello', 'hello"hallo"', etc. TQString escapedPathRx( "(([^)(\"'\\s]*)(\\\\\\s)?)*" ); //Matches /usr/I\ am \ a\ strange\ path/include TQRegExp includeRx( TQString( "%1(%2|%3)(?=\\s)" ).tqarg( includeParameterRx ).tqarg( quotedRx ).tqarg( 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(); TQString 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 TQString& source, const TQString& 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 -ltdecore -g -o includepathresolver * */ int main(int argc, char **argv) { TQApplication 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