load single android library cache from local folder

This commit is contained in:
Dave Holoway
2020-06-28 15:34:27 +01:00
parent 71600cf366
commit baabf1e7a9
3 changed files with 48 additions and 157 deletions

View File

@@ -42,8 +42,8 @@ function activateLanguageClient(context) {
// Register the server for plain text documents // Register the server for plain text documents
documentSelector: [{ scheme: 'file', language: 'java' }], documentSelector: [{ scheme: 'file', language: 'java' }],
initializationOptions: { initializationOptions: {
// globalStoragePath is used to cache decoded jar files // extensionPath points to the root of the extension (the folder where this file is)
globalStoragePath: context.globalStoragePath, extensionPath: context.extensionPath,
}, },
synchronize: { synchronize: {
// Notify the server about file changes to '.java files contained in the workspace // Notify the server about file changes to '.java files contained in the workspace

View File

@@ -1,174 +1,61 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { loadAndroidLibrary, CEIType } = require('java-mti'); const { CEIType, loadAndroidLibrary } = require('java-mti');
const android_system_library_cache_filename = {
regex: /^sdk-platforms-android-(.+?)-(\d+)\.json$/,
/**
* @param {string} version
* @param {fs.Stats} stat
*/
build(version, stat) {
return `sdk-platforms-${version}-${Math.trunc(stat.mtime.getTime())}.json`;
}
}
/** /**
* @param {string} cache_dir directory used to store decoded jar libraries * @param {string} extensionPath install path of extension
* @returns {Promise<Map<string,CEIType>>}
*/ */
function ensureFolderExists(cache_dir) { async function loadAndroidSystemLibrary(extensionPath) {
if (!cache_dir) { console.time('android-library-load');
throw new Error('missing cache dir value'); let library;
}
try { try {
const stat = fs.statSync(cache_dir); if (!extensionPath) {
if (!stat.isDirectory()) { throw new Error('Missing extension path')
throw new Error(`cache dir '${cache_dir}' is not a directory`);
} }
} catch (err) { const cache_folder = path.join(extensionPath, 'langserver', '.library-cache');
if (err.code !== 'ENOENT') { library = await loadHighestAPIPlatform(cache_folder);
throw new Error(`cache dir '${cache_dir}' check failed: ${err.message}`); } finally {
} console.timeEnd('android-library-load');
fs.mkdirSync(cache_dir);
} }
return library;
} }
/** /**
* * @param {string} cache_folder
* @param {string} cache_dir
* @param {RegExp} filter
* @returns {fs.Dirent[]}
*/ */
function findLibraryFiles(cache_dir, filter) { async function loadHighestAPIPlatform(cache_folder) {
const files = fs.readdirSync(cache_dir, { withFileTypes: true }); /** @type {fs.Dirent[]} */
const valid_files = files.filter(file => filter.test(file.name) && file.isFile()); const files = await new Promise((res, rej) => {
return valid_files; fs.readdir(cache_folder, {withFileTypes: true}, (err, files) => err ? rej(err) : res(files));
} });
/** // find the file with the highest API level
* let best_match = {
* @param {fs.Dirent[]} files
*/
function chooseAndroidSystemLibrary(files) {
let chosen = {
api: 0, api: 0,
/** @type {fs.Dirent} */ /** @type {fs.Dirent} */
file: null, file: null,
}; };
files.forEach(file => { files.forEach(file => {
const m = file.name.match(android_system_library_cache_filename.regex); const m = file.name.match(/^android-(\d+)\.zip$/);
if (!m) return; if (!m) return;
if (/^\d+$/.test(m[1])) { const api = parseInt(m[1], 10);
const api = parseInt(m[1]); if (api > best_match.api) {
if (api > chosen.api) { best_match = {
chosen.api = api; api,
chosen.file = file; file,
} }
} }
})
return chosen.file;
}
function findHighestAPISystemLibrary(android_sdk_platforms_root) {
let platform_folders = [], android_platform_jars = [];
try {
platform_folders = fs.readdirSync(android_sdk_platforms_root, { withFileTypes: true });
} catch {};
platform_folders.forEach(folder => {
if (!folder.isDirectory()) return;
// we assume stable SDK platform folders are named 'android-<api-level>'
if (!/^android-\d+$/.test(folder.name)) return;
// the platform folder must contain an android.jar file
let stat, filepath = path.join(android_sdk_platforms_root, folder.name, 'android.jar');
try { stat = fs.statSync(filepath) }
catch { return }
if (!stat.isFile()) return;
// add it to the list
android_platform_jars.push({
folder: folder.name,
api: parseInt(folder.name.split('-')[1], 10),
filepath,
stat,
})
}); });
if (android_platform_jars.length === 0) { if (!best_match.file) {
return null; throw new Error(`No valid platform cache files found in ${cache_folder}`)
} }
// choose the folder with the highest API number console.log(`loading android platform cache: ${best_match.file.name}`);
return android_platform_jars.sort((a,b) => b.api - a.api)[0].folder;
}
/** const cache_file = path.join(cache_folder, best_match.file.name);
* @param {string} cache_dir const typemap = loadAndroidLibrary(cache_file);
* @param {string|':latest'} [version]
*/
async function buildAndroidSystemLibrary(cache_dir, version) {
const android_sdk_root = process.env['ANDROID_SDK'] || process.env['ANDROID_HOME'];
if (!android_sdk_root) {
throw new Error('Cannot locate Android SDK folder - ANDROID_SDK env variable is not defined');
}
const android_sdk_platforms_root = path.join(android_sdk_root, 'platforms');
if (!version) { return typemap;
// choose the folder with the highest API number
version = findHighestAPISystemLibrary(android_sdk_platforms_root);
if (!version) {
throw new Error(`Cannot build Android library: No supported system libraries found in ${android_sdk_platforms_root}`);
}
}
let stat, filepath = path.join(android_sdk_platforms_root, version, 'android.jar');
try { stat = fs.statSync(filepath) }
catch { }
if (!stat || !stat.isFile()) {
throw new Error(`Cannot build Android library: '${filepath}' is not a valid android system library file`);
}
console.log(`Building ${version} library cache for code completion support. This might take a few minutes...`);
const cache_filename = path.join(cache_dir, android_system_library_cache_filename.build(version, stat));
try {
const library = await loadAndroidLibrary(cache_filename, { api: version, sdk_root: android_sdk_root });
console.log(`${version} library cache built.`);
return library;
} catch(err) {
throw new Error(`Cannot build Android library: ${err.message}`);
}
}
/**
* @param {string} cache_dir directory used to store decoded jar libraries
* @returns {Promise<Map<string,CEIType>>}
*/
async function loadAndroidSystemLibrary(cache_dir) {
console.time('android-library-load');
// for (let x;;) {
// console.log('waiting');
// if (x) break;
// await new Promise(res => setTimeout(res, 1000));
// }
let library;
try {
ensureFolderExists(cache_dir);
const library_files = findLibraryFiles(cache_dir, android_system_library_cache_filename.regex);
if (!library_files.length) {
return buildAndroidSystemLibrary(cache_dir);
}
// search for the highest android API number in the list of cached files
const library_file = chooseAndroidSystemLibrary(library_files);
if (!library_file) {
return buildAndroidSystemLibrary(cache_dir);
}
// load the library
const library_path_name = path.join(cache_dir, library_file.name);
console.log(`Loading android system library: ${library_path_name}`);
library = await loadAndroidLibrary(library_path_name, null);
} catch (err) {
console.error(`android library load failed: ${err.message}`);
library = new Map();
}
console.timeEnd('android-library-load');
return library;
} }
exports.loadAndroidSystemLibrary = loadAndroidSystemLibrary; exports.loadAndroidSystemLibrary = loadAndroidSystemLibrary;

View File

@@ -173,9 +173,6 @@ let first_parse_waiting = new Set();
*/ */
function reparse(uris, opts) { function reparse(uris, opts) {
trace('reparse'); trace('reparse');
if (androidLibrary instanceof Promise) {
return;
}
if (!uris || !uris.length) { if (!uris || !uris.length) {
return; return;
} }
@@ -186,6 +183,11 @@ function reparse(uris, opts) {
return; return;
} }
} }
if (androidLibrary instanceof Promise) {
// reparse after the library has loaded
androidLibrary.then(() => reparse(uris, opts));
return;
}
const cached_units = [], parsers = []; const cached_units = [], parsers = [];
for (let docinfo of liveParsers.values()) { for (let docinfo of liveParsers.values()) {
if (uris.includes(docinfo.uri)) { if (uris.includes(docinfo.uri)) {
@@ -291,14 +293,16 @@ let hasDiagnosticRelatedInformationCapability = false;
connection.onInitialize((params) => { connection.onInitialize((params) => {
// the storage path is passed to us by the client side of the extension
const library_cache_path = (params.initializationOptions || {}).globalStoragePath;
trace(`library_cache_path: ${library_cache_path}`);
// the android library is loaded asynchronously, with the global `androidLibrary` variable // the android library is loaded asynchronously, with the global `androidLibrary` variable
// set to the promise while it is loading. // set to the promise while it is loading.
androidLibrary = loadAndroidSystemLibrary(library_cache_path) androidLibrary = loadAndroidSystemLibrary((params.initializationOptions || {}).extensionPath)
.then(library => androidLibrary = library); .then(
library => androidLibrary = library,
err => {
console.log(`Android library load failed: ${err.message}\n Code completion may not be available.`);
return new Map();
}
);
let capabilities = params.capabilities; let capabilities = params.capabilities;