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