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