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 		@property override string name() { return "Visual Studio %d %s (%s)".format(year, edition, packageName); }
52 		@property override string subdirectory() { return "vs%s-%s".format(year, edition.toLower()); }
53 
54 		@property override string[] binPaths() { return modelBinPaths(null); }
55 
56 		@property private string packageMarkerFile() { return ".ae-sys-install-vs/" ~ packageName ~ ".installed"; }
57 
58 		@property override bool installedLocally()
59 		{
60 			if (directory.buildPath("packages.json").exists)
61 			{
62 				log("Old-style installation detected, deleting...");
63 				rmdirRecurse(directory);
64 			}
65 			return directory.buildPath(packageMarkerFile).exists();
66 		}
67 
68 	private:
69 		this() {}
70 		this(string packageName) { this.packageName = packageName; }
71 
72 		static string msdl(int n)
73 		{
74 			return "http://go.microsoft.com/fwlink/?LinkId=%d&clcid=0x409"
75 				.format(n)
76 				.I!resolveRedirect()
77 				.I!save();
78 		}
79 
80 		public static void decompileMSITo(string msi, string target)
81 		{
82 			wixInstaller.require();
83 			auto status = spawnProcess(["dark", msi, "-o", target]).wait();
84 			enforce(status == 0, "dark failed");
85 		}
86 		public static string setExtensionWXS(string fn) { return fn.setExtension(".wxs"); }
87 		alias decompileMSI = withTarget!(setExtensionWXS, cachedAction!(decompileMSITo, "Decompiling %s to %s..."));
88 
89 		static void installWXS(string wxs, string target)
90 		{
91 			log("Installing %s to %s...".format(wxs, target));
92 
93 			auto wxsDoc = wxs
94 				.readText()
95 				.xmlParse();
96 
97 			string[string] disks;
98 			foreach (media; wxsDoc["Wix"]["Product"].findChildren("Media"))
99 				disks[media.attributes["Id"]] = media
100 					.attributes["Cabinet"]
101 					.absolutePath(wxs.dirName.absolutePath())
102 					.relativePath()
103 					.I!unpack();
104 
105 			void processTag(XmlNode node, string dir)
106 			{
107 				switch (node.tag)
108 				{
109 					case "Directory":
110 					{
111 						auto id = node.attributes["Id"];
112 						switch (id)
113 						{
114 							case "TARGETDIR":
115 								dir = target;
116 								break;
117 							case "ProgramFilesFolder":
118 								dir = dir.buildPath("Program Files (x86)");
119 								break;
120 							case "SystemFolder":
121 								dir = dir.buildPath("windows", "system32");
122 								break;
123 							case "System64Folder":
124 								dir = dir.buildPath("windows", "system64");
125 								break;
126 							default:
127 								if ("Name" in node.attributes)
128 									dir = dir.buildPath(node.attributes["Name"]);
129 								break;
130 						}
131 						break;
132 					}
133 					case "File":
134 					{
135 						auto src = node.attributes["Source"];
136 						enforce(src.startsWith(`SourceDir\File\`));
137 						src = src[`SourceDir\File\`.length .. $];
138 						auto disk = disks[node.attributes["DiskId"]];
139 						src = disk.buildPath(src);
140 						auto dst = dir.buildPath(node.attributes["Name"]);
141 						if (dst.exists)
142 							break;
143 						//log(src ~ " -> " ~ dst);
144 						ensurePathExists(dst);
145 						src.hardLink(dst);
146 						break;
147 					}
148 					default:
149 						break;
150 				}
151 
152 				foreach (child; node.children)
153 					processTag(child, dir);
154 			}
155 
156 			processTag(wxsDoc, null);
157 		}
158 
159 		XmlNode getManifest()
160 		{
161 			if (!manifestCache)
162 				manifestCache =
163 					webInstaller
164 					.I!msdl()
165 					.I!unpack()
166 					.buildPath("0")
167 					.readText()
168 					.xmlParse()
169 					["BurnManifest"];
170 			return manifestCache;
171 		}
172 
173 		void installPackageImpl(string target)
174 		{
175 			windowsOnly();
176 
177 			bool seenPackage;
178 
179 			auto manifest = getManifest();
180 
181 			string[] payloadIDs;
182 			foreach (node; manifest["Chain"].findChildren("MsiPackage"))
183 				if (node.attributes["Id"] == packageName)
184 				{
185 					foreach (payload; node.findChildren("PayloadRef"))
186 						payloadIDs ~= payload.attributes["Id"];
187 					seenPackage = true;
188 				}
189 
190 			enforce(seenPackage, "Unknown package: " ~ packageName);
191 
192 			string[][string] files;
193 			foreach (node; manifest.findChildren("Payload"))
194 				if (payloadIDs.canFind(node.attributes["Id"]))
195 				{
196 					auto path =
197 						node
198 						.attributes["FilePath"]
199 						.prependPath("%s-payloads".format(subdirectory));
200 
201 					auto url = node.attributes["DownloadUrl"];
202 					urlDigests[url] = node.attributes["Hash"].toLower();
203 					files[path.extension.toLower()] ~= url.I!saveAs(path);
204 				}
205 
206 			foreach (cab; files[".cab"])
207 				cab
208 				.I!unpack();
209 
210 			foreach (msi; files[".msi"])
211 				msi
212 				.I!decompileMSI()
213 				.I!installWXS(target);
214 
215 			auto marker = target.buildPath(packageMarkerFile);
216 			marker.ensurePathExists();
217 			marker.touch();
218 		}
219 
220 		void getAllMSIs()
221 		{
222 			auto manifest = getManifest();
223 
224 			string[] payloadIDs;
225 			foreach (node; manifest["Chain"].findChildren("MsiPackage"))
226 				foreach (payload; node.findChildren("PayloadRef"))
227 					payloadIDs ~= payload.attributes["Id"];
228 
229 			foreach (node; manifest.findChildren("Payload"))
230 			{
231 				auto path =
232 					node
233 					.attributes["FilePath"]
234 					.prependPath("%s-payloads".format(subdirectory));
235 
236 				if (path.extension.toLower() == ".msi")
237 				{
238 					auto url = node.attributes["DownloadUrl"];
239 					urlDigests[url] = node.attributes["Hash"].toLower();
240 
241 					url
242 					.I!saveAs(path)
243 					.I!decompileMSI();
244 				}
245 			}
246 		}
247 
248 	protected:
249 		override void atomicInstallImpl()
250 		{
251 			windowsOnly();
252 			auto target = directory ~ "." ~ packageName;
253 			void installPackageImplProxy(string target) { installPackageImpl(target); } // https://issues.dlang.org/show_bug.cgi?id=14580
254 			atomic!installPackageImplProxy(target);
255 			if (!directory.exists)
256 				directory.mkdir();
257 			target.atomicMoveInto(directory);
258 			assert(installedLocally);
259 		}
260 
261 		static this()
262 		{
263 			urlDigests["http://download.microsoft.com/download/7/2/E/72E0F986-D247-4289-B9DC-C4FB07374894/wdexpress_full.exe"] = "8a4c07fa11b20b85126988e7eaf792924b319ae0";
264 			urlDigests["http://download.microsoft.com/download/7/1/B/71BA74D8-B9A0-4E6C-9159-A8335D54437E/vs_community.exe"  ] = "51e5f04fc4648bde3c8276703bf7251216e4ceaf";
265 		}
266 	}
267 
268 	int year; /// Version/year.
269 	int webInstaller; /// Microsoft download number for the web installer.
270 	string edition; /// Edition variant (e.g. "Express").
271 	string versionName; /// Numeric version (e.g. "12.0").
272 
273 	/// Returns the paths to the "bin" directory for the given model.
274 	/// Model is x86 (null), amd64, or x86_amd64
275 	string[] modelBinPaths(string model)
276 	{
277 		string[] result = [
278 			`windows\system32`,
279 			`Program Files (x86)\MSBuild\` ~ versionName ~ `\Bin`,
280 		];
281 
282 		if (!model || model == "x86")
283 		{
284 			result ~= [
285 				`Program Files (x86)\Microsoft Visual Studio ` ~ versionName ~ `\VC\bin`,
286 			];
287 		}
288 		else
289 		if (model == "amd64")
290 		{
291 			result ~= [
292 				`Program Files (x86)\Microsoft Visual Studio ` ~ versionName ~ `\VC\bin\amd64`,
293 			];
294 		}
295 		else
296 		if (model == "x86_amd64")
297 		{
298 			// Binaries which target amd64 are under x86_amd64, but there is only one copy of DLLs
299 			// under bin. Therefore, add the bin directory too, after the x86_amd64 directory.
300 			result ~= [
301 				`Program Files (x86)\Microsoft Visual Studio ` ~ versionName ~ `\VC\bin\x86_amd64`,
302 				`Program Files (x86)\Microsoft Visual Studio ` ~ versionName ~ `\VC\bin`,
303 			];
304 		}
305 
306 		return result;
307 	}
308 
309 	/// Constructs a component installer for the given package.
310 	VisualStudioComponentInstaller opIndex(string name)
311 	{
312 		return new VisualStudioComponentInstaller(name);
313 	}
314 
315 	/// Decompile all MSI files.
316 	/// Useful for finding the name of the package which contains the file you want.
317 	void getAllMSIs()
318 	{
319 		(new VisualStudioComponentInstaller()).getAllMSIs();
320 	}
321 
322 	/// The full installation directory.
323 	@property string directory() { return (new VisualStudioComponentInstaller()).directory; }
324 
325 private:
326 	XmlNode manifestCache;
327 }
328 
329 deprecated alias vs2013 = vs2013express;
330 
331 alias vs2013express   = singleton!(VisualStudioInstaller, 2013, "Express"  , 320697, "12.0"); /// Visual Studio 2013 Express Edition
332 alias vs2013community = singleton!(VisualStudioInstaller, 2013, "Community", 517284, "12.0"); /// Visual Studio 2013 Community Edition