@revopush/react-native-code-push 2.6.0-rc.4 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +5 -1
- package/android/build.gradle +12 -1
- package/android/codepush.gradle +21 -44
- package/android/gradle.properties +2 -0
- package/android/settings.gradle +8 -9
- package/android/src/main/java/com/microsoft/codepush/react/BaseBundleStore.java +78 -0
- package/android/src/main/java/com/microsoft/codepush/react/CodePush.java +14 -3
- package/android/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +2 -0
- package/android/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +3 -3
- package/android/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +56 -11
- package/android/src/main/java/com/microsoft/codepush/react/FileUtils.java +7 -5
- package/ios/CodePush/CodePush.h +14 -0
- package/ios/CodePush/CodePushBaseBundleStore.m +104 -0
- package/ios/CodePush/CodePushPackage.m +82 -9
- package/ios/CodePush.xcodeproj/project.pbxproj +6 -0
- package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64/DiffUpdates.framework/DiffUpdates +0 -0
- package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64/DiffUpdates.framework/Headers/DiffUpdates.h +18 -0
- package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64/DiffUpdates.framework/Headers/hpatch_objc.h +23 -0
- package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64/DiffUpdates.framework/Info.plist +0 -0
- package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/DiffUpdates +0 -0
- package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/Headers/DiffUpdates.h +18 -0
- package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/Headers/hpatch_objc.h +23 -0
- package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/Info.plist +0 -0
- package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/_CodeSignature/CodeResources +12 -9
- package/package.json +10 -14
- package/tsconfig.json +1 -4
- package/typings/react-native-code-push.d.ts +18 -0
- package/ios/Frameworks/DiffUpdates.xcframework/ios-arm64_x86_64-simulator/DiffUpdates.framework/_CodeSignature/CodeRequirements-1 +0 -0
- package/scripts/postlink/android/postlink.js +0 -87
- package/scripts/postlink/ios/postlink.js +0 -116
- package/scripts/postlink/run.js +0 -11
- package/scripts/postunlink/android/postunlink.js +0 -74
- package/scripts/postunlink/ios/postunlink.js +0 -87
- package/scripts/postunlink/run.js +0 -11
- package/scripts/tools/linkToolsAndroid.js +0 -57
- package/scripts/tools/linkToolsIos.js +0 -130
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
"allow": [
|
|
4
4
|
"Bash(git:*)",
|
|
5
5
|
"Bash(echo \"EXIT:$?\")",
|
|
6
|
-
"Bash(find:*)"
|
|
6
|
+
"Bash(find:*)",
|
|
7
|
+
"Bash(cd ios/CodePush)",
|
|
8
|
+
"Bash(grep -rln \"finalizePatchAndZip\\\\|DiffDownloadHandler\\\\|HDIFF\\\\|hpatch\\\\|createZipFileAtPath\\\\|bundleDiffBlob\" .)",
|
|
9
|
+
"Bash(grep -rln \"finalizePatchAndZip\\\\|DiffDownloadHandler\\\\|hpatch\\\\|HDIFF\\\\|createZipFileAtPath\" ios)",
|
|
10
|
+
"Bash(grep -v \".framework/DiffUpdates$\")"
|
|
7
11
|
]
|
|
8
12
|
}
|
|
9
13
|
}
|
package/android/build.gradle
CHANGED
|
@@ -43,12 +43,20 @@ android {
|
|
|
43
43
|
lint {
|
|
44
44
|
abortOnError = false
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
testOptions {
|
|
48
|
+
unitTests.returnDefaultValues = true
|
|
49
|
+
unitTests.all {
|
|
50
|
+
testLogging { events "passed", "skipped", "failed" }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
dependencies {
|
|
49
56
|
if (boolProp("REACT_NATIVE_REVOPUSH_DEV_MODE", false)) {
|
|
50
57
|
// for local dev only
|
|
51
58
|
implementation("org.revopush:revopush-diff")
|
|
59
|
+
// @react-native/gradle-plugin in package.json must use this same version
|
|
52
60
|
implementation 'com.facebook.react:react-android:0.79.6'
|
|
53
61
|
// end of local dev only
|
|
54
62
|
} else {
|
|
@@ -56,9 +64,12 @@ dependencies {
|
|
|
56
64
|
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
|
|
57
65
|
//noinspection GradleDynamicVersion
|
|
58
66
|
|
|
59
|
-
implementation "org.revopush:revopush-diff:0.0.
|
|
67
|
+
implementation "org.revopush:revopush-diff:0.0.9"
|
|
60
68
|
implementation "com.facebook.react:react-native:+"
|
|
61
69
|
}
|
|
70
|
+
|
|
71
|
+
testImplementation 'junit:junit:4.13.2'
|
|
72
|
+
testImplementation 'org.json:json:20231013'
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
def getExtOrIntegerDefault(name, defaultValue) {
|
package/android/codepush.gradle
CHANGED
|
@@ -5,7 +5,6 @@ import java.nio.file.Paths;
|
|
|
5
5
|
def config = project.extensions.findByName("react") ?: [:]
|
|
6
6
|
def bundleAssetName = config.bundleAssetName ? config.bundleAssetName.get() : "index.android.bundle"
|
|
7
7
|
|
|
8
|
-
// because elvis operator
|
|
9
8
|
def elvisFile(thing) {
|
|
10
9
|
return thing ? file(thing) : null;
|
|
11
10
|
}
|
|
@@ -18,30 +17,12 @@ void runBefore(String dependentTaskName, Task task) {
|
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* the file system root. This handles various cases, including:
|
|
24
|
-
*
|
|
25
|
-
* - Working in the open-source RN repo:
|
|
26
|
-
* Gradle: /path/to/react-native/ReactAndroid
|
|
27
|
-
* Node module: /path/to/react-native/node_modules/[package]
|
|
28
|
-
*
|
|
29
|
-
* - Installing RN as a dependency of an app and searching for hoisted
|
|
30
|
-
* dependencies:
|
|
31
|
-
* Gradle: /path/to/app/node_modules/react-native/ReactAndroid
|
|
32
|
-
* Node module: /path/to/app/node_modules/[package]
|
|
33
|
-
*
|
|
34
|
-
* - Working in a larger repo (e.g., Facebook) that contains RN:
|
|
35
|
-
* Gradle: /path/to/repo/path/to/react-native/ReactAndroid
|
|
36
|
-
* Node module: /path/to/repo/node_modules/[package]
|
|
37
|
-
*
|
|
38
|
-
* The search begins at the given base directory (a File object). The returned
|
|
39
|
-
* path is a string.
|
|
20
|
+
* Resolves an installed npm package the way Node does: walk up from baseDir looking for
|
|
21
|
+
* node_modules/<packageName>. Covers app, hoisted and monorepo layouts alike.
|
|
40
22
|
*/
|
|
41
23
|
static def findNodeModulePath(baseDir, packageName) {
|
|
42
24
|
def basePath = baseDir.toPath().normalize()
|
|
43
|
-
//
|
|
44
|
-
// after which the base path will be null
|
|
25
|
+
// null once the walk passes the filesystem root
|
|
45
26
|
while (basePath) {
|
|
46
27
|
def candidatePath = Paths.get(basePath.toString(), "node_modules", packageName)
|
|
47
28
|
if (candidatePath.toFile().exists()) {
|
|
@@ -56,7 +37,6 @@ gradle.projectsEvaluated {
|
|
|
56
37
|
def debuggableVariants = config.debuggableVariants ? config.debuggableVariants.get() : ['debug']
|
|
57
38
|
|
|
58
39
|
android.applicationVariants.all { variant ->
|
|
59
|
-
// No code push for debuggable variants
|
|
60
40
|
if (debuggableVariants.contains(variant.name)) {
|
|
61
41
|
return;
|
|
62
42
|
}
|
|
@@ -75,11 +55,9 @@ gradle.projectsEvaluated {
|
|
|
75
55
|
def resourcesDir;
|
|
76
56
|
def jsBundleFile;
|
|
77
57
|
|
|
78
|
-
// Additional node commandline arguments
|
|
79
58
|
def nodeExecutableAndArgs = config.nodeExecutableAndArgs ? config.nodeExecutableAndArgs.get() : ["node"]
|
|
80
59
|
def extraPackagerArgs = config.extraPackagerArgs ? config.extraPackagerArgs.get() : []
|
|
81
60
|
|
|
82
|
-
// Make this task run right after the bundle task
|
|
83
61
|
def generateBundledResourcesHash;
|
|
84
62
|
|
|
85
63
|
def reactBundleTask = tasks.findByName("createBundle${targetName}JsAndAssets")
|
|
@@ -99,8 +77,6 @@ gradle.projectsEvaluated {
|
|
|
99
77
|
name: "generateBundledResourcesHash${targetName}",
|
|
100
78
|
type: Exec) {
|
|
101
79
|
commandLine(*nodeExecutableAndArgs, "${nodeModulesPath}/scripts/generateBundledResourcesHash.js", resourcesDir, jsBundleFile, jsBundleDir)
|
|
102
|
-
|
|
103
|
-
enabled !debuggableVariants.contains(variant.name) ?: targetName.toLowerCase().contains("release")
|
|
104
80
|
}
|
|
105
81
|
|
|
106
82
|
runBefore("merge${targetName}Resources", generateBundledResourcesHash)
|
|
@@ -114,9 +90,7 @@ gradle.projectsEvaluated {
|
|
|
114
90
|
resourcesDir = elvisFile(config."${resourcesDirConfigName}") ? elvisFile(config."${resourcesDirConfigName}").get() :
|
|
115
91
|
file("$buildDir/intermediates/res/merged/${targetPath}")
|
|
116
92
|
|
|
117
|
-
//
|
|
118
|
-
// '$buildDir' has slightly different structure - 'merged' folder
|
|
119
|
-
// does not exists so '${targetPath}' folder contains directly in 'res' folder.
|
|
93
|
+
// AGP below 1.3.0 had no 'merged' folder: res/${targetPath} directly.
|
|
120
94
|
if (!resourcesDir.exists() && file("$buildDir/intermediates/res/${targetPath}").exists()) {
|
|
121
95
|
resourcesDir = file("$buildDir/intermediates/res/${targetPath}")
|
|
122
96
|
}
|
|
@@ -131,7 +105,6 @@ gradle.projectsEvaluated {
|
|
|
131
105
|
commandLine(*nodeExecutableAndArgs, "${nodeModulesPath}/scripts/generateBundledResourcesHash.js", resourcesDir, jsBundleFile, jsBundleDir, resourcesMapTempFileName)
|
|
132
106
|
}
|
|
133
107
|
|
|
134
|
-
// Make this task run right before the bundle task
|
|
135
108
|
def recordFilesBeforeBundleCommand = tasks.create(
|
|
136
109
|
name: "recordFilesBeforeBundleCommand${targetName}",
|
|
137
110
|
type: Exec) {
|
|
@@ -142,7 +115,7 @@ gradle.projectsEvaluated {
|
|
|
142
115
|
recordFilesBeforeBundleCommand.dependsOn("merge${targetName}Assets")
|
|
143
116
|
runBefore("bundle${targetName}JsAndAssets", recordFilesBeforeBundleCommand)
|
|
144
117
|
|
|
145
|
-
//
|
|
118
|
+
// the map recorded before bundling is the input to the hash
|
|
146
119
|
generateBundledResourcesHash.dependsOn("recordFilesBeforeBundleCommand${targetName}")
|
|
147
120
|
}
|
|
148
121
|
|
|
@@ -155,20 +128,24 @@ gradle.projectsEvaluated {
|
|
|
155
128
|
}
|
|
156
129
|
}
|
|
157
130
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
131
|
+
// withPlugin, not findByName: applying this script above the Android plugin would otherwise
|
|
132
|
+
// silently ship an APK with no CODE_PUSH_APK_BUILD_TIME.
|
|
133
|
+
project.pluginManager.withPlugin("com.android.application") {
|
|
134
|
+
project.extensions.getByName("androidComponents").finalizeDsl { extension ->
|
|
135
|
+
// One density bucket per device would make the build-time resources hash unreproducible.
|
|
136
|
+
extension.bundle.density.enableSplit = false
|
|
161
137
|
|
|
162
|
-
|
|
138
|
+
extension.androidResources.noCompress.add("png")
|
|
163
139
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
140
|
+
extension.buildTypes.all { buildType ->
|
|
141
|
+
// crunching would change the bytes Metro already hashed
|
|
142
|
+
if (!buildType.debuggable) {
|
|
143
|
+
buildType.crunchPngs = false
|
|
144
|
+
}
|
|
169
145
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
146
|
+
// quoted, or the long is restored incorrectly from strings.xml
|
|
147
|
+
// https://github.com/microsoft/cordova-plugin-code-push/issues/264
|
|
148
|
+
buildType.resValue('string', "CODE_PUSH_APK_BUILD_TIME", String.format("\"%d\"", System.currentTimeMillis()))
|
|
149
|
+
}
|
|
173
150
|
}
|
|
174
151
|
}
|
package/android/settings.gradle
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Only used when this directory is its own Gradle build root (standalone or monorepo).
|
|
2
|
+
// Autolinked apps include it as a subproject, so their settings file is used instead.
|
|
1
3
|
pluginManagement {
|
|
2
4
|
repositories {
|
|
3
5
|
mavenCentral()
|
|
@@ -10,22 +12,19 @@ pluginManagement {
|
|
|
10
12
|
id("com.android.library") version "8.8.2" apply false
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
// Inline, not via a helper: method calls inside pluginManagement {} bind to the block's
|
|
16
|
+
// delegate and never reach this script's own methods. Property access on `settings` does.
|
|
17
|
+
def gradlePluginPath = (settings.hasProperty("REACT_NATIVE_GRADLE_PLUGIN_PATH") ? settings["REACT_NATIVE_GRADLE_PLUGIN_PATH"] : null) ?: System.getenv("REACT_NATIVE_GRADLE_PLUGIN_PATH")
|
|
18
|
+
if (gradlePluginPath) {
|
|
16
19
|
includeBuild(gradlePluginPath)
|
|
17
20
|
}
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
def diffPath =
|
|
21
|
-
if (diffPath
|
|
23
|
+
def diffPath = (settings.hasProperty("REACT_NATIVE_REVOPUSH_DIFF_PATH") ? settings["REACT_NATIVE_REVOPUSH_DIFF_PATH"] : null) ?: System.getenv("REACT_NATIVE_REVOPUSH_DIFF_PATH")
|
|
24
|
+
if (diffPath) {
|
|
22
25
|
includeBuild(diffPath) {
|
|
23
26
|
dependencySubstitution {
|
|
24
27
|
substitute(module("org.revopush:revopush-diff")).using(project(":revopush-diff"))
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
|
-
|
|
29
|
-
def getProperty(propName) {
|
|
30
|
-
return settings[propName] ?: System.getenv(propName)
|
|
31
|
-
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
package com.microsoft.codepush.react;
|
|
2
|
+
|
|
3
|
+
import static org.revopush.ota.utils.FileUtils.copyFileIfExists;
|
|
4
|
+
|
|
5
|
+
import java.io.File;
|
|
6
|
+
import java.io.IOException;
|
|
7
|
+
import java.util.Arrays;
|
|
8
|
+
|
|
9
|
+
/** Bundles kept as diff bases, named by the package hash of the release they came from. */
|
|
10
|
+
public class BaseBundleStore {
|
|
11
|
+
|
|
12
|
+
public static final int MAX_SAVED_BASES = 3;
|
|
13
|
+
|
|
14
|
+
private final File mBasesDir;
|
|
15
|
+
|
|
16
|
+
public BaseBundleStore(String basesDirPath) {
|
|
17
|
+
mBasesDir = new File(basesDirPath);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public File lookup(String packageHash) {
|
|
21
|
+
if (!isValidEntryName(packageHash)) return null;
|
|
22
|
+
File file = new File(mBasesDir, packageHash);
|
|
23
|
+
return file.isFile() ? file : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Copies the bundle into the store, so it outlives its package folder. A hard link would
|
|
28
|
+
* be cheaper, but SELinux denies untrusted_app the link permission on its own files.
|
|
29
|
+
* Never fails an update.
|
|
30
|
+
*/
|
|
31
|
+
public boolean save(File sourceBundle, String packageHash) {
|
|
32
|
+
if (sourceBundle == null || !sourceBundle.isFile()) return false;
|
|
33
|
+
if (!isValidEntryName(packageHash)) return false;
|
|
34
|
+
|
|
35
|
+
File dest = new File(mBasesDir, packageHash);
|
|
36
|
+
if (dest.isFile()) {
|
|
37
|
+
// Saving is the moment the base gets used, so this keeps evict() ordered by last use.
|
|
38
|
+
dest.setLastModified(System.currentTimeMillis());
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Copied aside and renamed: a copy cut short by process death must not leave a truncated
|
|
43
|
+
// file under the hash, which lookup() would hand out forever.
|
|
44
|
+
File temp = new File(mBasesDir, packageHash + ".tmp");
|
|
45
|
+
try {
|
|
46
|
+
if (!copyFileIfExists(sourceBundle, temp) || !temp.renameTo(dest)) {
|
|
47
|
+
FileUtils.deleteFileOrFolderSilently(temp);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
} catch (IOException e) {
|
|
51
|
+
CodePushUtils.log("Could not save base bundle: " + e.getMessage());
|
|
52
|
+
FileUtils.deleteFileOrFolderSilently(temp);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
evict();
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The hash names a file directly inside the store, so it may not steer out of it. */
|
|
60
|
+
private static boolean isValidEntryName(String packageHash) {
|
|
61
|
+
return packageHash != null
|
|
62
|
+
&& !packageHash.isEmpty()
|
|
63
|
+
&& !packageHash.equals(".")
|
|
64
|
+
&& !packageHash.equals("..")
|
|
65
|
+
&& packageHash.indexOf('/') < 0
|
|
66
|
+
&& packageHash.indexOf('\\') < 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Keeps the MAX_SAVED_BASES most recently used entries. */
|
|
70
|
+
private void evict() {
|
|
71
|
+
File[] files = mBasesDir.listFiles();
|
|
72
|
+
if (files == null || files.length <= MAX_SAVED_BASES) return;
|
|
73
|
+
Arrays.sort(files, (a, b) -> Long.compare(a.lastModified(), b.lastModified()));
|
|
74
|
+
for (int i = 0; i < files.length - MAX_SAVED_BASES; i++) {
|
|
75
|
+
FileUtils.deleteFileOrFolderSilently(files[i]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -4,6 +4,7 @@ import android.content.Context;
|
|
|
4
4
|
import android.content.pm.PackageInfo;
|
|
5
5
|
import android.content.pm.PackageManager;
|
|
6
6
|
import android.content.res.Resources;
|
|
7
|
+
import android.os.Build;
|
|
7
8
|
|
|
8
9
|
import com.facebook.react.ReactHost;
|
|
9
10
|
import com.facebook.react.ReactInstanceManager;
|
|
@@ -83,11 +84,13 @@ public class CodePush implements ReactPackage {
|
|
|
83
84
|
mSettingsManager = new SettingsManager(mContext);
|
|
84
85
|
bundleMetadataManager = new NativeBundleManager(mContext);
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
// Guarded per value: overrideAppVersion is documented to run before this constructor, and
|
|
88
|
+
// must not stop the build number from being read (and vice versa).
|
|
89
|
+
if (sAppVersion == null || sBuildNumber == null) {
|
|
87
90
|
try {
|
|
88
91
|
PackageInfo pInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
|
|
89
|
-
sAppVersion = pInfo.versionName;
|
|
90
|
-
sBuildNumber = String.valueOf(pInfo
|
|
92
|
+
if (sAppVersion == null) sAppVersion = pInfo.versionName;
|
|
93
|
+
if (sBuildNumber == null) sBuildNumber = String.valueOf(getVersionCode(pInfo));
|
|
91
94
|
} catch (PackageManager.NameNotFoundException e) {
|
|
92
95
|
throw new CodePushUnknownException("Unable to get package info for " + mContext.getPackageName(), e);
|
|
93
96
|
}
|
|
@@ -127,6 +130,14 @@ public class CodePush implements ReactPackage {
|
|
|
127
130
|
mServerUrl = serverUrl;
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
// versionCode is only the low half of the version code once versionCodeMajor is set.
|
|
134
|
+
private static long getVersionCode(PackageInfo packageInfo) {
|
|
135
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
136
|
+
return packageInfo.getLongVersionCode();
|
|
137
|
+
}
|
|
138
|
+
return packageInfo.versionCode;
|
|
139
|
+
}
|
|
140
|
+
|
|
130
141
|
private String getPublicKeyByResourceDescriptor(int publicKeyResourceDescriptor) {
|
|
131
142
|
String publicKey;
|
|
132
143
|
try {
|
|
@@ -18,6 +18,8 @@ public class CodePushConstants {
|
|
|
18
18
|
public static final String ASSET_DOWNLOAD_URL_KEY = "assetDownloadUrl";
|
|
19
19
|
public static final String BUNDLE_DOWNLOAD_URL_KEY = "bundleBlobUrl";
|
|
20
20
|
public static final String BUNDLE_DIFF_DOWNLOAD_URL_KEY = "bundleDiffBlobUrl";
|
|
21
|
+
public static final String BASE_PACKAGE_KEY = "basePackage";
|
|
22
|
+
public static final String BASES_FOLDER_NAME = "bases";
|
|
21
23
|
|
|
22
24
|
public static final String FAILED_UPDATES_KEY = "CODE_PUSH_FAILED_UPDATES";
|
|
23
25
|
public static final String PACKAGE_FILE_NAME = BundleManager.PACKAGE_FILE_NAME;
|
|
@@ -123,9 +123,9 @@ public class CodePushNativeModule extends BaseJavaModule {
|
|
|
123
123
|
|
|
124
124
|
private boolean detectExpoEnvironment() {
|
|
125
125
|
try {
|
|
126
|
-
Class.forName("expo.modules.ReactNativeHostWrapper");
|
|
126
|
+
Class.forName("expo.modules.ReactNativeHostWrapper", false, getClass().getClassLoader());
|
|
127
127
|
return true;
|
|
128
|
-
} catch (
|
|
128
|
+
} catch (Throwable e) {
|
|
129
129
|
return false;
|
|
130
130
|
}
|
|
131
131
|
}
|
|
@@ -217,7 +217,7 @@ public class CodePushNativeModule extends BaseJavaModule {
|
|
|
217
217
|
String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFileName());
|
|
218
218
|
|
|
219
219
|
try {
|
|
220
|
-
if (
|
|
220
|
+
if (reactHost instanceof ReactHostImpl) {
|
|
221
221
|
ReactHostDelegate delegate = getReactHostDelegate((ReactHostImpl) reactHost);
|
|
222
222
|
if (delegate != null) {
|
|
223
223
|
// #2) Update the locally stored JS bundle file path
|
|
@@ -43,6 +43,10 @@ public class CodePushUpdateManager {
|
|
|
43
43
|
return appendPathComponent(getCodePushPath(), CodePushConstants.UNZIPPED_FOLDER_NAME);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
private BaseBundleStore getBaseBundleStore() {
|
|
47
|
+
return new BaseBundleStore(appendPathComponent(getCodePushPath(), CodePushConstants.BASES_FOLDER_NAME));
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
private String getDocumentsDirectory() {
|
|
47
51
|
return mDocumentsDirectory;
|
|
48
52
|
}
|
|
@@ -94,22 +98,22 @@ public class CodePushUpdateManager {
|
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
public String getCurrentPackageBundlePath(String bundleFileName) {
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
return getPackageBundlePath(getCurrentPackageHash(), bundleFileName);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private String getPackageBundlePath(String packageHash, String bundleFileName) {
|
|
105
|
+
if (packageHash == null) {
|
|
99
106
|
return null;
|
|
100
107
|
}
|
|
101
108
|
|
|
102
|
-
JSONObject
|
|
103
|
-
if (
|
|
109
|
+
JSONObject packageMetadata = getPackage(packageHash);
|
|
110
|
+
if (packageMetadata == null) {
|
|
104
111
|
return null;
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
String relativeBundlePath =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
} else {
|
|
111
|
-
return appendPathComponent(packageFolder, relativeBundlePath);
|
|
112
|
-
}
|
|
114
|
+
String relativeBundlePath = packageMetadata.optString(CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, null);
|
|
115
|
+
return appendPathComponent(getPackageFolderPath(packageHash),
|
|
116
|
+
relativeBundlePath != null ? relativeBundlePath : bundleFileName);
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
public String getPackageFolderPath(String packageHash) {
|
|
@@ -154,7 +158,47 @@ public class CodePushUpdateManager {
|
|
|
154
158
|
}
|
|
155
159
|
}
|
|
156
160
|
|
|
161
|
+
/** A base bundle on disk, and the package hash it is stored under. */
|
|
162
|
+
private static final class BaseBundle {
|
|
163
|
+
final File file;
|
|
164
|
+
final String packageHash;
|
|
165
|
+
|
|
166
|
+
BaseBundle(File file, String packageHash) {
|
|
167
|
+
this.file = file;
|
|
168
|
+
this.packageHash = packageHash;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The base named by basePackage: the saved copy, else that package's own bundle. */
|
|
173
|
+
private BaseBundle findBaseBundle(JSONObject updatePackage, String expectedBundleFileName) {
|
|
174
|
+
JSONObject basePackage = updatePackage.optJSONObject(CodePushConstants.BASE_PACKAGE_KEY);
|
|
175
|
+
if (basePackage == null) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
String packageHash = basePackage.optString(CODE_PUSH_PACKAGE_HASH, null);
|
|
179
|
+
if (packageHash == null || packageHash.isEmpty()) {
|
|
180
|
+
// Named a base with no hash: server and SDK disagree on the field name.
|
|
181
|
+
CodePushUtils.log("basePackage has no packageHash; patching against the binary.");
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
File bundle = getBaseBundleStore().lookup(packageHash);
|
|
185
|
+
if (bundle == null) {
|
|
186
|
+
String bundlePath = getPackageBundlePath(packageHash, expectedBundleFileName);
|
|
187
|
+
bundle = bundlePath == null ? null : new File(bundlePath);
|
|
188
|
+
}
|
|
189
|
+
if (bundle == null || !bundle.isFile()) {
|
|
190
|
+
CodePushUtils.log("Base " + packageHash + " not held; patching against the binary.");
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
return new BaseBundle(bundle, packageHash);
|
|
194
|
+
}
|
|
195
|
+
|
|
157
196
|
public void downloadPackage(JSONObject updatePackage, String expectedBundleFileName, DownloadProgressCallback progressCallback, String stringPublicKey) throws IOException {
|
|
197
|
+
BaseBundle base = findBaseBundle(updatePackage, expectedBundleFileName);
|
|
198
|
+
if (base != null) {
|
|
199
|
+
getBaseBundleStore().save(base.file, base.packageHash);
|
|
200
|
+
}
|
|
201
|
+
|
|
158
202
|
UpdatePackage updatePack = fromJson(updatePackage);
|
|
159
203
|
|
|
160
204
|
String newUpdateFolderPath = getPackageFolderPath(updatePack.getNewUpdateHash());
|
|
@@ -164,7 +208,8 @@ public class CodePushUpdateManager {
|
|
|
164
208
|
expectedBundleFileName, // exp bundle filename
|
|
165
209
|
stringPublicKey, // pub key
|
|
166
210
|
updatePack.getNewUpdateHash(), // new update hash
|
|
167
|
-
getCurrentPackage() // current package
|
|
211
|
+
getCurrentPackage(), // current package
|
|
212
|
+
base == null ? null : base.file.getAbsolutePath() // base bundle
|
|
168
213
|
);
|
|
169
214
|
|
|
170
215
|
|
|
@@ -27,11 +27,13 @@ public class FileUtils {
|
|
|
27
27
|
public static void deleteFileOrFolderSilently(File file) {
|
|
28
28
|
if (file.isDirectory()) {
|
|
29
29
|
File[] files = file.listFiles();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
if (files != null) {
|
|
31
|
+
for (File fileEntry : files) {
|
|
32
|
+
if (fileEntry.isDirectory()) {
|
|
33
|
+
deleteFileOrFolderSilently(fileEntry);
|
|
34
|
+
} else {
|
|
35
|
+
fileEntry.delete();
|
|
36
|
+
}
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
}
|
package/ios/CodePush/CodePush.h
CHANGED
|
@@ -136,6 +136,7 @@
|
|
|
136
136
|
error:(NSError **)error;
|
|
137
137
|
|
|
138
138
|
+ (NSString *)getPackageFolderPath:(NSString *)packageHash;
|
|
139
|
+
+ (NSString *)getCodePushPath;
|
|
139
140
|
|
|
140
141
|
+ (BOOL)installPackage:(NSDictionary *)updatePackage
|
|
141
142
|
removePendingUpdate:(BOOL)removePendingUpdate
|
|
@@ -149,6 +150,19 @@
|
|
|
149
150
|
|
|
150
151
|
@end
|
|
151
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Bundles kept as diff bases, named by the package hash of the release they came from.
|
|
155
|
+
* A base a package folder no longer holds is still findable here.
|
|
156
|
+
*/
|
|
157
|
+
@interface CodePushBaseBundleStore : NSObject
|
|
158
|
+
|
|
159
|
+
/** Path of the retained bundle for this package hash, or nil if the store does not hold it. */
|
|
160
|
+
+ (NSString *)lookup:(NSString *)packageHash;
|
|
161
|
+
|
|
162
|
+
+ (BOOL)save:(NSString *)sourceBundlePath forPackageHash:(NSString *)packageHash;
|
|
163
|
+
|
|
164
|
+
@end
|
|
165
|
+
|
|
152
166
|
@interface CodePushTelemetryManager : NSObject
|
|
153
167
|
|
|
154
168
|
+ (NSDictionary *)getBinaryUpdateReport:(NSString *)appVersion;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#import "CodePush.h"
|
|
2
|
+
|
|
3
|
+
// CPLog
|
|
4
|
+
#import <DiffUpdates/DiffUpdates.h>
|
|
5
|
+
|
|
6
|
+
@implementation CodePushBaseBundleStore
|
|
7
|
+
|
|
8
|
+
static NSString *const BasesFolderName = @"bases";
|
|
9
|
+
static const NSUInteger MaxSavedBases = 3;
|
|
10
|
+
|
|
11
|
+
+ (NSString *)lookup:(NSString *)packageHash
|
|
12
|
+
{
|
|
13
|
+
NSString *path = [self pathForPackageHash:packageHash];
|
|
14
|
+
return path && [[NSFileManager defaultManager] fileExistsAtPath:path] ? path : nil;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Copies the bundle into the store so it outlives its package folder: installPackage deletes the
|
|
19
|
+
* previous package on every install, but the server can still name that release as a diff base.
|
|
20
|
+
* Never fails an update - a base we could not save is simply one we do not hold.
|
|
21
|
+
*/
|
|
22
|
+
+ (BOOL)save:(NSString *)sourceBundlePath forPackageHash:(NSString *)packageHash
|
|
23
|
+
{
|
|
24
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
25
|
+
NSString *destinationPath = [self pathForPackageHash:packageHash];
|
|
26
|
+
if (!destinationPath || ![fileManager fileExistsAtPath:sourceBundlePath]) {
|
|
27
|
+
return NO;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if ([fileManager fileExistsAtPath:destinationPath]) {
|
|
31
|
+
[self touch:destinationPath];
|
|
32
|
+
return YES;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Copied aside and renamed: a copy cut short by process death must not leave a truncated file
|
|
36
|
+
// under the hash, which lookup would then hand out forever.
|
|
37
|
+
NSString *tempPath = [destinationPath stringByAppendingPathExtension:@"tmp"];
|
|
38
|
+
[fileManager removeItemAtPath:tempPath error:nil];
|
|
39
|
+
|
|
40
|
+
NSError *error = nil;
|
|
41
|
+
if (![fileManager createDirectoryAtPath:[self basesFolderPath] withIntermediateDirectories:YES attributes:nil error:&error]
|
|
42
|
+
|| ![fileManager copyItemAtPath:sourceBundlePath toPath:tempPath error:&error]
|
|
43
|
+
|| ![fileManager moveItemAtPath:tempPath toPath:destinationPath error:&error]) {
|
|
44
|
+
CPLog(@"Could not save base bundle %@: %@", packageHash, error);
|
|
45
|
+
[fileManager removeItemAtPath:tempPath error:nil];
|
|
46
|
+
return NO;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
[self touch:destinationPath];
|
|
50
|
+
[self evict];
|
|
51
|
+
return YES;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
+ (NSString *)basesFolderPath
|
|
55
|
+
{
|
|
56
|
+
return [[CodePushPackage getCodePushPath] stringByAppendingPathComponent:BasesFolderName];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** An entry is named by its hash alone, so a hash naming anything else is refused. */
|
|
60
|
+
+ (NSString *)pathForPackageHash:(NSString *)packageHash
|
|
61
|
+
{
|
|
62
|
+
if (packageHash.length == 0
|
|
63
|
+
|| [packageHash isEqualToString:@".."]
|
|
64
|
+
|| ![packageHash isEqualToString:packageHash.lastPathComponent]) {
|
|
65
|
+
return nil;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return [[self basesFolderPath] stringByAppendingPathComponent:packageHash];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A copy keeps the source's timestamp and reuse has to count as use, so the store stamps every
|
|
72
|
+
// save itself. evict orders by this.
|
|
73
|
+
+ (void)touch:(NSString *)path
|
|
74
|
+
{
|
|
75
|
+
[[NSFileManager defaultManager] setAttributes:@{NSFileModificationDate: [NSDate date]}
|
|
76
|
+
ofItemAtPath:path
|
|
77
|
+
error:nil];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Keeps the MaxSavedBases most recently saved entries. */
|
|
81
|
+
+ (void)evict
|
|
82
|
+
{
|
|
83
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
84
|
+
NSArray<NSURL *> *entries = [fileManager contentsOfDirectoryAtURL:[NSURL fileURLWithPath:[self basesFolderPath]]
|
|
85
|
+
includingPropertiesForKeys:@[NSURLContentModificationDateKey]
|
|
86
|
+
options:0
|
|
87
|
+
error:nil];
|
|
88
|
+
if (entries.count <= MaxSavedBases) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
NSArray<NSURL *> *oldestFirst = [entries sortedArrayUsingComparator:^(NSURL *a, NSURL *b) {
|
|
93
|
+
NSDate *dateA = nil, *dateB = nil;
|
|
94
|
+
[a getResourceValue:&dateA forKey:NSURLContentModificationDateKey error:nil];
|
|
95
|
+
[b getResourceValue:&dateB forKey:NSURLContentModificationDateKey error:nil];
|
|
96
|
+
return [dateA compare:dateB];
|
|
97
|
+
}];
|
|
98
|
+
|
|
99
|
+
for (NSUInteger i = 0; i < oldestFirst.count - MaxSavedBases; i++) {
|
|
100
|
+
[fileManager removeItemAtURL:oldestFirst[i] error:nil];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@end
|