1 module godot.util.pregenerate; 2 3 import godot.util.classfinder; 4 5 import godot.util.tools.classes; 6 7 import std.process; 8 import std.stdio; 9 import std.string; 10 import std.conv : text; 11 import std.getopt; 12 import std.file; 13 import std.path : asRelativePath, buildPath, dirName, isRooted; 14 import std.range; 15 import std.algorithm.searching : find, endsWith, countUntil, canFind; 16 import std.algorithm.sorting : sort; 17 import std.array : replace; 18 19 enum MakeEntryPoint { 20 no, 21 yes, 22 detect 23 } 24 25 enum classesFilename = "classes.csv"; 26 enum entryPointFilename = "entrypoint.d"; 27 28 enum entryPointRoot = "/// Godot-D entry point"; 29 static immutable string entryPointSource = q{$ENTRYPOINTROOT 30 /// This file was automatically generated by godot-d:pregenerate. 31 import godot.api.register; 32 33 mixin GodotNativeLibrary!("$PREFIX"); 34 35 }.replace("$ENTRYPOINTROOT", entryPointRoot); 36 37 /// Sanitize a space-separated list of absolute paths. 38 /// DUB's IMPORT_PATHS and STRING_IMPORT_PATHS seem to always be absolute paths. 39 string[] sanitized(const string paths) { 40 if (paths.empty) 41 return null; 42 43 auto parts = paths.split(' '); 44 if (!parts.front.isRooted) 45 assert(0, "Paths passed to sanitized() are not absolute paths!"); 46 47 string[] ret; 48 foreach (part; parts) { 49 if (part.isRooted) 50 ret ~= part; 51 else 52 ret[$ - 1] = ret[$ - 1] ~ " " ~ part; 53 } 54 return ret; 55 } 56 57 int main(string[] args) { 58 MakeEntryPoint makeEntryPoint = MakeEntryPoint.detect; 59 string prefix = environment.get("DUB_PACKAGE") 60 .replace('-', '_') 61 .replace(':', '_'); 62 auto opt = args.getopt( 63 "makeEntryPoint", "Create GodotNativeLibrary entry point (if it doesn't already exist)", &makeEntryPoint, 64 "prefix|p", "GDNativeLibrary symbolPrefix", &prefix 65 ); 66 67 bool firstTimeSetup = false; 68 void setupStart() { 69 if (firstTimeSetup) 70 return; 71 writeln("**********************************"); 72 writeln("* Performing first-time setup... *"); 73 writeln("**********************************"); 74 firstTimeSetup = true; 75 } 76 77 ProjectInfo project; 78 79 string packageDir = environment.get("DUB_PACKAGE_DIR"); 80 auto importPaths = environment.get("IMPORT_PATHS", null).sanitized; 81 if (importPaths.empty) { 82 if (exists("source")) 83 importPaths ~= "source"; 84 else 85 importPaths ~= "src"; 86 } 87 88 /* ************************************************************************ 89 Parse all D files to find the classes and potentially the entry point. 90 ************************************************************************ */ 91 string[] sourceFiles = environment.get("SOURCE_FILES", null).sanitized; 92 foreach (importPath; importPaths) { 93 if (exists(importPath) && isDir(importPath)) { 94 foreach (DirEntry de; dirEntries(importPath, SpanMode.depth)) { 95 if (de.isFile && de.name.endsWith(".d")) { 96 if (!sourceFiles.canFind(de.name)) 97 sourceFiles ~= de.name; 98 } 99 } 100 } 101 } 102 foreach (sourceFile; sourceFiles) { 103 /// TODO: `parse` needs to relativize the path differently. The cwd is irrelevant to Godot. 104 /// Maybe relative to DUB package or Godot project - how will it be used? 105 string relativePath = sourceFile.asRelativePath(getcwd()).text; 106 FileInfo file = parse(relativePath); 107 writefln!"%s classes: %s"(file.moduleName, file.classes); 108 project.files ~= file; 109 } 110 111 /* ************************************************************************ 112 Determine where to put the class list, creating a new string import folder 113 if necessary. 114 ************************************************************************ */ 115 string viewsEnv = environment.get("STRING_IMPORT_PATHS"); 116 string classesFile; 117 if (viewsEnv.empty) { 118 setupStart(); // `views` will not be recognized until DUB is restarted 119 string viewsDir = packageDir.buildPath("views"); 120 if (!exists(viewsDir)) { 121 mkdirRecurse(viewsDir); 122 classesFile = viewsDir.buildPath(classesFilename); 123 } 124 } else { 125 auto splitted = viewsEnv.sanitized.sort(); 126 foreach (dir; splitted) { 127 auto cf = dir.buildPath(classesFilename); 128 if (exists(cf)) 129 classesFile = cf; 130 } 131 if (classesFile.empty) { 132 string viewsDir; 133 auto found = splitted.find!(d => d.endsWith("/views") || d.endsWith("/views/")); 134 if (found.length) 135 viewsDir = found.front; 136 else 137 viewsDir = splitted.front; 138 classesFile = viewsDir.buildPath(classesFilename); 139 } 140 } 141 142 /* ************************************************************************ 143 Determine where to put the entry point, if it doesn't already exist. 144 Existing entry point does NOT need to be modified thanks to the class list. 145 ************************************************************************ */ 146 string entryPointFile; 147 if (makeEntryPoint) { 148 auto found = project.files.find!(f => f.hasEntryPoint); 149 if (found.length) { 150 writeln("Entry point found in ", found.front.name); 151 // re-generate it if it is the auto-generated one, in case the prefix was changed. 152 // TODO: detect based on content, not name 153 if (found.front.name.endsWith("entrypoint.d")) 154 entryPointFile = found.front.name; 155 } else { 156 entryPointFile = importPaths.front.buildPath(entryPointFilename); 157 if (entryPointFile.exists) 158 assert(0, entryPointFile ~ " exists but does not contain the GodotNativeLibrary mixin."); 159 } 160 } 161 162 /* ************************************************************************ 163 Write necessary files 164 ************************************************************************ */ 165 if (makeEntryPoint && entryPointFile) { 166 string entryPointText = entryPointSource 167 .replace("$PREFIX", prefix); 168 writeln("Writing entry point to ", entryPointFile); 169 std.file.write(entryPointFile, entryPointText); 170 } 171 writeln("Writing class list to ", classesFile); 172 std.file.write(classesFile, project.toCsv); 173 string gdignore = classesFile.dirName.buildPath(".gdignore"); 174 if (!gdignore.exists) 175 std.file.write(gdignore, ""); 176 177 if (firstTimeSetup) { 178 writeln("*******************************************************"); 179 writeln("* DUB must be restarted to complete first-time setup. *"); 180 writeln("* This will only be necessary once. *"); 181 writeln("*******************************************************"); 182 return 2; 183 } 184 185 return 0; 186 }