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