diff --git a/ios/RNNodeJsMobile.m b/ios/RNNodeJsMobile.m index 18e5038..5e03772 100644 --- a/ios/RNNodeJsMobile.m +++ b/ios/RNNodeJsMobile.m @@ -8,6 +8,7 @@ @implementation RNNodeJsMobile NSString* const BUILTIN_MODULES_RESOURCE_PATH = @"builtin_modules"; NSString* const NODEJS_PROJECT_RESOURCE_PATH = @"nodejs-project"; +NSString* const NODEJS_DLOPEN_OVERRIDE_FILENAME = @"override-dlopen-paths-preload.js"; NSString* nodePath; @synthesize bridge = _bridge; @@ -49,23 +50,54 @@ - (id)init -(void)callStartNodeWithScript:(NSString *)script { - NSArray* nodeArguments = [NSArray arrayWithObjects: - @"node", - @"-e", - script, - nil - ]; + NSArray* nodeArguments = nil; + + NSString* dlopenoverridePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@/%@", NODEJS_PROJECT_RESOURCE_PATH, NODEJS_DLOPEN_OVERRIDE_FILENAME] ofType:@""]; + // Check if the file to override dlopen lookup exists, for loading native modules from the Frameworks. + if(!dlopenoverridePath) + { + nodeArguments = [NSArray arrayWithObjects: + @"node", + @"-e", + script, + nil + ]; + } else { + nodeArguments = [NSArray arrayWithObjects: + @"node", + @"-r", + dlopenoverridePath, + @"-e", + script, + nil + ]; + } [[NodeRunner sharedInstance] startEngineWithArguments:nodeArguments:nodePath]; } -(void)callStartNodeProject:(NSString *)mainFileName { NSString* srcPath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@/%@", NODEJS_PROJECT_RESOURCE_PATH, mainFileName] ofType:@""]; - NSArray* nodeArguments = [NSArray arrayWithObjects: - @"node", - srcPath, - nil - ]; + NSArray* nodeArguments = nil; + + NSString* dlopenoverridePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@/%@", NODEJS_PROJECT_RESOURCE_PATH, NODEJS_DLOPEN_OVERRIDE_FILENAME] ofType:@""]; + // Check if the file to override dlopen lookup exists, for loading native modules from the Frameworks. + if(!dlopenoverridePath) + { + nodeArguments = [NSArray arrayWithObjects: + @"node", + srcPath, + nil + ]; + } else { + nodeArguments = [NSArray arrayWithObjects: + @"node", + @"-r", + dlopenoverridePath, + srcPath, + nil + ]; + } [[NodeRunner sharedInstance] startEngineWithArguments:nodeArguments:nodePath]; } diff --git a/package.json b/package.json index c0611fb..86b700d 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "react-native": ">=0.52.0" }, "dependencies": { - "nodejs-mobile-gyp": "^0.1.0", + "nodejs-mobile-gyp": "^0.2.0", "ncp": "^2.0.0", "mkdirp": "^0.5.1", "xcode": "^0.9.3" diff --git a/scripts/ios-create-plists-and-dlopen-override.js b/scripts/ios-create-plists-and-dlopen-override.js new file mode 100644 index 0000000..5e5fd22 --- /dev/null +++ b/scripts/ios-create-plists-and-dlopen-override.js @@ -0,0 +1,134 @@ +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const { spawnSync } = require('child_process'); + +function visitEveryFramework(projectPath) { + var foundFrameworks = []; + var countInvalidFrameworks = 0; + var countValidFrameworks = 0; + function recursivelyFindFrameworks(currentPath) { + let currentFiles = fs.readdirSync(currentPath); + for (let i = 0; i < currentFiles.length; i++) { + let currentFilename = path.normalize(path.join(currentPath,currentFiles[i])); + if (fs.lstatSync(currentFilename).isDirectory()) { + if (currentFilename.endsWith(".node")) { + let frameworkContents = fs.readdirSync(currentFilename); + // Frameworks output by nodejs-mobile-gyp are expected to have only one file inside, corresponding to the proper shared library. + if (frameworkContents.length != 1) { + console.log('Skipping a ".node". Expected to find only one file inside this path: ' + currentFilename); + countInvalidFrameworks++; + } else { + let currentBinaryName = frameworkContents[0]; + let checkFileType = spawnSync('file', [path.join(currentFilename,currentBinaryName)]); + // File inside a .framework should be a dynamically linked shared library. + if(checkFileType.stdout.toString().indexOf('dynamically linked shared library') > -1) + { + let newFrameworkObject = { + originalFileName: currentFilename, + originalRelativePath: '', + originalBinaryName: currentBinaryName, + newFrameworkName: '', + newFrameworkFileName: '' + }; + foundFrameworks.push(newFrameworkObject); + countValidFrameworks++; + } else { + console.log('Skipping a ".node". Couldn\'t find a dynamically linked shared library inside ' + currentFilename); + countInvalidFrameworks++; + } + } + } + recursivelyFindFrameworks(currentFilename); + } + } + } + recursivelyFindFrameworks(projectPath); + + console.log("Found " + countValidFrameworks + " valid frameworks and " + countInvalidFrameworks + " invalid frameworks after rebuilding the native modules for iOS."); + if (foundFrameworks.length<1) { + console.log("No valid framework native modules were found. Skipping integrating them into the App."); + return; + } + + for (let i = 0; i < foundFrameworks.length; i++) { + // Fill the helper fields for each framework. + let currentFramework = foundFrameworks[i]; + currentFramework.originalRelativePath = path.relative(projectPath,currentFramework.originalFileName); + + // To make each framework name unique while embedding, use a digest of the relative path. + let hash = crypto.createHash('sha1'); + hash.update(currentFramework.originalRelativePath); + currentFramework.newFrameworkName = 'node' + hash.digest('hex'); + currentFramework.newFrameworkFileName = path.join(path.dirname(currentFramework.originalFileName),currentFramework.newFrameworkName+'.framework'); + } + + for (let i = 0; i < foundFrameworks.length; i++) { + // Rename the binaries to the new framework structure and add a .plist + let currentFramework = foundFrameworks[i]; + fs.renameSync(currentFramework.originalFileName, currentFramework.newFrameworkFileName); + fs.renameSync( + path.join(currentFramework.newFrameworkFileName,currentFramework.originalBinaryName), + path.join(currentFramework.newFrameworkFileName,currentFramework.newFrameworkName) + ); + + // Read template Info.plist + let plistXmlContents = fs.readFileSync(path.join(__dirname,'plisttemplate.xml')).toString(); + + // Replace values with the new bundle name and XCode environment variables. + plistXmlContents = plistXmlContents + .replace(/\{ENV_MAC_OS_X_PRODUCT_BUILD_VERSION\}/g, process.env.MAC_OS_X_PRODUCT_BUILD_VERSION) + .replace(/\{VAR_BINARY_NAME\}/g, currentFramework.newFrameworkName) + .replace(/\{ENV_DEFAULT_COMPILER\}/g, process.env.DEFAULT_COMPILER) + .replace(/\{ENV_PLATFORM_PRODUCT_BUILD_VERSION\}/g, process.env.PLATFORM_PRODUCT_BUILD_VERSION) + .replace(/\{ENV_SDK_VERSION\}/g, process.env.SDK_VERSION) + .replace(/\{ENV_SDK_PRODUCT_BUILD_VERSION\}/g, process.env.SDK_PRODUCT_BUILD_VERSION) + .replace(/\{ENV_SDK_NAME\}/g, process.env.SDK_NAME) + .replace(/\{ENV_XCODE_VERSION_ACTUAL\}/g, process.env.XCODE_VERSION_ACTUAL) + .replace(/\{ENV_XCODE_PRODUCT_BUILD_VERSION\}/g, process.env.XCODE_PRODUCT_BUILD_VERSION); + + // Use plutil to generate the plist in the binary format. + let plistGeneration = spawnSync('plutil',[ + '-convert', + 'binary1', // Will convert the xml plist to binary. + '-o', + path.join(currentFramework.newFrameworkFileName,'Info.plist'), // target Info.plist path. + '-' // read the input from the process stdin. + ], { + input: plistXmlContents + }); + } + + var frameworkOverrideContents = [] + for (let i = 0; i < foundFrameworks.length; i++) { + // Generate the contents of a JSON file for overriding dlopen calls at runtime. + let currentFramework = foundFrameworks[i]; + frameworkOverrideContents.push( + { + originalpath: currentFramework.originalRelativePath.split(path.sep), + newpath: ['..', 'Frameworks', currentFramework.newFrameworkName+'.framework', currentFramework.newFrameworkName] + } + ); + } + fs.writeFileSync(path.join(projectPath, 'override-dlopen-paths-data.json'), JSON.stringify(frameworkOverrideContents)); + + // Copy runtime script that will override dlopen paths. + fs.copyFileSync(path.join(__dirname,'override-dlopen-paths-preload.js'),path.join(projectPath,'override-dlopen-paths-preload.js')); + + for (let i = 0; i < foundFrameworks.length; i++) { + // Put an empty file in each of the .node original locations, since some modules check their existence. + fs.closeSync(fs.openSync(foundFrameworks[i].originalFileName, 'w')); + } + +} + + +if (process.argv.length >=3) { + if (fs.existsSync(process.argv[2])) { + visitEveryFramework(path.normalize(process.argv[2])); + } + process.exit(0); +} else { + console.error("A path is expected as an argument."); + process.exit(1); +} diff --git a/scripts/module-postlink.js b/scripts/module-postlink.js index 873f6c3..8f52610 100644 --- a/scripts/module-postlink.js +++ b/scripts/module-postlink.js @@ -32,7 +32,7 @@ function getProjectConfig() { return returnObj; } - // Adds a custom function to remove script build phases, which is not supported in the xcode module yet. + // Adds a custom function to remove script build phases, which is not supported in the xcode module. xcode.project.prototype.myRemovePbxScriptBuildPhase = function (buildPhaseName, target) { var buildPhaseTargetUuid = target || this.getFirstTarget().uuid; @@ -193,10 +193,21 @@ else 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/nodejs-project/" -name "*.o" -type f -delete +find "$CODESIGNING_FOLDER_PATH/nodejs-project/" -name "*.a" -type f -delete +find "$CODESIGNING_FOLDER_PATH/nodejs-project/" -name "*.node" -type f -delete +# Delete bundle contents that may be there from previous builds. +find "$CODESIGNING_FOLDER_PATH/nodejs-project/" -path "*/*.node/*" -delete +find "$CODESIGNING_FOLDER_PATH/nodejs-project/" -name "*.node" -type d -delete +find "$CODESIGNING_FOLDER_PATH/nodejs-project/" -path "*/*.framework/*" -delete +find "$CODESIGNING_FOLDER_PATH/nodejs-project/" -name "*.framework" -type d -delete # Apply patches to the modules package.json -PATCH_SCRIPT_DIR="$( cd "$PROJECT_DIR" && cd ../node_modules/nodejs-mobile-react-native/scripts/ && pwd )" -NODEJS_PROJECT_MODULES_DIR="$( cd "$CODESIGNING_FOLDER_PATH" && cd nodejs-project/node_modules/ && pwd )" -node "$PATCH_SCRIPT_DIR"/patch-package.js $NODEJS_PROJECT_MODULES_DIR +if [ -d "$CODESIGNING_FOLDER_PATH"/nodejs-project/node_modules/ ]; then + PATCH_SCRIPT_DIR="$( cd "$PROJECT_DIR" && cd ../node_modules/nodejs-mobile-react-native/scripts/ && pwd )" + NODEJS_PROJECT_MODULES_DIR="$( cd "$CODESIGNING_FOLDER_PATH" && cd nodejs-project/node_modules/ && pwd )" + node "$PATCH_SCRIPT_DIR"/patch-package.js $NODEJS_PROJECT_MODULES_DIR +fi # Get the nodejs-mobile-gyp location NODEJS_MOBILE_GYP_DIR="$( cd "$PROJECT_DIR" && cd ../node_modules/nodejs-mobile-gyp/ && pwd )" NODEJS_MOBILE_GYP_BIN_FILE="$NODEJS_MOBILE_GYP_DIR"/bin/node-gyp.js @@ -205,9 +216,9 @@ NODEJS_HEADERS_DIR="$( cd "$PROJECT_DIR" && cd ../node_modules/nodejs-mobile-rea pushd $CODESIGNING_FOLDER_PATH/nodejs-project/ if [ "$PLATFORM_NAME" == "iphoneos" ] then - GYP_DEFINES="OS=ios" npm_config_nodedir="$NODEJS_HEADERS_DIR" npm_config_node_gyp="$NODEJS_MOBILE_GYP_BIN_FILE" npm_config_platform="ios" npm_config_node_engine="chakracore" npm_config_arch="arm64" npm --verbose rebuild --build-from-source + GYP_DEFINES="OS=ios" npm_config_nodedir="$NODEJS_HEADERS_DIR" npm_config_node_gyp="$NODEJS_MOBILE_GYP_BIN_FILE" npm_config_platform="ios" npm_config_format="make-ios" npm_config_node_engine="chakracore" npm_config_arch="arm64" npm --verbose rebuild --build-from-source else - GYP_DEFINES="OS=ios" npm_config_nodedir="$NODEJS_HEADERS_DIR" npm_config_node_gyp="$NODEJS_MOBILE_GYP_BIN_FILE" npm_config_platform="ios" npm_config_node_engine="chakracore" npm_config_arch="x64" npm --verbose rebuild --build-from-source + GYP_DEFINES="OS=ios" npm_config_nodedir="$NODEJS_HEADERS_DIR" npm_config_node_gyp="$NODEJS_MOBILE_GYP_BIN_FILE" npm_config_platform="ios" npm_config_format="make-ios" npm_config_node_engine="chakracore" npm_config_arch="x64" npm --verbose rebuild --build-from-source fi popd ` @@ -239,7 +250,26 @@ else fi fi if [ "1" != "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then exit 0; fi -/usr/bin/codesign --force --sign $EXPANDED_CODE_SIGN_IDENTITY --preserve-metadata=identifier,entitlements,flags --timestamp=none $(find "$CODESIGNING_FOLDER_PATH/nodejs-project/" -type f -name "*.node") +# Delete object files +find "$CODESIGNING_FOLDER_PATH/nodejs-project/" -name "*.o" -type f -delete +find "$CODESIGNING_FOLDER_PATH/nodejs-project/" -name "*.a" -type f -delete +# Create Info.plist for each framework built and loader override. +PATCH_SCRIPT_DIR="$( cd "$PROJECT_DIR" && cd ../node_modules/nodejs-mobile-react-native/scripts/ && pwd )" +NODEJS_PROJECT_DIR="$( cd "$CODESIGNING_FOLDER_PATH" && cd 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")" + 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/nodejs-project/" -name "*.framework" -type d | while read frmwrk_path; do embed_framework "$frmwrk_path"; done + +#Delete frameworks from their build paths +find "$CODESIGNING_FOLDER_PATH/nodejs-project/" -path "*/*.framework/*" -delete +find "$CODESIGNING_FOLDER_PATH/nodejs-project/" -name "*.framework" -type d -delete ` var signNativeModulesBuildPhase = xcodeProject.buildPhaseObject('PBXShellScriptBuildPhase', signNativeModulesBuildPhaseName, firstTargetUUID); if (signNativeModulesBuildPhase) { diff --git a/scripts/override-dlopen-paths-preload.js b/scripts/override-dlopen-paths-preload.js new file mode 100644 index 0000000..d8f6b56 --- /dev/null +++ b/scripts/override-dlopen-paths-preload.js @@ -0,0 +1,27 @@ +var fs = require('fs'); +var path = require('path'); + +var substitutionDataFile = path.join(__dirname,'override-dlopen-paths-data.json'); +// If the json file exists, override dlopen to load the specified framework paths instead. +if (fs.existsSync(substitutionDataFile)) { + var pathSubstitutionData = JSON.parse(fs.readFileSync(substitutionDataFile, 'utf8')); + + var pathSubstitutionDictionary = {}; + // Build a dictionary to convert paths at runtime, taking current sandboxed paths into account. + for (let i = 0; i < pathSubstitutionData.length; i++) { + pathSubstitutionDictionary[ + path.normalize(path.join.apply(null, [__dirname].concat(pathSubstitutionData[i].originalpath))) + ] = path.normalize(path.join.apply(null, [__dirname].concat(pathSubstitutionData[i].newpath))); + } + + var old_dlopen = process.dlopen; + // Override process.dlopen + process.dlopen = function(_module, _filename) { + if( pathSubstitutionDictionary[path.normalize(_filename)] ) { + _filename = pathSubstitutionDictionary[path.normalize(_filename)]; + } + old_dlopen(_module,_filename); + } +} + + diff --git a/scripts/plisttemplate.xml b/scripts/plisttemplate.xml new file mode 100644 index 0000000..a204661 --- /dev/null +++ b/scripts/plisttemplate.xml @@ -0,0 +1,57 @@ + + + + + BuildMachineOSBuild + {ENV_MAC_OS_X_PRODUCT_BUILD_VERSION} + CFBundleDevelopmentRegion + en + CFBundleExecutable + {VAR_BINARY_NAME} + CFBundleIdentifier + com.janeasystems.{VAR_BINARY_NAME} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + {VAR_BINARY_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSupportedPlatforms + + iPhoneOS + + CFBundleVersion + 1 + DTCompiler + {ENV_DEFAULT_COMPILER} + DTPlatformBuild + {ENV_PLATFORM_PRODUCT_BUILD_VERSION} + DTPlatformName + iphoneos + DTPlatformVersion + {ENV_SDK_VERSION} + DTSDKBuild + {ENV_SDK_PRODUCT_BUILD_VERSION} + DTSDKName + {ENV_SDK_NAME} + DTXcode + {ENV_XCODE_VERSION_ACTUAL} + DTXcodeBuild + {ENV_XCODE_PRODUCT_BUILD_VERSION} + MinimumOSVersion + 9.0 + NSHumanReadableCopyright + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + +