Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions CodePush.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,40 @@ Pod::Spec.new do |s|
s.source = { :git => 'https://github.com/Soomgo-Mobile/react-native-code-push.git', :tag => "v#{s.version}"}
s.ios.deployment_target = '15.5'
s.tvos.deployment_target = '15.5'
s.preserve_paths = '*.js'
s.preserve_paths = ['*.js', 'cpp/binarypatch/**/*.h']
s.library = 'z'
s.source_files = 'ios/CodePush/*.{h,m,mm}'
# The applier that installs a binary patch update is compiled from the C sources the
# other platform and the host build compile as well, referenced where they live rather
# than copied here, which is what keeps the appliers of the platforms from drifting
# apart. Only the decompressing half of zstd is needed, and its assembly fast path is
# deliberately not vendored, so this list is the same one the other platform builds.
s.source_files = [
'ios/CodePush/*.{h,m,mm}',
'cpp/binarypatch/binarypatch_zstd_decompressor.c',
'cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.c',
'cpp/binarypatch/vendor/zstd/common/debug.c',
'cpp/binarypatch/vendor/zstd/common/entropy_common.c',
'cpp/binarypatch/vendor/zstd/common/error_private.c',
'cpp/binarypatch/vendor/zstd/common/fse_decompress.c',
'cpp/binarypatch/vendor/zstd/common/xxhash.c',
'cpp/binarypatch/vendor/zstd/common/zstd_common.c',
'cpp/binarypatch/vendor/zstd/decompress/huf_decompress.c',
'cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.c',
'cpp/binarypatch/vendor/zstd/decompress/zstd_decompress.c',
'cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.c'
]
s.public_header_files = ['ios/CodePush/CodePush.h']

binary_patch_header_search_paths = '"$(PODS_TARGET_SRCROOT)/cpp/binarypatch" "$(PODS_TARGET_SRCROOT)/cpp/binarypatch/vendor/HDiffPatch" "$(PODS_TARGET_SRCROOT)/cpp/binarypatch/vendor/zstd"'
# ZSTD_DISABLE_ASM: the assembly fast path is intentionally not vendored.
# _IS_USED_MULTITHREAD=0: patches are applied on the thread that downloads them.
binary_patch_preprocessor_definitions = '$(inherited) ZSTD_DISABLE_ASM=1 _IS_USED_MULTITHREAD=0'

s.pod_target_xcconfig = {
'HEADER_SEARCH_PATHS' => binary_patch_header_search_paths,
'GCC_PREPROCESSOR_DEFINITIONS' => binary_patch_preprocessor_definitions
}

# Note: Even though there are copy/pasted versions of some of these dependencies in the repo,
# we explicitly let CocoaPods pull in the versions below so all dependencies are resolved and
# linked properly at a parent workspace level.
Expand All @@ -25,9 +54,12 @@ Pod::Spec.new do |s|

if ENV['RCT_NEW_ARCH_ENABLED'] == '1'
s.compiler_flags = '-DRCT_NEW_ARCH_ENABLED=1'
# This replaces pod_target_xcconfig rather than adding to it, so what the applier
# needs to build has to be carried into it as well.
s.pod_target_xcconfig = {
'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17',
'HEADER_SEARCH_PATHS' => "\"$(PODS_ROOT)/Headers/Public/ReactCodegen\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCodegen/ReactCodegen.framework/Headers\""
'HEADER_SEARCH_PATHS' => "\"$(PODS_ROOT)/Headers/Public/ReactCodegen\" \"${PODS_CONFIGURATION_BUILD_DIR}/ReactCodegen/ReactCodegen.framework/Headers\" #{binary_patch_header_search_paths}",
'GCC_PREPROCESSOR_DEFINITIONS' => binary_patch_preprocessor_definitions
}

s.dependency 'ReactCodegen'
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ Run `cd ios && pod install && cd ..`

(`npx pod-install`, `bundle exec pod install --project-directory=./ios`, ..)

The pod compiles the applier that installs binary patch updates from the C sources it
carries, so there is nothing else to install: CocoaPods builds them along with the rest
of the pod.


#### (2) Edit `AppDelegate` Code

Expand Down
10 changes: 5 additions & 5 deletions cli/README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,11 @@ npx code-push release [options]
절감량은 업로드 전에 출력됩니다. 릴리스 히스토리 항목에는 full 번들 URL과 함께 patch 번들을
내려받을 수 있는 URL이 기록됩니다.

Android 클라이언트는 patch 번들이 있는 릴리스라면 patch로 업데이트를 설치하고, patch를
적용할 수 없으면 full 번들을 대신 내려받으므로 patch 때문에 설치가 실패하지는 않습니다.
patch 적용은 네이티브 코드이며 Android 라이브러리가 소스에서 직접 빌드하므로, 이 라이브러리를
사용하는 앱을 빌드하려면 NDK와 CMake가 필요합니다. React Native 프로젝트라면 대개 이미
갖추고 있습니다.
클라이언트는 patch 번들이 있는 릴리스라면 patch로 업데이트를 설치하고, patch를 적용할 수
없으면 full 번들을 대신 내려받으므로 patch 때문에 설치가 실패하지는 않습니다. patch 적용은
네이티브 코드이며 라이브러리가 같은 소스에서 직접 빌드합니다. Android는 그래서 NDK와
CMake가 필요하지만 React Native 프로젝트라면 대개 이미 갖추고 있고, iOS는 pod가 소스를
함께 가지고 있어 CocoaPods가 빌드하므로 따로 준비할 것이 없습니다.

patch는 대체하려는 archive보다 작을 때만 배포할 가치가 있습니다. CLI는 사용자에게 묻지
않으므로, patch 크기가 full 이상일 때의 동작을 `--on-oversized-patch`로 미리 정합니다.
Expand Down
11 changes: 6 additions & 5 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,12 @@ applying it yields the same `packageHash` as the full bundle. Both sizes and the
are printed before either artifact is uploaded. The release history entry records where
the patch bundle can be downloaded, next to the full bundle URL.

An Android client installs the update from the patch bundle when the release has one, and
downloads the full bundle instead whenever the patch cannot be applied, so a release is
never left uninstallable by a patch. Applying a patch is native code, which the Android
library builds from source: an app that depends on it needs the NDK and CMake, both of
which a React Native project normally already has.
A client installs the update from the patch bundle when the release has one, and downloads
the full bundle instead whenever the patch cannot be applied, so a release is never left
uninstallable by a patch. Applying a patch is native code, which both libraries build from
the same sources: on Android that needs the NDK and CMake, both of which a React Native
project normally already has, and on iOS the pod carries the sources, so CocoaPods builds
them with nothing to add.

A patch is only worth publishing when it is smaller than the archive it replaces. The CLI
never prompts, so `--on-oversized-patch` decides in advance what happens when the patch
Expand Down
12 changes: 12 additions & 0 deletions ios/CodePush/CodePush.h
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,18 @@ failCallback:(void (^)(NSError *err))failCallback;

@interface CodePushUpdateUtils : NSObject

/*
* The SHA-256 of some bytes, written the way every hash of an update's
* contents is: lower case hex.
*/
+ (NSString *)computeHashForData:(NSData *)inputData;

/*
* The same hash, of the contents of a file. Returns nil when the file
* cannot be read.
*/
+ (NSString *)computeHashForFileAtPath:(NSString *)filePath;

+ (BOOL)copyEntriesInFolder:(NSString *)sourceFolder
destFolder:(NSString *)destFolder
error:(NSError **)error;
Expand Down
71 changes: 71 additions & 0 deletions ios/CodePush/CodePushBinaryPatch.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#import <Foundation/Foundation.h>

/*
* Why an update that was downloaded as a binary patch has to be downloaded in full
* instead.
*
* A failed restore is not an error the user ever hears about: it is the signal to
* download the update's full archive. These reasons are the vocabulary the appliers of
* every platform report, and the logs a rollout is judged from are read for exactly
* these words, so they must not be reworded.
*/

/** The bundle inside the app binary could not be opened or read. */
extern NSString *const CodePushBinaryPatchReasonBaseBundleUnavailable;

/** The bundle inside the app binary is not the one the patch was computed against. */
extern NSString *const CodePushBinaryPatchReasonBaseHashMismatch;

/** The manifest is missing, malformed, points outside the archive, or asks for too much. */
extern NSString *const CodePushBinaryPatchReasonInvalidManifest;

/** The patch was produced by a format or a codec this client cannot apply. */
extern NSString *const CodePushBinaryPatchReasonUnsupportedFormat;

/** The applier refused the patch, or the restored bundle could not be written. */
extern NSString *const CodePushBinaryPatchReasonPatchApplyFailed;

/** The restored bundle is not the one the manifest promised. */
extern NSString *const CodePushBinaryPatchReasonTargetVerificationFailed;

/**
* Rebuilds the JS bundle of an update that was downloaded as a binary patch archive.
*
* A patch archive holds everything the full archive holds except the JS bundle, which it
* carries as a patch against the bundle that shipped inside the app binary, plus a
* manifest describing how to rebuild it. Restoring means applying that patch, verifying
* the result, moving it to where the bundle belongs and deleting the two patch-only
* files. What is left is byte for byte the contents of the full archive, so the folder
* hash check that follows the install is unchanged and stays the last line of defence.
*
* Nothing here trusts the patch. Neither the diff format nor the zstd streams inside it
* carry a checksum of the data they produce, so an apply that reports success is not
* proof of a correct result: a base bundle of the right size but the wrong content, or a
* corrupted patch body, both produce wrong bytes without any error. The base bundle is
* hashed before the patch is applied and the restored bundle is hashed afterwards, and
* the restored bytes only reach the update contents once both checks have passed.
*
* Every failure is reported as a reason, never as an exception: the caller answers all
* of them the same way, by downloading the full archive instead.
*/
@interface CodePushBinaryPatch : NSObject

/**
* Turns the contents of a downloaded patch archive into the contents of the full one.
*
* This reads and hashes whole bundles, so it belongs on the queue the download runs on
* and never on the main queue.
*
* @param unzippedFolderPath the unzipped archive, which is modified in place
* @param workingFolderPath scratch directory for the restored bundle, emptied before
* and after the attempt so an interrupted run leaves nothing
* @param baseBundleURL the JS bundle that shipped inside the app binary
* @param failureReason set to the reason the full archive has to be downloaded
* instead, whenever this returns NO
*/
+ (BOOL)restoreBundleInUnzippedFolder:(NSString *)unzippedFolderPath
workingFolder:(NSString *)workingFolderPath
baseBundleURL:(NSURL *)baseBundleURL
failureReason:(NSString **)failureReason;

@end
Loading
Loading