1 module dbuild.util;
2 
3 import std.digest : isDigest;
4 import std.traits : isIntegral;
5 import core.time : dur, Duration;
6 
7 void feedDigestData(D)(ref D digest, in string s)
8 if (isDigest!D)
9 {
10     digest.put(cast(ubyte[])s);
11     digest.put(0);
12 }
13 
14 void feedDigestData(D)(ref D digest, in string[] ss)
15 if (isDigest!D)
16 {
17     import std.bitmanip : nativeToLittleEndian;
18 
19     digest.put(nativeToLittleEndian(cast(uint)ss.length));
20     foreach (s; ss) {
21         digest.put(cast(ubyte[])s);
22         digest.put(0);
23     }
24 }
25 
26 void feedDigestData(D, V)(ref D digest, in V val)
27 if (isDigest!D && (is(V == enum) || isIntegral!V))
28 {
29     import std.bitmanip : nativeToLittleEndian;
30 
31     digest.put(nativeToLittleEndian(val));
32     digest.put(0);
33 }
34 
35 bool checkMd5(in string path, in string md5)
36 in { assert(md5.length); }
37 body {
38     import std.digest : LetterCase, toHexString;
39     import std.digest.md : md5Of;
40     import std.uni : toLower;
41     import std.stdio : File;
42 
43     ubyte[1024] buf = void;
44     auto f = File(path, "rb");
45     return md5Of(f.byChunk(buf[])).toHexString!(LetterCase.lower)() == md5.toLower();
46 }
47 
48 /**
49    Obtain a lock for a file at the given path. If the file cannot be locked
50    within the given duration, an exception is thrown.  The file will be created
51    if it does not yet exist. Deleting the file is not safe as another process
52    could create a new file with the same name.
53    The returned lock will get unlocked upon destruction.
54 
55    Params:
56      path = path to file that gets locked
57      timeout = duration after which locking failed
58    Returns:
59      The locked file or an Exception on timeout.
60 */
61 auto lockFile(string path, Duration timeout=dur!"msecs"(500))
62 {
63     import core.thread : Thread;
64     import std.algorithm : move;
65     import std.datetime : Clock;
66     import std.exception : enforce;
67     import std.stdio : File;
68 
69     // Just a wrapper to hide (and destruct) the locked File.
70     static struct LockFile
71     {
72         // The Lock can't be unlinked as someone could try to lock an already
73         // opened fd while a new file with the same name gets created.
74         // Exclusive filesystem locks (O_EXCL, mkdir) could be deleted but
75         // aren't automatically freed when a process terminates, see dub#1149.
76         private File f;
77     }
78 
79     auto file = File(path, "w");
80     auto t0 = Clock.currTime();
81     auto duration = dur!"msecs"(1);
82     while (true)
83     {
84         if (file.tryLock())
85             return LockFile(move(file));
86         enforce(Clock.currTime() - t0 < timeout, "Failed to lock '"~path~"'.");
87         if (duration < dur!"msecs"(1024)) // exponentially increase sleep time
88             duration *= 2;
89         Thread.sleep(duration);
90     }
91 }
92 
93 void runCommand(string[] command, string workDir = null, bool quiet = false, string[string] env = null)
94 {
95     runCommands((&command)[0 .. 1], workDir, quiet, env);
96 }
97 
98 void runCommands(in string[][] commands, string workDir = null, bool quiet = false, string[string] env = null)
99 {
100     import std.conv : to;
101     import std.exception : enforce;
102     import std.process : Config, escapeShellCommand, Pid, spawnProcess, wait;
103     import std.stdio : stdin, stdout, stderr, File;
104 
105     version(Windows) enum nullFile = "NUL";
106     else version(Posix) enum nullFile = "/dev/null";
107     else static assert(0);
108 
109     auto childStdout = stdout;
110     auto childStderr = stderr;
111     auto config = Config.retainStdout | Config.retainStderr;
112 
113     if (quiet) {
114         childStdout = File(nullFile, "w");
115     }
116 
117     foreach(cmd; commands){
118         if (!quiet) {
119             stdout.writeln("running ", cmd.commandRep);
120         }
121         auto pid = spawnProcess(cmd, stdin, childStdout, childStderr, env, config, workDir);
122         auto exitcode = pid.wait();
123         enforce(exitcode == 0, "Command failed with exit code "
124             ~ to!string(exitcode) ~ ": " ~ cmd.commandRep);
125     }
126 }
127 
128 private @property string commandRep(in string[] cmd)
129 {
130     import std.array : join;
131 
132     return cmd.join(" ");
133 }
134 
135 /// environment variable path separator
136 version(Posix) enum envPathSep = ':';
137 else version(Windows) enum envPathSep = ';';
138 else static assert(false);
139 
140 /// Search for filename in the envPath variable content which can
141 /// contain multiple paths separated with sep depending on platform.
142 /// Returns: null if the file can't be found.
143 string searchInEnvPath(in string envPath, in string filename, in char sep=envPathSep)
144 {
145     import std.algorithm : splitter;
146     import std.file : exists;
147     import std.path : buildPath;
148 
149     foreach (dir; splitter(envPath, sep)) {
150         const filePath = buildPath(dir, filename);
151         if (exists(filePath)) return filePath;
152     }
153     return null;
154 }
155 
156 string searchExecutable(in string exe)
157 {
158     import std.process : environment;
159     version(Windows) {
160         import std.algorithm : endsWith;
161         const efn = exe.endsWith(".exe") ? exe : exe ~ ".exe";
162     }
163     else {
164         const efn = exe;
165     }
166 
167     return searchInEnvPath(environment["PATH"], efn);
168 }
169 
170 /// return the path of a file in a temp dir location
171 /// with a unique name. fnFmt should be a file name (without directory)
172 /// containing "%s" for use with std.format. It is used to insert a unique
173 /// random string in the path.
174 string tempFilePath(string fnFmt)
175 {
176     import std.ascii : letters;
177     import std.conv : to;
178     import std.file : tempDir;
179     import std.format : format;
180     import std.path : buildPath;
181     import std.random : randomSample;
182     import std.utf : byCodeUnit;
183 
184     // random id with 20 letters
185     const id = letters.byCodeUnit.randomSample(20).to!string;
186     return tempDir.buildPath(format(fnFmt, id));
187 }