react-native-community / cli

The React Native Community CLI - command line tools to help you build RN apps
MIT License
2.37k stars 902 forks source link

`react-native run-android` fails with `Not found the correct install APK file!` when using Android "flavors". #921

Closed eihli closed 4 years ago

eihli commented 4 years ago

Environment

info Fetching system and libraries information...
System:
    OS: Linux 4.19 Manjaro Linux
    CPU: (4) x64 Intel(R) Core(TM) i7-7600U CPU @ 2.80GHz
    Memory: 2.30 GB / 15.42 GB
    Shell: 5.7.1 - /bin/zsh
  Binaries:
    Node: 13.0.1 - ~/.nvm/versions/node/v13.0.1/bin/node
    Yarn: 1.19.1 - ~/.nvm/versions/node/v13.0.1/bin/yarn
    npm: 6.13.4 - ~/.nvm/versions/node/v13.0.1/bin/npm
    Watchman: 4.9.0 - /usr/bin/watchman
  SDKs:
    Android SDK:
      API Levels: 28, 29
      Build Tools: 28.0.3, 29.0.2
      System Images: android-29 | Intel x86 Atom_64, android-29 | Google APIs Intel x86 Atom
  IDEs:
    Android Studio: 3.5 AI-191.8026.42.35.6010548
  npmPackages:
    react: 16.9.0 => 16.9.0
    react-native: 0.61.5 => 0.61.5
  npmGlobalPackages:
    metro-react-native-babel-preset: 0.58.0
    react-native-cli: 2.0.1
    react-native-git-upgrade: 0.2.7

Description

npx react-native run-android fails with Not found the correct install APK file! when using flavors.

I started getting this error after adding productFlavors to my build.gradle

    productFlavors {
        pro {
            dimension = 'version'
            applicationId = 'com.ezmonic'
            applicationIdSuffix = 'pro'
            resValue "string", "app_name", "Ezmonic"
        }
        free {
            dimension = 'version'
            applicationId = 'com.ezmonic'
            resValue "string", "app_name", "Ezmonic Free"            
        }
    }
    buildTypes {
        debug {
            signingConfig signingConfigs.debug
        }
        release {
            // Caution! In production, you need to generate your own keystore file.
            // see https://facebook.github.io/react-native/docs/signed-apk-android.
            signingConfig signingConfigs.release
            minifyEnabled enableProguardInReleaseBuilds
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        }
    }
$ npx react-native run-android --variant proRelease --deviceId 3434363550553098
info Running jetifier to migrate libraries to AndroidX. You can disable it using "--no-jetifier" flag.
Jetifier found 2465 file(s) to forward-jetify. Using 4 workers...
info JS server already running.
...
BUILD SUCCESSFUL in 3m 49s
284 actionable tasks: 23 executed, 261 up-to-date
info Connecting to the development server...
error Failed to install the app on the device. Run CLI with --verbose flag for more details.
Error: Not found the correct install APK file!

Reproducible Demo

It's coming from the following lines in https://github.com/react-native-community/cli/blob/master/packages/platform-android/src/commands/runAndroid/index.ts

The function to get ApkName only checks variant, debug/release, and not flavor (paid/free in my case).

function getInstallApkName(
  appFolder: string,
  adbPath: string,
  variant: string,
  device: string,
  buildDirectory: string,
) {
  const availableCPUs = adb.getAvailableCPUs(adbPath, device);

  // check if there is an apk file like app-armeabi-v7a-debug.apk
  for (const availableCPU of availableCPUs.concat('universal')) {
    const apkName = `${appFolder}-${availableCPU}-${variant}.apk`;
    if (fs.existsSync(`${buildDirectory}/${apkName}`)) {
      return apkName;
    }
  }

  // check if there is a default file like app-debug.apk
  const apkName = `${appFolder}-${variant}.apk`;
  if (fs.existsSync(`${buildDirectory}/${apkName}`)) {
    return apkName;
  }

  throw new Error('Not found the correct install APK file!');
}

For anyone who runs into this issue before it's resolved, my immediate work-around is to just run the adb command manually to get the apk to the device.

adb -s ${device} install -r -d ${pathToApk}

eihli commented 4 years ago

What the Android docs refer to as "variant" is actually a combination of a build "type" and "flavor". https://developer.android.com/studio/build/build-variants.

"type" is debug vs release. "flavor" is free vs paid, or demo vs full.

I think the proper fix would be to split the --variant flag into separate --type and --flavor flags.

grabbou commented 4 years ago

Thank you so much for sending over this PR! Turns out that this is going to be fixed by a PR that we have submitted more than a year ago and never merged.

I came back to the repository and rebased it. Going to ship it within the next release, hoping that the issue goes away.

sakiv commented 7 months ago

As @grabbou is no more maintaining this repository so addressing @thymikee and @adamTrz. This issue still persists and not yet fixed. PR #934 has changed and fixed only the launch part of it, however, install APK piece is still broken.

Issue lies in tryInstallAppOnDevice() function. Use case is when build is triggered using non-interactive manner, via Terminal command, yarn react-native run-android --deviceId=emulator-5554 --mode=demoDebug --appIdSuffix=demo. As per code, split is done only for selectedTask, which is populated in case of interactive mode, but not for args.mode, which is one of the input during manual build.

function tryInstallAppOnDevice(
  args: Flags,
  adbPath: string,
  device: string,
  androidProject: AndroidProject,
  selectedTask?: string,
) {
  try {
    // "app" is usually the default value for Android apps with only 1 app
    const {appName, sourceDir} = androidProject;

    const defaultVariant = (args.mode || 'debug').toLowerCase();

    // handle if selected task from interactive mode includes build flavour as well, eg. installProductionDebug should create ['production','debug'] array
    const variantFromSelectedTask = selectedTask
      ?.replace('install', '')
      .split(/(?=[A-Z])/);

    // create path to output file, eg. `production/debug`
    const variantPath =
      variantFromSelectedTask?.join('/')?.toLowerCase() ?? defaultVariant;
    // create output file name, eg. `production-debug`
    const variantAppName =
      variantFromSelectedTask?.join('-')?.toLowerCase() ?? defaultVariant;

    let pathToApk;
    if (!args.binaryPath) {
      const buildDirectory = `${sourceDir}/${appName}/build/outputs/apk/${variantPath}`;
      const apkFile = getInstallApkName(
        appName,
        adbPath,
        variantAppName,
        device,
        buildDirectory,
      );
      pathToApk = `${buildDirectory}/${apkFile}`;
    } else {
      pathToApk = args.binaryPath;
    }

    const installArgs = ['-s', device, 'install', '-r', '-d'];
    if (args.user !== undefined) {
      installArgs.push('--user', `${args.user}`);
    }
    const adbArgs = [...installArgs, pathToApk];
    logger.info(`Installing the app on the device "${device}"...`);
    logger.debug(`Running command "cd android && adb ${adbArgs.join(' ')}"`);
    execa.sync(adbPath, adbArgs, {stdio: 'inherit'});
  } catch (error) {
    throw new CLIError(
      'Failed to install the app on the device.',
      error as any,
    );
  }
}