1 /** 2 * Copyright: Copyright Jason White, 2016 3 * License: MIT 4 * Authors: Jason White 5 */ 6 module button.task; 7 8 import button.command; 9 import button.log; 10 import button.resource; 11 import button.context; 12 13 /** 14 * Thrown if a task fails. 15 */ 16 class TaskError : Exception 17 { 18 this(string msg) 19 { 20 super(msg); 21 } 22 } 23 24 /** 25 * A task key must be unique. 26 */ 27 struct TaskKey 28 { 29 /** 30 * The commands to execute in sequential order. The first argument is the 31 * name of the executable. 32 */ 33 immutable(Command)[] commands; 34 35 /** 36 * The working directory for the commands, relative to the current working 37 * directory of the build system. If empty, the current working directory of 38 * the build system is used. 39 */ 40 string workingDirectory = ""; 41 42 this(immutable(Command)[] commands, string workingDirectory = "") 43 { 44 assert(commands.length, "A task must have >0 commands"); 45 46 this.commands = commands; 47 this.workingDirectory = workingDirectory; 48 } 49 50 /** 51 * Compares this key with another. 52 */ 53 int opCmp()(const auto ref typeof(this) that) const pure nothrow 54 { 55 import std.algorithm.comparison : cmp; 56 import std.path : filenameCmp; 57 58 if (immutable result = cmp(this.commands, that.commands)) 59 return result; 60 61 return filenameCmp(this.workingDirectory, that.workingDirectory); 62 } 63 64 /// Ditto 65 bool opEquals()(const auto ref typeof(this) that) const pure nothrow 66 { 67 return this.opCmp(that) == 0; 68 } 69 } 70 71 unittest 72 { 73 // Comparison 74 static assert(TaskKey([Command(["a", "b"])]) < TaskKey([Command(["a", "c"])])); 75 static assert(TaskKey([Command(["a", "c"])]) > TaskKey([Command(["a", "b"])])); 76 static assert(TaskKey([Command(["a", "b"])], "a") == TaskKey([Command(["a", "b"])], "a")); 77 static assert(TaskKey([Command(["a", "b"])], "a") != TaskKey([Command(["a", "b"])], "b")); 78 static assert(TaskKey([Command(["a", "b"])], "a") < TaskKey([Command(["a", "b"])], "b")); 79 } 80 81 unittest 82 { 83 import std.conv : to; 84 85 // Converting commands to a string. This is used to store/retrieve tasks in 86 // the database. 87 88 immutable t = TaskKey([ 89 Command(["foo", "bar"]), 90 Command(["baz"]), 91 ]); 92 93 assert(t.commands.to!string == `[["foo", "bar"], ["baz"]]`); 94 } 95 96 /** 97 * A representation of a task. 98 */ 99 struct Task 100 { 101 import std.datetime : SysTime; 102 103 TaskKey key; 104 105 alias key this; 106 107 /** 108 * Time this task was last executed. If this is SysTime.min, then it is 109 * taken to mean that the task has never been executed before. This is 110 * useful for knowing if a task with no dependencies needs to be executed. 111 */ 112 SysTime lastExecuted = SysTime.min; 113 114 /** 115 * Text to display when running the task. If this is null, the commands 116 * themselves will be displayed. This is useful for reducing the amount of 117 * noise that is displayed. 118 */ 119 string display; 120 121 /** 122 * The result of executing a task. 123 */ 124 struct Result 125 { 126 /** 127 * List of raw byte arrays of implicit inputs/outputs. There is one byte 128 * array per command. 129 */ 130 Resource[] inputs, outputs; 131 } 132 133 this(TaskKey key) 134 { 135 this.key = key; 136 } 137 138 this(immutable(Command)[] commands, string workDir = "", 139 string display = null, SysTime lastExecuted = SysTime.min) 140 { 141 assert(commands.length, "A task must have >0 commands"); 142 143 this.commands = commands; 144 this.display = display; 145 this.workingDirectory = workDir; 146 this.lastExecuted = lastExecuted; 147 } 148 149 /** 150 * Returns a string representation of the task. 151 * 152 * Since individual commands are in argv format, we format it into a string 153 * as one would enter into a shell. 154 */ 155 string toPrettyString(bool verbose = false) const pure 156 { 157 import std.array : join; 158 import std.algorithm.iteration : map; 159 160 if (display && !verbose) 161 return display; 162 163 // Just use the first command 164 return commands[0].toPrettyString; 165 } 166 167 /** 168 * Returns a short string representation of the task. 169 */ 170 @property string toPrettyShortString() const pure nothrow 171 { 172 if (display) 173 return display; 174 175 // Just use the first command 176 return commands[0].toPrettyShortString; 177 } 178 179 /** 180 * Compares this task with another. 181 */ 182 int opCmp()(const auto ref typeof(this) that) const pure nothrow 183 { 184 return this.key.opCmp(that.key); 185 } 186 187 /// Ditto 188 bool opEquals()(const auto ref typeof(this) that) const pure nothrow 189 { 190 return opCmp(that) == 0; 191 } 192 193 version (none) unittest 194 { 195 assert(Task([["a", "b"]]) < Task([["a", "c"]])); 196 assert(Task([["a", "b"]]) > Task([["a", "a"]])); 197 198 assert(Task([["a", "b"]]) < Task([["a", "c"]])); 199 assert(Task([["a", "b"]]) > Task([["a", "a"]])); 200 201 assert(Task([["a", "b"]]) == Task([["a", "b"]])); 202 assert(Task([["a", "b"]], "a") < Task([["a", "b"]], "b")); 203 assert(Task([["a", "b"]], "b") > Task([["a", "b"]], "a")); 204 assert(Task([["a", "b"]], "a") == Task([["a", "b"]], "a")); 205 } 206 207 Result execute(ref BuildContext ctx, TaskLogger logger) 208 { 209 import std.array : appender; 210 211 // FIXME: Use a set instead? 212 auto inputs = appender!(Resource[]); 213 auto outputs = appender!(Resource[]); 214 215 foreach (command; commands) 216 { 217 auto result = command.execute(ctx, workingDirectory, logger); 218 219 // FIXME: Commands may have temporary inputs and outputs. For 220 // example, if one command creates a file and a later command 221 // deletes it, it should not end up in either of the input or output 222 // sets. 223 inputs.put(result.inputs); 224 outputs.put(result.outputs); 225 } 226 227 return Result(inputs.data, outputs.data); 228 } 229 }