1*53c12298Saleqsioimport chalk from 'chalk'; 2*53c12298Saleqsioimport { pathExists, readJSON, unlink, writeJSON } from 'fs-extra'; 3*53c12298Saleqsioimport glob from 'glob-promise'; 4*53c12298Saleqsioimport ora from 'ora'; 5*53c12298Saleqsioimport * as path from 'path'; 6*53c12298Saleqsio 7*53c12298Saleqsioimport * as Directories from '../Directories'; 8*53c12298Saleqsioimport logger from '../Logger'; 9*53c12298Saleqsioimport { spawnAsync, SpawnResult } from '../Utils'; 10*53c12298Saleqsioimport { 11*53c12298Saleqsio AndroidProjectReport, 12*53c12298Saleqsio GradleDependency, 13*53c12298Saleqsio RawGradleDependency, 14*53c12298Saleqsio RawGradleReport, 15*53c12298Saleqsio} from './types'; 16*53c12298Saleqsio 17*53c12298Saleqsioexport const REVISIONS = ['release', 'milestone', 'integration'] as const; 18*53c12298Saleqsio 19*53c12298Saleqsioexport type Revision = typeof REVISIONS[number]; 20*53c12298Saleqsio 21*53c12298Saleqsioexport interface GradleTaskOptions { 22*53c12298Saleqsio revision: Revision; 23*53c12298Saleqsio clearCache: boolean; 24*53c12298Saleqsio} 25*53c12298Saleqsio 26*53c12298Saleqsiofunction flatSingle<T>(arr: T[][]) { 27*53c12298Saleqsio return arr.flatMap((it) => it); 28*53c12298Saleqsio} 29*53c12298Saleqsio 30*53c12298Saleqsiofunction generateReportCacheFilePath(dateTimestamp: Date, gradleTaskOptions: GradleTaskOptions) { 31*53c12298Saleqsio const date = `${dateTimestamp.getUTCFullYear()}.${dateTimestamp.getUTCMonth()}.${dateTimestamp.getUTCDate()}`; 32*53c12298Saleqsio return `${Directories.getExpotoolsDir()}/cache/android-gradle-updatesReport.${ 33*53c12298Saleqsio gradleTaskOptions.revision 34*53c12298Saleqsio }.${date}.cache.json`; 35*53c12298Saleqsio} 36*53c12298Saleqsio 37*53c12298Saleqsioasync function readCachedReports(reportFilename: string): Promise<AndroidProjectReport[] | null> { 38*53c12298Saleqsio if (!(await pathExists(reportFilename))) { 39*53c12298Saleqsio return null; 40*53c12298Saleqsio } 41*53c12298Saleqsio return readJSON(reportFilename); 42*53c12298Saleqsio} 43*53c12298Saleqsio 44*53c12298Saleqsioasync function cacheReports(reportFilename: string, reports: AndroidProjectReport[]) { 45*53c12298Saleqsio await writeJSON(reportFilename, reports); 46*53c12298Saleqsio} 47*53c12298Saleqsio 48*53c12298Saleqsioasync function clearCachedReports(reportFilename: string) { 49*53c12298Saleqsio if (await pathExists(reportFilename)) { 50*53c12298Saleqsio await unlink(reportFilename); 51*53c12298Saleqsio } 52*53c12298Saleqsio} 53*53c12298Saleqsio 54*53c12298Saleqsio/** 55*53c12298Saleqsio * Checks for gradle executable in provided android project directory. 56*53c12298Saleqsio */ 57*53c12298Saleqsioasync function determineGradleWrapperCommand(androidProjectDir: string): Promise<string> { 58*53c12298Saleqsio const gradleWrapperFilename = process.platform === 'win32' ? 'gradlew.bat' : 'gradlew'; 59*53c12298Saleqsio const gradleWrapperCommand = path.join(androidProjectDir, gradleWrapperFilename); 60*53c12298Saleqsio if (!(await pathExists(gradleWrapperCommand))) { 61*53c12298Saleqsio throw new Error(`Gradle ${gradleWrapperCommand} does not exist.`); 62*53c12298Saleqsio } 63*53c12298Saleqsio return gradleWrapperCommand; 64*53c12298Saleqsio} 65*53c12298Saleqsio 66*53c12298Saleqsio/** 67*53c12298Saleqsio * Executes `gradle dependencyUpdates` task that generates gradle dependencies updates report in 68*53c12298Saleqsio * `build/dependencyUpdates.json` files. 69*53c12298Saleqsio */ 70*53c12298Saleqsioasync function executeGradleTask( 71*53c12298Saleqsio androidProjectDir: string, 72*53c12298Saleqsio gradleTaskOptions: GradleTaskOptions 73*53c12298Saleqsio): Promise<SpawnResult | undefined> { 74*53c12298Saleqsio const gradleWrapperCommand = await determineGradleWrapperCommand(androidProjectDir); 75*53c12298Saleqsio 76*53c12298Saleqsio const gradleInitScriptCommand = `--init-script=${path.join( 77*53c12298Saleqsio __dirname, 78*53c12298Saleqsio '../../src/android-update-native-dependencies', 79*53c12298Saleqsio 'initScript.gradle' 80*53c12298Saleqsio )}`; 81*53c12298Saleqsio const gradleCommandArguments = [ 82*53c12298Saleqsio 'dependencyUpdates', 83*53c12298Saleqsio gradleInitScriptCommand, 84*53c12298Saleqsio '-DoutputFormatter=json', 85*53c12298Saleqsio `-DoutputDir=build/dependencyUpdates`, 86*53c12298Saleqsio `-Drevision=${gradleTaskOptions.revision}`, 87*53c12298Saleqsio ]; 88*53c12298Saleqsio const spinner = ora({ 89*53c12298Saleqsio spinner: 'dots', 90*53c12298Saleqsio text: `Executing gradle command ${chalk.yellow( 91*53c12298Saleqsio `${gradleWrapperCommand} ${gradleCommandArguments.join(' ')}` 92*53c12298Saleqsio )}. This might take a while.`, 93*53c12298Saleqsio }); 94*53c12298Saleqsio 95*53c12298Saleqsio spinner.start(); 96*53c12298Saleqsio try { 97*53c12298Saleqsio const result = await spawnAsync(gradleWrapperCommand, gradleCommandArguments, { 98*53c12298Saleqsio cwd: androidProjectDir, 99*53c12298Saleqsio }); 100*53c12298Saleqsio if (result.status !== 0) { 101*53c12298Saleqsio throw result.stderr; 102*53c12298Saleqsio } 103*53c12298Saleqsio spinner.succeed(); 104*53c12298Saleqsio return result; 105*53c12298Saleqsio } catch (error) { 106*53c12298Saleqsio logger.error('Gradle process failed with an error.', error); 107*53c12298Saleqsio spinner.fail(); 108*53c12298Saleqsio } 109*53c12298Saleqsio return undefined; 110*53c12298Saleqsio} 111*53c12298Saleqsio 112*53c12298Saleqsio/** 113*53c12298Saleqsio * Reads gradle reports and converts it into Android report 114*53c12298Saleqsio */ 115*53c12298Saleqsioasync function readGradleReportAndConvertIntoAndroidReport( 116*53c12298Saleqsio reportPath: string 117*53c12298Saleqsio): Promise<AndroidProjectReport> { 118*53c12298Saleqsio const mapRawGradleDependency = ({ 119*53c12298Saleqsio group, 120*53c12298Saleqsio name, 121*53c12298Saleqsio available, 122*53c12298Saleqsio version, 123*53c12298Saleqsio projectUrl, 124*53c12298Saleqsio }: RawGradleDependency): GradleDependency => ({ 125*53c12298Saleqsio group, 126*53c12298Saleqsio name, 127*53c12298Saleqsio fullName: `${group}:${name}`, 128*53c12298Saleqsio availableVersion: available?.release ?? available?.milestone ?? available?.integration ?? null, 129*53c12298Saleqsio currentVersion: version, 130*53c12298Saleqsio projectUrl, 131*53c12298Saleqsio }); 132*53c12298Saleqsio 133*53c12298Saleqsio const findChangelogFilePath = async (reportPath: string): Promise<string | null> => { 134*53c12298Saleqsio const changelogPath = path.resolve(reportPath, '../../../../CHANGELOG.md'); 135*53c12298Saleqsio if (!reportPath.includes('/packages/')) { 136*53c12298Saleqsio return null; 137*53c12298Saleqsio } 138*53c12298Saleqsio if (!(await pathExists(changelogPath))) { 139*53c12298Saleqsio return null; 140*53c12298Saleqsio } 141*53c12298Saleqsio return changelogPath; 142*53c12298Saleqsio }; 143*53c12298Saleqsio 144*53c12298Saleqsio const findGradleFilePath = async (reportPath: string): Promise<string> => { 145*53c12298Saleqsio const gradleBuildGroovy = path.resolve(reportPath, '../../../build.gradle'); 146*53c12298Saleqsio const gradleBuildKotlin = path.resolve(reportPath, '../../../build.gradle.kts'); 147*53c12298Saleqsio if (await pathExists(gradleBuildGroovy)) { 148*53c12298Saleqsio return gradleBuildGroovy; 149*53c12298Saleqsio } 150*53c12298Saleqsio if (await pathExists(gradleBuildKotlin)) { 151*53c12298Saleqsio return gradleBuildKotlin; 152*53c12298Saleqsio } 153*53c12298Saleqsio throw new Error(`Failed to locate gradle.build(.kts)? for report: ${reportPath}`); 154*53c12298Saleqsio }; 155*53c12298Saleqsio 156*53c12298Saleqsio const rawGradleUpdatesReport = (await readJSON(reportPath)) as RawGradleReport; 157*53c12298Saleqsio 158*53c12298Saleqsio const gradleFilePath = await findGradleFilePath(reportPath); 159*53c12298Saleqsio const projectPath = path.dirname(gradleFilePath).endsWith('android') 160*53c12298Saleqsio ? path.resolve(path.dirname(gradleFilePath), '..') 161*53c12298Saleqsio : path.dirname(gradleFilePath); 162*53c12298Saleqsio const projectName = projectPath.includes('/packages/') 163*53c12298Saleqsio ? path.relative(Directories.getPackagesDir(), projectPath) 164*53c12298Saleqsio : path.relative(Directories.getExpoRepositoryRootDir(), projectPath); 165*53c12298Saleqsio 166*53c12298Saleqsio return { 167*53c12298Saleqsio gradleReport: { 168*53c12298Saleqsio current: rawGradleUpdatesReport.current.dependencies.map(mapRawGradleDependency), 169*53c12298Saleqsio exceeded: rawGradleUpdatesReport.exceeded.dependencies.map(mapRawGradleDependency), 170*53c12298Saleqsio unresolved: rawGradleUpdatesReport.unresolved.dependencies.map(mapRawGradleDependency), 171*53c12298Saleqsio outdated: rawGradleUpdatesReport.outdated.dependencies.map(mapRawGradleDependency), 172*53c12298Saleqsio }, 173*53c12298Saleqsio gradleFilePath, 174*53c12298Saleqsio rawGradleReport: rawGradleUpdatesReport, 175*53c12298Saleqsio projectPath, 176*53c12298Saleqsio projectName, 177*53c12298Saleqsio changelogPath: await findChangelogFilePath(reportPath), 178*53c12298Saleqsio }; 179*53c12298Saleqsio} 180*53c12298Saleqsio 181*53c12298Saleqsioasync function readAndConvertReports(): Promise<AndroidProjectReport[]> { 182*53c12298Saleqsio const findGradleReportsFiles = async (cwd: string): Promise<string[]> => { 183*53c12298Saleqsio const result = await glob('**/build/dependencyUpdates/report.json', { 184*53c12298Saleqsio cwd, 185*53c12298Saleqsio ignore: ['**/node_modules, **/ios'], 186*53c12298Saleqsio }); 187*53c12298Saleqsio return Promise.all(result.map(async (el) => path.resolve(cwd, el))); 188*53c12298Saleqsio }; 189*53c12298Saleqsio 190*53c12298Saleqsio const gradleReportsPaths: string[] = flatSingle( 191*53c12298Saleqsio await Promise.all( 192*53c12298Saleqsio [ 193*53c12298Saleqsio Directories.getPackagesDir(), 194*53c12298Saleqsio Directories.getAndroidDir(), 195*53c12298Saleqsio path.join(Directories.getAppsDir(), 'bare-expo/android'), 196*53c12298Saleqsio ].map(findGradleReportsFiles) 197*53c12298Saleqsio ) 198*53c12298Saleqsio ); 199*53c12298Saleqsio 200*53c12298Saleqsio const androidProjectUpdatesReport: AndroidProjectReport[] = await Promise.all( 201*53c12298Saleqsio gradleReportsPaths.map(readGradleReportAndConvertIntoAndroidReport) 202*53c12298Saleqsio ); 203*53c12298Saleqsio 204*53c12298Saleqsio return androidProjectUpdatesReport.sort((a, b) => a.projectName.localeCompare(b.projectName)); 205*53c12298Saleqsio} 206*53c12298Saleqsio 207*53c12298Saleqsio/** 208*53c12298Saleqsio * Gets Android project report by: 209*53c12298Saleqsio * - running gradle task that generates json files describing gradle status unless 210*53c12298Saleqsio * there's report available for a current day 211*53c12298Saleqsio * - reading these json reports files and converting them into Android project report 212*53c12298Saleqsio * 213*53c12298Saleqsio * Date timestamping is used to prevent time-consuming gradle task reruns 214*53c12298Saleqsio * and caching accumulated gradle reports in json file. 215*53c12298Saleqsio */ 216*53c12298Saleqsioexport async function getAndroidProjectReports( 217*53c12298Saleqsio options: GradleTaskOptions 218*53c12298Saleqsio): Promise<AndroidProjectReport[]> { 219*53c12298Saleqsio const timestamp = new Date(); 220*53c12298Saleqsio const reportCacheFilePath = generateReportCacheFilePath(timestamp, options); 221*53c12298Saleqsio 222*53c12298Saleqsio if (options.clearCache) { 223*53c12298Saleqsio await clearCachedReports(reportCacheFilePath); 224*53c12298Saleqsio logger.log('\n Cleared cached gradle task reports.'); 225*53c12298Saleqsio } 226*53c12298Saleqsio 227*53c12298Saleqsio const cachedReports = await readCachedReports(reportCacheFilePath); 228*53c12298Saleqsio if (cachedReports) { 229*53c12298Saleqsio logger.info(' Using cached gradle updates reports.'); 230*53c12298Saleqsio return cachedReports; 231*53c12298Saleqsio } 232*53c12298Saleqsio 233*53c12298Saleqsio for (const androidProjectPath of [ 234*53c12298Saleqsio Directories.getAndroidDir(), 235*53c12298Saleqsio path.join(Directories.getAppsDir(), 'bare-expo/android'), 236*53c12298Saleqsio ]) { 237*53c12298Saleqsio await executeGradleTask(androidProjectPath, options); 238*53c12298Saleqsio } 239*53c12298Saleqsio 240*53c12298Saleqsio const reports = await readAndConvertReports(); 241*53c12298Saleqsio 242*53c12298Saleqsio await cacheReports(reportCacheFilePath, reports); 243*53c12298Saleqsio return reports; 244*53c12298Saleqsio} 245