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.resource;
10 import button.context;
11 import button.exceptions;
12 
13 /**
14  * A task key must be unique.
15  */
16 struct TaskKey
17 {
18     /**
19      * The commands to execute in sequential order. The first argument is the
20      * name of the executable.
21      */
22     immutable(Command)[] commands;
23 
24     /**
25      * The working directory for the commands, relative to the current working
26      * directory of the build system. If empty, the current working directory of
27      * the build system is used.
28      */
29     string workingDirectory = "";
30 
31     this(immutable(Command)[] commands, string workingDirectory = "")
32     {
33         assert(commands.length, "A task must have >0 commands");
34 
35         this.commands = commands;
36         this.workingDirectory = workingDirectory;
37     }
38 
39     /**
40      * Compares this key with another.
41      */
42     int opCmp()(const auto ref typeof(this) that) const pure nothrow
43     {
44         import std.algorithm.comparison : cmp;
45         import std.path : filenameCmp;
46 
47         if (immutable result = cmp(this.commands, that.commands))
48             return result;
49 
50         return filenameCmp(this.workingDirectory, that.workingDirectory);
51     }
52 
53     /// Ditto
54     bool opEquals()(const auto ref typeof(this) that) const pure nothrow
55     {
56         return this.opCmp(that) == 0;
57     }
58 }
59 
60 unittest
61 {
62     // Comparison
63     static assert(TaskKey([Command(["a", "b"])]) < TaskKey([Command(["a", "c"])]));
64     static assert(TaskKey([Command(["a", "c"])]) > TaskKey([Command(["a", "b"])]));
65     static assert(TaskKey([Command(["a", "b"])], "a") == TaskKey([Command(["a", "b"])], "a"));
66     static assert(TaskKey([Command(["a", "b"])], "a") != TaskKey([Command(["a", "b"])], "b"));
67     static assert(TaskKey([Command(["a", "b"])], "a") <  TaskKey([Command(["a", "b"])], "b"));
68 }
69 
70 unittest
71 {
72     import std.conv : to;
73 
74     // Converting commands to a string. This is used to store/retrieve tasks in
75     // the database.
76 
77     immutable t = TaskKey([
78             Command(["foo", "bar"]),
79             Command(["baz"]),
80             ]);
81 
82     assert(t.commands.to!string == `[["foo", "bar"], ["baz"]]`);
83 }
84 
85 /**
86  * A representation of a task.
87  */
88 struct Task
89 {
90     import std.datetime : SysTime;
91 
92     TaskKey key;
93 
94     alias key this;
95 
96     /**
97      * Time this task was last executed. If this is SysTime.min, then it is
98      * taken to mean that the task has never been executed before. This is
99      * useful for knowing if a task with no dependencies needs to be executed.
100      */
101     SysTime lastExecuted = SysTime.min;
102 
103     /**
104      * Text to display when running the task. If this is null, the commands
105      * themselves will be displayed. This is useful for reducing the amount of
106      * noise that is displayed.
107      */
108     string display;
109 
110     /**
111      * The result of executing a task.
112      */
113     struct Result
114     {
115         /**
116          * List of raw byte arrays of implicit inputs/outputs. There is one byte
117          * array per command.
118          */
119         Resource[] inputs, outputs;
120     }
121 
122     this(TaskKey key)
123     {
124         this.key = key;
125     }
126 
127     this(immutable(Command)[] commands, string workDir = "",
128             string display = null, SysTime lastExecuted = SysTime.min)
129     {
130         assert(commands.length, "A task must have >0 commands");
131 
132         this.commands = commands;
133         this.display = display;
134         this.workingDirectory = workDir;
135         this.lastExecuted = lastExecuted;
136     }
137 
138     /**
139      * Returns a string representation of the task.
140      *
141      * Since individual commands are in argv format, we format it into a string
142      * as one would enter into a shell.
143      */
144     string toPrettyString(bool verbose = false) const pure
145     {
146         import std.array : join;
147         import std.algorithm.iteration : map;
148 
149         if (display && !verbose)
150             return display;
151 
152         // Just use the first command
153         return commands[0].toPrettyString;
154     }
155 
156     /**
157      * Returns a short string representation of the task.
158      */
159     @property string toPrettyShortString() const pure nothrow
160     {
161         if (display)
162             return display;
163 
164         // Just use the first command
165         return commands[0].toPrettyShortString;
166     }
167 
168     /**
169      * Compares this task with another.
170      */
171     int opCmp()(const auto ref typeof(this) that) const pure nothrow
172     {
173         return this.key.opCmp(that.key);
174     }
175 
176     /// Ditto
177     bool opEquals()(const auto ref typeof(this) that) const pure nothrow
178     {
179         return opCmp(that) == 0;
180     }
181 
182     version (none) unittest
183     {
184         assert(Task([["a", "b"]]) < Task([["a", "c"]]));
185         assert(Task([["a", "b"]]) > Task([["a", "a"]]));
186 
187         assert(Task([["a", "b"]]) < Task([["a", "c"]]));
188         assert(Task([["a", "b"]]) > Task([["a", "a"]]));
189 
190         assert(Task([["a", "b"]])      == Task([["a", "b"]]));
191         assert(Task([["a", "b"]], "a") <  Task([["a", "b"]], "b"));
192         assert(Task([["a", "b"]], "b") >  Task([["a", "b"]], "a"));
193         assert(Task([["a", "b"]], "a") == Task([["a", "b"]], "a"));
194     }
195 }