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