nativescript
Version:
Command-line interface for building NativeScript projects
528 lines • 26.4 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AndroidPluginBuildService = void 0;
const path = require("path");
const constants_1 = require("../constants");
const helpers_1 = require("../common/helpers");
const xml2js_1 = require("xml2js");
const yok_1 = require("../common/yok");
const _ = require("lodash");
const resolve_package_path_1 = require("@rigor789/resolve-package-path");
const process_1 = require("process");
class AndroidPluginBuildService {
get $platformsDataService() {
return this.$injector.resolve("platformsDataService");
}
constructor($fs, $childProcess, $hostInfo, $options, $logger, $packageManager, $projectData, $projectDataService, $devicePlatformsConstants, $errors, $filesHashService, $hooksService, $injector, $watchIgnoreListService) {
this.$fs = $fs;
this.$childProcess = $childProcess;
this.$hostInfo = $hostInfo;
this.$options = $options;
this.$logger = $logger;
this.$packageManager = $packageManager;
this.$projectData = $projectData;
this.$projectDataService = $projectDataService;
this.$devicePlatformsConstants = $devicePlatformsConstants;
this.$errors = $errors;
this.$filesHashService = $filesHashService;
this.$hooksService = $hooksService;
this.$injector = $injector;
this.$watchIgnoreListService = $watchIgnoreListService;
}
getAndroidSourceDirectories(source) {
const directories = [constants_1.RESOURCES_DIR, "java", constants_1.ASSETS_DIR, "jniLibs", "cpp"];
const resultArr = [];
this.$fs.enumerateFilesInDirectorySync(source, (file, stat) => {
if (stat.isDirectory() &&
_.some(directories, (element) => file.endsWith(element))) {
resultArr.push(file);
return true;
}
});
return resultArr;
}
getManifest(platformsDir) {
const manifest = path.join(platformsDir, constants_1.MANIFEST_FILE_NAME);
return this.$fs.exists(manifest) ? manifest : null;
}
async updateManifestContent(oldManifestContent, defaultPackageName) {
let xml = await this.getXml(oldManifestContent);
let packageName = defaultPackageName;
// if the manifest file is full-featured and declares settings inside the manifest scope
if (xml["manifest"]) {
if (xml["manifest"]["$"]["package"]) {
packageName = xml["manifest"]["$"]["package"];
}
// set the xml as the value to iterate over its properties
xml = xml["manifest"];
}
// if the manifest file doesn't have a <manifest> scope, only the first setting will be picked up
const newManifest = { manifest: {} };
for (const prop in xml) {
newManifest.manifest[prop] = xml[prop];
}
newManifest.manifest["$"]["package"] = packageName;
const xmlBuilder = new xml2js_1.Builder();
const newManifestContent = xmlBuilder.buildObject(newManifest);
return newManifestContent;
}
createManifestContent(packageName) {
const newManifest = {
manifest: AndroidPluginBuildService.MANIFEST_ROOT,
};
newManifest.manifest["$"]["package"] = packageName;
const xmlBuilder = new xml2js_1.Builder();
const newManifestContent = xmlBuilder.buildObject(newManifest);
return newManifestContent;
}
async getXml(stringContent) {
const promise = new Promise((resolve, reject) => (0, xml2js_1.parseString)(stringContent, (err, result) => {
if (err) {
reject(err);
}
else {
resolve(result);
}
}));
return promise;
}
getIncludeGradleCompileDependenciesScope(includeGradleFileContent) {
const indexOfDependenciesScope = includeGradleFileContent.indexOf("dependencies");
const result = [];
if (indexOfDependenciesScope === -1) {
return result;
}
const indexOfRepositoriesScope = includeGradleFileContent.indexOf("repositories");
let repositoriesScope = "";
if (indexOfRepositoriesScope >= 0) {
repositoriesScope = this.getScope("repositories", includeGradleFileContent);
result.push(repositoriesScope);
}
const dependenciesScope = this.getScope("dependencies", includeGradleFileContent);
result.push(dependenciesScope);
return result;
}
getScope(scopeName, content) {
const indexOfScopeName = content.indexOf(scopeName);
const openingBracket = "{";
const closingBracket = "}";
let foundFirstBracket = false;
let openBrackets = 0;
let result = "";
let i = indexOfScopeName;
while (i !== -1 && i < content.length) {
const currCharacter = content[i];
if (currCharacter === openingBracket) {
if (openBrackets === 0) {
foundFirstBracket = true;
}
openBrackets++;
}
if (currCharacter === closingBracket) {
openBrackets--;
}
result += currCharacter;
if (openBrackets === 0 && foundFirstBracket) {
break;
}
i++;
}
return result;
}
/**
* Returns whether the build has completed or not
* @param {Object} options
* @param {string} options.pluginName - The name of the plugin. E.g. 'nativescript-barcodescanner'
* @param {string} options.platformsAndroidDirPath - The path to the 'plugin/src/platforms/android' directory.
* @param {string} options.aarOutputDir - The path where the aar should be copied after a successful build.
* @param {string} options.tempPluginDirPath - The path where the android plugin will be built.
*/
async buildAar(options) {
this.validateOptions(options);
const manifestFilePath = this.getManifest(options.platformsAndroidDirPath);
const androidSourceDirectories = this.getAndroidSourceDirectories(options.platformsAndroidDirPath);
const shortPluginName = (0, helpers_1.getShortPluginName)(options.pluginName);
const pluginTempDir = path.join(options.tempPluginDirPath, shortPluginName);
const pluginSourceFileHashesInfo = await this.getSourceFilesHashes(options.platformsAndroidDirPath, shortPluginName);
const shouldBuildAar = await this.shouldBuildAar({
manifestFilePath,
androidSourceDirectories,
pluginTempDir,
pluginSourceDir: options.platformsAndroidDirPath,
shortPluginName,
fileHashesInfo: pluginSourceFileHashesInfo,
});
if (shouldBuildAar) {
this.cleanPluginDir(pluginTempDir);
const pluginTempMainSrcDir = path.join(pluginTempDir, "src", "main");
await this.updateManifest(manifestFilePath, pluginTempMainSrcDir, shortPluginName);
this.copySourceSetDirectories(androidSourceDirectories, pluginTempMainSrcDir);
await this.setupGradle(pluginTempDir, options.platformsAndroidDirPath, options.projectDir, options.pluginName);
await this.buildPlugin({
gradlePath: options.gradlePath,
gradleArgs: options.gradleArgs,
pluginDir: pluginTempDir,
pluginName: options.pluginName,
projectDir: options.projectDir,
});
this.$watchIgnoreListService.addFileToIgnoreList(path.join(options.aarOutputDir, `${shortPluginName}.aar`));
this.copyAar(shortPluginName, pluginTempDir, options.aarOutputDir);
this.writePluginHashInfo(pluginSourceFileHashesInfo, pluginTempDir);
}
return shouldBuildAar;
}
cleanPluginDir(pluginTempDir) {
// In case plugin was already built in the current process, we need to clean the old sources as they may break the new build.
this.$fs.deleteDirectory(pluginTempDir);
this.$fs.ensureDirectoryExists(pluginTempDir);
}
getSourceFilesHashes(pluginTempPlatformsAndroidDir, shortPluginName) {
const pathToAar = path.join(pluginTempPlatformsAndroidDir, `${shortPluginName}.aar`);
const pluginNativeDataFiles = this.$fs.enumerateFilesInDirectorySync(pluginTempPlatformsAndroidDir, (file, stat) => file !== pathToAar);
return this.$filesHashService.generateHashes(pluginNativeDataFiles);
}
writePluginHashInfo(fileHashesInfo, pluginTempDir) {
const buildDataFile = this.getPathToPluginBuildDataFile(pluginTempDir);
this.$fs.writeJson(buildDataFile, fileHashesInfo);
}
async shouldBuildAar(opts) {
let shouldBuildAar = !!opts.manifestFilePath || !!opts.androidSourceDirectories.length;
if (shouldBuildAar &&
this.$fs.exists(opts.pluginTempDir) &&
this.$fs.exists(path.join(opts.pluginSourceDir, `${opts.shortPluginName}.aar`))) {
const buildDataFile = this.getPathToPluginBuildDataFile(opts.pluginTempDir);
if (this.$fs.exists(buildDataFile)) {
const oldHashes = this.$fs.readJson(buildDataFile);
shouldBuildAar = this.$filesHashService.hasChangesInShasums(oldHashes, opts.fileHashesInfo);
}
}
return shouldBuildAar;
}
getPathToPluginBuildDataFile(pluginDir) {
return path.join(pluginDir, constants_1.PLUGIN_BUILD_DATA_FILENAME);
}
async updateManifest(manifestFilePath, pluginTempMainSrcDir, shortPluginName) {
let updatedManifestContent;
this.$fs.ensureDirectoryExists(pluginTempMainSrcDir);
const defaultPackageName = "org.nativescript." + shortPluginName;
if (manifestFilePath) {
let androidManifestContent;
try {
androidManifestContent = this.$fs.readText(manifestFilePath);
}
catch (err) {
this.$errors.fail(`Failed to fs.readFileSync the manifest file located at ${manifestFilePath}. Error is: ${err.toString()}`);
}
updatedManifestContent = await this.updateManifestContent(androidManifestContent, defaultPackageName);
}
else {
updatedManifestContent = this.createManifestContent(defaultPackageName);
}
const pathToTempAndroidManifest = path.join(pluginTempMainSrcDir, constants_1.MANIFEST_FILE_NAME);
try {
this.$fs.writeFile(pathToTempAndroidManifest, updatedManifestContent);
}
catch (e) {
this.$errors.fail(`Failed to write the updated AndroidManifest in the new location - ${pathToTempAndroidManifest}. Error is: ${e.toString()}`);
}
}
copySourceSetDirectories(androidSourceSetDirectories, pluginTempMainSrcDir) {
for (const dir of androidSourceSetDirectories) {
const dirName = path.basename(dir);
const destination = path.join(pluginTempMainSrcDir, dirName);
this.$fs.ensureDirectoryExists(destination);
this.$fs.copyFile(path.join(dir, "*"), destination);
}
}
async setupGradle(pluginTempDir, platformsAndroidDirPath, projectDir, pluginName) {
const gradleTemplatePath = path.resolve(path.join(__dirname, "../../vendor/gradle-plugin"));
const allGradleTemplateFiles = path.join(gradleTemplatePath, "*");
const buildGradlePath = path.join(pluginTempDir, "build.gradle");
const settingsGradlePath = path.join(pluginTempDir, "settings.gradle");
this.$fs.copyFile(allGradleTemplateFiles, pluginTempDir);
this.addCompileDependencies(platformsAndroidDirPath, buildGradlePath);
const runtimeGradleVersions = await this.getRuntimeGradleVersions(projectDir);
this.replaceGradleVersion(pluginTempDir, runtimeGradleVersions.gradleVersion);
this.replaceGradleAndroidPluginVersion(buildGradlePath, runtimeGradleVersions.gradleAndroidPluginVersion);
this.replaceFileContent(buildGradlePath, "{{pluginName}}", pluginName);
this.replaceFileContent(settingsGradlePath, "{{pluginName}}", pluginName);
// gets the package from the AndroidManifest to use as the namespace or fallback to the `org.nativescript.${shortPluginName}`
const shortPluginName = (0, helpers_1.getShortPluginName)(pluginName);
const manifestPath = path.join(pluginTempDir, "src", "main", "AndroidManifest.xml");
const manifestContent = this.$fs.readText(manifestPath);
let packageName = `org.nativescript.${shortPluginName}`;
const xml = await this.getXml(manifestContent);
if (xml["manifest"]) {
if (xml["manifest"]["$"]["package"]) {
packageName = xml["manifest"]["$"]["package"];
}
}
this.replaceFileContent(buildGradlePath, "{{pluginNamespace}}", packageName);
}
async getRuntimeGradleVersions(projectDir) {
let runtimeGradleVersions = null;
if (projectDir) {
const projectData = this.$projectDataService.getProjectData(projectDir);
const platformData = this.$platformsDataService.getPlatformData(this.$devicePlatformsConstants.Android, projectData);
const projectRuntimeVersion = platformData.platformProjectService.getFrameworkVersion(projectData);
runtimeGradleVersions = await this.getGradleVersions(projectRuntimeVersion);
this.$logger.trace(`Got gradle versions ${JSON.stringify(runtimeGradleVersions)} from runtime v${projectRuntimeVersion}`);
}
if (!runtimeGradleVersions) {
const latestRuntimeVersion = await this.getLatestRuntimeVersion();
runtimeGradleVersions = await this.getGradleVersions(latestRuntimeVersion);
this.$logger.trace(`Got gradle versions ${JSON.stringify(runtimeGradleVersions)} from the latest runtime v${latestRuntimeVersion}`);
}
return runtimeGradleVersions || {};
}
async getLatestRuntimeVersion() {
var _a;
let runtimeVersion = null;
try {
let result = await this.$packageManager.view(constants_1.SCOPED_ANDROID_RUNTIME_NAME, {
"dist-tags": true,
});
result = (_a = result === null || result === void 0 ? void 0 : result["dist-tags"]) !== null && _a !== void 0 ? _a : result;
runtimeVersion = result.latest;
}
catch (err) {
this.$logger.trace(`Error while getting latest android runtime version from view command: ${err}`);
const registryData = await this.$packageManager.getRegistryPackageData(constants_1.SCOPED_ANDROID_RUNTIME_NAME);
runtimeVersion = registryData["dist-tags"].latest;
}
return runtimeVersion;
}
getLocalGradleVersions() {
// try reading from installed runtime first before reading from the npm registry...
const installedRuntimePackageJSONPath = (0, resolve_package_path_1.resolvePackageJSONPath)(constants_1.SCOPED_ANDROID_RUNTIME_NAME, {
paths: [this.$projectData.projectDir],
});
if (!installedRuntimePackageJSONPath) {
return null;
}
const installedRuntimePackageJSON = this.$fs.readJson(installedRuntimePackageJSONPath);
if (!installedRuntimePackageJSON) {
return null;
}
if (installedRuntimePackageJSON.version_info) {
const { gradle, gradleAndroid } = installedRuntimePackageJSON.version_info;
return {
gradleVersion: gradle,
gradleAndroidPluginVersion: gradleAndroid,
};
}
if (installedRuntimePackageJSON.gradle) {
const { version, android } = installedRuntimePackageJSON.gradle;
return {
gradleVersion: version,
gradleAndroidPluginVersion: android,
};
}
return null;
}
async getGradleVersions(runtimeVersion) {
var _a, _b;
let runtimeGradleVersions = null;
const localVersionInfo = this.getLocalGradleVersions();
if (localVersionInfo) {
return localVersionInfo;
}
// fallback to reading from npm...
try {
let output = await this.$packageManager.view(`${constants_1.SCOPED_ANDROID_RUNTIME_NAME}@${runtimeVersion}`, { version_info: true });
output = (_a = output === null || output === void 0 ? void 0 : output["version_info"]) !== null && _a !== void 0 ? _a : output;
if (!output) {
/**
* fallback to the old 'gradle' key in package.json
*
* format:
*
* gradle: { version: '6.4', android: '3.6.4' }
*
*/
output = await this.$packageManager.view(`${constants_1.SCOPED_ANDROID_RUNTIME_NAME}@${runtimeVersion}`, { gradle: true });
output = (_b = output === null || output === void 0 ? void 0 : output["gradle"]) !== null && _b !== void 0 ? _b : output;
const { version, android } = output;
// covert output to the new format...
output = {
gradle: version,
gradleAndroid: android,
};
}
runtimeGradleVersions = { versions: output };
}
catch (err) {
this.$logger.trace(`Error while getting gradle data for android runtime from view command: ${err}`);
const registryData = await this.$packageManager.getRegistryPackageData(constants_1.SCOPED_ANDROID_RUNTIME_NAME);
runtimeGradleVersions = registryData.versions[runtimeVersion];
}
const result = this.getGradleVersionsCore(runtimeGradleVersions);
return result;
}
getGradleVersionsCore(packageData) {
const packageJsonGradle = packageData && packageData.versions;
let runtimeVersions = null;
if (packageJsonGradle &&
(packageJsonGradle.gradle || packageJsonGradle.gradleAndroid)) {
runtimeVersions = {};
runtimeVersions.gradleVersion = packageJsonGradle.gradle;
runtimeVersions.gradleAndroidPluginVersion =
packageJsonGradle.gradleAndroid;
}
return runtimeVersions;
}
replaceGradleVersion(pluginTempDir, version) {
const gradleVersion = version || constants_1.AndroidBuildDefaults.GradleVersion;
const gradleVersionPlaceholder = "{{runtimeGradleVersion}}";
const gradleWrapperPropertiesPath = path.join(pluginTempDir, "gradle", "wrapper", "gradle-wrapper.properties");
this.replaceFileContent(gradleWrapperPropertiesPath, gradleVersionPlaceholder, gradleVersion);
}
replaceGradleAndroidPluginVersion(buildGradlePath, version) {
const gradleAndroidPluginVersionPlaceholder = "{{runtimeAndroidPluginVersion}}";
const gradleAndroidPluginVersion = version || constants_1.AndroidBuildDefaults.GradleAndroidPluginVersion;
this.replaceFileContent(buildGradlePath, gradleAndroidPluginVersionPlaceholder, gradleAndroidPluginVersion);
}
replaceFileContent(filePath, content, replacement) {
const fileContent = this.$fs.readText(filePath);
const contentRegex = new RegExp(content, "g");
const replacedFileContent = fileContent.replace(contentRegex, replacement);
this.$fs.writeFile(filePath, replacedFileContent);
}
addCompileDependencies(platformsAndroidDirPath, buildGradlePath) {
const includeGradlePath = path.join(platformsAndroidDirPath, constants_1.INCLUDE_GRADLE_NAME);
if (this.$fs.exists(includeGradlePath)) {
const includeGradleContent = this.$fs.readText(includeGradlePath);
const compileDependencies = this.getIncludeGradleCompileDependenciesScope(includeGradleContent);
if (compileDependencies.length) {
this.$fs.appendFile(buildGradlePath, "\n" + compileDependencies.join("\n"));
}
}
}
copyAar(shortPluginName, pluginTempDir, aarOutputDir) {
const finalAarName = `${shortPluginName}-release.aar`;
const pathToBuiltAar = path.join(pluginTempDir, "build", "outputs", "aar", finalAarName);
if (this.$fs.exists(pathToBuiltAar)) {
try {
if (aarOutputDir) {
this.$fs.copyFile(pathToBuiltAar, path.join(aarOutputDir, `${shortPluginName}.aar`));
}
}
catch (e) {
this.$errors.fail(`Failed to copy built aar to destination. ${e.message}`);
}
}
else {
this.$errors.fail(`No built aar found at ${pathToBuiltAar}`);
}
}
/**
* @param {Object} options
* @param {string} options.platformsAndroidDirPath - The path to the 'plugin/src/platforms/android' directory.
*/
migrateIncludeGradle(options) {
this.validatePlatformsAndroidDirPathOption(options);
const includeGradleFilePath = path.join(options.platformsAndroidDirPath, constants_1.INCLUDE_GRADLE_NAME);
if (this.$fs.exists(includeGradleFilePath)) {
let includeGradleFileContent;
try {
includeGradleFileContent = this.$fs
.readFile(includeGradleFilePath)
.toString();
}
catch (err) {
this.$errors.fail(`Failed to fs.readFileSync the include.gradle file located at ${includeGradleFilePath}. Error is: ${err.toString()}`);
}
const productFlavorsScope = this.getScope("productFlavors", includeGradleFileContent);
if (productFlavorsScope) {
try {
const newIncludeGradleFileContent = includeGradleFileContent.replace(productFlavorsScope, "");
this.$fs.writeFile(includeGradleFilePath, newIncludeGradleFileContent);
return true;
}
catch (e) {
this.$errors.fail(`Failed to write the updated include.gradle ` +
`in - ${includeGradleFilePath}. Error is: ${e.toString()}`);
}
}
}
return false;
}
async buildPlugin(pluginBuildSettings) {
var _a;
const gradlew = (_a = pluginBuildSettings.gradlePath) !== null && _a !== void 0 ? _a : (this.$hostInfo.isWindows ? "gradlew.bat" : "./gradlew");
const localArgs = [
"-p",
pluginBuildSettings.pluginDir,
"assembleRelease",
`-PtempBuild=true`,
`-PappPath=${this.$projectData.getAppDirectoryPath()}`,
`-PappResourcesPath=${this.$projectData.getAppResourcesDirectoryPath()}`,
];
if (pluginBuildSettings.gradleArgs) {
localArgs.push(pluginBuildSettings.gradleArgs);
}
if (this.$logger.getLevel() === "INFO") {
localArgs.push("--quiet");
}
const opts = {
cwd: pluginBuildSettings.pluginDir,
stdio: "inherit",
shell: this.$hostInfo.isWindows,
};
if (this.$options.hostProjectPath) {
opts.env = {
USER_PROJECT_PLATFORMS_ANDROID: path.resolve((0, process_1.cwd)(), this.$options.hostProjectPath), // TODO: couldn't `hostProjectPath` have an absolute path already?
...process.env, // TODO: any other way to pass automatically the current process.env?
};
}
try {
const sanitizedArgs = this.$hostInfo.isWindows
? localArgs.map((arg) => (0, helpers_1.quoteString)(arg))
: localArgs;
await this.$childProcess.spawnFromEvent(gradlew, sanitizedArgs, "close", opts);
}
catch (err) {
this.$errors.fail(`Failed to build plugin ${pluginBuildSettings.pluginName} : \n${err}`);
}
}
validateOptions(options) {
if (!options) {
this.$errors.fail("Android plugin cannot be built without passing an 'options' object.");
}
if (!options.pluginName) {
this.$logger.info("No plugin name provided, defaulting to 'myPlugin'.");
}
if (!options.aarOutputDir) {
this.$logger.info("No aarOutputDir provided, defaulting to the build outputs directory of the plugin");
}
if (!options.tempPluginDirPath) {
this.$errors.fail("Android plugin cannot be built without passing the path to a directory where the temporary project should be built.");
}
this.validatePlatformsAndroidDirPathOption(options);
}
validatePlatformsAndroidDirPathOption(options) {
if (!options) {
this.$errors.fail("Android plugin cannot be built without passing an 'options' object.");
}
if (!options.platformsAndroidDirPath) {
this.$errors.fail("Android plugin cannot be built without passing the path to the platforms/android dir.");
}
}
}
exports.AndroidPluginBuildService = AndroidPluginBuildService;
AndroidPluginBuildService.MANIFEST_ROOT = {
$: {
"xmlns:android": "http://schemas.android.com/apk/res/android",
},
};
__decorate([
(0, helpers_1.hook)("buildAndroidPlugin")
], AndroidPluginBuildService.prototype, "buildPlugin", null);
yok_1.injector.register("androidPluginBuildService", AndroidPluginBuildService);
//# sourceMappingURL=android-plugin-build-service.js.map