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 }