1 /**
2  * Copyright: Copyright Jason White, 2016
3  * License:   MIT
4  * Authors:   Jason White
5  *
6  * Description:
7  * Handles the 'build' command.
8  */
9 module button.cli.build;
10 
11 import std.parallelism : TaskPool;
12 
13 import button.cli.options : BuildOptions, GlobalOptions;
14 
15 import io.text, io.file;
16 
17 import button.state;
18 import button.rule;
19 import button.graph;
20 import button.build;
21 import button.resource;
22 import button.task;
23 import button.textcolor;
24 import button.events;
25 import button.watcher;
26 import button.context;
27 import button.exceptions;
28 
29 /**
30  * Updates the build.
31  *
32  * All outputs are brought up-to-date based on their inputs. If '--autopilot' is
33  * specified, once the build finishes, we watch for changes to inputs and run
34  * another build.
35  */
36 int buildCommand(BuildOptions opts, GlobalOptions globalOpts)
37 {
38     import std.parallelism : totalCPUs;
39     import std.path : dirName, absolutePath;
40     import button.loggers.console;
41 
42     if (opts.threads == 0)
43         opts.threads = totalCPUs;
44 
45     auto pool = new TaskPool(opts.threads - 1);
46     scope (exit) pool.finish(true);
47 
48     immutable color = TextColor(colorOutput(opts.color));
49 
50     auto events = new ConsoleLogger(stdout, stderr, opts.verbose, pool.size);
51 
52     string path;
53     BuildState state;
54 
55     try
56     {
57         path = buildDescriptionPath(opts.path);
58         state = new BuildState(path.stateName);
59     }
60     catch (BuildException e)
61     {
62         stderr.println(color.status, ":: ", color.error,
63                 "Error", color.reset, ": ", e.msg);
64         return 1;
65     }
66 
67     auto context = BuildContext(absolutePath(dirName(path)), pool, events,
68             state, opts.dryRun, opts.verbose, color);
69 
70     if (!opts.autopilot)
71     {
72         return doBuild(context, path);
73     }
74     else
75     {
76         // Do the initial build, checking for changes the old-fashioned way.
77         doBuild(context, path);
78 
79         return doAutoBuild(context, path, opts.watchDir, opts.delay);
80     }
81 }
82 
83 int doBuild(ref BuildContext ctx, string path)
84 {
85     import std.datetime.stopwatch : StopWatch, AutoStart;
86 
87     auto sw = StopWatch(AutoStart.yes);
88 
89     scope (exit)
90     {
91         import std.conv : to;
92         import core.time : Duration;
93         sw.stop();
94 
95         if (ctx.verbose)
96         {
97             println(ctx.color.status, ":: Total time taken: ", ctx.color.reset,
98                     cast(Duration)sw.peek());
99         }
100     }
101 
102     try
103     {
104         ctx.state.begin();
105         scope (exit)
106         {
107             if (ctx.dryRun)
108                 ctx.state.rollback();
109             else
110                 ctx.state.commit();
111         }
112 
113         syncBuildState(ctx, path);
114 
115         if (ctx.verbose)
116             println(ctx.color.status, ":: Checking for changes...", ctx.color.reset);
117 
118         queueChanges(ctx.state, ctx.pool, ctx.color);
119 
120         update(ctx);
121     }
122     catch (BuildException e)
123     {
124         stderr.println(ctx.color.status, ":: ", ctx.color.error,
125                 "Error", ctx.color.reset, ": ", e.msg);
126         return 1;
127     }
128     catch (Exception e)
129     {
130         stderr.println(ctx.color.status, ":: ", ctx.color.error,
131                 "Build failed!", ctx.color.reset,
132                 " See the output above for details.");
133         if (ctx.verbose)
134             println(ctx.color.status, ":: ", e.toString());
135 
136         return 1;
137     }
138 
139     return 0;
140 }
141 
142 int doAutoBuild(ref BuildContext ctx, string path,
143         string watchDir, size_t delay)
144 {
145     println(ctx.color.status, ":: Waiting for changes...", ctx.color.reset);
146 
147     ctx.state.begin();
148     scope (exit)
149     {
150         if (ctx.dryRun)
151             ctx.state.rollback();
152         else
153             ctx.state.commit();
154     }
155 
156     foreach (changes; ChangeChunks(ctx.state, watchDir, delay))
157     {
158         try
159         {
160             size_t changed = 0;
161 
162             foreach (v; changes)
163             {
164                 // Check if the resource contents actually changed
165                 auto r = ctx.state[v];
166 
167                 if (r.update())
168                 {
169                     ctx.state.addPending(v);
170                     ctx.state[v] = r;
171                     ++changed;
172                 }
173             }
174 
175             if (changed > 0)
176             {
177                 println(ctx.color.status, ":: Change detected. Building...",
178                         ctx.color.reset);
179                 syncBuildState(ctx, path);
180                 update(ctx);
181                 println(ctx.color.status, ":: Waiting for changes...",
182                         ctx.color.reset);
183             }
184         }
185         catch (BuildException e)
186         {
187             stderr.println(ctx.color.status, ":: ", ctx.color.error,
188                     "Error", ctx.color.reset, ": ", e.msg);
189             continue;
190         }
191         catch (Exception e)
192         {
193             stderr.println(ctx.color.status, ":: ", ctx.color.error,
194                     "Build failed!", ctx.color.reset,
195                     " See the output above for details.");
196             continue;
197         }
198     }
199 
200     // Unreachable
201 }
202 
203 /**
204  * Updates the database with any changes to the build description.
205  */
206 void syncBuildState(ref BuildContext ctx, string path)
207 {
208     // TODO: Don't store the build description in the database. The parent build
209     // system should store the change state of the build description and tell
210     // the child which input resources have changed upon an update.
211     auto r = ctx.state[BuildState.buildDescId];
212     r.path = path;
213     if (r.update())
214     {
215         if (ctx.verbose)
216             println(ctx.color.status,
217                     ":: Build description changed. Syncing with the database...",
218                     ctx.color.reset);
219 
220         path.syncState(ctx.state, ctx.pool);
221 
222         // Update the build description resource
223         ctx.state[BuildState.buildDescId] = r;
224     }
225 }
226 
227 /**
228  * Builds pending vertices.
229  */
230 void update(ref BuildContext ctx)
231 {
232     import std.array : array;
233     import std.algorithm.iteration : filter;
234 
235     auto resources = ctx.state.pending!Resource.array;
236     auto tasks     = ctx.state.pending!Task.array;
237 
238     if (resources.length == 0 && tasks.length == 0)
239     {
240         if (ctx.verbose)
241         {
242             println(ctx.color.status, ":: ", ctx.color.success,
243                     "Nothing to do. Everything is up to date.", ctx.color.reset);
244         }
245 
246         return;
247     }
248 
249     // Print what we found.
250     if (ctx.verbose)
251     {
252         printfln(" - Found %s%d%s modified resource(s)",
253                 ctx.color.boldBlue, resources.length, ctx.color.reset);
254         printfln(" - Found %s%d%s pending task(s)",
255                 ctx.color.boldBlue, tasks.length, ctx.color.reset);
256 
257         println(ctx.color.status, ":: Building...", ctx.color.reset);
258     }
259 
260     auto subgraph = ctx.state.buildGraph(resources, tasks);
261     subgraph.build(ctx);
262 
263     if (ctx.verbose)
264         println(ctx.color.status, ":: ", ctx.color.success, "Build succeeded",
265                 ctx.color.reset);
266 }