Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CB-11117: Use FileUpdater to optimize prepare for android platform #295

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions bin/templates/cordova/Api.js
Expand Up @@ -160,8 +160,8 @@ Api.prototype.getPlatformInfo = function () {
* @return {Promise} Return a promise either fulfilled, or rejected with
* CordovaError instance.
*/
Api.prototype.prepare = function (cordovaProject) {
return require('./lib/prepare').prepare.call(this, cordovaProject);
Api.prototype.prepare = function (cordovaProject, prepareOptions) {
return require('./lib/prepare').prepare.call(this, cordovaProject, prepareOptions);
};

/**
Expand Down Expand Up @@ -328,6 +328,9 @@ Api.prototype.clean = function(cleanOptions) {
return require('./lib/check_reqs').run()
.then(function () {
return require('./lib/build').runClean.call(self, cleanOptions);
})
.then(function () {
return require('./lib/prepare').clean.call(self, cleanOptions);
});
};

Expand Down
3 changes: 3 additions & 0 deletions bin/templates/cordova/clean
Expand Up @@ -39,6 +39,9 @@ var opts = nopt({
// Make buildOptions compatible with PlatformApi clean method spec
opts.argv = opts.argv.original;

// Skip cleaning prepared files when not invoking via cordova CLI.
opts.noPrepare = true;

require('./loggingHelper').adjustLoggerLevel(opts);

new Api().clean(opts)
Expand Down
202 changes: 141 additions & 61 deletions bin/templates/cordova/lib/prepare.js
Expand Up @@ -26,34 +26,58 @@ var AndroidManifest = require('./AndroidManifest');
var xmlHelpers = require('cordova-common').xmlHelpers;
var CordovaError = require('cordova-common').CordovaError;
var ConfigParser = require('cordova-common').ConfigParser;
var FileUpdater = require('cordova-common').FileUpdater;
var PlatformJson = require('cordova-common').PlatformJson;
var PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger;
var PluginInfoProvider = require('cordova-common').PluginInfoProvider;

module.exports.prepare = function (cordovaProject) {

module.exports.prepare = function (cordovaProject, options) {
var self = this;
var platformResourcesDir = path.relative(cordovaProject.root, path.join(this.locations.root, 'res'));

var platformJson = PlatformJson.load(this.locations.root, this.platform);
var munger = new PlatformMunger(this.platform, this.locations.root, platformJson, new PluginInfoProvider());

this._config = updateConfigFilesFrom(cordovaProject.projectConfig, 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))
return Q.when(updateWww(cordovaProject, this.locations))
.then(function () {
// update project according to config.xml changes.
return updateProjectAccordingTo(self._config, self.locations);
})
.then(function () {
handleIcons(cordovaProject.projectConfig, self.root);
handleSplashes(cordovaProject.projectConfig, self.root);
updateIcons(cordovaProject, platformResourcesDir);
updateSplashes(cordovaProject, platformResourcesDir);
})
.then(function () {
events.emit('verbose', 'Prepared android project successfully');
});
};

module.exports.clean = function (options) {
// A cordovaProject isn't passed into the clean() function, because it might have
// been called from the platform shell script rather than the CLI. Check for the
// noPrepare option passed in by the non-CLI clean script. If that's present, or if
// there's no config.xml found at the project root, then don't clean prepared files.
var projectRoot = path.resolve(this.root, '../..');
var projectConfigFile = path.join(projectRoot, 'config.xml');
if ((options && options.noPrepare) || !fs.existsSync(projectConfigFile) ||
!fs.existsSync(this.locations.configXml)) {
return Q();
}

var projectConfig = new ConfigParser(this.locations.configXml);
var platformResourcesDir = path.relative(projectRoot, path.join(this.locations.root, 'res'));

var self = this;
return Q().then(function () {
cleanWww(projectRoot, self.locations);
cleanIcons(projectRoot, projectConfig, platformResourcesDir);
cleanSplashes(projectRoot, projectConfig, platformResourcesDir);
});
};

/**
* Updates config files in project based on app's config.xml and config munge,
* generated by plugins.
Expand Down Expand Up @@ -89,6 +113,13 @@ function updateConfigFilesFrom(sourceConfig, configMunger, locations) {
return config;
}

/**
* Logs all file operations via the verbose event stream, indented.
*/
function logFileOp(message) {
events.emit('verbose', ' ' + message);
}

/**
* Updates platform 'www' directory by replacing it with contents of
* 'platform_www' and app www. Also copies project's overrides' folder into
Expand All @@ -98,21 +129,36 @@ function updateConfigFilesFrom(sourceConfig, configMunger, locations) {
* @param {Object} destinations An object that contains destination
* paths for www files.
*/
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);
function updateWww(cordovaProject, destinations) {
var sourceDirs = [
path.relative(cordovaProject.root, cordovaProject.locations.www),
path.relative(cordovaProject.root, destinations.platformWww)
];

// If project contains 'merges' for our platform, use them as another overrides
var merges_path = path.join(cordovaProject.root, 'merges', 'android');
if (fs.existsSync(merges_path)) {
events.emit('verbose', 'Found "merges/android" folder. Copying its contents into the android project.');
var overrides = path.join(merges_path, '*');
shell.cp('-rf', overrides, destinations.www);
sourceDirs.push(path.join('merges', 'android'));
}

var targetDir = path.relative(cordovaProject.root, destinations.www);
events.emit(
'verbose', 'Merging and updating files from [' + sourceDirs.join(', ') + '] to ' + targetDir);
FileUpdater.mergeAndUpdateDir(
sourceDirs, targetDir, { rootDir: cordovaProject.root }, logFileOp);
}

/**
* Cleans all files from the platform 'www' directory.
*/
function cleanWww(projectRoot, locations) {
var targetDir = path.relative(projectRoot, locations.www);
events.emit('verbose', 'Cleaning ' + targetDir);

// No source paths are specified, so mergeAndUpdateDir() will clear the target directory.
FileUpdater.mergeAndUpdateDir(
[], targetDir, { rootDir: projectRoot, all: true }, logFileOp);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing logger function here would produce a lot of output if www contains a lot of files. IMO everything that end user would need to know is that we're going to wipe all www contents

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also is there any advantage of using FileUpdater rather than just calling shell.rm()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was mostly going for consistent logging: why log some file operations but not others? Our customer feedback indicates they would like more visibility into the cordova build process, and this kind of verbose logging will help with that.

Anyway this is going away now that I'm removing the clean function.

}

/**
Expand Down Expand Up @@ -201,60 +247,72 @@ function default_versionCode(version) {
return versionCode;
}

function copyImage(src, resourcesDir, density, name) {
var destFolder = path.join(resourcesDir, (density ? 'drawable-': 'drawable') + density);
var isNinePatch = !!/\.9\.png$/.exec(src);
var ninePatchName = name.replace(/\.png$/, '.9.png');
function getImageResourcePath(resourcesDir, density, name, sourceName) {
if (/\.9\.png$/.test(sourceName)) {
name = name.replace(/\.png$/, '.9.png');
}
var resourcePath = path.join(resourcesDir, (density ? 'drawable-' + density : 'drawable'), name);
return resourcePath;
}

function updateSplashes(cordovaProject, platformResourcesDir) {
var resources = cordovaProject.projectConfig.getSplashScreens('android');

// default template does not have default asset for this density
if (!fs.existsSync(destFolder)) {
fs.mkdirSync(destFolder);
// if there are "splash" elements in config.xml
if (resources.length === 0) {
events.emit('verbose', 'This app does not have splash screens defined');
return;
}

var destFilePath = path.join(destFolder, isNinePatch ? ninePatchName : name);
events.emit('verbose', 'Copying image from ' + src + ' to ' + destFilePath);
shell.cp('-f', src, destFilePath);
var resourceMap = mapImageResources(cordovaProject.root, platformResourcesDir, 'screen.png');

var hadMdpi = false;
resources.forEach(function (resource) {
if (!resource.density) {
return;
}
if (resource.density == 'mdpi') {
hadMdpi = true;
}
var targetPath = getImageResourcePath(
platformResourcesDir, resource.density, 'screen.png', path.basename(resource.src));
resourceMap[targetPath] = resource.src;
});

// There's no "default" drawable, so assume default == mdpi.
if (!hadMdpi && resources.defaultResource) {
var targetPath = getImageResourcePath(
platformResourcesDir, 'mdpi', 'screen.png', path.basename(resources.defaultResource.src));
resourceMap[targetPath] = resources.defaultResource.src;
}

events.emit('verbose', 'Updating splash screens at ' + platformResourcesDir);
FileUpdater.updatePaths(
resourceMap, { rootDir: cordovaProject.root }, logFileOp);
}

function handleSplashes(projectConfig, platformRoot) {
function cleanSplashes(projectRoot, projectConfig, platformResourcesDir) {
var resources = projectConfig.getSplashScreens('android');
// if there are "splash" elements in config.xml
if (resources.length > 0) {
deleteDefaultResourceAt(platformRoot, 'screen.png');
events.emit('verbose', 'splash screens: ' + JSON.stringify(resources));

// The source paths for icons and splashes are relative to
// project's config.xml location, so we use it as base path.
var projectRoot = path.dirname(projectConfig.path);
var destination = path.join(platformRoot, 'res');

var hadMdpi = false;
resources.forEach(function (resource) {
if (!resource.density) {
return;
}
if (resource.density == 'mdpi') {
hadMdpi = true;
}
copyImage(path.join(projectRoot, resource.src), destination, resource.density, 'screen.png');
});
// There's no "default" drawable, so assume default == mdpi.
if (!hadMdpi && resources.defaultResource) {
copyImage(path.join(projectRoot, resources.defaultResource.src), destination, 'mdpi', 'screen.png');
}
var resourceMap = mapImageResources(projectRoot, platformResourcesDir, 'screen.png');
events.emit('verbose', 'Cleaning splash screens at ' + platformResourcesDir);

// No source paths are specified in the map, so updatePaths() will delete the target files.
FileUpdater.updatePaths(
resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
}
}

function handleIcons(projectConfig, platformRoot) {
var icons = projectConfig.getIcons('android');
function updateIcons(cordovaProject, platformResourcesDir) {
var icons = cordovaProject.projectConfig.getIcons('android');

// if there are icon elements in config.xml
if (icons.length === 0) {
events.emit('verbose', 'This app does not have launcher icons defined');
return;
}

deleteDefaultResourceAt(platformRoot, 'icon.png');
var resourceMap = mapImageResources(cordovaProject.root, platformResourcesDir, 'icon.png');

var android_icons = {};
var default_icon;
Expand Down Expand Up @@ -303,25 +361,47 @@ function handleIcons(projectConfig, platformRoot) {

// The source paths for icons and splashes are relative to
// project's config.xml location, so we use it as base path.
var projectRoot = path.dirname(projectConfig.path);
var destination = path.join(platformRoot, 'res');
for (var density in android_icons) {
copyImage(path.join(projectRoot, android_icons[density].src), destination, density, 'icon.png');
var targetPath = getImageResourcePath(
platformResourcesDir, density, 'icon.png', path.basename(android_icons[density].src));
resourceMap[targetPath] = android_icons[density].src;
}

// There's no "default" drawable, so assume default == mdpi.
if (default_icon && !android_icons.mdpi) {
copyImage(path.join(projectRoot, default_icon.src), destination, 'mdpi', 'icon.png');
var defaultTargetPath = getImageResourcePath(
platformResourcesDir, 'mdpi', 'icon.png', path.basename(default_icon.src));
resourceMap[defaultTargetPath] = default_icon.src;
}

events.emit('verbose', 'Updating icons at ' + platformResourcesDir);
FileUpdater.updatePaths(
resourceMap, { rootDir: cordovaProject.root }, logFileOp);
}

// remove the default resource name from all drawable folders
function deleteDefaultResourceAt(baseDir, resourceName) {
shell.ls(path.join(baseDir, 'res/drawable-*'))
function cleanIcons(projectRoot, projectConfig, platformResourcesDir) {
var icons = projectConfig.getIcons('android');
if (icons.length > 0) {
var resourceMap = mapImageResources(projectRoot, platformResourcesDir, 'icon.png');
events.emit('verbose', 'Cleaning icons at ' + platformResourcesDir);

// No source paths are specified in the map, so updatePaths() will delete the target files.
FileUpdater.updatePaths(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just like in cleanWww - is there any advantage of calling this rather than doing shell.rm on these files? This would eliminate the need to know how mapImageResources and FileUpdater.updatePaths works internally and IMO improve readability a lot.

resourceMap, { rootDir: projectRoot, all: true }, logFileOp);
}
}

/**
* Gets a map containing resources of a specified name from all drawable folders in a directory.
*/
function mapImageResources(rootDir, subDir, resourceName) {
var pathMap = {};
shell.ls(path.join(rootDir, subDir, 'drawable-*'))
.forEach(function (drawableFolder) {
var imagePath = path.join(drawableFolder, resourceName);
shell.rm('-f', [imagePath, imagePath.replace(/\.png$/, '.9.png')]);
events.emit('verbose', 'Deleted ' + imagePath);
var imagePath = path.join(subDir, path.basename(drawableFolder), resourceName);
pathMap[imagePath] = null;
});
return pathMap;
}

/**
Expand Down
15 changes: 0 additions & 15 deletions node_modules/.bin/istanbul

This file was deleted.

7 changes: 0 additions & 7 deletions node_modules/.bin/istanbul.cmd

This file was deleted.

16 changes: 15 additions & 1 deletion node_modules/.bin/nopt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions node_modules/.bin/nopt.cmd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion node_modules/.bin/shjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.