250 lines
7.2 KiB
Python
Vendored
250 lines
7.2 KiB
Python
Vendored
#!/usr/bin/env python3
|
|
|
|
import os
|
|
import re
|
|
import argparse
|
|
import datetime
|
|
from collections import deque
|
|
|
|
PARSE_FILE = 0
|
|
EXTERNAL_FILE = 1
|
|
ALREADY_SCANNED = 2
|
|
|
|
CPP_HEADER_FILE_EXT = set([".hpp" , ".h" , ".hxx" , ".hh" , ".inl"])
|
|
|
|
PRAGMA_ONCE_MATCHER = re.compile(r'#pragma once')
|
|
INCLUDE_FILE_MATCHER = re.compile(r'#include\s*[<\"]([\w.\\/]*)[>\"]')
|
|
LOCAL_INCLUDE_FILE_MATCHER = re.compile(r'#include\s*\"([\w.\\/]*)\"')
|
|
|
|
GUARDED_INCLUDES = [
|
|
{ "header": "version" },
|
|
{ "header": "coroutine", "condition": "(__cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L))" },
|
|
{ "header": "ranges", "condition": "(__cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L))" },
|
|
{ "header": "span", "condition": "(__cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L))" },
|
|
{ "header": "flat_map", "condition": "(__cplusplus >= 202302L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202302L))" },
|
|
{ "header": "flat_set", "condition": "(__cplusplus >= 202302L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202302L))" },
|
|
{ "header": "expected", "condition": "(__cplusplus >= 202302L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202302L))" },
|
|
{ "header": "move_only_function", "condition": "(__cplusplus >= 202302L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202302L))" },
|
|
]
|
|
|
|
def GetGuardedInclude(header):
|
|
for guarded in GUARDED_INCLUDES:
|
|
if guarded["header"] == header:
|
|
return guarded
|
|
return None
|
|
|
|
def IsCppHeaderFile(ext):
|
|
return ext in CPP_HEADER_FILE_EXT
|
|
|
|
def AdjustFileExtension(ext):
|
|
if ext[0] != '.':
|
|
ext = '.' + ext
|
|
|
|
def RemoveComments(text):
|
|
def BlotOutNonNewlines(strIn):
|
|
if strIn.startswith("/*"):
|
|
return "\n"
|
|
else:
|
|
return ""
|
|
|
|
def Replacer(match):
|
|
s = match.group(0)
|
|
if s.startswith('/'):
|
|
return BlotOutNonNewlines(s)
|
|
else:
|
|
return s
|
|
|
|
pattern = re.compile(
|
|
r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
|
|
re.DOTALL | re.MULTILINE
|
|
)
|
|
|
|
return re.sub(pattern, Replacer, text)
|
|
|
|
class SourceInfo:
|
|
def __init__(self, baseDir , outputDir, outputName):
|
|
self.includeDirs = list()
|
|
self.headerQueue = deque()
|
|
self.systemHeaders = set()
|
|
self.scannedFiles = set()
|
|
|
|
self.baseDir = baseDir
|
|
self.outputDir = outputDir
|
|
|
|
self.outputName = outputName
|
|
self.headerFileExt = ".h"
|
|
|
|
self.AddIncludeDirectory(self.baseDir)
|
|
self.AddIncludeDirectory(os.path.join(self.baseDir, "detail"))
|
|
|
|
def LogMessage(self, message):
|
|
print(message)
|
|
|
|
def AddIncludeDirectory(self, path):
|
|
if not os.path.exists(path):
|
|
return False
|
|
|
|
self.LogMessage(f"Include Directory Added: {path}")
|
|
|
|
self.includeDirs.append(path)
|
|
return True
|
|
|
|
def GetAbsoluteSourcePath(self, pwd, include):
|
|
if os.path.isabs(include):
|
|
return include
|
|
|
|
for includeDir in self.includeDirs:
|
|
absPath = os.path.normpath(os.path.join(includeDir, include))
|
|
if os.path.exists(absPath):
|
|
return absPath
|
|
|
|
return None
|
|
|
|
def ShouldParseFile(self , path , ext):
|
|
if (path in self.scannedFiles): return ALREADY_SCANNED
|
|
|
|
if not IsCppHeaderFile(ext) or not os.path.exists(path):
|
|
return EXTERNAL_FILE
|
|
|
|
return PARSE_FILE
|
|
|
|
def ScanSourceFile(self, path , depth):
|
|
dirpath, filename = os.path.split(path)
|
|
ext = os.path.splitext(filename)[1]
|
|
|
|
info = self.ShouldParseFile(path, ext)
|
|
if info != PARSE_FILE:
|
|
return info
|
|
|
|
self.LogMessage(f"Scan file: {path}")
|
|
self.scannedFiles.add(path)
|
|
|
|
with open (path , "r") as src:
|
|
lines = src.readlines()
|
|
|
|
for line in lines:
|
|
includeResult = INCLUDE_FILE_MATCHER.findall(line)
|
|
if not includeResult:
|
|
continue
|
|
|
|
localResult = LOCAL_INCLUDE_FILE_MATCHER.findall(line)
|
|
if localResult:
|
|
includeFile = self.GetAbsoluteSourcePath(dirpath, localResult[0])
|
|
if includeFile is None:
|
|
continue
|
|
|
|
call = self.ScanSourceFile(includeFile , depth + 1)
|
|
if call == EXTERNAL_FILE:
|
|
self.scannedFiles.add(includeFile)
|
|
else:
|
|
self.systemHeaders.add(includeResult[0])
|
|
|
|
self.AddFileToQueue(path, ext)
|
|
|
|
return info
|
|
|
|
def ParseDirectories(self):
|
|
all_files = []
|
|
|
|
for sourceDirectory in self.includeDirs:
|
|
for root, _, files in os.walk(sourceDirectory):
|
|
for filename in files:
|
|
all_files.append(os.path.join(root, filename))
|
|
|
|
for path in sorted(all_files):
|
|
self.ScanSourceFile(path, 0)
|
|
|
|
|
|
def WriteBeginFileHeader(self, filename, stream):
|
|
stream.write(f"\n// Begin File: {filename}\n")
|
|
|
|
def WriteEndFileHeader(self, filename, stream):
|
|
stream.write(f"\n// End File: {filename}\n")
|
|
|
|
def AddFileToQueue(self, filename, ext):
|
|
if IsCppHeaderFile(ext):
|
|
self.LogMessage(f"Enqueue header file: {filename}")
|
|
self.headerQueue.append(filename)
|
|
|
|
def AmalgamateQueue(self, queue, stream):
|
|
while (len(queue) > 0):
|
|
path = queue.popleft()
|
|
self.WriteFileToStream(path, stream)
|
|
|
|
def WriteFileToStream(self, path, stream):
|
|
self.LogMessage(f"Write File: {path}")
|
|
|
|
with open (path, 'r') as source:
|
|
self.WriteBeginFileHeader(path, stream)
|
|
|
|
lastLineWasEmpty = False
|
|
|
|
text = RemoveComments(source.read())
|
|
lines = text.replace("\r", "\n").split("\n")
|
|
|
|
for line in lines:
|
|
result = INCLUDE_FILE_MATCHER.findall(line)
|
|
if result:
|
|
continue
|
|
|
|
result = PRAGMA_ONCE_MATCHER.findall(line)
|
|
if result:
|
|
continue
|
|
|
|
stripped_line = line.strip()
|
|
if stripped_line or not lastLineWasEmpty:
|
|
stream.write(f"{line}\n")
|
|
|
|
lastLineWasEmpty = not stripped_line
|
|
|
|
self.WriteEndFileHeader(path, stream)
|
|
|
|
def WriteAlgamationFiles(self):
|
|
headerPath = os.path.join(self.outputDir, self.outputName + self.headerFileExt)
|
|
|
|
self.LogMessage(f"Creating source Amalgamation: {headerPath}")
|
|
|
|
with open (headerPath , 'w') as headerAmalgamation:
|
|
current_year = datetime.date.today().year
|
|
headerAmalgamation.write(f"// https://github.com/kunitoki/LuaBridge3\n")
|
|
headerAmalgamation.write(f"// Copyright {current_year}, kunitoki\n")
|
|
headerAmalgamation.write(f"// SPDX-License-Identifier: MIT\n\n")
|
|
headerAmalgamation.write(f"// clang-format off\n\n")
|
|
headerAmalgamation.write(f"#pragma once\n\n")
|
|
|
|
systemHeaders = list(self.systemHeaders)
|
|
for header in sorted(systemHeaders):
|
|
if GetGuardedInclude(header) is None:
|
|
headerAmalgamation.write(f"#include <{header}>\n")
|
|
headerAmalgamation.write("\n")
|
|
|
|
for header in reversed(sorted(systemHeaders)):
|
|
guard = GetGuardedInclude(header)
|
|
if guard is not None:
|
|
headerAmalgamation.write(f"#if defined(__has_include) && __has_include(<{header}>)")
|
|
if "condition" in guard and guard["condition"] is not None:
|
|
headerAmalgamation.write(f" && {guard['condition']}\n")
|
|
else:
|
|
headerAmalgamation.write("\n")
|
|
headerAmalgamation.write(f"#include <{header}>\n")
|
|
headerAmalgamation.write(f"#endif\n\n")
|
|
headerAmalgamation.write("\n")
|
|
|
|
self.AmalgamateQueue(self.headerQueue, headerAmalgamation)
|
|
|
|
headerAmalgamation.write("// clang-format on\n\n")
|
|
|
|
return headerPath
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description='Amalgamate LuaBridge.')
|
|
parser.add_argument('--base', action='store', default="Source/LuaBridge/")
|
|
parser.add_argument('--output', action='store', default="Distribution/LuaBridge/")
|
|
parser.add_argument('--name', action='store', default="LuaBridge")
|
|
|
|
args = parser.parse_args()
|
|
|
|
sourceInfo = SourceInfo(args.base, args.output, args.name)
|
|
sourceInfo.ParseDirectories()
|
|
sourceInfo.WriteAlgamationFiles()
|