1 /**
2  * Copyright: Copyright Jason White, 2016
3  * License:   MIT
4  * Authors:   Jason White
5  *
6  * Description:
7  * Handles running "dmd" processes.
8  *
9  * Inputs and outputs are detected by parsing and modifying the command line
10  * before execution. DMD has a "-deps" option for writing inputs to a file. This
11  * option is dynamically added to the command line and parsed after the process
12  * exits.
13  */
14 module button.handlers.dmd;
15 
16 import button.resource;
17 import button.context;
18 
19 import std.path;
20 import io.file;
21 
22 private struct Options
23 {
24     // Flags
25     bool compileFlag; // -c
26     bool coverageFlag; // -cov
27     bool libFlag; // -lib
28     bool sharedFlag; // -shared
29     bool docFlag; // -D
30     bool headerFlag; // -H
31     bool mapFlag; // -map
32     bool suppressObjectsFlag; // -o-
33     bool jsonFlag; // -X
34     bool opFlag; // -op
35 
36     // Options with arguments
37     string outputDir; // -od
38     string outputFile; // -of
39     string depsFile; // -deps=
40     string docDir; // -Dd
41     string docFile; // -Df
42     string headerDir; // -Hd
43     string headerFile; // -Hf
44     string[] importDirs; // -I
45     string[] stringImportDirs; // -J
46     string[] linkerFlags; // -L
47     const(string)[] run; // -run
48     string jsonFile; // -Xf
49     string cmdFile; // @cmdfile
50 
51     // Left over files on the command line
52     string[] files;
53 
54     /**
55      * Returns the object file path for the given source file path.
56      */
57     string objectPath(string sourceFile) const pure
58     {
59         if (!opFlag)
60             sourceFile = baseName(sourceFile);
61         return buildPath(outputDir, setExtension(sourceFile, ".o"));
62     }
63 
64     /**
65      * Returns a list of object file paths.
66      */
67     const(char[])[] objects() const pure
68     {
69         import std.algorithm.iteration : map, filter;
70         import std.algorithm.searching : endsWith;
71         import std.array : array;
72 
73         if (suppressObjectsFlag)
74             return [];
75 
76         // If -c is specified, all source files are compiled into separate
77         // object files. If -c is not specified, all sources files are compiled
78         // into a single object file which is named based on the first source
79         // file specified.
80         if (compileFlag)
81         {
82             if (outputFile)
83                 return [outputFile];
84             else
85                 return files
86                     .filter!(p => p.endsWith(".d"))
87                     .map!(p => objectPath(p))
88                     .array();
89         }
90 
91         // Object name is based on -of
92         if (outputFile)
93             return [objectPath(outputFile)];
94 
95         auto dSources = files.filter!(p => p.endsWith(".d"));
96         if (dSources.empty)
97             return [];
98 
99         return [objectPath(dSources.front)];
100     }
101 
102     /**
103      * Returns the static library file path.
104      */
105     string staticLibraryPath() const pure
106     {
107         import std.algorithm.iteration : filter;
108         import std.algorithm.searching : endsWith;
109 
110         // If the output file has no extension, ".a" is appended.
111 
112         // Note that -op and -o- have no effect when building static libraries.
113 
114         string path;
115 
116         if (outputFile)
117             path = defaultExtension(outputFile, ".a");
118         else
119         {
120             // If no output file is specified with -of, the output file is based on
121             // the name of the first source file.
122             auto dSources = files.filter!(p => p.endsWith(".d"));
123             if (dSources.empty)
124                 return null;
125 
126             path = setExtension(baseName(dSources.front), ".a");
127         }
128 
129         return buildPath(outputDir, path);
130     }
131 
132     /**
133      * Returns the shared library file path.
134      */
135     string sharedLibraryPath() const pure
136     {
137         import std.algorithm.iteration : filter;
138         import std.algorithm.searching : endsWith;
139 
140         if (outputFile)
141             return outputFile;
142 
143         // If no output file is specified with -of, the output file is based on
144         // the name of the first source file.
145         auto dSources = files.filter!(p => p.endsWith(".d"));
146         if (dSources.empty)
147             return null;
148 
149         return setExtension(baseName(dSources.front), ".so");
150     }
151 
152     /**
153      * Returns the static library file path.
154      */
155     string executablePath() const pure
156     {
157         import std.algorithm.iteration : filter;
158         import std.algorithm.searching : endsWith;
159 
160         if (outputFile)
161             return outputFile;
162 
163         // If no output file is specified with -of, the output file is based on
164         // the name of the first source file.
165         auto dSources = files.filter!(p => p.endsWith(".d"));
166         if (dSources.empty)
167             return null;
168 
169         return stripExtension(baseName(dSources.front));
170     }
171 }
172 
173 /**
174  * Parses DMD arguments.
175  */
176 private Options parseArgs(const(string)[] args) pure
177 {
178     import std.algorithm.searching : startsWith;
179     import std.exception : enforce;
180     import std.range : front, popFront, empty;
181 
182     Options opts;
183 
184     while (!args.empty)
185     {
186         string arg = args.front;
187 
188         if (arg == "-c")
189             opts.compileFlag = true;
190         else if (arg == "-cov")
191             opts.coverageFlag = true;
192         else if (arg == "-lib")
193             opts.libFlag = true;
194         else if (arg == "-shared")
195             opts.sharedFlag = true;
196         else if (arg == "-lib")
197             opts.docFlag = true;
198         else if (arg == "-H")
199             opts.headerFlag = true;
200         else if (arg == "-map")
201             opts.mapFlag = true;
202         else if (arg == "-X")
203             opts.jsonFlag = true;
204         else if (arg == "-op")
205             opts.opFlag = true;
206         else if (arg == "-o-")
207             opts.suppressObjectsFlag = true;
208         else if (arg == "-run")
209         {
210             args.popFront();
211             opts.run = args;
212             break;
213         }
214         else if (arg.startsWith("-deps="))
215             opts.depsFile = arg["-deps=".length .. $];
216         else if (arg.startsWith("-od"))
217             opts.outputDir = arg["-od".length .. $];
218         else if (arg.startsWith("-of"))
219             opts.outputFile = arg["-of".length .. $];
220         else if (arg.startsWith("-Xf"))
221             opts.jsonFile = arg["-Xf".length .. $];
222         else if (arg.startsWith("-Dd"))
223             opts.docDir = arg["-Dd".length .. $];
224         else if (arg.startsWith("-Df"))
225             opts.docFile = arg["-Df".length .. $];
226         else if (arg.startsWith("-Hd"))
227             opts.headerDir = arg["-Hd".length .. $];
228         else if (arg.startsWith("-Hf"))
229             opts.headerFile = arg["-Hf".length .. $];
230         else if (arg.startsWith("-I"))
231             opts.importDirs ~= arg["-I".length .. $];
232         else if (arg.startsWith("-J"))
233             opts.stringImportDirs ~= arg["-J".length .. $];
234         else if (arg.startsWith("-L"))
235             opts.stringImportDirs ~= arg["-L".length .. $];
236         else if (arg.startsWith("@"))
237             opts.cmdFile = arg["@".length .. $];
238         else if (!arg.startsWith("-"))
239             opts.files ~= arg;
240 
241         args.popFront();
242     }
243 
244     return opts;
245 }
246 
247 void execute(
248         ref BuildContext ctx,
249         const(string)[] args,
250         string workDir,
251         ref Resources inputs,
252         ref Resources outputs
253         )
254 {
255     import button.handlers.base : base = execute;
256 
257     import std.algorithm.iteration : map, filter, uniq;
258     import std.algorithm.searching : endsWith;
259     import std.range : enumerate, empty, popFront, front;
260     import std.regex : regex, matchAll;
261     import std.file : remove;
262     import std.array : array;
263 
264     import io.text, io.range;
265 
266     Options opts = parseArgs(args[1 .. $]);
267 
268     string depsPath;
269 
270     if (opts.depsFile is null)
271     {
272         // Output -deps to a temporary file.
273         depsPath = tempFile(AutoDelete.no).path;
274         args ~= "-deps=" ~ depsPath;
275     }
276     else
277     {
278         // -deps= was specified already. Just use this path to get the
279         // dependencies.
280         depsPath = opts.depsFile;
281     }
282 
283     // Delete the temporary -deps file when done.
284     scope (exit) if (opts.depsFile is null) remove(depsPath);
285 
286     base(ctx, args, workDir, inputs, outputs);
287 
288     // Add the inputs from the dependency file.
289     static r = regex(`\((.*?)\)`);
290     foreach (line; File(depsPath).byLine)
291         foreach (c; line.matchAll(r))
292             inputs.put(c[1]);
293 
294     inputs.put(opts.files);
295 
296     // Determine the output files based on command line options. If no output
297     // file name is specified with -of, the file name is based on the first
298     // source file specified on the command line.
299     if (opts.libFlag)
300     {
301         if (auto path = opts.staticLibraryPath())
302             outputs.put(path);
303     }
304     else if (opts.compileFlag)
305     {
306         outputs.put(opts.objects);
307     }
308     else if (opts.sharedFlag)
309     {
310         if (auto path = opts.sharedLibraryPath())
311             outputs.put(path);
312 
313         outputs.put(opts.objects);
314     }
315     else
316     {
317         if (!opts.suppressObjectsFlag)
318         {
319             // Binary executable.
320             if (auto path = opts.executablePath())
321                 outputs.put(path);
322 
323             // Objects
324             outputs.put(opts.objects);
325         }
326     }
327 }