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");