1 /**
2  This module contains the core data definitions that allow a build
3  to be expressed in. $(D Build) is a container struct for top-level
4  targets, $(D Target) is the heart of the system.
5  */
6 
7 module reggae.build;
8 
9 import reggae.ctaa;
10 import reggae.rules.common: Language, getLanguage;
11 import reggae.options;
12 import reggae.path: buildPath, deabsolutePath;
13 
14 import std..string: replace;
15 import std.algorithm;
16 import std.path: dirSeparator;
17 import std.typetuple: allSatisfy;
18 import std.traits: Unqual, isSomeFunction, ReturnType, arity;
19 import std.array: array, join;
20 import std.conv;
21 import std.exception;
22 import std.typecons;
23 import std.range;
24 import std.typecons;
25 
26 
27 /**
28  Contains the top-level targets.
29  */
30 struct Build {
31     static struct TopLevelTarget {
32         Target target;
33         bool optional;
34     }
35 
36     private TopLevelTarget[] _targets;
37 
38     this(Target[] targets) {
39         _targets = targets.map!createTopLevelTarget.array;
40     }
41 
42     this(R)(R targets) if(isInputRange!R && is(Unqual!(ElementType!R) == TopLevelTarget)) {
43         _targets = targets.array;
44     }
45 
46     this(T...)(T targets) {
47         foreach(t; targets) {
48             //the constructor needs to go from Target to TopLevelTarget
49             //and accepts functions that return a parameter as well as parameters themselves
50             //if a function, call it, if not, take the value
51             //if the value is Target, call createTopLevelTarget, if not, take it as is
52             static if(isSomeFunction!(typeof(t)) && is(ReturnType!(typeof(t))) == Target) {
53                 _targets ~= createTopLevelTarget(t());
54             } else static if(is(Unqual!(typeof(t)) == TopLevelTarget)) {
55                 _targets ~= t;
56             } else {
57                 _targets ~= createTopLevelTarget(t);
58             }
59         }
60     }
61 
62     auto targets() @trusted pure nothrow {
63         return _targets.map!(a => a.target);
64     }
65 
66     auto defaultTargets() @trusted pure nothrow {
67         return _targets.filter!(a => !a.optional).map!(a => a.target);
68     }
69 
70     string[] defaultTargetsOutputs(in string projectPath) @trusted pure {
71         return defaultTargets.map!(a => a.expandOutputs(projectPath)).join();
72     }
73 
74     auto range() @safe pure {
75         import reggae.range;
76         return UniqueDepthFirst(this);
77     }
78 
79     ubyte[] toBytes(in Options options) @safe pure {
80         ubyte[] bytes;
81         bytes ~= setUshort(cast(ushort)targets.length);
82         foreach(t; targets) bytes ~= t.toBytes(options);
83         return bytes;
84     }
85 
86     static Build fromBytes(ubyte[] bytes) @trusted {
87         immutable length = getUshort(bytes);
88         auto build = Build();
89         foreach(_; 0 .. length) {
90             build._targets ~= TopLevelTarget(Target.fromBytes(bytes), false);
91         }
92         return build;
93     }
94 }
95 
96 
97 /**
98  Designate a target as optional so it won't be built by default.
99  "Compile-time" version that can be aliased
100  */
101 Build.TopLevelTarget optional(alias targetFunc)() {
102     return optional(targetFunc());
103 }
104 
105 /**
106  Designate a target as optional so it won't be built by default.
107  */
108 Build.TopLevelTarget optional(Target target) {
109     return createTopLevelTarget(target, true);
110 }
111 
112 Build.TopLevelTarget createTopLevelTarget(Target target, bool optional = false) {
113     return Build.TopLevelTarget(target.inTopLevelObjDirOf(objDirOf(target), Yes.topLevel), optional);
114 }
115 
116 
117 immutable gBuilddir = "$builddir";
118 immutable gProjdir  = "$project";
119 
120 //a directory for each top-level target to avoid name clashes
121 //@trusted because of map -> buildPath -> array
122 Target inTopLevelObjDirOf(Target target, string objDir, Flag!"topLevel" isTopLevel = No.topLevel) @trusted {
123     if (target._outputs.any!(a => a.startsWith(gBuilddir) || a.startsWith(gProjdir)))
124         objDir = objDirOf(target);
125 
126     // base dir for relative outputs:
127     const baseDir = isTopLevel ? null :        // don't touch top-level target outputs
128                     target.isLeaf ? gProjdir : // leaf targets are src files in the project dir
129                     objDir;                    // obj directory specific to top-level target
130 
131     auto outputs = target._outputs.map!(a => expandOutput(a, gProjdir, baseDir)).array;
132 
133     return Target(outputs,
134                   target._command.expandVariables,
135                   target._dependencies.map!(a => a.inTopLevelObjDirOf(objDir)).array,
136                   target._implicits.map!(a => a.inTopLevelObjDirOf(objDir)).array);
137 }
138 
139 
140 string objDirOf(in Target target) @safe pure nothrow {
141     // remove $builddir and shorten $project to __project__
142     const output = expandOutput(target._outputs[0], "__project__");
143     return buildPath(".reggae", "objs", output.deabsolutePath ~ ".objs");
144 }
145 
146 string expandOutput(string path, in string projectPath, in string basePath = null) @safe pure nothrow {
147     path = buildPath(path); // normalize to native slashes
148     // $builddir/foo => foo
149     if (path.startsWith(gBuilddir ~ dirSeparator)) return path[gBuilddir.length+1 .. $];
150     // $project/foo => <projectPath>/foo
151     if (path.startsWith(gProjdir ~ dirSeparator)) return buildPath(projectPath, path[gProjdir.length+1 .. $]);
152     // don't touch paths starting with env variables
153     if (path.startsWith("$")) return path;
154     // /foo => /foo
155     // bar => <basePath>/bar
156     return buildPath(basePath, path);
157 }
158 
159 
160 enum isTarget(alias T) =
161      is(Unqual!(typeof(T)) == Target) ||
162      is(Unqual!(typeof(T)) == Build.TopLevelTarget) ||
163      isSomeFunction!T && is(ReturnType!T == Target) ||
164      isSomeFunction!T && is(ReturnType!T == Build.TopLevelTarget);
165 
166 unittest {
167     auto  t1 = Target();
168     const t2 = Target();
169     static assert(isTarget!t1);
170     static assert(isTarget!t2);
171     const t3 = Build.TopLevelTarget(Target());
172     static assert(isTarget!t3);
173 }
174 
175 mixin template buildImpl(targets...) if(allSatisfy!(isTarget, targets)) {
176     Build buildFunc() {
177         return Build(targets);
178     }
179 }
180 
181 /**
182  Two variations on a template mixin. When reggae is used as a library,
183  this will essentially build reggae itself as part of the build description.
184 
185  When reggae is used as a command-line tool to generate builds, it simply
186  declares the build function that will be called at run-time. The tool
187  will then compile the user's reggaefile.d with the reggae libraries,
188  resulting in a buildgen executable.
189 
190  In either case, the compile-time parameters of $(D build) are the
191  build's top-level targets.
192  */
193 version(reggaelib) {
194     mixin template build(targets...) if(allSatisfy!(isTarget, targets)) {
195         mixin reggaeGen!(targets);
196     }
197 } else {
198     alias build = buildImpl;
199 }
200 
201 package template isBuildFunction(alias T) {
202     static if(!isSomeFunction!T) {
203         enum isBuildFunction = false;
204     } else {
205         enum isBuildFunction = is(ReturnType!T == Build) && arity!T == 0;
206     }
207 }
208 
209 unittest {
210     Build myBuildFunction() { return Build(); }
211     static assert(isBuildFunction!myBuildFunction);
212     float foo;
213     static assert(!isBuildFunction!foo);
214 }
215 
216 
217 private static auto arrayify(E, T)(T value) {
218     static if(isInputRange!T && is(Unqual!(ElementType!T) == E))
219         return value.array;
220     else static if(is(Unqual!T == E))
221         return [value];
222     else static if(is(Unqual!T == void[])) {
223         E[] nothing;
224         return nothing;
225     } else static if(is(Unqual!T == string))
226         return [E(value)];
227     else {
228         import std.conv: text;
229         static assert(false, text("Can not arraify value of type ", T.stringof));
230     }
231 }
232 
233 
234 /**
235  The core of reggae's D-based DSL for describing build systems.
236  Targets contain outputs, a command to generate those outputs,
237  explicit dependencies and implicit dependencies. All dependencies
238  are themselves $(D Target) structs.
239 
240  The command is given as a string. In this string, certain words
241  have special meaning: $(D $in), $(D $out), $(D $project) and $(D builddir).
242 
243  $(D $in) gets expanded to all explicit dependencies.
244  $(D $out) gets expanded to all outputs.
245  $(D $project) gets expanded to the project directory (i.e. the directory including
246  the source files to build that was given as a command-line argument). This can be
247  useful when build outputs are to be placed in the source directory, such as
248  automatically generated source files.
249  $(D $builddir) expands to the build directory (i.e. where reggae was run from).
250  */
251 struct Target {
252     private string[] _outputs;
253     private Command _command; ///see $(D Command) struct
254     private Target[] _dependencies;
255     private Target[] _implicits;
256 
257     enum Target[] noTargets = [];
258 
259     this(string output) @safe pure nothrow {
260         this(output, "", noTargets, noTargets);
261     }
262 
263     this(O, C)(O outputs, C command) {
264         this(outputs, command, noTargets, noTargets);
265     }
266 
267     this(O, C, D)(O outputs, C command, D dependencies) {
268         this(outputs, command, dependencies, noTargets);
269     }
270 
271     this(O, C, D, I)(O outputs, C command, D dependencies, I implicits) {
272 
273         this._outputs = arrayify!string(outputs);
274 
275         static if(is(C == Command))
276             this._command = command;
277         else
278             this._command = Command(command);
279 
280         this._dependencies = arrayify!Target(dependencies);
281         this._implicits = arrayify!Target(implicits);
282     }
283 
284     /**
285        The outputs without expanding special variables
286      */
287     @property inout(string)[] rawOutputs(in string projectPath = "") @safe pure inout {
288         return _outputs;
289     }
290 
291     @property inout(Target)[] dependencyTargets(in string projectPath = "") @safe pure nothrow inout {
292         return _dependencies;
293     }
294 
295     @property inout(Target)[] implicitTargets(in string projectPath = "") @safe pure nothrow inout {
296         return _implicits;
297     }
298 
299     @property string[] dependenciesInProjectPath(in string projectPath) @safe pure const {
300         return depsInProjectPath(_dependencies, projectPath);
301     }
302 
303     @property string[] implicitsInProjectPath(in string projectPath) @safe pure const {
304         return depsInProjectPath(_implicits, projectPath);
305     }
306 
307     bool isLeaf() @safe pure const nothrow {
308         return _dependencies is null && _implicits is null && getCommandType == CommandType.shell && _command.command == "";
309     }
310 
311     Language getLanguage() @safe pure const nothrow {
312         import reggae.range: Leaves;
313         import reggae.rules.common: getLanguage;
314         import std.algorithm: any;
315 
316         auto leaves = () @trusted { return Leaves(this).array; }();
317 
318         foreach(language; [Language.D, Language.Cplusplus, Language.C]) {
319             if(leaves.any!(a => a._outputs.length && .getLanguage(a._outputs[0]) == language))
320                 return language;
321         }
322 
323         return Language.unknown;
324     }
325 
326     ///Replace special variables and return a list of outputs thus modified
327     string[] expandOutputs(in string projectPath) @safe pure const {
328         return _outputs.map!(o => expandOutput(o, projectPath)).array;
329     }
330 
331     //@trusted because of replace
332     string rawCmdString(in string projectPath = "") @safe pure const {
333         return _command.rawCmdString(projectPath);
334     }
335 
336     ///returns a command string to be run by the shell
337     string shellCommand(in Options options,
338                         Flag!"dependencies" deps = Yes.dependencies) @safe pure const {
339         return _command.shellCommand(options, getLanguage(), _outputs, inputs(options.projectPath), deps);
340     }
341 
342     // not const because the code commands take inputs and outputs as non-const strings
343     const(string)[] execute(in Options options) @safe const {
344         return _command.execute(options, getLanguage(), _outputs, inputs(options.projectPath));
345     }
346 
347     bool hasDefaultCommand() @safe const pure {
348         return _command.isDefaultCommand;
349     }
350 
351     CommandType getCommandType() @safe pure const nothrow {
352         return _command.getType;
353     }
354 
355     string[] getCommandParams(in string projectPath, in string key, string[] ifNotFound) @safe pure const {
356         return _command.getParams(projectPath, key, ifNotFound);
357     }
358 
359     const(string)[] commandParamNames() @safe pure nothrow const {
360         return _command.paramNames;
361     }
362 
363     static Target phony(T...)(string name, string shellCommand, T args) {
364         return Target(name, Command.phony(shellCommand), args);
365     }
366 
367     string toString(in Options options) nothrow const {
368         try {
369             if(isLeaf) return _outputs[0];
370             immutable _outputs = _outputs.length == 1 ? `"` ~ _outputs[0] ~ `"` : text(_outputs);
371             immutable depsStr = _dependencies.length == 0 ? "" : text(_dependencies);
372             immutable impsStr = _implicits.length == 0 ? "" : text(_implicits);
373             auto parts = [text(_outputs), `"` ~ shellCommand(options) ~ `"`];
374             if(depsStr != "") parts ~= depsStr;
375             if(impsStr != "") parts ~= impsStr;
376             return text("Target(", parts.join(",\n"), ")");
377         } catch(Exception) {
378             assert(0);
379         }
380     }
381 
382     ubyte[] toBytes(in Options options) @safe pure const {
383         ubyte[] bytes;
384         bytes ~= setUshort(cast(ushort)_outputs.length);
385         foreach(output; _outputs) {
386             bytes ~= arrayToBytes(expandOutput(output, options.projectPath, isLeaf ? options.projectPath : null));
387         }
388 
389         bytes ~= arrayToBytes(shellCommand(options));
390 
391         bytes ~= setUshort(cast(ushort)_dependencies.length);
392         foreach(dep; _dependencies) bytes ~= dep.toBytes(options);
393 
394         bytes ~= setUshort(cast(ushort)_implicits.length);
395         foreach(imp; _implicits) bytes ~= imp.toBytes(options);
396 
397         return bytes;
398     }
399 
400     static Target fromBytes(ref ubyte[] bytes) @trusted pure nothrow {
401         string[] outputs;
402         immutable numOutputs = getUshort(bytes);
403 
404         foreach(i; 0 .. numOutputs) {
405             outputs ~= cast(string)bytesToArray!char(bytes);
406         }
407 
408         auto command = Command(cast(string)bytesToArray!char(bytes));
409 
410         Target[] dependencies;
411         immutable numDeps = getUshort(bytes);
412         foreach(i; 0..numDeps) dependencies ~= Target.fromBytes(bytes);
413 
414         Target[] implicits;
415         immutable numImps = getUshort(bytes);
416         foreach(i; 0..numImps) implicits ~= Target.fromBytes(bytes);
417 
418         return Target(outputs, command, dependencies, implicits);
419     }
420 
421     bool opEquals()(auto ref const Target other) @safe pure const {
422 
423         bool sameSet(T)(const(T)[] fst, const(T)[] snd) {
424             if(fst.length != snd.length) return false;
425             return fst.all!(a => snd.any!(b => a == b));
426         }
427 
428         return
429             sameSet(_outputs, other._outputs) &&
430             _command == other._command &&
431             sameSet(_dependencies, other._dependencies) &&
432             sameSet(_implicits, other._implicits);
433     }
434 
435 private:
436 
437     string[] depsInProjectPath(in Target[] deps, in string projectPath) @safe pure const {
438         import reggae.range;
439         return deps.map!(a => a.expandOutputs(projectPath)).join;
440     }
441 
442     string[] inputs(in string projectPath) @safe pure nothrow const {
443         //functional didn't work here, I don't know why so sticking with loops for now
444         string[] inputs;
445         foreach(dep; _dependencies) {
446             foreach(output; dep._outputs) {
447                 //leaf objects are references to source files in the project path,
448                 //those need their path built. Any other dependencies are in the
449                 //build path, so they don't need the same treatment
450                 inputs ~= expandOutput(output, projectPath, dep.isLeaf ? projectPath : null);
451             }
452         }
453         return inputs;
454     }
455 }
456 
457 
458 enum CommandType {
459     shell,
460     compile,
461     link,
462     compileAndLink,
463     code,
464     phony,
465 }
466 
467 alias CommandFunction = void function(in string[], in string[]);
468 alias CommandDelegate = void delegate(in string[], in string[]);
469 
470 /**
471  A command to be execute to produce a targets outputs from its inputs.
472  In general this will be a shell command, but the high-level rules
473  use commands with known semantics (compilation, linking, etc)
474 */
475 struct Command {
476     alias Params = AssocList!(string, string[]);
477 
478     private string command;
479     private CommandType type;
480     private Params params;
481     private CommandFunction function_;
482     private CommandDelegate delegate_;
483 
484     ///If constructed with a string, it's a shell command
485     this(string shellCommand) @safe pure nothrow {
486         command = shellCommand;
487         type = CommandType.shell;
488     }
489 
490     /**Explicitly request a command of this type with these parameters
491        In general to create one of the builtin high level rules*/
492     this(CommandType type, Params params = Params()) @safe pure {
493         if(type == CommandType.shell || type == CommandType.code)
494             throw new Exception("Command rule cannot be shell or code");
495         this.type = type;
496         this.params = params;
497     }
498 
499     ///A D function call command
500     this(CommandDelegate dg) @safe pure nothrow {
501         type = CommandType.code;
502         this.delegate_ = dg;
503     }
504 
505     ///A D function call command
506     this(CommandFunction func) @safe pure nothrow {
507         type = CommandType.code;
508         this.function_ = func;
509     }
510 
511     static Command phony(in string shellCommand) @safe pure nothrow {
512         Command cmd;
513         cmd.type = CommandType.phony;
514         cmd.command = shellCommand;
515         return cmd;
516     }
517 
518     const(string)[] paramNames() @safe pure nothrow const {
519         return params.keys;
520     }
521 
522     CommandType getType() @safe pure const nothrow {
523         return type;
524     }
525 
526     bool isDefaultCommand() @safe pure const {
527         return type == CommandType.compile || type == CommandType.link || type == CommandType.compileAndLink;
528     }
529 
530     string[] getParams(in string projectPath, in string key, string[] ifNotFound) @safe pure const {
531         return getParams(projectPath, key, true, ifNotFound);
532     }
533 
534     Command expandVariables() @safe pure {
535         switch(type) with(CommandType) {
536             case shell:
537             case phony:
538                 string expCommand = command.replace(gBuilddir ~ dirSeparator, "");
539                 version(Windows)
540                     expCommand = expCommand.replace(gBuilddir ~ "/", "");
541                 expCommand = expCommand.replace(gBuilddir, ".");
542                 auto cmd = Command(expCommand);
543                 cmd.type = this.type;
544                 return cmd;
545             default:
546                 return this;
547         }
548     }
549 
550     ///Replace $in, $out, $project with values and remove $builddir
551     private static string expandCmd(in string cmd, in string projectPath,
552                                     in string[] outputs, in string[] inputs) @safe pure {
553         auto outs = outputs.map!buildPath;
554         auto ins = inputs.map!buildPath;
555         auto replaceIn = cmd.dup.replace("$in", ins.join(" "));
556         auto replaceOut = replaceIn.replace("$out", outs.join(" "));
557         auto r = replaceOut.replace(gProjdir, buildPath(projectPath));
558         r = r.replace(gBuilddir ~ dirSeparator, "");
559         version(Windows)
560             r = r.replace(gBuilddir ~ "/", "");
561         r = r.replace(gBuilddir, ".");
562         return r;
563     }
564 
565     string rawCmdString(in string projectPath) @safe pure const {
566         if(getType != CommandType.shell)
567             throw new Exception("Command type 'code' not supported for ninja backend");
568         return command.replace(gProjdir, buildPath(projectPath));
569     }
570 
571     private string[] getParams(string projectPath, in string key,
572                                bool useIfNotFound, string[] ifNotFound = []) @safe pure const {
573         projectPath = buildPath(projectPath);
574         return params.get(key, ifNotFound).map!(a => a.replace(gProjdir, projectPath)).array;
575     }
576 
577     static private string getDefaultDCompilerModelArg(in Options options) @safe pure nothrow {
578         version(Windows) {
579             import std.path: baseName, stripExtension;
580             const isDMD = baseName(stripExtension(options.dCompiler)) == "dmd";
581             return isDMD ? " -m32mscoff" : null;
582         } else {
583             return null;
584         }
585     }
586 
587     static string builtinTemplate(in CommandType type,
588                                   in Language language,
589                                   in Options options,
590                                   in Flag!"dependencies" deps = Yes.dependencies) @safe pure {
591 
592         final switch(type) with(CommandType) {
593             case phony:
594                 assert(0, "builtinTemplate cannot be phony");
595 
596             case shell:
597                 assert(0, "builtinTemplate cannot be shell");
598 
599             case link: {
600                 version(Windows)
601                     immutable cArgs = " /nologo /Fo$out $flags $in";
602                 else
603                     immutable cArgs = " -o $out $flags $in";
604 
605                 final switch(language) with(Language) {
606                     case D:
607                     case unknown:
608                         return options.dCompiler ~ getDefaultDCompilerModelArg(options) ~ " -of$out $flags $in";
609                     case Cplusplus:
610                         return options.cppCompiler ~ cArgs;
611                     case C:
612                         return options.cCompiler ~ cArgs;
613                 }
614             }
615 
616             case code:
617                 throw new Exception("Command type 'code' has no built-in template");
618 
619             case compile:
620                 return compileTemplate(type, language, options, deps).replace("$out $in", "$out -c $in");
621 
622             case compileAndLink:
623                 return compileTemplate(type, language, options, deps);
624         }
625     }
626 
627     private static string compileTemplate(in CommandType type,
628                                           in Language language,
629                                           in Options options,
630                                           in Flag!"dependencies" deps = Yes.dependencies) @safe pure {
631         version(Windows)
632         {
633             immutable ccParams =
634                 " /nologo $flags $includes" ~ (deps ? " /showIncludes" : null) ~ " /Fo$out $in";
635         }
636         else
637         {
638             immutable ccParams = deps
639                 ? " $flags $includes -MMD -MT $out -MF $out.dep -o $out $in"
640                 : " $flags $includes -o $out $in";
641         }
642 
643         final switch(language) with(Language) {
644             case D: {
645                 const modelArg = getDefaultDCompilerModelArg(options);
646                 return deps
647                     ? buildPath(".reggae/dcompile") ~ " --objFile=$out --depFile=$out.dep " ~
648                       options.dCompiler ~ modelArg ~ " $flags $includes $stringImports $in"
649                     : options.dCompiler ~ modelArg ~ " $flags $includes $stringImports -of$out $in";
650             }
651             case Cplusplus:
652                 return options.cppCompiler ~ ccParams;
653             case C:
654                 return options.cCompiler ~ ccParams;
655             case unknown:
656                 throw new Exception("Unsupported language for compiling");
657         }
658     }
659 
660     string defaultCommand(in Options options,
661                           in Language language,
662                           in string[] outputs,
663                           in string[] inputs,
664                           Flag!"dependencies" deps = Yes.dependencies) @safe pure const {
665 
666         import std.conv: text;
667 
668         assert(isDefaultCommand, text("This command is not a default command: ", this));
669         string cmd;
670         try
671             cmd = builtinTemplate(type, language, options, deps);
672         catch(Exception ex) {
673             throw new Exception(text(ex.msg, "\noutputs: ", outputs, "\ninputs: ", inputs));
674         }
675 
676         foreach(key; params.keys) {
677             immutable var = "$" ~ key;
678             immutable value = getParams(options.projectPath, key, []).join(" ");
679             cmd = cmd.replace(var, value);
680         }
681         return expandCmd(cmd, options.projectPath, outputs, inputs);
682     }
683 
684     ///returns a command string to be run by the shell
685     string shellCommand(in Options options,
686                         in Language language,
687                         in string[] outputs,
688                         in string[] inputs,
689                         Flag!"dependencies" deps = Yes.dependencies) @safe pure const {
690         return isDefaultCommand
691             ? defaultCommand(options, language, outputs, inputs, deps)
692             : expandCmd(command, options.projectPath, outputs, inputs);
693     }
694 
695     const(string)[] execute(in Options options, in Language language,
696                             in string[] outputs, in string[] inputs) const @trusted {
697         import std.process;
698 
699         final switch(type) with(CommandType) {
700             case shell:
701             case compile:
702             case link:
703             case compileAndLink:
704             case phony:
705                 immutable cmd = shellCommand(options, language, outputs, inputs);
706                 if(cmd == "") return outputs;
707 
708                 const string[string] env = null;
709                 Config config = Config.none;
710                 size_t maxOutput = size_t.max;
711 
712                 immutable res = executeShell(cmd, env, config, maxOutput, options.workingDir);
713                 enforce(res.status == 0, "Could not execute phony " ~ cmd ~ ":\n" ~ res.output);
714                 return [res.output];
715             case code:
716                 assert(function_ !is null || delegate_ !is null,
717                        "Command of type code with null function");
718                 function_ !is null ? function_(inputs, outputs) : delegate_(inputs, outputs);
719                 return ["code"];
720         }
721     }
722 
723     ubyte[] toBytes() @safe pure nothrow const {
724         final switch(type) {
725 
726         case CommandType.shell:
727             return [cast(ubyte)type] ~ cast(ubyte[])command.dup;
728 
729         case CommandType.compile:
730         case CommandType.compileAndLink:
731         case CommandType.link:
732         case CommandType.phony:
733             ubyte[] bytes;
734             bytes ~= cast(ubyte)type;
735             bytes ~= cast(ubyte)(params.keys.length >> 8);
736             bytes ~= (params.keys.length & 0xff);
737             foreach(key; params.keys) {
738                 bytes ~= arrayToBytes(key);
739                 bytes ~= cast(ubyte)(params[key].length >> 8);
740                 bytes ~= (params[key].length & 0xff);
741                 foreach(value; params[key])
742                     bytes ~= arrayToBytes(value);
743             }
744             return bytes;
745 
746         case CommandType.code:
747             assert(0);
748         }
749     }
750 
751     static Command fromBytes(ubyte[] bytes) @trusted pure {
752         immutable type = cast(CommandType)bytes[0];
753         bytes = bytes[1..$];
754 
755         final switch(type) {
756 
757         case CommandType.shell:
758             char[] chars;
759             foreach(b; bytes) chars ~= cast(char)b;
760             return Command(cast(string)chars);
761 
762         case CommandType.compile:
763         case CommandType.compileAndLink:
764         case CommandType.link:
765         case CommandType.phony:
766             Params params;
767 
768             immutable numKeys = getUshort(bytes);
769             foreach(i; 0..numKeys) {
770                 immutable key = cast(string)bytesToArray!char(bytes);
771                 immutable numValues = getUshort(bytes);
772 
773                 string[] values;
774                 foreach(j; 0..numValues) {
775                     values ~= bytesToArray!char(bytes);
776                 }
777                 params[key] = values;
778             }
779             return Command(type, params);
780 
781         case CommandType.code:
782             throw new Exception("Cannot serialise Command of type code");
783         }
784     }
785 
786     string toString() const pure @safe {
787         final switch(type) with(CommandType) {
788             case shell:
789             case phony:
790                 return `Command("` ~ command ~ `")`;
791             case compile:
792             case link:
793             case compileAndLink:
794             case code:
795                 return `Command(` ~ type.to!string ~
796                     (params.keys.length ? ", " ~ text(params) : "") ~
797                     `)`;
798         }
799     }
800 }
801 
802 
803 private ubyte[] arrayToBytes(T)(in T[] arr) {
804     auto bytes = new ubyte[arr.length + 2];
805     immutable length = cast(ushort)arr.length;
806     bytes[0] = length >> 8;
807     bytes[1] = length & 0xff;
808     foreach(i, c; arr) bytes[i + 2] = cast(ubyte)c;
809     return bytes;
810 }
811 
812 
813 private T[] bytesToArray(T)(ref ubyte[] bytes) {
814     T[] arr;
815     arr.length = getUshort(bytes);
816     foreach(i, b; bytes[0 .. arr.length]) arr[i] = cast(T)b;
817     bytes = bytes[arr.length .. $];
818     return arr;
819 }
820 
821 
822 private ushort getUshort(ref ubyte[] bytes) @safe pure nothrow {
823     immutable length = (bytes[0] << 8) + bytes[1];
824     bytes = bytes[2..$];
825     return length;
826 }
827 
828 private ubyte[] setUshort(in ushort length) @safe pure nothrow {
829     auto bytes = new ubyte[2];
830     bytes[0] = length >> 8;
831     bytes[1] = length & 0xff;
832     return bytes;
833 }
834 
835 
836 string replaceConcreteCompilersWithVars(in string cmd, in Options options) @safe pure nothrow {
837     return cmd.
838         replace(options.dCompiler, "$(DC)").
839         replace(options.cppCompiler, "$(CXX)").
840         replace(options.cCompiler, "$(CC)");
841 }