UNPKG

nativescript

Version:

Command-line interface for building NativeScript projects

528 lines • 26.4 kB
"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