Rork Max + EAS Build カスタムネイティブモジュール導入エラー完全解決ガイド
Rork MaxアプリでExpo SDKにない機能——Bluetoothデバイス連携、カスタムSDK統合、ハードウェアアクセスなど——を実装しようとすると、EAS Buildとカスタムネイティブモジュールの世界に踏み込むことになります。
これは一度乗り越えれば強力な武器になりますが、最初は「なぜビルドが通らないのか」「なぜモジュールが認識されないのか」と詰まることが多いです。
EAS BuildとConfig Pluginの仕組みを正しく理解する
カスタムネイティブモジュールをManaged WorkflowのExpo(Rork Maxが内部で使う方式)に追加するには、Config Pluginが必要です。Config Pluginとは、npx expo prebuild(またはEAS Buildが内部で実行する)のタイミングに、ネイティブコード(iOS PodfileやAndroid build.gradle)を自動的に変更するためのスクリプトです。
多くのライブラリは自前のConfig Pluginを提供しています(@config-plugins/react-native-ble-plxなど)。一方、公式Pluginがないライブラリでは自作が必要です。
全体フローの確認:
app.config.js の plugins 設定
↓
npx expo prebuild (EAS Build内で自動実行)
↓
ios/ と android/ ディレクトリが生成される
↓
iOS: pod install, Android: gradle build
↓
バイナリ生成
エラーパターン①:incompatible library または Podfile競合エラー
症状: EAS BuildのiOSビルドログに以下のようなエラーが出る。
[!] CocoaPods could not find compatible versions for pod "SomeLibrary":
In snapshot (Podfile.lock):
SomeLibrary (= 1.2.0)
In Podfile:
SomeLibrary (~> 2.0)
原因と解決法
複数のネイティブモジュールが同じPodの異なるバージョンを要求しているケースです。
# ios/Podfile に以下を追加して強制バージョンを固定する
# app.config.js の ios.podfileExtraContent ではなく、
# Config Plugin で Podfile を修正するのが正式な方法
# config-plugin.js(自作Config Plugin)
const { withPodfile } = require('@expo/config-plugins');
module.exports = (config) => {
return withPodfile(config, (modConfig) => {
const { modResults } = modConfig;
// 競合するPodのバージョンを固定
modResults.contents = modResults.contents.replace(
/# Add any custom podfile content here/,
`# Add any custom podfile content here
# Force version to resolve conflicts
pod 'SomeLibrary', '~> 2.0.0'`
);
return modConfig;
});
};app.config.jsで適用する:
// app.config.js
export default {
expo: {
plugins: [
'./config-plugins/force-pod-version'
]
}
};Podfile.lockをリセットする手順:
# ローカルのprebuildディレクトリをクリーン
npx expo prebuild --clean
# iOSディレクトリのPodをリセット
cd ios && rm Podfile.lock && pod install --repo-update
cd ..
# 変更を確認してからEAS Buildに投入エラーパターン②:Config Pluginが見つからない、または適用されない
症状: ビルドは通るが、モジュールが実行時に Native module cannot be null や Cannot read property of undefined でクラッシュします。
これはConfig Pluginが正しく適用されず、ネイティブモジュールのinitializationコードがネイティブプロジェクトに書き込まれていないことが原因です。
// app.config.js の plugins 設定を確認する
export default {
expo: {
plugins: [
// ❌ 間違い:プラグインのパスや名前が違う
'react-native-ble-plx', // このライブラリはConfig Pluginがない場合がある
// ✅ 正しい:@config-plugins名前空間のパッケージを使う
'@config-plugins/react-native-ble-plx',
// ✅ 設定が必要なプラグインは配列で指定
['@config-plugins/react-native-ble-plx', {
bluetoothAlwaysPermission: "このアプリはBluetooth機器との通信に使用します",
bluetoothPeripheralPermission: "このアプリはBluetooth機器に接続します"
}],
]
}
};Config Pluginが適用されているか確認する方法:
# prebuildを実行してiosとandroidディレクトリを生成
npx expo prebuild --clean
# iOSのInfo.plistを確認(権限が追加されているか)
cat ios/MyApp/Info.plist | grep -A 2 "NSBluetooth"
# AndroidのAndroidManifest.xmlを確認
cat android/app/src/main/AndroidManifest.xml | grep "BLUETOOTH"エラーパターン③:AndroidのGradleビルドエラー
症状: iOSビルドは成功するが、Androidで Execution failed for task ':app:mergeReleaseJavaResource' などのエラーが出る。
# よくあるAndroidビルドエラーのパターン
# 1. 重複クラスエラー
# Duplicate class kotlin.collections.jdk8...
# 2. minimum SDK versionの不一致
# Manifest merger failed: uses-sdk:minSdkVersion 21 cannot be smaller than version 23
# build.gradle競合解決の例(Config Plugin経由で適用)// config-plugins/fix-android-build.js
const { withAppBuildGradle } = require('@expo/config-plugins');
module.exports = (config) => {
return withAppBuildGradle(config, (modConfig) => {
const { modResults } = modConfig;
// minSdkVersionを上げる(カスタムモジュールの要件に合わせる)
modResults.contents = modResults.contents.replace(
/minSdkVersion = \d+/,
'minSdkVersion = 24'
);
return modConfig;
});
};// Kotlin version競合の解決
const { withProjectBuildGradle } = require('@expo/config-plugins');
module.exports = (config) => {
return withProjectBuildGradle(config, (modConfig) => {
const { modResults } = modConfig;
modResults.contents = modResults.contents.replace(
/kotlinVersion = '.*'/,
"kotlinVersion = '1.9.0'"
);
return modConfig;
});
};エラーパターン④:Hermes JavaScriptエンジンとの非互換
症状: Calling synchronous methods on native modules is not supported や、特定のモジュールがNew Architectureで動かありません。
// app.config.js でHermesとNew Architectureの設定を確認
export default {
expo: {
jsEngine: "hermes", // デフォルト(推奨)
// New Architecture (Fabric + TurboModules) はOpt-in
newArchEnabled: false, // 互換性問題がある場合はfalseに戻す
ios: {
newArchEnabled: false, // iOS個別の設定
},
android: {
newArchEnabled: false, // Android個別の設定
}
}
};Hermes非対応モジュールの一時的な回避策:
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
// Hermes非互換モジュールをJSCで動かすための設定(最後の手段)
// 基本的にはモジュールのアップデートを待つことを推奨
config.resolver.assetExts.push('cjs');
module.exports = config;エラーパターン⑤:ネイティブモジュールのinitializationが漏れている
症状: MainApplication.java でモジュールが初期化されていないため実行時エラーになります。
古いReact Nativeライブラリの中には、Config Pluginを提供しておらず、MainApplication.javaやAppDelegate.mに手動でコードを追加する必要があるものがあります。
// config-plugins/add-native-init.js
const { withMainApplication, withAppDelegate } = require('@expo/config-plugins');
// Android: MainApplication.javaにモジュールを追加
const withCustomAndroidModule = (config) => {
return withMainApplication(config, (modConfig) => {
const { modResults } = modConfig;
// import文を追加
if (!modResults.contents.includes('import com.example.CustomModule;')) {
modResults.contents = modResults.contents.replace(
'import com.facebook.react.ReactApplication;',
`import com.facebook.react.ReactApplication;
import com.example.CustomModule;`
);
}
// getPackages()にモジュールを追加
modResults.contents = modResults.contents.replace(
'packages.add(new MainReactPackage());',
`packages.add(new MainReactPackage());
packages.add(new CustomModule());`
);
return modConfig;
});
};
// iOS: AppDelegate.mmに初期化コードを追加
const withCustomIOSInit = (config) => {
return withAppDelegate(config, (modConfig) => {
const { modResults } = modConfig;
if (!modResults.contents.includes('#import <CustomSDK/CustomSDK.h>')) {
modResults.contents = modResults.contents.replace(
'#import <React/RCTBridge.h>',
`#import <React/RCTBridge.h>
#import <CustomSDK/CustomSDK.h>`
);
modResults.contents = modResults.contents.replace(
'- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions',
`- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions`
);
}
return modConfig;
});
};
module.exports = (config) => {
config = withCustomAndroidModule(config);
config = withCustomIOSInit(config);
return config;
};EAS Buildの効率的なデバッグ戦略
本番ビルドのデバッグはコストが高いため、ローカルでできる限り問題を再現・解決してからEAS Buildに投入する習慣をつけましょう。
# 1. prebuildで設定を事前確認
npx expo prebuild --clean
# → ios/ と android/ ディレクトリが生成される
# → 期待通りのネイティブコードが書き込まれているか確認
# 2. ローカルビルドでのテスト(macOS + Xcodeが必要)
npx expo run:ios --configuration Release
# または
npx expo run:android --variant Release
# 3. 問題の特定後にEAS Buildに投入
eas build --profile preview --platform ios
# 4. ビルドログの確認
eas build:list # ビルド一覧
eas build:view [BUILD_ID] # 特定ビルドのログ詳細eas.jsonのマルチプロファイル設計:
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"buildConfiguration": "Debug"
}
},
"preview": {
"distribution": "internal",
"ios": {
"buildConfiguration": "Release"
},
"env": {
"API_URL": "https://staging.api.example.com"
}
},
"production": {
"distribution": "store",
"ios": {
"buildConfiguration": "Release"
},
"env": {
"API_URL": "https://api.example.com"
}
}
}
}全体を振り返って
EAS Buildでのカスタムネイティブモジュール導入でよくあるエラーと解決策:
- Podfile競合 → Config Plugin経由でバージョンを強制固定
- Config Plugin未適用 →
@config-plugins/名前空間パッケージを確認し、npx expo prebuildで事前検証 - Gradleビルドエラー → withAppBuildGradleでminSdkVersion等を調整
- Hermes非互換 →
newArchEnabled: falseでフォールバック(モジュール更新を待つ) - ネイティブinitializationの漏れ → withMainApplication/withAppDelegateで自作Config Pluginを作成
ポイントは「EAS Buildに投入する前にnpx expo prebuildでローカル検証する」という習慣です。ビルドエラーのほとんどは、prebuildが生成したネイティブファイルを直接確認することで原因が特定できます。
EAS BuildとConfig Pluginの詳細については、Expo公式ドキュメントが最も信頼性の高いリファレンスです。