Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ios & android: Add support for prebuilt modules #73

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,30 @@ echo "1" > www/NODEJS_MOBILE_BUILD_NATIVE_MODULES_VALUE.txt
cordova run ios
```

#### Prebuilds

The plugin also automatically detects the presence of prebuilt native modules, and **disables** compiling them on the fly. The prebuilds are detected as `.node` files in a specific path in the npm package:

```
nodejs-project/node_modules/<MODULE_NAME>/prebuilds/<PLATFORM>-<ARCH>/<NAME>.node
```

Notice `PLATFORM` and `ARCH`. The supported values are:

- PLATFORM = `android`
- ARCH = `arm`
- ARCH = `arm64`
- ARCH = `x64`
- PLATFORM = `ios`
- ARCH = `arm64`
- ARCH = `x64`

Compilation will then be forcefully disabled by hacking the `<MODULE_NAME>` folder to delete its `binding.gyp` and modify its `package.json` such that node-gyp will ignore this module for compilation.

If you are a maintainer of a native module and want to support prebuilds for nodejs-mobile, check out the CLI tool [prebuild-for-nodejs-mobile](https://github.com/nodejs-mobile/prebuild-for-nodejs-mobile/tree/master).

The plugin also provides you a helper script [rebuild-native-modules.sh](install/helper-scripts/rebuild-native-modules.sh) that can be used to prebuild third-party modules and speedup apps built time.

## Troubleshooting

### Android
Expand Down
125 changes: 125 additions & 0 deletions install/helper-scripts/ios-build-native-modules.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/bin/zsh

set -e

# On M1 macs homebrew is located outside /usr/local/bin
if [[ ! $PATH =~ /opt/homebrew/bin: ]]; then
PATH="/opt/homebrew/bin/:/opt/homebrew/sbin:$PATH"
fi
# Xcode executes script build phases in independant shell environment.
# Force load users configuration file
[ -f "$ZDOTDIR"/.zshrc ] && source "$ZDOTDIR"/.zshrc

NODEPROJ="$CODESIGNING_FOLDER_PATH/www/nodejs-project/"

if [ -z "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then
# If build native modules preference is not set, look for it in the project's
# www/NODEJS_MOBILE_BUILD_NATIVE_MODULES_VALUE.txt
PREFERENCE_FILE_PATH="$CODESIGNING_FOLDER_PATH/www/NODEJS_MOBILE_BUILD_NATIVE_MODULES_VALUE.txt"
if [ -f "$PREFERENCE_FILE_PATH" ]; then
NODEJS_MOBILE_BUILD_NATIVE_MODULES="$(cat $PREFERENCE_FILE_PATH | xargs)"
fi
fi
if [ -z "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then
# If build native modules preference is not set, try to find .gyp files
#to turn it on.
gypfiles=($(find "$NODEPROJ" -type f -name "*.gyp"))
if [ ${#gypfiles[@]} -gt 0 ]; then
NODEJS_MOBILE_BUILD_NATIVE_MODULES=1
else
NODEJS_MOBILE_BUILD_NATIVE_MODULES=0
fi
fi

if [ "1" != "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then exit 0; fi

# Delete object files that may already come from within the npm package.
find "$NODEPROJ" -name "*.o" -type f -delete
find "$NODEPROJ" -name "*.a" -type f -delete

# Function to skip compilation of a prebuilt module
preparePrebuiltModule()
{
local DOT_NODE_PATH="$1"
local DOT_NODE_FULL="$(cd "$(dirname -- "$DOT_NODE_PATH")" >/dev/null; pwd -P)/$(basename -- "$DOT_NODE_PATH")"
local MODULE_ROOT="$(cd $DOT_NODE_PATH && cd .. && cd .. && cd .. && pwd)"
local MODULE_NAME="$(basename $MODULE_ROOT)"
echo "Preparing to use the prebuild in $MODULE_NAME"
# Move the prebuild to the correct folder:
rm -rf $MODULE_ROOT/build
mkdir -p $MODULE_ROOT/build/Release
mv $DOT_NODE_FULL $MODULE_ROOT/build/Release/
# Hack the npm package to forcefully disable compile-on-install:
rm -rf $MODULE_ROOT/binding.gyp
sed -i.bak 's/"install"/"dontinstall"/g; s/"rebuild"/"dontrebuild"/g; s/"gypfile"/"dontgypfile"/g' $MODULE_ROOT/package.json
}

# Delete bundle contents that may be there from previous builds.
# Handle the special case where the module has a prebuild that we want to use
if [[ "$PLATFORM_PREFERRED_ARCH" == "arm64" ]]; then
PREBUILD_ARCH="arm64"
else
PREBUILD_ARCH="x64"
fi
if [[ "$PLATFORM_NAME" == "iphonesimulator" ]] && [[ "$NATIVE_ARCH" == "arm64" ]]; then
SUFFIX="-simulator"
PREBUILD_ARCH="arm64"
else
SUFFIX=""
fi
find -E "$NODEPROJ" \
! -regex ".*/prebuilds/ios-$PREBUILD_ARCH$SUFFIX" \
-regex '.*/prebuilds/[^/]*$' -type d \
-prune -exec rm -rf "{}" \;
find -E "$NODEPROJ" \
! -regex ".*/prebuilds/ios-$PREBUILD_ARCH$SUFFIX/.*\.node$" \
-name '*.node' -type f \
-exec rm "{}" \;
find "$NODEPROJ" \
-name "*.framework" -type d \
-prune -exec rm -rf "{}" \;
for DOT_NODE in `find -E "$NODEPROJ" -regex ".*/prebuilds/ios-$PREBUILD_ARCH$SUFFIX/.*\.node$" -type d`; do
preparePrebuiltModule "$DOT_NODE"
done

# Symlinks to binaries are resolved by cordova prepare during the copy, causing build time errors.
# The original project's .bin folder will be added to the path before building the native modules.
find "$NODEPROJ" -path "*/.bin/*" -delete
find "$NODEPROJ" -name ".bin" -type d -delete
# Get the nodejs-mobile-gyp location
if [ -d "$PROJECT_DIR/../../plugins/@red-mobile/nodejs-mobile-cordova/node_modules/nodejs-mobile-gyp/" ]; then
NODEJS_MOBILE_GYP_DIR="$( cd "$PROJECT_DIR" && cd ../../plugins/@red-mobile/nodejs-mobile-cordova/node_modules/nodejs-mobile-gyp/ && pwd )"
else
NODEJS_MOBILE_GYP_DIR="$( cd "$PROJECT_DIR" && cd ../../node_modules/nodejs-mobile-gyp/ && pwd )"
fi
NODEJS_MOBILE_GYP_BIN_FILE="$NODEJS_MOBILE_GYP_DIR"/bin/node-gyp.js
# Rebuild modules with right environment
NODEJS_HEADERS_DIR="$( cd "$( dirname "$PRODUCT_SETTINGS_PATH" )" && cd Plugins/@red-mobile/nodejs-mobile-cordova/ && pwd )"
# Adds the original project .bin to the path. It's a workaround
# to correctly build some modules that depend on symlinked modules,
# like node-pre-gyp.
if [ -d "$PROJECT_DIR/../../www/nodejs-project/node_modules/.bin/" ]; then
PATH="$PROJECT_DIR/../../www/nodejs-project/node_modules/.bin/:$PATH"
fi

pushd $NODEPROJ
export GYP_DEFINES="OS=ios"
export npm_config_nodedir="$NODEJS_HEADERS_DIR"
export npm_config_node_gyp="$NODEJS_MOBILE_GYP_BIN_FILE"
export npm_config_format="make-ios"
export npm_config_node_engine="chakracore"
export NODEJS_MOBILE_GYP="$NODEJS_MOBILE_GYP_BIN_FILE"
export npm_config_platform="ios"

if [[ "$PLATFORM_NAME" == "iphoneos" ]]; then
export npm_config_arch="arm64"
else
if [[ "$HOST_ARCH" == "arm64" ]] ; then # M1 mac
export GYP_DEFINES="OS=ios iossim=true"
export npm_config_arch="arm64"
else
export npm_config_arch="x64"
fi
fi
npm --verbose rebuild --build-from-source
popd
33 changes: 33 additions & 0 deletions install/helper-scripts/ios-sign-native-modules.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/zsh

set -e

# On M1 macs homebrew is located outside /usr/local/bin
if [[ ! $PATH =~ /opt/homebrew/bin: ]]; then
PATH="/opt/homebrew/bin/:/opt/homebrew/sbin:$PATH"
fi
# Xcode executes script build phases in independant shell environment.
# Force load users configuration file
[ -f "$ZDOTDIR"/.zshrc ] && source "$ZDOTDIR"/.zshrc

# Delete object files
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name "*.o" -type f -delete
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name "*.a" -type f -delete

# Create Info.plist for each framework built and loader override.
PATCH_SCRIPT_DIR="$( cd "$PROJECT_DIR" && cd ../../Plugins/@red-mobile/nodejs-mobile-cordova/install/helper-scripts/ && pwd )"
NODEJS_PROJECT_DIR="$( cd "$CODESIGNING_FOLDER_PATH" && cd www/nodejs-project && pwd )"
node "$PATCH_SCRIPT_DIR"/ios-create-plists-and-dlopen-override.js $NODEJS_PROJECT_DIR
# Embed every resulting .framework in the application and delete them afterwards.
embed_framework()
{
FRAMEWORK_NAME="$(basename "$1")"
mkdir -p "$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/"
cp -r "$1" "$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/"
/usr/bin/codesign --force --sign $EXPANDED_CODE_SIGN_IDENTITY --preserve-metadata=identifier,entitlements,flags --timestamp=none "$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/$FRAMEWORK_NAME"
}
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name "*.framework" -type d | while read frmwrk_path; do embed_framework "$frmwrk_path"; done

#Delete gyp temporary .deps dependency folders from the project structure.
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -path "*/.deps/*" -delete
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name ".deps" -type d -delete
35 changes: 35 additions & 0 deletions install/helper-scripts/rebuild-native-modules.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/zsh

# Delete object files that may already come from within the npm package.
find "nodejs-project" -name "*.o" -type f -delete
find "nodejs-project" -name "*.a" -type f -delete
find "nodejs-project" -name "*.node" -type f -delete

# Delete bundle contents that may be there from previous builds.
find "nodejs-project" -path "*/*.node/*" -delete
find "nodejs-project" -name "*.node" -type d -delete

NATIVE_MODULES=($(find "nodejs-project" -type f -name "binding.gyp" | sed -E 's|/[^/]+$||' | sort -u))

echo "Found ${#NATIVE_MODULES[@]} native modules"

for module in "${NATIVE_MODULES[@]}"
do
pushd "$module"
echo "Building $(basename $module) for iOS devices"
npx prebuild-for-nodejs-mobile ios-arm64
echo "Building $(basename $module) for iOS simulator"
if [[ $(uname -m) == 'arm64' ]] # if M1 mac
then
npx prebuild-for-nodejs-mobile ios-arm64-simulator
else
npx prebuild-for-nodejs-mobile ios-x64-simulator
fi
echo "Building $(basename $module) for Android arm64"
npx prebuild-for-nodejs-mobile android-arm64
echo "Building $(basename $module) for Android arm"
npx prebuild-for-nodejs-mobile android-arm
echo "Building $(basename $module) for Android x86"
npx prebuild-for-nodejs-mobile android-x64
popd
done
10 changes: 10 additions & 0 deletions install/hooks/both/after-plugin-install-copy-helper-scripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const fs = require('fs');
const path = require('path');

module.exports = function(context)
{
fs.copyFileSync(
path.join(context.opts.plugin.dir, "install/helper-scripts/rebuild-native-modules.sh"),
path.join(context.opts.projectRoot, "rebuild-native-modules.sh")
)
}
141 changes: 2 additions & 139 deletions install/hooks/ios/after-plugin-install.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,87 +17,7 @@ module.exports = function(context) {

// Adds a build phase to rebuild native modules.
var rebuildNativeModulesBuildPhaseName = 'Build Node.js Mobile Native Modules';
var rebuildNativeModulesBuildPhaseScript = `
set -e

# On M1 macs homebrew is located outside /usr/local/bin
if [[ ! $PATH =~ /opt/homebrew/bin: ]]; then
PATH="/opt/homebrew/bin/:/opt/homebrew/sbin:$PATH"
fi
# Xcode executes script build phases in independant shell environment.
# Force load users configuration file
[ -f "$ZDOTDIR"/.zshrc ] && source "$ZDOTDIR"/.zshrc

if [ -z "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then
# If build native modules preference is not set, look for it in the project's
# www/NODEJS_MOBILE_BUILD_NATIVE_MODULES_VALUE.txt
PREFERENCE_FILE_PATH="$CODESIGNING_FOLDER_PATH/www/NODEJS_MOBILE_BUILD_NATIVE_MODULES_VALUE.txt"
if [ -f "$PREFERENCE_FILE_PATH" ]; then
NODEJS_MOBILE_BUILD_NATIVE_MODULES="$(cat $PREFERENCE_FILE_PATH | xargs)"
fi
fi
if [ -z "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then
# If build native modules preference is not set, try to find .gyp files
#to turn it on.
gypfiles=($(find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -type f -name "*.gyp"))
if [ \${#gypfiles[@]} -gt 0 ]; then
NODEJS_MOBILE_BUILD_NATIVE_MODULES=1
else
NODEJS_MOBILE_BUILD_NATIVE_MODULES=0
fi
fi
if [ "1" != "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then exit 0; fi
# Delete object files that may already come from within the npm package.
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name "*.o" -type f -delete
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name "*.a" -type f -delete
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name "*.node" -type f -delete
# Delete bundle contents that may be there from previous builds.
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -path "*/*.node/*" -delete
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name "*.node" -type d -delete
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -path "*/*.framework/*" -delete
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name "*.framework" -type d -delete
# Symlinks to binaries are resolved by cordova prepare during the copy, causing build time errors.
# The original project's .bin folder will be added to the path before building the native modules.
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -path "*/.bin/*" -delete
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name ".bin" -type d -delete
# Get the nodejs-mobile-gyp location
if [ -d "$PROJECT_DIR/../../plugins/@red-mobile/nodejs-mobile-cordova/node_modules/nodejs-mobile-gyp/" ]; then
NODEJS_MOBILE_GYP_DIR="$( cd "$PROJECT_DIR" && cd ../../plugins/@red-mobile/nodejs-mobile-cordova/node_modules/nodejs-mobile-gyp/ && pwd )"
else
NODEJS_MOBILE_GYP_DIR="$( cd "$PROJECT_DIR" && cd ../../node_modules/nodejs-mobile-gyp/ && pwd )"
fi
NODEJS_MOBILE_GYP_BIN_FILE="$NODEJS_MOBILE_GYP_DIR"/bin/node-gyp.js
# Rebuild modules with right environment
NODEJS_HEADERS_DIR="$( cd "$( dirname "$PRODUCT_SETTINGS_PATH" )" && cd Plugins/@red-mobile/nodejs-mobile-cordova/ && pwd )"
# Adds the original project .bin to the path. It's a workaround
# to correctly build some modules that depend on symlinked modules,
# like node-pre-gyp.
if [ -d "$PROJECT_DIR/../../www/nodejs-project/node_modules/.bin/" ]; then
PATH="$PROJECT_DIR/../../www/nodejs-project/node_modules/.bin/:$PATH"
fi

pushd $CODESIGNING_FOLDER_PATH/www/nodejs-project/
export GYP_DEFINES="OS=ios"
export npm_config_nodedir="$NODEJS_HEADERS_DIR"
export npm_config_node_gyp="$NODEJS_MOBILE_GYP_BIN_FILE"
export npm_config_format="make-ios"
export npm_config_node_engine="chakracore"
export NODEJS_MOBILE_GYP="$NODEJS_MOBILE_GYP_BIN_FILE"
export npm_config_platform="ios"

if [[ "$PLATFORM_NAME" == "iphoneos" ]]; then
export npm_config_arch="arm64"
else
if [[ "$HOST_ARCH" == "arm64" ]] ; then # M1 mac
export GYP_DEFINES="OS=ios iossim=true"
export npm_config_arch="arm64"
else
export npm_config_arch="x64"
fi
fi
npm --verbose rebuild --build-from-source
popd
`
var rebuildNativeModulesBuildPhaseScript = 'zsh "$PROJECT_DIR/../../plugins/@red-mobile/nodejs-mobile-cordova/install/helper-scripts/ios-build-native-modules.sh"'
var rebuildNativeModulesBuildPhase = xcodeProject.buildPhaseObject('PBXShellScriptBuildPhase', rebuildNativeModulesBuildPhaseName, firstTargetUUID);
if (!(rebuildNativeModulesBuildPhase)) {
xcodeProject.addBuildPhase(
Expand All @@ -111,63 +31,7 @@ popd

// Adds a build phase to sign native modules.
var signNativeModulesBuildPhaseName = 'Sign Node.js Mobile Native Modules';
var signNativeModulesBuildPhaseScript = `
set -e

# On M1 macs homebrew is located outside /usr/local/bin
if [[ ! $PATH =~ /opt/homebrew/bin: ]]; then
PATH="/opt/homebrew/bin/:/opt/homebrew/sbin:$PATH"
fi
# Xcode executes script build phases in independant shell environment.
# Force load users configuration file
[ -f "$ZDOTDIR"/.zshrc ] && source "$ZDOTDIR"/.zshrc

if [ -z "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then
# If build native modules preference is not set, look for it in the project's
# www/NODEJS_MOBILE_BUILD_NATIVE_MODULES_VALUE.txt
PREFERENCE_FILE_PATH="$CODESIGNING_FOLDER_PATH/www/NODEJS_MOBILE_BUILD_NATIVE_MODULES_VALUE.txt"
if [ -f "$PREFERENCE_FILE_PATH" ]; then
NODEJS_MOBILE_BUILD_NATIVE_MODULES="$(cat $PREFERENCE_FILE_PATH | xargs)"
# Remove the preference file so it doesn't get in the application package.
rm "$PREFERENCE_FILE_PATH"
fi
fi
if [ -z "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then
# If build native modules preference is not set, try to find .gyp files
#to turn it on.
gypfiles=($(find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -type f -name "*.gyp"))
if [ \${#gypfiles[@]} -gt 0 ]; then
NODEJS_MOBILE_BUILD_NATIVE_MODULES=1
else
NODEJS_MOBILE_BUILD_NATIVE_MODULES=0
fi
fi
if [ "1" != "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then exit 0; fi
# Delete object files
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name "*.o" -type f -delete
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name "*.a" -type f -delete
# Create Info.plist for each framework built and loader override.
PATCH_SCRIPT_DIR="$( cd "$PROJECT_DIR" && cd ../../Plugins/@red-mobile/nodejs-mobile-cordova/install/helper-scripts/ && pwd )"
NODEJS_PROJECT_DIR="$( cd "$CODESIGNING_FOLDER_PATH" && cd www/nodejs-project/ && pwd )"
node "$PATCH_SCRIPT_DIR"/ios-create-plists-and-dlopen-override.js $NODEJS_PROJECT_DIR
# Embed every resulting .framework in the application and delete them afterwards.
embed_framework()
{
FRAMEWORK_NAME="$(basename "$1")"
mkdir -p "$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/"
cp -r "$1" "$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/"
/usr/bin/codesign --force --sign $EXPANDED_CODE_SIGN_IDENTITY --preserve-metadata=identifier,entitlements,flags --timestamp=none "$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/$FRAMEWORK_NAME"
}
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name "*.framework" -type d | while read frmwrk_path; do embed_framework "$frmwrk_path"; done

#Delete gyp temporary .deps dependency folders from the project structure.
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -path "*/.deps/*" -delete
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name ".deps" -type d -delete

#Delete frameworks from their build paths
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -path "*/*.framework/*" -delete
find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name "*.framework" -type d -delete
`
var signNativeModulesBuildPhaseScript = 'zsh "$PROJECT_DIR/../../plugins/@red-mobile/nodejs-mobile-cordova/install/helper-scripts/ios-sign-native-modules.sh"'
var signNativeModulesBuildPhase = xcodeProject.buildPhaseObject('PBXShellScriptBuildPhase', signNativeModulesBuildPhaseName, firstTargetUUID);
if (!(signNativeModulesBuildPhase)) {
xcodeProject.addBuildPhase(
Expand All @@ -181,5 +45,4 @@ find "$CODESIGNING_FOLDER_PATH/www/nodejs-project/" -name "*.framework" -type d

// Write the changes into the Xcode project.
fs.writeFileSync(pbxprojPath, xcodeProject.writeSync());

}
Loading