1 /* 2 *Copyright (C) 2018 Laurent Tréguier 3 * 4 *This file is part of DLS. 5 * 6 *DLS is free software: you can redistribute it and/or modify 7 *it under the terms of the GNU General Public License as published by 8 *the Free Software Foundation, either version 3 of the License, or 9 *(at your option) any later version. 10 * 11 *DLS is distributed in the hope that it will be useful, 12 *but WITHOUT ANY WARRANTY; without even the implied warranty of 13 *MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 *GNU General Public License for more details. 15 * 16 *You should have received a copy of the GNU General Public License 17 *along with DLS. If not, see <http://www.gnu.org/licenses/>. 18 * 19 */ 20 21 module dls.bootstrap; 22 23 import std.json : JSONValue; 24 25 immutable apiEndpoint = "https://api.github.com/repos/d-language-server/dls/%s"; 26 27 version (Windows) 28 { 29 private immutable os = "windows"; 30 } 31 else version (OSX) 32 { 33 private immutable os = "osx"; 34 } 35 else version (linux) 36 { 37 private immutable os = "linux"; 38 } 39 else version (FreeBSD) 40 { 41 private immutable os = "linux"; 42 } 43 else 44 { 45 private immutable os = "none"; 46 } 47 48 version (Windows) 49 { 50 private immutable dlsExecutable = "dls.exe"; 51 } 52 else 53 { 54 private immutable dlsExecutable = "dls"; 55 } 56 57 private immutable string dlsArchiveName; 58 private immutable string dlsDirName = "dls-%s"; 59 private immutable string dlsLatestDirName = "dls-latest"; 60 private string downloadUrl; 61 private string downloadVersion; 62 63 version (X86_64) 64 version = IntelArchitecture; 65 else version (X86) 66 version = IntelArchitecture; 67 68 shared static this() 69 { 70 import std.format : format; 71 72 version (IntelArchitecture) 73 { 74 import core.cpuid : isX86_64; 75 76 version (Posix) 77 { 78 import std.process : execute; 79 import std..string : strip; 80 import std.uni : toLower; 81 82 immutable arch = execute("uname").output.strip() 83 .toLower() != "freebsd" && isX86_64 ? "x86_64" : "x86"; 84 } 85 else 86 { 87 immutable arch = isX86_64 ? "x86_64" : "x86"; 88 } 89 } 90 else 91 { 92 immutable arch = "none"; 93 } 94 95 dlsArchiveName = format("dls-%%s.%s.%s.zip", os, arch); 96 } 97 98 @property JSONValue[] allReleases() 99 { 100 import std.format : format; 101 import std.json : parseJSON; 102 import std.net.curl : get; 103 104 return parseJSON(get(format!apiEndpoint("releases"))).array; 105 } 106 107 @property bool canDownloadDls() 108 { 109 import core.time : hours; 110 import std.algorithm : min; 111 import std.datetime : Clock, SysTime; 112 import std.format : format; 113 import std.json : JSON_TYPE; 114 115 try 116 { 117 foreach (release; allReleases) 118 { 119 immutable releaseDate = SysTime.fromISOExtString(release["published_at"].str); 120 121 if (Clock.currTime.toUTC() - releaseDate > 1.hours 122 && release["prerelease"].type == JSON_TYPE.FALSE) 123 { 124 foreach (asset; release["assets"].array) 125 { 126 if (asset["name"].str == format(dlsArchiveName, release["tag_name"].str)) 127 { 128 downloadUrl = asset["browser_download_url"].str; 129 downloadVersion = release["tag_name"].str; 130 return true; 131 } 132 } 133 } 134 } 135 } 136 catch (Exception e) 137 { 138 // The download URL couldn't be retrieved 139 } 140 141 return false; 142 } 143 144 void downloadDls(const void function(size_t size) totalSizeCallback = null, 145 const void function(size_t size) chunkSizeCallback = null, 146 const void function() extractCallback = null) 147 { 148 import std.array : appender; 149 import std.net.curl : HTTP; 150 import std.file : exists, isFile, mkdirRecurse, remove, rmdirRecurse, write; 151 import std.format : format; 152 import std.path : buildNormalizedPath; 153 import std.zip : ZipArchive; 154 155 if (downloadUrl.length > 0 || canDownloadDls) 156 { 157 immutable dlsDir = buildNormalizedPath(dubBinDir, format(dlsDirName, downloadVersion)); 158 auto request = HTTP(downloadUrl); 159 auto archiveData = appender!(ubyte[]); 160 161 if (exists(dlsDir)) 162 { 163 if (isFile(dlsDir)) 164 { 165 remove(dlsDir); 166 } 167 else 168 { 169 rmdirRecurse(dlsDir); 170 } 171 } 172 173 mkdirRecurse(dlsDir); 174 175 request.onReceive = (ubyte[] data) { 176 archiveData ~= data; 177 return data.length; 178 }; 179 180 request.onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) { 181 import core.time : Duration, msecs; 182 183 static if (__VERSION__ >= 2075L) 184 { 185 import std.datetime.stopwatch : StopWatch; 186 } 187 else 188 { 189 import std.datetime : StopWatch; 190 } 191 192 static bool started; 193 static bool stopped; 194 static StopWatch watch; 195 196 if (!started && dlTotal > 0) 197 { 198 started = true; 199 watch.start(); 200 201 if (totalSizeCallback !is null) 202 { 203 totalSizeCallback(dlTotal); 204 } 205 } 206 207 if (started && !stopped && chunkSizeCallback !is null && dlNow > 0 208 && (cast(Duration) watch.peek() >= 500.msecs || dlNow == dlTotal)) 209 { 210 watch.reset(); 211 chunkSizeCallback(dlNow); 212 213 if (dlNow == dlTotal) 214 { 215 stopped = true; 216 watch.stop(); 217 } 218 } 219 220 return 0; 221 }; 222 223 request.perform(); 224 225 if (extractCallback !is null) 226 { 227 extractCallback(); 228 } 229 230 auto archive = new ZipArchive(archiveData.data); 231 232 foreach (name, member; archive.directory) 233 { 234 immutable memberPath = buildNormalizedPath(dlsDir, name); 235 write(memberPath, archive.expand(member)); 236 237 version (Posix) 238 { 239 import std.process : execute; 240 241 if (name == dlsExecutable) 242 { 243 execute(["chmod", "+x", memberPath]); 244 } 245 } 246 } 247 } 248 else 249 { 250 throw new UpgradeFailedException("Cannot download DLS"); 251 } 252 } 253 254 void buildDls(const string dlsDir, const string[] additionalArgs = []) 255 { 256 import core.cpuid : isX86_64; 257 import std.path : buildNormalizedPath; 258 import std.process : Config, execute; 259 260 auto cmdLine = ["dub", "build", "--build=release"] ~ additionalArgs; 261 262 version (Windows) 263 { 264 cmdLine ~= ["--compiler=dmd", "--arch=" ~ (isX86_64 ? "x86_64" : "x86_mscoff")]; 265 } 266 267 immutable result = execute(cmdLine, null, Config.none, size_t.max, dlsDir); 268 269 if (result.status != 0) 270 { 271 throw new UpgradeFailedException("Build failed: " ~ result.output); 272 } 273 } 274 275 string linkDls() 276 { 277 import std.file : exists, isFile, mkdirRecurse, remove; 278 import std.format : format; 279 import std.path : baseName, buildNormalizedPath; 280 import std..string : endsWith; 281 282 mkdirRecurse(dubBinDir); 283 284 immutable dlsDir = buildNormalizedPath(dubBinDir, format(dlsDirName, downloadVersion)); 285 immutable oldDlsLink = buildNormalizedPath(dubBinDir, dlsExecutable); 286 immutable dlsLatestDir = buildNormalizedPath(dubBinDir, dlsLatestDirName); 287 288 if (exists(oldDlsLink) && !exists(dlsLatestDir)) 289 { 290 makeLink(buildNormalizedPath(dlsLatestDir, dlsExecutable), oldDlsLink, false); 291 } 292 293 makeLink(dlsDir, dlsLatestDir, true); 294 295 return buildNormalizedPath(dlsLatestDir, dlsExecutable); 296 } 297 298 @property string dubBinDir() 299 { 300 import std.path : buildNormalizedPath; 301 import std.process : environment; 302 303 version (Windows) 304 { 305 immutable dubDirPath = environment["LOCALAPPDATA"]; 306 immutable dubDirName = "dub"; 307 } 308 else version (Posix) 309 { 310 immutable dubDirPath = environment["HOME"]; 311 immutable dubDirName = ".dub"; 312 } 313 else 314 { 315 static assert(false, "Platform not supported"); 316 } 317 318 return buildNormalizedPath(dubDirPath, dubDirName, "packages", ".bin"); 319 } 320 321 private void makeLink(const string target, const string link, bool directory) 322 { 323 version (Windows) 324 { 325 import std.array : join; 326 import std.file : exists, isFile, remove, rmdir; 327 import std.format : format; 328 import std.process : execute; 329 330 if (exists(link)) 331 { 332 if (isFile(link)) 333 { 334 remove(link); 335 } 336 else 337 { 338 rmdir(link); 339 } 340 } 341 342 immutable mklinkCommand = format!`mklink %s "%s" "%s"`(directory ? "/J" : "", link, target); 343 const powershellArgs = ["Start-Process", "-Wait", "-FilePath", "cmd.exe", 344 "-ArgumentList", format!"'/c %s'"(mklinkCommand), "-WindowStyle", "Hidden"] ~ (directory 345 ? [] : ["-Verb", "runas"]); 346 immutable result = execute(["powershell.exe", powershellArgs.join(' ')]); 347 348 if (result.status != 0) 349 { 350 throw new UpgradeFailedException("Symlink failed: " ~ result.output); 351 } 352 } 353 else version (Posix) 354 { 355 import std.file : exists, remove, symlink; 356 357 if (exists(link)) 358 { 359 remove(link); 360 } 361 362 symlink(target, link); 363 } 364 else 365 { 366 static assert(false, "Platform not supported"); 367 } 368 } 369 370 class UpgradeFailedException : Exception 371 { 372 this(const string message) 373 { 374 super(message); 375 } 376 }