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 }