1 module reggae.dcompile;
2 
3 import std.stdio;
4 import std.exception;
5 import std.process;
6 import std.conv;
7 import std.algorithm;
8 import std.getopt;
9 import std.array;
10 
11 
12 version(ReggaeTest) {}
13 else {
14     int main(string[] args) {
15         try {
16             return dcompile(args);
17         } catch(Exception ex) {
18             stderr.writeln(ex.msg);
19             return 1;
20         }
21     }
22 }
23 
24 /**
25 Only exists in order to get dependencies for each compilation step.
26  */
27 private int dcompile(string[] args) {
28 
29     version(Windows) {
30         // expand any response files in args (`dcompile @file.rsp`)
31         import std.array: appender;
32         import std.file: readText;
33 
34         auto expandedArgs = appender!(string[]);
35         expandedArgs.reserve(args.length);
36 
37         foreach (arg; args) {
38             if (arg.length > 1 && arg[0] == '@') {
39                 expandedArgs ~= parseResponseFile(readText(arg[1 .. $]));
40             } else {
41                 expandedArgs ~= arg;
42             }
43         }
44 
45         args = expandedArgs[];
46     }
47 
48     string depFile, objFile;
49     auto helpInfo = getopt(
50         args,
51         std.getopt.config.passThrough,
52         "depFile", "The dependency file to write", &depFile,
53         "objFile", "The object file to output", &objFile,
54     );
55 
56     enforce(args.length >= 2, "Usage: dcompile --objFile <objFile> --depFile <depFile> <compiler> <compiler args>");
57     enforce(!depFile.empty && !objFile.empty, "The --depFile and --objFile 'options' are mandatory");
58 
59     const compArgs = compilerArgs(args[1 .. $], objFile);
60     const compRes = invokeCompiler(compArgs, objFile);
61 
62     if (compRes.status != 0) {
63         stderr.writeln("Error compiling!");
64         return compRes.status;
65     }
66 
67     auto file = File(depFile, "w");
68     file.write(dependenciesToFile(objFile, dMainDependencies(compRes.output)).join("\n"));
69     file.writeln;
70 
71     return 0;
72 }
73 
74 
75 private string[] compilerArgs(string[] args, string objFile) @safe pure {
76     import std.path: absolutePath, baseName, dirName, stripExtension;
77 
78     enum Compiler { dmd, gdc, ldc }
79 
80     const compilerBinName = baseName(stripExtension(args[0]));
81     Compiler compiler = Compiler.dmd;
82     Compiler cli = Compiler.dmd;
83     switch (compilerBinName) {
84         default:
85             break;
86         case "gdmd":
87             compiler = Compiler.gdc;
88             break;
89         case "gdc":
90             compiler = Compiler.gdc;
91             cli = Compiler.gdc;
92             break;
93         case "ldmd":
94         case "ldmd2":
95             compiler = Compiler.ldc;
96             break;
97         case "ldc":
98         case "ldc2":
99             compiler = Compiler.ldc;
100             cli = Compiler.ldc;
101             break;
102     }
103 
104     if (compiler == Compiler.ldc && args.length > 1 && args[1] == "-lib") {
105         /* Unlike DMD, LDC does not write static libraries directly, but writes
106          * object files and archives them to a static lib.
107          * Make sure the temporary object files don't collide across parallel
108          * compiler invocations in the same working dir by placing the object
109          * files into the library's output directory via -od.
110          */
111         const od = "-od=" ~ dirName(objFile);
112 
113         if (cli == Compiler.ldc) { // ldc2
114             // mimic ldmd2 - uniquely-name and remove the object files
115             args.insertInPlace(2, "-oq", "-cleanup-obj", od);
116 
117             // dub adds `-od=.dub/obj`, remove it as it defeats our purpose
118             foreach (i; 5 .. args.length) {
119                 if (args[i] == "-od=.dub/obj") {
120                     args = args[0 .. i] ~ args[i+1 .. $];
121                     break;
122                 }
123             }
124         } else { // ldmd2
125             args.insertInPlace(2, od);
126             // As with dmd, -od may affect the final path of the static library
127             // (relative to -od) - make -of absolute to prevent this.
128             objFile = absolutePath(objFile);
129         }
130     }
131 
132     args ~= ["-color=on", "-of" ~ objFile, "-c", "-v"];
133 
134     final switch (cli) {
135         case Compiler.dmd: return args;
136         case Compiler.gdc: return mapToGdcOptions(args);
137         case Compiler.ldc: return mapToLdcOptions(args);
138     }
139 }
140 
141 //takes a dmd command line and maps arguments to gdc ones
142 private string[] mapToGdcOptions(in string[] compArgs) @safe pure {
143     string[string] options = [
144         "-v": "-fd-verbose",
145         "-O": "-O2",
146         "-debug": "-fdebug",
147         "-of": "-o",
148         "-color=on": "-fdiagnostics-color=always",
149     ];
150 
151     string doMap(string a) {
152         foreach(k, v; options) {
153             if(a.startsWith(k)) a = a.replace(k, v);
154         }
155         return a;
156     }
157 
158     return compArgs.map!doMap.array;
159 }
160 
161 
162 //takes a dmd command line and maps arguments to ldc2 ones
163 private string[] mapToLdcOptions(in string[] compArgs) @safe pure {
164     string doMap(string a) {
165         switch (a) {
166             case "-m32mscoff": return "-m32";
167             case "-fPIC":      return "-relocation-model=pic";
168             case "-gs":        return "-frame-pointer=all";
169             case "-inline":    return "-enable-inlining";
170             case "-profile":   return "-fdmd-trace-functions";
171             case "-color=on": return "-enable-color";
172             default:
173                 if (a.startsWith("-version="))
174                     return "-d-version=" ~ a[9 .. $];
175                 if (a.startsWith("-debug"))
176                     return "-d-debug" ~ a[6 .. $];
177                 return a;
178         }
179     }
180 
181     return compArgs.map!doMap.array;
182 }
183 
184 
185 private auto invokeCompiler(in string[] args, in string objFile) @safe {
186     version(Windows) {
187         static string quoteArgIfNeeded(string a) {
188             return !a.canFind(' ') ? a : `"` ~ a.replace(`"`, `\"`) ~ `"`;
189         }
190 
191         const rspFileContent = args[1..$].map!quoteArgIfNeeded.join("\n");
192 
193         // max command-line length (incl. args[0]) is ~32,767 on Windows
194         if (rspFileContent.length > 32_000) {
195             import std.file: mkdirRecurse, remove, write;
196             import std.path: dirName;
197 
198             const rspFile = objFile ~ ".dcompile.rsp"; // Ninja uses `<objFile>.rsp`, don't collide
199             mkdirRecurse(dirName(rspFile));
200             write(rspFile, rspFileContent);
201             const res = execute([args[0], "@" ~ rspFile], /*env=*/null, Config.stderrPassThrough);
202             remove(rspFile);
203             return res;
204         }
205     }
206 
207     // pass through stderr, capture stdout with -v output
208     return execute(args, /*env=*/null, Config.stderrPassThrough);
209 }
210 
211 
212 /**
213  * Given the output of compiling a file, return
214  * the list of D files to compile to link the executable
215  * Includes all dependencies, not just source files to
216  * compile.
217  */
218 string[] dMainDependencies(in string output) @safe {
219     import reggae.dependencies: tryExtractPathFromImportLine;
220     import std..string: indexOf, splitLines;
221 
222     string[] dependencies;
223 
224     foreach(line; output.splitLines) {
225         const importPath = tryExtractPathFromImportLine(line);
226         if (importPath !is null) {
227             dependencies ~= importPath;
228         } else if (line.startsWith("file ") && line[$-1] == ')') {
229             const i = line.indexOf('(');
230             if (i > 0)
231                 dependencies ~= line[i+1 .. $-1];
232         }
233     }
234 
235     return dependencies;
236 }
237 
238 
239 string[] dependenciesToFile(in string objFile, in string[] deps) @safe pure nothrow {
240     import std.array: join, replace;
241     static string escape(string arg) {
242         return arg.replace(" ", `\ `); // TODO: there'd be more...
243     }
244     return [
245         escape(objFile) ~ ": \\",
246         deps.map!escape.join(" "),
247     ];
248 }
249 
250 
251 // Parses the arguments from the specified response file content.
252 version(Windows)
253 string[] parseResponseFile(in string data) @safe pure {
254     import std.array: appender;
255     import std.ascii: isWhite;
256 
257     auto args = appender!(string[]);
258     auto currentArg = appender!(char[]);
259     void pushArg() {
260         if (currentArg[].length > 0) {
261             args ~= currentArg[].idup;
262             currentArg.clear();
263         }
264     }
265 
266     args.reserve(128);
267     currentArg.reserve(512);
268 
269     char currentQuoteChar = 0;
270     foreach (char c; data) {
271         if (currentQuoteChar) {
272             // inside quoted arg/fragment
273             if (c != currentQuoteChar) {
274                 currentArg ~= c;
275             } else {
276                 auto a = currentArg[];
277                 if (currentQuoteChar == '"' && a.length > 0 && a[$-1] == '\\') {
278                     a[$-1] = c; // un-escape: \" => "
279                 } else { // closing quote
280                     currentQuoteChar = 0;
281                 }
282             }
283         } else if (isWhite(c)) {
284             pushArg();
285         } else if (c == '"' || c == '\'') {
286             // beginning of quoted arg/fragment
287             currentQuoteChar = c;
288         } else {
289             // inside unquoted arg/fragment
290             currentArg ~= c;
291         }
292     }
293 
294     pushArg();
295 
296     return args[];
297 }