"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GradleFile = void 0; const path_1 = require("path"); const os_1 = __importDefault(require("os")); const tempy_1 = __importDefault(require("tempy")); const utils_fs_1 = require("@ionic/utils-fs"); const subprocess_1 = require("../util/subprocess"); const text_1 = require("../util/text"); const vfs_1 = require("../vfs"); const detect_indent_1 = __importDefault(require("../util/detect-indent")); const logger_1 = require("../logger"); const fs_1 = require("../util/fs"); class GradleFile extends vfs_1.VFSStorable { constructor(filename, vfs) { super(); this.filename = filename; this.vfs = vfs; this.source = null; this.parsed = null; this.tempFile = null; this.gradleCommitFn = async (file) => { await (0, fs_1.assertParentDirs)(file.getFilename()); return (0, utils_fs_1.writeFile)(file.getFilename(), file.getData().getDocument()); }; this.gradleDiffFn = async (file) => { var _a; let old = ''; try { old = await (0, utils_fs_1.readFile)(file.getFilename(), { encoding: 'utf-8' }); } catch (e) { } return { old, new: (_a = this.source) !== null && _a !== void 0 ? _a : '', }; }; } getDocument() { return this.source; } /** * Replace the given properties at the specified point in the Gradle file or insert * if the replacement doesn't exist * * exact specifies whether the pathObject should be exact from the root of the document or * if it can match on a sub-object **/ async replaceProperties(pathObject, toReplace, exact = false) { await this.parse(); if (!this.parsed) { throw new Error('Call parse() first to load Gradle file'); } const found = this.find(pathObject, exact); if (!found.length) { // Create a parent selector object since we're going to insert instead const parent = this._makeReplacePathObject(pathObject, Object.keys(toReplace)[0]); const foundParent = this.find(parent, exact); if (foundParent.length) { this.insertIntoGradleFile([toReplace], foundParent[0], "infer" /* AndroidGradleInjectType.Infer */); return; } else { throw new Error('Unable to find target in Gradle file to replace or insert'); } } const target = found[0]; return this.replaceInGradleFile(toReplace, target); } // Build a new pathObject that is the path to the parent rather than // the path in pathObject _makeReplacePathObject(pathObject, injectKey) { let x = {}; let y = x; let a = pathObject; while (a) { const keys = Object.keys(a); if (keys[0] === injectKey || !keys.length) { return y; } const o = {}; x[keys[0]] = o; x = o; a = a[keys[0]]; } return y; } /** * Replace an entry in the gradle file. */ // This is a beast, sorry. Hey, at least there's tests // In the future, this could be moved to the Java `gradle-parse` package provided in this monorepo // along with modifying the AST to inject our script but this works fine for now async replaceInGradleFile(toInject, targetNode) { var _a; // These values are 1-indexed not 0-indexed //let { line, column, lastLine, lastColumn } = targetNode.node.source; let { line, column, lastLine, lastColumn } = targetNode.node.source; const source = (_a = (await this.getGradleSource())) !== null && _a !== void 0 ? _a : ''; const sourceLines = source.split(/\r?\n/); if (line == -1) { // Set to first line (remember, 1-indexed) line = 1; } if (lastLine === -1) { // Set to last line (remember, 1-indexed) lastLine = sourceLines.length + 1; } const detectedIndent = (0, detect_indent_1.default)(source); let lines = []; this.createGradleSource([toInject], lines /* out */, detectedIndent.indent, undefined, targetNode.node, "infer" /* AndroidGradleInjectType.Infer */); const resolvedLastLine = lastLine < 0 ? sourceLines.length : lastLine; const formatted = lines.join('\n'); const indentAmount = targetNode.depth; const indented = (0, text_1.indent)(formatted, detectedIndent.indent, indentAmount - 1); // Replace the target lines with our new source line const newSource = sourceLines.slice(0, Math.max(0, line - 1)).join('\n') + '\n' + indented + '\n' + sourceLines .slice(Math.max(0, resolvedLastLine), sourceLines.length) .join('\n'); this.source = newSource; } /** * Insert the given properties at the specified point in the Gradle file. * exact specifies whether the pathObject should be exact from the root of the document or * if it can match on a sub-object **/ async insertProperties(pathObject, toInject, type = "method" /* AndroidGradleInjectType.Method */, exact = false) { await this.parse(); if (!this.parsed) { throw new Error('Call parse() first to load Gradle file'); } const found = this.find(pathObject, exact); if (!found.length) { throw new Error('Unable to find method in Gradle file to inject'); } const target = found[0]; return this.insertIntoGradleFile(toInject, target, type); } /** * Inject the given properties at the specified point in the Gradle file. * exact specifies whether the pathObject should be exact from the root of the document or * if it can match on a sub-object **/ async insertFragment(pathObject, toInject, exact = false) { await this.parse(); if (!this.parsed) { throw new Error('Call parse() first to load Gradle file'); } const found = this.find(pathObject, exact); if (!found.length) { throw new Error('Unable to find method in Gradle file to inject'); } const target = found[0]; return this.insertIntoGradleFile(toInject, target, "infer" /* AndroidGradleInjectType.Infer */); } /** * Parse the underlying Gradle file and build the AST. Note: this calls out to * a Java process which incurs some overhead and requires java to be installed * This is because Gradle is actually a DSL for the Groovy language, which is * a JVM language. Additionally, the Groovy parser is based on a modified version * of the Antlr project that is tightly bound to the JVM. Ultimatley, this means * the only safe, accurate way to feasibly build a Gradle AST is to use the Groovy * parser API which this uses under the hood. */ async parse() { var _a, _b; if (!(await (0, utils_fs_1.pathExists)(this.filename))) { throw new Error(`Unable to locate file at ${this.filename}`); } const vfsRef = this.vfs.get(this.filename); // We keep a temp file updated with the latest source so the parser can operate // on the current state of the file so we can handle multiple modifications to it // in sequence if (!this.tempFile) { // If the temp file doesn't exist yet, create it and write the current file source to it const gradleContents = await this.getGradleSource(); this.tempFile = tempy_1.default.file({ extension: 'gradle' }); await (0, utils_fs_1.writeFile)(this.tempFile, gradleContents); } else if (vfsRef) { // Otherwise if it already exists then write the current vfs data to it if ((_a = vfsRef === null || vfsRef === void 0 ? void 0 : vfsRef.getData()) === null || _a === void 0 ? void 0 : _a.getDocument()) { await (0, utils_fs_1.writeFile)(this.tempFile, (_b = vfsRef === null || vfsRef === void 0 ? void 0 : vfsRef.getData()) === null || _b === void 0 ? void 0 : _b.getDocument()); } } const parserRoot = this.getGradleParserPath(); const java = await this.getJava(); if (!java) { throw new Error(this.gradleParseError()); } logger_1.Logger.v('gradle', 'parse', `running Gradle parse with Java path ${java}`); logger_1.Logger.v('gradle', 'parse', `read gradle file at ${this.filename}`); try { let json = null; if (os_1.default.platform() === 'win32') { json = await (0, subprocess_1.spawnCommand)(java, [ '-cp', 'lib/groovy-3.0.9.jar;lib/json-20210307.jar;capacitor-gradle-parse.jar;.', 'com.capacitorjs.gradle.Parse', this.tempFile, ], { cwd: parserRoot, stdio: 'pipe', }); } else { json = await (0, subprocess_1.spawnCommand)(java, [ '-cp', 'lib/*:capacitor-gradle-parse.jar:.', 'com.capacitorjs.gradle.Parse', this.tempFile, ], { cwd: parserRoot, stdio: 'pipe', }); } this.parsed = JSON.parse(json || '{}'); return this.parsed; } catch (e) { throw new Error(`Unable to load or parse gradle file: ${e}`); } } /** * Inject a modification into the gradle file. */ // This is a beast, sorry. Hey, at least there's tests // In the future, this could be moved to the Java `gradle-parse` package provided in this monorepo // along with modifying the AST to inject our script but this works fine forn ow async insertIntoGradleFile(toInject, targetNode, type) { var _a; // These values are 1-indexed not 0-indexed let { line, column, lastLine, lastColumn } = targetNode.node.source; const source = (_a = (await this.getGradleSource())) !== null && _a !== void 0 ? _a : ''; const sourceLines = source.split(/\r?\n/); if (line == -1) { // Set to first line (remember, 1-indexed) line = 1; } if (lastLine === -1) { // Set to last line (remember, 1-indexed) lastLine = sourceLines.length + 1; } const detectedIndent = (0, detect_indent_1.default)(source); let lines = []; if (Array.isArray(toInject)) { this.createGradleSource(toInject, lines /* out */, detectedIndent.indent, undefined, targetNode.node, type); } else { lines = toInject.split(/\r?\n/); } const resolvedLastLine = lastLine < 0 ? sourceLines.length : lastLine; const formatted = lines.join('\n'); const indentAmount = targetNode.depth; let newSource = null; if (line === lastLine) { // Block is empty, like dependencies {} const indented = (0, text_1.indent)(formatted, detectedIndent.indent, indentAmount); const sourceLine = sourceLines[line - 1]; // The new line is the slice from the start of the line to one character before the end (remember, // the lines and columns are 1-indexed so lastColumn - 2 is one character before the end const newLine = sourceLine.slice(0, Math.max(0, lastColumn - 2)) + '\n' + indented + '\n' + (0, text_1.indent)(sourceLine.slice(Math.max(0, lastColumn - 2)).trim(), detectedIndent.indent, Math.max(0, indentAmount - 1)); newSource = sourceLines.slice(0, Math.max(0, resolvedLastLine - 1)).join('\n') + '\n' + newLine + '\n' + sourceLines .slice(Math.max(0, resolvedLastLine), sourceLines.length) .join('\n'); } else { const indented = (0, text_1.indent)(formatted, detectedIndent.indent, indentAmount); newSource = sourceLines.slice(0, Math.max(0, resolvedLastLine - 1)).join('\n') + '\n' + indented + '\n' + sourceLines .slice(Math.max(0, resolvedLastLine - 1), sourceLines.length) .join('\n'); } this.source = newSource; } find(pathObject, exact = false) { var _a; if (!this.parsed) { throw new Error('Call parse() first to load Gradle file'); } // Null or empty object means the root node if (!pathObject || !Object.keys(pathObject).length) { const firstChild = (_a = this.parsed.children) === null || _a === void 0 ? void 0 : _a[0]; if (firstChild) { return [{ node: firstChild, depth: 0 }]; } return []; } const found = []; this._find(pathObject, this.parsed, pathObject, exact, [], found); return found; } _find(pathObject, node, pathNode, exact, pathToNode, found, depth = 0) { var _a, _b; if (!pathNode) { return; } const targetKey = (_a = Object.keys(pathNode)) === null || _a === void 0 ? void 0 : _a[0]; if (!targetKey) { return; } for (const c of ((_b = node.children) !== null && _b !== void 0 ? _b : [])) { if (this.isTargetNode(c) && c.name === targetKey) { pathNode = pathNode[targetKey]; if (!pathNode || Object.keys(pathNode).length == 0) { // We've run out of path nodes to match if (!exact) { found.push({ node: c, depth }); } else if (exact && this.matchesExact(pathObject, c, [...pathToNode, c])) { found.push({ node: c, depth }); } } } const newPathToNode = this.isTargetNode(node) ? [...pathToNode, node] : pathToNode; this._find(pathObject, c, pathNode, exact, newPathToNode, found, c.type === 'block' ? depth + 1 : depth); } } getSource(node) { if (!this.parsed || !this.source) { throw new Error('Call parse() first to load Gradle file'); } const lines = this.source.split(/\r?\n/); const sourceLines = lines.slice(node.source.line - 1, node.source.lastLine); const firstLine = sourceLines[0].slice(Math.max(0, node.source.column - 1)); const lastLine = sourceLines[sourceLines.length - 1].slice(0, node.source.lastColumn); if (sourceLines.length > 2) { return [firstLine, ...sourceLines.slice(1, sourceLines.length - 1), lastLine].join('\n'); } else if (sourceLines.length == 2) { return [firstLine, lastLine].join('\n'); } else { return firstLine; } } getDepth(pathObject) { let depth = 0; let n = pathObject; while (n) { const keys = Object.keys(n); if (keys.length > 0) { depth++; n = n[keys[0]]; } else { break; } } return depth; } // When doing an exact match, need to check the path to the node // and verify the hierarchy matches matchesExact(pathObject, node, pathToNode) { var _a; const targetDepth = this.getDepth(pathObject); const currentDepth = pathToNode.length; if (currentDepth != targetDepth) { return false; } let n = pathObject; let m = pathToNode; while (n && m) { const key = Object.keys(n)[0]; if (key && key !== ((_a = m[0]) === null || _a === void 0 ? void 0 : _a.name)) { return false; } n = n[key]; m = m.slice(1); } return true; } isTargetNode(node) { return node.type === 'method' || node.type === 'variable'; } async getJava() { try { if (process.env.JAVA_HOME) { return (0, path_1.join)(process.env.JAVA_HOME, 'bin', 'java'); } const v = await (0, subprocess_1.spawnCommand)('java', ['-version'], { stdio: 'pipe', combineStreams: true }); if (!v) { throw new Error('Unable to find java on PATH'); } return 'java'; } catch (e) { } return null; } getGradleParserPath() { return (0, path_1.dirname)(require.resolve('@trapezedev/gradle-parse')); } async setApplicationId(applicationId) { const source = await this.getGradleSource(); if (source) { this.source = source.replace(/(applicationId\s+)["'][^"']+["']/, `$1"${applicationId}"`); } } async getApplicationId() { const source = await this.getGradleSource(); if (source) { const applicationId = source.match(/applicationId\s+["']([^"']+)["']/); if (!applicationId) { return null; } return applicationId[1]; } return null; } async setVersionCode(versionCode) { const source = await this.getGradleSource(); if (source) { logger_1.Logger.v('gradle', 'setVersionCode', `to ${versionCode} in ${this.filename}`); return this.replaceProperties({ android: { defaultConfig: { versionCode: {} } } }, { versionCode }); } } async getVersionCode() { const source = await this.getGradleSource(); if (source) { const versionCode = source.match(/versionCode\s+(\w+)/); if (!versionCode) { return null; } return parseInt(versionCode[1]); } return null; } async incrementVersionCode() { const source = await this.getGradleSource(); if (source) { const versionCode = source.match(/versionCode\s+(\w+)/); if (!versionCode) { return; } const num = parseInt(versionCode[1]); if (!isNaN(num)) { logger_1.Logger.v('gradle', 'incrementVersionCode', `to ${num} in ${this.filename}`); return this.setVersionCode(num + 1); } } } async setVersionName(versionName) { const source = await this.getGradleSource(); if (source) { logger_1.Logger.v('gradle', 'setVersionName', `to ${versionName} in ${this.filename}`); return this.replaceProperties({ android: { defaultConfig: { versionName: {} } } }, { versionName: `"${versionName}"` }); } } async getVersionName() { const source = await this.getGradleSource(); if (source) { const versionName = source.match(/versionName\s+["']([^"']+)["']/) || null; if (!versionName) { return null; } return versionName[1]; } return null; } async setVersionNameSuffix(versionNameSuffix) { const source = await this.getGradleSource(); if (source) { logger_1.Logger.v('gradle', 'setVersionNameSuffix', `to ${versionNameSuffix} in ${this.filename}`); return this.replaceProperties({ android: { defaultConfig: { versionNameSuffix: {} } } }, { versionNameSuffix: `"${versionNameSuffix}"` }); } } async getVersionNameSuffix() { const source = await this.getGradleSource(); if (source) { const versionName = source.match(/versionNameSuffix\s+["']([^"']+)["']/) || null; if (!versionName) { return null; } return versionName[1]; } return null; } async getNamespace() { const source = await this.getGradleSource(); if (source) { const namespace = source.match(/namespace\s+["']([^"']+)["']/); if (!namespace) { return null; } return namespace[1]; } return null; } async setNamespace(namespace) { const source = await this.getGradleSource(); if (source) { logger_1.Logger.v('gradle', 'setNamespace', `to ${namespace} in ${this.filename}`); return this.replaceProperties({ android: { namespace: {} } }, { namespace: `"${namespace}"` }); } } /* Generate a fragment of Gradle/Groovy code given the inject object A gradle edit will be of the form: [ { maven: [{ url: 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1', name: 'Duo-SDK-Feed' }] } ] */ createGradleSource(injectObj, lines, indentation, depth = 0, targetNode, type) { for (const entry of injectObj) { const keys = Object.keys(entry); for (const key of keys) { const editEntry = entry[key]; if (Array.isArray(editEntry)) { if (typeof editEntry[0] === 'object') { lines.push(`${key} {`); this.createGradleSource(editEntry, lines, indentation, depth + 1, targetNode, type); lines.push('}'); } else { // Create a variable entry if the target node type is a variable or // the provided type is a variable if (targetNode.type === 'variable' || type === "variable" /* AndroidGradleInjectType.Variable */) { lines.push(`${key} = ${JSON.stringify(editEntry)}`); } else { lines.push(`${key} ${editEntry}`); } } } else if (typeof editEntry === 'string' || typeof editEntry === 'number' || typeof editEntry === 'boolean') { if (targetNode.type === 'variable' || type === "variable" /* AndroidGradleInjectType.Variable */) { lines.push((0, text_1.indent)(`${key} = ${editEntry}`, indentation, depth)); } else { lines.push((0, text_1.indent)(`${key} ${editEntry}`, indentation, depth)); } } else { const fields = Object.keys(editEntry); for (const fieldKey of fields) { const fieldEntry = editEntry[fieldKey]; if (typeof fieldEntry === 'string') { lines.push((0, text_1.indent)(`${fieldKey} ${fieldEntry}`, indentation, depth)); } else if (Array.isArray(fieldEntry)) { lines.push('{'); this.createGradleSource(fieldEntry, lines, indentation, depth + 1, targetNode, type); lines.push('}'); } } } } } } async getGradleSource() { var _a, _b; const ref = this.vfs.get(this.filename); if (ref) { return (_b = (_a = ref.getData()) === null || _a === void 0 ? void 0 : _a.getDocument()) !== null && _b !== void 0 ? _b : ''; } const contents = await (0, utils_fs_1.readFile)(this.filename, { encoding: 'utf-8' }); this.source = contents; this.vfs.open(this.filename, this, this.gradleCommitFn, this.gradleDiffFn); return contents; } gradleParseError() { return `java not found on path and JAVA_HOME not set. Please set JAVA_HOME to the root of your Java installation.\n\nGradle parse functionality depends on a local Java install for accurate Gradle file modification.`; } } exports.GradleFile = GradleFile; //# sourceMappingURL=gradle-file.js.map