1 module dls.bootstrap;
2 
3 import std.file : exists, isFile, mkdirRecurse, remove;
4 import std.format : format;
5 import std.path : buildNormalizedPath;
6 
7 immutable repoBase = import("repo.txt");
8 immutable apiEndpoint = format!"https://api.github.com/repos/%s/dls/%%s"(repoBase);
9 
10 version (Windows)
11 {
12     immutable os = "windows";
13 }
14 else version (OSX)
15 {
16     immutable os = "osx";
17 }
18 else version (linux)
19 {
20     immutable os = "linux";
21 }
22 else
23 {
24     immutable os = "none";
25 }
26 
27 version (X86_64)
28 {
29     immutable arch = "x86_64";
30 }
31 else version (X86)
32 {
33     immutable string arch;
34 }
35 else
36 {
37     immutable arch = "none";
38 }
39 
40 version (Windows)
41 {
42     immutable dlsExecutable = "dls.exe";
43 }
44 else
45 {
46     immutable dlsExecutable = "dls";
47 }
48 
49 private immutable string dlsArchiveName;
50 private immutable string dlsDirName = "dls-%s";
51 private string downloadUrl;
52 private string downloadVersion;
53 private string[] archiveMemberPaths;
54 
55 shared static this()
56 {
57     version (X86)
58     {
59         import core.cpuid : isX86_64;
60 
61         arch = isX86_64 ? "x86_64" : "x86";
62     }
63 
64     dlsArchiveName = format("dls-%%s.%s.%s.zip", os, arch);
65 }
66 
67 @property bool canDownloadDls()
68 {
69     import std.algorithm : min;
70     import std.json : JSONException, parseJSON;
71     import std.net.curl : get;
72 
73     try
74     {
75         const releases = parseJSON(get(format!apiEndpoint("releases"))).array;
76 
77         foreach (release; releases[0 .. min($, 2)])
78         {
79             foreach (asset; release["assets"].array)
80             {
81                 if (asset["name"].str == format(dlsArchiveName, release["tag_name"].str))
82                 {
83                     downloadUrl = asset["browser_download_url"].str;
84                     downloadVersion = release["tag_name"].str;
85                     return true;
86                 }
87             }
88         }
89     }
90     catch (Exception e)
91     {
92         // The download URL couldn't be retrieved
93     }
94 
95     return false;
96 }
97 
98 void downloadDls(in void function(size_t size) totalSizeCallback = null,
99         in void function(size_t size) chunkSizeCallback = null,
100         in void function() extractCallback = null)
101 {
102     import std.array : appender;
103     import std.net.curl : HTTP;
104     import std.file : rmdirRecurse, write;
105     import std.zip : ZipArchive;
106 
107     if (downloadUrl.length > 0 || canDownloadDls)
108     {
109         const dlsDir = buildNormalizedPath(dubBinDir, format(dlsDirName, downloadVersion));
110         auto request = HTTP(downloadUrl);
111         auto archiveData = appender!(ubyte[]);
112 
113         if (exists(dlsDir))
114         {
115             if (isFile(dlsDir))
116             {
117                 remove(dlsDir);
118             }
119             else
120             {
121                 rmdirRecurse(dlsDir);
122             }
123         }
124 
125         mkdirRecurse(dlsDir);
126 
127         request.onReceive = (in ubyte[] data) {
128             archiveData ~= data;
129             return data.length;
130         };
131 
132         request.onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) {
133             import core.time : msecs;
134             import std.datetime.stopwatch : StopWatch;
135 
136             static bool started;
137             static StopWatch watch;
138 
139             if (!started && dlTotal > 0)
140             {
141                 started = true;
142                 watch.start();
143 
144                 if (totalSizeCallback !is null)
145                 {
146                     totalSizeCallback(dlTotal);
147                 }
148             }
149 
150             if (started && chunkSizeCallback !is null && dlNow > 0 && watch.peek() >= 500.msecs)
151             {
152                 watch.reset();
153                 chunkSizeCallback(dlNow);
154             }
155 
156             return 0;
157         };
158 
159         request.perform();
160 
161         if (extractCallback !is null)
162         {
163             extractCallback();
164         }
165 
166         auto archive = new ZipArchive(archiveData.data);
167         archiveMemberPaths = [];
168 
169         foreach (name, member; archive.directory)
170         {
171             const memberPath = buildNormalizedPath(dlsDir, name);
172             write(memberPath, archive.expand(member));
173             archiveMemberPaths ~= memberPath;
174 
175             version (Posix)
176             {
177                 import core.sys.posix.sys.stat : chmod;
178                 import std.conv : octal;
179                 import std.string : toStringz;
180 
181                 if (name == dlsExecutable)
182                 {
183                     chmod(memberPath.toStringz(), octal!755);
184                 }
185             }
186         }
187     }
188     else
189     {
190         throw new UpgradeFailedException("Cannot download DLS");
191     }
192 }
193 
194 void buildDls(in string dlsDir, in string[] additionalArgs = [])
195 {
196     import std.process : Config, execute;
197 
198     auto cmdLine = ["dub", "build", "--build=release"] ~ additionalArgs;
199 
200     version (Windows)
201     {
202         cmdLine ~= ["--compiler=dmd", "--arch=x86_mscoff"];
203     }
204 
205     const result = execute(cmdLine, null, Config.none, size_t.max, dlsDir);
206 
207     if (result.status != 0)
208     {
209         throw new UpgradeFailedException("Build failed: " ~ result.output);
210     }
211 
212     archiveMemberPaths = [buildNormalizedPath(dlsDir, dlsExecutable)];
213 }
214 
215 string linkDls()
216 {
217     import std.file : FileException;
218     import std.path : baseName;
219     import std.string : endsWith;
220 
221     string dlsLinkPath;
222 
223     foreach (memberPath; archiveMemberPaths)
224     {
225         if (!isFile(memberPath))
226         {
227             throw new FileException(format!"%s doesn't exist"(memberPath));
228         }
229 
230         const linkPath = buildNormalizedPath(dubBinDir, baseName(memberPath));
231 
232         mkdirRecurse(dubBinDir);
233 
234         if (exists(linkPath))
235         {
236             remove(linkPath);
237         }
238 
239         if (memberPath.endsWith(dlsExecutable))
240         {
241             dlsLinkPath = linkPath;
242         }
243     }
244 
245     version (Windows)
246     {
247         import std.algorithm : joiner, map;
248         import std.conv : to;
249         import std.file : FileException;
250         import std.format : format;
251         import std.process : Config, execute;
252 
253         string[] mklinks;
254 
255         foreach (memberPath; archiveMemberPaths)
256         {
257             mklinks ~= format("mklink %s %s", buildNormalizedPath(dubBinDir,
258                     baseName(memberPath)), memberPath);
259         }
260 
261         const mklinkCommand = mklinks.joiner(" & ").to!string;
262         const command = [
263             "powershell.exe",
264             format!"Start-Process -FilePath cmd.exe -ArgumentList '/c %s' -Verb runas"(
265                 mklinkCommand)
266         ];
267         const result = execute(command);
268 
269         if (result.status != 0)
270         {
271             throw new FileException("Symlink failed: " ~ result.output);
272         }
273     }
274     else version (Posix)
275     {
276         import std.file : symlink;
277 
278         foreach (memberPath; archiveMemberPaths)
279         {
280             const linkPath = buildNormalizedPath(dubBinDir, baseName(memberPath));
281             symlink(memberPath, linkPath);
282         }
283     }
284     else
285     {
286         static assert(false, "Platform not suported");
287     }
288 
289     return dlsLinkPath;
290 }
291 
292 @property string dubBinDir()
293 {
294     import std.process : environment;
295 
296     version (Windows)
297     {
298         const dubDirPath = environment["LOCALAPPDATA"];
299         const dubDirName = "dub";
300     }
301     else
302     {
303         const dubDirPath = environment["HOME"];
304         const dubDirName = ".dub";
305     }
306 
307     return buildNormalizedPath(dubDirPath, dubDirName, "packages", ".bin");
308 }
309 
310 class UpgradeFailedException : Exception
311 {
312     this(in string message)
313     {
314         super(message);
315     }
316 }