/***************************************************************************
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 <stdio.h>
# include <unistd.h>
# include <memory>
# 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 <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 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 < TQString , struct stat > 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