Wednesday, 9 May 2012

Qt way to Include all source files

About a year ago I got roped into becoming a Qt developer for its nice and easy support for the Symbian platform. Using Qt gave you the ability to create cross-platform desktop (and Symbian) applications using the same IDE.

The big pain has always been whenever I added a new directory to a project.

In xCode I could re-drag and drop the folder into the project and it create the make file based on the files it found.

In Eclipse for Android, you could just modify the makefile to include *.cpp and *.h files in certain subfolders.

In Qt however, you had to individually specify the files to be included.
There were some workarounds where you'd create a .pro file per subfolder and somehow link them. I found this approach too confusing and didn't want to bloat my project source folders for the other platforms I supported.

The solution I came up with was to move out all the source files into a sources.pri file which was to be included by the main project file. I'd then run a Java script I wrote which parsed through all the directories and write in the cpp, c, and h files found.

project.pro file

# CCRELEASE: Remove for release
debug:DEFINES += DEBUGON

QT              += opengl network webkit network script phonon
TARGET    = 2play
TEMPLATE   = app

OTHER_FILES += \
    ../../Engine/* \
    ../../Engine/Shaders/basic.fx \
    ../../Engine/Shaders/phong.fx

include( sources.pri )
...
The important part is the one in bold stating include(sources.pri)

sources.pri file
INCLUDEPATH += \ 
  \ 
 ../Source \ 
 ../Source/Rendering \ 
 ../Source/Tools \ 
 ../../Engine/Source \ 
 ../../Engine/Source/AI \ 
 ../../Engine/Source/Objects \ 
 ../../Engine/Source/Rendering \ 
 ../../Engine/Source/Scenes \ 
 ../../Engine/Source/Tools \ 
 ../../Engine/Source/UI \ 
 ../../External/3dsloader \ 
 ../../External/jansson-2.1/src \ 
 ../../External/ObjLoader3 \ 
 ../../App/Source \ 

SOURCES += \ 
  \ 
 ../Source/CCFBWebView.cpp \ 
 ../Source/CCGLView.cpp \ 
 ../Source/CCMainWindow.cpp \ 
 ../Source/CCPlatform.cpp \ 
 ../Source/CCRenderThread.cpp \ 
 ../Source/CCVideoView.cpp \ 
 ../Source/main.cpp \ 
  \ 
 ../Source/Rendering/CCDeviceRenderer.cpp \ 
 ../Source/Rendering/CCTexturePNG.cpp \ 
  \ 
 ../Source/Tools/CCDeviceControls.cpp \ 
 ../Source/Tools/CCDeviceFileManager.cpp \ 
...

HEADERS += \ 
  \ 
 ../Source/CCFBWebView.h \ 
 ../Source/CCGLView.h \ 
 ../Source/CCMainWindow.h \ 
 ../Source/CCPlatform.h \ 
 ../Source/CCRenderThread.h \ 
 ../Source/CCVideoView.h \ 
  \ 
 ../Source/Rendering/CCDeviceRenderer.h \ 
 ../Source/Rendering/CCTexturePNG.h \ 
  \ 
 ../Source/Tools/CCDeviceControls.h \ 
 ../Source/Tools/CCDeviceFileManager.h \ 
...

Now the Java file simply loads in the sources.pri file, inputs in the folders listed in the INCLUDEPATH, then re-writes the file with all the source and header files.
import java.io.*;
import java.util.ArrayList;

import javax.swing.JOptionPane;

public class QtMaker 
{
 /**
  * @param args
  */
 public static void main(String[] args) 
 {
  String currentDirectory = System.getProperty( "user.dir" );
  String qtProjectDirectory = currentDirectory + "/";
  
  boolean findProjectDirectory = true;
  {
   // See if we're in a folder with a .pri file
   File dir = new File( qtProjectDirectory );
   String[] files = dir.list();
   for( int i=0; i<files.length; ++i )
   {
    String file = files[i];
    if( file.endsWith( ".pri" ) )
    {
     findProjectDirectory = false;
     break;
    }
   }
  }
  
  // Our project specific case when running the program from our Tools directory structure
  if( findProjectDirectory )
  {
   String[] splitResults = qtProjectDirectory.split( "/" );
   qtProjectDirectory = "";
   for( int i=0; i<splitResults.length; ++i )
   {
    String directory = splitResults[i];
    if( directory.equals( "Tools" ) )
    {
     break;
    }
    qtProjectDirectory += directory;
    qtProjectDirectory += "/";
   }
   
   qtProjectDirectory += "Dev/Qt/Project/";
  }
  
  // Find the first .pri file to play with
  String projectFilename = "sources.pri";
  {
   File dir = new File( qtProjectDirectory );
   String[] files = dir.list();
   for( int i=0; i<files.length; ++i )
   {
    String file = files[i];
    if( file.endsWith( ".pri" ) )
    {
     projectFilename = file;
     break;
    }
   }
  }
  
  File projectFile = new File( qtProjectDirectory + projectFilename );
  if( !projectFile.exists() )
  {
   JOptionPane.showMessageDialog( null, "Unable to find sources.pri\n" + qtProjectDirectory );
  }
  else
  {
   ArrayList<String> projectFileContents = new ArrayList<String>();
   try
   {
      FileInputStream fstream = new FileInputStream( projectFile.getPath() );
      DataInputStream in = new DataInputStream(fstream);
      BufferedReader br = new BufferedReader( new InputStreamReader( in ) );
      
      //Read File Line By Line
      String readLine;
      while( ( readLine = br.readLine() ) != null )
      {
       projectFileContents.add( readLine );
      }
      
      //Close the input stream
      in.close();   
   } 
   catch ( Exception e )
   {
    System.err.println( "Error: " + e.getMessage() ); 
   }
   
   if( projectFileContents.size() > 0 )
   {
    ArrayList<String> sourceDirectories = new ArrayList<String>();
    findSourceFilePaths( sourceDirectories, projectFileContents );
    
    ArrayList<String[]> sourceFiles = new ArrayList<String[]>(); 
    for( int i=0; i<sourceDirectories.size(); ++i )
    {
     String path = sourceDirectories.get( i );
     String directoryPath = qtProjectDirectory + path;
     File dir = new File( directoryPath );
     String[] files = dir.list();
     sourceFiles.add( files );
    }
    
    String outputString = "INCLUDEPATH += \\ \n";
    for( int i=0; i<sourceDirectories.size(); ++i )
    {
     String path = sourceDirectories.get( i );
     outputString += "\t" + path + " \\ \n";
    }
    
    outputString += "\n";
    
    outputString += "SOURCES\t+= \\ \n";
    for( int i=0; i<sourceFiles.size(); ++i )
    {
     String path = sourceDirectories.get( i );
     String[] files = sourceFiles.get( i );
     for( int j=0; j<files.length; ++j )
     {
      String file = files[j];
      if( file.endsWith( ".cpp" ) || file.endsWith( ".c" ) )
      {
       outputString += "\t" + path + "/" + file + " \\ \n";
      }
     }

     outputString += "\t \\ \n";
    }
    
    
    outputString += "\n";
    
    outputString += "HEADERS\t+= \\ \n";
    for( int i=0; i<sourceFiles.size(); ++i )
    {
     String path = sourceDirectories.get( i );
     String[] files = sourceFiles.get( i );
     for( int j=0; j<files.length; ++j )
     {
      String file = files[j];
      if( file.endsWith( ".h" ) )
      {
       outputString += "\t" + path + "/" + file + " \\ \n";
      }
     }

     outputString += "\t \\ \n";
    }
    
    try
    { 
     FileWriter fstream = new FileWriter( qtProjectDirectory + projectFilename );
     BufferedWriter out = new BufferedWriter( fstream );
     out.write( outputString );
     out.close();
    }
    catch ( Exception e )
    {
     System.err.println("Error: " + e.getMessage());
    }
    
    //JOptionPane.showMessageDialog( null, outputString );
   }
     }
 }

 static void findSourceFilePaths( ArrayList<String> sourceDirectories, ArrayList<String> projectFileContents )
 {
  int includePathStartIndex = 0;
  for( int i=0; i<projectFileContents.size(); ++i )
  {
   String line = projectFileContents.get( i );
   String[] splitResults = line.split( "INCLUDEPATH" );
   if( splitResults.length > 0 )
   {
    includePathStartIndex = i;
    break;
   }
  }
  
  for( int i=includePathStartIndex; i<projectFileContents.size(); ++i )
  {
   String line = projectFileContents.get( i );
   if( line.length() == 0 )
   {
    break;
   }
   
   if( line.equals( "\\" ) )
   {
    continue;
   }
   
   String[] splitResults = line.split( "\\\\" );
   if( splitResults.length == 0 )
   {
    break;
   }
   else
   {
    String directoryString = findDirectoryString( line );
    
    // Trim any invalid characters
    directoryString = directoryString.replaceAll( "\t", "" );
    
    // Make sure we don't add repeat folders
    boolean found = false;
    for( int j=0; j<sourceDirectories.size(); ++j )
    {
     String currentDirectory = sourceDirectories.get( j );
     if( currentDirectory.equals( directoryString ) )
     {
      found = true;
      break;
     }
    }
    
    if( found == false )
    {
     sourceDirectories.add( directoryString );
    }
   }
  }
  
  for( int i=0; i<sourceDirectories.size(); ++i )
  {
   String line = sourceDirectories.get( i );
   System.out.println( line );
  }
 }
 
 static String findDirectoryString(String line)
 {
  String[] splitResults = line.split( " " );

  int longestLength = 0;
  String longestString = "";
  for( int i=0; i<splitResults.length; ++i )
  {
   String result = splitResults[i];
   if( result.contains( "/" ) )
   {
    if( result.length() > longestLength )
    {
     longestLength = result.length();
     longestString = result;
    }
   }
  }
  
  return longestString;
 }
}

It's simple. It works. It saves me the pain of having to include files individually.

Hopefully it'll help someone out there too.