UNPKG

15 kBJavaScriptView Raw
1/**
2 * Detects VisualStudio installs and their Windows Phone SDKs.
3 *
4 * @module visualstudio
5 *
6 * @copyright
7 * Copyright (c) 2014 by Appcelerator, Inc. All Rights Reserved.
8 *
9 * @license
10 * Licensed under the terms of the Apache Public License.
11 * Please see the LICENSE included with this distribution for details.
12 */
13
14const
15 appc = require('node-appc'),
16 async = require('async'),
17 fs = require('fs'),
18 magik = require('./utilities').magik,
19 path = require('path'),
20 spawn = require('child_process').spawn,
21 vs2017support = path.resolve(__dirname, '..', 'bin', 'vs2017support.exe'),
22 __ = appc.i18n(__dirname).__;
23
24var cache,
25 runningBuilds = {};
26
27exports.detect = detect;
28exports.build = build;
29
30function runVS2017Tool(next) {
31 var child = spawn(vs2017support),
32 out = '';
33 child.stdout.on('data', function (data) {
34 out += data.toString();
35 });
36
37 child.stderr.on('data', function (data) {
38 out += data.toString();
39 });
40
41 child.on('error', function (err) {
42 next(null, {
43 issues:[
44 {
45 id: 'WINDOWS_VISUALSTUDIO_2017_DETECT',
46 type: 'error',
47 message: err
48 }
49 ]
50 });
51 });
52
53 child.on('close', function (code) {
54 if (code) {
55 next(null, {
56 issues:[
57 {
58 id: 'WINDOWS_VISUALSTUDIO_2017_DETECT',
59 type: 'error',
60 message: out.trim()
61 }
62 ]
63 });
64 } else {
65 try {
66 next(null, JSON.parse(out));
67 } catch (E) {
68 next(null, {});
69 }
70 }
71 });
72}
73
74/**
75 * Detects Visual Studio installations.
76 *
77 * @param {Object} [options] - An object containing various settings.
78 * @param {Boolean} [options.bypassCache=false] - When true, re-detects all Visual Studio installations.
79 * @param {String} [options.preferredVisualStudio] - The version of Visual Studio to use by default. Example: "13".
80 * @param {String} [options.preferredWindowsPhoneSDK] - The preferred version of the Windows Phone SDK to use by default. Example "8.0".
81 * @param {String} [options.supportedMSBuildVersions] - A string with a version number or range to check if a MSBuild version is supported.
82 * @param {String} [options.supportedVisualStudioVersions] - A string with a version number or range to check if a Visual Studio install is supported.
83 * @param {String} [options.supportedWindowsPhoneSDKVersions] - A string with a version number or range to check if a Windows Phone SDK is supported.
84 * @param {Function} [callback(err, results)] - A function to call with the Visual Studio information.
85 *
86 * @emits module:visualstudio#detected
87 * @emits module:visualstudio#error
88 *
89 * @returns {EventEmitter}
90 */
91function detect(options, callback) {
92 return magik(options, callback, function (emitter, options, callback) {
93 if (cache && !options.bypassCache) {
94 emitter.emit('detected', cache);
95 return callback(null, cache);
96 }
97
98 detectInstallations(emitter, options, callback);
99 });
100}
101
102function detectVS2017Installations(emitter, options, callback) {
103 var results = {
104 selectedVisualStudio: null,
105 visualstudio: null,
106 issues: []
107 };
108
109 runVS2017Tool(function(err, result) {
110 if (result.issues) {
111 results.issues = result.issues;
112 }
113
114 if (result.visualstudio) {
115 results.visualstudio = {};
116 async.each(Object.keys(result.visualstudio), function (vsname, next) {
117 var vsinfo = result.visualstudio[vsname];
118
119 // Try to be compatible with previous version
120 vsinfo.wpsdk = null;
121 vsinfo.registryKey = null;
122 vsinfo.clrVersion = null;
123 vsinfo.selected = false;
124 vsinfo.vsDevCmd = path.join(vsinfo.path, 'Common7', 'Tools', 'VsDevCmd.bat');
125
126 // Get the vcvarsall script
127 var vcvarsall = path.join(vsinfo.path, 'VC', 'Auxiliary', 'Build', 'vcvarsall.bat');
128 if (fs.existsSync(vcvarsall)) {
129 appc.subprocess.getRealName(vcvarsall, function (err, vcvarsall) {
130 if (!err) {
131 vsinfo.vcvarsall = vcvarsall;
132 results.visualstudio[vsname] = vsinfo;
133 }
134 next();
135 });
136 } else {
137 next();
138 }
139 }, function() {
140 callback(err, results);
141 });
142 } else {
143 callback(err, results);
144 }
145 });
146}
147
148function detectInstallations(emitter, options, callback) {
149 // Try to find Visual Studio 2017 first
150 detectVS2017Installations(emitter, options, function(err, results) {
151 var keyRegExp = /.+\\(\d+\.\d)_config$/i,
152 possibleVersions = {};
153
154 function finalize() {
155 cache = results;
156 emitter.emit('detected', results);
157 callback(null, results);
158 }
159
160 async.each([
161 'HKEY_LOCAL_MACHINE\\Software\\Microsoft\\VisualStudio', // there should not be anything here because VS is currently 32-bit and we'll find it next
162 'HKEY_LOCAL_MACHINE\\Software\\Wow6432Node\\Microsoft\\VisualStudio', // this is where VS should be found because it's 32-bit
163 'HKEY_CURRENT_USER\\Software\\Microsoft\\VisualStudio', // should be the same as the one above, but just to be safe
164 'HKEY_LOCAL_MACHINE\\Software\\Microsoft\\VSWinExpress', // there should not be anything here because VS is currently 32-bit and we'll find it next
165 'HKEY_LOCAL_MACHINE\\Software\\Wow6432Node\\Microsoft\\VSWinExpress', // this is where VS should be found because it's 32-bit
166 'HKEY_CURRENT_USER\\Software\\Microsoft\\VSWinExpress', // should be the same as the one above, but just to be safe
167 'HKEY_CURRENT_USER\\Software\\Microsoft\\VPDExpress' // VS express
168 ], function (keyPath, next) {
169 appc.subprocess.run('reg', ['query', keyPath], function (code, out, err) {
170 if (!code) {
171 out.trim().split(/\r\n|\n/).forEach(function (configKey) {
172 configKey = configKey.trim();
173 var m = configKey.match(keyRegExp);
174 if (m) {
175 possibleVersions[configKey] = {
176 version: m[1],
177 configKey: configKey
178 };
179 }
180 });
181 }
182 next();
183 });
184 }, function () {
185 // if we didn't find any Visual Studios, then we're done
186 if (!results.visualstudio && !Object.keys(possibleVersions).length) {
187 results.issues.push({
188 id: 'WINDOWS_VISUAL_STUDIO_NOT_INSTALLED',
189 type: 'error',
190 message: __('Microsoft Visual Studio not found.') + '\n' +
191 __('You will be unable to build Windows Phone or Windows Store apps.')
192 });
193 return finalize();
194 }
195
196 // fetch Visual Studio install information
197 async.each(Object.keys(possibleVersions), function (configKey, next) {
198 appc.subprocess.run('reg', ['query', configKey, '/v', '*'], function (code, out, err) {
199 results.visualstudio || (results.visualstudio = {});
200
201 var ver = possibleVersions[configKey].version,
202 info = results.visualstudio[ver] = {
203 version: ver,
204 registryKey: configKey,
205 supported: !options.supportedVisualStudioVersions || appc.version.satisfies(ver, options.supportedVisualStudioVersions, true),
206 vcvarsall: null,
207 msbuildVersion: null,
208 wpsdk: null,
209 selected: false
210 };
211
212 if (!code) {
213 // get only the values we are interested in
214 out.trim().split(/\r\n|\n/).forEach(function (line) {
215 var parts = line.trim().split(' ').map(function (p) { return p.trim(); });
216 if (parts.length == 3) {
217 if (parts[0] == 'CLR Version') {
218 info.clrVersion = parts[2];
219 } else if (parts[0] == 'ShellFolder') {
220 info.path = parts[2];
221 }
222 }
223 });
224
225 // verify that this Visual Studio actually exists
226 if (info.path && fs.existsSync(info.path)) {
227
228 info.vsDevCmd = path.join(info.path, 'Common7', 'Tools', 'VsDevCmd.bat');
229
230 // get the vcvarsall script
231 var vcvarsall = path.join(info.path, 'VC', 'vcvarsall.bat');
232 if (fs.existsSync(vcvarsall)) {
233 info.vcvarsall = vcvarsall;
234 }
235
236 // detect all Windows Phone SDKs
237 var wpsdkDir = path.join(info.path, 'VC', 'WPSDK');
238 fs.existsSync(wpsdkDir) && fs.readdirSync(wpsdkDir).forEach(function (ver) {
239 var vcvarsphone = path.join(wpsdkDir, ver, 'vcvarsphoneall.bat');
240 if (fs.existsSync(vcvarsphone) && /^wp\d+$/i.test(ver)) {
241 // we found a windows phone sdk!
242 var name = (parseInt(ver.replace(/^wp/i, '')) / 10).toFixed(1);
243 info.wpsdk || (info.wpsdk = {});
244 info.wpsdk[name] = {
245 vcvarsphone: vcvarsphone
246 };
247 }
248 });
249 }
250 }
251
252 if (info.vcvarsall) {
253 appc.subprocess.getRealName(info.vcvarsall, function (err, vcvarsall) {
254 if (!err) {
255 info.vcvarsall = vcvarsall;
256
257 // escape any spaces or brackets
258 vcvarsall = vcvarsall.replace(/(\(|\)|\s)/g, '^$1');
259
260 // now that we have vcvarsall, get the msbuild version
261 appc.subprocess.run('cmd', [ '/C', vcvarsall + ' && MSBuild /version' ], function (code, out, err) {
262 if (code) {
263 results.issues.push({
264 id: 'WINDOWS_MSBUILD_ERROR',
265 type: 'error',
266 message: __('Failed to run MSBuild.') + '\n' +
267 __('This is most likely due to Visual Studio cannot find a suitable .NET framework.') + '\n' +
268 __('Please install the latest .NET framework.')
269 });
270 } else {
271 var chunks = out.trim().split(/\r\n\r\n|\n\n/);
272 chunks.shift(); // strip off the first chunk
273
274 var ver = info.msbuildVersion = chunks.shift().split(/\r\n|\n/).pop().trim();
275
276 if (options.supportedMSBuildVersions && !appc.version.satisfies(ver, options.supportedMSBuildVersions)) {
277 results.issues.push({
278 id: 'WINDOWS_MSBUILD_TOO_OLD',
279 type: 'error',
280 message: __('The MSBuild version %s is too old.', ver) + '\n' +
281 __("Titanium requires .NET MSBuild '%s'.", options.supportedMSBuildVersions) + '\n' +
282 __('Please install the latest .NET framework.')
283 });
284 }
285 }
286
287 next();
288 });
289 } else {
290 next();
291 }
292 });
293 } else {
294 next();
295 }
296 });
297 }, function () {
298 // double check if we didn't find any Visual Studios, then we're done
299 if (!Object.keys(results.visualstudio).length) {
300 results.issues.push({
301 id: 'WINDOWS_VISUAL_STUDIO_NOT_INSTALLED',
302 type: 'error',
303 message: __('Microsoft Visual Studio not found.') + '\n' +
304 __('You will be unable to build Windows Phone or Windows Store apps.')
305 });
306 return finalize();
307 }
308
309 var preferred = options.preferredVisualStudio;
310 if (!results.visualstudio[preferred] || !results.visualstudio[preferred].supported) {
311 preferred = Object.keys(results.visualstudio).filter(function (v) { return results.visualstudio[v].supported; }).sort().pop();
312 }
313 if (preferred) {
314 results.visualstudio[preferred].selected = true;
315 results.selectedVisualStudio = results.visualstudio[preferred];
316 }
317
318 finalize();
319 });
320 });
321 });
322};
323
324/**
325 * Builds a Visual Studio project.
326 *
327 * @param {Object} options - An object containing various settings.
328 * @param {Boolean} [options.bypassCache=false] - When true, re-detects all Visual Studio installations.
329 * @param {String} [options.buildConfiguration='Release'] - The type of configuration to build using. Example: "Release" or "Debug".
330 * @param {String} [options.preferredVisualStudio] - The version of Visual Studio to use by default. Example: "13".
331 * @param {String} [options.preferredWindowsPhoneSDK] - The preferred version of the Windows Phone SDK to use by default. Example "8.0".
332 * @param {String} options.project - The path to the Visual Studio project to build.
333 * @param {String} [options.supportedMSBuildVersions] - A string with a version number or range to check if a MSBuild version is supported.
334 * @param {String} [options.supportedVisualStudioVersions] - A string with a version number or range to check if a Visual Studio install is supported.
335 * @param {String} [options.supportedWindowsPhoneSDKVersions] - A string with a version number or range to check if a Windows Phone SDK is supported.
336 * @param {Function} [callback(err, result)] - A function to call with the build output.
337 *
338 * @emits module:visualstudio#error
339 * @emits module:visualstudio#success
340 *
341 * @returns {EventEmitter}
342 */
343function build(options, callback) {
344 return magik(options, callback, function (emitter, options, callback) {
345 // validate project was specified
346 if (typeof options.project !== 'string' || !options.project) {
347 var ex = new Error(__('Missing required "%s" argument', 'options.project'));
348 emitter.emit('error', ex);
349 return callback(ex);
350 }
351
352 // validate project exists
353 if (!fs.existsSync(options.project)) {
354 var err = new Error(__('Specified project does not exists: %s', options.project));
355 emitter.emit('error', err);
356 return callback(err);
357 }
358
359 detect(options, function (err, results) {
360 if (err) {
361 emitter.emit('error', err);
362 return callback(err);
363 }
364
365 var vsInfo = results.selectedVisualStudio;
366
367 if (!vsInfo || !vsInfo.vcvarsall) {
368 var e = new Error(__('Unable to find a supported Visual Studio installation'));
369 emitter.emit('error', e);
370 return callback(e);
371 }
372
373 // it's possible that this function could be called multiple times for the same
374 // project such as when wptool.exe needs to be built and the master detect() is
375 // running each module's detection code in parallel. so, we need to detect this
376 // and create a queue of events to process after the build completes.
377 if (runningBuilds[options.project]) {
378 runningBuilds[options.project].push({
379 emitter: emitter,
380 callback: callback
381 });
382 return;
383 }
384
385 runningBuilds[options.project] = [ {
386 emitter: emitter,
387 callback: callback
388 } ];
389
390 var p = spawn((process.env.comspec || 'cmd.exe'), [ '/S', '/C', vsInfo.vsDevCmd.replace(/[ \(\)\&]/g, '^$&'),
391 `&& MSBuild /t:rebuild /p:configuration=${(options.buildConfiguration || 'Release')} "${options.project}"`
392 ], { windowsVerbatimArguments: true }),
393 out = '',
394 err = '';
395
396 p.stdout.on('data', function(data) {
397 out += data.toString();
398 });
399
400 p.stderr.on('data', function(data) {
401 err += data.toString();
402 });
403
404 p.on('close', function (code) {
405 var queue = runningBuilds[options.project];
406 delete runningBuilds[options.project];
407
408 var result = {
409 code: code,
410 out: out,
411 err: err
412 };
413
414 queue.forEach(function (p) {
415 if (code) {
416 var err = new Error(__('Failed to build project %s (code %s)', options.project, code));
417 err.extendedError = result;
418 p.emitter.emit('error', err);
419 p.callback(err);
420 } else {
421 p.emitter.emit('success', result);
422 p.callback(null, result);
423 }
424 });
425 });
426 });
427 });
428}