xref: /expo/tools/src/versioning/android/index.ts (revision 2f85d796)
1import chalk from 'chalk';
2import fs from 'fs-extra';
3import glob from 'glob-promise';
4import inquirer from 'inquirer';
5import path from 'path';
6import semver from 'semver';
7import spawnAsync from '@expo/spawn-async';
8
9import { JniLibNames, getJavaPackagesToRename } from './libraries';
10import * as Directories from '../../Directories';
11import { getListOfPackagesAsync } from '../../Packages';
12
13const EXPO_DIR = Directories.getExpoRepositoryRootDir();
14const ANDROID_DIR = Directories.getAndroidDir();
15const EXPOTOOLS_DIR = Directories.getExpotoolsDir();
16const SCRIPT_DIR = path.join(EXPOTOOLS_DIR, 'src/versioning/android');
17
18const appPath = path.join(ANDROID_DIR, 'app');
19const expoviewPath = path.join(ANDROID_DIR, 'expoview');
20const versionedAbisPath = path.join(ANDROID_DIR, 'versioned-abis');
21const versionedExpoviewAbiPath = (abiName) => path.join(versionedAbisPath, `expoview-${abiName}`);
22const expoviewBuildGradlePath = path.join(expoviewPath, 'build.gradle');
23const appManifestPath = path.join(appPath, 'src', 'main', 'AndroidManifest.xml');
24const templateManifestPath = path.join(
25  EXPO_DIR,
26  'template-files',
27  'android',
28  'AndroidManifest.xml'
29);
30const settingsGradlePath = path.join(ANDROID_DIR, 'settings.gradle');
31const appBuildGradlePath = path.join(appPath, 'build.gradle');
32const buildGradlePath = path.join(ANDROID_DIR, 'build.gradle');
33const sdkVersionsPath = path.join(ANDROID_DIR, 'sdkVersions.json');
34const rnActivityPath = path.join(
35  expoviewPath,
36  'src/main/java/host/exp/exponent/experience/MultipleVersionReactNativeActivity.java'
37);
38const expoviewConstantsPath = path.join(
39  expoviewPath,
40  'src/main/java/host/exp/exponent/Constants.java'
41);
42const testSuiteTestsPath = path.join(
43  appPath,
44  'src/androidTest/java/host/exp/exponent/TestSuiteTests.java'
45);
46const reactAndroidPath = path.join(ANDROID_DIR, 'ReactAndroid');
47const reactCommonPath = path.join(ANDROID_DIR, 'ReactCommon');
48const versionedReactAndroidPath = path.join(ANDROID_DIR, 'versioned-react-native/ReactAndroid');
49const versionedReactAndroidJniPath = path.join(versionedReactAndroidPath, 'src/main');
50const versionedReactAndroidJavaPath = path.join(versionedReactAndroidJniPath, 'java');
51const versionedReactCommonPath = path.join(ANDROID_DIR, 'versioned-react-native/ReactCommon');
52
53async function transformFileAsync(filePath: string, regexp: RegExp, replacement: string = '') {
54  const fileContent = await fs.readFile(filePath, 'utf8');
55  await fs.writeFile(filePath, fileContent.replace(regexp, replacement));
56}
57
58async function removeVersionReferencesFromFileAsync(sdkMajorVersion: string, filePath: string) {
59  console.log(
60    `Removing code surrounded by ${chalk.gray(`// BEGIN_SDK_${sdkMajorVersion}`)} and ${chalk.gray(
61      `// END_SDK_${sdkMajorVersion}`
62    )} from ${chalk.magenta(path.relative(EXPO_DIR, filePath))}...`
63  );
64  await transformFileAsync(
65    filePath,
66    new RegExp(
67      `\\s*//\\s*BEGIN_SDK_${sdkMajorVersion}(_\d+)*\\n.*?//\\s*END_SDK_${sdkMajorVersion}(_\d+)*`,
68      'gs'
69    ),
70    ''
71  );
72}
73
74async function removeVersionedExpoviewAsync(versionedExpoviewAbiPath: string) {
75  console.log(
76    `Removing versioned expoview at ${chalk.magenta(
77      path.relative(EXPO_DIR, versionedExpoviewAbiPath)
78    )}...`
79  );
80  await fs.remove(versionedExpoviewAbiPath);
81}
82
83async function removeFromManifestAsync(sdkMajorVersion: string, manifestPath: string) {
84  console.log(
85    `Removing code surrounded by ${chalk.gray(
86      `<!-- BEGIN_SDK_${sdkMajorVersion} -->`
87    )} and ${chalk.gray(`<!-- END_SDK_${sdkMajorVersion} -->`)} from ${chalk.magenta(
88      path.relative(EXPO_DIR, manifestPath)
89    )}...`
90  );
91  await transformFileAsync(
92    manifestPath,
93    new RegExp(
94      `\\s*<!--\\s*BEGIN_SDK_${sdkMajorVersion}(_\d+)*\\s*-->.*?<!--\\s*END_SDK_${sdkMajorVersion}(_\d+)*\\s*-->`,
95      'gs'
96    ),
97    ''
98  );
99}
100
101async function removeFromSettingsGradleAsync(abiName: string, settingsGradlePath: string) {
102  console.log(
103    `Removing ${chalk.green(`expoview-${abiName}`)} from ${chalk.magenta(
104      path.relative(EXPO_DIR, settingsGradlePath)
105    )}...`
106  );
107  await transformFileAsync(settingsGradlePath, new RegExp(`\\n\\s*"${abiName}",[^\\n]*`, 'g'), '');
108}
109
110async function removeFromBuildGradleAsync(abiName: string, buildGradlePath: string) {
111  console.log(
112    `Removing maven repository for ${chalk.green(`expoview-${abiName}`)} from ${chalk.magenta(
113      path.relative(EXPO_DIR, buildGradlePath)
114    )}...`
115  );
116  await transformFileAsync(
117    buildGradlePath,
118    new RegExp(`\\s*maven\\s*{\\s*url\\s*".*?/expoview-${abiName}/maven"\\s*}[^\\n]*`),
119    ''
120  );
121}
122
123async function removeFromSdkVersionsAsync(version: string, sdkVersionsPath: string) {
124  console.log(
125    `Removing ${chalk.cyan(version)} from ${chalk.magenta(
126      path.relative(EXPO_DIR, sdkVersionsPath)
127    )}...`
128  );
129  await transformFileAsync(sdkVersionsPath, new RegExp(`"${version}",\s*`, 'g'), '');
130}
131
132async function removeTestSuiteTestsAsync(version: string, testsFilePath: string) {
133  console.log(
134    `Removing test-suite tests from ${chalk.magenta(path.relative(EXPO_DIR, testsFilePath))}...`
135  );
136  await transformFileAsync(
137    testsFilePath,
138    new RegExp(`\\s*(@\\w+\\s+)*@ExpoSdkVersionTest\\("${version}"\\)[^}]+}`),
139    ''
140  );
141}
142
143async function findAndPrintVersionReferencesInSourceFilesAsync(version: string): Promise<boolean> {
144  const pattern = new RegExp(
145    `(${version.replace(/\./g, '[._]')}|(SDK|ABI).?${semver.major(version)})`,
146    'ig'
147  );
148  let matchesCount = 0;
149
150  const files = await glob('**/{src/**/*.@(java|kt|xml),build.gradle}', { cwd: ANDROID_DIR });
151
152  for (const file of files) {
153    const filePath = path.join(ANDROID_DIR, file);
154    const fileContent = await fs.readFile(filePath, 'utf8');
155    const fileLines = fileContent.split(/\r\n?|\n/g);
156    let match;
157
158    while ((match = pattern.exec(fileContent)) != null) {
159      const index = pattern.lastIndex - match[0].length;
160      const lineNumberWithMatch = fileContent.substring(0, index).split(/\r\n?|\n/g).length - 1;
161      const firstLineInContext = Math.max(0, lineNumberWithMatch - 2);
162      const lastLineInContext = Math.min(lineNumberWithMatch + 2, fileLines.length);
163
164      ++matchesCount;
165
166      console.log(
167        `Found ${chalk.bold.green(match[0])} in ${chalk.magenta(
168          path.relative(EXPO_DIR, filePath)
169        )}:`
170      );
171
172      for (let lineIndex = firstLineInContext; lineIndex <= lastLineInContext; lineIndex++) {
173        console.log(
174          `${chalk.gray(1 + lineIndex + ':')} ${fileLines[lineIndex].replace(
175            match[0],
176            chalk.bgMagenta(match[0])
177          )}`
178        );
179      }
180      console.log();
181    }
182  }
183  return matchesCount > 0;
184}
185
186export async function removeVersionAsync(version: string) {
187  const abiName = `abi${version.replace(/\./g, '_')}`;
188  const sdkMajorVersion = `${semver.major(version)}`;
189
190  console.log(`Removing SDK version ${chalk.cyan(version)} for ${chalk.blue('Android')}...`);
191
192  // Remove expoview-abi*_0_0 library
193  await removeVersionedExpoviewAsync(versionedExpoviewAbiPath(abiName));
194  await removeFromSettingsGradleAsync(abiName, settingsGradlePath);
195  await removeFromBuildGradleAsync(abiName, buildGradlePath);
196
197  // Remove code surrounded by BEGIN_SDK_* and END_SDK_*
198  await removeVersionReferencesFromFileAsync(sdkMajorVersion, expoviewBuildGradlePath);
199  await removeVersionReferencesFromFileAsync(sdkMajorVersion, appBuildGradlePath);
200  await removeVersionReferencesFromFileAsync(sdkMajorVersion, rnActivityPath);
201  await removeVersionReferencesFromFileAsync(sdkMajorVersion, expoviewConstantsPath);
202
203  // Remove test-suite tests from the app.
204  await removeTestSuiteTestsAsync(version, testSuiteTestsPath);
205
206  // Update AndroidManifests
207  await removeFromManifestAsync(sdkMajorVersion, appManifestPath);
208  await removeFromManifestAsync(sdkMajorVersion, templateManifestPath);
209
210  // Remove SDK version from the list of supported SDKs
211  await removeFromSdkVersionsAsync(version, sdkVersionsPath);
212
213  console.log(`\nLooking for SDK references in source files...`);
214
215  if (await findAndPrintVersionReferencesInSourceFilesAsync(version)) {
216    console.log(
217      chalk.yellow(`Please review all of these references and remove them manually if possible!\n`)
218    );
219  }
220}
221
222function renameLib(lib: string, abiVersion: string) {
223  for (let i = 0; i < JniLibNames.length; i++) {
224    if (lib.endsWith(JniLibNames[i])) {
225      return `${lib}_abi${abiVersion}`;
226    }
227  }
228
229  return lib;
230}
231
232function processLine(line: string, abiVersion: string) {
233  if (
234    line.startsWith('LOCAL_MODULE') ||
235    line.startsWith('LOCAL_SHARED_LIBRARIES') ||
236    line.startsWith('LOCAL_STATIC_LIBRARIES')
237  ) {
238    let splitLine = line.split('=');
239    let libs = splitLine[1].split(' ');
240    for (let i = 0; i < libs.length; i++) {
241      libs[i] = renameLib(libs[i], abiVersion);
242    }
243    splitLine[1] = libs.join(' ');
244    line = splitLine.join('=');
245  }
246
247  return line;
248}
249
250async function processMkFileAsync(filename: string, abiVersion: string) {
251  let file = await fs.readFile(filename);
252  let fileString = file.toString();
253  await fs.truncate(filename, 0);
254  let lines = fileString.split('\n');
255  for (let i = 0; i < lines.length; i++) {
256    let line = lines[i];
257    line = processLine(line, abiVersion);
258    await fs.appendFile(filename, `${line}\n`);
259  }
260}
261
262async function processJavaCodeAsync(libName: string, abiVersion: string) {
263  const abiName = `abi${abiVersion}`;
264  return spawnAsync(
265    `find ${versionedReactAndroidJavaPath} ${versionedExpoviewAbiPath(
266      abiName
267    )} -iname '*.java' -type f -print0 | ` +
268      `xargs -0 sed -i '' 's/"${libName}"/"${libName}_abi${abiVersion}"/g'`,
269    [],
270    { shell: true }
271  );
272}
273
274async function updateVersionedReactNativeAsync() {
275  await fs.remove(versionedReactAndroidPath);
276  await fs.remove(versionedReactCommonPath);
277  await fs.copy(reactAndroidPath, versionedReactAndroidPath);
278  await fs.copy(reactCommonPath, versionedReactCommonPath);
279}
280
281async function renameJniLibsAsync(version: string) {
282  const abiVersion = version.replace(/\./g, '_');
283  const abiPrefix = `abi${abiVersion}`;
284  const versionedAbiPath = path.join(
285    Directories.getAndroidDir(),
286    'versioned-abis',
287    `expoview-${abiPrefix}`
288  );
289
290  // Update JNI methods
291  const packagesToRename = await getJavaPackagesToRename();
292  for (const javaPackage of packagesToRename) {
293    const pathForPackage = javaPackage.replace(/\./g, '\\/');
294    await spawnAsync(
295      `find ${versionedReactCommonPath} ${versionedReactAndroidJniPath} -type f ` +
296        `\\( -name \*.java -o -name \*.h -o -name \*.cpp -o -name \*.mk \\) -print0 | ` +
297        `xargs -0 sed -i '' 's/${pathForPackage}/abi${abiVersion}\\/${pathForPackage}/g'`,
298      [],
299      { shell: true }
300    );
301
302    // reanimated
303    const oldJNIReanimatedPackage =
304      'versioned\\/host\\/exp\\/exponent\\/modules\\/api\\/reanimated\\/';
305    const newJNIReanimatedPackage = 'host\\/exp\\/exponent\\/modules\\/api\\/reanimated\\/';
306    await spawnAsync(
307      `find ${versionedAbiPath} -type f ` +
308        `\\( -name \*.java -o -name \*.h -o -name \*.cpp -o -name \*.mk \\) -print0 | ` +
309        `xargs -0 sed -i '' 's/${oldJNIReanimatedPackage}/abi${abiVersion}\\/${newJNIReanimatedPackage}/g'`,
310      [],
311      { shell: true }
312    );
313  }
314
315  // Update LOCAL_MODULE, LOCAL_SHARED_LIBRARIES, LOCAL_STATIC_LIBRARIES fields in .mk files
316  let [reactCommonMkFiles, reactAndroidMkFiles, reanimatedMKFiles] = await Promise.all([
317    glob(path.join(versionedReactCommonPath, '**/*.mk')),
318    glob(path.join(versionedReactAndroidJniPath, '**/*.mk')),
319    glob(path.join(versionedAbiPath, '**/*.mk')),
320  ]);
321  let filenames = [...reactCommonMkFiles, ...reactAndroidMkFiles, ...reanimatedMKFiles];
322  await Promise.all(filenames.map((filename) => processMkFileAsync(filename, abiVersion)));
323
324  // Rename references to JNI libs in Java code
325  for (let i = 0; i < JniLibNames.length; i++) {
326    let libName = JniLibNames[i];
327    await processJavaCodeAsync(libName, abiVersion);
328  }
329
330  // 'fbjni' is loaded without the 'lib' prefix in com.facebook.jni.Prerequisites
331  await processJavaCodeAsync('fbjni', abiVersion);
332  await processJavaCodeAsync('fb', abiVersion);
333
334  console.log('\nThese are the JNI lib names we modified:');
335  await spawnAsync(
336    `find ${versionedReactAndroidJavaPath} ${versionedAbiPath} -name "*.java" | xargs grep -i "_abi${abiVersion}"`,
337    [],
338    { shell: true, stdio: 'inherit' }
339  );
340
341  console.log('\nAnd here are all instances of loadLibrary:');
342  await spawnAsync(
343    `find ${versionedReactAndroidJavaPath} ${versionedAbiPath} -name "*.java" | xargs grep -i "loadLibrary"`,
344    [],
345    { shell: true, stdio: 'inherit' }
346  );
347
348  const { isCorrect } = await inquirer.prompt<{ isCorrect: boolean }>([
349    {
350      type: 'confirm',
351      name: 'isCorrect',
352      message: 'Does all that look correct?',
353      default: false,
354    },
355  ]);
356  if (!isCorrect) {
357    throw new Error('Fix JNI libs');
358  }
359}
360
361async function copyUnimodulesAsync(version: string) {
362  const packages = await getListOfPackagesAsync();
363  for (const pkg of packages) {
364    if (
365      pkg.isSupportedOnPlatform('android') &&
366      pkg.isIncludedInExpoClientOnPlatform('android') &&
367      pkg.isVersionableOnPlatform('android')
368    ) {
369      await spawnAsync(
370        './android-copy-unimodule.sh',
371        [version, path.join(pkg.path, pkg.androidSubdirectory)],
372        {
373          shell: true,
374          cwd: SCRIPT_DIR,
375        }
376      );
377      console.log(`   ✅  Created versioned ${pkg.packageName}`);
378    }
379  }
380}
381
382async function addVersionedActivitesToManifests(version: string) {
383  const abiVersion = version.replace(/\./g, '_');
384  const abiName = `abi${abiVersion}`;
385  const majorVersion = semver.major(version);
386
387  await transformFileAsync(
388    templateManifestPath,
389    new RegExp('<!-- ADD DEV SETTINGS HERE -->'),
390    `<!-- ADD DEV SETTINGS HERE -->
391    <!-- BEGIN_SDK_${majorVersion} -->
392    <activity android:name="${abiName}.com.facebook.react.devsupport.DevSettingsActivity"/>
393    <!-- END_SDK_${majorVersion} -->`
394  );
395
396  await transformFileAsync(
397    templateManifestPath,
398    new RegExp('<!-- Versioned Activity for Stripe -->'),
399    `<!-- Versioned Activity for Stripe -->
400    <!-- BEGIN_SDK_${majorVersion} -->
401    <activity
402      android:exported="true"
403      android:launchMode="singleTask"
404      android:name="${abiName}.expo.modules.payments.stripe.RedirectUriReceiver"
405      android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
406      <intent-filter>
407        <action android:name="android.intent.action.VIEW" />
408        <category android:name="android.intent.category.DEFAULT" />
409        <category android:name="android.intent.category.BROWSABLE" />
410        <data android:scheme="${abiName}.expo.modules.payments.stripe" />
411      </intent-filter>
412    </activity>
413    <!-- END_SDK_${majorVersion} -->`
414  );
415}
416
417async function registerNewVersionUnderSdkVersions(version: string) {
418  let fileString = await fs.readFile(sdkVersionsPath, 'utf8');
419  let jsConfig;
420  // read the existing json config and add the new version to the sdkVersions array
421  try {
422    jsConfig = JSON.parse(fileString);
423  } catch (e) {
424    console.log('Error parsing existing sdkVersions.json file, writing a new one...', e);
425    console.log('The erroneous file contents was:', fileString);
426    jsConfig = {
427      sdkVersions: [],
428    };
429  }
430  // apply changes
431  jsConfig.sdkVersions.push(version);
432  await fs.writeFile(sdkVersionsPath, JSON.stringify(jsConfig));
433}
434
435async function cleanUpAsync(version: string) {
436  const abiVersion = version.replace(/\./g, '_');
437  const abiName = `abi${abiVersion}`;
438
439  const versionedAbiSrcPath = path.join(
440    versionedExpoviewAbiPath(abiName),
441    'src/main/java',
442    abiName
443  );
444
445  let filesToDelete: string[] = [];
446
447  // delete PrintDocumentAdapter*Callback.java
448  // their package is `android.print` and therefore they are not changed by the versioning script
449  // so we will have duplicate classes
450  const printCallbackFiles = await glob(
451    path.join(versionedAbiSrcPath, 'expo/modules/print/*Callback.java')
452  );
453  for (const file of printCallbackFiles) {
454    const contents = await fs.readFile(file, 'utf8');
455    if (!contents.includes(`package ${abiName}`)) {
456      filesToDelete.push(file);
457    } else {
458      console.log(`Skipping deleting ${file} because it appears to have been versioned`);
459    }
460  }
461
462  // delete versioned loader providers since we don't need them
463  filesToDelete.push(path.join(versionedAbiSrcPath, 'expo/loaders'));
464
465  console.log('Deleting the following files and directories:');
466  console.log(filesToDelete);
467
468  for (const file of filesToDelete) {
469    await fs.remove(file);
470  }
471
472  // misc fixes for versioned code
473  const versionedExponentPackagePath = path.join(
474    versionedAbiSrcPath,
475    'host/exp/exponent/ExponentPackage.java'
476  );
477  await transformFileAsync(
478    versionedExponentPackagePath,
479    new RegExp('// WHEN_VERSIONING_REMOVE_FROM_HERE', 'g'),
480    '/* WHEN_VERSIONING_REMOVE_FROM_HERE'
481  );
482  await transformFileAsync(
483    versionedExponentPackagePath,
484    new RegExp('// WHEN_VERSIONING_REMOVE_TO_HERE', 'g'),
485    'WHEN_VERSIONING_REMOVE_TO_HERE */'
486  );
487
488  await transformFileAsync(
489    path.join(versionedAbiSrcPath, 'host/exp/exponent/VersionedUtils.java'),
490    new RegExp('// DO NOT EDIT THIS COMMENT - used by versioning scripts[^,]+,[^,]+,'),
491    'null, null,'
492  );
493
494  await transformFileAsync(
495    path.join(versionedAbiSrcPath, 'expo/modules/payments/stripe/PayFlow.java'),
496    new RegExp('// ADD BUILDCONFIG IMPORT HERE'),
497    `import ${abiName}.host.exp.expoview.BuildConfig;`
498  );
499
500  // replace abixx_x_x...R with abixx_x_x.host.exp.expoview.R
501  await spawnAsync(
502    `find ${versionedAbiSrcPath} -iname '*.java' -type f -print0 | ` +
503      `xargs -0 sed -i '' 's/import ${abiName}\.[^;]*\.R;/import ${abiName}.host.exp.expoview.R;/g'`,
504    [],
505    { shell: true }
506  );
507  await spawnAsync(
508    `find ${versionedAbiSrcPath} -iname '*.kt' -type f -print0 | ` +
509      `xargs -0 sed -i '' 's/import ${abiName}\\..*\\.R$/import ${abiName}.host.exp.expoview.R/g'`,
510    [],
511    { shell: true }
512  );
513
514  // add new versioned maven to build.gradle
515  await transformFileAsync(
516    buildGradlePath,
517    new RegExp('// For old expoviews to work'),
518    `// For old expoviews to work
519    maven {
520      url "$rootDir/versioned-abis/expoview-${abiName}/maven"
521    }`
522  );
523}
524
525async function prepareReanimatedAsync(version: string): Promise<void> {
526  const abiVersion = version.replace(/\./g, '_');
527  const abiName = `abi${abiVersion}`;
528  const versionedExpoviewPath = versionedExpoviewAbiPath(abiName);
529
530  const buildReanimatedSO = async () => {
531    await spawnAsync(`./gradlew :expoview-${abiName}:packageNdkLibs`, [], {
532      shell: true,
533      cwd: path.join(versionedExpoviewPath, '../../'),
534      stdio: 'inherit',
535    });
536  };
537
538  const removeLeftoverDirectories = async () => {
539    const mainPath = path.join(versionedExpoviewPath, 'src', 'main');
540    const toRemove = ['Common', 'JNI', 'cpp'];
541    for (let dir of toRemove) {
542      await fs.remove(path.join(mainPath, dir));
543    }
544  };
545
546  const removeLeftoversFromGradle = async () => {
547    await spawnAsync('./android-remove-reanimated-code-from-gradle.sh', [version], {
548      shell: true,
549      cwd: SCRIPT_DIR,
550      stdio: 'inherit',
551    });
552  };
553
554  await buildReanimatedSO();
555  await removeLeftoverDirectories();
556  await removeLeftoversFromGradle();
557}
558
559export async function addVersionAsync(version: string) {
560  console.log(' ��   1/9: Updating android/versioned-react-native...');
561  await updateVersionedReactNativeAsync();
562  console.log(' ✅  1/9: Finished\n\n');
563
564  console.log(' ��   2/9: Creating versioned expoview package...');
565  await spawnAsync('./android-copy-expoview.sh', [version], {
566    shell: true,
567    cwd: SCRIPT_DIR,
568  });
569
570  console.log(' ✅  2/9: Finished\n\n');
571
572  console.log(' ��   3/9: Renaming JNI libs in android/versioned-react-native and Reanimated...');
573  await renameJniLibsAsync(version);
574  console.log(' ✅  3/9: Finished\n\n');
575
576  console.log(' ��   4/9: prepare versioned Reanimated...');
577  await prepareReanimatedAsync(version);
578  console.log(' ✅  4/9: Finished\n\n');
579
580  console.log(' ��   5/9: Building versioned ReactAndroid AAR...');
581  await spawnAsync('./android-build-aar.sh', [version], {
582    shell: true,
583    cwd: SCRIPT_DIR,
584    stdio: 'inherit',
585  });
586  console.log(' ✅  5/9: Finished\n\n');
587
588  console.log(' ��   6/9: Creating versioned unimodule packages...');
589  await copyUnimodulesAsync(version);
590  console.log(' ✅  6/9: Finished\n\n');
591
592  console.log(' ��   7/9: Adding extra versioned activites to AndroidManifest...');
593  await addVersionedActivitesToManifests(version);
594  console.log(' ✅  7/9: Finished\n\n');
595
596  console.log(' ��   8/9: Registering new version under sdkVersions config...');
597  await registerNewVersionUnderSdkVersions(version);
598  console.log(' ✅  8/9: Finished\n\n');
599
600  console.log(' ��   9/9: Misc cleanup...');
601  await cleanUpAsync(version);
602  console.log(' ✅  9/9: Finished');
603}
604