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 : Installer 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 void requirePackages(string[] packages) 46 { 47 foreach (p; packages) 48 if (!this.packages.canFind(p)) 49 this.packages ~= p; 50 } 51 52 @property override string name() { return "Visual Studio %d %s (%-(%s, %))".format(year, edition, packages); } 53 @property override string subdirectory() { return "vs%s-%s".format(year, edition.toLower()); } 54 55 @property override string[] binPaths() 56 { 57 return [ 58 `windows\system32`, 59 `Program Files (x86)\Microsoft Visual Studio ` ~ versionName ~ `\VC\bin`, 60 `Program Files (x86)\MSBuild\` ~ versionName ~ `\Bin`, 61 ]; 62 } 63 64 @property override bool installedLocally() 65 { 66 if (!directory.exists) 67 return false; 68 69 auto installedPackages = 70 packageFile 71 .prependPath(directory) 72 .readText() 73 .jsonParse!(string[]) 74 .sort().uniq().array(); 75 auto wantedPackages = packages.sort().uniq().array(); 76 if (installedPackages != wantedPackages) 77 { 78 log("Requested package set differs from previous install - deleting " ~ directory); 79 directory.forceDelete(Yes.recursive); 80 return false; 81 } 82 83 return true; 84 } 85 86 private: 87 enum packageFile = "packages.json"; 88 89 static string msdl(int n) 90 { 91 return "http://go.microsoft.com/fwlink/?LinkId=%d&clcid=0x409" 92 .format(n) 93 .I!resolveRedirect() 94 .I!save(); 95 } 96 97 public static void decompileMSITo(string msi, string target) 98 { 99 wixInstaller.require(); 100 auto status = spawnProcess(["dark", msi, "-o", target]).wait(); 101 enforce(status == 0, "dark failed"); 102 } 103 public static string setExtensionWXS(string fn) { return fn.setExtension(".wxs"); } 104 alias decompileMSI = withTarget!(setExtensionWXS, cachedAction!(decompileMSITo, "Decompiling %s to %s...")); 105 106 static void installWXS(string wxs, string target) 107 { 108 log("Installing %s to %s...".format(wxs, target)); 109 110 auto wxsDoc = wxs 111 .readText() 112 .xmlParse(); 113 114 string[string] disks; 115 foreach (media; wxsDoc["Wix"]["Product"].findChildren("Media")) 116 disks[media.attributes["Id"]] = media 117 .attributes["Cabinet"] 118 .absolutePath(wxs.dirName.absolutePath()) 119 .relativePath() 120 .I!unpack(); 121 122 void processTag(XmlNode node, string dir) 123 { 124 switch (node.tag) 125 { 126 case "Directory": 127 { 128 auto id = node.attributes["Id"]; 129 switch (id) 130 { 131 case "TARGETDIR": 132 dir = target; 133 break; 134 case "ProgramFilesFolder": 135 dir = dir.buildPath("Program Files (x86)"); 136 break; 137 case "SystemFolder": 138 dir = dir.buildPath("windows", "system32"); 139 break; 140 case "System64Folder": 141 dir = dir.buildPath("windows", "system64"); 142 break; 143 default: 144 if ("Name" in node.attributes) 145 dir = dir.buildPath(node.attributes["Name"]); 146 break; 147 } 148 break; 149 } 150 case "File": 151 { 152 auto src = node.attributes["Source"]; 153 enforce(src.startsWith(`SourceDir\File\`)); 154 src = src[`SourceDir\File\`.length .. $]; 155 auto disk = disks[node.attributes["DiskId"]]; 156 src = disk.buildPath(src); 157 auto dst = dir.buildPath(node.attributes["Name"]); 158 if (dst.exists) 159 break; 160 //log(src ~ " -> " ~ dst); 161 ensurePathExists(dst); 162 src.hardLink(dst); 163 break; 164 } 165 default: 166 break; 167 } 168 169 foreach (child; node.children) 170 processTag(child, dir); 171 } 172 173 processTag(wxsDoc, null); 174 } 175 176 protected: 177 int year, webInstaller; 178 string edition, versionName; 179 string[] packages; 180 181 XmlNode getManifest() 182 { 183 return webInstaller 184 .I!msdl() 185 .I!unpack() 186 .buildPath("0") 187 .readText() 188 .xmlParse() 189 ["BurnManifest"]; 190 } 191 192 override void installImpl(string target) 193 { 194 windowsOnly(); 195 196 assert(packages.length, "No packages specified"); 197 auto seenPackage = new bool[packages.length]; 198 199 auto manifest = getManifest(); 200 201 string[] payloadIDs; 202 foreach (node; manifest["Chain"].findChildren("MsiPackage")) 203 if (packages.canFind(node.attributes["Id"])) 204 { 205 foreach (payload; node.findChildren("PayloadRef")) 206 payloadIDs ~= payload.attributes["Id"]; 207 seenPackage[packages.countUntil(node.attributes["Id"])] = true; 208 } 209 210 enforce(seenPackage.all, "Unknown package(s): %s".format(packages.length.iota.filter!(i => !seenPackage[i]).map!(i => packages[i]))); 211 212 string[][string] files; 213 foreach (node; manifest.findChildren("Payload")) 214 if (payloadIDs.canFind(node.attributes["Id"])) 215 { 216 auto path = 217 node 218 .attributes["FilePath"] 219 .prependPath("%s-payloads".format(subdirectory)); 220 221 auto url = node.attributes["DownloadUrl"]; 222 urlDigests[url] = node.attributes["Hash"].toLower(); 223 files[path.extension.toLower()] ~= url.I!saveAs(path); 224 } 225 226 foreach (cab; files[".cab"]) 227 cab 228 .I!unpack(); 229 230 foreach (msi; files[".msi"]) 231 msi 232 .I!decompileMSI() 233 .I!installWXS(target); 234 235 std.file.write(buildPath(target, packageFile), packages.toJson()); 236 } 237 238 /// Decompile all MSI files. 239 /// Useful for finding the name of the package which contains the file you want. 240 public void getAllMSIs() 241 { 242 auto manifest = getManifest(); 243 244 string[] payloadIDs; 245 foreach (node; manifest["Chain"].findChildren("MsiPackage")) 246 foreach (payload; node.findChildren("PayloadRef")) 247 payloadIDs ~= payload.attributes["Id"]; 248 249 foreach (node; manifest.findChildren("Payload")) 250 { 251 auto path = 252 node 253 .attributes["FilePath"] 254 .prependPath("%s-payloads".format(subdirectory)); 255 256 if (path.extension.toLower() == ".msi") 257 { 258 auto url = node.attributes["DownloadUrl"]; 259 urlDigests[url] = node.attributes["Hash"].toLower(); 260 261 url 262 .I!saveAs(path) 263 .I!decompileMSI(); 264 } 265 } 266 } 267 268 static this() 269 { 270 urlDigests["http://download.microsoft.com/download/7/2/E/72E0F986-D247-4289-B9DC-C4FB07374894/wdexpress_full.exe"] = "8a4c07fa11b20b85126988e7eaf792924b319ae0"; 271 urlDigests["http://download.microsoft.com/download/7/1/B/71BA74D8-B9A0-4E6C-9159-A8335D54437E/vs_community.exe" ] = "51e5f04fc4648bde3c8276703bf7251216e4ceaf"; 272 } 273 } 274 275 deprecated alias vs2013 = vs2013express; 276 277 alias vs2013express = singleton!(VisualStudioInstaller, 2013, "Express" , 320697, "12.0"); 278 alias vs2013community = singleton!(VisualStudioInstaller, 2013, "Community", 517284, "12.0");