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(in 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 /// Search for filename pattern in the envPath variable content which can
157 /// contain multiple paths separated with sep depending on platform.
158 /// Returns: array of matching file names
159 string[] searchPatternInEnvPath(in string envPath, in string pattern, in char sep=envPathSep)
160 {
161     import std.algorithm : map, splitter;
162     import std.array : array;
163     import std.file : dirEntries, exists, isDir, SpanMode;
164 
165     string[] res = [];
166 
167     foreach (dir; splitter(envPath, sep)) {
168         if (!exists(dir) || !isDir(dir)) continue;
169         res ~= dirEntries(dir, pattern, SpanMode.shallow)
170             .map!(de => de.name)
171             .array;
172     }
173     return res;
174 }
175 
176 string searchExecutable(in string exe)
177 {
178     import std.process : environment;
179     version(Windows) {
180         import std.algorithm : endsWith;
181         const efn = exe.endsWith(".exe") ? exe : exe ~ ".exe";
182     }
183     else {
184         const efn = exe;
185     }
186 
187     return searchInEnvPath(environment["PATH"], efn);
188 }
189 
190 /// return the path of a file in a temp dir location
191 /// with a unique name. fnFmt should be a file name (without directory)
192 /// containing "%s" for use with std.format. It is used to insert a unique
193 /// random string in the path.
194 string tempFilePath(string fnFmt)
195 {
196     import std.ascii : letters;
197     import std.conv : to;
198     import std.file : tempDir;
199     import std.format : format;
200     import std.path : buildPath;
201     import std.random : randomSample;
202     import std.utf : byCodeUnit;
203 
204     // random id with 20 letters
205     const id = letters.byCodeUnit.randomSample(20).to!string;
206     return tempDir.buildPath(format(fnFmt, id));
207 }
208 
209 /// Install file to target.
210 /// target's directory is recursively created if does not exist.
211 /// if a target exists and is newer or have same date than file, install is not performed
212 void installCopy(in string file, in string target)
213 {
214     import std.exception : enforce;
215     import std.path : dirName;
216     import std.file : copy, exists, mkdirRecurse, isDir, timeLastModified;
217 
218     enforce(exists(file), "attempt to install non-existing file");
219 
220     const directory = dirName(target);
221 
222     if (exists(target) && timeLastModified(target) >= timeLastModified(file)) {
223         return;
224     }
225     enforce(
226         !exists(directory) || isDir(directory),
227         directory ~ " exists but is not a directory"
228     );
229 
230     mkdirRecurse(directory);
231     copy(file, target);
232 }