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