Skip to content

Commit

Permalink
Multiple version checking (#4914)
Browse files Browse the repository at this point in the history
<!-- Thanks for submitting a pull request! We appreciate you spending
the time to work on these changes. Please follow the template so that
the reviewers can easily understand what the code changes affect. -->

## Summary

This PR is a pinnacle of Reanimated's version detection grand scheme.

Since there are numerous errors stemming from having parts of Reanimated
used from different version we decided to enforce runtime checks for
those parts - to make the troubleshooting process easier.

Adding:
- C++ checks JavaScript version
- C++ checks Java version
- Java checks C++ version


Requires #4915  

## Test plan

1. C++ checks JavaScript version:

Trigger failed resolution:

```diff
// src/reanimated2/NativeReanimated/NativeReanimated.ts
-  global._REANIMATED_VERSION_JS = jsVersion;
```

Trigger wrong version:

```diff
// src/reanimated2/NativeReanimated/NativeReanimated.ts
- global._REANIMATED_VERSION_JS = jsVersion;
+ global._REANIMATED_VERSION_JS = 'wrong-version';
```

2. C++ checks Java version:

Trigger failed resolution:

```diff
// android/src/main/java/com/swmansion/reanimated/nativeProxy/NativeProxyCommon.java
  public String getReanimatedJavaVersion() {
-   return BuildConfig.REANIMATED_VERSION_JAVA;
+   throw new RuntimeException("forced fail");
  }
```

Trigger wrong version:

```diff
// android/src/main/java/com/swmansion/reanimated/nativeProxy/NativeProxyCommon.java
  public String getReanimatedJavaVersion() {
-   return BuildConfig.REANIMATED_VERSION_JAVA;
+   return new String("wrong-version");
  }
```

3. Java checks C++ version:

Trigger failed resolution:

```diff
// android/src/main/java/com/swmansion/reanimated/nativeProxy/NativeProxyCommon.java
  protected void setCppVersion(String version) {
-   cppVersion = version;
+   // NOOP
  }
```

Trigger wrong version:

```diff
// android/src/main/java/com/swmansion/reanimated/nativeProxy/NativeProxyCommon.java
  protected void setCppVersion(String version) {
-   cppVersion = version;
+   cppVersion = new String("wrong-version");
  }
```
  • Loading branch information
tjzel committed Sep 27, 2023
1 parent 81c7a60 commit df30b68
Show file tree
Hide file tree
Showing 15 changed files with 236 additions and 112 deletions.
7 changes: 5 additions & 2 deletions Common/cpp/ReanimatedRuntime/RNRuntimeDecorator.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "RNRuntimeDecorator.h"
#ifdef DEBUG
#include "ReanimatedVersion.h"
#endif // DEBUG

namespace reanimated {

Expand Down Expand Up @@ -29,8 +31,9 @@ void RNRuntimeDecorator::decorate(
#endif // RCT_NEW_ARCH_ENABLED
rnRuntime.global().setProperty(rnRuntime, "_IS_FABRIC", isFabric);

auto version = getReanimatedVersionString(rnRuntime);
rnRuntime.global().setProperty(rnRuntime, "_REANIMATED_VERSION_CPP", version);
#ifdef DEBUG
checkJSVersion(rnRuntime);
#endif // DEBUG

rnRuntime.global().setProperty(
rnRuntime, "_REANIMATED_IS_REDUCED_MOTION", isReducedMotion);
Expand Down
60 changes: 58 additions & 2 deletions Common/cpp/Tools/ReanimatedVersion.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#include "ReanimatedVersion.h"
#include <regex>
#include <string>

#ifdef REANIMATED_VERSION
#define STRINGIZE(x) #x
Expand All @@ -10,8 +12,62 @@ using namespace facebook;

namespace reanimated {

jsi::String getReanimatedVersionString(jsi::Runtime &rt) {
return jsi::String::createFromUtf8(rt, REANIMATED_VERSION_STRING);
std::string getReanimatedCppVersion() {
return std::string(REANIMATED_VERSION_STRING);
}

// This function is pretty much a copy of
// `src/reanimated2/platform-specific/checkVersion.ts`.
#ifdef DEBUG
bool matchVersion(const std::string &version1, const std::string &version2) {
std::regex pattern("^\\d+\\.\\d+\\.\\d+$");
if (std::regex_match(version1, pattern) &&
std::regex_match(version2, pattern)) {
auto majorPattern = std::regex("^\\d+");
auto major1 = std::regex_search(version1, majorPattern);
auto major2 = std::regex_search(version2, majorPattern);
if (major1 != major2) {
return false;
}
auto minorPattern = std::regex("\\.\\d+\\.");
auto minor1 = std::regex_search(version1, minorPattern);
auto minor2 = std::regex_search(version2, minorPattern);
if (minor1 != minor2) {
return false;
}
return true;
} else {
return version1 == version2;
}
}

void checkJSVersion(jsi::Runtime &rnRuntime) {
auto cppVersion = getReanimatedCppVersion();

auto maybeJSVersion =
rnRuntime.global().getProperty(rnRuntime, "_REANIMATED_VERSION_JS");
if (maybeJSVersion.isUndefined()) {
throw std::runtime_error(
std::string(
"[Reanimated] C++ side failed to resolve JavaScript code version\n") +
"See `https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#c-side-failed-to-resolve-javascript-code-version` for more details.");
}

auto jsVersion = maybeJSVersion.asString(rnRuntime).utf8(rnRuntime);

if (!matchVersion(cppVersion, jsVersion)) {
throw std::runtime_error(
std::string(
"[Reanimated] Mismatch between C++ code version and JavaScript code version (") +
cppVersion + " vs. " + jsVersion + " respectively).\n" +
"See `https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#mismatch-between-c-code-version-and-javascript-code-version` for more details.");
}

rnRuntime.global().setProperty(
rnRuntime,
"_REANIMATED_VERSION_CPP",
jsi::String::createFromUtf8(rnRuntime, cppVersion));
}
#endif // DEBUG

}; // namespace reanimated
10 changes: 8 additions & 2 deletions Common/cpp/Tools/ReanimatedVersion.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
#pragma once

#include <jsi/jsi.h>
#include <string>

using namespace facebook;

namespace reanimated {

jsi::String getReanimatedVersionString(jsi::Runtime &rt);
std::string getReanimatedCppVersion();

};
#ifdef DEBUG
bool matchVersion(const std::string &, const std::string &);
void checkJSVersion(jsi::Runtime &);
#endif // DEBUG

}; // namespace reanimated
1 change: 0 additions & 1 deletion RNReanimated.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ require_relative './scripts/reanimated_utils'

reanimated_package_json = JSON.parse(File.read(File.join(__dir__, "package.json")))
config = find_config()
assert_no_multiple_instances(config)
assert_latest_react_native_with_new_architecture(config, reanimated_package_json)
assert_minimal_react_native_version(config)

Expand Down
60 changes: 2 additions & 58 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,6 @@ def getPlaygroundAppName() { // only for the development
return playgroundAppName
}

def shouldAssertNoMultipleInstances() {
if (rootProject.hasProperty("disableMultipleInstancesCheck")) {
return rootProject.property("disableMultipleInstancesCheck") != "true"
} else {
return true
}
}

def getReanimatedVersion() {
def inputFile = file(projectDir.path + '/../package.json')
def json = new JsonSlurper().parseText(inputFile.text)
Expand Down Expand Up @@ -267,6 +259,7 @@ android {
versionCode 1
versionName "1.0"
buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", IS_NEW_ARCHITECTURE_ENABLED.toString())
buildConfigField("String", "REANIMATED_VERSION_JAVA", "\"${REANIMATED_VERSION}\"")
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared",
Expand Down Expand Up @@ -385,55 +378,6 @@ android {
}
}

abstract class NoMultipleInstancesAssertionTask extends DefaultTask {
@Inject abstract ObjectFactory getObjectFactory()

@Input abstract Property<File> getProjectDirFile()
@Input abstract Property<File> getRootDirFile()
@Input abstract Property<Boolean> getShouldCheck()

def findReanimatedInstancesForPath(String path) {
return objectFactory.fileTree().from(path)
.include("**/react-native-reanimated/package.json")
.exclude("**/.yarn/**")
.exclude({ Files.isSymbolicLink(it.getFile().toPath()) })
.findAll()
}

@TaskAction
def check() {
if (shouldCheck.get()) {
// Assert there are no multiple installations of Reanimated
Set<File> files

if (projectDirFile.get().parent.contains(rootDirFile.get().parent)) {
// standard app
files = findReanimatedInstancesForPath(rootDirFile.get().parent + "/node_modules")
} else {
// monorepo
files = findReanimatedInstancesForPath(rootDirFile.get().parent + "/node_modules")
files.addAll(
findReanimatedInstancesForPath(projectDirFile.get().parentFile.parent)
)
}

if (files.size() > 1) {
String parsedLocation = files.stream().map({
File file -> "- " + file.toString().replace("/package.json", "")
}).collect().join("\n")
String exceptionMessage = "\n[Reanimated] Multiple versions of Reanimated were detected in `build.gradle`. See `https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting/#multiple-versions-of-reanimated-were-detected` for more details.\n\nConflict between: \n" + parsedLocation + "\n"
throw new GradleException(exceptionMessage)
}
}
}
}

tasks.register('assertNoMultipleInstances', NoMultipleInstancesAssertionTask) {
shouldCheck = shouldAssertNoMultipleInstances()
rootDirFile = rootDir
projectDirFile = projectDir
}

def assertLatestReactNativeWithNewArchitecture = task assertLatestReactNativeWithNewArchitectureTask {
onlyIf { IS_NEW_ARCHITECTURE_ENABLED && REANIMATED_MAJOR_VERSION == 3 && REACT_NATIVE_MINOR_VERSION < 72 }
doFirst {
Expand Down Expand Up @@ -465,7 +409,7 @@ task prepareHeadersForPrefab(type: Copy) {
}

tasks.preBuild {
dependsOn assertNoMultipleInstances, assertLatestReactNativeWithNewArchitecture, assertMinimalReactNativeVersion
dependsOn assertLatestReactNativeWithNewArchitecture, assertMinimalReactNativeVersion
}

task cleanCmakeCache() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public NativeProxy(ReactApplicationContext context) {
fabricUIManager);
prepareLayoutAnimations(LayoutAnimations);
installJSIBindings();
if (BuildConfig.DEBUG) {
checkCppVersion();
}
}

private native HybridData initHybrid(
Expand Down
46 changes: 46 additions & 0 deletions android/src/main/cpp/NativeProxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
#include "RNRuntimeDecorator.h"
#include "ReanimatedJSIUtils.h"
#include "ReanimatedRuntime.h"
#ifdef DEBUG
#include "ReanimatedVersion.h"
#endif // DEBUG
#include "WorkletRuntime.h"
#include "WorkletRuntimeCollector.h"

Expand Down Expand Up @@ -101,12 +104,55 @@ jni::local_ref<NativeProxy::jhybriddata> NativeProxy::initHybrid(
/**/);
}

#ifdef DEBUG
void NativeProxy::checkJavaVersion(jsi::Runtime &rnRuntime) {
std::string javaVersion;
try {
javaVersion =
getJniMethod<jstring()>("getReanimatedJavaVersion")(javaPart_.get())
->toStdString();
} catch (std::exception &) {
throw std::runtime_error(
std::string(
"[Reanimated] C++ side failed to resolve Java code version.\n") +
"See `https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#c-side-failed-to-resolve-java-code-version` for more details.");
}

auto cppVersion = getReanimatedCppVersion();
if (cppVersion != javaVersion) {
throw std::runtime_error(
std::string(
"[Reanimated] Mismatch between C++ code version and Java code version (") +
cppVersion + " vs. " + javaVersion + " respectively).\n" +
"See `https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#mismatch-between-c-code-version-and-java-code-version` for more details.");
}
}

void NativeProxy::injectCppVersion() {
auto cppVersion = getReanimatedCppVersion();
try {
static const auto method =
getJniMethod<void(jni::local_ref<JString>)>("setCppVersion");
method(javaPart_.get(), make_jstring(cppVersion));
} catch (std::exception &) {
throw std::runtime_error(
std::string(
"[Reanimated] C++ side failed to resolve Java code version (injection).\n") +
"See `https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#c-side-failed-to-resolve-java-code-version` for more details.");
}
}
#endif // DEBUG

void NativeProxy::installJSIBindings() {
jsi::Runtime &rnRuntime = *rnRuntime_;
WorkletRuntimeCollector::install(rnRuntime);
auto isReducedMotion = getIsReducedMotion();
RNRuntimeDecorator::decorate(
rnRuntime, nativeReanimatedModule_, isReducedMotion);
#ifdef DEBUG
checkJavaVersion(rnRuntime);
injectCppVersion();
#endif // DEBUG

registerEventHandler();
setupLayoutAnimations();
Expand Down
4 changes: 4 additions & 0 deletions android/src/main/cpp/NativeProxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ class NativeProxy : public jni::HybridClass<NativeProxy> {
jsi::Runtime *rnRuntime_;
std::shared_ptr<NativeReanimatedModule> nativeReanimatedModule_;
jni::global_ref<LayoutAnimations::javaobject> layoutAnimations_;
#ifdef DEBUG
void checkJavaVersion(jsi::Runtime &);
void injectCppVersion();
#endif // DEBUG
#ifdef RCT_NEW_ARCH_ENABLED
// removed temporarily, event listener mechanism needs to be fixed on RN side
// std::shared_ptr<facebook::react::Scheduler> reactScheduler_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.facebook.soloader.SoLoader;
import com.swmansion.common.GestureHandlerStateManager;
import com.swmansion.reanimated.AndroidUIScheduler;
import com.swmansion.reanimated.BuildConfig;
import com.swmansion.reanimated.NativeProxy;
import com.swmansion.reanimated.NodesManager;
import com.swmansion.reanimated.ReanimatedModule;
Expand Down Expand Up @@ -44,6 +45,7 @@ public abstract class NativeProxyCommon {
private ReanimatedKeyboardEventListener reanimatedKeyboardEventListener;
private Long firstUptime = SystemClock.uptimeMillis();
private boolean slowAnimationsEnabled = false;
protected String cppVersion = null;

protected NativeProxyCommon(ReactApplicationContext context) {
mAndroidUIScheduler = new AndroidUIScheduler(context);
Expand Down Expand Up @@ -97,6 +99,37 @@ public void requestRender(AnimationFrameCallback callback) {
mNodesManager.postOnAnimation(callback);
}

@DoNotStrip
public String getReanimatedJavaVersion() {
return BuildConfig.REANIMATED_VERSION_JAVA;
}

@DoNotStrip
@SuppressWarnings("unused")
// It turns out it's pretty difficult to set a member of a class
// instance through JNI so we decided to use a setter instead.
protected void setCppVersion(String version) {
cppVersion = version;
}

protected void checkCppVersion() {
if (cppVersion == null) {
throw new RuntimeException(
"[Reanimated] Java side failed to resolve C++ code version. "
+ "See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#java-side-failed-to-resolve-c-code-version for more information.");
}
String javaVersion = getReanimatedJavaVersion();
if (!cppVersion.equals(javaVersion)) {
throw new RuntimeException(
"[Reanimated] Mismatch between Java code version and C++ code version ("
+ javaVersion
+ " vs. "
+ cppVersion
+ " respectively). See "
+ "https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#mismatch-between-java-code-version-and-c-code-version for more information.");
}
}

@DoNotStrip
public void updateProps(int viewTag, Map<String, Object> props) {
mNodesManager.updateProps(viewTag, props);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public NativeProxy(ReactApplicationContext context) {
messageQueueThread);
prepareLayoutAnimations(LayoutAnimations);
installJSIBindings();
if (BuildConfig.DEBUG) {
checkCppVersion();
}
}

private native HybridData initHybrid(
Expand Down
Loading

0 comments on commit df30b68

Please sign in to comment.