Skip to content

Commit

Permalink
ios: build native modules as frameworks
Browse files Browse the repository at this point in the history
Changes the native modules build steps to integrate them as framework
bundles containing a MH_DYLIB binary, so they're accepted in the App
Store. Generates a list of the native modules and their framework
equivalence at build time. Overrides dlopen to load the modules from
their framework file using a preloaded module.
  • Loading branch information
jaimecbernardo committed Jun 18, 2018
1 parent 89ce6b3 commit 5f5447d
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 19 deletions.
54 changes: 43 additions & 11 deletions ios/RNNodeJsMobile.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
134 changes: 134 additions & 0 deletions scripts/ios-create-plists-and-dlopen-override.js
Original file line number Diff line number Diff line change
@@ -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);
}
44 changes: 37 additions & 7 deletions scripts/module-postlink.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand All @@ -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
`
Expand Down Expand Up @@ -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) {
Expand Down
27 changes: 27 additions & 0 deletions scripts/override-dlopen-paths-preload.js
Original file line number Diff line number Diff line change
@@ -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);
}
}


57 changes: 57 additions & 0 deletions scripts/plisttemplate.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>{ENV_MAC_OS_X_PRODUCT_BUILD_VERSION}</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>{VAR_BINARY_NAME}</string>
<key>CFBundleIdentifier</key>
<string>com.janeasystems.{VAR_BINARY_NAME}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>{VAR_BINARY_NAME}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>DTCompiler</key>
<string>{ENV_DEFAULT_COMPILER}</string>
<key>DTPlatformBuild</key>
<string>{ENV_PLATFORM_PRODUCT_BUILD_VERSION}</string>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>DTPlatformVersion</key>
<string>{ENV_SDK_VERSION}</string>
<key>DTSDKBuild</key>
<string>{ENV_SDK_PRODUCT_BUILD_VERSION}</string>
<key>DTSDKName</key>
<string>{ENV_SDK_NAME}</string>
<key>DTXcode</key>
<string>{ENV_XCODE_VERSION_ACTUAL}</string>
<key>DTXcodeBuild</key>
<string>{ENV_XCODE_PRODUCT_BUILD_VERSION}</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<key>NSHumanReadableCopyright</key>
<string></string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
</dict>
</plist>

0 comments on commit 5f5447d

Please sign in to comment.