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 20, 2018
1 parent 39e8ee5 commit 6cd38b0
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 17 deletions.
134 changes: 134 additions & 0 deletions install/helper-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);
}
25 changes: 25 additions & 0 deletions install/helper-scripts/override-dlopen-paths-preload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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 install/helper-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>
37 changes: 34 additions & 3 deletions install/hooks/ios/after-plugin-install.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ if [ -z "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then
NODEJS_MOBILE_BUILD_NATIVE_MODULES=0
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
# Get the nodejs-mobile-gyp location
if [ -d "$PROJECT_DIR/../../plugins/nodejs-mobile-cordova/node_modules/nodejs-mobile-gyp/" ]; then
NODEJS_MOBILE_GYP_DIR="$( cd "$PROJECT_DIR" && cd ../../plugins/nodejs-mobile-cordova/node_modules/nodejs-mobile-gyp/ && pwd )"
Expand All @@ -35,9 +44,9 @@ NODEJS_HEADERS_DIR="$( cd "$( dirname "$PRODUCT_SETTINGS_PATH" )" && cd Plugins/
pushd $CODESIGNING_FOLDER_PATH/www/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 All @@ -60,7 +69,29 @@ if [ -z "$NODEJS_MOBILE_BUILD_NATIVE_MODULES" ]; then
NODEJS_MOBILE_BUILD_NATIVE_MODULES=0
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/www/nodejs-project/" -type f -name "*.node")
# 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/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")"
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 signNativeModulesBuildPhase = xcodeProject.buildPhaseObject('PBXShellScriptBuildPhase', signNativeModulesBuildPhaseName, firstTargetUUID);
if (!(signNativeModulesBuildPhase)) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"author": "janeasystems",
"license": "MIT",
"dependencies": {
"nodejs-mobile-gyp": "^0.1.0",
"nodejs-mobile-gyp": "^0.2.0",
"tar.gz2": "^1.0.0"
},
"homepage": "https://code.janeasystems.com/nodejs-mobile",
Expand Down
58 changes: 45 additions & 13 deletions src/ios/CDVNodeJS.mm
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ void handleAppChannelMessage(NSString* msg) {
// The callback id of the Cordova channel listener
NSString* allChannelsListenerCallbackId = nil;

// script name to preload and override dlopen to open native modules from the Framework Path.
NSString* const NODEJS_DLOPEN_OVERRIDE_FILENAME = @"override-dlopen-paths-preload.js";

// path where the nodejs-project is contained inside the Application package.
NSString* const NODE_ROOT = @"/www/nodejs-project/";

+ (CDVNodeJS*) activeInstance {
return activeInstance;
}
Expand All @@ -83,7 +89,6 @@ - (void) pluginInitialize {

NSString* const NODE_PATH = @"NODE_PATH";
NSString* const BUILTIN_MODULES = @"/www/nodejs-mobile-cordova-assets/builtin_modules";
NSString* const NODE_ROOT = @"/www/nodejs-project/";

// The 'onAppTerminate', 'onReset' and 'onMemoryWarning' events are already
// registered in the super class while 'onPause' and 'onResume' are not.
Expand Down Expand Up @@ -278,7 +283,7 @@ - (void) startEngine:(CDVInvokedUrlCommand*)command {
errorMsg = @"Invalid filename";
} else {
NSString* appPath = [[NSBundle mainBundle] bundlePath];
scriptPath = [appPath stringByAppendingString:@"/www/nodejs-project/"];
scriptPath = [appPath stringByAppendingString:NODE_ROOT];
scriptPath = [scriptPath stringByAppendingString:scriptFileName];
if ([[NSFileManager defaultManager] fileExistsAtPath:scriptPath] == FALSE) {
errorMsg = @"File not found";
Expand All @@ -287,11 +292,24 @@ - (void) startEngine:(CDVInvokedUrlCommand*)command {
}

if (errorMsg == nil) {
NSArray* arguments = [NSArray arrayWithObjects:
@"node",
scriptPath,
nil
];
NSArray* arguments = nil;
NSString* dlopenoverridePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@/%@", NODE_ROOT, NODEJS_DLOPEN_OVERRIDE_FILENAME] ofType:@""];
// Check if the file to override dlopen lookup exists, for loading native modules from the Frameworks.
if(!dlopenoverridePath) {
arguments = [NSArray arrayWithObjects:
@"node",
scriptPath,
nil
];
} else {
arguments = [NSArray arrayWithObjects:
@"node",
@"-r",
dlopenoverridePath,
scriptPath,
nil
];
}
engineAlreadyStarted = YES;

[NodeJSRunner startEngineWithArguments:arguments];
Expand Down Expand Up @@ -322,12 +340,26 @@ - (void) startEngineWithScript:(CDVInvokedUrlCommand*)command {
errorMsg = @"Script is empty";
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:errorMsg];
} else {
NSArray* arguments = [NSArray arrayWithObjects:
@"node",
@"-e",
scriptBody,
nil
];
NSArray* arguments = nil;
NSString* dlopenoverridePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@/%@", NODE_ROOT, NODEJS_DLOPEN_OVERRIDE_FILENAME] ofType:@""];
// Check if the file to override dlopen lookup exists, for loading native modules from the Frameworks.
if(!dlopenoverridePath) {
arguments = [NSArray arrayWithObjects:
@"node",
@"-r",
dlopenoverridePath,
@"-e",
scriptBody,
nil
];
} else {
arguments = [NSArray arrayWithObjects:
@"node",
@"-e",
scriptBody,
nil
];
}
engineAlreadyStarted = YES;

[NodeJSRunner startEngineWithArguments:arguments];
Expand Down

0 comments on commit 6cd38b0

Please sign in to comment.