1 module regenerate;
2 
3 import std.string;
4 import std.file;
5 import std.path;
6 import std.range;
7 import std.algorithm;
8 import std.process;
9 import std.getopt;
10 import std.experimental.logger;
11 import std.stdio : writeln;
12 
13 struct Options
14 {
15     @("Path to temporary directory, `temp` by default.") 
16     string tempDirectory = "temp";
17 
18     @("Path to d++ executable. Only needed when doing bindings. Found in path if not provided.") 
19     string dppExecutablePath;
20 
21     @("Path to bindbc generator. Only needed when doing bindings.") 
22     string generatorRepoPath;
23 
24     @("Path to `cimgui.h`.")
25     string cimguiHeaderPath;
26 
27     @("Whether to get cimgui from github and compile into a dll and lib.")
28     bool compileImgui;
29 
30     enum CimguiBranch
31     {
32         docking, master
33     }
34     @("Controls which branch of cimgui to build. (Can be `docking` or `master`.)")
35     CimguiBranch cimguiBranch = CimguiBranch.docking;
36 
37     @("Whether to regenerate bindbc-cimgui bindings using the generator.")
38     bool generateBindings;
39     
40     @("Whether to get dpp from github and compile it.")
41     bool compileDpp;
42 }
43 
44 __gshared Options op;
45 __gshared bool hasErrors;
46 
47 void _error(string message)
48 {
49     hasErrors = true;
50     error(message);
51 }
52 
53 int main(string[] args)
54 {
55     string getoptMixin()
56     {
57         auto ret = "auto helpInformation = getopt(args";
58         static foreach (field; Options.tupleof)
59         {
60             import std.format;
61             ret ~= `, "%s", "%s", &op.%1$s`.format(__traits(identifier, field), __traits(getAttributes, field)[0]);
62         }
63         ret ~= ");";
64         return ret;
65     }
66     mixin(getoptMixin());
67 
68     if (helpInformation.helpWanted || op == Options.init)
69     {
70         defaultGetoptPrinter("Help message", helpInformation.options);
71         return 0;
72     }
73 
74     void ensurePathExistsIfProvided(string name, string path)
75     {
76         if (path && !exists(path))
77         {
78             _error(name ~ " not found by the specified path " ~ path);
79         }
80     }
81     ensurePathExistsIfProvided("Dpp", op.dppExecutablePath);
82     ensurePathExistsIfProvided("Generator", op.generatorRepoPath);
83     ensurePathExistsIfProvided("cimgui header", op.cimguiHeaderPath);
84 
85     if (op.compileDpp && op.dppExecutablePath)
86     {
87         _error("Incompatible arguments: `compileDpp` and `dppExecutablePath`");
88     }
89 
90     if (op.compileImgui && op.cimguiHeaderPath)
91     {
92         _error("Incompatible arguments: `compileImgui` and `cimguiHeaderPath`");
93     }
94 
95     op.tempDirectory = absolutePath(op.tempDirectory);
96     mkdirRecurse(op.tempDirectory);
97 
98     if (hasErrors)
99         return 1;
100 
101     if (op.compileDpp)
102     {
103         op.dppExecutablePath = dppCompilationWorkflow();
104 
105         if (!op.dppExecutablePath)
106             return 1;
107 
108         log("Dpp has been written to " ~ op.dppExecutablePath);
109     }
110 
111     if (op.compileImgui)
112     {
113         string outputDirectory = imguiCompilationWorkflow();
114 
115         if (!outputDirectory)
116             return 1;
117         
118         log("Imgui binaries have been written to " ~ outputDirectory);
119     }
120 
121     if (op.generateBindings)
122     {
123         generateBindingsWorkflow();
124     }
125 
126     return hasErrors ? 1 : 0;
127 }
128 
129 string takeAfterLast(string str, string pattern)
130 {
131     auto index = lastIndexOf(str, pattern);
132     return str[index + 1..$];
133 }
134 
135 // Returns the path to the cloned repo
136 string gitClone(string repoUrl, bool recursive = true, string branch = null)
137 {
138     string repoName = repoUrl.takeAfterLast("/");
139     string repoPath = buildPath(op.tempDirectory, repoName);
140     if (exists(repoPath))
141     {
142         log("Found " ~ repoName ~ " clone at " ~ repoPath ~ ", skipping cloning.");
143         return repoPath;
144     }
145 
146     string[] args = ["git", "clone", repoUrl];
147     if (branch)
148     {
149         args ~= "--branch=" ~ branch;
150     }
151     if (recursive)
152     {
153         args ~= "--recursive";
154     }
155 
156     auto result = _exec(args, op.tempDirectory);
157     if (result.status == 0)
158         return repoPath;
159 
160     _error("Failed to clone " ~ repoName ~ ": " ~ result.output);
161     return null;
162 }
163 
164 string dppCompilationWorkflow()
165 {
166     const dppRepoUrl = "https://github.com/atilaneves/dpp";
167     log("You will need to install LLVM if it's not installed already, see the dpp repo: " ~ dppRepoUrl);
168     
169     if (!checkCommandsExist(["git", "dub"]))
170         return null;
171 
172     const dppClonedRepoPath = gitClone(dppRepoUrl);
173     if (!dppClonedRepoPath)
174         return null;
175 
176     const result = _exec(["dub", "build"], dppClonedRepoPath);
177     if (result.status != 0)
178     {
179         _error("Failed to build dpp: " ~ result.output);
180         return null;
181     }
182 
183     string dppExecutablePath;
184     version (Windows)
185     {
186         dppExecutablePath = buildPath(dppClonedRepoPath, "bin/d++.exe");
187         if (!exists(dppExecutablePath))
188         {
189             _error("Expected d++ at " ~ dppExecutablePath);
190         }
191     }
192     return dppExecutablePath;
193 }
194 
195 string gitCloneImgui()
196 {
197     const repoUrl = "https://www.github.com/cimgui/cimgui";
198     string commitHash, branch;
199     if (op.cimguiBranch == Options.CimguiBranch.docking)
200     {
201         commitHash = "873c03c3673033bf8e8dd22901d4a3934b7407b2";
202         branch = "docking_inter";
203     }
204     else
205     {
206         commitHash = "17ffa736d353591b9545d1cedaa6373482f7f1a3";
207         branch = "master";
208     }
209     const recursive = false;
210 
211     const clonedRepoPath = gitClone(repoUrl, recursive, branch);
212     if (!clonedRepoPath)
213         return null;
214 
215     if (commitHash)
216     {
217         auto result = _exec(["git", "checkout", commitHash], clonedRepoPath);
218         if (result.status != 0) return null;
219     }
220     
221     // result = _exec(["git", "submodule", "init"], clonedRepoPath);
222     // if (result.status != 0) return null;
223 
224     auto result = _exec(["git", "submodule", "update", "--init", "--recursive"], clonedRepoPath);
225     if (result.status != 0) return null;
226     
227     return clonedRepoPath;
228 }
229 
230 string imguiCompilationWorkflow()
231 {
232     if (!checkCommandsExist(["git", "cmake"]))
233         return null;
234     
235     const imguiClonedRepoPath = gitCloneImgui();
236     if (!imguiClonedRepoPath)
237         return null;
238 
239     const imguiBuildDirectory = "imgui_build";
240     const configuration = "RelWithDebInfo";
241     auto result = _exec(
242         ["cmake", "-Hcimgui", "-B" ~ imguiBuildDirectory, "-DCMAKE_BUILD_TYPE=" ~ configuration], op.tempDirectory);
243     if (result.status != 0)
244     {
245         _error("Failed to run cmake: " ~ result.output);
246         return null;
247     }
248 
249     result = _exec(["cmake", "--build", imguiBuildDirectory, "--config", configuration], op.tempDirectory);
250     if (result.status != 0)
251     {
252         _error("Failed to run cmake build: " ~ result.output);
253         return null;
254     }
255 
256     // The output should be in RelWithDebInfo
257     const outputDirectory = buildPath(op.tempDirectory, imguiBuildDirectory, configuration);
258     return outputDirectory;
259 }
260 
261 
262 void generateBindingsWorkflow()
263 {
264     if (!checkCommandsExist(["git", "dub"]))
265         return;
266 
267     string generatorRepoPath;
268     if (op.generatorRepoPath)
269     {
270         generatorRepoPath = op.generatorRepoPath;
271     }
272     else
273     {
274         generatorRepoPath = gitClone("https://github.com/MrcSnm/bindbc-generator");
275         if (!generatorRepoPath) 
276             return;
277     }
278 
279     string imguiPath;
280     string cimguiHeaderPath;
281     if (op.cimguiHeaderPath)
282     {
283         imguiPath = dirName(op.cimguiHeaderPath);
284         cimguiHeaderPath = op.cimguiHeaderPath;
285     }
286     else
287     {    
288         imguiPath = gitCloneImgui();
289         if (!imguiPath)
290             return;
291         cimguiHeaderPath = buildPath(imguiPath, "cimgui.h");
292     }
293 
294 
295     string cimguiPluginGenPath = buildPath(generatorRepoPath, "bindbc/cimgui");
296     auto generatorArgs = [
297         "dub", "--", 
298         "--recompile", "--load-all", 
299         "--file", cimguiHeaderPath, 
300         "--temp-path", op.tempDirectory, 
301         "--presets", "cimgui"
302     ];
303     if (op.dppExecutablePath)
304     {
305         generatorArgs ~= "--dpp-path";
306         generatorArgs ~= op.dppExecutablePath;
307     }
308 
309     string argumentsString = escapeShellCommand(generatorArgs);
310     
311     // We need to append the plugin arguments as strings, since otherwise they get escaped
312     // and the default parser in the generator cannot deal with that.
313     argumentsString ~= ` --plugin-args cimgui-overloads="[%s, %s, %s]"`.format(imguiPath, cimguiPluginGenPath, "d-conv");
314     log(argumentsString);
315     
316     auto result = executeShell(argumentsString, null, Config.none, size_t.max, generatorRepoPath);
317     if (result.status != 0)
318     {
319         _error("Failed to apply the generator: ");
320         writeln(result.output);
321         return;
322     }
323     writeln(result.output);
324 
325     log("Find the output files somewhere in " ~ generatorRepoPath ~ " and in here " ~ cimguiPluginGenPath);
326 }
327 
328 
329 auto _exec(string[] args, string workingDirectory = null)
330 {
331     string cmd = escapeShellCommand(args);
332     log(cmd);
333     return execute(args, null, Config.none, size_t.max, workingDirectory);
334 }
335 
336 bool existsCommand(string command)
337 {
338     version (Windows)
339     {
340         return execute(["where", command]).status == 0;
341     }
342     assert(0, "Linux and the alike are not implemented");
343 }
344 
345 bool checkCommandsExist(string[] commands)
346 {
347     foreach (command; commands)
348     {
349         if (!existsCommand(command))
350         {
351             _error("`" ~ command ~ "` not found in path");
352         }   
353     }
354     return !hasErrors;
355 }