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 }