A python script to create makefile

A Makefile Generator. This solution can be extended to other file generation as well e.x., testbench component generation

A Makefile Generator

Scenario

 In certain cases, we need for auto-generation of stuffs, be it files of testbench or other scripts etc., to reduce time and increase efficiency

Problem Statement

- Let's create a basic python script for generating a C/C++ based Makefile

Solution

- We will create a configuration file to store certain configurations related to the compilation of filetypes (say C and C++)

- Configuration file can be created in (.ini) format which will be subsequently read by "configparser" module of python

- After that, we will create a string object leveraging the all powerful yet simple "+=" or concatenation operator, which can then be written to a file to generate our desired file

Implementation

The configparser module from Python's standard library defines functionality for reading and writing configuration files as used by Microsoft Windows OS. Such files usually have .INI extension in Windows OS and can be of any extension in Unix OS

The INI file consists of sections, each led by a [section] header. Between square brackets, we can put the section’s name. Section is followed by key/value entries separated by = or : character. It may include comments, prefixed by # or ; symbol
# A sample Configuration file named makegen.cfg


[c] # c compiler section
compiler = gcc # items within section
flags = -g -Wall 
installPath = ~/bin/

[cpp] # c++ compiler section
compiler = g++ # items within section
flags = -Wall 
installPath = ~/bin/
- Now our script will be named as makegen.py which will take some input command line args as options
- When a script has some runtime options to it we generally have a -h or --help option which will print the runtime options the file can take and also their description
- For this we can make use of "optparse" module of python
- Our help message will be as follows:

Usage
Usage: makegen.py [ -bcdfihostvx ]

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -b BUILDDIR, --build-dir=BUILDDIR
                        set build directory for project to use. Default: .
  -c COMPILER, --compiler=COMPILER
                        set compiler to use. Default: gcc
  -d DIRECTORY, --directory=DIRECTORY
                        directory for makegen to create Makefile for. Default:
                        .
  -f FLAGS, --flags=FLAGS
                        flags for the compiler and typed within quotes
  -i INSTALLPATH, --install-dir=INSTALLPATH
                        directory for 'make install'. Default: /usr/local/bin
  -o OUTPUTFILE, --output-target=OUTPUTFILE
                        output file name from compiler. Default: a.out
  -s SRCDIR, --source-dir=SRCDIR
                        set source directory for project to use. Default: .
  -t FILETYPE, --file-type=FILETYPE
                        the file type of your source files (ex. c, cpp).
                        Default: c
  -v                    enable verbose output
  -x CONFIGFILE, --config-file=CONFIGFILE
                        path to makegen config file. Default: ~/makegen.cfg
#!/usr/bin/env python3

###########################
#       makegen.py        #
###########################

#Import the needed modules
import os
from optparse import OptionParser
from configparser import ConfigParser 

## Global variables which will be taken as default and can be accessed within functions
VERSION = "0.0.1"
flags = ""
outputFile = ""
directory = ""
compiler = ""
installPath = ""
builddir = ""
srcdir = ""
configFile = ""
fileType = ""
verbose = False

# Writing every part of code as functions and classes increases reusability and scalability and will refactor the code block

def debugPrint(*args): # Function to enable verbose output
    if verbose:
        for a in args:
            print((a), end=' ')
        print("")

def parseCommandline(): # Function to parse cmdline options and print utility help message
    global flags, outputFile, directory, compiler, installPath, builddir, srcdir, configFile, verbose, fileType
    # Get commandline arguments
    parser = OptionParser(usage="Usage: makegen.py [ -bcdfihostvx ]", version="makegen Version " + VERSION)
    parser.add_option("-b", "--build-dir", dest="builddir", help="set build directory for project to use. Default: .", default=".")
    parser.add_option("-c", "--compiler", dest="compiler", help="set compiler to use. Default: is gcc", default="")
    parser.add_option("-d", "--directory", dest="directory", help="directory for makegen to create Makefile for. Default: .", default=".")
    parser.add_option("-f", "--flags", dest="flags", help="flags for the compiler and typed within quotes", default="")
    parser.add_option("-i", "--install-dir", dest="installPath", help="directory for 'make install'. Default: /usr/local/bin", default="/usr/local/bin")
    parser.add_option("-o", "--output-target", dest="outputFile", help="output file name from compiler. Default: a.out", default="a.out")
    parser.add_option("-s", "--source-dir", dest="srcdir", help="set source directory for project to use. Default: .", default=".")
    parser.add_option("-t", "--file-type", dest="fileType", help="the file type of your source files (ex. c, cpp). Default: c", default = "c")
    parser.add_option("-v", dest="verbose", help="enable verbose output", action="store_true")
    parser.add_option("-x", "--config-file", dest="configFile", help="path to makegen config file. Default: ~/makegen.cfg", default="~/makegen.cfg")

    (options, args) = parser.parse_args() #parse_args() function returns as a tuple
    
    # Store parsed arguments as variables
    outputFile = options.outputFile
    flags = options.flags
    directory = options.directory
    compiler = options.compiler
    installPath = options.installPath
    builddir = options.builddir
    srcdir = options.srcdir
    configFile = options.configFile
    verbose = options.verbose
    fileType = options.fileType

# Function to read the config file "makegen.cfg" through configparser module
# We store the items of each section in global variables "flags", "compiler", "installPath"

def parseConfig(fileType):
    global flags, compiler, installPath
    conf = ConfigParser()
    f = conf.read(os.path.expanduser(configFile))
    if len(f) == 0:
        debugPrint("Unable to find config file at " + configFile)
        return False
    debugPrint("Config file found at " + configFile)
    confFlags = confCompiler = confInstallDir = ""
    try:
      confFlags = conf.get(fileType, "flags")
    except:
        pass
    try:
        confCompiler = conf.get(fileType, "compiler")
    except:
        pass
    try:
        confInstallDir = conf.get(fileType, "installPath")
    except:
        pass
    if len(confFlags) != 0 :
        flags = confFlags
    if len(confCompiler) != 0:
        compiler = confCompiler
    if len(confInstallDir) != 0:
        installPath = confInstallDir
    return True

# Function to store compiler variable depending on "file-type" option passed
def setCompiler(fileType):
    global compiler
    if compiler != "":
        return
    if fileType in ["c", "s", "asm"]:
        compiler = "gcc"
    elif fileType in ["cc", "cpp", "cxx", "cp"]:
        compiler = "g++"
    else:
        print(("File type '" + fileType + "' not supported yet. Defaulting to gcc."))
        compiler = "gcc"


# Function to generate flags for the particular compiler
def flagsForCompiler(compilerName): # CompilerName will be obtained from the global compiler variable
    compilerVarName = ""
    compilerFlagsName = ""

    if compilerName == "g++":
        compilerVarName = "CXX"
        compilerFlagsName = "CXXFLAGS"
    else:
        # Default to gcc with C setup
        compilerVarName = "CC"
        compilerFlagsName = "CFLAGS"

    flagVars =  compilerVarName + " := " + compilerName + "\n"
    
    flagVars += compilerFlagsName + " := " + flags + "\n"
    ## Use MAKEGEN_COMPILER_FLAGS and MAKEGEN_COMPILER to keep it track of compiler and flags for internal use
    flagVars += "MAKEGEN_COMPILER := $(" + compilerVarName + ")\n"
    flagVars += "MAKEGEN_COMPILER_FLAGS := $(" + compilerFlagsName + ")\n"
    
    return flagVars


# Function to generate contents of the Makefile
def generateFileContents(fileType, compilerName):
    fileContents = "# Generated by makegen version " + VERSION + "\n# Makegen was written by Soham Mondal \n\n"

    # Compiler specific variables
    fileContents += flagsForCompiler(compilerName)
    fileContents += "SRCEXT := " + fileType + "\n"
    fileContents += "SRCDIR := " + srcdir + "\n"
    fileContents += "BUILDDIR := " + builddir + "\n"
    fileContents += "INSTALL_PATH := " + installPath + "\n"
    fileContents += "TARGET := " + outputFile + "\n"
    fileContents += "SOURCES := $(wildcard $(SRCDIR)/*.$(SRCEXT))\n"
    fileContents += "OBJECTS := $(patsubst $(SRCDIR)/%.o,$(BUILDDIR)/%.o,$(SOURCES:.$(SRCEXT)=.o))\n"

    fileContents += "\n\nall: $(TARGET)\n\n"

     # Main compilation
    fileContents += "$(TARGET): " + ("$(OBJECTS)\n")
    fileContents += "\t$(MAKEGEN_COMPILER) " + ("") + "-o $(TARGET) " + ("$^") + "\n\n"

    # Object files
    fileContents += "$(BUILDDIR)/%.o: $(SRCDIR)/%.$(SRCEXT)\n"
    fileContents +="\t$(MAKEGEN_COMPILER) $< $(MAKEGEN_COMPILER_FLAGS) -c -o $@\n"

    ## Clean
    fileContents += "\n"
    fileContents += "clean:" + "\n"
    fileContents += "\t" + "-rm $(TARGET) " + ("$(OBJECTS)") + "\n"

    ## Run
    fileContents += "\n"
    fileContents += "run: all\n"
    fileContents += "\t./$(TARGET)\n"

    ## Install
    fileContents += "\n"
    fileContents += "install: $(TARGET)\n"
    fileContents += "\tinstall $(TARGET) $(INSTALL_PATH)\n"

    ## Uninstall
    fileContents += "\n"
    fileContents += "uninstall:\n"
    fileContents += "\t-rm $(INSTALL_PATH)/$(TARGET)\n"


    return fileContents


# Write to Makefile the file contents
def writeToMakefile(fileContents):
    makefile = open("Makefile", 'w')
    makefile.write(fileContents)
    makefile.close()

# Main function calling all sub-functions in order
def start():
    parseCommandline()
    os.chdir(directory) # Change the directory to the directory that is passed in the options
    debugPrint("src directory ",srcdir)
    setCompiler(fileType)
    debugPrint("Compiler is set to " + compiler)

    parseConfig(fileType)

    fileContents = generateFileContents(fileType, compiler)
    writeToMakefile(fileContents)
    debugPrint("Successfully generated Makefile.")

    exit(0)


# Call the main function 
if __name__ == "__main__":
    start()
Sample run command:

First make the script executable (one time)
- chmod +x makegen.py

Then execute the script to generate Makefile
- makegen.py --build_dir="build" --compiler="g++" --directory="." --output-target="a.out" --source-dir="src" --file-type="cpp" -v --config-file="~/makegen.cfg"
# A sample makefile that will be created...


# Generated by makegen version 0.0.1
# Makegen was written by Soham Mondal

CXX := g++
CXXFLAGS := -Wall -g
MAKEGEN_COMPILER := $(CXX)
MAKEGEN_COMPILER_FLAGS := $(CXXFLAGS)
SRCEXT := cpp
SRCDIR := src
BUILDDIR := build
INSTALL_PATH := ~/bin/
TARGET := a.out
SOURCES := $(wildcard $(SRCDIR)/*.$(SRCEXT))
OBJECTS := $(patsubst $(SRCDIR)/%.o,$(BUILDDIR)/%.o,$(SOURCES:.$(SRCEXT)=.o))


all: $(TARGET)

$(TARGET): $(OBJECTS)
	$(MAKEGEN_COMPILER) -o $(TARGET) $^

$(BUILDDIR)/%.o: $(SRCDIR)/%.$(SRCEXT)
	$(MAKEGEN_COMPILER) $< $(MAKEGEN_COMPILER_FLAGS) -c -o $@

clean:
	-rm $(TARGET) $(OBJECTS)

run: all
	./$(TARGET)

install: $(TARGET)
	install $(TARGET) $(INSTALL_PATH)

uninstall:
	-rm $(INSTALL_PATH)/$(TARGET)
So, as you can see it has created a Makefile with all the passed options and the remaining options args which were unspecified, were taken from the configuration file suitably !!

This Makefile can be treated as a sample generic makefile for compiling and installing C and C++ source files

The same concept can be applied for automatic generation of other files which deals with a one time solution or have monotonicity associated with them