1 /**
2  * Copyright: Copyright Jason White, 2016
3  * License:   MIT
4  * Authors:   Jason White
5  *
6  * Description:
7  * Handles running gcc processes.
8  *
9  * Inputs and outputs are detected by adding the -MMD option and parsing the
10  * deps file that gcc produces.
11  */
12 module button.handlers.gcc;
13 
14 import button.log;
15 import button.resource;
16 import button.context;
17 
18 import std.range.primitives : isInputRange, ElementEncodingType,
19                               front, empty, popFront;
20 
21 import std.traits : isSomeChar;
22 
23 /**
24  * Exception that is thrown on invalid GCC deps syntax.
25  */
26 class MakeParserError : Exception
27 {
28     this(string msg)
29     {
30         // TODO: Include line information?
31         super(msg);
32     }
33 }
34 
35 /**
36  * Helper function to escape a character.
37  */
38 private C escapeChar(C)(C c)
39     if (isSomeChar!C)
40 {
41     switch (c)
42     {
43     case 't': return '\t';
44     case 'v': return '\v';
45     case 'r': return '\r';
46     case 'n': return '\n';
47     case 'b': return '\b';
48     case 'f': return '\f';
49     case '0': return '\0';
50     default:  return c;
51     }
52 }
53 
54 /**
55  * A single Make rule.
56  */
57 struct MakeRule
58 {
59     string target;
60     string[] deps;
61 }
62 
63 /**
64  * An input range of Make rules.
65  *
66  * This parses a deps file that gcc produces. The file consists of simple Make
67  * rules. Rules are separated by lines (discounting line continuations). Each
68  * rule consists of a target file and its dependencies.
69  */
70 struct MakeRules(Source)
71     if (isInputRange!Source && isSomeChar!(ElementEncodingType!Source))
72 {
73     private
74     {
75         import std.array : Appender;
76         import std.traits : Unqual;
77 
78         alias C = Unqual!(ElementEncodingType!Source);
79 
80         Source source;
81         bool _empty;
82         MakeRule current;
83 
84         Appender!(C[]) buf;
85     }
86 
87     this(Source source)
88     {
89         this.source = source;
90         popFront();
91     }
92 
93     @property
94     bool empty() const pure nothrow
95     {
96         return _empty;
97     }
98 
99     @property
100     const(MakeRule) front() const pure nothrow
101     {
102         return current;
103     }
104 
105     /**
106      * Parses a single file name.
107      */
108     private string parseFileName()
109     {
110         import std.uni : isWhite;
111 
112         buf.clear();
113 
114         while (!source.empty)
115         {
116             immutable c = source.front;
117 
118             if (c == ':' || c.isWhite)
119             {
120                 // ':' delimits a target.
121                 break;
122             }
123             else if (c == '\\')
124             {
125                 // Skip past the '\\'
126                 source.popFront();
127                 if (source.empty)
128                     break;
129 
130                 immutable e = source.front;
131                 if (e == '\n')
132                 {
133                     // Line continuation
134                     source.popFront();
135                 }
136                 else
137                 {
138                     buf.put(escapeChar(e));
139                     source.popFront();
140                 }
141 
142                 continue;
143             }
144 
145             // Regular character
146             buf.put(c);
147             source.popFront();
148         }
149 
150         return buf.data.idup;
151     }
152 
153     /**
154      * Skips spaces.
155      */
156     private void skipSpace()
157     {
158         import std.uni : isSpace;
159         while (!source.empty && isSpace(source.front))
160             source.popFront();
161     }
162 
163     /**
164      * Skips whitespace
165      */
166     private void skipWhite()
167     {
168         import std.uni : isWhite;
169         while (!source.empty && isWhite(source.front))
170             source.popFront();
171     }
172 
173     /**
174      * Parses the list of dependencies after the target.
175      *
176      * Returns: The list of dependencies.
177      */
178     private string[] parseDeps()
179     {
180         Appender!(string[]) deps;
181 
182         skipSpace();
183 
184         while (!source.empty)
185         {
186             // A new line delimits the dependency list
187             if (source.front == '\n')
188             {
189                 source.popFront();
190                 break;
191             }
192 
193             auto dep = parseFileName();
194             if (dep.length)
195                 deps.put(dep);
196 
197             if (!source.empty && source.front == ':')
198                 throw new MakeParserError("Unexpected ':'");
199 
200             skipSpace();
201         }
202 
203         return deps.data;
204     }
205 
206     /**
207      * Parses a rule.
208      */
209     private MakeRule parseRule()
210     {
211         string target = parseFileName();
212         if (target.empty)
213             throw new MakeParserError("Empty target name");
214 
215         skipSpace();
216 
217         if (source.empty)
218             throw new MakeParserError("Unexpected end of file");
219 
220         if (source.front != ':')
221             throw new MakeParserError("Expected ':' after target name");
222 
223         source.popFront();
224 
225         // Parse dependency names
226         auto deps = parseDeps();
227 
228         skipWhite();
229 
230         return MakeRule(target, deps);
231     }
232 
233     void popFront()
234     {
235         skipWhite();
236 
237         if (source.empty)
238         {
239             _empty = true;
240             return;
241         }
242 
243         current = parseRule();
244     }
245 }
246 
247 /**
248  * Convenience function for constructing a MakeRules range.
249  */
250 MakeRules!Source makeRules(Source)(Source source)
251     if (isInputRange!Source && isSomeChar!(ElementEncodingType!Source))
252 {
253     return MakeRules!Source(source);
254 }
255 
256 unittest
257 {
258     import std.array : array;
259     import std.exception : collectException;
260     import std.algorithm.comparison : equal;
261 
262     static assert(isInputRange!(MakeRules!string));
263 
264     {
265         auto rules = makeRules(
266                 "\n\nfoo.c : foo.h   \\\n   bar.h \\\n"
267                 );
268 
269         assert(rules.equal([
270                 MakeRule("foo.c", ["foo.h", "bar.h"]),
271         ]));
272     }
273 
274     {
275         auto rules = makeRules(
276                 "foo.c : foo.h \\\n bar.h\n"~
277                 "   \nbar.c : bar.h\n"~
278                 "\n   \nbaz.c:\n"~
279                 "ba\\\nz.c: blah.h\n"~
280                 `foo\ bar: bing\ bang`
281                 );
282 
283         assert(rules.equal([
284                 MakeRule("foo.c", ["foo.h", "bar.h"]),
285                 MakeRule("bar.c", ["bar.h"]),
286                 MakeRule("baz.c", []),
287                 MakeRule("baz.c", ["blah.h"]),
288                 MakeRule("foo bar", ["bing bang"]),
289         ]));
290     }
291 
292     assert(collectException!MakeParserError(makeRules(
293                 "foo.c: foo.h: bar.h"
294                 ).array));
295 }
296 
297 void execute(
298         ref BuildContext ctx,
299         const(string)[] args,
300         string workDir,
301         ref Resources inputs,
302         ref Resources outputs,
303         TaskLogger logger
304         )
305 {
306     import std.file : remove;
307 
308     import io.file : File, tempFile, AutoDelete;
309     import io.range : byBlock;
310 
311     import button.handlers.base : base = execute;
312 
313     // Create the temporary file for the dependencies.
314     auto depsPath = tempFile(AutoDelete.no).path;
315     scope (exit) remove(depsPath);
316 
317     // Tell gcc to write dependencies to our temporary file.
318     args ~= ["-MMD", "-MF", depsPath];
319 
320     base(ctx, args, workDir, inputs, outputs, logger);
321 
322     // TODO: Parse the command line arguments for -I and -o options.
323 
324     // Parse the dependencies
325     auto deps = File(depsPath).byBlock!char;
326     foreach (rule; makeRules(&deps))
327     {
328         outputs.put(rule.target);
329         inputs.put(rule.deps);
330     }
331 }