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 }