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

[TIMOB-24446] Allow replacing bundled Android Support Libraries #9008

Merged
merged 15 commits into from
Nov 10, 2017
Merged
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
42 changes: 28 additions & 14 deletions android/cli/commands/_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -1658,16 +1658,16 @@ process.exit(1);
if (unresolvedDependencies.length) {
/*
let msg = 'could not find required module dependencies:';
for (let dependency of unresolvedDependencies) {
msg += __('\n id: %s version: %s platform: %s required by %s',
dependency.id,
dependency.version ? dependency.version : 'latest',
dependency.platform ? dependency.platform : 'all',
dependency.depended.id);
}
logger.error(msg);
process.exit(1);
*/
for (let dependency of unresolvedDependencies) {
msg += __('\n id: %s version: %s platform: %s required by %s',
dependency.id,
dependency.version ? dependency.version : 'latest',
dependency.platform ? dependency.platform : 'all',
dependency.depended.id);
}
logger.error(msg);
process.exit(1);
*/

// re-validate modules
return this.validateTiModules('android', this.deployType, validateTiModulesCallback.bind(this));
Expand Down Expand Up @@ -2887,7 +2887,7 @@ AndroidBuilder.prototype.getNativeModuleBindings = function getNativeModuleBindi
};

AndroidBuilder.prototype.processTiSymbols = function processTiSymbols(next) {
const depMap = JSON.parse(fs.readFileSync(path.join(this.platformPath, 'dependency.json'))),
var depMap = this.dependencyMap,
modulesMap = JSON.parse(fs.readFileSync(path.join(this.platformPath, 'modules.json'))),
modulesPath = path.join(this.platformPath, 'modules'),
moduleBindings = {},
Expand Down Expand Up @@ -2949,7 +2949,9 @@ AndroidBuilder.prototype.processTiSymbols = function processTiSymbols(next) {
let jar = moduleJarMap[namespace];
if (jar) {
jar = jar === 'titanium.jar' ? path.join(this.platformPath, jar) : path.join(this.platformPath, 'modules', jar);
if (fs.existsSync(jar) && !jarLibraries[jar]) {
if (this.isExternalAndroidLibraryAvailable(jar)) {
this.logger.debug('Excluding library ' + jar.cyan);
} else if (fs.existsSync(jar) && !jarLibraries[jar]) {
this.logger.debug(__('Adding library %s', jar.cyan));
jarLibraries[jar] = 1;
}
Expand All @@ -2958,7 +2960,13 @@ AndroidBuilder.prototype.processTiSymbols = function processTiSymbols(next) {
}

depMap.libraries[namespace] && depMap.libraries[namespace].forEach(function (jar) {
if (fs.existsSync(jar = path.join(this.platformPath, jar)) && !jarLibraries[jar]) {
jar = path.join(this.platformPath, jar);
if (this.isExternalAndroidLibraryAvailable(jar)) {
this.logger.debug('Excluding dependency library ' + jar.cyan);
return;
}

if (fs.existsSync(jar) && !jarLibraries[jar]) {
this.logger.debug(__('Adding dependency library %s', jar.cyan));
jarLibraries[jar] = 1;
}
Expand Down Expand Up @@ -3144,7 +3152,13 @@ AndroidBuilder.prototype.copyModuleResources = function copyModuleResources(next
resPkgFile = jarFile.replace(/\.jar$/, '.respackage');

if (fs.existsSync(resPkgFile) && fs.existsSync(resFile)) {
this.resPackages[resFile] = fs.readFileSync(resPkgFile).toString().split('\n').shift().trim();
const packageName = fs.readFileSync(resPkgFile).toString().split(/\r?\n/).shift().trim();
if (!this.hasAndroidLibrary(packageName)) {
this.resPackages[resFile] = packageName;
} else {
this.logger.info(__('Excluding core module resources of %s (%s) because Android Library with same package name is available.', jarFile, packageName));
return done();
}
}

if (!fs.existsSync(jarFile) || !fs.existsSync(resFile)) {
Expand Down
33 changes: 30 additions & 3 deletions android/cli/commands/_buildModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ AndroidModuleBuilder.prototype.run = function run(logger, config, cli, finished)
cli.emit('build.module.pre.compile', this, next);
},

'replaceBundledSupportLibraries',
'processResources',
'compileAidlFiles',
'compileModuleJavaSrc',
Expand Down Expand Up @@ -426,6 +427,27 @@ AndroidModuleBuilder.prototype.loginfo = function loginfo() {
this.logger.info(__('Resources Dir: %s', this.resourcesDir.cyan));
};

/**
* Replaces any .jar file in the Class Path that comes bundled with our SDK
* with a user provided one if available.
*
* We need to do this in this in an extra step because by the time our bundled
* Support Libraries will be added, we haven't parsed any other Android
* Libraries yet.
*
* @param {Function} next Callback function
*/
AndroidModuleBuilder.prototype.replaceBundledSupportLibraries = function replaceBundledSupportLibraries(next) {
Object.keys(this.classPaths).forEach(function (libraryPathAndFilename) {
if (this.isExternalAndroidLibraryAvailable(libraryPathAndFilename)) {
this.logger.debug('Excluding library ' + libraryPathAndFilename.cyan);
delete this.classPaths[libraryPathAndFilename];
}
}, this);

next();
};

/**
* Processes resources for this module.
*
Expand Down Expand Up @@ -497,8 +519,13 @@ AndroidModuleBuilder.prototype.processResources = function processResources(next
const resArchivePathAndFilename = path.join(modulesPath, file.replace(/\.jar$/, '.res.zip'));
const respackagePathAndFilename = path.join(modulesPath, file.replace(/\.jar$/, '.respackage'));
if (fs.existsSync(resArchivePathAndFilename) && fs.existsSync(respackagePathAndFilename)) {
extraPackages.push(fs.readFileSync(respackagePathAndFilename).toString().split('\n').shift().trim());
resArchives.push(resArchivePathAndFilename);
const packageName = fs.readFileSync(respackagePathAndFilename).toString().split(/\r?\n/).shift().trim();
if (!this.hasAndroidLibrary(packageName)) {
extraPackages.push(packageName);
resArchives.push(resArchivePathAndFilename);
} else {
this.logger.info(__('Excluding core module resources of %s (%s) because Android Library with same package name is available.', file, packageName));
}
}
}, this);

Expand Down Expand Up @@ -1200,7 +1227,7 @@ AndroidModuleBuilder.prototype.compileJsClosure = function (next) {

this.logger.info(__('Generating v8 bindings'));

const dependsMap = JSON.parse(fs.readFileSync(this.dependencyJsonFile));
const dependsMap = this.dependencyMap;
Array.prototype.push.apply(this.metaData, dependsMap.required);

Object.keys(dependsMap.dependencies).forEach(function (key) {
Expand Down
37 changes: 37 additions & 0 deletions android/cli/hooks/aar-transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,43 @@ exports.init = function (logger, config, cli, appc) {
scanModuleAndStartTransform(builder, logger, callback);
}
});

cli.on('build.android.dexer', {
priority: 1100,
/**
* Fixes an issue with Hyperloop 2.1.0 which causes a crash when trying to
* override the Android Support Libraries with local .aar files. Hyperloop
* 2.1.0 will always manually add our bundled Android Support Libraries
* to the dexer paths even if they were replaced by the builder. To fix this
* we check the altered dexer paths again and remove any replaced libraries.
*
* @param {Object} data Hook data
* @param {Function} callback Callback function
*/
pre: function (data, callback) {
const builder = data.ctx;
const dexerOptions = data.args[1].slice(0, 6);
const dexerPaths = data.args[1].slice(6);
let hyperloopModule = null;
builder.nativeLibModules.forEach(function (module) {
if (module.id === 'hyperloop' && module.version === '2.1.0') {
hyperloopModule = module;
}
});
if (hyperloopModule && builder.androidLibraries.length > 0) {
let fixedDexerPaths = [];
dexerPaths.forEach(function (entryPathAndFilename) {
if (!this.isExternalAndroidLibraryAvailable(entryPathAndFilename)) {
fixedDexerPaths.push(entryPathAndFilename);
} else {
logger.trace('Removed duplicate library ' + entryPathAndFilename + ' from dexer paths.');
}
}, builder);
data.args[1] = dexerOptions.concat(fixedDexerPaths);
}
callback();
}
});
};

/**
Expand Down
76 changes: 76 additions & 0 deletions android/cli/lib/base-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,30 @@ const appc = require('node-appc'),
__ = i18nLib.__,
xml = appc.xml;

/**
* The base Android builder that includes common functionality that is used
* in both app and module builds.
*/
function AndroidBaseBuilder() {
Builder.apply(this, arguments);

this.androidLibraries = [];
this.dependencyMap = JSON.parse(fs.readFileSync(path.join(this.platformPath, 'dependency.json')));
}

util.inherits(AndroidBaseBuilder, Builder);

/**
* Utility function to merge the contents of XML files.
*
* If the destination file does not exists, the source file will simply
* be copied over. If both source and destination already exists AND the source
* document contains Android resources, the files will be merged. Otherwise the
* destination file will be overwritten.
*
* @param {string|Document} srcOrDoc Path to source XML file or already parsed Document object
* @param {string} dest Path of the destination XML file
*/
AndroidBaseBuilder.prototype.writeXmlFile = function writeXmlFile(srcOrDoc, dest) {
let destExists = fs.existsSync(dest),
destDoc;
Expand Down Expand Up @@ -99,4 +115,64 @@ AndroidBaseBuilder.prototype.writeXmlFile = function writeXmlFile(srcOrDoc, dest
fs.writeFileSync(dest, '<?xml version="1.0" encoding="UTF-8"?>\n' + dom.documentElement.toString());
};

/**
* Checks wether an Android Library with the given package name is available.
*
* @param {string} packageName Package name of the Android Library to search for
* @return {Boolean} True if the Android Library is available, false if not
*/
AndroidBaseBuilder.prototype.hasAndroidLibrary = function hasAndroidLibrary(packageName) {
return this.androidLibraries.some(function (libraryInfo) {
return libraryInfo.packageName === packageName;
});
};

/**
* Checks if one of our bundled Android Support Libraries (.jar) is also available
* as an Android Library (.aar) provided by the user.
*
* This is used during the build process to allow users to replace any of our
* bundled Android Support Libraries with one of their own choosing. Currently
* supported Android Support Library versions are 24.2.0 - 25.x.
*
* To find out which .jar library can be replaces by which Android Library,
* we depend on a hardcoded list of Android Library package names and the bundled
* library filenames they can replace. This list is manually taken from
* android/dependency.json and needs to be maintained if anything changes there.
*
* @param {string} libraryPathAndFilename Path and filename to the .jar file to check
* @return {Boolean} True if the given library is available as an Android Library, false if not
*/
AndroidBaseBuilder.prototype.isExternalAndroidLibraryAvailable = function isExternalAndroidLibraryAvailable(libraryPathAndFilename) {
const replaceableAndroidLibraries = {
'android.support.graphics.drawable': [ 'android-support-vector-drawable.jar' ],
'android.support.graphics.drawable.animated': [ 'android-support-animated-vector-drawable.jar' ],
'android.support.v4': [ 'android-support-v4.jar' ],
'android.support.compat': [ 'android-support-compat.jar' ],
'android.support.coreui': [ 'android-support-core-ui.jar' ],
'android.support.coreutils': [ 'android-support-core-utils.jar' ],
'android.support.design': [ 'android-support-design.jar' ],
'android.support.fragment': [ 'android-support-fragment.jar' ],
'android.support.mediacompat': [ 'android-support-media-compat.jar' ],
'android.support.transition': [ 'android-support-transition.jar' ],
'android.support.v7.appcompat': [ 'android-support-v7-appcompat.jar' ],
'android.support.v7.cardview': [ 'android-support-v7-cardview.jar' ],
'android.support.v7.recyclerview': [ 'android-support-v7-recyclerview.jar' ]
};
return this.androidLibraries.some(function (libraryInfo) {
if (!replaceableAndroidLibraries[libraryInfo.packageName]) {
return false;
}

const libraryFilename = path.basename(libraryPathAndFilename);
const shouldExcludeLibrary = replaceableAndroidLibraries[libraryInfo.packageName].indexOf(libraryFilename) !== -1;
if (shouldExcludeLibrary) {
this.logger.trace(__('Android library %s (%s) available, marking %s to be excluded.', libraryInfo.task.aarPathAndFilename, libraryInfo.packageName.cyan, libraryPathAndFilename.cyan));
return true;
}

return false;
}, this);
};

module.exports = AndroidBaseBuilder;