from pathlib import Path, PurePath from collections import OrderedDict import sys bundle_header = """ // This is an automatically generated bundle of the Raven library. Do not edit this file directly. // Please see the end of the file for licensing information. // // This is a header-only bundle of the Raven library. Add this file to your project and // use `#define RAVEN_IMPLEMENTATION` before including to create the implementation. // Raven depends on these libraries, please make sure you include them before building: // xcb xcb-xkb cairo pangocairo xkbcommon xkbcommon-x11 // Compile command example: // gcc -Wall -Wextra -std=gnu99 $(pkg-config --cflags --libs xcb xcb-xkb cairo pangocairo xkbcommon xkbcommon-x11) -lm main.c -o main // // For more information, please see: https://git.hippoz.xyz/hippoz/raven // """ bundle_footer = """ // MIT License // Copyright (c) 2023 hippoz // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ def get_local_includes(lines): includes = [] for line_index, line_content in enumerate(lines): line = line_content.strip() if line.startswith("#include"): inside_string = False token = "" for char in line: if inside_string: if char == '"': break else: token += char else: if char == '"': inside_string = True if len(token): includes.append({ "line_index": line_index, "include_path": token }) return includes def recurse_header_dependencies(header_to_deps, header_name): result = [] header_deps = header_to_deps[header_name] for dep in header_deps: result += recurse_header_dependencies(header_to_deps, dep) result.append(header_name) return result def main(argc, argv) -> int: if argc != 2: print("usage: headerify source_dir") return 1 source_directory = Path(argv[1]) header_files = list(source_directory.glob("*.h")) source_files = list(source_directory.glob("*.c")) # We need a mapping between the name of a header file and a list of header files that it depends on, # since we will compute the order in which the header files need to be appended afterwards. We # also need a mapping between the name of a header file and the content of that header file, # because we need to comment out the lines that have `#include`s relative to the project, since # we are bundling all of them together. header_to_deps = {} header_to_content = {} for header_file in header_files: header_file_name = header_file.name with header_file.open() as f: lines = f.readlines() local_includes = get_local_includes(lines) header_to_deps[header_file_name] = [] for local_include in local_includes: # We keep track of the header files that this header file depends on header_to_deps[header_file_name].append(local_include["include_path"]) # We'll comment out the includes because they're not needed in a single-header bundle lines[local_include["line_index"]] = f"// {lines[local_include['line_index']].strip()} // -- commented because of single-header bundle\n" header_to_content[header_file_name] = "".join(lines) # Find the order we need to append the header files in based on their dependencies recursed_deps = [] for header_file in header_files: recursed_deps += recurse_header_dependencies(header_to_deps, header_file.name) header_file_order = list(OrderedDict.fromkeys(recursed_deps)) # Find the order we need to append the source files in, based on their respective header files, for consistency source_file_order = [] for header_file_name in header_file_order: for source_file in source_files: if source_file.stem == PurePath(header_file_name).stem: source_file_order.append(source_file) break # Make sure any source files without a corresponding header are added at the start for source_file in source_files: if source_file not in source_file_order and source_file.stem != "main": source_file_order.insert(0, source_file) # We're now ready to stitch the final bundle together bundle = "" bundle += bundle_header # Header files bundle += "// ----- header files -----" for header_file_name in header_file_order: bundle += f"\n\n\n// --- {header_file_name} ---\n\n" + header_to_content[header_file_name] bundle += "\n\n// ----- end header files -----\n\n\n\n" # Source files bundle += "// ----- source files -----\n#ifdef RAVEN_IMPLEMENTATION\n#define RAVEN_IMPLEMENTATION\n\n" for source_file in source_file_order: with source_file.open() as f: lines = f.readlines() local_includes = get_local_includes(lines) for inc in local_includes: # We'll comment out the includes because they're not needed in a single-header bundle lines[inc["line_index"]] = f"// {lines[inc['line_index']].strip()} // -- commented because of single-header bundle\n" bundle += f"\n\n\n// --- {source_file.name} ---\n\n" + "".join(lines) bundle += "\n\n#endif\n// ----- end source files -----\n\n" bundle += bundle_footer print(bundle) if __name__ == "__main__": sys.exit(main(len(sys.argv), sys.argv))