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