http://git-wip-us.apache.org/repos/asf/cordova-windows/blob/58047a3d/template/cordova/lib/build.js ---------------------------------------------------------------------- diff --git a/template/cordova/lib/build.js b/template/cordova/lib/build.js index 0c065da..e8586f3 100644 --- a/template/cordova/lib/build.js +++ b/template/cordova/lib/build.js @@ -17,20 +17,21 @@ under the License. */ -var Q = require('q'), - path = require('path'), - nopt = require('nopt'), - shell = require('shelljs'), - utils = require('./utils'), - et = require('elementtree'), - prepare = require('./prepare'), - package = require('./package'), - MSBuildTools = require('./MSBuildTools'), - ConfigParser = require('./ConfigParser'), - fs = require('fs'); - -// Platform project root folder -var ROOT = path.join(__dirname, '..', '..'); +var Q = require('q'); +var path = require('path'); +var nopt = require('nopt'); +var shell = require('shelljs'); +var utils = require('./utils'); +var prepare = require('./prepare'); +var package = require('./package'); +var MSBuildTools = require('./MSBuildTools'); +var AppxManifest = require('./AppxManifest'); +var ConfigParser = require('./ConfigParser'); +var fs = require('fs'); + +var events = require('cordova-common').events; +var CordovaError = require('cordova-common').CordovaError; + var projFiles = { phone: 'CordovaApp.Phone.jsproj', win: 'CordovaApp.Windows.jsproj', @@ -44,72 +45,35 @@ var projFilesToManifests = { 'CordovaApp.Windows10.jsproj': 'package.windows10.appxmanifest' }; +var ROOT = path.resolve(__dirname, '../..'); + // builds cordova-windows application with parameters provided. // See 'help' function for args list -module.exports.run = function run (argv) { +module.exports.run = function run (buildOptions) { + + ROOT = this.root || ROOT; - if (!utils.isCordovaProject(ROOT)){ - return Q.reject('Could not find project at ' + ROOT); + if (!utils.isCordovaProject(this.root)){ + return Q.reject(new CordovaError('Could not find project at ' + this.root)); } - return Q.all([parseAndValidateArgs(argv), MSBuildTools.findAllAvailableVersions()]) - .spread(function(buildConfig, msbuildTools) { - // Apply build related configs - prepare.updateBuildConfig(buildConfig); - if (buildConfig.publisherId) { - updateManifestWithPublisher(msbuildTools, buildConfig); - } - // bug: Windows 8 build fails on a system with MSBuild 14 on it. - // Don't regress, make sure MSBuild 4 is selected for a Windows 8 build. - cleanIntermediates(); - return buildTargets(msbuildTools, buildConfig).then(function(pkg) { - console.log(' BUILD OUTPUT: ' + pkg.appx); - return pkg; - }); - }, function(error) { - return Q.reject(error); - }); -}; + var buildConfig = parseAndValidateArgs(buildOptions); -// help/usage function -module.exports.help = function help() { - console.log(''); - console.log('Usage: build [--debug | --release] [--phone | --win] [--bundle]'); - console.log(' [--archs="<list of architectures...>"'); - console.log(' [--packageCertificateKeyFile="key path"]'); - console.log(' [--packageThumbprint="thumbprint"] [--publisherId]'); - console.log(' [--buildConfig="file path"]'); - console.log(' --help : Displays this dialog.'); - console.log(' --debug : Builds project in debug mode. (Default).'); - console.log(' --release (-r) : Builds project in release mode.'); - console.log(' --phone, --win : Specifies, what type of project to build.'); - console.log(' --bundle : Tells the compiler to create a .appxbundle.'); - console.log(' Bundling is disabled when `anycpu` is built.'); - console.log(' --archs : Builds project binaries for specific chip'); - console.log(' architectures (`anycpu`, `arm`, `x86`, `x64`).'); - console.log(' Separate multiple choices with spaces and if'); - console.log(' passing multiple choices, enclose with " ".'); - console.log(' --appx=<8.1-win|8.1-phone|uap>'); - console.log(' : Overrides windows-target-version to build'); - console.log(' Windows 8.1, Windows Phone 8.1, or'); - console.log(' Windows 10 Universal.'); - console.log(' --packageCertificateKeyFile : Builds the project using provided certificate.'); - console.log(' --packageThumbprint : Thumbprint associated with the certificate.'); - console.log(' --publisherId : Sets publisher id field in manifest.'); - console.log(' --buildConfig : Sets build settings from configuration file.'); - console.log(''); - console.log('examples:'); - console.log(' build '); - console.log(' build --debug'); - console.log(' build --release'); - console.log(' build --release --archs="arm x86" --bundle'); - console.log(' build --appx=8.1-phone -r'); - console.log(' build --packageCertificateKeyFile="CordovaApp_TemporaryKey.pfx"'); - console.log(' build --publisherId="CN=FakeCorp, C=US"'); - console.log(' build --buildConfig="build.json"'); - console.log(''); - - process.exit(0); + return MSBuildTools.findAllAvailableVersions() + .then(function(msbuildTools) { + // Apply build related configs + prepare.updateBuildConfig(buildConfig); + if (buildConfig.publisherId) { + updateManifestWithPublisher(msbuildTools, buildConfig); + } + // bug: Windows 8 build fails on a system with MSBuild 14 on it. + // Don't regress, make sure MSBuild 4 is selected for a Windows 8 build. + cleanIntermediates(); + return buildTargets(msbuildTools, buildConfig).then(function(pkg) { + events.emit('verbose', ' BUILD OUTPUT: ' + pkg.appx); + return pkg; + }); + }); }; // returns list of projects to be built based on config.xml and additional parameters (-appx) @@ -125,11 +89,11 @@ module.exports.getBuildTargets = function(isWinSwitch, isPhoneSwitch, projOverr case 'uap': return [projFiles.win10]; default: - console.warn('Unrecognized --appx parameter passed to build: "' + projOverride + '", ignoring.'); + events.emit('warn', 'Unrecognized --appx parameter passed to build: "' + projOverride + '", ignoring.'); break; } } - + var configXML = new ConfigParser(path.join(ROOT, 'config.xml')); var targets = []; var noSwitches = !(isPhoneSwitch || isWinSwitch); @@ -150,7 +114,7 @@ module.exports.getBuildTargets = function(isWinSwitch, isPhoneSwitch, projOverr targets.push(projFiles.win10); break; default: - throw new Error('Unsupported windows-target-version value: ' + windowsTargetVersion); + throw new CordovaError('Unsupported windows-target-version value: ' + windowsTargetVersion); } } @@ -178,122 +142,130 @@ module.exports.getBuildTargets = function(isWinSwitch, isPhoneSwitch, projOverr return targets; }; -function parseAndValidateArgs(argv) { - return Q.promise(function(resolve, reject) { - // parse and validate args - var args = nopt({ - 'debug': Boolean, - 'release': Boolean, - 'archs': [String], - 'appx': String, - 'phone': Boolean, - 'win': Boolean, - 'bundle': Boolean, - 'packageCertificateKeyFile': String, - 'packageThumbprint': String, - 'publisherId': String, - 'buildConfig': String - }, {'-r': '--release'}, argv); - - var config = {}; - var buildConfig = {}; - - // Validate args - if (args.debug && args.release) { - reject('Only one of "debug"/"release" options should be specified'); - return; - } - if (args.phone && args.win) { - reject('Only one of "phone"/"win" options should be specified'); - return; - } +/** + * Parses and validates buildOptions object and platform-specific CLI arguments, + * provided via argv field + * + * @param {Object} [options] An options object. If not specified, result + * will be populated with default values. + * + * @return {Object} Build configuration, used by other methods + */ +function parseAndValidateArgs(options) { + // parse and validate args + var args = nopt({ + 'archs': [String], + 'appx': String, + 'phone': Boolean, + 'win': Boolean, + 'bundle': Boolean, + 'packageCertificateKeyFile': String, + 'packageThumbprint': String, + 'publisherId': String, + 'buildConfig': String + }, {}, options.argv, 0); + + var config = {}; + var buildConfig = {}; + + // Validate args + if (options.debug && options.release) { + throw new CordovaError('Only one of "debug"/"release" options should be specified'); + } - // get build options/defaults - config.buildType = args.release ? 'release' : 'debug'; - config.buildArchs = args.archs ? args.archs.split(' ') : ['anycpu']; - config.phone = args.phone ? true : false; - config.win = args.win ? true : false; - config.projVerOverride = args.appx; - // only set config.bundle if architecture is not anycpu - if (args.bundle) { - if ((config.buildArchs.indexOf('anycpu') > -1 || config.buildArchs.indexOf('any cpu') > -1) && config.buildArchs.length > 1) { - // Not valid to bundle anycpu with cpu-specific architectures. warn, then don't bundle - console.warn('Warning: anycpu and CPU-specific architectures were selected. This is not valid'); - console.warn(' when enabling bundling with --bundle. Disabling bundling for this build.'); - } else { - config.bundle = true; - } - } + if (args.phone && args.win) { + throw new CordovaError('Only one of "phone"/"win" options should be specified'); + } - // if build.json is provided, parse it - var buildConfigPath = args.buildConfig; - if (buildConfigPath) { - buildConfig = parseBuildConfig(buildConfigPath, config); - for (var prop in buildConfig) { config[prop] = buildConfig[prop]; } + // get build options/defaults + config.buildType = options.release ? 'release' : 'debug'; + config.buildArchs = args.archs ? args.archs.split(' ') : ['anycpu']; + config.phone = args.phone ? true : false; + config.win = args.win ? true : false; + config.projVerOverride = args.appx; + // only set config.bundle if architecture is not anycpu + if (args.bundle) { + if (config.buildArchs.length > 1 && (config.buildArchs.indexOf('anycpu') > -1 || config.buildArchs.indexOf('any cpu') > -1)) { + // Not valid to bundle anycpu with cpu-specific architectures. warn, then don't bundle + events.emit('warn', '"anycpu" and CPU-specific architectures were selected. ' + + 'This is not valid when enabling bundling with --bundle. Disabling bundling for this build.'); + } else { + config.bundle = true; } + } - // CLI arguments override build.json config - if (args.packageCertificateKeyFile) { - args.packageCertificateKeyFile = path.resolve(process.cwd(), args.packageCertificateKeyFile); - config.packageCertificateKeyFile = args.packageCertificateKeyFile; - } + // if build.json is provided, parse it + var buildConfigPath = args.buildConfig; + if (buildConfigPath) { + buildConfig = parseBuildConfig(buildConfigPath, config.buildType); + for (var prop in buildConfig) { config[prop] = buildConfig[prop]; } + } - config.packageThumbprint = config.packageThumbprint || args.packageThumbprint; - config.publisherId = config.publisherId || args.publisherId; - resolve(config); - }); + // CLI arguments override build.json config + if (args.packageCertificateKeyFile) { + args.packageCertificateKeyFile = path.resolve(process.cwd(), args.packageCertificateKeyFile); + config.packageCertificateKeyFile = args.packageCertificateKeyFile; + } + + config.packageThumbprint = config.packageThumbprint || args.packageThumbprint; + config.publisherId = config.publisherId || args.publisherId; + + return config; } -function parseBuildConfig(buildConfigPath, config) { +function parseBuildConfig(buildConfigPath, buildType) { var buildConfig, result = {}; - console.log('Reading build config file: '+ buildConfigPath); + events.emit('verbose', 'Reading build config file: '+ buildConfigPath); try { var contents = fs.readFileSync(buildConfigPath, 'utf8'); buildConfig = JSON.parse(contents); } catch (e) { if (e.code === 'ENOENT') { - throw Error('Specified build config file does not exist: ' + buildConfigPath); - } else { + throw new CordovaError('Specified build config file does not exist: ' + buildConfigPath); + } + else { throw e; } } - if (buildConfig.windows && buildConfig.windows[config.buildType]) { - var windowsInfo = buildConfig.windows[config.buildType]; + if (!(buildConfig.windows && buildConfig.windows[buildType])) return {}; - // If provided assume it's a relative path - if(windowsInfo.packageCertificateKeyFile) { - var buildPath = path.dirname(fs.realpathSync(buildConfigPath)); - result.packageCertificateKeyFile = path.resolve(buildPath, windowsInfo.packageCertificateKeyFile); - } + var windowsInfo = buildConfig.windows[buildType]; - if(windowsInfo.packageThumbprint) { - result.packageThumbprint = windowsInfo.packageThumbprint; - } + // If provided assume it's a relative path + if(windowsInfo.packageCertificateKeyFile) { + var buildPath = path.dirname(fs.realpathSync(buildConfigPath)); + result.packageCertificateKeyFile = path.resolve(buildPath, windowsInfo.packageCertificateKeyFile); + } - if(windowsInfo.publisherId) { - // Quickly validate publisherId - var publisherRegexStr = '(CN|L|O|OU|E|C|S|STREET|T|G|I|SN|DC|SERIALNUMBER|(OID\\.(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))+))=' + - '(([^,+="<>#;])+|".*")(, (' + - '(CN|L|O|OU|E|C|S|STREET|T|G|I|SN|DC|SERIALNUMBER|(OID\\.(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))+))=' + - '(([^,+="<>#;])+|".*")))*'; + if(windowsInfo.packageThumbprint) { + result.packageThumbprint = windowsInfo.packageThumbprint; + } - var publisherRegex = new RegExp(publisherRegexStr); + if(windowsInfo.publisherId) { + // Quickly validate publisherId + var publisherRegexStr = '(CN|L|O|OU|E|C|S|STREET|T|G|I|SN|DC|SERIALNUMBER|(OID\\.(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))+))=' + + '(([^,+="<>#;])+|".*")(, (' + + '(CN|L|O|OU|E|C|S|STREET|T|G|I|SN|DC|SERIALNUMBER|(OID\\.(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))+))=' + + '(([^,+="<>#;])+|".*")))*'; - if (!publisherRegex.test(windowsInfo.publisherId)) { - throw Error('Invalid publisher id: ' + windowsInfo.publisherId); - } + var publisherRegex = new RegExp(publisherRegexStr); - result.publisherId = windowsInfo.publisherId; + if (!publisherRegex.test(windowsInfo.publisherId)) { + throw new CordovaError('Invalid publisher id: ' + windowsInfo.publisherId); } + + result.publisherId = windowsInfo.publisherId; } return result; } -// Note: This function is very narrow and only writes to the app manifest if an update is done. See CB-9450 for the +// Note: This function is very narrow and only writes to the app manifest if an update is done. See CB-9450 for the // reasoning of why this is the case. function updateManifestWithPublisher(allMsBuildVersions, config) { + if (!config.publisherId) return; + var selectedBuildTargets = getBuildTargets(config); var msbuild = getMsBuildForTargets(selectedBuildTargets, config, allMsBuildVersions); var myBuildTargets = filterSupportedTargets(selectedBuildTargets, msbuild); @@ -302,30 +274,20 @@ function updateManifestWithPublisher(allMsBuildVersions, config) { }); manifestFiles.forEach(function(file) { var manifestPath = path.join(ROOT, file); - var contents = fs.readFileSync(manifestPath, 'utf-8'); - if (!contents) { - return; - } - - // Skip BOM - contents = contents.substring(contents.indexOf('<')); - var manifest = new et.ElementTree(et.XML(contents)); - var identityNode = manifest.find('.//Identity'); - if (config.publisherId && config.publisherId !== identityNode.attrib.Publisher) { - identityNode.attrib.Publisher = config.publisherId; - fs.writeFileSync(manifestPath, manifest.write({indent: 4}), 'utf-8'); - } + AppxManifest.get(manifestPath) + .getIdentity().setPublisher(config.publisherId) + .write(); }); } function buildTargets(allMsBuildVersions, config) { // filter targets to make sure they are supported on this development machine var selectedBuildTargets = getBuildTargets(config); - var msbuild = getMsBuildForTargets(selectedBuildTargets, config, allMsBuildVersions); + var msbuild = getMsBuildForTargets(selectedBuildTargets, config, allMsBuildVersions); if (!msbuild) { - return Q.reject('No valid MSBuild was detected for the selected target.'); + return Q.reject(new CordovaError('No valid MSBuild was detected for the selected target.')); } - console.log('MSBuildToolsPath: ' + msbuild.path); + events.emit('vebose', 'Using MSBuild v' + msbuild.version + ' from ' + msbuild.path); var myBuildTargets = filterSupportedTargets(selectedBuildTargets, msbuild); var buildConfigs = []; @@ -334,7 +296,7 @@ function buildTargets(allMsBuildVersions, config) { var shouldBundle = !!config.bundle; if (myBuildTargets.indexOf(projFiles.win80) > -1) { if (shouldBundle) { - console.warn('Warning: Bundling is disabled because a Windows 8 project was detected.'); + events.emit('warn', 'Bundling is disabled because a Windows 8 project was detected.'); } shouldBundle = false; } @@ -400,8 +362,7 @@ function clearIntermediatesAndGetPackage(bundleTerms, config, hasAnyCpu) { // However, that generates intermediate bundles, like "CordovaApp.Windows10_0.0.1.0_x64.appxbundle" // We need to clear the intermediate bundles, or else "cordova run" will fail because of too // many .appxbundle files. - - console.log('Clearing intermediates...'); + events.emit('verbose', 'Clearing intermediates...'); var appPackagesPath = path.join(ROOT, 'AppPackages'); var childDirectories = shell.ls(path.join(appPackagesPath, '*')).map(function(pathName) { return { path: pathName, stats: fs.statSync(pathName) }; @@ -422,7 +383,7 @@ function clearIntermediatesAndGetPackage(bundleTerms, config, hasAnyCpu) { if (hasAnyCpu) { archSearchString = 'AnyCPU' + (config.buildType === 'debug' ? '_debug' : '') + '.appxbundle'; } - + var filesToDelete = shell.ls(path.join(outputDirectory.path, '*.appx*')).filter(function(appxbundle) { var isMatch = appxbundle.indexOf(archSearchString) === -1; if (!isMatch) { @@ -498,7 +459,7 @@ function getBuildTargets(buildConfig) { targets = [projFiles.win10]; break; default: - console.warn('Unrecognized --appx parameter passed to build: "' + buildConfig.projVerOverride + '", ignoring.'); + events.emit('warn', 'Unrecognized --appx parameter passed to build: "' + buildConfig.projVerOverride + '", ignoring.'); break; } } @@ -528,7 +489,8 @@ function getBuildTargets(buildConfig) { } function getMsBuildForTargets(selectedTargets, buildConfig, allMsBuildVersions) { - var availableVersions = allMsBuildVersions.reduce(function(obj, msbuildVersion) { + var availableVersions = allMsBuildVersions + .reduce(function(obj, msbuildVersion) { obj[msbuildVersion.version] = msbuildVersion; return obj; }, {}); @@ -538,7 +500,7 @@ function getMsBuildForTargets(selectedTargets, buildConfig, allMsBuildVersions) if (selectedTargets.indexOf(projFiles.win80) > -1) { // building Windows 8; prefer 4.0, unless phone is also present, in which case prefer 12 // prefer 12. If not present, can't build this; error in the filterSupportedTargets function - result = availableVersions['12.0'] || availableVersions['4.0']; + result = availableVersions['12.0'] || availableVersions['4.0']; } else { // 14 can build Windows 10, Windows 8.1, and Windows Phone 8.1, so resolve to 14 if available, else 12 result = (availableVersions['14.0'] || availableVersions['12.0']); @@ -565,7 +527,7 @@ function msBuild14TargetsFilter(target) { function filterSupportedTargets (targets, msbuild) { if (!targets || targets.length === 0) { - console.warn('\r\nNo build targets are specified.'); + events.emit('warn', 'No build targets are specified.'); return []; } @@ -577,15 +539,16 @@ function filterSupportedTargets (targets, msbuild) { var filter = targetFilters[msbuild.version]; if (!filter) { - console.warn('Unsupported msbuild version "' + msbuild.version + '", aborting.'); + events.emit('warn', 'Unsupported msbuild version "' + msbuild.version + '", aborting.'); return []; } var supportedTargets = targets.filter(filter); // unsupported targets have been detected if (supportedTargets.length !== targets.length) { - console.warn('Warning: Not all desired build targets are compatible with the current build environment.'); - console.warn('Please install Visual Studio 2015 for Windows 8.1 and Windows 10, or Visual Studio 2013 Update 2 for Windows 8 and 8.1.'); + events.emit('warn', 'Not all desired build targets are compatible with the current build environment. ' + + 'Please install Visual Studio 2015 for Windows 8.1 and Windows 10, ' + + 'or Visual Studio 2013 Update 2 for Windows 8 and 8.1.'); } return supportedTargets; } @@ -596,3 +559,13 @@ function cleanIntermediates() { shell.rm('-rf', buildPath); } } + +// cleans the project, removes AppPackages and build folders. +module.exports.clean = function () { + var projectPath = this.root; + ['AppPackages', 'build'] + .forEach(function(dir) { + shell.rm('-rf', path.join(projectPath, dir)); + }); + return Q.resolve(); +};
http://git-wip-us.apache.org/repos/asf/cordova-windows/blob/58047a3d/template/cordova/lib/deployment.js ---------------------------------------------------------------------- diff --git a/template/cordova/lib/deployment.js b/template/cordova/lib/deployment.js index 9ba17ad..a728d11 100644 --- a/template/cordova/lib/deployment.js +++ b/template/cordova/lib/deployment.js @@ -17,40 +17,17 @@ under the License. */ +/*jshint -W069 */ + var Q = require('q'), fs = require('fs'), - /* jshint ignore:start */ // 'path' only used in ignored blocks - path = require('path'), - /* jshint ignore:end */ - proc = require('child_process'); + path = require('path'); +var spawn = require('cordova-common').superspawn.spawn; +var events = require('cordova-common').events; var E_INVALIDARG = 2147942487; -// neither 'exec' nor 'spawn' was sufficient because we need to pass arguments via spawn -// but also need to be able to capture stdout / stderr -function run(cmd, args, opt_cwd) { - var d = Q.defer(); - try { - var child = proc.spawn(cmd, args, {cwd: opt_cwd, maxBuffer: 1024000}); - var stdout = '', stderr = ''; - child.stdout.on('data', function(s) { stdout += s; }); - child.stderr.on('data', function(s) { stderr += s; }); - child.on('exit', function(code) { - if (code) { - d.reject({ message: stderr, code: code, toString: function() { return stderr; }}); - } else { - d.resolve(stdout); - } - }); - } catch(e) { - console.error('error caught: ' + e); - d.reject(e); - } - return d.promise; -} - function DeploymentTool() { - } /** @@ -148,10 +125,8 @@ function AppDeployCmdTool(targetOsVersion) { DeploymentTool.call(this); this.targetOsVersion = targetOsVersion; - /* jshint ignore:start */ /* Ignore jshint to use dot notation for 2nd process.env access for consistency */ var programFilesPath = process.env['ProgramFiles(x86)'] || process.env['ProgramFiles']; this.path = path.join(programFilesPath, 'Microsoft SDKs', 'Windows Phone', 'v' + this.targetOsVersion, 'Tools', 'AppDeploy', 'AppDeployCmd.exe'); - /* jshint ignore:end */ } AppDeployCmdTool.prototype = Object.create(DeploymentTool.prototype); @@ -164,7 +139,7 @@ AppDeployCmdTool.prototype.enumerateDevices = function() { // [(line), 9, 'Emulator 8.1 720P 4.7 inch'] // Expansion is: space, index, spaces, name var LINE_TEST = /^\s(\d+?)\s+(.+?)$/m; - return run(that.path, ['/EnumerateDevices']).then(function(result) { + return spawn(that.path, ['/EnumerateDevices']).then(function(result) { var lines = result.split('\n'); var matchedLines = lines.filter(function(line) { return LINE_TEST.test(line); @@ -205,12 +180,12 @@ AppDeployCmdTool.prototype.installAppPackage = function(pathToAppxPackage, targe } var that = this; - var result = run(this.path, [command, pathToAppxPackage, '/targetdevice:' + targetDevice.__shorthand]); + var result = spawn(this.path, [command, pathToAppxPackage, '/targetdevice:' + targetDevice.__shorthand]); if (targetDevice.type === 'emulator') { result = result.then(null, function(e) { // CB-9482: AppDeployCmd also reports E_INVALIDARG during this process. If so, try to repeat. if (e.code === E_INVALIDARG) { - return run(that.path, [command, pathToAppxPackage, '/targetdevice:' + targetDevice.__shorthand]); + return spawn(that.path, [command, pathToAppxPackage, '/targetdevice:' + targetDevice.__shorthand]); } throw e; @@ -227,11 +202,11 @@ AppDeployCmdTool.prototype.uninstallAppPackage = function(packageInfo, targetDev // state, it allows install to proceed. (Install will fail if there is a legitimate // uninstall failure such as due to no device). var assureSuccess = function() {}; - return run(this.path, ['/uninstall', packageInfo, '/targetdevice:' + targetDevice.__shorthand]).then(assureSuccess, assureSuccess); + return spawn(this.path, ['/uninstall', packageInfo, '/targetdevice:' + targetDevice.__shorthand]).then(assureSuccess, assureSuccess); }; AppDeployCmdTool.prototype.launchApp = function(packageInfo, targetDevice) { - return run(this.path, ['/launch', packageInfo, '/targetdevice:' + targetDevice.__shorthand]); + return spawn(this.path, ['/launch', packageInfo, '/targetdevice:' + targetDevice.__shorthand]); }; function WinAppDeployCmdTool(targetOsVersion) { @@ -240,10 +215,8 @@ function WinAppDeployCmdTool(targetOsVersion) { DeploymentTool.call(this); this.targetOsVersion = targetOsVersion; - /* jshint ignore:start */ /* Ignore jshint to use dot notation for 2nd process.env access for consistency */ var programFilesPath = process.env['ProgramFiles(x86)'] || process.env['ProgramFiles']; this.path = path.join(programFilesPath, 'Windows Kits', '10', 'bin', 'x86', 'WinAppDeployCmd.exe'); - /* jshint ignore:end */ } WinAppDeployCmdTool.prototype = Object.create(DeploymentTool.prototype); @@ -257,7 +230,7 @@ WinAppDeployCmdTool.prototype.enumerateDevices = function() { // The expansion is: IP address, spaces, GUID, spaces, text name var LINE_TEST = /^([\d\.]+?)\s+([\da-fA-F\-]+?)\s+(.+)$/m; - return run(that.path, ['devices']).then(function(result) { + return spawn(that.path, ['devices']).then(function(result) { var lines = result.split('\n'); var matchedLines = lines.filter(function(line) { return LINE_TEST.test(line); @@ -283,8 +256,8 @@ WinAppDeployCmdTool.prototype.enumerateDevices = function() { WinAppDeployCmdTool.prototype.installAppPackage = function(pathToAppxPackage, targetDevice, shouldLaunch, shouldUpdate, pin) { if (shouldLaunch) { - console.warn('Warning: Cannot launch app with current version of Windows 10 SDK tools.'); - console.warn(' You will have to launch the app after installation is completed.'); + events.emit('warn', 'Cannot launch app with current version of Windows 10 SDK tools. ' + + 'You will have to launch the app after installation is completed.'); } var args = [shouldUpdate ? 'update' : 'install', '-file', pathToAppxPackage, '-ip', targetDevice.__ip]; @@ -293,13 +266,13 @@ WinAppDeployCmdTool.prototype.installAppPackage = function(pathToAppxPackage, ta args.push(pin); } - return run(this.path, args).then(function() { - console.log('Deployment completed successfully.'); + return spawn(this.path, args).then(function() { + events.emit('log', 'Deployment completed successfully.'); }); }; WinAppDeployCmdTool.prototype.uninstallAppPackage = function(packageInfo, targetDevice) { - return run(this.path, ['uninstall', '-package', packageInfo, '-ip', targetDevice.__ip]); + return spawn(this.path, ['uninstall', '-package', packageInfo, '-ip', targetDevice.__ip]); }; // usage: require('deployment').getDeploymentTool('8.1'); http://git-wip-us.apache.org/repos/asf/cordova-windows/blob/58047a3d/template/cordova/lib/exec.js ---------------------------------------------------------------------- diff --git a/template/cordova/lib/exec.js b/template/cordova/lib/exec.js deleted file mode 100644 index d71eda7..0000000 --- a/template/cordova/lib/exec.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -var child_process = require('child_process'), - Q = require('q'); - -// Takes a command and optional current working directory. -// Returns a promise that either resolves with the stdout, or -// rejects with an error message and the stderr. -module.exports = function(cmd, opt_cwd) { - var d = Q.defer(); - try { - child_process.exec(cmd, {cwd: opt_cwd, maxBuffer: 1024000}, function(err, stdout, stderr) { - if (err) d.reject('Error executing "' + cmd + '": ' + stderr); - else d.resolve(stdout); - }); - } catch(e) { - console.error('error caught: ' + e); - d.reject(e); - } - return d.promise; -}; http://git-wip-us.apache.org/repos/asf/cordova-windows/blob/58047a3d/template/cordova/lib/log.js ---------------------------------------------------------------------- diff --git a/template/cordova/lib/log.js b/template/cordova/lib/log.js index f095f4b..d01c4af 100644 --- a/template/cordova/lib/log.js +++ b/template/cordova/lib/log.js @@ -20,11 +20,11 @@ // requires var path = require('path'), et = require('elementtree'), - Q = require('q'), - cp = require('child_process'), ConfigParser = require('./ConfigParser.js'), nopt = require('nopt'); +var spawn = require('cordova-common').superspawn.spawn; + // paths var platformRoot = path.join(__dirname, '..', '..'), projectRoot = path.join(platformRoot, '..', '..'), @@ -186,16 +186,12 @@ function dumpLogs(startTime) { } function getEvents(channel, startTime) { - var d = Q.defer(); - var command = 'wevtutil qe ' + channel + ' /q:"*[System [(TimeCreated [@SystemTime>\'' + startTime + '\'])]]" /e:root'; - cp.exec(command, function (error, stdout, stderr) { - if (error) { - d.reject('Failed to run wevtutil command. ' + error); - } else { - d.resolve(parseEvents(stdout)); - } + var command = 'wevtutil'; + var args = ['qe', channel, '/q:"*[System [(TimeCreated [@SystemTime>\'' + startTime + '\'])]]"', '/e:root']; + return spawn(command, args) + .then(function(stdout) { + return parseEvents(stdout); }); - return d.promise; } function getElementValue(et, element, attribute) { @@ -206,7 +202,7 @@ function getElementValue(et, element, attribute) { if (!!attribute) { result = found[0].get(attribute); } else { - result = found[0].text; + result = found[0].text; } } @@ -267,7 +263,7 @@ function parseEvents(output) { result.displayName = undefined; } - // cut out uninformative fields + // cut out uninformative fields if ((result.line === '0') && (result.column === '0')) { result.line = undefined; result.column = undefined; @@ -333,14 +329,16 @@ function stringifyEvent(event) { } function getLogState(channel) { - return exec('wevtutil get-log "' + channel + '"').then(function(output) { + return spawn('wevtutil', ['get-log', channel]) + .then(function(output) { return output.indexOf('enabled: true') != -1; }); } function enableChannel(channel) { - return exec('wevtutil set-log "' + channel + '" /e:false /q:true').then(function() { - return exec('wevtutil set-log "' + channel + '" /e:true /rt:true /ms:4194304 /q:true'); + return spawn('wevtutil', ['set-log', channel, '/e:false', '/q:true']) + .then(function() { + return spawn('wevtutil', ['set-log', channel, '/e:true', '/rt:true', '/ms:4194304','/q:true']); }, function() { console.warn('Cannot enable log channel: ' + channel); console.warn('Try running the script with administrator privileges.'); @@ -349,16 +347,5 @@ function enableChannel(channel) { function disableChannel(channel) { console.log('Disabling channel ' + channel); - exec('wevtutil set-log "' + channel + '" /e:false /q:true'); -} - -function exec(command) { - var d = Q.defer(); - cp.exec(command, function (error, stdout) { - if (error) { - d.reject('An error occured while executing following command:\n' + command + '\n' + error); - } - d.resolve(stdout); - }); - return d.promise; + spawn('wevtutil', ['set-log', channel, '/e:false', '/q:true']); } http://git-wip-us.apache.org/repos/asf/cordova-windows/blob/58047a3d/template/cordova/lib/package.js ---------------------------------------------------------------------- diff --git a/template/cordova/lib/package.js b/template/cordova/lib/package.js index 9664987..cd3de0e 100644 --- a/template/cordova/lib/package.js +++ b/template/cordova/lib/package.js @@ -17,11 +17,14 @@ under the License. */ -var Q = require('q'), - fs = require('fs'), - path = require('path'), - spawn = require('./spawn'), - utils = require('./utils'); +var Q = require('q'); +var fs = require('fs'); +var path = require('path'); +var utils = require('./utils'); +var AppxManifest = require('./AppxManifest'); +var events = require('cordova-common').events; +var spawn = require('cordova-common').superspawn.spawn; +var CordovaError = require('cordova-common').CordovaError; // returns folder that contains package with chip architecture, // build and project types specified by script parameters @@ -96,9 +99,9 @@ module.exports.getPackageFileInfo = function (packageFile) { // return package app ID fetched from appxmanifest // return rejected promise if appxmanifest not valid module.exports.getAppId = function (platformPath) { - var manifest = path.join(platformPath, 'package.phone.appxmanifest'); try { - return /PhoneProductId="(.*?)"/gi.exec(fs.readFileSync(manifest, 'utf8'))[1]; + return AppxManifest.get(path.join(platformPath, 'package.phone.appxmanifest')) + .getPhoneIdentity().getPhoneProductId(); } catch (e) { throw new Error('Can\'t read appId from phone manifest', e); } @@ -110,9 +113,9 @@ function getPackageName(platformPath) { // Can reliably read from package.windows.appxmanifest even if targeting Windows 10 // because the function is only used for desktop deployment, which always has the same // package name when uninstalling / reinstalling - var manifest = path.join(platformPath, 'package.windows.appxmanifest'); try { - return Q.resolve(/Identity Name="(.*?)"/gi.exec(fs.readFileSync(manifest, 'utf8'))[1]); + return Q.when(AppxManifest.get(path.join(platformPath, 'package.windows.appxmanifest')) + .getIdentity().getName()); } catch (e) { return Q.reject('Can\'t read package name from manifest ' + e); } @@ -154,23 +157,20 @@ module.exports.findDevice = function (deploymentTool, target) { // returns array of available devices names module.exports.listDevices = function (deploymentTool) { - + return deploymentTool.enumerateDevices().then(function(deviceList) { return deviceList.map(function(device) { return device.toString(); }); }, function(e) { - console.warn('Failed to enumerate devices'); - console.warn(e); - - throw e; + events.emit('error', new Error('Failed to enumerate devices: ' + e)); }); }; function uninstallAppFromPhone(appDeployUtils, package, target) { - console.log('Attempting to remove previously installed application...'); + events.emit('log', 'Attempting to remove previously installed application...'); return appDeployUtils.uninstallAppPackage(package.phoneId, target); } @@ -186,24 +186,24 @@ module.exports.deployToPhone = function (package, deployTarget, targetWindows10, return deployment.then(function(deploymentTool) { return module.exports.findDevice(deploymentTool, deployTarget).then(function(target) { - return uninstallAppFromPhone(deploymentTool, package, target).then( - function() {}, function() {}).then(function() { - // shouldUpdate = false because we've already uninstalled - console.log('Deploying app package...'); - return deploymentTool.installAppPackage(package.appx, target, /*shouldLaunch*/ true, /*shouldUpdate*/ false); - }).then(function() { }, function(error) { - if (error.message.indexOf('Error code 2148734208 for command') === 0) { - return deploymentTool.installAppPackage(package.appx, target, /*shouldLaunch*/ true, /*shouldUpdate*/ true); - } else if (error.message.indexOf('Error code -2146233088') === 0) { - throw new Error('No Windows Phone device was detected.'); - } else { - console.warn('Unexpected error from installation:'); - console.warn(error.message); - console.warn('You may have previously installed the app with an earlier version of cordova-windows.'); - console.warn('Ensure the app is uninstalled from the phone and then try to run again.'); - throw error; - } - }); + return uninstallAppFromPhone(deploymentTool, package, target) + .then(function() {}, function() {}) + .then(function() { + // shouldUpdate = false because we've already uninstalled + events.emit('log', 'Deploying app package...'); + return deploymentTool.installAppPackage(package.appx, target, /*shouldLaunch*/ true, /*shouldUpdate*/ false); + }) + .then(function() { }, function(error) { + if (error.message.indexOf('Error code 2148734208 for command') === 0) { + return deploymentTool.installAppPackage(package.appx, target, /*shouldLaunch*/ true, /*shouldUpdate*/ true); + } else if (error.message.indexOf('Error code -2146233088') === 0) { + throw new CordovaError('No Windows Phone device was detected.'); + } else { + throw new CordovaError('Unexpected error from installation: ' + error.message + + ' You may have previously installed the app with an earlier version of cordova-windows.' + + ' Ensure the app is uninstalled from the phone and then try to run again.'); + } + }); }); }); }; @@ -219,19 +219,25 @@ module.exports.deployToDesktop = function (package, deployTarget) { var oldArch; // uninstalls previous application instance (if exists) - console.log('Attempt to uninstall previous application version...'); - return spawn('powershell', ['-ExecutionPolicy', 'RemoteSigned', 'Import-Module "' + appStoreUtils + '"; Uninstall-App ' + pkgname]) + events.emit('log', 'Attempting to uninstall previous application version...'); + return spawn('powershell', ['-ExecutionPolicy', 'RemoteSigned', + 'Import-Module "' + appStoreUtils + '"; Uninstall-App ' + pkgname], + { stdio: 'inherit' }) .then(function() { - console.log('Attempt to install application...'); + events.emit('log', 'Attempting to install application...'); oldArch = process.env.PROCESSOR_ARCHITECTURE; if (package.arch === 'x64') { process.env.PROCESSOR_ARCHITECTURE = 'AMD64'; } - return spawn('powershell', ['-ExecutionPolicy', 'RemoteSigned', 'Import-Module "' + appStoreUtils + '"; Install-App', utils.quote(package.script)]); + return spawn('powershell', ['-ExecutionPolicy', 'RemoteSigned', + 'Import-Module "' + appStoreUtils + '"; Install-App', utils.quote(package.script)], + { stdio: 'inherit' }); }).then(function() { process.env.PROCESSOR_ARCHITECTURE = oldArch; - console.log('Starting application...'); - return spawn('powershell', ['-ExecutionPolicy', 'RemoteSigned', 'Import-Module "' + appStoreUtils + '"; Start-Locally', pkgname]); + events.emit('log', 'Starting application...'); + return spawn('powershell', ['-ExecutionPolicy', 'RemoteSigned', + 'Import-Module "' + appStoreUtils + '"; Start-Locally', pkgname], + { stdio: 'inherit' }); }, function (error) { process.env.PROCESSOR_ARCHITECTURE = oldArch; throw error; http://git-wip-us.apache.org/repos/asf/cordova-windows/blob/58047a3d/template/cordova/lib/prepare.js ---------------------------------------------------------------------- diff --git a/template/cordova/lib/prepare.js b/template/cordova/lib/prepare.js index b930f62..01b8798 100644 --- a/template/cordova/lib/prepare.js +++ b/template/cordova/lib/prepare.js @@ -17,61 +17,40 @@ under the License. */ -var path = require('path'), - fs = require('fs'), - et = require('elementtree'), - subElement = et.SubElement, - shell = require('shelljs'), - MSBuildTools = require('./MSBuildTools'), - Version = require('./Version'), - ConfigParser = require('./ConfigParser'); - -var ROOT = path.join(__dirname, '..', '..'), - PROJECT_WINDOWS10 = 'CordovaApp.Windows10.jsproj', +var Q = require('q'); +var fs = require('fs'); +var path = require('path'); +var shell = require('shelljs'); +var et = require('elementtree'); +var Version = require('./Version'); +var AppxManifest = require('./AppxManifest'); +var MSBuildTools = require('./MSBuildTools'); +var ConfigParser = require('./ConfigParser'); +var events = require('cordova-common').events; +var xmlHelpers = require('cordova-common').xmlHelpers; + +var PROJECT_WINDOWS10 = 'CordovaApp.Windows10.jsproj', MANIFEST_WINDOWS8 = 'package.windows80.appxmanifest', MANIFEST_WINDOWS = 'package.windows.appxmanifest', MANIFEST_PHONE = 'package.phone.appxmanifest', - MANIFEST_WINDOWS10 = 'package.windows10.appxmanifest', - BASE_UAP_VERSION = new Version(10, 0, 10240, 0), - UAP_RESTRICTED_CAPS = ['enterpriseAuthentication', 'sharedUserCertificates', - 'documentsLibrary', 'musicLibrary', 'picturesLibrary', - 'videosLibrary', 'removableStorage', 'internetClientServer', - 'privateNetworkClientServer'], - // UAP namespace capabilities come from the XSD type ST_Capability_Uap from AppxManifestTypes.xsd - CAPS_NEEDING_UAPNS = ['documentsLibrary', 'picturesLibrary', 'videosLibrary', - 'musicLibrary', 'enterpriseAuthentication', 'sharedUserCertificates', - 'removableStorage', 'appointments', 'contacts', 'userAccountInformation', - 'phoneCall', 'blockedChatMessages', 'objects3D']; + MANIFEST_WINDOWS10 = 'package.windows10.appxmanifest'; var TEMPLATE = '<?xml version="1.0" encoding="utf-8"?>\n' + '<!--\n This file is automatically generated.\n' + ' Do not modify this file - YOUR CHANGES WILL BE ERASED!\n-->\n'; +/** Note: this is only for backward compatibility, since it is being called directly from windows_parser */ module.exports.applyPlatformConfig = function() { - var config = new ConfigParser(path.join(ROOT, 'config.xml')); - - // Apply appxmanifest changes - [{ fileName: MANIFEST_WINDOWS, namespacePrefix: 'm2:' }, - { fileName: MANIFEST_WINDOWS8, namespacePrefix: '' }, - { fileName: MANIFEST_WINDOWS10, namespacePrefix: 'uap:' }, - { fileName: MANIFEST_PHONE, namespacePrefix: 'm3:' }].forEach( - function(manifestFile) { - // Break out Windows 10-specific functionality because we also need to - // apply UAP versioning to Windows 10 appx-manifests. - var isTargetingWin10 = (manifestFile.fileName === MANIFEST_WINDOWS10); - updateManifestFile(config, path.join(ROOT, manifestFile.fileName), manifestFile.namespacePrefix, isTargetingWin10); - }); - - if (process.platform === 'win32') { - applyUAPVersionToProject(path.join(ROOT, PROJECT_WINDOWS10), getUAPVersions()); - } - - copyImages(config); + var projectRoot = path.join(__dirname, '../..'); + var appConfig = new ConfigParser(path.join(projectRoot, '../../config.xml')); + updateProjectAccordingTo(appConfig); + copyImages(appConfig, projectRoot); }; module.exports.updateBuildConfig = function(buildConfig) { - var config = new ConfigParser(path.join(ROOT, 'config.xml')); + var projectRoot = path.join(__dirname, '../..'); + var config = new ConfigParser(path.join(projectRoot, 'config.xml')); // if no buildConfig is provided dont do anything buildConfig = buildConfig || {}; @@ -96,7 +75,7 @@ module.exports.updateBuildConfig = function(buildConfig) { if (config.packageCertificateKeyFile) { // Convert packageCertificateKeyFile from absolute to relative path - packageCertificateKeyFile = path.relative(ROOT, packageCertificateKeyFile); + packageCertificateKeyFile = path.relative(projectRoot, packageCertificateKeyFile); } var certificatePropertyElement = new et.Element('PackageCertificateKeyFile'); @@ -120,88 +99,78 @@ module.exports.updateBuildConfig = function(buildConfig) { propertyGroup.append(defaultLocaleElement); var buildConfigFileName = buildConfig.buildType === 'release' ? - path.join(ROOT, 'CordovaAppRelease.projitems') : - path.join(ROOT, 'CordovaAppDebug.projitems'); + path.join(projectRoot, 'CordovaAppRelease.projitems') : + path.join(projectRoot, 'CordovaAppDebug.projitems'); fs.writeFileSync(buildConfigFileName, TEMPLATE + buildConfigXML.write({indent: 2, xml_declaration: false}), 'utf-8'); }; -function updateManifestFile (config, manifestPath, namespacePrefix, isTargetingWin10) { - var contents = fs.readFileSync(manifestPath, 'utf-8'); - if(contents) { - //Windows is the BOM. Skip the Byte Order Mark. - contents = contents.substring(contents.indexOf('<')); - } +function updateManifestFile (config, manifestPath) { + var manifest = AppxManifest.get(manifestPath); + // Break out Windows 10-specific functionality because we also need to + // apply UAP versioning to Windows 10 appx-manifests. + var isTargetingWin10 = manifest.prefix === 'uap:'; - var manifest = new et.ElementTree(et.XML(contents)); + applyCoreProperties(config, manifest); + applyStartPage(config, manifest, isTargetingWin10); - applyCoreProperties(config, manifest, manifestPath, namespacePrefix, isTargetingWin10); - // sort Capability elements as per CB-5350 Windows8 build fails due to invalid 'Capabilities' definition - sortCapabilities(manifest); - applyAccessRules(config, manifest, isTargetingWin10); - applyBackgroundColor(config, manifest, namespacePrefix); - applyToastCapability(config, manifest, namespacePrefix); + if (isTargetingWin10) { + applyNavigationWhitelist(config, manifest); + } else { + applyAccessRules(config, manifest); + } + + // Apply background color, splashscreen background color, etc. + manifest.getVisualElements() + .trySetBackgroundColor(config.getPreference('BackgroundColor')) + .setSplashBackgroundColor(config.getPreference('SplashScreenBackgroundColor')) + .setToastCapable(config.getPreference('WindowsToastCapable')) + .setOrientation(config.getPreference('Orientation')); if (isTargetingWin10) { - applyTargetPlatformVersion(config, manifest); - checkForRestrictedCapabilities(config, manifest); - ensureUapPrefixedCapabilities(manifest.find('.//Capabilities')); + manifest.setDependencies(config.getAllMinMaxUAPVersions()); + + var badCaps = manifest.getRestrictedCapabilities(); + if (config.hasRemoteUris() && badCaps) { + events.emit('warn', 'The following Capabilities were declared and are restricted:' + + '\n\t' + badCaps.map(function(a){return a.name;}).join(', ') + + '\nYou will be unable to on-board your app to the public Windows Store with these ' + + 'capabilities and access rules permitting access to remote URIs.'); + } } //Write out manifest - fs.writeFileSync(manifestPath, manifest.write({indent: 4}), 'utf-8'); + manifest.write(); } -function applyCoreProperties(config, manifest, manifestPath, xmlnsPrefix, targetWin10) { - var version = fixConfigVersion(config.windows_packageVersion() || config.version()); - var name = config.name(); - // CB-9450: iOS/Android and Windows Store have an incompatibility here; Windows Store assigns the +function applyCoreProperties(config, manifest) { + // CB-9450: iOS/Android and Windows Store have an incompatibility here; Windows Store assigns the // package name that should be used for upload to the store. However, this can't be set for typical // Cordova apps. So, we have to create a Windows-specific preference here. var pkgName = config.getPreference('WindowsStoreIdentityName') || config.packageName(); - var author = config.author(); - - var identityNode = manifest.find('.//Identity'); - if(!identityNode) { - throw new Error('Invalid manifest file (no <Identity> node): ' + manifestPath); - } - // Update identity name and version if (pkgName) { - (identityNode.attrib.Name = pkgName); + manifest.getIdentity().setName(pkgName); } + + var version = config.windows_packageVersion() || config.version(); if (version) { - (identityNode.attrib.Version = version); + manifest.getIdentity().setVersion(version); } // Update publisher id (identity) - if (config.publisherId && identityNode.attrib.Publisher !== config.publisherId) { - identityNode.attrib.Publisher = config.publisherId; + if (config.publisherId) { + manifest.getIdentity().setPublisher(config.publisherId); } // Update name (windows8 has it in the Application[@Id] and Application.VisualElements[@DisplayName]) - var app = manifest.find('.//Application'); - if(!app) { - throw new Error('Invalid manifest file (no <Application> node): ' + manifestPath); - } - var baselinePackageName = config.packageName(); if (baselinePackageName) { - // 64 symbols restriction goes from manifest schema definition - // http://msdn.microsoft.com/en-us/library/windows/apps/br211415.aspx - var appId = baselinePackageName.length <= 64 ? baselinePackageName : baselinePackageName.substr(0, 64); - app.attrib.Id = appId; + manifest.getApplication().setId(baselinePackageName); } - applyStartPage(app, config, targetWin10); - - var visualElementsName = './/' + xmlnsPrefix + 'VisualElements'; - var visualElems = manifest.find(visualElementsName); - - if(!visualElems) { - throw new Error('Invalid manifest file (no <' + xmlnsPrefix + 'VisualElements> node): ' + manifestPath); - } + var name = config.name(); if (name) { - (visualElems.attrib.DisplayName = name); + manifest.getVisualElements().setDisplayName(name); } // CB-9410: Get a display name and publisher display name. In the Windows Store, certain @@ -209,76 +178,18 @@ function applyCoreProperties(config, manifest, manifestPath, xmlnsPrefix, target // Here, we check for Windows-specific preferences, and if we find it, prefer that over // the Cordova <widget> areas. var displayName = config.getPreference('WindowsStoreDisplayName') || name; - var publisherName = config.getPreference('WindowsStorePublisherName') || author; + var publisherName = config.getPreference('WindowsStorePublisherName') || config.author(); // Update properties - var properties = manifest.find('.//Properties'); - if (properties && properties.find) { - var displayNameElement = properties.find('.//DisplayName'); - if (displayNameElement && displayName) { - displayNameElement.text = displayName; - } - - var publisherNameElement = properties.find('.//PublisherDisplayName'); - if (publisherNameElement && publisherName) { - publisherNameElement.text = publisherName; - } - } - - // Supported orientations - var rotationPreferenceName = xmlnsPrefix + 'Rotation'; - var rotationPreferenceRootName = xmlnsPrefix + 'InitialRotationPreference'; - - var orientation = config.getPreference('Orientation'); - var rotationPreferenceRoot; - if (orientation) { - rotationPreferenceRoot = manifest.find('.//' + rotationPreferenceRootName); - if(rotationPreferenceRoot === null) { - visualElems.append(et.Element(rotationPreferenceRootName)); - rotationPreferenceRoot = manifest.find('.//' + rotationPreferenceRootName); - } - - rotationPreferenceRoot.clear(); - - var applyOrientations = function(orientationsArr) { - orientationsArr.forEach(function(orientationValue) { - var el = et.Element(rotationPreferenceName); - el.attrib.Preference = orientationValue; - rotationPreferenceRoot.append(el); - }); - }; - - // Updates supported orientations - //<InitialRotationPreference> - // <Rotation Preference = "portrait" | "landscape" | "portraitFlipped" | "landscapeFlipped" /> {1,4} - //</InitialRotationPreference> - if(orientation === 'default') { - // This means landscape and portrait - applyOrientations(['portrait', 'landscape', 'landscapeFlipped']); - } else if(orientation === 'portrait') { - applyOrientations(['portrait']); - } else if(orientation === 'landscape') { - applyOrientations(['landscape', 'landscapeFlipped']); - } else { // Platform-specific setting like "portrait,landscape,portraitFlipped" - applyOrientations(orientation.split(',')); - } - } else { - // Remove InitialRotationPreference root element to revert to defaults - rotationPreferenceRoot = visualElems.find('.//' + rotationPreferenceRootName); - if(rotationPreferenceRoot !== null) { - visualElems.remove(null, rotationPreferenceRoot); - } - } + manifest.getProperties() + .setDisplayName(displayName) + .setPublisherDisplayName(publisherName); } -function applyStartPage(appNode, config, targetingWin10) { - var startPage = config.startPage(); - - if (!startPage) { - // If not specified, set default value - // http://cordova.apache.org/docs/en/edge/config_ref_index.md.html#The%20config.xml%20File - startPage = 'index.html'; - } +function applyStartPage(config, manifest, targetingWin10) { + // If not specified, set default value + // http://cordova.apache.org/docs/en/edge/config_ref_index.md.html#The%20config.xml%20File + var startPage = config.startPage() || 'index.html'; var uriPrefix = ''; if (targetingWin10) { @@ -308,41 +219,19 @@ function applyStartPage(appNode, config, targetingWin10) { uriPrefix += '/'; // add a 3rd trailing forward slash for correct area resolution } - appNode.attrib.StartPage = uriPrefix + startPagePrefix + startPage; + manifest.getApplication().setStartPage(uriPrefix + startPagePrefix + startPage); } -// Adjust version number as per CB-5337 Windows8 build fails due to invalid app version -function fixConfigVersion (version) { - if(version && version.match(/\.\d/g)) { - var numVersionComponents = version.match(/\.\d/g).length + 1; - while (numVersionComponents++ < 4) { - version += '.0'; - } - } - return version; -} - -function applyAccessRules (config, manifest, isTargetingWin10) { - // Updates WhiteListing rules - //<ApplicationContentUriRules> - // <Rule Match="https://www.example.com" Type="include"/> - //</ApplicationContentUriRules> - - var AppContentUriRulesElementName = 'ApplicationContentUriRules', - RuleElementName = 'Rule'; - - if (isTargetingWin10) { - return applyNavigationWhitelist(config, manifest); - } - - var accessRules = config.getAccessRules().filter(function(rule) { +function applyAccessRules (config, manifest) { + var accessRules = config.getAccesses() + .filter(function(rule) { // https:// rules are always good, * rules are always good - if (rule.indexOf('https://') === 0 || rule === '*') { - return true; - } else { - console.warn('Access rules must begin with "https://", the following rule will be ignored: ' + rule); - } + if (rule.origin.indexOf('https://') === 0 || rule.origin === '*') return true; + + events.emit('warn', 'Access rules must begin with "https://", the following rule will be ignored: ' + rule.origin); return false; + }).map(function (rule) { + return rule.origin; }); // If * is specified, emit no access rules. @@ -350,7 +239,7 @@ function applyAccessRules (config, manifest, isTargetingWin10) { accessRules = []; } - createApplicationContentUriRules(manifest, AppContentUriRulesElementName, RuleElementName, accessRules, { Type: 'include' }); + manifest.getApplication().setAccessRules(accessRules); } /** @@ -358,17 +247,23 @@ function applyAccessRules (config, manifest, isTargetingWin10) { * Allows WinRT access to origins specified by <allow-navigation href="origin" /> elements. */ function applyNavigationWhitelist(config, manifest) { - var AppContentUriRulesElementName = 'uap:ApplicationContentUriRules'; - var RuleElementName = 'uap:Rule'; + + if (manifest.prefix !== 'uap:') { + // This never should happen, but to be sure let's check + throw new Error('AllowNavigation whitelist rules must be applied to Windows 10 appxmanifest only.'); + } + var UriSchemeTest = /^(?:https?|ms-appx-web):\/\//i; - var whitelistRules = config.getNavigationWhitelistRules().filter(function(rule) { - if (UriSchemeTest.test(rule)) { - return true; - } else { - console.warn('The following navigation rule had an invalid URI scheme and is ignored: "' + rule + '".'); - } + var whitelistRules = config.getAllowNavigations() + .filter(function(rule) { + if (UriSchemeTest.test(rule.href)) return true; + + events.emit('warn', 'The following navigation rule had an invalid URI scheme and is ignored: "' + rule.href + '".'); return false; + }) + .map(function (rule) { + return rule.href; }); var defaultPrefix = config.getPreference('WindowsDefaultUriPrefix'); @@ -381,96 +276,17 @@ function applyNavigationWhitelist(config, manifest) { } } - createApplicationContentUriRules(manifest, AppContentUriRulesElementName, RuleElementName, whitelistRules, { - Type: 'include', - WindowsRuntimeAccess: 'all' - }); -} - -/** - * Private function used by applyNavigationWhitelist and applyAccessRules - * which creates the corresponding section in the app manifest. - * @param manifest {et.ElementTree} The manifest document. - * @param acurElementName {string} The name of the AccessContentUriRules element, including prefix if applicable. - * @param ruleElementName {string} The name of the Rule element, including prefix if applicable. - * @param rulesOrigins {string[]} The origins that will be permitted. - * @param commonAttributes {Object} Property bag of additional attributes that should be applied to every rule. - */ -function createApplicationContentUriRules(manifest, acurElementName, ruleElementName, rulesOrigins, commonAttributes) { - var appUriRulesRoot = manifest.find('.//Application'), - appUriRules = appUriRulesRoot.find(acurElementName); - - if (appUriRules !== null) { - appUriRulesRoot.remove(null, appUriRules); - } - - // No rules defined - if (rulesOrigins.length === 0) { - return; - } - - appUriRules = et.Element(acurElementName); - appUriRulesRoot.append(appUriRules); - - rulesOrigins.forEach(function(rule) { - var el = et.Element(ruleElementName); - el.attrib.Match = rule; - - var attributes = Object.keys(commonAttributes); - attributes.forEach(function(attributeName) { - el.attrib[attributeName] = commonAttributes[attributeName]; - }); - appUriRules.append(el); - }); -} - -function sortCapabilities(manifest) { - - // removes namespace prefix (m3:Capability -> Capability) - // this is required since elementtree returns qualified name with namespace - function extractLocalName(tag) { - return tag.split(':').pop(); // takes last part of string after ':' - } - - var capabilitiesRoot = manifest.find('.//Capabilities'), - capabilities = capabilitiesRoot._children || []; - // to sort elements we remove them and then add again in the appropriate order - capabilities.forEach(function(elem) { // no .clear() method - capabilitiesRoot.remove(0, elem); - // CB-7601 we need local name w/o namespace prefix to sort capabilities correctly - elem.localName = extractLocalName(elem.tag); - }); - capabilities.sort(function(a, b) { - return (a.localName > b.localName) ? 1: -1; - }); - capabilities.forEach(function(elem) { - capabilitiesRoot.append(elem); - }); + manifest.getApplication().setAccessRules(whitelistRules); } -function checkForRestrictedCapabilities(config, manifest) { - var hasRemoteUris = checkForRemoteModeUris(config); - if (hasRemoteUris) { - var capabilitiesRoot = manifest.find('.//Capabilities'); - var badCaps = checkForRestrictedRemoteCapabilityDeclarations(capabilitiesRoot); - if (badCaps) { - console.warn('The following Capabilities were declared and are restricted:'); - console.warn(' ' + badCaps.join(',')); - console.warn('You will be unable to on-board your app to the public Windows Store with'); - console.warn(' these capabilities and access rules permitting access to remote URIs.'); - } - } -} +function copyImages(config, platformRoot) { -function copyImages(config) { - var platformRoot = ROOT; - // TODO find the way to detect whether command was triggered by CLI or not - var appRoot = path.join(platformRoot, '..', '..'); + var appRoot = path.dirname(config.path); function copyImage(src, dest) { src = path.join(appRoot, src); dest = path.join(platformRoot, 'images', dest); - //console.log('Copying image from ' + src + ' to ' + dest); + events.emit('verbose', 'Copying image from ' + src + ' to ' + dest); shell.cp('-f', src, dest); } @@ -488,7 +304,7 @@ function copyImages(config) { }); // warn if no images found if (images.length === 0) { - console.log('No images found for target: ' + destFileName); + events.emit('warn', 'No images found for target: ' + destFileName); return; } // copy images with new name but keeping scale suffix @@ -550,44 +366,12 @@ function copyImages(config) { if (targetImg) { copyImage(img.src, targetImg.dest); } else { - console.log('The following image is skipped due to unsupported size: ' + img.src); + events.emit('warn', 'The following image is skipped due to unsupported size: ' + img.src); } } }); } -function applyBackgroundColor (config, manifest, xmlnsPrefix) { - var visualElems =null; - - function refineColor(color) { - // return three-byte hexadecimal number preceded by "#" (required for Windows) - color = color.replace('0x', '').replace('#', ''); - if (color.length == 3) { - color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2]; - } - // alpha is not supported, so we remove it - if (color.length == 8) { // AArrggbb - color = color.slice(2); - } - return '#' + color; - } - // background color - var bgColor = config.getPreference('BackgroundColor'); - if (bgColor) { - var visualElementsName = './/' + xmlnsPrefix + 'VisualElements'; - visualElems = manifest.find(visualElementsName); - visualElems.attrib.BackgroundColor = refineColor(bgColor); - } - - // Splash Screen background color - bgColor = config.getPreference('SplashScreenBackgroundColor'); - if (bgColor) { - var splashScreenElementsName = './/' + xmlnsPrefix + 'SplashScreen'; - visualElems = manifest.find(splashScreenElementsName); - visualElems.attrib.BackgroundColor = refineColor(bgColor); - } -} - function applyUAPVersionToProject(projectFilePath, uapVersionInfo) { // No uapVersionInfo means that there is no UAP SDKs installed and there is nothing to do for us if (!uapVersionInfo) return; @@ -603,25 +387,6 @@ function applyUAPVersionToProject(projectFilePath, uapVersionInfo) { fs.writeFileSync(projectFilePath, xml.write({ indent: 4 }), {}); } -function applyTargetPlatformVersion(config, manifest) { - var dependencies = manifest.find('./Dependencies'); - while (dependencies.len() > 0) { - dependencies.delItem(0); - } - - var uapVersionSet = getAllMinMaxUAPVersions(config); - var platformNames = Object.keys(uapVersionSet); - for (var i = 0; i < platformNames.length; i++) { - var curTargetPlatformName = platformNames[i]; - var curTargetPlatformInfo = uapVersionSet[curTargetPlatformName]; - - var elem = subElement(dependencies, 'TargetDeviceFamily'); - elem.set('Name', curTargetPlatformName); - elem.set('MinVersion', curTargetPlatformInfo.MinVersion.toString()); - elem.set('MaxVersionTested', curTargetPlatformInfo.MaxVersionTested.toString()); - } -} - // returns {minUAPVersion: Version, targetUAPVersion: Version} | false function getUAPVersions() { var baselineVersions = MSBuildTools.getAvailableUAPVersions(); @@ -637,149 +402,153 @@ function getUAPVersions() { }; } +module.exports.prepare = function (cordovaProject) { + var self = this; + + this._config = updateConfigFilesFrom(cordovaProject.projectConfig, + this._munger, this.locations); + + // Update own www dir with project's www assets and plugins' assets and js-files + return Q.when(updateWwwFrom(cordovaProject, this.locations)) + .then(function () { + // update project according to config.xml changes. + return updateProjectAccordingTo(self._config, self.locations); + }) + .then(function () { + copyImages(cordovaProject.projectConfig, self.root); + // CB-5421 Add BOM to all html, js, css files to ensure app can pass Windows Store Certification + addBOMSignature(self.locations.www); + }) + .then(function () { + self.events.emit('verbose', 'Updated project successfully'); + }); +}; + /** - * Gets min/max UAP versions from the configuration. If no version preferences are - * in the configuration file, this will provide Windows.Universal at BASE_UAP_VERSION for both min and max. - * This will always return a rational object or will fail; for example, if a platform expects - * a higher min-version than max-version, it will raise the max version to the min version. + * Adds BOM signature at the beginning of all js|html|css|json files in + * specified folder and all subfolders. This is required for application to + * pass Windows store certification successfully. * - * @param config {ConfigParser} The configuration parser - * @return An object in the shape of: { 'Windows.Mobile': {'MinVersion': Version, 'MaxVersion': Version } } (where Version is a Version object) - * @exception {RangeError} Thrown if a Version string is badly formed. + * @param {String} directory Directory where we need to update files */ -function getAllMinMaxUAPVersions(config) { - var uapVersionPreferenceTest = /(Microsoft.+?|Windows.+?)\-(MinVersion|MaxVersionTested)/i; - var platformBag = Object.create(null); - var preferenceList = config.getMatchingPreferences(uapVersionPreferenceTest); - preferenceList.forEach(function(verPref) { - var matches = uapVersionPreferenceTest.exec(verPref.name); - // 'matches' should look like: ['Windows.Universal-MinVersion', 'Windows.Universal', 'MinVersion'] - var platformName = matches[1]; - var versionPropertyName = matches[2]; - - var platformVersionSet = platformBag[platformName]; - if (typeof platformVersionSet === 'undefined') { - platformVersionSet = { }; - platformBag[platformName] = platformVersionSet; +function addBOMSignature(directory) { + shell.ls('-R', directory) + .forEach(function (file) { + if (!file.match(/\.(js|html|css|json)$/i)) { + return; } - var versionTest = Version.tryParse(verPref.value); - if (!versionTest) { - throw new RangeError('Could not comprehend a valid version from the string "' + verPref.value + '" of platform-boundary "' + verPref.name + '".'); + var filePath = path.join(directory, file); + // skip if this is a folder + if (!fs.lstatSync(filePath).isFile()) { + return; } - platformVersionSet[versionPropertyName] = versionTest; - }); - - for (var platformName in platformBag) { - // Go through each and make sure there are min/max set - var versionPref = platformBag[platformName]; - if (!versionPref.MaxVersionTested && !!versionPref.MinVersion) { // min is set, but max is not - versionPref.MaxVersionTested = versionPref.MinVersion; + var content = fs.readFileSync(filePath); + if (content[0] !== 0xEF && content[1] !== 0xBE && content[2] !== 0xBB) { + fs.writeFileSync(filePath, '\ufeff' + content); } - else if (!versionPref.MinVersion && !!versionPref.MaxVersionTested) { // max is set, min is not - versionPref.MinVersion = versionPref.MaxVersionTested; - } - else if (!versionPref.MinVersion && !versionPref.MaxVersionTested) { // neither are set - versionPref.MinVersion = BASE_UAP_VERSION; - versionPref.MaxVersionTested = BASE_UAP_VERSION; - } - else { // both are set - if (versionPref.MinVersion.gt(versionPref.MaxVersionTested)) { - versionPref.MaxVersionTested = versionPref.MinVersion; - } - } - } - - if (Object.keys(platformBag).length === 0) { - platformBag['Windows.Universal'] = { MinVersion: BASE_UAP_VERSION, MaxVersionTested: BASE_UAP_VERSION }; - } - - return platformBag; -} - -/** - * Checks to see whether access rules or - * @param config {ConfigParser} The configuration parser - * @return {boolean} True if the config specifies remote URIs for access or start; false otherwise. - */ -function checkForRemoteModeUris(config) { - var accessRules = config.getNavigationWhitelistRules(); - var startPage = config.startPage(); - var test = /(https?|ms-appx-web):\/\//i; - - var hasRemoteUri = test.test(startPage); - hasRemoteUri = hasRemoteUri || accessRules.some(function(rule) { - return test.test(rule); }); - - return hasRemoteUri; } /** - * Checks for capabilities which are Restricted in Windows 10 UAP. - * @param appxManifestCapabilitiesElement {ElementTree.Element} The appx manifest element for <capabilities> - * @return {string[]|false} An array of restricted capability names, or false. + * Updates config files in project based on app's config.xml and config munge, + * generated by plugins. + * + * @param {ConfigParser} sourceConfig A project's configuration that will + * be merged into platform's config.xml + * @param {ConfigChanges} configMunger An initialized ConfigChanges instance + * for this platform. + * @param {Object} locations A map of locations for this platform + * + * @return {ConfigParser} An instance of ConfigParser, that + * represents current project's configuration. When returned, the + * configuration is already dumped to appropriate config.xml file. */ -function checkForRestrictedRemoteCapabilityDeclarations(appxManifestCapabilitiesElement) { - if (!appxManifestCapabilitiesElement) - return false; - - var hasRestrictedCapabilities = false; - var foundRestrictedCapabilities = []; +function updateConfigFilesFrom(sourceConfig, configMunger, locations) { + + events.emit('verbose', 'Generating config.xml from defaults for platform "windows"'); + + // First cleanup current config and merge project's one into own + var defaultConfig = locations.defaultConfigXml; + var ownConfig = locations.configXml; + var sourceCfg = sourceConfig.path; + // If defaults.xml is present, overwrite platform config.xml with it. + // Otherwise save whatever is there as defaults so it can be + // restored or copy project config into platform if none exists. + if (fs.existsSync(defaultConfig)) { + events.emit('verbose', 'Generating config.xml from defaults for platform "' + this.platform + '"'); + shell.cp('-f', defaultConfig, ownConfig); + } else if (fs.existsSync(ownConfig)) { + shell.cp('-f', ownConfig, defaultConfig); + } else { + shell.cp('-f', sourceCfg, ownConfig); + } - var children = appxManifestCapabilitiesElement.getchildren(); - var declaredCapabilities = children.map(function(el) { - return el.attrib.Name; - }); + // Then apply config changes from global munge to all config files + // in project (including project's config) + configMunger.reapply_global_munge().save_all(); - UAP_RESTRICTED_CAPS.forEach(function(cap) { - if (declaredCapabilities.indexOf(cap) > -1) { - hasRestrictedCapabilities = true; - foundRestrictedCapabilities.push(cap); - } - }); + // Merge changes from app's config.xml into platform's one + var config = new ConfigParser(ownConfig); + xmlHelpers.mergeXml(sourceConfig.doc.getroot(), + config.doc.getroot(), 'windows', /*clobber=*/true); + // CB-6976 Windows Universal Apps. For smooth transition and to prevent mass api failures + // we allow using windows8 tag for new windows platform + xmlHelpers.mergeXml(sourceConfig.doc.getroot(), + config.doc.getroot(), 'windows8', /*clobber=*/true); - return hasRestrictedCapabilities ? foundRestrictedCapabilities : hasRestrictedCapabilities; + config.write(); + return config; } /** - * Checks for capabilities which require the uap: prefix in Windows 10. - * @param appxManifestCapabilitiesElement {ElementTree.Element} The appx manifest element for <capabilities> + * Updates platform 'www' directory by replacing it with contents of + * 'platform_www' and app www. Also copies project's overrides' folder into + * the platform 'www' folder + * + * @param {Object} cordovaProject An object which describes cordova project. + * @param {Object} destinations An object that contains destination + * paths for www files. */ -function ensureUapPrefixedCapabilities(appxManifestCapabilitiesElement) { - var children = appxManifestCapabilitiesElement.getchildren(); - var declaredCapabilities = children.map(function(el) { - return { name: el.attrib.Name, element: el, elementName: el.tag }; - }); - - declaredCapabilities.forEach(function(cap) { - if (CAPS_NEEDING_UAPNS.indexOf(cap.name) > -1) { - if (cap.elementName.indexOf('uap:') === -1) { - cap.elementName = 'uap:' + cap.elementName; - cap.element.tag = cap.elementName; - } - } +function updateWwwFrom(cordovaProject, destinations) { + + shell.rm('-rf', destinations.www); + shell.mkdir('-p', destinations.www); + // Copy source files from project's www directory + shell.cp('-rf', path.join(cordovaProject.locations.www, '*'), destinations.www); + // Override www sources by files in 'platform_www' directory + shell.cp('-rf', path.join(destinations.platformWww, '*'), destinations.www); + + // If project contains 'merges' for our platform, use them as another overrides + // CB-6976 Windows Universal Apps. For smooth transition from 'windows8' platform + // we allow using 'windows8' merges for new 'windows' platform + ['windows8', 'windows'].forEach(function (platform) { + var mergesPath = path.join(cordovaProject.root, 'merges', platform); + // if no 'merges' directory found, no further actions needed + if (!fs.existsSync(mergesPath)) return; + + events.emit('verbose', 'Found "merges" for ' + platform + ' platform. Copying over existing "www" files.'); + var overrides = path.join(mergesPath, '*'); + shell.cp('-rf', overrides, destinations.www); }); } /** - * Applies the ToastCapable attribute to the VisualElements tag - * @param config {ConfigParser} The configuration reader - * @param manifest {et.ElementTree} The manifest file - * @namespacePrefix {String} The XML namespace for the VisualElements tag, in the form 'm2:' + * Updates project structure and AppxManifest according to project's configuration. + * + * @param {ConfigParser} projectConfig A project's configuration that will + * be used to update project + * @param {Object} locations A map of locations for this platform */ -function applyToastCapability(config, manifest, namespacePrefix) { - var isToastCapable = config.getPreference('WindowsToastCapable'); - isToastCapable = (isToastCapable && isToastCapable.toString().toLowerCase() === 'true'); - - var visualElementsName = './/' + namespacePrefix + 'VisualElements'; - var visualElems = manifest.find(visualElementsName); +function updateProjectAccordingTo(projectConfig, locations) { + // Apply appxmanifest changes + [MANIFEST_WINDOWS, MANIFEST_WINDOWS8, MANIFEST_WINDOWS10, MANIFEST_PHONE] + .forEach(function(manifestFile) { + updateManifestFile(projectConfig, path.join(locations.root, manifestFile)); + }); - if (isToastCapable) { - visualElems.attrib.ToastCapable = 'true'; - } - else { - delete visualElems.attrib.ToastCapable; + if (process.platform === 'win32') { + applyUAPVersionToProject(path.join(locations.root, PROJECT_WINDOWS10), getUAPVersions()); } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
