Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 98 additions & 43 deletions src/bundler.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
#!/usr/bin/python3
import argparse
import os
import re
import argparse

###define some defaults
# define some defaults
DEFAULT_INPUT_FILEPATH = 'main.cpp'
DEFAULT_INCLUDE_GUARD_PREFIX = '__INCLUDE_GUARD_'

###launch the argument parser for a proper cli experience
parser = argparse.ArgumentParser(description='A script for bundling small C++ projects into a single file, to upload to CodinGame by resolving includes, include guards, and accompanying .cpp files')
# launch the argument parser for a proper cli experience
parser = argparse.ArgumentParser(
description='A script for bundling small C++ projects into a single file, to upload to CodinGame by resolving includes, include guards, and accompanying .cpp files')
parser.add_argument(
'-i',
'--input',
help='A path to the file to start reading from (this should typically be the file containing your main() method). Defaults to \'main.cpp\' if omitted'
help='A path to a file to start reading from (this should typically be the file containing your main() method). Defaults to \'main.cpp\' if omitted. May be specified multiple times',
action='append'
)
parser.add_argument(
'-o',
Expand Down Expand Up @@ -43,21 +45,34 @@
help='A flag indicating that the script should not attempt to detect source files that accompany any header file included in the input',
action='store_true'
)
parser.add_argument(
'-p',
'--path',
help='A flag indicating an additional path that the script should search for include files on (beyond the directory of the input file). May be specified multiple times',
action='append'
)

arguments = parser.parse_args()
inputFilePath = arguments.input or DEFAULT_INPUT_FILEPATH
inputFilePaths = arguments.input or [DEFAULT_INPUT_FILEPATH]
for i in range(len(inputFilePaths)):
inputFilePaths[i] = os.path.abspath(inputFilePaths[i])
outputFilePath = arguments.output
if arguments.no_guard:
includeGuardPrefix = None
else:
includeGuardPrefix = arguments.guard_prefix or '__INCLUDE_GUARD_'

includePath = arguments.path or []
for i in range(len(includePath)):
includePath[i] = os.path.abspath(includePath[i])
alwaysOnce = arguments.always_once
noSource = arguments.no_source
###set some values, etc to use in the processing
# set some values, etc to use in the processing
definesFound = {}
ifStack=[]
ifStack = []

# processing section


###processing section
def processInclude(line):
macro = re.search('^#include ".+"', line)
if macro is not None:
Expand All @@ -66,7 +81,8 @@ def processInclude(line):
includeFilePath = macro.group(0).split('#include "')[1].split('"')[0]

if alwaysOnce:
associatedDefine = ('ALWAYS ONCE: ' + os.path.abspath(includeFilePath))
associatedDefine = (
'ALWAYS ONCE: ' + os.path.abspath(includeFilePath))
if associatedDefine in definesFound:
return ''
else:
Expand All @@ -76,50 +92,78 @@ def processInclude(line):
if lastDirSepPos == -1:
lastDirSepPos = includeFilePath.rfind('\\')
if lastDirSepPos != -1:
includePath, includeFileName = includeFilePath[0:lastDirSepPos], includeFilePath[lastDirSepPos+1:]
includePath, includeFileName = includeFilePath[0:
lastDirSepPos], includeFilePath[lastDirSepPos+1:]
os.chdir(includePath)
else:
includeFileName = includeFilePath
includeFileName = resolveIncludeFile(includeFileName)
with open(includeFileName, 'r') as subFile:
subFileContents = scan(subFile)
fileContents += '%s%s' % (subFileContents, '\n' if len(subFileContents) > 0 else '')
#now get the corresponding .cpp, unless it was for some reason the actual include above, or the cli flag --no-source is set
fileContents = mergeSubFile(
fileContents, subFileContents)
# now get the corresponding .cpp, unless it was for some reason the actual include above, or the cli flag --no-source is set
if not noSource:
splitFileName = includeFileName.rsplit('.', 1)
if len(splitFileName) == 2:
name, ext = splitFileName
if ext.lower() != 'cpp':
srcFileName = name + '.cpp'
if not os.path.isfile(srcFileName):
#try a .c file instead
# try a .c file instead
srcFileName = name + '.c'

if os.path.isfile(srcFileName):
with open(srcFileName, 'r') as subFile:
associatedDefine = ('DETECT SOURCE ONCE:' + os.path.abspath(srcFileName))
associatedDefine = (
'DETECT SOURCE ONCE:' + os.path.abspath(srcFileName))
if not associatedDefine in definesFound:
definesFound[associatedDefine] = True
subFileContents = scan(subFile)
fileContents += '%s%s' % (subFileContents, '\n' if len(subFileContents) > 0 else '')
fileContents = mergeSubFile(
fileContents, subFileContents)

os.chdir(oldWorkingDirectory)
fileContents = fileContents.strip()
return '%s%s' % (fileContents, '\n' if len(fileContents) > 0 else '')
else:
return None


def mergeSubFile(fileContents, subFileContents):
fileContents += '%s%s' % (subFileContents,
'\n' if len(subFileContents) > 0 else '')
return fileContents


def resolveIncludeFile(path):
if os.path.isfile(path):
return path

for includeDir in includePath:
testFileName = os.path.join(includeDir, path)
if os.path.isfile(testFileName):
return testFileName

raise FileNotFoundError('Fail to find %s on include path' % path)


def processDefine(line):
macro = re.search('^#define %s(_|\w|[0-9])+' % includeGuardPrefix, line)
if macro is not None:
definedThing = macro.group(0).split('#define %s' % includeGuardPrefix, 1)[1]
definedThing = macro.group(0).split(
'#define %s' % includeGuardPrefix, 1)[1]
definesFound[definedThing] = True
return line
else:
return None


def processUndef(line):
macro = re.search('^#undef %s(_|\w|[0-9])+' % includeGuardPrefix, line)
if macro is not None:
definedThing = macro.group(0).split('#undef %s' % includeGuardPrefix, 1)[1]
definedThing = macro.group(0).split(
'#undef %s' % includeGuardPrefix, 1)[1]
if definedThing in definesFound:
del definesFound[definedThing]
return line
Expand All @@ -134,6 +178,7 @@ def processIf(line):
else:
return None


def processIfDef(line):
macro = re.search('^#ifdef (_|\w|[0-9])+', line)
if macro is not None:
Expand All @@ -146,10 +191,12 @@ def processIfDef(line):
else:
return None


def processIfNDef(line):
macro = re.search('^#ifndef (_|\w|[0-9])+', line)
if macro is not None:
targetThing = macro.group(0).split('#ifndef %s' % includeGuardPrefix, 1)
targetThing = macro.group(0).split(
'#ifndef %s' % includeGuardPrefix, 1)
if len(targetThing) > 1:
targetThing = targetThing[1]
return 1 if not (targetThing in definesFound) else 0
Expand All @@ -158,14 +205,17 @@ def processIfNDef(line):
else:
return None


def processEndif(line):
macro = re.search('^#endif', line)
if macro is not None:
return True
else:
return None

#if the macro was found, this returns whether or not associated file already has been pragma-onced, otherwise returns None
# if the macro was found, this returns whether or not associated file already has been pragma-onced, otherwise returns None


def processPragmaOnce(line, file):
macro = re.search('^#pragma once', line)
if macro is not None:
Expand All @@ -178,6 +228,7 @@ def processPragmaOnce(line, file):
else:
return None


def scan(file):
fileContents = ""
for line in file.readlines():
Expand All @@ -201,13 +252,13 @@ def scan(file):

temp = processIfDef(line)

#handle ifdef and ifndef with the same main thing below this one
# handle ifdef and ifndef with the same main thing below this one
if temp is None:
temp = processIfNDef(line)

if temp is not None:
#ignore any macros in an #if we're not fully handling, other than includes
#this means that the contents of #ifs cannot take advantage of the size reduction, but won't produce invalid code
# ignore any macros in an #if we're not fully handling, other than includes
# this means that the contents of #ifs cannot take advantage of the size reduction, but won't produce invalid code
if len(ifStack) == 0 or ifStack[-1] == 1:
nextIfValue = temp
else:
Expand All @@ -216,56 +267,60 @@ def scan(file):
fileContents += line
ifStack.append(nextIfValue)
continue
#only try to deal with macros if not nested in a macro we are skipping.
# only try to deal with macros if not nested in a macro we are skipping.
if len(ifStack) == 0 or ifStack[-1] == 1:
if includeGuardPrefix is not None:
temp = processDefine(line)
if temp is not None:
# fileContents += temp
# fileContents += temp
continue

temp = processUndef(line)
if temp is not None:
fileContents += temp
continue
#don't include anything in this file if we see #pragma once
# don't include anything in this file if we see #pragma once
temp = processPragmaOnce(line, file)
if temp is not None:
if temp == True:
return ""
else:
continue
#include process includes if we are not ommitting this entire block
# include process includes if we are not ommitting this entire block
if len(ifStack) == 0 or ifStack[-1] != 0:
temp = processInclude(line)
if temp is not None:
fileContents += temp
continue

#if nothing else happened, just add the line to the file
# if nothing else happened, just add the line to the file
fileContents += line

return fileContents.strip()


#process the initial input file path, moving to to the appropriate path if neccessary
# process the initial input file path, moving to to the appropriate path if neccessary
oldWorkingDirectory = os.getcwd()
lastDirSepPos = inputFilePath.rfind('/')
if lastDirSepPos == -1:
lastDirSepPos = inputFilePath.rfind('\\')
if lastDirSepPos != -1:
inputPath, inputFileName = inputFilePath[0:lastDirSepPos], inputFilePath[lastDirSepPos+1:]
os.chdir(inputPath)
else:
inputFileName = inputFilePath
#now scan the file
with open(inputFileName, 'r') as inputFile:
result = scan(inputFile)

#now jump back to the initial directory
result = ''
for inputFilePath in inputFilePaths:
lastDirSepPos = inputFilePath.rfind('/')
if lastDirSepPos == -1:
lastDirSepPos = inputFilePath.rfind('\\')
if lastDirSepPos != -1:
inputPath, inputFileName = inputFilePath[0:
lastDirSepPos], inputFilePath[lastDirSepPos+1:]
os.chdir(inputPath)
else:
inputFileName = inputFilePath
# now scan the file
with open(inputFileName, 'r') as inputFile:
result = mergeSubFile(result, scan(inputFile))

# now jump back to the initial directory
os.chdir(oldWorkingDirectory)
if outputFilePath is not None:
with open(outputFilePath, 'w') as outFile:
outFile.write(result)
else:
print(result)
print(result)