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