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