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