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     return buildPath("objs", target._outputs[0].expandBuildDir ~ ".objs");
146 }
147 
148 //targets that have outputs with $builddir or $project in them want to be placed
149 //in a specific place. Those don't get touched. Other targets get
150 //placed in their top-level parent's object directory
151 string realTargetPath(in string dirName, in Target target, in string output) @trusted pure {
152     return target.isLeaf
153         ? output
154         : realTargetPath(dirName, output);
155 }
156 
157 
158 //targets that have outputs with $builddir or $project in them want to be placed
159 //in a specific place. Those don't get touched. Other targets get
160 //placed in their top-level parent's object directory
161 string realTargetPath(in string dirName, in string output) @trusted pure {
162     import std.algorithm: canFind;
163 
164     if(output.startsWith(gProjdir)) return output;
165 
166     return output.canFind(gBuilddir)
167         ? output.expandBuildDir
168         : buildPath(dirName, output);
169 }
170 
171 //replace $builddir with the current directory
172 string expandBuildDir(in string output) @trusted pure {
173     import std.path: buildNormalizedPath;
174     import std.algorithm;
175     return output.
176         splitter.
177         map!(a => a.canFind(gBuilddir) ? a.replace(gBuilddir, ".").buildNormalizedPath : a).
178         join(" ");
179 }
180 
181  enum isTarget(alias T) =
182      is(Unqual!(typeof(T)) == Target) ||
183      is(Unqual!(typeof(T)) == Build.TopLevelTarget) ||
184      isSomeFunction!T && is(ReturnType!T == Target) ||
185      isSomeFunction!T && is(ReturnType!T == Build.TopLevelTarget);
186 
187 unittest {
188     auto  t1 = Target();
189     const t2 = Target();
190     static assert(isTarget!t1);
191     static assert(isTarget!t2);
192     const t3 = Build.TopLevelTarget(Target());
193     static assert(isTarget!t3);
194 }
195 
196 mixin template buildImpl(targets...) if(allSatisfy!(isTarget, targets)) {
197     Build buildFunc() {
198         return Build(targets);
199     }
200 }
201 
202 /**
203  Two variations on a template mixin. When reggae is used as a library,
204  this will essentially build reggae itself as part of the build description.
205 
206  When reggae is used as a command-line tool to generate builds, it simply
207  declares the build function that will be called at run-time. The tool
208  will then compile the user's reggaefile.d with the reggae libraries,
209  resulting in a buildgen executable.
210 
211  In either case, the compile-time parameters of $(D build) are the
212  build's top-level targets.
213  */
214 version(reggaelib) {
215     mixin template build(targets...) if(allSatisfy!(isTarget, targets)) {
216         mixin reggaeGen!(targets);
217     }
218 } else {
219     alias build = buildImpl;
220 }
221 
222 package template isBuildFunction(alias T) {
223     static if(!isSomeFunction!T) {
224         enum isBuildFunction = false;
225     } else {
226         enum isBuildFunction = is(ReturnType!T == Build) && arity!T == 0;
227     }
228 }
229 
230 unittest {
231     Build myBuildFunction() { return Build(); }
232     static assert(isBuildFunction!myBuildFunction);
233     float foo;
234     static assert(!isBuildFunction!foo);
235 }
236 
237 
238 private static auto arrayify(E, T)(T value) {
239     static if(isInputRange!T && is(Unqual!(ElementType!T) == E))
240         return value.array;
241     else static if(is(Unqual!T == E))
242         return [value];
243     else static if(is(Unqual!T == void[])) {
244         E[] nothing;
245         return nothing;
246     } else static if(is(Unqual!T == string))
247         return [E(value)];
248     else {
249         import std.conv: text;
250         static assert(false, text("Can not arraify value of type ", T.stringof));
251     }
252 }
253 
254 
255 /**
256  The core of reggae's D-based DSL for describing build systems.
257  Targets contain outputs, a command to generate those outputs,
258  explicit dependencies and implicit dependencies. All dependencies
259  are themselves $(D Target) structs.
260 
261  The command is given as a string. In this string, certain words
262  have special meaning: $(D $in), $(D $out), $(D $project) and $(D builddir).
263 
264  $(D $in) gets expanded to all explicit dependencies.
265  $(D $out) gets expanded to all outputs.
266  $(D $project) gets expanded to the project directory (i.e. the directory including
267  the source files to build that was given as a command-line argument). This can be
268  useful when build outputs are to be placed in the source directory, such as
269  automatically generated source files.
270  $(D $builddir) expands to the build directory (i.e. where reggae was run from).
271  */
272 struct Target {
273     private string[] _outputs;
274     private Command _command; ///see $(D Command) struct
275     private Target[] _dependencies;
276     private Target[] _implicits;
277 
278     enum Target[] noTargets = [];
279 
280     this(string output) @safe pure nothrow {
281         this(output, "", noTargets, noTargets);
282     }
283 
284     this(O, C)(O outputs, C command) {
285         this(outputs, command, noTargets, noTargets);
286     }
287 
288     this(O, C, D)(O outputs, C command, D dependencies) {
289         this(outputs, command, dependencies, noTargets);
290     }
291 
292     this(O, C, D, I)(O outputs, C command, D dependencies, I implicits) {
293 
294         this._outputs = arrayify!string(outputs);
295 
296         static if(is(C == Command))
297             this._command = command;
298         else
299             this._command = Command(command);
300 
301         this._dependencies = arrayify!Target(dependencies);
302         this._implicits = arrayify!Target(implicits);
303     }
304 
305     /**
306        The outputs without expanding special variables
307      */
308     @property inout(string)[] rawOutputs(in string projectPath = "") @safe pure inout {
309         return _outputs;
310     }
311 
312     @property inout(Target)[] dependencyTargets(in string projectPath = "") @safe pure nothrow inout {
313         return _dependencies;
314     }
315 
316     @property inout(Target)[] implicitTargets(in string projectPath = "") @safe pure nothrow inout {
317         return _implicits;
318     }
319 
320     @property string[] dependenciesInProjectPath(in string projectPath) @safe pure const {
321         return depsInProjectPath(_dependencies, projectPath);
322     }
323 
324     @property string[] implicitsInProjectPath(in string projectPath) @safe pure const {
325         return depsInProjectPath(_implicits, projectPath);
326     }
327 
328     bool isLeaf() @safe pure const nothrow {
329         return _dependencies is null && _implicits is null && getCommandType == CommandType.shell && _command.command == "";
330     }
331 
332     Language getLanguage() @safe pure const nothrow {
333         import reggae.range: Leaves;
334         const leaves = () @trusted { return Leaves(this).array; }();
335         foreach(language; [Language.D, Language.Cplusplus, Language.C]) {
336             if(leaves.any!(a => a._outputs.length && reggae.rules.common.getLanguage(a._outputs[0]) == language))
337                 return language;
338         }
339 
340         return Language.unknown;
341     }
342 
343     ///Replace special variables and return a list of outputs thus modified
344     auto expandOutputs(in string projectPath) @safe pure const {
345 
346         string inProjectPath(in string path) {
347 
348             return path.startsWith(gProjdir)
349                 ? path
350                 : path.startsWith(gBuilddir)
351                     ? path.replace(gBuilddir ~ dirSeparator, "")
352                     : path[0] == '$'
353                         ? path
354                         : buildPath(projectPath, path);
355         }
356 
357         return _outputs.map!(a => isLeaf ? inProjectPath(a) : a).
358             map!(a => a.replace("$project", projectPath)).
359             map!(a => expandBuildDir(a)).
360             array;
361     }
362 
363     //@trusted because of replace
364     string rawCmdString(in string projectPath = "") @safe pure const {
365         return _command.rawCmdString(projectPath);
366     }
367 
368     ///returns a command string to be run by the shell
369     string shellCommand(in Options options,
370                         Flag!"dependencies" deps = Yes.dependencies) @safe pure const {
371         return _command.shellCommand(options, getLanguage(), _outputs, inputs(options.projectPath), deps);
372     }
373 
374     // not const because the code commands take inputs and outputs as non-const strings
375     const(string)[] execute(in Options options) @safe const {
376         return _command.execute(options, getLanguage(), _outputs, inputs(options.projectPath));
377     }
378 
379     bool hasDefaultCommand() @safe const pure {
380         return _command.isDefaultCommand;
381     }
382 
383     CommandType getCommandType() @safe pure const nothrow {
384         return _command.getType;
385     }
386 
387     string[] getCommandParams(in string projectPath, in string key, string[] ifNotFound) @safe pure const {
388         return _command.getParams(projectPath, key, ifNotFound);
389     }
390 
391     const(string)[] commandParamNames() @safe pure nothrow const {
392         return _command.paramNames;
393     }
394 
395     static Target phony(T...)(string name, string shellCommand, T args) {
396         return Target(name, Command.phony(shellCommand), args);
397     }
398 
399     string toString(in Options options) nothrow const {
400         try {
401             if(isLeaf) return _outputs[0];
402             immutable _outputs = _outputs.length == 1 ? `"` ~ _outputs[0] ~ `"` : text(_outputs);
403             immutable depsStr = _dependencies.length == 0 ? "" : text(_dependencies);
404             immutable impsStr = _implicits.length == 0 ? "" : text(_implicits);
405             auto parts = [text(_outputs), `"` ~ shellCommand(options) ~ `"`];
406             if(depsStr != "") parts ~= depsStr;
407             if(impsStr != "") parts ~= impsStr;
408             return text("Target(", parts.join(",\n"), ")");
409         } catch(Exception) {
410             assert(0);
411         }
412     }
413 
414     ubyte[] toBytes(in Options options) @safe pure const {
415         ubyte[] bytes;
416         bytes ~= setUshort(cast(ushort)_outputs.length);
417         foreach(output; _outputs) {
418             bytes ~= arrayToBytes(isLeaf ? inProjectPath(options.projectPath, output) : output);
419         }
420 
421         bytes ~= arrayToBytes(shellCommand(options));
422 
423         bytes ~= setUshort(cast(ushort)_dependencies.length);
424         foreach(dep; _dependencies) bytes ~= dep.toBytes(options);
425 
426         bytes ~= setUshort(cast(ushort)_implicits.length);
427         foreach(imp; _implicits) bytes ~= imp.toBytes(options);
428 
429         return bytes;
430     }
431 
432     static Target fromBytes(ref ubyte[] bytes) @trusted pure nothrow {
433         string[] outputs;
434         immutable numOutputs = getUshort(bytes);
435 
436         foreach(i; 0 .. numOutputs) {
437             outputs ~= cast(string)bytesToArray!char(bytes);
438         }
439 
440         auto command = Command(cast(string)bytesToArray!char(bytes));
441 
442         Target[] dependencies;
443         immutable numDeps = getUshort(bytes);
444         foreach(i; 0..numDeps) dependencies ~= Target.fromBytes(bytes);
445 
446         Target[] implicits;
447         immutable numImps = getUshort(bytes);
448         foreach(i; 0..numImps) implicits ~= Target.fromBytes(bytes);
449 
450         return Target(outputs, command, dependencies, implicits);
451     }
452 
453     bool opEquals()(auto ref const Target other) @safe pure const {
454 
455         bool sameSet(T)(const(T)[] fst, const(T)[] snd) {
456             if(fst.length != snd.length) return false;
457             return fst.all!(a => snd.any!(b => a == b));
458         }
459 
460         return
461             sameSet(_outputs, other._outputs) &&
462             _command == other._command &&
463             sameSet(_dependencies, other._dependencies) &&
464             sameSet(_implicits, other._implicits);
465     }
466 
467 private:
468 
469     string[] depsInProjectPath(in Target[] deps, in string projectPath) @safe pure const {
470         import reggae.range;
471         return deps.map!(a => a.expandOutputs(projectPath)).join;
472     }
473 
474     string[] inputs(in string projectPath) @safe pure nothrow const {
475         //functional didn't work here, I don't know why so sticking with loops for now
476         string[] inputs;
477         foreach(dep; _dependencies) {
478             foreach(output; dep._outputs) {
479                 //leaf objects are references to source files in the project path,
480                 //those need their path built. Any other dependencies are in the
481                 //build path, so they don't need the same treatment
482                 inputs ~= dep.isLeaf ? inProjectPath(projectPath, output) : output;
483             }
484         }
485         return inputs;
486     }
487 }
488 
489 string inProjectPath(in string projectPath, in string name) @safe pure nothrow {
490     if(name.startsWith(gBuilddir)) return name;
491     if(name[0] == '$') return name;
492     return buildPath(projectPath, name);
493 }
494 
495 
496 enum CommandType {
497     shell,
498     compile,
499     link,
500     compileAndLink,
501     code,
502     phony,
503 }
504 
505 alias CommandFunction = void function(in string[], in string[]);
506 alias CommandDelegate = void delegate(in string[], in string[]);
507 
508 /**
509  A command to be execute to produce a targets outputs from its inputs.
510  In general this will be a shell command, but the high-level rules
511  use commands with known semantics (compilation, linking, etc)
512 */
513 struct Command {
514     alias Params = AssocList!(string, string[]);
515 
516     private string command;
517     private CommandType type;
518     private Params params;
519     private CommandFunction function_;
520     private CommandDelegate delegate_;
521 
522     ///If constructed with a string, it's a shell command
523     this(string shellCommand) @safe pure nothrow {
524         command = shellCommand;
525         type = CommandType.shell;
526     }
527 
528     /**Explicitly request a command of this type with these parameters
529        In general to create one of the builtin high level rules*/
530     this(CommandType type, Params params = Params()) @safe pure {
531         if(type == CommandType.shell || type == CommandType.code)
532             throw new Exception("Command rule cannot be shell or code");
533         this.type = type;
534         this.params = params;
535     }
536 
537     ///A D function call command
538     this(CommandDelegate dg) @safe pure nothrow {
539         type = CommandType.code;
540         this.delegate_ = dg;
541     }
542 
543     ///A D function call command
544     this(CommandFunction func) @safe pure nothrow {
545         type = CommandType.code;
546         this.function_ = func;
547     }
548 
549     static Command phony(in string shellCommand) @safe pure nothrow {
550         Command cmd;
551         cmd.type = CommandType.phony;
552         cmd.command = shellCommand;
553         return cmd;
554     }
555 
556     const(string)[] paramNames() @safe pure nothrow const {
557         return params.keys;
558     }
559 
560     CommandType getType() @safe pure const nothrow {
561         return type;
562     }
563 
564     bool isDefaultCommand() @safe pure const {
565         return type == CommandType.compile || type == CommandType.link || type == CommandType.compileAndLink;
566     }
567 
568     string[] getParams(in string projectPath, in string key, string[] ifNotFound) @safe pure const {
569         return getParams(projectPath, key, true, ifNotFound);
570     }
571 
572     Command expandVariables() @safe pure {
573         switch(type) with(CommandType) {
574         case shell:
575             auto cmd = Command(expandBuildDir(command));
576             cmd.type = this.type;
577             return cmd;
578         default:
579             return this;
580         }
581     }
582 
583     ///Replace $in, $out, $project with values
584     private static string expandCmd(in string cmd, in string projectPath,
585                                     in string[] outputs, in string[] inputs) @safe pure {
586         auto replaceIn = cmd.dup.replace("$in", inputs.join(" "));
587         auto replaceOut = replaceIn.replace("$out", outputs.join(" "));
588         return replaceOut.replace("$project", projectPath).replace(gBuilddir ~ dirSeparator, "");
589     }
590 
591     string rawCmdString(in string projectPath) @safe pure const {
592         if(getType != CommandType.shell)
593             throw new Exception("Command type 'code' not supported for ninja backend");
594         return command.replace("$project", projectPath);
595     }
596 
597     //@trusted because of replace
598     private string[] getParams(in string projectPath, in string key,
599                                bool useIfNotFound, string[] ifNotFound = []) @safe pure const {
600         return params.get(key, ifNotFound).map!(a => a.replace("$project", projectPath)).array;
601     }
602 
603     static string builtinTemplate(in CommandType type,
604                                   in Language language,
605                                   in Options options,
606                                   in Flag!"dependencies" deps = Yes.dependencies) @safe pure {
607 
608         final switch(type) with(CommandType) {
609             case phony:
610                 assert(0, "builtinTemplate cannot be phony");
611 
612             case shell:
613                 assert(0, "builtinTemplate cannot be shell");
614 
615             case link:
616                 final switch(language) with(Language) {
617                     case D:
618                     case unknown:
619                         return options.dCompiler ~ " -of$out $flags $in";
620                     case Cplusplus:
621                         return options.cppCompiler ~ " -o $out $flags $in";
622                     case C:
623                         return options.cCompiler ~ " -o $out $flags $in";
624                 }
625 
626             case code:
627                 throw new Exception("Command type 'code' has no built-in template");
628 
629             case compile:
630                 return compileTemplate(type, language, options, deps).replace("$out $in", "$out -c $in");
631 
632             case compileAndLink:
633                 return compileTemplate(type, language, options, deps);
634         }
635     }
636 
637     private static string compileTemplate(in CommandType type,
638                                           in Language language,
639                                           in Options options,
640                                           in Flag!"dependencies" deps = Yes.dependencies) @safe pure {
641         immutable ccParams = deps
642             ? " $flags $includes -MMD -MT $out -MF $out.dep -o $out $in"
643             : " $flags $includes -o $out $in";
644 
645         final switch(language) with(Language) {
646             case D:
647                 return deps
648                     ? ".reggae/dcompile --objFile=$out --depFile=$out.dep " ~
649                     options.dCompiler ~ " $flags $includes $stringImports $in"
650                     : options.dCompiler ~ " $flags $includes $stringImports -of$out $in";
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 }