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