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