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