From eb827d6d32598c93cb6964913baa487d80238d0c Mon Sep 17 00:00:00 2001 From: Geoffrey Goh Date: Wed, 24 Feb 2016 17:15:58 -0800 Subject: [PATCH] generate binary resources hash --- CodePush.js | 20 +++- .../CodePushDemoApp/android/app/build.gradle | 1 + .../microsoft/codepush/react/CodePush.java | 15 +++ .../codepush/react/CodePushUtils.java | 20 ++++ android/codepush.gradle | 66 +++++++++++++ ios/CodePush/CodePush.h | 32 +++++-- ios/CodePush/CodePush.m | 35 +++++-- ios/CodePush/CodePushPackage.m | 53 +++++++++-- ios/CodePush/CodePushUpdateUtils.m | 57 ++++++++++++ scripts/build-and-test.sh | 45 --------- scripts/generateBundledResourcesHash.js | 93 +++++++++++++++++++ scripts/getFilesInFolder.js | 18 ++++ scripts/recordFilesBeforeBundleCommand.js | 31 +++++++ scripts/start-packager.sh | 6 -- scripts/stop-packager.sh | 8 -- 15 files changed, 414 insertions(+), 86 deletions(-) create mode 100644 android/codepush.gradle delete mode 100755 scripts/build-and-test.sh create mode 100644 scripts/generateBundledResourcesHash.js create mode 100644 scripts/getFilesInFolder.js create mode 100644 scripts/recordFilesBeforeBundleCommand.js delete mode 100755 scripts/start-packager.sh delete mode 100755 scripts/stop-packager.sh diff --git a/CodePush.js b/CodePush.js index baf1c57..8b993c6 100644 --- a/CodePush.js +++ b/CodePush.js @@ -2,6 +2,7 @@ import { AcquisitionManager as Sdk } from "code-push/script/acquisition-sdk"; import { Alert } from "./AlertAdapter"; import requestFetchAdapter from "./request-fetch-adapter"; import semver from "semver"; +import { Platform } from "react-native"; let NativeCodePush = require("react-native").NativeModules.CodePush; const PackageMixins = require("./package-mixins")(NativeCodePush); @@ -45,7 +46,7 @@ async function checkForUpdate(deploymentKey = null) { const update = await sdk.queryUpdateWithCurrentPackage(queryPackage); /* - * There are three cases where checkForUpdate will resolve to null: + * There are four cases where checkForUpdate will resolve to null: * ---------------------------------------------------------------- * 1) The server said there isn't an update. This is the most common case. * 2) The server said there is an update but it requires a newer binary version. @@ -56,6 +57,11 @@ async function checkForUpdate(deploymentKey = null) { * the currently running update. This should _never_ happen, unless there is a * bug in the server, but we're adding this check just to double-check that the * client app is resilient to a potential issue with the update check. + * 4) On Android: the server said there is an update, but the update's hash is the + * same as that of the binary's currently running version. We did not attach the + * binary's hash to the updateCheck request because we want to avoid having to + * install diff updates against the binary's version, which we can't do yet on + * Android. */ if (!update || update.updateAppVersion || localPackage && (update.packageHash === localPackage.packageHash)) { if (update && update.updateAppVersion) { @@ -63,7 +69,17 @@ async function checkForUpdate(deploymentKey = null) { } return null; - } else { + } else { + if (Platform.OS === "android") { + // Diff updates against the binary version not supported on Android + if (!localPackage) { + const binaryHash = await NativeCodePush.getBinaryHash(); + if (update.packageHash === binaryHash) { + return null; + } + } + } + const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) }; remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash); remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey; diff --git a/Examples/CodePushDemoApp/android/app/build.gradle b/Examples/CodePushDemoApp/android/app/build.gradle index e82e2a2..19f6777 100644 --- a/Examples/CodePushDemoApp/android/app/build.gradle +++ b/Examples/CodePushDemoApp/android/app/build.gradle @@ -58,6 +58,7 @@ import com.android.build.OutputFile */ apply from: "react.gradle" +apply from: "../../node_modules/react-native-code-push/android/codepush.gradle" /** * Set this to true to create three separate APKs instead of one: diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java index b6dc518..36d15f8 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java @@ -50,6 +50,7 @@ public class CodePush { private final String ASSETS_BUNDLE_PREFIX = "assets://"; private final String BINARY_MODIFIED_TIME_KEY = "binaryModifiedTime"; + private final String CODE_PUSH_HASH_FILE_NAME = "CodePushHash.json"; private final String CODE_PUSH_PREFERENCES = "CodePush"; private final String DOWNLOAD_PROGRESS_EVENT_NAME = "CodePushDownloadProgress"; private final String FAILED_UPDATES_KEY = "CODE_PUSH_FAILED_UPDATES"; @@ -392,6 +393,20 @@ public class CodePush { asyncTask.execute(); } + @ReactMethod + public void getBinaryHash(Promise promise) { + try { + promise.resolve(CodePushUtils.getStringFromInputStream(mainActivity.getAssets().open(CODE_PUSH_HASH_FILE_NAME))); + } catch (IOException e) { + if (!isDebugMode) { + // Only print this message in "Release" mode. In "Debug", we may not have the + // hash if the build skips bundling the files. + CodePushUtils.log("Unable to get the hash of the binary's bundled resources - \"codepush.gradle\" may have not been added to the build definition."); + } + promise.resolve(""); + } + } + @ReactMethod public void getConfiguration(Promise promise) { WritableNativeMap configMap = new WritableNativeMap(); diff --git a/android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java b/android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java index 63d2709..9ef9078 100644 --- a/android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java +++ b/android/app/src/main/java/com/microsoft/codepush/react/CodePushUtils.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.Iterator; @@ -183,6 +184,25 @@ public class CodePushUtils { return jsonObj; } + public static String getStringFromInputStream(InputStream inputStream) throws IOException { + BufferedReader bufferedReader = null; + try { + StringBuilder buffer = new StringBuilder(); + bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + + String line; + while ((line = bufferedReader.readLine()) != null) { + buffer.append(line); + buffer.append("\n"); + } + + return buffer.toString().trim(); + } finally { + if (bufferedReader != null) bufferedReader.close(); + if (inputStream != null) inputStream.close(); + } + } + public static WritableMap getWritableMapFromFile(String filePath) throws IOException { String content = FileUtils.readFileToString(filePath); diff --git a/android/codepush.gradle b/android/codepush.gradle new file mode 100644 index 0000000..2b24b77 --- /dev/null +++ b/android/codepush.gradle @@ -0,0 +1,66 @@ +// Adapted from https://raw.githubusercontent.com/facebook/react-native/master/local-cli/generator-android/templates/src/app/react.gradle + +def config = project.hasProperty("react") ? project.react : []; +def bundleAssetName = config.bundleAssetName ?: "index.android.bundle" + +def elvisFile(thing) { + return thing ? file(thing) : null; +} + +void runBefore(String dependentTaskName, Task task) { + Task dependentTask = tasks.findByPath(dependentTaskName); + if (dependentTask != null) { + dependentTask.dependsOn task + } +} + +gradle.projectsEvaluated { + def buildTypes = android.buildTypes.collect { type -> type.name } + def productFlavors = android.productFlavors.collect { flavor -> flavor.name } + if (!productFlavors) productFlavors.add('') + + productFlavors.each { productFlavorName -> + buildTypes.each { buildTypeName -> + + def sourceName = "${buildTypeName}" + def targetName = "${sourceName.capitalize()}" + if (productFlavorName) { + sourceName = "${productFlavorName}${targetName}" + } + + def jsBundleDirConfigName = "jsBundleDir${targetName}" + def jsBundleDir = elvisFile(config."$jsBundleDirConfigName") ?: + file("$buildDir/intermediates/assets/${sourceName}") + + def resourcesDirConfigName = "jsBundleDir${targetName}" + def resourcesDir = elvisFile(config."${resourcesDirConfigName}") ?: + file("$buildDir/intermediates/res/merged/${sourceName}") + def jsBundleFile = file("$jsBundleDir/$bundleAssetName") + + // Make this task run right before the bundle task + def recordFilesBeforeBundleCommand = tasks.create( + name: "recordFilesBeforeBundleCommand${targetName}", + type: Exec) { + commandLine "node", "../../node_modules/react-native-code-push/scripts/recordFilesBeforeBundleCommand.js", resourcesDir + } + + recordFilesBeforeBundleCommand.dependsOn("merge${targetName}Resources") + recordFilesBeforeBundleCommand.dependsOn("merge${targetName}Assets") + runBefore("bundle${targetName}JsAndAssets", recordFilesBeforeBundleCommand) + + // Make this task run right after the bundle task + def generateBundledResourcesHash = tasks.create( + name: "generateBundledResourcesHash${targetName}", + type: Exec) { + commandLine "node", "../../node_modules/react-native-code-push/scripts/generateBundledResourcesHash.js", resourcesDir, "$jsBundleDir/$bundleAssetName", "$buildDir/intermediates/assets/${sourceName}" + } + + generateBundledResourcesHash.dependsOn("bundle${targetName}JsAndAssets") + runBefore("processArmeabi-v7a${targetName}Resources", generateBundledResourcesHash) + runBefore("processX86${targetName}Resources", generateBundledResourcesHash) + runBefore("processUniversal${targetName}Resources", generateBundledResourcesHash) + runBefore("process${targetName}Resources", generateBundledResourcesHash) + runBefore("process${targetName}Manifest", generateBundledResourcesHash) + } + } +} \ No newline at end of file diff --git a/ios/CodePush/CodePush.h b/ios/CodePush/CodePush.h index 5584325..cf77e86 100644 --- a/ios/CodePush/CodePush.h +++ b/ios/CodePush/CodePush.h @@ -2,6 +2,7 @@ @interface CodePush : NSObject ++ (NSURL *)binaryBundleURL; /* * This method is used to retrieve the URL for the most recent * version of the JavaScript bundle. This could be either the @@ -68,10 +69,12 @@ failCallback:(void (^)(NSError *err))failCallback; @interface CodePushPackage : NSObject -+ (void)installPackage:(NSDictionary *)updatePackage - removePendingUpdate:(BOOL)removePendingUpdate - error:(NSError **)error; ++ (void)downloadPackage:(NSDictionary *)updatePackage + progressCallback:(void (^)(long long, long long))progressCallback + doneCallback:(void (^)())doneCallback + failCallback:(void (^)(NSError *err))failCallback; ++ (NSString *)getBinaryAssetsPath; + (NSDictionary *)getCurrentPackage:(NSError **)error; + (NSString *)getCurrentPackageFolderPath:(NSError **)error; + (NSString *)getCurrentPackageBundlePath:(NSError **)error; @@ -81,18 +84,17 @@ failCallback:(void (^)(NSError *err))failCallback; error:(NSError **)error; + (NSString *)getPackageFolderPath:(NSString *)packageHash; + ++ (void)installPackage:(NSDictionary *)updatePackage + removePendingUpdate:(BOOL)removePendingUpdate + error:(NSError **)error; + + (BOOL)isCodePushError:(NSError *)err; - -+ (void)downloadPackage:(NSDictionary *)updatePackage - progressCallback:(void (^)(long long, long long))progressCallback - doneCallback:(void (^)())doneCallback - failCallback:(void (^)(NSError *err))failCallback; - + (void)rollbackPackage; // The below methods are only used during tests. -+ (void)downloadAndReplaceCurrentBundle:(NSString *)remoteBundleUrl; + (void)clearUpdates; ++ (void)downloadAndReplaceCurrentBundle:(NSString *)remoteBundleUrl; @end @@ -109,8 +111,18 @@ failCallback:(void (^)(NSError *err))failCallback; + (void)copyEntriesInFolder:(NSString *)sourceFolder destFolder:(NSString *)destFolder error:(NSError **)error; + + (NSString *)findMainBundleInFolder:(NSString *)folderPath error:(NSError **)error; + ++ (NSString *)getDefaultAssetsFolderName; ++ (NSString *)getDefaultJsBundleName; + ++ (NSString *)getHashForBinaryContents:(NSURL *)binaryBundleUrl + error:(NSError **)error; + ++ (NSString *)getManifestFolderPrefix; + + (BOOL)verifyHashForDiffUpdate:(NSString *)finalUpdateFolder expectedHash:(NSString *)expectedHash error:(NSError **)error; diff --git a/ios/CodePush/CodePush.m b/ios/CodePush/CodePush.m index 192a350..5028f78 100644 --- a/ios/CodePush/CodePush.m +++ b/ios/CodePush/CodePush.m @@ -30,6 +30,7 @@ static NSString *const PendingUpdateIsLoadingKey = @"isLoading"; // These keys are used to inspect/augment the metadata // that is associated with an update's package. +static NSString *const AppVersionKey = @"appVersion"; static NSString *const BinaryBundleDateKey = @"binaryDate"; static NSString *const PackageHashKey = @"packageHash"; static NSString *const PackageIsPendingKey = @"isPending"; @@ -47,6 +48,11 @@ static NSString *bundleResourceName = @"main"; #pragma mark - Public Obj-C API ++ (NSURL *)binaryBundleURL +{ + return [[NSBundle mainBundle] URLForResource:bundleResourceName withExtension:bundleResourceExtension]; +} + + (NSURL *)bundleURL { return [self bundleURLForResource:bundleResourceName]; @@ -85,7 +91,7 @@ static NSString *bundleResourceName = @"main"; } NSString *packageDate = [currentPackageMetadata objectForKey:BinaryBundleDateKey]; - NSString *packageAppVersion = [currentPackageMetadata objectForKey:@"appVersion"]; + NSString *packageAppVersion = [currentPackageMetadata objectForKey:AppVersionKey]; if ([[self modifiedDateStringOfFileAtURL:binaryBundleURL] isEqualToString:packageDate] && ([CodePush isUsingTestConfiguration] ||[binaryAppVersion isEqualToString:packageAppVersion])) { // Return package file because it is newer than the app store binary's JS bundle @@ -150,11 +156,6 @@ static NSString *bundleResourceName = @"main"; @synthesize bridge = _bridge; @synthesize methodQueue = _methodQueue; -+ (NSURL *)binaryBundleURL -{ - return [[NSBundle mainBundle] URLForResource:bundleResourceName withExtension:bundleResourceExtension]; -} - /* * This method is used by the React Native bridge to allow * our plugin to expose constants to the JS-side. In our case @@ -458,11 +459,33 @@ RCT_EXPORT_METHOD(getCurrentPackage:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { NSError *error; + if (isRunningBinaryVersion) { + // isRunningBinaryVersion will not get set to "YES" if running against the packager. + NSString *binaryHash = [CodePushUpdateUtils getHashForBinaryContents:[CodePush binaryBundleURL] error:&error]; + if (error) { + NSLog(@"Error obtaining hash for binary contents: %@", error); + resolve(nil); + return; + } else if (binaryHash == nil) { + resolve(nil); + return; + } + + resolve(@{ + PackageHashKey:binaryHash, + AppVersionKey:[[CodePushConfig current] appVersion] + }); + return; + } + NSMutableDictionary *package = [[CodePushPackage getCurrentPackage:&error] mutableCopy]; if (error) { reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error); return; + } else if (package == nil) { + resolve(nil); + return; } // Add the "isPending" virtual property to the package at this point, so that diff --git a/ios/CodePush/CodePushPackage.m b/ios/CodePush/CodePushPackage.m index d5f4d4f..269adba 100644 --- a/ios/CodePush/CodePushPackage.m +++ b/ios/CodePush/CodePushPackage.m @@ -12,6 +12,12 @@ NSString * const StatusFile = @"codepush.json"; NSString * const UpdateBundleFileName = @"app.jsbundle"; NSString * const UnzippedFolderName = @"unzipped"; + ++ (NSString *)getBinaryAssetsPath +{ + return [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:[CodePushUpdateUtils getDefaultAssetsFolderName]]; +} + + (NSString *)getCodePushPath { NSString* codePushPath = [[CodePush getApplicationSupportDirectory] stringByAppendingPathComponent:@"CodePush"]; @@ -209,7 +215,7 @@ NSString * const UnzippedFolderName = @"unzipped"; NSError *error; if ([[NSFileManager defaultManager] fileExistsAtPath:newUpdateFolderPath]) { - // This removes any stale data in newPackageFolderPath that could have been left + // This removes any stale data in newUpdateFolderPath that could have been left // uncleared due to a crash or error during the download or install process. [[NSFileManager defaultManager] removeItemAtPath:newUpdateFolderPath error:&error]; @@ -267,12 +273,41 @@ NSString * const UnzippedFolderName = @"unzipped"; return; } - [[NSFileManager defaultManager] copyItemAtPath:currentPackageFolderPath - toPath:newUpdateFolderPath - error:&error]; - if (error) { - failCallback(error); - return; + if (currentPackageFolderPath == nil) { + // Currently running the binary version, copy files from the bundled resources + NSString *newUpdateCodePushPath = [newUpdateFolderPath stringByAppendingPathComponent:[CodePushUpdateUtils getManifestFolderPrefix]]; + [[NSFileManager defaultManager] createDirectoryAtPath:newUpdateCodePushPath + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + failCallback(error); + return; + } + + [[NSFileManager defaultManager] copyItemAtPath:[self getBinaryAssetsPath] + toPath:[newUpdateCodePushPath stringByAppendingPathComponent:[CodePushUpdateUtils getDefaultAssetsFolderName]] + error:&error]; + if (error) { + failCallback(error); + return; + } + + [[NSFileManager defaultManager] copyItemAtPath:[[CodePush binaryBundleURL] path] + toPath:[newUpdateCodePushPath stringByAppendingPathComponent:[CodePushUpdateUtils getDefaultJsBundleName]] + error:&error]; + if (error) { + failCallback(error); + return; + } + } else { + [[NSFileManager defaultManager] copyItemAtPath:currentPackageFolderPath + toPath:newUpdateFolderPath + error:&error]; + if (error) { + failCallback(error); + return; + } } // Delete files mentioned in the manifest. @@ -355,7 +390,7 @@ NSString * const UnzippedFolderName = @"unzipped"; userInfo:@{ NSLocalizedDescriptionKey: NSLocalizedString(@"Update is invalid - no files with extension .jsbundle or .bundle were found in the update package.", nil) - }]; + }]; failCallback(error); return; } @@ -382,7 +417,7 @@ NSString * const UnzippedFolderName = @"unzipped"; userInfo:@{ NSLocalizedDescriptionKey: NSLocalizedString(@"The update contents failed the data integrity check.", nil) - }]; + }]; failCallback(error); return; } diff --git a/ios/CodePush/CodePushUpdateUtils.m b/ios/CodePush/CodePushUpdateUtils.m index d48e95d..5b1c460 100644 --- a/ios/CodePush/CodePushUpdateUtils.m +++ b/ios/CodePush/CodePushUpdateUtils.m @@ -3,6 +3,10 @@ @implementation CodePushUpdateUtils +NSString * const ManifestFolderPrefix = @"CodePush"; +NSString * const DefaultJsBundleName = @"main.jsbundle"; +NSString * const DefaultAssetsFolderName = @"assets"; + + (void)addContentsOfFolderToManifest:(NSString *)folderPath pathPrefix:(NSString *)pathPrefix manifest:(NSMutableArray *)manifest @@ -127,6 +131,55 @@ return nil; } ++ (NSString *)getDefaultAssetsFolderName +{ + return DefaultAssetsFolderName; +} + ++ (NSString *)getDefaultJsBundleName +{ + return DefaultJsBundleName; +} + ++ (NSString *)getHashForBinaryContents:(NSURL *)binaryBundleUrl + error:(NSError **)error +{ + NSString *assetsPath = [CodePushPackage getBinaryAssetsPath]; + NSMutableArray *manifest = [NSMutableArray array]; + [self addContentsOfFolderToManifest:assetsPath + pathPrefix:[NSString stringWithFormat:@"%@/%@", [self getManifestFolderPrefix], @"assets"] + manifest:manifest + error:error]; + if (*error) { + return nil; + } + + NSData *jsBundleContents = [NSData dataWithContentsOfURL:binaryBundleUrl]; + NSString *jsBundleContentsHash = [self computeHash:jsBundleContents]; + [manifest addObject:[[NSString stringWithFormat:@"%@/%@", [self getManifestFolderPrefix], [self getDefaultJsBundleName]] stringByAppendingString:jsBundleContentsHash]]; + + NSArray *sortedManifest = [manifest sortedArrayUsingSelector:@selector(compare:)]; + NSData *manifestData = [NSJSONSerialization dataWithJSONObject:sortedManifest + options:kNilOptions + error:error]; + if (*error) { + return nil; + } + + NSString *manifestString = [[NSString alloc] initWithData:manifestData + encoding:NSUTF8StringEncoding]; + // The JSON serialization turns path separators into "\/", e.g. "CodePush\/assets\/image.png" + manifestString = [manifestString stringByReplacingOccurrencesOfString:@"\\/" + withString:@"/"]; + NSString *manifestHash = [self computeHash:[NSData dataWithBytes:manifestString.UTF8String length:manifestString.length]]; + return manifestHash; +} + ++ (NSString *)getManifestFolderPrefix +{ + return ManifestFolderPrefix; +} + + (BOOL)verifyHashForDiffUpdate:(NSString *)finalUpdateFolder expectedHash:(NSString *)expectedHash error:(NSError **)error @@ -144,6 +197,10 @@ NSData *updateContentsManifestData = [NSJSONSerialization dataWithJSONObject:sortedUpdateContentsManifest options:kNilOptions error:error]; + if (*error) { + return NO; + } + NSString *updateContentsManifestString = [[NSString alloc] initWithData:updateContentsManifestData encoding:NSUTF8StringEncoding]; // The JSON serialization turns path separators into "\/", e.g. "CodePush\/assets\/image.png" diff --git a/scripts/build-and-test.sh b/scripts/build-and-test.sh deleted file mode 100755 index f0db861..0000000 --- a/scripts/build-and-test.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - -set -e - -SCRIPTS_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) - -ROOT=$1 -XCODEPROJ=$2 -XCODESCHEME=$3 - -export REACT_PACKAGER_LOG="$ROOT/server.log" - -cd $ROOT - -function cleanup { - EXIT_CODE=$? - set +e - - sleep 3 - $SCRIPTS_PATH/stop-packager.sh - - if [ $EXIT_CODE -ne 0 ]; - then - WATCHMAN_LOGS=/usr/local/Cellar/watchman/3.1/var/run/watchman/$USER.log - #[ -f $WATCHMAN_LOGS ] && cat $WATCHMAN_LOGS - - #[ -f $REACT_PACKAGER_LOG ] && cat $REACT_PACKAGER_LOG - fi -} -trap cleanup EXIT - -#$SCRIPTS_PATH/stop-packager.sh -$SCRIPTS_PATH/start-packager.sh $ROOT - -xctool \ - -project $XCODEPROJ \ - -scheme $XCODESCHEME -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 5,OS=8.3' \ - build - -xctool \ - -project $XCODEPROJ \ - -scheme $XCODESCHEME -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 5,OS=8.3' \ - test - - diff --git a/scripts/generateBundledResourcesHash.js b/scripts/generateBundledResourcesHash.js new file mode 100644 index 0000000..851a2a8 --- /dev/null +++ b/scripts/generateBundledResourcesHash.js @@ -0,0 +1,93 @@ +/* + * This script generates a hash of all the React Native bundled assets and writes it into + * into the APK. The hash in "updateCheck" requests to prevent downloading an identical + * update to the one already present in the binary. + * + * It first creates a snapshot of the contents in the resource directory by creating + * a map with the modified time of all the files in the directory. It then compares this + * snapshot with the one saved earlier in "recordFilesBeforeBundleCommand.js" to figure + * out which files were generated by the "react-native bundle" command. It then computes + * the hash for each file to generate a manifest, and then computes a hash over the entire + * manifest to generate the final hash, which is saved to the APK's assets directory. + */ + +var crypto = require("crypto"); +var fs = require("fs"); +var path = require("path"); + +var getFilesInFolder = require("./getFilesInFolder"); + +var CODE_PUSH_FOLDER_PREFIX = "CodePush"; +var CODE_PUSH_HASH_FILE_NAME = "CodePushHash.json"; +var HASH_ALGORITHM = "sha256"; +var JS_BUNDLE_FILE_NAME = "main.jsbundle"; +var TEMP_FILE_PATH = path.join(require("os").tmpdir(), "CodePushResourcesMap.json"); + +var resourcesDir = process.argv[2]; +var jsBundleFilePath = process.argv[3]; +var assetsDir = process.argv[4]; +var resourceFiles = []; + +getFilesInFolder(resourcesDir, resourceFiles); + +var oldFileToModifiedTimeMap = require(TEMP_FILE_PATH); +var newFileToModifiedTimeMap = {}; + +resourceFiles.forEach(function(resourceFile) { + newFileToModifiedTimeMap[resourceFile.path.substring(resourcesDir.length)] = resourceFile.mtime; +}); + +var bundleGeneratedAssetFiles = []; + +for (var newFilePath in newFileToModifiedTimeMap) { + if (!oldFileToModifiedTimeMap[newFilePath] || oldFileToModifiedTimeMap[newFilePath] < newFileToModifiedTimeMap[newFilePath].getTime()) { + bundleGeneratedAssetFiles.push(newFilePath); + } +} + +var manifest = []; + +if (bundleGeneratedAssetFiles.length) { + bundleGeneratedAssetFiles.forEach(function(assetFile) { + // Generate hash for each asset file + var readStream = fs.createReadStream(resourcesDir + assetFile); + var hashStream = crypto.createHash(HASH_ALGORITHM); + + readStream.pipe(hashStream) + .on("error", function(error) { + throw error; + }) + .on("finish", function() { + hashStream.end(); + var buffer = hashStream.read(); + var fileHash = buffer.toString("hex"); + manifest.push(CODE_PUSH_FOLDER_PREFIX + assetFile + ":" + fileHash); + + if (manifest.length === bundleGeneratedAssetFiles.length) { + // Generate hash for JS bundle + readStream = fs.createReadStream(jsBundleFilePath); + hashStream = crypto.createHash(HASH_ALGORITHM); + readStream.pipe(hashStream) + .on("error", function(error) { + throw error; + }) + .on("finish", function() { + hashStream.end(); + var buffer = hashStream.read(); + var fileHash = buffer.toString("hex"); + manifest.push(CODE_PUSH_FOLDER_PREFIX + "/" + JS_BUNDLE_FILE_NAME + ":" + fileHash); + manifest = manifest.sort(); + + var finalHash = crypto.createHash(HASH_ALGORITHM) + .update(JSON.stringify(manifest)) + .digest("hex"); + + var savedResourcesManifestPath = assetsDir + "/" + CODE_PUSH_HASH_FILE_NAME; + fs.writeFileSync(savedResourcesManifestPath, finalHash); + }); + } + }); + }); +} + +fs.unlinkSync(TEMP_FILE_PATH); \ No newline at end of file diff --git a/scripts/getFilesInFolder.js b/scripts/getFilesInFolder.js new file mode 100644 index 0000000..d8fbfe0 --- /dev/null +++ b/scripts/getFilesInFolder.js @@ -0,0 +1,18 @@ +var fs = require("fs"); + +// Utility function that collects the stats of every file in a directory +// as well as in its subdirectories. +function getFilesInFolder(folderName, fileList) { + var folderFiles = fs.readdirSync(folderName); + folderFiles.forEach(function(file) { + var fileStats = fs.statSync(folderName + "/" + file); + if (fileStats.isDirectory()) { + getFilesInFolder(folderName + "/" + file, fileList); + } else { + fileStats.path = folderName + "/" + file; + fileList.push(fileStats); + } + }); +} + +module.exports = getFilesInFolder; \ No newline at end of file diff --git a/scripts/recordFilesBeforeBundleCommand.js b/scripts/recordFilesBeforeBundleCommand.js new file mode 100644 index 0000000..bc5e03d --- /dev/null +++ b/scripts/recordFilesBeforeBundleCommand.js @@ -0,0 +1,31 @@ +/* + * This script creates a snapshot of the contents in the resource directory + * by creating a map with the modified time of all the files in the directory + * and saving it to a temp file. This snapshot is later referenced in + * "generatePackageHash.js" to figure out which files have changed or were + * newly generated by the "react-native bundle" command. + */ + +var fs = require("fs"); +var path = require("path"); + +var getFilesInFolder = require("./getFilesInFolder"); + +var TEMP_FILE_PATH = path.join(require("os").tmpdir(), "CodePushResourcesMap.json"); + +var resourcesDir = process.argv[2]; +var resourceFiles = []; + +getFilesInFolder(resourcesDir, resourceFiles); + +var fileToModifiedTimeMap = {}; + +resourceFiles.forEach(function(resourceFile) { + fileToModifiedTimeMap[resourceFile.path.substring(resourcesDir.length)] = resourceFile.mtime.getTime(); +}); + +fs.writeFile(TEMP_FILE_PATH, JSON.stringify(fileToModifiedTimeMap), function(err) { + if (err) { + throw err; + } +}); \ No newline at end of file diff --git a/scripts/start-packager.sh b/scripts/start-packager.sh deleted file mode 100755 index 669659b..0000000 --- a/scripts/start-packager.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -$ROOT=$1 - -command=`node -e "console.log(require('./package').scripts.start)"` -$command & diff --git a/scripts/stop-packager.sh b/scripts/stop-packager.sh deleted file mode 100755 index 2dd845e..0000000 --- a/scripts/stop-packager.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -result=`lsof -t -i4TCP:8081` - -if [ $result ] -then - kill -9 `lsof -t -i4TCP:8081` -fi -