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 "http://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 .prependPath("%s-payloads".format(subdirectory)); 201 202 auto url = node.attributes["DownloadUrl"]; 203 urlDigests[url] = node.attributes["Hash"].toLower(); 204 files[path.extension.toLower()] ~= url.I!saveAs(path); 205 } 206 207 foreach (cab; files[".cab"]) 208 cab 209 .I!unpack(); 210 211 foreach (msi; files[".msi"]) 212 msi 213 .I!decompileMSI() 214 .I!installWXS(target); 215 216 auto marker = target.buildPath(packageMarkerFile); 217 marker.ensurePathExists(); 218 marker.touch(); 219 } 220 221 void getAllMSIs() 222 { 223 auto manifest = getManifest(); 224 225 string[] payloadIDs; 226 foreach (node; manifest["Chain"].findChildren("MsiPackage")) 227 foreach (payload; node.findChildren("PayloadRef")) 228 payloadIDs ~= payload.attributes["Id"]; 229 230 foreach (node; manifest.findChildren("Payload")) 231 { 232 auto path = 233 node 234 .attributes["FilePath"] 235 .prependPath("%s-payloads".format(subdirectory)); 236 237 if (path.extension.toLower() == ".msi") 238 { 239 auto url = node.attributes["DownloadUrl"]; 240 urlDigests[url] = node.attributes["Hash"].toLower(); 241 242 url 243 .I!saveAs(path) 244 .I!decompileMSI(); 245 } 246 } 247 } 248 249 protected: 250 override void atomicInstallImpl() 251 { 252 windowsOnly(); 253 auto target = directory ~ "." ~ packageName; 254 void installPackageImplProxy(string target) { installPackageImpl(target); } // https://issues.dlang.org/show_bug.cgi?id=14580 255 atomic!installPackageImplProxy(target); 256 if (!directory.exists) 257 directory.mkdir(); 258 target.atomicMoveInto(directory); 259 assert(installedLocally); 260 } 261 262 static this() 263 { 264 urlDigests["http://download.microsoft.com/download/7/2/E/72E0F986-D247-4289-B9DC-C4FB07374894/wdexpress_full.exe"] = "8a4c07fa11b20b85126988e7eaf792924b319ae0"; 265 urlDigests["http://download.microsoft.com/download/7/1/B/71BA74D8-B9A0-4E6C-9159-A8335D54437E/vs_community.exe" ] = "51e5f04fc4648bde3c8276703bf7251216e4ceaf"; 266 } 267 } 268 269 int year; /// Version/year. 270 int webInstaller; /// Microsoft download number for the web installer. 271 string edition; /// Edition variant (e.g. "Express"). 272 string versionName; /// Numeric version (e.g. "12.0"). 273 274 /// Returns the paths to the "bin" directory for the given model. 275 /// Model is x86 (null), amd64, or x86_amd64 276 string[] modelBinPaths(string model) 277 { 278 string[] result = [ 279 `windows\system32`, 280 `Program Files (x86)\MSBuild\` ~ versionName ~ `\Bin`, 281 ]; 282 283 if (!model || model == "x86") 284 { 285 result ~= [ 286 `Program Files (x86)\Microsoft Visual Studio ` ~ versionName ~ `\VC\bin`, 287 ]; 288 } 289 else 290 if (model == "amd64") 291 { 292 result ~= [ 293 `Program Files (x86)\Microsoft Visual Studio ` ~ versionName ~ `\VC\bin\amd64`, 294 ]; 295 } 296 else 297 if (model == "x86_amd64") 298 { 299 // Binaries which target amd64 are under x86_amd64, but there is only one copy of DLLs 300 // under bin. Therefore, add the bin directory too, after the x86_amd64 directory. 301 result ~= [ 302 `Program Files (x86)\Microsoft Visual Studio ` ~ versionName ~ `\VC\bin\x86_amd64`, 303 `Program Files (x86)\Microsoft Visual Studio ` ~ versionName ~ `\VC\bin`, 304 ]; 305 } 306 307 return result; 308 } 309 310 /// Constructs a component installer for the given package. 311 VisualStudioComponentInstaller opIndex(string name) 312 { 313 return new VisualStudioComponentInstaller(name); 314 } 315 316 /// Decompile all MSI files. 317 /// Useful for finding the name of the package which contains the file you want. 318 void getAllMSIs() 319 { 320 (new VisualStudioComponentInstaller()).getAllMSIs(); 321 } 322 323 /// The full installation directory. 324 @property string directory() { return (new VisualStudioComponentInstaller()).directory; } 325 326 private: 327 XmlNode manifestCache; 328 } 329 330 deprecated alias vs2013 = vs2013express; 331 332 alias vs2013express = singleton!(VisualStudioInstaller, 2013, "Express" , 320697, "12.0"); /// Visual Studio 2013 Express Edition 333 alias vs2013community = singleton!(VisualStudioInstaller, 2013, "Community", 517284, "12.0"); /// Visual Studio 2013 Community Edition