Files
2026-09-16 14:07:40 +08:00

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()