1 /** 2 * Visual Studio components 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.sys.install.vs; 15 16 import std.algorithm; 17 import std.array; 18 import std.exception; 19 import std.file; 20 import std.path; 21 import std.process; 22 import std.range; 23 import std.string; 24 25 import ae.sys.archive; 26 import ae.sys.file; 27 import ae.sys.install.wix; 28 import ae.sys.net; 29 import ae.utils.json; 30 import ae.utils.meta : singleton, I; 31 import ae.utils.xmllite; 32 33 public import ae.sys.install.common; 34 35 /// Installs Visual Studio components. 36 class VisualStudioInstaller 37 { 38 this(int year, string edition, int webInstaller, string versionName) 39 { 40 this.year = year; 41 this.edition = edition; 42 this.webInstaller = webInstaller; 43 this.versionName = versionName; 44 } /// 45 46 /// Installs a Visual Studio component. 47 class VisualStudioComponentInstaller : Installer 48 { 49 string packageName; /// 50 51 protected: 52 @property override string name() { return "Visual Studio %d %s (%s)".format(year, edition, packageName); } 53 @property override string subdirectory() { return "vs%s-%s".format(year, edition.toLower()); } 54 55 @property override string[] binPaths() { return modelBinPaths(null); } 56 57 @property private string packageMarkerFile() { return ".ae-sys-install-vs/" ~ packageName ~ ".installed"; } 58 59 @property override bool installedLocally() 60 { 61 if (directory.buildPath("packages.json").exists) 62 { 63 log("Old-style installation detected, deleting..."); 64 rmdirRecurse(directory); 65 } 66 return directory.buildPath(packageMarkerFile).exists(); 67 } 68 69 private: 70 this() {} 71 this(string packageName) { this.packageName = packageName; } 72 73 static string msdl(int n) 74 { 75 return "https://go.microsoft.com/fwlink/?LinkId=%d&clcid=0x409" 76 .format(n) 77 .I!resolveRedirect() 78 .I!save(); 79 } 80 81 public static void _decompileMSITo(string msi, string target) 82 { 83 wixInstaller.require(); 84 auto status = spawnProcess(["dark", msi, "-o", target]).wait(); 85 enforce(status == 0, "dark failed"); 86 } 87 public static string _setExtensionWXS(string fn) { return fn.setExtension(".wxs"); } 88 alias decompileMSI = withTarget!(_setExtensionWXS, cachedAction!(_decompileMSITo, "Decompiling %s to %s...")); 89 90 static void installWXS(string wxs, string target) 91 { 92 log("Installing %s to %s...".format(wxs, target)); 93 94 auto wxsDoc = wxs 95 .readText() 96 .xmlParse(); 97 98 string[string] disks; 99 foreach (media; wxsDoc["Wix"]["Product"].findChildren("Media")) 100 disks[media.attributes["Id"]] = media 101 .attributes["Cabinet"] 102 .absolutePath(wxs.dirName.absolutePath()) 103 .relativePath() 104 .I!unpack(); 105 106 void processTag(XmlNode node, string dir) 107 { 108 switch (node.tag) 109 { 110 case "Directory": 111 { 112 auto id = node.attributes["Id"]; 113 switch (id) 114 { 115 case "TARGETDIR": 116 dir = target; 117 break; 118 case "ProgramFilesFolder": 119 dir = dir.buildPath("Program Files (x86)"); 120 break; 121 case "SystemFolder": 122 dir = dir.buildPath("windows", "system32"); 123 break; 124 case "System64Folder": 125 dir = dir.buildPath("windows", "system64"); 126 break; 127 default: 128 if ("Name" in node.attributes) 129 dir = dir.buildPath(node.attributes["Name"]); 130 break; 131 } 132 break; 133 } 134 case "File": 135 { 136 auto src = node.attributes["Source"]; 137 enforce(src.startsWith(`SourceDir\File\`)); 138 src = src[`SourceDir\File\`.length .. $]; 139 auto disk = disks[node.attributes["DiskId"]]; 140 src = disk.buildPath(src); 141 auto dst = dir.buildPath(node.attributes["Name"]); 142 if (dst.exists) 143 break; 144 //log(src ~ " -> " ~ dst); 145 ensurePathExists(dst); 146 src.hardLink(dst); 147 break; 148 } 149 default: 150 break; 151 } 152 153 foreach (child; node.children) 154 processTag(child, dir); 155 } 156 157 processTag(wxsDoc, null); 158 } 159 160 XmlNode getManifest() 161 { 162 if (!manifestCache) 163 manifestCache = 164 webInstaller 165 .I!msdl() 166 .I!unpack() 167 .buildPath("0") 168 .readText() 169 .xmlParse() 170 ["BurnManifest"]; 171 return manifestCache; 172 } 173 174 void installPackageImpl(string target) 175 { 176 windowsOnly(); 177 178 bool seenPackage; 179 180 auto manifest = getManifest(); 181 182 string[] payloadIDs; 183 foreach (node; manifest["Chain"].findChildren("MsiPackage")) 184 if (node.attributes["Id"] == packageName) 185 { 186 foreach (payload; node.findChildren("PayloadRef")) 187 payloadIDs ~= payload.attributes["Id"]; 188 seenPackage = true; 189 } 190 191 enforce(seenPackage, "Unknown package: " ~ packageName); 192 193 string[][string] files; 194 foreach (node; manifest.findChildren("Payload")) 195 if (payloadIDs.canFind(node.attributes["Id"])) 196 { 197 auto path = 198 node 199 .attributes["FilePath"] 200 .replace(`\`, dirSeparator) 201 .prependPath("%s-payloads".format(subdirectory)); 202 203 auto url = node.attributes["DownloadUrl"] 204 .I!resolveRedirect(); 205 urlDigests[url] = node.attributes["Hash"].toLower(); 206 207 string downloaded; 208 try 209 downloaded = url.I!saveAs(path); 210 catch (Exception e) 211 { 212 log("Download failed (" ~ e.msg ~ "), trying mirror..."); 213 auto mirrorUrl = mirrorPrefix ~ url.findSplit("://")[2]; 214 urlDigests[mirrorUrl] = urlDigests[url]; 215 216 url = mirrorUrl; 217 downloaded = url.I!saveAs(path); 218 } 219 220 files[path.extension.toLower()] ~= downloaded; 221 } 222 223 foreach (cab; files[".cab"]) 224 cab 225 .I!unpack(); 226 227 foreach (msi; files[".msi"]) 228 msi 229 .I!decompileMSI() 230 .I!installWXS(target); 231 232 auto marker = target.buildPath(packageMarkerFile); 233 marker.ensurePathExists(); 234 marker.touch(); 235 } 236 237 void getAllMSIs() 238 { 239 auto manifest = getManifest(); 240 241 string[] payloadIDs; 242 foreach (node; manifest["Chain"].findChildren("MsiPackage")) 243 foreach (payload; node.findChildren("PayloadRef")) 244 payloadIDs ~= payload.attributes["Id"]; 245 246 foreach (node; manifest.findChildren("Payload")) 247 { 248 auto path = 249 node 250 .attributes["FilePath"] 251 .prependPath("%s-payloads".format(subdirectory)); 252 253 if (path.extension.toLower() == ".msi") 254 { 255 auto url = node.attributes["DownloadUrl"]; 256 urlDigests[url] = node.attributes["Hash"].toLower(); 257 258 url 259 .I!saveAs(path) 260 .I!decompileMSI(); 261 } 262 } 263 } 264 265 protected: 266 override void atomicInstallImpl() 267 { 268 windowsOnly(); 269 auto target = directory ~ "." ~ packageName; 270 void installPackageImplProxy(string target) { installPackageImpl(target); } // https://issues.dlang.org/show_bug.cgi?id=14580 271 atomic!installPackageImplProxy(target); 272 if (!directory.exists) 273 directory.mkdir(); 274 target.atomicMoveInto(directory); 275 assert(installedLocally); 276 } 277 278 static this() 279 { 280 urlDigests["https://download.microsoft.com/download/7/2/E/72E0F986-D247-4289-B9DC-C4FB07374894/wdexpress_full.exe"] = "8a4c07fa11b20b85126988e7eaf792924b319ae0"; 281 urlDigests["https://download.microsoft.com/download/7/1/B/71BA74D8-B9A0-4E6C-9159-A8335D54437E/vs_community.exe" ] = "51e5f04fc4648bde3c8276703bf7251216e4ceaf"; 282 } 283 } 284 285 int year; /// Version/year. 286 int webInstaller; /// Microsoft download number for the web installer. 287 string edition; /// Edition variant (e.g. "Express"). 288 string versionName; /// Numeric version (e.g. "12.0"). 289 290 string mirrorPrefix = "https://cy.md/d/ae-sys-install-mirror/"; 291 292 /// Returns the paths to the "bin" directory for the given model. 293 /// Model is x86 (null), amd64, or x86_amd64 294 string[] modelBinPaths(string model) 295 { 296 string[] result = [ 297 `windows\system32`, 298 `Program Files (x86)\MSBuild\` ~ versionName ~ `\Bin`, 299 ]; 300 301 if (!model || model == "x86") 302 { 303 result ~= [ 304 `Program Files (x86)\Microsoft Visual Studio ` ~ versionName ~ `\VC\bin`, 305 ]; 306 } 307 else 308 if (model == "amd64") 309 { 310 result ~= [ 311 `Program Files (x86)\Microsoft Visual Studio ` ~ versionName ~ `\VC\bin\amd64`, 312 ]; 313 } 314 else 315 if (model == "x86_amd64") 316 { 317 // Binaries which target amd64 are under x86_amd64, but there is only one copy of DLLs 318 // under bin. Therefore, add the bin directory too, after the x86_amd64 directory. 319 result ~= [ 320 `Program Files (x86)\Microsoft Visual Studio ` ~ versionName ~ `\VC\bin\x86_amd64`, 321 `Program Files (x86)\Microsoft Visual Studio ` ~ versionName ~ `\VC\bin`, 322 ]; 323 } 324 325 return result; 326 } 327 328 /// Constructs a component installer for the given package. 329 VisualStudioComponentInstaller opIndex(string name) 330 { 331 return new VisualStudioComponentInstaller(name); 332 } 333 334 /// Decompile all MSI files. 335 /// Useful for finding the name of the package which contains the file you want. 336 void getAllMSIs() 337 { 338 (new VisualStudioComponentInstaller()).getAllMSIs(); 339 } 340 341 /// The full installation directory. 342 @property string directory() { return (new VisualStudioComponentInstaller()).directory; } 343 344 private: 345 XmlNode manifestCache; 346 } 347 348 deprecated alias vs2013 = vs2013express; 349 350 alias vs2013express = singleton!(VisualStudioInstaller, 2013, "Express" , 320697, "12.0"); /// Visual Studio 2013 Express Edition 351 alias vs2013community = singleton!(VisualStudioInstaller, 2013, "Community", 517284, "12.0"); /// Visual Studio 2013 Community Edition